翻看了一下之前的博客发现了好多地方写错了,所以今天特地写了这一篇博客来纠正之前的错误。同时也是为了更深入的去理解Javascript中的概念。注:本文都是个人理解与看法,如果有错欢迎指正。另外转载请注明出处。

本文适合具有一定基础,了解最基本的异步用法的人群阅读。当然如果不会也没关系,我已经尽量将本文写得适合零基础的小白阅读了。

什么是异步?

之前我给出的解释其实太偏向于底层方面的解释了(其中可能还有错),但其实在JS中不存在那么多且复杂的概念。你只需要理解JS的设计本身是单线程的,设计者的初衷是希望JS能够无阻塞地执行完所有的任务。而异步则是在这基础上将花费时间较多的(亦或是不确定的)的任务按顺序放在最后执行,比如像AJAX这样的操作。而由这些异步任务组成的队列我们通常叫做任务队列

换个角度,JS在遇到异步任务时其实只会将他加入任务队列而不是立即执行。这样做的好处是当页面资源未完全加载完毕时就可以让用户看到整个页面的,使用户不用等待页面资源完全加载完毕。当然这样也造成了当用户跳转到其他页面时当前的无法阻止未完成的异步的问题,这一点在使用模板引擎时更是明显。不过好在你也可以自己包装异步操作,使用flag去取消异步。

注意: 当异步任务中产生新的异步任务时新产生的任务始终是被添加到任务队列末尾的,这应该不难理解。

是否应该使用异步?

一般而言用户(程序员)没有必要刻意去使用异步,因为虽然异步有以上谈到的种种优点,但其实写出来的代码并不是非常好阅读的。当然这一点在ES6标准推出以后的Promise里有了很大的改善,甚至其他的语言里也仿照ES6做出了自己的Promise-Library。不过因为ES6标准至今占有率还是太低和Promise中的微任务和宏任务之分会让本文变得更加复杂,所以本文并不会花太多的笔墨去提及它带来的新概念。取而代之的是着重于介绍如何去使用它。

如何实现异步?

因为前面说过我们并不谈及ES6中的Promise新概念,所以这里也不再有微任务宏任务之分,而是统称为异步任务JS中实现异步的方法是将我们想实现异步的代码写成函数,作为回调传入异步函数(或者说是构造器?)即可。 (2月30日更正)JS中只能通过内置的异步函数实现异步。

常用的异步函数

实例代码

// 别忘了异步始终最后执行!
setTimeout(function() {
  console.log("Hello!");
}, 0);
console.log("== Start ==");

以上代码的输出为:

== Start ==
Hello!

关于以上异步函数(定时器和Promise)的使用方法可以参考MDN的文档与教程,这里就不再赘述了。

如何使异步顺序执行?

这里的“顺序执行”指的是如何让我们的代码能在异步操作完成后再执行。实际上使异步顺序执行的方法非常简单,与上面一样我们只需要将我们需要执行的操作包装成函数,当做回调参数传入并在对应的处理方法上调用就可以了。

这句话读起来可能不太好理解,所以我们供上代码以方便理解。下面就是一个非常典型的通过回调顺序执行的AJAX范例。

function fetchDataAsync(url, onReady) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onload = function () {
    onReady(this.responseText);
  };
  xhr.send(null);
}

fetchDataAsync("https://moeloli.ml", function(data) {
  console.log(data);
});

但正如上面提到过的,如果大量使用这种结构的代码我们的代码将会损失可读性,变得难以阅读。所以我个人是不推荐的。

增强可读性(类Promise实现)

因为XMLHttpRequest比较特殊(可能异步任务内部又触发了新的异步任务),使用setTimeout并不能将我们的回调直接加在其后,所以我们实现上只能使用闭包回调实现获取异步操作后需要resolve的值了。

AJAX是异步过程,但并非一定由一个异步完成。第一个异步任务完成后可能会在任务队列添加新的异步任务,所以使用setTimeout不一定能获取ajax请求中的值。

依赖:Runthen.js

连带注释也不足百行的精简类Promise实现。

/*
 * Copyright 2018 urain39
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

(function (global) {
  var ST_PENDING = 0,
      ST_RESOLVED = 1,
      ST_REJECTED = 2;

  function Runthen(executor) {
    var self = this;
    self._state = ST_PENDING;
    self._executor = executor;
    self._resolveList = [];
    self._onCatch = null;
  }

  Runthen.prototype.then = function (resolve) {
    var self = this;
    self._resolveList.push(resolve);
    return self;
  };

  Runthen.prototype.catch = function (onCatch) {
    var self = this;
    self._onCatch = onCatch;
    return self;
  };

  Runthen.prototype.destroy = function () {
    var self = this;
    self._resolveList.length = 0;
    self._onCatch = null;
  };

  Runthen.prototype.done = function () {
    var self = this,
        context = null,
        resolve = null,
        onCatch = null;
    if (self._state !== ST_PENDING) {
      return; // State has been changed, so just ignore it.
    }
    // The executor's callback
    resolve = function (value) {
      context = value;
      // Handle Promise-like then-chains.
      while (self._resolveList.length > 0) {
        resolve = self._resolveList.shift();
        // Call callbacks one by one.
        try {
          context = resolve(context);
        } catch (error) {
          onCatch = self._onCatch;
          if (typeof onCatch === "function") {
            onCatch(error);
          }
          // Mark state is rejected.
          self._state = ST_REJECTED;
          break;
        }
      }
      // Whatever, destroy it first.
      self.destroy();
      if (self._state === ST_REJECTED) {
        return; // State changed.
      }
      // Mark state is resolved.
      self._state = ST_RESOLVED;
    };
    // Call the executor directly(main-thread).
    self._executor(resolve);
  };

  // Syntactic Sugar?
  Runthen.resolve = function (value) {
    return new Runthen(function (resolve) {
      resolve(value);
    });
  };

  global.Runthen = Runthen;
})(this);

下面就是使用类Promise的方法增强可读性实现的代码。

function fetchDataAsync(url, onReady) {
  var xhr = new XMLHttpRequest();
  xhr.open("GET", url, true);
  xhr.onload = function () {
    onReady(this.responseText);
  };
  xhr.send(null);
}

function fetchData(url) {
  return new Runthen(function (resolve) {
    fetchDataAsync(url, resolve);
  });
}

fetchData("https://moeloli.ml")
.then(function(data) {
  console.log(data);
})
.catch(function (err) {
  alert(err);
  throw err;
}).done();

对比一下是不是感觉可读性高了很多呢?

注意:ES6原生的Promise并没有.done方法。

本文小结

扩展阅读

宏任务与微任务

正文里没写的部分放到这里来写吧。ES6后引入Promise概念的同时添加了一个叫做微任务的概念,与常规的异步任务(宏任务)对应。每当执行一个宏任务时,JS都会检测微任务队列中是否为空,如果不为空则一个接一个的取出并执行微任务。直到清空为止,然后再执行宏任务

这样做的好处是微任务会比宏任务费时更短,能够更有效的实现无阻塞的概念。否则的话宏任务之间的阻塞会影响到末尾新添加的微任务的调用时间(看到这你也应该明白了其实JS也是有阻塞这个概念的吧?)。

同步和异步:

简而言之异步的目的是实现无阻塞,而不是让费时的任务阻塞整个流程;异步和多线程并不是一回事,异步是最终目的,多线程只是实现的一种方法。

并行和并发:

并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生(也可以是同一时刻);并行可以通过多线程实现,而并发则不一定需要。

扩展小结