React原理 执行流程与架构

在 react v15 之前成为 stack 架构,从 v16 开始重构了整个架构,新的架构叫做 fiber 架构,最大的改变就是可以进行时间切片。

  • cpu 瓶颈如果主进程存在大量的计算任务,会阻塞后续的任务,同样也会影响浏览器的绘制任务,从而造成掉帧,用户交互无响应。
    在 v15 的版本当嵌套组件过多时,虚拟 DOM 会递归执行,导致执行时间过长。

  • io 瓶颈,对前端来讲 io 瓶颈主要来自于网络,对于 React 所有的更新都来自于内部状态的变化,所以 react 将触发状态变化的事件分为不同的优先级,统一调度这些任务,挡在更新过程中有更高优先级的任务产生,需要中断当前任务,处理更高优先级的任务。

因此需要 React 实现任务调度算法,可以中断的 dom 更新,任务分级的机制, 以下是 React 升级的关键设计思想:

  • 在 UI 中,不需要每次更新都立即应用;事实上,这样做可能会造成浪费,导致帧丢失并降低用户体验。
  • 不同类型的更新有不同的优先级——动画更新需要比数据存储更新更快地完成。
  • react 使用的拉的模式,他会自己安排更新工作,而不需要让程序员推送任务执行的方式。

fiber 相当于代替了之前版本的调用栈信息, 一个 fiber 相当于一个调用栈帧,生命周期比调用栈长,可以保存在内存中,在需要的时候调用。

为什么在 fiber 架构之前无法中断更新,假设一个列表有 4 个 div 每个 div 中的文字是 2,现在触发一个事件需要将列表中的每个文字更新为 4, stack reconciler 递归的执行每个更新,在更新到第三个元素的时候,如果想要中断了更新那么会让出主进程允许浏览器更新 UI,这就会造成只有三个元素显示了正确的 UI, 造成 UI 和状态不一致。

fiber 架构的第一个阶段是 render/reconciliation, 他会解析当前 fiber 链接起来的树,弄清楚哪些需要更新,这个过程是可以中断的,react 从根节点向下递归,标记那些已经被删除或是更新的节点,并在每个节点处理过后检查是否还剩余处理时间,如果时间不足会中断当前处理让出主线程,当主线程任务结束后会从中断的位置继续处理剩余的节点,所有节点处理完成后,会递归向上回到根节点,将所有需要更新和修改的节点作为一个副作用链表一直连接到 root fiber。

第二个阶段是提交 commit 阶段,它会将识别到的更改应用到元素上,这一过程是不可以中断的。

优先级:

  • Synchronous 同步相当于 stack reconciler
  • task 在下一次事件循环前
  • animation 在下一次渲染之前
  • High 即使处理
  • Low 网络请求等
  • Offscreen

整体执行流程

初始化事件相关对象

  • registerSimpleEvents 创建对象相关对象
变量名称 变量对象 说明
allNativeEvents Set 集合
保存所有原生事件的名称 例如 0:"cancel"
eventPriorities Map 集
保存事件名称和事件优先级对应关系 例如 click=>0
topLevelEventsToReactNames Map 集
保存原始事件名称和 React 事件的对应关系 例如 "cancel" => "onCancel"
registrationNameDependencies Object
保存 React 事件和原生事件的对应关系 例如 onClick:(1) ['click'] 每个 React 事件对应一个数组用于保存合成事件对应关系
possibleRegistrationNames Object
保存小写的 React 事件名称和正确的驼峰命名事件的对应关系,用于校验用户输入 例如 onclick:onClick

入口

render : ReactDom.render()
createRootImpl : 创建 FiberRootNode 根节点
listenToAllSupportedEvents : 绑定所有原生事件在 root 节点上

render 阶段

unbatchedUpdates : 非批量更新,让用户尽早看见页面内容,如果是 batchedUpdates 会以异步执行
scheduleUpdateOnFiber : 调度 Fiber 节点更新优先级
performUnitOfWork : 以 Fiber 节点为单位,深度优先递归遍历每一个节点
reconcileChildren : 创建对比 Fiber 节点,标记有副作用的节点 (添加,删除,移动,更新)
completeUnitOfWork : 从下至上遍历节点,创建相应的 DOM 节点,并创建 Effects 链表,交给 commit 阶段使用

commit 阶段

commitBeforeMutationEffects: 操作真实节点前执行,会执行getSnapshotBeforeUpdate
commitMutationEffects: 执行节点操作
commitLayoutEffects: 执行副作用函数,包括 componentDidUpdateeffect回调函数

JSX

jsx 是 js 语言的扩展,react 通过 babel 词法解析,将 jsx 转换成 React.createElement,React.createElement 方法返回 virtual-dom 对象(内存中用来描述 dom 阶段的对象),所有 jsx 本质上就是 React.createElement 的语法糖,它能声明式的编写我们想要组件呈现出什么样的 ui 效果.

Fiber 双缓存

Fiber 对象上面保存了包括这个节点的属性、类型、dom 等,Fiber 通过 child、sibling、return(指向父节点)来形成 Fiber 树,还保存了更新状态时用于计算 state 的 updateQueue,updateQueue 是一种链表结构,上面可能存在多个未计算的 update,update 也是一种数据结构,上面包含了更新的数据、优先级等,除了这些之外,上面还有和副作用有关的信息。

双缓存是指存在两颗 Fiber 树,current Fiber 树描述了当前呈现的 dom 树,workInProgress Fiber 是正在更新的 Fiber 树,这两颗 Fiber 树都是在内存中运行的,在 workInProgress Fiber 构建完成之后会将它作为 current Fiber 应用到 dom 上

在 mount 时(首次渲染),会根据 jsx 对象(Class Component 或的 render 函数者 Function Component 的返回值),构建 Fiber 对象,形成 Fiber 树,然后这颗 Fiber 树会作为 current Fiber 应用到真实 dom 上,在 update(状态更新时如 setState)的时候,会根据状态变更后的 jsx 对象和 current Fiber 做对比形成新的 workInProgress Fiber,然后 workInProgress Fiber 切换成 current Fiber 应用到真实 dom 就达到了更新的目的,而这一切都是在内存中发生的,从而减少了对 dom 好性能的操作。

Lane 模型

react 之前的版本用 expirationTime 属性代表优先级,该优先级和 IO 不能很好的搭配工作(io 的优先级高于 cpu 的优先级),现在有了更加细粒度的优先级表示方法 Lane,Lane 用二进制位表示优先级,二进制中的 1 表示位置,同一个二进制数可以有多个相同优先级的位,这就可以表示‘批’的概念,而且二进制方便计算。

这好比赛车比赛,在比赛开始的时候会分配一个赛道,比赛开始之后大家都会抢内圈的赛道(react 中就是抢优先级高的 Lane),比赛的尾声,最后一名赛车如果落后了很多,它也会跑到内圈的赛道,最后到达目的地(对应 react 中就是饥饿问题,低优先级的任务如果被高优先级的任务一直打断,到了它的过期时间,它也会变成高优先级)

Lane 的二进制位如下,1 的 bits 越多,优先级越低

1
2
3
4
5
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = /* */ 0b0000000000000000000000000000010;

Scheduler

Scheduler 的作用是调度任务,react15 没有 Scheduler 这部分,所以所有任务没有优先级,也不能中断,只能同步执行。

我们知道了要实现异步可中断的更新,需要浏览器指定一个时间,如果没有时间剩余了就需要暂停任务,requestIdleCallback 貌似是个不错的选择,但是它存在兼容和触发不稳定的原因,react17 中采用 MessageChannel 来实现。

在 Scheduler 中的每的每个任务的优先级使用过期时间表示的,如果一个任务的过期时间离现在很近,说明它马上就要过期了,优先级很高,如果过期时间很长,那它的优先级就低,没有过期的任务存放在 timerQueue 中,过期的任务存放在 taskQueue 中,timerQueue 和 timerQueue 都是小顶堆,所以 peek 取出来的都是离现在时间最近也就是优先级最高的那个任务,然后优先执行它。

reconciler

Reconciler 发生在 render 阶段,render 阶段会分别为节点执行 beginWork 和 completeWork,或者计算 state,对比节点的差异,为节点赋值相应的 effectFlags(对应 dom 节点的增删改)。

协调器是在 render 阶段工作的,简单一句话概括就是 Reconciler 会创建或者更新 Fiber 节点。在 mount 的时候会根据 jsx 生成 Fiber 对象,在 update 的时候会根据最新的 state 形成的 jsx 对象和 current Fiber 树对比构建 workInProgress Fiber 树,这个对比的过程就是 diff 算法。

