vue3 源码学习,实现一个 mini-vue(八):构建 renderer 渲染器之 ELEMENT 节点的更新与删除

简介: vue3 源码学习,实现一个 mini-vue(八):构建 renderer 渲染器之 ELEMENT 节点的更新与删除

highlight: vs2015

前言

原文来自 我的个人博客

再上一章中,我们完成了 renderer 的基础架构,完成了 ELEMENT 节点的挂载并且导出了可用的 render 函数。

我们知道对于 render 而言,除了有 挂载 操作之外,还存在 更新和删除 的操作。

那么在本章就让我们一起来实现一下它们吧。

1. 新旧元素相同时 ELEMENT 节点的更新操作

所谓更新操作指的是:生成一个新的虚拟 DOM 树,运行时渲染器遍历这棵新树,将它与旧树进行比较,然后将必要的更新应用到真实 DOM 上去

我们来看下面的代码:

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  // 延迟两秒,生成新的 vnode,进行更新操作
  setTimeout(() => {
    const vnode2 = h(
      'div',
      {
        class: 'active'
      },
      'update'
    )
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

以上代码执行了两遍 render 操作,第一遍是挂载操作,第二遍是更新操作

1.1 源码阅读

我们知道每次的 render 渲染 ELEMENT,其实都会触发 processElement,所以我们可以直接在 processElement 中增加断点,进入 debugger

  1. 第一次触发 processElement挂载 操作,可以直接 跳过
  2. 第二次触发 processElement更新操作,我们直接进入 processElement

image.png

此时的 n1(旧值)和 n2(新值)分别为:

image.png

  1. 我们进入 patchElement 执行更新操作:

image.png

  1. 执行 const el = (n2.el = n1.el!)。使 新旧 vnode 指向 同一个 el 元素。继续执行 patchElement

image.png

  1. 接着会执行 patchChildren(...) 方法,表示 为子节点打补丁。我们进入 patchChildren 方法:

image.png

  1. patchChildren 方法首先会对 c1c2 进行赋值,此时 c1 为 旧节点的 childrenc2新节点的 children。我们继续执行 patchChildren,跳过没用的 if,来到第 1648 行:

image.png

  1. 由上图可知会触发了 hostSetElementText。我们知道 hostSetElementText 其实是一个 设置 text 的方法。那么此时 patchChildren 执行完成。 text 内容更新完成,浏览器展示的 text 会发生变化。返回 patchElement 继续执行:

image.png

  1. 程序会执行 patchProps(....) 方法,表示 props 打补丁,我们进入 patchProps

image.png

  1. 查看代码可以发现代码执行了两次 for 循环操作:

    1. 第一次循环执行 for in newProps,执行 hostPatchProp 方法设置新的 props
    2. 第二次循环执行 for in oldProps,执行 hostPatchProp,配合 !(key in newProps) 判断,删除 没有被指定的旧属性 ,比如:
// 原属性:
{
  class: 'test',
  id: 'test-id'
}
// 新属性:
{
  class: 'active'
}

删除 id

  1. 至此 props 更新完成
  2. 至此,更替更新完成

总结:

由以上代码可知:

  1. 无论是 挂载 还是 更新 都会触发 processElement 方法,状态根据 oldValue 进行判定
  2. Element 的更新操作有可能 会在同一个 el 中完成。(注意: 仅限元素没有发生变化时,如果新旧元素不同,那么是另外的情况。)
  3. 更新操作分为:

    1. children 更新
    2. props 更新

1.2 代码实现

根据以上逻辑,我们可以直接为 processElement 方法,新增对应的 else 逻辑:

  1. packages/runtime-core/src/renderer.ts 中,为 processElement 增加新的判断:
/**
 * Element 的打补丁操作
 */
const processElement = (oldVNode, newVNode, container, anchor) => {
  if (oldVNode == null) {
    // 挂载操作
    mountElement(newVNode, container, anchor)
  } else {
    // 更新操作
    patchElement(oldVNode, newVNode)
  }
}
  1. 创建 patchElement 方法:
/**
 * element 的更新操作
 */
const patchElement = (oldVNode, newVNode) => {
  // 获取指定的 el
  const el = (newVNode.el = oldVNode.el!)

  // 新旧 props
  const oldProps = oldVNode.props || EMPTY_OBJ
  const newProps = newVNode.props || EMPTY_OBJ

  // 更新子节点
  patchChildren(oldVNode, newVNode, el, null)

  // 更新 props
  patchProps(el, newVNode, oldProps, newProps)
}
  1. 创建 patchChildren 方法:
/**
 * 为子节点打补丁
 */
const patchChildren = (oldVNode, newVNode, container, anchor) => {
  // 旧节点的 children
  const c1 = oldVNode && oldVNode.children
  // 旧节点的 prevShapeFlag
  const prevShapeFlag = oldVNode ? oldVNode.shapeFlag : 0
  // 新节点的 children
  const c2 = newVNode.children

  // 新节点的 shapeFlag
  const { shapeFlag } = newVNode

  // 新子节点为 TEXT_CHILDREN
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 旧子节点为 ARRAY_CHILDREN
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // TODO: 卸载旧子节点
    }
    // 新旧子节点不同
    if (c2 !== c1) {
      // 挂载新子节点的文本
      hostSetElementText(container, c2 as string)
    }
  } else {
    // 旧子节点为 ARRAY_CHILDREN
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 新子节点也为 ARRAY_CHILDREN
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // TODO: 这里要进行 diff 运算
      }
      // 新子节点不为 ARRAY_CHILDREN,则直接卸载旧子节点
      else {
        // TODO: 卸载
      }
    } else {
      // 旧子节点为 TEXT_CHILDREN
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        // 删除旧的文本
        hostSetElementText(container, '')
      }
      // 新子节点为 ARRAY_CHILDREN
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // TODO: 单独挂载新子节点操作
      }
    }
  }
}
  1. 创建 patchProps 方法:
