带你从0~1手写React源码--实现事件合成和组件的批量更新

907 阅读5分钟

前言

带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,如果只有一个状态改变,整个页面都会进行更新,这明显性能较差,而在REACT源码中,事件的更新可能是异步的,是批量的,不是同步的。

class Couter extends React.Component{
    constructor(props){
        super(props);
        this.state={name:this.props.name,number:0}
    }
    handleClick=()=>{
        this.setState({number:this.state.number+1});
        console.log(this.state.number); //0
        this.setState({number:this.state.number+1});
        console.log(this.state.number);//0
        setTimeout(()=>{
            console.log(this.state.number);//1
            this.setState({number:this.state.number+1});
            console.log(this.state.number);//2
            this.setState({number:this.state.number+1});
            console.log(this.state.number);//3
        },1000)
    }
    render(){
        return (
            <div>
                <p>{this.state.name}</p>
                <p>{this.state.number}</p>
                <button onClick={this.handleClick}>+</button>
            </div>
        )
    }
    
}

上面这段代码在React源码中运行输出的结果是0 0 1 2 3。这说明在执行该事件方法时,调用setState改变state状态并没有立即更新state,而是将state先缓存起来,所以在事件中,state是异步批量更新的。而setTimeout中的方法执行,state会立即更新,所有在事件中,state是同步更新的。

批量更新的实现原理

React源码中我们需要定义一个更新器Updater,用于控制state状态的更新

class Updater {
  constructor(classInstance) {
    this.classInstance = classInstance //类数组实例
    this.pendingStates = [] //等待生效的状态;可能是一个对象也可能是一个函数
    this.callbacks = [] //等待生效的回调函数方法
  }
  addState(partialState, callback) {
    this.pendingStates.push(partialState)
    if (typeof callback === 'function') {
      this.callbacks.push(callback)
    }
    if (updateQueue.isBatchUpdate) {
      //如果是批量更新,先缓存updater
      updateQueue.updaters.add(this) //本次setState调用结束
    } else {
      this.updateClassComponent() //直接更新组件
    }
  }
  updateClassComponent() {
    const { classInstance, pendingStates, callbacks } = this
    if (pendingStates.length > 0) {
      //如果有需要等待更新的状态对象的话
      classInstance.state = this.getState() //计算新状态
      classInstance.forceUpdate() //强行更新组件
      callbacks.forEach((callback) => {
        callback()
      })
      callbacks.length = 0 //清空数组
    }
  }
  getState() {
    const { classInstance, pendingStates, callbacks } = this
    let { state } = classInstance
    pendingStates.forEach((pendingState) => {
      //如果pendingStat是函数,需要传递老状态,返回新状态然后进行合并
      if (typeof pendingState === 'function') {
        pendingState = pendingState.call(classInstance, state)
      }
      state = { ...state, ...pendingState }
    })
    pendingStates.length = 0 //清空数组
    return state
  }
}

这里我们通过定义一个队列对象updateQueue来判断是否state需要批量更新

const updateQueue={
    isBatchUpdate:false,//是否批量更新
    updaters:[],
    batchUpdate(){ //批量更新
        for(let updater of this.updater){
            updater.updateClassComponent()
        }
        this.isBatchUpdate=false;
        this.updaters.length=0;
    }
}

在上述代码中,我们在队列对象中定义一个属性isBatchUpdate,来控制事件是否需要批量更新,如果需要批量更新,我们将该实例先缓存到队列对象updateQueue中,如果不需要批量更新,直接更新组件。直接更新组件,需要做的三件事情:

  • 计算新的状态getState,也就是合并新的状态。注意:如果setState传递的第一个参数是函数,需要将该函数执行,其函数返回结果就是新的状态。
  • 执行setState传递的第二个参数callback
  • 强制更新组件forceUpdate。该方法挂载在Component上。

带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,我们没有对setState的状态进行处理,下面是Component类添加了setState方法和forceUpdate方法的代码:

