首发在个人博客 深刻 Vue3 虚拟 DOMhtml
译自:diving-into-the-vue-3s-virtual-dom-mediumvue
做者:Lachlan Millernode
此篇咱们将深刻 Vue3 虚拟 DOM,以及了解它是如何遍历找到对应 vnode 的。git
多数状况下咱们不须要考虑 Vue 组件内部是如何构成的。但有一些库会帮助咱们理解,好比 Vue Test Utils 的 findComponent 函数。还有一个咱们都应该很熟悉的 Vue 开发工具 —— Vue DevTools,它显示了应用的组件层次结构,而且咱们能够对它进行编辑操做等。github
咱们本篇要作的是:实现 Vue Test Utils API 的一部分,即 findComponent
函数。数组
首先,咱们都知道虚拟 DOM 是基于“提高性能”提出的,当数据发生变化时,Vue 会判断此是否须要进行更新、或进行表达式的计算、或进行最终的 DOM 更新。markdown
好比这样:app
- div
- span (show: true)
- 'Visible'
复制代码
它的内部层次关系是:dom
HTMLDivElement -> HTMLSpanElement -> TextNode
复制代码
若是 show
属性变成 false
。Vue 虚拟 DOM 会进行以下更新:async
- div
- span (show: false)
- 'Visible'
复制代码
接着,Vue 会更新 DOM,移除'span'
元素。
那么,咱们设想一下,findComponent
函数,它的调用可能会是相似这样的结构:
const { createApp } = require('vue')
const App = {
template: `
<C>
<B>
<A />
</B>
</C>
`
}
const app = createApp(App).mount('#app')
const component = findComponent(A, { within: app })
// 咱们经过 findComponent 方法找到了 <A/> 标签。
复制代码
接着,咱们先写几个简单组件,以下:
// import jsdom-global. We need a global `document` for this to work.
require('jsdom-global')()
const { createApp, h } = require('vue')
// some components
const A = {
name: 'A',
data() {
return { msg: 'msg' }
},
render() {
return h('div', 'A')
}
}
const B = {
name: 'B',
render() {
return h('span', h(A))
}
}
const C = {
name: 'C',
data() {
return { foo: 'bar' }
},
render() {
return h('p', { id: 'a', foo: this.foo }, h(B))
}
}
// mount the app!
const app = createApp(C).mount(document.createElement('div'))
复制代码
咱们能够看到 A , B , C 三个组件,其中 A , C 组件有 data 属性,它会帮助咱们深刻研究 VDOM。
你能够打印试试:
console.log(app)
console.log(Object.keys(app))
复制代码
结果为 {}
,由于 Object.keys
只会显示可枚举的属性。
咱们能够尝试打印隐藏的不可枚举的属性:
console.log(app.$)
复制代码
能够获得大量输出信息:
<ref *1> {
uid: 0,
vnode: {
__v_isVNode: true,
__v_skip: true,
type: {
name: 'C',
data: [Function: data],
render: [Function: render],
__props: []
}, // hundreds of lines ...
复制代码
再打印:
console.log(Object.keys(app.$))
复制代码
输出:
Press ENTER or type command to continue
[
'uid', 'vnode', 'type', 'parent', 'appContext', 'root', 'next', 'subTree', 'update', 'render', 'proxy', 'withProxy', 'effects', 'provides', 'accessCache', 'renderCache', 'ctx', 'data', 'props', 'attrs', 'slots', 'refs', 'setupState', 'setupContext', 'suspense', 'asyncDep', 'asyncResolved', 'isMounted', 'isUnmounted', 'isDeactivated', 'bc', 'c', 'bm', 'm', 'bu', 'u', 'um', 'bum', 'da', 'a', 'rtg', 'rtc', 'ec', 'emit', 'emitted'
]
复制代码
咱们能够看到一些很熟悉的属性:好比 slots
、data
,suspense
是一个新特性,emit
无需多言。还有好比 attrs
、bc
、 c
、bm
这些是生命周期钩子:bc
是 beforeCreate
, c
是 created
。也有一些内部惟一的生命周期钩子,如 rtg
,也就是 renderTriggered
, 当 props
或 data
发生变化时,用于更新操做,从而再渲染。
本篇咱们须要特别关注的是:
vnode
、subTree
、component
、type
和children
。
来先看 vnode
,它有不少属性,咱们须要关注的是 type
和 component
这两个。
// 打印 console.log(app.$.vnode.component)
console.log(app.$.vnode.component)
<ref *1> {
uid: 0,
vnode: {
__v_isVNode: true,
__v_skip: true,
type: {
name: 'C',
data: [Function: data],
render: [Function: render],
__props: []
}, // ... many more things ... } }
复制代码
type
颇有意思!它与咱们以前定义的 C
组件同样,咱们能够看到它也有 [Function: data]
(咱们在前面定义了一个 msg
数据是咱们的查找目标)。实际上咱们尝试能够做如下打印:
console.log(C === app.$.vnode.component.type) //=> true
复制代码
天呐!两者居然是相等的!😮
console.log(C === app.$.vnode.type) //=> true
复制代码
这样也是相等的!😮
(你是否会疑问这两个属性为何会指向同一个对象?这里先暂且按下不表、自行探索。)
不管如何,咱们算是获得了寻找到组件的途径。
经过这里的找寻过程,咱们还能再进一步获得如下相等关系:
console.log(
app.$
.subTree.children[0].component
.subTree.children[0].component.type === A) //=> true
复制代码
在本例中,div
节点的 subTree.children
数组长度是 2 。咱们知道了虚拟 DOM 的递归机制,就能够沿着这个方向:subTree -> children -> component
来给出咱们的递归解决方案。
咱们首先实现 matches
函数,用于判断是当前 vnode 节点和目标是否相等。
function matches(vnode, target) {
return vnode?.type === target
}
复制代码
而后是 findComponent
函数,它是咱们调用并查找内部递归函数的公共 API。
function findComponent(comp, { within }) {
const result = find([within.$], comp)
if (result) {
return result
}
}
复制代码
此处的 find
方法的实现是咱们要重点讨论的。
咱们知道写递归,最重要的是判断何时结束 loop,因此 find 函数应该先是这样的:
function find(vnodes, target) {
if (!Array.isArray(vnodes)) {
return
}
}
复制代码
而后,在遍历 vnode 时,若是找到匹配的组件,则将其返回。若是找不到匹配的组件,则可能须要检查 vnode.subTree.children 是否已定义,从而更深层次的查询及匹配。最后,若是都没有,咱们则返回累加器 acc。因此,代码以下:
function find(vnodes, target) {
if (!Array.isArray(vnodes)) {
return
}
return vnodes.reduce((acc, vnode) => {
if (matches(vnode, target)) {
return vnode
}
if (vnode?.subTree?.children) {
return find(vnode.subTree.children, target)
}
return acc
}, {})
}
复制代码
若是你在 if (vnode?.subTree?.children) {
这里进行一个打印 console.log
,你能找到 B
组件,可是咱们的目标 A
组件的路径以下:
app.$
.subTree.children[0].component
.subTree.children[0].component.type === A) //=> true
复制代码
因此咱们再次调用了 find
方法:find(vnode.subTree.children, target)
,在下一次迭代中查找的第一个参数将是app.$.subTree.children
,它是 vnode 的数组。咱们不只须要检查vnode.subTree.children
,还须要检查vnode.component.subTree
。
因此,最后 find 方法以下:
function find(vnodes, target) {
if (!Array.isArray(vnodes)) {
return
}
return vnodes.reduce((acc, vnode) => {
if (matches(vnode, target)) {
return vnode
}
if (vnode?.subTree?.children) {
return find(vnode.subTree.children, target)
}
if (vnode?.component?.subTree) {
return find(vnode.component.subTree.children, target)
}
return acc
}, {})
}
复制代码
而后咱们再调用它:
const result = findComponent(A, { within: app })
console.log( result.component.proxy.msg ) // => 'msg'
复制代码
咱们成功了!经过 findComponent
,找到了 msg
!
若是你之前使用过 Vue Test Utils,可能见过相似的东西 wrapper.vm.msg
,它其实是在内部访问 proxy
(对于Vue 3)或 vm
(对于Vue 2)。
本篇的实现并不是完美,现实实现上还须要执行更多检查。例如,若是使用 template
或 Suspense
组件时,须要做更多判断。不过这些你能够在 Vue Test Utils 源码 中能够看到,但愿能帮助你进一步理解虚拟 DOM。
本篇 源码地址,小手一动、一下就懂~
好啦,以上就是本次分享~
若是喜欢,点赞关注👍👍👍~我是掘金安东尼,关注公众号【掘金安东尼】,持续输出ing!