Vuejs设计与实现 —— 渲染器核心:挂载与更新

简介: Vuejs设计与实现 —— 渲染器核心:挂载与更新

image.png


前言

挂载更新渲染器 的核心功能,也是渲染器应该要提供的基本功能,而 挂载更新 又是基于 VNode 虚拟节点的,因为 VNode 节点描述了其对应的 真实 DOM 应该是什么样子的。

挂载与卸载

VNode 节点

无论是 vue 还是 react 都引入了 虚拟 DOM,只不过它们定义 虚拟 DOM 的结构不同,但本质上都只是一个普通的 JavaScript 对象。

VDOMVNode 是从 本质上 看是一个东西,因为 VDOMVNode 节点组成,每个 VNode 节点也能代表局部 VDOM,上篇文章中也提到过:VNodeVDOM 是可以互换的。

但从 整体上 看显然 VDOM 是包含或者等于 VNode,也就是说从严格意义上来讲,它们并不是一直相等的,取决于你的 VNode 节点的个数,如果它的节点数量是 1 那么它们是相等的。

不过现在所谈论的 VNode 就是 VDOM,谈论的 VDOM 就是 VNode,这只不过是一个简单的概念,不必过于纠结。

下面是 Vue3.x 中定义最基本的 VNode 结构:

  • vnode.type 是节点类型:标签、文本、注释、Fragment、Component 等
  • vnode.props 是节点属性数据:HTML Attributes 和 DOM Properties
const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  } as VNode
复制代码

设置正确的元素属性

HTML Attributes 和 DOM Properties

  • HTML Attributes 指的就是定义在 HTML 标签上的属性,如:id="app"、type="text"、value="hello world" 等等
  • DOM Properties 指的是通过 JavaScript 来访问真实 DOM 元素时能够访问到的属性,很多 HTML Attributes 都能在 DOM Properties 上存在同名属性(如:el.id、el.title)等,不同名属性(如:el.className、el.textContext)等
  • 核心原则:HTML Attributes 的作用是设置 DOM Properties初始值
    image.png

正确处理普通的 props

  • 通过 in 操作符判断 props.key 是否存在 el(即 DOM Properties)
  • 存在 则优先设置 DOM Properties,即 el[props.key] = props.value
  • 不存在 则通过 el.setAttribute(key, value) 完成属性设置
  • 针对 只读 属性的 DOM Properties,不能直接进行赋值,因此也必须转换为 el.setAttribute(key, value) 的处理,如:<input form="form1"> 中的 form 属性就是只读属性 源码中抽离了 shouldSetAsProp 用于去判断是否可通过 DOM Properties 去更新:

image.png

特殊处理 class

Vue.jsclass 做了增强:

  • 指定 class 为普通 字符串
  • 指定 class 为一个 对象
  • 指定 class 为包含上述两种类型的 数组

由于 class 的值以多种形式存在,因此需要对 class 进行一些特殊处理,将 class 的值统一为字符串的形式,因为 HTML 只接收这样的 class

源码中通过 normaliz 处理不同的 class 类型,并统一返回字符串形式:

image.png

选择设置 class 最合适的方式

浏览器中设置 class 的方式有三种:el.className、el.classList、el.setAttribute,既然有多种方式,那么在选择时肯定要选择最优的设置方式,而其中最优的方式就是 el.className

可以做个小测试,时间不一定准确,但是差值却很明显:

const body = document.documentElement;
console.time('className:')
for (let i = 0; i < 1000; i++) {
  body.className += i;
}
console.timeEnd('className:')
console.time('setAttribute:')
for (let i = 0; i < 1000; i++) {
  body.setAttribute('class', body.className + ' ' + i); 
}
console.timeEnd('setAttribute:')
console.time('classList:')
for (let i = 0; i < 1000; i++) {
  body.classList.add(i+''); 
}
console.timeEnd('classList:')
// 输出结果:
className:: 5.760009765625 ms
setAttribute:: 651.76611328125 ms
classList:: 1750.427978515625 ms
复制代码

