时间:2017-09-13 来源:

如何实现一个VirtualDOM及源码分析

如何实现一个 Virtual DOM 及源码分析

Virtual DOM算法

    web页面有一个对应的DOM树div+css+js切图,在传统开发页面时,div+css+js切图每次页面需要被更新时页面div重构,都需要手动操作DOM来进行更新,页面div重构但是我们知道DOM操作对性能来说是非常不友好的psd切图html,会影响页面的重排,手机div+css从而影响页面的性能.因此在React和VUE2.0+引入了虚拟DOM的概念web外包,他们的原理是:把真实的DOM树转换成javascript对象树,web外包也就是虚拟DOM,每次数据需要被更新的时候,div页面它会生成一个新的虚拟DOM,并且和上次生成的虚拟DOM进行对比,div+css+js切图对发生变化的数据做批量更新.---(因为操作JS对象会更快承接网站前端,更简单,承接网站前端比操作DOM来说).
我们知道web页面是由一个个HTML元素嵌套组合而成的psd切图html,当我们使用javascript来描述这些元素的时候,承接网页制作这些元素可以简单的被表示成纯粹的JSON对象.

比如如下HTML代码:

<div id="container" class="container">
   <ul id="list">
     <li class="item">111</li>
     <li class="item">222</li>
     <li class="item">333</li>
   </ul>
   <button class="btn btn-blue"><em>提交</em></button>
</div>

上面是真实的DOM树结构web外包,我们可以使用javascript中的json对象来表示的话,web外包变成如下:

var element = {
      tagName: 'div',
        class: 'container'
      },
          props: {
            id: 'list'
          }, props: {class: 'item'},
            {tagName: 'li', children: ['222']}, props: {class: 'item'},
        {
          tagName: 'button',
          children: [
            {
              tagName: 'em',
              children: ['提交']
            }
          ]
        }
      ]
   };

因此我们可以使用javascript对象表示DOM的信息和结构,手机div+css当状态变更的时候web外包,重新渲染这个javascript对象的结构,web外包然后可以使用新渲染的对象树去和旧的树去对比网页切图制作,记录两颗树的差异,div页面两颗树的差异就是我们需要对页面真正的DOM操作div+css+js切图,然后把他们应用到真正的DOM树上,div+css+js切图页面就得到更新.视图的整个结构确实全渲染了承接网站前端,但是最后操作DOM的时候,承接网站前端只变更不同的地方.
因此我们可以总结一下 Virtual DOM算法:
1. 用javascript对象结构来表示DOM树的结构psd切图html,然后用这个树构建一个真正的DOM树,承接网页制作插入到文档中.
2. 当状态变更的时候web外包,重新构造一颗新的对象树,web外包然后使用新的对象树与旧的对象树进行对比网页切图制作,记录两颗树的差异.
3. 把记录下来的差异用到步骤1所构建的真正的DOM树上.视图就更新了.

算法实现:
2-1 使用javascript对象模拟DOM树.
使用javascript来表示一个DOM节点,网页切图制作有如上JSON的数据div+css+js切图,我们只需要记录它的节点类型,网页重构报价属性和子节点即可.

element.js 代码如下:

function Element(tagName, children) {
  this.tagName = tagName;
  this.props = props;
  this.children = children;
}
Element.prototype.render = function() {
  var el = document.createElement(this.tagName);
  var props = this.props;
  // 遍历子节点psd切图html, propValue);
  }
  // 保存子节点
  var childrens = this.children || [];
  // 遍历子节点web外包,递归构建DOM节点
      : document.createTextNode(child);    // 如果是字符串的话网页切图制作, props, props, {id: 'container', [
  el('ul',[
    el('li', ['111']), {class: 'item'},
    el('li', ['333']),
  el('button', [
    el('em', ['提交'])
  ])
]);

var elemRoot = element.render();
document.body.appendChild(elemRoot);

打开页面即可看到效果.

2-2 比较两颗虚拟DOM树的差异及差异的地方进行dom操作

上面的div只会和同一层级的div对比承接网站前端,第二层级的只会和第二层级的对比,承接网站前端这样的算法的复杂度可以达到O(n).
但是在实际代码中psd切图html,会对新旧两颗树进行一个深度优先的遍历,手机div+css因此每个节点都会有一个标记.如下图所示:

在遍历的过程中web外包,每次遍历到一个节点就把该节点和新的树进行对比,div切图排版如果有差异的话就记录到一个对象里面.

现在我们来看下我的目录下 有哪些文件;然后分别对每个文件代码进行解读网页切图制作,看看做了哪些事情,网页切图制作旧的虚拟dom和新的虚拟dom是如何比较的div+css+js切图,且是如何更新页面的 如下目录:
目录结构如下:

vdom  ---- 工程名
|   | ---- index.html  html页面
|   | ---- element.js  实例化元素组成json数据 且 提供render方法 渲染页面
|   | ---- util.js     提供一些公用的方法
|   | ---- diff.js     比较新旧节点数据 如果有差异保存到一个对象里面去
|   | ---- patch.js    对当前差异的节点数据 进行DOM操作
|   | ---- index.js    页面代码初始化调用

首先是 index.js文件 页面渲染完成后 变成如下html结构 

<div id="container">
  <h1 style="color: red;">simple virtal dom</h1>
  <p>the count is :1</p>
  <ul>
    <li>Item #0</li>
  </ul>
</div>

假如发生改变后,div+css+js切图变成如下结构 

<div id="container">
  <h1 style="color: blue;">simple virtal dom</h1>
  <p>the count is :2</p>
  <ul>
    <li>Item #0</li>
    <li>Item #1</li>
  </ul>
</div>

可以看到 新旧节点页面数据的改变承接网站前端,h1标签从属性 颜色从红色 变为蓝色,页面div重构p标签的文本发生改变psd切图html,ul新增了一项元素li.
基本的原理是:先渲染出页面数据出来,手机div+css生成第一个模板页面web外包,然后使用定时器会生成一个新的页面数据出来,web外包对新旧两颗树进行一个深度优先的遍历网页切图制作,因此每个节点都会有一个标记.
然后调用diff方法对比对象新旧节点遍历进行对比,div页面找出两者的不同的地方存入到一个对象里面去div+css+js切图,最后通过patch.js找出对象不同的地方,div+css+js切图分别进行dom操作.

index.js代码如下:

var el = require('./element');
var diff = require('./diff');
var patch = require('./patch');

var count = 0;
function renderTree() {
  count++;
  var items = [];
  var color = (count % 2 === 0) ? 'blue' : 'red';
  for (var i = 0; i < count; i++) {
    items.push(el('li', {'id': 'container'}, {style: 'color: ' + color},
    el('p',
    el('ul', newTree)
  console.log(patches)
  patch(root, 1000);

执行 var tree = renderTree()方法后psd切图html,
1. 依次遍历子节点(从内到外调用)依次为 li, p, li和h1和p有一个文本子节点div+css+js切图,因此遍历完成后,div+css+js切图count就等于1,
但是遍历ul的时候,页面div重构因为有一个子节点li,因此 count += 1; 所以调用完成后,手机div+cssul的count等于2. 因此会对每个element属性添加count属性.对于最外层的container容器就是对每个子节点的依次增加web外包,循环完成后 +1;因此变为2,循环完成后 +1,ul为2,因此变为3, props, 2).filter(utils.truthy); } return new Element(tagName, children); } // 如果没有属性的话div+css+js切图,第二个参数是一个数组,网页重构报价说明第二个参数传的是子节点 if (utils.isArray(props)) { children = props; props = {}; } this.tagName = tagName; this.props = props || {}; this.children = children || []; // 保存key键 如果有属性 保存key, function(child, i) { // 如果是元素的实列的话 if (child instanceof Element) { count += child.count; } else { // 如果是文本节点的话,手机div+css直接赋值 children[i] = '' + child; } count++; }); this.count = count; }

oldTree数据最终变成如下:

var oldTree = {
  tagName: 'div',
  count: 7,
  children: [
    {
      tagName: 'h1',
      children: ['simple virtal dom']
    },
      key: undefined
      count: 1
      props: {},
    {
      tagName: 'ul',
      children: [
        {
          tagName: 'li',
          count: 1,
          children: ['Item #0']
        }
      ]
    },
  ]
};

定时器 执行 var newTree = renderTree()后,承接网站前端调用方法步骤还是和第一步一样:
2. 依次遍历子节点(从内到外调用)依次为 li, p, li和h1和p有一个文本子节点网页切图制作,因此遍历完成后,网页切图制作count就等于1,count都为1,因此遍历完成第一个li时候psd切图html,当遍历完成第二个li的时候web外包, key: undefined, props: {id: 'container'}, key: undefined count: 1 props: {style: 'colod: red'}, { tagName: 'p', children: ['the count is :1'] }, key: undefined count: 4 props: {}, key: undefined, props: {}, { tagName: 'li', count: 1, children: ['Item #1'] } ] }, newTree);

调用diff方法可以比较新旧两棵树节点的数据div+css+js切图,把两颗树的不同节点找出来.(注意,网页重构报价查看diff对比数据的方法承接网站前端,找到不同的节点,承接网站前端可以查看这篇文章diff算法)如下调用代码:

function diff (oldTree, newTree, patches);
  return patches;
}

执行deepWalk如下代码:

function deepWalk(oldNode, index, patches) {
  var currentPatch = [];
  // 节点被删除掉
  if (newNode === null) {
    // 真正的DOM节点时,div+css+js切图将删除执行重新排序承接网站前端, content: newNode});
    }
  } else if(oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
    // 相同的节点psd切图html, newNode);
    if (propsPatches) {
      currentPatch.push({type: patch.PROPS,
        newNode.children,
        patches,
        currentPatch
      )
    }
  } else {
    // 不同的节点,div+css+js切图那么新节点替换旧节点
    currentPatch.push({type: patch.REPLACE,如果为null,说明节点被删除掉.
2. 判断新旧节点是否为字符串,承接网页制作如果为字符串说明是文本节点web外包,并且新旧两个文本节点不同的话,web外包存入数组里面去网页切图制作, content: newNode});
   patch.TEXT 为 patch.js里面的 TEXT = 3;content属性为新节点.

3. 如果新旧tagName相同的话div+css+js切图,并且新旧节点的key相同的话,网页重构报价继续比较新旧节点的属性承接网站前端, newNode);

diffProps方法的代码如下:

function diffProps(oldNode,
        value;
      var propsPatches = {};
      // 找出不同的属性值
      for (key in oldProps) {
        value = oldProps[key];
        if (newProps[key] !== value) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 找出新增属性
      for (key in newProps) {
        value = newProps[key];
        if (!oldProps.hasOwnProperty(key)) {
          count++;
          propsPatches[key] = newProps[key];
        }
      }
      // 如果所有的属性都是相同的话
      if (count === 0) {
        return null;
      }
      return propsPatches;
   }

diffProps代码解析如下:

for (key in oldProps) {
   value = oldProps[key];
   if (newProps[key] !== value) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上代码是 判断旧节点的属性值是否在新节点中找到web外包,如果找不到的话,div切图排版count++; 把新节点的属性值赋值给 propsPatches 存储起来.

for (key in newProps) {
   value = newProps[key];
   if (!oldProps.hasOwnProperty(key)) {
      count++;
      propsPatches[key] = newProps[key];
   }
}

如上代码是 判断新节点的属性是否能在旧节点中找到网页切图制作,如果找不到的话,网页切图制作count++; 把新节点的属性值赋值给 propsPatches 存储起来.

if (count === 0) {
   return null;
}
return propsPatches;

最后如果count 等于0的话div+css+js切图,说明所有属性都是相同的话,div+css+js切图所以不需要做任何变化.否则的话承接网站前端,返回新增的属性.

如果有 propsPatches 的话,页面div重构执行如下代码:

if (propsPatches) {
   currentPatch.push({type: patch.PROPS, props: propsPatches});
}

因此currentPatch数组里面也有对应的更新的属性,手机div+cssprops就是需要更新的属性对象.

继续代码:

// 不同的子节点 
if (!isIgnoreChildren(newNode)) {
   diffChildren(
     oldNode.children,
     index,
     currentPatch
   )
}
function isIgnoreChildren(node) {
  return (node.props && node.props.hasOwnProperty('ignore'));
}

如上代码判断子节点是否相同div+css+js切图, newChildren, patches, newChildren, moves: diffs.moves}; currentPatch.push(recorderPatch); } var leftNode = null; var currentNodeIndex = index; utils.each(oldChildren, i) { var newChild = newChildren[i]; currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1; // 递归 deepWalk(child, currentNodeIndex, newChildren, 'key'); 新旧节点按照key来比较,手机div+css目前key为undefined, children: [ { tagName: 'h1', children: ['simple virtal dom'] }, key: undefined count: 1 props: {}, { tagName: 'ul', children: [ { tagName: 'li', count: 1, children: ['Item #0'] }, key: undefined, props: {}, key: undefined count: 1 props: {style: 'colod: red'}, { tagName: 'p', children: ['the count is :1'] }, key: undefined count: 2 props: {}, key: undefined, props: {}, 第一次遍历时 leftNode 为null,因此 currentNodeIndex = currentNodeIndex + 1 = 0 + 1 = 1; 不是第一次遍历,网页切图制作那么leftNode都为上一次遍历的子节点div+css+js切图,因此不是第一次遍历的话,div+css+js切图那么 currentNodeIndex = currentNodeIndex + leftNode.count + 1; 
然后递归调用 deepWalk(child, currentNodeIndex, patches); 方法,手机div+css接着把child赋值给leftNode,leftNode = child;

所以一直递归遍历,web外包最终把不相同的节点 会存储到 currentPatch 数组内.最后执行

if (currentPatch.length) {
   patches[index] = currentPatch;
}

把对应的currentPatch 存储到 patches对象内中的对应项网页切图制作,最后就返回 patches对象.

4. 返回到index.js 代码内,div页面把两颗不相同的树节点的提取出来后div+css+js切图, props: {style: 'color: blue'}}], content: 'the count is :2'}], moves: [ { index: 1, props: {}, key: undefined, patches);
执行patch方法承接网站前端, patches) {
var walker = {index: 0}; deepWalk(node, patches); }

deepWalk 代码如下:

function deepWalk(node, patches) {
   var currentPatches = patches[walker.index];
      // node.childNodes 返回指定元素的子元素集合网页切图制作,包括HTML节点,网页切图制作所有属性div+css+js切图, walker, currentPatches);
   }
}

1. 首次调用patch的方法psd切图html,root就是container的节点,手机div+css因此调用deepWalk方法web外包,
var len = node.childNodes ? node.childNodes.length : 0; 因此 len = 3; 很明显该子节点的长度为3, p, 和ul元素;


2. 然后进行for循环,div+css+js切图获取该父节点的子节点承接网站前端,因此第一个子节点为 h1 元素,承接网站前端walker.index++; 因此walker.index = 1; 再进行递归 deepWalk(child, patches); 此时子节点为h1, 因此获取 currentPatches = patches[1]; 获取值网页切图制作,再获取 h1的子节点的长度,网页切图制作len = 1; 然后再for循环div+css+js切图,获取child为文本节点,网页重构报价此时 walker.index++; 所以此时walker.index 为2, 在调用deepwalk方法递归,承接网站前端因此再继续获取 currentPatches = patches[2]; 值为undefined,再获取len = 0; 因为文本节点么有子节点,手机div+css所以for循环跳出web外包,所以判断currentPatches是否有值,div切图排版因为此时 currentPatches 为undefined,所以递归结束,网页切图制作再返回到 h1元素上来div+css+js切图,所以currentPatches = patches[1]; 所以有值,div+css+js切图所以调用 applyPatches()方法来更新dom元素.


3. 继续循环 i, 此时i = 1; 获取子节点 child = p元素,页面div重构walker.index++, 继续调用 deepWalk方法web外包,获取 var currentPatches = patches[walker.index] = patches[3]的值,web外包var len = 1; 因为p元素下有一个子节点(文本节点),再进for循环,div页面此时 walker.index++; 因此walker.index = 4; child此时为文本节点div+css+js切图,在调用 deepwalk方法的时候,div+css+js切图再获取var currentPatches = patches[walker.index] = patches[4]; 再执行len 代码的时候 len = 0;因此跳出for循环承接网站前端,判断 currentPatches是否有值,承接网站前端有值的话psd切图html,更新对应的DOM元素.

