Promise相关

2018-11-28  本文已影响0人  Robot_Lee

在 javascript 中, 所有代码都是单线程执行的

由于这个缺陷,导致 javascript 的所有网络操作、浏览器事件,都必须是异步执行,异步执行可以用回调函数实现。

function callback () {
  console.log('Done');
}
console.log('before setTimeout()...');
setTimeout(callback, 1000);
console.log('after setTimeout()...');

观察上述代码执行,在Chrome的控制台输出可以看到:

before setTimeout()
after setTimeout()
(等待1秒后)
Done

可见异步操作会在将来某个时间点触发一个函数调用。
Ajax 就是典型的异步操作,以上一节的代码为例:

var xhr = null;
if (window.XMLHttpRequest) {
   xhr = new XMLHttpRequest();
} else if (window.ActiveXObject()) {
   xhr = new ActiveXObject('Microsoft.XMLHTTP');
}
if (xhr !== null) {
  xhr.onreadystatechange = function () {
    if (xhr.readystate === 4) {
      if (xhr.state === 200) {
        return success(xhr.responseText);
      } else {
        return fail(xhr.status);
      }
    }
  }
}

把回调函数success(xhr.responseText)fail(xhr.status)写到一个Ajax操作里很正常,但是不好看,而且不利于代码复用.

有没有更好的写法,比如写成这样:

var ajax = ajaxGet('http://...');
ajax.ifSuccess(success)
    .ifFail(fail);

这种链式写法的好处在于:先统一执行Ajax的逻辑,不关心如何处理结果,然后,根据结果是成功还是失败,在将来的某个时候调用success函数或fail函数.

古人云:"君子一诺千金".这种“承诺将来会执行”的对象在Javascript中称为Promise对象.

Promise有各种开源实现,在ES6中被统一规范,由浏览器直接支持。

我们先看一个最简单的Promise例子:生成一个0-2之间的随机数,如果小于1,则等待一段时间后返回成功,否则返回失败:

function test (resolve, reject) {
  var timeOut = Math.random() * 2;
  console.log('set timeout to: ' + timeOut + ' seconds');
  setTimeout(function () {
    if (timeOut < 1) {
      console.log('call resolve()...');
      resolve('200 OK');
    } else {
      console.log('call reject()...');
      reject('timeout in ' + timeOut + ' seconds');
    }
  }, timeOut * 1000);
}

这个test()函数有两个参数,这两个参数都是函数,如果执行成功,我们将调用resolve('200 OK'),如果执行失败,我们将调用reject('timeout in ' + timeOut + ' seconds.')。可以看出,test()函数只关心自身的逻辑,并不关心具体的resolvereject将如何处理结果。

有了执行函数,我们就可以用一个Promise对象来执行它,并在将来某个时刻获得成功或失败的结果.

var p1 = new Promise(test);
var p2 = p1.then(function (result) {
  console.log('成功: ' + result);
});
var p3 = p2.catch(function (reason) {
  console.log('失败: ' + reason);
})

变量p1是一个Promise对象,它负责执行test函数,由于test函数在内部是异步执行的,当test函数执行成功时,我们告诉Promise对象:

// 如果成功,执行这个函数:
p1.then(function (result) {
    console.log('成功:' + result);
});

test函数执行失败时,我们告诉Promise对象:

p2.catch(function (reason) {
    console.log('失败:' + reason);
});

Promise对象可以串联起来,所以上述代码可以简化为:

new Promise(test).then(function (result) {
  console.log('成功: ' + result);
}).catch(function (reason){
  console.log('失败: ' + reason);
})

实际测试一下,看看Promise是如何异步执行的:

new Promise(function (resolve, reject) {
  console.log('start new Promise...');
  var timeOut = Math.random() * 2;
  console.log('set timeout to: ' + timeOut + ' seconds');
  setTimeout (function () {
    if (timeOut < 1) {
      console.log('call resolve()...');
      resolve('200 OK');
    } else {
      console.log('call reject()...');
      reject('timeout in ' + timeOut + ' seconds');
    }
  }, timeOut * 1000);
}).then(function (result) {
  console.log('Done: ' + result);
}).catch(function (reason) {
  console.log('Failed' + reason);
})

打开控制台,复制上面代码,粘贴后回车,看输出结果:

start new Promise...

set timeout to: 0.9133320468510968 seconds.

call resolve()...

Done: 200 OK

可见Promise最大的好处在于:异步执行的流程中,把执行代码和处理结果的代码清晰的分离了

Promise的好处是把执行代码和处理结果的代码分离了.png
Promise还可以做更多的事情,比如,有若干个异步任务,需要先做任务1,如果成功后再做任务2,任何任务失败则不再继续并执行错误处理函数。

