【深入浅出】Vue3 虚拟 DOM

简介: 此篇我们将深入 Vue3 虚拟 DOM,以及了解它是如何遍历找到对应 vnode 的。

image.png

序言



首发在我的博客 深入 Vue3 虚拟 DOM

译自:diving-into-the-vue-3s-virtual-dom-medium

作者:Lachlan Miller


此篇我们将深入 Vue3 虚拟 DOM,以及了解它是如何遍历找到对应 vnode 的。

多数情况下我们不需要考虑 Vue 组件内部是如何构成的。但有一些库会帮助我们理解,比如 Vue Test Utils 的 findComponent 函数。还有一个我们都应该很熟悉的 Vue 开发工具 —— Vue DevTools,它显示了应用的组件层次结构,并且我们可以对它进行编辑操作等。


image.png


我们本篇要做的是:实现 Vue Test Utils API 的一部分,即 findComponent 函数。


设计 findComponent



首先,我们都知道虚拟 DOM 是基于“提升性能”提出的,当数据发生变化时,Vue 会判断此是否需要进行更新、或进行表达式的计算、或进行最终的 DOM 更新。

比如这样:


- div 
  - span (show: true) 
    - 'Visible'


它的内部层次关系是:


HTMLDivElement -> HTMLSpanElement -> TextNode


如果 show 属性变成 false。Vue 虚拟 DOM 会进行如下更新:


- 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/> 标签。


打印 findComponent



接着,我们先写几个简单组件,如下:


// 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'))


  • 我们需要在 Node.js v14+ 环境,因为我们要用到 可选链。且需要安装 Vue、jsdom 和 jsdom-global。


我们可以看到 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' 
]


我们可以看到一些很熟悉的属性:比如 slotsdatasuspense 是一个新特性,emit 无需多言。还有比如 attrsbccbm 这些是生命周期钩子:bcbeforeCreate, ccreated。也有一些内部唯一的生命周期钩子,如 rtg,也就是 renderTriggered, 当 propsdata 发生变化时,用于更新操作,从而再渲染。


本篇我们需要特别关注的是:vnodesubTreecomponenttypechildren


匹配 findComponent



来先看 vnode,它有很多属性,我们需要关注的是 typecomponent 这两个。

// 打印 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 来给出我们的递归解决方案。


实现 findComponent



我们首先实现 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)。


小结



本篇的实现并非完美,现实实现上还需要执行更多检查。例如,如果使用 templateSuspense组件时,需要作更多判断。不过这些你可以在 Vue Test Utils 源码 中可以看到,希望能帮助你进一步理解虚拟 DOM。


本篇 源码地址,小手一动、一下就懂~

好啦,以上就是本次分享~

如果喜欢,点赞关注👍👍👍~我是掘金安东尼,关注公众号【掘金安东尼】,持续输出ing!


相关文章
|
2月前
|
JavaScript 前端开发 安全
Vue 3
Vue 3以组合式API、Proxy响应式系统和全面TypeScript支持,重构前端开发范式。性能优化与生态协同并进,兼顾易用性与工程化,引领Web开发迈向高效、可维护的新纪元。(238字)
530 139
|
2月前
|
缓存 JavaScript 算法
Vue 3性能优化
Vue 3 通过 Proxy 和编译优化提升性能,但仍需遵循最佳实践。合理使用 v-if、key、computed,避免深度监听,利用懒加载与虚拟列表,结合打包优化,方可充分发挥其性能优势。(239字)
251 1
|
7月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
1007 5
|
3月前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
409 11
|
2月前
|
JavaScript 安全
vue3使用ts传参教程
Vue 3结合TypeScript实现组件传参,提升类型安全与开发效率。涵盖Props、Emits、v-model双向绑定及useAttrs透传属性,建议明确声明类型,保障代码质量。
287 0
|
4月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
464 1
|
4月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
276 0
|
5月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
143 0
|
7月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
623 17
|
9月前
|
资源调度 JavaScript
Vue 3 中如何通过状态管理库来更新虚拟 DOM?
Vue 3 中如何通过状态管理库来更新虚拟 DOM?
279 57