• Vue虚拟DOM理解-深入且易懂


    前言

    本篇文章部分内容来源于霍春阳 《Vue.js设计与实现》这本书的理解, 感兴趣的小伙伴可以自行购买阅读。可以非常明确的感受到作者对 Vue 的深刻理解以及用心, 富含非常全面的 Vue 的知识点, 强烈推荐给大家。

    虚拟DOM理解

    先抛出一个结论, 这个结论对本篇文章的理解很有帮助: Vue是一个保留了运行时+编译时架构的框架, 编译时, 用户可以提供 HTML 字符串, 我们将其编译为数据对象再交给运行时处理; 运行时, 根据提供的数据对象渲染到页面中。这里不太理解没关系, 下文会逐步帮大家理解。

    什么是虚拟DOM

    上面提到的这个数据对象其实就是所谓的虚拟DOM, 他是一个 JS 树型结构的数据对象, 通俗易懂的说, 虚拟 DOM 就是一个 JS 普通对象, 这个 JS 对象是真实 DOM 的描述。没有太懂没关系, 看看下面这个例子。

    例如, 我们下面这样有一段 HTML 字符串:

    const html = `
    	
    Hello World
    `
    • 1
    • 2
    • 3
    • 4
    • 5

    假定有这样 Compiler 一个应用程序作为编译器, 它的作用是在将一个 HTML 字符串转换为树形的数据结构。

    const obj = Compiler(html)
    // obj 结果如下
    obj = {
      tag: "div",
      props: {
        id: 'app'
      }
      children: [
        {tag: "span", children: "Hello World"}
      ]
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个转换出来的树形的数据结构 obj 就可以看做是虚拟 DOM, 其中 tag 用来描述标签名称; props 是一个对象, 用来描述标签属性、事件等内容; children 用来描述标签的子节点, 即可以是一个数组, 代表子节点, 也可以是一个字符串, 代表子节点为文本节点。现在我们可以更深刻的感受到, 虚拟 DOM 就是对真实 DOM 的一个描述。

    事实上, 你完全可以设计自己设计虚拟 DOM 的结构, 比如使用 tagName 来描述标签名。

    虚拟DOM转为真实DOM

    上面我们提到过, Vue是一个运行时 + 编译时架构的框架, 我们再来理解一下。上面我们的过程, 就是在编译时进行的, 将 HTML 字符串编译为 JS 数据对象, 也就是虚拟 DOM。那么运行时, 我们就根据这个 JS 数据对象(虚拟 DOM), 渲染元素到页面当中了, 也就是转为真实的 DOM。那么虚拟 DOM 究竟是如何转换为真实 DOM 的? 其实它是通过渲染器实现的。渲染器是非常重要的一个角色, 平时我们使用 Vue.js 就是依赖渲染器进行的工作, 下面我们来简单认识一下渲染器。

    我们可以编写一个简单版的渲染器, 将虚拟 DOM 渲染到页面当中。例如我们有下面这样一个虚拟 DOM:

    • tag: 描述标签, 表示渲染一个
    • props: 描述属性或事件, 表示我们给
    • children: 用来描述子节点, 这里意思是表示 button 中的文本是"按钮" -->
    const vnode = {
      tag: "button",
      props: {"onClick": () => alert("Hello World")},
      children: "按钮"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    接下来我们就需要一个渲染器 renderer, 将上面这段虚拟 DOM, 转换为真实的 DOM:

    function renderer (vnode, container){
      // 获取标签名, 并创建一个DOM元素
      const curEl = document.createElement(vnode.tag)
    
      // 遍历属性, 将事件或属性添加到DOM元素上
      for (const key in vnode.props) {
        // on开头说明是事件, 则为DOM元素添加事件
        if(/^on/.test(key)) 
          curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
      }
    
      // 如果子节点为stirng类型, 说明是文本子节点
      if (typeof vnode.children === "string") 
        curEl.appendChild(document.createTextNode(vnode.children))
      // 如果子节点为数组类型, 递归调用渲染函数
      else if (Array.isArray(vnode.children)) 
        vnode.children.forEach(child => renderer(child, curEl));
    
      // 将元素添加到挂载点下
      container.appendChild(curEl)  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    下面我们传入刚刚的虚拟 DOM, 将它挂载到 body 下, 在浏览器运行代码, 我们就可以在页面中得到一个按钮, 点击按钮就会出现弹出 “Hello World”。

    renderer(vnode, document.body)
    
    • 1

    当然实际上的渲染器 renderer 内部会更为复杂, 这里我们只是做了一个简单实现。更加详情的实现我们可以去查看 Vue 的源码。

    组件的本质

    对于虚拟 DOM 和渲染器我们都有了初步的理解, 那么组件又是什么呢? 组件和虚拟 DOM 之间的关系是什么? 渲染器又是如何渲染组件的?

    事实上, 虚拟 DOM 除了可描述真实的 DOM 之外, 还可以用来描述组件, 但是组件毕竟不是一个真实的 DOM 元素, 那么我们该如何进行描述? 在讲述这个问题之前, 我要在抛出一个问题, 组件的本质是什么? 本质: 组件就是一组 DOM 元素的封装, 这组 DOM 元素就是该组件要渲染的内容。那如果这样的话, 我们就可以定义一个函数来代表组件。

    例如上一节的 vnode 我们将其定义到一个组件当中, 用一个函数来代表; 函数的返回值就是要渲染的内容, 也就是虚拟 DOM:

    function MyComponent() {
      return {
        tag: "button",
        props: {"onClick": () => alert("Hello World")},
        children: "按钮"
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们已经搞清楚组件的本质, 那么我们就可以使用虚拟 DOM 像描述标签一样来描述组件, 只不过 tag 属性中存放的不再是标签名, 而是组件函数。

    const vnode = {
      tag: MyComponent,
    }
    
    • 1
    • 2
    • 3

    当然, 我们也需要让渲染器 renderer 支持组件, 才能渲染, 所以我们需要对上本中的 renderer 函数进行一些修改为如下所示:

    function renderer(vnode, container) {
      // 说明描述的是标签
      if (typeof vnode.tag === "string") mountElement(vnode, container)
      // 说明描述的时组件
      else if(typeof vnode.tag === "function") mountComponent(vnode, container)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    我们先将上文中的 renderer 函数的名称修改为 mountElement, 让渲染器用来处理标签, 如下:

    function mountElement (vnode, container){
      // 获取标签名, 并创建一个DOM元素
      const curEl = document.createElement(vnode.tag)
    
      // 遍历属性, 将事件或属性添加到DOM元素上
      for (const key in vnode.props) {
        // on开头说明是事件, 则为DOM元素添加事件
        if(/^on/.test(key)) 
          curEl.addEventListener(key.slice(2).toLowerCase(), vnode.props[key])
      }
    
      // 如果子节点为stirng类型, 说明是文本子节点
      if (typeof vnode.children === "string") 
        curEl.appendChild(document.createTextNode(vnode.children))
      // 如果子节点为数组类型, 递归调用渲染函数
      else if (Array.isArray(vnode.children)) 
        vnode.children.forEach(child => renderer(child, curEl));
    
      // 将元素添加到挂载点下
      container.appendChild(curEl)  
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    再实现一个 mountComponent 函数, 让渲染器用来处理组件:

    function mountComponent (vnode, container) {
      // 获取到组件返回的虚拟DOM
      const subtree = vnode.tag()
      // 递归调用renderer渲染组件返回的虚拟DOM
      renderer(subtree, container)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    传入vNode 和 body 作为挂载点, 在浏览器中运行, 同样可以实现上文中的效果。

    renderer(vNode, document.body)
    
    • 1

    这样我们就通过虚拟 DOM 描述组件, 转换为真实渲染到页面当中, 但是组件一定是函数吗? 学习过 react 的小伙伴一定知道, 在 react 中有函数组件, 也有类组件。所以我们完全也可以使用一个 JS 对象来表达组件:

    const MyComponent = {
      render() {
        return {
          tag: "button",
          props: {"onClick": () => alert("Hello World")},
          children: "按钮"
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    当然渲染器 renderer 以及 mountComponent 函数都需要做一些修改, 支持对象组件:

    // 渲染器的修改
    function renderer(vnode, container) {
      const type = vnode.tag 
      if (typeof type === "string") mountElement(vnode, container)
      else if (
        typeof type === 'function' ||
        Object.prototype.toString.call(type) === '[object Object]'
      )
        mountComponent(vnode, container)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    // mountComponent函数的修改
    function mountComponent (vnode, container) {
      const type = vnode.tag 
      let subtree
      // 获取到组件返回的虚拟DOM
      if (typeof type === "function") subtree = vnode.tag()
      else if (typeof type === "object") subtree = vnode.tag.render()
    
      // 递归调用renderer渲染组件返回的虚拟DOM
      renderer(subtree, container)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们只做了很小的修改, 就能够满足用对象来表达组件的需求。到这里, 我们就完成了虚拟 DOM 将组件转为真实 DOM 的操作, 并且支持函数表达组件对象表达组件两种方式。

    模板工作原理

    前面我们知道了虚拟 DOM 是如何渲染为真实 DOM 的, 那么下面我们就来探讨一下模板是怎么工作的? 这也是 Vue 中另一个非常重要的角色: 编译器。我们再来回忆一下, 文章开头提到运行时+编译时。我们已经知道运行时, 是通过渲染器将虚拟 DOM 渲染为真实 DOM; 以及编译时将 HTML 字符串编译成虚拟 DOM。

    那么 Vue 中的模板就怎样进行工作的呢? 就是通过编译器, 编译器和渲染器一样, 只是一段程序而已, 编译器的作用是将模板编译成渲染函数。对于编译器来说, 模板就是一个普通字符串, 它会对字符串进行分析, 并生成一个功能相同的渲染函数(也就是 h 函数, 不知道 h 函数的可以去网上看看资料, 简单了解一下如何使用即可)。

    下面以一个 .vue 文件举个栗子, 有如下所示一个 Vue 文件:

    <template>
      <div @click="handler">
        按钮
      </div>
    </template>
    
    <script>
    export default {
      data() {/* ... */},
      methods: {
        handler: () => {/* ... */}
      }
    }
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    其中 template 标签中就是模板的内容, 编译器会把模板内容编译成一个渲染函数, 并添加到 script 标签中。上面代码经过编译器处理, 最终在浏览器中运行的代码如下:

    <script>
    export default {
      data() {/* ... */},
      methods: {
        handler: () => {/* ... */}
      },
      render() {
        return h('div', { onClick: handler }, '按钮')
      }
    }
    </script>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    所以无论是我们自己手写渲染函数, 还是使用模板, 它最终渲染的内容都是通过渲染函数产生的, 所以模板我们可以看做是手写渲染函数的一个语法糖。

    组件的实现依赖于渲染器,模板的编译依赖于编译器。编译器会将模板内容编译成一个渲染函数, 渲染函数的返回值就是虚拟 DOM, 渲染器再根据返回的这个虚拟 DOM 渲染成真实 DOM。这个过程就是模板的工作原理, 也是 Vue 渲染到页面的流程。

  • 相关阅读:
    获取PDF中的布局信息——如何获取段落
    poium测试库之JavaScript API封装原理
    LeetCode 面试题 16.05. 阶乘尾数
    盘点8款流行的网红纱帘,以及它们的特点 - 江南爱窗帘十大品牌
    bctoolbox 交叉编译
    Mac系统必装CleanMyMacX系统优化清理软件使用经验分享
    Js中clientX/Y、offsetX/Y和screenX/Y之间区别
    视频太大怎么压缩变小 视频太大了怎么压缩
    机器学习终极指南:统计和统计建模03/3 — 第 -3 部分
    为啥不建议使用Select *
  • 原文地址:https://blog.csdn.net/m0_71485750/article/details/133813669