vue双向绑定的法则及贯彻双向绑定MVVM源码分析上

动手实践

我们了解了Object.defineProperty和发布订阅者模式后,我们不难可以想到,vue.js是基于以上两者来实现数据监听的。

  1. vue.js首先通过Object.defineProperty来对要监听的数据进行getter和setter劫持,当数据的属性被赋值/取值的时候,vue.js就可以察觉到并做相应的处理。
  2. 通过订阅发布模式,我们可以为对象的每个属性都创建一个发布者,当有其他订阅者依赖于这个属性的时候,则将订阅者加入到发布者的队列中。利用Object.defineProperty的数据劫持,在属性的setter调用的时候,该属性的发布者通知所有订阅者更新内容。

接下来,我们来动手实现(详情可以看注释):

class Observer {
  constructor(data) {
    // 如果不是对象,则返回
    if (!data || typeof data !== 'object') {
      return;
    }
    this.data = data;
    this.walk();
  }

  // 对传入的数据进行数据劫持
  walk() {
    for (let key in this.data) {
      this.defineReactive(this.data, key, this.data[key]);
    }
  }
  // 创建当前属性的一个发布实例,使用Object.defineProperty来对当前属性进行数据劫持。
  defineReactive(obj, key, val) {
    // 创建当前属性的发布者
    const dep = new Dep();
    /*
    * 递归对子属性的值进行数据劫持,比如说对以下数据
    * let data = {
    *   name: 'cjg',
    *   obj: {
    *     name: 'zht',
    *     age: 22,
    *     obj: {
    *       name: 'cjg',
    *       age: 22,
    *     }
    *   },
    * };
    * 我们先对data最外层的name和obj进行数据劫持,之后再对obj对象的子属性obj.name,obj.age, obj.obj进行数据劫持,层层递归下去,直到所有的数据都完成了数据劫持工作。
    */
    new Observer(val);
    Object.defineProperty(obj, key, {
      get() {
        // 若当前有对该属性的依赖项,则将其加入到发布者的订阅者队列里
        if (Dep.target) {
          dep.addSub(Dep.target);
        }
        return val;
      },
      set(newVal) {
        if (val === newVal) {
          return;
        }
        val = newVal;
        new Observer(newVal);
        dep.notify();
      }
    })
  }
}

// 发布者,将依赖该属性的watcher都加入subs数组,当该属性改变的时候,则调用所有依赖该属性的watcher的更新函数,触发更新。
class Dep {
  constructor() {
    this.subs = [];
  }

  addSub(sub) {
    if (this.subs.indexOf(sub) < 0) {
      this.subs.push(sub);
    }
  }

  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    })
  }
}

Dep.target = null;

// 观察者
class Watcher {
  /**
   *Creates an instance of Watcher.
   * @param {*} vm
   * @param {*} keys
   * @param {*} updateCb
   * @memberof Watcher
   */
  constructor(vm, keys, updateCb) {
    this.vm = vm;
    this.keys = keys;
    this.updateCb = updateCb;
    this.value = null;
    this.get();
  }

  // 根据vm和keys获取到最新的观察值
  get() {
    Dep.target = this;
    const keys = this.keys.split('.');
    let value = this.vm;
    keys.forEach(_key => {
      value = value[_key];
    });
    this.value = value;
    Dep.target = null;
    return this.value;
  }

  update() {
    const oldValue = this.value;
    const newValue = this.get();
    if (oldValue !== newValue) {
      this.updateCb(oldValue, newValue);
    }
  }
}

let data = {
  name: 'cjg',
  obj: {
    name: 'zht',
  },
};

new Observer(data);
// 监听data对象的name属性,当data.name发现变化的时候,触发cb函数
new Watcher(data, 'name', (oldValue, newValue) => {
  console.log(oldValue, newValue);
})

data.name = 'zht';

// 监听data对象的obj.name属性,当data.obj.name发现变化的时候,触发cb函数
new Watcher(data, 'obj.name', (oldValue, newValue) => {
  console.log(oldValue, newValue);
})

data.obj.name = 'cwc';
data.obj.name = 'dmh';

上海时时乐走势图官网 1

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

从很久之前就已经接触过了angularjs了,当时就已经了解到,angularjs是通过脏检查来实现数据监测以及页面更新渲染。之后,再接触了vue.js,当时也一度很好奇vue.js是如何监测数据更新并且重新渲染页面。今天,就我们就来一步步解析vue.js响应式的原理,并且来实现一个简单的demo。

var updater = {
  textUpdater: function(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value;
  },
  htmlUpdater: function(node, value) {
    node.innerHTML = typeof value === 'undefined' ? '' : value;
  },
  classUpdater: function(node, newValue, oldValue) {
    var className = node.className;
    className = className.replace(oldValue, '').replace(/s$/, '');
    var space = className && String(newValue) ? ' ' : '';
    node.className = className   space   newValue;
  },
  modelUpdater: function(node, newValue) {
    node.value = typeof newValue === 'undefined' ? '' : newValue;
  }
};

上面例子直接用了dom操作改变了文本节点的值,而且是在我们知道是哪个id的情况下,通过document.getElementById 获取到相应的文本节点,然后直接修改文本节点的值,这种做法是最简单粗暴的。

发布订阅模式

发布订阅模式是设计模式中比较常见的一种,其中有两个角色:发布者和订阅者。多个订阅者可以向同一发布者订阅一个事件,当事件发生的时候,发布者通知所有订阅该事件的订阅者。我们来看一个例子了解下。

class Dep {
  constructor() {
    this.subs = [];
  }
  // 增加订阅者
  addSub(sub) {
    if (this.subs.indexOf(sub) < 0) {
      this.subs.push(sub);
    }
  }
  // 通知订阅者
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    })
  }
}

const dep = new Dep();

const sub = {
  update() {
    console.log('sub1 update')
  }
}

const sub1 = {
  update() {
    console.log('sub2 update');
  }
}

dep.addSub(sub);
dep.addSub(sub1);
dep.notify(); // 通知订阅者事件发生,触发他们的更新函数

调用 defineReactive  代码如下:

虽然实现的比较简单,有很多功能没有考虑,不过这个过程还是可以理解到Vue实现双向绑定的原理。过程中,有思考:

结语

这样,一个简单的响应式数据监听就完成了。当然,这个也只是一个简单的demo,来说明vue.js响应式的原理,真实的vue.js源码会更加复杂,因为加了很多其他逻辑。

