我是一名摸金校尉。
我们这行起源于东汉末年三国时期。曹操为了弥补军饷的不足,设立发丘中郎将,摸金校尉等军衔,专司盗墓取财,贴补军饷。
曹操之后,盗墓者皆各自为政,同行之间并无师徒之分,凡以摸金之法盗墓,均为摸金校尉。
拜近几年“盗墓”题材小说所赐,越来越多的人了解我们这行。但这些小说以讹传讹,为了吸引眼球往往故作神秘、夸大其词。
摸金险象环生,稍不留意便万劫不复。事实上,不像小说里靠“主角光环”每每死里逃生,我们有严谨的工作流程。
高风险,收益不确定。随着时间推移,从业者越来越少。最近我也决定转行当前端了。
为了防止这老祖宗的手艺失传,这里我就和你唠唠我们这行怎么工作的。
你问为啥转行前端?嘿,别说,我们这行的工作原理和浏览器工作原理还真像,学起来毫无压力。
安全第一
万事安全第一。
我们这行容错率太低,稍有差次,那就是个狗带。所以下墓后的每一步,都得慎之又慎,按章办事。
古墓暗无天日,机关暗道错综复杂。最重要的,就是及时绘制地图。
每过一炷香的时间,都需要将这段时间路过的坑道,遇到的机关悉数绘制下来,此谓绘图
。
![](https://i-blog.csdnimg.cn/blog_migrate/0e02741b1cc26f7895eef5aadc5c028f.png)
绘图
前这段时间用来做事
。
做事
那我们具体都做什么事呢?比如探路
、寻宝
、测机关
...
要测机关
就得停止探路
,要寻宝
就不能测机关
。总之,一次只能做一件事。
活着出去固然是最重要的,但是又不能空手而归。
老祖宗早已为“要做的事”划分了轻重缓急,既要保证“重要的事”先做,又要保证其他事不至于不做。
![](https://i-blog.csdnimg.cn/blog_migrate/af60695fa42cf8fd3e997b909ae2a080.png)
做事
本身也很有讲究,每次做完事
后可能会有一些琐碎的后续工作。这些工作需要在下次做事
前完成。
拿测机关
来说,当测完机关后还需要检查一遍装备,以免下次使用出什么差次。
比如检查绳索
、检查手电
...
如果事情做的麻利,那一炷香
的时间其实可以做很多事。
![](https://i-blog.csdnimg.cn/blog_migrate/aa40d64e557687a50c63dc6201560ef6.png)
比如这一炷香的时间依次做了:
测机关
测机关后的一些琐碎工作
探路
绘图
所以,下墓后的工作流程是:
按一炷香
为周期完成一或多件事,最后完成绘图
。
接着开始下一炷香的周期。
![](https://i-blog.csdnimg.cn/blog_migrate/15cd72b8ac6d1972fa58f56a7008008a.png)
按照这个流程操作,不说万无一失,那也是很有保障的。
坏就坏在,有些同行太过贪心,比如这样:
![](https://i-blog.csdnimg.cn/blog_migrate/7f40da03699dbf99f21cb47a125755ac.png)
如果在一炷香
时间,一件事做的时间太长,那就没有时间绘图
了!!
地图缺失一块,哪里有机关,哪里有暗道被少标记了,各种风险不言而喻!
终究这行还是太过搏命,好在我及时转行前端,接下来让我从浏览器
角度再来解读下吧。
浏览器的一帧
一般浏览器的刷新率为60HZ,即1秒钟刷新60次。
1000ms / 60hz = 16.6
大概每过16.6ms浏览器会渲染一帧画面,也就是说浏览器一炷香
的时间是16.6ms。
在这段时间内,大体会做两件事:task
与render
。
![](https://i-blog.csdnimg.cn/blog_migrate/aa3a3877532e7256bf07f8ee4bcab645.png)
其中task
被称为宏任务
,就像下墓后我们要做的事一样。
包括setTimeout
,setInterval
,setImmediate
,postMessage
,requestAnimationFrame
,I/O
,DOM 事件
等。
render
指渲染页面。
eventLoop
task
按优先级被划分到不同的task queue
中。就像老祖宗定的“轻重缓急”。
比如:为了及时响应用户交互,浏览器会为鼠标键盘(Mouse、Key)事件所在task queue
分配3/4优先权。
这样可以及时响应用户交互,又不至于不执行其他task queue
中的task
。
![](https://i-blog.csdnimg.cn/blog_migrate/e154e9378aecd2a273684956511f891a.png)
虚线框部分要做的工作是:
将新产生的
task
插入不同task queue
中。按优先级从某个
task queue
中选择一个task
作为本次要执行的task
。
这就是事件循环
(eventLoop
)。
task
执行过程中如果调用Promise
、MutationObserver
、process.nextTick
会将其作为microTask
(微任务)保存在microTask queue
中。
就像做事
后的琐碎工作。
每当执行完task
,在执行下一个task
前,都需要检查microTask queue
,执行并清空里面的microTask
。
![](https://i-blog.csdnimg.cn/blog_migrate/36433b1ca9526a9a12b1092f7844d4f7.png)
比如如下代码
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
就像一炷香
时间可以做多件事,在一帧时间可以执行多个task
。
执行如下代码后,屏幕会先显示红色再显示黑色,还是直接显示黑色?
document.body.style.background = 'red';
setTimeout(function () {
document.body.style.background = 'black';
})
答案是:不一定。
全局代码执行
和setTimeout
为不同的2个task
。
如果这2个task
在同一帧中执行,则页面渲染一次,直接显示黑色(如下图情况一)。
如果这2个task
被分在不同帧中执行,则每一帧页面会渲染一次,屏幕会先显示红色再显示黑色(如下图情况二)。
![](https://i-blog.csdnimg.cn/blog_migrate/ea5799c346321b1b80f17ace65a76a13.png)
如果我们将setTimeout
的延迟时间增大到17ms
,那么基本可以确定这2个task
会在不同帧执行,则“屏幕会先显示红色再显示黑色”的概率会大很多。
requestAnimationFrame
可以发现,task
没有办法精准的控制执行时机。那么有什么办法可以保证代码在每一帧都执行呢?
答案是:使用requestAnimationFrame
(简称rAF
)。
rAF
会在每一帧render
前被调用。
![](https://i-blog.csdnimg.cn/blog_migrate/f6348b2a480769754d4427de8a779824.png)
一般被用来绘制动画,因为当动画代码执行完后接下来就进入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
。
requestIdleCallback
如果render
完后这一帧还有剩余时间呢?
如图中绿色部分:
![](https://i-blog.csdnimg.cn/blog_migrate/19e816162526c50fea37ecc588d967dd.png)
此时你可以使用requestIdleCallback
API,如果渲染完成后还有空闲时间,则这个API
会被调用。
掉帧与时间切片
如果task
执行时间过长会怎么样呢?
如图taskA
执行时间超过了16.6ms(比如taskA
中有个很耗时的while
循环)。
那么这一帧就没有时间render
,页面直到下一帧render
后才会更新。
![](https://i-blog.csdnimg.cn/blog_migrate/1099cb465a697c41c386a5e352dffe93.png)
表现为页面卡顿一帧,或者说掉帧
。就像下墓后我们没有时间绘图
。
有什么好的解决办法么?
刚才提到的requestIdleCallback
是一个解决办法。我们可以将一部分工作放到空闲时间
中执行。
但是遇到长时间task
还是会掉帧。
更好的办法是:时间切片。即把长时间task
分割为几个短时间task
。
如图我们将taskA
拆分为2个task
。则每一帧都有机会render
。这样就能减少掉帧的可能。
![](https://i-blog.csdnimg.cn/blog_migrate/f7cf652a552bf61750d0a590227de0f3.png)
这React15
中,采用递归
的方式构建虚拟DOM树
。
如果树层级
很深,对应task
的执行时间很长,就可能出现掉帧的情况。
![](https://i-blog.csdnimg.cn/blog_migrate/81ca3baf3d9009e6db72bc0a0a390c27.png)
为了解决掉帧造成的卡顿,React16
将递归
的构建方式改为可中断的遍历
。
以5ms
的执行时间划分task
,每遍历
完一个节点,就检查当前task
是否已经执行了5ms
。
如果超过5ms
,则中断本次task
。
![](https://i-blog.csdnimg.cn/blog_migrate/dce990514fddc2695c549bea28be8020.png)
通过将task
执行时间切分为一个个小段,减少长时间task
造成无法render
的情况。这就是时间切片
。
摸了摸手边的摸金符,我欣慰的想到:虽然996,但好歹身边都是活人。
这行,是转对了。
关于奇舞周刊
《奇舞周刊》是360公司专业前端团队「奇舞团」运营的前端技术社区。关注公众号后,直接发送链接到后台即可给我们投稿。