IT虾米网

virtual dom & mvvm详解

admin 2018年05月27日 编程语言 143 0

虚拟dom

  1. 用js对象来表示dom树的结构,然后用这个对象来构建一个真正的dom树插入文档中;

  2. 当状态有变时,重新构造一个新的对象树,然后比较新的和旧的树,记录两个数的差异;

  3. 把差异部分应用到真正的dom树上,更新视图。

核心算法实现(diff算法)

  1. 用js对象表示dom树

var e = { 
  tagName: 'ul', 
  props: { 
    id: 'list' 
  }, 
  children: [ 
    {tagName: 'li', props: {class: 'li1'}, children: ['item1']}, 
    {tagName: 'li', props: {class: 'li2'}, children: ['item2']}, 
    {tagName: 'li', props: {class: 'li3'}, children: [ 
      {tagName: 'h2', props: {class: 'h'}, children: ['hello qq']} 
    ]}, 
  ]  
}
  1. 把js对象渲染成dom树

function dom(tagName, props, children){ 
  function Element(tagName, props, children){ this.tagName = tagName; this.props = props; this.children = children; 
  } 
  Element.prototype.render = function(){ 
    const el = document.createElement(this.tagName); 
    const props = this.props; for(let key in props){ 
      el.setAttribute(key, props[key]); 
    } 
    const children = this.children || []; 
    children.forEach(child => { 
      const c = child.tagName ? new Element(child.tagName, child.props, child.children).render() : document.createTextNode(child); 
      el.appendChild(c); 
    }) return el; 
  } 
  return new Element(tagName, props, children); 
}

  1. 比较两个虚拟dom树的差异,同层节点进行比较(时间复杂度O(n));
    对每一个树在深度优先遍历的时候,每遍历到一个节点就把该节点和新的的树进行对比,把差异部分记录到一个对象里面。

// diff 函数,对比两棵树 function diff (oldTree, newTree) { 
  var index = 0 // 当前节点的标志 
  var 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) { 
  var leftNode = null 
  var currentNodeIndex = index 
  oldChildren.forEach(function (child, i) { var newChild = newChildren[i] 
    currentNodeIndex = (leftNode && leftNode.count) // 计算节点的标识 
      ? currentNodeIndex + leftNode.count + 1 
      : currentNodeIndex + 1 
    dfsWalk(child, newChild, currentNodeIndex, patches) // 深度遍历子节点 
    leftNode = child 
  }) 
}

  1. 因为步骤一所构建的 JavaScript 对象树和render出来真正的DOM树的信息、结构是一样的。所以我们可以对那棵DOM树也进行深度优先的遍历,遍历的时候从步骤二生成的paches对象中找出当前遍历的节点差异,然后进行 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操作   } 
} 
 
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) 
    } 
  }) 
}

Vue之MVVM简单实现

<!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>MVVM</title> </head> <body> 
  <div id='app'> <h2>{{ song }}</h2> <h3>{{ obj.name }}</h3> <h3>{{ obj.a }}</h3> <h3>{{ obj.obj1.name1 }}</h3> <input type="text" v-model='msg'> <h3>{{ msg }}</h3> <h2>{{ total }}</h2> 
  </div> 
  <script src="index.js"></script> 
  <script> 
    let vm = new Vm({ 
      el: '#app', 
      data(){ return { 
          song: 'Time', 
          obj: { 
            name: 'xxx', 
            a: 20, 
            b: 3, 
            obj1: { 
              name1: 'll' 
            } 
          }, 
          msg: 'hello' 
        } 
      }, 
      computed: { 
        total() { 
          return this.obj.a * this.obj.b; 
        } 
      }, 
      mounted() { 
        console.log(this.$el) 
        console.log('everything is done') 
      } 
    }) 
  </script> </body> </html>
function Vm(opts = {}){ 
  this.$opts = opts; 
  let data = this.$data = this.$opts.data(); 
   
  initComputed.call(this); 
 
  // 数据监测   observer(data); 
  for (let key in data) { 
    Object.defineProperty(this, key, { 
        configurable: true, 
        get() { return this.$data[key];  
        }, 
        set(newVal) { this.$data[key] = newVal; 
        } 
    }); 
  } 
 
  // 数据编译 
  new Compile(opts.el, this); 
  opts.mounted.call(this); 
} 
 