接下来我可能会将其与html联系起来,实现v-model、computed和{{}}语法。代码地址 有兴趣的欢迎来一起研究探讨下。点击这里查看第二节的内容。如果觉得有收获的话也请点个赞,嘿嘿。

public: function() {
    this.subs.forEach(function(sub){
      sub.update();
    });
  }

然后定义订阅者Watcher

Object.defineProperty

es5新增了Object.defineProperty这个api,它可以允许我们为对象的属性来设定getter和setter,从而我们可以劫持用户对对象属性的取值和赋值。比如以下代码:

const obj = {
};

let val = 'cjg';
Object.defineProperty(obj, 'name', {
  get() {
    console.log('劫持了你的取值操作啦');
    return val;
  },
  set(newVal) {
    console.log('劫持了你的赋值操作啦');
    val = newVal;
  }
});

console.log(obj.name);
obj.name = 'cwc';
console.log(obj.name);

我们通过Object.defineProperty劫持了obj[name]的取值和赋值操作,因此我们就可以在这里做一些手脚啦,比如说,我们可以在obj[name]被赋值的时候触发更新页面操作。

从而更新视图。

二、Object.defineProperty()

首先,先让我们来了解一些基础知识。

key 分别为 someStr, className, htmlStr, child,
data[key]值分别为:

上海时时乐走势图官网 2

基础知识

四: 实现MVVM
MVVM是数据绑定的入口,整合 Observer, Compile, 和 Watcher,通过Observer来监听model数据变化,通过Compile来解析编译模板指令,最后使用watcher搭起Observer和Compile之间的通信桥梁。 达到数据变化 --》视图更新,视图变化 --》 数据model更新的 双向绑定效果。
代码如下:

上海时时乐走势图官网 3

执行完setVMVal方法后,会拿到最新值,然后会调用 Object.defineProperty 里面的setter方法,如下代码:

  var obj = {};
  Object.defineProperty(obj, 'hello', {
    get: function() {
      console.log('get val:'  val);
      return val;
     },
    set: function(newVal) {
      val = newVal;
      console.log('set val:'  val);
    }
  });
  obj.hello;
  obj.hello='111';

页面初始化的时候,vm.data.child.someStr 会被mvvm.js 里面的 代理方法转换成 vm.child.someStr, 因此给 vm.child.someStr 设置新值的时候,会调用 Object.defineProperty方法来监听属性值的改变,因此需要返回一个新值,所以先调用mvvm.js中的get方法,之后会调用 Observer.js代码 的Object.defineProperty(obj, key, {})中的get方法,返回页面初始化的值,然后会调用Observer.js该对应的set方法,获取新值,然后判断新旧值是否相等,如果不相等的话,就会把消息发布出去该订阅者,订阅者会接收该消息。从而和上面 视图 -》模型 步骤一样 渲染视图页面。
git代码

实现效果如下:

update: function() {
  this.run();
},

上海时时乐走势图官网 4

因此 参数options值为对象(Object):

至此,比较简单地实现了我们第三步用dom操作实现的双向绑定效果,代码:

<!DOCTYPE html>
 <html>
  <head>
    <meta charset="utf-8">
    <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    <meta content="telephone=no" name="format-detection">
    <meta content="email=no" name="format-detection">
    <title>标题</title>
    <link rel="shortcut icon" href="/favicon.ico">
  </head>
  <body>
    <script>
      function Observer(data) {
        this.data = data;
        this.init();
      }
      Observer.prototype = {
        init: function() {
          var self = this;
          var data = self.data;
          // 遍历data对象
          Object.keys(data).forEach(function(key) {
            self.defineReactive(data, key, data[key]);
          });
        },
        defineReactive: function(data, key, value) {

          // 递归遍历子对象
          var childObj = observer(value);

          // 对对象的属性使用Object.defineProperty进行监听
          Object.defineProperty(data, key, {
            enumerable: true,  // 可枚举
            configurable: false, // 不能删除目标属性或不能修改目标属性
            get: function() {
              return value;
            },
            set: function(newVal) {
              if (newVal === value) {
                return;
              }
              console.log('已经监听到值的变化了', value, '==>', newVal);
              value = newVal;
            }
          });
        }
      }

      function observer(value) {
        if (!value || typeof value !== 'object') {
          return;
        }
        return new Observer(value);
      }

      // 测试demo
      var data = {name: 'kongzhi'};
      observer(data);
      data.name = 'kongzhi2';  // 控制台打印出  已经监听到值的变化了,kongzhi ==> kongzhi2
    </script>
  </body>
</html>

三、实现简单的双向绑定

查看效果

您可能感兴趣的文章:

  • vue数据双向绑定的注意点
  • Vue2.0利用 v-model 实现组件props双向绑定的优美解决方案
  • vue.js利用Object.defineProperty实现双向绑定
  • vue数据双向绑定原理解析(get & set)
  • vue2.0数据双向绑定与表单bootstrap vue组件
  • 深入理解vue.js双向绑定的实现原理
  • Vue2实现组件props双向绑定
  • Vue.js第一天学习笔记(数据的双向绑定、常用指令)
  • Angular和Vue双向数据绑定的实现原理(重点是vue的双向绑定)
  • Vue 实现双向绑定的四种方法
function Observer(data) {
  this.data = data;
  this.init();
}
var element = {
 tagName: 'ul', // 节点标签名
 props: { // DOM的属性,用一个对象存储键值对
  id: 'list'
 },
 children: [ // 该节点的子节点
  {tagName: 'li', props: {class: 'item'}, children: ["Item 1"]},
  {tagName: 'li', props: {class: 'item'}, children: ["Item 2"]},
  {tagName: 'li', props: {class: 'item'}, children: ["Item 3"]},
 ]
}

接着会调用run方法,代码如下:

六、订阅发布模式

vue是通过数据劫持的方式来做数据绑定的,最核心的方法是通过 Object.defineProperty()方法来实现对属性的劫持,达到能监听到数据的变动。要实现数据的双向绑定,需要实现如下几点
1. 需要实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动拿到最新值并通知订阅者。

上海时时乐走势图官网 5

从而发布消息, 执行方法 dep.public(); 因此会调用Dep里面的public方法,代码如下:

首先我们需要获取文本中真实的dom节点,然后再分析节点的类型,根据节点类型做相应的处理。

node.addEventListener('input', function(e) {
    var newValue = e.target.value;
    if (val === newValue) {
      return;
    }
    self.setVMVal(vm, attrValue, newValue);
    val = newValue;
 });
