preact源码分析(四)

4次阅读

共计 5232 个字符,预计需要花费 14 分钟才能阅读完成。

前言
上一期我们了解到了 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 的组件时渲染的大致流程如下图。

正文完
 0