这篇文章就再也不聊关于promise的各类好处和用法了,若是不了解请自行Google啦!javascript
我相信不少人在面试的时候遇到过这样一道面试题:html
console.log(0)
let p = Promise.resolve()
setTimeout(()=>{
console.log(4);
setTimeout(()=>{
console.log(5);
},0);
},0);
p.then(data=>{
console.log(2);
setTimeout(()=>{
console.log(3);
},0);
})
console.log(6)
复制代码
那么你的答案是什么呢? 粘贴到chrome的控制台里运行一下,结果以下vue
// 0
// 6
// 2
// 4
// 3
// 5
复制代码
interesting的是,并非在全部浏览器里都是这样的打印顺序的,例如,在safari 9.1.2中测试,输出却这样的:java
// 0
// 6
// 4
// 2
// 5
// 3
复制代码
再放到safari 10.0.1中却又获得了和chrome同样的结果;node
固然,这只是这道面试题的一个简单版本哟!git
那么这道题到底在考察什么呢?es6
其实,我相信不少同窗均可以一眼看出0和6会先输出,可是setTimeout和promise哪一个先执行就有一丢丢小纠结了github
不再想为这样的执行顺序所困扰?让咱们先来了解一下js的event loop机制和promises的实现原理吧。web
咱们都知道promise是用来处理异步的,也知道js是单线程的,那么js的异步是什么呢? 这里咱们先明确一批概念,是的没看错,一批面试
ECMAScript + DOM + BOM 咱们说js异步背后的“靠山”就是event loops。 其实这里的异步准确的说应该叫浏览器的event loops或者说是javaScript运行环境的event loops,由于ECMAScript中没有event loops, event loops是在HTML Standard定义的。
event loop也就是咱们常说的事件循环,能够理解为实现异步的一种方式,咱们来看看event loop在HTML Standard中的定义:
为了协调事件,用户交互,脚本,渲染,网络等,用户代理必须使用本节所述的event loop。
咱们知道javascript在最初设计时设计成了单线程,为何不是多线程呢? 进程是操做系统分配资源和调度任务的基本单位,线程是创建在进程上的一次程序运行单位,一个进程上能够有多个线程。
以浏览器为例
因而可知浏览器是多进程的,而且从咱们的角度来看咱们更加关心主进程,也就是浏览器渲染引擎
而单独看渲染引擎,内部又是多线程的,包含两个最为重要的线程,即ui线程和js线程。并且ui线程和js线程是互斥的,由于JS运行结果会影响到ui线程的结果。
这里也就回答了javascript为何是单线程得问题,试想一下,若是多个线程同时操做DOM那岂不会很混乱?
固然,这里所谓的单线程指的是主线程,也就是渲染引擎是单线程的,一样的,在Node中主线程也是单线程的。
既然说js单线程指的是主线程是单线程的,那么还有哪些其余的线程呢?
单线程特色是节约了内存,而且不须要在切换执行上下文。并且单线程不须要管其余语言如java里锁的问题;
ps:这里简单说下锁的概念。例以下课了你们都要去上厕所,厕所就一个,至关于全部人都要访问同一个资源。那么先进去的就要上锁。而对于node来讲。 下课了就一我的去厕所,因此免除了锁的问题!
一个event loop有一个或者多个task队列。
当用户代理安排一个任务,必须将该任务增长到相应的event loop的一个tsak队列中。
每个task都来源于指定的任务源,好比能够为鼠标、键盘事件提供一个task队列,其余事件又是一个单独的队列。能够为鼠标、键盘事件分配更多的时间,保证交互的流畅。
task也被称为macrotask,task队列仍是比较好理解的,就是一个先进先出的队列,由指定的任务源去提供任务。
哪些是task任务源呢?
规范在Generic task sources中有说起:
DOM操做任务源: 此任务源被用来相应dom操做,例如一个元素以非阻塞的方式插入文档。
用户交互任务源: 此任务源用于对用户交互做出反应,例如键盘或鼠标输入。响应用户操做的事件(例如click)必须使用task队列。
网络任务源: 网络任务源被用来响应网络活动。
history traversal任务源: 当调用history.back()等相似的api时,将任务插进task队列。
总之,task任务源很是宽泛,好比ajax的onload,click事件,基本上咱们常常绑定的各类事件都是task任务源,还有数据库操做(IndexedDB ),须要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来讲task任务源:
每个event loop都有一个microtask队列,一个microtask会被排进microtask队列而不是task队列。
有两种microtasks:分别是solitary callback microtasks和compound microtasks。规范值只覆盖solitary callback microtasks。
若是在初期执行时,spin the event loop,microtasks有可能被移动到常规的task队列,在这种状况下,microtasks任务源会被task任务源所用。一般状况,task任务源和microtasks是不相关的。
microtask 队列和task 队列有些类似,都是先进先出的队列,由指定的任务源去提供任务,不一样的是一个 event loop里只有一个microtask 队列。
HTML Standard没有具体指明哪些是microtask任务源,一般认为是microtask任务源有:
task和microtask都是推入栈中执行的 来看下面一段代码:
function bar() {
console.log('bar');
}
function foo() {
console.log('foo');
bar();
}
foo();
复制代码
在规范的Processing model定义了event loop的循环过程: 一个event loop只要存在,就会不断执行下边的步骤:
主线程以外,还存在一个任务队列,用来放置microtask。
简单来讲,event loop会不断循环的去取tasks队列的中最老的一个任务推入栈中执行,当次循环同步任务执行结束以后检查是否存在microtasks队列,若是有microtasks则先执行microtasks,执行结束清空microtasks栈,把下一个task放入执行栈内,如此循环。
说了这么多关于event loop的东西,好像跟开篇的面试题并无什么关系啊?
别着急,下面咱们聊一下promise的实现; 咱们知道,promise是属于es6的,在之前浏览器并不支持,也就衍生了各家诸如bluebird,q,when等promise库,这些promise库的实现方式不尽相同,但都遵循Promises/A+规范;
其中2.2.4就是:
onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
这就意味着,在实现promise时,onFulfilled和onRejected要在新的执行上下文里才能执行;
而在3.1中说起了
This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick.
即promise的then方法能够采用“宏任务(macro-task)”机制或者“微任务(micro-task)”机制来实现。有的浏览器将then放入了macro-task队列,有的放入了micro-task 队列。开头打印顺序不一样也正是源于此,不过一个广泛的共识是promises属于microtasks队列。
那么咱们就来简单看一下promise的“宏任务(macro-task)”机制实现:
class Promise {
constructor(executor) {
this.status = 'pending';
this.value = undefined;
this.reason = undefined;
this.onResolvedCallbacks = [];
this.onRejectedCallbacks = [];
let resolve = (data) => {
if (this.status === 'pending') {
this.value = data;
this.status = 'resolved';
this.onResolvedCallbacks.forEach(fn => fn());
}
}
let reject = (reason) => {
if (this.status === 'pending') {
this.reason = reason;
this.status = 'rejected';
this.onRejectedCallbacks.forEach(fn => fn());
}
}
try {
executor(resolve, reject);
} catch (e) {
reject(e);
}
}
then(onFulFilled, onRejected) {
onFulFilled = typeof onFulFilled === 'function' ? onFulFilled : y => y;
onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };
let promise2;
if (this.status === 'resolved') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //“宏任务(macro-task)”机制实现
try {
let x = onFulFilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
}
if (this.status === 'rejected') {
promise2 = new Promise((resolve, reject) => {
setTimeout(() => { //“宏任务(macro-task)”机制实现
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0);
});
}
if (this.status === 'pending') {
promise2 = new Promise((resolve, reject) => {
this.onResolvedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onFulFilled(this.value);
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e);
}
}, 0)
});
// 存放失败的回调
this.onRejectedCallbacks.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch (e) {
reject(e);
}
}, 0);
});
})
}
return promise2; // 调用then后返回一个新的promise
}
// catch接收的参数 只用错误
catch(onRejected) {
// catch就是then的没有成功的简写
return this.then(null, onRejected);
}
}
复制代码
没错咱们看到了setTimeout; 这种就是经过macro-task机制实现的,打印出来的顺序就是如在safari 9.1.2中同样了。 测试了一下bluebird的promise的实现,输出的结果又和上面的都不同:
// 0
// 6
// 4
// 2
// 5
// 3
复制代码
因此到底哪一个先输出,要看你所使用的promise的实现方式;
固然正如上面提到的一个广泛的共识是promises属于microtasks队列,因此通常状况下,promise.then并非上面的这种实现,而是mic-task机制;
那么再来看开篇的题目
console.log(0) // 同步
let p = Promise.resolve();
setTimeout(()=>{ // 异步 macrotask
console.log(4);
setTimeout(()=>{
console.log(5); // 异步 macrotask
},0);
},0);
p.then(data=>{ // 异步 (经过macro-task实现则为macrotask,经过micro-task实现则为microtask)
console.log(2);
setTimeout(()=>{ // 异步 macrotask
console.log(3);
},0);
})
console.log(6) // 同步
复制代码
这样就很清晰了对吧
上面有列出microtask有
不知道用过vue1.0的同窗有没有了解过vue1.0的nextTick是如何实现的呢?
有兴趣能够看一下源码,就是经过MutationObserver实现的,只是由于兼容问题已经被取代了;
没用过MutationObserver?不要紧,咱们举一个简单的例子 假如咱们要往一个id为parent的dom中添加元素,咱们指望全部的添加操做都完成才执行咱们的回调 以下
let observe = new MutationObserver(function () {
console.log('dom所有塞进去了');
});
// 一个微任务
observe.observe(parent,{childList:true});
for (let i = 0; i < 100; i++) {
let p = document.createElement('p');
div.appendChild(p);
}
console.log(1);
let img = document.createElement('p');
div.appendChild(img);
复制代码
That's all ,如上;