preact源码分析(四)

前言
上一期我们了解到了Preact渲染普通的节点(VNode的type不是Function类型)时的过程。本期我带大家来了解下当渲染的是一个组件时preact中发生了什么。当然为了降低阅读源码的复杂度, 我们本次只讨论初次渲染组件的情况。暂不考虑setState时组件更新的情况。
我们将从官方的Demo入手, 一步步了解渲染的过程

import { h, render, Component } from ‘preact’;

class Clock extends Component {
render() {
let time = new Date().toLocaleTimeString();
return <span>{ time }</span>;
}
}

// 将一个时钟渲染到 <body > 标签:
render(<Clock />, document.body);
src/render.js
同渲染普通类型的VNode节点一样, 将VNode包裹一层Fragment后, VNode进入了diffChildren方法中。唯一值得注意的时VNode的type为Clock, 而非普通的字符串类型。我们将在diff方法看到两者的具体的区别。

export function render(vnode, parentDom) {

// render时_prevVNode还没没有挂载, 此时为null
let oldVNode = parentDom._prevVNode;

// 使用Fragment包裹VNode
// {
// type: ‘Fragment’,
// props: {
// children: [
// {
// type: Clock, 这里类型不是字符串
// props: {
// }
// }
// ]
// }
// }
vnode = createElement(Fragment, null, [vnode]);

let mounts = [];
// 将当前的VNode挂载到parentDom的_prevVNode属性
diffChildren(
parentDom,
parentDom._prevVNode = vnode,
oldVNode,
EMPTY_OBJ,
parentDom.ownerSVGElement!==undefined, oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes), mounts,
vnode
);

// 执行已挂载组件的componentDidMount生命周期
commitRoot(mounts, vnode);
}
src/diff/children.js
在渲染组件VNode时, 流程大致和渲染普通的VNode一致。都是将通过diff算法返回Dom节点append到parentDom中。