diff 算法发生在 render 阶段的 reconcileChildFibers 函数中,diff 算法分为单节点的 diff 和多节点的 diff(例如一个节点中包含多个子节点就属于多节点的 diff),单节点会根据节点的 key 和 type,props 等来判断节点是复用还是直接新创建节点,多节点 diff 会涉及节点的增删和节点位置的变化。

reconcile 时会在这些 Fiber 上打上 Flags 标签,在 commit 阶段把这些标签应用到真实 dom 上,这些标签代表节点的增删改,如

1
2
export const Placement = /*             */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;

render 阶段遍历 Fiber 树类似 dfs 的过程,处理发生在 beginWork 函数中,该函数做的主要工作是创建 Fiber 节点,计算 state 和 diff 算法,‘冒泡’阶段发生在 completeWork 中,该函数主要是做一些收尾工作,例如处理节点的 props、和形成一条 effectList 的链表,该链表是被标记了更新的节点形成的链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
const [count, setCount] = useState(0);
return (
<>
<h1
onClick={() => {
setCount(() => count + 1);
}}
>
<p title={count}>{count}</p> hello
</h1>
</>
);
}

如果 p 和 h1 节点更新了则 effectList 如下,从 rootFiber->h1->p,,顺便说下 fiberRoot 是整个项目的根节点,只存在一个,rootFiber 是应用的根节点,可能存在多个。

React原理 事件系统

创建事件集合

React会在初始化的时候生成事件系统所需要的事件对象集合。

  • 把系统事件按照优先级分为三大类,每种事件对应着一个优先级
事件类别 变量名称 说明 优先级 举例
独立事件 discreteEventPairsForSimpleEventPlugin DiscreteEvent=0 例如: click
用户阻塞事件 userBlockingPairsForSimpleEventPlugin
因为事件持触发,所以如果某段逻辑执行之间过长,会影响用户的交互
UserBlockingEvent=1 例如:drag
连续事件 continuousPairsForSimpleEventPlugin 需要系统一直监听是否触发的事件 ContinuousEvent=2 例如: animationEnd
  • 通过循环每种事件对应的数组,将事件保存在一下几种事件对象中
变量名称 变量对象 说明
allNativeEvents Set集合
保存所有原生事件的名称 例如 0:"cancel"
eventPriorities Map集
保存事件名称和事件优先级对应关系 例如 click=>0
topLevelEventsToReactNames Map集
保存原始事件名称和 React事件的对应关系 例如 "cancel" => "onCancel"
registrationNameDependencies Object
保存React事件和原生事件的对应关系 例如 onClick:(1) ['click'] 每个React事件对应一个数组用于保存合成事件对应关系
possibleRegistrationNames Object
保存小写的React事件名称和正确的驼峰命名事件的对应关系,用于校验用户输入 例如 onclick:onClick
  • 注册合成事件

合成事件: 某些React事件会对应多个原生事件 例如:

1
'onChange' => ['change', 'click', 'focusin', 'focusout', 'input', 'keydown', 'keyup', 'selectionchange']

合成事件会维护在registrationNameDependencies变量中,例如 onChange事件最终会变为 ``

创建事件对象

事件对象用于不用的事件类型,当React事件触发时,将会传入对应的事件对象,而不是原生的事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createSyntheticEvent(Interface) {
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
_assign(SyntheticBaseEvent.prototype, {
preventDefault: function () {},
stopPropagation: function () {this.isPropagationStopped = functionThatReturnsTrue;},
persist: function () {},
isPersistent: functionThatReturnsTrue
});

return SyntheticBaseEvent;
}
var UIEventInterface = _assign({}, EventInterface, {
view: 0,
detail: 0
});

var SyntheticUIEvent = createSyntheticEvent(UIEventInterface);

事件绑定

React 创建 FiberRoot 根节点阶段, 会循环所有的原声事件,将事件绑定在 root 元素上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function listenToAllSupportedEvents(rootContainerElement) {
{
var listeningMarker = '_reactListening' + Math.random().toString(36).slice(2);
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach(function (domEventName) {
// 没有事件委托的事件,也就是不能冒泡到document的事件
if (!nonDelegatedEvents.has(domEventName)) {
listenToNativeEvent(domEventName, false, rootContainerElement, null);
}

listenToNativeEvent(domEventName, true, rootContainerElement, null);
});
}
}

根据事件的优先级不同,事件类型不同,绑定的事件处理函数也会不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriorityForPluginSystem(domEventName);
var listenerWrapper;

switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;

case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;

case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}

return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
  • 对于独立事件,如果更新时存在副作用,会在下一个事件前立即执行
1
2
3
4
function flushDiscreteUpdates() {
flushPendingDiscreteUpdates();
flushPassiveEffects();
}

对于用户阻塞事件,通过原生事件的targetsrcElement 获取到触发事件的元素,再通过定义在原生DOM上的属性 internalInstanceKey 找到DOM对应的Fiber节点

1
var targetInst = targetNode[internalInstanceKey];

通过 batchedEventUpdates 标记批处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function batchedEventUpdates(fn, a, b) {
if (isBatchingEventUpdates) {
// If we are currently inside another batch, we need to wait until it
// fully completes before restoring state.
return fn(a, b);
}

isBatchingEventUpdates = true;

try {
return batchedEventUpdatesImpl(fn, a, b);
} finally {
isBatchingEventUpdates = false;
finishEventHandler();
}
}

为不同的事件类型选择不同的事件对象,通过FiberNode上的props属性获取到事件,加入事件队列,
用队列来模拟冒泡事件和捕获事件

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
switch (domEventName) {
case 'click':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return;
}

case 'auxclick':
case 'dblclick':
case 'mousedown':
case 'mousemove':
case 'mouseup': // TODO: Disabled elements should not respond to mouse events

/* falls through */

case 'mouseout':
case 'mouseover':
case 'contextmenu':
SyntheticEventCtor = SyntheticMouseEvent;
break;
}
while (instance !== null) {
var _instance2 = instance,
stateNode = _instance2.stateNode,
tag = _instance2.tag; // Handle listeners that are on HostComponents (i.e. <div>)

if (reactEventName !== null) {
var props = getFiberCurrentPropsFromNode(stateNode);
var listener = props[registrationName];

if (captureListener != null) {
listeners.unshift(createDispatchListener(instance, captureListener, currentTarget));
}

var bubbleListener = getListener(instance, reactName);

if (bubbleListener != null) {
listeners.push(createDispatchListener(instance, bubbleListener, currentTarget));
}
}
instance = instance.return;
}


dispatchQueue.push({
event: _event,
listeners: _listeners
});

循环事件队列,如果执行过stopPropagation 直接跳出循环,方法定义在上面的事件对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {gfdg
var previousInstance;

for (var i = dispatchListeners.length - 1; i >= 0; i--) {
var _dispatchListeners$i = dispatchListeners[i],
instance = _dispatchListeners$i.instance,
currentTarget = _dispatchListeners$i.currentTarget,
listener = _dispatchListeners$i.listener;

if (instance !== previousInstance && event.isPropagationStopped()) {
return;
}

executeDispatch(event, listener, currentTarget);
previousInstance = instance;
}
}

FAQ

  • React 为什么有自己的事件系统?

为了抹平浏览器间差异,封装了事件对象,模拟了事件冒泡和捕获。

  • 什么是事件合成?

一个React可能是一个事件,也可能是多个原生事件。而且事件对象也是封装过的对象。

  • 如何实现的批量更新?

通过 isBatchingEventUpdates 标记批处理开始, 一次用户阻塞事件中的所有相同类型事件,都会合并成一次更新

  • 事件系统如何模拟冒泡和捕获阶段?

通过队列,冒泡事件插入在队列尾部,捕获事件插入队列的头部

  • 如何通过 dom 元素找到与之匹配的fiber?

通过原生事件对象获取触发事件的DOM元素,通过DOM元素上的内置属性,获取fiber节点

  • 阻止默认行为 ?

原生事件: e.preventDefault() 和 return false 可以用来阻止事件默认行为,由于在 React 中给元素的事件并不是真正的事件处理函数。所以导致 return false 方法在 React 应用中完全失去了作用。

React事件 在React应用中,可以用 e.preventDefault() 阻止事件默认行为,这个方法并非是原生事件的 preventDefault ,由于 React 事件源 e 也是独立组建的,所以 preventDefault 也是单独处理的。

  • 事件是绑定在真实的dom上吗?如何不是绑定在哪里?

绑定在root节点上

③ ReactSSR 实现路由

整理文件结构

