Preact源码阅读(一)- 从render开始

2,110 阅读8分钟

本系列研究Preact10.4.6的具体实现,包括生命周期、diff、更新机制、hooks和异常处理等功能的是实现。本节内容研究render功能、初始化阶段和生命周期的映射及执行,可能需要了解JSX到VNo的转换,可以参考如下的文章。

1. App的转换

对于render函数,是我们常见的写法,但它在产物的实际内容是什么那,其实转换成虚拟的vnode节点。

import { createElement, render, Component, Fragment } from 'preact';
class App extends Component {}
render(<App />, document.body);

如在某个demo中,webpack的产物,render(, document.body)转换为如下的形式

Object(preact__WEBPACK_IMPORTED_MODULE_0__["render"]) // render函数调用
// createElement调用
(Object(preact__WEBPACK_IMPORTED_MODULE_0__["createElement"])(App, null), 
// 挂载的节点
document.body);

因此,转化为createElement(App, null)的调用,从而生成vnode dom tree,更详细的分析可以参见 Preact 源码阅读(零)- JSX到vdom的转换 。 这里其实有个问题,createElement(App, null)实际只完成了App的vnode dom tree, App的render函数是在什么时候调用生成vnode的,我们看下render后续的调用是如何完成初始化及生命周期的映射的。

2. 生命周期映射

2.1 render/hybrate

在分析render函数之前,我们先看下render/hybrate的区别。相对于render,hybrate的功能就是“hydrate 描述的是 ReactDOM 复用 ReactDOMServer 服务端渲染的内容时尽可能保留结构,并补充事件绑定等 Client 特有内容的过程。” Hybrate的应用就是SSR的场景,在Server将首帧的html插入根节点中,这样首帧时,我们只需要完善事件绑定等内容即可。例如Preact10+版本里,preact使用preact-render-to-string将内容转换成string,插入到根节点,从而可以复用html结构及内容,提高渲染速度。

2.2 初始化过程

如下图所示,为Preact render的函数调用图,初始化阶段调用diff,diff、diffChildren、diffElementNodes三者之间递归调用,完成整个vdom tree的构建及vdom tree到dom的转换。

本节主要是分析render、diff及初始化阶段生命周期的映射,具体的diff算法将在后续的章节深入分析。

2.2.1 render

render函数的功能很简单,就是将preact虚拟node插入到某个Element中。如1中的demo, render(, document.body)就是的vnode转换成dom插入到document.body中。 render的参数有三个vnode、parentDom、replaceNode, 其具体的功能如下:

  • vnode。等待插入的虚拟节点。
  • parentDom。待插入的父元素(Dom Element)。
  • replaceNode。当前待替换的虚拟节点。

render函数的处理步骤可以分为三个,具体的功能分析见注释:

// debug/test场景使用,用来检测vnode/parentDom的合法性,参见debug/src/debug
if (options._root) options._root(vnode, parentDom);

// hybrate模式判断,hybrate函数调用render(vnode, parentoDom, IS_HYDRATE);
let isHydrating = replaceNode === IS_HYDRATE;

// 获取首次的oldNode
// isHydrating模式下: oldNode = null
// render模式下: (replaceNode && replaceNode._children) || parentDom._children
let oldVNode = isHydrating
   ? null
   : (replaceNode && replaceNode._children) || parentDom._children;

// 包裹在Fragment下,作为Fragment的子元素
vnode = createElement(Fragment, null, [vnode]);

// 定义diff后队列,diff结束后调用,包括render后生命周期及setState callback
let commitQueue = [];// 后续的diff函数中分析
diff(
    parentDom,
    ((isHydrating ? parentDom : replaceNode || parentDom)._children = vnode),
    oldVNode || EMPTY_OBJ,
    EMPTY_OBJ,
    parentDom.ownerSVGElement !== undefined,
    replaceNode && !isHydrating
        ? [replaceNode]
        : oldVNode
        ? null
        : parentDom.childNodes.length
        ? EMPTY_ARR.slice.call(parentDom.childNodes)
        : null,
    commitQueue,
    replaceNode || EMPTY_OBJ,
    isHydrating
 );
// 触发队列中的callback
commitRoot(commitQueue, vnode);
// 功能参照如下的注释
export function commitRoot(commitQueue, root) {
    // _commit hooks,参见hooks/src/index
    if (options._commit) {
        options._commit(root, commitQueue);
    }
    // commitQueue队列_renderCallbacks调用
    // 后续可以看到render生命周期的回调会push进commitQueue
    commitQueue.some(c => {
        try {
            commitQueue = c._renderCallbacks;
            c._renderCallbacks = [];
            commitQueue.some(cb => {
                cb.call(c);
            });
        } catch (e) {
            options._catchError(e, c._vnode);
        }
    });
}

2.2.2 diff

diff函数是Preact框架的关键函数,主要用来比较两个虚拟节点并将变化插入到Dom Element中。其函数如参如下:

export function diff(
    parentDom,
    newVNode,
    oldVNode,
    globalContext,
    isSvg,
    excessDomChildren,
    commitQueue,
    oldDom,
    isHydrating
) {};

