最近在学习慕课网 手写 React 高质量源码迈向高阶开发,之前自己也尝试看过源码,不过最终放弃了
放弃的最主要原因是 react 内部的调用链太长了,每天在缕清调用链上都花了不少时间,createRoot 都没有看完
最近看到慕课网有一个 react 源码课,就想着跟着课程然后在自己源码,看看这次能够看到什么地步
它这个课程前八章 是 react@16 的源码,从第九章开始才是 react@18 的源码
React 源码系列:
- 第 1 篇:createElement 和 render 函数实现原理
- 第 2 篇:函数组件和类组件及 ref 和 setState 的实现
- 第 3 篇:优化渲染过程之 dom diff
- 第 4 篇:类组件增强------生命周期函数
通过上面的学习,我们已经可以实现了 react 中的渲染和更新
接下来我们来实现 react 中的生命周期函数
其实生命周期函数的本质是回调函数,在特定的时机被调用,执行特定的逻辑
componentDidMount
componentDidMount 是在组件初始化时调用,可以在这里进行一些初始化的操作:
- 它是在组件挂载到页面上后调用
- 可以操作
DOM,也就是说可以使用document.getElementById(xxx) - 可以进行网络请求
- 可以做事件订阅,但需要在
componentWillUnmount中取消订阅 - 不适合在这里调用
setState,会触发一次更新,state初始值最好在constructor中赋值
我们应该在那里调用 componentDidMount 呢?
根据上面的分析,应该是在 DOM 挂载到页面之后调用
DOM 是在什么时候挂载的呢?
通过查看之前的源码,我们发现是在 mount 函数中完成 DOM 的挂载
js
function mount(VNode, containerDOM) {
let newDOM = createDOM(VNode);
newDOM && containerDOM.appendChild(newDOM);
}
componentDidMount 函数是在类组件中的,我们处理类组件时,需要将类组件的实例传递出来
js
function getDomByClassComponent(VNode) {
let instance = new type(props);
// ...
let dom = createDOM(renderVNode);
return { dom, instance };
}
js
function createDOM(VNode) {
// ...
if (
typeof type === "function" &&
$$typeof === REACT_ELEMENT &&
type.IS_CLASS_COMPONENT
) {
const classComponent = getDomByClassComponent(VNode);
// 将实例保存到全局
classComponentInstance = classComponent.instance;
return classComponent.dom;
}
// ...
}
然后我们在 mount 函数中调用,类组件的 componentDidMount 函数
js
function mount(VNode, containerDOM) {
let newDOM = createDOM(VNode);
// 将 dom 挂载到页面
newDOM && containerDOM.appendChild(newDOM);
// 如果存在生命周期函数 componentDidMount 函数,调用
if (classComponentInstance && classComponentInstance.componentDidMount)
classComponentInstance.componentDidMount();
}
componentDidUpdate
componentDidUpdate 是在组件更新时调用,可以在这里进行一些更新后的操作:
- 更新完成后调用,初始化渲染不会调用
- 当组件完成更新,需要对
DOM进行某种操作的时候,适合在这个函数中进行 - 当
props有所变化时,可以进行一些操作,比如网络请求 - 这里虽然可以调用
setState,但这是有条件的调用,否则会陷入死循环 - 如果
shouldComponentUpdate返回false,componentDidUpdate不会执行 - 如果实现了
getSnapshotBeforeUpdate,componentDidUpdate会接收第三个参数 - 如果将
props中的内容拷贝到state,可以考虑直接使用props,而不是在这里进行拷贝,参见文档:Common Bugs When Using Derived State
我们应该在哪里调用 componentDidUpdate 呢?
通过代码分析,调用 componentDidUpdate 的时机是在 DOM 更新完成时
DOM 更新完成时又是在哪里呢?
通过查看代码,我们找到是在 updateDomTree 函数中完成的,所以我们应该在 updateDomTree 函数执行完成后调用 componentDidUpdate
js
class Component {
update() {
// 拿到 oldVNode
let oldVNode = this.oldVNode;
// 将 oldVNode 转换成真实 DOM
let oldDOM = findDOMByVNode(oldVNode);
// 调用 render 方法,得到新的 VNode
let newVNode = this.render();
// 更新 DOM,并将新的 DOM 挂载到页面上
updateDomTree(oldVNode, newVNode, oldDOM);
}
}
在调用 componentDidUpdate 函数前,需要对 update 方法进行改造
通过阅读官方文档,我们知道 componentDidUpdate 函数接收三个参数:
prevPropsprevStatesnapshot(这里暂时先不考虑这个参数,在后面实现getSnapshotBeforeUpdate函数时再来传递)
在 update 函数中,我们没法拿到 prevProps 和 prevState 这张两个参数
那我们看下是谁调用了 update 函数,往上查找后发现是在 Updater.launchUpdate 中调用的
js
class Updater {
launchUpdate() {
const { ClassComponentInstance, pendingStates } = this;
// state 合并,只合并第一层
ClassComponentInstance.state = pendingStates.reduce(
(state, partialState) => {
return { ...state, ...partialState };
},
ClassComponentInstance.state
);
// 清空 pendingStates
pendingStates.length = 0;
// 更新视图
ClassComponentInstance.update();
}
}
Updater.launchUpdate 函数实现了 state 合并,那合并前的 state 不就是 prevState 吗
在这里我们就拿到了 prevState 和 prevProps
js
class Updater {
launchUpdate() {
const { ClassComponentInstance, pendingStates } = this;
// 拿到 preState 和 preProps
const preState = ClassComponentInstance.state;
const preProps = ClassComponentInstance.props;
// state 合并,只合并第一层
ClassComponentInstance.state = pendingStates.reduce(
(state, partialState) => {
return { ...state, ...partialState };
},
ClassComponentInstance.state
);
// 清空 pendingStates
pendingStates.length = 0;
// 更新视图,将 preProps 和 preState 传递给 update 函数
ClassComponentInstance.update(preProps, preState);
}
}
然后我们在 update 函数中调用 componentDidUpdate 函数
js
class Component {
update(preProps, preState) {
// 拿到 oldVNode
let oldVNode = this.oldVNode;
// 将 oldVNode 转换成真实 DOM
let oldDOM = findDOMByVNode(oldVNode);
// 调用 render 方法,得到新的 VNode
let newVNode = this.render();
// 更新 DOM,并将新的 DOM 挂载到页面上
updateDomTree(oldVNode, newVNode, oldDOM);
// 如果存在生命周期函数 componentDidUpdate 函数,调用,并传入 preProps 和 preState
if (this.componentDidUpdate) this.componentDidUpdate(preProps, preState);
}
}
componentWillUnmount
componentWillUnmount 是在组件卸载时调用,可以在这里进行一些清理操作:
- 组件从
DOM树上卸载完成的时候调用该函数 - 执行一些清理操作,比如清除定时器,取消事件订阅,取消网络请求等
- 不要在这里调用
setState,不会产生任何效果,卸载后不会重新渲染
我们应该在哪里调用 componentWillUnmount 呢?
通过代码分析,调用 componentWillUnmount 的时机是在 DOM 卸载完成时
DOM 卸载完成时又是在哪里呢?
通过查看代码我们找到了 removeVNode,这个函数是在删除或者替换 DOM 时调用的
js
function removeVNode(VNode) {
// 找到 dom 节点
const currentDOM = findDOMByVNode(VNode);
// 删除 dom 节点
if (currentDOM) currentDOM.remove();
}
也就是说,我们应该在 removeVNode 函数中调用 componentWillUnmount 函数
那我们就在这里面调用 componentWillUnmount 函数,完成组件卸载
js
function removeVNode(VNode) {
// 找到 dom 节点
const currentDOM = findDOMByVNode(VNode);
// 删除 dom 节点
if (currentDOM) currentDOM.remove();
// 如果存在生命周期函数 componentWillUnmount 函数,调用
if (VNode.classInstance && VNode.classInstance.componentWillUnmount)
VNode.classInstance.componentWillUnmount();
}
shouldComponentUpdate
shouldComponentUpdate 是在 props 或者 state 变化时,决定是否需要更新组件:
- 如果函数返回
true,则会继续执行更新操作 - 如果函数返回
false,则不会执行更新操作
- 界面展示不受到
props和state的变化的影响的时候使用 - 默认行为是返回
true,也就是需要更新 - 返回
false,render和componentDidUpdate都不会执行 - 该函数在
render函数执行之前调用 - 初始化渲染,或者执行
forceUpdate的时候,不会调用该函数 - 仅仅作为性能优化的手段,不建议手动编写,而是使用
PureComponent
通过上面分析,我们知道 shouldComponentUpdate 函数会在 componentDidUpdate 函数之前调用
componentDidUpdate 函数是在 Component.update 函数中调用,再往上一层就是 Updater.launchUpdate 函数调用
通过官方文档得知,shouldComponentUpdate 调用时,props 和 state 还没有更新完成,也就是说最新的 props 和 state 是通过 shouldComponentUpdate 的参数获取
如何在 launchUpdate 中获取到最新的 props 和 state 呢?
nextState 比较好获取,是因为 launchUpdate 本身就是在做 state 合并
这是之前的代码:
js
ClassComponentInstance.state = pendingStates.reduce((state, partialState) => {
return { ...state, ...partialState };
}, ClassComponentInstance.state);
先不直接给实例更新 state
js
let nextState = pendingStates.reduce((state, partialState) => {
return { ...state, ...partialState };
}, ClassComponentInstance.state);
那 nextProps 哪里来呢?
我们看下代码中在哪里调用了 launchUpdate
一个是在 setState 时调用的,一个是在类组件更新时调用 updateClassComponent
setState 中调用肯定是是拿不到 nextProps,所以只可能在 updateClassComponent 调用时处理 nextProps
js
function updateClassComponent(oldVNode, newVNode) {
// 对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps 和 oldProps
const classInstance = (newVNode.classInstance = oldVNode.classInstance);
classInstance.updater.launchUpdate();
}
nextProps 可以通过 newVNode.props 中获取,因为 props 都是挂载在 VNode 上的
所以代码改造如下
js
function updateClassComponent(oldVNode, newVNode) {
// 对于当前界面,旧的实例是与页面上已渲染的组件是相对应的,在生命周期函数中,会尝试比较 newProps 和 oldProps
const classInstance = (newVNode.classInstance = oldVNode.classInstance);
// 将 nextProps 传递给 launchUpdate
classInstance.updater.launchUpdate(newVNode.props);
}
现在 nextProps 和 nextState 都有了,调用 shouldComponentUpdate 函数,传入 nextProps 和 nextState
通过变量 isShouldUpdate 来判断是否需要调用 Component.update 函数
js
class Updater {
launchUpdate(nextProps) {
// 用来控制是否需要调用 update 方法
let isShouldUpdate = true;
// 调用 shouldComponentUpdate 生命周期函数,如果返回 true,将 isShouldUpdate 设置为 false
if (
ClassComponentInstance.shouldComponentUpdate &&
!ClassComponentInstance.shouldComponentUpdate(nextProps, nextState)
) {
isShouldUpdate = false;
}
// 如果 isShouldUpdate 为 false,则不用调用 update
if (isShouldUpdate) {
// 更新视图,将 preProps 和 preState 传递给 update 函数
ClassComponentInstance.update(preProps, preState);
}
}
}
getDerivedStateFromProps
getDerivedStateFromProps 函数是在 render 函数执行之前调用
主要作用是根据 props 来更新 state
- 在
render函数执行之前调用,同时也在shouldComponentUpdate之前调用 - 返回一个对象则更新
state,返回null表示没有任何更新 - 使用这个函数的场景很少,当
state需要随着props的变化而变化的时候才会用到,其实相当于一种缓冲机制 - 如果需要使用的时候,可以考虑用
memoization技术 - 静态函数不能访问类实例,因此多个类组件可以抽取为纯函数的公用逻辑
- 该函数在初始化挂载,更新,调用
forceUpdate都会执行,与场景无关,而UNSAFE_componentWillReceiveProps只在由于父组件导致的更新的场景下调用,组件内的setState导致的更新不会调用
那我们应该如何实现 getDerivedStateFromProps 函数呢?
上面我们实现了 shouldComponentUpdate 函数,getDerivedStateFromProps 函数在 shouldComponentUpdate 之前调用
所以在 shouldComponentUpdate 函数之前调用 getDerivedStateFromProps 函数即可
getDerivedStateFromProps 函数接收两个参数,nextProps 和 prevState
nextProps是最新的是launchUpdate函数中传递过来的prevState是Component.state,此时的Component.state还没有更新
js
class Updater {
launchUpdate(nextProps) {
// ...
// 生命周期函数 getDerivedStateFromProps
// 访问静态方法可以通过 constructor
if (ClassComponentInstance.constructor.getDerivedStateFromProps) {
// 拿到合并 props 后的 state
let newState =
// 调用 getDerivedStateFromProps 生命周期函数
ClassComponentInstance.constructor.getDerivedStateFromProps(
// 传入的 props
nextProps,
// 之前的 state
ClassComponentInstance.state
);
// 将 newState 合并到 nextState 中
nextState = { ...nextState, ...newState };
}
// 生命周期函数 shouldComponentUpdate
// 调用 shouldComponentUpdate 生命周期函数,如果返回 true,将 isShouldUpdate 设置为 false
if (
ClassComponentInstance.shouldComponentUpdate &&
!ClassComponentInstance.shouldComponentUpdate(nextProps, nextState)
) {
isShouldUpdate = false;
}
}
}
getSnapshotBeforeUpdate
getSnapshotBeforeUpdate 函数并不常用,仅仅在一些特定 UI 变化的场景才会用到
- 在
render函数执行完成生成真实DOM后,DOM挂载到页面前执行 - 使得组件在
DOM发生变化之前可以获取一些信息 - 返回的任何值都会作为
componentDidUpdate的第三个参数传入
通过上面分析 getSnapshotBeforeUpdate 函数在 render 之前调用,并且它的返回值作为 componentDidUpdate 的第三个参数传入
js
class Component {
update(preProps, preState) {
// 调用 getSnapShotBeforeUpdate 生命周期函数,传入 preProps 和 preState
let snapshot =
this.getSnapshotBeforeUpdate &&
this.getSnapshotBeforeUpdate(preProps, preState);
// 调用 render 方法,得到新的 VNode
let newVNode = this.render();
// 如果存在生命周期函数 componentDidUpdate 函数,调用,并传入 preProps 和 preState
if (this.componentDidUpdate)
// 将 snapshot 作为第三个参数传递给 componentDidUpdate
this.componentDidUpdate(preProps, preState, snapshot);
}
}
这个函数实现还是比较简单的
这里要重点讲一下 deepClone 函数
之前我们拿 prevState 和 prevProps 时,是直接从 Component 中取的,这里最好要做一下深拷贝
js
const preState = ClassComponentInstance.state;
const preProps = ClassComponentInstance.props;
做深拷贝,就需要单独写一个深拷贝的函数 deepClone
深拷贝本质就是调用该数据的 toString 方法
先定义一个 getType 函数,用来获取数据 toString 后的类型
js
function getType(obj) {
let typeMap = {
"[Object Boolean]": "boolean",
"[Object Number]": "number",
"[Object String]": "string",
"[Object Function]": "function",
"[Object Array]": "array",
"[Object Date]": "date",
"[Object RegExp]": "regExp",
"[Object Undefined]": "undefined",
"[Object Null]": "null",
"[Object Object]": "object",
};
return typeMap[Object.prototype.toString.call(obj)];
}
这里的深拷贝只考虑了数组和对象,其他的类型可以自己扩展
js
const deepClone = (data) => {
let type = getType(data);
let resultValue;
// 如果不是数组或者对象,直接返回
if (type !== "array" && type !== "object") return data;
// 对数据进行深拷贝
if (type === "array") {
resultValue = [];
// 遍历数组,递归调用 deepClone
data.forEach((item) => {
resultValue.push(deepClone(item));
});
return resultValue;
}
// 对对象进行深拷贝
if (type === "object") {
resultValue = {};
// 遍历对象,递归调用 deepClone
for (const key in data) {
// 只拷贝对象自身的属性,不拷贝原型链上的属性
if (data.hasOwnProperty(key)) {
resultValue[key] = deepClone(data[key]);
}
}
return resultValue;
}
};
总结
6 个生命周期函数:
componentDidMount- 它是在组件挂载到页面上后调用
- 可以操作
DOM,也就是说可以使用document.getElementById(xxx) - 可以进行网络请求
- 可以做事件订阅,但需要在
componentWillUnmount中取消订阅 - 不适合在这里调用
setState,会触发一次更新,state初始值最好在constructor中赋值
componentDidUpdate- 更新完成后调用,初始化渲染不会调用
- 当组件完成更新,需要对
DOM进行某种操作的时候,适合在这个函数中进行 - 当
props有所变化时,可以进行一些操作,比如网络请求 - 这里虽然可以调用
setState,但这是有条件的调用,否则会陷入死循环 - 如果
shouldComponentUpdate返回false,componentDidUpdate不会执行 - 如果实现了
getSnapshotBeforeUpdate,componentDidUpdate会接收第三个参数
componentWillUnmount- 组件从
DOM树上卸载完成的时候调用该函数 - 执行一些清理操作,比如清除定时器,取消事件订阅,取消网络请求等
- 不要在这里调用
setState,不会产生任何效果,卸载后不会重新渲染
- 组件从
shouldComponentUpdate- 界面展示不受到
props和state的变化的影响的时候使用 - 默认行为是返回
true,也就是需要更新 - 返回
false,render和componentDidUpdate都不会执行 - 该函数在
render函数执行之前调用 - 初始化渲染,或者执行
forceUpdate的时候,不会调用该函数 - 仅仅作为性能优化的手段,不建议手动编写,而是使用
PureComponent
- 界面展示不受到
getDerivedStateFromProps- 在
render函数执行之前调用,同时也在shouldComponentUpdate之前调用 - 返回一个对象则更新
state,返回null表示没有任何更新 - 使用这个函数的场景很少,当
state需要随着props的变化而变化的时候才会用到,其实相当于一种缓冲机制 - 该函数在初始化挂载,更新,调用
forceUpdate都会执行,与场景无关,而UNSAFE_componentWillReceiveProps只在由于父组件导致的更新的场景下调用,组件内的setState导致的更新不会调用
- 在
getSnapshotBeforeUpdate- 在
render函数执行完成生成真实DOM后,DOM挂载到页面前执行 - 使得组件在
DOM发生变化之前可以获取一些信息 - 返回的任何值都会作为
componentDidUpdate的第三个参数传入
- 在