function initComputed(){ 
  let vm = this; 
  let computed = this.$opts.computed; 
  Object.keys(computed).forEach(key => { 
    Object.defineProperty(vm, key, { 
      get: typeof computed[key] === 'function' ? computed[key] : computed[key].get, 
      set(){} 
    }) 
  }) 
} 
 
function observer(data) { 
  if(!data || typeof data !== 'object') return; 
  return new Observer(data); 
} 
 
function Observer(data) { 
  let dep = new Dep(); 
  for (let key in data) { 
    let val = data[key]; 
    observer(val); 
    Object.defineProperty(data, key, { 
      configurable: true, 
      get() { 
        Dep.target && dep.addSub(Dep.target); return val; 
      }, 
      set(newVal) { if(val === newVal) return; 
        val = newVal; 
        observer(newVal); 
        dep.notify(); 
      } 
    }) 
  } 
} 
 
function Compile(el, vm){ 
  vm.$el = document.querySelector(el); 
  var fragment = document.createDocumentFragment(); 
  var child; 
  while(child = vm.$el.firstChild) { 
    fragment.appendChild(child); 
  } 
  function replace(fragment){ 
    Array.from(fragment.childNodes).forEach(item => { 
      let text = item.textContent; 
      let reg = /\{\{(.*?)\}\}/g; 
      if(item.nodeType === 3 && reg.test(text)){ // 重点重点重点!!!!!! // 去掉空格!!!!!!!! function replaceTxt() { 
          item.textContent = text.replace(reg, (matched, placeholder) => {    
            console.log(placeholder);   // 匹配到的分组 如:song, album.name, singer... new Watcher(vm, placeholder.trim(), replaceTxt);   // 监听变化,进行匹配替换内容   return placeholder.trim().split('.').reduce((val, key) => { 
              return val[key];  
            }, vm); 
          }); 
        }; 
        replaceTxt(); 
      } 
      if(item.nodeType === 1){ 
        let itemAttr = item.attributes; 
        Array.from(itemAttr).forEach(attr => { 
          let name = attr.name; 
          let exp = attr.value; 
          if(name.includes('v-')){ 
            item.value = vm[exp]; 
          } 
          new Watcher(vm, exp, newVal => { 
            item.value = newVal; 
          }) 
          item.addEventListener('input', e => { 
            vm[exp] = e.target.value; 
          }) 
        }) 
      } 
      if(item.childNodes && item.childNodes.length){ 
        replace(item); 
      } 
    }) 
  } 
  replace(fragment); 
  vm.$el.appendChild(fragment); 
} 
 
// 发布订阅 function Dep(){ 
  this.subs = []; 
} 
Dep.prototype = { 
  addSub(sub){ this.subs.push(sub); 
  }, 
  notify(){ this.subs.forEach(sub => { 
      sub.update() 
    }); 
  } 
} 
function Watcher(vm, exp, fn){ 
  this.fn = fn; 
  this.vm = vm; 
  this.exp = exp; 
  Dep.target = this; 
  let arr = exp.split('.'); 
  let val = vm; 
  arr.forEach(key => { 
    val = val[key]; 
  }); 
  Dep.target = null; 
} 
Watcher.prototype.update = function(){ 
  let arr = this.exp.split('.'); 
  let val = this.vm; 
  arr.forEach(key => {     
    val = val[key];   // 通过get获取到新的值   }); 
  this.fn(val); 
} 
// let watcher = new Watcher(() => { 
//   console.log('watch') // }) 
// let dep = new Dep(); 
// dep.addSub(watcher); 
// dep.addSub(watcher); 
// dep.notify();
 
发布评论

分享到:

IT虾米网

微信公众号号:IT虾米 (左侧二维码扫一扫)欢迎添加!

js来监控复制粘贴详解
你是第一个吃螃蟹的人
发表评论

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。