一位摸金校尉决定转行前端

2,446 阅读7分钟

一位摸金校尉决定转行前端

我是一名摸金校尉。

我们这行起源于东汉末年三国时期。曹操为了弥补军饷的不足,设立发丘中郎将,摸金校尉等军衔,专司盗墓取财,贴补军饷。

曹操之后,盗墓者皆各自为政,同行之间并无师徒之分,凡以摸金之法盗墓,均为摸金校尉。

拜近几年“盗墓”题材小说所赐,越来越多的人了解我们这行。但这些小说以讹传讹,为了吸引眼球往往故作神秘、夸大其词。

摸金险象环生,稍不留意便万劫不复。事实上,不像小说里靠“主角光环”每每死里逃生,我们有严谨的工作流程。

高风险,收益不确定。随着时间推移,从业者越来越少。最近我也决定转行当前端了。

为了防止这老祖宗的手艺失传,这里我就和你唠唠我们这行怎么工作的。

你问为啥转行前端?嘿,别说,我们这行的工作原理和浏览器工作原理还真像,学起来毫无压力。

安全第一

万事安全第一。

我们这行容错率太低,稍有差次,那就是个狗带。所以下墓后的每一步,都得慎之又慎,按章办事。

古墓暗无天日,机关暗道错综复杂。最重要的,就是及时绘制地图

每过一炷香的时间,都需要将这段时间路过的坑道,遇到的机关悉数绘制下来,此谓绘图

绘图前这段时间用来做事

做事

那我们具体都做什么事呢?比如探路寻宝测机关...

测机关就得停止探路,要寻宝就不能测机关。总之,一次只能做一件事。

活着出去固然是最重要的,但是又不能空手而归。

老祖宗早已为“要做的事”划分了轻重缓急,既要保证“重要的事”先做,又要保证其他事不至于不做。

做事本身也很有讲究,每次做完事后可能会有一些琐碎的后续工作。这些工作需要在下次做事前完成。

测机关来说,当测完机关后还需要检查一遍装备,以免下次使用出什么差次。

比如检查绳索检查手电...

如果事情做的麻利,那一炷香的时间其实可以做很多事。

比如这一炷香的时间依次做了:

  • 测机关

  • 测机关后的一些琐碎工作

  • 探路

  • 绘图

所以,下墓后的工作流程是:

一炷香为周期完成一或多件事,最后完成绘图

接着开始下一炷香的周期。

按照这个流程操作,不说万无一失,那也是很有保障的。

坏就坏在,有些同行太过贪心,比如这样:

如果在一炷香时间,一件事做的时间太长,那就没有时间绘图了!!

地图缺失一块,哪里有机关,哪里有暗道被少标记了,各种风险不言而喻!

终究这行还是太过搏命,好在我及时转行前端,接下来让我从浏览器角度再来解读下吧。

浏览器的一帧

一般浏览器的刷新率为60HZ,即1秒钟刷新60次。

1000ms / 60hz = 16.6

大概每过16.6ms浏览器会渲染一帧画面,也就是说浏览器一炷香的时间是16.6ms。

在这段时间内,大体会做两件事:taskrender

其中task被称为宏任务,就像下墓后我们要做的事一样。

包括setTimeoutsetIntervalsetImmediatepostMessagerequestAnimationFrameI/ODOM 事件等。

render指渲染页面。

eventLoop

task按优先级被划分到不同的task queue中。就像老祖宗定的“轻重缓急”。

比如:为了及时响应用户交互,浏览器会为鼠标键盘(Mouse、Key)事件所在task queue分配3/4优先权。

这样可以及时响应用户交互,又不至于不执行其他task queue中的task

虚线框部分要做的工作是:

  1. 将新产生的task插入不同task queue中。

  2. 按优先级从某个task queue中选择一个task作为本次要执行的task

这就是事件循环eventLoop)。

task执行过程中如果调用PromiseMutationObserverprocess.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');

执行过程为:

  1. “全局作用域的代码执行”是第一个task

  2. 执行过程中调用setTimeout计时器线程会去处理计时,在计时结束后会将计时器回调加入task queue中。

  3. 调用Promise.resolve,产生microTask,插入microTask queue

  4. 打印global

  5. “全局作用域的代码执行”的task执行完毕,开始遍历清理microTask queue

  6. 打印promise1

  7. 调用Promise.resolve,产生microTask,插入当前microTask queue

  8. 继续遍历microTask queue,执行microTask打印promise2

  9. 开始第二个task,打印timeout

一帧执行多个task

就像一炷香时间可以做多件事,在一帧时间可以执行多个task

执行如下代码后,屏幕会先显示红色再显示黑色,还是直接显示黑色?

document.body.style.background = 'red';
setTimeout(function () {
    document.body.style.background = 'black';
})

答案是:不一定。

全局代码执行setTimeout为不同的2个task

如果这2个task在同一帧中执行,则页面渲染一次,直接显示黑色(如下图情况一)。

如果这2个task被分在不同帧中执行,则每一帧页面会渲染一次,屏幕会先显示红色再显示黑色(如下图情况二)。

如果我们将setTimeout的延迟时间增大到17ms,那么基本可以确定这2个task会在不同帧执行,则“屏幕会先显示红色再显示黑色”的概率会大很多。

requestAnimationFrame

可以发现,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                                 

setTimeout1setTimeout2作为2个task,使用默认延迟时间(不传延迟时间参数时,大概会有4ms延迟),那么大概率会在同一帧调用。

rAF1rAF2则一定会在不同帧的render前调用。

所以,大概率我们会在同一帧先后调用setTimeout1setTimeout2rAF1,再在另一帧调用rAF2

requestIdleCallback

如果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,但好歹身边都是活人。

这行,是转对了。