0
点赞
收藏
分享

微信扫一扫

Vue的双向绑定原理

求阙者 2022-03-12 阅读 66
vuevue.js

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入口函数,整合以上三者
在这里插入图片描述

举报

相关推荐

0 条评论