手写React(四)实现合成事件+批处理
从零到1,手写实现react!
本文接着前面 手写react之类组件更新,继续实现合成事件+批处理!
传送
还是那句话:
网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!
自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!
我这个一步步手写实现React的笔记,希望可以帮到你。
前言
在开始手写之前,你需要了解到以下知识点:
- dom事件机制
- react合成事件原理
- js事件循环机制
1.dom事件机制
2.js事件循环机制
这里简单描述下js事件循环机制:
执行顺序:宏任务 > 同步任务 > 微任务 > 宏任务 > 同步任务 > 微任务 ……
举个例子:
假如你需要做三件事:追剧、泡方便面吃(10分钟)、洗衣服(50分钟)
如果你用同步的方式去做这三件事,那么你只能:
- 1追剧,直到追完才能做其他(很可能饿死都没追完)
- 2追完剧 泡方便面吃
- 3吃完方便面 把衣服丢进洗衣机,等着衣服洗完
如果你用异步方式做这三件事:
- 1先把方便面泡好(1分钟)
- 2随后把衣服丢进洗衣机
- 3回来方便面泡好了,一边看剧一边吃泡面,
- 4看了一集电视剧后衣服洗好了;
- 5最后你在50分钟内,剧也追了,面也吃了,衣服也洗了
我们假设追剧是同步任务,泡方便面是微任务,洗衣服是宏任务:
- 执行同步任务时(追剧),发现有个微任务(泡方便面),就把方便面泡好(加入微任务队列)
- 遇到一个宏任务(洗衣服),把衣服丢进洗衣机(加入宏任务队列)
- 回来发现方便面泡好了,开始吃泡面(清空微任务队列)
- 然后衣服洗好了,清空宏任务队列。
- 继续执行下一轮同步任务(追剧)
总结:
- 微任务一定会在当前执行栈中的 所有同步任务执行完,才会执行!你不可刚泡好方便面就立即吃吧?
- 宏任务一定会在执行栈中的 所有微任务执行完后,才会执行!你不可能刚把衣服丢进洗衣机就拿出来凉吧?
- 宏任务执行完,如果里面又有新的同步任务,那么会继续执行新的任务队列!
最后:你了解上面的原理后,只需要记住哪些是宏任务,哪些是微任务就可以了!
- 宏任务:定时器、script标签(就是最外层的宏任务)
- 同步任务,就是我们写的普通代码,从上往下依次执行,遇到函数调用,就进入新的执行栈
- 微任务:promise、nextTick、3个html观察器(IntersectionObserver、MutationObserver、ResizeObserver)……
二、实现合成事件
完善合成事件处理:
- 将props属性处理中,对事件的绑定方式进行修改
- 创建event.js,用来写合成事件相关的逻辑
- 实现addEvent、dispatchEvent、createSyntheticEvent
1.修改updateProps事件处理
对事件的绑定方式进行修改
function updateProps (dom, oldProps, newProps) {
for (let key in newProps) {
if (key === "children") {
continue; // 暂时跳过,后面会单独处理子节点
}
if (key === "style") {
let styleObj = newProps[key];
for (let attr in styleObj) {
dom.style[attr] = styleObj[attr];
}
/* ------------------------------------------------ */
} else if (key.startsWith("on")) {
// 执行合成事件,传入: dom 事件名(小写) 事件绑定的handler
addEvent(dom, key.toLocaleLowerCase(), newProps[key]);
} else {
/* ------------------------------------------------ */
if (newProps[key]) dom[key] = newProps[key];
}
}
}
接下来我们需要实现addEvent方法!
2.addEvent
创建event.js > 实现addEvent方法
/**
* @description: 添加事件处理函数,做合成事件处理!
* @param {*} dom 事件源
* @param {*} eventType 事件类型
* @param {*} handler 事件触发的绑定函数
* @return {*} void
*/
export function addEvent (dom, eventType, handler) {
let store; // 用于存放dom身上绑定的所有事件的handler
if (dom.store) {
// 如果dom上已经有store对象,就取出来用
store = dom.store;
} else {
// 如果没有store,就创建一个对象
dom.store = store = {};
}
// 将事件放入store对象中 store.onclick = ()=>{}
store[eventType] = handler;
// 这里做了一个去重,如果一个元素绑定多个onClick事件,实际只绑定一个
if (!document[eventType]) {
/*
绑定的dispatchEvent函数中去对事件做统一处理!
这里也可以改为addEventListaner……
+ v17以前:事件委托在document上
+ v17以后:事件委托在root元素上
*/
document[eventType] = dispatchEvent;
}
}
3.dispatchEvent
接着需要实现dispatchEvent方法,派发事件
/**
* @description: 事件派发
* @param {*} event 事件源
* @return {*} void
*/
function dispatchEvent (event) {
// 解构出原生事件的event上的target和type属性
let { target, type } = event;
let eventType = `on${type}`; // 此时type为click 需 拼接为 onclick
updateQueue.isBatchingUpdate = true; // 切换为批量更新模式
// 获取合成事件
let syntheticEvent = createSyntheticEvent(event);
//模拟事件冒泡的过程
while (target) {
// 拿到上面addEvent函数中绑定在dom身上的store
let { store } = target;
// 取出store中的handler
let handler = store && store[eventType];
// 执行handler,并把合成事件对象传递过去!!
handler && handler.call(target, syntheticEvent);
// 将target修改为parentNode,进入下一个while循环, 继续执行冒泡事件
// 直到target是html,它的parent为undefiined,循环结束!!!
target = target.parentNode;
}
// 重置批量更新状态
updateQueue.isBatchingUpdate = false;
// 执行批量更新!!!
updateQueue.batchUpdate();
}
4.createSyntheticEvent
可以看到,上面的dispatchEvent派发事件时,实际传递给handler的是合成事件对象(react的合成事件对象,并不是原生dom事件对象!),合成事件对象中,会做很多处理:
- 阻止冒泡的兼容处理
- 阻止默认事件的兼容处理
- ……
/**
* @description : 创建合成事件
* 在源码里此处做了一些浏览器兼容性的适配
* 例如: 对事件冒泡的兼容处理, 对阻止默认事件的兼容处理等...
* @param { } event
* @return { }
*/
function createSyntheticEvent (event) {
let syntheticEvent = {}; // 合成事件对象
// 遍历原生事件对象,赋值给合成事件对象,源码中还会做一些特殊处理
for (let key in event) {
syntheticEvent[key] = event[key];
// 做兼容处理...省略,后面再完善
}
return syntheticEvent; // 返回合成事件对象
}
5.查看效果
此时可以对点击事件稍作修改,查看一下效果
addNum = (event) => {
console.log("合成事件对象", event);
};
三、实现批处理
1.创建updateQueue对象
回到Component.js中
/*
更新队列对象
+ isBatchingUpdate:是否批量更新
+ updaters:待执行的更新队列
+ batchUpdate:执行更新队列的方法
*/
export let updateQueue = {
isBatchingUpdate: false, // 是否批量更新
updaters: [], // 队列
batchUpdate () {
for (let updater of updateQueue.updaters) {
// 遍历队列, 执行队属性身上的updateComponent方法!!
updater.updateComponent();
}
// 重置批量更新状态
updateQueue.isBatchingUpdate = false;
// 清空队列
updateQueue.updaters.length = 0;
},
};
可以看到:上面的代码中,updaters
队列中存放的实际是updater
,也就是我们的Updater构造类的实例
!
如何将updater放入updaters队列中?在哪里放入比较好?那就交给updater的emitUpdate方法去处理!
2.完善emitUpdate
回到Updater构造类中,继续完善emitUpdate方法
- 判断是否批量更新:updateQueue.isBatchingUpdate === true
- 如果是批量更新,就将将updater(也就是this)放入updaters队列中
- 如果不是批量更新,就执行之前的updateComponent逻辑!
/**
* @description: 组件更新拦截,判断
* 不管状态和属性的变化 都会让组件刷新,不管状态变化和属性变化 都会执行此方法
* @param {*} nextProps 最新的props,父组件传递的props变化,也会触发更新!
* @return {*} void
*/
emitUpdate (nextProps) {
this.nextProps = nextProps; //可能会传过来了一新的属性对象[父组件更新] 后面会用
//如果当前处于批量更新模式,那么就把此updater实例添加到updateQueue里去
if (updateQueue.isBatchingUpdate) {
// 将更新放入任务队列中! 进行批处理
updateQueue.updaters.push(this);
} else {
this.updateComponent(); // 直接让组件更新
}
}
提前完善一下updateComponent:
- 前面emitUpdate将接收nextProps放到了this上,props变化也要更新组件
- 所以updateComponent方法中除了对pendingState队列长度判断,还要增加对nextProps的判断
/* 处理组件更新 */
updateComponent () {
const { classInstance, pendingState, nextProps } = this;
// 如果当前更新队列中有数据 或者 父组件传递进来的props有变化,才执行更新
if (nextProps || pendingState.length > 0) {
// 将组件实例,和最新的状态,传递给shouldUpdate函数,开始组件更新
shouldUpdate(classInstance, this.getState());
}
}
3.完善dispatchEvent
此时我们发现一切都准备就绪了,要想执行批量更新,就差去改变updateQueueisBatchingUpdate的状态了!
那么在哪里修改这个状态比较好?答案是执行合成事件的时候!
回到event.js中! 对dispatchEvent方法进行完善!
/**
* @description: 事件派发
* @param {*} event 事件源
* @return {*} void
*/
function dispatchEvent (event) {
// 解构出原生事件的event上的target和type属性
let { target, type } = event;
let eventType = `on${type}`; // 此时type为click 需 拼接为 onclick
/* ----------------------关键代码位置----------------- */
updateQueue.isBatchingUpdate = true; // 切换为批量更新模式
/* ----------------------关键代码位置----------------- */
// 获取合成事件
let syntheticEvent = createSyntheticEvent(event);
//模拟事件冒泡的过程
while (target) {
// 拿到上面addEvent函数中绑定在dom身上的store
let { store } = target;
// 取出store中的handler
let handler = store && store[eventType];
// 执行handler,并把合成事件对象传递过去!!
handler && handler.call(target, syntheticEvent);
// 将target修改为parentNode,进入下一个while循环, 继续执行冒泡事件
// 直到target是html,它的parent为undefiined,循环结束!!!
target = target.parentNode;
}
/* ----------------------关键代码位置----------------- */
// 重置批量更新状态
updateQueue.isBatchingUpdate = false;
// 执行批量更新!!!
updateQueue.batchUpdate();
/* ----------------------关键代码位置----------------- */
}
4.流程梳理
5.查看效果
index.jsx中写个案例
/*
* @Description:
* @Author: zhangyuru
* @Date: 2023-09-04 08:54:32
* @LastEditors: zhangyuru
* @LastEditTime: 2023-09-04 11:43:25
* @FilePath: \my-react\src\index.js
*/
// import React from "react";
// import ReactDOM from "react-dom/client";
// 用自己封装的
import React from "./my-react/react";
import ReactDOM from "./my-react/react-dom";
class ClassComponent extends React.Component {
state = { date: new Date(), num: 1 };
addNum = (event) => {
console.log("合成事件对象", event);
for (let i = 0; i < 10; i++) {
this.setState({ num: this.state.num + 1 }, () => {
console.log("最新的state", this.state.num);
});
}
};
changeDate = () => {
setTimeout(() => {
this.setState({ date: new Date() });
console.log("setTimeout中的state", this.state.date);
});
}
render () {
console.log('render')
return (
<div id="abc">
<div>num: {this.state.num}</div>
<div>date: {this.state.date.toLocaleTimeString()}</div>
<button onClick={this.addNum}>addNum</button>
<button onClick={this.changeDate}>changeDate</button>
</div>
);
}
}
let element = React.createElement(ClassComponent);
ReactDOM.render(element, document.getElementById("root"));
完美实现!~
转载自:https://juejin.cn/post/7274536210730680356