<!DOCTYPE html>
 <head></head>
 <body>
 <div id="app">
  <input type="text" id="a">

 </div>

 <script type="text/javascript">
  var obj = {};
  Object.defineProperty(obj, 'hello', {
    get: function() {
      console.log('get val:'  val);
      return val;
    },
    set: function(newVal) {
      val = newVal;
      console.log('set val:'  val);
      document.getElementById('a').value = val;
      document.getElementById('b').innerHTML = val;
    }
  });
  document.addEventListener('keyup', function(e) {
   obj.hello = e.target.value;
  });
  </script>
 </body>
</html>
_initComputed: function() {
  var self = this;
  var computed = this.$options.computed;
  if (typeof computed === 'object') {
    Object.keys(computed).forEach(function(key) {
      Object.defineProperty(self, key, {
        get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
        set: function() {}
      })
    })
  }
}
  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

理清思路:
视图 --> 模型的改变:

模板

如下图所示:

最后需要订阅的地方

options.el参数就是传进去的Id为 #mvvm-app, 如果没有的话,就是 document.body;
compile实列化代码如下:

由此可见,Object.defineProperty() 这个API是Vue实现双向数据绑定的关键,我们先简单了解下这个API,了解更多戳这里

一: 实现Observer
我们可以使用Object.defineProperty()来监听属性的变动,我们需要对属性对象进行递归遍历,包括子属性对象的属性。再加上getter和setter方法,我们可以和上面的demo一样,监听input值的变化,即 视图 -> 模型。且当我们给某个对象的属性赋值的话,会自动调用setter方法,来动态修改数据,即 模型 -> 视图。
引入我们可以使用 object.defineProperty来实现监听属性的变动,如下代码:

<ul id='list'>
 <li class='item'>Item 1</li>
 <li class='item'>Item 2</li>
 <li class='item'>Item 3</li>
</ul>

compile做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数。

到这,我们做到了获取文本中真实的dom节点,然后分析节点的类型,并能处理节点中相应的变量如上面代码中的{{text}},最后渲染到页面中。接着我们需要和双向绑定联系起来,实现{{text}}响应式的数据绑定。

调用init方法会进行compileElement 执行解析编译操作,找到id为#mvvm-app的childNodes, 遍历子节点首先会判断是元素节点还是文本节点,如果是元素节点会调用 该如下方法:
this.compile(node); 进行元素节点编译和解析操作, 如果是文本节点的话,就会 调用 this.compileText(node, RegExp.$1); 方法编译文本节点。

2.Compile模块对v-model节点的解析,事件的绑定,我只实现简单的,特定的v-model,还有其它事件绑定如v-on等没有分析,看了别人的代码,情况一多起来,看得就有些吃力,希望后面自己会再来完善,给自己定一个这样的框架在这.

三: 实现Watcher
Watcher是订阅者是作为 Observer和Compile之间通信的桥梁。
相关代码如下:

添加订阅者Watcher到主题对象Dep,发布者发出通知放到属性监听里面

parseGetter: function(exp) {
    if (/[^w.$]/.test(exp)) return; 
    var exps = exp.split('.');
    return function(obj) {
      for (var i = 0, len = exps.length; i < len; i  ) {
        if (!obj) return;
        obj = obj[exps[i]];
      }
      return obj;
    }
}
<!DOCTYPE html>
 <head></head>
 <body>
 <div id="app">
  <input type="text" id="a" v-model="text">
  {{text}}
 </div>
 <script type="text/javascript">
  function Compile(node, vm) {
   if(node) {this.$frag = this.nodeToFragment(node, vm);
    return this.$frag;
   }
  }
  Compile.prototype = {
   nodeToFragment: function(node, vm) {
    var self = this;
    var frag = document.createDocumentFragment();
    var child;

    while(child = node.firstChild) {
     self.compileElement(child, vm);
     frag.append(child); // 将所有子节点添加到fragment中,child是指向元素首个子节点的引用。将child引用指向的对象append到父对象的末尾,原来child引用的对象就跳到了frag对象的末尾,而child就指向了本来是排在第二个的元素对象。如此循环下去,链接就逐个往后跳了
    }
    return frag;
   },
   compileElement: function(node, vm) {
    var reg = /{{(.*)}}/;

    //节点类型为元素
    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.addEventListener('input', function(e) {
        // 给相应的data属性赋值,进而触发该属性的set方法
         vm.data[name]= e.target.value;
       });
       node.value = vm.data[name]; // 将data的值赋给该node
       node.removeAttribute('v-model');
      }
     };
    }
    //节点类型为textif(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();
      node.nodeValue = vm.data[name]; // 将data的值赋给该node
     }
    }
   },
  }
   function Vue(options) {
   this.data = options.data;
   var data = this.data;
   var id = options.el;
   var dom =new Compile(document.getElementById(id),this);
   // 编译完成后,将dom返回到app中
   document.getElementById(id).appendChild(dom);
  }
  var vm = new Vue({
   el: 'app',
   data: {
    text: 'hello world'
   }
  });
 </script>
 </body>
</html>

脏值检查:是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval()定时轮询检测数据的变动。

之前有听说用VDOM,在Vue源代码中,也找过是否有创建文档碎片,结果没找到。看了参考资料4中,VDOM的介绍,好像是把节点用JS对象模拟。类似:

// 具体看如下代码,代码中有对应的注释

/*
 * @param {el} 元素节点容器 如:el: '#mvvm-app'
 * @param {vm} Object 对象传递数据 
*/
function Compile(el, vm) {
  this.$vm = vm;
  // 判断是元素节点 还是 选择器
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    /*
     因为遍历解析过程有多次操作dom节点,为了提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,
     解析完成后,再将fragment添加回原来真实的dom节点中。有关 DocumentFragment 请看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
     */
    this.$fragment = this.node2Fragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}

Compile.prototype = {
  node2Fragment: function(el) {
    var fragment = document.createDocumentFragment(),
      child;
    // 将原生节点拷贝到fragment中
    while (child = el.firstChild) {
      fragment.appendChild(child);
    }
    return fragment;
  },
  init: function() {
    // 进行解析编译操作
    this.compileElement(this.$fragment);
  },
  compileElement: function(el) {
    var childNodes = el.childNodes;
    // 遍历所有的子节点 判断子节点是 元素节点还是文本节点 分别进行编译解析操作
    [].slice.call(childNodes).forEach(function(node) {

      // 获取节点的文本内容和它的所有后代
      var text = node.textContent;

      // 正则匹配 {{xx}} 这样的xx文本值
      var reg = /{{(.*)}}/;
      // 判断是否是元素节点,然后进行编译
      if (this.isElementNode(node)) {
        this.compile(node);

      } else if(this.isTextNode(node) && reg.test(text)) {
        // 判断node是否是文本节点 且 符合正则匹配的 {{xx}} 那么就会进行编译解析
        this.compileText(node, RegExp.$1);
      }

      // 如果该节点还有子节点的话,那么递归进行判断编译
      if (node.childNodes && node.childNodes.length) {
        this.compileElement(node);
      }
    });
  },
  // 元素节点编译
  compile: function(node) {
    // 获取节点的所有属性
    var nodeAttrs = node.attributes;
    var self = this;
    /*
     遍历该节点的所有属性,判断该属性是事件指令还是普通指令
     */
    [].slice.call(nodeAttrs).forEach(function(attr) {
      // 获取属性名
      var attrName = attr.name;
      // 先判断属性名是否是 以 v- 开头的
      if (self.isDirective(attrName)) {
        var attrValue = attr.value;
        // 获取 v-xx 中从xx开始的所有字符串
        var dir = attrName.substring(2);
        // 判断是否是事件指令
        if (self.isEventDirective(dir)) {
          compileUtil.eventHandler(node, self.$vm, attrValue, dir);
        } else {
          // 普通指令
          compileUtil[dir] && compileUtil[dir](node, self.$vm, attrValue);
        }
        // 循环完成一次后 删除该属性
        node.removeAttribute(attrName);
      }
    });
  },
  // 编译文本
  compileText: function(node, exp) {
    compileUtil.text(node, this.$vm, exp);
  },
  // 是否是v- 开始的指令
  isDirective: function(attrName) {
    return attrName.indexOf('v-') === 0;
  },
  /*
   * 是否是事件指令 事件指令以 v-on开头的
   */
   isEventDirective: function(dir) {
     return dir.indexOf('on') === 0;
   },
   // 是否是元素节点
   isElementNode: function(node) {
     return node.nodeType === 1;
   },
   // 是否是文本节点
   isTextNode: function(node) {
     return node.nodeType === 3;
   }
};

// 指令处理
var compileUtil = {
  text: function(node, vm, exp) {
    this.bind(node, vm, exp, 'text');
  },
  html: function(node, vm, exp) {
    this.bind(node, vm, exp, 'html');
  },
  /*
   * 普通指令 v-model 开头的,调用model方法
   * @param {node} 容器节点
   * @param {vm} 数据对象
   * @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
   */
  model: function(node, vm, exp) {
    this.bind(node, vm, exp, 'model');
    var self = this;
    var val = this.getVMVal(vm, exp);
    // 监听input的事件
    node.addEventListener('input', function(e) {
      var newValue = e.target.value;
      // 比较新旧值是否相同
      if (val === newValue) {
        return;
      }
      // 重新设置新值
      self.setVMVal(vm, exp, newValue);
      val = newValue;
    });
  },
  /*
   *  返回 v-mdoel = "xx" 中的xx的值, 比如data对象会定义如下:
    data: {
      "xx" : "111"
    }
    * @param {vm} 数据对象
    * @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
   */
  getVMVal: function(vm, exp) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(k) {
      val = val[k];
    });
    return val;
  },
  /*
    设置普通指令的值
    @param {vm} 数据对象
    @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
    @param {value} 新值
   */
  setVMVal: function(vm, exp, value) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(key, index) {
      // 如果不是最后一个元素的话,更新值
      /*
       数据对象 data 如下数据
       data: {
        child: {
          someStr: 'World !'
        }
      },
       如果 v-model="child.someStr" 那么 exp = ["child", "someStr"], 遍历该数组,
       val = val["child"]; val 先储存该对象,然后再继续遍历 someStr,会执行else语句,因此val['someStr'] = value, 就会更新到对象的值了。
       */
      if (i < exp.length - 1) {
        val = val[key];
      } else {
        val[key] = value;
      }
    });
  },
  class: function(node, vm, exp) {
    this.bind(node, vm, exp, 'class');
  },
  /* 
   事件处理
   @param {node} 元素节点
   @param {vm} 数据对象
   @param {attrValue} attrValue 属性值
   @param {dir} 事件指令的值 比如 v-on:click="xx" 那么dir就等 on:click
   */
  eventHandler: function(node, vm, attrValue, dir) {
    // 获取事件类型 比如dir=on:click 因此 eventType="click" 
    var eventType = dir.split(':')[1];
    /*
     * 获取事件的函数 比如 v-on:click="clickBtn" 那么就会对应vm里面的clickBtn 函数。
     * 比如 methods: {
        clickBtn: function(e) {
          console.log(11)
        }
      }
     */
    var fn = vm.$options.methods && vm.$options.methods[attrValue];
    if (eventType && fn) {
      // 如果有事件类型和函数的话,就绑定该事件
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  },
  /*
   @param {node} 节点
   @param {vm} 数据对象
   @param {exp} 正则匹配的值
   @param {dir} 字符串类型 比如 'text', 'html' 'model'
   */
  bind: function(node, vm, exp, dir) {
    // 获取updater 对象内的对应的函数
    var updaterFn = updater[dir   'Updater'];

    // 如有该函数的话就执行该函数 参数node为节点,第二个参数为 指令的值。 比如 v-model = 'xx' 那么返回的就是xx的值
    updaterFn && updaterFn(node, this.getVMVal(vm, exp));

    // 调用订阅者watcher
    new Watcher(vm, exp, function(newValue, oldValue) {
      updaterFn && updaterFn(node, newValue, oldValue);
    });
  }
};

var updater = {
  textUpdater: function(node, value) {
    node.textContent = typeof value === 'undefined' ? '' : value;
  },
  htmlUpdater: function(node, value) {
    node.innerHTML = typeof value === 'undefined' ? '' : value;
  },
  classUpdater: function(node, newValue, oldValue) {
    var className = node.className;
    className = className.replace(oldValue, '').replace(/s$/, '');
    var space = className && String(newValue) ? ' ' : '';
    node.className = className   space   newValue;
  },
  modelUpdater: function(node, newValue) {
    node.value = typeof newValue === 'undefined' ? '' : newValue;
  }
};

简单例子:

this.data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: 'red',
  child: {
    someStr: 'World !'
  }
}

七、总结

然后 遍历data对象,如下:

前端MVVM最令人激动的就是双向绑定机制了,实现双向数据绑定的做法大致有如下三种:

_proxyData: function(key, setter, getter) {
  var me = this;
  setter = setter || 
  Object.defineProperty(me, key, {
      configurable: false,
      enumerable: true,
      get: function proxyGetter() {
          return me._data[key];
      },
      set: function proxySetter(newVal) {
          me._data[key] = newVal;
      }
  });
}

