前言:

对于传统的dom操作,当数据变动时更新视图须要先获取到指标节点,而后将扭转后的值放入节点中,视图发生变化时,须要绑定事件批改数据。双向数据恰好能解决这种简单的操作,当数据发生变化时会自动更新视图,视图发生变化时也会自动更新数据,极大的进步了开发效率。那双向数据绑定到底是怎么实现的了,上面来讲述双向数据绑定的原理。

1、Vue双向数据绑定的原理。

Vue实现双向数据绑定是采纳数据劫持和发布者-订阅者模式。数据劫持是利用ES5的Object.defineProperty(obj,key,val)办法来劫持每个属性的getter和setter,在数据变动是公布音讯给订阅者,从而触发相应的回调来更新视图,上面来一步步实现。

<div id="app">    用户名:<input type="text" v-model="name">    明码:<input type="text" v-model="passWord">    {{name}}   {{passWord}}    <div><div>{{name}}</div></div></div><script>    function Vue(option){      this.data = option.data;      this.id = option.el;      var dom = nodeToFragment(document.getElementById(this.id), this);      document.getElementById(this.id).appendChild(dom);    }     var vm = new Vue({      el: "app",      data: {        name: "zhangsan",        passWord: "123456"      }    })<script>

如上一段html,想要实现双向数据绑定,咱们须要先解析这一段html,找到带有v-model指令和{{}}的节点(此处节点包含元素节点和文本节点),而后咱们定义了一个Vue的构造函数,在实例化创建对象vm时,传入id='app'和对应的数据data,咱们当初须要实现的性能是,当实例化创建对象时,将对应的'name'和'passWord'属性渲染到页面上。

在解析下面一段模板时,须要先理解一下DocuemntFragment(碎片化文档)这个概念,你能够把他认为是一个dom节点收容器,当你发明了10个节点,当每个节点都插入到文档当中都会引发一次浏览器的回流,也就是说浏览器要回流10次,非常耗费资源。而应用碎片化文档,也就是说我把10个节点都先放入到一个容器当中,最初我再把容器直接插入到文档就能够了!浏览器只回流了1次。
解析html

// 此办法是将传入的dom节点转为文档碎片,参数node是须要解析的html片段,vm是Vue构造函数实例化的对象。function nodeToFragment(node, vm){      // 创立一个文档碎片      var fragment = document.createDocumentFragment();      var child;      // 获取到node中的第一个子节点,当有子节点时,执行循环体      while(child = node.firstChild){        // appendChild会将参数中节点移除,因而此循环会将node中的节点一个个移除,挪动到fragment文档碎片中,直到node中没有节点,循环完结。        fragment.appendChild(child);      }      // 此处fragment曾经获取到node中所有节点,loopNode函数用来循环每一层的节点。      loopNode(fragment.childNodes, vm);      return fragment;    }    function loopNode(nodes, vm){      //此处传入的nodes是一个类数组,应用Array.from()办法将其转化为数组。      Array.from(nodes).forEach((node) => {        // 此处失去的node是nodes中的间接子节点,compile函数是用来解析这些节点,如果是元素节点,解析是否有v-model指令,如果是文本节点,解析是否有{{}}。        compile(node, vm);        // 如果node还有子节点,则持续解析        if(node.childNodes.length>0){          loopNode(node.childNodes, vm);        }      })  }function compile(node, vm){      // 如果是元素节点      if(node.nodeType === 1){        // 取得元素节点上所有的属性,以键值对的形式存储在attrs中,attrs属于类数组        var attrs = node.attributes;        Array.from(attrs).forEach(element => {          if(element.nodeName == "v-model"){            var name = element.nodeValue;            // 初始化带有v-model指令的元素的值            node.value = vm.data[name];          }        });      }      // 正则匹配到文本中有{{}}的文本      var reg = /\{\{([^}]*)\}\}/g;      var textContent = node.textContent;      // 如果是文本节点且文本中带有{{}}的节点      if(node.nodeType === 3 && reg.test(textContent)){        // 将文本内容寄存在以后节点的自定义属性上        node.my = textContent;        // 此处node.textContent 和 node.my的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无奈匹配到{{}},replace办法用来替换文本中的{{name}}和{{passWord}}。        node.textContent = node.my.replace(reg, function(){          var attr = arguments[0].slice(2,arguments[0].length-2);           return vm.data[attr];        })      }    }

