虚拟Dom

115次阅读

共计 4155 个字符,预计需要花费 11 分钟才能阅读完成。

Virtual Dom

  • vdom 是 vue 和 react 的核心
  • vdom 是什么东西,有什么用,为什么会存在 vdom?
  • vdom 如何应用,核心 API 是什么?
  • diff 算法

## 什么是 vdom ##

  • 用 js 模拟 DOM 结构
  • DOM 变化的对比,放在 JS 层来做
  • 提高重绘性能
 <ul id="list">
   <li class="item">Item 1</li>
   <li class="item">Item 2</li>
</ul>

用 js 来模拟

 {
   tag:"ul",
   attrs:{id:"list"},
   children:[
    {
       tag:"li",
       attrs:{className: "item"},  //class 是 js 的保留字,所以用 className
       children:['Item 1']  
    },{
       tag:"li",
       attrs:{className: "item"},
       children:['Item 2']  
      }
    ]
 }

设计一个需求场景,渲染一个数组成表格

 //Jquery 的实现
 <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script type="text/javascript" src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>
        <script type="text/javascript">
           var dataList = [
               {
                   name:'111',
                   age:1
               },{
                   name:'222',
                   age:2
               },{
                   name:'333',
                   age:3
               },{
                   name:'444',
                   age:4
               },
           ]
           $(document).ready(function () {function render(data){var $container = $('#container')
               $container.html('')
               // 拼接 tabel
               $table = $('<table>')
               $table.append($('<tr><td>name</td><td>age</td></tr>'))
               // 渲染到页面
               data.forEach(item => {$table.append($(`<tr><td>${item.name}</td><td>${item.age}</td></tr>`))
               });
               $container.append($table)
           }  
            render(dataList)
            $("#btn-change").click(function(){dataList[1].age=30  // 每次修改数据都会清空 dom,然后重绘表格
               render(dataList)
            })
           })
        
        </script>
    </head>
    <body>
        <div id="container"></div>
        <button id="btn-change"> 修改数据 </button>
    </body>
    </html>
    
    
   


