首先欢迎你们关注个人掘金帐号和Github博客,也算是对个人一点鼓励,毕竟写东西无法得到变现,能坚持下去也是靠的是本身的热情和你们的鼓励。
以前分享过几篇关于React的文章:javascript
其实我在阅读React源码的时候,真的很是痛苦。React的代码及其复杂、庞大,阅读起来挑战很是大,可是这却又挡不住咱们的React的原理的好奇。前段时间有人就安利过Preact,千行代码就基本实现了React的绝大部分功能,相比于React动辄几万行的代码,Preact显得别样的简洁,这也就为了咱们学习React开辟了另外一条路。本系列文章将重点分析相似于React的这类框架是如何实现的,欢迎你们关注和讨论。若有不许确的地方,欢迎你们指正。
在上篇文章从preact了解一个类React的框架是怎么实现的(一): 元素建立咱们了解了咱们平时所书写的JSX是怎样转化成Preact中的虚拟DOM结构的,接下来咱们就要了解一下这些虚拟DOM节点是如何渲染成真实的DOM节点的以及虚拟DOM节点的改变如何映射到真实DOM节点的改变(也就是diff算法的过程)。这篇文章相比第一篇会比较冗长和枯燥,为了能集中分析diff过程,咱们只关注dom元素,暂时不去考虑组件。 css
render
函数 咱们知道在React中渲染是并非由React完成的,而是由ReactDOM中的render
函数去实现的。其实在最先的版本中,render
函数也是属于React的,只不事后来React的开发者想实现一个于平台无关的库(其目的也是为了React Native服务的),所以将Web中渲染的部分独立成ReactDOM库。Preact做为一个极度精简的库,render
函数是属于Preact自己的。Preact的render
函数与ReactDOM的render
函数也是有有所区别的:html
ReactDOM.render(
element,
container,
[callback]
)复制代码
ReactDOM.render
接受三个参数,element
是须要渲染的React元素,而container
挂载点,即React元素将被渲染进container
中,第三个参数callback
是可选的,当组件被渲染或者更新的时候会被调用。ReactDOM.render
会返回渲染组元素的真实DOM节点。若是以前container
中含有dom节点,则渲染时会将以前的全部节点清除。例如:java
html:node
<div id="root">
<div>Hello React!</div>
</div>复制代码
javascript:react
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);复制代码
最终的显示效果为:git
Hello, world!github
而Preact的render
函数为: 算法
Preact.render(
vnode,
parent,
[merge]
)复制代码
Preact.render
与ReactDOM.render
的前两个参数表明的意义相同,区域在于最后一个,Preact.render
可选的第三个参数merge
,要求必须是第二个参数的子元素,是指会被替换的根节点,不然,若是没有这个参数,Preact 默认追加,而不是像React进行替换。
例如不存在第三个参数的状况下:浏览器
html:
<div id="root">
<div id='container'>Hello Preact!</div>
</div>复制代码
javascript:
preact.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);复制代码
最终的显示效果为:
Hello Preact
Hello, world!
若是调用函数有第三个参数:
javascript:
preact.render(
<h1>Hello, world!</h1>,
document.getElementById('root'),
document.getElementById('container')
);复制代码
显示效果是:
Hello, world!
实现
其实在Preact中不管是初次渲染仍是以后虚拟DOM改变致使的UI更新最终调用的都是diff
函数,这也是很是合理的,毕竟咱们能够将首次渲染当作是diff
过程当中用现有的虚拟dom去与空的真实dom基础上进行更新的过程。下面咱们首先给出整个diff
过程的大体流程图,咱们能够对照流程图对代码进行分析:
render
函数入手,
render
函数调用的就是
diff
函数:
function render(vnode, parent, merge) {
return diff(merge, vnode, {}, false, parent, false);
}复制代码
咱们能够看到Preact中的render
调用了diff
函数,而diff
定义在vdom/diff
中:
function diff(dom, vnode, context, mountAll, parent, componentRoot) {
// diffLevel为 0 时表示第一次进入diff函数
if (!diffLevel++) {
// 第一次执行diff,查看咱们是否在diff SVG元素或者是元素在SVG内部
isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
// hydration 指示的是被diff的现存元素是否含有属性props的缓存
// 属性props的缓存被存在dom节点的__preactattr_属性中
hydrating = dom!=null && !(ATTR_KEY in dom);
}
let ret = idiff(dom, vnode, context, mountAll, componentRoot);
// 若是父节点以前没有建立的这个子节点,则将子节点添加到父节点以后
if (parent && ret.parentNode!==parent) parent.appendChild(ret);
// diffLevel回减到0说明已经要结束diff的调用
if (!--diffLevel) {
hydrating = false;
// 负责触发组件的componentDidMount生命周期函数
if (!componentRoot) flushMounts();
}
return ret;
}复制代码
这部分的函数内容比较庞杂,很难作到面面俱到,我会在代码中作相关的注释。diff
函数主要负责就是将当前的虚拟node节点映射到真实的DOM节点中。参数以下:
vnode
: 不用说,就是咱们须要渲染的虚拟dom节点parent
: 就是你要将虚拟dom挂载的父节点dom
: 这里的dom其实就是当前的vnode所对应的以前未更新的真实dom。那么就有两种可能: 第一就是null
或者是上面例子的contaienr
(就是render
函数对应的第三个参数),其本质都是首次渲染,第二种就是vnode的对应的未更新的真实dom,那么对应的就是渲染刷新界面。context
: 组件相关,暂时能够不考虑,对应React中的context
。mountAll
: 组件相关,暂时能够不考虑componentRoot
: 组件相关,暂时能够不考虑 vnode
对应的就是一个递归的结构,那么不用想diff
函数确定也是递归的。咱们首先看一下函数初始的几个变量:
diffLevel
:用来记录当前渲染的层数(递归的深度),其实在代码中并无在进入每层递归的时候都增长而且退出递归的时候减少。只是记录了是否是渲染的第一层,因此对应的值只有0
与1
。isSvgMode
:用来指代当前的渲染是否内SVG元素的内部或者咱们是否在diff一个SVG元素(SVG元素须要特殊处理)。hydrating
: 这个变量是我一直所困惑的,我还专门查了一下,hydrating
指的是保湿、吸水 的意思。hydrating = dom != null && !(ATTR_KEY in dom);
(ATTR_KEY
对应常量__preactattr_
,preact会将props等缓存信息存储在dom的__preactattr_
属性中),做者给的是下面的注释:hydration is indicated by the existing element to be diffed not having a prop cache
也就是说hydrating
是指当前的diff
的元素没有缓存可是对应的dom元素必须存在。那么何时才会出现dom节点中没有存储缓存?只有当前的dom节点并不是由Preact所建立并渲染的才会使得hydrating
为true。
idiff
函数就是diff
算法的内部实现,相对来讲代码会比较复杂,idiff
会返回虚拟dom对应建立的真实dom节点。下面的代码是是向父级元素有选择性添加建立的dom节点,之因此这么作,主要是有可能以前该节点就没有渲染过,因此须要将新建立的dom节点添加到父级dom。可是若是仅仅只是修改了以前dom中的某一个属性(好比样式),那么实际上是不须要添加的,由于该dom节点已经存在于父级dom。
后面的内容,一方面结束递归以后,回置diffLevel
(diffLevel
此时应该为0,代表此时要退出diff
函数),退出diff
前,将hydrating
置为false
,至关于一个复位的功能。下面的flushMounts
函数是组件相关,在这里咱们只须要知道它要作的就是去执行全部刚才安装组件的componentDidMount
生命周期函数。
下面让咱们看看idiff
的实现(代码已经分块,具体见注释),代码比较长,能够先大体浏览一下,作到内心有数,下面会逐块分析,能够对照流程图看:
/** 内部的diff函数 */
function idiff(dom, vnode, context, mountAll, componentRoot) {
// block-1
let out = dom, prevSvgMode = isSvgMode;
// 空的node 渲染空的文本节点
if (vnode==null || typeof vnode==='boolean') vnode = '';
// String & Number 类型的节点 建立/更新 文本节点
if (typeof vnode==='string' || typeof vnode==='number') {
// 更新若是存在的原有文本节点
// 这里若是节点值是文本类型,其父节点又是文本类型的节点,则直接更新
if (dom && dom.splitText!==undefined && dom.parentNode && (!dom._component || componentRoot)) {
if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}
}
else {
// 不是文本节点,替换以前的节点,回收以前的节点
out = document.createTextNode(vnode);
if (dom) {
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
recollectNodeTree(dom, true);
}
}
out[ATTR_KEY] = true;
return out;
}
// block-2
// 若是是VNode表明的是一个组件,使用组件的diff
let vnodeName = vnode.nodeName;
if (typeof vnodeName==='function') {
return buildComponentFromVNode(dom, vnode, context, mountAll);
}
// block-3
// 沿着树向下时记录记录存在的SVG命名空间
isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;
// 若是不是一个已经存在的元素或者类型有问题,则从新建立一个
vnodeName = String(vnodeName);
if (!dom || !isNamedNode(dom, vnodeName)) {
out = createNode(vnodeName, isSvgMode);
if (dom) {
// 移动dom中的子元素到out中
while (dom.firstChild) out.appendChild(dom.firstChild);
// 若是以前的元素已经属于某一个DOM节点,则将其替换
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// 回收以前的dom元素(跳过非元素类型)
recollectNodeTree(dom, true);
}
}
// block-4
let fc = out.firstChild,
props = out[ATTR_KEY],
vchildren = vnode.children;
if (props==null) {
props = out[ATTR_KEY] = {};
for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
}
// 优化: 对于元素只包含一个单一文本节点的优化路径
if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
if (fc.nodeValue!=vchildren[0]) {
fc.nodeValue = vchildren[0];
}
}
// 不然,若是有存在的子节点或者新的孩子节点,执行diff
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
}
// 将props和atrributes从VNode中应用到DOM元素
diffAttributes(out, vnode.attributes, props);
// 恢复以前的SVG模式
isSvgMode = prevSvgMode;
return out;
}复制代码
idiff
函数所接受的参数与diff
是彻底相同的,可是两者也是有所区别的。diff
在渲染过程(或者更新过程)中仅仅会调用一次,因此说diff
函数接受的vnode
就是整个应用的虚拟dom,而dom
也就是当前整个虚拟dom所对应的节点。可是idiff
的调用是递归的,所以dom
和vnode
在开始时与diff
函数相等,可是在以后递归的过程当中,就对应的是整个应用的部分。
变量prevSvgMode
用来存储以前的isSvgMode
,目的就是在退出这一次递归调用时恢复到调用前的值。而后若是vnode是null
或者布尔类型,都按照空字符去处理。接下的渲染是整对于字符串(sting
或者number
类型),主要分为两部分: 更新或者建立元素。若是dom自己存在而且就是一个文本节点,那就只须要将其中的值更新为当前的值便可。不然建立一个新的文本节点,而且将其替换到父元素上,并回收以前的节点值。由于文本节点是没有什么须要缓存的属性值(文本的颜色等属性实际是存储的父级的元素中),因此直接将其ATTR_KEY
(实际值为__preactattr_
)赋值为true
,并返回新建立的元素。这段代码有两个须要注意的地方:
if (dom.nodeValue!=vnode) {
dom.nodeValue = vnode;
}复制代码
为何在赋值文本节点值时,须要首先进行一个判断?根据代码注释得知Firfox浏览器不会默认作等值比较(其余的浏览器例如Chrome即便直接赋值,若是相等也不会修改dom元素),因此人为的增长了比较的过程,目的就是为了防止文本节点每次都会被更新,这算是一个浏览器怪癖(quirk)。
回收dom节点的recollectNodeTree
函数作了什么?看代码:
/** * 递归地回收(或者卸载)节点及其后代节点 * @param node * @param unmountOnly 若是为`true`,仅仅触发卸载的生命周期,跳过删除 */
function recollectNodeTree(node, unmountOnly) {
let component = node._component;
if (component) {
// 若是该节点属于某个组件,卸载该组件(最终在这里递归),主要包括组件的回收和相依卸载生命周期的调用
unmountComponent(component);
}
else {
// 若是节点含有ref函数,则执行ref函数,参数为null(这里是React的规范,用于取消设置引用)
// 确实在React若是设置了ref的话,在卸载的时候,也会被回调,获得的参数是null
if (node[ATTR_KEY]!=null && node[ATTR_KEY].ref) node[ATTR_KEY].ref(null);
if (unmountOnly===false || node[ATTR_KEY]==null) {
//要作的无非是从父节点将该子节点删除
removeNode(node);
}
//递归删除子节点
removeChildren(node);
}
}
/** * 回收/卸载全部的子元素 * 咱们在这里使用了.lastChild而不是使用.firstChild,是由于访问节点的代价更低。 */
export function removeChildren(node) {
node = node.lastChild;
while (node) {
let next = node.previousSibling;
recollectNodeTree(node, true);
node = next;
}
}
/** 从父节点删除该节点 * @param {Element} node 待删除的节点 */
function removeNode(node) {
let parentNode = node.parentNode;
if (parentNode) parentNode.removeChild(node);
}复制代码
咱们看到在函数recollectNodeTree
中,若是dom元素属于某个组件,首先递归卸载组件(不是本次讲述的重点,主要包括组件的回收和相依卸载生命周期的调用)。不然,只须要先判别该dom节点中是否被在jsx中存在ref
函数(也是缓存在__preactattr_
属性中),由于存在ref
函数时,咱们在组件卸载时以null
参数做为回调(React文档作了相应的规定,详情见Refs and the DOM)。recollectNodeTree
中第二个参数unmountOnly
,表示仅仅触发卸载的生命周期,跳过删除的过程,若是unmountOnly
为false
或者dom中的ATTR_KEY
属性不存在(说明这个属性不是preact所渲染的,不然确定会存在该属性),则直接将其从父节点删除。最后递归删除子节点,咱们能够看到递归删除子元素的过程是从右到左删除的(首先删除的lastChild
元素),主要考虑到的是从后访问会有性能的优点。咱们在这里(block-1)调用函数recollectNodeTree
的第二个参数是true
,缘由是在调用以前咱们已经将其在父元素中进行替换,因此是不须要进行调用的函数removeNode
再进行删除该节点的。
第二块代码,主要是针对的组件的渲染,若是vnode.nodeName
对应的是函数类型,代表要渲染的是一个组件,直接调用了函数buildComponentFromVNode
(组件不是本次叙述内容)。
第三块代码,首先:
isSvgMode = vnodeName==='svg' ? true : vnodeName==='foreignObject' ? false : isSvgMode;复制代码
变量isSvgMode
仍是用来标记当前建立的元素是不是SVG元素。foreignObject
元素容许包含外来的XML命名空间,一个foreignObject
内部的任何SVG元素都不会被绘制,因此若是是vnodeName
为foreignObject
话,isSvgMode
会被置为false
(其实Svg对我来讲也是比较生疏的内容,可是不影响咱们分析整个渲染过程)。
// 若是不是一个已经存在的元素或者类型有问题,则从新建立一个
vnodeName = String(vnodeName);
if (!dom || !isNamedNode(dom, vnodeName)) {
out = createNode(vnodeName, isSvgMode);
if (dom) {
// 移动dom中的子元素到out中
while (dom.firstChild) out.appendChild(dom.firstChild);
// 若是以前的元素已经属于某一个DOM节点,则将其替换
if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
// 回收以前的dom元素(跳过非元素类型)
recollectNodeTree(dom, true);
}
}复制代码
而后开始尝试建立dom元素,若是以前的dom为空(说明以前没有渲染)或者dom的名称与vnode.nodename不一致时,说明咱们要建立新的元素,而后若是以前的dom节点中存在子元素,则将其所有移入新建立的元素中。若是以前的dom已经有父元素了,则将其替换成新的元素,最后回收该元素。
在判断节点dom类型与虚拟dom的vnodeName类型是否相同时使用了函数isNamedNode
:
function isNamedNode(node, nodeName) {
return node.normalizedNodeName===nodeName || node.nodeName.toLowerCase()===nodeName.toLowerCase();
}复制代码
若是节点是由Preact建立的(即由函数createNode
建立的),其中dom节点中含有属性normalizedNodeName
(node.normalizedNodeName = nodeName
),则使用normalizedNodeName
去判断节点类型是否相等,不然直接采用dom节点中的nodeName
属性去判断。
到此为止渲染的当前虚拟dom的过程已经结束,接下来就是处理子元素的过程。
let fc = out.firstChild,
props = out[ATTR_KEY],
vchildren = vnode.children;
if (props==null) {
props = out[ATTR_KEY] = {};
for (let a=out.attributes, i=a.length; i--; ) props[a[i].name] = a[i].value;
}
// 优化: 对于元素只包含一个单一文本节点的优化路径
if (!hydrating && vchildren && vchildren.length===1 && typeof vchildren[0]==='string' && fc!=null && fc.splitText!==undefined && fc.nextSibling==null) {
if (fc.nodeValue!=vchildren[0]) {
fc.nodeValue = vchildren[0];
}
}
// 不然,若是有存在的子节点或者新的孩子节点,执行diff
else if (vchildren && vchildren.length || fc!=null) {
innerDiffNode(out, vchildren, context, mountAll, hydrating || props.dangerouslySetInnerHTML!=null);
}复制代码
而后咱们看到,若是out是新建立的元素或者该元素不是由Preact建立的(即不存在属性__preactattr_
),咱们会初始化out
中的__preactattr_
属性中并将out元素(刚建立的dom元素)中属性attributes
缓存在out
元素的ATTR_KEY
(__preactattr_
)属性上。可是须要注意的是,好比某个节点的属性发生改变,好比name
由1
变成了2
,那么out属性中的缓存(__preactattr_
)也须要获得更新,可是更新的操做并不发生在这里,而是下面的diffAttributes
函数中。
接下来就是处理子元素只有一个文本节点的状况(其实这部分也能够没有,经过下一层的递归也能解决,这样作只不过是为了优化性能),好比处理下面的情形:
<l1>1</li>复制代码
进入单个节点的判断条件也是比较明确的,惟一须要注意的一点是,必须知足hydrating
不为true
,由于咱们知道当hydrating
为true
是说明当前的节点并非由Preact渲染的,所以不能进行直接的优化,须要由下一层递归中建立新的文本元素。
//将props和atrributes从VNode中应用到DOM元素
diffAttributes(out, vnode.attributes, props);
// 恢复以前的SVG模式
isSvgMode = prevSvgMode;
return out;复制代码
函数diffAttributes
的主要做用就是将虚拟dom中attributes
更新到真实的dom中(后面详细讲)。最后重置变量isSvgMode
,并返回vnode所渲染的真实dom节点。
看完了函数idiff
,接下来要关心的就是,在idiff
中对虚拟dom的子元素调用的innerDiffNode
函数(代码依然很长,咱们依然作分块,对照流程图看):
function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
let originalChildren = dom.childNodes,
children = [],
keyed = {},
keyedLen = 0,
min = 0,
len = originalChildren.length,
childrenLen = 0,
vlen = vchildren ? vchildren.length : 0,
j, c, f, vchild, child;
// block-1
// 建立一个包含key的子元素和一个不包含有子元素的Map
if (len!==0) {
for (let i=0; i<len; i++) {
let child = originalChildren[i],
props = child[ATTR_KEY],
key = vlen && props ? child._component ? child._component.__key : props.key : null;
if (key!=null) {
keyedLen++;
keyed[key] = child;
}
else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
children[childrenLen++] = child;
}
}
}
// block-2
if (vlen!==0) {
for (let i=0; i<vlen; i++) {
vchild = vchildren[i];
child = null;
// 尝试经过键值匹配去寻找节点
let key = vchild.key;
if (key!=null) {
if (keyedLen && keyed[key]!==undefined) {
child = keyed[key];
keyed[key] = undefined;
keyedLen--;
}
}
// 尝试从现有的孩子节点中找出类型相同的节点
else if (!child && min<childrenLen) {
for (j=min; j<childrenLen; j++) {
if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
child = c;
children[j] = undefined;
if (j===childrenLen-1) childrenLen--;
if (j===min) min++;
break;
}
}
}
// 变形匹配/寻找到/建立的DOM子元素来匹配vchild(深度匹配)
child = idiff(child, vchild, context, mountAll);
f = originalChildren[i];
if (child && child!==dom && child!==f) {
if (f==null) {
dom.appendChild(child);
}
else if (child===f.nextSibling) {
removeNode(f);
}
else {
dom.insertBefore(child, f);
}
}
}
}
// block-3
// 移除未使用的带有keyed的子元素
if (keyedLen) {
for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
}
// 移除没有父节点的不带有key值的子元素
while (min<=childrenLen) {
if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
}
}复制代码
首先看innerDiffNode函数的参数:
dom
: diff
的虚拟子元素的父元素对应的真实dom节点vchildren
: diff
的虚拟子元素context
: 相似于React中的context,组件使用mountAll
: 组件相关,暂时能够不考虑componentRoot
: 组件相关,暂时能够不考虑函数代码将近百行,为了方便阅读,咱们将其分为四个部分(看代码注释):
// 建立一个包含key的子元素和一个不包含有子元素的Map
if (len!==0) {
//len === dom.childNodes.length
for (let i=0; i<len; i++) {
let child = originalChildren[i],
props = child[ATTR_KEY],
key = vlen && props ? child._component ? child._component.__key : props.key : null;
if (key!=null) {
keyedLen++;
keyed[key] = child;
}
else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
children[childrenLen++] = child;
}
}
}复制代码
咱们所但愿的diff
的过程确定是以最少的dom操做使得更改后的dom与虚拟dom相匹配,因此以前父节点的dom重用也是很是必要。len
是父级dom的子元素个数,首先对全部的子元素进行遍历,若是该元素是由Preact所渲染(也就是有props的缓存)而且含有key值(不考虑组件的状况下,咱们暂时只看该元素props中是否有key值),咱们将其存储在keyed
中,不然若是该元素也是Preact所渲染(有props的缓存)或者知足条件(child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)
时,咱们将其分配到children
中。这样咱们其实就将子元素划分为两类,一类是带有key值的子元素,一类是没有key的子元素。
关于条件(child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)
咱们分析一下,咱们知道hydrating
为true
时表示的是dom元素不是Preact建立的,咱们知道调用函数innerDiffNode
时,isHydrating
的值是hydrating || props.dangerouslySetInnerHTML!=null
,那么isHydrating
为true
表示的就是子dom节点不是由Preact所建立的,那么如今看起来上面的判断条件也很是容易理解了。若是节点child
不是文本节点,根据该节点是不是由Preact所建立的作决定,若是是否是由Preact建立的,则添加到children
,不然不添加。若是是文本节点的话,若是是由Preact建立的话则添加,不然执行child.nodeValue.trim()
,咱们知道函数trim
返回的是去掉字符串先后空格的新字符串,若是该节点有非空字符,则会被添加到children
中,不然不添加。这样作的目的也无非是最大程度利用以前的文本节点,减小建立没必要要的文本节点。
if (vlen!==0) {
for (let i=0; i<vlen; i++) {
vchild = vchildren[i];
child = null;
// 尝试经过键值匹配去寻找节点
let key = vchild.key;
if (key!=null) {
if (keyedLen && keyed[key]!==undefined) {
child = keyed[key];
keyed[key] = undefined;
keyedLen--;
}
}
// 尝试从现有的孩子节点中找出类型相同的节点
else if (!child && min<childrenLen) {
for (j=min; j<childrenLen; j++) {
if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
child = c;
children[j] = undefined;
if (j===childrenLen-1) childrenLen--;
if (j===min) min++;
break;
}
}
}
// 变形匹配/寻找到/建立的DOM子元素来匹配vchild(深度匹配)
child = idiff(child, vchild, context, mountAll);
f = originalChildren[i];
if (child && child!==dom && child!==f) {
if (f==null) {
dom.appendChild(child);
}
else if (child===f.nextSibling) {
removeNode(f);
}
else {
dom.insertBefore(child, f);
}
}
}
}复制代码
该部分代码首先对虚拟dom中的子元素进行遍历,对每个子元素,首先判断该子元素是否含有属性key,若是含有则在keyed
中查找对应keyed的dom元素,并在keyed
将该元素删除。不然在children
查找是否含有和该元素相同类型的节点(利用函数isSameNodeType
),若是查找到相同类型的节点,则在children
中删除并根据对应的状况(即查到的元素在children
查找范围的首尾)缩小排查范围。而后递归执行函数idiff
,若是以前child
没有查找到的话,会在idiff
中建立对应类型的节点。而后根据以前的所分析的,idiff
会返回新的dom节点。
若是idiff
返回dom不为空而且该dom与原始dom中对应位置的dom不相同时,将其添加到父节点。若是不存在对应位置的真实节点,则直接添加到父节点。若是child
已经添加到对应位置的真实dom后,则直接将其移除当前位置的真实dom,不然都将其添加到对应位置以前。
if (keyedLen) {
for (let i in keyed) if (keyed[i]!==undefined) recollectNodeTree(keyed[i], false);
}
// 移除没有父节点的不带有key值的子元素
while (min<=childrenLen) {
if ((child = children[childrenLen--])!==undefined) recollectNodeTree(child, false);
}复制代码
这段代码所做的工做就是将keyed
中与children
中没有用到的原始dom节点回收。到此咱们已经基本讲完了整个diff
的全部大体流程,还剩idiff
中的diffAttributes
函数没有讲,由于里面涉及到dom中的事件触发,因此仍是有必要讲一下:
function diffAttributes(dom, attrs, old) {
let name;
// 经过将其设置为undefined,移除不在vnode中的属性
for (name in old) {
// 判断的条件是若是old[name]中存在,但attrs[name]不存在
if (!(attrs && attrs[name]!=null) && old[name]!=null) {
setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
}
}
// 增长或者更新的属性
for (name in attrs) {
// 若是attrs中的属性不是 children或者 innerHTML 而且
// 要么 以前的old里面没有该属性 ====> 说明是新增属性
// 要么 若是name是value或者checked属性(表单), attrs[name] 与 dom[name] 不一样,或者不是value或者checked属性,则和old[name]属性不一样 ====> 说明是更新属性
if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
}
}
}复制代码
diffAttributes
的参数分别对应于:
dom
: 虚拟dom对应的真实domattrs
: 指望的最终键值属性对old
: 当前或者以前的属性(从以前的VNode或者元素props属性缓存中)
函数diffAttributes
并不复杂,首先遍历old
中的属性,若是当前的属性attrs
中不存在是,则经过函数setAccessor
将其删除。而后将attr
中的属性赋值经过setAccessor
赋值给当前的dom元素。是否须要赋值须要同时知足下满三个条件:
属性不能是children
,缘由children
表示的是子元素,其实Preact在h函数已经作了处理(详情见系列文章第一篇),这里实际上是不会存在children
属性的。
innerHTML
。其实这一点Preact与React是在这点是相同的,不能经过innerHTML
给dom添加内容,只能经过dangerouslySetInnerHTML
进行设置。value
或者checked
时,缓存的属性(old)必须和如今的属性(attrs)不同,若是该属性是value
或者checked
时,则dom的属性必须和如今不同,这么判断的主要目的就是若是属性值是value
或者checked
代表该dom属于表单元素,防止该表单元素是不受控的,缓存的属性存在可能不等于当前dom中的属性。那为何不都用dom中的属性呢?确定是因为JavaScript对象中取属性要比dom中拿到属性的速度快不少。 到这里咱们有个地方须要注意的是,调用函数setAccessor
时的第三个实参为old[name] = undefined
或者old[name] = attrs[name]
,咱们在前面说过,若是虚拟dom中的attributes
发生改变时也须要将真实dom中的__preactattr_
进行更新,其实更新的过程就发生在这里,old
的实参就是props = out[ATTR_KEY]
,因此更新old
时也对应修改了dom的缓存。
咱们最后须要关注的是函数setAccessor
,这个函数比较长可是结构是及其的简单:
function setAccessor(node, name, old, value, isSvg) {
if (name === 'className') name = 'class';
if (name === 'key') {
// key属性忽略
}
else if (name === 'ref') {
// 若是是ref 函数被改变了,以null去执行以前的ref函数,并以node节点去执行新的ref函数
if (old) old(null);
if (value) value(node);
}
else if (name === 'class' && !isSvg) {
// 直接赋值
node.className = value || '';
}
else if (name === 'style') {
if (!value || typeof value === 'string' || typeof old === 'string') {
node.style.cssText = value || '';
}
if (value && typeof value === 'object') {
if (typeof old !== 'string') {
// 从dom的style中剔除已经被删除的属性
for (let i in old) if (!(i in value)) node.style[i] = '';
}
for (let i in value) {
node.style[i] = typeof value[i] === 'number' && IS_NON_DIMENSIONAL.test(i) === false ? (value[i] + 'px') : value[i];
}
}
}
else if (name === 'dangerouslySetInnerHTML') {
//dangerouslySetInnerHTML属性设置
if (value) node.innerHTML = value.__html || '';
}
else if (name[0] == 'o' && name[1] == 'n') {
// 事件处理函数 属性赋值
// 若是事件的名称是以Capture为结尾的,则去掉,并在捕获阶段节点监听事件
let useCapture = name !== (name = name.replace(/Capture$/, ''));
name = name.toLowerCase().substring(2);
if (value) {
if (!old) node.addEventListener(name, eventProxy, useCapture);
}
else {
node.removeEventListener(name, eventProxy, useCapture);
}
(node._listeners || (node._listeners = {}))[name] = value;
}
else if (name !== 'list' && name !== 'type' && !isSvg && name in node) {
setProperty(node, name, value == null ? '' : value);
if (value == null || value === false) node.removeAttribute(name);
}
else {
// SVG元素
let ns = isSvg && (name !== (name = name.replace(/^xlink\:?/, '')));
if (value == null || value === false) {
if (ns) node.removeAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase());
else node.removeAttribute(name);
}
else if (typeof value !== 'function') {
if (ns) node.setAttributeNS('http://www.w3.org/1999/xlink', name.toLowerCase(), value);
else node.setAttribute(name, value);
}
}
}复制代码
整个函数都是if-else
的结构,首先看看各个参数:
node
: 对应的dom节点name
: 属性名old
: 该属性以前存储的值value
: 该属性当前要修改的值isSvg
: 是否为SVG元素而后看一下函数的流程:
className
,则属性名修改成class
,这一点Preact与React是不相同的,React对css中的类仅支持属性名className
,但Preact既支持className
的属性名也支持class
,而且Preact更推荐使用class
.key
时,不作任何处理。class
而且不是svg元素
,则直接将值赋值给dom元素。style
时,第一种状况是将字符串类型的样式赋值给dom.style.cssText
。若是value是空或者是字符串这么赋值很是可以理解,可是为何以前的属性值old
是字串符为何也须要经过dom.style.cssText
,通过个人实验发现做用应该是覆盖以前经过cssText
赋值的样式(因此这里的代码并非if-else
),而是两个if
的结构。下面的第二种状况是value
是对象类型,所进行的操做是剔除取消的属性,添加新的或者更改的属性。dangerouslySetInnerHTML
,则将value
中的__html
值赋值给innerHtml
属性。on
开头,说明要绑定的是事件,由于咱们知道Preact不一样于React,并无采用事件代理的机制,全部的事件都会被注册到真实的dom中。并且另外一点与React不相同的是,若是你的事件名后添加Capture
,例如onClickCapture
,那么该事件将在dom的捕获阶段响应,默认会在冒泡事件响应。若是value
存在则是注册事件,不然会将注册的事件移除。咱们发如今调用addEventListener
并无直接将value
做为其第二个参数传入,而是传入了eventProxy
:function eventProxy(e) {
return this._listeners[e.type](e);
}复制代码
咱们看到由于有语句(node._listeners || (node._listeners = {}))[name] = value
,因此某个对应事件的处理函数是保存在node._listeners
对象中,所以当函数eventProxy
调用时,就能够触发对应的事件处理程序,其实这也算是一种简单的事件代理机制,若是该元素对应的某个事件处理程序发生改变时,也就不须要删除以前的处理事件并绑定新的处理,只须要改变node._listeners
对象存储的对应事件处理函数便可。
type
和list
之外的自有属性进行赋值或者删除。其中函数setProperty
为:function setProperty(node, name, value) {
try {
node[name] = value;
} catch (e) {
}
}复制代码
这个函数尝试给为DOM的自有属性赋值,赋值的过程可能在于IE浏览器和FireFox中抛出异常。因此这里有一个try-catch
的结构。setAttribute
进行赋值。 到此为止,咱们已经基本所有分析完了Preact中diff
算法的过程,咱们看到Preact相比于庞大的React,短短数百行语句就实现了diff
的功能并能达到一个至关不错的性能。因为本人能力所限,不能达到面面俱到,但但愿这篇文章能起到抛砖引玉的做用,若是不正确指出,欢迎指出和讨论~