博文地址: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);
        };
    }
}());
复制代码

参考: