开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
本次学习笔记是来源是阮一峰老师的ES6标准入门教程 第三版 第19章节
什么是异步
所谓异步就是指一个任务不是连续完成,用流程图打颗下班做饭的栗子:
以上可以很直观的看到同步与异步的区别,同步就是严格按照任务的先后顺序依次执行,这样操作效率低所花费的时间更长;异步就是在备菜的同时执行烧水和煮饭任务,无需等待每一项子任务的完成继续向下执行,这样做的好处是同一时间内可以执行更多的操作,效率高,时间少。
在ES6诞生之前,js异步编程的方法有四种:
- 回调函数
- 事件监听
- 发布/订阅
- Promise对象
回调函数
老师教程中写到js对异步编程的实现就是回调函数。而回调函数就是把任务的第二段单独写在一个函数里,等重新执行这个任务时,直接调用这个函数。
fs.readFile('/etc/passwd', 'utf-8', function (err, data) {
if (err) throw err;
console.log(data);
});
引用原文的代码,这里的回调函数就是readFile函数的第三个参数,等系统返回文件后,回调函数才会执行。但有个问题,为什么回调函数的第一个参数必须是err;老师再教材中的解答是,因为执行分两段,第一段执行完后,任务所在的上下文环境已经结束,在这之后抛出的错误原来的上下文环境已经无法捕获,只能当做参数,传入第二段手动捕获。
ok,这一段基本也懂了,继续。
Promise
Promise的出现本身是为了解决回调函数出现的多个回调函数嵌套的问题。
fs.readFile(fileA, 'utf-8', function (err, data) {
fs.readFile(fileB, 'utf-8', function (err, data) {
// ...
});
});
当有多个文件,就会出多重嵌套,且多个异步形成了强耦合,如果其中一个需要修改,则它的上下层回调函数都要进行修改,这种情况称为“回调函数地狱(callback hell)”。
Promise对象它不是一种新的语法功能,而是一种新的写法,它允许将回调函数的嵌套,改成链式调用
var readFile = require('fs-readfile-promise');
readFile(fileA)
.then(function (data) {
console.log(data.toString());
})
.then(function () {
return readFile(fileB);
})
.then(function (data) {
console.log(data.toString());`
})
.catch(function (err) {`
console.log(err);`
});
这样看的话确实比回调函数要更直观一点,代码是纵向发展了,但存在代码冗余问题。
generator 函数
协程
指多个线程相互协作,完成异步任务 运行流程如下:
- 协程A开始执行
- 协程A执行到一半,进入暂停,执行权转移到协程B
- 协程B执行完成,交还执行权
- 协程A恢复执行 上述的协程A就是异步任务,分为两段或多段执行。上代码
function* asyncJob() {
// ...其他代码
var f = yield readFile(fileA);
// ...其他代码
}
上面的asyncJob就是一个协程,在遇到yield语句时将执行权交给其他协程,等执行权返回再从暂停的地方继续执行。从写法上看和同步操作几乎一样。
协程的Generator函数实现
Generator函数是协程在ES6的实现,特点就可以交出函数的执行权(暂停执行)。
整个Generator函数就是一个封装的异步容器,或者说是异步任务的容器。需要暂停的地方都用yield语句注明。
function* gen(x) {
var y = yield x + 2;
return y;
}`
var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }
上面的代码返回的数据,在我的上一篇文章有做具体的解释,这里不做具体讲解,大家只要记得Generator函数返回的遍历器,只要一遇到yield语句就会暂停转交执行权给其它任务。
Generator函数的数据交换和错误处理
Generator函数除了可以暂停执行和恢复执行这两大特性外,还有函数体外的数据交换和错误处理机制。
首先需要知道next返回的value属性,是Generator函数向外输出数据,next方法还可以接受参数,向函数内部输入数据。
function* gen(x){
var y = yield x + 2;
return y;
}
var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }
上面代码,第一个next方法的value属性,返回表达式x + 2的值3,在调用gen函数时传入了一个1,由于调用时并不执行这个函数,而是返回这个函数的遍历器对象(即内部指针),所以第一个next方法不需要传入参数,返回的value是3;如果调用时不传参返回的value是NaN;第二个next带有参数2,这个参作为上个阶段异步任务的结果,传入Generator函数,被函数体内的y接收,以此这一步返回的value为2。这是向函数体内输入数据的操作。
Generator函数内部部署错误代码,捕获函数体外抛出的错误。
function* gen1(x){
try {
var y = yield x + 2;
} catch (e){
console.log(e);
}
return y;
}
var g1 = gen1(1);
console.log(g1.next());
console.log(g1.throw('出错了'));// 出错了
上面代码的最后一行,Generator函数体外,使用指针对象的throw方法抛出错误,可以被函数体内的try...catch代码块捕获。实现了出错代码和处理错误代码的分离。
异步任务的封装
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
这段代码执行了一个真实的异步任务,Generator函数封装了一个异步操作,该操作先是读取了一个远程接口,就是然后从JSON格式的数据解析信息。 执行方法如下
var g = gen();//获取遍历器对象
var result = g.next(); //执行异步任务的第一阶段
// fetch返回的是Promise对象需要then方法调用下一个next方法
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
虽然Generator函数将异步操作表示的很简洁,但流程管理不太方便(即何时执行第一阶段、何时执行第二阶段)。
由此引入一个新知识点,Thunk函数。
Thunk函数
自动执行Generator函数的方法。
Thunk函数自上世纪60年代就诞生了,当时有一个争论焦点于“求值策略”,函数的参数该何时求值。
var x = 1;
function f(m) {
return m * 2;
}
f(x + 5)
第一种意见:传值调用
f(x + 5)
//等同于
f(6)
第二种一件:传名调用
f(x + 5)
// 等同于
(x + 5) * 2
Thunk函数的含义
编译器的传名调用实现,是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就是Thunk函数。
function f(m) {
return m * 2;
}
f(x + 5);
// 等同于
var thunk = function () {
return x + 5;
};
function f(thunk) {
return thunk() * 2;
}
传名调用的实现策略就是用Thunk函数替换某个表达式。
js语言的Thunk函数
js语言是传值调用,在js中替换的不是表达式,而是多参函数,将其替换成一个只能接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);
// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
return function (callback) {
return fs.readFile(fileName, callback);
};
};
var readFileThunk = Thunk(fileName);
readFileThunk(callback);
任何函数,只要参数有回调函数,就能写成Thunk函数。
Generator函数的流程管理
Generator函数本身可以自动执行
function* gen() {
// ...
}
var g = gen();
var res = g.next();
while(!res.done){
console.log(res.value);
res = g.next();
}
上面的gen会自动完成所有步骤,但不适合异步操作,如果必须要保证前一步执行完,才能执行后一步,就需要用到Thunk。
var fs = require('fs');
var thunkify = require('thunkify');
var readFileThunk = thunkify(fs.readFile);
var gen = function* (){
var r1 = yield readFileThunk('/etc/fstab');
console.log(r1.toString());
var r2 = yield readFileThunk('/etc/shells');
console.log(r2.toString());
};
上面的代码yield用于将程序的执行移除generator函数,那就需要一种方法将执行权交还给Generator函数。
以下手动执行上面的Genertator函数
var g = gen();
var r1 = g.next();
r1.value(function (err, data) {`
if (err) throw err;
var r2 = g.next(data);
r2.value(function (err, data) {`
if (err) throw err;
g.next(data);
});
});
上面代码中,变量g是Generator函数的指针,表示目前执行到了哪一步。next负责将指针移到下一步,并返回该布的信息。
Thunk函数的自动流程管理
function run(fn) {
var gen = fn();
function next(err, data) {
var result = gen.next(data);
if (result.done) return;
result.value(next);
}
next();
}
function* g() {
var f1 = yield readFileThunk('fileA');
var f2 = yield readFileThunk('fileB');
// ...
var fn = yield readFileThunk('fileN');
}
run(g);
这里的run函数就是一个Generator自动执行器,内部的next就是Thunk函数的回调函数。next先将指针移到Generator函数的下一步,然后判断Generator函数是否结束,如果没结束就再次将next传入Thunk函数,否则就直接退出。这个执行器执行异步操作的前提是,在yield后面的必须是Thunk函数。
Thunk函数并非是自动执行Generator函数的唯一方式;因为自动执行的关键是自动控制Generator函数的流程,接收和交还程序的执行权。回调函数和Promise都可以做到这一点。在真实的开发场景中应该灵活应用。
结语
这篇文章作为第二篇的学习笔记,很多内容还是引用老师教程的原话。自己并没有延伸更深度的思考,对js异步和同步的区别并没做更深入的探讨;但学习都是一个知识点发散到另一个知识点,长期坚持就能构建自己的知识网络。
这次的学习让我对JS的单线程与异步感兴趣;下一个知识点学习已经有了。
加油!!!