前言

  • 列表

image.png
Vue是一个快速实现双向绑定的库,思想是最重要的,库永远是会被替代的东西。

正文

Vue 架构概览

  • github src 目录

image.png

  • config 里面也是和我们平常写的东西那样分成类似于 开发环境 和 生产环境的,web开发用的 server端开发用的,weex 开发用的,因为 vue 是一个库,所以就是将所有的东西都给支持好,这样才是一个合格的库

image.png
image.png

  • vue 的核心文件夹就是 code 和 platforms (平台)

image.png

双向数据绑定

  1. 引入概念

image.png
image.png

  1. 主要实现逻辑的源码

image.png

  1. 主要用到的五大方法

image.png

  • Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

  • Observer

监听器

  • Watcher

观察者

  • Dep

收集依赖

  • Directive

组件

  1. vue 双向绑定的实现图(必须要理解的东西)

image.png

  • 上面流程图整个的思路解析

首先是 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
image.png

  • 基于流程图的源码小示例

image.png

  1. var obj = {};
  2. var a;
  3. Object.defineProperty(obj,'a',{
  4. get:function(){
  5. console.log('get val');
  6. return a;
  7. },
  8. set:function(newVal){
  9. console.log('Set Val' + newVal);
  10. a = newVal;
  11. }
  12. });
  13. obj.a;//get val
  14. obj.a = '111';//Set Val 111

{{ a }} 就相当于是 get 读
v-model=”a” 就相当于是 set 写
image.png

  • 双向数据绑定官方示图,相较于上面的少了 Dep 这个东西

image.png
setter 触发消息到 Watcher ,Watcher 帮忙告诉 Dircetive 更新 Dom ,DOM 中修改了数据也会通知给 Watcher ,Watcher 帮忙修改数据。

  1. 解析双向绑定实现的源码

