异步
-
JavaScript引本身没有时间的概念,只是一个按需执行任意JavaScript代码片段的环境。“事件”(JavaScript代码的执行)调度由包含它的宿主环境进行;
-
Javascript设计为单线程的原因:与其作用相关。作为浏览器脚本语言,主要作用是用户交互,以及操作DOM,单线程的设计可以避免很多多线程带来的同步问题。比如,在不同的线程对同一DOM做了不同的修改。
HTML5标准的Web Worker并不改变JavaScript单线程的事实。因为Web Worker所创建的子线程完全受主线程控制,且不得操作DOM。 -
js中任务分为同步任务和异步任务:同步任务有一个执行栈,所有同步任务顺序执行,需要等待前一个任务执行完成才能执行后一个任务;异步任务不直接进入主线程,而是有一个用于异步任务调度的任务队列;当某个异步任务可以执行时,任务队列会通知主线程该任务可以执行,然后该任务才进入主线程执行。
-
js中的任务还可以分为宏任务(Macro Tasks)与微任务(Micro Tasks),同一循环中的微任务优先于宏任务执行;
- Macro Tasks:setTimeout, setInterval, setImmediate, I/O tasks, etc.
- Micro Tasks:process.nextTick, Promises, etc.
-
异步执行的运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈;
- 主线程之外有一个任务队列。只要异步任务有了运行结果,就在任务队列中放置一个事件;
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。对应的异步任务会进入执行栈开始执行;
- 主线程不断重复上面的第三步;主线程从任务队列读取事件的过程是不断循环的,故又称作事件循环(Event Loop)。事件循环的更多例子点击此处
-
setTimeout(fn, 0)
、setImmediate()
、process.nextTick()
执行顺序:setTimeout(fn, 0)
回调会加入到当前任务队列的尾部,即要等到当前的执行栈和任务队列的任务全部执行完成后再执行;setImmediate()
总是在当前事件循环的尾部执行,并且总是在当前事件循环相关I/O完成之后,及任何计划在下一轮事件循环执行的timer之前执行;process.nextTick()
回调会加入到当前执行栈的尾部,总是在当前执行栈执行完且当前任务队列开始执行之前执行;- ES6引入的Promise:与process.nextTick()类似,但总是晚于process.nextTick();
/* * setTimeout(fn, 0)对比setImmediate() */ setTimeout(function () { console.log(1); }, 0); setImmediate(function() { console.log(2); }); // 使用多个在线编译器测试,结果可能为2,1;可能为1,2; /* * setImmediate()对比process.nextTick() */ setImmediate(function() { console.log(1) }); process.nextTick(function () { console.log(2); process.nextTick(function () { console.log(3); }); console.log(4); }); // 运行结果:2,4,3,1 /* * Promise对比process.nextTick() */ new Promise((resolve, reject) => { resolve('promise excuted') }) .then(val => console.log(val)) process.nextTick(function() { console.log('process.nextTick') }) // 运行结果:process.nextTick、promise excuted,
-
并发和并行是有区别的:
并发可以看做是“任务级别”的并行,而不是运算级别的并行;其实质是两个任务交替执行,看起来像是在同时执行;
单线程事件循环是并发的一种形式;
并发两种方式:竞态与协作;竞态是多个任务相互竞争资源,取得优先执行的权利,而协作是在处理需要长时间运行的任务时将任务分割为多个步骤或多个子任务,使其他并发任务的运算可以插入到时间循环队列中交替运行,从而避免导致因为该任务的长时间运行而阻塞页面交互;
回调
-
回调地狱(callback hell),又称毁灭金字塔(pyramid of doom):回调最大的问题是控制反转,会导致信任链的完全断裂;另外,基本层面的影响就是会导致代码难以阅读、理解及维护(缺乏顺序性和可管理性);
-
控制反转(inversion of control):把自己程序一部分的执行控制交给某个第三方;
-
创建latch来处理对回调的多个并发调用;
-
信任问题与防御性代码;
-
回调的信任问题:
- 调用回调过早;
- 调用回调过晚或不调用;
- 调用回调次数过多或过少;
- 未能传递所需的环境和参数;
- 吞掉可能出现的异常和错误;
-
分离回调设计:提供了成功情况的通知以及失败情况或错误情况下的通知;ES6的Promise实现也用到了分离回调设计;
-
Node风格回调模式,又称“error-first”回调模式:绝大多数Node.js API均采用这种风格,它的第一个参数保留用作错误对象,若成功该参数会被清空或置位假,后续参数为成功的数据;否则第一个参数会被置位真,通常不会再传递其他结果;
Promise
-
Promise封装了依赖于时间的状态——等待底层值的完成或拒绝,它本身是与时间无关的,故不用关心时序或底层的结果;
-
Promise决议后(resolve或reject)就永远保持这个状态,对外是一个不可变的值;
-
识别Promise就是定义某种称为thenable的东西,将其定义为任何具有then(…)方法的对象和函数。任何这样的值称为Promise一致的thenable。thenable的鸭子类型检测类似于:
if (p !== null && (typeof p === "function" || typeof p === "object") && typeof p.then === "function") { // 假定是thenable } else { // 不是thenable }
不过这样的识别存在一定的局限性,将非Promise的内容识别为Promise就可能导致意想不到的bug。
-
Promise中的then方法总是会异步调用,即使其对应的Promise立即决议(resolve,reject);
-
zalgo现象:指一个接受回调函数作为参数的函数,其执行结果的不确定性。该回调函数可能会立即同步执行,也可能在将来某个时刻进行异步执行;为了解决该问题,必须保证回调的执行方式确定:始终为同步或者异步;通过
process.nextTick()
可以让回调始终异步执行; -
Promise的信任问题:
- 回调过早: then(…)函数总是异步执行,因此可以阻止zalgo现象;
- 回调过晚:Promise决议后,resolve(…)或reject(…)执行后then(…)注册的所有观察回调会在下一个异步时间点依次自动异步调用;
p.then(function () { p.then(function () { console.log('C'); }); console.log('A') }) p.then(function () { console.log('B'); }) // 结果总是 A B C,C不会抢占B而先执行,回调的任意一个均无法影响或延误对其他回调的调用
- 回调未调用:为防止Promise永远不被决议,可以使用
Promise.race()
,合理使用竞态并提供一个一定有输出信号的函数作为竞争者;
Promise.race(foo(), promiseObj); function foo () { let tid = setTimeout(function () { console.log('excuted'); clearTimeout(tid); }) }
- 调用次数过多或过少:过少即为0次,同未调用情况;Promise不会多次调用,因为其定义决定了它只能被决议一次,即使
resolve(...)
或者reject(...)
多次,Promise只会接受第一次决议而忽略其他的所有决议; - 未能传递参数或环境值:Promise的决议仅接受一个参数且该参数有一个默认值undefined,
resolve(...)
或者reject(...)
不传值时取undefined;传递多个值时可以封装为对象或数组的形式,否则Promise只会接受第一个参数; - 吞掉错误或异常:查看如下的Promise示例,在Promise创建或者决议过程中出现异常错误时,这个异常总是会被捕获并造成Promise执行reject,此时如果
then(...)
中没有设置onRejected回调,那么错误在下一级的catch仍然会被捕获;如果在Promise决议后在then(…)回调之前出现了异常错误,根据Promise决议结果的不可变性,在then(…)调用过程中不会改变已决议的Promise状态,这些异常不会被then(…)回调捕获
// demo1 var p1 = new Promise(function (resolve, reject) { foo(); }); p1.then(function onResolved(val) { console.log('onResolved: ', val); }, function onRejected(e) { console.log('onRejected: ', e); }).catch(err => console.log('catch err: ', err)); // 运行结果:onRejected: ReferenceError: foo is not defined // demo2 var p1 = new Promise(function (resolve, reject) { foo(); }); p1.then(function onResolved(val) { console.log('onResolved: ', val); }).catch(err => console.log('catch err: ', err)); // 运行结果:catch err: ReferenceError: foo is not defined // demo3 var p1 = new Promise(function (resolve, reject) { throw 1; }); p1.then(function onResolved(val) { console.log('onResolved: ', val); }, function onRejected(e) { console.log('onRejected: ', e); }).catch(err => console.log('catch err: ', err)); // 运行结果: onRejected: 1 // demo4 var p1 = new Promise(function (resolve, reject) { throw 1; }); p1.then(function onResolved(val) { console.log('onResolved: ', val); }).catch(err => console.log('catch err: ', err)); // 运行结果: catch err: 1
-
Promise.resolve()
返回的总是一个真正的Promise,即使传入的是非thenable值;当传入一个Promise时,返回的Promise为传入的Promise对象;Promise.resolve(undefined) instanceof Promise // true var p1 = new Promise((resolve, reject) => { resolve(1); }) var p2 = Promise.resolve(p1); p1 === p2;
-
Promise的强大之处在于它可以在链式调用的过程中很好地处理引入的异步;每个Promise的决议可以作为下一个
then(...)
继续执行的信号。var p = Promise.resolve(1); p.then(val => { console.log(val); // 1 return new Promise((resolve, reject) => { setTimeout(() => { resolve(val + 1); }, 100); }) }) .then(val => console.log(val)); // 2, 总是在上一步的延迟之后,即Promise决议之后执行;
-
Promise在省略显示的拒绝处理函数时有一个默认处理函数将错误抛出;从而使得错误可以继续沿着Promise链继续传播下去而不会被吞并,知道遇到显示定义的拒绝处理函数;
var p = Promise.resolve(1); p.then(function (val){ foo(); return val; }) .then(function (val) { console.log(val * 2); }, function (err) { console.log(err); return 2; }) .then(val => console.log(val)); // 运行结果: ReferenceError: foo is not defined // 2 var p = Promise.resolve(1); p.then(function (val){ foo(); return val; }) .then(function (val) { // 默认拒绝处理函数只会将错误重新抛出,错误会在后边的Promise链继续传播 console.log(val * 2); }) .then(val => console.log(val)) .catch(err => console.log('catched: ', err)) // 运行结果: catched: ReferenceError: foo is not defined
-
Promise(...)
构造器的第一个参数回调会展开thenable(和Promise.resolve(...)
一样)或真正的Promise;而第二个参数则会将这个值原封不动的设置为拒绝理由并传下去,并不是传递其底层值;var p = new Promise(function (resolve, reject) { resolve(Promise.reject('Oops')); }) p.then(function onFulfilled(val) { // 永远不会执行到这里 console.log('impossible'); }, function onRejected(err) { console.log(err); }) // Oops var p = new Promise(function (resolve, reject) { reject(Promise.resolve('ah')); // reject不会展开thenable或者Promise }) p.then(function onFulfilled(val) { // 永远不会执行到这里 console.log('fulfilled: ', val instanceof Promise); }, function onRejected(err) { console.log('rejected: ', err); }) // 运行结果:rejected: Promise {<resolved>: "ah"}
-
一旦创建了一个Promise并为其注册了完成或拒绝处理函数,如果出现某种情况使得该任务悬而未决,你也没有办法从外部停止它的进程;
Promise错误处理
- ```try…catch`只能用于同步模式,不能用于异步模式;
try { var p = new Promise(function (resolve, reject) { reject('Oops'); }) } catch (err) { console.log(err); } // 运行报错:Uncaught (in promise) Oops
- Promise错误处理容易出错,与Promise决议值的不变性有关;Promise对象的
.catch(...)
等同于promise.then(undefined, onRejected)
,但两者仍有一定的区别;var p = Promise.resolve(1); p.then(function onFulfilled(msg) { console.log('resolved: ', msg.toLowerCase()); return msg; }, function onRejected(err){ // 永远不会执行到这里 console.log('err: ', err); }) // 运行结果:报错:Uncaught (in promise) TypeError: msg.toLowerCase is not a function,其错误处理会在后续的`then(...)`或者`catch(...)`中被处理。
Promise模式
Promise.all([...]).then(functiion (msg){})
,多个任务完成后再执行后续的操作,数组内的元素通常为Promise实例;msg是一个代表完成消息的数组,是数组中指定顺序的所有的promise完成消息的组合;如果数组中的元素会经过Promise.resolve(...)
处理从而获得一个真正的Promise,因此传递的元素可以是Promise、thenable、立即值等;空数组会立即决议;任何一个决议为拒绝,均会拒绝;
另外,Promise.all([...])
数组中传递的所有Promise对象是并行执行的;var promise1 = Promise.resolve(3); var promise2 = 42; var promise3 = new Promise(function(resolve, reject) { setTimeout(resolve, 100, 'foo'); }); Promise.all([promise1, promise2, promise3]).then(function(values) { console.log(values); // [3, 42, "foo"] }); // Promise.all([...])中的每个Promise对象同时执行 function delayPromise(delay) { return new Promise(function (resolve, reject) { setTimeout(function (){ resolve(delay); }, delay); }) } var startTime = Date.now(); Promise.all([ delayPromise(1), delayPromise(16), delayPromise(32), delayPromise(64) ]) .then(function (vals) { console.log(Date.now() - startTime + 'ms'); console.log(vals); }) // 执行结果: // 64ms // [1, 16, 32, 64]
Promise.race([...])
传入空数组时Promise永不决议;其中任何一个Promise决议完成(resolved或rejected),Promise.race([...])
就会完成;任何一个Promise决议为拒绝,它就会拒绝;then(msg => {})
中msg为单个已决议Promise的消息。为防止传入的所有Promise都不能被决议或者非常长的时间才会决议,可以在数组增加一个自定义定时函数,超时在竞态中执行,防止Promise一直处于Pending状态;
值得注意的是,Promise.race()
在第一个promise对象变为Fulfilled之后,并不会取消其他promise对象的执行;var winnerPromise = new Promise(function(resolve) { setTimeout(function() { console.log('console: this is winner'); resolve('this is winner'); }, 4); }); var loserPromise = new Promise(function(resolve) { setTimeout(function() { console.log('console: this is loser'); resolve('this is loser'); }, 1000); }); loserPromise.then(function (val) { console.log('failed: ', val); }) Promise.race([winnerPromise, loserPromise]).then(function(value) { console.log(value); // => 'this is winner' }); // console: this is winner // this is winner // console: this is loser // failed: this is loser
- xhr-promise示例:
function getUrlPromise(url) { return new Promise(function (resolve, reject) { var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function () { if (xhr.readyState == 4 && xhr.status === 200) { resolve(xhr.response); } else { reject(new Error(xhr.statusText)) } } xhr.onerror = function () { reject(new Error(xhr.statusText)); } xhr.send(); }) } var url = 'http://apisgame.xxx.com/pageinfo/activityid=12'; getUrlPromise(url).then(function onFulfilled(val) { console.log(JSON.parse(val)); }, function onRejected(err) { console.log('err: ', err) }) // 运行结果:{code: 200, msg: "请求成功", data: {…}}
- 借助
Array.prototype.reduce
实现自定义顺序执行的任务队列:function sequenceTasks (tasks) { function recordValues(results, value) { results.push(value); return results; } var pushValue = recordValues.bind(null, []); return tasks.reduce(function (promise, task) { return promise.then(task).then(pushValue) }, Promise.resolve()) } // arr.reduce(callback(accumulator, currentValue, currentIndex, array),[, initialValue]) var reqSeq = [function a() {return 1;}, function b() {return 2;}] sequenceTasks(reqSeq).then(function (val) { console.log(val); // [1, 2] })
生成器
- Generator和普通函数在调用时存在一定的区别,需要调用一次
next()
才相当于普通函数的调用;next()
调用返回的结果是一个对象,包含value和done两个属性;function *foo(x, y) { return x + y; } let it = foo(1, 2); console.log(it); // {} let res = it.next(); console.log(res); // { value: 3, done: true }
- yield可以使生成器运行暂停在该位置,并且,可以使用
yield
和next(...)
进行迭代消息传递;并且,两者可以进行双向消息传递;规范规定向第一个next(...)
传参会被默认忽略,因为不存在接收参数的yield
。yield
用于从生成器函数返回值,默认为undefined;可以看做next(...)
与yield
是一一对应的,next(...)
许神生成器函数传入的下一个值是什么,yield对此进行解答;最后一个next(...)
的问题由函数的return
语句回答;function *foo(x) { var y = x * (yield); return y; } var it = foo(6); // 创建一个generator实例对象 it.next(); // 开始运行 var res = it.next(7); // 向第一个暂停的yield传参 console.log(res.value); // 42 function *foo(x) { var y = x * (yield 'hello world'); return y; } var it = foo(6); var res = it.next(); console.log(res); // { value: 'hello world', done: false } res = it.next(7); console.log(res); // { value: 42, done: true }
- 同一个生成器的多个实例可以并行,并进行相互交互;
function *foo () { var x = yield 2; // 生成器函数的返回值,默认为undefined z++; var y = yield (x * z); console.log(x, y, z); } var z = 1; var it1 = foo(); var it2 = foo(); var val1 = it1.next().value; // 2 var val2 = it2.next().value; // 2 console.log('1-val1: ', val1); console.log('1-val2: ', val2); val1 = it1.next(val2 * 10).value; // val2 * 10传递给x,val1为x*z的值 val2 = it2.next(val1 * 5).value; // val1 * 5传递给x console.log('2-val1: ', val1); console.log('2-val2: ', val2); console.log(it1.next(1)) console.log(it2.next(2)) // 运行结果如下: // 1-val1: 2 // 1-val2: 2 // 2-val1: 40 // 2-val2: 600 // 20 1 3 // { value: undefined, done: true } // 200 2 3 // { value: undefined, done: true }
Object.keys()
与for...in
遍历对象属性名时的差异:前者只遍历当前对象自有的属性,后者会同时遍历继承属性;- 迭代器支持
next()
方法,是iterable的;iterable必定支持一个名为Symbol.iterator(ES6)的函数,它每次调用会返回一个全新的迭代器;for...of
会自动循环调用Symbol.iterator函数;let arr = [1, 1, 2, 3]; let it = arr[Symbol.iterator](); it.next().value; // 1 it.next().value; // 1 it.next().value; // 2 it.next().value; // 3
- 生成器迭代器:生成器本身并不是iterable,但执行一个生成器时会得到一个迭代器(
it = foo()
),可以通过迭代器接口的next()
调用每次提取一个值; -
生成器内部有
try...finally
语句时,它将总是运行,即使生成器已经外部结束;可以在外部通过return(...)
手动终止生成器的迭代器实例;并且它会把返回的value值设置为return(...)
的内容;function *foo () { try { var nextVal; while (true) { if (nextVal === undefined) { nextVal = 1; } else { nextVal = nextVal * 2; } yield nextVal; } } finally { console.log('finally...'); } } var it = foo(); for (var i of it) { console.log(i); if (i > 8) { console.log(it.return('hello world').value); } } // 运行结果如下: // 1 // 2 // 4 // 8 // 16 // finally... // hello world
- 异步迭代生成器:demo如下图所示;使用
it.next()
开始运行,在遇到*main
函数中的yield ajax()
时暂停运行;此时先执行ajax()
,由于这是一个异步请求,函数返回值是undefined,所以该句相当于yield undefined
;因此此处的yield
只是用于流程控制实现暂停流程;同时,该句的执行会发送异步请求,等到异步请求返回成功后,会触发it.next(res)
,res会向yield传递值同时赋值给text
,因此第二行输出值为异步请求的响应;var img = new Image(); img.src = 'https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js'; var url = 'http://apisgame.xxx.com/api/info'; function ajax () { $.ajax({ url: url, success: function (res) { it.next(res); }, error: function (err) { it.throw(err); } }); } function *main() { try { var text = yield ajax(); console.log('text: ', text); } catch (err) { console.log(err); } } it = main(); it.next();
-
同步错误处理:上例中的
yield
表明我们不仅可以通过yield
同步从异步函数中获取函数返回值,也可以同步捕获异步函数中抛到生成器中的错误(比如在 异步函数中通过it.throw()
向生成器抛出错误); -
生成器 + Promise:生成器使得回调的顺序和合理性上有所提升,而Promise在可信任性和可组合性上有其优势,结合两者的优势可以更好地造福我们;比较有效的方式是yield出来一个Promise,然后通过Promise控制生成器的迭代器;(es7规范中的
async await
其实是该功能的标准实现);
-
生成器委托,即在一个生成器
*bar()
中调用另一个生成器*foo()
;语法为yield *__
,调用foo()会创建一个生成器实例,然后yield *
把实际控制权从bar()
委托给foo()
迭代器;it迭代器控制消耗量整个foo()
迭代器后,it的控制器会被自动归还给bar()
;委托的目的是为了代码组织,以达到与普通函数调用的对应;
function *foo() { console.log('*foo() starting'); yield 3; yield 4; console.log('*foo() finished'); } function *bar() { yield 1; yield 2; yield *foo(); // yield委托 yield 5; } var it = bar(); console.log(it.next().value); // 1 console.log(it.next().value); // 2 console.log(it.next().value); // foo() starting // 3 console.log(it.next().value); // 4 console.log(it.next().value); // *foo() finished // 5
-
尾调用优化:
一个出现在另一个函数结尾处的函数调用,且在该函数调用结束后外层的函数也运行完毕;
function foo() { console.log('fool'); } function bar() { return foo(); // 尾调用 } function baz() { return bar() + ' '; // 非尾调用 } // 典型示例 function factorial (n) { function fact (n, res) { if (n < 2) return res; return fact(n - 1, n * res); } return fact(n, 1); } for (let i=1; i<6; i++) { console.log(factorial(i)); } // 运行结果: 1 2 6 24 120
参考文献
- 《You Don’t Know JavaScript》(中)
- http://www.ruanyifeng.com/blog/2014/10/event-loop.html
- https://nodejs.org/en/docs/guides/timers-in-node/
- https://flaviocopes.com/javascript-event-loop/
- https://abc.danch.me/microtasks-macrotasks-more-on-the-event-loop-881557d7af6f
- https://github.com/oren/oren.github.io/blob/81384a37cc339c7694779797b1e25e4f075ba9d4/posts/old/zalgo.md
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
- 《JavaScript Promise迷你书(中文版)》