0%

Concurrent Mode

官方对于 Concurrent Mode,用了一个有趣的比喻,把 Concurrent Mode 隐喻为版本控制,版本控制大家都了解,平常开发项目时,大家会在同一个项目的不同分支中修改内容,比如修改同一个文件

假设现在没有版本控制,为了避免冲突,A 同学在修改 a 文件时,B、C、D 等其他同学就得等着 A 同学修改完然后释放文件,这种情况下会阻塞 B,C,D 等其他同学的开发进度

Concurrent Mode 则提供了一个类似分支的概念,把 A,B,C 等同学比作 React 中的 render 任务,浏览器的渲染进程,IO 进程等等,当React 中的 render 任务执行时,可以不阻塞浏览器中的其他进程

其实严格来说,Concurrent Mode 比作版本控制也不是那么妥当,毕竟场景不是一模一样,只是希望大家能理解到其中的含义即可

个人理解的话,React 中的 Concurrent Mode 是指在 Reconciler 中处理 long task 时,可以不阻塞浏览器中的其他进程,并且 React 中的 render 任务 具有各自的优先级,任务可以通过过时间分片 + 优先级调度的方式在执行暂停之间切换状态

Legacy & Concurrent & Blocking

React v17 中提供了三种可选的方式来创建 React 应用,分别是 Legacy ModeConcurrent ModeBlocking Moode

Legacy Mode 是我们熟悉的传统模式,目前是通过 ReactDOM.render 来触发这个模式,Legacy Mode 下大家都熟悉,Reconcile Fiber 流程一步到位,不可中断,当页面中存在大量组件 render 时会导致时间过长,阻塞浏览器的渲染进程,页面响应速度慢,交互卡顿明显

Concurrent Mode 目的是为了能让渲染流程变的可中断,也就是我们现在常听到的 Interruptible Rendering - 可中断式渲染 这一概念

React 计划在 v18 中正式默认启用 Concurrent Mode,但是让大型的 React App 一口气升级上来难免会遇到一些问题,主要体现在 React 现在的生态中会有些组件还在使用一些不安全的生命周期,一些老旧的 React Library 可能没办法跟 Concurrent Mode 兼容运行

为了能更加平滑过渡到 Concurrent Mode,新增了 Blocking Mode,目前 React v17 中则是采用了 Blocking Mode 的过渡模式,可以通过 ReactDOM.createBlockingRoot 开启过渡模式,在 Blocking Mode 期间,减少 unsafe_liftcycleString RefsLegacy ContextfindDOMNode 等不稳定或者不兼容 api 的使用,等待 React 的生态逐步跟进后,再尝试使用 React v18 的新特性

关于 Blocking Mode 特性的参考链接

阅读全文 »

React UpdateQueue

使用过 React 的小伙伴肯定也都知道 React 在事件回调或生命周期回调中会对同步任务内的更新做批量处理

那么这里的批量更新又是通过什么机制去实现的呢?

此文基于 react v17.0.2 分析,仓库传送门

UpdateQueue 的创建

Update 的结构

每次更新都新创建一个 Update 对象,这个 Update 对象的结构如下

1
2
3
4
5
6
7
8
9
10
11
12
// @flow
export type Update<State> = {|
// eventTime 是临时属性,后续会通过在 root 节点存储一个 transition 映射到 event-time 的 map 来替他这个属性
eventTime: number,
lane: Lane,

tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,

next: Update<State> | null,
|};

Update 的数据结构可以简单了解到,Update 是用来描述当前的对象操作的一些基本信息:时间(eventTime)、优先级(lane)、回调函数(callback)、下一个更新对象(next)等

Update 对象创建后,会通过 enqueueUpdate 函数,挂载到 Fiber 实例的 updateQueue 队列中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export function enqueueUpdate<State>(
fiber: Fiber,
update: Update<State>,
lane: Lane,
) {
const updateQueue = fiber.updateQueue;
const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

if (isInterleavedUpdate(fiber, lane)) {
// ....
} else {
const pending = sharedQueue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
sharedQueue.pending = update;
}
}

UpdateQueue 的结构

