一、准备
- 合成事件和DOM原生事件的区别。
- DOM原生事件事件流:
冒泡
(button->body->document->window)、目标
(目标元素捕获事件,开始像根节点冒泡)、捕获
(window->body->document->button) - React合成事件:事件没有在目标对象上绑定,而是在
document
上监听所支持的所有事件,当事件发生并冒泡至document时,react将事件内容封装并交由真正的处理函数运行。
- 合成事件的原理是通过事件委托实现的。
- React17之前:事件都委托在document上
- React17之后:事件都委托在外层容器上。目的是可以在一个页面中存在多个不同的React应用
作用:
- 可以做浏览器的兼容。不同浏览器的api不一样,可以把不同的事件对象做成一个标准化的事件对象,提供标准的api访问供用户使用。
- 如果react事件绑定在了真实DOM节点上,一个节点同时有多个事件时,页面的响应和内存的占用会受到很大的影响。因此
SyntheticEvent
作为中间层出现,把事件处理函数委托在外部容器上,性能上得到了大的提升。
- React的更新,可能是同步,可能是异步。
- 异步:React能够管理范围内,比如:事件函数、生命周期函数里,都是异步的
- 同步:除以上情况外,比如:setTimeout等原生的事件,都是同步的
二、批量更新
事件函数处理之前,开启批量更新模式,事件函数处理结束之前,关闭批量更新模式,然后遍历更新状态队列实现组件更新。
1. 实现思路
1-1. 更新队列updateQueue
定义变量对象:更新队列updateQueue
,属性:记录同步/异步更新的标志isBatchingUpdate
默认值为规定为false
即同步更新、将要更新的数组updaters更新器的数组
、批量更新的方法batachUpdate
。setState
之前置为true
,开始进行批量更新,更新之后置为false
1-2. 设置批量更新模式
每次状态更新,一定会执行emitUpdate
触发更新函数,进入updateComponent
方法来更新组件,那么①我们需要在此判断,如果updateQueue.isBatchingUpdate
值为真,那么就开启批量更新模式,把要更新的状态存入更新队列中。②否则就是同步更新直接调用updateComponent
更新组件
2. 代码实现
2-1. src/index.js
import React from "./react";
import ReactDOM from "./react-dom";
// import { updateQueue } from "./component";
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { number: 0, title: "计数器" };
}
handleClick = (event) => {
console.log("event", event);
// 更改状态前把标识置为true,进行批量更新
// updateQueue.isBatchingUpdate = true;
// debugger;
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
setTimeout(() => {
this.setState({ number: this.state.number + 1 });
console.log(this.state);
this.setState({ number: this.state.number + 1 });
console.log(this.state);
});
// 更改后重置为同步、并拿到所有updater调用更新
// updateQueue.isBatchingUpdate = false;
// updateQueue.batchUpdate();
};
handleParentClick() {
console.log("handleParentClick");
}
render() {
return (
<div onClick={this.handleParentClick}>
<p>{this.state.number}</p>
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
ReactDOM.render(<Counter />, document.getElementById("root"));
2-2. src/component.js
import { compareToVdom } from "./react-dom";
> > > export let updateQueue = {
> > > // 控制同步更新和异步更新
> > > isBatchingUpdate: false,
> > > // 记录异步更新的
> > > updaters: [],
> > > // 批量更新
> > > batchUpdate() {
> > > updateQueue.updaters.forEach((updater) => updater.updateComponent());
> > > updateQueue.isBatchingUpdate = false;
> > > updateQueue.updaters.length = 0;
> > > },
> > > };
/** 更新器 */
class Updater {
constructor(classInstance) {
// 保存实例
this.classInstance = classInstance;
// 等待更新的状态数组
this.pendingStates = [];
}
addState(partialState) {
this.pendingStates.push(partialState);
// 触发更新
this.emitUpdate();
}
emitUpdate() {
// 说明当前是批量更新模式
> > > if (updateQueue.isBatchingUpdate) {
> > > // 批量更新为异步,先存入数组
> > > updateQueue.updaters.push(this);
> > > } else {
> > > this.updateComponent();
> > > }
}
updateComponent() {
// 解构出实例、等待更新的状态
let { classInstance, pendingStates } = this;
if (pendingStates.length > 0) {
shouldUpdate(classInstance, this.getState());
}
}
/** 基于老状态和pendingStates获取新状态 */
getState() {
let { classInstance, pendingStates } = this;
let { state } = classInstance; //老状态
// 每个分状态 ==> 合并属性
pendingStates.forEach((nextState) => {
state = { ...state, ...nextState };
});
// 清空等待更新的分状态数组
pendingStates.length = 0;
return state;
}
}
function shouldUpdate(classInstance, nextState) {
classInstance.state = nextState; // 先把新状态赋值给实例的state
classInstance.forceUpdate(); // 让类的实例强行更新
}
class Component {
// 当子类继承父类的时候,父类的静态属性也是可以继承的
// 函数组件和类组件编译后,都会变成函数,因此加上isReactComponent属性来区分是函数组件还是类组件
static isReactComponent = true;
constructor(props) {
this.props = props;
this.state = {}; // 初始值
this.updater = new Updater(this);
}
/** 更新分状态 */
setState(partialState) {
this.updater.addState(partialState);
}
/** 根据新的属性状态,计算新的要渲染的虚拟DOM */
forceUpdate() {
//获取老的虚拟DOM
let oldRenderVdom = this.oldRenderVdom;
//获取老的真实DOM
let oldDOM = oldRenderVdom.dom;
// 基于新的属性和状态,计算新的真实DOM
let newRenderVdom = this.render();
compareToVdom(oldDOM.parentNode, oldRenderVdom, newRenderVdom);
// 使下次更新时以上次的新DOM为比较
this.oldRenderVdom = newRenderVdom;
}
}
export { Component };
三、合成事件
通过事件委托把dom的处理函数委托在外部容器上,模拟冒泡捕获目标元素,执行对应的事件处理函数。
1. 实现思路
1-1. 添加合成事件
添加工具函数
addEvent
,修改原先绑定事件方法为addEvent
,入参:dom元素、事件/属性名、事件函数。这么做的目的是为了实现目标元素的事件委托。给dom添加store
自定义属性,赋值为事件处理函数,初始值为{}。没有给dom自身绑定事件,但是事件处理函数是在每个dom的store属性上存放的,最后把所有事件处理函数委托给dispatchEvent
进行统一处理
1-2. 实现dispatchEvent
在事件处理函数打印event:event以上并不是真正的原生的事件对象,是React合成事件对象,真正的原生的事件对象存放在nativeEvent属性里。
dispatchEvent
:合成事件的统一代理处理函数,入参为事件对象event
。从事件对象上可以拿到目标元素target
和事件类型type
。把原生的事件对象传给createSyntheticEvent
方法创建合成事件。此时,函数执行前开启批量更新。从目标元素target
上可以拿到store
属性,store
属性上又存放着事件处理函数,可以直接调用。事件函数函数执行完毕后关闭批量更新模式,遍历更新队列的状态数组进行更新
1-3. 创建合成事件
createSyntheticEvent
方法用来创建合成事件。把原生对象的属性拷贝到合成事件对象上一份。添加对应的属性存放需要兼容的方法,比如preventDefault
阻止浏览器默认事件、stopPropagation
阻止冒泡等
1-4. 实现React事件冒泡
实现React事件冒泡,本质模拟浏览器的冒泡。点击button触发
onclick
事件处理函数,冒泡到文档对象document
后,原生浏览器的冒泡过程已经结束,并没有捕获阶段。而且,节点上的onclick
属性是我们手动创建的自定义事件,浏览器不识别此属性,因此需要我们自己模拟冒泡查找到事件源开始执行事件函数(如果不模拟冒泡,父节点div上的onclick事件就不会执行)。
2. 代码实现
2-1. src/event.js
> > > import { updateQueue } from "./component";
> > >
> > > /**
> > > * 绑定事件处理函数
> > > * @param {*} dom 要绑定事件的DOM元素 button
> > > * @param {*} eventType 事件类型 onclick
> > > * @param {*} handler 事件处理函数 handleClick
> > > */
> > > export function addEvent(dom, eventType, handler) {
> > > // 确保dom上始终有一个store属性,初始值为{}
> > > let store = dom.store || (dom.store = {});
> > > store[eventType] = handler; // dom.store['onclice'] = hanldeClick 这个事件处理函数委托给了根容器上
> > > if (!document[eventType]) {
> > > document[eventType] = dispatchEvent;
> > > }
> > > }
> > >
> > > /**
> > > * 合成事件的统一代理处理函数
> > > * @param {} event
> > > */
> > > function dispatchEvent(event) {
> > > let { target, type } = event;
> > > let eventType = `on${type}`;
> > > let syntheticEvent = createSyntheticEvent(event);
> > > // 事件函数执行前,先设置为批量更新
> > > updateQueue.isBatchingUpdate = true;
> > >
> > > // 模拟React事件冒泡
> > > while (target) {
> > > let { store } = target;
> > > let handler = store && store[eventType];
> > > handler && handler(syntheticEvent); // 这个event不是原生事件对象
> > > target = target.parentNode; // 向上冒泡
> > > }
> > > // 执行对应的事件函数
> > > // 执行结束后重置批量更新对象
> > > updateQueue.isBatchingUpdate = false;
> > > updateQueue.batchUpdate();
> > > }
> > >
> > > /**
> > > * 为什么React不把原生事件对象直接传给事件处理函数
> > > * 1.为了兼容性 比如阻止默认行为、冒泡等
> > > * @param {*} nativeEvent
> > > * @returns
> > > */
> > > function createSyntheticEvent(nativeEvent) {
> > > let syntheticEvent = [];
> > > for (let key in nativeEvent) {
> > > // 把原生事件对象上的属性拷贝到合成事件对象上
> > > syntheticEvent[key] = nativeEvent[key];
> > > syntheticEvent.nativeEvent = nativeEvent;
> > > syntheticEvent.preventDefault = preventDefault;
> > > syntheticEvent.stopPropagation = stopPropagation;
> > > return syntheticEvent;
> > > }
> > > }
> > >
> > > /**
> > > * 阻止默认事件
> > > * @param {} event 原生事件对象
> > > */
> > > function preventDefault(event) {
> > > // 兼容ie
> > > if (!event) {
> > > window.event.returnValur = false;
> > > }
> > > // 标准浏览器
> > > if (event.preventDefault) {
> > > event.preventDefault();
> > > }
> > > }
> > >
> > > /**
> > > * 阻止冒泡
> > > */
> > > function stopPropagation() {
> > > const event = this.nativeEvent;
> > > if (event.stopPropagation) {
> > > event.stopPropagation();
> > > } else {
> > > // 兼容
> > > event.cancelBubble = true;
> > > }
> > > this.isPropagationStopped = true;
> > > }
2-2. src/react-dom.js
import { REACT_TEXT } from "./constants";
> > > import { addEvent } from "./event";
/**
*把虚拟DOM变成真实DOM插入容器
* @param {*} vdom 虚拟DOM/React元素
* @param {*} container 真实DOM容器
*/
function render(vdom, container) {
mount(vdom, container);
}
/** 页面挂载真实DOM */
function mount(vdom, parentDOM) {
//把虚拟DOM变成真实DOM
let newDOM = createDOM(vdom);
//把真实DOM追加到容器上
parentDOM.appendChild(newDOM);
}
/**
* 把虚拟DOM变成真实DOM
* @param {*} vdom 虚拟DOM
* @return 真实DOM
*/
function createDOM(vdom) {
if (!vdom) return null; // null/und也是合法的dom
let { type, props } = vdom;
let dom; //真实DOM
if (type === REACT_TEXT) {
// 如果元素为文本,创建文本节点
dom = document.createTextNode(props.content);
} else if (typeof type === "function") {
if (type.isReactComponent) {
// 说明这是一个类组件
return mountClassComponent(vdom);
} else {
// 函数组件
return mountFunctionComponent(vdom);
}
} else if (typeof type === "string") {
//创建DOM节点 span div p
dom = document.createElement(type);
}
// 处理属性
if (props) {
//更新DOM的属性 后面我们会实现组件和页面的更新。
updateProps(dom, {}, props);
let children = props.children;
//如果说children是一个React元素,也就是说也是个虚拟DOM
if (typeof children === "object" && children.type) {
//把这个儿子这个虚拟DOM挂载到父节点DOM上
mount(children, dom);
} else if (Array.isArray(children)) {
reconcileChildren(children, dom);
}
}
vdom.dom = dom; // 给虚拟dom添加dom属性指向这个虚拟DOM对应的真实DOM
return dom;
}
/** 挂载类组件 */
function mountClassComponent(vdom) {
let { type: ClassComponent, props } = vdom;
// 把类组件的属性传递给类组件的构造函数,
// 创建类组件的实例,返回组件实例对象
let classInstance = new ClassComponent(props);
//可能是原生组件的虚拟DOM,也可能是类组件的的虚拟DOM,也可能是函数组件的虚拟DOM
let renderVdom = classInstance.render();
//在第一次挂载类组件的时候让类实例上添加一个oldRenderVdom=renderVdom
classInstance.oldRenderVdom = renderVdom;
return createDOM(renderVdom);
}
/** 挂载函数组件 */
function mountFunctionComponent(vdom) {
let { type: functionComponent, props } = vdom;
//获取组件将要渲染的虚拟DOM
let renderVdom = functionComponent(props);
return createDOM(renderVdom);
}
/** 如果子元素为数组,遍历挂载到容器 */
function reconcileChildren(children, parentDOM) {
children.forEach((childVdom) => mount(childVdom, parentDOM));
}
/**
* 把新的属性更新到真实DOM上
* @param {*} dom 真实DOM
* @param {*} oldProps 旧的属性对象
* @param {*} newProps 新的属性对象
*/
function updateProps(dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
// 子节点另外处理
continue;
} else if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
} else if (/^on[A-Z].*/.test(key)) {
> > > // 绑定事件 ==> dom.onclick = 事件函数
> > > // dom[key.toLowerCase()] = newProps[key];
> > > // 之后不再把事件函数绑定在对应的DOM上,而是事件委托到文档对象
> > > addEvent(dom, key.toLowerCase(), newProps[key]);
} else {
dom[key] = newProps[key];
}
}
for (let key in oldProps) {
//如果说一个属性老的属性对象里有,新的属性没有,就需要删除
if (!newProps.hasOwnProperty(key)) {
dom[key] = null;
}
}
}
/**
* @param {*} parentDOM 父真实DOM
* @param {*} oldVdom 老的虚拟DOM
* @param {*} newVdom 新的虚拟DOM
*/
export function compareToVdom(parentDOM, oldVdom, newVdom) {
// 获取oldRenderVdom对应的真实DOM
let oldDOM = oldVdom.dom;
// 根据新的虚拟DOM得到新的真实DOM
let newDOM = createDOM(newVdom);
// 把老的真实DOM替换为新的真实DOM
parentDOM.replaceChild(newDOM, oldDOM);
}
const ReactDOM = {
render,
};
export default ReactDOM;
四、总结
绑定事件:
第一次先绑定父节点事件handleParentClick即store.onclick=handleParentClick
,绑定后进行委托; 第二次绑定子节点事件handleClick
;因为第一次已经委托了onclick事件所以第二次不再委托,但子节点上store属性保存了自己的事件处理函数handleClick
过程:
- 点击button,
事件处理函数经过冒泡后开始执行
--> - 先拿到事件源button和事件类型click,拼出
onclick
后开始创建合成事件
(除了原生的属性外,多出我们自己添加的preventDefault等属性)--> 开启批量更新
模式,开始执行真正的事件处理函数
-->- 走到
setState
更新状态,进入到更新批量状态 --> - 因为开启了批量更新,所以在更新阶段是把新状态放入更新数组而
没有立即更新
因此打印 0、0(前两次setState都是如此)--> - 走完状态的更新后,执行
模拟React事件冒泡
,冒泡到父节点
,也有onclick事件,调用事件处理函数后,打印handleParentClick --> - 开始走
批量更新
的更新逻辑,更新数组队列中两个状态(更新逻辑与上一节内容一致,不做赘述) --> - 最后执行setTimeout,因为没有走合成事件逻辑,还是
同步更新
,因此会打印2、3。