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 算法。



目录
相关文章
|
8月前
|
JavaScript
Vue实例挂载的过程
Vue实例挂载的过程
65 0
|
5月前
|
JavaScript
Vue自定义组件实现类似elementUI的append-to-body功能,将创建的元素插入、挂载到body上面(网页最外层),适用于父元素overflow: hidden、绝对定位fixed的场景
Vue自定义组件实现类似elementUI的append-to-body功能,将创建的元素插入、挂载到body上面(网页最外层),适用于父元素overflow: hidden、绝对定位fixed的场景
|
9月前
|
JavaScript
Vue实例挂载的过程
Vue实例挂载的过程
61 0
|
5月前
|
JavaScript
Vue实例挂载的过程发生了什么
Vue实例挂载的过程发生了什么
22 0
|
5月前
|
JavaScript
Vue实例挂载的过程
Vue实例挂载的过程
28 0
|
6月前
|
JavaScript
vue挂载全局函数
vue挂载全局函数
27 0
|
8月前
|
JavaScript 前端开发
vue2项目:快速创建vue页面模板+aixos和Message的挂载+async与await的异步操作
vue2项目:快速创建vue页面模板+aixos和Message的挂载+async与await的异步操作
78 0
|
JavaScript 前端开发 视频直播
利用Docker挂载Nginx-rtmp(服务器直播流分发)+FFmpeg(推流)+Vue.js结合Video.js(播放器流播放)来实现实时网络直播
众所周知,在视频直播领域,有不同的商家提供各种的商业解决方案,其中比较靠谱的服务商有阿里云直播,腾讯云直播,以及又拍云和网易云的有偿直播服务,服务包括软硬件设备,摄像机,编码器,流媒体服务器等。但是其高昂的费用以及较高的准入门槛让许多个人和小型企业望而却步,本文要讲解的是如何使用nginx-rtmp搭建直播服务器,配合FFmpeg推流,在网页端vue.js作为载体利用video.js作为流播放器,打造一套可用的在线视频直播方案。
利用Docker挂载Nginx-rtmp(服务器直播流分发)+FFmpeg(推流)+Vue.js结合Video.js(播放器流播放)来实现实时网络直播
|
JavaScript API Go
vue3 源码学习,实现一个 mini-vue(七):构建 renderer 渲染器之 ELEMENT 节点的挂载
vue3 源码学习,实现一个 mini-vue(七):构建 renderer 渲染器之 ELEMENT 节点的挂载
vue3 源码学习,实现一个 mini-vue(七):构建 renderer 渲染器之 ELEMENT 节点的挂载