我是一名摸金校尉。
我们这行起源于东汉末年三国时期。曹操为了弥补军饷的不足,设立发丘中郎将,摸金校尉等军衔,专司盗墓取财,贴补军饷。
曹操之后,盗墓者皆各自为政,同行之间并无师徒之分,凡以摸金之法盗墓,均为摸金校尉。
拜近几年“盗墓”题材小说所赐,越来越多的人了解我们这行。但这些小说以讹传讹,为了吸引眼球往往故作神秘、夸大其词。
摸金险象环生,稍不留意便万劫不复。事实上,不像小说里靠“主角光环”每每死里逃生,我们有严谨的工作流程。
高风险,收益不确定。随着时间推移,从业者越来越少。最近我也决定转行当前端了。
为了防止这老祖宗的手艺失传,这里我就和你唠唠我们这行怎么工作的。
你问为啥转行前端?嘿,别说,我们这行的工作原理和浏览器工作原理还真像,学起来毫无压力。
万事安全第一。
我们这行容错率太低,稍有差次,那就是个狗带。所以下墓后的每一步,都得慎之又慎,按章办事。
古墓暗无天日,机关暗道错综复杂。最重要的,就是及时绘制地图。
每过一炷香的时间,都需要将这段时间路过的坑道,遇到的机关悉数绘制下来,此谓绘图。

绘图前这段时间用来做事。
那我们具体都做什么事呢?比如探路、寻宝、测机关...
要测机关就得停止探路,要寻宝就不能测机关。总之,一次只能做一件事。
活着出去固然是最重要的,但是又不能空手而归。
老祖宗早已为“要做的事”划分了轻重缓急,既要保证“重要的事”先做,又要保证其他事不至于不做。

做事本身也很有讲究,每次做完事后可能会有一些琐碎的后续工作。这些工作需要在下次做事前完成。
拿测机关来说,当测完机关后还需要检查一遍装备,以免下次使用出什么差次。
比如检查绳索、检查手电...
如果事情做的麻利,那一炷香的时间其实可以做很多事。

比如这一炷香的时间依次做了:
所以,下墓后的工作流程是:
按一炷香为周期完成一或多件事,最后完成绘图。
接着开始下一炷香的周期。

按照这个流程操作,不说万无一失,那也是很有保障的。
坏就坏在,有些同行太过贪心,比如这样:

如果在一炷香时间,一件事做的时间太长,那就没有时间绘图了!!
地图缺失一块,哪里有机关,哪里有暗道被少标记了,各种风险不言而喻!
终究这行还是太过搏命,好在我及时转行前端,接下来让我从浏览器角度再来解读下吧。
一般浏览器的刷新率为60HZ,即1秒钟刷新60次。
1000ms / 60hz = 16.6
大概每过16.6ms浏览器会渲染一帧画面,也就是说浏览器一炷香的时间是16.6ms。
在这段时间内,大体会做两件事:task与render。

其中task被称为宏任务,就像下墓后我们要做的事一样。
包括setTimeout,setInterval,setImmediate,postMessage,requestAnimationFrame,I/O,DOM 事件等。
render指渲染页面。
task按优先级被划分到不同的task queue中。就像老祖宗定的“轻重缓急”。
比如:为了及时响应用户交互,浏览器会为鼠标键盘(Mouse、Key)事件所在task queue分配3/4优先权。
这样可以及时响应用户交互,又不至于不执行其他task queue中的task。

虚线框部分要做的工作是:
task插入不同task queue中。task queue中选择一个task作为本次要执行的task。这就是事件循环(eventLoop)。
task执行过程中如果调用Promise、MutationObserver、process.nextTick会将其作为microTask(微任务)保存在microTask queue中。
就像做事后的琐碎工作。
每当执行完task,在执行下一个task前,都需要检查microTask queue,执行并清空里面的microTask。

比如如下代码
setTimeout(() => console.log('timeout'));
Promise.resolve().then(() => {
console.log('promise1');
Promise.resolve().then(() => console.log('Promise2'));
});
console.log('global');
执行过程为:
task。setTimeout,计时器线程会去处理计时,在计时结束后会将计时器回调加入task queue中。Promise.resolve,产生microTask,插入microTask queue。global。task执行完毕,开始遍历清理microTask queue。promise1。Promise.resolve,产生microTask,插入当前microTask queue。microTask queue,执行microTask打印promise2。task,打印timeout。就像一炷香时间可以做多件事,在一帧时间可以执行多个task。
执行如下代码后,屏幕会先显示红色再显示黑色,还是直接显示黑色?
document.body.style.background = 'red';
setTimeout(function () {
document.body.style.background = 'black';
})
答案是:不一定。
全局代码执行和setTimeout为不同的2个task。
如果这2个task在同一帧中执行,则页面渲染一次,直接显示黑色(如下图情况一)。
如果这2个task被分在不同帧中执行,则每一帧页面会渲染一次,屏幕会先显示红色再显示黑色(如下图情况二)。

如果我们将setTimeout的延迟时间增大到17ms,那么基本可以确定这2个task会在不同帧执行,则“屏幕会先显示红色再显示黑色”的概率会大很多。
可以发现,task没有办法精准的控制执行时机。那么有什么办法可以保证代码在每一帧都执行呢?
答案是:使用requestAnimationFrame(简称rAF)。
rAF会在每一帧render前被调用。

一般被用来绘制动画,因为当动画代码执行完后接下来就进入render。动画效果可以最快被呈现。
如下代码执行结果是什么呢:
setTimeout(() => {
console.log("setTimeout1");
requestAnimationFrame(() => console.log("rAF1"));
})
setTimeout(() => {
console.log("setTimeout2");
requestAnimationFrame(() => console.log("rAF2"));
})
Promise.resolve().then(() => console.log('promise1'));
console.log('global');
向右翻动展示答案? 大概率是: 1. global 2. promise1 3. setTimeout1 4. setTimeout2 5. rAF1 6. rAF2
setTimeout1与setTimeout2作为2个task,使用默认延迟时间(不传延迟时间参数时,大概会有4ms延迟),那么大概率会在同一帧调用。
rAF1与rAF2则一定会在不同帧的render前调用。
所以,大概率我们会在同一帧先后调用setTimeout1、setTimeout2、rAF1,再在另一帧调用rAF2。
如果render完后这一帧还有剩余时间呢?
如图中绿色部分:

此时你可以使用requestIdleCallbackAPI,如果渲染完成后还有空闲时间,则这个API会被调用。
如果task执行时间过长会怎么样呢?
如图taskA执行时间超过了16.6ms(比如taskA中有个很耗时的while循环)。
那么这一帧就没有时间render,页面直到下一帧render后才会更新。

表现为页面卡顿一帧,或者说掉帧。就像下墓后我们没有时间绘图。
有什么好的解决办法么?
刚才提到的requestIdleCallback是一个解决办法。我们可以将一部分工作放到空闲时间中执行。
但是遇到长时间task还是会掉帧。
更好的办法是:时间切片。即把长时间task分割为几个短时间task。
如图我们将taskA拆分为2个task。则每一帧都有机会render。这样就能减少掉帧的可能。

这React15中,采用递归的方式构建虚拟DOM树。
如果树层级很深,对应task的执行时间很长,就可能出现掉帧的情况。

为了解决掉帧造成的卡顿,React16将递归的构建方式改为可中断的遍历。
以5ms的执行时间划分task,每遍历完一个节点,就检查当前task是否已经执行了5ms。
如果超过5ms,则中断本次task。

通过将task执行时间切分为一个个小段,减少长时间task造成无法render的情况。这就是时间切片。
摸了摸手边的摸金符,我欣慰的想到:虽然996,但好歹身边都是活人。
这行,是转对了。