美文网首页
虚拟DOM的实现

虚拟DOM的实现

作者: 野薇薇 | 来源:发表于2017-08-24 11:11 被阅读0次

    [维护状态,更新视图]
    用js对象表示Dom元素
    js:

    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"]},
      ]
    }
    

    表示dom结构为:

    <ul id='list'>
      <li class='item'>Item 1</li>
      <li class='item'>Item 2</li>
      <li class='item'>Item 3</li>
    </ul>
    
    Virtual DOM 算法,包括几个步骤:
    • 用 JavaScript 对象结构表示 DOM 树的结构;然后用这个树构建一个真正的 DOM 树,插到文档当中
    • 当状态变更的时候,重新构造一棵新的对象树。然后用新的树和旧的树进行比较,记录两棵树差异
    • 把2所记录的差异应用到步骤1所构建的真正的DOM树上,视图就更新了

    算法实现

    1. js对象模拟dom元素

    js对象表示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.prototype.render = function () {
      var el = document.createElement(this.tagName) // 根据tagName构建
      var props = this.props
    
      for (var propName in props) { // 设置节点的DOM属性
        var propValue = props[propName]
        el.setAttribute(propName, propValue)
      }
    
      var children = this.children || []
    
      children.forEach(function (child) {
        var childEl = (child instanceof Element)
          ? child.render() // 如果子节点也是虚拟DOM,递归构建DOM节点
          : document.createTextNode(child) // 如果字符串,只构建文本节点
        el.appendChild(childEl)
      })
    
      return el
    }
    

    render方法会根据tagName构建一个真正的DOM节点,然后设置这个节点的属性,最后递归地把自己的子节点也构建起来。所以只需要:

    var 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>
    

    2.比较两颗dom树的差异

    diff算法是重点

    • 实现一个简单的diff算法--比较两个字符串的差异
    var oldStr = 'aaabbbccc';
    var newStr = 'aaagggccc';
    diff信息:[3, "-3", "+ggg", 3]
    

    整数代表无变化的字符数量,“-”开头的字符串代表被移除的字符数量,“+”开头的字符串代表新加入的字符。所以我们可以写一个 minimizeDiffInfo 函数:

    function minimizeDiffInfo(originalInfo){
        var result = originalInfo.map(info => {
            if(info.added){
                return '+' + info.value;
            }
            if(info.removed){
                return '-' + info.count;
            }
            return info.count;
        });
        return JSON.stringify(result);
    }
     
    var diffInfo = [
        { count: 3, value: 'aaa' },
        { count: 3, added: undefined, removed: true, value: 'bbb' },
        { count: 3, added: true, removed: undefined, value: 'ggg' },
        { count: 3, value: 'ccc' }
    ];
    minimizeDiffInfo(diffInfo);
    //=> '[3, "-3", "+ggg", 3]'
    

    用户端接受到精简之后的 diff 信息,生成最新的资源:

    mergeDiff('aaabbbccc', '[3, "-3", "+ggg", 3]');
    //=> 'aaagggccc'
     
    function mergeDiff(oldString, diffInfo){
        var newString = '';
        var diffInfo = JSON.parse(diffInfo);
        var p = 0;
        for(var i = 0; i < diffInfo.length; i++){
            var info = diffInfo[i];
            if(typeof(info) == 'number'){
                newString += oldString.slice(p, p + info);
                p += info;
                continue;
            }
            if(typeof(info) == 'string'){
                if(info[0] === '+'){
                    var addedString = info.slice(1, info.length);
                    newString += addedString;
                }
                if(info[0] === '-'){
                    var removedCount = parseInt(info.slice(1, info.length));
                    p += removedCount;
                }
            }
        }
        return newString;
    }
    
    • 虚拟dom的diff算法会比较难一点,因为会涉及到不仅是同级的元素,要跨越层级进行增删改移;我们需要对dom进行深度优先遍历。

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

    结语

    虚拟dom实现流程的概括:

    // 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)
    

    参考:深度剖析:如何实现一个 Virtual DOM 算法

    相关文章

      网友评论

          本文标题:虚拟DOM的实现

          本文链接:https://www.haomeiwen.com/subject/qqbsdxtx.html