我是一名摸金校尉。
我们这行起源于东汉末年三国时期。曹操为了弥补军饷的不足,设立发丘中郎将,摸金校尉等军衔,专司盗墓取财,贴补军饷。
曹操之后,盗墓者皆各自为政,同行之间并无师徒之分,凡以摸金之法盗墓,均为摸金校尉。
拜近几年“盗墓”题材小说所赐,越来越多的人了解我们这行。但这些小说以讹传讹,为了吸引眼球往往故作神秘、夸大其词。
摸金险象环生,稍不留意便万劫不复。事实上,不像小说里靠“主角光环”每每死里逃生,我们有严谨的工作流程。
高风险,收益不确定。随着时间推移,从业者越来越少。最近我也决定转行当前端了。
为了防止这老祖宗的手艺失传,这里我就和你唠唠我们这行怎么工作的。
你问为啥转行前端?嘿,别说,我们这行的工作原理和浏览器工作原理还真像,学起来毫无压力。
万事安全第一。
我们这行容错率太低,稍有差次,那就是个狗带。所以下墓后的每一步,都得慎之又慎,按章办事。
古墓暗无天日,机关暗道错综复杂。最重要的,就是及时绘制地图。
每过一炷香的时间,都需要将这段时间路过的坑道,遇到的机关悉数绘制下来,此谓绘图
。
绘图
前这段时间用来做事
。
那我们具体都做什么事呢?比如探路
、寻宝
、测机关
...
要测机关
就得停止探路
,要寻宝
就不能测机关
。总之,一次只能做一件事。
活着出去固然是最重要的,但是又不能空手而归。
老祖宗早已为“要做的事”划分了轻重缓急,既要保证“重要的事”先做,又要保证其他事不至于不做。
做事
本身也很有讲究,每次做完事
后可能会有一些琐碎的后续工作。这些工作需要在下次做事
前完成。
拿测机关
来说,当测完机关后还需要检查一遍装备,以免下次使用出什么差次。
比如检查绳索
、检查手电
...
如果事情做的麻利,那一炷香
的时间其实可以做很多事。
比如这一炷香的时间依次做了:
所以,下墓后的工作流程是:
按一炷香
为周期完成一或多件事,最后完成绘图
。
接着开始下一炷香的周期。
按照这个流程操作,不说万无一失,那也是很有保障的。
坏就坏在,有些同行太过贪心,比如这样:
如果在一炷香
时间,一件事做的时间太长,那就没有时间绘图
了!!
地图缺失一块,哪里有机关,哪里有暗道被少标记了,各种风险不言而喻!
终究这行还是太过搏命,好在我及时转行前端,接下来让我从浏览器
角度再来解读下吧。
一般浏览器的刷新率为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
完后这一帧还有剩余时间呢?
如图中绿色部分:
此时你可以使用requestIdleCallback
API,如果渲染完成后还有空闲时间,则这个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,但好歹身边都是活人。
这行,是转对了。