博文地址:JS异步编程方法
众所周知, JS是一门单线程的语言,它不像服务端语言可以同时处理多个任务,但这不是JS的缺点,这是由执行环境决定的。由于JS是运行在浏览器端,而浏览器上不能同时存在两个任务对同一处DOM或者数据进行修改,否则浏览器就不知道该听谁的了,因此,这也决定了JS必须是单线程的语言。这种模式的好处是实现起来简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队,会拖慢这个程序的执行。为了解决这个问题,JS将任务的执行模式分为同步(Synchronous)和异步(Asynchronous)。
回调函数
首先看一下回调函数的定义:函数A作为参数(函数引用)传递到另一个函数B中,并且这个函数B执行函数A,那么函数A就是回调函数。 回调函数和异步并没有必然的联系,但我们常用回调函数来处理异步进程,比如Ajax。
ajax(url, () => {
// 处理逻辑
})
复制代码
回调函数有一个致命的弱点,就是容易写出回调地狱。这样的代码不利于阅读和维护,因为业务之间耦合性过高,而且不容易捕获错误。
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
复制代码
Promise
Promise是一种新的异步解决方案,ES6将其写进了语言标准,原生提供Promise对象。 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 Promise有三个状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。并且,Promise的状态是不可逆和不可变的,它只有两个发生的可能,从pending到fulfilled,从pending到rejected。
// 创建一个Promise实例
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
复制代码
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
// success
}, function(error) {
// failure
});
复制代码
Promise相对于回调函数的优势在于Promise实现了链式调用,也就是说每次调用then之后返回的都是一个Promise,并且是一个全新的Promise,原因也是因为状态不可变。如果你在then中使用了return,那么return的值会被Promise.resolve()包装。
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
复制代码
Generator
Generator是一个状态机,它封装了多个内部状态。执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了是一个状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
复制代码
上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(hello和world),即该函数有三个状态:hello,world 和 return 语句(结束执行)。 Generator函数与普通函数的调用方式相同,但是调用了之后不会立即执行,它会返回一个指向内部状态的对象,你必须调用遍历器对象的next方法,它才会使对象指针指向下一个状态,也就是说,Generator函数式分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。(可以想象这是一个代码打断点后步进的过程)
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
复制代码
next方法返回一个对象,它的value属性就是当前yield表达式的值,done属性表示遍历是否结束。 Generator函数编写起来还是比较麻烦的,但它也可以解决回调地狱问题:
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
复制代码
async/await
ES2017标准引入了async函数,使得异步操作变得更加方便,它被称为异步编程的终极解决方案。那么asnc函数是什么呢?其实,它就是Generator函数的语法糖。async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。 async函数相对于Generator函数的改进体现在: 1)内置执行器; 2)更好的语义; 3)更广的适用性; 4)返回值是Promise。(可以直接调用then方法,比Generator函数返回迭代器(Iterator)对象方便)
function resolveAfter2Seconds() {
return new Promise(resolve => {
setTimeout(() => {
resolve('resolved');
}, 2000);
});
}
async function asyncCall() {
console.log('calling');
var result = await resolveAfter2Seconds();
console.log(result);
// expected output: 'resolved'
}
asyncCall();
复制代码
async函数也有缺点,因为 await 将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低,但我们可以使用Primise.all解决。
async function test() {
// 以下代码没有依赖性的话,完全可以使用 Promise.all 的方式
// 如果有依赖性的话,其实就是解决回调地狱的例子了
await fetch(url)
await fetch(url1)
await fetch(url2)
}
复制代码
定时器函数
常用的定时器函数包括setTimeout、setInterval、requestAnimationFrame。这里主要讲requestAnimationFrame,它的兼容性请点这里查看。 由于JS是单线程语言,代码执行期间必定需要时间,因此导致setTimeout和setInterval必然会出现些许偏差,而requestAnimationFrame不会出现这样的情况,它接受一个回调函数作为参数,而回调函数通常会每秒执行60次,这正是为专门设置动画而出现的定时器,现代浏览器均支持这个api,但是一些低版本的浏览器可能不支持,我们可以借用setTimeout来模拟requestAnimationFrame:
// 参考张鑫旭大神博客
(function() {
var lastTime = 0;
var vendors = ['webkit', 'moz'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || // Webkit中此取消方法的名字变了
window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16.7 - (currTime - lastTime));
var id = window.setTimeout(function() {
callback(currTime + timeToCall);
}, timeToCall);
lastTime = currTime + timeToCall;
return id;
};
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}
}());
复制代码
参考: