简单版源码分析双向绑定

2019-10-31  本文已影响0人  zdxhxh

从源码分析双向绑定

这部分代码,是源码的简化版,相对比较容易理解。

html代码:

<body>
  <div id="mvvm-app">
    <input type="text" v-model="message" />
    <p>{{message}}</p>
    <button v-on:click="sayHi">change model</button>
  </div>
</body>
<script src="./index.js"></script>
<script>
  var vm = new MVVM({
    el: "#mvvm-app",
    data: {
      message: "hello world"
    },
    methods: {
      clickBtn: function(message) {
        vm.message = "clicked";
      }
    }
  });
</script>

从html代码,vue仅仅从初始化vm实例就完成了双向绑定,简直溜啊,我们还在想是用模块还是啥玩意搞的时候,人家就直接实例->视图,完成全部,秀啊。

在看看vm实例初始化过程中干了啥

function MVVM(options) {
  this.$options = options;
  var data = (this._data = this.$options.data),
  self = this;
  Object.keys(data).forEach(function(key) {
    self._proxy(key);
  });
  observe(data, this);
  this.$compile = new Compile(options.el || document.body, this);
}

我们来数:

就此打断,我们来看看_proxy怎么玩

MVVM.prototype = {
  _proxy: 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;
      }
    });
  }
};

这段代码,是将自身的 vm.data.key1,vm.data.key2 变成 vm.key1和vm.key2,这就是为什么你可以在vm的方法中调用this.key1的原因了。

继续构造函数的解析:

我们来看看observe的实现

function observe(data) {
  if (!data || typeof data !== "object") {
    return;
  }
  if (Array.isArray(data)) {
    throw new TypeError("data must Object");
  }
  defineReactive(data);
}

结果是它只做了一些类型判断,并调用了defineReactive这个函数

我们看看defineReactive的实现

function defineReactive(data) {
  // 创建一个消息订阅器实例
  var dep = new Dep();
  for (let key in data) {
    var type = Object.prototype.toString.call(data[key]);
    if (type === "[object Array]") {
      Object.defineProperty(data[key], "push", {
        value: arrayMethods.push
      });
    } else if (type === "[object Object]") {
      // 递归调用
      defineReactive(data[key]);
    } else {
      proxy(data, key,dep);
    }
  }
}

function proxy(obj,prop,dep) {
  var val = obj[prop];
  Object.defineProperty(obj, prop, {
    get: function() {
      if (Dep.target) {
        dep.depend();
      }
      return val;
    },
    set: function(newVal) {
      val = newVal;
      dep.notify();
    }
  });
}

这里

我们再来看proxy怎么实现的

好,现在data对象劫持完成了,再无数次递归后,你可以想象一下dep实例的分布。

假设data是这样的一个结构

data : { 
  user : { 
    name : '2222娘',
    age : '18'
  },
  key : 1
}

dep的分布应该是这样的

data(dep1) : { 
  key : { 
    getter : function(){ dep1 },
    setter : function() { dep1 }
  },
  user(dep2) : { 
    name : { 
      getter : function(){ dep2 },
      setter : function() { dep2 }
    },
    age : { 
      getter : function(){ dep2 },
      setter : function() { dep2 }
    },
  }
}

dep寄生在data实例以及子属性为对象的身上

好,回到vm的构造函数,看看这句

this.$compile = new Compile(options.el || document.body, this);

这里创建了一个Compile实例,并挂载到自身的$compile属性身上。来看Compile的构造函数

/**
 * @param {dom} 传入的dom节点
 * @param vm 传入的vm实例
 */
function Compile(el, vm) {
  this.$vm = vm;  // 挂载到自身
  this.$el = this.isElementNode(el) ? el : document.querySelector(el);  // 是节点直接用
  if (this.$el) {
    // 以下这句是提高性能的
    this.$fragment = this.node2Fragment(this.$el);
    // 调用原型方法
    this.init();
    // 调用完后,给$el添加$fragment
    this.$el.appendChild(this.$fragment);
  }
}

我们来看一下init方法

Compile.prototype = {
  init: function() {
    this.compileElement(this.$fragment);
  },
  compileElement: function(el) {
    var childNodes = el.childNodes;
    var self = this;
    Array.prototype.slice.call(childNodes).forEach(function(node) {
      var text = node.textContent;
      var reg = /\{\{(.*)\}\}/; // 表达式文本呢
      if (self.isElementNode(node)) {
        self.compile(node);
      } else if (self.isTextNode(node) && reg.test(text)) {
        self.compileText(node, RegExp.$1);
      }
      // 遍历编译子节点
      if (node.childNodes && node.childNodes.length) {
        self.compileElement(node);
      }
    });
  },
  compile: function(node) {
    var nodeAttrs = node.attributes;
    var self = this;
    Array.prototype.slice.call(nodeAttrs).forEach(function(attr) {
      var attrName = attr.name; // v-text
      if (self.isDirecitive(attrName)) {
        var exp = attr.value;
        var dir = attrName.substring(2);
        if (self.isEventDirective(dir)) {
          // 事件指令, 如 v-on:click
          compileUtil.eventHandler(node, self.$vm, exp, dir);
        } else {
          // 普通指令
          compileUtil[dir] && compileUtil[dir](node, self.$vm, exp);
        }
      }
    });
  }
  isElementNode: function(el) {
    return el.nodeType && el.nodeType === 1;
  },
  isTextNode: function(el) {
    return el.nodeType && el.nodeType === 3;
  },
  isDirecitive: function(attrName) {
    return attrName.indexOf("v-") == 0;
  },
  isEventDirective: function(dir) {
    return dir.indexOf("on") === 0;
  },
  node2Fragment: function(el) {
    var fragment = document.createDocumentFragment();
    var child;
    while ((child = el.firstChild)) {
      fragment.appendChild(child);
    }
    return fragment;
  },
  compileText: function(node, exp) {
    compileUtil.text(node, this.$vm, exp);
  }
};