关于双向绑定的实现,看了网上很多资料,开始看到是对Vue源码的解析,看的过程似懂非懂。后来找到参考资料1,然后自己跟着实现一遍,才理解许多。感谢这篇文章的作者,写的由浅入深,比较好理解。为了加深自己的理解,于是自己顺着这个思路写下这个笔记。本文主要了解了几种双向绑定的做法,然后先用原生JS,dom操作实现一个最简单双向绑定,在这个基础上进行改装,为减少dom操作,实现简单的Compile(编译HTML);接着为了实现数据监听,实现observe;最后为了实现数据的双向绑定实现订阅发布模式。

因此会实例化 Observer;代码如下:

<!DOCTYPE html>
 <head></head>
 <body>
 <div id="app">
  <input type="text" id="a" v-model="text">
  {{text}}
 </div>
 <script type="text/javascript">
  function Compile(node, vm) {
   if(node) {
    this.$frag = this.nodeToFragment(node, vm);
    return this.$frag;
   }
  }
  Compile.prototype = {
   nodeToFragment: function(node, vm) {
    var self = this;
    var frag = document.createDocumentFragment();
    var child;

    while(child = node.firstChild) {
     self.compileElement(child, vm);
     frag.append(child); // 将所有子节点添加到fragment中
    }
    return frag;
   },
   compileElement: function(node, vm) {
    var reg = /{{(.*)}}/;

    //节点类型为元素
    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.addEventListener('input', function(e) {
        // 给相应的data属性赋值,进而触发该属性的set方法
         vm[name]= e.target.value;
       });
       // node.value = vm[name]; // 将data的值赋给该node
       new Watcher(vm, node, name, 'value');
      }
     };
    }
    //节点类型为text
    if(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();
      // node.nodeValue = vm[name]; // 将data的值赋给该node
      new Watcher(vm, node, name, 'nodeValue');
     }
    }
   },
  }
  function Dep() {
   this.subs = [];
  }
  Dep.prototype = {
   addSub: function(sub) {
    this.subs.push(sub);
   },
   notify: function() {
    this.subs.forEach(function(sub) {
     sub.update();
    })
   }
  }
  function Watcher(vm, node, name, type) {
   Dep.target = this;
   this.name = name;
   this.node = node;
   this.vm = vm;
   this.type = type;
   this.update();
   Dep.target = null;
  }

  Watcher.prototype = {
   update: function() {
    this.get();
    this.node[this.type] = this.value; // 订阅者执行相应操作
   },
   // 获取data的属性值
   get: function() {
    this.value = this.vm[this.name]; //触发相应属性的get
   }
  }
  function defineReactive (obj, key, val) {
   var dep = new Dep();
   Object.defineProperty(obj, key, {
    get: function() {
      //添加订阅者watcher到主题对象Dep
     if(Dep.target) {
      // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
      dep.addSub(Dep.target);
     }
     return val;
    },
    set: function (newVal) {
     if(newVal === val) return;
     val = newVal;
     console.log(val);
     // 作为发布者发出通知
     dep.notify();
    }
   })
  }
  function observe(obj, vm) {
   Object.keys(obj).forEach(function(key) {
    defineReactive(vm, key, obj[key]);
   })
  }

   function Vue(options) {
   this.data = options.data;
   var data = this.data;
   observe(data, this);
   var id = options.el;
   var dom =new Compile(document.getElementById(id),this);
   // 编译完成后,将dom返回到app中
   document.getElementById(id).appendChild(dom);
  }
  var vm = new Vue({
   el: 'app',
   data: {
    text: 'hello world'
   }
  });
 </script>
 </body>
</html>
this.$options = {
  el: '#mvvm-app',
  data: {
    someStr: 'hello ',
    className: 'btn',
    htmlStr: 'red',
    child: {
      someStr: 'World !'
    }
  },
  computed: {
    getHelloWord: function() {
      return this.someStr   this.child.someStr;
    }
  },
  methods: {
    clickBtn: function(e) {
      var randomStrArr = ['childOne', 'childTwo', 'childThree'];
      this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
    }
  }
}

var data = this._data = this.$options.data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: 'red',
  child: {
    someStr: 'World !'
  }
}

思路:angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,angular只有在指定的事件触发时进入脏值检测,大致如下:

/*
    设置普通指令的值
    @param {vm} 数据对象
    @param {exp} 普通指令的值 比如 v-model="xx" 那么exp就等于xx
    @param {value} 新值
   */
  setVMVal: function(vm, exp, value) {
    var val = vm;
    exp = exp.split('.');
    exp.forEach(function(key, index) {
      // 如果不是最后一个元素的话,更新值
      /*
       数据对象 data 如下数据
       data: {
        child: {
          someStr: 'World !'
        }
      },
       如果 v-model="child.someStr" 那么 exp = ["child", "someStr"], 遍历该数组,
       val = val["child"]; val 先储存该对象,然后再继续遍历 someStr,会执行else语句,因此val['someStr'] = value, 就会更新到对象的值了。
       */
      if (index < exp.length - 1) {
        val = val[key];
      } else {
        val[key] = value;
      }
    });
  }

四、实现简单Compile

1. 代码初始化执行

上海时时乐走势图官网 6

data = {
  someStr: 'hello ',
  className: 'btn',
  htmlStr: 'red',
  child: {
    someStr: 'World !'
  }
}

一、实现双向绑定的做法

<div id="mvvm-app">
  <input type="text" v-model="someStr">
  <input type="text" v-model="child.someStr">
  <p>{{getHelloWord}}</p>
  <p v-html="child.htmlStr"></p>
  <button v-on:click="clickBtn">change model</button>
</div>

思路:使用自定义的data属性在HTML代码中指明绑定。所有绑定起来的JavaScript对象以及DOM元素都将“订阅”一个发布者对象。任何时候如果JavaScript对象或者一个HTML输入字段被侦测到发生了变化,我们将代理事件到发布者-订阅者模式,这会反过来将变化广播并传播到所有绑定的对象和元素。

get: function() {
    Dep.target = this;
    var value = this.getter.call(this.vm, this.vm);
    Dep.target = null;
    return value;
},

上海时时乐走势图官网 7

// 事件处理
eventHandler: function(node, vm, attrValue, dir) {
  var eventType = dir.split(':')[1],
    fn = vm.$options.methods && vm.$options.methods[attrValue];
    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
}

