本文参考的虚拟 dom 库为 https://github.com/Matt-Esch/virtual-dom
本文可以告诉下列东西,篇幅较长:
转载请保留出处
// 1: Create a function that declares what the DOM should look like
function render(count) {
return h('div', {
style: {
textAlign: 'center',
lineHeight: (100 + count) + 'px',
border: '1px solid red',
width: (100 + count) + 'px',
height: (100 + count) + 'px'
}
}, [String(count)]);
}
// 2: Initialise the document
var count = 0; // We need some app data. Here we just store a count.
var tree = render(count); // We need an initial tree
var rootNode = createElement(tree); // Create an initial root DOM node ...
document.body.appendChild(rootNode); // ... and it should be in the document
// 3: Wire up the update logic
setInterval(function () {
count++;
var newTree = render(count);
var patches = diff(tree, newTree);
rootNode = patch(rootNode, patches);
tree = newTree;
}, 1000);
我们可以看到,这个使用过程为:
render
函数,函数体内会依据 count
参数,调用 h
函数生成不同的虚拟 dom 对象render
函数,依据 count
的初始值,生成初次的虚拟 dom 对象createElement
函数,将虚拟 dom 对象生成真实 domcount
值, 依据新的 count
值生成新的虚拟 dom 对象diff
新的虚拟 dom 对象与原有的虚拟 dom 对象diff
的结果 patch
到真实 dom 中试着考虑下整个过程,我们不难发现,整个虚拟 dom 概念,需要提供的就是:
我们首先来看 h
函数, 这个函数作用为生成虚拟 dom 的 js 对象
先考虑下我们的 html
节点,比如一个 <p>
标签:
<p class="anning" id="amnhh">这是一个P标签</p>
我们把它拆分来看:
可以得到虚拟 dom 的几个关键字:
我们简单来看下参考库中的 h
函数的定义
function h(tagName, properties, children) {
var childNodes = [];
var tag, props, key, namespace;
// 兼容 h(tagName, children) 的调用方式
if (!children && isChildren(properties)) {
children = properties;
props = {};
}
props = props || properties || {};
// 解析 tagName
tag = parseTag(tagName, props);
// support keys
if (props.hasOwnProperty('key')) {
// 给 key 赋值,并且将 props 对象中的 key 置空
// 因为我们设置 key 的本意,并不是为 dom 元素设置一个常规属性 key
// 而是为了给 dom 元素增加索引,所以 key 属性并不应该出现在 props 这个对象中
key = props.key;
props.key = undefined;
}
// support namespace
if (props.hasOwnProperty('namespace')) {
namespace = props.namespace;
props.namespace = undefined;
}
// 主要用来处理事件绑定
transformProperties(props);
// 处理 children
if (children !== undefined && children !== null) {
addChild(children, childNodes, tag, props);
}
// 返回一个 VNode 对象
return new VNode(tag, props, childNodes, key, namespace);
}
我们可以看到,h
函数其实就只是对很多参数做了处理之后,返回一个以这些参数生成的 VNode
实例:
我们刚刚可以看到,h
函数最后返回的是 VNode
的实例,这里把 VNode
叫成是一个类可能更好一些。
这个类的定义具体细节就不展开说了,只说下它做的事情:
tagName
, properties
, children
, key
, namespace
到实例下hooks
属性下children
的个数,缓存到 count
属性下具体代码可以移步有完整注释的这里 :
https://github.com/amnhh/virtual-dom/blob/master/vnode/vnode.js#L12
我们有了拥有着 tagName
, properties
, children
等属性的 js 对象后,需要将这些遵循 html 的规范,生成真实 DOM 结构
这个 createElement
函数就是用来将虚拟 dom 对象转化为真实 DOM 结构的函数
假设我们现有的是这样一个树对象:
我们先要考虑这个节点,是一个类似 <p>
的标签,还是只是一个文本节点
创建标签我们一般的手段为 document.createElement
, 创建文本节点为 document.createTextNode
方法,根据不同的情况选用不同的方法
所以这里我们会执行:
var node = document.createElement('div')
此时得到的元素为一个 <div></div>
接下来需要将 properties
应用到这个 div
上,这里我们需要考虑到,我们在 properties
不光会存储类似 id
, class
这样特殊的属性,这些属性可以直接以 property
的形式赋值,表现为 node
的 attribute
,类似 src
、id
、checked
等
还有类似:ev-click
(ev-*
为 vdom
参考库的事件前缀,可能为函数)、style
(样式)、attributes
(标签属性)这些可能为对象的形式
在代码中需要对这些进行兼容处理,如 style
会对 node.style[stylePropName]
进行赋值,attributes
会使用 setAttribute
、removeAttribute
进行赋值等
function applyProperties(node, props, previous) {
// for in 遍历所有 vnode.properties
for (var propName in props) {
var propValue = props[propName]
if (propValue === undefined) {
// 不允许空...
} else if (isHook(propValue)) {
// 是事件 hook 的情况....
} else {
// 是对象的情况,此时会对 attribute 和 style 做单独处理
if (isObject(propValue)) {
patchObject(node, props, previous, propName, propValue);
} else {
// 否则就是一个 property
// 直接可以考虑为一个 node 对象的属性
node[propName] = propValue
}
}
}
}
function patchObject(node, props, previous, propName, propValue) {
var previousValue = previous ? previous[propName] : undefined
// 是 attribute 则使用 setAttribute 和 removeAttribute
if (propName === "attributes") {
for (var attrName in propValue) {
var attrValue = propValue[attrName]
if (attrValue === undefined) {
node.removeAttribute(attrName)
} else {
node.setAttribute(attrName, attrValue)
}
}
return
}
// other code...
// 处理 style
var replacer = propName === "style" ? "" : undefined
// 赋值为类似 node.style.color = 'red' 的形式
for (var k in propValue) {
var value = propValue[k]
node[propName][k] = (value === undefined) ? replacer : value
}
}
处理 properties
之后,紧接着会去处理 children
, 显而易见通过递归创建 child
, 然后使用 node.appendChild
:
var children = vnode.children
// 生成并插入
for (var i = 0; i < children.length; i++) {
var childNode = createElement(children[i], opts)
if (childNode) {
node.appendChild(childNode)
}
}
// 最后将整个生成处理好的 node 返回
return node
总结起来经历了这些步骤:
有完善注释的代码可以查阅
https://github.com/amnhh/virtual-dom/blob/master/vdom/create-element.js#L18
diff
函数可以说是我们认知里虚拟 dom 最复杂的部分了,不要方,我们一层一层拨开来看
首先 diff
的主体是两个 vnode
节点,这里先称作新旧节点,简单来说有以下的情况:
其中可复用节点的 diff
过于复杂,可以后期新开一篇来讲,这里就不展开来说了
我们可以看到在每种情况的末尾,有不同的 VPatch.VTEXT
, VPatch.VNODE
, VPatch.REMOVE
等,其实这东西是一种标记,标记着新旧节点之间的差异
这里贴出有详尽注释的代码供大家查阅:
function walk(a, b, patch, index) {
// 如果 a 和 b 两个引用都完全相同
// 直接啥都不做
// 对 patch 也啥都不做,直接终结
if (a === b) {
return
}
var apply = patch[index]
var applyClear = false
// 由于我们入参是 tree 和 newTree
// 都是 h 函数直接返回的 VNode
// 所以直接跳过 isThunk 还有 b == null 的环节
if (isThunk(a) || isThunk(b)) {
thunks(a, b, patch, index)
} else if (b == null) {
// If a is a widget we will add a remove patch for it
// Otherwise any child widgets/hooks must be destroyed.
// This prevents adding two remove patches for a widget.
if (!isWidget(a)) {
clearState(a, patch, index)
apply = patch[index]
}
apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b))
// 如果说 b 是一个 VNode
} else if (isVNode(b)) {
// 直接进入到 a, b 都是 VNode 的环节
if (isVNode(a)) {
// 如果是相同标签,相同 namespace,相同 key
// 这特么也太严格了...
// 感觉上应该是一种提升性能的体现
// 直接调用 appendPatch 应该也是 ok 的
// 但是如果说都满足的话,可以大量的节省时间
// emmm... 这暂时只是个猜想
if (
a.tagName === b.tagName
&& a.namespace === b.namespace
&& a.key === b.key
) {
// tagName,namespace,key都相同的话,说明可以认为是一个元素
// 则我们不会去增减这个元素,只需要 diff props 看看是否需要更改就完事了
// 对 properties 这个对象进行 diff
// 以 test example1 为例的话
// 这里就会返回: { style : { lineHeight : '101px', width: '101px', height: '101px' } }
var propsPatch = diffProps(a.properties, b.properties)
if (propsPatch) {
// 这里我们只修改了 props
// 所以 apply 被赋值为了一个 VPatch 的实例
// 这个 VPatch实例中,
// type 参数传入的是 VPatch.PROPS, 也就是只是修改了属性 props
// vNode 参数传入的是 a, 也就是我们老的那个 DOM 对象,即要发生改变的那个 DOM
// patch 参数传入的是 propsDiff 的结果,这里就是 { style : { lineHeight : '101px', width: '101px', height: '101px' } }
// 此时 apply 其实就是我们 appendPatch 里说的,patch1
// 这时候 apply 就被赋值成了 {type : 4, vNode : a, patch : {style : { lineHeight : '101px', width: '101px', height: '101px' }}}
apply = appendPatch(apply,
new VPatch(VPatch.PROPS, a, propsPatch))
}
// 接下来就进入到 children 的 diff
// 我们的两个 div 同时拥有着一个 VText 节点
apply = diffChildren(a, b, patch, apply, index)
} else {
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else {
// b 是 vnode 而 a 不是 vnode 的话
// 则向 patch 里 append VNODE 类型的 patch
// VNODE 类型的 patch,最终反映到真实 dom 上是会直接调用 replaceNode 的
apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b))
applyClear = true
}
} else if (isVText(b)) {
// 如果说,b 是一个 text 节点而 a 不是一个 text 节点
if (!isVText(a)) {
// 这里只是多于一个 applyClear 的样子...
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
applyClear = true
} else if (a.text !== b.text) {
// 如果两者都是 VTEXT
// 也是需要一次 patch 的
apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b))
}
} else if (isWidget(b)) {
if (!isWidget(a)) {
applyClear = true
}
apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b))
}
if (apply) {
patch[index] = apply
}
if (applyClear) {
clearState(a, patch, index)
}
}
patch
用来将新旧节点的差异,更新到真实 dom 中
看到这里可能大家已经想到了各个 VPatch.*
的作用,其实 VPatch.*
是一种纽带,一边标示着新旧节点差异的类型,一边对应着不同的差异类型该执行的更新函数
VPatch
类型各种各样,在这个参考库中列出了以下类型:
VirtualPatch.NONE = 0
VirtualPatch.VTEXT = 1
VirtualPatch.VNODE = 2
VirtualPatch.WIDGET = 3
VirtualPatch.PROPS = 4
VirtualPatch.ORDER = 5
VirtualPatch.INSERT = 6
VirtualPatch.REMOVE = 7
VirtualPatch.THUNK = 8
每一种 VPatch
类型都对应着相应的处理函数:
function applyPatch(vpatch, domNode, renderOptions) {
var type = vpatch.type
var vNode = vpatch.vNode
var patch = vpatch.patch
switch (type) {
case VPatch.REMOVE:
return removeNode(domNode, vNode)
case VPatch.INSERT:
return insertNode(domNode, patch, renderOptions)
case VPatch.VTEXT:
return stringPatch(domNode, vNode, patch, renderOptions)
case VPatch.WIDGET:
return widgetPatch(domNode, vNode, patch, renderOptions)
case VPatch.VNODE:
return vNodePatch(domNode, vNode, patch, renderOptions)
case VPatch.ORDER:
reorderChildren(domNode, patch)
return domNode
case VPatch.PROPS:
applyProperties(domNode, patch, vNode.properties)
return domNode
case VPatch.THUNK:
return replaceRoot(domNode,
renderOptions.patch(domNode, patch, renderOptions))
default:
return domNode
}
}
会根据每种不同的 VPatch
type 来调用不同的处理函数去处理旧节点,比如 REMOVE
就是移除节点,则会使用 parent.removeChild
, INSERT
就是新增节点,使用 parent.appendChild
,VTEXT
和 VNODE
就是会执行文本、节点的替换,使用 parent.replaceChild
来替换,等等
总结来看,patch 就是一个将 diff 的结果转换到真实 dom 中的过程:
是不是总体看下来,对虚拟 dom 有了个大致的了解?完整注释可以参考这里
下面放出完整的思维导图,供大家理解:
谢谢阅读 ^ v ^
原文:https://www.cnblogs.com/amnhhh/p/12358913.html