下面咱们曾经实现了将data中的属性填充到页面中,接下来咱们须要做的是,当data中属性值发生变化时,咱们须要监听到数据的变动,Vue中对数据监听应用的是Object.defineProperty(data,key,val)办法(不分明该办法的可查阅),Object.defineProperty(data,key,val)能够监听对象属性的变动,当获取data中某个属性的值时,会调用该属性的get()办法,当批改某个属性的值时会调用以后属性的set()办法。

    function observe(data){      if(typeof data != 'object' || !data){        return      }      Object.keys(data).forEach((key)=>{        defineReactive(data, key, data[key]);      })    }    function defineReactive(data, key, val){      // data中子属性是对象时,持续监听      observe(val);      Object.defineProperty(data, key, {        get: function(){          return val;        },        set: function(newVal){          if(newVal !== val){            val = newVal;          } else {            return;          }        }      })    }

批改Vue构造函数如下,当实例化Vue时,实现了对数据data的监听,并解析模板,将data中对应属性填充到页面中。

然而,当data中属性值发生变化时,页面并不会更新,那接下来咱们须要解决的就是,当data中属性发生变化时,自动更新视图,视图发生变化时,被动更新数据,连贯视图和数据咱们须要在定义一个构造函数Watcher。
首先咱们来思考下当data中name属性发生变化时,咱们须要更新的视图有如下三个节点,一个元素节点和两个文本节点

当data中passWord属性变动时,须要更新的视图有两个节点

也就是说,当某个属性发生变化时,咱们可能要更新多个视图,那咱们如何去定位须要更新那些节点了?因而咱们须要将绑定了data中属性的节点保留到一个数组中,当data中对应属性发生变化时,循环数组,拿到节点,执行更新办法。
回顾一下咱们compile中的代码,下图中标记1、2处就是获取到data中的属性名。接下来咱们定义一个Watcher构造函数,在解析模板时实例化Watcher,3、4处是新增代码,实例化Watcher。

Watcher构造函数中有两个办法,一个update办法和一个get办法,实例化Watcher时调用Watcher中的get办法,此办法会触发data中对应的属性的get办法。

    function Watcher(vm, node, name){      this.vm = vm;      this.node = node;      this.name = name;      this.value = this.get();    }        Watcher.prototype.get = function(){      //触发data中属性get办法前将以后实例化对象存入target属性中      Dep.target = this;      //取data中的this.name属性,会触发该属性的get办法      var value = this.vm.data[this.name];      Dep.target = null;      return value;    }        Watcher.prototype.update = function(){      }

上文中提到,咱们须要定义数组来存储对应属性的节点,也就是说,data中每个属性都必须有一个数组来存储节点,上面咱们来定义一个Dep构造函数,用来收集节点。

    function Dep(){      // 寄存Watcher的实例对象      this.subs = [];    }     Dep.prototype.addSub = function(sub){      this.subs.push(sub);    }    Dep.prototype.notify = function(){      this.subs.forEach((sub)=>{        sub.update();      })    }

每个属性都须要一个数组,因而咱们在监听data属性时实例化Dep,Dep的实例在闭包的状况下创立,咱们能够批改数据监听中的get办法,上文在实例化Watcher时,触发get办法,将Watcher的实例存入数组中,当批改data中属性值时,调用set办法,Dep实例对象调用notify办法,实现更新。

    //批改后的defineReactive办法    function defineReactive(data, key, val){      //为每个属性创立一个Dep实例      var dep = new Dep();      observe(val);      Object.defineProperty(data, key, {        get: function(){          //实例化Watcher时,触发了get办法,此时Dep.target为Watcher实例化对象          Dep.target && dep.addSub(Dep.target);          return val;        },        set: function(newVal){          if(newVal !== val){            val = newVal;            // 当调用set办法时,告诉所有订阅者执行更新办法            dep.notify();          } else {            return;          }        }      })    }

实现更新办法

    function Watcher(vm, node, name){      ...    }        Watcher.prototype.get = function(){      ...    }        Watcher.prototype.update = function(){        if(this.node.nodeType === 1){            this.node.nodeValue = this.get();        } else {            this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function(){              var attr = arguments[0].slice(2,arguments[0].length-2);              return this.vm.data[attr];            })        }    }

实现到这里,咱们就曾经实现了数据变动时自动更新视图,咱们来梳理一下流程。就拿下面例子来说,当咱们执行vm.data['name'] = 'lisi'时,便会触发set办法,set办法中调用Dep实例的notify办法,此办法会遍历this.subs数组,这个数组中寄存的元素是Watcher的实例化对象,调用sub.update()办法便会更新视图。

当视图发生变化时,须要批改相应数据,只须要给相应节点绑定事件即可,批改compile办法如下,给相应节点减少input事件。

  if(node.nodeType === 1){    // 取得元素节点上所有的属性,以键值对的形式存储在attr中,attr属于类数组    var attr = node.attributes;    Array.from(attr).forEach(element => {      if(element.nodeName == "v-model"){        var name = element.nodeValue;        // 给带有v-model指令的元素绑定input事件        node.addEventListener('input', function(e){          vm.data[name] = e.target.value;        })        // 初始化带有v-model指令的元素的值        node.value = vm.data[name];        new Watcher(vm, node, name);      }    });  }

到这里双向数据绑定就实现了,上面附上残缺代码。

<!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>Vue双向数据绑定</title></head><body>  <div id="app">    用户名:<input type="text" v-model="name">    明码:<input type="text" v-model="passWord">    {{name}} {{passWord}}    <div>      <div>{{name}}</div>    </div>  </div>  <script>    function Vue(option) {      this.data = option.data;      this.id = option.el;      observe(this.data);      var dom = nodeToFragment(document.getElementById(this.id), this);      document.getElementById(this.id).appendChild(dom);    }    function nodeToFragment(node, vm) {      // 创立一个文档碎片      var fragment = document.createDocumentFragment();      var child;      // 获取到node中的第一个节点      while (child = node.firstChild) {        // appendChild会将传入的节点移除,因而此循环会将node中的节点一个个移除,挪动到fragment文档碎片中。        fragment.appendChild(child);      }      // console.dir(fragment);      loopNode(fragment.childNodes, vm);      return fragment;    }    function loopNode(nodes, vm) {      //此处传入的nodes是一个类数组,将其转化为数组      Array.from(nodes).forEach((node) => {        compile(node, vm);        // 如果node还有子节点,则持续解析        if (node.childNodes.length > 0) {          loopNode(node.childNodes, vm);        }      })    }    function compile(node, vm) {      // 如果是元素节点      if (node.nodeType === 1) {        // 取得元素节点上所有的属性,以键值对的形式存储在attr中,attr属于类数组        var attr = node.attributes;        Array.from(attr).forEach(element => {          if (element.nodeName == "v-model") {            var name = element.nodeValue;            // 给带有v-model指令的元素绑定input工夫            node.addEventListener('input', function (e) {              vm.data[name] = e.target.value;            })            // 初始化带有v-model指令的元素的值            node.value = vm.data[name];            new Watcher(vm, node, name);          }        });      }      // 正则匹配到文本中有{{}}的文本      var reg = /\{\{([^}]*)\}\}/g;      var textContent = node.textContent;      // 如果是文本节点且文本中带有{{}}的节点      if (node.nodeType === 3 && reg.test(textContent)) {        // 将文本内容寄存在以后节点的自定义属性上        node.my = textContent;        // 此处node.textContent 和 node.my的文本一样,如果上一步不将文本存储到自定义属性中,那么下次将无奈匹配到{{}}。        node.textContent = node.my.replace(reg, function () {          var attr = arguments[0].slice(2, arguments[0].length - 2);          new Watcher(vm, node, attr);          return vm.data[attr];        })      }    }    function observe(data) {      if (typeof data != 'object' || !data) {        return      }      Object.keys(data).forEach((key) => {        defineReactive(data, key, data[key]);      })    }    function defineReactive(data, key, val) {      var dep = new Dep();      observe(val);      Object.defineProperty(data, key, {        get: function () {          Dep.target && dep.addSub(Dep.target);          return val;        },        set: function (newVal) {          if (newVal !== val) {            val = newVal;            dep.notify();          } else {            return;          }        }      })    }    function Dep() {      this.subs = [];    }    Dep.prototype.addSub = function (sub) {      this.subs.push(sub);    }    Dep.prototype.notify = function () {      this.subs.forEach((sub) => {        sub.update();      })    }    function Watcher(vm, node, name) {      this.vm = vm;      this.node = node;      this.name = name;      this.value = this.get();    }    Watcher.prototype.get = function () {      Dep.target = this;      var value = this.vm.data[this.name];      Dep.target = null;      return value;    }    Watcher.prototype.update = function () {      if (this.node.nodeType === 1) {        this.node.nodeValue = this.get();      } else {        this.node.textContent = this.node.my.replace(/\{\{([^}]*)\}\}/g, function () {          var attr = arguments[0].slice(2, arguments[0].length - 2);          return this.vm.data[attr];        })      }    }    var vm = new Vue({      el: "app",      data: {        name: "lishibo",        passWord: "123456",        obj: {          obj1: 'obj1'        },        arr: ['arr1', 'arr2']      }    })  </script></body></html>