4. 继续循环i = 2; 获取子节点 child = ul元素,承接网页制作walker.index++; 此时walker.index = 5; 在调用deepWalk方法递归web外包, 因为ul元素下有一个li元素网页切图制作,在继续for循环遍历,网页切图制作获取子节点li,此时walker.index++; walker.index = 6; 再递归调用deepwalk方法,网页重构报价再获取var currentPatches = patches[walker.index] = patches[6]; len = 1; 因为li的元素下有一个文本节点承接网站前端,再进行for循环,承接网站前端此时child为文本节点psd切图html,walker.index++;此时walker.index = 7; 再执行 deepwalk方法,手机div+css再获取 var currentPatches = patches[walker.index] = patches[7]; 这时候 len = 0了web外包,因此跳出for循环,div切图排版判断 当前的currentPatches是否有值网页切图制作,没有,网页切图制作就跳出div+css+js切图,然后再返回ul元素,div+css+js切图获取该自己li的时候承接网站前端,因此var currentPatches = patches[walker.index] = patches[5]; 然后判断 currentPatches是否有值psd切图html,有值就进行更新DOM元素.

最后就是 applyPatches 方法更新dom元素了,手机div+css如下代码:

function applyPatches(node, function(currentPatch) {
    switch (currentPatch.type) {
      case REPLACE:
        var newNode = (typeof currentPatch.node === 'string') 
          ? document.createTextNode(currentPatch.node)
          : currentPatch.node.render();
        node.parentNode.replaceChild(newNode, currentPatch.moves);
        break;
      case PROPS: 
        setProps(node, currentPatch.props);
        break;
      case TEXT:
        if(node.textContent) {
          node.textContent = currentPatch.content;
        } else {
          // ie bug
          node.nodeValue = currentPatch.content;
        }
        break;
      default:
        throw new Error('Unknow patch type' + currentPatch.type);
    }
  });
}

