# React 专题
# 概念
# 受控组件
- 受控组件
对于某个组件,它的状态是否收到外界的控制,如
input
的值是否受value
控制,并且改变的值通过change
改变外界的值 - 非受控组件
对应地,组件的状态不受外界控制,如
input
只传入defaultValue
作为初始值
# jsx
jsx 是一种描述当前组件内容的数据结构
# Fiber
Fiber 是一种架构
通过保存的信息链接整个 fiber 树
// Fiber,用来链接其他fiber节点形成的fiber树 this.return = null; this.child = null; this.sibling = null; this.index = 0;
Fiber 是一种数据结构
储存节点的静态信息以及动态信息
静态信息
每个
fiber
节点都对应着一个react element
,保存了该组件的类型(函数组件,类组件,原生组件对应的dom
节点信息)// Instance,静态节点的数据结构属性 this.tag = tag; this.key = key; this.elementType = null; this.type = null; this.stateNode = null;
动态信息
保存了每个组件改变的状态,要执行的工作(删除,插入或者更新)
this.ref = null; // 作为动态的工作单元的属性 this.pendingProps = pendingProps; this.memoizedProps = null; this.updateQueue = null; this.memoizedState = null; this.dependencies = null;
Fiber 是一种更新机制
保存着更新的优先级,副作用等,节点之间通过单向链链接,可随时被高优先级或者申请执行时间不够而被中断
this.mode = mode; this.effectTag = NoEffect; this.subtreeTag = NoSubtreeEffect; this.deletions = null; this.nextEffect = null; this.firstEffect = null; this.lastEffect = null; // 作为调度优先级的属性 this.lanes = NoLanes; this.childLanes = NoLanes; // 指向该fiber在另一次更新时对应的fiber this.alternate = null;
# 源码
# render 阶段
# beginWork
beginWork
的工作是传入当前Fiber节点
,创建子Fiber节点
,通过 current===null 判断是 mount 还是 update
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null
# update
满足一定条件时可以复用current节点
,这样就能克隆current.child
作为workInProgress.child
- didReceiveUpdate === false
- oldProps === newProps && workInProgress.type === current.type (props 和 fiber.type 不变)
- !includesSomeLane(renderLanes,updateLanes),当前
fiber节点
优先级不够
# mount
除fiberRootNode
以外,会根据fiber.tag
不同,创建不同类型的子fiber节点
,最终调用的是recocileChildren
,最终都会生成新的Fiber
节点返回并赋值给workInProgress.child
- 对于
mount
的组件,会创建新的fiber节点
- 对于
update
的组件,会与current
进行对比(diff
算法),将比较结果生成新的fiber节点
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes
);
} else {
// 对于update的组,会带上effectTag属性
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes
);
}
}
# completeWork
与beginWork
一样通过判断current !== null
来判断是mount
还是update
处理props
等参数,提交commit
export const completeWork = (workInProgress: FiberNode) => {
if (__LOG__) {
console.log("complete流程", workInProgress);
}
const newProps = workInProgress.pendingProps;
const current = workInProgress.alternate;
switch (workInProgress.tag) {
case HostComponent:
if (current !== null && workInProgress.stateNode) {
// 更新
// TODO 更新元素属性
// 不应该在此处调用updateFiberProps,应该跟着判断属性变化的逻辑,在这里打flag
// 再在commitWork中更新fiberProps,我准备把这个过程留到「属性变化」相关需求一起做
updateFiberProps(workInProgress.stateNode, newProps);
} else {
// 初始化DOM
const instance = createInstance(workInProgress.type, newProps);
// 挂载DOM
appendAllChildren(instance, workInProgress);
workInProgress.stateNode = instance;
// TODO 初始化元素属性
}
// 冒泡flag
bubbleProperties(workInProgress);
return null;
case HostRoot:
bubbleProperties(workInProgress);
return null;
case HostText:
if (current !== null && workInProgress.stateNode) {
// 更新
const oldText = current.memoizedProps?.content;
const newText = newProps.content;
if (oldText !== newText) {
markUpdate(workInProgress);
}
} else {
// 初始化DOM
const textInstance = createTextInstance(newProps.content);
workInProgress.stateNode = textInstance;
}
// 冒泡flag
bubbleProperties(workInProgress);
return null;
case FunctionComponent:
bubbleProperties(workInProgress);
return null;
default:
console.error("completeWork未定义的fiber.tag", workInProgress);
return null;
}
};
# update
判断current
的同时还需要判断workInProgress.stateNode
,如果不为null
则是update
,主要处理的 prop:
onClick
、onChange
等事件注册- 处理
style
- 处理
children prop
最终处理完的props
会被赋值给workInProgress.updateQueue
,最后在commit
阶段被渲染,props
是一个数组,基数值为key
,偶数值为value
# mount
主要逻辑:
- 通过
beginWork
生成的 tag 为 fiber 节点生成 DOM 节点 - 将子 DOM 节点插入到刚刚生成的 DOM 节点中
- 与
update
步骤中的updateHostComponent
类似的props
处理过程
每次都把生成的 DOM 节点插入到父亲节点中,commit
后只需把rootFiber
的stateNode
插入到fiberRoot
即可以更新全文档
# effectList
WARNING
v18 已经进行重构,变为subtreeFlags
,通过冒泡传递到rootFiber
,最终还得遍历树处理effect
关于 effectList 重构为 subtreeFlags (opens new window)
commit阶段
需要对存在effectTag
的fiber节点
进行对应的操作,来源一条叫effectList
的单向链表
在completeWork
中的上层completeUnitOfWork
中,每执行完completeWork
且存在effectTag
,就会把fiber节点
添加到该链表中
# 触发更新
# 触发更新方式
- render
- setState
- dispatch reducer
- forceUpdate
# Q&A
# React Fiber作用
- 作为架构
在v15的Reconciler采用递归执行,数据存储递归调用栈,被称为Stack Reconciler;在v16+Reconciler基于Fiber节点
实现,被成为Fiber Reconciler
- 作为静态数据结构
每一个Fiber 节点
对应一个React Element
,保存了该组件的类型(函数式组件,类组件,原生组件),对应DOM节点等数据 - 作为动态单元
每个Fiber节点
保存了本次更新中该组件改变的状态,要执行的工作(被删除,被插入,被更新等)
# 更新时wip tree如何生成?
每次触发更新,都会从FiberRootNode开始自定向下的遍历fiber tree执行beginWork
,如果遇到的是FC
或者是classComponent
,则会执行改函数,得到reactElement
,与current fiber
进行Reconciler
,生成wip
的children,在此阶段为fiber打上flag
供completeWork
阶段消费
# 优先级插队如何实现?
React每次产生update
都会生成对应的优先级,并创建performConcurrentWork
函数交由Scheduler
进行调度,优先级足够高将排序到taskQueue
的最前面,调度该函数时会进行优先级判断,以及FiberRootNode
中是否有callbackNode
,如果存在也会把旧的回调事件取消
# 如何生成新的 fiber 链呢?
那就是在beginworke
里,在处理子节点时调用的reconcileChildFibers
然后调用reconcileSingleElement
下层调用 useFiber
产生alternate
,最后commitRoot
更新视图
# 生成的真实节点如何合并的呢?
beginwork
的时候对 fiber 完成 tag 的标记,completework
时向上遍历,如果判断是原生节点就调用appendAllChildren
把子节点添加到当前节点下,fiber
保存 DOM 的字段是stateNode
# 优化
# 渲染优化
# ReRender 三要素
props 默认是全等比较,每次传入的 props 都是全新的对象,oldProps !== newProps true
- props
从父组件接受 data children 等参数
- state
作为 props 传递给子孙组件
- context
从 provider 中获取数据
# 自顶向下更新
在React
中数据发生变化,会自顶向下构建一颗完整的组件树,即使组件内部没有发生变化,默认都会被重新渲染
function Demo1() {
const [count, setCount] = useState(0);
return (
<>
<input
type="number"
value={count}
onChange={(e) => setCount(+e.target.value)}
/>
<p>num is {count}</p>
<ExpensiveCpn />
</>
);
}
function ExpensiveCpn() {
const now = performance.now();
while (performance.now() - now > 100) {}
console.log("耗时组件 render");
return <p>耗时组件</p>;
}
# 抽离变的部分
把 Input 及展示需要用到 state 的部分抽离成一个组件,把变与不变分离
function Demo2() {
return (
<>
<Input></Input>
<ExpensiveCpn />
</>
);
}
function Input() {
const [count, setCount] = useState(0);
return (
<>
<input
type="number"
value={count}
onChange={(e) => setCount(+e.target.value)}
/>
<p>num is {count}</p>
</>
);
}
# 优化父组件
父组件不变,props 传递不变组件到可变组件中,也可以达到不变组件不重渲染
function Demo3() {
return (
<InputWrapper>
<ExpensiveCpn />
</InputWrapper>
);
}
function InputWrapper({ children }) {
const [count, setCount] = useState(0);
return (
<div title={count + ""}>
<input
type="number"
value={count}
onChange={(e) => setCount(+e.target.value)}
/>
<p>num is {count}</p>
{children}
</div>
);
}
# context 传参
const numCtx = createContext(0);
const updateNumCtx = createContext(() => {});
function Demo4() {
const [num, updateNum] = useState(0);
return (
<>
<numCtx.Provider value={num}>
<updateNumCtx.Provider value={updateNum}>
<Middle></Middle>
</updateNumCtx.Provider>
</numCtx.Provider>
</>
);
}
function Middle() {
return (
<>
<Button></Button>
<Show></Show>
</>
);
}
function Button() {
const updateNum = useContext(updateNumCtx);
console.log("btn render");
return <button onClick={() => updateNum(Math.random())}>随机数</button>;
}
function Show() {
const num = useContext(numCtx);
return <p>num is: {num}</p>;
}
# memo
使用memo对中间组件的渲染结果缓存,原理是 memo 会对 props 进行浅比较
const Middle = memo(() => {
return (
<>
<Button></Button>
<Show></Show>
</>
);
});
# useMemo
使用 useMemo 对渲染结果缓存
const Middle = () => {
return useMemo(() => {
return (
<>
<Button></Button>
<Show></Show>
</>
);
}, []);
};
# BrowserRouter 核心原理实现
- 历史操作:pushState,replaceState 不会触发popState
- 监听变更:window.addEventListener('popState',()={})
- 操作:history.back(),history.forward(),history.go() 会触发popState
# 相关库
immutable.js React-app-rewired