事件处理

区分事件

在虚拟 DOM 中,事件可以被看作是一种特殊的属性,在 vue 中约定 vnode.props 对象中,凡是以字符串 on 开头的属性都视为 事件.

const vnode = {
   type: 'div',
   props: {
       onClick: () => {
         alert('hello');
       }
   },
   children: 'click here'
}
复制代码

注册和更新事件

注册事件 通过 el.addEventListener 的方式进行注册即可,那如何实现 更新事件 呢?

最简单的方法:

  • 移除 之前的事件处理函数
  • 重新绑定 新的事件处理函数

但这种方式并不是最优的方式,毕竟需要来回 移除、注册 才能实现事件更新,有没有什么方法是可以只注册一次事件,也能实现事件更新的方式呢?

确实有,vue 中也是这么设计的:

  • 伪造一个事件处理函数 invoker.value,将真正的事件处理函数设置为 invoker.value 属性的值
  • 事件绑定时,先从 el._vei 读取对应的 invoker,若不存在,则将伪造的 invoker 作为事件处理函数,并将它缓存到 el._vei 属性中
  • 将真正的事件处理函数赋值给 invoker.value 属性,把伪造的 invoker 函数作为事件处理函数绑定到元素上
  • 事件触发时,实际上执行的是伪造的 invoker 函数,而 invoker 事件处理函数中会执行 invoker.value() 即 真正的事件处理函数
  • 事件需要进行更新时,直接将 invoker.value 的值重新赋值即可,不需通过 removeEventListener 移除事件
  • 当然若事件更新时确实属于事件移除操作,则还是需要通过 removeEventListener 移除事件

源码如下:

image.png

挂载节点

通过 patch(n1, n2, container, anchor = null, ...) 函数的初次调用实现元素挂载:

  • 首次调用 patch 函数时,n1 = null 因为是挂载阶段,因此没有旧 vnode,当 patch 函数执行时,会递归调用 mountElement 函数完成挂载
  • 第三个参数 anchor 是挂载点,最终通过 insertBefore 插入到文档中

在挂载过程中还会触发不同生命周期钩子的执行,具体的内容就不在详细进行分析了,感兴趣的可自行阅读源码

卸载操作

卸载操作实际上是发生在更新阶段,这里的更新时指,在初次挂载完成之后,后续渲染还会触发更新,只不过新 vnode 会变成 null,从而进入卸载阶段:

  • 容器的内容可能是一个或多个组件渲染的,当卸载发生时,应该正确地调用这些组件的 beforeUnmount、unmounted 等生命周期函数
  • 即使内容不是由组件渲染的,有的 元素上存在自定义指令 等,也应该要在卸载操作发生时,正确地执行对应的指令钩子函数
  • 同时需要移除绑定在 DOM元素上的事件处理函数

基于以上原因,卸载不能简单的通过 innerHTML 来完成卸载操作,源码中通过 unmount 函数,以及一些对应移除函数实现卸载操作

image.png

更新子节点最佳方式

对于一个元素来说,其子节点拥有以下 3 种情况:

  • 没有子节点,即 vnode.children = null
  • 子节点是 文本节点,即 vnode.children 的值为字符串
  • 其他情况,无论是单个子元素,还是多个子节点(可能存在文本和元素的混合),都可以用数组来表示,即 vnode.children = [...]

有了规范化的子节点类型,那就可以总结更新子节点时的全部可能:

image.png

而在的实际的代码中,并不需要罗列去处理以上的所有情况,而更新方式必然也不是采用 "笨方式"卸载所有子节点,在挂载所有新节点,更好的做法是,通过 Diff 算法比较新旧两组子节点,试图最大程度复用 DOM 元素。

