本系列研究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),我来给您投递。