js异步那些事

图片 3

1. 讲个笑话

JavaScript 是一门编程语言

简单来说,异步编程就是在执行一个指令之后不是马上得到结果,而是继续执行后面的指令,等到特定的事件触发后,才得到结果。用
JavaScript 构建一个应用的时候经常会遇到异步编程,不管是 Node 服务端还是
Web 前端。

回调地狱

2. 异步编程

JavaScript 由于某种原因是被设计为单线程的,同时由于 JavaScript
在设计之初是用于浏览器的 GUI 编程,这也就需要线程不能进行阻塞。

所以在后续的发展过程中基本都采用异步非阻塞的编程模式。

简单来说,异步编程就是在执行一个指令之后不是马上得到结果,而是继续执行后面的指令,等到特定的事件触发后,才得到结果。

也正是因为这样,我们常常会说: JavaScript 是由事件驱动的。

那如何去进行异步编程呢?就目前的标准以及草案来看,主要有下面的几种方式:

对 JavaScript
程序员来说,处理回调是家常,但是处理层次过深的回调就没有那么美好了,下面的示例代码片段用了三层回调,再补脑一下更多层的场景,简直是酸爽,这就是传说中的回调地狱。

3. 异步实现

用 JavaScript 构建一个应用的时候经常会遇到异步编程,不管是 Node
服务端还是 Web 前端。

那如何去进行异步编程呢?就目前的标准以及草案来看,主要有下面的几种方式:

  • 回调
  • promise
  • Generator
  • await/async
  • 回调
  • promise
  • Generator
  • await/async
getDirectories { getFiles(dirs[0], function { getContent(files[0], function { console.log; console.log; function getDirectories { setTimeout { callback; }, 1000);} function getFiles { setTimeout { callback([dir + '/test1.txt', dir + '/test2.txt']); }, 1000)} function getContent { setTimeout { callback; }, 1000)}

3.1 回调

这种异步的方式是最基础的实现,如果你曾经写过一点的 Node,
可能经常会遇到这样的代码:

connection.query(sql, (err, result) => {
    if(err) {
        console.err(err)
    } else {
        connection.query(sql, (err, result) => {
            if(err) {
                console.err(err)
            } else {
                ...
            }
        })
    }
})

如此,connection.query() 是一个异步的操作,我们在调用他的时候,不会马上得到结果,而是会继续执行后面的代码。这样,如果我们需要在查到结果之后才做某些事情的话,就需要把相关的代码写在回调里面,如果涉及到多个这样的异步操作,就势必会陷入到回调地狱中去。

这种回调地狱不仅看起来很不舒服,可读性比较差;除此之外还有比较重要的一点就是对异常的捕获无法支持。

一 、回调

回调函数是一段可执行的代码段,它作为一个参数传递给其他的代码,其作用是在需要的时候方便调用这段(回调函数)代码。

在JavaScript中函数也是对象的一种,同样对象可以作为参数传递给函数,因此函数也可以作为参数传递给另外一个函数,这个作为参数的函数就是回调函数。

我们在调用他的时候,不会马上得到结果,而是会继续执行后面的代码。这样,如果我们需要在查到结果之后才做某些事情的话,就需要把相关的代码写在回调里面,如果涉及到多个这样的异步操作,就势必会陷入到回调地狱中去。

回调什么时候执行

回调函数,一般在同步情境下是最后执行的,而在异步情境下有可能不执行,因为事件没有被触发或者条件不满足。

回调函数的使用场合

  • 资源加载:动态加载 js 文件后执行回调,加载 iframe 后执行回调,ajax
    操作回调,图片加载完成执行回调,AJAX 等等。

  • DOM 事件及 Node.js 事件基于回调机制(Node.js
    回调可能会出现多层回调嵌套的问题)。

  • setTimeout 的延迟时间为 0,这个 hack 经常被用到,settimeout
    调用的函数其实就是一个 callback 的体现

  • 链式调用:链式调用的时候,在赋值器(setter)
    法中(或者本身没有返回值的方法中)很容易实现链式调用,而取值器(getter)相对来说不好实现链式调用,因为你需要取值器返回你需要的数据而不是
    this 指针,如果要实现链式方法,可以用回调函数来实现。

  • setTimeout、setInterval
    的函数调用得到其返回值。由于两个函数都是异步的,即:他们的调用时序和程序的主流程是相对独立的,所以没有办法在主体里面等待它们的返回值,它们被打开的时候程序也不会停下来等待,否则也就失去了
    setTimeout 及 setInterval 的意义了,所以用 return
    已经没有意义,只能使用 callback。callback 的意义在于将 timer
    执行的结果通知给代理函数进行及时处理。

例如:

var friends = ["Mike", "Stacy", "Andy", "Rick"];

friends.forEach(function (eachName, index){
console.log(index + 1 + ". " + eachName); // 1. Mike, 2. Stacy, 3. Andy, 4. Rick
});

解决方案

3.2 Promise

Promise 是 ES 2015 原生支持的,他把原来嵌套的回调改为了级联的方式。

一般着,我们对一个 Promise 可以这样写:

var a = new Promise(function(resolve, reject) {
  setTimeout(function() {
      resolve('1')
  }, 2000)
})
a.then(function(val) {
    console.log(val)
})

如果要涉及到多个异步操作的顺序执行问题,我们可以这样写:

var a = new Promise(function(resolve, reject) {
  setTimeout(function() {
      resolve('1')
  }, 2000)
})

a
  .then(function(val){
    console.log(val)
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve('2')
      }, 2000)
    })
  })
  .then(function(val) {
    console.log(val)
  })