判断类型,div+css+js切图替换对应的属性和节点.
最后就是对子节点进行排序的操作承接网站前端, moves) { var staticNodeList = utils.toArray(node.childNodes); var maps = {}; utils.each(staticNodeList, function(move) { var index = move.index; if (move.type === 0) { // remove Item if (staticNodeList[index] === node.childNodes[index]) { node.removeChild(node.childNodes[index]); } staticNodeList.splice(index, 0, node.childNodes[index] || null); } }); }

遍历moves,等于0的话是删除操作承接网站前端, type: 1, key: undefined, count: 1, children: ['#Item 1'] } };

node节点 就是 'ul'元素,网页切图制作var staticNodeList = utils.toArray(node.childNodes); 把ul的旧子节点li转成Array形式div+css+js切图,所以直接跳到下面遍历代码来承接网站前端,获取某一项的索引index, 目前等于1,是新增一项,web外包但是没有key,因此调用move.item.render(); 渲染完后,div页面对staticNodeList数组里面的旧节点的li项从第二项开始插入节点li, node.childNodes[index] || null); node就是ul父节点承接网站前端,insertNode节点插入到 node.childNodes[1]的前面.因此把在第二项的前面插入第一项.
查看github上源码

点击次数:19629
作者:
web前端行业资讯
Web new NewsList
谷歌发布Tacotron2:能更简单地训练AI学习演讲 ,,2017年12月21日TensorFlow漏洞爆发背后:关于AI安全我们的傻与天真 ,,2017年12月21日Android端Edge浏览器新版发布:常规性能优化和BUG修复 ,,2017年12月21日三星开发出全球最小的DRAM芯片技术领先优势扩大 ,,2017年12月21日腾讯绝艺AI下一步将学习AlphaGozero自对弈训练 ,,2017年12月21日Facebook社交VR应用Spaces扩大覆盖面:入驻HTCVive ,,2017年12月21日设计图曝光:三星双屏折叠手机原来是这样的 ,,2017年12月21日微信支付和支付宝已成为世界移动支付的"老师" ,,2017年12月21日新专利表明FaceID未来有望装备在iPad、MacBook和iMac等设备 ,,2017年12月21日首批九个建议加入EE4J的项目 ,,2017年12月21日这就是SurfacePhone?微软可折叠手机概念图曝光 ,,2017年12月21日继“Angel”开源后,腾讯又开放TDinsight机器学习平台 ,,2017年12月21日谷歌母公司研发“闪光”网络技术无需铺设线缆 ,,2017年12月21日微软投资5千万美元利用人工智能对抗气候变化 ,,2017年12月21日谷歌中国2017:面向开发者的1年AI先行的1年 ,,2017年12月21日GreenKey加入Symphony软件基金会,将开源语音软件 ,,2017年12月21日腾讯发现者揭秘:怎么应对TensorFlow的安全风险,修复有多难 ,,2017年12月21日清华新成立两大交叉研究机构探索智能与未来 ,,2017年12月21日微软将AI融入生产力工具和搜索引擎与其它巨头竞争 ,,2017年12月21日Gfycat将利用机器学习技术创建高分辨率GIF动图 ,,2017年12月21日安全软件公司Avast开源化机器码反编译器RetDec ,,2017年12月21日谷歌开源TFGAN,让训练和评估GAN变得更加简单 ,,2017年12月21日社区对模块化不感兴趣时隔三周经典版FedoraServer27发布 ,,2017年12月21日Windows10加入OpenSSH客户端 ,,2017年12月21日FirefoxQuantum发布一个月安装量1.7亿 ,,2017年12月21日吴恩达宣布创业新项目已与富士康达成战略合作 ,,2017年12月21日Scala入门系列(十二):隐式转换2017年12月20日speedment入门教程2017年12月20日SLAM入门笔记(1):特征点的匹配2017年12月20日深入浅出了解frame和bounds2017年12月20日J2SE1.5版本的新特性一览2014年01月29日【Java-IO】Java文件操作 【编程语言】2015年05月13日Python标准库:内置函数any(iterable)2014年11月04日implicit修饰符 【Web前端】2015年06月25日Web(瓦片)地图的工作原理 【Web前端】2015年03月09日Mycat(6):聊天消息表,按月分表java客户端跨月查询数据 【综合】2015年07月29日UVALive2963Hypertransmission 【移动开发】2015年05月11日js获取easyui的日期输入框控件的值 【编程语言】2015年01月24日需要注意的13种的房间禁忌2014年01月29日Android实现平板的类股票列表联动 【移动开发】2014年12月29日cocos2dx-lua捕获用户touch事件的几种方式 【综合】2015年07月14日RGBA与半透明背景【数据库】2014年12月23日Ubuntu中的编程语言(下) 【编程语言】2015年07月29日【翻译】ExtJS6早期访问版本发布 【架构设计】2015年04月17日Androidapps浅析01-Amazed:一个简单但令人上瘾的加速度为基础的大理石指导游戏, 【编程语言】2015年01月24日LeetCode-EditDistance【编程语言】2015年08月27日C#托管内存与非托管内存之间的转换(结合Unity3d的实际开发)【移动开发】2014年12月17日Java数据结构系类之——链表(3):双向链表及相关常用操作【移动开发】2014年11月24日hiho一下第四十周题目1:三分·三分求极值【编程语言】2015年04月07日最小公倍数UVa11889 【Web前端】2015年05月25日CC++:迭代器的简单二分查找【编程语言】2015年04月08日脚踏实地学习轻松网赚2014年01月29日linuxmakefile编译c和c++文件【互联网】2015年04月07日monkeytest使用与分析笔记【编程语言】2015年08月28日自带十几种动画的NiftyDialogEffects对话框源码【互联网】2015年03月13日IOSSDK详解之UIAlertController(IOS8之后替代AlertView和ActionSheet) 【架构设计】2015年04月02日UVA10453【编程语言】2015年08月15日一些常用的sql语句总结 【移动开发】2015年05月27日剑指offer面试题28—字符串的排列 【编程语言】2015年05月05日自定义控件中的DrawingCache 【互联网】2015年08月03日
我们保证
We guarantee
> psd效果文件手工切图,保证图片效果最好体积最小利于传输
> 100%手写的HTML(DIV+CSS)编码,绝对符合W3C标准
> 代码精简、css沉余量小、搜索引擎扫描迅速,网页打开快捷
> 应用Css Sprite能够减少HTTP请求数,提高网页性能
> 跨浏览器兼容(IE6、7、8、9,Firefox火狐,Chrome谷歌)