preact系列之diff

210 阅读3分钟

阅读须知

  • 文章参考的preact版本是10.5.13
  • 文章会省略大部分逻辑,比如hydrating,context,isSvg等。所以如果有大佬点进来需谨慎。
  • 简略版本代码

diff概览

  • 对于组件(函数 or 类组件):执行组件的生命周期(函数组件会被包装为类组件),调用diffChildren继续递归。
  • 对于原生dom节点: 调用diffElementNodes.
export function diff (
  parentDom,
  newVNode,
  oldVNode,
  commitQueue,
  oldDom,
) {

  const newType = newVNode.type;
  try {
    let c, isNew, oldProps, oldState;
    let newProps = newVNode.props;
    outer: if (typeof newType == 'function') { 
        // 1.判断是否初始化
        // 2. 执行对对应的生命周期
        // 3. 继续对比子节点
    } 
        
    // 原生节点的处理方式
    else {}
        
}

组件处理

判断是否初始化

  • 根据oldVnode._component 判断是否完成初始化,
    • 初始化完成直接使用旧的preact实例。
    • 初始化未完成:完成组件实例化。标识 isNew = true,后续执行初始化的生命周期。函数组件和Fragment会被封装为类组件。
      • 做法:实例化一个空类组件,在类组件的render函数去执行函数组件,这也是为什么每一次更新函数组件都会重新执行,因为每一次更新都会重新调用render函数。
      • 好处:函数组件和类组件的逻辑统一。
// 初始化已完成:调用vnode._component(React Component组件实例)
// 初始化未完成:完成React组件初始化

if (oldVNode._component) {
    newVNode._component = c = oldVNode._component;
} else {
    // Class Component, prototype和render存在时
    // Function Component, 非Class Component
    
    if ('prototype' in newType && newType.prototype.render) {
      newVNode._component = c = new newType(newProps);
    } else {
      // 函数组件或者Fragment 通过类组件去实例化  Component原型上有setState, forceUpdate, render = Fragment => (return this.props.children)
        
      newVNode._component = c = new Component(newProps);
      c.constructor = newType;
      c.render = doRender;
    }
    
    // 初始化数据
    c.props = newProps;
    c.state = c.state || {};
    c._renderCallbacks = [];
    isNew = c._dirty = true;  // _dirty主要是做批量更新的
}

  • 组件本质是原型链,平时使用的state,props都在原型链上面

image.png

执行生命周期

  • preact目前还兼容旧版生命周期,(componentWillMount,componentWilReceiveProps),根据是否定义getDerivedStateFromProps做判断是否调用

image.png

image.png

数据的变化

生命周期执行过程中state,props会有三种状态,oldState, state, _nextState,oldProps, props, newProps。分别为旧状态(oldState) render能拿到的数据(state),最新的数据(_nextState).

  • c.state 和 c.props, c._nextState是组件的属性,其余是变量。主要是为了生命周期函数可以拿到不同状态的props和state。
    • 因为有一些生命周期执行过程中会修改state和props,会造成数据变化。并且有一些生命周期需要拿到最新的数据,有一些需要拿到旧数据。所以需要备份一份作为最新的数据,一份作为旧数据。render执行前把最新的数据赋值到组件的属性(c.state, c.props)上,代码中通过this.state, this.props可以拿到最新的数据。
      // 复制一份作为最新数据,一份作为旧数据
      c._nextState = c._nextState || Object.assign({}, c.state);
      oldProps = c.props;
      oldState = c.state;
      
      // 执行render之前的生命周期
      
      
      // 将最新的状态赋值到component上面
      c.props = newProps;
      c.state = c._nextState;
      c._vnode = newVNode;
      c._parentDom = parentDom;
      
      // 执行render
    

生命周期执行

  • 子组件执行完render之后才会执行父组件的componentDidMount,componentDidUpdate。会将这两个存储在组件的_renderCallbacks数组上。等到组件执行完成之后,再执行commitQueue去执行剩余的生命周期。
      // 执行生命周期
      if (newType.getDerivedStateFromProps) {
        Object.assign(
          c._nextState,
          newType.getDerivedStateFromProps(newProps, c._nextState),
        )
      }

      if (isNew) {
        if (c.componentDidMount) {
          c._renderCallbacks.push(c.componentDidMount);
        }
      } else {
        if (
          !c._force &&
          c.shouldComponentUpdate &&
          c.shouldComponentUpdate(newProps, c._nextState) === false
        ) {
          c.props = newProps;
          c.state = c._nextState;
          c._vnode = newVNode;

          newVNode._dom = oldVNode._dom;
          newVNode._children = oldVNode._children;
          newVNode._children.forEach(vnode => {
            if (vnode) vnode._parent = newVNode;
          });
          if (c._renderCallbacks.length) {
            commitQueue.push(c);
          }
          break outer;
        }

        if (c.componentDidUpdate) {
          c._renderCallbacks.push(() => {
            c.componentDidUpdate(oldProps, oldState);
          });
        }
      }
      
      let tmp = c.render(c.props, c.state);

      if (!isNew && c.getSnapshotBeforeUpdate) {
        c.getSnapshotBeforeUpdate(oldProps, oldState);
      }

对比子节点

let isTopLevelFragment =
    tmp && tmp.type === Fragment && tmp.key == null;
let renderResult = isTopLevelFragment ? tmp.props.children : tmp;

// 对比子节点
diffChildren(
    parentDom,
    Array.isArray(renderResult) ? renderResult : [renderResult],
    newVNode,
    oldVNode,
    commitQueue,
    oldDom,
);

if (c._renderCallbacks.length) {
    commitQueue.push(c);
}

c._dirty = false;
c._force = false;

原生节点处理

newVNode._dom = diffElementNodes(
    oldVNode._dom,
    newVNode,
    oldVNode,
    excessDomChildren,
    commitQueue,
);