init()方法分析

complie方法分析

指令处理集合compileUtil代码分析,我这里之分析几个重要的方法

// 指令处理集合
var compileUtil = {
  text: function(node, vm, exp) {
    this.bind(node, vm, exp, "text");
  },
  // ...省略
  bind: function(node, vm, exp, dir) {
    var updaterFn = updater[dir + "Updater"];
    // 第一次初始化视图
    updaterFn && updaterFn(node, this._getVMVal(vm, exp));
    // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
    new Watcher(vm, exp, function(value, oldValue) {
      // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
      updaterFn && updaterFn(node, value, oldValue);
    });
  },
  _getVMVal: function(vm, exp) {
    var val = vm;
    exp = exp.split(".");
    exp.forEach(function(k) {
      val = val[k];
    });
    return val;
  },
  model: function(node, vm, exp) {
    this.bind(node, vm, exp, "model");
    var me = this,
        val = this._getVMVal(vm, exp);
    node.addEventListener("input", function(e) {
      var newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      me._setVMVal(vm, exp, newValue);
      val = newValue;
    });
  },

  _setVMVal: function(vm, exp, value) {
    var val = vm;
    exp.forEach(function(k, i) {
      // 非最后一个key,更新val的值
      if (i < exp.length - 1) {
        val = val[k];
      } else {
        val[k] = value;
      }
    });
  },
  // 事件处理
  eventHandler: function(node, vm, exp, dir) {
    var eventType = dir.split(":")[1],
      fn = vm.$options.methods && vm.$options.methods[exp];

    if (eventType && fn) {
      node.addEventListener(eventType, fn.bind(vm), false);
    }
  }
};

当我们遇到v-model这样的指令会调用compileUtil.model方法

入参

调用过程

这里就完成了input中绑定原生事件,回调更新数据层

再看bind方法,以下是形参说明

bind: function(node, vm, exp, dir) {
  var updaterFn = updater[dir + "Updater"];
  // 第一次初始化视图
  updaterFn && updaterFn(node, this._getVMVal(vm, exp));
  // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
  new Watcher(vm, exp, function(value, oldValue) {
    // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
    updaterFn && updaterFn(node, value, oldValue);
  });
}
// 更新函数
var updater = {
  textUpdater: function(node, value) {
    node.textContent = typeof value == "undefined" ? "" : value;
  },
  modelUpdater: function(node, value, oldValue) {
    node.value = typeof value == 'undefined' ? '' : value;
}
  // ...省略
};

这里它做这些事情

再来看Watcher的构造函数

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.depIds = {};
  // 此处为了触发get方法,从而在dep添加自己
  this.value = this.get();
}

它做了如下事情

再看它的原型get方法

Watcher.prototype = {
  get: function() {
    Dep.target = this; // 将订阅者指向自己
    var value =  compileUtil._getVMVal(this.vm,this.exp); // 触发getter,添加自己到属性订阅器
    Dep.target = null; // 添加完毕 重置
    return value;
  },
  update: function() {
    this.run(); // this.run();  // 属性值变化收到通知
  },
  run: function() {
    var value = this.get(); // 取到最新值
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal); // 执行compile中的回调 更新视图
    }
  },
  addDep: function(dep) {
    if (!hasOwnProperty(this.depIds, dep.id)) {
      dep.addSub(this);
      this.depIds[dep.id] = dep;
    }
  }
};

在它的get方法触发后

我们来看一下watcher与dep的引用数谁的多

data(dep1) : { 
  user(dep2) : { name : 22333}
}

<div>{{user.name}}</div>
<div>{{user.name}}</div>
<div>{{user.name}}</div>

这里会创建三个watcher ,
watcher1: { depIds :[dep1,dep2]}
watcher2: { depIds :[dep1,dep2]}
watcher3: { depIds :[dep1,dep2]}


dep1 : { subs : [watcher1,wathcher2,watcher3] }
dep2 : { subs : [watcher1,wathcher2,watcher3] }

以上就完成了 数据层 --(数据劫持)--> DOM

在触发某个属性的setter 后,有关的dep会通知所有订阅该属性的watcher,并触发watcher的更新视图方法。

事实上,最难理解的是加入dep与watcher这样的相互映射。有点像笛卡尔积与二维表

Watcher\ Dep dep1 dep2 dep3
wathcer1
wathcer2
wathcer3
watcher4

事实上,depIds的作用是用于记录当前watcher实例订阅dep实例,如果已经订阅过了,则不再订阅。

最后,当触发data.xxx = "xxx"的时候,dep就会调用notify通知相关的watcher更新视图

这就完成了 当数据层变化时,更新input或关联元素的value (2),最后,双向绑定就实现了

相关术语

收集依赖

对于dep.addSub(watcher) 这个过程,我们叫做收集依赖,这个过程实在complie中实现的,每次新建完watcher后,都会在相关的dep添加该watcher实例。

image.png

面试怎么回答?

双向绑定怎么实现啊 ? 面试你可不能回答大白话,毕竟造航母

答 :双向绑定的基本原理是在vm实例初始化过程中对data对象进行数据劫持,并创建订阅容器dep,在render(compile)过程中遍历每个节点并创建watcher依赖,创建依赖过程中通过getter触发订阅容器的依赖收集。最后,当data对象下的属性触发setter操作时,订阅容器通知相关依赖触发更新。

上一篇 下一篇

猜你喜欢

热点阅读