具体的 diff 算法,会在下一篇文章中进行介绍,并且会对比 vue2vue3 中的 diff 算法。



目录
相关文章
|
2月前
|
JavaScript
vue异步渲染
vue异步渲染
|
2月前
|
存储 API
vue3中如何动态自定义创建组件并挂载
vue3中如何动态自定义创建组件并挂载
385 90
|
10天前
|
监控 JavaScript 前端开发
Vue 异步渲染
【10月更文挑战第23天】Vue 异步渲染是提高应用性能和用户体验的重要手段。通过理解异步渲染的原理和优化策略,我们可以更好地利用 Vue 的优势,开发出高效、流畅的前端应用。同时,在实际开发中,要注意数据一致性、性能监控和调试等问题,确保应用的稳定性和可靠性。
|
2月前
|
JavaScript 前端开发
Vue学习笔记8:解决Vue学习笔记7中用v-for指令渲染列表遇到两个问题
Vue学习笔记8:解决Vue学习笔记7中用v-for指令渲染列表遇到两个问题
|
2月前
|
JavaScript 前端开发 API
Vue学习笔记7:使用v-for指令渲染列表
Vue学习笔记7:使用v-for指令渲染列表
|
2月前
|
人工智能 JavaScript 索引
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
这篇文章讨论了在Vue中进行列表渲染时遇到的“Duplicate keys detected”错误。这个错误通常发生在使用 `v-for` 指令渲染列表时,如果没有为每个循环项指定一个唯一的 `key` 属性,或者指定的 `key` 属性值重复了。文章提供了导致错误的原始代码示例,并给出了修正后的代码,通过在 `key` 绑定中加入索引确保 `key` 的唯一性。此外,文章还解释了为什么需要唯一 `key` 以及如何解决这个问题。
Duplicate keys detected: This may cause an update error.【Vue遍历渲染报错的解决】
|
2月前
|
JavaScript 前端开发 UED
组件库实战 | 用vue3+ts实现全局Header和列表数据渲染ColumnList
该文章详细介绍了如何使用Vue3结合TypeScript来开发全局Header组件和列表数据渲染组件ColumnList,并提供了从设计到实现的完整步骤指导。
|
3月前
|
JavaScript 算法 前端开发
"揭秘Vue.js的高效渲染秘诀:深度解析Diff算法如何让前端开发快人一步"
【8月更文挑战第20天】Vue.js是一款备受欢迎的前端框架,以其声明式的响应式数据绑定和组件化开发著称。在Vue中,Diff算法是核心之一,它高效计算虚拟DOM更新时所需的最小实际DOM变更,确保界面快速准确更新。算法通过比较新旧虚拟DOM树的同层级节点,递归检查子节点,并利用`key`属性优化列表更新。虽然存在局限性,如难以处理跨层级节点移动,但Diff算法仍是Vue高效更新机制的关键,帮助开发者构建高性能Web应用。
67 1
|
3月前
|
JavaScript
Vue学习之--------列表渲染、v-for中key的原理、列表过滤的实现(2022/7/13)
这篇博客文章详细介绍了Vue中列表渲染的基础知识、`v-for`指令的使用、`key`的原理和列表过滤的实现。通过代码实例和测试效果,展示了如何遍历数组和对象、使用`key`属性优化渲染性能,以及如何实现列表的动态过滤功能。
Vue学习之--------列表渲染、v-for中key的原理、列表过滤的实现(2022/7/13)
|
3月前
|
JavaScript 前端开发
Vue学习之--------绑定样式、条件渲染、v-show和v-if的区别(2022/7/12)
这篇博客文章讲解了Vue中绑定样式和条件渲染的方法,包括类样式绑定的不同写法、`v-show`和`v-if`的条件渲染区别以及它们的使用场景和特点,并通过代码实例和测试效果来展示具体应用。
Vue学习之--------绑定样式、条件渲染、v-show和v-if的区别(2022/7/12)