从认识 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)
         })
       }
     }
   }
 }
}


目录
相关文章
|
JavaScript
报错[Vue warn]: $listeners is readonly. $attrs is readonly.怎么解决?
报错[Vue warn]: $listeners is readonly. $attrs is readonly.怎么解决?
|
前端开发 JavaScript Java
没错,你可以移动式编码了:4款最好的Android设备HTML编辑器
作为出色的应用平台,Android系统不仅可以用于登录Facebook或是玩“愤怒的小鸟”,它还可以为web开发人员提供可行的移动式解决方案。然而,web开发者是不可能对那些陈旧的文本编辑器表示满意的——他们需要使用专门的代码编辑器,以便让工作更快速更便捷地完成。下面我将要介绍4款名列前茅用于Android设备的HTML编辑器,任何web开发人员都能利用它们在平板电脑上处理大量工作,或是在智能手机上进行一些快速修改。
3993 0
没错,你可以移动式编码了:4款最好的Android设备HTML编辑器
|
SQL Go 数据库
SqlServer数据库(可疑)解决办法4种
亲自试过,可行!!!!! SqlServer数据库(可疑)解决办法4种   重启服务--------------------------------------------------日志文件丢了,建一个日志文件------------------------------------------...
2728 0
|
8月前
|
算法 安全 搜索推荐
算法备案办官方流程
企业办理算法备案需登录备案系统,填写主体及算法信息并提交相关材料。流程包括注册备案、算法信息填报、产品信息提交、审核与公示等环节。企业需提前准备营业执照、身份证明等文件,确保资质真实有效。审核分为主体审核、一审和二审,通过后进入国家网信办公示,公示无异议即获备案号。整个流程约需1个月左右,具体以官方要求为准。
|
JavaScript 定位技术 API
Vue获取照片拍摄的地理位置信息
iPhone屏幕尺寸和开发适配
993 155
|
JSON 数据格式
解决报错TypeError: Converting circular structure to JSON --> starting at object with constructor
解决报错TypeError: Converting circular structure to JSON --> starting at object with constructor
引用 AspNetCoreRateLimit => StatusCode cannot be set because the response has already started.
引用 AspNetCoreRateLimit => StatusCode cannot be set because the response has already started.
374 0
|
网络协议
【qt】TCP的监听 (设置服务器IP地址和端口号)
【qt】TCP的监听 (设置服务器IP地址和端口号)
1045 0