前端这两年的新技术铺天盖地,各类框架、工具层出不穷眼花缭乱。最近打算好好复习下 js 基础,夯实的基础才是学习新技术的基石。本文做为读书笔记简单的总结下 js 异步的基础知识。javascript
本系列目前已有四篇:html
本文首发于我的博客:(www.ferecord.com/lujs-async.…)。往后还会更新修改,如若转载请附上原文地址,以便溯源。前端
回调是编写和处理 JavaScript 程序异步逻辑的最经常使用方式,不管是 setTimeout 仍是 ajax,都是以回调的方式把咱们打算作的事情在某一时刻执行。java
// request(..) 是个支持回调的请求函数
request('http://my.data', function callback(res) {
console.log(res)
})
// 或者延时的回调
setTimeout(function callback() {
console.log('hi')
}, 1000)
复制代码
函数 callback 即为回调函数,它做为参数传进请求函数,并将在合适的时候被调用执行。es6
回调主要有如下两点问题。golang
1. 线性理解能力缺失,回调地狱ajax
过深的嵌套,致使回调地狱,难以追踪回调的执行顺序。编程
2. 控制反转信任缺失,错误处理没法保证数组
回调函数的调用逻辑是在请求函数内部,咱们没法保证回调函数必定会被正确调用。回调自己没有错误处理机制,须要额外设计。可能出现的错误包括:回调返回错误结果、吞掉可能出现的错误与异常、回调没有执行、回调被屡次执行、回调被同步执行等等。promise
能够经过构造器 Promise(..) 构造 promise 实例:
var p = new Promise(function(resovle, reject) {
if(1 > 0){
resolve() // 一般用于完成
} esle {
reject() // 用于拒绝
}
})
var onFulfilled = function() {} // 用于处理完成
var onRjected = function() {} // 用于处理拒绝
p.then(onFulfilled, onRjected)
复制代码
先理解下几个术语:决议(resolve)、 完成(fulfill)和拒绝(reject)。
fulfill 与 reject 都很好理解,一个完成,一个拒绝。而我上例代码中的 resolve() 注释“一般用于完成”,是因为 resolve 意思是决议,若是给 resolve 传入一个拒绝值,它会返回拒绝,例如 resolve(Promise.reject())。
假设 request(..) 是一个请求函数:
// 回调的写法
request('http://my.data', function onResult(res) {
if(res.error) {
// 处理错误
}
// 处理返回数据
})
// Promise 的写法
var p = request('http://my.data');
p.then(function onFullfill(res) {
// 处理返回数据
})
.catch(function onRjected() {
// 处理错误
})
复制代码
Promise 不是对回调的替代。 Promise 在回调代码和将要执行这个任务的异步代码之间提供了一种可靠的中间机制来管理回调。
使用回调的话,通知就是任务 request(..) 调用的回调。而使用 Promise 的话,咱们把这个关系反转了过来,侦听来自 request(..) 的事件,而后在获得通知的时候,根据状况继续。
你确定已经注意到 Promise 并无彻底摆脱回调。它们只是改变了传递回调的位置。咱们并非把回调传递给 request(..),而是从 request(..) 获得某个东西(外观上看是一个真正的 Promise),而后把回调传给这个东西。
Promise 归一保证了行为的一致性,Promise 给了肯定的值,resolve、reject、pendding。一旦 Promise 决议,它就永远保持在这个状态。此时它就成为了避免变值(immutable value),能够根据需求屡次查看。
Promise.all(iterable) 方法返回一个 Promise。参数 iterable 为数组。当 iterable 参数中全部的 Promise 都返回完成(resolve), 或者当参数不包含 Promise 时,该方法返回完成(resolve),。当有一个 Promise 返回拒绝(reject)时, 该方法返回拒绝(reject)。
对 Promise.all([ .. ]) 来讲,只有传入的全部 promise 都完成,返回 promise 才能完成。若是有任何 promise 被拒绝,返回的主 promise 就当即会被拒绝(抛弃任何其余 promise 的结果)。若是完成的话,你会获得一个数组,其中包含传入的全部 promise 的完成值。对于拒绝的状况,你只会获得第一个拒绝 promise 的拒绝理由值。这种模式传统上被称为门:全部人都到齐了才开门。
严格说来,传给Promise.all([..])的数组中的值能够是 Promise、thenable,甚至是当即值。就本质而言,列表中的每一个值都会经过 Promise.resolve(..) 过滤,以确保要等待的是一个真正的 Promise,因此当即值会被规范化为为这个值构建的 Promise。若是数组是空的,主 Promise 就会当即完成。
注意:
若向 Promise.all([ .. ]) 传入空数组,它会当即完成,但 Promise.race([ .. ]) 会挂住,且永远不会决议。
Promise.race(iterable) 方法返回一个 promise ,并伴随着 promise对象解决的返回值或拒绝的错误缘由。参数 iterable 为数组, 只要 iterable 中有一个 promise 对象"解决(resolve)"或"拒绝(reject)"。
对 Promise.race([ .. ]) 来讲,只有第一个决议的 promise(完成或拒绝)取胜,而且其决议结果成为返回 promise 的决议。这种模式传统上称为门闩:第一个到达者打开门闩经过。
注意:
一项竞赛须要至少一个“参赛者”。因此,若是你传入了一个空数组,主race([..]) Promise 永远不会决议,而不是当即决议。这很容易搬起石头砸本身的脚! ES6 应该指定它完成或拒绝,抑或只是抛出某种同步错误。遗憾的是,由于 Promise 库在时间上早于 ES6 Promise,它们不得已遗留了这个问题,因此,要注意,永远不要递送空数组。
var p1 = Promise.resolve( 42 );
var p2 = Promise.resolve( "Hello World" );
var p3 = Promise.reject( "Oops" );
Promise.race( [p1,p2,p3] )
.then( function(msg){
console.log( msg ); // 42
} );
Promise.all( [p1,p2,p3] )
.catch( function(err){
console.error( err ); // "Oops"
} );
Promise.all( [p1,p2] )
.then( function(msgs){
console.log( msgs ); // [42,"Hello World"]
} );
复制代码
all 与 race 的使用示例
Promise.reject(reason)方法返回一个用reason拒绝的Promise。
如下两个 promise 是等价的:
var p1 = new Promise( function(resolve,reject){
reject( "Oops" );
});
var p2 = Promise.reject( "Oops" );
复制代码
Promise.resolve(value)方法返回一个以给定值解析后的 Promise 对象。但若是这个值是个 thenable(即带有 then 方法),返回的 promise 会“跟随”这个 thenable 的对象,采用它的最终状态(指 resolved/rejected/pending/settled);不然以该值为成功状态返回 promise 对象。
then(..) 接受一个或两个参数:第一个用于完成回调,第二个用于拒绝回调。若是二者中的任何一个被省略或者做为非函数值传入的话,就会替换为相应的默认回调。默认完成回调只是把消息传递下去,而默认拒绝回调则只是从新抛出(传播)其接收到的出错缘由。
p.then( fulfilled );
p.then( fulfilled, rejected );
复制代码
catch(..) 只接受一个拒绝回调做为参数,并自动替换默认完成回调。换句话说,它等价于 then(null,..):
p.catch( rejected ); // 或者p.then( null, rejected )
复制代码
then(..) 和 catch(..) 也会建立并返回一个新的 promise,这个 promise 能够用于实现Promise 链式流程控制。若是完成或拒绝回调中抛出异常,返回的 promise 是被拒绝的。若是任意一个回调返回非 Promise、非 thenable 的当即值,这个值会被用做返回 promise 的完成值。若是完成处理函数返回一个 promise 或 thenable,那么这个值会被展开,并做为返回promise 的决议值。
1. 顺序错误处理
Promise 的设计局限性(具体来讲,就是它们连接的方式)形成了一个让人很容易中招的陷阱,即 Promise 链中的错误很容易被无心中默默忽略掉。
例如:
// foo(..), STEP2(..)以及STEP3(..)都是支持promise的工具
var p = foo( 42 )
.then( STEP2 )
.then( STEP3 );
p.catch( handleErrors );
复制代码
若是链中的任何一个步骤事实上进行了自身的错误处理(可能以隐藏或抽象的不可见的方式),那你的最后的 catch 就不会获得通知。这多是你想要的——毕竟这是一个“已处理的拒绝”——但也可能并非。彻底不能获得(对任何“已经处理”的拒绝错误的)错误通知也是一个缺陷,它限制了某些用例的功能。
2. 单一值
根据定义, Promise 只能有一个完成值或一个拒绝理由。若是但愿处理函数接收到多个结果的话只能使用数组或对象封装要传递的结果。就像这样:
function foo (a) {
var b= a + 1;
return new Promise(resolve => {
resolve([a, b])
})
}
foo(1).then(function(msg) {
console.log(msg[0], msg[1]) // 1, 2
})
复制代码
这个解决方案能够起做用,但要在 Promise 链中的每一步都进行封装和解封,就十分丑陋和笨重了。
在封装解封单一值的方法上还有如下两种:
function getB(a) {
return new Promise(resolve => {
return resolve(a + 1)
});
}
function foo(a) {
return [
Promise.resolve(a),
getB(a)
];
}
Promise.all(foo(1))
.then(function (msg){
console.log(msg[0], msg[1]) // 1, 2
});
复制代码
恩,这个看起来相对第一种没什么改进,反而看起来还更麻烦了。但这种方法更符合 Promise 的设计理念。若是之后须要重构代码把对 a 和 b 的计算分开,这种方法就简单得多。由调用代码来决定如何安排这两个 promise,而不是把这种细节放在 foo(..) 内部抽象,这样更整洁也更灵活。
var p = new Promise (resolve => {
return resolve([1,2])
})
// 使用 apply
p.then(Function.prototype.apply(function(a, b){
console.log(a, b) // 1, 2
})
// 使用解构
p.then(function([a, b]) {
console.log(a, b) // 1, 2
})
复制代码
总结一下,单一值是 Promise 的局限之一,致使若是咱们须要处理有多个参数的结果,只能把结果封装在对象或数组这种集合中,再使用各类方法在处理函数中进行解构。
3. 单决议
Promise 最本质的一个特征是: Promise 只能被决议一次(完成或拒绝)。
因此下面的代码就是有问题的:
var p = new Promise(function(resolve) {
$('.mybtn').click(resolve)
})
p.then(function(e) {
var btnId = evt.currentTarget.id;
return fetch('http://myurl.url/?id=' + btnId)
})
.then(function(res) {
console.log(res)
})
复制代码
只有在你的应用只须要响应按钮点击一次的状况下,这种方式才能工做。若是这个按钮被点击了第二次的话, promise p 已经决议,所以第二次的 resolve(..) 调用就会被忽略。
所以,你可能须要转化这个范例,为每一个事件的发生建立一整个新的 Promise 链:
$('#mybtn').click(function(e) {
var btnId = evt.currentTarget.id;
fetch('http://myurl.url/?id=' + btnId)
.then(function(res) {
console.log(res)
})
});
复制代码
这种方法能够工做,由于针对这个按钮上的每一个 "click" 事件都会启动一整个新的 Promise 序列。因为须要在事件处理函数中定义整个 Promise 链,这很丑陋。除此以外,这个设计在某种程度上破坏了关注点与功能分离(Separation of concerns, SoC, 或称关注度分离)的思想。你极可能想要把事件处理函数的定义和对事件的响应(那个 Promise 链)的定义放在代码中的不一样位置。若是没有辅助机制的话,在这种模式下很难这样实现。
4. 惯性
现存的全部代码都还不理解 Promise,你得本身把须要回调的函数封装为支持 Promise 的函数。
5. 没法取消的 Promise
一旦建立了一个 Promise 并为其注册了完成和或拒绝处理函数,若是出现某种状况使得这个任务悬而未决的话,你也没有办法从外部中止它的进程。
6. Promise 的性能
把基本的基于回调的异步任务链与 Promise 链中须要移动的部分数量进行比较。很显然,Promise 进行的动做要多一些,这天然意味着它也会稍慢一些。
生成器是一种返回迭代器的函数,经过 function 关键字后的 * 号来表示。
迭代器是一种对象,它具备一些专门为迭代过程设计的专有接口,全部迭代器对象都有一个 next 方法,每次调用都返回一个结果对象。结果对象有两个属性,一个是 value,表示下一个将要返回的值;另外一个是 done,它是一个布尔类型的值,当没有更多可返回数据时返回 true。迭代器还会保存一个内部指针,用来指向当前集合中值的位置,每调用一次 next() 方法,都会返回下一个可用的值。
可迭代对象具备 Symbol.iterator 属性,是一种与迭代器密切相关的对象。Symbol.iterator 经过指定的函数能够返回一个做用于附属对象的迭代器。 在 ECMCScript 6 中,全部的集合对象(数组、Set、及 Map 集合)和字符串都是可迭代对象,这些对象中都有默认的迭代器。
此外,因为生成器会默认为 Symbol.iterator 属性赋值,所以全部经过生成器建立的迭代器都是可迭代对象。
for-of 循环每执行一次都会调用可迭代对象的迭代器接口的 next() 方法,并将迭代器返回的结果对象的 value 属性储存在一个变量中,循环将持续执行这一过程直到返回对象的属性值为 true。
function *foo() {
var x = yield 2;
var y = x * (yield x + 1)
console.log( x, y );
return x + y
}
var it = foo();
it.next() // {value: 2, done: false}
it.next(3) // {value: 4, done: false}
it.next(3) // 3 9, {value: 12, done: true}
复制代码
yield.. 和 next(..) 这一对组合起来, 在生成器的执行过程当中构成了一个双向消息传递系统。
有几点须要注意一下:
var obj = {
[Symbol.iterator]: function *() {
var result = 1
while(result < 500) {
result = result * 2
yield result
}
}
}
for(let value of obj) {
console.log(value)
}
// 2 4 8 16 32 64 128 256 512
复制代码
来看一下下面这段代码,咱们在生成器里 yeild 请求函数(暂停生成器继续执行,同时并执行请求函数),执行生成器产成可迭代对象后,又在请求函数里经过 next() 方法获取到请求结果、将结果传进生成器并恢复生成器的执行。
function foo() {
ajax('http://my.data', function(res) {
if(res.error) {
// 向*main()抛出一个错误
it.throw(res.error)
}
// 用收到的data恢复*main()
it.next(res.data)
})
}
function *main() {
try {
var data = yeild foo();
console.log(data)
} catch(e) {
console.error(e)
}
}
var it = main();
// 这里启动!
it.next();
复制代码
本例中咱们在 *main() 中发起 foo() 请求,以后暂停;又在 foo() 中相应数据恢复 *mian() 继续运行,并将 foo() 的运行结果经过 next() 传递出来。
从本质上而言,咱们把异步做为实现细节抽象了出去,使得咱们能够以同步顺序的形式追踪流程控制:“发出一个 Ajax 请求,等它完成以后打印出响应结果。”而且,固然,咱们只在这个流程控制中表达了两个步骤,而这种表达能力是能够无限扩展的,以便咱们不管须要多少步骤均可以表达。
咱们在生成器内部有了看似彻底同步的代码(除了 yield 关键字自己),但隐藏在背后的是,在 foo(..) 内的运行能够彻底异步。而且在异步代码中实现看似同步的错误处理(经过 try..catch)在可读性和合理性方面也都是一个巨大的进步。
Promise 和生成器最大效用的最天然的方法就是 yield 出来一个 Promise,而后经过这个 Promise 来控制生成器的迭代器。
建议看下面这段代码而后脑海中反复思索上面这段话。
function foo() {
return fetch('http://my.data')
}
function *main() {
try {
var data = yeild foo();
console.log(data)
} catch(e) {
console.error(e)
}
}
var it = main();
var p = it.next().value; // p 的值是 foo()
// 等待 promise p 决议
p.then(
function(data) {
it.next(data); // 将 data 赋值给 yield
},
function(err) {
it.throw(err);
}
)
复制代码
这样就实现了 promise + 生成器来管理异步流程:*mian() 中执行 foo() 发起请求,使用 *mian() 生成的迭代器获取 foo() 的 promise 决议结果,再根据结果选择继续运行迭代器或抛出错误。
咱们能够将等待决议、执行 next()这一过程抽象出来,实现自动等待决议并继续执行,直到结束:
// 定义 run 函数
functiton run(gen) {
var args = [].slice.call(arguments, 1), it;
// 在当前上下文中初始化生成器
it = gen.apply(this, args);
// 返回一个 promise 用于生成器完成
return Promise.resolve()
.then(function handleNext(value) {
// 对下一个 yield 值出的值运行
var next = it.next(value);
return (function handleValue(next){
// 判断生成器是否运行完毕
if(next.done) {
return next.value;
}
// 不然继续运行
else {
return Promise.resolve(next.value)
.then(
// 成功就恢复异步循环,把决议的值发回生成器
handleNext,
// 若是 value 是被拒绝的 promise
// 就把错误传回生成器进行出错处理
function handleErr(err) {
return Promise.resolve(
it.throw(err)
)
}
)
}
})(next)
})
}
function foo(p) {
return fetch('http://my.data?p=' + p)
}
function *main(p) {
try {
var data = yeild foo(p);
console.log(data)
} catch(e) {
console.error(e)
}
}
// 运行!
run(main, '1')
复制代码
建议花费几分钟时间学习这段代码,以更好地理解生成器 + Promise 协同运做模式。
run() 函数起到的做用跟咱们接下来要讲的 async/await 函数是同样的。
async 函数是什么?一句话,它就是 Generator 函数的语法糖。它在形式上相似咱们刚刚写的 run(..) 函数。
一个 async 函数的基本使用形式以下:
function foo(p) {
return fetch('http://my.data?p=' + p)
}
async function main(p) {
try {
var data = await foo(p);
return data
} catch(e) {
console.error(e)
}
}
main(1)
.then(data => console.log(data))
复制代码
与 Generator 函数的显著不一样是,*
变成了async
、yeild
变成了await
,同时咱们也不用再定义 run(..) 函数来实现 Promise 与 Generator 的结合。async 函数执行的时候,一旦遇到 await 就会先返回,等到异步操做完成,再接着执行函数体内后面的语句,而且最终返回一个 Promise 对象。
正常状况下,await 命令后面是一个 Promise 对象。若是不是,会被转成一个当即 resolve 的 Promise 对象。await 命令后面的 Promise 对象若是变为 reject 状态,则 reject 的参数会被 catch 方法的回调函数接收到。
async 函数对 Generator 函数的改进,体如今如下四点。
1. 内置执行器。
async 函数内置执行器(相似内部已实现咱们刚刚的 run(..) 函数),省去了咱们手动迭代生成器的麻烦;
2. 更好的语义。
async 和 await,比起星号和 yield,语义更清楚了。async 表示函数里有异步操做,await 表示紧跟在后面的表达式须要等待结果。
3. 更广的适用性。
co模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,能够是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操做)。
4. 返回值是 Promis**
async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你能够用 then 方法指定下一步的操做。
关于 async 函数的使用有三点须要注意一下:
1. 前面已经说过,await 命令后面的 Promise 对象,运行结果多是 rejected,因此最好把 await 命令放在 try...catch 代码块中。
2. 多个 await 命令后面的异步操做,若是不存在继发关系,最好让它们同时触发。
3. await 命令只能用在 async 函数之中,若是用在普通函数,就会报错。
//getFoo 与 getBar 是两个互相独立、互不依赖的异步操做
// 错误写法,会致使 getBar 在 getFoo 完成后才执行
let foo = await getFoo();
let bar = await getBar();
// 正确写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 正确写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;
复制代码
无继发关系的异步操做应当同步触发
就像生成器函数返回一个同步遍历器对象同样,异步生成器函数的做用,是返回一个异步迭代器对象。
在语法上,异步 Generator 函数就是 async 函数与 Generator 函数的结合。
异步迭代器与迭代器相似,也是一种对象,也有 next 方法,与迭代器不一样的是迭代器的 next 方法每次调用返回的是返回的对象的结构是{value, done}
,其中value
表示当前的数据的值,done
是一个布尔值,表示迭代是否结束。。而异步迭代器的 next 方法每次返回的是一个 Promise 对象,等到 Promise 对象 resolve 了,再返回一个{value, done}
结构的对象。这就是说,异步迭代器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,做为中介。
对于普通迭代器来讲,next
方法必须是同步的,只要调用就必须马上返回值。也就是说,一旦执行next
方法,就必须同步地获得value
和done
这两个属性。若是咱们须要迭代异步数据,同步迭代器就没法工做。例如在下面的代码中,readLinesFromFile() 就没法经过同步迭代器呈现它的异步数据:
// readLinesFromFile 是一个异步返回数据的函数
for (const line of readLinesFromFile(fileName)) {
console.log(line);
}
复制代码
ES2018 引入了”异步迭代器“(Async Iterator),为异步操做提供原生的迭代器接口,即value
和done
这两个属性都是异步产生。
asyncIterator
.next()
.then(
({ value, done }) => /* ... */
);
复制代码
异步迭代器的最大的语法特色,就是调用迭代器的next
方法,返回的是一个 Promise 对象。
下面是一个更具体的异步迭代器的例子。
// createAsyncIterable(..) 是一个建立可异步迭代对象的函数,咱们稍后解释它
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
asyncIterator.next()
.then(iterResult1 => {
console.log(iterResult1); // { value: 'a', done: false }
return asyncIterator.next();
})
.then(iterResult2 => {
console.log(iterResult2); // { value: 'b', done: false }
return asyncIterator.next();
})
.then(iterResult3 => {
console.log(iterResult3); // { value: undefined, done: true }
});
复制代码
因为异步遍历器的next
方法,返回的是一个 Promise 对象。所以,能够把它放在await
命令后面。
async function foo() {
const asyncIterable = createAsyncIterable(['a', 'b']);
const asyncIterator = asyncIterable[Symbol.asyncIterator]();
console.log(await asyncIterator.next());
// { value: 'a', done: false }
console.log(await asyncIterator.next());
// { value: 'b', done: false }
console.log(await asyncIterator.next());
// { value: undefined, done: true }
}
复制代码
可迭代对象具备 Symbol.asyncIterator 属性,咱们知道,一个对象的同步迭代器的接口,部署在Symbol.iterator
属性上面。一样地,对象的异步迭代器接口,部署在Symbol.asyncIterator
属性上面。无论是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。
for-of 循环用于遍历同步的 Iterator 接口。新引入的 for-await-of 循环,则是用于遍历异步的 asyncIterator 接口。
// createAsyncIterable 是一个建立可异步迭代对象的函数
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// Output:
// a
// b
复制代码
若是 next 方法返回的 Promise 对象被 reject,for-await-of 就会报错,要用 try...catch 捕捉。
function createRejectingIterable() {
return {
[Symbol.asyncIterator]() {
return this;
},
next() {
return Promise.reject(new Error('Problem!'));
},
};
}
(async function () { // (A)
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
// Error: Problem!
}
})(); // (B)
复制代码
另外 for-await-of 也可用于遍历同步的可迭代对象。
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// Output:
// a
// b
复制代码
for-await-of 会经过 Promise.resolve() 将每一个迭代值都转换成 Promise。
在语法上,异步 Generator 函数就是 async 函数与 Generator 函数的结合。
async function *createAsyncIterable() {
var x = yield 2;
var y = x * (yield x + 1)
return x + y
}
var it = createAsyncIterable()
function onFulfilled(obj){
console.log(obj)
}
it.next().then(onFulfilled) // {value: 2, done: false}
it.next(3).then(onFulfilled) // {value: 4, done: false}
it.next(3).then(onFulfilled) // 3 9, {value: 12, done: true}
复制代码
var obj = {
[Symbol.asyncIterator]: async function *gen() {
var result = 1
while(result < 500) {
result = result * 2
yield result
}
}
};
(async function foo () {
for await (const x of obj) {
console.log(x);
}
})();
// 2 4 8 16 32 64 128 256 512
复制代码
异步 Generator 函数出现之后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不一样之处。基本上,若是是一系列按照顺序执行的异步操做(好比读取文件,而后写入新内容,再存入硬盘),可使用 async 函数;若是是一系列产生相同数据结构的异步操做(好比一行一行读取文件),可使用异步 Generator 函数。
任什么时候候,只要把一段代码包装成一个函数,并指定它在响应某个事件(定时器、鼠标点击、 Ajax 响应等)时执行,你就是在代码中建立了一个未来执行的块,也由此在这个程序中引入了异步机制。
多个异步之间可能存在如下三种关系:
js 的运行环境都提供了一种机制来处理程序中多个块的执行,且执行每块时调用 JavaScript 引擎,这种机制被称为事件循环。
主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)。
ES6 中介绍了一种叫 “任务队列(Job Queue)”的新概念。它是事件循环队列之上的一层。遗憾的是,目前为止,这是一个没有公开 API 的机制,所以要展现清楚有些困难。因此咱们目前只从概念上进行描述。
我认为对于任务队列最好的理解方式就是,它是挂在事件循环队列的每一个 tick 以后的一个队列。在事件循环的每一个 tick 中,可能出现的异步动做不会致使一个完整的新事件添加到事件循环队列中,而会在当前 tick 的任务队列末尾添加一个项目(一个任务)。
这就像是在说:“哦,这里还有一件事未来要作,但要确保在其余任何事情发生以前就完成它。”
事件循环队列相似于一个游乐园游戏:玩过了一个游戏以后,你须要从新到队尾排队才能再玩一次。而任务队列相似于玩过了游戏以后,插队接着继续玩。
一个任务可能引发更多任务被添加到同一个队列末尾。因此,理论上说, 任务循环(job loop)可能无限循环(一个任务老是添加另外一个任务,以此类推),进而致使程序的饿死,没法转移到下一个事件循环 tick。
任务和 setTimeout(..0) hack 的思路相似,可是其实现方式的定义更加良好,对顺序的保证性更强。
Promise 的异步特性是基于任务的
来看一段代码:
var a = 1;
var b = 2;
function foo() {
a++;
b = b * a;
a = b + 3;
}
function bar() {
b--;
a = 8 + b;
b = a * 2;
}
// ajax(..)是某个库中提供的某个Ajax函数
ajax( "http://some.url.1", foo );
ajax( "http://some.url.2", bar );
复制代码
咱们没法在程序执行前肯定 a 和 b 的最后的值,由于它们的值取决于 foo 和 bar 哪一个先执行,这段代码里咱们没法肯定谁会先执行。
竞态条件: 在 JavaScript 的特性中,这种函数顺序的不肯定性就是一般所说的竞态条件(race condition), foo() 和 bar() 相互竞争,看谁先运行。具体来讲,由于没法可靠预测 a 和 b的最终结果,因此才是竞态条件。
门: 它的特性能够描述为“全部都经过后再经过”。形似 if (a && b) 传统上称为门,咱们虽然不能肯定 a 和 b 到达的顺序,可是会等到它们两个都准备好再进一步打开门。 在经典的编程术语中,门( gate)是这样一种机制要等待两个或更多并行 / 并发的任务都完成才能继续。它们的完成顺序并不重要,可是必须都要完成,门才能打开并让流程控制继续。
门闩: 它的特性能够描述为“只有第一名取胜”。须要“竞争”到终点,且只有惟一的胜利者。
顺序和并发是指不相关任务的设计结构。
顺序 是指多个任务的执行依次执行。
并发 一个并发程序是指能同时执行一般不相关的各类任务。并发是一段时间内某个系统或单元的各个组成部分经过相互配合来处理大量的任务,强调结构和调度。
举例,吃饭时同时打电话,这是并发。
串行和并行是指单个任务的执行方式。
串行 指单任务的多个步骤依次执行。
并行 并行是兵分几路干同一个事,即单个任务的多个步骤同时执行。
参考:举例,吃饭时把饭和菜一块塞嘴里吃掉,这是并行。
Concurrency is not parallelism
并发(Concurrency)和并行(Parallelism)的区别_vaikan
并行计算最多见的工具就是进程和线程,进程和线程独立运行,并可能同时运行,多个线程可以共享单个进程的内存。
进程是具备必定独立功能的程序、它是系统进行资源分配和调度的一个独立单位,重点在系统调度和单独的单位,也就是说进程是能够独立运行的一段程序。
线程是进程的一个实体,是 CPU 调度和分派的基本单位,他是比进程更小的能独立运行的基本单位,线程本身基本上不拥有系统资源。在运行时,只是暂用一些计数器、寄存器和栈。
他们之间的关系:
他们之间的区别:
并行线程的交替执行和异步事件的交替调度,其粒度是彻底不一样的。 事件循环把自身的工做分红一个个任务并顺序执行,不容许对共享内存的并行访问和修改。经过分立线程中彼此合做的事件循环,并行和顺序执行能够共存。