前言
在本节内容, 我将带领大家浏览一边 Preact 的 render 过程。当然为了降低阅读源码的难度, 我们这一次不会考虑 render 组件的情况, 也不会考虑 setState 更新的情况。大家在阅读文章的时候, 请仔细阅读我添加在代码中的注释。
src/render.js
render
render 方法在 Preact 文档中用法如下。可以看出 render 方法第一个参数是 jsx, 第二个参数是需要挂载的 DOM 节点。
import {h, render} from ‘preact’;
render((
<div id=”foo”>
<span>Hello, world!</span>
<button onClick={e => alert(“hi!”) }>Click Me</button>
</div>
), document.body);
第一个参数虽然不是 VNode, 但是可以通过 Babel 的插件 transform-react-jsx, 转换为如下的形式。其中 h 函数就是 createElement 函数。
h(
‘div’,
{‘id’: ‘foo},
h(‘span’, {}, ‘Hello, world!’),
h(‘button’, { onClick: e => alert(“hi!”) }, ‘Click Me’)
)
export function render(vnode, parentDom) {
// 第一次 render 时, parentDom 上还没有挂载_prevVNode 属性, 故 oldVNode 为 null
let oldVNode = parentDom. ;
// 使用 Fragment 包裹 vNode, 这时 VNode 的内容, 如下
// {
// type: ‘Fragment’,
// props: {
// children: {
// type: ‘content’,
// props: {
// children: [
// {
// type: ‘h1’,
// props: {
// children: [‘HelloWorld’]
// },
// }
// ]
// }
// }
// }
// }
vnode = createElement(Fragment, null, [vnode]);
let mounts = [];
// 使用 diffChildren 方法比较新旧 Vnode, 具体分析我们跳到下一节。注意这里同时挂载了_prevVNode 的属性
diffChildren(
parentDom,
parentDom._prevVNode = vnode,
oldVNode,
EMPTY_OBJ,
parentDom.ownerSVGElement!==undefined,
oldVNode ? null : EMPTY_ARR.slice.call(parentDom.childNodes),
mounts,
vnode
);
// 执行已挂载组件的 componentDidMount 生命周期, 本期文章不涉及组件的 render 故不展开
commitRoot(mounts, vnode);
}
src/diff/children.js
diff 算法是我们在学习 Preact 源码里的重头戏, 这里也是源码中最复杂的一部分。
因为整个 VNode 是一个树形的结构, 我们将从 root 节点开始,一步步分析它做了什么, 在 diff 的过程中会有很多递归的操作, 所以我们需要留意每一次递归的时函数参数的不同。
我们假设需要渲染的 Dom 如下所示,我也会忽略一些边界情况比如 type 为 SVG 标签的情况。尽可能的简单,方便理解。
render((
<div id=”content”>
<h1>HelloWorld</h1>
</div>
), document.getElementById(‘app’));
diffChildren
/**
* parentDom 为挂载的 Dom 节点 (document.body)
* newParentVNode 新的 Vnode 节点
* oldParentVNode 之前的 Vnode 节点 (第一次渲染时这里 oldParentVNode 为 null, setState 更新时这里将不为 null)
* context(第一次渲染时这里为空对象)
* isSvg(判断是否为 SVG)
* excessDomChildren 在第一次 render 时这里的值应当为 parentDom 的所有的子节点, 这里为空数组
* mounts 为空数组, mounts 中为已挂载的组件的列表
* ancestorComponent 直接父组件
*/
export function diffChildren(
parentDom,
newParentVNode,
oldParentVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent
) {
let childVNode, i, j, p, index, oldVNode, newDom,
nextDom, sibDom, focus,
childDom;
// 在这里进行操作是将 newParentVNode, oldParentVNode 扁平化。并将扁平化的子 VNode 数组挂载到 VNode 节点的_children 属性上
// vnode._children = [{ type: ‘div’, props: {…} } ]
let newChildren = newParentVNode._children ||
toChildArray(
newParentVNode.props.children,
newParentVNode._children=[],
coerceToVNode
);
let oldChildren = []
childDom = null
// … 省略一部分源码
// 对子 VNode 集合进行循环
for (i=0; i<newChildren.length; i++) {
// childVNode 为 newChildren 中的每一个 VNode
childVNode = newChildren[i] = coerceToVNode(newChildren[i]);
oldVNode = index = null;
// … 省略一部分源码
// 使用 diff 算法依次的对比每一个新旧 VNode 节点, 因为是第一次 render 所以这里 oldVNode 始终为 null
// diff 返回的是一个对比后的 dom 节点
// 我们接下来跳转到下一节去看 diff 方法的具体实现
newDom = diff(
oldVNode==null ? null : oldVNode._dom,
parentDom,
childVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
null
);
if (childVNode!=null && newDom !=null) {
if (childVNode._lastDomChild != null) {
} else if (excessDomChildren==oldVNode || newDom!=childDom || newDom.parentNode==null) {
outer: if (childDom==null || childDom.parentNode!==parentDom) {
// 将 diff 比较后更新 Dom 挂载到 parentDom 中, 完成渲染
parentDom.appendChild(newDom);
} else {
}
}
}
}
// … 省略一部分源码
}
toChildArray
toChildArray 函数会遍历的 VNode 的 Children, 将 Children 以数组的形式挂载到 VNode 的_children 属性上。
export function toChildArray(children, flattened, map) {
if (flattened == null) {
flattened = []
}
if (children==null || typeof children === ‘boolean’) {
} else if (Array.isArray(children)) {
// 如果 children 为数组, 进行递归操作
for (let i=0; i < children.length; i++) {
toChildArray(children[i], flattened);
}
} else {
flattened.push(map ? map(children) : children);
}
return flattened;
}
src/diff/index.js
diff
/**
* 旧的 Vnode 节点上的_dom 属性, 原有的 dom 节点。第一次 render 时为 null,不能复用原有的 dom 节点
* parentDom 需要挂载的父节点
* newVNode 新的 Vnode 节点
* oldVNode 旧的 Vnode 节点, 第一次 render 这里为 null
* context 此时为空对象
* isSvg
* excessDomChildren 为空数组
* mounts 为空数组
* ancestorComponent 直接父组件, render 时的 VNode 节点
* force 为 null
*/
export function diff(
dom,
parentDom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent,
force
) {
// 如果 oldVNode,newVNode 类型不同, dom 节点不能复用。
if (oldVNode==null || newVNode==null || oldVNode.type!==newVNode.type) {
// … 省略一部分源码
dom = null;
oldVNode = {};
}
let c, p, isNew = false, oldProps, oldState, oldContext
// newType 为 newVNode 节点的类型
let newType = newVNode.type;
let clearProcessingException;
try {
outer: if (oldVNode.type===Fragment || newType===Fragment) {
// 当 type 为 Fragment 时,这里先略过
// … 省略一部分源码
} else if (typeof newType===’function’) {
// 当 type 为组件时
// … 省略一部分源码, 这里先略过
} else {
// 我们这里先关注 type 等于 string 的情况
// 我们接下来跳转到下一节去看 diffElementNodes 方法的具体实现
// diffElementNodes 将会返回对比后 dom
dom = diffElementNodes(
dom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent
)
if (newVNode.ref && (oldVNode.ref !== newVNode.ref)) {
applyRef(newVNode.ref, dom, ancestorComponent);
}
}
// 挂载 dom 节点到_dom 的属性上
newVNode._dom = dom;
// … 省略一部分源码
}
catch (e) {
catchErrorInComponent(e, ancestorComponent);
}
// 返回 dom 节点
return dom;
}
diffElementNodes
diffElementNodes 顾名思义就是对新旧的非组件的 ElementNodes 节点比较
function diffElementNodes(
dom,
newVNode,
oldVNode,
context,
isSvg,
excessDomChildren,
mounts,
ancestorComponent
) {
let d = dom;
// … 省略一部分源码
if (dom==null) {
// 由于是第一次渲染所以不能复用原有的 dom, 需要创建 dom 节点
dom = newVNode.type===null ? document.createTextNode(newVNode.text) : document.createEleme(newVNode.type)
// 创建了一个新的父节点,因此以前的子节点都不能重用, excessDomChildren 置为 null
excessDomChildren = null;
}
// 将创建好的 dom 节点挂载到_dom 属性上
newVNode._dom = dom;
if (newVNode.type===null) {
// … 省略一部分源码
}
else {
if (excessDomChildren!=null && dom.childNodes!=null) {
// … 省略一部分源码
}
if (newVNode!==oldVNode) {
let oldProps = oldVNode.props;
if (oldProps==null) {
oldProps = {};
if (excessDomChildren!=null) {
// … 省略一部分源码
}
}
// … 省略一部分源码, 这里是对 dangerouslySetInnerHTML 的处理
// 子节点依然可能包含子节点所以将当前的节点做为父节点,使用 diffChildren 遍历子节点 diff
// 注意这时 parentDom 这个参数的值,是刚刚创建的 dom
diffChildren(
dom,
newVNode,
oldVNode,
context,
newVNode.type===’foreignObject’ ? false : isSvg,
excessDomChildren,
mounts,
ancestorComponent
)
// 对比新旧的 props, 挂载到 dom 上,diffProps 函数本身并复杂
diffProps(
dom,
newVNode.props,
oldProps,
isSvg
);
}
}
// 返回更新后的 dom 节点
return dom;
}
src/diff/props.js
diffProps
export function diffProps(dom, newProps, oldProps, isSvg) {
for (let i in newProps) {
// 对新的 props 处理
if (i!==’children’ && i!==’key’ && (!oldProps || oldProps[i]!=newProps[i])) {
setProperty(dom, i, newProps[i], oldProps[i], isSvg);
}
}
for (let i in oldProps) {
// 对新的 props 中不存在的属性处理
if (i!==’children’ && i!==’key’ && (!newProps || !(i in newProps))) {
setProperty(dom, i, null, oldProps[i], isSvg);
}
}
}
setProperty
function setProperty(dom, name, value, oldValue, isSvg) {
let v;
// 对 class 属性处理
// 如果是 svg 属性使用 className
if (name===’class’ || name===’className’) name = isSvg ? ‘class’ : ‘className’;
// 对 style 属性进行处理
if (name===’style’) {
let s = dom.style;
if (typeof value===’string’) {
s.cssText = value;
} else {
if (typeof oldValue===’string’) {
s.cssText = ”;
}
for (let i in oldValue) {
// 对驼峰的样式处理
if (value==null || !(i in value)) {
s.setProperty(i.replace(CAMEL_REG, ‘-‘), ”);
}
}
for (let i in value) {
v = value[i];
// 对驼峰的样式和数字进行处理
if (oldValue==null || v!==oldValue[i]) {
s.setProperty(
i.replace(CAMEL_REG, ‘-‘),
typeof v===’number’ && IS_NON_DIMENSIONAL.test(i)===false ? (v + ‘px’) : v
);
}
}
}
} else if (name===’dangerouslySetInnerHTML’) {
return;
} else if (name[0]===’o’ && name[1]===’n’) {
// 对事件进行处理
let useCapture = name !== (name=name.replace(/Capture$/, ”));
let nameLower = name.toLowerCase();
name = (nameLower in dom ? nameLower : name).substring(2);
if (value) {
if (!oldValue) dom.addEventListener(name, eventProxy, useCapture);
}
else {
dom.removeEventListener(name, eventProxy, useCapture);
}
(dom._listeners || (dom._listeners = {}))[name] = value;
} else if (name!==’list’ && !isSvg && (name in dom)) {
dom[name] = value==null ? ” : value;
} else if (value==null || value===false) {
// 删除属性
dom.removeAttribute(name);
} else if (typeof value!==’function’) {
// 设置属性
dom.setAttribute(name, value);
}
}
结语
我们通过上面???? 精简后的源码可知,如果 render 中是 VNode 的 type 为普通 ElementNodes 节点的渲染的大致流程如下。
如果渲染流程外,我们也知道 VNode 节点上,几个私有属性的含义
下面两期将会介绍, 渲染组件的流程以及 setState 非首次渲染的流程,加油~!