需要监控data的属性值,这个对象的某个值赋值,就会触发setter,这样就能监听到数据变化。然后注意vm.data[name]属性将改为vm[name]

/*
 * @param {vm} 数据对象
 * @param {expOrFn} 属性值 比如 v-model='xx' v-on:click='yy' v-text="tt" 中的 xx, yy, tt 
 * @param {cb}  回调函数
 */
function Watcher(vm, expOrFn, cb) {
  this.vm = vm;
  this.expOrFn = expOrFn;
  this.cb = cb;
  this.depIds = {};

  // expOrFn 是事件函数的话
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  }
  // 为了触发属性getter,从而在dep添加自己作为订阅者
  this.value = this.get();
}

Watcher.prototype = {
  update: function() {
    this.run();    // observer 中的属性值发生变化 收到通知
  },
  run: function() {
    var value = this.get();
    var oldValue = this.value;
    // 新旧值不相等的话
    if (value !== oldValue) {
      // 把当前的值赋值给 this.value 更新this.value的值
      this.value = value;
      this.cb.call(this.vm, value, oldValue);  // 执行Compile中绑定的回调 更新视图
    }
  },
  get: function() {
    Dep.target = this; // 将当前订阅者指向自己
    var value = this.getter.call(this.vm, this.vm); // 触发getter,添加自己到属性订阅器中
    Dep.target = null;  // 添加完成后 清空数据
    return value;
  },
  parseGetter: function(exp) {
    var reg = /[^w.$]/;
    if (reg.test(exp)) {
      return;
    }
    var exps = exp.split('.');
    return function(obj) {
      for(var i = 0, len = exps.length; i < len; i  ) {
        if (!obj) {
          return;
        }
        obj = obj[exps[i]];
      }
      return obj;
    }
  }
}

结果:

function MVVM(options) {
  this.$options = options || {};
  var data = this._data = this.$options.data;
  var self = this;
  /*
   数据代理,实现 vm.xxx 
   上面的代码看出 监听数据的对象是 options.data, 因此每次更新视图的时候;如:
   var vm = new MVVM({
     data: {name: 'kongzhi'}
   });
   那么更新数据就变成 vm._data.name = 'kongzhi2'; 但是我们想实现这样更改 vm.name = "kongzhi2";
   因此这边需要使用属性代理方式,利用Object.defineProperty()方法来劫持vm实列对象属性的读写权,使读写vm实列的属性转成vm.name的属性值。
   */
   Object.keys(data).forEach(function(key) {
     self._proxyData(key);
   });

   this._initComputed();

   // 初始化Observer
   observer(data, this);

   // 初始化 Compile
   this.$compile = new Compile(options.el || document.body, this);
}

到这,虽然set方法触发了,但是文本节点{{text}}的内容没有变化,要让绑定的文本节点同步变化,我们需要引入订阅发布模式。

因此 this.data值为:

如果去掉 obj.hello=‘111' 这行代码,则get的返回值val会报错val is not defined。可见Object.defineProperty() 监控对数据的操作,可以自动触发数据同步。下面我们先用Object.defineProperty()来实现一个非常简单的双向绑定。

首先html代码如下:

恩,这就又牵扯出模板了。先收住,我先尽量把简单的搞懂。

数据劫持:vue.js 则是采用数据劫持结合发布者-订阅者模式,通过Object.defineProperty()来劫持各个属性的setter,getter。在数据变动时发布消息给订阅者,触发响应的监听回调。

上海时时乐走势图官网 8

接着调用 parseGetter 方法代码:

五、实现简单observe

遍历data,获取每一个key,也就是 key 可以为 someStr, className, htmlStr, child等。
然后执行 self.defineReactive 方法,参数如下三个

首先我们要一个收集订阅者的容器,定义一个Dep作为主题对象

vue双向绑定的原理及实现双向绑定MVVM源码分析

上海时时乐走势图官网 9

从上面的分析得到,需要实现一个Watcher,作为链接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,所以订阅者就是Watcher,var dep = new Dep();是在 defineReactive方法内部定义的,是想通过dep添加订阅者,通过Dep定义一个全局的target属性,暂存watcher,添加完后会移除,因此最后一句代码设置 Dep.target = null;
执行getter方法会返回值,执行setter方法会判断新旧值是否相等,如果不相等的话,再次递归遍历设置的对象的所有子对象,然后会通知所有的订阅者。

2.脏值检查(angular.js)

_proxyData 方法代码如下:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

<!DOCTYPE html>
 <html>
  <head>
    <meta charset="utf-8">
    <meta content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no" name="viewport">
    <meta content="yes" name="apple-mobile-web-app-capable">
    <meta content="black" name="apple-mobile-web-app-status-bar-style">
    <meta content="telephone=no" name="format-detection">
    <meta content="email=no" name="format-detection">
    <title>标题</title>
    <link rel="shortcut icon" href="/favicon.ico">
  </head>
  <body>
    <h3>使用Object.defineProperty实现简单的双向数据绑定</h3>
    <input type="text" id="input" />
    <div id="div"></div>
    <script>
        var obj = {};
        var inputVal = document.getElementById("input");
        var div = document.getElementById("div");

        Object.defineProperty(obj, "name", {
          set: function(newVal) {
            inputVal.value = newVal;
            div.innerHTML = newVal;
          }
        });
        inputVal.addEventListener('input', function(e){
          obj.name = e.target.value;
        });
    </script>
  </body>
</html>

1.发布者-订阅者模式(backbone.js)

_proxyData: function(key) {
  var self = this;
  Object.defineProperty(self, key, {
    configurable: false,  // 是否可以删除或修改目标属性
    enumerable: true,   // 是否可枚举
    get: function proxyGetter() {
      return self._data[key];
    },
    set: function proxySetter(newVal) {
      self._data[key] = newVal;
    }
  })
}

封装成一个框架,肯定不能是这种做法,所以我们需要一个解析dom,并能修改dom中相应的变量的模块。

/*
 * @param {el} 元素节点容器 如:el: '#mvvm-app'
 * @param {vm} Object 对象传递数据 
*/
function Compile(el, vm) {
  this.$vm = vm;
  // 判断是元素节点 还是 选择器
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);
  if (this.$el) {
    /*
     因为遍历解析过程有多次操作dom节点,为了提高性能和效率,会先将根节点el转换成文档碎片fragment进行解析编译操作,
     解析完成后,再将fragment添加回原来真实的dom节点中。有关 DocumentFragment 请看 http://www.cnblogs.com/tugenhua0707/p/7465915.html
     */
    this.$fragment = this.node2Fragment(this.$el);
    this.init();
    this.$el.appendChild(this.$fragment);
  }
}

