余为前端菜鸟,感姿式水平匮乏,难观前端之大局。遂决定循前端知识之脉络,以兴趣为引,辅以几分坚持,望于己能解惑致知、于同道能助力一二,岂不美哉。前端
本系列代码及文档均在 此处node
继续啃老本...让人又爱又恨的异步c++
同步和异步git
function sync(){
const doA = '12'
const doB = '34'
}
function async(){
ajax('/api/doC1', (res) => {
doC2(res)
})
}
复制代码
同步很好理解,任务一个个执行,doA之后才能doB。github
异步任务能够理解为分两个阶段,doC的前一阶段是发出请求,后一阶段是在请求结束后的将来时刻处理。web
二者各有优劣,同步任务会致使阻塞,异步任务须要由有机制实现先后两部分的分离,使得主线程可以在这间歇内继续工做而不浪费时间等待。ajax
以浏览器为例大体过程:shell
主线程调用web api,经过工做线程发起请求,而后主线程继续处理别的任务(这是part1)。工做线程执行完了异步任务之后往事件队列里注册回调,等待主线程空闲后去队列中取出到主线程执行栈中执行(这是part2)。编程
并发和并行api
简单描述:并发是交替作不一样事情,并行是同时作不一样事情。
咱们能够经过多线程去处理并发,但说到底CPU只是在快速切换上下文来实现快速的处理。而并行则是利用多核,同时处理多个任务。
单线程和多线程
咱们总说js是单线程的,node是单线程的,其实这样的说法并不完美。所谓单线程指的是js引擎解释和执行js代码的线程是一个,也便是咱们常说的主线程。
又好比对于咱们熟悉的node,I/O操做实际上都是经过线程池来完成的,js->调用c++函数->libuv方法->I/O操做执行->完毕后js线程继续执行后续。
ajax('/a', (res) => {
ajax('/b, (res) => {
// ...
})
})
复制代码
丑陋的callback形式,再也不多说
Promise
诞于社区,初为异步编程之解决方案,后有ES6将其写入语言标准,终成今人所言之 Promise
对象// 接收以resolve和reject方法为参数的函数
const pr = new Promise((resolve, reject) => {
// do sth
resolve(1) // pending -> resolved
reject(new Error()) // pending -> rejected
})
复制代码
pr.then((value) => {
// onresolved cb
}, (err) => {
// onrejected cb
})
复制代码
Promise.prototype.then
采用链式写法,返回一个新的Promise,上一个回调的返回做为参数传递到下一个回调
Promise.prototype.catch
其实是.then(null, rejection)
的别名
一样支持链式写法,最后一个catch能够catch到前面任一个Promise跑抛出的未catch的error
Promise.all
参数需具备Iterator接口,返回为多个Promise实例
var p = Promise.all([p1, p2, p3]);
复制代码
p1, p2, p3均resolve后p才resolve,任一个reject则p就reject。
若内部有catch,则外部catch捕获不到异常。
Promise.race
// 若5秒未返回则抛错
const p = Promise.race([
fetch('/resource-that-may-take-a-while'),
new Promise(function (resolve, reject) {
setTimeout(() => reject(new Error('request timeout')), 5000)
})
]);
p.then(response => console.log(response));
p.catch(error => console.log(error));
复制代码
第一个状态改变的Promise会引发p状态改变。
Promise.resolve/reject
Promise.resolve('1')
Promise.resolve({ then: function() {
console.log(123)
} })
复制代码
Promise.prototype.finally
Promise.prototype.finally = function (callback) {
let P = this.constructor;
return this.then(
value => P.resolve(callback()).then(() => value),
reason => P.resolve(callback()).then(() => { throw reason })
);
};
复制代码
不管如何都会执行最后的cb
Promise为咱们提供了优于callback嵌套的异步选择,但实际上仍是基于回调来实现的。
简单的Promise实现代码能够看这里 github
基本概念
function * gen() {
const a = yield 1;
return 2
}
const m = gen() // gen{<suspended>}
m.next() // {value: 1, done: false}
m.next() // {value: 2, done: true}
m.next() // {value: undefined, done: true}
m // gen {<closed>}
复制代码
{ value, done }
对象。value
属性表示当前的内部状态的值,是yield表达式后面那个表达式的值,done
属性是一个布尔值,表示是否遍历结束{ value: undefined, done: true }
,Generator的内部属性[[GeneratorStatus]]
变为closed状态yield
Generator.prototype.next()
经过传入参数为Generator函数内部注入不一样的值来调整函数接下来的行为
// 这里利用参数实现了重置
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
// 传递的参数会被赋值给i(yield后的表达式的值(i))
// 而后执行var reset = i赋值给reset
g.next(true) // { value: 0, done: false }
复制代码
Generator.prototype.throw()
Generator.prototype.return()
try ... finally
存在时,return会在finally执行完后执行,最后的返回结果是return方法的参数,这以后Generator运行结束,下次访问会获得{value: undefined, done: true}
try ... finally
不存在时,直接执行return,后续和上一条一致以上三种方法都是让Generator恢复执行,并用语句替换yield表达式
yield*
在一个Generator内部直接调用另外一个Generator是没用的,若是须要在一个Generator内部yield另外一个Generator对象的成员,则须要使用yield*
function* inner() {
yield 'a'
// yield outer() // 返回一个遍历器对象
yield* outer() // 返回一个遍历器对象的内部值
yield 'd'
}
function* outer() {
yield 'b'
yield 'c'
}
let s = inner()
for (let i of s) {
console.log(i)
} // a b c d
复制代码
yield*
后跟一个遍历器对象(全部实现了iterator的数据结构实际上均可以被yield*
遍历)
被代理的Generator函数若是有return,return的值会被for...of忽略,因此next不会返回,可是实际上能够向外部Generetor内部返回一个值,以下:
function *foo() {
yield 2;
yield 3;
return "foo";
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}
复制代码
举个🌰
// 处理嵌套数组
function* Tree(tree){
if(Array.isArray(tree)){
for(let i=0;i<tree.length;i++) {
yield* Tree(tree[i])
}
} else {
yield tree
}
}
let ss = [[1,2],[3,4,5],6,[7]]
for (let i of Tree(ss)) {
console.log(i)
} // 1 2 3 4 5 6 7
// 理解for ...of 其实是一个while循环
var it = iterateJobs(jobs);
var res = it.next();
while (!res.done){
var result = res.value;
// ...
res = it.next();
}
复制代码
做为对象的属性的Generator函数
写法很清奇
let obj = {
* sss() {
// ...
}
}
let obj = ={
sss: function* () {
// ...
}
}
复制代码
Generator函数的this
Generator函数返回的是遍历器对象,会继承prototype的方法,可是因为返回的不是this,因此会出现:
function* ss () {
this.a = 1
}
let f = ss()
f.a // undefined
复制代码
想要在内部的this绑定遍历器对象?
function * ss() {
this.a = 1
yield this.b = 2;
yield this.c = 3;
}
let f = ss.call(ss.prototype)
// f.__proto__ === ss.prototype
f.next()
f.next()
f.a // 1
f.b // 2
f.c // 3
复制代码
举个🌰
// 利用暂停状态的特性
let clock = function* () {
while(true) {
console.log('tick')
yield
console.log('tock')
yield
}
}
复制代码
异步操做的同步化表达
// Generator函数
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
// ajax请求函数,回调函数中要将response传给next方法
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
// 须要第一次执行next方法,返回yield后的表达式,触发异步请求,跳到request函数中执行
var it = main();
it.next();
复制代码
控制流管理
// 同步steps
let steps = [step1Func, step2Func, step3Func];
function *iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
// 异步后续讨论
复制代码
TO BE CONTINUED
回到最初提到的异步:将异步任务看作两个阶段,第一阶段如今执行,第二阶段在将来执行,这里就须要将任务 暂停
。而前面说到的Generator彷佛刚好提供了这么一个当口,暂停
结束后第二阶段开启不就对应下一个next调用嘛!
想像我有一个异步操做,我能够经过Generator的next方法传入操做须要的参数,第二阶段执行完后返回值的value又能够向外输出,maybe Generator真的能够做为异步操做的容器?
协程A执行->协程A暂停,执行权转交给协程B->一段时间后执行权交还A->A恢复执行
// yield是异步两个阶段的分割线
function* asyncJob() {
// ...其余代码
var f = yield readFile(fileA);
// ...其余代码
}
复制代码
参数的求值策略
js中的Thunk函数
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
复制代码
看起来只是换了个样子,好像并无什么用Generator看起来很美妙,可是next调用方式看起来很麻烦,如何实现自执行呢?
Generator函数自动执行
function* gen() {
yield a // 表达式a
yield 2
}
let g = gen()
let res = g.next()
while(!res.done) {
console.log(res.value)
res = g.next() // 表达式b
}
复制代码
可是,这不适合异步操做。若是必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。
next方法是同步的,执行时必须马上返回值,yield后是同步操做固然没问题,是异步操做时就不能够了。处理方式就是返回一个Thunk函数或者Promise对象。此时value值为该函数/对象,done值仍是按规矩办事。
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);
});
});
复制代码
Thunk函数的自动流程管理
思路:
Generator函数中yield 异步Thunk函数,经过yield将控制权转交给Thunk函数,而后在Thunk函数的回调函数中调用Generator的next方法,将控制权交回给Generator。此时,异步操做确保完成,开启下一个任务。
Generator是一个异步操做的容器,实现自动执行须要一个机制,这个机制的关键是控制权的交替,在异步操做有告终果之后自动交回控制权,而回调函数执行正是这么个时间点。
// Generator函数的执行器
function run(fn) {
let gen = fn()
// 传给Thunk函数的回调函数
function cb(err, data) {
// 控制权交给Generator,获取下一个yield表达式(异步任务)
let result = gen.next(data)
// 没任务了,返回
if (result.done) return
// 控制权交给Thunk函数,传入回调
result.value(cb)
}
cb()
}
// Generator函数
function* g() {
let f1 = yield readFileThunk('/a')
let f2 = yield readFileThunk('/b')
let f3 = yield readFileThunk('/c')
}
// Thunk函数readFileThunk
const Thunk = function(fn) {
return function (...args) {
return function (callback) {
return fn.call(this, ...args, callback);
}
};
};
var readFileThunk = Thunk(fs.readFile);
readFileThunk(fileA)(callback);
// 自动执行
run(g)
复制代码
说明
基于Promise的执行器
function run(fn) {
let gen = fn()
function cb(data) {
// 将上一个任务返回的data做为参数传给next方法,控制权交回到Generator
// 这里将result变量引用{value, done}对象
// 不要和Generator中的`let result = yield xxx`搞混
let result = gen.next(data)
if (result.done) return result.value
result.value.then(function(data){
// resolved以后会执行cb(data)
// 开启下一次循环,实现自动执行
cb(data)
})
}
cb()
}
复制代码
源码分析
其实和上面的实现相似
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1) // 除第一个参数外的全部参数
// 返回一个Promise对象
return new Promise(function(resolve, reject) {
// 若是是Generator函数,执行获取遍历器对象gen
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
// 第一次执行遍历器对象gen的next方法获取第一个任务
onFulfilled();
// 每次异步任务执行完,resolved之后会调用,控制权又交还给Generator
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res); // 获取{value,done}对象,控制权在这里暂时交给异步任务,执行yield后的异步任务
} catch (e) {
return reject(e);
}
next(ret); // 进入next方法
}
// 同理可得
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
// 关键
function next(ret) {
// 遍历执行完异步任务后,置为resolved,并将最后value值返回
if (ret.done) return resolve(ret.value);
// 获取下一个异步任务,并转为Promise对象
var value = toPromise.call(ctx, ret.value);
// 异步任务结束后会调用onFulfilled方法(在这里为yield后的异步任务设置then的回调参数)
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
})
}
复制代码
其实仍是同样,为Promise对象then方法指定回调函数,在异步任务完成后触发回调函数,在回调函数中执行Generator的next方法,进入下一个异步任务,实现自动执行。
举个🌰
'use strict';
const fs = require('fs');
const co =require('co');
function read(filename) {
return new Promise(function(resolve, reject) {
fs.readFile(filename, 'utf8', function(err, res) {
if (err) {
return reject(err);
}
return resolve(res);
});
});
}
co(function *() {
return yield read('./a.js');
}).then(function(res){
console.log(res);
});
复制代码
比较
function* asyncReadFile () {
const f1 = yield readFile('/etc/fstab');
const f2 = yield readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
const asyncReadFile = async function () {
const f1 = await readFile('/etc/fstab');
const f2 = await readFile('/etc/shells');
console.log(f1.toString());
console.log(f2.toString());
};
复制代码
看起来只是写法的替换,实际上有这样的区别
用法
返回的Promise
async命令及其后的Promise
相互独立的异步任务能够改造下让其并发执行(Promise.all)
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
复制代码
await 与 for ... of
应该还在提案阶段吧
for await (const item of list) {
console.log(item)
}
复制代码
async function logInOrder(urls) {
// 并发读取远程URL
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});
// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
复制代码
目前了解到的异步解决方案大概就这样,Promise是主流,Generator做为容器,配合async await语法糖提供了看起来彷佛更加优雅的写法,但实际上由于一切都是Promise,同步任务也会被包装成异步任务执行,我的感受仍是有不足之处的。
虽发表于此,却毕竟为一人之言,又是每日学有所得之笔记,内容未必详实,看官老爷们还望海涵。