Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
819b6d7452 | ||
|
|
b066c8a5d7 | ||
|
|
80fb60c6fb |
@@ -0,0 +1,308 @@
|
||||
## 1 引言
|
||||
|
||||
函数缓存是重要概念,本质上就是用空间(缓存存储)换时间(跳过计算过程)。
|
||||
|
||||
对于无副作用的纯函数,在合适的场景使用函数缓存是非常必要的,让我们跟着 https://whatthefork.is/memoization 这篇文章深入理解一下函数缓存吧!
|
||||
|
||||
## 2 概述
|
||||
|
||||
假设又一个获取天气的函数 `getChanceOfRain`,每次调用都要花 100ms 计算:
|
||||
|
||||
```jsx
|
||||
import { getChanceOfRain } from "magic-weather-calculator";
|
||||
function showWeatherReport() {
|
||||
let result = getChanceOfRain(); // Let the magic happen
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
|
||||
showWeatherReport(); // (!) Triggers the calculation
|
||||
showWeatherReport(); // (!) Triggers the calculation
|
||||
showWeatherReport(); // (!) Triggers the calculation
|
||||
```
|
||||
|
||||
很显然这样太浪费计算资源了,当已经计算过一次天气后,就没有必要再算一次了,我们期望的是后续调用可以直接拿上一次结果的缓存,这样可以节省大量计算。因此我们可以做一个 `memoizedGetChanceOfRain` 函数缓存计算结果:
|
||||
|
||||
```jsx
|
||||
import { getChanceOfRain } from "magic-weather-calculator";
|
||||
let isCalculated = false;
|
||||
let lastResult;
|
||||
// We added this function!
|
||||
function memoizedGetChanceOfRain() {
|
||||
if (isCalculated) {
|
||||
// No need to calculate it again.
|
||||
return lastResult;
|
||||
}
|
||||
// Gotta calculate it for the first time.
|
||||
let result = getChanceOfRain();
|
||||
// Remember it for the next time.
|
||||
lastResult = result;
|
||||
isCalculated = true;
|
||||
return result;
|
||||
}
|
||||
function showWeatherReport() {
|
||||
// Use the memoized function instead of the original function.
|
||||
let result = memoizedGetChanceOfRain();
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
```
|
||||
|
||||
在每次调用时判断优先用缓存,如果没有缓存则调用原始函数并记录缓存。这样当我们多次调用时,除了第一次之外都会立即从缓存中返回结果:
|
||||
|
||||
```jsx
|
||||
showWeatherReport(); // (!) Triggers the calculation
|
||||
showWeatherReport(); // Uses the calculated result
|
||||
showWeatherReport(); // Uses the calculated result
|
||||
showWeatherReport(); // Uses the calculated result
|
||||
```
|
||||
|
||||
然而对于有参数的场景就不适用了,因为缓存并没有考虑参数:
|
||||
|
||||
```jsx
|
||||
function showWeatherReport(city) {
|
||||
let result = getChanceOfRain(city); // Pass the city
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
showWeatherReport("London"); // Uses the calculated answer
|
||||
```
|
||||
|
||||
由于参数可能性很多,所以有三种解决方案:
|
||||
|
||||
### 1. 仅缓存最后一次结果
|
||||
|
||||
仅缓存最后一次结果是最节省存储空间的,而且不会有计算错误,但带来的问题就是当参数变化时缓存会立即失效:
|
||||
|
||||
```jsx
|
||||
import { getChanceOfRain } from "magic-weather-calculator";
|
||||
let lastCity;
|
||||
let lastResult;
|
||||
function memoizedGetChanceOfRain(city) {
|
||||
if (city === lastCity) {
|
||||
// Notice this check!
|
||||
// Same parameters, so we can reuse the last result.
|
||||
return lastResult;
|
||||
}
|
||||
// Either we're called for the first time,
|
||||
// or we're called with different parameters.
|
||||
// We have to perform the calculation.
|
||||
let result = getChanceOfRain(city);
|
||||
// Remember both the parameters and the result.
|
||||
lastCity = city;
|
||||
lastResult = result;
|
||||
return result;
|
||||
}
|
||||
function showWeatherReport(city) {
|
||||
// Pass the parameters to the memoized function.
|
||||
let result = memoizedGetChanceOfRain(city);
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
showWeatherReport("Tokyo"); // Uses the calculated result
|
||||
showWeatherReport("Tokyo"); // Uses the calculated result
|
||||
showWeatherReport("London"); // (!) Triggers the calculation
|
||||
showWeatherReport("London"); // Uses the calculated result
|
||||
```
|
||||
|
||||
在极端情况下等同于没有缓存:
|
||||
|
||||
```jsx
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
showWeatherReport("London"); // (!) Triggers the calculation
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
showWeatherReport("London"); // (!) Triggers the calculation
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
```
|
||||
|
||||
### 2. 缓存所有结果
|
||||
|
||||
第二种方案是缓存所有结果,使用 Map 存储缓存即可:
|
||||
|
||||
```jsx
|
||||
// Remember the last result *for every city*.
|
||||
let resultsPerCity = new Map();
|
||||
function memoizedGetChanceOfRain(city) {
|
||||
if (resultsPerCity.has(city)) {
|
||||
// We already have a result for this city.
|
||||
return resultsPerCity.get(city);
|
||||
}
|
||||
// We're called for the first time for this city.
|
||||
let result = getChanceOfRain(city);
|
||||
// Remember the result for this city.
|
||||
resultsPerCity.set(city, result);
|
||||
return result;
|
||||
}
|
||||
function showWeatherReport(city) {
|
||||
// Pass the parameters to the memoized function.
|
||||
let result = memoizedGetChanceOfRain(city);
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
|
||||
showWeatherReport("Tokyo"); // (!) Triggers the calculation
|
||||
showWeatherReport("London"); // (!) Triggers the calculation
|
||||
showWeatherReport("Tokyo"); // Uses the calculated result
|
||||
showWeatherReport("London"); // Uses the calculated result
|
||||
showWeatherReport("Tokyo"); // Uses the calculated result
|
||||
showWeatherReport("Paris"); // (!) Triggers the calculation
|
||||
```
|
||||
|
||||
这么做带来的弊端就是内存溢出,当可能参数过多时会导致内存无限制的上涨,最坏的情况就是触发浏览器限制或者页面崩溃。
|
||||
|
||||
### 3. 其他缓存策略
|
||||
|
||||
介于只缓存最后一项与缓存所有项之间还有这其他选择,比如 LRU(least recently used)只保留最小化最近使用的缓存,或者为了方便浏览器回收,使用 WeakMap 替代 Map。
|
||||
|
||||
最后提到了函数缓存的一个坑,必须是纯函数。比如下面的 CASE:
|
||||
|
||||
```jsx
|
||||
// Inside the magical npm package
|
||||
function getChanceOfRain() {
|
||||
// Show the input box!
|
||||
let city = prompt("Where do you live?");
|
||||
// ... calculation ...
|
||||
}
|
||||
// Our code
|
||||
function showWeatherReport() {
|
||||
let result = getChanceOfRain();
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
```
|
||||
|
||||
`getChanceOfRain` 每次会由用户输入一些数据返回结果,导致缓存错误,原因是 “函数入参一部分由用户输入” 就是副作用,我们不能对有副作用的函数进行缓存。
|
||||
|
||||
这有时候也是拆分函数的意义,将一个有副作用函数的无副作用部分分解出来,这样就能局部做函数缓存了:
|
||||
|
||||
```jsx
|
||||
// If this function only calculates things,
|
||||
// we would call it "pure".
|
||||
// It is safe to memoize this function.
|
||||
function getChanceOfRain(city) {
|
||||
// ... calculation ...
|
||||
}
|
||||
// This function is "impure" because
|
||||
// it shows a prompt to the user.
|
||||
function showWeatherReport() {
|
||||
// The prompt is now here
|
||||
let city = prompt("Where do you live?");
|
||||
let result = getChanceOfRain(city);
|
||||
console.log("The chance of rain tomorrow is:", result);
|
||||
}
|
||||
```
|
||||
|
||||
最后,我们可以将缓存函数抽象为高阶函数:
|
||||
|
||||
```jsx
|
||||
function memoize(fn) {
|
||||
let isCalculated = false;
|
||||
let lastResult;
|
||||
return function memoizedFn() {
|
||||
// Return the generated function!
|
||||
if (isCalculated) {
|
||||
return lastResult;
|
||||
}
|
||||
let result = fn();
|
||||
lastResult = result;
|
||||
isCalculated = true;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
这样生成新的缓存函数就方便啦:
|
||||
|
||||
```jsx
|
||||
let memoizedGetChanceOfRain = memoize(getChanceOfRain);
|
||||
let memoizedGetNextEarthquake = memoize(getNextEarthquake);
|
||||
let memoizedGetCosmicRaysProbability = memoize(getCosmicRaysProbability);
|
||||
```
|
||||
|
||||
`isCalculated` 与 `lastResult` 都存储在 `memoize` 函数生成的闭包内,外部无法访问。
|
||||
|
||||
## 3 精读
|
||||
|
||||
### 通用高阶函数实现函数缓存
|
||||
|
||||
原文的例子还是比较简单,没有考虑函数多个参数如何处理,下面我们分析一下 Lodash `memoize` 函数源码:
|
||||
|
||||
```jsx
|
||||
function memoize(func, resolver) {
|
||||
if (
|
||||
typeof func != "function" ||
|
||||
(resolver != null && typeof resolver != "function")
|
||||
) {
|
||||
throw new TypeError(FUNC_ERROR_TEXT);
|
||||
}
|
||||
var memoized = function () {
|
||||
var args = arguments,
|
||||
key = resolver ? resolver.apply(this, args) : args[0],
|
||||
cache = memoized.cache;
|
||||
|
||||
if (cache.has(key)) {
|
||||
return cache.get(key);
|
||||
}
|
||||
var result = func.apply(this, args);
|
||||
memoized.cache = cache.set(key, result) || cache;
|
||||
return result;
|
||||
};
|
||||
memoized.cache = new (memoize.Cache || MapCache)();
|
||||
return memoized;
|
||||
}
|
||||
```
|
||||
|
||||
原文有提到缓存策略多种多样,而 Lodash 将缓存策略简化为 key 交给用户自己管理,看这段代码:
|
||||
|
||||
```jsx
|
||||
key = resolver ? resolver.apply(this, args) : args[0];
|
||||
```
|
||||
|
||||
也就是缓存的 key 默认是执行函数时第一个参数,也可以通过 `resolver` 拿到参数处理成新的缓存 key。
|
||||
|
||||
在执行函数时也传入了参数 `func.apply(this, args)`。
|
||||
|
||||
最后 `cache` 也不再使用默认的 Map,而是允许用户自定义 `lodash.memoize.Cache` 自行设置,比如设置为 WeakMap:
|
||||
|
||||
```jsx
|
||||
_.memoize.Cache = WeakMap;
|
||||
```
|
||||
|
||||
### 什么时候不适合用缓存
|
||||
|
||||
以下两种情况不适合用缓存:
|
||||
|
||||
1. 不经常执行的函数。
|
||||
2. 本身执行速度较快的函数。
|
||||
|
||||
对于不经常执行的函数,本身就不需要利用缓存提升执行效率,而缓存反而会长期占用内存。对于本身执行速度较快的函数,其实大部分简单计算速度都很快,使用缓存后对速度没有明显的提升,同时如果计算结果比较大,反而会占用存储资源。
|
||||
|
||||
对于引用的变化尤其重要,比如如下例子:
|
||||
|
||||
```jsx
|
||||
function addName(obj, name){
|
||||
return {
|
||||
...obj,
|
||||
name:
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
为 `obj` 添加一个 key,本身执行速度是非常快的,但添加缓存后会带来两个坏处:
|
||||
|
||||
1. 如果 `obj` 非常大,会在闭包存储完整 `obj` 结构,内存占用加倍。
|
||||
2. 如果 `obj` 通过 mutable 方式修改了,则普通缓存函数还会返回原先结果(因为对象引用没有变),造成错误。
|
||||
|
||||
如果要强行进行对象深对比,虽然会避免出现边界问题,但性能反而会大幅下降。
|
||||
|
||||
## 4 总结
|
||||
|
||||
函数缓存非常有用,但并不是所有场景都适用,因此千万不要极端的将所有函数都添加缓存,仅限于计算耗时、可能重复利用多次,且是纯函数的。
|
||||
|
||||
> 讨论地址是:[精读《函数缓存》· Issue #261 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/261)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,109 @@
|
||||
## 1 引言
|
||||
|
||||
[「可视化搭建系统」——从设计到架构,探索前端的领域和意义](https://juejin.im/post/6854573220532748302) 这篇文章主要分析了现阶段可视化搭建的几种表现形式和实现原理,并重点介绍了基于富文本的可视化搭建思路,让人耳目一新。
|
||||
|
||||
基于富文本的可视化搭建看似很新颖,但其实早就被广泛使用了,任何一个富文本编辑器几乎都有插入表格功能,这就是一个典型插入自定义组件的场景。
|
||||
|
||||
使用过 [语雀](https://www.yuque.com/) 的同学应该知道,这个产品的富文本编辑器可以插入各种各样自定义区块,是 “最像搭建” 的富文本编辑器。
|
||||
|
||||
那么积木式搭建和富文本搭建存在哪些差异,除了富文本更倾向于记录静态内容外,还有哪些差异,两者是否可以结合?本文将围绕这两点进行讨论。
|
||||
|
||||
## 2 精读
|
||||
|
||||
还是先顺着原文谈谈对可视化搭建的理解:
|
||||
|
||||
可视化搭建是通过可视化方式代替开发。**前端代码开发主要围绕的是 html + js + css**,那么无论是 markdown 语法,还是创建另一套模版语言亦或 JSON 构成的 DSL,**都是用一种 dsl + 组件 + css 的方式代替 html + js + css**,可视化搭建则更进一步,用 ui 代替了 dsl + 组件,**即精简为 ui 操作 + css**。
|
||||
|
||||
可以看到,这种转换的推演过程存在一定瑕疵,因为每次转换都有部分损耗:
|
||||
|
||||
**用 dsl + 组件 代替 html + js。**
|
||||
|
||||
如果 dsl 拓展得足够好,理论上可以达到 html 的水平,尤其在垂直业务场景是不需要那么多特殊 html 标签的。
|
||||
|
||||
但用组件代替 js 就有点奇怪了,首先并不是所有 js 逻辑都沉淀在组件里,一定有组件间的联动逻辑是无法通过一个组件 js 完成的,另一方面如果将 js 逻辑寄托在组件代码里,本质上是没有提效的,用源码开发项目与开发搭建平台的组件都是 pro code,更极端一点来说,无论是组件间联动还是整个应用都可以用一个组件来写,那搭建平台就无事可做了,这个组件也成了整个应用,game over。
|
||||
|
||||
为了弥补这块缺憾,低代码能力的呼声越来越高,而低代码能力的核心在于设计是否合理,比如暴露哪些 API 可以覆盖大部分需求?写多少代码合适,如何以最小 API 透出最大弥补组件间缺失的 js 能力?目前来看,以状态数据驱动的低代码是相对优雅的。
|
||||
|
||||
**用 ui 操作 代替 dsl + 组件。**
|
||||
|
||||
UI 操作并不是标准的,相比直接操作模版或者 JSON DSL,UI 化后就仁者见仁智者见智了,但 UI 化带来的效率提升是巨大的,因为所见即所得是生产力的源泉,从直观的 UI 布局来看,就比维护代码更轻松。但 UI 化也存在两个问题,一个是可能有人觉得不如 markdown 效率高,另一个是功能有丢失。
|
||||
|
||||
对于第一点 UI 操作效率不如 markdown 高,可能很多程序员都崇尚用 markdown 维护文档而不是富文本,原因是觉得程序员维护代码的效率反而比所见即所得高,但那可能是错觉,原因是还没有遇到好用的富文本编辑器,体验过语雀富文本编辑器后,相信大部分程序员都不会再想回头写 markdown。当然语雀富文本战胜 markdown 的原因有很多,我觉得主要两点是吸收并兼容了 markdown 操作习惯,与支持了更多仅 UI 能做到的拓展能力,对 markdown 形成降维打击。
|
||||
|
||||
第二点功能丢失很好理解,markdown 有一套标准语法和解析器可以验证,但 UI 操作并没有标准化,也没有独立验证系统,如果无法回退到源码模式,UI 没有实现的功能就做不到。
|
||||
|
||||
回到富文本搭建上,其实富文本搭建和普通网页构建并没有本质区别。html 是超文本标记语言,富文本是跨平台文档格式,从逻辑上这两个格式是可以互转的,只要富文本规则作出足够多的拓展,就可以大致覆盖 html 的能力。
|
||||
|
||||
但富文本搭建有着显著的特征,就是光标。
|
||||
|
||||
### 积木式搭建和富文本搭建的区别
|
||||
|
||||
富文本以文本为中心,因此编辑文字的光标会常驻,编辑的核心逻辑是排版文字,并考虑如何在文字周围添加一些自定义区块。
|
||||
|
||||
有了光标后,圈选也非常重要,因为大家编辑文字时有一种很自然的想法是,任何文字圈选后复制,可以粘贴到任何地方,那么所有插入到富文本中的自定义组件也要支持被圈选,被复制。
|
||||
|
||||
实际上富文本内插入自定义区块也可以转换为积木式搭建方案解决,比如下面的场景:
|
||||
|
||||
```text
|
||||
文本 A
|
||||
图表 B
|
||||
文本 C
|
||||
```
|
||||
|
||||
我们在文本 A 与 文本 C 之间插入图表 B,也可以理解为拖拽了三个组件:文本组件 A + 图表组件 B + 文本组件 C,然后分别编辑这三个组件,微调样式后可以达到与富文本一样的编辑效果,甚至加上自由布局后,在布局能力上会超越富文本。
|
||||
|
||||
虽然功能层面上富文本略有输给积木式搭建,但富文本在编辑体验上是胜出的,对于文字较多的场景,我们还是会选择富文本方式编辑而不是积木式搭建拖拽 N 个文本组件。
|
||||
|
||||
所以微软 OneNote 也吸取了这个经验,毕竟笔记本主要还是记录文字,因此还是采用富文本的编辑模式,但创造性的加入了一个个独立区块,点击任何区域都会创造一个区块,整个文档可以由一个区块构成,也可以是多个区块组合而成,这样对于连贯性的文字场景可以采用一个富文本区块,对于自定义区块较多,比如大部分是图片和表格的,还可以回到积木式搭建的体验。由于 OneNote 采用绝对定位模拟流式布局的思路,当区块重叠时还可以自动挤压底部区块,因此多区块模式下编辑体验还是相对顺畅的。
|
||||
|
||||
可以看出来这是一种结合的尝试,从前端角度来看,富文本本质上是对一个 div 进行 contenteditable 申明,那么一个应用可以整体是 contenteditable 的,也可以局部几个区块是,这种代码层面的自由度体现在搭建上就是积木式搭建可以与富文本搭建自由结合。
|
||||
|
||||
### 积木式搭建与富文本搭建如何结合
|
||||
|
||||
对于积木式搭建来说,富文本只是其中一个组件,在不考虑有富文本组件时是完全没有富文本能力的。比如一个搭建平台只提供了几个图表和基础控件,你是不可能在其基础上使用富文本能力的,甚至连写静态文本都做不到。
|
||||
|
||||
所以富文本只是搭建中一个组件,就像 contenteditable 也只能依附于一个标签,整个网页还是由标签组成的。但对于一个提供了富文本组件的积木式搭建系统来说,文字与控件混排又是一个痛点,毕竟要以一个个区块组件的方式去拖拽文本节点,成本比富文本模式大得多。
|
||||
|
||||
所以理想情况是富文本与整个搭建系统使用同一套 DSL 描述结构,富文本只是在布局上有所简化,简化为简单的平铺模式即可,但因为 DSL 描述打通,富文本也可以描述使用搭建提供的任意组件嵌套在内,所以只要用户愿意,可以将富文本组件拉到最大,整个页面都基于富文本模式去搭建,这就变成了富文本搭建,也可以将富文本缩小,将普通控件以积木方式拖拽到画布中,走积木式搭建路线。
|
||||
|
||||
用代码方式描述积木式搭建:
|
||||
|
||||
```html
|
||||
<bar-chart />
|
||||
<div>
|
||||
<p>header</p>
|
||||
<line-chart />
|
||||
<p>footer</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
上述模式需要拖拽 `bar-chart`、`div`、`p`、`line-chart`、`p` 共 5 个组件。富文本模式则类似下面的结构:
|
||||
|
||||
```html
|
||||
<bar-chart />
|
||||
<div contenteditable>
|
||||
<p>header</p>
|
||||
<line-chart />
|
||||
<p>footer</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
只要拖拽 `bar-chart`、`div` 两个组件即可,`div` 内部的文字通过光标输入,`line-chart` 通过富文本某个按钮或者键盘快捷键添加。
|
||||
|
||||
可以看到虽然操作方式不同,但本质上描述协议并没有本质区别,我们理论上可以将任何容器标签切换为富文本模式。
|
||||
|
||||
## 3 总结
|
||||
|
||||
富文本是一种重要的交互模式,可以基于富文本模式做搭建,也可以在搭建系统中嵌入富文本组件,甚至还可以追求搭建与富文本的结合。
|
||||
|
||||
富文本组件既可以是搭建系统中一个组件,又可以在内部承载搭建系统的所有组件,做到这一步才算是真正发挥出富文本的潜力。
|
||||
|
||||
> 讨论地址是:[精读《可视化搭建思考 - 富文本搭建》· Issue #262 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/262)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
@@ -0,0 +1,185 @@
|
||||
## 1 引言
|
||||
|
||||
本周跟着 [Tasks, microtasks, queues and schedules](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) 这篇文章一起深入理解这些概念间的区别。
|
||||
|
||||
先说结论:
|
||||
|
||||
- Tasks 按顺序执行,浏览器可能在 Tasks 之间执行渲染。
|
||||
- Microtasks 也按顺序执行,时机是:
|
||||
- 如果没有执行中的 js 堆栈,则在每个回调之后。
|
||||
- 在每个 task 之后。
|
||||
|
||||
## 2 概述
|
||||
|
||||
### Event Loop
|
||||
|
||||
在说这些概念前,先要介绍 Event Loop。
|
||||
|
||||
首先浏览器是多线程的,每个 JS 脚本都在单线程中执行,每个线程都有自己的 Event Loop,同源的所有浏览器窗口共享一个 Event Loop 以便通信。
|
||||
|
||||
Event Loop 会持续循环的执行所有排队中的任务,浏览器会为这些任务划分优先级,按照优先级来执行,这就会导致 Tasks 与 Microtasks 执行顺序与调用顺序的不同。
|
||||
|
||||
### promise 与 setTimeout
|
||||
|
||||
看下面代码的输出顺序:
|
||||
|
||||
```js
|
||||
console.log("script start");
|
||||
|
||||
setTimeout(function () {
|
||||
console.log("setTimeout");
|
||||
}, 0);
|
||||
|
||||
Promise.resolve()
|
||||
.then(function () {
|
||||
console.log("promise1");
|
||||
})
|
||||
.then(function () {
|
||||
console.log("promise2");
|
||||
});
|
||||
|
||||
console.log("script end");
|
||||
```
|
||||
|
||||
正确答案是 `script start`, `script end`, `promise1`, `promise2`, `setTimeout`,在线程中,同步脚本执行优先级最高,然后 promise 任务会存放到 Microtasks,setTimeout 任务会存放到 Tasks,Microtasks 会优先于 Tasks 执行。
|
||||
|
||||
Microtasks 中文可以翻译为微任务,只要有 Microtasks 插入,就会不断执行 Microtasks 队列直到结束,在结束前都不会执行到 Tasks。
|
||||
|
||||
### 点击冒泡 + 任务
|
||||
|
||||
下面给出了更复杂的例子,提前说明后面的例子 Chrome、Firefox、Safari、Edge 浏览器的结果完全不一样,但只有 Chrome 的运行结果是对的!为什么 Chrome 是对的呢,请看下面的分析:
|
||||
|
||||
```html
|
||||
<div class="outer">
|
||||
<div class="inner"></div>
|
||||
</div>
|
||||
```
|
||||
|
||||
```js
|
||||
// Let's get hold of those elements
|
||||
var outer = document.querySelector(".outer");
|
||||
var inner = document.querySelector(".inner");
|
||||
|
||||
// Let's listen for attribute changes on the
|
||||
// outer element
|
||||
new MutationObserver(function () {
|
||||
console.log("mutate");
|
||||
}).observe(outer, {
|
||||
attributes: true,
|
||||
});
|
||||
|
||||
// Here's a click listener…
|
||||
function onClick() {
|
||||
console.log("click");
|
||||
|
||||
setTimeout(function () {
|
||||
console.log("timeout");
|
||||
}, 0);
|
||||
|
||||
Promise.resolve().then(function () {
|
||||
console.log("promise");
|
||||
});
|
||||
|
||||
outer.setAttribute("data-random", Math.random());
|
||||
}
|
||||
|
||||
// …which we'll attach to both elements
|
||||
inner.addEventListener("click", onClick);
|
||||
outer.addEventListener("click", onClick);
|
||||
```
|
||||
|
||||
点击 `inner` 区块后,正确输出顺序应该是:
|
||||
|
||||
```text
|
||||
click
|
||||
promise
|
||||
mutate
|
||||
click
|
||||
promise
|
||||
mutate
|
||||
timeout
|
||||
timeout
|
||||
```
|
||||
|
||||
逻辑如下:
|
||||
|
||||
1. 点击触发 `onClick` 函数入栈。
|
||||
2. 立即执行 `console.log('click')` 打印 `click`。
|
||||
3. `console.log('timeout')` 入栈 Tasks。
|
||||
4. `console.log('promise')` 入栈 microtasks。
|
||||
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
|
||||
6. `onClick` 函数执行完毕,此时线程调用栈为空,开始执行 microtasks 队列。
|
||||
7. 打印 `promise`,打印 `mutate`,此时 microtasks 已空。
|
||||
8. 执行冒泡机制,outer div 也触发 `onClick` 函数,同理,打印 `promise`,打印 `mutate`。
|
||||
9. 都执行完后,执行 Tasks,打印 `timeout`,打印 `timeout`。
|
||||
|
||||
### 模拟点击冒泡 + 任务
|
||||
|
||||
如果将触发 `onClick` 行为由点击改为:
|
||||
|
||||
```js
|
||||
inner.click();
|
||||
```
|
||||
|
||||
结果会不同吗?答案是会(单元测试与用户行为不符合,单测也有无解的时候)。然而四大浏览器的执行结果也是完全不一样,但从逻辑上讲仍然 Chrome 是对的,让我们看下 Chrome 的结果:
|
||||
|
||||
```text
|
||||
click
|
||||
click
|
||||
promise
|
||||
mutate
|
||||
promise
|
||||
timeout
|
||||
timeout
|
||||
```
|
||||
|
||||
逻辑如下:
|
||||
|
||||
1. `inner.click()` 触发 `onClick` 函数入栈。
|
||||
2. 立即执行 `console.log('click')` 打印 `click`。
|
||||
3. `console.log('timeout')` 入栈 Tasks。
|
||||
4. `console.log('promise')` 入栈 microtasks。
|
||||
5. `outer.setAttribute('data-random')` 的触发导致监听者 `MutationObserver` 入栈 microtasks。
|
||||
6. 由于冒泡改为 js 调用栈执行,所以此时 js 调用栈未结束,不会执行 microtasks,反而是继续执行冒泡,outer 的 `onClick` 函数入栈。
|
||||
7. 立即执行 `console.log('click')` 打印 `click`。
|
||||
8. `console.log('timeout')` 入栈 Tasks。
|
||||
9. `console.log('promise')` 入栈 microtasks。
|
||||
10. `MutationObserver` 由于还没调用,因此这次 `outer.setAttribute('data-random')` 的改动实际上没有作用。
|
||||
11. js 调用栈执行完毕,开始执行 microtasks,按照入栈顺序,打印 `promise`,`mutate`,`promise`。
|
||||
12. microtasks 执行完毕,开始执行 Tasks,打印 `timeout`,`timeout`。
|
||||
|
||||
## 3 精读
|
||||
|
||||
基于任务调度这么复杂,且浏览器实现方式很不同,下面两件事是我很不推荐的:
|
||||
|
||||
1. 业务逻辑 “巧妙” 依赖了 microtasks 与 Tasks 执行逻辑的微妙差异。
|
||||
2. 死记硬背调用顺序。
|
||||
|
||||
且不说依赖了调用顺序的业务逻辑本身就很难维护,不同浏览器之间对任务调用顺序还是不同的,这可能源于对 W3C 标准规范理解的偏差,也可能是 BUG,这会导致依赖于此的逻辑非常脆弱。
|
||||
|
||||
虽然上面两个例子非常复杂,但我们也不必把这个例子当作经典背诵,只要记住文章开头提到的执行逻辑就可以推导:
|
||||
|
||||
- Tasks 按顺序执行,浏览器可能在 Tasks 之间执行渲染。
|
||||
- Microtasks 也按顺序执行,时机是:
|
||||
- 如果没有执行中的 js 堆栈,则在每个回调之后。
|
||||
- 在每个 task 之后。
|
||||
|
||||
记住 `Promise` 是 `Microtasks`,`setTimeout` 是 `Tasks`,JS 一次 Event Loop 完毕后,即调用栈没有内容时才会执行 `Microtasks` -> `Tasks`,在执行 `Microtasks` 过程中插入的 `Microtasks` 会按顺序继续执行,而执行 `Tasks` 中插入的 `Microtasks` 得等到调用栈执行完后才继续执行。
|
||||
|
||||
上面说的内容都是指一次 Event Loop 时立即执行的优先级,不要和执行延迟时间弄混淆了。
|
||||
|
||||
把 JS 线程的 Event Loop 当作一个函数,函数内同步逻辑执行优先级是最高的,如果遇到 `Microtasks` 或 `Tasks` 就会立即记录下来,当一次 Event Loop 执行完后立即调用 `Microtasks`,等 `Microtasks` 队列执行完毕后可能进行一些渲染行为,等这些浏览器操作完成后,再考虑执行 `Tasks` 队列。
|
||||
|
||||
## 4 总结
|
||||
|
||||
最后,还是要强调一句,不要依赖 `Microtasks` 与 `Tasks` 的执行顺序,尤其在申明式编程环境中,我们可以把 `Microtasks` 与 `Tasks` 都当作是异步内容,在渲染时做好状态判断即可,不用关心先后顺序。
|
||||
|
||||
> 讨论地址是:[精读《Tasks, microtasks, queues and schedules》· Issue #264 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/264)
|
||||
|
||||
**如果你想参与讨论,请 [点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**
|
||||
|
||||
> 关注 **前端精读微信公众号**
|
||||
|
||||
<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">
|
||||
|
||||
> 版权声明:自由转载-非商用-非衍生-保持署名([创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh))
|
||||
Reference in New Issue
Block a user