前言
现有框架几乎都引入了虚拟 DOM 来对真实 DOM 进行抽象,也就是现在大家所熟知的 VNode 和 VDOM,那么为什么需要引入虚拟 DOM 呢?下面就一起来了解下吧!!!
VNode & VDOM
VNode 和 VDOM 是什么?
直接看 vue3 中关于 VNode 部分的源码,文件位置:packages\runtime-core\src\vnode.ts
通过源码部分,可以很明显的看到 VNode
本身就是一个 JavaScript
对象,只不过它是通过不同的属性去描述一个真实 dom
.
VDOM
其实就是多个 VNode
组成的树结构,这就好比 HTML
元素和 DOM
树之间的关系:多个 HTML
元素能够组成树形结构就称之为 DOM
树.
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode { ... return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ) } function createBaseVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag = 0, dynamicProps: string[] | null = null, shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT, isBlockNode = false, needFullChildrenNormalization = false ) { 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 ... return vnode } 复制代码
为什么要使用 VDOM ?
既然要使用肯定是因为 虚拟 DOM 拥有一些 真实 DOM 没有的优势:
- 对真实元素节点抽象成 VNode,减少直接操作 dom 时的性能问题
- 直接操作 dom 是有限制的,比如:diff、clone 等操作,一个真实元素上有许多的内容,如果直接对其进行 diff 操作,会去额外 diff 一些没有必要的内容;同样的,如果需要进行 clone 那么需要将其全部内容进行复制,这也是没必要的。但是,如果将这些操作转移到 JavaScript 对象上,那么就会变得简单了。
- 直接操作 dom 容易引起页面的重绘和回流,但是通过 VNode 进行中间处理,可以避免一些不要的重绘和回流
- 方便实现跨平台
- 同一 VNode 节点可以渲染成不同平台上的对应的内容,比如:渲染在浏览器是 dom 元素节点,渲染在 Native( iOS、Android) 变为对应的控件、可以实现 SSR 、渲染到 WebGL 中等等
- 而且 Vue 允许开发者基于 VNode 实现自定义渲染器(renderer),以便于针对不同平台上的渲染
虚拟 DOM 的渲染过程
Vue 三大核心系统
Vue 中的三大核心系统如下:
- Compiler 模块:涉及 AST 抽象语法树的内容,再通过 generate 将 AST 生成渲染函数,这里暂不实现
- Runtime 模块:也可称为 Renderer 模块,将虚拟 dom 生成真实 dom 元素,并渲染到浏览器上
- Reactivity 模块:响应式系统
三大系统的关系
实现 Runtime 模块
下面的实现部分只实现最简单、最核心的内容,不涉及各种复杂的边界条件.
createVNode & h
VNode 主要作用就是将外部传入的各种参数组合成一个 JavaScript 对象.
其中 createVNode
就是用于创建 VNode
,而 h
函数(render function)负责将创建好的 VNode
进行返回.
function createVNode(type, props, children) { // vnode ——> js 对象 return { type, props, children } } function h(type, props, children) { return createVNode(type, props, children) } 复制代码
mount
得到 VNode
之后,接下来就需要将 VNode
变成真实的 dom
元素,并渲染到浏览器上.
- 通过
document.createElement
方法将VNode
变成dom
元素 - 处理传入的
props
对象
- 以
on
开头的默认为事件,通过addEventListener
为dom
元素注册事件 - 其他属性默认为
dom
上的属性,通过setAttribute
为dom
元素设置属性
- 处理
children
,只考虑children
为String
和Array
的情况
children
为String
默认为是文本节点,通过textContent
属性进行设置children
为Array
默认为是多个VNode
集合,通过递归调用mount
方法进行挂载
function mount(vnode, container) { // 1. 获取容器 element if (container.nodeType !== 1) { container = document.querySelector(container) } // 2. vnode ——> element const { type, props, children } = vnode const el = document.createElement(type) vnode.el = el // 3. 处理 props if (props) { for (const key in props) { // 事件 if (key.startsWith('on')) { el.addEventListener(key.slice(2).toLowerCase(), props[key]); } else { // 属性 el.setAttribute(key, props[key]) } } } // 4. 处理 children if (typeof children === 'string') { el.textContent = children } else { children.forEach(v => { mount(v, el) }); } // 5. 挂载到容器中 container.appendChild(el) } 复制代码
patch
实现了能够将 VNode 渲染为真实 DOM 之后,就需要考虑更新时 VNode 间的 diff 比较了,这就属于 patch 的过程.
- 新旧 VNode 类型不一致,先删除旧节点,用新的替换旧的
- 新旧 VNode 类型一致
- 更新 props:更新 dom 属性 & 更新 dom 事件
- 新旧属性或事件存在且不一致,直接更新
- 新属性存在 & 旧属性不存在,直接添加
- 新属性都存在 & 新旧值不一致,直接删除
- 更新 children
- 新 children 是字符串,只要和旧的 children 不相等,直接使用 innerHTML 替换旧的内容
- 新 children 是数组 & 旧 children 是字符串,先清空旧节点的内容,循环调用 mount 新增元素
- 新旧 children 都是数组,取新旧 children 中最小长度,用于减少循环 patch 次数,若 oldLength < newLength 需要通过 mount 新增元素, 若 oldLength > newLength 需要通过 el.removeChild 删除多余旧元素
/** * * @param {oldVnode} n1 * @param {newVnode} n2 */ function patch(n1, n2) { // 1. 类型不一致 if (n1.type !== n2.type) { const parent = n1.el.parentElement // 删除 oldVnode.el parent.removeChild(n1.el) // 渲染 newVnode.el mount(n2, parent) } else { // 2. 类型一致 // 2.1 统一 el 对象,因为最终修改的是 oldVnode.el,因此,使用 n1.el 作为最终值 const el = n2.el = n1.el // 2.2 处理 props const oldProps = n1.props const newProps = n2.props // 处理 props 不一致 for (const key in newProps) { const newValue = newProps[key] const oldValue = oldProps[key] // 旧的有值,新的没值,移除该属性 if (newValue !== oldValue) { // 事件不一致 if (key.startsWith('on')) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]) el.addEventListener(key.slice(2).toLowerCase(), newProps[key]) } else { // props 值不一致 el.setAttribute(key, newValue) } } } // 删除旧的 props for (const key in oldProps) { if (!(key in newProps)) { // 旧事件不存在 newProps 中 if (key.startsWith('on')) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]) } else { // oldProps 中的属性不存在 newProps 中 el.removeAttribute(key) } } } // 2.3 处理 children const oldChildren = n1.children const newChildren = n2.children // 新的子节点是字符串 if (typeof newChildren === 'string') { // 新旧子节点不一致,直接使用新节点进行替换旧节点 if (newChildren !== oldChildren) el.innerHTML = newChildren } else { // 新的子节点为数组 // 旧的子节点为字符串 if (typeof oldChildren === 'string') { el.innerHTML = '' newChildren.forEach(v => { mount(v, el) }) } else { // 旧的子节点也为数组 // 取最小的长度进行最少的循环 let commonLength = Math.min(newChildren.length, oldChildren.length) for (let i = 0; i < commonLength; i++) { // 递归调用 patch 新老节点 patch(oldChildren[i], newChildren[i]) } // 循环结束:oldLength < newLength || oldLength > newLength // oldLength < newLength,需要添加新节点 if(oldChildren.length < newChildren.length){ newChildren.slice(oldChildren.length).forEach(v => { mount(v, el) }) } // oldLength > newLength,需要删除旧节点 if(oldChildren.length > newChildren.length){ oldChildren.slice(newChildren.length).forEach(v => { el.removeChild(v.el) }) } } } } }