likes
comments
collection
share

手写React(四)实现合成事件+批处理

作者站长头像
站长
· 阅读数 14

从零到1,手写实现react!

本文接着前面 手写react之类组件更新,继续实现合成事件+批处理!

传送

还是那句话:

网上那些八股文,都是别人对某个知识点掌握后整理的描述,背下来没有任何意义!代码是数学题,不是背课文!

自己写一遍,彻底搞清楚它的实现过程原理,这样收获的才是自己的!用自己的理解总结出来的,才是真的掌握了!知其然,知其所以然!

我这个一步步手写实现React的笔记,希望可以帮到你。

前言

在开始手写之前,你需要了解到以下知识点:

  • dom事件机制
  • react合成事件原理
  • js事件循环机制

1.dom事件机制

2.js事件循环机制

这里简单描述下js事件循环机制:

执行顺序:宏任务 > 同步任务 > 微任务 > 宏任务 > 同步任务 > 微任务 ……

举个例子:

假如你需要做三件事:追剧、泡方便面吃(10分钟)、洗衣服(50分钟)

如果你用同步的方式去做这三件事,那么你只能:

  • 1追剧,直到追完才能做其他(很可能饿死都没追完)
  • 2追完剧 泡方便面吃
  • 3吃完方便面 把衣服丢进洗衣机,等着衣服洗完

如果你用异步方式做这三件事:

  • 1先把方便面泡好(1分钟)
  • 2随后把衣服丢进洗衣机
  • 3回来方便面泡好了,一边看剧一边吃泡面,
  • 4看了一集电视剧后衣服洗好了;
  • 5最后你在50分钟内,剧也追了,面也吃了,衣服也洗了

我们假设追剧是同步任务,泡方便面是微任务,洗衣服是宏任务:

  • 执行同步任务时(追剧),发现有个微任务(泡方便面),就把方便面泡好(加入微任务队列)
  • 遇到一个宏任务(洗衣服),把衣服丢进洗衣机(加入宏任务队列)
  • 回来发现方便面泡好了,开始吃泡面(清空微任务队列)
  • 然后衣服洗好了,清空宏任务队列。
  • 继续执行下一轮同步任务(追剧)

总结:

  1. 微任务一定会在当前执行栈中的 所有同步任务执行完,才会执行!你不可刚泡好方便面就立即吃吧?
  2. 宏任务一定会在执行栈中的 所有微任务执行完后,才会执行!你不可能刚把衣服丢进洗衣机就拿出来凉吧?
  3. 宏任务执行完,如果里面又有新的同步任务,那么会继续执行新的任务队列!

最后:你了解上面的原理后,只需要记住哪些是宏任务,哪些是微任务就可以了!

  • 宏任务:定时器、script标签(就是最外层的宏任务)
  • 同步任务,就是我们写的普通代码,从上往下依次执行,遇到函数调用,就进入新的执行栈
  • 微任务:promise、nextTick、3个html观察器(IntersectionObserver、MutationObserver、ResizeObserver)……

二、实现合成事件

完善合成事件处理:

  1. 将props属性处理中,对事件的绑定方式进行修改
  2. 创建event.js,用来写合成事件相关的逻辑
  3. 实现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);
  };

手写React(四)实现合成事件+批处理

三、实现批处理

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.流程梳理

手写React(四)实现合成事件+批处理

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"));

手写React(四)实现合成事件+批处理

完美实现!~