JavaScript 和别的语言有很大的区别,他始终是单线程的,这是因为在浏览器环境,浏览器只提供了 JavaScript 一条线程(webwork),而 web 应用的所有 JavaScript 代码都会使用这一条线程来执行。在这个背景下,如果我们有很复杂的计算或者很长时间的 http 请求就会阻塞住 JavaScript 的这条线程,导致之后的代码无法被执行。

异步编程就显得非常重要,JavaScript 语言的一个优势就是知道如何处理异步代码而不是阻塞线程。我们用到的 setTimeout、http 请求的回调等其实都是使用到了异步编程的思想。

一、Timer 定时器

JavaScript 当中最简单的异步方法就是定时器 setTimeout 与 setInterval。这个函数用来在指定时间触发一个函数。

console.log('started Timer');  
setTimeout(function(){ console.log('finished Timer'); }, 1000);  

上面的代码中 console.log('finished Timer'); 这个方法并没有被立即触发,如果不出意外的话,会在解析器解析完 JavaScript 代码 1s 后打印出 finished Timer。但是在这个过程中并不影响 setTimeout 之后代码的运行,JavaScript 解析器并不是卡在 setTimeout 方法这里,持续等待 1s 钟才往下读取。相反的 JavaScript 解析器会读取当前这个文件的所有代码完毕后,再去执行 setTimeout 的方法。如果你给 setTimeout 设置的时间过小(以 4 毫秒为例),而当前文件体积很大或者 cpu 的负载过重等等甚至会有可能导致 console.log('finished Timer'); 大于 4 毫秒。

Timer 定时器会把设定的回调推进一个队列当中,这个队列就是我们说的 Event Loop 事件循环。Event Loop 其实是一个回调方法的队列,当你设定了 setTimeout 或者 setInterval 方法的时候,回调方法就进入了这个队列。而这个时候 JavaScript 引擎并不会立即执行这个队列,直到当前区域的所有代码都被执行了。看起来好像是实现了多线程,但是 JavaScript 引擎只是『转换』了方法执行的顺序,Event Loop 让 JavaScript 看起来实现了『永不阻塞』。

二、AJAX (回调)

大家都使用过 ajax 来实现 http 请求,我们在处理 http 的响应的时候,并不是在立即发起请求之后就使用该请求的响应。因为基于网络速度或者服务器响应速度等原因,响应总是需要一点时间。

var http = new XMLHttpRequest();  
http.open('get', 'www.wuchengkai.com', true);  
http.send();

// 直接使用 
console.log( http.responseText ); // undefined

// 回调使用
http.onload = function(){  
    console.log( http.responseText );
};

如果我们立即使用响应的请求很有可能拿到的就是 undefined。为了保险起见,回调总是被派上用场。假如这条请求需要 100 秒,JavaScript 引擎不会傻傻的等待 100 秒。他会去执行别的工作,等待这条响应完成之后触发注册的回调事件。回调事件其实就是将一个函数作为另外一个函数的参数,在函数的尾部执行给定的参数回调。

UserModel.find({}, function(err, docs){  
    if( err ) throw err;
    console.log( docs );
});

这种方式倒是看起来简单,当一个方法完成之后立即去执行另外一个方法。但是如果逻辑变得图片很复杂,回调次数陡然增多代码就变得复杂、嵌套太多、高耦合。比如实现一个简单的爬虫。

const website = 'https://www.wuchengkai.com/';  
request(website, function( err, res ){  
    if( err ) throw err;
    const $ = cheerio.load( res.body.toString() );
    $('.g-article a').each(function(){
        request( url.resolve( website, $( this ).attr('href') ), function( err, res ){
            ...
            ...
            ...
        });
    });
});

逻辑的关系导致了回调次数的陡然增加陷入了一种 callback hell(回调噩梦),代码变得不直观难以维护。幸好 ES 之后的版本给我们提供了其他的方法来使用。

三、Promise

