@
haozhang 微博和GitHub都是otakustay
我来说下为啥上面 @
haozhang 说的原理是错的。这个问题最根本的一个关键在于:race(或者all等其它和Promise有关的方法)的决定因素不是“Promise状态的变化”,而是“callback的执行”,所以“p1的状态是最先改变”这样的说法完全没用,比如我搞这样的代码:
var p1 = new Promise(function (resolve) { setTimeout(resolve, 1000); });
var p2 = Promise.reject();
setTimeout(function () {
var p3 = Promise.race(p1, p2);
p3.then(logResolved, logRejected);
}, 1500);
这代码里p3会进入resolved状态,但p1显然“比p2更晚改变状态”
所以再重复一次核心:决定因素是callback的执行
那么其原理要研究的就是,callback是啥时候执行的
在上面我贴规范的时候已经有提到一个词,叫enqueueTask。简单来说,就是浏览器有一个全局的任务队列,我们把一个个任务放到队列里,浏览器从队列里拿出来一个执行完再去拿下一个,每个任务的执行都是异步的
当然浏览器其实不一定只有一个队列,任务也不止一种,Promise产生的任务和setTimeout产生的就不一样,这种细节的事不展开说
然后还需要研究的是,在Promise执行过程中,什么时候执行enqueuTask。对于Promise来说也很简单,当then/catch被调用的时候,形成2种可能性:
1. Promise还没有resolve或reject,即处在pending状态,此时会把callback存下来,等进入某个状态时再把对应的callback一一enqueuTask
2. Promise本身已经在resolve或reject状态,则把给到的callback(当然要符合当前状态)直接enqueueTask
到这里这个逻辑其实就很简单了,主楼的代码分析下来是这样的:
1. 创建p1,此时任务队列为空,[]
2. 由于执行new Promise时给的executor函数,p1进入resolved状态,但注意此时并没有任何的callback,所以没有东西执行enqueueTask,队列还是空,[]
3. 创建p2、p3,不多说了,和p1一样,关键就是他们状态虽然是改变了,但根本没有任何callback,所以到此时为止队列依旧是空的,[]
4. 创建p4,用的Promise.race,race方法会分别给传入的Promise们调用then添加callback。所以这一段代码会依次**按顺序**给p1、p2、p3添加resolve和reject的回调,因为3个Promise状态都已经是确定的,所以会执行3次enqueueTask,队列变成[因p1导致p4变resolve, 因p2导致p4变reject, 因p3导致p4变resolve],这3个任务任意一个执行就会导致p4状态变化
5. 给p4挂上2个callback,不过此时p4状态没有改变还是pending,所以这2个callback不会enqueueTask
6. 这段代码执行完了,浏览器算是完成了一个任务,要去获取下一个任务,所以从队列中拿出第1个,即“p1导致p4变resolve”,队列变成2个任务,[因p2导致p4变reject, 因p3导致p4变resolve]
7. 执行拿出来的任务,于是p4被resolve了,且对应的value是p1里的“this is p1”,resolve的同时会把关联的callback放进队列,所以此时队列变成3个任务,[因p2导致p4变reject, 因p3导致p4变resolve, console.log(value)]
8. 继续拿下一个任务,拿出来“因p2导致p4变reject”并执行,但很遗憾p4现在已经是resolve状态了,所以这个任务执行了啥也不会发生,此时队列变成2个任务,[因p3导致p4变resolve, console.log(value)]
9. 下一个任务不多说了,反正p4是不会再变状态了,于是队列里还剩最后一个任务,[console.log(value)]
10. 把这最后一个任务拿出来执行,于是打印出来“this is p1”
JavaScript里凡遇到异步的问题,拿个笔画这个任务队列是最好的理思路的手段