preact系列之render

483 阅读2分钟

阅读须知

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

组件和VNode的转换

  • preact类型: string | ComponentClass | FunctionComponent; (string:"div", "span"等节点)
export function createVNode(type, props, key, ref, original) {
    // 大部分属性会在diffChildren赋值
    const vnode = {
        type,  // 组件的类型 
        props,
        key,
        _children: null,
        _parent: null,         // vNode的parentVNode
        _dom: null,            // _dom是vNode的第一个子节点 
        _nextDom: undefined,   // 下一个子节点
        _depth: 0,
        _component: null,
    };

    return vnode;
}

render

  • 调用:ReactDOM.render(<Home />, document.body)。Home组件会被webpack转换为vnode。调用diff进行组件生命周期调用,新旧节点对比,替换等操作。
  • 注意点:初始会被套上Fragment标签。
export function render (vnode, parentDom) {
  const commitQueue = [];
  const oldVNode = parentDom._children;
  // 默认都会包裹一层Fragment
  vnode = parentDom._children = createElement(Fragment, null, vnode);

  diff(
    parentDom,
    vnode,
    oldVNode || EMPTY_OBJ,
    commitQueue,
    oldVNode ? oldVNode._dom : parentDom.firstChild,
  );

  commitRoot(commitQueue);
}

diff 三部曲

  • diff,diffChildren,diffElementNodes的调用关系
    • 每一个vNode都是由diff开始,完成生命周期执行。在diffElementNodes完成dom的创建或者复用,props比较。在diffChildren中完成dom的插入。
    • image.png

例子

class Child extends React.Component {
    render() {
        return (
            <span>hi</span>
        )
    }
}
class Home extends React.Component {
    render() {
        return (
            <div>
                <h1>hello</h1>
                <Child />
            </div>
        )
    }
}

ReactDOM.render(<Home />, document.body);

image.png

注意点

  • parentDom,excessDomChildren, oldDom参数。这几个只对nodes起作用,组件只做传递作用。 也就是会在diffElementNodes修改,diff,diffChildren只做传递。
    • parentDom:当前nodes的父节点。diffElementNodes对比真实nodes之后,传递的parentDom就会变为当前节点。因为子noeds的父节点是当前节点。
    • excessDomChildren: 当前nodes的子节点,只有真实nodes才会有子节点,diffElementNodes对比真实nodes之后会重新赋值。
    • oldDom:大部分情况都是oldChildren[0]._dom, 也就是旧节点的第一个真实子节点。
  • dom参数:diff调用diffElementNodes,真实的旧节点,oldVNodes._dom。

commitRoot

  • 首次渲染时,子组件的compomentDidMount要比父组件的compomentDidMount先执行,componentDidUpdate是要等到render之后才执行。preact做法是将compomentDidMount和componentDidUpdate保存在组件_renderCallbacks本身的数组中。
  • this.setState的callback回调函数也是放在_renderCallbacks中
 export function commitRoot(commitQueue, root) {
    commitQueue.some(c => {
        try {
            commitQueue = c._renderCallbacks;
            c._renderCallbacks = [];
            commitQueue.some(cb => {
                cb.call(c);
            });
        } catch (e) {
            options._catchError(e, c._vnode);
        }
    });
}
  • 用一开始的render函数中往下传递commitQueue数组,根据渲染顺序push所有的组件,这样深度比较深的组件会在数组的前面,渲染完之后再去执行对应的生命周期。