最近在学习慕课网 手写 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
函数接收三个参数:
prevProps
prevState
snapshot
(这里暂时先不考虑这个参数,在后面实现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
的第三个参数传入
- 在