简单的observe定义如下:

new Watcher(vm, attrValue, function(value, oldValue) {
    updaterFn && updaterFn(node, value, oldValue);
});

结果:

所以会对事件进行处理: 如下函数:

完整代码如下:

首先会获取当前的值,然后执行 setVMVal该方法,如下代码:

发布者发出通知 => 主题对象收到通知并推送给订阅者 => 订阅者执行相应操作

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>MVVM</title>
</head>
<body>

<div id="mvvm-app">
  <input type="text" v-model="someStr">
  <input type="text" v-model="child.someStr">
  <p>{{getHelloWord}}</p>
  <p v-html="child.htmlStr"></p>
  <button v-on:click="clickBtn">change model</button>
</div>
<script src="./observer.js"></script>
<script src="./watcher.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
    var vm = new MVVM({
      el: '#mvvm-app',
      data: {
        someStr: 'hello ',
        className: 'btn',
        htmlStr: 'red',
        child: {
          someStr: 'World !'
        }
      },
      computed: {
        getHelloWord: function() {
          return this.someStr   this.child.someStr;
        }
      },
      methods: {
        clickBtn: function(e) {
          var randomStrArr = ['childOne', 'childTwo', 'childThree'];
          this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
        }
      }
    });
    vm.$watch('child.someStr', function() {
        console.log(arguments);
    });
</script>
</body>
</html>

上海时时乐走势图官网 10

// 判断是否是事件指令
if (self.isEventDirective(dir)) {
  // 事件指令 比如 v-on:click 这样的
  compileUtil.eventHandler(node, self.$vm, attrValue, dir);
}
  1. Vue的源代码中,用了文档碎片fragment作为真实节点的存储吗?

数据调用如下:

<!DOCTYPE html>
 <head></head>
 <body>
 <div id="app">
  <input type="text" id="a" v-model="text">
  {{text}}
 </div>
<script type="text/javascript">
  function Compile(node, vm) {
   if(node) {
    this.$frag = this.nodeToFragment(node, vm);
    return this.$frag;
   }
  }
  Compile.prototype = {
   nodeToFragment: function(node, vm) {
    var self = this;
    var frag = document.createDocumentFragment();
    var child;

    while(child = node.firstChild) {
     self.compileElement(child, vm);
     frag.append(child); // 将所有子节点添加到fragment中
    }
    return frag;
   },
   compileElement: function(node, vm) {
    var reg = /{{(.*)}}/;

    //节点类型为元素
    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.addEventListener('input', function(e) {
        // 给相应的data属性赋值,进而触发该属性的set方法
         vm[name]= e.target.value;
       });
       node.value = vm[name]; // 将data的值赋给该node
       node.removeAttribute('v-model');
      }
     };
    }
    //节点类型为text
    if(node.nodeType === 3) {
     if(reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();
      node.nodeValue = vm[name]; // 将data的值赋给该node
      // new Watcher(vm, node, name);
     }
    }
   },
  }
  function defineReactive (obj, key, val) {
   Object.defineProperty(obj, key, {
    get: function() {
     return val;
    },
    set: function (newVal) {
     if(newVal === val) return;
     val = newVal;
     console.log(val);
    }
   })
  }
  function observe(obj, vm) {
   Object.keys(obj).forEach(function(key) {
    defineReactive(vm, key, obj[key]);
   })
  }
   function Vue(options) {
   this.data = options.data;
   var data = this.data;
   observe(data, this);
   var id = options.el;
   var dom =new Compile(document.getElementById(id),this);
   // 编译完成后,将dom返回到app中
   document.getElementById(id).appendChild(dom);
  }
  var vm = new Vue({
   el: 'app',
   data: {
    text: 'hello world'
   }
  });
 </script>
 </body>
</html>

二: 实现 Compile

3.数据劫持(Vue.js)

先调用get方法拿到当前的值,代码如下:

思路: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

然后调用init方法,如下代码:

结果:

因此 this.child.someStr 会重新赋值一个随机数,也就是说值会得到更新,因此首先会调用 mvvm.js的_proxyData中的get方法;代码如下:

上海时时乐走势图官网 11

同理 使用 Object.defineProperty 监听对象属性值的变化。执行完成后,下一步代码执行如下:
// 初始化 Compile
this.$compile = new Compile(options.el || document.body, this);

代码:戳这里

// expOrFn 是事件函数的话
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  }

在上面例子我们多次操作了dom节点,为提高性能和效率,会先将所有的节点转换城文档碎片fragment进行编译操作,解析操作完成后,再将fragment添加到原来的真实dom节点中。

执行 new MVVM 后 mvvm.js 实列化,mvvm.js 代码如下:

js对象

// 对所有的属性数据进行监听
function Observer(data) {
  this.data = data;
  this.init();
}

Observer.prototype = {
  init: function() {
    var self = this;
    var data = self.data;
    // 遍历data对象
    Object.keys(data).forEach(function(key) {
      self.defineReactive(data, key, data[key]);
    });
  },
  defineReactive: function(data, key, value) {
    var dep = new Dep();
    // 递归遍历子对象
    var childObj = observer(value);

    // 对对象的属性使用Object.defineProperty进行监听
    Object.defineProperty(data, key, {
      enumerable: true,  // 可枚举
      configurable: false, // 不能删除目标属性或不能修改目标属性
      get: function() {
        if (Dep.target) {
          dep.depend();
        }
        return value;
      },
      set: function(newVal) {
        if (newVal === value) {
          return;
        }
        value = newVal;
        // 如果新值是对象的话,递归该对象 进行监听
        childObj = observer(newVal);
        // 通知订阅者 
        dep.notify();
      }
    });
  }
}

function observer(value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
}

function Dep() {
  this.subs = [];
}

Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  depend: function() {
    Dep.target.addDep(this);
  },
  removeSub: function(sub) {
    var index = this.subs.indexOf(sub);
    if (index != -1) {
      this.subs.splice(index, 1);
    }
  },
  notify: function() {
    // 遍历所有的订阅者 通知所有的订阅者
    this.subs.forEach(function(sub) {
      sub.update();
    })
  }
};

Dep.target = null;

 最简单例子:

init: function() {
  var self = this;
  var data = self.data;
  // 遍历data对象
  Object.keys(data).forEach(function(key) {
    self.defineReactive(data, key, data[key]);
  });
},
  1. 脏值检查(angular.js)
  2. 数据劫持(vue.js)