diff的功能依据vnode的type类型,分为两种类型:

  • 组件(Function/Class Component)。完成组件生命周期的调用、调用diffChildren完成children的diff。
  • 标签元素(div/h1/...等)。调用diffElementNodes完成节点的diff。

2.2.2.1 参数初始化

diff在执行核心代码前,定义了临时变量、执行_diff Hooks。

// 定义tmp临时变量,newType
let tmp,
    newType = newVNode.type;
// 防止JSON-Injecttion注入
if (newVNode.constructor !== undefined) return null;
// _diff hooks, 开发者可在diff之前调用
if ((tmp = options._diff)) tmp(newVNode);
复制代码

2.2.2.2 组件处理

diff对于组件的核心处理,包括生命周期的映射、diffChildren的调用,其执行流程图下:

// 初始化相关变量
let c, isNew, oldProps, oldState, snapshot, clearProcessingException;
let newProps = newVNode.props;

//context处理,放到后面context分析时介绍
tmp = newType.contextType;
let provider = tmp && globalContext[tmp._id];
let componentContext = tmp
    ? provider
        ? provider.props.value
        : tmp._defaultValue
    : globalContext;// 分为两种场景

// 初始化已完成:调用vnode._component(React Component组件实例)
// 初始化未完成:完成React组件初始化
if (oldVNode._component) {
    c = newVNode._component = oldVNode._component;
    clearProcessingException = c._processingException = c._pendingError;
} else {
  // 两类组件处理
  // Class Component, prototype和render存在时
  // Function Component, 非Class Component
  if ('prototype' in newType && newType.prototype.render) {
     // new Class Component得到Preact Component实例
     newVNode._component = c = new newType(newProps, componentContext);
  } else {
    // Function Component,new Component完成组件的实例化
    newVNode._component = c = new Component(newProps, componentContext);
    // 设置constructor为Function Component
    // render为{ return this.constructor(props, context); }
    c.constructor = newType;
    c.render = doRender;
  }
  // context 发不事件
  if (provider) provider.sub(c);
  // 设置当前props为传入props
  c.props = newProps;
  // 初始化state
  if (!c.state) c.state = {};
  // 设置context
  c.context = componentContext;
  // 设置全局context
  c._globalContext = globalContext;
  // 设置首次更新、更新为true
  isNew = c._dirty = true;
  // 设置render回调,render结束后调用
  c._renderCallbacks = [];
}

// 触发getDerivedStateFromProps生命周期
// 初始化_nextState
if (c._nextState == null) {
    c._nextState = c.state;
}

// getDerivedStateFromProps存在时,执行getDerivedStateFromProps生命周期
if (newType.getDerivedStateFromProps != null) {
    // 两者相等时,浅拷贝c.state
    if (c._nextState == c.state) {
        c._nextState = assign({}, c._nextState);
    }
    // getDerivedStateFromProps得到的state浅拷贝到_nextState
    assign(
        c._nextState,
        newType.getDerivedStateFromProps(newProps, c._nextState)
    );
}

// 获取oldProps/oldState
oldProps = c.props;
oldState = c.state;
// 分为两种:
// 首次触发时,componentWillMount/componentDidMount
// 非首次触发,componentWillReceiveProps/shouldComponentUpdate触发
if (isNew) {
    // getDerivedStateFromProps不存在且componentWillMount存在时调用
    if (
        newType.getDerivedStateFromProps == null &&
        c.componentWillMount != null
    ) {
        c.componentWillMount();
    }
    // componentDidMount存在时,放入c._renderCallbacks
    if (c.componentDidMount != null) {
        c._renderCallbacks.push(c.componentDidMount);
    }
} else {
    // getDerivedStateFromProps不存在且newProps不等于oldProps且componentWillReceiveProps存在
    if (
        newType.getDerivedStateFromProps == null &&
        newProps !== oldProps &&
        c.componentWillReceiveProps != null
    ) {
        c.componentWillReceiveProps(newProps, componentContext);
    }

    // 两种状态不需要diff更新:
    // 非强制更新且shouldComponentUpdate存在且shouldComponentUpdate()返回false
    // 新旧虚拟节点_original相等
    if (
        (!c._force &&
            c.shouldComponentUpdate != null &&
            c.shouldComponentUpdate(
                newProps,
                c._nextState,
                componentContext
            ) === false) ||
        newVNode._original === oldVNode._original
    ) {
        // 更新Props/State
        c.props = newProps;
        c.state = c._nextState;

        // https://gist.github.com/JoviDeCroock/bec5f2ce93544d2e6070ef8e0036e4e8
        if (newVNode._original !== oldVNode._original) c._dirty = false;

        // 更新_vnode/_dom/_children
        c._vnode = newVNode;
        newVNode._dom = oldVNode._dom;
        newVNode._children = oldVNode._children;

        // _renderCallbacks不为空时,push进回调数组
        if (c._renderCallbacks.length) {
            commitQueue.push(c);
        }

        // children re order
        reorderChildren(newVNode, oldDom, parentDom);
        break outer;
    }

    // componentWillUpdate存在时调用
    if (c.componentWillUpdate != null) {
        c.componentWillUpdate(newProps, c._nextState, componentContext);
    }

    // componentDidUpdate存在时,push进去_renderCallbacks
    if (c.componentDidUpdate != null) {
        c._renderCallbacks.push(() => {
            c.componentDidUpdate(oldProps, oldState, snapshot);
        });
    }
}

