MVVM模型
M:模型(Model):对应data中的数据
 V:视图(View):模板
 VM:视图模型(ViewModel):Vue实例对象
 
分解任务
 <div id="app">
    <input type="text"  v-model="text">
    {{text}}
  </div>
 
拟实现:
 1、输入框以及文本节点与data中的数据绑定
2、输入框内容变化时,data中的数据同步变化。即view => model的变化。
3、data中的数据变化时,文本节点的内容同步变化。即model => view的变化。
要实现任务一,需要对DOM进行编译,这里有一个知识点:DocumentFragment。
1、DocumentFragment
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
var dom=nodeToFragment(document.getElementById('app'))
    console.log(dom)
    function nodeToFragment(node){
      var flag=document.createDocumentFragment();
      var child;
      while(child=node.firstChild){
        flag.append(child)  //劫持node中的所有子节点
      }
      return flag;
    }
 

2、数据初始化绑定
function nodeToFragment(node,vm){
  var flag=document.createDocumentFragment();
  var child;
  while(child=node.firstChild){
    compile(child,vm)
    flag.append(child)  //劫持node中的所有子节点
  }
  
  return flag;
}
function compile(node,vm){
  //节点类型为元素
  if(node.nodeType===1){
    var attr=node.attributes;
    for(var i=0;i<attr.length;i++){
      if(attr[i].nodeName=='v-model'){
        var name=attr[i].nodeValue; //获取v-model绑定的属性名
        node.value=vm.data[name];  //将data的值赋给该node
        node.removeAttribute('v-model')
      }
    }
  }
  var reg = /\{\{(.*)\}\}/;
  //节点类型为文本
  if(node.nodeType===3){
    if(reg.test(node.textContent)){
      var name=RegExp.$1;  //获取匹配到的字符串
      name=name.trim(); 
      node.nodeValue=vm.data[name];//将data的值赋给该node
    }
  }
}
function MVVM(options){
  this.data=options.data;
  var id=options.el;
  var dom=nodeToFragment(document.getElementById(id),this)
  //编译完成后,把dom返回到app中
  console.log(dom)
  document.getElementById(id).appendChild(dom)
}
var vm=new MVVM({
  el:'app',
  data:{
    text:'hello world'
  }
})
 
以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。
 
3、响应式的数据绑定
再来看任务二的实现思路:当我们在输入框输入数据的时候,首先触发input事件(或者keyup、change事件),在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text劫持为vm的访问器属性,因此给vm.text赋值,就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务三再说。
function defineReactive(obj,key,val) {
  Object.defineProperty(obj,key,{
    get:function(){
      return val
    },
    set:function(newVal){
      if(newVal === val) return
      val=newVal
      console.log('set被调用了',val)
    }
  })
}
function observe(obj,vm) {
  Object.keys(obj).forEach(function(key){
    defineReactive(vm,key,obj[key])
  })
  
}
function MVVM(options){
  this.data=options.data;
  var data=this.data
  observe(data,this)
  var id=options.el;
  var dom=nodeToFragment(document.getElementById(id),this)
  //编译完成后,把dom返回到app中
  document.getElementById(id).appendChild(dom)
}
function compile(node,vm){
  //节点类型为元素
  if(node.nodeType===1){
    //...
   	//...{
      //...{
        var name=attr[i].nodeValue; //获取v-model绑定的属性名
        node,addEventListener('input',function(e){
          vm[name]=e.target.value  //触发vm的访问器属性的set
        })
        node.value=vm[name];  //将data的值赋给该node
        node.removeAttribute('v-model')  
      }
    }
  }
  //...
  if(node.nodeType===3){
    if(reg.test(node.textContent)){
      //..
      node.nodeValue=vm[name];//将data的值赋给该node
    }
  }
}
 
任务二也就完成了,text属性值会与输入框的内容同步变化(实现view->model):
 
4双向绑定的实现
回顾一下,每当new一个Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译HTML:nodeToFragement(id)。
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
 在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的set方法。
 接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的update方法 => 更新视图。
 这里的关键逻辑是:如何将watcher添加到关联属性的dep中。
 
function Watcher(vm,node,name){
  Dep.target=this
  this.name=name
  this.node=node
  this.vm=vm
  this.updater() 
  Dep.target=null
}
Watcher.prototype={
  updater:function() {
    this.get()
    this.node.nodeValue=this.value
  },
  //获取data中的属性值
  get:function(){
    this.value=this.vm[this.name]
  }
}
 
首先,Watcher将自己赋给了一个全局变量Dep.target;
 其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;
 再次,获取属性的值,然后更新视图。
 最后,将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
 
function Dep(){
  this.subs=[]
}
Dep.prototype={
  addSub:function(sub){
    this.subs.push(sub)
  },
  notify:function(){
    this.subs.forEach(function(sub){
      sub.update()
    })
  },
}
 
至此实现了双向绑定
 
总结
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
 1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
 2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
 3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
 4、mvvm入口函数,整合以上三者
 










