state
/setState
源码解析
引言
前置知识
文章中涉及到的知识都是渐进式的讲解开发,当然如果对之间内容不感兴趣(已经了解),也可以直接切入本文内容,每一个章节都和之前不会有很强的耦合。
文章中涉及的代码地址, 戳这里👇查看。
文章中的内容会分为两个步骤:
- 解析
React
中setState
的解析流程。 - 实现
React
中setState
触发页面重新渲染。 - 合成事件和批量异步
state
更新。
虽然说
react
目前已经不推荐class component
,但是react
官方也明确表示并不会移除class
。因为class
组件聚集了太多react
思想,所以我们先渐进式的从class
组件入手之后再来理解FC
就会容易很多了,所以现在我们可以不使用它,但是永远不要小瞧class component
!
setState
原理流程
引言
之前的文章中我们实现了从零渲染babel
转译后的jsx
元素和组件。
在React.js
中我们实现了这样的代码:
import { transformVNode } from './utils';
import { Component } from './component';
const React = {
// 重点是Component
Component,
// 这个逻辑是之前的jsx原理 和本章关联不大 可以直接使用
createElement: function (type, config, children) {
const props = {
...config,
};
if (arguments.length > 3) {
props.children = Array.prototype.slice
.call(arguments, 2)
.map(transformVNode);
} else {
props.children = transformVNode(children);
}
return {
type,
props,
};
},
};
export default React;
我们都知道在React
中所有类组件都需要extends React.Component
。
首先,我们要明白这里的Component
就是在实现它的父Component
。
Component
当我们在class component
中调用setState
时,其实我们自定义的组件上并没有setState
这个方法吧。换而言之我们首先需要实现的就是这个setState
方法定义。
此时我们在react.js
同级别目录新建一个component.js
,并且引入它。
component.js
:
class Component {
constructor(props) {
this.props = props;
// 默认state
this.state = {};
}
// 我们说过第二个参数callback是在页面更新后才会执行
setState(partialState, callback) {
//
}
}
// 实现类组件独有属性
Component.prototype.isReactComponent = {};
export { Component };
上边这段代码,
Component.prototype.isReactComponent
我们之前说过在react
源码中class cmp
和function cmp
经过处理后他们的vdom
对象上的type
属性都是函数类型(一个指向自身函数,一个指向自身class
类)。
通过原型上是否存在
isReactComponent
属性来区分这两个组件。
紧接着来看其他代码,Component
类的构造函数中接受子组件(继承时super
)传递的props
,这不是现在的重点,我们先不关注他。
之后我们在父类上定义了setState
方法和state
默认空对象。
当然我们都清楚setState
中可以接受两个参数,所以这里我们接受了两个参数作为入参。
Updater
...
// 我们说过第二个参数callback是在页面更新后才会执行
setState(partialState, callback) {
this.updater.addState(partialState, callback);
}
看到这段代码你可以不解,this.updater
是一个什么东西?
首先,在React.component
中是不存在任何状态的调度,换而言之它内部并不会控制你到底是同步还是异步更新,它仅仅负责根据组件state
管理组件渲染。
所以每个组件内部都存在一个updater
实例对象作为控制state
更新器。
每次调用setState(partialState,callback?)
本质上都会传递给自身的this.updater.addState(partialState,callback)
。
让我们来完善这段代码:
// 更新器类
class Updater {
constructor(componentInstance) {
this.instance = componentInstance;
this.pendingState = []; // 异步setState存储的队列
this.callbacks = []; // 缓存批量执行回调函数
}
}
class Component {
constructor(props) {
this.props = props;
// 默认state
this.state = {};
// 每个类组件的实例上 存在一个自己的更新器Updater
this.updater = new Updater(this);
}
// 我们说过第二个参数callback是在页面更新后才会执行
setState(partialState, callback) {
this.updater.addState(partialState, callback);
}
}
我们可以看到在Component
组件构造函数中,我们创建了一个updater
实例对象并且传入了组件实例作为参数。
- 同时
Updater
这个类的构造函数中接受了组件实例this.instance
。 - 定义了一个
pendingState
,也就是之前我们讲到每一次setSetate
都会将新的state
推入一个队列中。没错,就是它。 - 定义了一个
this.callbacks
,setState
的第二个参数支持一个可选的回调函数,这里我们用callbacks
来缓存。
Updater
接下来我们来实现addState
方法,也就是每次调用setState
的参数都会转发给component
内部的updater
实例的addState
方法。
首先我们能想到的是addState
方法要做的一定是将最新的setState
修改推入栈,以及将callback
推入(如果存在的话)。
class Updater {
...
// 更新状态
addState(partialState, callback) {
this.pendingState.push(partialState);
if (isPlainFunction(callback)) {
this.callbacks.push(callback);
}
// 当我们推入队列完成之后
// emitUpdate()函数触发
this.emitUpdate();
}
}
在使用的时候,我们知道每次setSetate
都有可能会造成页面更新,所以在每次setState
执行完毕后我们需要去调用触发更新。
可以看到在updater
的addState
添加完setState
后,内部调用了emitUpdate()
这个方法去触发更新。
// props/state变化触发更新
emitUpdate() {
// 这里后来会添加批量更新的判断 判断是否是批量更新
this.updateComponent();
}
emitUpdate
这里有两个问题需要解释下。
-
updateComponent()
方法是真正去调用组件更新,因为我们这里只处理了state
,我们清楚还有props
改变也会造成页面渲染。这里多层转发只是为了以后方便扩展。 -
之后我们会在
updateComponent()
函数中判断是批量异步更新还是同步更新,这里我们先处理同步。也就是每次setState()
之后就会触发页面重新渲染。
我们去实现更新逻辑:
// updateComponent
class Updater {
...
// 让组件更新
updateComponent() {
const { classInstance, pendingState } = this;
// 存在等待更新
if (pendingState.length > 0) {
// 让组件进行更新
shouldUpdate(classInstance, this.getState());
}
}
}
可以看到当pendingState
中存在的时候,我们来调用shouldUpdate
方法去更新组件实例。
传入了两个参数,分别是
-
this.classInstance
,这个参数是class
组件实例。 -
第二个参数是合并后的
state
,这里我们通过实例的this.getState()
方法去合并得到最终的state
。
我们先放下shouldUpdate()
这个方法,先来看看this.getState()
方法。
这个方法其实现在很简单,我们需要将penddingState
中的每次setState
和组件内部(旧的)state
进行合并。
需要注意的是
setState
第一个参数有可能是一个callback
。
那么我们就来实现它:
// utils
const toString = (value) => Object.prototype.toString.call(value);
const isPlainObject = (value) => toString(value) === '[object Object]';
const isPlainString = (value) => toString(value) === '[object String]';
const isPlainNumber = (value) => toString(value) === '[object Number]';
export const isPlainFunction = (value) =>
toString(value) === '[object Function]';
// Updater
class Updater {
// 获取当前state
getState() {
let { state } = this.classInstance; // old State
const { pendingState } = this; // new State
// reduce去累加执行state的变化
pendingState.reduce((preState, newState) => {
// 如果是函数
if (isPlainFunction(newState)) {
state = { ...preState, ...newState(preState) };
} else {
state = { ...preState, ...newState };
}
}, state);
// 这里应该是页面渲染后在调用callbacks 这里先暂时放在这里
this.callbacks.forEach((cb) => {
cb();
});
// 清空
pendingState.length = 0;
this.callbacks.length = 0;
return state;
}
}
此时我们的this.getState()
方法,返回的就是清空当前pendingState
队列,并且返回一个最新的state
值。
接下来我们去实现一下shouldUpdate
这个方法:
// 接受两个参数 一个是组件实例instance 一个是最新的state
function shouldUpdate(instance, state) {
instance.state = state; // 内部真正修改实例的state
// 调用实例的方法进行重新渲染
instance.forceUpdate();
}
shouldUpdate
方法内部其实做的事情很简单,
instance.state=state
,修改组件实例内部的state
变为最新的state
。- 调用组件内部的
forceUpdate()
方法来更新组件重新渲染。
其实这里我们也就明白平常如果我们直接修改
this.state = { ... }
是不会触发页面重新渲染,因为直接修改state
的话,这里并不会updater
任何方法自然也不会调用组件更新方法。
setState
流程
其实我们可以看到目前为止整个流程还是非常清晰的:
setState
的流程还是非常清晰的,接下来我们重点进入实现react
中setState
是如何触发页面更新的
React
中setState
页面重新渲染
接下来我们重点实现一下forceUpdate()
这个实例方法。
我们先从思路上来讲解这个方法需要做的事情,代码其实并不是很多,主要就是更新的思路过程:
state改变rerender流程
当我们需要调用forceUpdate()
方法最主要的事就是通过state
变化重新调用render()
生成新的vDom
,然后和旧的vDom
对象进行dom-diff
从而进行对比更新页面真实DOM
元素,主要思路为下面几个步骤:
- 我们需要旧的
Vdom
对象。 - 通过旧的
Vdom
对象我们拿到当前页面上这个Vdom
渲染的真实DOM
元素,以及它的parentNode
。 - 获取最新的
Vdom
对象,通过重新调用render
方法获得。 进行,这一步我们先省略。Dom-diff
- 通过之前的
createDom
方法,生成新的真实dom
元素。 - 调用
parentNode.reaplce(newDom,oldDom)
完成组件替换重新渲染页面。
大致看看这个流程,本质上就是通过vdom
查找/生成真实DOM
然后找到父节点,通过父节点去替换。
renderVdom
& Vdom
在开始实现之前我们要先掌握两个概念,分别是renderVdom
和Vdom
。
你可以将Vdom
看成一个大的集合,而renderVdom
是它其中的一个子集。实质上他们都是Vdom
对象,只是分别代表不同的含义。
来看看这段代码:
class MyComponent extends React.Component {
render() {
return <div>wang.haoyu</div>
}
}
const element = <MyComponent />
ReactDOM.render(element)
上边这个代码中,<MyComponent>
就是一个Vdom
对象。那什么是renderVdom
呢?
比如一个class
组件,这个组件的vdom
并不会真实挂载在dom
节点上,他的实例的render
方法返回的元素就叫做renderVdom
。
MyComponent
是一个class
组件,经过babel
转椅后React.createElement
返回一个type
为自身的vdom
。
而MyComponent
实例的render
方法返回的是一个<div>wang.haoyu</div>
这段jsx
同样会经过babel
处理后成为vdom
。不过这个div
节点会转化为真实dom
挂载在页面上。我们就称他为renderVdom
对象。
实现
获得oldRenderVDom
要获取组件的更新,我们首先要找到对应组件的Vdom
对象。
forceUpdate() {
// 通过实例上的`oldRenderVDom`获得旧的`renderVdom`对象
const oldRenderVDom = this.oldRenderVDom
}
你可能会疑惑this.oldRenderVDom
是哪里来的。
上一节jsx
原理中我们实现了reactDOM.render(vdom)
这个方法,这个方法将vdom
进行递归生成真实的dom
节点。
当遇到class
组件时,去执行了mountclassComponent
这个方法。
// 挂载ClassComponent
function mountClassComponent(vDom) {
const { type, props } = vDom;
const instance = new type(props);
const renderVDom = instance.render();
// 考虑根节点是class组件 所以 vDom.oldRenderVDom = renderVDom
instance.oldRenderVDom = vDom.oldRenderVDom = renderVDom; // 挂载时候给类实例对象上挂载当前RenderVDom
return createDom(renderVDom);
}
我们在mountClassComponent
方法中,new [Class](props)
创建了组件的实例对象,并且通过instance.render()
获得了组件返回的renderVdom
对象。
此时我们将instance.oldRenderVDom = renderVDom
,将组件render
返回的renderVdom
挂载在了实例this
上,此时我们在父类中当然可以通过this.oldRenderVDom
拿到当时的renderVDom
对象。
如果你没有看过之前的文章,只需要记住在
classComponent
实例上是存在一个oldRenderVDom
属性,指向它上一个调用this.render()
返回的renderVDom
对象即可。
renderOldVDom
获得真实Dom
接下来我们需要通过这个旧的renderOldVDom
对象去获得它对应页面上的真实DOM
元素。细心的同学可以已经发现了,之前我们在createDom
这个将vDom
变化成为真实Dom
对象时,给每一个Vdom
对象上挂载了一个dom
属性,它的指向就是对应生成的Dom
。
有了这个dom
属性,我们想要通过vdom
去获得对应页面上的真实dom
节点就简单了。
forceUpdate() {
// 通过实例上的`oldRenderVDom`获得旧的`renderVdom`对象
const oldRenderVDom = this.oldRenderVDom
// 通过findDom方法将renderVDom找到对应的真实Dom节点
// 并且获得对应组件渲染的真实节点
const parentDom = findDOM(oldRenderVDom).parentNode;
}
我们来实现一下findDom
这方法,将它放在之前的reactDom.js
文件中:
/**
* 真实源码中非常复杂
* 简化下根据VDom返回真实Dom节点
* 需要额外注意的是 如果renderVDom是class或者Function那么他并不是真实的渲染节点 继续递归查找
* 如果是普通Dom节点 直接返回挂载的dom属性
* @param {*} vdom
*/
export function findDOM(vDom) {
const { type } = vDom;
if (typeof type === 'function') {
// 非普通DOM节点 是class组件或者functionComponent
return findDOM(vDom.oldRenderVDom);
} else {
return vDom.dom;
}
}
其实它的实现非常简单,本质上就是通过vdom
的dom
属性去查找真实的DOM
。需要额外注意的一点是,我们需要查找的renderVDom
如果内部仍然具有funcitonComponent/classComponent
的话我们要一直去递归,知道查找到它真实渲染在页面上对应的vdom
元素。
因为你可以看到下面这段代码,在type
是函数的时候(表示他是一个fc/class component
)直接进行了return
并不会进行挂载dom
属性。
再来看看这两个方法mountFunction/mountClassComponent
:
在调用函数组件,类组件时:
-
如果是类组件,我们给它的实例对象上以及类本身挂载
oldRenderVDom
属性,指向它的实例render()
返回的renderVDom
对象。 -
如果是
FC
,我们调用FC
将它返回的renderVdom
挂载在函数自身上。
这样最终我们在查找的时候,如果碰到FC
或者classComponent
,我们只需要继续递归去它的oldRenderVDom
属性上去查找真实可以渲染到页面上的vdom
对象。
当我们拿到这个renderVDom
对象时,再通过renderVDom.dom
就可以拿到它对应在页面上渲染的真实DOM
元素。然后通过parentNode
获得他的父亲容器。
获取最新的Vdom
对象
这一步其实很简单,我们在forceUpdate()
方法中已经更新了this
上最新的state
。我们只需要重新调用实例的render()
方法就可以返回最新的组件实例了。
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
// 真实挂载DOM节点
const parentDom = findDOM(oldRenderVDom).parentNode;
// 重新调用render()方法 然后获取最新的Vdom对象
const newRenderVDom = this.render();
}
进行Dom-diff
这一步我们这里先省略,之后在补充这里的内容。
先进性简单粗暴的组件整体替换,把流程走通。
我们先来定义这个渲染以及diff
的方法,他在react-dom.js
中:
/**
* Dom-diff比较更新 (暂时不实现)
* 将更新同步到真实Dom上 (强行replace)
*/
export function compareToVDom(parentDom, oldVDom, newVDom) {
// ...
}
// Component.js
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
const newRenderVDom = this.render();
// 真实挂载DOM节点
const parentDom = findDOM(oldRenderVDom).parentNode;
// diff算法 比较差异 将差别更新到真实DOM上
compareToVDom(parentDom, oldRenderVDom, newRenderVDom);
}
通过newVdom
生成新的dom
元素 & 渲染页面
核心渲染方法在compareToVDom
这个方法中进行,这个方法是进行dom-diff
和替换页面dom
元素的核心方法。
dom-diff
先放下,那我们就来着手实现更新页面元素。(这里粗暴的直接替换--组件state
改变--重新渲染组件--将页面上组件对应的dom
替换成为新的vdom
生成的dom
对象)。
export function compareToVDom(parentDom, oldVDom, newVDom) {
// 通过调用findDOM(oldVDom) 找到oldVDom对应页面上的真实DOM节点
const oldDom = findDOM(oldVDom);
// 通过createDOM方法将新的VDom对象转化为真实DOM
const newDom = createDom(newVDom);
// 在页面上的parentNode.replace将旧的DOM元素替换成为新的Dom对象 完成页面更新
parentDom.replaceChild(newDom, oldDom);
}
当然最后不要忘记更新实例上的renderVDom
对象为更新后的vdom
:
forceUpdate() {
const oldRenderVDom = this.oldRenderVDom;
const newRenderVDom = this.render();
// 真实挂载DOM节点
const parentDom = findDOM(oldRenderVDom).parentNode;
// diff算法 比较差异 将差别更新到真实DOM上
compareToVDom(parentDom, oldRenderVDom, newRenderVDom);
// 更新实例上vDom属性为最新的 注意这里是renderVDom
this.oldRenderVDom = newRenderVDom;
}
over
这里我们已经能完成---组件调用setState
----state
改变----页面对应组件更新!
这个流程目前我们已经可以run
通了,Demo
以及完成代码在这里。
但是我们目前的setState
仅仅是同步,每次调用setState
都是同步的,也就是调用一次setState
就会触发一次页面渲染。
接下来我们来实现合成事件和异步批量更新。
合成事件和异步state
上一步我们已经实现了点击触发事件->setState
->页面刷新。
当我们点击页面上的元素触发对应事件函数,函数内部通过setState
修改了state
的值并且调用实例的forceUpdate
进行了页面刷新。
但是我们现在实现的逻辑,setState
仅仅支持同步调用刷新,并不支持异步批量更新。也就是每次调用setState
都会实时更新setState
并且反应到页面上去。
这当然不是我们想要的结果,关于react
中的state
何时是异步何时同步,可以参考之前讲过的[这篇文章]
(juejin.cn/post/700074…
接下来,我们先从思路上大致梳理一下要实现的细节:
流程梳理
- 我们清楚
react
内部是通过一个变量去控制是否是异步批量更新还是同步批量更新。我们需要一个全局变量去控制更新逻辑。 - 基于事件处理函数的批量更新,我们需要"劫持"
react
中的事件处理函数,也就是将所有的事件代理到document
上去。通过document
统一执行并且添加部分前置/后置逻辑处理。 - 在事件处理函数前置条件中开启批量更新标识位(
react
内部修改全局变量)-> 执行事件处理函数(我们自己定义) -> 后置函数(react
调用,关闭标识位,执行缓存的批量更新)。 -> 刷新页面。
updateQueue
我们先来在component.js
中定义一个全局变量updateQueue
来控制是否是批量更新:
// Component.js
// 控制批量更新
export const updateQueue = {
// 控制是否是批量更新
isBatchUpdating: false,
// 缓存批量更新实例
updaters: new Set(),
// 批量更新方法
batchUpdate() {
},
};
isBatchUpdating
是控制是否开启批量更新,true
为开启。updaters
是我们定义的一个set
对象,用来缓存批量更新的实例。batchUpdate
方法是执行清空之前缓存的批量state
。
Updater
接下来我们要修改Updater
这个类,让他支持批量更新的逻辑:
// 专门管理更新调度逻辑
class Updater {
constructor(classInstance) {
this.classInstance = classInstance;
// state队列
this.pendingState = [];
// callback队列
this.callbacks = [];
}
// 更新状态
addState(partialState, callback) {
this.pendingState.push(partialState);
if (isPlainFunction(callback)) {
this.callbacks.push(callback);
}
this.emitUpdate();
}
// props/state变化触发更新
emitUpdate() {
if (updateQueue.isBatchUpdating) {
// 批量更新
updateQueue.updaters.add(this);
} else {
// 非批量更新
this.updateComponent();
}
}
// 让组件更新
updateComponent() {
const { classInstance, pendingState } = this;
// 存在等待更新
if (pendingState.length > 0) {
// 让组件进行更新
shouldUpdate(classInstance, this.getState());
}
}
// 获取当前state
getState() {
let { state } = this.classInstance; // old State
const { pendingState } = this; // new State
pendingState.reduce((preState, newState) => {
if (isPlainFunction(newState)) {
state = { ...preState, ...newState(preState) };
} else {
state = { ...preState, ...newState };
}
}, state);
// 这里应该是页面渲染后在调用callbacks 这里先暂时放在这里
this.callbacks.forEach((cb) => {
cb();
});
// 清空
pendingState.length = 0;
this.callbacks.length = 0;
return state;
}
}
我们可以看到主要修改的内容就是针对于emitUpdate
方法的修改,让他支持批量更新。
如果updateQueue.isBatchUpdating
是true
开启批量更新标识位的话那么就将当前update
实例推入updateQueue.updaters
中去。
ps: 每次调用
setState
首先会调用当前组件的updater
实例的addState(partialState,callback)
将最新的修改和callback
推入到updater
实例的pendingState
和callback
缓存进去。
我们修改了
emitUpdate
逻辑,如果开启了批量更新就将实例添加到updateQueue.updates
中去进行缓存起来。如果未开启那么就走之前的直接更新逻辑。
事件代理
我们已经实现了每次调用setState
在Updater
实例上根据isBatchUpdating
标记为来判断是否进入批量更新逻辑。
那么我们什么时候开启isBatchUpdating
,什么时候关闭呢?
接下来我们就来实现一下react
中的事件代理。
原理: 在React
中的所以事件是被代理到document
上去执行的,也就是所以事件的绑定其实是通过事件代理的方式去在document
上去执行。通过这样的方式react
可以劫持我们的事件,在事件执行函数中添加一些前置/后置逻辑。
我们先来修改之前的react-dom.js
,之前我们在针对事件处理时是直接将事件绑定在了对应的元素之上。这显然是不合理的
import { addEvents } from './events.js'
// react-dom.js
// 更新props
...
function updateProps(dom, oldProps, newProps) {
Object.keys(newProps).forEach((key) => {
if (key === 'children' || key === 'content') {
return;
}
// 处理事件 之后会使用合成事件和事件委托 之后会渐进式处理的
if (key === 'style') {
addStyleToElement(dom, newProps[key]);
} else if (key.startsWith('on')) {
// 重新定义一个addEvents方法 通过这个方法进行事件代理
addEvents(dom, key.toLocaleLowerCase(), newProps[key]);
// addEventToElement(dom, key.toLocaleLowerCase(), newProps[key]);
} else {
dom[key] = newProps[key];
}
});
}
...
接下来我们来实现addEvents
这个方法:
新建一个event.js
文件
import { updateQueue } from './component';
/**
* 实现事件委托,将所有的事件劫持绑定在根元素上
* @export
* @param {*} dom 事件元素
* @param {*} eventName 事件名称
* @param {*} eventHandler 事件函数
*/
export function addEvents(dom, eventName, eventHandler) {
// 为该元素挂载对应事件属性
let store = dom.store ? dom.store : (dom.store = {});
store[eventName] = eventHandler;
if (!document[eventName]) {
// 如果有很多个相同事件 比如click
// 那么事件委托的时候 仅仅委托一次处理
document[eventName] = dispatchEvent;
}
function dispatchEvent(event) {
let { target, type } = event;
const eventType = `on${type}`;
// 合成时间tart
const syntheticEvent = createSynthetic(target);
// 开启批量更新
updateQueue.isBatchUpdating = true;
// 实现事件处理器调用 注意事件冒泡实现
while (target) {
const { store } = target;
const eventHandler = store && store[eventType];
// 执行事件处理函数
eventHandler && eventHandler.call(target, syntheticEvent);
// 递归冒泡
target = target.parentNode;
}
// 关闭批量更新标志位
updateQueue.isBatchUpdating = false;
// 进行批量更新
updateQueue.batchUpdate();
}
}
// 合成事件target 源码中额外做了各种事件兼容性处理
function createSynthetic(target) {
const result = {};
Object.keys(target).forEach((key) => {
result[key] = target[key];
});
return result;
}
其实实现的思路还是很简单的,首先给每一个dom
节点上添加一个store
的对象,这个对象上会存在我们在当前react
节点上绑定的事件name
为key
,以及对应的事件处理函数value
.比如
store = {
onclick: function () {
// do something
},
ontouchmove: function () {
// do something
}
...
}
然后通过统一监听docuemnt
上的事件进行代理。
当document
上触发对应的事件,比如click
点击页面某个元素,触发document.onclick
,执行dispatchEvent
这个方法。
dispatchEvent
这个方法获得点击的真实元素event.target
,然后获取event.target
当前dom
元素,通过dom.store[eventType]
获得应该触发的事件处理函数。
当然我们在事件处理函数前后进行了isBatchUpdating
的修改,就完成了事件处理函数执行前->开启批量更新,执行完毕->关闭标识为false
。
这里需要额外注意的是,当我们触发event.target
的事件时,同时也要还原向上冒泡递归向上查找对应的parentNode
进行事件冒泡的触发,触发父元素的事件。
updateQueue
实现批量更新
当我们通过事件代理的方法实现了异步批量更新,在事件代理函数最终我们执行了updateQueue.batchUpdate()
进行批量更新。让我们在回到Component.js
这个文件中,去完善这个函数内容。
// 控制批量更新
export const updateQueue = {
isBatchUpdating: false,
updaters: new Set(),
// 批量更新方法
batchUpdate() {
for (let updater of updateQueue.updaters) {
updater.updateComponent();
}
updateQueue.isBatchUpdating = false;
updateQueue.updaters.clear();
},
};
其实
batchUpdate()
这个函数内容非常简单,通过迭代updateQueue.updaters
这个set,然后去带哦用每一个的updater
的updateComponent()
方法拿到实例上的penddingState
进行updateComponent
就可以了。
总结
其实关于react
异步事件更新总体上来说实现还是比较简单的,主要还是把握好思路进行梳理。
首先异步更新是通过标志位判断是否开启异步,其次在事件触发时候通过事件代理每次事件执行都放置到document
上的处理函数去执行。
当document
上的事件处理函数执行时,首先会更新标记位开启批量更新,然后通过event.target.store[eventType]
找到对应的事件函数执行。
同时进行递归向上查找,实现事件冒泡的执行。
最终所有冒泡结束后,关闭标识位,统一批量更新。
最终来看看我们实现的Demo
:
import React from './react/react';
import ReactDOM from './react/react-dom';
// class Component地址
class ClassComponent extends React.Component {
constructor() {
super();
this.state = {
number: 0,
};
}
handleClick = () => {
this.setState({ number: this.state.number + 1 });
console.log(this.state.number);
this.setState({ number: this.state.number + 1 });
console.log(this.state.number);
setTimeout(() => {
console.log('开启定时器');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'number');
});
};
handleClickParent = () => {
console.log('parent-parent触发');
};
handleParent = () => {
console.log('parent触发', this.state.number);
this.setState({ number: this.state.number + 1 });
console.log(this.state.number, 'parent 中的state');
};
render() {
// console.log(this.state.number, 'render');
return (
<div onClick={this.handleClickParent}>
<div onClick={this.handleParent}>
父亲元素
<div onClick={this.handleClick}>{this.state.number}</div>
</div>
</div>
);
}
}
const element = <ClassComponent></ClassComponent>;
ReactDOM.render(element, document.getElementById('root'));
当我们点击页面上的0
时最终执行结果:
击