Promise 是 ES6 为了解决异步编程问题提供的一种解决方案,Promise 翻译过来就是保证的意思,保证你写的方法会执行,但是不能保证到底什么时候执行,我猜这个就是初衷吧。Promise 是一个构造函数,实例化的时候接受带有 resolve 、reject两个参数的函数对象,第一个参数用在处理执行成功的场景,第二个参数则用在处理执行失败的场景。 一旦我们的操作完成即可调用这些函数。

var promise = new Promise(function(resolve, reject) {  
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

一个实例化过的 Promise 对象具有几种状态:

  • pending: 初始状态, 既不是 fulfilled 也不是 rejected.
  • fulfilled: 成功的操作.
  • rejected: 失败的操作.
  • 当 promise 的状态是 pending 的时候回调方法都不执行,fulfilled 代表异步操作成功执行 resolve 方法,rejected 则去执行 reject 回调。

    JavaScript 异步编程

    Promise 的一个优点就是他可以使用 Promise.prototype.then 这个原型上的方法来实现类似 jQuery 的链式调用方法,使得回调变得清晰简单。

    var p2 = new Promise(function(resolve, reject) {  
      resolve(1);
    });
    
    p2.then(function(value) {  
      console.log(value); // 1
      return value + 1;
    }).then(function(value) {
      console.log(value); // 2
    });
    

    第一个 then 方法执行完毕可以将运行的结果提供了第二个 then 作为参数来使用,这样就能对整个的 Promise 流程做一个把控。由于 Promise 是 ES6 的版本,IE 系列全军覆没,生产环境慎用。

    四、Generator 与 async

    Promise 是 ES6 版本的异步编程解决方案目前还没有被浏览器全方位支持,而 ES6 版本的 Generator 已经在 nodejs 上大行其道。那么为什么有了 Promise 还需要用 Generator 呢?

    Generator 同样被用来解决异步编程的问题,相对于使用 Promise 而言,他的表现方式看起来更像是『非异步的代码』。Generator函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield语句,定义不同的内部状态(yield语句在英语里的意思就是“产出”)。

    function *generatorDemo(){  
        //... some code 1
        var val1 = yield task1();
        //... some code 2
        var val2 = yield task2(val1);
        //... some code 3
        var val3 = yield task3(val2);
        return val3;
    }
    
    generatorDemo.next();  
    

    标明当前的方法是 Generator 方法,yield 相当于函数的阻断器。存在 yield 标示的代码不会执行,只有当你去使用了 next 方法的时候。才会触发 yield 中断的方法。next 方法用来继续 Generator 方法的执行, 而 next 可以接受一个参数作为上一个 yield 方法的返回值。

    function* foo(x) {  
      var y = 2 * (yield (x + 1));
      var z = yield (y / 3);
      return (x + y + z);
    }
    
    var a = foo(5);  
    a.next() // Object{value:6, done:false}  
    a.next() // Object{value:NaN, done:false}  
    a.next() // Object{value:NaN, done:true}
    
    var b = foo(5);  
    b.next() // { value:6, done:false }  
    b.next(12) // { value:8, done:false }  
    b.next(13) // { value:42, done:true }  
    

    在使用 b.next(12) 的时候,传入了一个参数值 12 ,这就相当于将 var y = 2 * (yield (x + 1)) 中的 yield (x + 1) 返回值改为 12。

    async 其实是 ES7 提出的一种异步函数方法,看起来和 ES6 的 Generator 差不多。这是把 Generator 的 * 声明替换成了 async,yeild 替换成了 await 关键字。这看起来让异步函数更加的语义化。async 是未来异步编程的标准。

    async function main() {  
        var result1 = await request( "http://some.url.1" );
        var data = JSON.parse( result1 );
    
        var result2 = await request( "http://some.url.2?id=" + data.id );
        var resp = JSON.parse( result2 );
        console.log( "The value you asked for: " + resp.value );
    }
    
    main();