也可以把函数抽离出来

var a = new Promise(function(resolve, reject) {
  setTimeout(function() {
      resolve('1')
  }, 2000)
})

function b(val) {
  console.log(val)

  return new Promise(function(resolve, reject) {
    setTimeout(function() {
        resolve('2')
    }, 2000)
  })
}

a.then(b).then(function(val) {
console.log(val)
})

我们只需要 return 一个 Promise 即可实现这种多个异步操作的顺序执行。

粗略来看,这是一个比较优雅的异步解决方案了,并且在 Promise 中我们也可以实现分级的 catch

但对于之前接触过其他语言的同学来说还是比较别扭的。那能否用同步的方式来书写异步呢?

二、Promise

Promise 是 ES 2015 原生支持的,他把原来嵌套的回调改为了级联的方式。

一般着,我们对一个 Promise 可以这样写:

var a = new Promise(function(resolve, reject) {
  setTimeout(function() {
      resolve('1')
  }, 2000)
})
a.then(function(val) {
    console.log(val)
})

如果要涉及到多个异步操作的顺序执行问题,我们可以这样写:

var a = new Promise(function(resolve, reject) {
  setTimeout(function() {
      resolve('1')
  }, 2000)
})

a
  .then(function(val){
    console.log(val)
    return new Promise(function(resolve, reject) {
      setTimeout(function() {
          resolve('2')
      }, 2000)
    })
  })
  .then(function(val) {
    console.log(val)
  })

我们只需要 return 一个 Promise 即可实现这种多个异步操作的顺序执行。

生态圈中有很多异步解决方案可以处理回调地狱的问题,比如 bluebird、Q
等,本文重点介绍 ECMAScript 6/7 规范中对异步编程的支持。

3.3 Generator

在 ES 2015 中,出现了 Generator
的语法,熟悉 Python 的同学肯定对这种语法有点了解。

简单来说,Generator
可以理解为一个可以遍历的状态机,调用 next 就可以切换到下一个状态。

在 JavaScript 中,Generator 的 function 与 函数名之间有一个 *,
函数内部使用 yield 关键词,定义不同的状态。

先看一段代码:

function a() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 2000)
  });
};

var b = co(function *() {
    var val = yield a();
    console.log(val)
})

b()

上面的这段代码是借助 TJ 的 co 实现的,依照约定,co 中 yield 后面只能跟
Thunk 或者 Promise.

co 的实现代码很短,简单来说大体是这样:

// http://www.alloyteam.com/2015/04/solve-callback-hell-with-generator/

function co(genFun) {
    // 通过调用生成器函数得到一个生成器
    var gen = genFun();
    return function(fn) {
        next();
        function next(err, res) {
            if(err) return fn(err);
            // 将res传给next,作为上一个yield的返回值
            var ret = gen.next(res);
            // 如果函数还没迭代玩,就继续迭代
            if(!ret.done) return ret.value(next);
            // 返回函数最后的值
            fn && fn(null, res);
        }
    }
}

简单来说就是一直借助 generator 的 next
进行迭代,直到完成这个异步操作才返回。当前人家官方的 co 是 200
行代码,支持异步操作的并行:

co(function *() {
    var val = yield [
        yield asyn1(),
        yield asyn2()
    ]
})()

但如果我们使用 co,强迫症们就会觉得这不是标准的写法,有点 hack
小子的感觉。

幸运的是,在 ES 2016 的草案中,终于提出了标准的写法。

三、Generator

在 ES 2015 中,出现了 Generator 的语法。

简单来说,Generator 可以理解为一个可以遍历的状态机,调用 next
就可以切换到下一个状态。

在 JavaScript 中,Generator 的 function 与 函数名之间有一个 *,
函数内部使用 yield 关键词,定义不同的状态。

ES6 Promise

3.4 await/async

这是在 ES 2016 中引入的新关键词,这将在语言层面彻底解决 JavaScript
的异步回调问题,目前可以借助 babel 在生产环境中使用。使用 await/async
可以让异步的操作以同步的方式来写。

使用方法和 co 非常类似,同时也支持同步写法的异常捕获。

function a() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 2000)
  })
}

var b = async function() {
  var val = await a()
  console.log(val)
}

b()

如果上述的代码完全用 Promise 实现,极有可能是下面的代码:

function a() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1);
    }, 2000);
  });
};

var b = function() {
  a().then(val) {
    console.log(val)
  }
  console.log(val)
};

b();

相比较来说,await/async 解决了完全使用 Promise
的一个极大痛点——不同Promise之间共享数据问题:

