前言
在本节内容, 我将带领大家浏览一边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非首次渲染的流程,加油~!
发表回复