class Component {
  static isReactComponent = true
  constructor(props) {
    this.props = props
    this.state = {}
    this.updater = new Updater(this)
  }
  setState(partialState, callback) {
    this.updater.addState(partialState,callback)
    /**
         * let state=this.state;
        this.state={...state,...partialState};
        //state改变组件更新 得到新的虚拟DOM
        let newVdom=this.render();
        //挂载到真实DOM去
        updateClassComponent(this,newVdom)
         */
  }
  //强行更新组件
  forceUpdate() {
    let newVdom = this.render()
    updateClassComponent(this, newVdom)
  }
  render() {
    throw new Error('此方法为抽象方法,需要子类实现')
  }
}
function updateClassComponent(classInstance, newVdom) {
  let oldDom = classInstance.dom //取出上次类组件渲染出来的真实DOM
  let newDom = createDOM(newVdom)
  oldDom.parentNode.replaceChild(newDom, oldDom)
  classInstance.dom = newDom
}

合成事件的实现原理

合成事件的作用:

  • 兼容处理,兼容浏览器
  • 可以在写的事件处理函数之前和之后做一些事情(比如让状态进行批量更新)

带你从0~1手写React源码----实现函数组件及类组件的渲染和更新中,我们对虚拟DOM的props的事件进行了处理,这里我们需要改下:

/**
 * 使用虚拟dom属性更新刚刚创建出来的真实DOM属性
 * @param {*} vdom 真实DOM
 * @param {*} props 新属性对象
 */
function updateProps(vdom, props) {
  for (let key in props) {
    if (key === 'children') continue //单独处理
    if (key === 'style') {
      let styleObj = props[key]
      for (let attr in styleObj) {
        dom.style[attr] = styleObj[attr]
      }
    } else if (key.startsWith('on')) {  //这里对事件方法进行处理
      dom[key.toLocaleLowerCase()] = props[key]
    } else {
      dom[key] = porps[key]
    }
  }
}
/**
 * 使用虚拟dom属性更新刚刚创建出来的真实DOM属性
 * @param {*} vdom 真实DOM
 * @param {*} props 新属性对象
 */
function updateProps(dom,newProps,oldProps){
    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[key.toLocaleLowerCase()]=props[key]
            addEvent(dom,key.toLocaleLowerCase(),newProps[key])
        }else{
            dom[key]=newProps[key]
        }        
    }
}

addEvent方法:

/**
 * 给真实DOM添加事件处理函数
 *
 */
export const addEvent = function addEvent(dom, eventType, listener) {
  let store = dom.store || (dom.store = {})
  store[eventType] = listener
  if (!document(eventType)) {
    //事件委托
    document(eventType) = dispatchEvent //
  }
}
let syntheticEvent = {}
function dispatchEvent(event) {
  const { target, type } = event
  let eventType = `on${type}`
  updateQueue.isBatchUpdate = true //把队列设置为批量更新模式
  syntheticEvent = createSyntheticEvent(event)
  
  //事件冒泡到target上
  while (target) {
    let { store } = target
    let listener = store && store[eventType]
    listener && listener.call(target, syntheticEvent)
    target = target.parentNode
  }
  for (let key in syntheticEvent) {
    //syntheticEvent始终只有一个
    syntheticEvent[key] = null
  }
  updateQueue.batchUpdate() //批量更新一下
}

function createSyntheticEvent(navtiveEvent) {
  for (let key in navtiveEvent) {
    //把原生事件都合成到syntheticEvent对象中
    syntheticEvent[key] = navtiveEvent[key]
  }
  return syntheticEvent
}

这里所有的事件对象都委托到document上。在dispatchEvent方法中,需要处理的事情:

  • 把队列设置为批量更新模式。pdateQueue.isBatchUpdate = true

  • 合成事件处理

    syntheticEvent = createSyntheticEvent(event)
    function createSyntheticEvent(navtiveEvent) {
      for (let key in navtiveEvent) {
        //把原生事件都合成到syntheticEvent对象中
        syntheticEvent[key] = navtiveEvent[key]
      }
      return syntheticEvent
    }
    
  • 执行事件listener

    while (target) {
    let { store } = target
    let listener = store && store[eventType]
    listener && listener.call(target, syntheticEvent)
    target = target.parentNode
    }
    
  • 执行事件后把syntheticEvent事件清空

      for (let key in syntheticEvent) {
        //syntheticEvent始终只有一个
        syntheticEvent[key] = null
      }
    
  • 批量更新缓存的状态

    updateQueue.batchUpdate() //批量更新一下
    
    const updateQueue={
    isBatchUpdate:false,//是否批量更新
    updaters:[],
    batchUpdate(){ //批量更新
        for(let updater of this.updater){
            updater.updateClassComponent()
        }
        this.isBatchUpdate=false;
        this.updaters.length=0;
    }
    }