从认识 VNode & VDOM 到实现 mini-vue

简介: 从认识 VNode & VDOM 到实现 mini-vue

image.png

前言

现有框架几乎都引入了虚拟 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 的渲染过程

image.png

Vue 三大核心系统

Vue 中的三大核心系统如下:

  • Compiler 模块:涉及 AST 抽象语法树的内容,再通过 generate 将 AST 生成渲染函数,这里暂不实现
  • Runtime 模块:也可称为 Renderer 模块,将虚拟 dom 生成真实 dom 元素,并渲染到浏览器上
  • Reactivity 模块:响应式系统

三大系统的关系

image.png

实现 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 开头的默认为事件,通过 addEventListenerdom 元素注册事件
  • 其他属性默认为 dom 上的属性,通过 setAttributedom 元素设置属性
  • 处理 children ,只考虑 childrenStringArray 的情况
  • childrenString 默认为是文本节点,通过 textContent 属性进行设置
  • childrenArray 默认为是多个 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)
         })
       }
     }
   }
 }
}


目录
相关文章
|
3月前
|
存储 自然语言处理 JavaScript
vue中template到VDOM发生了什么
vue中template到VDOM发生了什么
vue中template到VDOM发生了什么
|
6月前
|
JavaScript 算法
vue如何通过VNode渲染节点
vue如何通过VNode渲染节点
119 0
|
6月前
|
JavaScript 前端开发
第15节:Vue3 DOM 更新完成nextTick()
第15节:Vue3 DOM 更新完成nextTick()
97 0
|
6月前
|
JavaScript
vue中$children的理解
vue中$children的理解
|
6月前
|
JavaScript 前端开发 测试技术
Vue 3.0 Teleport
Vue 3.0 Teleport
40 0
|
JavaScript 前端开发 安全
vue中的 render 和 h() 详解
当使用Vue.js进行前端开发时,理解和掌握&quot;render&quot;函数和&quot;h()&quot;函数是非常重要的,因为它们是Vue组件的核心构建和渲染部分 render 和 h()是在Vue.js中常用的两个概念,它们通常用于创建和渲染Vue组件。
319 0
|
JavaScript
vue2的Mounted和vue3的onMounted,这两个钩子有何不同?
vue2的Mounted和vue3的onMounted,这两个钩子有何不同?
861 0
|
JavaScript 前端开发 API
vue3 源码学习,实现一个 mini-vue(五):watch 侦听器
vue3 源码学习,实现一个 mini-vue(五):watch 侦听器
vue3 源码学习,实现一个 mini-vue(五):watch 侦听器
|
JavaScript
自己创建一个mini-vue
mini-vue 本章在之前的章节的基础中实现了一个简单的vue框架,其中响应式的函数有略微变化不过大致原理相同。 致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)
60 0
|
JavaScript 前端开发
从认识 VNode & VDOM 到实现 mini-vue(下)
从认识 VNode & VDOM 到实现 mini-vue
69 0