Virtual DOM 的实现原理

  • Virtual DOM 的实现原理
    • 什么是虚拟DOM
    • 为什么要用虚拟DOM
    • 虚拟 DOM 的作用和虚拟 DOM 库
      • 虚拟 DOM 的作用
      • 虚拟 DOM 库
    • Snabbdom
      • Snabbdom 模块
      • Snabbdom 源码解析
        • 概述
        • h 函数
        • 常用快捷键
        • patch
        • patchVnode
        • updateChildren
      • 问题
        • key

Virtual DOM 的实现原理

什么是虚拟DOM

  • Virtual DOM(虚拟 DOM),即是由普通的JS对象来描述 DOM 对象

  • 真实 DOM 成员(创建真实DOM的成本非常高)

  • 使用 Virtual DOM 来描述真实 DOM

    • 就是普通的JavaScript对象,通过sel来描述选择器,通过text来描述文本,其他的之后再研究,创建一个虚拟DOM,它的成员非常少,也就是创建一个虚拟DOM的成本比创建一个真实DOM的成本要低很多

为什么要用虚拟DOM

  • 前端开发刀耕火种的时代(那时候开发前端需要手动操作DOM,还需要考虑浏览器兼容问题,非常麻烦,后来有了jQuery库简化了DOM操作,我们也不需要考虑浏览器兼容性问题,但是随着前端项目的复杂,我们操作DOM也越来越复杂,我们又要考虑操作数据又要操作DOM)
  • MVVM框架解决视图和状态同步问题(为了简化DOM的复杂操作,于是出现了各种各样的MVVM框架,MVVM框架解决视图和状态同步问题也就是当数据发生变化的时候自动更新视图,当视图发生变化自动同步数据)
  • 模板引擎可以简化视图操作,没办法跟踪状态(在过去为了简化视图操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态的问题,就是当数据发生变化的时候无法获取上一次的状态,只好把界面上的元素全部删除再重新创建,这样有个问题是性能低,页面闪烁)
  • 虚拟DOM跟踪状态变化(为了解决跟踪状态问题于是有了虚拟DOM,虚拟DOM的好处是当状态改变的时候不需要立刻更新DOM,只需要创建一个虚拟DOM树来描述真实的DOM树,虚拟DOM内部将清楚如何有效更新真实DOM,它会使用 dif 算法找到状态的差异只更新变化的部分)
  • 参考github上 virtual-dom 的动机描述
    • 虚拟DOM可以维护程序的状态,跟踪上一次的状态
    • 通过比较前后两次状态差异更新真实DOM

虚拟 DOM 的作用和虚拟 DOM 库

虚拟 DOM 的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 跨平台
    • 浏览器平台渲染DOM
    • 服务端渲染SSR(Nuxt.js–基于Vue/Next.js–基于React)
    • 原生应用(Weex/React Native)
    • 小程序(mpvue/uni-app)等

虚拟 DOM 库

  • Snabbdom
  • Vue.js 2.x 内部使用的虚拟DOM就是改造的 Snabbdom
  • 大约 200 SLOC(single line of code)
  • 通过模块可扩展
  • 源码使用 TypeScript 开发
  • 最快的 Virtual DOM 之一
  • virtual-dom

Snabbdom

Snabbdom

  • init 函数:高阶函数,它接收一个数组作为参数,这个数组里加载的是 Snabbdom 的模块,init 返回一个 patch 函数,patch 函数对我们来说非常重要,它的核心是把虚拟 dom 转换为真实 dom 渲染到界面上
  • h 函数:在创建Vue实例的时候会传入一个选项 render 函数,在 render 函数中它有一个参数是我们的 h 函数,render 中 h 函数的作用和 Snabbdom 中函数的作用是一样的,都是创建虚拟节点

Snabbdom 模块

  • 模块的作用

    • Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
    • Snabbdom 中的模块可以用来扩展 Snabbdom 的功能
    • Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的
  • 官方提供的模块
    • attributes:设置 vnode 的对应的 dom 元素的属性,它内部使用的是 DOM 的标准方法,对 dom 元素的布尔类型进行判断
    • props:设置 DOM 对应的属性,props 设置 dom 对象的属性是通过 对象.属性来设置的,另外它内部不会处理 布尔类型的属性
    • dataset:用来处理 HTML5 中的提供的 data- 这样的自定义属性
    • class:不是用来设置 类样式的是用来切换类样式的,如果想设置类样式可以通过 h 函数的第一个参数来设置
    • style:用来设置 行的样式,并且使用这个模块可以很容易设置过度动画,它内部还设置了trans-end 这个事件
    • eventlisteners:注册和溢出 event 事件的
  • 模块的使用步骤
    • 导入需要的模块
    • init() 中注册模块
    • h() 函数的第二个参数处使用模块

Snabbdom 源码解析

概述

 - 如何学习源码- 宏观了解- 带着目标看源码- 看源码的过程要不求甚解- 调试- 参考资料
  • Snabbdom 的核心

    • init() 设置模块,创建 patch() 函数
    • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
    • patch() 比较新旧两个 Vnode
    • 把变化的内容更新到真实 DOM 树

h 函数

  • 介绍

    • 作用:创建 VNode 对象

    • Vue 中的 h 函数

    • h 函数最早见于 hyperscript,使用 JavaScript 创建超文本

  • 函数重载
  • 参数个数或参数类型不同的函数
  • JavaScript 中没有重载的概念
  • TypeScript 中有重载,不过重载的实现还是通过代码调整参数

常用快捷键

patch

  • patch 整体过程分析

    • patch(oldVnode, newVnode)
    • 把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
    • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容
    • 如果新的 VNode 有 children,判断子节点是否有变化
  • 功能:

    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode, 作为下一次 patch() 的 oldVnode
  • 执行过程
    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnode 和 vnode 相同(key 和 sel 相同)
      • 调用 patchVnode(), 找节点的差异并更新 DOM
    • 如果 oldVnode 是 DOM 元素
      • 把 DOM 元素转换成 oldVnode
      • 调用 createElm() 把 vnode 转换为真实 DOM, 记录到 vnode.elm
      • 把刚创建的 DOM 元素插入到 parent 中
      • 移除老节点
      • 触发用户设置的 create 钩子函数

patchVnode

  • 功能

    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM
  • 执行过程
  • 首先执行用户设置的 prepatch 钩子函数
  • 执行 create 钩子函数
    • 首先执行模块的 create 钩子函数
  • 然后执行用户设置的 create 钩子函数
  • 如果 vnode.text 未定义
    • 如果 oldVnode.children 和 vnode.children 都有值

      • 调用 updateChildren()
      • 使用 diff 算法对比子节点,更新子节点
    • 如果 vnode.children 有值,oldVnode.children 无值
      • 清空 DOM 元素
      • 调用 addVnodes(), 批量添加子节点
    • 如果 oldVnode.children 有值, vnode.children 无值
      • 调用 removeVnodes(), 批量删除子节点
    • 如果 oldVnode.text 有值
      • 清空 DOM 元素内容
  • 如果设置了 node.text 并且和 oldVnode.text 不等
    • 如果老节点有子节点,全部移除
    • 设置 DOM 元素的 textContent 为 vnode.text
  • 最后执行用户设置的 postpatch 钩子函数

updateChildren

 在patchVnode 内部当新旧 vnode 都有子节点,并且子节点不相同的时候会调用 updateChildren 对比子节点,updateChildren实现比较复杂也是 diff 算法的核心,它负责对比所有子节点的差异并更新真实 DOM
  • Diff 算法

    • 虚拟DOM 中为什么要使用 diff 算法?

      • (渲染真实 DOM 的开销很大,DOM 操作会引起浏览器的重排和重绘,也就是浏览器的重新渲染,浏览器重新渲染页面是非常消耗性能的,因为要重新绘制整个页面,当数据变化后尤其是大量数据变化后如列表中的数据,如果直接操作 DOM的话会让浏览器重新渲染整个列表,虚拟 DOM diff 的核心是当数据变化后不直接操作 DOM,而是用 js 对象来描述真实 DOM,当数据变化后会先比较 js 对象是否发生变化,找到所有变化后的位置,最后只去最小化的更新变化的位置,从而提高性能。diff 算法类似排序算法,排序算法对一组树进行排序,排序算法的实现有很多种,比如冒泡排序、快速排序。)
    • 虚拟 DOM 中的 diff 算法

      • 查找两棵树每一个节点的差异

        • 最麻烦的是把第一棵树的每一个节点和第二棵树的每一个节点对比,如果有 N 个节点的话就会有 N 的平方次,找到差异后再进行一次循环更新一差异的部分
    • Snabbdom 根据 DOM 的特点对传统的 diff 算法做了优化

      • DOM 操作时候很少会跨级别操作节点
      • 只比较同级别的节点(减少节点比较的次数,这样如果有 N 个节点就只比较 N 次比较过程中找到差异,相对传统 diff 算法大大减少比较次数)

加倍理解

  • 功能
    - diff 算法的核心,对比新旧节点的 children,更新 DOM
    - 执行过程:
    - 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二棵树的每一个节点比较,但是这样的时间复杂度为 O(n^3)
    - 在 DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
    - 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样的时间复杂度为 O(n)
    - 在进行同级别比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍历过程中移动索引
  • 执行过程

    • 在对开始和结束节点比较的时候,总共有四种情况

      • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
      • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
      • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点)
      • oldEndVnode / newStartVnode (旧介绍节点 / 新开始节点)
    • 开始和结束节点

      • 如果新旧开始节点是 sameVnode (key 和 sel 相同)

        • 调用 patchVnode() 对比和更新节点
        • 把旧开始和新开始索引往后移动 oldStartIdx++ / newStartIdx++
    • 旧开始节点 / 新结束节点

      • 调用 patchVnode() 对比和更新节点
      • 把 oldStartVnode 对应的 DOM 元素,移动到右边,更新索引
    • 旧结束节点 / 新开始节点

      • 调用 patchVnode() 对比和更新节点
      • 把 oldEndVnode 对应的 DOM 元素,移动到左边,更新索引
    • 非上述四种情况

  • 循环结束

    • 当老节点的所有子节点先遍历完(oldStartIdx > oldEndIdx),循环结束

    • 新节点的所有子节点先遍历完(newStartIdx > newEndIdx),循环结束

    • oldStartIdx > oldEndIdx

      • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx)

        • 说明新节点有剩余,把剩余节点批量插入到右边
    • newStartIdx > newEndIdx

      • 如果新节点的数组先遍历完(newStartIdx > newEndIdx)
      • 说明老节点有剩余,把剩余节点批量删除
  • key 的意义

    • Snabbdom 中使用 key 的意义跟 Vue 中 使用 key 的意义一样,都是在 diff 算法中用来比较 vnode 是否是相同节点,如果不设置 key 会最大程度重用当前的 DOM 元素,但是重用 DOM 元素会有一个问题,发生渲染错误。给所有相同父元素的子元素设置具有唯一值的key,否则有可能造成渲染错误

问题

key

  • key 的作用

  • Vue2.x 的解释

  • Vue3.x 的解释

  • 有相同元素的子元素必须有独特的 key。重复的 key 会造成渲染错误

  • 它也可以用于强制替换元素/组件而不是重复使用它

    • 在适当的时候触发组件的生命周期钩子
    • 触发过渡
  • 什么时候使用插槽传值,什么时候用prop传值
    - router-link prop: { to: String }
    - prop 的作用是父组件向子组件传值时用
    - 插槽是占位 插槽文档
    - dialog <slot></slot> <dialog><div>登录</div></dialog>

  • routerLink 可以在组件中使用吗?单页面和多页面应用对比、区别,怎么抉择?

    • routerLink 可以在组件中使用,routerLink就是个组件
    • 一般我们说的多页面是传统的开发例如以前的jap,jxp开发的页面,以前使用传统开发的时候每一个功能都用一个页面去开发,用户访问我们这个项目的时候每访问一个页面都要去发送一个请求,拿到新的内容然后刷新整个页面,这是多页应用,一般我们说的多页应用是指通过服务端帮我们把数据渲染到网页上来,多页应用的好处是有更好的 SEO,数据直接在服务端拼接到 HTML 页面上来,在服务端返回的时候我们在浏览器打开网页的时候就已经把数据渲染出来了,而不需要再去发送 ajax 请求,这样的话有更好的 SEO,也就是搜索引擎的小机器人,去抓取页面的时候不需要再去做额外的事情都快一点可以筛选出里面的内容并且根据一些关键字把需要的内容储存起来,另外多页应用首次访问速度相对来说比单页应用,因为我们知道单页应用在首次渲染的时候会把所有组件全部渲染下来,当然我们可以做一些简单的优化,比如说懒加载,单页应用还有一个缺点是不利于 SEO,比如基于 Vue 或 react 做的项目都是单页应用,去请求首页的时候,index.html的时候我们拿回来的页面上只有一个id是app 的div,搜索引擎去抓取的时候只有一个div什么都没有就拜拜走了,在单页应用显示的所有的内容都是通过 js 来创建的,展示的数据都需要通过 ajax 请求来获取到然后通过 js 把它渲染到页面上来,所以单页应用不利于 SEO,那我们怎么去抉择?我们做的90%都是偏于后台管理的,是不需要考虑 SEO的,更多的是考虑用户的体验,考虑用户体验的话单页应用的体验要比多页应用的好很多,因为单页应用做出的效果可以和桌面的相媲美,它首次访问的速度略逊于多页应用,那我们可以通过一些优化不断保证它的首页访问速度,如果我们做的是电商项目用户看到页面需要被搜索索引搜索到那我们去选择多页应用,其实现在的话,我们会把单页应用和多页应用这种好处结合起来,后面会用到 SSR 渲染的话,就会有 Vue 或者 react的 基于 Vue的Nuxt 或 基于 react 的Next的这种服务端渲染的事情,有现代化的服务端渲染
  • 源码学习有什么好的办法吗,学完对于我来说有什么提升跟帮助

    • 快速定位和解决问题 -> 响应式数据的了解
    • this.obj.arr = [] ,要在某个对象增加一个属性,或者在某个时候这个属性的值发生了变化,它的值发生了变化页面的内容没有立即重新渲染,这个问题是我们不能给一个对象随意动态的添加一个响应式的数据,想添加一个响应式数据的时候有其他方式,当出现意料不到的问题我们可以快速定位和解决问题
    • this.arr[0] = 5, 把数组第一项的值改变了这是否是响应式的,不是,为什么不是?数组的属性不会是响应式的,
    • 单向绑定和双向绑定哪个更好,使用双向绑定比单向绑定更方便,但是性能没有单向绑定的好,Vue的最初目的是让小白也可以使用这种高级的前端框架
    • 组件本质 -> 渲染函数 render -> h() 函数
 function fn1 () {console.dir(this)console.log(1)}function fn2 () {console.log(2)}fn1.call(fn2)  // f fn2,1fn1.call.call(fn2) // 2fn1.call.call(fn2, 1, 23)  // 2Function.prototype.mycall = function (context, ...args) {//  此处要去处理 context 是原始值的问题if (typeof context === 'number') {context = new Number(context)}//  context = context || window//  ?? 的作用 跟 || 一样的, ?? 只有 context 的值是 null 或者 undefined 的时候才返回 windowcontext = context ?? window    // ?? 的作用 跟 || 一样的//  此处的 this 指向的是 fn1 函数context.fn = thisconst result = context.fn(...args)delete context.fnreturn result}const obj = { name: 'zs' }//  fn1.mycall(obj)   // 1fn1.mycall.mycall(fn2) // 2fn1.mycall.mycall(fn2, 1, 2) // 2
  • 虚拟 DOM

    • 使用 VNode 来描述真实 DOM
    • 跨平台
    • 首次渲染的性能下降,界面复杂的情况更新视图会提高性能
  • No Virtual DOM

    • svelte
    • svelte
    • svelte.github
  • Diff 算法

    • 对比两棵树上所有的节点,传统方式使用依次比较两棵树的每一个节点,这样的时间复杂度是 O(n^3). 比如,当前有三个节点,比较完树上 的每一个节点需要的时间是 O(n^3).其中 n 是节点个数
    • patch(oldVnode, newVnode)
    • 打补丁,把新节点中变化的内容渲染到真实DOM,最后返回新节点作为下一次处理的旧节点
    • 对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同)
    • 如果不是相同节点,删除之前的内容,重新渲染
    • 如果是相同节点,再判断新的 VNode 是否有 text , 如果有并且和 oldVnode 的text 不同,直接更新文本内容
    • 如果新的 VNode 有 children,判断子节点是否有变化
    • diff 过程只进行同层级比较,时间复杂度 O(n)
  • 更改第一个元素,没有设置 key

  • updateChildren() 的时候比较新旧 VNode 数组中的第一个 VNode (li),此时是 sameVnode() 调用 patchVnode() 比较 VNode (li), 都有子节点(文本节点)继续调用 updateChildren()
    文本节点也都是 sameVnode() 调用 patchVnode(), 此时有 text 属性,直接更新 li 的 text
    继续比较第二个 vnode…最终都是更新文本的操作 只更了一次文本节点
  • 更改第一个元素,设置 key

如果把数组中的当前项,设置为 li 的 key 的话,第一个新的 VNode, 和第一个老的 VNode 不是 sameVnode,
于是比较最后一个老的旧节点和最后一个老的新节点, 是 sameVnode, 节点内容也什么一样什么都不做 倒数第一个节点也一样
回到比较第一个节点的过程,新的第一个节点,在老节点中找不到相同节点, 这时候创建一个新的 li,插入到第一个老的 li 之前。
最后再把老的第一个 li 节点,从界面上移除

只有一次插入的 DOM 操作,和一次移除的 DOM 操作

Virtual DOM 的实现原理相关推荐

  1. [react] 你知道Virtual DOM的工作原理吗?

    [react] 你知道Virtual DOM的工作原理吗? Virtual DOM是什么:虚拟DOM是真实DOM的javascript对象的映射 Virtual DOM的工作原理:数据驱动视图更新这个 ...

  2. Vue进阶之Virtual DOM(虚拟DOM) 实现原理

    Vue进阶之Virtual DOM(虚拟DOM) 实现原理 Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual D ...

  3. Virtual DOM的简单实现

    了解React的同学都知道,React提供了一个高效的视图更新机制:Virtual DOM,因为DOM天生就慢,所以操作DOM的时候要小心翼翼,稍微改动就会触发重绘重排,大量消耗性能. 1.Virtu ...

  4. 深度理解 Virtual DOM

    目录: 1 前言 2 技术发展史 3 Virtual DOM 算法 4 Virtual DOM 实现 5 Virtual DOM 树的差异(Diff算法) 6 结语 7 参考链接 1 前言 我会尽量把 ...

  5. 实现 Virtual DOM 下的一个 VNode 节点

    实现 Virtual DOM 下的一个 VNode 节点 什么是VNode 我们知道,render function 会被转化成 VNode 节点.Virtual DOM 其实就是一棵以 JavaSc ...

  6. 谈谈Virtual DOM

    前言 目前最流行的两大前端框架,React和Vue,都不约而同的借助Virtual DOM技术提高页面的渲染效率.那么,什么是Virtual DOM?它是通过什么方式去提升页面渲染效率的呢? 什么是V ...

  7. 基于Virtual DOM与Diff DOM的测试代码生成

    尽管是在年末,并且也还没把书翻译完,也还没写完书的第一稿.但是,我还是觉得这是一个非常不错的话题--测试代码生成. 当我们在写一些UI测试的时候,我们总需要到浏览器去看一下一些DOM的变化.比如,我们 ...

  8. 了不起的Virtual DOM(一):起源

    前言 首先欢迎大家关注我的掘金账号和Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励. 之所以想写本系列文章的主要原因是将近一个月时间没有写点 ...

  9. vue的Virtual Dom实现- snabbdom解密

    vue在官方文档中提到与react的渲染性能对比中,因为其使用了snabbdom而有更优异的性能. JavaScript 开销直接与求算必要 DOM 操作的机制相关.尽管 Vue 和 React 都使 ...

最新文章

  1. 计算机网络技术包括哪几种,计算机网络技术包含的两个主要技术是计算机技术和( )。...
  2. 如何(以及为什么需要)创建一个好的验证集
  3. MySQL—异常处理
  4. python学习记录(三)
  5. 按照linux文件出现的时间来删除文件
  6. 西工大c语言noj100作业,西工大17秋《C语言程序设计》平时作业
  7. 《Excel 职场手册:260招菜鸟变达人》一第 13 招 利用数据验证给单元格添加注释,不用批注...
  8. JQuery 中选择多选择框,和单选框,实现获取相应选择的值
  9. linux组的管理命令,linux 用户和组管理命令(示例代码)
  10. Quartz.NET常用方法 01
  11. 掌握鸿蒙轻内核静态内存的使用,从源码分析开始
  12. 使用go制作微服务数据计算
  13. php 通过ajax上传文件,php – 通过ajax上传文件
  14. TFS Two Build Definations Share the Same Code Branch
  15. unicode编码表查询
  16. z世代消费力白皮书_LSPACE丨Z世代虽穷但买的态度你真的懂吗
  17. 旷视研究院参会PRCV2019 推进模式识别与CV技术交流
  18. 微信公众号里面使用定位
  19. POI 复制 word 表中的行操作 以及样式
  20. java 和c 多态比较_多态在 Java 和 C 编程语言中的实现比较

热门文章

  1. 湛江C语言培训,湛江c语言编程学习,湛江学c语言编程报班,湛江学c语言编程自学好还是报班好...
  2. Python解析URL参数的方法
  3. 随机森林(RFC)实现模型优化与特征提取
  4. teradata ttu_Teradata Studio中文乱码解决方法
  5. Vin码/车架号OCr识别
  6. 【信管1.3】计算机网络基础(一)网络标准与协议
  7. 笔记dng图片在premiere和ae中不一致
  8. java计算机毕业设计基于springboot+vue+elementUI的旅游网站(源码+数据库+Lw文档)
  9. vue中,获取一个div的高赋值给另一个div (自适应)
  10. 独立同分布的中心极限定理