众所周知 javascript 是单线程的, 它的设计之初是为浏览器设计的 GUI 编程语言, GUI 编程的特性之一是保证 UI 线程一定不能阻塞, 否则体验不佳, 甚至界面卡死.
所谓的单线程就是一次只能完成一个任务, 其任务的调度方式就是排队, 这就和火车站洗手间门口的等待一样, 前面的那个人没有搞定, 你就只能站在后面排队等着.
这种模式的好处是实现起来简单, 执行环境相对单纯, 坏处就是只要有一个任务耗时很长, 后面的任务都会必须排队等着, 会拖延整个程序的执行. 常见的浏览器无响应(假死), 往往就是因为某一段 Javascript 代码长时间运行(比如死循环), 导致了整个页面卡在这个地方, 其他任务无法执行.
为了解决这个问题, Javascript 语言将任务的执行模式分成两种: 同步 (Synchronous) 和异步(Asynchronous).
"同步" 就是上面所说的, 后面的任务等待上一个任务结束, 然后再执行.
什么是 "异步"?
所谓异步简单说就是一个任务分成两段, 先执行一段, 转而执行其他任务, 等做好了准备转而执行第二段.
以下是当有 ABC 三个任务, 同步或异步执行的流程图:
同步
thread ->|----A-----||-----B-----------||-------C------|
复制代码
异步:
- A-Start ---------------------------------------- A-End
- | B-Start ----------------------------------------|--- B-End
- | | C-Start -------------------- C-End | |
- V V V V V V
- thread-> |-A-|---B---|-C-|-A-|-C-|--A--|-B-|--C--|---A-----|--B--|
复制代码
"异步" 非常重要. 在浏览器端, 耗时很长的操作都应该异步执行, 避免浏览器失去响应, 最好的例子就是 Ajax 操作. 在服务器端,"异步模式" 甚至是唯一的模式, 因为执行环境是单线程的, 如果允许同步执行所有 http 请求, 服务器性能会急剧下降, 很快就会失去响应.
本文简单梳理总结了 JavaScript 异步函数的发展历史如下图:
回调函数
- Promise
- Generator+co
- async,await
回调函数 Callbacks
似乎一切应该从回调函数开始谈起.
异步 JavaScript
在 Javascript 中, 异步编程方式只能通过 JavaScript 中的一等公民函数才能完成: 这种方式意味着我们可以将一个函数作为另一个函数的参数, 在这个函数的内部可以调用被传递进来的函数(即回调函数).
这也正是回调函数诞生的原因: 如果你将一个函数作为参数传递给另一个函数(此时它被称为高阶函数), 那么在函数内部, 你可以调用这个函数来完成相应的任务.
回调函数没有返回值(不要试图用 return), 仅仅被用来在函数内部执行某些动作.
看下面的例子:
- step1(function (value1) {
- step2(value1, function(value2) {
- step3(value2, function(value3) {
- step4(value3, function(value4) {
- // Do something with value4
- });
- });
- });
- });
复制代码
这里只是做 4 步, 嵌套了 4 层回调, 如果更多步骤呢? 显然这样的代码只是写起来比较爽但是缺点也很多.
过度使用回调函数所会遇到的挑战:
如果不能合理的组织代码, 非常容易造成回调地狱(callback hell), 这会使得你的代码很难被别人所理解.
不能捕获异常 (try catch 同步执行, 回调函数会加入队列, 无法捕获错误)
无法使用 return 语句返回值, 并且也不能使用 throw 关键字.
也正是基于这些原因, 在 JavaScript 世界中, 一直都在寻找着能够让异步 JavaScript 开发变得更简单的可行的方案. 这个时候就出现了 promise, 它解决了上述的问题.
Promise
Promise 的最大优势是标准化, 各类异步工具库都按照统一规范实现, 即使是 async 函数也可以无缝集成. 所以用 Promise 封装 API 通用性强, 用起来简单, 学习成本低.
一个 Promise 代表的是一个异步操作的最终结果.
Promise 意味着 [许愿 | 承诺] 一个还没有完成的操作, 但在未来会完成的. 与 Promise 最主要的交互方法是通过将函数传入它的 then 方法从而获取得 Promise 最终的值或 Promise 最终拒绝 (reject) 的原因. 要点有三个:
递归, 每个异步操作返回的都是 promise 对象
状态机: 三种状态转换, 只在 promise 对象内部可以控制, 外部不能改变状态
全局异常处理
1)定义
- var promise = new Promise(function(resolve, reject) {
- // do a thing, possibly async, then...
- if (/* everything turned out fine */) {
- resolve("Stuff worked!");
- }
- else {
- reject(Error("It broke"));
- }
- });
复制代码
每个 Promise 定义都是一样的, 在构造函数里传入一个匿名函数, 参数是 resolve 和 reject, 分别代表成功和失败时候的处理.
2) 调用
- promise.then(function(text){
- console.log(text)// Stuff worked!
- return Promise.reject(new Error('我是故意的'))
- }).catch(function(err){
- console.log(err)
- })
复制代码
它的主要交互方式是通过 then 函数, 如果 Promise 成功执行 resolve 了, 那么它就会将 resolve 的值传给最近的 then 函数, 作为它的 then 函数的参数. 如果出错 reject, 那就交给 catch 来捕获异常就好了.
我们可以通过调用 promise 的示例, 了解一下 propmise 的一些原理及特性:
普通调用实例:
- let fs = require('fs');
- let p = new Promise(function(resolve,reject){
- fs.readFile('./1.txt','utf8',(err,data)=>{
- err?reject(err):resolve(data);
- })
- })
- p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
复制代码
1.promise 实例可以多次调用 then 方法:
- p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
- p.then((data)=>{console.log(data)},(err)=>{console.log(err)});
复制代码
2.promise 实例可以支持 then 方法的链式调用, jquery 实现链式是通过返回当前的 this. 但是 promise 不可以通过返回 this 来实现. 因为后续通过链式增加的 then 不是通过原始的 promise 对象的状态来决定走成功还是走失败的.
p.then((data)=>{console.log(data)},(err)=>{console.log(err)}).then((data)=>{console.log(data)})
复制代码
3. 只要 then 方法中的成功回调和失败回调, 有返回值(包括 undefiend), 都会走到下个 then 方法中的成功回调中, 并且把返回值作为下个 then 成功回调的参数传进去.
第一个 then 走成功:
p.then((data)=>{return undefined},(err)={console.log()}).then((data)=>{console.log(data)})
输出: undefiend
第一个 then 走失败:
p.then((data)=>{console.log(1)},(err)={return undefined).then((data)=>{console.log(data)})
输出: undefiend
复制代码
4. 只要 then 方法中的成功回调和失败回调, 有一个抛出异常, 则都会走到下一个 then 中的失败回调中
第一个 then 走成功:
p.then((data)=>{throw new Err("错误")},(err)={console.log(1)}).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
输出: 错误
第一个 then 走失败:
p.then((data)=>{console.log(1)},(err)={throw new Err("错误")).then((data)=>{console.log('成功')},(err)=>{console.log(err)})
输出: 错误
复制代码
5. 成功和失败 只能走一个, 如果成功了, 就不会走失败, 如果失败了, 就不会走成功;
6. 如果 then 方法中, 返回的不是一个普通值, 仍旧是一个 promise 对象, 该如何处理?
答案: 它会等待这个 promise 的执行结果, 并且传给下一个 then 方法. 如果成功, 就把这个 promise 的结果传给下一个 then 的成功回调并且执行, 如果失败就把错误传给下一个 then 的失败回调并且执行.
7. 具备 catch 捕获错误; 如果 catche 前面的所有 then 方法都没有失败回调, 则 catche 会捕获到错误信息执行他就是用来兜儿底用的
p 是一个失败的回调:
p.then((data)=>{console.log('成功')}).then((data)=>{成功}).catche(e){console.log('错误')}
复制代码
8. 返回的结果和 promise 是同一个, 永远不会成功和失败
- var r = new Promise(function(resolve,reject){
- return r;
- })
- r.then(function(){
- console.log(1)
- },function(err){
- console.log(err)
- })
复制代码
可以看到结果一直都是 pending 状态
当你没有现成的 Promise 时, 你可能需要借助一些 Promise 库, 一个流行的选择是使用 https://github.com/petkaantonov/bluebird . 这些库可能会提供比原生方案更多的功能, 并且不局限于 Promise/A + 标准所规定的特性.
Generator(ECMAScript6)+co
JavaScript 生成器 https://blog.risingstack.com/introduction-to-koa-generators/ 是个相对较新的概念, 它是 ES6(也被称为 ES2015)的新特性. 想象下面这样的一个场景:
当你在执行一个函数的时候, 你可以在某个点暂停函数的执行, 并且做一些其他工作, 然后再返回这个函数继续执行, 甚至是携带一些新的值, 然后继续执行.
上面描述的场景正是 JavaScript 生成器函数所致力于解决的问题. 当我们调用一个生成器函数的时候, 它并不会立即执行, 而是需要我们手动的去执行迭代操作(next 方法). 也就是说, 你调用生成器函数, 它会返回给你一个迭代器. 迭代器会遍历每个中断点.
- function* foo () {
- var index = 0;
- while (index <2) {
- yield index++; // 暂停函数执行, 并执行 yield 后的操作
- }
- }
- var bar = foo(); // 返回的其实是一个迭代器
- console.log(bar.next()); // { value: 0, done: false }
- console.log(bar.next()); // { value: 1, done: false }
- console.log(bar.next()); // { value: undefined, done: true }
复制代码
更进一步的, 如果你想更轻松的使用生成器函数来编写异步 JavaScript 代码, 我们可以使用 https://www.npmjs.com/package/co 这个库, co 是著名的 tj 大神写的.
Co 是一个为 Node.js 和浏览器打造的基于生成器的流程控制工具, 借助于 Promise, 你可以使用更加优雅的方式编写非阻塞代码.
使用 co, 前面的示例代码, 我们可以使用下面的代码来改写:
- co(function* (){
- yield Something.save();
- }).then(function() {
- // success
- })
- .catch(function(err) {
- //error handling
- });
复制代码
你可能会问: 如何实现并行操作呢? 答案可能比你想象的简单, 如下(其实它就是 Promise.all 而已):
yield [Something.save(), Otherthing.save()];
复制代码
终极解决方案 Async/ await
简而言之, 使用 async 关键字, 你可以轻松地达成之前使用生成器和 co 函数所做到的工作.
在这背后, async 函数实际使用的是 Promise, 这就是为什么 async 函数会返回一个 Promise 的原因.
因此, 我们使用 async 函数来完成类似于前面代码所完成的工作, 可以使用下面这样的方式来重新编写代码:
- async function save(Something) {
- try {
- await Something.save(); // 等待 await 后面的代码执行完, 类似于 yield
- } catch (ex) {
- //error handling
- }
- console.log('success');
- }
复制代码
使用 async 函数, 你需要在函数声明的最前面加上 async 关键字. 这之后, 你可以在函数内部使用 await 关键字了, 作用和之前的 yield 作用是类似的.
使用 async 函数完成并行任务与 yiled 的方式非常的相似, 唯一不同的是, 此时 Promise.all 不再是隐式的, 你需要显示的调用它:
- async function save(Something) {
- await Promise.all[Something.save(), Otherthing.save()]
- }
复制代码
Async/Await 是异步操作的终极解决方案, Koa 2 在 node 7.6 发布之后, 立马发布了正式版本, 并且推荐使用 async 函数来编写 Koa 中间件.
这里给出一段 Koa 2 应用里的一段代码:
- exports.list = async (ctx, next) => {
- try {
- let students = await Student.getAllAsync();
- await ctx.render('students/index', {
- students : students
- })
- } catch (err) {
- return ctx.api_error(err);
- }
- };
复制代码
它做了 3 件事儿
通过 await Student.getAllAsync(); 来获取所有的 students 信息.
通过 await ctx.render 渲染页面
由于是同步代码, 使用 try/catch 做的异常处理
之后还会分享 node 的基本概念和 eventLoop(宏任务和微任务)
(完)
参考: The Evolution of Asynchronous JavaScript https://blog.risingstack.com/asynchronous-javascript/
来源: https://juejin.im/post/5b643d9ee51d4519596bce1f