image.png

  • 首先 new Vue() 这个 Vue 必须是一个全局变量,这个全局变量是从 MVVM.js 里面来的
  • MVVM.js 源码 ``` function Vue(options) {
    1. this.data = options.data;
    2. var data = this.data;
    3. observe(data, this);
    4. var id = options.el;
    5. var dom =new Compile(document.getElementById(id),this);
    6. // 编译完成后,将dom返回到app中
    7. document.getElementById(id).appendChild(dom);
    }
  1. > 里面的 options 就等于 new Vue() 里面传递的对象参数。<br />
  2. 这里面的 this 都指向的是新 new 出来的实例 vm<br />
  3. 首先在 this 上增加了一个 data 属性 = options.data;相当于是 this.data = { text:'hello world' } ;<br />
  4. 之后又把这个 data 取出来 传递给了 observer 。<br />
  5. 之后取出传递的 id 名,再将 id 传递给 Compile 进行操作,操作返回的结果 dom 再追加到 id 的子级中
  6. - 加注释版本

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. //1.this.data = { text: 'hello world' }
  2. //2.observer Compile
  1. > 可以看到无论是 observer 还是 Compile 都是随时带着 this (新实例)走的
  2. - 上面将 new 的过程分析完了,下面分析 observer
  3. - Observer.js 源码
  1. function defineReactive (obj, key, val) {
  2. var dep = new Dep();
  3. Object.defineProperty(obj, key, {
  4. get: function() {
  5. //添加订阅者watcher到主题对象Dep
  6. if(Dep.target) {
  7. // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
  8. dep.addSub(Dep.target);
  9. }
  10. return val;
  11. },
  12. set: function (newVal) {
  13. if(newVal === val) return;
  14. val = newVal;
  15. console.log(val);
  16. // 作为发布者发出通知
  17. dep.notify();
  18. }
  19. })
  20. }
  21. function observe(obj, vm) {
  22. Object.keys(obj).forEach(function(key) {
  23. defineReactive(vm, key, obj[key]);
  24. })
  25. }
  1. - observer 函数
  2. > ![image.png](https://upload-images.jianshu.io/upload_images/9064013-f98af90d4b2acaf9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  3. - 带注释版的 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]); }) }

  1. > 经过上面的步骤可以看到,this -> vm 这个实例已经是可以随时带着走了,而且 data 里面的对象也已经实现了分离,分离之后就可以和 html 中写的 {{ text }} v-model="text" 这种的进行联合起来了
  2. - 之后看 Observer.js 里面的 defineReactive (定义反应)函数

function defineReactive (obj, key, val) {

  1. var dep = new Dep();
  2. Object.defineProperty(obj, key, {
  3. get: function() {
  4. //添加订阅者watcher到主题对象Dep
  5. if(Dep.target) {
  6. // JS的浏览器单线程特性,保证这个全局变量在同一时间内,只会有同一个监听器使用
  7. dep.addSub(Dep.target);
  8. }
  9. return val;
  10. },
  11. set: function (newVal) {
  12. if(newVal === val) return;
  13. val = newVal;
  14. console.log(val);
  15. // 作为发布者发出通知
  16. dep.notify();
  17. }
  18. })
  19. }
  1. > 什么时候会有反应呢?就是通过 set get 的函数回调
  2. - 带注释版的 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 这两个是同一个东西了 }

  1. - 之后是 defineReavtive 函数内使用的 Dep ,上面的分析之后会想到现在需要的是让页面上的 v-model="text" set 相关联起来,可以同步的发生变化
  2. - Dep.js 源码

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

  1. - 带注释版的 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(); }) } }

  1. - 之后回到 Observer.js 中的 defineReactive 函数中去

//这里 new Dep() 创建了一个实例 该实例有一个 subs 属性 值为 [],还有 addSub(往 subs 属性里面推东西)和 noify(通知)两个方法 var dep = new Dep();

  1. - 上面分析的代码有两个关键的东西暂时没有分析到

sub.update()

Dep.terget

  1. > ![image.png](https://upload-images.jianshu.io/upload_images/9064013-bacfa6c46376e741.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)<br />
  2. ![image.png](https://upload-images.jianshu.io/upload_images/9064013-3a9c9de4fb52351d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  3. - 接着分析
  4. > get 里面用到了 Dep.target ,set里面执行的是如果之前的 'hello world' 被改变了就给 dep 发布一个通知 dep.notify(); 这个通知里面将这个dep 实例的属性 subs 数组里面所存储的所有的元素都循环并执行了一遍他的 update 方法,在上面是将 Dep.target 使用 addSub 添加到了实例化的 dep 上的,所以在循环的时候执行的相当于是 Dep.target.update()
  5. - 回到之前的流程图上分析
  6. > ![image.png](https://upload-images.jianshu.io/upload_images/9064013-d756aa6da73eb73d.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)<br />
  7. new Vue() Observer Dep 的整个的过程都已经分析过了,可以想到现在需要一个 Watcher 将他们给连贯起来
  8. - 再看 MVVM.js 中的代码,有个 Compile 方法没有分析到,这个是起到了编译的作用
  9. > ![image.png](https://upload-images.jianshu.io/upload_images/9064013-4fe274536cbcd863.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
  10. - 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;

  1. while (child = node.firstChild) {
  2. self.compileElement(child, vm);
  3. frag.append(child); // 将所有子节点添加到fragment中
  4. }
  5. return frag;

}, compileElement: function (node, vm) { var reg = /{{(.*)}}/;

  1. //节点类型为元素
  2. if (node.nodeType === 1) {
  3. var attr = node.attributes;
  4. // 解析属性
  5. for (var i = 0; i < attr.length; i++) {
  6. if (attr[i].nodeName == 'v-model') {
  7. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
  8. node.addEventListener('input', function (e) {
  9. // 给相应的data属性赋值,进而触发该属性的set方法
  10. vm[name] = e.target.value;
  11. });
  12. // node.value = vm[name]; // 将data的值赋给该node
  13. new Watcher(vm, node, name, 'value');
  14. }
  15. };
  16. }
  17. //节点类型为text
  18. if (node.nodeType === 3) {
  19. if (reg.test(node.nodeValue)) {
  20. var name = RegExp.$1; // 获取匹配到的字符串
  21. name = name.trim();
  22. // node.nodeValue = vm[name]; // 将data的值赋给该node
  23. new Watcher(vm, node, name, 'nodeValue');
  24. }
  25. }

}, }

  1. > 这是一个 Compile 构造函数和其prototype 方法,这个函数重写了当前函数所有的原型链
  2. - 带注释的 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

  1. //节点类型为元素
  2. if (node.nodeType === 1) {
  3. var attr = node.attributes;
  4. // 解析属性
  5. for (var i = 0; i < attr.length; i++) {
  6. //nodeName 属性指定节点的节点名称。
  7. // 如果节点是元素节点,则 nodeName 属性返回标签名。
  8. // 如果节点是属性节点,则 nodeName 属性返回属性的名称。
  9. // 对于其他节点类型,nodeName 属性返回不同节点类型的不同名称。
  10. if (attr[i].nodeName == 'v-model') {
  11. var name = attr[i].nodeValue; // 获取v-model绑定的属性名
  12. node.addEventListener('input', function (e) {
  13. // 给相应的data属性赋值,进而触发该属性的set方法
  14. vm[name] = e.target.value;
  15. });
  16. // node.value = vm[name]; // 将data的值赋给该node
  17. new Watcher(vm, node, name, 'value');
  18. }
  19. };
  20. }
  21. //节点类型为text
  22. if (node.nodeType === 3) {
  23. //nodeValue 属性设置或返回指定节点的节点值。
  24. //http://www.w3school.com.cn/jsref/prop_node_nodevalue.asp
  25. //这里的 nodeValue 取到的是 {{ text }} 这里面的值
  26. if (reg.test(node.nodeValue)) {
  27. //RegExp.$1是RegExp的一个属性,指的是与正则表达式匹配的第一个子匹配(以括号为标志)字符串,
  28. var name = RegExp.$1; // 获取匹配到的字符串
  29. //trim() 方法会从一个字符串的两端删除空白字符。在这个上下文中的空白字符是所有的空白字符 (space, tab, no-break space 等) 以及所有行终止符字符(如 LF,CR)。
  30. //https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/String/Trim
  31. name = name.trim();
  32. // node.nodeValue = vm[name]; // 将data的值赋给该node
  33. //参数解析:vm -> this new Vue() 实例化的对象,node -> 传进来的某个 dom 节点 node.firstChild,name -> text {{ text }} 里面的字符串,'nodeValue' 这个是写死的
  34. new Watcher(vm, node, name, 'nodeValue');
  35. }
  36. }

}, }

  1. - 之后引出 Watch.js 文件,可以看到 Watcher 里面加了 Dep.target 属性,可以确定在 Observer 中的 set 里面就是调用了这个属性,这个属性指向的是每一个 new Watcher() 的将 watch push 到每一个 sub 里面,sub 还有一个 value 属性记录想应的 vm 的属性值
  2. - 带注释的 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

  1. //上面的是在初始化编译时执行到的方法触发了 get 方法,如果是在 set 的时候执行的是 dep.notify(); sub.update(); 相当于还是执行的上面的 update 方法 然后到这里来了
  2. }

}

  1. - 总结: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;

  1. - 依照源码口述整个的实现过程
  2. > 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 函数,这样就实现了双向数据绑定的效果。
  3. - 拓展思维
  4. > ![image.png](https://upload-images.jianshu.io/upload_images/9064013-8a148d635aa51145.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)<br />
  5. Vue 3.0 里面去掉了多余的 Dep 用到了 proxy (代理)代替了 Object.defineProperty() 方法,代理可以监测到上面里面对象的 get set 他都可以,用了一个 proxy 对象就完事儿了,代理了一个对象对象的读和写都可以监测到,这样就省去了 dep 甚至是省去了 watcher ,这个最后在写的时候因为兼容性的问题直接用 babel 一编译就完事儿了。
  6. - proxy defineProperty 的区别
  7. > proxy 可以监测到你里面对象的任意的改变<br />
  8. [Proxy--MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy)
  9. - 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

```