export function diffChildren(
parentDom,
newParentVNode,
oldParentVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent
) {
let childVNode, i, j, p, index, oldVNode, newDom,
nextDom, sibDom, focus,
childDom;

// 将扁平的的VNode挂载到_children属性上
// [ { type: Clock, props: { … } } ]
let newChildren = newParentVNode._children || toChildArray(newParentVNode.props.children, newParentVNode._children=[], coerceToVNode);
// oldChildren此时为[]
let oldChildren = []

let oldChildrenLength = oldChildren.length;

// … 省略一部分源码

// 遍历VNode节点
for (i=0; i<newChildren.length; i++) {
childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
oldVNode = index = null;

p = oldChildren[i];

// … 省略一部分源码,这里主要是查询可以复用的DOM节点,

// 进入diff算法比较新旧VNode节点
newDom = diff(
oldVNode==null ? null : oldVNode._dom,
parentDom,
childVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
null
);

if (childVNode!=null && newDom !=null) {

else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {

if (childDom==null || childDom.parentNode!==parentDom) {
parentDom.appendChild(newDom);
}
}
}
}
}
src/diff/index.js
在diff方法中, 主要会比较三种类型的节点。第一种Fragment类型, 第二种Function类型, 和其他类型的节点。我们的示例会用到两种判断,请仔细阅读我给源码添加的注释。
我们在首次diff中, newVNode为Clock组件, 所以我们会进入VNode.type为Functiond的分支。我们接下来会调用组件实例的render函数, 返回VNode( { tpye: ‘span’, props: { //… } } )。接下来, 递归的使用diff算法比较render返回的VNode。我们在递归的时, diff函数将进入其他类型的节点的分支,比较返回的VNode, 并且会调用diffElementNodes函数, 返回创建后dom节点。最后并添加到parentDom中。

// 这里每一个参数的含义请参考, 上一期的文章
export function diff(
dom,
parentDom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
force
) {

// 因为是初始化渲染, dom是不能复用, 我们删除整个子树, oldVNode重置为null
if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) {
dom = null;
oldVNode = EMPTY_OBJ;
}

let c, p, isNew = false, oldProps, oldState, oldContext

// 组件的类型(组件的类)
let newType = newVNode.type;

try {
outer: if (oldVNode.type===Fragment || newType===Fragment) {
// … 省略一部分源码, 这里是对Fragment类型的处理
}
else if (typeof newType===’function’) {

if (oldVNode._component) {
// … 省略一部分源码,如果是,不是第一次渲染组件的处理
}
else {
// 第一次渲染的组件处理
isNew = true;

// 如果组件的类拥有render函数, c和_component存储的是组件的实例
if (newType.prototype && newType.prototype.render) {
newVNode._component = c = new newType(newVNode.props, cctx);
} else {
// 如果组件的类没有render函数, 使用基础的Component类构建组件的实例
newVNode._component = c = new Component(newVNode.props, cctx);
// 实例c的构造函数等于组件类的构造函数
c.constructor = newType;
// render函数直接返回构造函数返回的结果
c.render = doRender;
}
// _ancestorComponent挂载的是自己父VNode, 目前指的是被Fragment包裹的那一层组件
c._ancestorComponent = ancestorComponent;

// 初始化c的props和state
c.props = newVNode.props;
if (!c.state) c.state = {};

// _dirty属性表明是否更新组件
c._dirty = true;
// 用于存储setState回调的数组
c._renderCallbacks = [];
}

// _vnode挂载组件实例的VNode节点
c._vnode = newVNode;

// s变量存储了组件的state状态
let s = c._nextState || c.state;

// 调用静态的生命周期方法getDerivedStateFromProps
// getDerivedStateFromProps生命周期返回的对象用于更新state
if (newType.getDerivedStateFromProps!=null) {
oldState = assign({}, c.state);
if (s === c.state) {
s = assign({}, s);
}
// 更新state, 如果返回null, 不更新
assign(s, newType.getDerivedStateFromProps(newVNode.props, s));
}

if (isNew) {
// 如果是第一次渲染, 并且componentWillMount不为null, 执行componentWillMount的生命周期函数
if (newType.getDerivedStateFromProps==null && c.componentWillMount!=null) {
c.componentWillMount();
}
// 将组件的实例push到已经挂载的组件的列表中
if (c.componentDidMount!=null) {
mounts.push(c);
}
}
else {
// … 省略一部分源码,不是第一次渲染组件的处理
}

// 渲染更新前的oldProps,oldState
oldProps = c.props;
if (!oldState) {
oldState = c.state;
}

// 更新组件的props和state
c.props = newVNode.props;
c.state = s;

// 初次渲染prev为null
let prev = c._prevVNode;
// 组件实例上挂载的_prevVNode的属性为当前实例调用render函数返回的VNode节点(之前c存储的是组件的实例)
let vnode = c._prevVNode = coerceToVNode(c.render(c.props, c.state, c.context));
// 这个组件禁止更新
c._dirty = false;

// 调用getSnapshotBeforeUpdate的生命周期函数
// getSnapshotBeforeUpdate()在最新的渲染输出提交给DOM前将会立即调用
if (!isNew && c.getSnapshotBeforeUpdate!=null) {
oldContext = c.getSnapshotBeforeUpdate(oldProps, oldState);
}

// vnode现在存储的是组件实例render返回的VNode
// 将render返回VNode,递归的使用diff方法继续比较,返回的dom存储base属性中
c.base = dom = diff(
dom,
parentDom,
vnode,
prev,
context,
isSvg,
excessDomChildren,
mounts,
c,
null
);

c._parentDom = parentDom;

}
else {
// 当上面的c组件render后, 将render返回VNode节点,使用diff进行递归的比较时
// render返回VNode的type是string类型, 所以进入这个分支中, 进行dom比较, 具体内容可以参考上一篇文章
dom = diffElementNodes(dom, newVNode, oldVNode, context, isSvg, excessDomChildren, mounts, ancestorComponent);
}

// 将dom挂载到_dom中
newVNode._dom = dom;
}
catch (e) {}

// 返回dom
return dom;
}
结语
我们通过上面????精简后的源码可知,如果render中是VNode的组件时渲染的大致流程如下图。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理