要串行执行这样的异步任务,不用Promise需要写一层一层的嵌套代码。有了Promise,我们只需要简单地写:
job1.then(job2).then(job3).catch(handleError)
其中,job1job2job3都是Promise对象。

下面的例子演示了如何串行执行一系列需要异步计算获得结果的任务:

// 0.5秒后返回 input * input 的计算结果
function multiply (input) {
  return new Promise(function (resolve, reject) {
    console.log('calculating ' + input + ' x ' + input + '...');
    setTimeout(resolve, 500, input * input);
  })
}

// 0.5秒后返回input+input的计算结果:
function add (input) {
  return new Promise(function (resolve, reject) {
    console.log('calculating ' + input + ' + ' + input + '...')
    setTimeout(resolve, 500, input + input);
  })
}

var p = new Promise (function (resolve, reject) {
  console.log('start new promise...');
  resolve(123);
})

p.then(multiply)
  .then(add)
  .then(multiply)
  .then(add)
  .then(function (result) {
    console.log('Got value: ' + result);
  })

可以看到控制台输出为:

start new Promise...

calculating 123 x 123...

calculating 15129 + 15129...

calculating 30258 x 30258...

calculating 915546564 + 915546564...

Got value: 1831093128

setTimeout 可以看成一个模拟网络等异步执行的函数。现在,我们把上一节的AJAX异步执行函数转换为Promise对象,看看用Promise如何简化异步处理:

// ajax函数将返回Promise对象:
function ajax (method, url, data) {
  var request = new XMLHttpRequest();
  return new Promise(function (resolve, reject) {
    request.onreadystatechange = function () {
      if (request.readystate === 4) {
        if (request.status === 200) {
          resolve(request.responseText);
        } else {
          reject(request.status);
        }
      }
    }
    request.open(method, url);
    request.send(data);
  })  
}

var p = ajax('GET', 'https://suggest.taobao.com/sug?code=utf-8&q=大衣');
p.then(function (text) {
  console.log(text);
}).catch(function (status) {
  console.log('ERROR CODE: ' + status);
})

控制台输出结果为:

{"result":[
    ["大衣女","17274309"],
    ["大衣男","5516628"],
    ["大衣女长款","9112453"],
    ["大衣男长款","6513003"],
    ["大衣女学生","1590887"],
    ["大衣男短款","640927"],
    ["大衣扣子","412405"],
    ["大衣架","822564"],
    ["大衣女短款","1384908"],
    ["大衣女加厚","3997340"]
  ]
}

除了串行执行若干异步任务外,Promise还可以并行执行异步任务。

试想一个页面聊天系统,我们需要从两个不同的URL分别获得用户的个人信息和好友列表,这两个任务是可以并行执行的,用Promise.all()实现如下:

var p1 = new Promise (function (resolve, reject) {
  setTimeout(resolve, 500, 'P1');
});
var p2 = new Promise (function (resolve, reject) {
  setTimeout(resolve, 500, 'P2');
})

// 同时执行p1和p2,并在它们都完成后执行then:
Promise.all([p1, p2]).then(function (res) {
  console.log(res);//获得一个Array: ['P1', 'P2']
})

有些时候,多个异步任务是为了容错。比如,同时向两个URL读取用户的个人信息,只需要获得先返回的结果即可。这种情况下,用Promise.race()实现:

var p1 = new Promise(function (resolve, reject) {
  setTimeout(resolve, 500, 'P1');
})
var p2 = new Promise(function (resolve, reject) {
  setTimeout(resolve, 600, 'P2');
})

Promise.race([p1, p2]).then(function (res) {
  console.log(res); // 'P1'
})

由于p1执行较快,Promisethen()将获得结果'P1'。p2仍在继续执行,但执行结果将被丢弃。

如果我们组合使用Promise,就可以把很多异步任务以并行串行的方式组合起来执行。


1: async、await

先说一下async的用法,它作为一个关键字放到函数前面,用于表示函数是一个异步函数,因为async就是异步的意思, 异步函数也就意味着该函数的执行不会阻塞后面代码的执行。 写一个async 函数

async function timeout () {
  return 'hello, async';
}

语法很简单,就是在函数前面加上async 关键字,来表示它是异步的,那怎么调用呢?async 函数也是函数,平时我们怎么使用函数就怎么使用它,直接加括号调用就可以了,为了表示它没有阻塞它后面代码的执行,我们在async 函数调用之后加一句console.log;

async function timeout() {
  return 'hello, async';
}
timeout();
console.log('虽然我在后面,但是我先执行');

打开浏览器控制台,我们看到了

async-test1.png
async 函数 timeout 调用了,但是没有任何输出,它不是应该返回 'hello, async'? 先不要着急, 看一看timeout()执行返回了什么? 把上面的 timeout() 语句改为console.log(timeout())
async function timeout () {
    return 'hello, async';
}
console.log(timeout());
console.log('虽然我在后面,但是我先执行');