1
2
3
4
5
6
7
export type UpdateQueue<State> = {|
baseState: State,
firstBaseUpdate: Update<State> | null,
lastBaseUpdate: Update<State> | null,
shared: SharedQueue<State>,
effects: Array<Update<State>> | null,
|};

谈到队列,普遍印象中的队列应该是先进先出,先进来排前面,按照进入的时间顺序排列

但是 React UpdateQueue 的结构比较,pending 指向的是最后一个 Update 对象,第一个进入 Update 对象排在第一个,后面依次按照进入的顺序排列

阅读全文 »

JSON Web Token 是什么

JSON Web Token 是基于开放标准(RFC 7519)定制的字符串,可以安全地把各方之间的信息作为 JSON 字符串传输

JSON Web Token 通过 RSA 或 ECDSA(具有HMAC算法),公共/私钥和消息体生成 JWT 签名,保证 JSON Web Token 的安全性和唯一性

JSON Web Token 定义为紧凑自包含的形式,可以做到无依赖校验和解析

JSON Web Token 的结构

JWT由三部分组成,每部分都通过 . 链接起来,大概长这样:xxx.yyy.zzz

每部分都经过 base64 编码

第一部分 header

header 原文是一个 JSON 字符串,其中包含两部分信息

  1. typ: ‘JWT’ token类型
  2. alg: ‘HS256’ 加密算法类型
1
2
3
4
{
alg: 'HS256',
typ: 'JWT',
}

然后,用Base64对这个JSON编码就得到JWT的第一部分

1
const headerBase64 = window.btoa(JSON.stringify({ alg: 'HS256', typ: 'JWT' }));

MDNS btoa

阅读全文 »

Scheduler 多任务时间管理大师

react 提出 Fiber 之前,复杂耗时任务会阻塞页面的渲染,降低页面的响应速度,为了缓解耗时任务与渲染等其他进程之间资源争夺的情况,react 增加了 scheduler 这一模块,通过 scheduler 更好的调度多任务,控制任务的执行顺序

scheduler 通过划分任务优先级, 时间切片, 任务中断、任务恢复等机制来保证高优任务的执行,只占用每一帧中尽可能短的时间用于处理任务,把主要资源在有必要的情况下交还给其他线程,以此提高页面响应速度,提升用户体验

能做到的这些不得不说是“多任务时间管理大师”了😁

此文基于 react v17.0.2 scheduler v0.20.0 分析,仓库传送门

React & Scheduler 缠缠绵绵

前面提到 scheduler 可以帮助 react 更好的去调度和控制多任务的执行,那么 react 是怎么把交给 scheduler 调度和控制执行的呢?

这里不对react到scheduler的流程做深入解读,仅用于交代从react到scheduler的流程过渡

话不多说,老规矩,我们先来看一个简单的 demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import * as React from "react";

class A extends React.PureComponent {
state = {
val: 1
};

onClickBtn = () => {
this.setState({ val: 2 });
this.setState({ val: 3 });
this.setState({ val: 4 });
setTimeout(() => {
this.setState({ val: 5 });
this.setState({ val: 6 });
});
};

render() {
const { val } = this.state;
return (
<p>
<button onClick={this.onClickBtn}>click</button>
{val}
</p>
);
}
}

export default function App() {
return (
<A />
);
}

案例的视图很简单,就只有一个按钮和一个值,当点击 click 时,会触发 setState,视图的值也会发生变化

阅读全文 »

terser-webpack-plugin 是什么

terser-webpack-plugin 内部封装了 terser 库,用于处理 js 的压缩和混淆,通过 webpack plugin 的方式对代码进行处理

terser-webpack-plugin 的使用方式也很简单

官方文档提供了一份通用的配置:terser 配置的使用和具体含义可以参考 minify-options

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
ecma: undefined,
parse: {},
compress: {},
mangle: true,
module: false,
output: null,
format: null,
toplevel: false,
nameCache: null,
ie8: false,
keep_classnames: undefined,
keep_fnames: false,
safari10: false,
},
}),
],
},
};

terser-webpack-plugin 执行机制

terser-webpack-plugin 本质是个 webpack-plugin,通过注册运行时的某个钩子,可以在合适的时间点对代码做压缩和混淆的优化

那么 terser-webpack-plugin 是在哪个钩子中做这件事的呢,我们先看看插件的 apply 函数

applywebpack-plugin 插件的初始化入口函数,terser-webpack-plugin 本身并不复杂且易读,简化后的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
apply(compiler) {
const { devtool, output, plugins } = compiler.options;
const pluginName = this.constructor.name;
// ... 中间做了一些别的处理
// 比如 sourceMap 处理,弱缓存的初始化 weakCache,terserOptions 的初始化

compiler.hooks.compilation.tap(pluginName, (compilation) => {
// ...
// 注册 optimizeChunkAssets 钩子,这个钩子对应了 webpack4 中 optimise 配置
compilation.hooks.optimizeChunkAssets.tapPromise(pluginName, (assets) =>
// 优化处理的真正入口
this.optimize(compiler, compilation, assets, CacheEngine, weakCache)
);
});
}

通过代码很容易了解到,terser-webpack-plugin 先通过 compilation 钩子获取到 compilercompilation 实例(这里不展开讨论 webpack-plugin 插件系统架构和 tapable 的设计,会在后面的篇幅中展开解读 webpack-plugin 插件系统架构 和 tapable v2

注册 webpack compilation 提供的 hooks: optimizeChunkAssets 运行时钩子,注册时使用了 tapPromise 异步注册的方式,注册的回调函数返回一个 Promise

webpack 执行 optimise 阶段,每个 chunk 都会触发这个异步钩子然后执行回调函数,回调函数本质是通过 optimise 执行代码的优化任务处理

可以理解为 terser-webpack-plugin 中的实际任务处理入口是 optimise 函数

apply流程

阅读全文 »

代码的执行

说到代码的执行,大家的第一印象应该是一行一行的代码,一个函数一个函数的在顺序执行,但是在怪异的JavaScript中也是如此吗,我们先来看几个案例:

案例1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log('foo');
}

foo();

function bar() {
console.log('bar');
}

bar();
// 结果
// foo
// bar

案例2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log('foo');
}

foo();

function foo() {
console.log('bar');
}

foo();
// 结果
// foo
// foo

案例3

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log('foo');
}

foo();

var foo = function() {
console.log('bar');
}

foo();
// 结果
// foo
// bar

我们先来看上述三个案例的结果,对于案例1的结果应该是毫无疑问的,但是案例2、3的却与常识不相符。

原因在于,js代码在执行之前,js引擎会对代码先做分析,分析过程中把代码划分以一段一段可以执行的代码,这一段一段的可执行代码我们统称为执行上下文。

执行上下文(Execution context,简称EC)

执行上下文类型

执行上下文一般分为3种

  • 全局执行上下文:全局上下文在浏览器中指的是window对象,也是this globalThis
  • 函数执行上下文:函数被调用的时候会被创建,调用函数时都会创建一个新的执行上下文。
  • eval执行上下文:通过eval运行的代码
阅读全文 »

本文将从这几个方面带大家去理解JavaScript中的浮点数问题

1、介绍JS的Number

2、0.1 + 0.2 为什么不等于 0.3

3、最大安全数为什么是 2^53 - 1

JS中的Number到底是什么

在深入问题之前,我们先来了解下JS的Number在二进制中是怎么存储的

双精度浮点数

JS中的Number是以双精度浮点数的形式计算的,双精度浮点数总共有8个字节(byte),每字节有8比特(bit-位),即 8bit/byet,所以总共占位64位。

根据IEEE754的标准,双精度浮点数中的占位分为3个部分

这三个部分组成这样一个公式

阅读全文 »

什么是 vue-loader

简单来说,vue-loader 是把 .Vue 文件编译成 .js,即可在浏览器中运行,也可以通过 vue-server-render 在 node 环境运行。

vue-loader 15

vue-loader 15 向较于过去的版本,有许多重要的改动,这些改动体现在:

  1. loader推导策略变化
  2. 独立出 VueLoaderPlugin
  3. …等等

更多细节可以查阅官方迁移指南:Vue Loader 迁移

vue-loader 编译过程

vue-loader 的处理流程可以大致分为几个部分

  1. 入口函数解析 .vue 文件
  2. parse 解析 .vue 文件,生成包含不同模块的 descriptor
  3. 根据不同模块做 loader 推导
  4. VueLoaderPlugin 处理

vue-loader 入口函数

vue-loader 入口代码不多,我把入口函数的流程做成了一个简单的 UML 图,通过图也能快速对流程有个初步的印象

阅读全文 »

如何理解响应式

「响应式数据」的特性是数据改变时自动更新视图,当数据变化后,自动通知到对应的观察者,更新操作函数

Vue的响应式则用到了 Object.defineProperty,可能很多小伙伴之前都了解过,在分析 Vue 的响应式原理之前,我们先简单介绍一下
Object.defineProperty 的用法

Object.defineProperty

该方法允许精确地添加或修改对象的属性。

通过设置 get/set 属性,可以代理属性的获取和写入操作,这两个是实现 Vue 响应式的关键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const object1 = {};

Object.defineProperty(object1, 'property1', {
// 赋值
set(val) {
console.log('set', val);
this.value = val;
},
get() {
console.log('get');
return this.value || 1;
},
// value: 1,
});

// object1.property1 = 77;

console.log(object1.property1);
object1.property1 = 2;
console.log(object1.property1);

此外,defineProperty 可以实现控制属性的可覆写、可枚举、可配置等操作

1
2
3
4
5
6
7
8
9
10
11
12
13
const object1 = {};

Object.defineProperty(object1, 'property1', {
// 赋值
value: 42,
// 不可覆写
writable: false
});

object1.property1 = 77;
// 严格模式下抛出错误,不可覆写

console.log(object1.property1); // 42

简单了解 defineProperty 后我们开始进入正题

Vue响应式

reactive1

这是官方的响应式流程图,我们今天需要重点关注的地方有几个

  1. Data 模块是如何转换成响应式数据结构?
  2. getter 是如何收集依赖?依赖具体是什么?
  3. setter 如何通知 Watcher?
  4. Watcher 是什么?Watcher 的作用是什么?
  5. Data 和 Watcher 的关系是什么?
  6. Watcher 是如何触发组件 render?
阅读全文 »

响应式原理 提到过数据更新时会把 watcher 推入 queue 队列,在下一个 tick 中遍历逐个执行

这次我们从一个 demo 开始一探究竟,从数据更新到 nextTick 的过程

一个数据更新的demo

先来看一个常见的案例,点击click后会循环1000次,每次都给this.number+1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<div>
<div>{{number}}</div>
<div @click="onClick">click</div>
</div>
</template>
export default {
data () {
return {
number: 0,
};
},
methods: {
onClick () {
for(let i = 0; i < 1000; i++) {
this.number += 1;
}
},
},
}

按照我们对vue响应式的理解,number变化后会触发setter函数,进而触发Dept.notify,最后通过Watcher.update()来更新视图,循环1000次会导致视图的更新1000次。

但是连续更新1000次会造成不必要的视图更新,频繁操作DOM的效率也非常低,为了减少布局和渲染,Vue把DOM更新设计为异步更新,每次侦听到数据变化,将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。然后在下一个的事件循环tick中,Vue才会真正执行队列中的数据变更,然后页面才会重新渲染。相当于把多个地方的DOM更新放到一个地方一次性全部更新。

回顾响应式原理

在了解了响应式原理后,大家应该都清楚触发 setter 后会通知 watcher 需要更新,这之后的过程是我们这次关注的重点

对应图中触发 dep.notify() 后到 watcher.run() 执行的部分

更新队列 queue

为了避免频繁操作DOM,造成不必要的视图更新,Vue在数据发生变化时,触发setter方法后,setter会把Watcher push到队列queue中。

为此,Vue提供了异步更新的监听接口 —— Vue.nextTick(callback) 或 this.$nextTick(callback) 。当数据发生改变,异步DOM更新完成后,callback回调将被调用。开发者可以在回调中,操作更新后的DOM。

通知的关键在与 Watcher 中的 update 函数,update 实际最终会调用 queueWatcher 把当前 watcher 推入 queue 队列中

阅读全文 »