学习vue的双向数据绑定首先要知道几个知识点:

1.Object.defineProperty: 用于劫持数据

Object.defineProperty(obj, prop, descriptor)

参数

obj
需要被操作的目标对象
prop
目标对象需要定义或修改的属性的名称。
descriptor
将被定义或修改的属性的描述符。

返回值

被传递给函数的对象。

eg:
  var obj = { }; // 为obj定义一个名为 hello 的访问器属性

  Object.defineProperty(obj, "hello", {

    get: function () {return sth},

    set: function (val) {/* do sth */}

  })

  obj.hello // 可以像普通属性一样读取访问器属性

  访问器属性的"值"比较特殊,读取或设置访问器属性的值,实际上是调用其内部特性:get和set函数。

  obj.hello // 读取属性,就是调用get函数并返回get函数的返回值

  obj.hello = "abc" // 为属性赋值,就是调用set函数,赋值其实是传参  

ps: descriptor中还能更改以下属性

configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable
当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。

value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable
当且仅当该属性的 writable 为 true 时,该属性才能被赋值运算符改变。默认为 false。

2.DocumentFragmentappendChild : 用于劫持节点

  • DocumentFragment
描述

DocumentFragment是DOM节点。它们不是主DOM树的一部分。通常的用例是创建文档片段,将元素附加到文档片段,然后将文档片段附加到DOM树。在DOM树中,文档片段被其所有子代替。

由于文档片段位于内存中,而不是主DOM树的一部分,因此将其附加到其上不会导致页面回流(元素的位置和几何的计算)。因此,使用文档片段往往会导致更好的性能。

  • appendChild
描述

appendChild 方法会把要插入的这个节点引用作为返回值返回.

示例

// 创建一个新的段落p元素,然后添加到body的最尾部
var p = document.createElement("p");
document.body.appendChild(p);

附注

如果被插入的节点已经存在于当前文档的文档树中,则那个节点会首先从原先的位置移除,然后再插入到新的位置.

如果你需要保留这个子节点在原先位置的显示,则你需要先用Node.cloneNode方法复制出一个节点的副本,然后在插入到新位置.

这个方法只能将某个子节点插入到同一个文档的其他位置,如果你想跨文档插入,你需要先调用document.importNode方法.


3.发布者与订阅者模式

// 发布者
var pub = {
  publish: function() {
    dep.notify();
  }
}

// 订阅者
var watcher1 = { update: function () {console.log(1);}}
var watcher2 = { update: function () {console.log(2);}}
var watcher3 = { update: function () {console.log(3);}}

// 主题对象
function Dep() {
  this.watchers = [watcher1, watcher2, watcher3];
}

// 主题方法
Dep.prototype.notify = function () {
  this.watchers.forEach(function (watcher) {
    watcher.update();
  })
}

// 发布者发布消息,则调用notify()从而促发订阅者的update
var dep = new Dep();
pub.publish();

Vue双向数据绑定实现流程

  1. 劫持el对应的data对象里面的数据,为每个key添加,用于获取及更新数据的getter和setter的方法,并利用闭包的方法,
    为每个key创建一个独立的dep(主题)对象。
  2. 通过documentFragmentappendChild劫持视图,对其所有节点进行重新编译。最后再返回编译结果。
  3. 在编译过程中,遍历所有节点,判断节点类型,如果节点有关于数据绑定的指令,则做相应的数据监听处理,并将其创建为watcher对象(即订阅者),通过watcher的getter方法,触发key的getter方法,从而把watcher加到对应的主题当中,
    一旦key发生改变(节点的数据监听生效,或者js改动data数据),会触发key自身被修改的setter,从而触发主题对象的notify
    函数,发布更新信息(即调用保存的subs内所有订阅者(watcher)的update函数),最终实现双向数据绑定。

具体实现代码(简单实现), 如下:

// watcher:订阅者身份 
function Watcher(vm, node, name, nodeType) {
  Dep.target = this;
  this.name = name;
  this.node = node;
  this.vm = vm;
  this.nodeType = nodeType;
  this.update();
  Dep.target = null;
}

Watcher.prototype = {
  update: function () {
    this.get();
    this.node.nodeValue = this.value;
  },

  get: function () {
    this.value = this.vm[this.name];
  }
}


// 根据节点类型的不同作相应的监听等处理, 共同处理:为每个节点创建watcher订阅者,一旦new watcher调用构造函数,
// 暂存数据到Dep.target中, 通过watcher的update函数调用watcher的get,从而调动this.vm[this.name],促发在
// defineReactive中为每个key设置的getter方法,把暂存了该节点(watcher)各种数据的dep.target放到dep(相应
// 主题)的subs中
function compile (node, vm) {
  var reg = /\{\{(.*)\}\}/;
  // nodeType 返回节点的类型
  // 1时,节点为元素
  if (node.nodeType === 1) {
    // node.attributes 返回标签所有属性 
    var attr = node.attributes;
    console.log (attr);
    for (let i = 0; i < attr.length; i++) {
      if (attr[i].nodeName == 'v-model') {
        var name = attr[i].nodeValue;
        node.addEventListener('input', function(e) {
          vm[name] = e.target.value;
        });
        node.value = vm[name];
        node.removeAttribute('v-model');
      }
    }
    new Watcher(vm, node, name, 'input');
  }
  // 3时,节点为text
  if (node.nodeType === 3) {
    if (reg.test(node.nodeValue)) {
      var name = RegExp.$1;// 正则匹配的第一个括号内的项
      name = name.trim();
      // node.nodeValue = vm[name];
      new Watcher(vm, node, name, 'text');
    }
  }
}

// 劫持节点到DocumentFragment中,在对其所有节点处理后再返回节点
function nodeToFragment(node, vm) {
  var flag = document.createDocumentFragment();
  var child;
  while(child = node.firstChild) {
    compile(child, vm);
    flag.appendChild(child);
  }
  return flag; 
}

// Dep主题,有addSub:添加订阅者 和 notify: 提醒订阅者更新的方法
function Dep () {
  this.subs = []
}

Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  }, 
  notify: function() {
    this.subs.forEach(sub => {
      sub.update();
    })
  }
}

function defineReactive (obj, key, val) {
  var dep = new Dep();

  // 把主题放到重构变量getter, setter的时候调用
  Object.defineProperty(obj, key, {
    get: function() {
      if (Dep.target) dep.addSub(Dep.target);
      return val
    },
    set: function (newVal) {
      if (newVal === val) return 
      val = newVal;
      // 发布者发布通知
      dep.notify();
      console.log(val);
     }
  })
}

function observe (obj, vm) {
  Object.keys(obj).forEach(key => {
    // 由于闭包关系每个key,都有其对应的dep(主题)
    defineReactive(vm, key, obj[key]);
  })
}

function Vue(options) {
  this.data = options.data;

  var data = this.data;

  // 为所有data对象的值添加setter方法,同时将vm.data[key]获取值的方式转变成vm[key]
  observe(data, this);

  var id = options.el;
  var dom = nodeToFragment(document.getElementById(id), this);
  // 把编译完成后的dom重新返回到app中
  document.getElementById(id).appendChild(dom);
}

var vm = new Vue({
  el: 'app',
  data: {
    text: 'hello world'
  }
})

参考

  1. https://developer.mozilla.org/zh-CN/
    2.http://www.cnblogs.com/kidney/p/6052935.html?utm_source=gold_browser_extension