我们上一节了了解了函数式组件和类组件的处理方式,本质就是处理基于 babel
处理后的 type
类型,最后还是要处理虚拟 dom
。本小节我们学习下组件的更新机制。
我们知道我们定义类组件的时候,只能通过 setState
方式修改状态值,这样页面才会重新渲染。如果你直接修改 state
,其实没有什么作用的。
实现简版更新机制
我们先写下 Counter
的例子,点击加一,如下:
// src/index.js
class Counter extends React.Component {
constructor(props) {
super(props);
// 类的构造函数中 唯一给 state 赋值的地方
this.state = {
number: 0,
};
}
handleClick = () => {
// 明显 setState 是继承来的
this.setState({
number: this.state.number,
});
};
render() {
return (
<div>
<p>{this.props.title}</p>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>plus</button>
</div>
);
}
}
ReactDOM.render(<Counter title="加法" />, document.getElementById("root"));
实现 setState
代码语言:txt复制// src/Component.js
export class Component {
constructor(props) {
// react 的实例单独注册了一个更新器,回来统一处理 state,类似写函数嵌套多了,把不同功能单独提出去
this.updater = new Updater(this) // 把组件实例传入
}
setState(partialState) {
// 我们可以写多个 setState 方法,react 会统一处理,所以很明显使用一个栈存储的
this.updater.addState(partialState)
}
}
class Updater{
constructor(classInstance) {
this.classInstance = classInstance
this.pendingStates = []
}
addState(partialState) {
// 状态存储
this.pendingStates.push(partialState)
// 触发更新
this.emitUpdate()
}
emitUpdate() {
this.updaetComponent()
}
// 组件更新原理
//1.计算新的 state
//2.重新执行render
//3.得到新的虚拟dom,真实dom
//4.覆盖重新挂载
updaetComponent() {
const {calssInstance, pendingStates} = this
if (pendingStates.length) {
// 获取新的状态
let newState = this.getState()
// 查看是否更新
showUpdate(classInstance, newState)
}
}
getState() {
const {classInstance, pendingStates} = this
// 得到老状态
let {state} = classInstance
// 合并状态
pendingStates.forEach(nextState => {
state = {
...state,
...nextState
}
})
// 清空
pendingStates.length = []
return state
}
}
function showUpdate(classInstance, newState) {
classInstace.state = newState // 用新的状态 直接覆盖组件实例的状态
classInstance.forceUpadte() // 强制更新, 此方法在父组件上
}
强制更新
代码语言:txt复制// Components.js Component 类
// 这里的逻辑是 获取老的真实dom,获取新的虚拟dom 生成的真实dom,使用 replaceChild 方法,用新的dom替换旧的真实dom
forceUpdate() {
let oldRenderVdom = this.oldRenderVdom
let oldDOM = finDOM(oldRenderVdom) // 根据虚拟 dom 获取真实dom
let newRenderVdom = this.render()
compareTowVdom(oldDOM.parentNode, oldDOM, newRenderVdom)
this.oldRenderVdom = newRenderVdom
}
这里可能有的朋友有疑问,旧的虚拟 dom
,旧的真实 dom
,那里获取的呢,我们还的改动下上一小节的代码:
// react-dom.js
function createDOM(vdom) {
...
vdom.dom = dom // 我们把得到的真实 dom,添加到虚拟 dom 对象上
...
}
// 函数组件 babel 把属性转为 props 对像
function mountFunctionComponent(vdom) {
const { type, props } = vdom;
// 函数式组件 type 是个 函数 返回虚拟函数
let renderVdom = type(props);
// 把旧的 渲染的虚拟dom 存起来 ///////////// 看这里
vdom.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
// 类组件
function mountClassComponent(vdom) {
const { type, props } = vdom;
const classInstance = new type(props);
const renderVdom = classInstance.render();
// 把旧的 渲染的虚拟dom 存储 ///////////// 看这里
classInstance.oldRenderVdom = vdom.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
通过上面添加的代码,我们已经在虚拟 dom
上和类的实例上绑定了虚拟 dom
,所以回过头来我们可以在 forceUpdate
方法中获取旧的虚拟 dom
,那如何拿到旧的真实 dom
呢?
// react-dom.js
function findDOM(vdom) {
if (!vdom) return null
if (vdom.dom) return vdom.dom // 我们在上面 createDOM 中做了绑定
// 如果没有虚拟dom
let renderVdom = vdom.oldRenderVdom
return findDOM(renderVdom)
}
什么情况下会没有虚拟
dom
呢?例如function One() {return <h1>123<h1/>}
,function Two() {return <One />}
,我们调用Two
函数的时候,得到的就不是虚拟dom
,这个时候就需要继续递归findDOM
。
实现 compareTwoVdom
进行 dom
替换
// react-dom.js
function compareTwoVdom(parentDOM, oldDOM, newVdom) {
let newDOM = createDOM(newVdom) // 我们执行 render 得到的新的虚拟 dom
parentDOM.replace(newDOM, oldDOM) // 完成替换
}
这里遗漏了一个小问题,我们没有处理点击事件,我们需要在属性中判断是否是 on
开头的变量:
// react-dom.js
function updateProps() {
....
if (/^on[A-Z].*/.test(key)) {
// 转成 js 原生的小写
dom[key.toLowerCase()] = newProps[key]
}
...
}
我们在入口文件中改用我们自己写的代码,发现效果一样。当让这里这是简单的实现完全的 dom
替换,没有对 setState
做异步处理,但是我们已经能理解 react
类组件的更新原理。
我们下一小节实现批量更新和合成事件,如果有不对,欢迎指正!