Promise 需要设定外层数据开始共享,这样就需要在每个then里面进行赋值,而
await/async 就不存在这样的问题,只需要以同步的方式去写就可以了。

await/async 对异常的支持也是特别好的:

function a() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 2000)
  });
};

var b = async function() {
    try {
        var val = await a()
        console.log(val)
    } catch (err) {
        console.log(err)
    }
};

b();

四、await/async

这是在 ES 2016 中引入的新关键词,这将在语言层面彻底解决 JavaScript
的异步回调问题,目前可以借助 babel 在生产环境中使用。使用 await/async
可以让异步的操作以同步的方式来写。

相比较来说,await/async 解决了完全使用 Promise
的一个极大痛点——不同Promise之间共享数据问题:

Promise 需要设定外层数据开始共享,这样就需要在每个then里面进行赋值,而
await/async 就不存在这样的问题,只需要以同步的方式去写就可以了。

最后借用阮一峰老师微博上的图片来比较各异步的写法:

图片 1

回调,可能导致回调地狱

图片 2

async/await

图片 3

Promise

Promise 是一种异步编程的解决方案,是解决回调地狱问题的利器。

参考文献

  • 回调地狱 @
    奇舞团博客
  • ES6 Generator介绍 @ 腾讯全端 AlloyTeam 团队
    Blog
  • 使用Generator解决回调地狱 @ 腾讯全端 AlloyTeam 团队
    Blog
  • 「大概可能也许是」目前最好的 JavaScript 异步方案 async/await @
    LearnClound

Promise 在 JavaScript 生态圈被主流接受是在 2007 年 Dojo 框架增加了
dojo.Deferred 的功能。随着 dojo.Deferred 的流行,在 2009 年 Kris Zyp
提出了 CommonJS Promises/A 规范。随后生态圈中出现了大量 Promise 实现包括
Q.js、FuturesJS 等。当然 Promise 之所有这么流行很大程度上是由于 jQuery
的存在,只是 jQuery 并不完全遵守 CommonJS Promises/A
规范。随后正如大家看到的,ES 6 规范包含了 Promise。MDN 中对 Promise
是这样描述的:

Promise
对象是一个返回值的代理,这个返回值在promise对象创建时未必已知。它允许你为异步操作的成功或失败指定处理方法。
这使得异步方法可以像同步方法那样返回值:异步方法会返回一个包含了原返回值的以下的代码是「回调地狱」一节中的示例通过
Promise
实现,看上去代码也不是很简洁,但是比起传统的层级回调有明显改善,代码可维护性和可读性更强。

getDirectories { return getFiles.then { return getContent.then { console.log('filename:', val.file); console.log; function getDirectories() { return new Promise(function  { setTimeout { resolve; }, 1000); });} function getFiles { return new Promise(function  { setTimeout { resolve([dir + '/test1.txt', dir + '/test2.txt']); }, 1000); });} function getContent { return new Promise(function  { setTimeout { resolve({file: file, content: 'content'}); }, 1000); });}

ES6 Generator

Promise 的实现方式还不够简洁,我们还需要更好的选择,co 就是选择之一。co
是基于 Generator的异步流控制器,了解 co 之前首先需要理解 Generator。熟悉
C# 的同学应该都有了解,C# 2.0 的版本就引入了 yield
关键字,用于迭代生成器。ES 6 Generator 跟 C# 相似,也使用了 yield
语法糖,内部实现了状态机。具体用法可以参考 MDN 的文档 function*
一节,原理可以参考AlloyTeam 团队 Blog 深入理解 Generator。使用 co
巧妙结合 ES6 Generator 和 ES6 Promise 让异步调用更加和谐。

co{ var dirs = yield getDirectories(); var files = yield getFiles; var contentVal = yield getContent; console.log('filename:', contentVal.file); console.log;});

co
非常巧妙,其核心代码可以简化如下的示例,大体思路是采用递归遍历生成器直到状态完成,当然
co 做的跟多。

runGenerator{ var dirs = yield getDirectories(); var files = yield getFiles; var contentVal = yield getContent; console.log('filename:', contentVal.file); console.log;} function runGenerator; function go{ if return; result.value.then{ go; } go;}

ES7 Async/Await

ES6 Generator 确实很好,只可惜需要第三方库的支持。好消息是 ES 7 会引入
Async/Await 关键字完美解决异步调用的问题。好吧,.net 又领先了一步,.net
framework 4.5 已经率先支持了。今后的代码写起来是这样:

run();async function run() { var dirs = await getDirectories(); var files = await getFiles; var contentVal = await getContent; console.log('filename:', contentVal.file); console.log;}

结论

从经典的回调的异步编程方式,到 ES6 Promise 规范对异步编程的改善,再到 co
结合 ES Generator 优雅处理,最后 ES7 async/await
完美收官,可以让我们了解为什么 ECMAScript
会出现这些特性以及解决了什么问题,更加清晰地看到 JavaScript
异步编程发展的脉络。

You can leave a response, or trackback from your own site.

Leave a Reply

网站地图xml地图