最后的文件结构会变为下面的结构,有一些文件需要我们去继续完善

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
src/
├── app koa服务器相关文件
│ ├── errorHandle.ts
│ └── index.ts
├── components 组件文件夹
│ ├── header
│ │ ├── header.tsx (新增)
│ │ └── index.tsx (新增)
│ ├── hello
│ │ ├── hello.tsx
│ │ └── index.tsx
│ ├── index.tsx 组件的入口
│ ├── login
│ │ ├── index.tsx
│ │ └── login.tsx
│ ├── router.tsx 客户端路由 (新增)
│ └── routes.tsx 路由子项,可以和服务端渲染公用 (新增)
├── config
│ └── default.config.ts
├── router
│ ├── index.tsx koa的路由配置
│ └── router.tsx 服务端路由 (新增)
├── server.ts koa服务器入口文件
└── util
└── errorTypes.ts

客户端添加路由

添加一个简单的<Header/>组件,使用<Link/>组件添加路由跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import {
Link
} from "react-router-dom";

const Header:React.FC = () =>{
return <div>
<Link to="/">hello</Link>
<Link to="/login">login</Link>
</div>
}

export default Header;

添加 src/routes.ts 提取路由公用部分

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
import React from "react";
import {
Switch,
Route,
} from "react-router-dom";


import Hello from "./hello";
import Login from "./login";

const Routes = () => {
return (
<Switch>
<Route path="/hello">
<Hello />
</Route>
<Route path="/login">
<Login />
</Route>
<Route path="/">
<Hello />
</Route>
</Switch>
)
}
export default Routes;

添加 src/router.ts 客户端路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from "react";
import Koa from 'koa';
import {
BrowserRouter,
} from "react-router-dom";

import Routes from './routes'

const Router:React.FC<unknown> = () =>{
return (
<BrowserRouter>
<Routes />
</BrowserRouter>
)
}

export default Router;

src/index.ts 最终导出App

1
2
3
4
5
6
7
8
9
10
import ReactDom from 'react-dom';
import React from 'react';
import Router from './router';

const App = () => {

return <Router />
}

ReactDom.hydrate(<App/>, document.getElementById('root'));

服务端路由

添加 router/touter.ts

因为服务端并不能感知到路由的变化,所以需要手动传递路由

当客户端渲染了一个 <Redirect> 浏览器历史改变并显示了新的屏幕,在服务端不能改变App的状态,所以使用context拿到渲染的结果,如果可以拿到context.url,就可以知道重定向的结果。可以让我们在服务中发起重定向.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from "react";
import Koa from 'koa';
import {
StaticRouter,
} from "react-router-dom";

import Routes from '../components/routes'

const context={};

const Router:React.FC<unknown> = ({ctx}) =>{
return (
<StaticRouter location={ctx.url} context={context}>
<Routes />
</StaticRouter>
)
}

export default Router;

router/index.tsx

需要添加root元素,并匹配所有路径

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
import Router from '@koa/router';
import ReactDOMServer from 'react-dom/server';
import React from 'react';
import App from './router';
const router = new Router();

router.get("/(.*)",async (ctx,next)=>{
console.log(ctx);
ctx.body=`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id='root'>${ReactDOMServer.renderToString(<App {...{ctx}}/>)}</div>
<script src='/index.js'></script>
</body>
</html>
`
})

export default router;

② ReactSSR 最简单的同构

什么是同构

简单说同构就是前后端公用一套代码。

首先需要明确的是同构是SSR的一种实现方式,同构也是通过服务端渲染,将生成的HTML返回给浏览器展示,但是在React组件中绑定的事件会丢失,因为同构只返回了HTML字符串,并没有返回JavaScript文件。

所以可以把组件打包成JavaScript文件浏览器中在执行一次,用于绑定交互事件。真实的场景中还会有路由,和数据状态管理工具(redux等)的同步。

打包的组件也用于在服务端生成HTML字符串,它们公用一套代码这也是同构的本质。

打包组件

把组件打包成一个单独的文件,放在static静态资源目录下面,当浏览器访问的时候会从静态资源文件夹读取文件

webpack.config.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import * as path from 'path';
import * as webpack from 'webpack';

const config: webpack.Configuration = {
entry:'./src/components/hello.tsx',
output:{
filename:'index.js',
path:path.resolve(__dirname,'static')
},
module:{
rules:[
{
test:/\.tsx?$/,
use:['babel-loader']
}
]
},
resolve: {
extensions: ['.tsx', '.ts', '.js'],
},
mode:'development',
}
export default config;

静态资源访问

koa提供了一个中间件 koa-static 用于静态资源访问

注意路径是否正确,如果是在打包好的文件夹中执行,可以通过PWD获取Node进程执行时候的位置

static 用于指定静态资源文件夹的名称,如果想访问static中的文件不需要在路径中添加static, 例如静态资源路径为 static/js/index.js,script 标签中的src路径为 /js/index.js

最后在返回的HTML中添加打包好的JS文件

src/server.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import koaStatic from 'koa-static';

router.get('/',async (ctx,next)=>{
ctx.body=`
<html lang="en">
<body>
${ReactDOMServer.renderToString(<Home />)}
<script src='/index.js'></script>
</body>
</html>
`
})

app
.use(koaStatic(path.join((process.env as any).PWD,'./static')))
.use(router.routes())
.use(router.allowedMethods())

app.listen(conf.PORT,()=>{
`server is running in port 3000`
});

点击无效? 请把组件放在root中

按照上面的步骤已经可以在页面中加载 index.js 的文件,但是点击事件并没有生效,这是因为还没有将组件插入到root节点中

虽然服务端通过HTML直接返回给浏览器可以展示组件,但是在浏览器中执行React打包好的文件的时候需要将节点挂载在元素上,通常为 <div id='root'></div>

所以尝试添加这样的逻辑

src/components/hello.ts

1
2
3
4
5
6
7
8
import React from 'react';

const Hello:React.FC = () =>{
return <div onClick={()=>{alert("hello")}}>Hello</div>
}

ReactDom.render(<Hello/>, document.getElementById('root'));
export default Hello;

很不幸会提示一个错误,document没有定义,因为这个组件用于生成HTML字符串,当他被执行的时候node环境中并没有document 这个全局变量

所以我们把他放到一个新的文件中

src/component/index.ts

1
2
3
4
5
import ReactDom from 'react-dom';
import React from 'react';
import Home from './home'

ReactDom.render(<Home/>, document.getElementById('root'));

这时没有再报错,但是收到了一个警告

Warning: render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v18. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML

需要把ReactDOM.render() 替换为 ReactDOM.hydrate()

1
ReactDom.hydrate(<Home/>, document.getElementById('root'));

① ReactSSR 渲染一个简单组件

客户端渲染 vs 服务端渲染

SSR:服务端渲染(Server side render)

优点:① 利于 SEO ②TTFP 首屏渲染时间比较快

缺点:① 复杂度增加 ② 服务器消耗资源增大,需要处理 IO 和执行 JavaScript

&nbsp;

CSR:客户端渲染(Client side render)

优点:① 前后端分离,加快开发效率

缺点:①TTFP 首屏渲染时间比较长 ② 不能 SEO

由服务端返回页面

万事开头难,现实服务器返回静态页面。

使用 Koa 搭建一个 Node 服务器,并在访问根路径的时候返回静态页面。

src/server.js

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
import Koa from "koa";
import Router from "@koa/router";

const router = new Router();
const app = new Koa();