继续看控制台

async-test2.png
原来async 函数返回的是一个promise对象,如果要获取到promise 返回值,我们应该用then 方法, 继续修改代码
async function timeout () {
    return 'hello, async';
}
timeout().then(res => {
    console.log(res);
})
console.log('虽然我在后面,但是我先执行');

看控制台

async-test3.png
我们获取到了"hello, async', 同时timeout 的执行也没有阻塞后面代码的执行,和 我们刚才说的一致。
这时,你可能注意到控制台中的 Promise 有一个resolved,这是async 函数内部的实现原理。如果async 函数中有返回一个值 ,当调用该函数时,内部会调用Promise.resolve() 方法把它转化成一个promise 对象作为返回,但如果timeout 函数内部抛出错误呢? 那么就会调用Promise.reject() 返回一个promise 对象, 这时修改一下timeout 函数
async function timeout (flag) {
  if (flag) {
    return 'hello, async';
  } else {
    throw 'my god, failure';
  }
}
console.log(timeout(true));  // 调用 Promise.resolve() 返回 promise 对象
console.log(timeout(false)); // 调用 Promise.reject() 返回 promise 对象

控制台如下:


async-test4.png

如果函数内部抛出错误, promise 对象有一个catch 方法进行捕获。

timeout(false).catch(err => {
  console.log(err);
})

async 关键字差不多了,我们再来考虑await 关键字,await是等待的意思,那么它等待什么呢,它后面跟着什么呢?其实它后面可以放任何表达式,不过我们更多的是放一个返回 promise 对象的表达式。注意await 关键字只能放到async 函数里面
现在写一个函数,让它返回promise 对象,该函数的作用是2s 之后让数值乘以2

// 2s 之后返回双倍的值, doubleAfter2Seconds函数返回值为 Promise 对象
async function doubleAfter2Seconds (num) {
  return new Promise((resolve, reject) => {
    setTimeout (() => {
      resolve(num * 2);
    }, 2000)
  })
}
//此时如果调用 doubleAfter2Seconds函数(如下),2秒后控制台会输出双倍结果(60)
doubleAfter2Seconds(30).then(res => {
  console.log(res);
});

现在再写一个async 函数,从而可以使用await 关键字, await 后面放置的就是返回promise对象的一个表达式,所以它后面可以写上 doubleAfter2seconds 函数的调用

async function testResult () {
  let result = await doubleAfter2Seconds(30);
  console.log(result);
}

现在调用testResult 函数

testResult();

打开控制台,2s 之后,输出了60.
现在我们看看代码的执行过程,调用testResult 函数,它里面遇到了await, await 表示等一下,代码就暂停到这里,不再向下执行了,它等什么呢?等后面的promise对象执行完毕,然后拿到promise resolve 的值并进行返回,返回值拿到之后,它继续向下执行。具体到 我们的代码, 遇到await 之后,代码就暂停执行了, 等待doubleAfter2seconds(30)执行完毕,doubleAfter2seconds(30) 返回的promise 开始执行,2秒 之后,promise resolve 了, 并返回了值为60, 这时await 才拿到返回值60, 然后赋值给result, 暂停结束,代码才开始继续执行,执行 console.log语句。


就这一个函数,我们可能看不出 async/await 的作用,如果我们要计算3个数的值,然后把得到的值进行输出呢?

async function testResult () {
  let first = await doubleAfter2Seconds(10);
  let second = await doubleAfter2Seconds(20);
  let third = await doubleAfter2Seconds(30);
  console.log(first + second + third);
}
testResult();

6秒后,控制台输出120, 我们可以看到,写异步代码就像写同步代码一样了,再也没有回调地域了。


再写一个真实的例子,我原来做过一个小功能,话费充值,当用户输入电话号码后,先查找这个电话号码所在的省和市,然后再根据省和市,找到可能充值的面值,进行展示。

为了模拟一下后端接口,我们新建一个node 项目。 新建一个文件夹 async, 然后npm init -y 新建package.json文件,npm install express --save 安装后端依赖,再新建server.js 文件作为服务端代码, public文件夹作为静态文件的放置位置, 在public 文件夹里面放index.html 文件, 整个目录如下:

async-await的项目应用1.png
server.js 文件如下,建立最简单的web 服务器
const express = require('express');
const app = express();
// express.static 提供静态文件,就是html, css, js 文件
app.use(express.static('public'));
app.use('/node_modules', express.static('node_modules'))

app.listen(3000, () => {
    console.log('http://localhost:3000');
})