compile.js里面的updater对象如下:

代码分析:

    双向数据绑定的原理是:可以将对象的属性绑定到UI,具体的说,我们有一个对象,该对象有一个name属性,当我们给这个对象name属性赋新值的时候,新值在UI上也会得到更新。同样的道理,当我们有一个输入框或者textarea的时候,我们输入一个新值的时候,也会在该对象的name属性得到更新。

  1. 需要实现一个指令解析器Compile, 对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数。
    3. 需要实现一个Watcher,作为链接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应的回调函数,从而更新视图。

首先判断 computed 是否为对象,然后遍历computed,判断如果computed[key] 键是否是一个函数,如果是一个函数的话,就执行该函数,否则的话,就执行该get调用。所以我们看到使用vue的
时候会看到 computed方法初始时调用。

用完成后,会执行到get方法里面代码 var value = this.getter.call(this.vm, this.vm); 因此会继续调用到 parseGetter 返回函数的代码,所以最后返回新值。
返回新值后,在执行函数run方法后,判断新旧值是否相等,不相等的话,就会执行 compile.js里面bind的方法的回调,如下代码:

如上代码,会调用 this.getter方法,this.getter方法初始化代码在watcher如下:

在输入框输入新值的时,会在 compile.js里面会监听input事件,如下代码:

下面是一个通过 Object.defineProperty 来实现一个简单的数据双向绑定。通过该方法来监听属性的变化。
实现的效果简单如下:页面上有一个input输入框和div显示框,当在input输入框输入值的时候,div也会显示对应的值,当我打开控制台改变 obj.name="输入任意值"的时候,按回车键运行下,input输入框的值也会跟着变,可以简单的理解为 模型-> 视图的 改变,以及 视图 -> 模型的改变。如下代码:

然后 初始化Observer
observer(data, this);

function MVVM(options) {
  this.$options = options || {};
  var data = this._data = this.$options.data;
  var self = this;

  /*
   数据代理,实现 vm.xxx 
   上面的代码看出 监听数据的对象是 options.data, 因此每次更新视图的时候;如:
   var vm = new MVVM({
     data: {name: 'kongzhi'}
   });
   那么更新数据就变成 vm._data.name = 'kongzhi2'; 但是我们想实现这样更改 vm.name = "kongzhi2";
   因此这边需要使用属性代理方式,利用Object.defineProperty()方法来劫持vm实列对象属性的读写权,使读写vm实列的属性转成vm.name的属性值。
   */
   Object.keys(data).forEach(function(key) {
     self._proxyData(key);
   });

   this._initComputed();

   // 初始化Observer
   observer(data, this);

   // 初始化 Compile
   this.$compile = new Compile(options.el || document.body, this);
}

MVVM.prototype = {
  $watch: function(key, cb, options) {
    new Watcher(this, key, cb);
  },

  _proxyData: function(key) {
    var self = this;
    Object.defineProperty(self, key, {
      configurable: false,  // 是否可以删除或修改目标属性
      enumerable: true,   // 是否可枚举
      get: function proxyGetter() {
        return self._data[key];
      },
      set: function proxySetter(newVal) {
        self._data[key] = newVal;
      }
    })
  },

  _initComputed: function() {
    var self = this;
    var computed = this.$options.computed;
    if (typeof computed === 'object') {
      Object.keys(computed).forEach(function(key) {
        Object.defineProperty(self, key, {
          get: typeof computed[key] === 'function' ? computed[key] : computed[key].get,
          set: function() {}
        })
      })
    }
  }
}
var vm = new MVVM({
  el: '#mvvm-app',
  data: {
    someStr: 'hello ',
    className: 'btn',
    htmlStr: 'red',
    child: {
      someStr: 'World !'
    }
  },
  computed: {
    getHelloWord: function() {
      return this.someStr   this.child.someStr;
    }
  },
  methods: {
    clickBtn: function(e) {
      var randomStrArr = ['childOne', 'childTwo', 'childThree'];
      this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
    }
  }
});
vm.$watch('child.someStr', function() {
  console.log(arguments);
});
run: function() {
    var value = this.get();
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
 },

如上代码我们可以监听每个属性对象数据的变化了,那么监听到属性值变化后我们需要把消息通知到订阅者,因此我们需要实现一个消息订阅器,该订阅器的作用是收集所有的订阅者,当有属性值发生改变的时候,就把该消息通知给所有订阅者。因此我们可以实现如下代码:

当页面初始化时,会判断是普通指令 v-model, v-text, or v-html,还是事件指令 v-on:click; compile.js相对应的代码如下:

实现数据绑定的做法有如下几种:
1. 发布者-订阅模式(

html代码初始化如下:

'hello ', 'btn', 'red', 和 
{
  someStr: 'World !'
}
clickBtn: function(e) {
  var randomStrArr = ['childOne', 'childTwo', 'childThree'];
  this.child.someStr = randomStrArr[parseInt(Math.random() * 3)];
}

从而调用 watcher.js 里面的update方法,代码如下:

set: function(newVal) {
    if (newVal === value) {
      return;
    }
    value = newVal;
    // 发布消息
    dep.public();
  }

使用Object.defineProperty来监听对象属性的变化,使vm实列的属性转变成vm._data的属性值。
接着初始化 this._initComputed();方法;代码如下:

Object.keys(data).forEach(function(key) {
  self._proxyData(key);
});

因此会对该节点进行绑定 'click'事件,代码 node.addEventListener(eventType, fn.bind(vm), false); 就是对绑定的事件 进行监听,demo里面是使用 v-on:click, 因此eventType就是click事件,因此会对 点击事件 进行监听。然后执行相对应的回调函数。该demo中的回调函数为 clickBtn(函数名)。
clickBtn函数代码如下:

defineReactive: function(data, key, value) {
  var dep = new Dep();
  // 递归遍历子对象
  var childObj = observer(value);

  // 对对象的属性使用Object.defineProperty进行监听
  Object.defineProperty(data, key, {
    enumerable: true,  // 可枚举
    configurable: false, // 不能删除目标属性或不能修改目标属性
    get: function() {
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set: function(newVal) {
      if (newVal === value) {
        return;
      }
      value = newVal;
      // 如果新值是对象的话,递归该对象 进行监听
      childObj = observer(newVal);
      // 通知订阅者 
      dep.notify();
    }
  });
}

模型 -> 视图的改变

本文由上海时时乐走势图发布于web前端,转载请注明出处:vue双向绑定的法则及贯彻双向绑定MVVM源码分析上

VUE

您可能还会对下面的文章感兴趣: