精华内容
下载资源
问答
  • 2021-08-19 23:04:03

    虚拟DOM



    一、什么是虚拟DOM

    1、虚拟DOM(virtual DOM)简称vdom,是一个普通的js对象,用来描述真实dom结构,因为它不是真实的DOM,所以称为虚拟DOM
    2、属性:vdom具有三个属性
    (1)tag:标签名
    (2)attrs:属性
    (3)children:子元素对象
    3、举例
    真实dom

    <ul id='list'>
    	<li class='item'>姓名</li>
    	<li class='item'>班级</li>
    </ul>
    

    虚拟dom

       {
                tag:'ul',
                attrs:{
                    id:'list'
                },
                children:[
                    {
                        tag:'li',
                        attrs:{
                            className:'item'
                        },
                        children:['姓名']
                    },{
                        tag:'li',
                        attrs:{
                            className:'item'
                        },
                        children:['班级']
                    }
                ]
            }
    

    4、虚拟DOM的设计思想
    (1)提供一种方便的工具,使开发效率得到保证;
    (2)保证最小化的DOM操作,使得执行效率得到保证

    二、为什么引入虚拟DOM

    1、原因一:真实的DOM运行是很慢的,其元素非常庞大,页面的性能问题,大部分都是由DOM操作引起的,真实的DOM节点,哪怕一个最简单的div也包含着很多属性,由此可见,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户的体验。
    PS:大家可以写一个div在页面里,打印出它的属性,会发现一个简单的元素都会有很多属性
    2、原因二:虚拟 dom 是相对于浏览器所渲染出来的真实 dom而言的,在react,vue等技术出现之前,我们要改变页面展示的内容只能通过遍历查询 dom 树的方式找到需要修改的 dom 然后修改样式行为或者结构,来达到更新 ui 的目的,这种方式相当消耗计算资源,因为每次查询 dom 几乎都需要遍历整颗 dom 树,如果建立一个与 dom 树对应的虚拟 dom 对象( js 对象),以对象嵌套的方式来表示 dom 树及其层级结构,那么每次 dom 的更改就变成了对 js 对象的属性的增删改查,这样一来查找 js 对象的属性变化要比查询 dom 树的性能开销小
    3、原因三:创建真实DOM成本比较高,而如果用js对象来描述一个dom节点,成本比较低,另外我们在频繁操作dom是一种比较大的开销。所以建议用虚拟dom来描述真实dom
    PS:这里有一些相关问题值得思考:
    (1)为什么操作真实DOM的成本比较高?
    ① dom 树的实现模块和 js 模块是分开的这些跨模块的通讯增加了成本
    ② dom 操作引起的浏览器的回流和重绘,使得性能开销巨大。
    原本在 pc 端是没有性能问题的,因为pc端的计算能力强,但是随着移动端的发展,越来越多的网页在智能手机上运行,而手机的性能参差不齐,会有性能问题。我们之前用jquery在pc端写那些商城页面都没有问题,但放到移动端浏览器访问时,就会出现很多问题。
    (2)浏览器收到一个html页面是如何解析成页面呈现给用户的呢
    ① 解析html:会按顺序解析。浏览器有专门的html解析器来解析HTML,并在解析的过程中构建DOM树
    ②构建dom树:它和步骤(1) 是同步进行,可以理解为边解析边构建。
    ③构建呈现树renderTree:将dom树与css结合,也就是将样式应用到dom节点上
    ④布局:计算呈现树节点的大小和位置,这一位置是递归进行的。
    ⑥绘制:布局完成后,便是将呈现树绘制出来显示在屏幕上
    在浏览器解析过程中,每次对真实DOM的操作都会需要大量的回流与重置,这对浏览器的性能来说造成了极大地浪费

    三、虚拟DOM的作用

    1、我们都知道,传统dom数据发送变化的时候,我们都需要不断的去操作dom,才能更新dom的数据,虽然后面出现了模板引擎这种东西,可以让我们一次性去更新多个dom。但模板引擎依旧没有一种可以追踪状态的机制,当引擎内某个数据发生变化时,他依然要操作dom去重新渲染整个引擎,而虚拟dom可以很好的跟踪当前dom状态,因为他会根据当前数据生成一个描述当前dom结构的虚拟dom,然后数据发送变化时,又会生成一个新的虚拟dom,而这两个虚拟dom恰恰保存了变化前后的状态。然后通过diff算法,计算出两个前后两个虚拟dom之间的差异,得出一个更新的最优方法(哪些发生改变,就更新哪些)。可以很明显的提升渲染效率以及用户的体验。
    2、因为虚拟dom是一个普通的javascript对象,故他不单单只能允许在浏览器端,渲染出来的虚拟dom可同时在node环境下或者weex的app环境下运行。有很好的跨端性

    四、Virtual DOM 算法包括步骤

    1、将DOM树转换成JS对象树,产生第一个虚拟DOM树(与真实DOM树一样)
    2、数据发生变化时(当你有增删操作)产生第二个虚拟DOM树
    3、diff算法逐层比较两个虚拟DOM树并标记增删操作(不会渲染)
    4、将标记出来的差异(虚拟节点)应用到真正的 DOM 树,而不是将整个虚拟DOM树覆盖到真正的DOM树上

    五、diff算法

    在进行新旧虚拟dom树的比较时,用到了diff算法,这里简单说一下这个算法的思路,有兴趣的小伙伴可以去找更多的资料
    1、diff 比较两个虚拟dom只会在同层级之间进行比较,不会跨层级进行比较。而用来判断是否是同层级的标准就是
    ①是否在同一层 ②是否有相同的父级
    下面用一张网上很经典的图片来理解
    在这里插入图片描述2、diff是采用先序深度优先遍历得方式进行节点比较的,即,当比较某个节点时,如果该节点存在子节点,那么会优先比较他的子节点,直到所有子节点全部比较完成,才会开始去比较改节点的下一个同层级节点
    在这里插入图片描述当比较新旧两个dom时,会按照图中1-9的顺序去进行比较

    六、虚拟DOM优缺点

    1、优点

    • 保证性能下限:
      虚拟DOM可以经过diff找出最小差异,然后批量进行patch,这种操作虽然比不上手动优化,但是比起粗暴的DOM操作性能要好很多,因此虚拟DOM可以保证性能下限
    • 无需手动操作DOM: 虚拟DOM的diff和patch都是在一次更新中自动进行的,我们无需手动操作DOM,极大提高开发效率
    • 跨平台:
      虚拟DOM本质上是JavaScript对象,而DOM与平台强相关,相比之下虚拟DOM可以进行更方便地跨平台操作,例如服务器渲染、移动端开发等等
      2、缺点
      无法进行极致优化: 在一些性能要求极高的应用中虚拟DOM无法进行针对性的极致优化,比如VScode采用直接手动操作DOM的方式进行极端的性能优化
      思考:
      1、你觉得使用了虚拟DOM就真的不操作dom元素了吗
      并不是的,只是减少用户操作dom,虚拟DOM在渲染的时候其实还是会操作dom的
      2、虚拟DOM为什么会提高性能
      虚拟DOM提高性能,不是说不操作DOM,而是减少操作DOM的次数,减少回流和重绘
      虚拟 dom 相当于在 js 和真实 dom 中间加了一个缓存,利用 dom diff 算法避免了没有必要的 dom 操作,从而提高性能
      3、虚拟DOM 比 原生DOM快吗
      虚拟Dom不比原生DOM快,没有任何框架可以比手动优化DOM更快,因为框架的DOM操作层需要应对任何上层可能产生的操作,所以他的实现需具有普适性。并且在内部,虚拟DOM还使用真实DOM来呈现页面或内容。因此,虚拟DOM不可能比真实dom更快
      4、虚拟DOM的目的:当数据不管怎么变化,都能以最小的代价来更新DOM,在不进行手动优化的情况下,给你提供过得去的性能体验。
    更多相关内容
  • 杰特 在JQuery环境下用MVVM开发复杂组件,仅几kb超小体积。 定义一个开关组件,挂载到jquery后, $('#el').switch()就可以将#el变成一个开关组件,和$('#el').val()可以获取值( js也能取),放进form也能直接提交,...
  • 今天小编就为大家分享一篇解决vue中虚拟dom,无法实时更新的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
  • vue 虚拟DOM的原理

    2021-01-21 10:58:35
    为什么需要虚拟DOM?  如果对前端工作进行抽象的话,主要就是维护状态和更新视图,而更新视图和维护状态都需要DOM操作。其实近年来,前端的框架主要发展方向就是解放DOM操作的复杂性。  运行js的速度是很快的,...
  • 一个主要的令人兴奋的新特性就是更新页面的”虚拟DOM”的加入。 虚拟 DOM 可以做什么? React 和 Ember 都使用了虚拟DOM来提升页面的刷新速度。为了理解其如何工作,让我们先讨论一下几个概念: 更新DOM的花费时间...
  • 它使用虚拟 DOM 以获得更好的性能。 灵感 建立在 Matt Esch实现之上。 这个库的灵感来自其他虚拟 DOM 框架中的一些想法: 、 、 和 。 目标 该库的目标是仅解决视图层(为 Web 应用程序创建动态 HTML)。 虚拟 ...
  • vdom-to-html, 将虚拟DOM节点转换为 HTML vdom-to-html 将虚拟dom节点转换为 HTML安装npm install --save vdom-to-html用法var VNode = require('virtual-dom
  • 解析真实DOM操作处理以及虚拟Dom
  • Vidact将您的React源代码编译为无虚拟DOM的VanillaJS代码 :trade_mark: 。它类似于Svelte,但与不同,Vidact不会引入新的语法。它采用纯兼容ReactJavaScript(JSX)并输出纯JavaScript。 Vidact当前处于Alpha阶段...
  • 1.为什么要使用虚拟dom? 网页性能优化->尽量少操作DOM 2..虚拟DOM(Virtual DOM) VS js直接操作原生DOM(innerHTML) function Raw() { var data = _buildData(), html = ; ... for(var i=0; i<data.length; i...
  • 主要介绍了vue 源码解析 --虚拟Dom-render的相关知识,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
  • vnode2canvas - 基于Vue插件将虚拟DOM渲染到canvas中
  • 主要介绍了Vue使用虚拟dom进行渲染view的方法,本文给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
  • 将炒作放回上标,将OM放回虚拟DOM; 一袋的招数。 组件是流行的主流抽象,但是组件组成的真正力量在很大程度上尚未得到开发。 秘银机器工具以实用方式演示了寻求公开(而不是封装)秘银超级脚本和虚拟DOM接口功能的...
  • 主要介绍了浅谈React的最大亮点之虚拟DOM,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 虚拟 dom 的表单接口 工作正在进行中 目的是将其作为一组表单节点实用程序函数,允许我们修补属性以按照我们期望的方式运行。 这可能比尝试实现 VNode 转换更好:首先创建您想要的东西总是更好。 这是很早的,不起...
  • 虚拟dom

    2022-03-22 22:00:31
    虚拟DOM


    1. 为什么要使用虚拟DOM

    先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解我们为什么需要虚拟dom。

    webkit引擎的处理流程:
    在这里插入图片描述
    所有浏览器的引擎工作流程都差不多,大致分为5步:创建DOM tree–> 创建Style Rules–> 构建Render tree --> 布局Layout --> 绘制Painting。

    1. 用HTML分析器,分析HTML元素,构建一颗DOM树。
    2. 用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。
    3. 将上面的DOM树和样式表,关联起来,构建一颗Render树。这一过程又称为Attachment。每个DOM节点都有attach方法,接收样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。
    4. 有了Render树后,浏览器开始布局,会为每个Render树上的节点确定一个在显示屏上出现的精确坐标值。
    5. Render树有了,节点显示的位置坐标也有了,最后就是调用每个节点的paint方法,让它们显示出来。

    当你用传统的原生api或者jQuery去操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。

    比如当你在一次操作时,需要更新10个DOM节点。

    理想的状态是一次性构建完DOM树,再执行后续操作。

    但浏览器没有如此智能,收到第一个更新DOM请求后,并不知道后续还有9次更新操作,因此会马上执行流程,最终执行10次流程。

    显然例如计算DOM节点的坐标值等都是白白浪费性能,可能这次计算完,紧接着下一个DOM更新请求,节点值就会改变,前面的计算就是浪费。

    即使计算机硬件一直在更新迭代,操作DOM的代价也是很贵的,频繁操作会出现页面卡顿,影响用户体验。

    真实的DOM节点,即使只是一个简单的div也包含很多的属性:
    在这里插入图片描述
    在这里插入图片描述
    虚拟DOM就是为了解决这个浏览器的性能问题而被设计出来的。

    例如前面的例子,假如一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地的一个js对象中,最终将这个js对象一次性attach到DOM树上,通知浏览器去执行绘制工作,这样可以避免大量的无谓的计算量。


    2. Virtual DOM 算法

    相对于 DOM 对象,原生的 JavaScript 对象处理起来更快,而且更简单。

    DOM 树上的结构、属性信息我们都可以很容易地用 JavaScript 对象表示出来:

    var element = {
      tagName: 'ul', // 节点标签名
      props: { // DOM的属性,用一个对象存储键值对
        id: 'list'
      },
      children: [ // 该节点的子节点
        {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
        {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
      ]
    }
    

    对应HTML写法为:

    <ul id='list'>
      <li class='item'>Item 1</li>
      <li class='item'>Item 2</li>
      <li class='item'>Item 3</li>
    </ul>
    

    既然原来 DOM 树的信息都可以用 JavaScript 对象来表示。
    反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的DOM树。

    可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。
    记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。
    这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作DOM的时候确实只变更有不同的地方。

    Virtual DOM 算法:

    1. 用JavaScript对象结构表示DOM树的结构,然后用这个树构建一个真正的DOM树,插到文档当中。
    2. 当状态变更的时候,重新构造一颗新的对象树。用新的树和旧的树进行比较,记录两棵树的差异。
    3. 把步骤2所记录的差异应用到步骤1所构建的真实DOM树上,更新视图。

    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。
    可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。
    CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。


    3. 算法实现

    3.1 步骤一:用js对象模拟DOM树

    用JavaScript来表示一个DOM节点,记录它的节点类型、属性、子节点:

    • element.js
    function Element (tagName, props, children) {
      this.tagName = tagName
      this.props = props
      this.children = children
    }
    
    module.exports = function (tagName, props, children) {
      return new Element(tagName, props, children)
    }
    

    例如上面的DOM结构可以表示:

    var el = require('./element');
    
    var ul = el('ul',{id:'list'},[
        el('li', {class: 'item'}, ['Item 1']),
        el('li', {class: 'item'}, ['Item 2']),
        el('li', {class: 'item'}, ['Item 3'])
    ])
    

    现在ul只是一个JavaScript对象表示的DOM结构,页面上没有这个结构。
    我们可以根据ul构建真正的< ul >:

    • element.js 继续添加
    Element.prototype.render = function() {
        let el = document.createElement(this.tagName);  //根据tagName构建
        let props = this.props;
    
        for(let propsName in props){
            let propsValue = props[propsName];
            el.seAttribute(propsName,propsValue);
        }
    
        var children = this.children || [];
        children.forEach(function(child){
            let childEl = (child instanceof Element) 
                        ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
                        : document.createTextNode(child); // 如果是字符串,只构建文本节点
            el.appendChild(childEl)
        })
        return el;
    }
    

    render方法会根据tagName构建一个真正的DOM节点,然后设置节点的属性,最后递归构建自己的子节点。

    let ulRoot = ul.render();
    document.body.appendChild(ulRoot)
    

    上面的ulRoot是真正的DOM节点,把它塞入文档中,这样body里面就有了真正的< ul >的DOM结构:

    <ul id='list'>
      <li class='item'>Item 1</li>
      <li class='item'>Item 2</li>
      <li class='item'>Item 3</li>
    </ul>
    

    3.2 比较两颗虚拟DOM树的差异

    比较两颗DOM树的差异是Virtual DOM算法最核心的部分,也就是Virtual DOM的diff算法。

    Virtual DOM 只会对同一个层级的元素进行对比:
    在这里插入图片描述
    上面的div只会和同一层级的div对比,第二层级的只会跟第二层级对比。这样算法复杂度就可以达到 O(n)。

    3.2.1 深度优先遍历,记录差异

    在实际的代码中,会对新旧两棵树进行一个深度优先的遍历,这样每个节点都会有一个唯一的标记:
    在这里插入图片描述
    在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比。如果有差异的话就记录到一个对象里面。

    // diff函数,对比两棵树
    function diff(oldTree,newTree){
        let index = 0;  // 当前节点的标志
        let patches = {};   //记录每个节点的差异对象
        dfsWalk(oldTree, newTree, index, patches);
        return patches;
    }
    
    // 对两棵树进行深度优先遍历
    function dfsWalk(oldNode,newNode,index,patches){
        // 对比oldNode和newNode的不同,记录下来
        patches[index] = [...]
        diffChildren(oldNode.children, newNode.children, index, patches)
    }
    
    // 遍历子节点
    function diffChildren(oldChildren, newChildren, index, patches){
        let leftNode = null;
        let currentNodeIndex = index;
        oldChildren.forEach(function(child,i){
            let newChild = newChildren[i];
            currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识
          ? currentNodeIndex + leftNode.count + 1
          : currentNodeIndex + 1
            dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点
            leftNode = child
        });
    }
    

    例如,上面的div和新的div有差异,当前的标记是0,那么:

    patches[0] = [{difference}, {difference}, ...] // 用数组存储新旧节点的不同
    

    同理p是patches[1],ul是patches[3],类推。


    3.2.2 差异类型

    上面说的节点的差异指的是什么呢?
    对 DOM 操作可能会:

    • 替换掉原来的节点,例如把上面的div换成了section
    • 移动、删除、新增子节点,例如上面div的子节点,把p和ul顺序互换
    • 修改了节点的属性
    • 对于文本节点,文本内容可能会改变。例如修改上面的文本节点2内容为Virtual DOM 2。

    所以我们定义了几种差异类型:

    var REPLACE = 0
    var REORDER = 1
    var PROPS = 2
    var TEXT = 3
    

    对于节点替换,很简单。判断新旧节点的tagName和是不是一样的,如果不一样的说明需要替换掉。如div换成section,就记录下:

    patches[0] = [{
      type: REPALCE,
      node: newNode // el('section', props, children)
    }]
    

    如果给div新增了属性id为container,就记录下:

    patches[0] = [{
      type: REPALCE,
      node: newNode // el('section', props, children)
    }, {
      type: PROPS,
      props: {
        id: "container"
      }
    }]
    

    如果是文本节点,如上面的文本节点2,就记录下:

    patches[2] = [{
      type: TEXT,
      content: "Virtual DOM2"
    }]
    

    那如果把我div的子节点重新排序呢?例如p, ul, div的顺序换成了div, p, ul。这个该怎么对比?

    如果按照同层级进行顺序对比的话,它们都会被替换掉。如p和div的tagName不同,p会被div所替代。
    最终,三个节点都会被替换,这样DOM开销就非常大。
    而实际上是不需要替换节点,而只需要经过节点移动就可以达到,我们只需知道怎么进行移动。


    3.2.3 列表对比算法

    假设现在可以英文字母唯一地标识每一个子节点:
    旧的节点顺序:a b c d e f g h i

    现在对节点进行了删除、插入、移动的操作。新增j节点,删除e节点,移动h节点:
    新的节点顺序:a b c h d f g i j

    现在知道了新旧的顺序,求最小的插入、删除操作(移动可以看成是删除和插入操作的结合)。
    这个问题抽象出来其实是字符串的最小编辑距离问题(Edition Distance),最常见的解决算法是 Levenshtein Distance,通过动态规划求解,时间复杂度为 O(M * N)。
    但是我们并不需要真的达到最小的操作,我们只需要优化一些比较常见的移动情况,牺牲一定DOM操作,让算法时间复杂度达到线性的(O(max(M, N))。

    但是要注意的是,因为tagName是可重复的,不能用这个来进行对比。
    所以需要给子节点加上唯一标识key,列表对比的时候,使用key进行对比,这样才能复用老的 DOM 树上的节点。

    这样,我们就可以通过深度优先遍历两棵树,每层的节点进行对比,记录下每个节点的差异了。


    3.3 把差异应用到真正的DOM树上

    因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。

    所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的patches对象中找出当前遍历的节点差异,然后进行 DOM 操作。

    function patch (node, patches) {
      var walker = {index: 0}
      dfsWalk(node, walker, patches)
    }
    
    function dfsWalk (node, walker, patches) {
      var currentPatches = patches[walker.index] // 从patches拿出当前节点的差异
    
      var len = node.childNodes
        ? node.childNodes.length
        : 0
      for (var i = 0; i < len; i++) { // 深度遍历子节点
        var child = node.childNodes[i]
        walker.index++
        dfsWalk(child, walker, patches)
      }
    
      if (currentPatches) {
        applyPatches(node, currentPatches) // 对当前节点进行DOM操作
      }
    }
    

    applyPatches,根据不同类型的差异对当前节点进行 DOM 操作:

    function applyPatches (node, currentPatches) {
      currentPatches.forEach(function (currentPatch) {
        switch (currentPatch.type) {
          case REPLACE:
            node.parentNode.replaceChild(currentPatch.node.render(), node)
            break
          case REORDER:
            reorderChildren(node, currentPatch.moves)
            break
          case PROPS:
            setProps(node, currentPatch.props)
            break
          case TEXT:
            node.textContent = currentPatch.content
            break
          default:
            throw new Error('Unknown patch type ' + currentPatch.type)
        }
      })
    }
    

    4. 完整代码

    util.js

    var _ = exports
    
    _.type = function (obj) {
      return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '')
    }
    
    _.isArray = function isArray (list) {
      return _.type(list) === 'Array'
    }
    
    _.slice = function slice (arrayLike, index) {
      return Array.prototype.slice.call(arrayLike, index)
    }
    
    _.truthy = function truthy (value) {
      return !!value
    }
    
    _.isString = function isString (list) {
      return _.type(list) === 'String'
    }
    
    _.each = function each (array, fn) {
      for (var i = 0, len = array.length; i < len; i++) {
        fn(array[i], i)
      }
    }
    
    _.toArray = function toArray (listLike) {
      if (!listLike) {
        return []
      }
    
      var list = []
    
      for (var i = 0, len = listLike.length; i < len; i++) {
        list.push(listLike[i])
      }
    
      return list
    }
    
    _.setAttr = function setAttr (node, key, value) {
      switch (key) {
        case 'style':
          node.style.cssText = value
          break
        case 'value':
          var tagName = node.tagName || ''
          tagName = tagName.toLowerCase()
          if (
            tagName === 'input' || tagName === 'textarea'
          ) {
            node.value = value
          } else {
            // if it is not a input or textarea, use `setAttribute` to set
            node.setAttribute(key, value)
          }
          break
        default:
          node.setAttribute(key, value)
          break
      }
    }
    

    element.js:

    var _ = require('./util');
    
    /**
     * Virtual-dom Element.
     * @param {String} tagName
     * @param {Object} props - Element's properties,
     *                       - using object to store key-value pair
     * @param {Array<Element|String>} - This element's children elements.
     *                                - Can be Element instance or just a piece plain text.
     */
    function Element(tagName,props,children) {
        if(!(this instanceof Element)){
            if (!_.isArray(children) && children != null) {
                children = _.slice(arguments, 2).filter(_.truthy)
            }
            return new Element(tagName, props, children)
        }
    
        if (_.isArray(props)) {
            children = props
            props = {}
        }
        
        this.tagName = tagName
        this.props = props || {}
        this.children = children || []
        this.key = props
            ? props.key
            : void 666
    
        var count = 0
    
        _.each(this.children, function (child, i) {
            if (child instanceof Element) {
                count += child.count
            } else {
                children[i] = '' + child
            }
            count++
        })
        
        this.count = count
    }
    
    /**
     * Render the hold element tree.
     */
    Element.prototype.render = function () {
        var el = document.createElement(this.tagName)
        var props = this.props
    
        for (var propName in props) {
            var propValue = props[propName]
            _.setAttr(el, propName, propValue)
        }
    
        _.each(this.children, function (child) {
            var childEl = (child instanceof Element)
            ? child.render()
            : document.createTextNode(child)
            el.appendChild(childEl)
        })
    
        return el
    }
      
    module.exports = Element
    

    diff.js:

    var _ = require('./util')
    var patch = require('./patch')
    var listDiff = require('list-diff2')
    
    function diff (oldTree, newTree) {
      var index = 0
      var patches = {}
      dfsWalk(oldTree, newTree, index, patches)
      return patches
    }
    
    function dfsWalk (oldNode, newNode, index, patches) {
      var currentPatch = []
    
      // Node is removed.
      if (newNode === null) {
        // Real DOM node will be removed when perform reordering, so has no needs to do anything in here
      // TextNode content replacing
      } else if (_.isString(oldNode) && _.isString(newNode)) {
        if (newNode !== oldNode) {
          currentPatch.push({ type: patch.TEXT, content: newNode })
        }
      // Nodes are the same, diff old node's props and children
      } else if (
          oldNode.tagName === newNode.tagName &&
          oldNode.key === newNode.key
        ) {
        // Diff props
        var propsPatches = diffProps(oldNode, newNode)
        if (propsPatches) {
          currentPatch.push({ type: patch.PROPS, props: propsPatches })
        }
        // Diff children. If the node has a `ignore` property, do not diff children
        if (!isIgnoreChildren(newNode)) {
          diffChildren(
            oldNode.children,
            newNode.children,
            index,
            patches,
            currentPatch
          )
        }
      // Nodes are not the same, replace the old node with new node
      } else {
        currentPatch.push({ type: patch.REPLACE, node: newNode })
      }
    
      if (currentPatch.length) {
        patches[index] = currentPatch
      }
    }
    
    function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
      var diffs = listDiff(oldChildren, newChildren, 'key')
      newChildren = diffs.children
    
      if (diffs.moves.length) {
        var reorderPatch = { type: patch.REORDER, moves: diffs.moves }
        currentPatch.push(reorderPatch)
      }
    
      var leftNode = null
      var currentNodeIndex = index
      _.each(oldChildren, function (child, i) {
        var newChild = newChildren[i]
        currentNodeIndex = (leftNode && leftNode.count)
          ? currentNodeIndex + leftNode.count + 1
          : currentNodeIndex + 1
        dfsWalk(child, newChild, currentNodeIndex, patches)
        leftNode = child
      })
    }
    
    function diffProps (oldNode, newNode) {
      var count = 0
      var oldProps = oldNode.props
      var newProps = newNode.props
    
      var key, value
      var propsPatches = {}
    
      // Find out different properties
      for (key in oldProps) {
        value = oldProps[key]
        if (newProps[key] !== value) {
          count++
          propsPatches[key] = newProps[key]
        }
      }
    
      // Find out new property
      for (key in newProps) {
        value = newProps[key]
        if (!oldProps.hasOwnProperty(key)) {
          count++
          propsPatches[key] = newProps[key]
        }
      }
    
      // If properties all are identical
      if (count === 0) {
        return null
      }
    
      return propsPatches
    }
    
    function isIgnoreChildren (node) {
      return (node.props && node.props.hasOwnProperty('ignore'))
    }
    
    module.exports = diff
    

    patch.js:

    var _ = require('./util')
    
    var REPLACE = 0
    var REORDER = 1
    var PROPS = 2
    var TEXT = 3
    
    function patch (node, patches) {
      var walker = {index: 0}
      dfsWalk(node, walker, patches)
    }
    
    function dfsWalk (node, walker, patches) {
      var currentPatches = patches[walker.index]
    
      var len = node.childNodes
        ? node.childNodes.length
        : 0
      for (var i = 0; i < len; i++) {
        var child = node.childNodes[i]
        walker.index++
        dfsWalk(child, walker, patches)
      }
    
      if (currentPatches) {
        applyPatches(node, currentPatches)
      }
    }
    
    function applyPatches (node, currentPatches) {
      _.each(currentPatches, function (currentPatch) {
        switch (currentPatch.type) {
          case REPLACE:
            var newNode = (typeof currentPatch.node === 'string')
              ? document.createTextNode(currentPatch.node)
              : currentPatch.node.render()
            node.parentNode.replaceChild(newNode, node)
            break
          case REORDER:
            reorderChildren(node, currentPatch.moves)
            break
          case PROPS:
            setProps(node, currentPatch.props)
            break
          case TEXT:
            if (node.textContent) {
              node.textContent = currentPatch.content
            } else {
              // fuck ie
              node.nodeValue = currentPatch.content
            }
            break
          default:
            throw new Error('Unknown patch type ' + currentPatch.type)
        }
      })
    }
    
    function setProps (node, props) {
      for (var key in props) {
        if (props[key] === void 666) {
          node.removeAttribute(key)
        } else {
          var value = props[key]
          _.setAttr(node, key, value)
        }
      }
    }
    
    function reorderChildren (node, moves) {
      var staticNodeList = _.toArray(node.childNodes)
      var maps = {}
    
      _.each(staticNodeList, function (node) {
        if (node.nodeType === 1) {
          var key = node.getAttribute('key')
          if (key) {
            maps[key] = node
          }
        }
      })
    
      _.each(moves, function (move) {
        var index = move.index
        if (move.type === 0) { // remove item
          if (staticNodeList[index] === node.childNodes[index]) { // maybe have been removed for inserting
            node.removeChild(node.childNodes[index])
          }
          staticNodeList.splice(index, 1)
        } else if (move.type === 1) { // insert item
          var insertNode = maps[move.item.key]
            ? maps[move.item.key].cloneNode(true) // reuse old item
            : (typeof move.item === 'object')
                ? move.item.render()
                : document.createTextNode(move.item)
          staticNodeList.splice(index, 0, insertNode)
          node.insertBefore(insertNode, node.childNodes[index] || null)
        }
      })
    }
    
    patch.REPLACE = REPLACE
    patch.REORDER = REORDER
    patch.PROPS = PROPS
    patch.TEXT = TEXT
    
    module.exports = patch
    

    5. 结语

    Virtual DOM 算法主要是实现上面步骤的三个函数:element,diff,patch。然后就可以实际的进行使用:

    // 1. 构建虚拟DOM
    var tree = el('div', {'id': 'container'}, [
        el('h1', {style: 'color: blue'}, ['simple virtal dom']),
        el('p', ['Hello, virtual-dom']),
        el('ul', [el('li')])
    ])
    
    // 2. 通过虚拟DOM构建真正的DOM
    var root = tree.render()
    document.body.appendChild(root)
    
    // 3. 生成新的虚拟DOM
    var newTree = el('div', {'id': 'container'}, [
        el('h1', {style: 'color: red'}, ['simple virtal dom']),
        el('p', ['Hello, virtual-dom']),
        el('ul', [el('li'), el('li')])
    ])
    
    // 4. 比较两棵虚拟DOM树的不同
    var patches = diff(tree, newTree)
    
    // 5. 在真正的DOM元素上应用变更
    patch(root, patches)
    

    当然这是非常粗糙的实践,实际中还需要处理事件监听等;生成虚拟 DOM 的时候也可以加入 JSX 语法。这些事情都做了的话,就可以构造一个简单的ReactJS了。

    参考博客:Virtual DOM

    展开全文
  • 查询秘银虚拟dom以进行测试 安装 npm install mithril-query --save-dev 设置 为了在mithril 2.x中运行测试,我们需要进行一些设置。 那是模拟domril渲染和请求模块的dom。 这可以通过在“ mocha”测试中要求具有...
  • )等)提供虚拟DOM”(或其他实体)创建,扩散和修补的实现。 基本上,最终用户将创建虚拟实体树,而不是直接绘制到渲染目标,并且框架将渲染新的“真实”实体,或者对现有的渲染实体进行差异化和修补。 去做 ...
  • js代码-手写虚拟dom

    2021-07-15 00:36:31
    js代码-手写虚拟dom
  • Vue的核心是双向绑定和虚拟DOM, vdom因为是纯粹的JS对象,所以操作它会很高效,但是vdom的变更最终会转换成DOM操作,为了实现高效的DOM操作,一套高效的虚拟DOM diff算法显得很有必要。
  • 虚拟Dom实现为真实Dom结构并进行更新的集合说明,包含虚拟Dom和真实Dom的互相转换以及更新时候的比较,在react和Vue以及后续版本应用的逻辑说明。本文展示的代码仅仅是简单初级代码,关于React和Vue中的diff算法和...

    浏览器中的Dom更新

      在浏览器中渲染引擎将 node 节点添加到 另外节点中时会触发样式计算、布局、绘制、栅格化、合成等任务,这一过程称为重排。

      除了重排之外,还有可能引起重绘或者合成操作,也就是“牵一发而动全身”。

      另外,对于 DOM 的不当操作还有可能引发强制同步布局和布局抖动的问题,这些操作都会大大降低渲染效率。

      因此,对于 DOM 的操作时刻都需要非常小心谨慎。

      对于一些复杂的页面或者目前使用非常多的单页应用来说,其 DOM 结构是非常复杂的,而且还需要不断地去修改 DOM 树,每次操作 DOM 渲染引擎都需要进行重排、重绘或者合成等操作,因为 DOM 结构复杂,所生成的页面结构也会很复杂,对于这些复杂的页面,执行一次重排或者重绘操作都是非常耗时的,这就给浏览器带来了真正的性能问题。

      这解决办法就是虚拟Dom。

    加入虚拟dom之后的浏览器更新

    1. 将页面改变的内容应用到虚拟 DOM 上,而不是直接应用到 DOM 上;
    2. 变化被应用到虚拟 DOM 上时,虚拟 DOM 并不急着去渲染页面,而仅仅是调整虚拟 DOM 的内部状态,这样操作虚拟 DOM 的代价就变得非常轻了。
    3. 在虚拟 DOM 收集到足够的改变时,再把这些变化一次性应用到真实的 DOM 上。

      如下图:

    1、创建阶段

      首先依据 JSX 和基础数据创建出来虚拟 DOM(并缓存起来),它反映了初始的真实的 DOM 树的结构。

      然后由虚拟 DOM 树创建出真实 DOM 树,真实的 DOM 树生成完后,再触发渲染流水线往屏幕输出页面。

    2、更新阶段

      如果数据发生了改变,那么就需要根据新的数据创建一个新的虚拟 DOM 树;

      然后比较(原虚拟Dom树和新虚拟Dom树,此时用到了Diff算法)两个树,找出变化的地方,并把变化的地方一次性更新到真实的 DOM 树上;

      最后渲染引擎更新渲染流水线,并生成新的页面。

    框架中的Dom更新

    如上图,可以把虚拟 DOM 看成是 MVC 的视图部分,其控制器和模型可以是redux也可以是Vuex。其具体实现过程如下:

    1. 图中的控制器是用来监控 DOM 的变化,一旦 DOM 发生变化,控制器便会通知模型,让其更新数据;
    2. 模型数据更新好之后,控制器会通知视图,通知其模型的数据发生了变化;
    3. 视图接收到更新消息之后,会根据模型所提供的数据来生成新的虚拟 DOM;
    4. 新的虚拟 DOM 生成好之后,就需要与之前的虚拟 DOM 进行比较,找出变化的节点(使用了Diff算法);
    5. 比较出变化的节点之后,React 将变化的虚拟节点应用到 DOM 上,这样就会触发 DOM 节点的更新;
    6. DOM 节点的变化又会触发后续一系列渲染流水线的变化,从而实现页面的更新。

    真实Dom转化为虚拟Dom

    具体实现思路:

    // 将真实DOM转化为虚拟DOM
    // <div />  => {tag:'div'}   元素转化
    // 文本节点 => {tag:undefined,value:'文本节点'}   文本节点转化
    // <div title="1" class="c"  />  => { tag:'div',data:{ title:'1',class:"c" } }   多属性转化
    // <div><div /></div> => {tag:'div',children:[{ tag:'div' }]} 
    // 用构造函数来 进行以上转换

            其中进行元素节点和文本节点以及多属性节点的区分是利用了node节点的nodeType属性:

    • 如果节点是一个元素节点,nodeType 属性返回 1。
    • 如果节点是属性节点, nodeType 属性返回 2。
    • 如果节点是一个文本节点,nodeType 属性返回 3。
    • 如果节点是一个注释节点,nodeType 属性返回 8。

            该属性是只读的。

            大概实现代码:

    class VNode {
        // 构造函数
        constructor( tag,data,value,type ){
            // tag:用来表述 标签  
            // data:用来描述属性  
            // value:用来描述文本 
            // type:用来描述类型
            this.tag = tag && tag.toLowerCase();//文本节点时 tagName是undefined
            this.data = data;
            this.value = value;
            this.type = type;
            this.children = [];
    
        }
        appendChild(vnode){
            this.children.push(vnode);
        }
    }
    /**
        利用递归 来遍历DOM元素 生成虚拟DOM
        Vue中的源码使用栈结构,使用栈存储父元素来实现递归生成
    */
    function getVNode(node){
        let nodeType = node.nodeType;
        let _vnode = null;
        if(nodeType === 1){
            // 元素
            let nodeName = node.nodeName;//元素名,什么标签?
            let attrs = node.attributes;//属性伪数组元素上的属性
            let _attrObj = {};
        
            //attrs[i] 属性节点(nodeType == 2) 是对象                    
            for(let i=0;i<attrs.length;i++){
                 //attrs[i].nodeName:属性名 attrs[i].nodeValue:属性值
                _attrObj[attrs[i].nodeName] = attrs[i].nodeValue;               
            }
            //标签名(DIV UI LI...)、所有属性对象、value值(只有文本标签有)、type类型(是元素还是文本)
            _vnode = new VNode( nodeName,_attrObj,undefined,nodeType);
            // 考虑node的子元素
            let childNodes = node.childNodes;
            for(let i = 0;i<childNodes.length;i++){
                _vnode.appendChild(getVNode(childNodes[i]));//递归
            }
        }else if(nodeType === 3){
            // 文本节点
           _vnode = new VNode(undefined,undefined,node.nodeValue,nodeType);//无标签名、无属性、有value、有type
        }
        return _vnode;
    }
    
     // 读取根节点下dom数据,或者改成指定dom下数据
    let root = document.querySelector("#root");
    
    let vroot = getVNode ( root );//虚拟DOM
    console.log(vroot);

    虚拟Dom转化为真实Dom

            无论是什么类型的节点,只有三种类型的节点会被创建并插入到的Dom中:元素节点、注释节点、和文本节点:

    // {tag:'div'} => <div />   元素转化
    // {tag:undefined,value:'文本节点'} => 文本节点    文本节点转化
    // { tag:'div',data:{ title:'1',class:"c" } } => <div title="1" class="c"  />     多属性转化
    // {tag:'div',children:[{ tag:'div' }]} => <div><div /></div>

    具体实现思路:

            vnode数据类型:

    vnode = {
        tag:'div', // 以div为例
        // attrs中包含插在节点上的属性,就比如类或者样式,内联样式等
        attrs:{
            class:'a'            
        }
        children:[], // children之中可能是另外的vnode
    }
    • 将字符串转化为文本节点;
    • 将数字转化为字符串再转化为文本节点;
    • 将多属性节点转换为文本节点,子节点再延续上面的过程;

            大概实现代码如下:

    //Virtual DOM => DOM
    function render(vnode, container) {
      container.appendChild(_render(vnode));
    }
    
    function _render(vnode) {
      // 如果是数字类型转化为字符串,然后转到生成文本节点
      if (typeof vnode === 'number') {
        vnode = String(vnode);
      }
      // 字符串类型直接就是文本节点
      if (typeof vnode === 'string') {
        return document.createTextNode(vnode);
      }
      // 普通DOM
      const dom = document.createElement(vnode.tag);
      if (vnode.attrs) {
        // 遍历属性
        Object.keys(vnode.attrs).forEach(key => {
          const value = vnode.attrs[key];
          dom.setAttribute(key, value);
        })
      }
      // 子数组进行递归操作
      vnode.children.forEach(child => render(child, dom));
      return dom;
    }

    使用到的document方法解析:

            1.DOM appendChild() :

            appendChild() 方法向节点添加最后一个子节点。

            也可以使用 appendChild() 方法从一个元素向另一个元素中移动元素。

            2.DOM createElement():

            createElement() 方法通过指定名称创建一个元素:

            3.DOM setAttribute():

            setAttribute() 方法添加指定的属性,并为其赋指定的值。

            如果这个指定的属性已存在,则仅设置/更改值。

    DOM-Diff算法

            DOM-Diff就是一种比较算法,比较两个虚拟DOM的区别,也就是比较两个对象的区别。

            也有真实DOM与虚拟DOM的比较,不过这里先只讨论虚拟DOM之间的比较。

    DOM-Diff(传统)算法

            处理方案: 循环递归每一个节点,如下图:

            左侧树a节点依次进行如下对比:

            a->e、a->d、a->b、a->c、a->a

            之后左侧树其它节点b、c、d、e亦是与右侧树每个节点对比, 算法复杂度能达到O(n^2)

            查找完差异后还需计算最小转换方式,最终达到的算法复杂度是O(n^3)。

            将两颗树中所有的节点一一对比需要O(n²)的复杂度,在对比过程中发现旧节点在新的树中未找到,那么就需要把旧节点删除,删除一棵树的一个节点(找到一个合适的节点放到被删除的位置)的时间复杂度为O(n),同理添加新节点的复杂度也是O(n),合起来diff两个树的复杂度就是O(n³)

    DOM-Diff(优化后)算法

            vue和react的虚拟DOM的diff算法大致相同,其核心是基于两个简单的假设:

    1. 两个相同的组件产生类似的DOM结构,不同的组件产生不同的DOM结构
    2. 同一层级的一组节点,他们可以通过唯一的id进行区分

            考虑到很少进行跨层移动,所以用平层对比,时间复杂度从O(n^3)缩短为O(n),在对比过程中直接对真实dom更新。变更一般有三种: 文本 ,节点属性,节点变更,增删节点(绑定key值的作用)

    DOM-Diff三种优化策略

    • web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。
    • 拥有相同类型的两个组件将会生成相似的树形结构,拥有不同类型的两个组件将会生成不同树形结构。
    • 对于同一层级的一组自节点,他们可以通过唯一id进行区分。

            也就是:

            比较只会在同层级进行, 不会跨层级比较。

            具体表现如下:

    • 只比较平级

    • 不会跨级比较

    • 同一级的变化节点,如果节点相同只是位置交换,则会复用。(通过key来实现)

    React优化Diff算法

            基于以上优化的diff三点策略,react分别进行以下算法优化

    • tree diff
    • component diff
    • element diff

    tree diff

            react对树的算法进行了分层比较。react 通过 updateDepth对Virtual Dom树进行层级控制,只会对相同颜色框内的节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在,则该节点和其子节点都会被删除。这样是需要遍历一次dom树,就完成了整个dom树的对比

            分层比较 :

            如果是跨层级的移动操作,如图

            当根结点发现A消失了,会删除掉A以及他的子节点。当发现D上多了一个A节点,会创建A(包括其子节点)节点作为子节点

            所以:当进行跨层级的移动操作,react并不是简单的进行移动,而是进行了删除和创建的操作,这会影响到react性能。所以要尽量避免跨层级的操作。(例如:控制display来达到显示和隐藏,而不是真的添加和删除dom。

    component diff

    • 如果是组件类相同(class)的组件,则直接对比virtual Dom tree
    • 如果组件类不同(结构相似)的组件,则判断为 dirty component(脏组件),整个替换掉组件以及组件下的所有子组件
    • 如果组件类相同,但是可能virtual DOM 没有变化,这种情况下我们可以使用shouldComponentUpdate() 来判断是否需要进行diff

            如果组件D和组件G,如果组件类不同,但是结构类似。这种情况下,因为组件类不同,所以react会删除D,创建G。所以我们可以使用shouldComponentUpdate()返回false不进行diff。

            针对react15, 16出了新的生命周期

            所以:component diff 主要是使用shouldComponentUpdate() 来进行优化

    element diff

            element diff 涉及三种操作:

    • 插入
    • 移动
    • 删除

            不使用key的情况:

            不使用key的话,react对新老集合对比,发现新集合中B不等于老集合中的A,于是删除了A,创建了B,依此类推直到删除了老集合中的D,创建了C于新集合。

    这样会产生渲染性能瓶颈,于是react允许添加key进行区分。

            使用key的情况:

            react首先对新集合进行遍历,通过唯一key来判断老集合中是否存在相同的节点,如果没有的话创建,如果有的话进行移动操作

    移动优化

            在移动前,会将节点在新集合中的位置(_mountIndex)和在老集合中位置(lastIndex)进行比较,如果if (child._mountIndex < lastIndex) 进行移动操作,否则不进行移动操作。这是一种顺序移动优化。只有在 新集合的位置 小于 在老集合中的位置 才进行移动。

            如果遍历的过程中,发现在新集合中没有,但是在老集合中的节点,会进行删除操作

            所以:element diff 通过唯一key 进行diff 优化。

    总结:

    • react中尽量减少跨层级的操作。
    • 可以使用shouldComponentUpdate() 来避免react重复渲染。
    • 尽量添加唯一key,以减少不必要的重渲染

    Vue2.x优化Diff

            vue2.0加入了virtual dom,和react拥有相同的 diff 优化原则

            差异就在于, diff的过程就是调用patch函数,就像打补丁一样修改真实dom。也就是通过js层面的计算,根据两个虚拟对象创建出差异的补丁对象patch,用来描述改变了哪些内容,然后用特定的操作解析patch对象,更新dom完成页面的重新渲染。

            使用的主要方法:

    • patchVnode
    • updateChildren

            updateChildren是vue diff的核心

            过程可以概括为:oldCh和newCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和newCh至少有一个已经遍历完了,就会结束比较。

            Vue2的核心Diff算法采用了双端比较的算法,同时从新旧children的两端开始进行比较,借助key值找到可复用的节点,再进行相关操作。相比React的Diff算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅。

    Vue 3.x

            Vue3.x借鉴了 ivi算法和 inferno算法。在创建VNode时就确定其类型,以及在mount/patch的过程中采用位运算来判断一个VNode的类型,在这个基础之上再配合核心的Diff算法,使得性能上较Vue2.x有了提升。

            该算法中还运用了动态规划的思想求解最长递归子序列。

    参考及复制文章链接

            引用不分前后

    展开全文
  • vue核心之虚拟DOM(vdom)

    2022-02-23 10:16:04
    一、真实DOM和其解析流程? 浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting 第一步,用HTML分析器,分析HTML元素,构建一颗DOM树...

    一、真实DOM和其解析流程?

        浏览器渲染引擎工作流程都差不多,大致分为5步,创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting

        第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。

        第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。

        第三步,将DOM树和样式表,关联起来,构建一颗Render树(这一过程又称为Attachment)。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。

        第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。

        第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。 

        DOM树的构建是文档加载完成开始的?构建DOM数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render数和布局。

        Render树是DOM树和CSSOM树构建完毕才开始构建的吗?这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。

        CSS的解析是从右往左逆向解析的(从DOM树的下-上解析比上-下解析效率高),嵌套标签越多,解析越慢。

    webkit渲染引擎工作流程:

    二、JS操作真实DOM的代价!

            用我们传统的开发模式,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。即使计算机硬件一直在迭代更新,操作DOM的代价仍旧是昂贵的,频繁操作还是会出现页面卡顿,影响用户体验。

    三、为什么需要虚拟DOM,它有什么好处?

            Web界面由DOM树(树的意思是数据结构)来构建,当其中一部分发生变化时,其实就是对应某个DOM节点发生了变化,

            虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。

    模版转换成视图的过程

      在正式介绍Virtual DOM之前,我们有必要先了解下模版转换成视图的整个过程(如下图):

    • Vue.js通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个虚拟DOM
    • 在对模型进行操作的时候,会触发对应的Dep中的Watcher对象。Watcher对象会调用对应的update来修改视图。这个过程主要是将新旧虚拟DOM进行差异对比,然后根据结果进行对比。
      简单点讲,在Vue的实现上,Vue模版编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应用到DOM操作上。

    • 我们先对上图几个概念嵌入解释:

    • 渲染函数:渲染函数是用来生成虚拟DOM的。Vue推荐使用模版来构建我们的应用界面,在实现中Vue布局模版编译成渲染函数,当然我们也可以不写模版,直接写渲染函数,这样子更接近编译后的模版。
    • vnode虚拟节点:它可以代表一个真实的DOM节点通过createElement方法能将vnode渲染成DOM节点,简单地说,虚拟节点可以理解成节点描述对象,它描述了应该怎样去创建真实的DOM节点。
    • patch(也称为patching算法):虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。这点我们从单词含义就可以看出, patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

    Virtual DOM 是什么?

      Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。

      简单来说,可以把Virtual DOM 理解为一个简单的JS对象,并且最少包含标签名( tag)、属性(attrs)和子元素对象( children)三个属性。不同的框架对这三个属性的命名会有点差别。

    对于虚拟DOM,咱们来看一个简单的实例,就是下图所示的这个,详细的阐述了模板 → 渲染函数 → 虚拟DOM树 → 真实DOM的一个过程

    Virtual DOM的优势

    • 具备跨平台的优势
      由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。

    • 操作 DOM 慢,js运行效率高。我们可以将DOM对比操作放在JS层,提高效率。
      因为DOM操作的执行速度远不如Javascript的运算速度快,因此,把大量的DOM操作搬运到Javascript中,运用patching算法来计算出真正需要更新的节点,最大限度地减少DOM操作,从而显著提高性能。

    Virtual DOM 本质上就是在 JS 和 DOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JS 和 DOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)

    • 提升渲染性能
      Virtual DOM的优势不在于单次的操作,而是在大量、频繁的数据更新下,能够对视图进行合理、高效的更新。

    四、实现虚拟DOM

            例如一个真实的DOM节点。


            我们用JS来模拟DOM节点实现虚拟DOM。

     

            其中的Element方法具体怎么实现的呢?

     

            第一个参数是节点名(如div),第二个参数是节点的属性(如class),第三个参数是子节点(如ul的li)。除了这三个参数会被保存在对象上外,还保存了key和count。其相当于形成了虚拟DOM树。



            有了JS对象后,最终还需要将其映射成真实DOM

     

            我们已经完成了创建虚拟DOM并将其映射成真实DOM,这样所有的更新都可以先反应到虚拟DOM上,如何反应?需要用到Diff算法

            两棵树如果完全比较时间复杂度是O(n^3),但参照《深入浅出React和Redux》一书中的介绍,React的Diff算法的时间复杂度是O(n)。要实现这么低的时间复杂度,意味着只能平层的比较两棵树的节点,放弃了深度遍历。这样做,似乎牺牲掉了一定的精确性来换取速度,但考虑到现实中前端页面通常也不会跨层移动DOM元素,这样做是最优的。

            深度优先遍历,记录差异

            。。。。

            Diff操作

            在实际代码中,会对新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。

            下面我们创建一棵新树,用于和之前的树进行比较,来看看Diff算法是怎么操作的。

    diff 算法包括几个步骤:

    • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
    • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
    • 把所记录的差异应用到所构建的真正的DOM树上,视图就更新了

    old Tree :

    new Tree:

            平层Diff,只有以下4种情况:

            1、节点类型变了,例如下图中的P变成了H3。我们将这个过程称之为REPLACE。直接将旧节点卸载并装载新节点。旧节点包括下面的子节点都将被卸载,如果新节点和旧节点仅仅是类型不同,但下面的所有子节点都一样时,这样做效率不高。但为了避免O(n^3)的时间复杂度,这样是值得的。这也提醒了开发者,应该避免无谓的节点类型的变化,例如运行时将div变成p没有意义。

            2、节点类型一样,仅仅属性或属性值变了。我们将这个过程称之为PROPS。此时不会触发节点卸载和装载,而是节点更新。

    查找不同属性方法 :

            3、文本变了,文本对也是一个Text Node,也比较简单,直接修改文字内容就行了,我们将这个过程称之为TEXT

            4、移动/增加/删除 子节点,我们将这个过程称之为REORDER。看一个例子,在A、B、C、D、E五个节点的B和C中的BC两个节点中间加入一个F节点。

            我们简单粗暴的做法是遍历每一个新虚拟DOM的节点,与旧虚拟DOM对比相应节点对比,在旧DOM中是否存在,不同就卸载原来的按上新的。这样会对F后边每一个节点进行操作。卸载C,装载F,卸载D,装载C,卸载E,装载D,装载E。效率太低。

            如果我们在JSX里为数组或枚举型元素增加上key后,它能够根据key,直接找到具体位置进行操作,效率比较高。常见的最小编辑距离问题,可以用Levenshtein Distance算法来实现,时间复杂度是O(M*N),但通常我们只要一些简单的移动就能满足需要,降低精确性,将时间复杂度降低到O(max(M,N))即可。

    最终Diff出来的结果:

    映射成真实DOM

            虚拟DOM有了,Diff也有了,现在就可以将Diff应用到真实DOM上了。深度遍历DOM将Diff的内容更新进去。

    根据Diff更新DOM:

    根据Diff更新DOM :

     

    我们会有两个虚拟DOM(js对象,new/old进行比较diff),用户交互我们操作数据变化new虚拟DOM,old虚拟DOM会映射成实际DOM(js对象生成的DOM文档)通过DOM fragment操作给浏览器渲染。当修改new虚拟DOM,会把newDOM和oldDOM通过diff算法比较,得出diff结果数据表(用4种变换情况表示)。再把diff结果表通过DOM fragment更新到浏览器DOM中。

    虚拟DOM的存在的意义?vdom 的真正意义是为了实现跨平台,服务端渲染,以及提供一个性能还算不错 Dom 更新策略。vdom 让整个 mvvm 框架灵活了起来

    Diff算法只是为了虚拟DOM比较替换效率更高,通过Diff算法得到diff算法结果数据表(需要进行哪些操作记录表)。原本要操作的DOM在vue这边还是要操作的,只不过用到了js的DOM fragment来操作dom(统一计算出所有变化后统一更新一次DOM)进行浏览器DOM一次性更新。其实DOM fragment我们不用平时发开也能用,但是这样程序员写业务代码就用把DOM操作放到fragment里,这就是框架的价值,程序员才能专注于写业务代码

    总结:Vue.js通过编译将模版转换成渲染函数(render),执行渲染函数就可以得到一个虚拟节点树(虚拟DOM),虚拟节点树(虚拟DOM)提供虚拟节点vnode和对新旧两个vnode进行比对并根据比对结果进行DOM操作来更新视图,达到减少对DOM的目的,从而减少浏览器的开销,提高渲染速度,改善用户体验。

    展开全文
  • 虚拟DOM-diff算法

    2022-04-21 11:31:21
    本次掌握: 虚拟DOM如何被渲染函数(h函数)产生? 要手写h函数 ...diff算法是发生在虚拟DOM上的,新虚拟DOM和老虚拟DOM进行diff(精细化比较),算出应如何最小量更新,最后反映到真正的DOM 真实DOM如何编.
  • 文章目录一、什么是虚拟DOM二、为什么需要虚拟DOM三、如何实现虚拟DOM小结参考文献 一、什么是虚拟DOM 虚拟 DOM (Virtual DOM )这个概念相信大家都不陌生,从 React 到 Vue ,虚拟 DOM 为这两个框架都带来了跨...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 73,053
精华内容 29,221
关键字:

虚拟dom

友情链接: AAFont.rar