一位摸金校尉决定转行前端
我是一名摸金校尉。
我们这行起源于东汉末年三国时期。曹操为了弥补军饷的不足,设立发丘中郎将,摸金校尉等军衔,专司盗墓取财,贴补军饷。
曹操之后,盗墓者皆各自为政,同行之间并无师徒之分,凡以摸金之法盗墓,均为摸金校尉。
拜近几年“盗墓”题材小说所赐,越来越多的人了解我们这行。但这些小说以讹传讹,为了吸引眼球往往故作神秘、夸大其词。
摸金险象环生,稍不留意便万劫不复。事实上,不像小说里靠“主角光环”每每死里逃生,我们有严谨的工作流程。
高风险,收益不确定。随着时间推移,从业者越来越少。最近我也决定转行当前端了。
为了防止这老祖宗的手艺失传,这里我就和你唠唠我们这行怎么工作的。
你问为啥转行前端?嘿,别说,我们这行的工作原理和浏览器工作原理还真像,学起来毫无压力。
安全第一
万事安全第一。
我们这行容错率太低,稍有差次,那就是个狗带。所以下墓后的每一步,都得慎之又慎,按章办事。
古墓暗无天日,机关暗道错综复杂。最重要的,就是及时绘制地图。
每过一炷香的时间,都需要将这段时间路过的坑道,遇到的机关悉数绘制下来,此谓绘图
。
绘图
前这段时间用来做事
。
做事
那我们具体都做什么事呢?比如探路
、寻宝
、测机关
...
要测机关
就得停止探路
,要寻宝
就不能测机关
。总之,一次只能做一件事。
活着出去固然是最重要的,但是又不能空手而归。
老祖宗早已为“要做的事”划分了轻重缓急,既要保证“重要的事”先做,又要保证其他事不至于不做。
做事
本身也很有讲究,每次做完事
后可能会有一些琐碎的后续工作。这些工作需要在下次做事
前完成。
拿测机关
来说,当测完机关后还需要检查一遍装备,以免下次使用出什么差次。
比如检查绳索
、检查手电
...
如果事情做的麻利,那一炷香
的时间其实可以做很多事。
比如这一炷香的时间依次做了:
- 测机关
- 测机关后的一些琐碎工作
- 探路
- 绘图
所以,下墓后的工作流程是:
按一炷香
为周期完成一或多件事,最后完成绘图
。
接着开始下一炷香的周期。
按照这个流程操作,不说万无一失,那也是很有保障的。
坏就坏在,有些同行太过贪心,比如这样:
如果在一炷香
时间,一件事做的时间太长,那就没有时间绘图
了!!
地图缺失一块,哪里有机关,哪里有暗道被少标记了,各种风险不言而喻!
终究这行还是太过搏命,好在我及时转行前端,接下来让我从浏览器
角度再来解读下吧。
浏览器的一帧
一般浏览器的刷新率为60HZ,即1秒钟刷新60次。
1000ms / 60hz = 16.6
大概每过16.6ms浏览器会渲染一帧画面,也就是说浏览器一炷香
的时间是16.6ms。
在这段时间内,大体会做两件事:task
与render
。
其中task
被称为宏任务
,就像下墓后我们要做的事一样。
包括setTimeout
,setInterval
,setImmediate
,postMessage
,requestAnimationFrame
,I/O
,DOM 事件
等。
render
指渲染页面。
eventLoop
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
就像一炷香
时间可以做多件事,在一帧时间可以执行多个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
setTimeout1
与setTimeout2
作为2个task
,使用默认延迟时间(不传延迟时间参数时,大概会有4ms延迟),那么大概率会在同一帧调用。
rAF1
与rAF2
则一定会在不同帧的render
前调用。
所以,大概率我们会在同一帧先后调用setTimeout1
、setTimeout2
、rAF1
,再在另一帧调用rAF2
。
requestIdleCallback
如果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,但好歹身边都是活人。
这行,是转对了。
- TensorFlow从0到1 - 16 - L2正则化对抗“过拟合”
- 使用虚拟环境,搭建python3+scrapy
- Matplotlib基础全攻略
- Python 小爬虫 - 爬取今日头条街拍美女图
- python3使用zookeeper和私钥解密及编码转化配置信息
- Python中os.path.dirname(__file__)的用法
- TensorFlow从0到1 - 18 - TensorFlow 1.3.0安装手记
- Python + Splinter 实现浏览器自动化操作入门指南
- 动态地理信息可视化——leaflet在线地图简介
- python中的递归函数
- 对抗思想与强化学习的碰撞-SeqGAN模型原理和代码解析
- 玩转数据地图系列之——地图上的迷你条形图
- 树上倍增求LCA及例题
- 深度强化学习-DDPG算法原理和实现
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- mybatis逆向工程
- ssm之spring+springmvc+mybatis整合初探
- mybatis插件开发小例子
- java之如何在eclipse中新建对象时自动补全
- mybatis文件映射之当输入的参数不只一个时
- mybatis插件开发初探
- 剑指offer(25-30)题解
- 如何实时迁移MySQL到TcaplusDB
- 如何利用Terraform工具编排管理TcaplusDB
- 如何实时迁移AWS DynamoDB到TcaplusDB
- 腾讯云TcaplusDB基础能力介绍
- 游戏架构上云实战
- 【JUC】CyclicBarrier的了解和使用
- 完美解决-RuntimeError: CUDA error: device-side assert triggered
- springmvc之异常处理SimpleMappingExceptionResolver