router.get("/", async (ctx, next) => {
ctx.body = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
Hello
</body>
</html>`;
});

app.use(router.routes()).use(router.allowedMethods());

app.listen(conf.PORT, () => {
`server is running in port 3000`;
});
使用 ES6 语法

你也可能注意到这里使用了 ES6 的语法,因此需要添加@babel/register这个工具,帮助我们在执行 ES6 的 Node 文件时即时编译。

添加 src/start.js 入口文件

1
2
require("@babel/register");
module.exports = require("./server");

添加.babelrc 配置文件

需要添加下面几个依赖,注意区分依赖环境

1
2
3
4
5
6
yarn add @babel/runtime

yarn add core-js@3

# 用于定义一些编译后的依赖函数
yarn add -D @babel/plugin-transform-runtime
1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "entry",
"corejs": "3"
}
]
],
"plugins": ["@babel/plugin-transform-runtime"]
}

最后使用 node src/start.js 执行入口文件

使用 TS

添加 tsconfig.json 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"compilerOptions": {
/* TypeScript文件编译后生成的javascript文件里的语法应该遵循哪个JavaScript的版本 "ES5", "ES6"/ "ES2015", "ES2016", "ES2017"或 "ESNext"*/
"target": "es6",
"jsx": "react",
/* 编译后生成的javascript文件中的module,采用何种方式实现,可选项为:"None", "CommonJS", "AMD", "System", "UMD", "ES6"或 "ES2015"。 */
"module": "commonjs",
/*采用何种方式解析(也就是查找)TypeScript文件中依赖的模块的位置*/
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"sourceMap": true /* Create source map files for emitted JavaScript files. */,
"outDir": "dist" /* Specify an output folder for all emitted files. */,
"allowSyntheticDefaultImports": true /* Allow 'import x from y' when a module doesn't have a default export. */,
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This */,
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true
},
"include": ["src/**/*", "global.d.ts"],
"exclude": ["node_modules", "**/*.spec.ts"]
}

添加 React 组件

我们想把 Hello 换成 React 组件试一试,新建 components 文件创建<Hello/>组件并导出

1
2
3
4
5
6
7
import Hello from "react";

const Home: React.FC = () => {
return <div>Hello</div>;
};

export default Hello;
1
2
3
4
5
6
7
import Koa from "koa";
import Home from "../components/home";

const router = new Router();
router.get("/", async (ctx, next) => {
ctx.body = <Home />;
});

再一次运行的时候遇到了问题

因为现在并不能编译 JSX 语法需要添加一些配置。

如果使用的babel-register,安装依赖后,可以修改.babelrc 文件添加对 react 的支持

1
2
3
{
"presets": ["@babel/preset-react"]
}

如果使用的是 TS,添加在tsconfig.json中添加 jsx 支持

1
"jsx": "react",

现在服务器可以正常启动了,但是我们的组件还不能正常加载

这是因为我们的组件只被解析为 React.Element 而不是字符串,所以我们需要使用 ReactDOM 提供的服务端渲染的方法,将 React.Element 转换为字符串

1
2
3
4
import ReactDOMServer from 'react-dom/server';
router.get('/',async (ctx,next)=>{
ctx.body= ReactDOMServer.renderToString(<Home />;
})

现在一个最简单的组件就通过服务端渲染并返回给浏览器。

React原理 海量数据处理

requestIdleCallback

图中是浏览器每一帧的生命周期,requestIdleCallback则会在某一帧结束后的空闲时间或者用户处于不活跃状态时,处理我们的工作。

靠自己人工的安排不必要的工作是很困难的。比如,要弄清楚一帧剩余的时间,这显然是不可能的,因为当requestAnimationFrame的回调完成后,还要进行样式的计算,布局,渲染以及浏览器内部的工作等等。为了确保用户不以某种方式进行交互,你需要为各种交互行为添加监听事件(scroll、touch、click),即使你并不需要这些功能,只有这样才能绝对确保用户没有进行交互。另一方面,浏览器能够确切地知道在一帧的结束时有多少的可用时间,如果用户正在交互,通过使用requestIdleCallback这个API,允许我们尽可能高效地利用任何的空闲时间。

当 myNonEssentialWork 被调用,会返回一个 deadline 对象,这个对象包含一个方法,该方法会返回一个数字表示你的工作还能执行多长时间:

1
2
3
4
requestIdleCallback(function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0)
doWorkIfNeeded();
})

调用 timeRemaining 这个方法能获得最后的剩余时间,当 timeRemaining() 返回0,如果你仍有其他任务需要执行,可以在下一次空闲时间继续执行

1
2
3
4
5
6
7
function myNonEssentialWork (deadline) {
while (deadline.timeRemaining() > 0 && tasks.length > 0)
doWorkIfNeeded();

if (tasks.length > 0)
requestIdleCallback(myNonEssentialWork);
}

如果任务耗时太长,可能回调函数永远不能执行,requestIdleCallback有一个可选的第二个参数:含有timeout属性的对象。如果设置了timeout这个值,回调函数还没被调用的话,则浏览器必须在设置的这个毫秒数时,去强制调用对应的回调函数。

如果你的回调函数是因为设置的这个timeout而触发的,你会注意到:

timeRemaining()会返回0
deadline对象的didTimeout属性值是true

注意事项
  • 对非高优先级的任务使用空闲回调。 已经创建了多少回调,用户系统的繁忙程度,你的回调多久会执行一次(除非你指定了 timeout),这些都是未知的。不能保证每次事件循环(甚至每次屏幕更新)后都能执行空闲回调;如果事件循环用尽了所有可用时间,那可能你的任务永远不能执行。在你需要的时候要用 timeout,但记得只在需要的时候才用。 使用 timeout可以保证你的代码按时执行,但是在剩余时间不足以强制执行你的代码的同时保证浏览器的性能表现的情况下,timeout就会造成延迟或者动画不流畅。设置了timeout,如果回调中的任务也是个长时间任务,可能会导致影响用户交互。最好的办是确认任务足够小。

  • 空闲回调应尽可能不超支分配到的时间。尽管即使你超出了规定的时间上限,通常来说浏览器、代码、网页也能继续正常运行,这里的时间限制是用来保证系统能留有足够的时间去完成当前的事件循环然后进入下一个循环,而不会导致其他代码卡顿或动画效果延迟。目前,timeRemaining() 有一个50 ms 的上限时间,但实际上你能用的时间比这个少,因为在复杂的页面中事件循环可能已经花费了其中的一部分,浏览器的扩展插件也需要处理时间,等等。

  • 避免在空闲回调中改变 DOM。 空闲回调执行的时候,当前帧已经结束绘制了,所有布局的更新和计算也已经完成。如果你做的改变影响了布局, 你可能会强制停止浏览器并重新计算,而从另一方面来看,这是不必要的。 如果你的回调需要改变DOM,它应该使用Window.requestAnimationFrame()来调度它。

  • 避免运行时间无法预测的任务。 你的空闲回调必须避免做任何占用时间不可预测的事情。比如说,应该避免做任何会影响页面布局的事情。你也必须避免 执行Promise (en-US) 的resolve和reject,因为这会在你的回调函数返回后立即引用Promise对象对resolve和reject的处理程序。可能导致任务阻塞。

回退兼容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
window.requestIdleCallback = window.requestIdleCallback || function(handler) {
let startTime = Date.now();

return setTimeout(function() {
handler({
didTimeout: false,
timeRemaining: function() {
return Math.max(0, 50.0 - (Date.now() - startTime));
}
});
}, 1);
}

window.cancelIdleCallback = window.cancelIdleCallback || function(id) {
clearTimeout(id);
}
上报数据

有时我们希望,避免在用户交互行为发生的时候立即上报数据,思路就是将上报信息添加到队列中,在空闲的时间处理

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
var eventsToSend = [];

function onNavOpenClick () {
eventsToSend.push(
{
category: 'button',
action: 'click',
label: 'nav',
value: 'open'
});

schedulePendingEvents();
}

function schedulePendingEvents() {

// 如果已经在调度中则返回
if (isRequestIdleCallbackScheduled)
return;

isRequestIdleCallbackScheduled = true;

if ('requestIdleCallback' in window) {
// 最晚两秒之后上报数据
requestIdleCallback(processPendingAnalyticsEvents, { timeout: 2000 });
} else {
processPendingAnalyticsEvents();
}
}

function processPendingAnalyticsEvents (deadline) {

// 重置状态
isRequestIdleCallbackScheduled = false;

// 针对没有实现 requestIdleCallback 的环境
if (typeof deadline === 'undefined')
deadline = { timeRemaining: function () { return Number.MAX_VALUE } };

while (deadline.timeRemaining() > 0 && eventsToSend.length > 0) {
var evt = eventsToSend.pop();

ga('send', 'event',
evt.category,
evt.action,
evt.label,
evt.value);
}

// 如果还有任务在下次空闲时发送
if (eventsToSend.length > 0)
schedulePendingEvents();
}

渲染分片

对于大量数据渲染,可以考虑利用浏览器空闲时间分片处理

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
const generateColor = () => {
const r = Math.floor(Math.random()*255);
const g = Math.floor(Math.random()*255);
const b = Math.floor(Math.random()*255);
return 'rgba('+ r +','+ g +','+ b +',0.8)';
}
const getRandomPos = (width,height,distance = 0) => ({
x: distance/2 + (width - distance/2) * Math.random(),
y: distance/2 + (height - distance/2) * Math.random(),
})
const Circle = ({position,color}) =>{
return <div style={{
width:10,
height:10,
borderRadius:"50%",
backgroundColor:color,
position:'absolute',
left:position.x,
top:position.y
}}></div>
}

const getKey =()=> Math.random().toString(36).substring(2,8)
const renderCircle = (count,width,height)=> new Array(count).fill('circle').map(()=>(
<Circle position={getRandomPos(width,height)} key={getKey()} color={generateColor()}/>
))

const calcRenderCount = (count,perRenderCount,times) => {
if(perRenderCount * times > count) return count % perRenderCount;
return perRenderCount;
}

export default function Magnanimity(){
const [list,setList] = useState([]);
const total = useRef(20000);
const throttleCount= useRef(500);

const timeSpliceRender = useCallback((renderTimes,currentTime,width,height)=>{
if(currentTime > renderTimes) return;
const task = () => {
const renderCount = calcRenderCount(total.current,throttleCount.current,currentTime);
setList(list=> list.concat(renderCircle(renderCount,width,height)))
currentTime++;
}

requestIdleCallback((deadline)=>{
while (deadline.timeRemaining()>0) {
task();
}
timeSpliceRender(renderTimes,currentTime,width,height);
})

},[setList,
total,
throttleCount
])

useEffect(()=>{
const currentTime = 1;
const renderTimes = Math.ceil(total.current/throttleCount.current);
const width = document.documentElement.clientWidth;
const height = document.documentElement.clientHeight;
timeSpliceRender(renderTimes,currentTime,width,height)

},[
timeSpliceRender,
total,
throttleCount
])

return <div
onClick={()=>console.log("click")}
style={{
width:document.documentElement.clientWidth,
height:document.documentElement.clientHeight,
}}>{list}</div>
}

长列表渲染

比较容易想到的思路是:

  • 计算出列表能容纳的个数。

  • 计算需要渲染列表的高度

  • 筛选能显示在列表中的元素,为了保证用户体验,靠近列表头部和尾部的元素也可以渲染出来。

  • 为每个列表元素设置偏移量

  • 当滚动列表时,重新筛选显示元素并设置偏移量

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
function List({data}){
const [height,setHeight] = useState(0);
const [filterData,setFilterData] = useState([]);
const [scrollTop,setScrollTop] = useState(0);
const wrapperRef = useRef(null)

useEffect(()=>{
setHeight(()=> data.length * 40)
},[
data,
setHeight
])

useEffect(()=>{
const arr = [];
const boxHeight = wrapperRef.current.offsetHeight;

// 如果离列表头尾超过三个元素则不在显示
data.forEach((item,index) => {
if(index * 40 - 1 - scrollTop <= -80) return
if(index * 40 - 1 - boxHeight - scrollTop >= 80) return
console.log(item)
arr.push({
...item,
top: index * 40
})
});
setFilterData(arr);

},[
scrollTop,
data,
height,
setFilterData,
wrapperRef
])

const scroll = useCallback((e)=>{
// 滚动时重新筛选元素
setScrollTop(e.target.scrollTop)
},[setScrollTop])

if(!data || !Array.isArray(data)) return null;
return (
<div style={{
height:'100%',
width:'100%',
overflow:'auto'
}}
ref={wrapperRef}
onScroll={scroll}
>
<ul
style={{height,position:'relative'}}
>
{filterData.map(item => (
<li
key={item.key}
style={{
padding:'5px 4px',
position:'absolute',
left:0,
right:0,
top:item.top
}}
>
<div
style={{
background:'pink',
lineHeight:'30px',
height:'30px'
}}
>
{item.value}
</div>
</li>)
)}
</ul>
</div>
)
}

export default function Comp(){
const [data, setData] = useState(new Array(10).fill(1).map((item,index)=>({key:index,value:index})))
return <div style={{width:300,height:600,border:'1px solid #000'}}>
<List data={data}></List>
</div>
}

上面的实现存在几个问题:

  • 常量没有抽离,不能灵活配置

  • 每次滚动触发,需要遍历数据,性能损耗大

所以,可以优化为根据滚动高度计算出需要展示数据的区间,用transform变换待定绝对定位

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
function List({data=[]}){
const wrapperRef = useRef(null)
// 元素容器
const containerRef = useRef(null);
const [range,setRange] = useState([0,0])
const info = useRef({
// 屏幕外预渲染的个数
preRenderCount:2,
// 元素高度
itemHeight:40

})

useEffect(()=>{
const boxHeight = wrapperRef.current.offsetHeight;
const {itemHeight,preRenderCount} = info.current;

// 不需要关心data长度,如果截取的长度溢出,仍然会包含data中所有元素

const end = Math.ceil(boxHeight/itemHeight) + preRenderCount;
setRange([0,end]);

},[
info,
wrapperRef,
setRange
])

const scroll = useCallback((e)=>{
const scrollTop = e.target.scrollTop;
const boxHeight = wrapperRef.current.offsetHeight;

const {itemHeight,preRenderCount} = info.current;

// 顶部有两个缓冲元素,滚动高度还在这两个元素高度范围内,则前两个元素会被保留
const start = Math.floor(Math.max(0, scrollTop - preRenderCount * itemHeight) / itemHeight);

// 不需要关心data长度,如果截取的长度溢出,仍然会包含data中所有元素
const end = Math.ceil((scrollTop + boxHeight)/itemHeight) + preRenderCount;

// 计算内容区域偏移量 移动距离大于元素高度,重置视图框的位置
const offset = scrollTop - preRenderCount * itemHeight > 0 ? scrollTop - (scrollTop % itemHeight) - preRenderCount * itemHeight : 0;

containerRef.current.style.transform= `translate3D(0,${offset}px,0)`

setRange([start,end])

},[1
setRange,
wrapperRef,
containerRef
])

const {itemHeight} = info.current;
const height = useMemo(()=>data.length * itemHeight,[data,itemHeight]);
if(!data || !Array.isArray(data)) return null;
return (
<div style={{
height:'100%',
width:'100%',
overflow:'auto',
position:'relative'
}}
ref={wrapperRef}
onScroll={scroll}
>
<div style={{height}}></div>
{/* 注意:对ul列表的操作可能会触发上层滚动条的事件,所以仍然让他脱离文档流 */}
<ul
ref={containerRef}
style={{
position:'absolute',
top:0,
left:0,
right:0
}}
>
{data.slice(...range).map(item => (
<li
key={item.key}
style={{
height:itemHeight,
overflow:'hidden'
}}
>
<div
style={{
background:'pink',
lineHeight:'30px',
height:'30px',
margin:'5px 4px'
}}
>
{item.value}
</div>
</li>)
)}
</ul>
</div>
)
}

export default function Comp(){
const [data, setData] = useState(new Array(100).fill(1).map((item,index)=>({key:index,value:index})))
return <div style={{width:300,height:600,border:'1px solid #000'}}>
<List data={data}></List>
</div>
}

React原理 diff算法

随机树差异

查找两个随机树之间的最小差异是一个 O(n^3)问题。

如你所想,这么高复杂度的算法是无法满足我们的需求的。React 使用了一种更为简单且直观的算法使得算法复杂度优化至 O(n)。

React 只会逐层对比两颗随机树。这大大降低了 diff 算法的复杂度。并且在 web 组件中很少会将节点移动到不同的层级,经常只会在同一层级中移动。

diff 策略

  • Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。

  • 拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。

  • 对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。

tree diff

基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。

既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

如下图,A 节点(包括其子节点)整个被移动到 D 节点下,由于 React 只会简单的考虑同层级节点的位置变换,而对于不同层级的节点,只有创建和删除操作。当根节点发现子节点中 A 消失了,就会直接销毁 A;当 D 发现多了一个子节点 A,则会创建新的 A(包括子节点)作为其子节点。此时,React diff 的执行情况:create A -> create B -> create C -> delete A。

由此可发现,当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。

component diff

  • React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。

  • 如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。

  • 如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。

  • 对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。

如下图,当 component D 改变为 component G 时,即使这两个 component 结构相似,一旦 React 判断 D 和 G 是不同类型的组件,就不会比较二者的结构,而是直接删除 component D,重新创建 component G 以及其子节点。虽然当两个 component 是不同类型但结构相似时,React diff 会影响性能,但正如 React 官方博客所言:不同类型的 component 是很少存在相似 DOM tree 的机会,因此这种极端因素很难在实现开发过程中造成重大影响的。

diff 过程

  • 判断新的子元素中是否有重复的 key,如果有则抛出警告。方法是通过创建一个 Set 对象,判断 key 时候已经存在。
1
2
3
4
for (var i = 0; i < newChildren.length; i++) {
var child = newChildren[i];
knownKeys = warnOnInvalidKey(child, knownKeys, returnFiber);
}
  • 利用循环新的子元素数组,遍历老的子元素 Fiber
    然后通过调用 updateSlot ,updateSlot 内部会判断当前的 tag 和 key 是否匹配,如果匹配复用老 fiber 形成新的 fiber ,如果不匹配,返回 null ,此时 newFiber 等于 null 。跳出循环。
    如果是处于更新流程,找到与新节点对应的老 fiber ,但是不能复用 alternate === null ,那么会删除老 fiber 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
if (oldFiber.index > newIdx) {
nextOldFiber = oldFiber;
oldFiber = null;
} else {
nextOldFiber = oldFiber.sibling;
}
const newFiber = updateSlot(returnFiber,oldFiber,newChildren[newIdx],expirationTime,);
if (newFiber === null) { break }
// ..一些其他逻辑
}
if (shouldTrackSideEffects) { // shouldTrackSideEffects 为更新流程。
if (oldFiber && newFiber.alternate === null) {
/* 找到了与新节点对应的fiber,但是不能复用,那么直接删除老节点 */
deleteChild(returnFiber, oldFiber);
}
}
}
  • 当第一步结束完 newIdx === newChildren.length 此时证明所有 newChild 已经全部被遍历完,那么剩下没有遍历 oldFiber 也就没有用了,那么调用 deleteRemainingChildren 统一删除剩余 oldFiber 。
1
2
3
4
5
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
  • 当经历过第一步,oldFiber 为 null , 证明 oldFiber 复用完毕,那么如果还有新的 children ,说明都是新的元素,只需要调用 createChild 创建新的 fiber 。
1
2
3
4
5
6
7
8
9
10
if (oldFiber === null) {
for (; newIdx < newChildren.length; newIdx++) {
const newFiber = createChild(
returnFiber,
newChildren[newIdx],
expirationTime
);
// ...
}
}
  • 针对移动元素的场景

mapRemainingChildren 返回一个 map ,map 里存放剩余的老的 fiber 和对应的 key (或 index )的映射关系。

接下来遍历剩下没有处理的 Children ,通过 updateFromMap ,判断 mapRemainingChildren 中有没有可以复用 oldFiber ,如果有,那么复用,如果没有,新创建一个 newFiber 。

复用的 oldFiber 会从 mapRemainingChildren 删掉。

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
const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
if (_newFiber2 !== null) {
if (shouldTrackSideEffects) {
if (_newFiber2.alternate !== null) {
// The new fiber is a work in progress, but if there exists a
// current, that means that we reused the fiber. We need to delete
// it from the child list so that we don't add it to the deletion
// list.
existingChildren.delete(_newFiber2.key === null ? newIdx : _newFiber2.key);
}
}

lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);

if (previousNewFiber === null) {
resultingFirstChild = _newFiber2;
} else {
previousNewFiber.sibling = _newFiber2;
}

previousNewFiber = _newFiber2;
}
}

if (shouldTrackSideEffects) {
// Any existing children that weren't consumed above were deleted. We need
// to add them to the deletion list.
existingChildren.forEach(function (child) {
return deleteChild(returnFiber, child);
});
}

位置交换 lastPlacedIndex初始化为 0,如果一个新元素的索引大于lastPlacedIndex表示他从后面一动到前面,那么位置保持不变,如果新元素的索引小于lastPlacedIndex表示需要移动到后面,会被打上移动的 flag,最后通过previousNewFiber(他保存着上一次的最后节点)和sibling将 Fiber 链表链接

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
function placeChild(newFiber, lastPlacedIndex, newIndex) {
newFiber.index = newIndex;

if (!shouldTrackSideEffects) {
// Noop.
return lastPlacedIndex;
}

var current = newFiber.alternate;

if (current !== null) {
var oldIndex = current.index;

if (oldIndex < lastPlacedIndex) {
// This is a move.
newFiber.flags = Placement;
return lastPlacedIndex;
} else {
// This item can stay in place.
return oldIndex;
}
} else {
// This is an insertion.
newFiber.flags = Placement;
return lastPlacedIndex;
}
}
}

React原理 Suspense lazy

lazy

/react/packages/react/src/ReactLazy.js

lazy的本质是返回一个包含thenable的对象

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
function lazyInitializer<T>(payload: Payload<T>): T {
if (payload._status === Uninitialized) {
const ctor = payload._result;
const thenable = ctor();
// Transition to the next state.
// This might throw either because it's missing or throws. If so, we treat it
// as still uninitialized and try again next time. Which is the same as what
// happens if the ctor or any wrappers processing the ctor throws. This might
// end up fixing it if the resolution was a concurrency bug.
thenable.then(
moduleObject => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const resolved: ResolvedPayload<T> = (payload: any);
resolved._status = Resolved;
resolved._result = moduleObject;
}
},
error => {
if (payload._status === Pending || payload._status === Uninitialized) {
// Transition to the next state.
const rejected: RejectedPayload = (payload: any);
rejected._status = Rejected;
rejected._result = error;
}
},
);
if (payload._status === Uninitialized) {
// In case, we're still uninitialized, then we're waiting for the thenable
// to resolve. Set it as pending in the meantime.
const pending: PendingPayload = (payload: any);
pending._status = Pending;
pending._result = thenable;
}
}
if (payload._status === Resolved) {
const moduleObject = payload._result;

return moduleObject.default;
} else {
throw payload._result;
}
}

export function lazy<T>(
ctor: () => Thenable<{default: T, ...}>,
): LazyComponent<T, Payload<T>> {
const payload: Payload<T> = {
// We use these fields to store the result.
_status: -1,
_result: ctor,
};

const lazyType: LazyComponent<T, Payload<T>> = {
$$typeof: REACT_LAZY_TYPE,
_payload: payload,
_init: lazyInitializer,
};

return lazyType;
}

Render阶段

beginWork中遇到LazyComponent类型组件,会调用mountLazyComponent 方法处理

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
34
35
36
function mountLazyComponent(_current, workInProgress, elementType, updateLanes, renderLanes) {
if (_current !== null) {
// A lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect

workInProgress.flags |= Placement;
}

var props = workInProgress.pendingProps;
var lazyComponent = elementType;
var payload = lazyComponent._payload;
var init = lazyComponent._init;

// 第一次挂载组件时,因为promise状态不是完成状态,会抛出错误被上层的try catch捕获
var Component = init(payload);

workInProgress.type = Component;
// 如果已经加载成功,分析出异步组件的类型
var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);

// 合并异步组件的props和通过lazy创建组件时传入的props
var resolvedProps = resolveDefaultProps(Component, props);
var child;

// 根据不同的组件类型处理,如果没有则抛出错误
switch (resolvedTag) {
//...
}
{
throw Error( "Element type is invalid. Received a promise that resolves to: " + Component + ". Lazy element type must resolve to a class or function." + hint );
}
}

下面是Suspense如何被创建并影响lazy创建的异步组件

React并没有直接创建Suspense组件,最开始的时候Suspense组件只是一个标识用于导出 REACT_SUSPENSE_TYPE as Suspense

节点类型的创建,在初始只会创建出根节点的 fiber,后续的创建在 beginWork 入口,进入 reconcile 过程,会判断节点可复用性,然后不能复用的就通过 createFiberFromTypeAndProps 创建新节点。

1
2
3
4
5
6
7
8
9
10
function createFiberFromSuspense(pendingProps, mode, lanes, key) {
var fiber = createFiber(SuspenseComponent, pendingProps, key, mode); // TODO: The SuspenseComponent fiber shouldn't have a type. It has a tag.
// This needs to be fixed in getComponentName so that it relies on the tag
// instead.

fiber.type = REACT_SUSPENSE_TYPE;
fiber.elementType = REACT_SUSPENSE_TYPE;
fiber.lanes = lanes;
return fiber;
}

beginWork中发现是Suspense类型会执行 updateSuspenseComponent

第一次执行创建子节点 workInProgress.child = mountChildFibers

第二次执行,建立节点间联系

1
2
3
4
5
6
7
fallbackChildFragment.return = workInProgress;

primaryChildFragment.sibling = fallbackChildFragment;

workInProgress.child = primaryChildFragment;

return fallbackChildFragment

由于第一次执行的时候lazy组件抛出错误,会被renderRootSync捕获

1
2
3
4
5
6
7
8
9
10
function renderRootSync(root, lanes) {
do {
try {
workLoopSync();
break;
} catch (thrownValue) {
handleError(root, thrownValue);
}
} while (true);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function handleError(root, thrownValue) {
do {
try {
...
// 继续执行
throwException(...);
// 这里完成时 会将wip设置为自己的父节点 也就是 suspense 节点
workInProgress = completeUnitOfWork(workInProgress);
} catch (yetAnotherThrownValue) {
...
continue
}
// Return to the normal work loop.
return;
} while (true);
}

继续执行 throwException,这里会将抛出的 promise 放入子组件的 updateQueue

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function throwException(
root: FiberRoot,
returnFiber: Fiber,
sourceFiber: Fiber,
value: mixed,
renderExpirationTime: ExpirationTime,
) {
...
if (
value !== null &&
typeof value === 'object' &&
typeof value.then === 'function'
) {
// This is a thenable.
const thenable: Thenable = (value: any);

...
do {
if (
workInProgress.tag === SuspenseComponent &&
shouldCaptureSuspense(workInProgress, hasInvisibleParentBoundary)
) {
// 一个 set 结构存储在 updateQueue
const thenables: Set<Thenable> = (workInProgress.updateQueue: any);
if (thenables === null) {
const updateQueue = (new Set(): any);
updateQueue.add(thenable);
// 第一次新增
workInProgress.updateQueue = updateQueue;
} else {
// 追加
thenables.add(thenable);
}
...
// 同步设置
sourceFiber.expirationTime = Sync;

return;
}

...
workInProgress = workInProgress.return;
} while (workInProgress !== null);

}
...
}

commit阶段

commitWork 中会处理队列中的Promise

会对之前渲染的 fallback 组件标记删除,对新的渲染数据标记更新。

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
function attachSuspenseRetryListeners(finishedWork) {
// If this boundary just timed out, then it will have a set of wakeables.
// For each wakeable, attach a listener so that when it resolves, React
// attempts to re-render the boundary in the primary (pre-timeout) state.
var wakeables = finishedWork.updateQueue;

if (wakeables !== null) {
finishedWork.updateQueue = null;
var retryCache = finishedWork.stateNode;

if (retryCache === null) {
retryCache = finishedWork.stateNode = new PossiblyWeakSet();
}

wakeables.forEach(function (wakeable) {
// Memoize using the boundary fiber to prevent redundant listeners.
var retry = resolveRetryWakeable.bind(null, finishedWork, wakeable);

if (!retryCache.has(wakeable)) {
{
if (wakeable.__reactDoNotTraceInteractions !== true) {
retry = tracing.unstable_wrap(retry);
}
}

retryCache.add(wakeable);
// 通过then方法在resolve之后执行
wakeable.then(retry, retry);
}
});
}
}

实现一个异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
*
* @param {*} Component 需要异步数据的component
* @param {*} api 请求数据接口,返回Promise,可以再then中获取与后端交互的数据
* @returns
*/
function AysncComponent(Component,api){
const AysncComponentPromise = () => new Promise(async (resolve)=>{
const data = await api()
resolve({
default: (props) => <Component rdata={data} { ...props} />
})
})
return React.lazy(AysncComponentPromise)
}
  • 用 AysncComponent 作为一个 HOC 包装组件,接受两个参数,第一个参数为当前组件,第二个参数为请求数据的 api 。
  • 声明一个函数给 React.lazy 作为回调函数,React.lazy 要求这个函数必须是返回一个 Promise 。在 Promise 里面通过调用 api 请求数据,然后根据返回来的数据 rdata 渲染组件,别忘了接受并传递 props 。
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
34
35
/* 数据模拟 */
const getData=()=>{
return new Promise((resolve)=>{
//模拟异步
setTimeout(() => {
resolve({
name:'alien',
say:'let us learn React!'
})
}, 1000)
})
}
/* 测试异步组件 */
function Test({ rdata , age}){
const { name , say } = rdata
console.log('组件渲染')
return <div>
<div> hello , my name is { name } </div>
<div>age : { age } </div>
<div> i want to say { say } </div>
</div>
}
/* 父组件 */
export default class Index extends React.Component{
LazyTest = AysncComponent(Test,getData)
/* 需要每一次在组件内部声明,保证每次父组件挂载,都会重新请求数据 ,防止内存泄漏。 */
render(){
const { LazyTest } = this
return <div>
<Suspense fallback={<div>loading...</div>} >
<LazyTest age={18} />
</Suspense>
</div>
}
}

React原理 HOC高阶组件

属性代理

属性代理,就是用组件包裹一层代理组件,在代理组件上,可以做一些,对源组件的强化操作。这里注意属性代理返回的是一个新组件,被包裹的原始组件,将在新的组件里被挂载。

1
2
3
4
5
6
7
8
9
10
function HOC(WrapComponent){
return class Advance extends React.Component{
state={
name:'alien'
}
render(){
return <WrapComponent { ...this.props } { ...this.state } />
}
}
}

优点:

① 属性代理可以和业务组件低耦合,零耦合,对于条件渲染和 props 属性增强,只负责控制子组件渲染和传递额外的 props 就可以了,所以无须知道,业务组件做了些什么。所以正向属性代理,更适合做一些开源项目的 HOC ,目前开源的 HOC 基本都是通过这个模式实现的。
② 同样适用于类组件和函数组件。
③ 可以完全隔离业务组件的渲染,因为属性代理说白了是一个新的组件,相比反向继承,可以完全控制业务组件是否渲染。
④ 可以嵌套使用,多个 HOC 是可以嵌套使用的,而且一般不会限制包装 HOC 的先后顺序。

缺点:

① 一般无法直接获取原始组件的状态,如果想要获取,需要 ref 获取组件实例。
② 无法直接继承静态属性。如果需要继承需要手动处理,或者引入第三方库。
③ 因为本质上是产生了一个新组件,所以需要配合 forwardRef 来转发 ref。

反向继承

反向继承和属性代理有一定的区别,在于包装后的组件继承了原始组件本身,所以此时无须再去挂载业务组件。

1
2
3
4
5
6
7
8
9
10
11
class Index extends React.Component{
render(){
return <div> hello,world </div>
}
}
function HOC(Component){
return class wrapComponent extends Component{ /* 直接继承需要包装的组件 */

}
}
export default HOC(Index)

优点:

① 方便获取组件内部状态,比如 state ,props ,生命周期,绑定的事件函数等。
② es6继承可以良好继承静态属性。所以无须对静态属性和方法进行额外的处理。

缺点:

① 函数组件无法使用。
② 和被包装的组件耦合度高,需要知道被包装的原始组件的内部状态,具体做了些什么?
③ 如果多个反向继承 HOC 嵌套在一起,当前状态会覆盖上一个状态。这样带来的隐患是非常大的,比如说有多个 componentDidMount ,当前 componentDidMount 会覆盖上一个 componentDidMount 。这样副作用串联起来,影响很大。

HOC的几种场景

强化props

强化 props 就是在原始组件的 props 基础上,加入一些其他的 props ,强化原始组件功能。举个例子,为了让组件也可以获取到路由对象,进行路由跳转等操作,所以 React Router 提供了类似 withRouter 的 HOC 。

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
function withRouter(Component) {
const displayName = `withRouter(${Component.displayName || Component.name})`;
const C = props => {
/* 获取 */
const { wrappedComponentRef, ...remainingProps } = props;
return (
<RouterContext.Consumer>
{context => {
return (
<Component
{...remainingProps} // 组件原始的props
{...context} // 存在路由对象的上下文history location
ref={wrappedComponentRef}
/>
);
}}
</RouterContext.Consumer>
);
};

C.displayName = displayName;
C.WrappedComponent = Component;
/* 继承静态属性 */
return hoistStatics(C, Component);
}
export default withRouter

分离出 props 中 wrappedComponentRef 和 remainingProps , remainingProps 是原始组件真正的 props, wrappedComponentRef 用于转发 ref。
用 Context.Consumer 上下文模式获取保存的路由信息。( React Router 中路由状态是通过 context 上下文保存传递的)
将路由对象和原始 props 传递给原始组件,所以可以在原始组件中获取 history ,location 等信息。

控制渲染

HOC 反向继承模式,可以通过 super.render() 得到 render 之后的内容,利用这一点,可以做渲染劫持 ,更有甚者可以修改 render 之后的 React element 对象。

1
2
3
4
5
6
7
8
9
10
const HOC = (WrapComponent) =>
class Index extends WrapComponent {
render() {
if (this.props.visible) {
return super.render()
} else {
return <div>暂无数据</div>
}
}
}
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
class Index extends React.Component{
render(){
return <div>
<ul>
<li>react</li>
<li>vue</li>
<li>Angular</li>
</ul>
</div>
}
}
function HOC (Component){
return class Advance extends Component {
render() {
const element = super.render()
const otherProps = {
name:'alien'
}
/* 替换 Angular 元素节点 */
const appendElement = React.createElement('li' ,{} , `hello ,world , my name is ${ otherProps.name }` )
const newchild = React.Children.map(element.props.children.props.children,(child,index)=>{
if(index === 2) return appendElement
return child
})
return React.cloneElement(element, element.props, newchild)
}
}
}
export default HOC(Index)
动态加载

dva 中 dynamic 就是配合 import ,实现组件的动态加载的,而且每次切换路由,都会有 Loading 效果,接下来看看大致的实现思路。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export default function dynamicHoc(loadRouter) {
return class Content extends React.Component {
state = {Component: null}
componentDidMount() {
if (this.state.Component) return
loadRouter()
.then(module => module.default) // 动态加载 component 组件
.then(Component => this.setState({Component},
))
}
render() {
const {Component} = this.state
return Component ? <Component {
...this.props
}
/> : <Loading />
}
}
}
组件赋能

ref获取实例

对于属性代理虽然不能直接获取组件内的状态,但是可以通过 ref 获取组件实例,获取到组件实例,就可以获取组件的一些状态,或是手动触发一些事件,进一步强化组件,但是注意的是:类组件才存在实例,函数组件不存在实例。

1
2
3
4
5
6
7
8
9
10
11
function Hoc(Component){
return class WrapComponent extends React.Component{
constructor(){
super()
this.node = null /* 获取实例,可以做一些其他的操作。 */
}
render(){
return <Component {...this.props} ref={(node) => this.node = node } />
}
}
}

事件监控

HOC 不一定非要对组件本身做些什么?也可以单纯增加一些事件监听,错误监控。接下来,接下来做一个 HOC ,只对组件内的点击事件做一个监听效果。

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
function ClickHoc (Component){
return function Wrap(props){
const dom = useRef(null)
useEffect(()=>{
const handerClick = () => console.log('发生点击事件')
dom.current.addEventListener('click',handerClick)
return () => dom.current.removeEventListener('click',handerClick)
},[])
return <div ref={dom} ><Component {...props} /></div>
}
}

@ClickHoc
class Index extends React.Component{
render(){
return <div className='index' >
<p>hello,world</p>
<button>组件内部点击</button>
</div>
}
}
export default ()=>{
return <div className='box' >
<Index />
<button>组件外部点击</button>
</div>
}

总结

下面对 HOC 具体能实现那些功能,和如何编写做一下总结:

  • 强化 props ,可以通过 HOC ,向原始组件混入一些状态。

  • 渲染劫持,可以利用 HOC ,动态挂载原始组件,还可以先获取原始组件的渲染树,进行可控性修改。

  • 可以配合 import 等 api ,实现动态加载组件,实现代码分割,加入 loading 效果。

  • 可以通过 ref 来获取原始组件实例,操作实例下的属性和方法。

  • 可以对原始组件做一些事件监听,错误监控等。

注意事项

谨慎修改原型链

如上 HOC 作用仅仅是修改了原来组件原型链上的 componentDidMount 生命周期。但是这样有一个弊端就是如果再用另外一个 HOC 修改原型链上的 componentDidMount ,那么这个HOC的功能即将失效。

1
2
3
4
5
6
7
8
function HOC (Component){
const proDidMount = Component.prototype.componentDidMount
Component.prototype.componentDidMount = function(){
console.log('劫持生命周期:componentDidMount')
proDidMount.call(this)
}
return Component
}

不要在函数组件内部或类组件render函数中使用HOC

由于属性的引用不同,每次都会重新渲染

ref的处理

高阶组件的约定是将所有 props 传递给被包装组件,但这对于 ref 并不适用。那是因为 ref 实际上并不是一个 prop , 就像 key 一样,对于 ref 属性它是由 React 专门处理的。那么如何通过 ref 正常获取到原始组件的实例呢?在 ref 章节已经讲到,可以用 forwardRef做 ref 的转发处理。

修饰器的嵌套顺序

静态属性需要处理

React原理 context深入

context三中用法

定义context

1
2
3
const ThemeContext = React.createContext(null) //
const ThemeProvider = ThemeContext.Provider //提供者
const ThemeConsumer = ThemeContext.Consumer // 订阅消费者

使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ConsumerDemo extends React.Component{
render(){
const { color,background } = this.context
return <div style={{ color,background } } >消费者</div>
}
}
ConsumerDemo.contextType = ThemeContext

function ProviderDemo(){
const [ contextValue , setContextValue ] = React.useState({ color:'#ccc', background:'pink' })
return <div>
<ThemeProvider value={ contextValue } >
<ConsumerDemo />
</ThemeProvider>
</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ConsumerDemo extends React.Component {
render() {
return <ThemeConsumer>
{({ color, background }) => <div style={{ color, background }} >消费者</div>}
</ThemeConsumer>
}
}
function ProviderDemo() {
const [contextValue, setContextValue] = React.useState({ color: '#ccc', background: 'pink' })
return <div>
<ThemeProvider value={contextValue} >
<ConsumerDemo />
</ThemeProvider>
</div>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
function ConsumerDemo() {
const { color, background } = React.useContext(ThemeContext);
return <div style={{ color, background }} >消费者</div>
}

function ProviderDemo() {
const [contextValue, setContextValue] = React.useState({ color: '#ccc', background: 'pink' })
return <div>
<ThemeProvider value={contextValue} >
<ConsumerDemo />
</ThemeProvider>
</div>
}

displayName

context 对象接受一个名为 displayName 的 property,类型为字符串。React DevTools 使用该字符串来确定 context 要显示的内容。

1
2
3
4
5
const MyContext = React.createContext(/* 初始化内容 */);
MyContext.displayName = 'MyDisplayName';

<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中
<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中

源码

createContext 创建了一个包含, Provider 和 Consumer 组件的对象,通过_context属性形成相互引用的循环链表结构

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
function createContext(defaultValue, calculateChangedBits) {
var context = {
$$typeof: REACT_CONTEXT_TYPE,
_currentValue: defaultValue,
_currentValue2: defaultValue,
Provider: null,
Consumer: null
};
context.Provider = {
$$typeof: REACT_PROVIDER_TYPE,
_context: context
};

{
var Consumer = {
$$typeof: REACT_CONTEXT_TYPE,
_context: context,
};
Object.defineProperties(Consumer, {
// 添加getter 和 setter
});

context.Consumer = Consumer;
}

return context;
}

如果当前类型的 fiber 不需要更新,那么会 FinishedWork 中止当前节点和子节点的更新。

如果当前类型 fiber 需要更新,那么会调用不同类型 fiber 的处理方法。当然 ContextProvider 也有特有的 fiber 更新方法 —— updateContextProvider

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
34
35
36
37
38
39
40
41
42
function updateContextProvider(current, workInProgress, renderLanes) {
// 通过type属性获取Provider组件
var providerType = workInProgress.type;
// 拿到createContext定义的上下文
var context = providerType._context;

// 获取传递到Provider组件的属性
var newProps = workInProgress.pendingProps;
var oldProps = workInProgress.memoizedProps;
var newValue = newProps.value;

// 方法内部通过context._currentValue = nextValue 给context赋值
pushProvider(workInProgress, newValue);

// 上一次props有值得时候需要判断是否进行子元素的调度
if (oldProps !== null) {
var oldValue = oldProps.value;

// 1.判断引用是否相同
// 2.尝试使用自定义函数判断是否相同 changedBits & MAX_SIGNED_31_BIT_INT) !== changedBits 如果不是有效数字则报错
// 3 通过 |0 操作将非法结果强制转换成0;
var changedBits = calculateChangedBits(context, newValue, oldValue);

if (changedBits === 0) {
// context没有变化,且子节点没有变化,legacy context没改变,则退出更新
// No change. Bailout early if children are the same.
if (oldProps.children === newProps.children && !hasContextChanged()) {
// 子元素引用没有变化则停止调度
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
} else {
// The context value changed. Search for matching consumers and schedule
// them to update.
propagateContextChange(workInProgress, context, changedBits, renderLanes);
}
}

// 获取到子节点并继续在子节点上调度
var newChildren = newProps.children;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
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
function updateContextConsumer(current, workInProgress, renderLanes) {
var context = workInProgress.type;

var newProps = workInProgress.pendingProps;
var render = newProps.children;

// 如果子元素不是一个函数则抛出错误
{
if (typeof render !== 'function') {
error('A context consumer was rendered with multiple children, or a child ' + "that isn't a function. A context consumer expects a single child " + 'that is a function. If you did pass a function, make sure there ' + 'is no trailing or leading whitespace around it.');
}
}

prepareToReadContext(workInProgress, renderLanes);
// 获取最新的值
var newValue = readContext(context, newProps.unstable_observedBits);
var newChildren;

// 执行函数获取下一个节点
{
ReactCurrentOwner$1.current = workInProgress;
setIsRendering(true);
newChildren = render(newValue);
setIsRendering(false);
} // React DevTools reads this flag.

//继续在下一个节点上调度
workInProgress.flags |= PerformedWork;
reconcileChildren(current, workInProgress, newChildren, renderLanes);
return workInProgress.child;
}
  • Copyrights © 2015-2025 SunZhiqi

此时无声胜有声!

支付宝
微信