渐进式React源码解析--State源码

944 阅读20分钟

state/setState源码解析

引言

前置知识

文章中涉及到的知识都是渐进式的讲解开发,当然如果对之间内容不感兴趣(已经了解),也可以直接切入本文内容,每一个章节都和之前不会有很强的耦合。

文章中涉及的代码地址, 戳这里👇查看

文章中的内容会分为两个步骤:

  1. 解析ReactsetState的解析流程。
  2. 实现ReactsetState触发页面重新渲染。
  3. 合成事件和批量异步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 cmpfunction 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.callbackssetState的第二个参数支持一个可选的回调函数,这里我们用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执行完毕后我们需要去调用触发更新。

可以看到在updateraddState添加完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流程

其实我们可以看到目前为止整个流程还是非常清晰的:

image.png

setState的流程还是非常清晰的,接下来我们重点进入实现reactsetState是如何触发页面更新的

ReactsetState页面重新渲染

接下来我们重点实现一下forceUpdate()这个实例方法。

我们先从思路上来讲解这个方法需要做的事情,代码其实并不是很多,主要就是更新的思路过程:

state改变rerender流程

当我们需要调用forceUpdate()方法最主要的事就是通过state变化重新调用render()生成新的vDom,然后和旧的vDom对象进行dom-diff从而进行对比更新页面真实DOM元素,主要思路为下面几个步骤:

  1. 我们需要旧的Vdom对象。
  2. 通过旧的Vdom对象我们拿到当前页面上这个Vdom渲染的真实DOM元素,以及它的parentNode
  3. 获取最新的Vdom对象,通过重新调用render方法获得。
  4. 进行Dom-diff,这一步我们先省略。
  5. 通过之前的createDom方法,生成新的真实dom元素。
  6. 调用parentNode.reaplce(newDom,oldDom)完成组件替换重新渲染页面。

大致看看这个流程,本质上就是通过vdom查找/生成真实DOM然后找到父节点,通过父节点去替换。

renderVdom & Vdom

在开始实现之前我们要先掌握两个概念,分别是renderVdomVdom

你可以将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节点。

image.png

当遇到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

image.png

有了这个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;
	}
}

其实它的实现非常简单,本质上就是通过vdomdom属性去查找真实的DOM。需要额外注意的一点是,我们需要查找的renderVDom如果内部仍然具有funcitonComponent/classComponent的话我们要一直去递归,知道查找到它真实渲染在页面上对应的vdom元素。

因为你可以看到下面这段代码,在type是函数的时候(表示他是一个fc/class component)直接进行了return并不会进行挂载dom属性。

image.png

再来看看这两个方法mountFunction/mountClassComponent:

image.png

在调用函数组件,类组件时:

  1. 如果是类组件,我们给它的实例对象上以及类本身挂载oldRenderVDom属性,指向它的实例render()返回的renderVDom对象。

  2. 如果是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…

接下来,我们先从思路上大致梳理一下要实现的细节:

流程梳理

  1. 我们清楚react内部是通过一个变量去控制是否是异步批量更新还是同步批量更新。我们需要一个全局变量去控制更新逻辑。
  2. 基于事件处理函数的批量更新,我们需要"劫持"react中的事件处理函数,也就是将所有的事件代理到document上去。通过document统一执行并且添加部分前置/后置逻辑处理。
  3. 在事件处理函数前置条件中开启批量更新标识位(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.isBatchUpdatingtrue开启批量更新标识位的话那么就将当前update实例推入updateQueue.updaters中去。

ps: 每次调用setState首先会调用当前组件的updater实例的addState(partialState,callback)将最新的修改和callback推入到updater实例的pendingStatecallback缓存进去。

我们修改了emitUpdate逻辑,如果开启了批量更新就将实例添加到updateQueue.updates中去进行缓存起来。如果未开启那么就走之前的直接更新逻辑。

事件代理

我们已经实现了每次调用setStateUpdater实例上根据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节点上绑定的事件namekey,以及对应的事件处理函数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,然后去带哦用每一个的updaterupdateComponent()方法拿到实例上的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时最终执行结果:

image.png