前言
- 列表
Vue是一个快速实现双向绑定的库,思想是最重要的,库永远是会被替代的东西。正文
Vue 架构概览
- github src 目录
- config 里面也是和我们平常写的东西那样分成类似于 开发环境 和 生产环境的,web开发用的 server端开发用的,weex 开发用的,因为 vue 是一个库,所以就是将所有的东西都给支持好,这样才是一个合格的库
- vue 的核心文件夹就是 code 和 platforms (平台)
双向数据绑定
- 引入概念
- 主要实现逻辑的源码
- 主要用到的五大方法
- Object.defineProperty
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
- Observer
监听器
- Watcher
观察者
- Dep
收集依赖
- Directive
组件
- vue 双向绑定的实现图(必须要理解的东西)
- 上面流程图整个的思路解析
首先是 Dom 有 times 属性,之后是 Directive(组件、指令)v-text=”times” 与 times 属性相关联。
整个的双向绑定有一个完整的链条。
Directive (指令)里面所有的更新机制有 Watcher 来负责,每一个 Directive 对应一个 Watcher 。
Watcher 是负责更新 Directive 的,更新的数据从 Dep(所有的组织的依赖项里来),Dep 就像是一个所有东西的控制中心、依赖项,依赖项指的是 times 这个属性写出来之后刚开始谁也不认识,但是当你写一个 times 的时候会自动的创建一个 Watcher ,Watcher 就是来观察对应的 Directive 的,一个 Directive 对应一个 Watcher 。
重点:每当创建一个 Directive 的时候都会自动的创建一个相应的 Watcher。
对于基本的 Js 来讲的话写一个 Directive 谁也不会认识的,谁也不知道这个 times 怎么更新,要到哪去,现在需要理解的是 Vue 是如何实现的,times 属性可以在 Dom 中进行双向的绑定的,奥秘就在 Watcher。
Watcher 是如何做到的:
当你去创建一个 Directive (指令)然后自动创建 Watcher 的时候,Watcher 负责收集,如果是一个非常基础的读的东西,Watcher 负责将这个 times 收进来,收进来之后再放到 Dep 指令集里去,Dep 就是所有的依赖项,可以理解成为一个快递的物流中心之类的东西,但是这个 times 在加入依赖中心之前想要实现 Dom 和 Directive 之间的互动,Dep 这个依赖中心只是收集了,但是它不知道什么时候该变,就相当于是将一堆的快递都扔到了里面,这个时候就需要 Observer 来实现双方关系的连接。
Observer 可以理解为一个小机器人,Observer 实现的过程是:
Watcher 在 addDep (加入到 Dep 依赖集的那一刻)告诉 Dep Dom 和 Directive 双方有依赖关系,他们俩都在被 ”我“ 管着,“我” 把他俩加到 Dep 里面,他俩谁变的话 “我” 好去通知另一个,但是 Dep 只是收集,它自身是做不到将改变的信息传递给 Watcher 之后把前面的 Dom 和 Directive 同步更新的,这样的话 Dep 就找 Observer 来帮忙了,Dep 在这里只是充当的收集者的角色, Observer 相当于是幕后的老板,具有真正的执行力,Observer 实现用到了 setter getter 方法,这两个方法在 times 的值被修改的时候是可以被运行的,这两个是 ES5 提供的方法,在 get 和 set 一个对象的时候,这两个方法都能够有一个回调帮你去处理,比如说你去取这个 times 属性值的时候 getter 函数会帮助你去执行,在动(修改)这个 times 的时候 setter 函数可以帮助你去执行,所以在 get 和 set 的时候都可以得到这个值的,所以在 Watcher 在把这个相互的关系 times 往 Dep 里面放的时候,Dep 去通知了他的老板 Observer ,Observer 在注册这个 times 的时候,把这个 times 通过 get set 方法注册回调函数,在 times 被 get 的时候将这个 times 属性 depend(依赖) 装到 Dep 依赖里面,当他被 set 的时候 notify(通知)Dep 刚才谁依赖我了,然后再告诉相应的 Watcher 之后执行 update
- 基于流程图的源码小示例
var obj = {};
var a;
Object.defineProperty(obj,'a',{
get:function(){
console.log('get val');
return a;
},
set:function(newVal){
console.log('Set Val' + newVal);
a = newVal;
}
});
obj.a;//get val
obj.a = '111';//Set Val 111
{{ a }} 就相当于是 get 读
v-model=”a” 就相当于是 set 写
- 双向数据绑定官方示图,相较于上面的少了 Dep 这个东西
setter 触发消息到 Watcher ,Watcher 帮忙告诉 Dircetive 更新 Dom ,DOM 中修改了数据也会通知给 Watcher ,Watcher 帮忙修改数据。
- 解析双向绑定实现的源码
- 首先
new Vue()
这个 Vue 必须是一个全局变量,这个全局变量是从 MVVM.js 里面来的 - MVVM.js 源码
```
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);
> 里面的 options 就等于 new Vue() 里面传递的对象参数。<br />
这里面的 this 都指向的是新 new 出来的实例 vm<br />
首先在 this 上增加了一个 data 属性 = options.data;相当于是 this.data = { text:'hello world' } ;<br />
之后又把这个 data 取出来 传递给了 observer 。<br />
之后取出传递的 id 名,再将 id 传递给 Compile 进行操作,操作返回的结果 dom 再追加到 id 的子级中
- 加注释版本
function Vue(options) { this.data = options.data; var data = this.data; //data:{ text: ‘hello world’ } this:vm 新 new 出来的实例,且这个 this 上面也是有 data: { text: ‘hello world’ } observe(data, this); var id = options.el; //编译的时候也传了 this -> data : { text: ‘hello world’ } var dom =new Compile(document.getElementById(id),this); // 编译完成后,将dom返回到app中 document.getElementById(id).appendChild(dom); }
//1.this.data = { text: 'hello world' }
//2.observer Compile
> 可以看到无论是 observer 还是 Compile 都是随时带着 this (新实例)走的
- 上面将 new 的过程分析完了,下面分析 observer
- Observer.js 源码
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]);
})
}
- observer 函数
> 
- 带注释版的 observer 函数
function observe(obj, vm) { //obj -> { text: ‘hello world’ } vm -> this 创建出来的实例 -> data:{ text: ‘hello world’ } //之后进行遍历,这里可以看出为什么传了两个 data,前面的是为了遍历来用的,后面的 this 是为了保持它本身 Object.keys(obj).forEach(function(key) { //三个参数: this 新建的实例、键(属性名) 、值(属性值) //vm text ‘hello world’ defineReactive(vm, key, obj[key]); }) }
> 经过上面的步骤可以看到,this -> vm 这个实例已经是可以随时带着走了,而且 data 里面的对象也已经实现了分离,分离之后就可以和 html 中写的 {{ text }} v-model="text" 这种的进行联合起来了
- 之后看 Observer.js 里面的 defineReactive (定义反应)函数
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();
}
})
}
> 什么时候会有反应呢?就是通过 set get 的函数回调
- 带注释版的 defineReactive 函数
function defineReactive (obj, key, val) { //三个传入的参数:this -> vm test ‘hello world’ var dep = new Dep(); //在 js 当中万物皆对象,而且 new 出来的实例更是对象,所以这里用了 Object.defineProperty() 方法来给 this -> vm 这个实例定义 key 和 value Object.defineProperty(obj, key, { //get 函数实现的是在 new Vue() 之后,在你 get 取这个 key -> ‘text’ 的时候 return val -> ‘hello world’ get: function() { //添加订阅者watcher到主题对象Dep if(Dep.target) { // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用 dep.addSub(Dep.target); } return val; }, //set 实现的是在 set 这个属性 text 的时候,首先判断如果新值不等于老值得时候再将 新值 赋值给 老值 实现同步 set: function (newVal) { if(newVal === val) return; val = newVal; console.log(val); // 作为发布者发出通知 dep.notify(); } }) //在上面 get set 之后就把一个普通的属性变得更加有活性了,将所有的属性全部都直接添加到这个 vm 实例化的对象身上了 //因为在 new Vue() 的时候 this.data = options.data; var data = this.data; 相当于是这个传过来的 data 和 vm.data 指向的是同一个地址(对象按址传递)所以会发生同步的改变,结果就是 vm.data.text === vm.text 这两个是同一个东西了 }
- 之后是 defineReavtive 函数内使用的 Dep ,上面的分析之后会想到现在需要的是让页面上的 v-model="text" 与 set 相关联起来,可以同步的发生变化
- Dep.js 源码
function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }) } }
- 带注释版的 Dep.js
//一个基本的构造函数 里面定义了一个 subs 的属性值为 [] 空数组 一看到数组就应该想到 存储 数组就是为了存储而生的 function Dep() { this.subs = []; } //原型链上定义了两个方法 Dep.prototype = { //往 subs 属性里面推东西 addSub: function(sub) { this.subs.push(sub); }, //通知:循环当前的依赖里面的所有东西之后执行被塞过来的 subs 数组中的元素的 update 方法 notify: function() { this.subs.forEach(function(sub) { sub.update(); }) } }
- 之后回到 Observer.js 中的 defineReactive 函数中去
//这里 new Dep() 创建了一个实例 该实例有一个 subs 属性 值为 [],还有 addSub(往 subs 属性里面推东西)和 noify(通知)两个方法 var dep = new Dep();
- 上面分析的代码有两个关键的东西暂时没有分析到
sub.update()
Dep.terget
> <br />

- 接着分析
> get 里面用到了 Dep.target ,set里面执行的是如果之前的 'hello world' 被改变了就给 dep 发布一个通知 dep.notify(); 这个通知里面将这个dep 实例的属性 subs 数组里面所存储的所有的元素都循环并执行了一遍他的 update 方法,在上面是将 Dep.target 使用 addSub 添加到了实例化的 dep 上的,所以在循环的时候执行的相当于是 Dep.target.update()
- 回到之前的流程图上分析
> <br />
new Vue() 、Observer 和 Dep 的整个的过程都已经分析过了,可以想到现在需要一个 Watcher 将他们给连贯起来
- 再看 MVVM.js 中的代码,有个 Compile 方法没有分析到,这个是起到了编译的作用
> 
- Compile.js 源码
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');
}
}
}, }
> 这是一个 Compile 构造函数和其prototype 方法,这个函数重写了当前函数所有的原型链
- 带注释的 Compile.js 源码
function Compile(node, vm) { //两个参数:js 获取的 id 的DOM节点 vm -> data : { text: ‘hello world’ } 对象 if (node) { //两个参数:js 获取的 id 的DOM节点 vm -> data : { text: ‘hello world’ } 对象 //注册了一个 $.frag 属性 = 一个片段 this.$frag = this.nodeToFragment(node, vm); return this.$frag; } } Compile.prototype = { nodeToFragment: function (node, vm) { //缓存 this var self = this; //createDocumentFragment 创建一个新的空白的文档片段 这个是为了优化性能所使用的方法,在平时的时候在追加的时候会影响性能,这个方法可以更好的增加 DOM 的渲染性能 //https://developer.mozilla.org/zh-CN/docs/Web/API/Document/createDocumentFragment var frag = document.createDocumentFragment(); var child; //node.firstChild 只读属性返回树中节点的第一个子节点,如果节点是无子节点,则返回 null。 //https://developer.mozilla.org/zh-CN/docs/Web/API/Node/firstChild //child = node.firstChild 这个判断相当于是将 node.firstChild 直接赋值给了 child,只有在 node.firstChild = null 也就是无任何的子节点的时候才会返回 fasle 所以在之前会一直的执行里面的循环体 这里是因为下面的 compileElement 在编译的时候将原先的 node(#app) 做了修改,所以这里的 firsetChild 才会每次都找的是下一个节点 while (child = node.firstChild) { //编译子节点 node.firstChild 和 vm - > 这个 new Vue() 实例化后的对象 self.compileElement(child, vm); //将当前元素的子节点不停的塞给 frag 文档片段 frag.append(child); // 将所有子节点添加到fragment中 console.log(child); } //最后将处理过的文档片段再返回回去 return frag; }, compileElement: function (node, vm) { var reg = /{{(.*)}}/; //nodeType 属性返回以数字值返回指定节点的节点类型。 //http://www.w3school.com.cn/jsref/prop_node_nodetype.asp
//节点类型为元素
if (node.nodeType === 1) {
var attr = node.attributes;
// 解析属性
for (var i = 0; i < attr.length; i++) {
//nodeName 属性指定节点的节点名称。
// 如果节点是元素节点,则 nodeName 属性返回标签名。
// 如果节点是属性节点,则 nodeName 属性返回属性的名称。
// 对于其他节点类型,nodeName 属性返回不同节点类型的不同名称。
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) {
//nodeValue 属性设置或返回指定节点的节点值。
//http://www.w3school.com.cn/jsref/prop_node_nodevalue.asp
//这里的 nodeValue 取到的是 {{ text }} 这里面的值
if (reg.test(node.nodeValue)) {
//RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个子匹配(以括号为标志)字符串,
var name = RegExp.$1; // 获取匹配到的字符串
//trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR)。
//https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
name = name.trim();
// node.nodeValue = vm[name]; // 将data的值赋给该node
//参数解析:vm -> this new Vue() 实例化的对象,node -> 传进来的某个 dom 节点 node.firstChild,name -> text {{ text }} 里面的字符串,'nodeValue' 这个是写死的
new Watcher(vm, node, name, 'nodeValue');
}
}
}, }
- 之后引出 Watch.js 文件,可以看到 Watcher 里面加了 Dep.target 属性,可以确定在 Observer 中的 set 里面就是调用了这个属性,这个属性指向的是每一个 new Watcher() 的将 watch push 到每一个 sub 里面,sub 还有一个 value 属性记录想应的 vm 的属性值
- 带注释的 Watcher.js 源码
function Watcher(vm, node, name, type) { Dep.target = this; // name -> text {{ text }} 里面的字符串 this.name = name; //node -> 传进来的某个 dom 节点 node.firstChild this.node = node; //vm -> this new Vue() 实例化的对象 this.vm = vm; //type -> ‘nodeValue’ this.type = type; this.update(); //这里在上面都操作玩之后将其置为 null 是因为 js 是单线程的,这是个全局的变量需要在内存中销毁掉 Dep.target = null; }
Watcher.prototype = {
update: function() {
this.get();
var batcher = new Batcher();
batcher.push(this);
// this.node[this.type] = this.value; // 订阅者执行相应操作
},
cb:function(){
this.node[this.type] = this.value; // 订阅者执行相应操作
},
// 获取data的属性值
get: function() {
//!!!核心
//这个 this 指的就是当前的 Watcher
//this.vm -> new Vue() 实例
//this.name -> text
//因为在 Observer.js 中的 defineReactive 函数中用 Object.defineProperty 写了 get 函数,所以一旦调用 get 属性便会执行 get 内部的函数 dep.addSub(Dep.target); 并返回这个属性的值给 this.value
//这里实现的结果;
//1.this.value 设置成员属性 hello world 取出来了
//2.通知 Observer -> Dep 将 Dep.target 也就是这个 Watcher 塞到想应的 Dep 的属性 subs 数组里面
this.value = this.vm[this.name]; //触发相应属性的get
//上面的是在初始化编译时执行到的方法触发了 get 方法,如果是在 set 的时候执行的是 dep.notify(); sub.update(); 相当于还是执行的上面的 update 方法 然后到这里来了
}
}
- 总结:Vue 主要是通过的 Object.defineProperty() 里面的 set get 方法来进行同步操作的,下面是小示例
var obj = {}; var val = ‘’; Object.defineProperty(obj,’test’,{ get:function(){ console.log(‘我要开始获取了’); return val; }, set:function(newVal){ console.log(‘我要重新设置了’); val = newVal; } }); obj.test = 90; obj.test;
- 依照源码口述整个的实现过程
> 先 new Vue() 一个实例,之后将这个实例的 data 属性和实例本身交给 Observer(小机器人处理所有的属性的) 进行处理,observer 这里是将 data 上的所有的属性循环遍历到了这个实例身上,且每个属性都增加了 get set 方法,在 get 和 set 回调函数中与 dep (依赖集)进行关系的连接,回到 new Vue() 的过程,在 Observer 的下面对实例的 DOM 节点进行了 Compile(编译)的操作,Compile 实现的是遍历实例的每一个节点将为元素和文本节点进行加工,元素判断是否具有 v-model 属性,之后为其绑定事件,文本的话正则判断是否是 {{ }}包起来的,如果是的话讲内部的值去掉两侧的空白符号,之后讲这个文本(示例为:text)取出来,在编译处理操作的最后都将其进行了 new Watcher() 这个 watcher 实现的是先自执行了 update 函数,在函数里面执行 watcher 的 get 方法会取实例的属性, 这样会触发之前的 Observer 里面的 set 属性进行回调,将 Dep.target 也就是这个 Watcher push 到每个 dep 的 subs 数组里面去,同时又将实例的属性(示例为:text)的值赋值给 watcher 的 value 属性,因为在 compile 的时候给元素绑定了 input 事件,里面写的是取 target 输入的值并将其赋值给实例的属性,这样就会触发 Observer 里面的 set 函数,执行 dep.notify() 也就是又执行了一遍 watcher 的 update 函数,这样就实现了双向数据绑定的效果。
- 拓展思维
> <br />
在 Vue 3.0 里面去掉了多余的 Dep 用到了 proxy (代理)代替了 Object.defineProperty() 方法,代理可以监测到上面里面对象的 get 和 set 他都可以,用了一个 proxy 对象就完事儿了,代理了一个对象对象的读和写都可以监测到,这样就省去了 dep 甚至是省去了 watcher ,这个最后在写的时候因为兼容性的问题直接用 babel 一编译就完事儿了。
- proxy 和 defineProperty 的区别
> proxy 可以监测到你里面对象的任意的改变<br />
[Proxy--MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
- proxy 基础示例
let handler = { get: function(target, name){ return name in target ? target[name] : 37; } };
let p = new Proxy({}, handler);
p.a = 1; p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log(‘c’ in p, p.c); // false, 37
```