/**
 * 为 props 打补丁
 */
const patchProps = (el: Element, vnode, oldProps, newProps) => {
  // 新旧 props 不相同时才进行处理
  if (oldProps !== newProps) {
    // 遍历新的 props,依次触发 hostPatchProp ,赋值新属性
    for (const key in newProps) {
      const next = newProps[key]
      const prev = oldProps[key]
      if (next !== prev) {
        hostPatchProp(el, key, prev, next)
      }
    }
    // 存在旧的 props 时
    if (oldProps !== EMPTY_OBJ) {
      // 遍历旧的 props,依次触发 hostPatchProp ,删除不存在于新props 中的旧属性
      for (const key in oldProps) {
        if (!(key in newProps)) {
          hostPatchProp(el, key, oldProps[key], null)
        }
      }
    }
  }
}

至此,更新操作完成。

创建新的测试实例 packages/vue/examples/runtime/render-element-update.html

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  // 延迟两秒,生成新的 vnode,进行更新操作
  setTimeout(() => {
    const vnode2 = h(
      'div',
      {
        class: 'active'
      },
      'update'
    )
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

测试更新成功。

2. 新旧节点不同元素时,ELEMENT 节点的更新操作

上一小节中,完成了 Element 的更新操作,但是我们之前的更新操作是针对 相同 元素的,在 不同 元素下,ELEMENT 的更新操作会产生什么样的变化呢?

2.1 源码阅读

我们从下面代码开始阅读源码:

<script>
  const { h, render } = Vue

  const vnode = h('div', {
    class: 'test'
  }, 'hello render')
  // 挂载
  render(vnode, document.querySelector('#app'))

  // 延迟两秒,生成新的 vnode,进行更新操作
  setTimeout(() => {
    const vnode2 = h('h1', {
      class: 'active'
    }, 'update')
    render(vnode2, document.querySelector('#app'))
  }, 2000);
</script>
  1. 等待第二次进入 render:

  1. vnode 存在,执行 patch 方法:

image.png

  1. 可以看到这里直接执行了 unmount 卸载方法,我们进入 unmount
  2. unmount 方法中,虽然代码很多,但是大多数代码都没有执行。最终会执行到 remove(vnode) ,表示删除 vnode

image.png

  1. 进入 remove 方法,同样大多数代码没有执行,直接到 performRemove(),执行 hostRemove(el!),进入 hostRemove,触发的是 nodeOps 中的 remove 方法,代码为 parent.removeChild(child):

image.png

  1. 至此 el 被删除
  2. 然后将 n1 = null

image.png

  1. 此时,进入 switch,触发 processElement

image.png

  1. 因为 n1 === null,所以会触发 mountElement 挂载新节点 操作

image.png

总结:
由以上代码可知:

  1. 当节点元素不同时,更新操作执行的其实是:先删除、后挂载 的逻辑
  2. 删除元素的代码从 unmount 开始,虽然逻辑很多,但是最终其实是触发了 nodeOps 下的 remove 方法,通过 parent.removeChild(child)` 完成的删除操作。

2.2 代码实现

  1. packages/runtime-core/src/renderer.tspatch 方法中增加 type 判断:
/**
 * 判断是否为相同类型节点
 */
if (oldVNode && !isSameVNodeType(oldVNode, newVNode)) {
  unmount(oldVNode)
  oldVNode = null
}
  1. packages/runtime-core/src/vnode.ts 中,创建 isSameVNodeType 方法:
/**
 * VNode
 */
export interface VNode {
  key: any
  ...
}

/**
 * 根据 key || type 判断是否为相同类型节点
 */
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key
}
  1. packages/runtime-core/src/renderer.ts 实现 unmount 方法:
export interface RendererOptions {
  /**
   * 卸载指定dom
   */
  remove(el): void
}

/**
 * 解构 options,获取所有的兼容性方法
 */
const { ...remove } = options

const unmount = vnode => {
  hostRemove(vnode.el!)
}
  1. packages/runtime-dom/src/nodeOps.ts 中,实现 remove 方法:
/**
 * 删除指定元素
 */
remove: child => {
  const parent = child.parentNode
  if (parent) {
    parent.removeChild(child)
  }
}

此时代码完成。

创建对应测试实例 packages/vue/examples/runtime/render-element-update-2.html

<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  // 延迟两秒,生成新的 vnode,进行更新操作
  setTimeout(() => {
    const vnode2 = h(
      'h1',
      {
        class: 'active'
      },
      'update'
    )
    render(vnode2, document.querySelector('#app'))
  }, 2000)
</script>

测试成功

3. 删除元素,ELEMENT 节点的卸载操作

此时我们已经有了 unmount 函数,我们知道触发 unmount 函数,即可卸载元素。

那么接下来我们就可以基于这样的函数来去实现 卸载 操作了。

这块代码比较简单,我们直接实现即可:

  1. packages/runtime-core/src/renderer.ts 中为 render 函数补充卸载逻辑:
const render = (vnode, container) => {
  if (vnode == null) {
    // TODO: 卸载
    if (container._vnode) {
      unmount(container._vnode)
    }
  } else {
    // 打补丁(包括了挂载和更新)
    patch(container._vnode || null, vnode, container)
  }
  container._vnode = vnode
}
  1. 创建如下测试实例 packages/vue/examples/imooc/runtime/render-element-remove.html
<script>
  const { h, render } = Vue

  const vnode = h(
    'div',
    {
      class: 'test'
    },
    'hello render'
  )
  // 挂载
  render(vnode, document.querySelector('#app'))

  // 延迟两秒,执行卸载操作
  setTimeout(() => {
    render(null, document.querySelector('#app'))
  }, 2000)
</script>

测试实例,卸载成功。

相关文章
|
6天前
|
开发工具 iOS开发 MacOS
基于Vite7.1+Vue3+Pinia3+ArcoDesign网页版webos后台模板
最新版研发vite7+vue3.5+pinia3+arco-design仿macos/windows风格网页版OS系统Vite-Vue3-WebOS。
106 10
|
4月前
|
缓存 JavaScript PHP
斩获开发者口碑!SnowAdmin:基于 Vue3 的高颜值后台管理系统,3 步极速上手!
SnowAdmin 是一款基于 Vue3/TypeScript/Arco Design 的开源后台管理框架,以“清新优雅、开箱即用”为核心设计理念。提供角色权限精细化管理、多主题与暗黑模式切换、动态路由与页面缓存等功能,支持代码规范自动化校验及丰富组件库。通过模块化设计与前沿技术栈(Vite5/Pinia),显著提升开发效率,适合团队协作与长期维护。项目地址:[GitHub](https://github.com/WANG-Fan0912/SnowAdmin)。
735 5
|
1月前
|
缓存 前端开发 大数据
虚拟列表在Vue3中的具体应用场景有哪些?
虚拟列表在 Vue3 中通过仅渲染可视区域内容,显著提升大数据列表性能,适用于 ERP 表格、聊天界面、社交媒体、阅读器、日历及树形结构等场景,结合 `vue-virtual-scroller` 等工具可实现高效滚动与交互体验。
249 1
|
1月前
|
缓存 JavaScript UED
除了循环引用,Vue3还有哪些常见的性能优化技巧?
除了循环引用,Vue3还有哪些常见的性能优化技巧?
145 0
|
2月前
|
JavaScript
vue3循环引用自已实现
当渲染大量数据列表时,使用虚拟列表只渲染可视区域的内容,显著减少 DOM 节点数量。
95 0
|
4月前
|
JavaScript API 容器
Vue 3 中的 nextTick 使用详解与实战案例
Vue 3 中的 nextTick 使用详解与实战案例 在 Vue 3 的日常开发中,我们经常需要在数据变化后等待 DOM 更新完成再执行某些操作。此时,nextTick 就成了一个不可或缺的工具。本文将介绍 nextTick 的基本用法,并通过三个实战案例,展示它在表单验证、弹窗动画、自动聚焦等场景中的实际应用。
410 17
|
5月前
|
JavaScript 前端开发 算法
Vue 3 和 Vue 2 的区别及优点
Vue 3 和 Vue 2 的区别及优点
|
5月前
|
存储 JavaScript 前端开发
基于 ant-design-vue 和 Vue 3 封装的功能强大的表格组件
VTable 是一个基于 ant-design-vue 和 Vue 3 的多功能表格组件,支持列自定义、排序、本地化存储、行选择等功能。它继承了 Ant-Design-Vue Table 的所有特性并加以扩展,提供开箱即用的高性能体验。示例包括基础表格、可选择表格和自定义列渲染等。
412 6
|
4月前
|
JavaScript 前端开发 API
Vue 2 与 Vue 3 的区别:深度对比与迁移指南
Vue.js 是一个用于构建用户界面的渐进式 JavaScript 框架,在过去的几年里,Vue 2 一直是前端开发中的重要工具。而 Vue 3 作为其升级版本,带来了许多显著的改进和新特性。在本文中,我们将深入比较 Vue 2 和 Vue 3 的主要区别,帮助开发者更好地理解这两个版本之间的变化,并提供迁移建议。 1. Vue 3 的新特性概述 Vue 3 引入了许多新特性,使得开发体验更加流畅、灵活。以下是 Vue 3 的一些关键改进: 1.1 Composition API Composition API 是 Vue 3 的核心新特性之一。它改变了 Vue 组件的代码结构,使得逻辑组
1500 0
|
6月前
|
JavaScript 前端开发 UED
vue2和vue3的响应式原理有何不同?
大家好,我是V哥。本文详细对比了Vue 2与Vue 3的响应式原理:Vue 2基于`Object.defineProperty()`,适合小型项目但存在性能瓶颈;Vue 3采用`Proxy`,大幅优化初始化、更新性能及内存占用,更高效稳定。此外,我建议前端开发者关注鸿蒙趋势,2025年将是国产化替代关键期,推荐《鸿蒙 HarmonyOS 开发之路》卷1助你入行。老项目用Vue 2?不妨升级到Vue 3,提升用户体验!关注V哥爱编程,全栈开发轻松上手。
440 2