上述办法遇到的问题

    • js 原生或者是 Jquery 框架时代,都是直接操作 DOM 节点来进行渲染页面,可是这样的代价确实是很大,需要将原本的 DOM 全部清除,然后在重新渲染一遍
    • 操作 Dom 非常昂贵。每个 Dom 自带了太多的属性。js 运行效率高
    • 尽量减少 Dom 操作
    • 项目越复杂,运行效率越低,影响越严重
    • vdom 可以解决这个问题,将 Dom 操作方在 Js 层,提高效率

    vdom 如何应用,核心 API

    • snabbdom
      为什么是 snabbdom.js
      由于虚拟 dom 有那么多的好处而且现代前端框架中 react 和 vue 均不同程度的使用了虚拟 dom 的技术,因此通过一个简单的 库赖学习虚拟 dom 技术就十分必要了,至于为什么会选择 snabbdom.js 这个库呢?原因主要有两个:

      源码简短,总体代码行数不超过 500 行。
      著名的 vue 的虚拟 dom 实现也是参考了 snabbdom.js 的实现。

      • 用 snabbdomjs 实现上述例子
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <meta http-equiv="X-UA-Compatible" content="ie=edge">
        <title>Document</title>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom.js"></script>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-class.js"></script>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-props.js"></script>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-style.js"></script>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/snabbdom-eventlisteners.js"></script>
        <script src="https://cdn.bootcss.com/snabbdom/0.7.1/h.min.js"></script>
    
    </head>
    <body>
        <div id="container"></div>
        <button id="btn-change"> 修改数据 </button>
      
        <script type="text/javascript">
            var snabbdom = window.snabbdom
     
            // 定义 patch
            var patch = snabbdom.init([
               snabbdom_class,
               snabbdom_props,
               snabbdom_style,
               snabbdom_eventlisteners
            ])
     
            var h = snabbdom.h
            var container = document.getElementById("container")
            // 生成 vnode
            var vnode= h('ul#list',{},[h('li.item',{},'Item 1'),
                h('li.item',{},'Item 2')
            ])
     
            patch(container,vnode)
     
            document.getElementById("btn-change").addEventListener('click',function(){console.log("111")
                var newVnode =  h('ul#list',{},[h('li.item',{},'Item 1'),
                h('li.item',{},'Item B'),
                h('li.item',{},'Item 3')
                ])
    
               patch(vnode,newVnode)
            })
         </script>
    </body>
    </html>
    

    // 修改数据只是修改了 第二个 item 第三,第一个数据没变化(F12 查看 Element 第一个 item 没有闪烁)

    diff 算法

    • 什么是 diff 算法
    • 去繁就简
    • vdom 为何用 diff 算法
    • diff 算法的实现流程

    diff 命令是 linux 系统自带的基础命令
    git diff 判断文本文件哪里被修改了
    diff 算法一直都在,并不是因为 react、vue 才出现的

    vdom 为何使用 diff 算法

    • DOM 操作是昂贵的,因此尽量减少 DOM 操作
    • 找出本次 DOM 必须更新的节点来更新,其他的不更新
    • 这个找出的过程,就需要 diff 算法

    diff 实现过程

    只需要明白

    • path(container,vnode)
    • path(vnode,newnode)

    通过 VNode 创建一个真实的 DOM 的流程

      function createElement(vnode){
       var tag= vnode.tag
       var attrs = vnode.attrs||{}
       var children = vnode.children || []
       if(!tag){return null}
       var elem = document.createElement(tag)
        var attrName 
        for(attrName in attrs){if(attrs.hasOwnProperty(attrName)){elem.setAttribute(attrName,attrs[attrName])
          }
        }
    
        children.forEach(childNode => {elem.appendChild(createElement(childNode))
        });
        // 返回真实的 Dom
        return elem
    }
    
    

    path(vnode,newVnode) 的实现,

    function updateChildren(vnode,newVnode){var children = vnode.children || []
         var newChildren = newVnode.children || []
        
         // 遍历现有的 children
         children.forEach((child,index)=> {var newChild = newChildren[index]
             if(newChild == null){return}
    
             if(child.tag === newChild.tag){updateChildren(child,newChild)
             }else{replaceNode(child,newChild)
             }
         });
    }
    
    function replaceNode(vnode,newVnode){
        var elem = vnode.elem
        var newElem = createElement(newVnode)
    }
    

    不仅仅是以上的内容,还有以下的内容

    • 节点新增和删除
    • 节点重新排序
    • 节点属性、样式、事件绑定

    正文完
     0

    虚拟DOM

    116次阅读

    共计 4869 个字符,预计需要花费 13 分钟才能阅读完成。

    虚拟 DOM
    可以看看这个文章如何理解虚拟 DOM?– 戴嘉华的回答 – 知乎
    https://www.zhihu.com/questio…
    深度剖析:如何实现一个 Virtual DOM 算法 #13
    是什么
    什么是 DOM?
    DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。DOM 就是将网页转化为一个对象并提供操作这个对象接口 (即操作这个对象的方法),所以可以通过 DOM 对网页中的元素进行操作。如对某个节点增加属性,增加孩子,删除等。DOM 就是网页里你看得见的对应的某个元素。
    什么是虚拟 DOM?
    举例说明:如果网页中有一个表格,表头是姓名,年级,分数。如果我希望点击姓名表格就按照字典序排序,点击年级,按照年级从大到小排序等等操作,那么如果直接去操作 DOM 的话就很难实现。例如,我们删除了一个 DOM 结点,或者新增了一条数据,那么重新进行排序,就会删除所有 DOM 然后重新渲染一遍 DOM。如果数据很多的话,就会很浪费资源,影响网页的性能,可能会卡顿。
    为什么会卡顿呢?是因为一个节点元素实际上包含很多属性方法,创建一个 DOM 就包含上百条数据,加载上绑定的事件等。性能开销很大。我可以根据 DOM 结构,然后自己创建一个数据结构,自己创建的这个 DOM 和真实的 DOM 是一一映射的。然后我们操作的时候就操作自己的数据结构,数据量很小,不管进行排序或其他处理都会很迅速。处理好之后,再根据这个数据结构把它变为真实的 DOM。即我们用虚拟的 DOM 结构替换需要处理的 DOM 结构,对虚拟的 DOM 进行操作之后再进行渲染,就成为了真实的数据。
    有什么用
    这样的好处是如果我们需要对 DOM 结点进行改变,那么我们只需要查看我们自己创建的虚拟 DOM,看看其中哪条数据发生了改变,然后修改虚拟 DOM,并把它渲染成真实的数据即可。例如我们本来就有 500 条数据,然后需要添加 10 条,那么我们只添加 10 条新的虚拟 DOM,然后再把这 10 条虚拟 DOM 转化为真实的 DOM 即可,不需要从新吧 510 跳全部重新渲染一遍。这样性能会提升。
    所谓的虚拟 DOM 实际上就是我们根据真实的 DOM 结构,创建一个和真实 DOM 映射的一个数据结构,然后对数据结构进行操作,最后把这个数据结构反映到真实的 DOM 中。
    我们可以在逻辑上把这个数据结构渲染成真实的 DOM,他在数据结构上和真实 DOM 是差不多的
    举个例子:我们可以使用一个数据结构来映射 DOM(用 JS 对象模拟 DOM 树):我们将节点用一个对象来表示,tag 属性表示他的种类,children 属性表示他拥有的儿子数组。那么:
    这就是虚拟的 DOM,体积很轻量,没有所有的属性和接口! 用来操作的时候不需要耗费很高的性能。代码如下:
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset=”utf-8″>
    <title>JS Bin</title>
    </head>
    <body>
    <!– <div>
    <p><span>xiedaimala.com</span></p>
    <span>jirengu.coms</span>
    </div> –>
    </body>
    </html>
    let nodesData = {
    tag: ‘div’,
    children: [
    {
    tag: ‘p’,
    children: [
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘xiedaimala.com’
    }
    ]
    }
    ]
    },
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘jirengu.com’
    }
    ]
    }
    ]
    }

    接下来我们只需要将这个虚拟的 DOM 渲染成真实的 DOM 就可以了,例如写一个函数来渲染 DOM。
    function createElement (data){

    }
    举例说明虚拟 DOM 的作用:这时我们修改了 DOM,例如我们将 div 中的 p 标签中 span 标签的内容由 xiedaimala.com 修改为 baidu.com,那么我们只需要修改我们创建的数据结构中的 span 标签 text 那个属性,然后将原来内存中的 nodesData 与修改后的 nodesData2 进行比较。例如:
    let nodesData2 = {
    tag: ‘div’,
    children: [
    {
    tag: ‘p’,
    children: [
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘baidu.com’// 这里变了
    }
    ]
    }
    ]
    },
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘jirengu.com’
    }
    ]
    }
    ]
    }

    发现 span 标签的 text 内容改变了,那么我们在修改真实 DOM 的时候不需要把所有的真实 DOM 的很多属性和方法都检索一遍,然后重新渲染一遍,而只需要重新渲染在虚拟 DOM 中比较出来的修改的部分,即只需要重新渲染 text 部分就可以了。
    以下为深度剖析:如何实现一个 Virtual DOM 算法 #13 文章中的一段解释
    既然原来 DOM 树的信息都可以用 JavaScript 对象来表示,反过来,你就可以根据这个用 JavaScript 对象表示的树结构来构建一棵真正的 DOM 树。之前的章节所说的,状态变更 -> 重新渲染整个视图的方式可以稍微修改一下:用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。当然这样做其实没什么卵用,因为真正的页面其实没有改变。
    但是可以用新渲染的对象树去和旧的树进行对比,记录这两棵树差异。记录下来的不同就是我们需要对页面真正的 DOM 操作,然后把它们应用在真正的 DOM 树上,页面就变更了。这样就可以做到:视图的结构确实是整个全新渲染了,但是最后操作 DOM 的时候确实只变更有不同的地方。

    如何实现
    简单实现:
    虚拟 DOM 渲染为真实 DOM

    /**
    * @author ruoyu
    * @description 虚拟 DOM Demo
    * @todo 暂时不考虑复杂情况
    */

    class VNode {
    constructor(tag, children, text) {
    this.tag = tag
    this.text = text
    this.children = children
    }

    render() {
    if(this.tag === ‘#text’) {
    return document.createTextNode(this.text)
    }
    let el = document.createElement(this.tag)
    this.children.forEach(vChild => {
    el.appendChild(vChild.render())
    })
    return el
    }
    }

    /* 以上为 ES6 写法, 改为 ES5 写法为:*/
    /*******
    function VNode() {
    this.tag = tag
    this.text = text
    this.children = children
    }
    VNode.prototype.render = function() {
    if(this.tag === ‘#text’) {
    return document.createTextNode(this.text)
    }
    let el = document.createElement(this.tag)
    this.children.forEach(vChild => {
    el.appendChild(vChild.render())// 递归生成子节点
    })
    return el
    }
    ******
    这几句代码的作用是将 js 对象表示的虚拟 DOM 渲染为真实的 DOM
    */

    /* 这个函数的作用是传入几个参数, 然后返回对象 */
    function v(tag, children, text) {
    if(typeof children === ‘string’) {
    text = children
    children = []
    }
    return new VNode(tag, children, text)
    }

    /* 这里是 js 对象虚拟 dom 的数据结构

    let nodesData = {
    tag: ‘div’,
    children: [
    {
    tag: ‘p’,
    children: [
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘xiedaimala.com’
    }
    ]
    }
    ]
    },
    {
    tag: ‘span’,
    children: [
    {
    tag: ‘#text’,
    text: ‘jirengu.com’
    }
    ]
    }
    ]
    }

    */

    /* 使用 v 函数将几个参数转化为对象并返回 */
    let vNodes = v(‘div’, [
    v(‘p’, [
    v(‘span’, [ v(‘#text’, ‘xiedaimala.com’) ] )
    ]
    ),
    v(‘span’, [
    v(‘#text’, ‘jirengu.com’)
    ])
    ]
    )
    /* 渲染为真实的 DOM*/
    console.log(vNodes) /* 下方有打印的结果 */
    console.log(vNodes.render())

    我们看一下打印的结果

    DOM 数据更新
    以下仅为简单实现,是为了理解原理,实际上要想做到很完美的虚拟 DOM,需要考虑很多
    function patchElement(parent, newVNode, oldVNode, index = 0) {
    if(!oldVNode) {// 如果没有, 直接创建新的 DOM, 例如 patchElement(root, vNodes1)
    parent.appendChild(newVNode.render())
    } else if(!newVNode) {// 删除 DOM 的操作, 例如 patchElement(root)
    parent.removeChild(parent.childNodes[index])
    } else if(newVNode.tag !== oldVNode.tag || newVNode.text !== oldVNode.text)// 替换 (修改)DOM 操作, 例如两个 VNode 比较简单, 然后互相比较
    {
    parent.replaceChild(newVNode.render(), parent.childNodes[index])
    } else {// 递归替换孩子 DOM, 递归比较
    for(let i = 0; i < newVNode.children.length || i < oldVNode.children.length; i++) {
    patchElement(parent.childNodes[index], newVNode.children[i], oldVNode.children[i], i)
    }
    }
    }

    let vNodes1 = v(‘div’, [
    v(‘p’, [
    v(‘span’, [ v(‘#text’, ‘xiedaimala.com’) ] )
    ]
    ),
    v(‘span’, [
    v(‘#text’, ‘jirengu.com’)
    ])
    ]
    )

    let vNodes2 = v(‘div’, [
    v(‘p’, [
    v(‘span’, [
    v(‘#text’, ‘xiedaimala.com’)
    ] )
    ]
    ),
    v(‘span’, [
    v(‘#text’, ‘jirengu.coms’),
    v(‘#text’, ‘ruoyu’)
    ])
    ]
    )
    const root = document.querySelector(‘#root’)

    patchElement(root, vNodes1)// 创建新的 DOM,
    patchElement(root)// 删除 DOM 的操作
    patchElement(root, vNodes2,vNodes1)// 替换 (修改)DOM 操作
    以上只是简单实现! 有很多 bug
    总结
    问:说说虚拟 DOM:当我们修改真正的 DOM 树的时候,因为 DOM 中元素节点有许多的属性和方法,当 DOM 中节点过多时往往需要消耗很大的性能。解决方法是:使用 js 对象来表示 DOM 树的信息和结构,这个 js 对象可以构建一个真正的 DOM 树。当状态变更的时候用修改后的新渲染的的 js 对象和旧的虚拟 DOM js 对象作对比,记录着两棵树的差异。把差别反映到真实的 DOM 结构上最后操作真正的 DOM 的时候只操作有差异的部分就可以了

    正文完
     0