一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第7天,点击查看活动详情。
上一节我们学习了 react 类组件的生命周期实现,其实就是基于 js 单线程机制,在操作的的节点插入自己的实现,唯一的方法。vue 也是同样的道理,但是 vue 做了数组合并处理。本节我们了解下含有子组件时的生命周期实现。
定义组件结构
父组件如下,我们主要关注打印结果
// src/index.js
class Counter extends React.Component {
// 如果没有 shouldComponentUpdate 的 nextProps 属性默认会打印这里,影响到不大
static defaultProps = {
name: "aa",
};
constructor(props) {
super(props);
this.state = {
number: 0,
};
console.log("init");
}
componentWillMount() {
console.log("willMount");
}
componentDidMount() {
console.log("didMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("shouldUpdate", nextProps, nextState);
return nextState.number % 2 === 0; // 返回 boolean
}
componentWillUpdate() {
console.log("willUpdate");
}
componentDidUpdate() {
console.log("didUpdate");
}
handleClick = () => {
this.setState({
number: this.state.number + 1,
});
};
render() {
console.log("render");
return (
// 没有判断 <>
<div>
<div>{this.state.number}</div>
<p>————————————————————-</p>
{this.state.number === 4 ? null : (
<Child count={this.state.number}></Child>
)}
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
子组件结构如下,观察状态改变后打印结果
// src/index.js
class Child extends React.Component {
constructor(props) {
super(props);
this.state = {
number: 0,
};
console.log("child init");
}
componentWillMount() {
console.log("child willMount");
}
componentDidMount() {
console.log("child didMount");
}
shouldComponentUpdate(nextProps, nextState) {
console.log("child shouldUpdate", nextProps, nextState);
return nextState.count % 3 === 0; // 返回 boolean
}
componentWillUpdate() {
console.log("child willUpdate");
}
componentDidUpdate() {
console.log("child didUpdate");
}
componentWillReceiveProps(newProps) {
console.log("child componentWillReceiveProps");
}
componentWillUnmount() {
console.log("child will unmount");
}
render() {
console.log("child render");
return <div>{this.props.count}</div>;
}
}
官方库的打印如下(mac 如果有好用的 gif 录制软件谢谢推荐):
实现
我们注意到子组件有 shouldComponentUpdate 和 componentWillReceiveProps 两个不同的生命周期,componentWillUnmount 也在父组件中有判断是否显示子组件。所以我们从 setState 方法入手,找到 forceUpdate 方法内部的 compareTwoVdom 方法,跟进数据流动。
注意:因为我们本节涉及到子组件的挂载和删除,所以这里做一个简单的
diff比较,只跟索引绑定一对一对比,dom类型相同直接复用,否则删除。详细的diff算法会在后面的章节中书写。
// src/react-dom.js
我们上一节中对组件进行了完全替换,这样就不能很好的对比子组件也不好拿到新的属性,需要改写
export function compareTwoVdom(parentDOM, oldVdom, newVdom) {
let newDOM = createDOM(newVdom);
parentDOM.replaceChild(newDOM, oldDOM);
}
// 大家看的时候顺序看,广度的看,不要纠结一个方法看到底。捋清思路
// 简单的 diff 对比。[1, 2, 3] [1,3] 从头到尾一一对比
function compareTwoVdom(parentDOM, oldVdom, newVdom) {
// 没有新 老节点
if (!oldVdom && !newVdom) return null
// 老节点存在,没有新节点, 直接删除
if (oldVdom && !newVdom) {
unMountVdom(oldVdom) // 下面实现
} else if (!oldVdom && newVdom) {
// 没有老节点,有新节点,新增
let newDOM = createDOM(newVdom) // 创建新 dom
parentDOM.appendChild(newDOM) // 插入新dom (有点小问题看大家能否发现)
// 上一行已经挂载到页面了,所以如果有 didmount 方法,直接执行
if (newDOM.componentDidMount) {
newDOM.componentDidMount()
}
} else if(oldVdom && newVdom && oldVdom.type !== newVdom.type) {
// 新老节点 类型不同
unMountVdom(oldVdom) // 直接卸载 老节点
let newDOM = createDOM(newVdom);
parentDOM.appendChild(newDOM); // bug
if (newDOM.componentDidMount) {
newDOM.componentDidMount();
}
} else {
// 新的有 老节点也有,类型也一样,需要复用
updateElement(oldVdom, newVdom)
}
}
实现辅助方法
- 卸载
function unMountVdom(vdom) {
let {classInstance, props, ref} = vdom
let currentDOM = findDOM(vdom)
// 将要卸载方法 相信大家应该有感觉了,能理解了
if (classInstance && classInstance.componentWillUnmount) {
classInstance.componentWillUnmount()
}
// 引用类型 都要清空,会影响
if(ref) {
ref.current = null
}
// 递归卸载子, 而没有直接清空父组件innerhtml,子里面可能还有类组件,继续执行卸载方法
if (props.children) {
let children = Array.isArray(props.children) ? props.children : [props.children]
children.forEach(unMountVdom)
}
// 从父组件中移除
if(currentDOM) currenDOM.parentNode.removeChild(currentDOM)
}
- 节点复用更新
function updateElement(oldVdom, newVdom) {
if(oldVdom.type === REACT_TEXT) {
// 文本节点 内容不同直接替换
let currentDOM = newVdom.dom = findDOM(oldVdom)
if (oldVdom.props !== newVdom.props) {
currentDOM.textContent = newVdom.props
}
} else if (typeof oldVdoml.type === 'string') {
// 原生标签 div。p h1
let currentDOM = newVdom.dom = findDOM(oldVdom)
// 更新属性, 我们上节课写过
updateProps(currentDOM, oldVdom.props, newVdom.props)
// 递归的对比子
updateChildren(currentDOM, oldVdom.props.children, newVdom.props.children)
} else if (typeof oldVdom.type === 'function') {
// 函数组件或类组件
if (oldVdom.type.isReactComponent) {
updateCLassComponent(oldVdom, newVdom)
} else {
updateFunctionComponent(oldVdom, newVdom)
}
}
}
- 更新子节点
function updateChildren(parentDOM, oldVChildren, newVChildren) {
// 子可能是数组可能是对象
oldVChildren = Array.isArray(oldVChildren) ? oldVCHIldren : [oldVChildren]
newVChildren = Array.isArray(newVCHildren) ? newVCHIldren : [newVChildren]
// 获取长度最大值
let maxLen = Math.max(oldVChildren.length, newVChildren.length)
for(let i = 0;i<maxLen;i++){
// 递归就是父已经实现了,获取到子,再重新执行一遍父执行过的方法
compareTwoVdom(parentDOM, oldVChildren[i], newVChildren[i])
}
}
- 更新类组件
function updateClassComponent(oldVdom, newVdom) {
// 实例可以复用
const classInstance = newVdom.classInstance = oldVdom.classInstance
if (classInstance.componentWillReceiveProps) {
// 子组件更新,可以获取属性
classInstance.componentWillReceiveProps()
}
// 触发类的更新,传入新的属性
classInstace.updater.emiUpdate(newVdom.props)
}
- 更新函数组件
function updateFunctionComponent(oldVdom, newVdom) {
let currentDOM = findDOM(oldVom)
if (!currentDOM) return null
let {type, props} = newVdom
// 函数组件的更新就在于重新执行一遍函数获取新的虚拟dom
let newRenderVdom = type(props)
newVdom.oldRenderVdom = newRenderVdom
}
我们修改下类组件更新出发的方法
// src/component.js
emitUpdate(nextProps) {
// 我们对新属性做下存储,更新时获取
this.nextProps = nextProps;
....
updateComponent() {
const { classInstance, pendingStates, nextProps } = this;
// 等待更新的状态有多个 有新属性 或者新状态就会更新
if (nextProps || pendingStates.length) {
// 获取新状态
let newState = this.getState();
shouldUpdate(classInstance, nextProps, newState); // 是否更新
}
...
}
function shouldUpdate(classInstance, nextProps, newState) {
...
if (
classInstance.shouldComponentUpdate &&
// newProps 后面在加
!classInstance.shouldComponentUpdate(
nextProps || classInstance.constructor.defaultProps, // 这里返回新的属性,没有的话返回构造函数的默认属性
newState
)
) {
willUpdate = false;
}
if (willUpdate && classInstance.componentWillUpdate) {
classInstance.componentWillUpdate();
}
// 实例属性重新赋值
if (nextProps) {
classInstance.props = nextProps;
}
...
}
我们自己的代码实现如下:和原生的打印结果一样
大家可能会有疑问,我都有 vdom 了,为什么还有个 renderVdom 的概念呢?vdom 指的是我们引用的如
<Counter />的解析结果,renderVdom是具体的render方法返回的虚拟dom,两者是不同的
至此我们的子组件生命周期基本实现完了,整片文章代码居多,但是必要的地方都写了注视,相信看过之前文章的小伙伴也可以梳理清楚。如果真的有疑问,可以在面留言,我会进行解答的。写一小节我们写出真实的 diff 算法,加深大家对虚拟 dom 的认识,谢谢阅读!