再写index.html 文件,我在这里用了vue构建页面,用axios 发送ajax请求, 为了简单,可以用cdn 引入它们(这里我是通过npm安装引入的)。 html部分很简单,一个输入框,让用户输入手机号,一个充值金额的展示区域, js部分,按照vue 的要求搭建了模版

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Async/Await</title>
    <!-- 引入vue 和 axios -->
    <script src="/node_modules/vue/dist/vue.js"></script>
    <script src="/node_modules/axios/dist/axios.js"></script>
</head>
<body>
    <div id="app">

        <!-- 输入框区域 -->
        <div style="height: 50px;">
            <input type="text" placeholder="请输入电话号码" v-model="phoneNum">
            <button @click="getFaceResult">确定</button>
        </div>

        <!-- 充值面值 显示区域 -->
        <div>
            充值面值:
            <span v-for="(item, index) in faceList" :key="item">
                {{ item }}
            </span>
        </div>
    </div>

    <!-- js 代码区域 -->
    <script>
        new Vue({
            el: '#app',
            data: {
                phoneNum: '12345',
                faceList: ['20元', '30元', '50元']
            },
            methods: {
                getFaceResult () {
                    
                }
            }
        })
    </script>
</body>
</html>

为了得到用户输入的手机号,给input 输入框添加v-model指令,绑定phoneNum变量。展示区域则是 绑定到faceList 数组,v-for 指令进行展示, 这时命令行nodemon server 启动服务器,如果你没有安装nodemon, 可以npm install -g nodemon 安装它。启动成功后,在浏览器中输入 http://localhost:3000, 可以看到页面如下, 展示正确

async-await的项目应用2.png
现在我们来动态获取充值面值。当点击确定按钮时, 我们首先要根据手机号得到省和市,所以写一个方法来发送请求获取省和市,方法命名为getLocation, 接受一个参数phoneNum , 后台接口名为phoneLocation,当获取到城市位置以后,我们再发送请求获取充值面值,所以还要再写一个方法getFaceList, 它接受两个参数, province 和city, 后台接口为faceList,在methods 下面添加这两个方法getLocation, getFaceList
methods: {
  // 获取城市信息
  getLocation (phoneNum) {
    return axios.post('/phoneLocation', {
      phoneNum
    })
  },

  // 获取面值
  getFaceList (province, city) {
    return axios.post('/faceList', {
      province,
      city
    })
  },
  
  // 点击确定按钮,获取面值列表
  getFaceResult () {
    
  }
}

现在再把两个后台接口写好,为了演示,写的非常简单,没有进行任何的验证,只是返回前端所需要的数据。Express 写这种简单的接口还是非常方便的,在app.use 和app.listen 之间添加如下代码

// 电话号码返回省和市,为了模拟延迟,使用了setTimeout
app.post('/phoneLocation', (req, res) => {
  setTimeout(() => {
    res.json({
      success: true,
      obj: {
        province: '广东',
        city: '深圳'
      }
    })
  }, 1000)
})

// 返回面值列表
app.post('/faceList', (req, res) => {
  setTimeout(() => {
    res.json({
      success: true,
      obj: ['20元', '30元', '50元', '100元']
    })
  }, 1000)
})

最后是前端页面中的click 事件的getFaceResult, 由于axios 返回的是promise 对象,我们使用then 的链式写法,先调用getLocation方法,在其then方法中获取省和市,然后再在里面调用getFaceList,再在getFaceList 的then方法获取面值列表,

                // 点击确定按钮时,获取面值列表
                getFaceResult () {
                    this.getLocation(this.phoneNum)
                        .then(res => {
                            if (res.status === 200 && res.data.success) {
                                let province = res.data.province;
                                let city = res.data.city;

                                this.getFaceList(province, city)
                                    .then(res => {
                                        if (res.status === 200 && res.data.success) {
                                            this.faceList = res.data.obj;
                                        }
                                    })
                            }
                        })
                        .catch(err => {
                            console.log(err);
                        })
                }

现在点击确定按钮,可以看到页面中输出了 从后台返回的面值列表。这时你看到了then 的链式写法,有一点回调地域的感觉。现在我们在有async/ await 来改造一下。

首先把 getFaceResult 转化成一个async 函数,就是在其前面加async, 因为它的调用方法和普通函数的调用方法是一致,所以没有什么问题。然后就把 getLocation 和

getFaceList 放到await 后面,等待执行, getFaceResult 函数修改如下

async getFaceResult () {
    try {
        let location = await this.getLocation(this.phoneNum);
        if (location.data.success) {
            let province = location.data.province;
            let city = location.data.city;
            let result = await this.getFaceList(province, city);
            if (result.data.success) {
                this.faceList = result.data.obj;
            }
        }
    } catch (err) {
        console.log(err);
    }
}

现在把服务器停掉,可以看到控制台中输出net Erorr,整个程序正常运行。

上一篇下一篇

猜你喜欢

热点阅读