// render前调用与更新, context、props、state
c.context = componentContext;
c.props = newProps;
c.state = c._nextState;

// _render hooks, 开发者可注册
if ((tmp = options._render)) tmp(newVNode);

// render更新_dirty/vnode/_parentDom
c._dirty = false;
c._vnode = newVNode;
c._parentDom = parentDom;

// 调用render生成新的vnode节点
tmp = c.render(c.props, c.state, c.context);
// 更新state为nextState
c.state = c._nextState;

// childContext,后续context分析时调用
if (c.getChildContext != null) {
    globalContext = assign(assign({}, globalContext), c.getChildContext());
}

// getSnapshotBeforeUpdate生命周期调用
if (!isNew && c.getSnapshotBeforeUpdate != null) {
    snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState);
}

// 是否为根节点(root节点)
let isTopLevelFragment =
    tmp != null && tmp.type == Fragment && tmp.key == null;

// 根Fragment节点,返回props.children,否则返回tmp。
let renderResult = isTopLevelFragment ? tmp.props.children : tmp;// 递归children的处理
diffChildren(
    parentDom,
    Array.isArray(renderResult) ? renderResult : [renderResult],
    newVNode,
    oldVNode,
    globalContext,
    isSvg,
    excessDomChildren,
    commitQueue,
    oldDom,
    isHydrating
);

// c._base等于_dom
c.base = newVNode._dom;

// _renderCallbacks不为空时,push commitQueue队列
if (c._renderCallbacks.length) {
    commitQueue.push(c);
}
// 清空error/exception
if (clearProcessingException) {
    c._pendingError = c._processingException = null;
}

// 设置强制更新为false
c._force = false;

diff函数对React Component,完成了生命周期的映射和调用,依据是否实例化,可以分为如下两种场景:

  • 首次调用。完成React Component实例化,依据是否有getDerivedStateFromProps,走新旧的生命周期。

  • 第二次调用。更新_component, 依据是否有getDerivedStateFromProps,走新旧的生命周期更新流程。

diff函数除了组件的生命周期映射,就是Render函数的调用及diffChildren函数的调用。render函数的调用,对于Function Component,定义c.constructor = newType;c.render = doRender;直接调用函数得到Vnode。对于Class Component函数,直接调用Class组件的render函数得到vnode。

tmp = c.render(c.props, c.state, c.context);
...
diffChildren(...);

diffChildren处理了children的vdom diff及生命周期的调用。diffChilren里调用diff,去处理每个children的的diff,实际上也就是完成组件和标签的diff过程, 后续的diff过程将在vdom diff算法详细分析,我们这里就先看看父子组件的生命周期调用顺序,下面以React 新生命周期初始化阶段为例,简单介绍父子组件的生命周期执行顺序。

  • render之前的生命周期会在diff运行时调用,因此父子组件的render前执行顺序为父->子。

  • render之后的生命周期将放在commitQueue队列中,将按照子进队、父进队的形式进入,因此commitRoot的some调用时,render后的生命周期将是子->父。

2.2.2.3 标签元素处理

在非组件的类型元素下,preact分为两种场景去处理:

  • parentDom子节点为null且_original相同时,不走diff流程,直接赋值即可。

  • 标签元素的正常diff流程,使用diffElementNodes完成标签元素的diff。

    // parentDom子节点为null且_original相等时,不走diff过程,直接赋值 if ( excessDomChildren == null && newVNode._original === oldVNode._original ) { newVNode._children = oldVNode._children; newVNode._dom = oldVNode._dom; } else { // 标签元素的diff过程,diffElementNodes newVNode._dom = diffElementNodes( oldVNode._dom, newVNode, oldVNode, globalContext, isSvg, excessDomChildren, commitQueue, isHydrating ); }

3. 总结

本节内容主要介绍了render、diff、commitRoot的功能,重点介绍了React组件是如何实例化及生命周期的映射及执行顺序的分析。

  • diff函数依据isNew的判断,对Class/Function Component 进行实例化。
  • 组件的生命周期分为两类,render之前,diff时调用,render之后,在完成整体的diff之后,执行commitQueue队列,完成后续的生命周期的执行。
  • 父子组件的生命周期,按照父render前-render-子render前-子render-子render后-父render后的执行顺序执行。 下一节的内容,主要是介绍Preact整体的diff流程及vnode到html的转换。

打个小广告,团队持续扩展中,欢迎各位同学投递。大家可以如下的方式进行投递:

  • 通过如下的链接投递(job.toutiao.com/s/JMpw6Rr).
  • 可以将简历发送我的邮箱(leisureljc@outlook.com),我来给您投递。

4. 参考文档