vue响应式原理

对于vue响应式原理(底层)原理,明天和大家一起探讨钻研,结尾附上手敲代码以及git下载地址,如有有余或不精确请及时留言斧正,期待共同进步~

本文将采纳webpack环境进行编写,我的项目目录如下。
index.js: 入口文件
arrar.js: 数组文件
def.js: 定义一个对象属性
defineReactive.js: 给对象data的属性key定义监听
Dep.js: Dep类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等。
Observe.js: 监听 value,试图创立bserver实例,如果value曾经是响应式数据(依据是否具备__ob__属性判断),就不须要再创立Observer实例,间接返回曾经创立的Observer实例即可。
Observer.js: 将一个失常的object转换为每个层级的属性都是响应式(能够被侦测)的object。
Wather.js: Watcher是一个中介的角色,数据发生变化时告诉它,而后它再告诉其余中央.

先从index.js代码编写,环境搭建完后咱们编写代码,输入就会在控制台展现,定义obj,Object.defineProperty进行数据劫持。

// index.js 入口文件let obj = {  a: 1,  b: {    c: {      d: 4,    },  },  e: [22, 33, 44, 55],};//定义一个函数 defineReactive let val;function defineReactive(data, key, value) {  Object.defineProperty(obj, key, {    // 收集依赖 getter    get() {      console.log('您试图拜访' + key  + '属性');      return val    },    // 触发依赖 setter    set (newVal) {      console.log('你试图拜访 ' + key + '属性', newVal);      if (val === newVal) {        return       }      val = newVal          }  })}defineReactive(obj, 'a', 10)console.log('obj.a', obj.a); // 1obj.a = 8console.log('obj.a扭转后', obj.a); // 8 

在Vue2.X 响应式中应用到了 Object.defineProperty 进行数据劫持,所以咱们对它必须有肯定的理解,Object.defineProperty中有两个十分重要的函数,getter和setter,getter负责收集依赖,简略来说,getter就是收集以后的obj对象的依赖,setter函数则是当你的数据扭转时进行触发的函数。

接下来咱们思考obj.a是一个简略简略的数据类型,那如果我想要简单的对象obj.b.c.d 的数值呢,打印一下控制台发现尽管Object.defineProperty拜访了getter,然而并未监测到obj.b.c.d,只是说监测到b属性。如图2

那咱们该怎么做能力实现对深层次的对象进行监听呢? 咱们是否能够讲每一层对象都循环调用,增加监听,这时是不是听到循环调用就想到了递归,没错咱们能够应用---递归侦听

为了代码参差,首先咱们将defineReactive函数提出来,造成一个独立文件,代码和index.js拆散。

// defineReactive.js/** * 给对象data的属性key定义监听 * @param {*} data 传入的数据 * @param {*} key 监听的属性 * @param {*} value 闭包环境提供的周转变量 */export default function defineReactive (data, key , val) {  if (arguments.length === 2) {    val = data[key]  }    Object.defineProperty(data, key, {      // 可枚举 能够循环      enumerable: true,      // 可被配置,比方能够被删除      configurable: true,      get() {        console.log('您试图拜访' + key  + '属性');        return val      },      set (newVal) {        console.log('你试图拜访 ' + key + '属性', newVal);        if (val === newVal) {          return         }        val = newVal      }    })}

在index.js中引入该文件,代码如下

// index.jsimport defineReactive from "./defineReactive";let obj = {  a: 1,  b: {    c: {      d: 4,    },  },  e: [22, 33, 44, 55],};defineReactive(obj, 'a')defineReactive(obj, 'b')console.log(obj.b.c.d);

接下来开始写递归侦听,新建一个observe类,这个类的作用就是判断是否须要试创立新的Observer类,检测value身上是否有__ob__属性,如果有能够了解为value为响应式,响应式数据,就不须要再创立Observer实例,间接返回曾经创立的Observer实例即可,防止反复侦测value变动的问题,否则,创立Observer实例。

// observe.jsimport Observer from "./Observer";/** * 监听 value * 尝试创立Observer实例,如果value曾经是响应式数据,就不须要再创立Observer实例,间接返回曾经创立的Observer实例即可,防止反复侦测value变动的问题 * @param {*} value  * @returns  */export default function observe(value) {  // 如果value不是对象,就什么都不做  if (typeof value != "object") return;  let ob; // Observer的实例  if (typeof value.__ob__ !== "undefined") {    ob = value.__ob__;  } else {    ob = new Observer(value);  }    return ob;}

接下来写observer类。

// observer.jsimport observe from "./observe";export default class Observer {  constructor (value) {    console.log('我是observer结构器', value);    // 给实例增加__ob__属性,值是以后Observer的实例,不可枚举     // def被独自拎进去了 次要作用就是为了增加__ob__属性带哦    // this是以后new的实例    def(value, "__ob__", this, false);    this.walk(value);  }  // 遍历  walk(value) {    for (let k in value) {      defineReactive(value, k)    }  } }// def.js/** * 定义一个对象属性 * @param {*} obj  * @param {*} key  * @param {*} value  * @param {*} enumerable  */export default function def(obj, key, value, enumerable) {  Object.defineProperty(obj, key, {    value,    enumerable,    writable: true,    configurable: true,  });}

在observer类中,咱们在结构器内执行def函数,def函数次要作用是为以后实例(obj)增加__ob__属性,后面说过了这个属性是代表实例是否是响应式的标记。而后调用walk办法循环实例,在循环里调用defineReactive函数。至此,外层的属性(obj.a)曾经成为响应式了
在index.js中 创立observe 函数 observe(obj),能够看见控制台打印如下,并发现__ob__属性已存在。

然而咱们会发现在obj.b obj.c的身上没有__ob__属性。

咱们写Observer的目标是为了递归侦听,当初咱们对外层的元素曾经实现了监测,思考下咱们当初只剩下对外部的属性进行侦听了,那么该怎么做呢?

拆解一下,首先要监测外部元素,少不了循环,那如果在循环中对每一层进行监测不就ok了吗?循环咱们写过了,剩下的就是须要在efineReactive函数外部调用observe类,observe子元素,而observe中又会调用observer类,而后循环,最初在setter中监测新的子元素的值即可。看下整体代码。

// index.jsimport observe from "./observe";import defineReactive from "./defineReactive";let obj = {  a: 1,  b: {    c: {      d: 4,    },  },  e: [22, 33, 44, 55],};// 创立observe 函数 observe(obj)// observe.jsimport Observer from "./Observer";/** * 监听 value * 尝试创立Observer实例,如果value曾经是响应式数据,就不须要再创立Observer实例,间接返回曾经创立的Observer实例即可,防止反复侦测value变动的问题 * @param {*} value  * @returns  */export default function observe(value) {  // 如果value不是对象,就什么都不做  if (typeof value != "object") return;  let ob; // Observer的实例  if (typeof value.__ob__ !== "undefined") {    ob = value.__ob__;  } else {    ob = new Observer(value);  }    return ob;}// observer.jsdef from "./def";import defineReactive from "./defineReactive";export default class Observer {  constructor (value) {    console.log('我是observer结构器', value);    // 给实例增加__ob__属性,值是以后Observer的实例,不可枚举     def(value, "__ob__", this, false);    this.walk(value)  }  // 遍历  walk(value) {    for (let k in value) {      defineReactive(value, k)    }  }  }// def.js/** * 定义一个对象属性 * @param {*} obj  * @param {*} key  * @param {*} value  * @param {*} enumerable  */export default function def(obj, key, value, enumerable) {  Object.defineProperty(obj, key, {    value,    enumerable,    writable: true,    configurable: true,  });}// defineReactive.jsimport observe from "./observe";/** * 给对象data的属性key定义监听 * @param {*} data 传入的数据 * @param {*} key 监听的属性 * @param {*} value 闭包环境提供的周转变量 */export default function defineReactive (data, key , val) {  console.log('递归侦听属性',key);  if (arguments.length === 2) {    val = data[key]  }  // 子元素要进行observe,造成递归  let childOb = observe(val);    Object.defineProperty(data, key, {      // 可枚举 能够循环      enumerable: true,      // 可被配置,比方能够被删除      configurable: true,      get() {        console.log('您试图拜访' + key  + '属性');        return val      },      set (newVal) {        console.log('你试图拜访 ' + key + '属性', newVal);        if (val === newVal) {          return         }        // 当设置了新值,新值也要被observe        childOb = observe(newVal);        val = newVal      }    })}

在具体解说一下下面这写代码逻辑,将代码串联一下。
首先,在index.js中let一个对象obj,调用obeserve函数(留神不是observer)在observe函数外部,首先查看是否为响应式,如果是,则不须要创立observer实例,防止反复监听,节约资源。显然以后的obj是非响应式的,那么就须要创立一个observer实例,进入observer.js文件,进入observer外部执行defineReactive办法,进defineReactive文件,最要害一步,子元素要进行observe,造成递归(这个递归不是本人调用本人,而是多个函数嵌套调用)---〉这行代码 let childOb = observe(val);实现了递归的重要一步,接下来要在setter函数中,更新值,childOb = observe(newVal);能够看见控制台会输出obj的每个属性,b、c、d

咱们设置obj.b.c.d = 110, 打印控制台看下是否会失效。

以上递归侦听能够演绎为下图

上面,咱们在已有函数的根底上将数组的响应式原理补上去。数组的响应式原理。尤雨溪老师讲数组的七个办法进行了改写("push", "pop", "shift", "unshift", "splice",
"sort", "reverse",),数组的侦听能够简略的了解为如下原理

咱们将数组的隐式原型链__proto__指向arrayMethods,而arrayMethods是以Array.prototype为原型创立的,咱们在arrayMethods上改写办法即可,新建array.js

首先备份一份,而后调用def.js, 增加属性__ob__。小伙伴们是否记得咱们在observer外部应用walk办法循环了实例,而后调用defineReactive办法,就是这里咱们须要补充一下数组的办法,减少代码如下,判断实例是对象还是数组,数组的话,就将这个数组的原型指向arrayMethods,去改写数组办法,而后执行observeArray(数组的遍历办法),简略来说就是去侦测数组中的每一项,一一进行observe。

// arrar.jsimport def from "./def";const arrayPrototype = Array.prototype;// 以Array.prototype为原型创立arrayMethodexport const arrayMethods = Object.create(arrayPrototype);// 要被改写的7个数组办法const methodsNeedChange = [  "push",  "pop",  "shift",  "unshift",  "splice",  "sort",  "reverse",];// 批量操作这些办法methodsNeedChange.forEach((methodName) => {  // 备份原来的办法  const original = arrayPrototype[methodName];  // 定义新的办法  def(    arrayMethods,    methodName,    function () {      console.log("array数据曾经被劫持");      // 复原原来的性能(数组办法)      const result = original.apply(this, arguments);      // 把类数组对象变成数组      const args = [...arguments];      // 把这个数组身上的__ob__取出来      // 在拦截器中获取Observer的实例      const ob = this.__ob__;      // 有三种办法 push、unshift、splice能插入新项,要劫持(侦测)这些数据(插入新项)      let inserted = [];      switch (methodName) {        case "push":        case "unshift":          inserted = args;          break;        case "splice":          inserted = args.slice(2);          break;      }      // 查看有没有新插入的项inserted,有的话就劫持      // ob.observeArray实例外部办法      if (inserted) {        ob.observeArray(inserted);      }      return result;    },    false  );});// observer.js// 在observer 外部判断是否为数组import def from "./def";import defineReactive from "./defineReactive";import { arrayMethods } from "./array";export default class Observer {  constructor (value) {    console.log('我是observer结构器', value);    // 给实例增加__ob__属性,值是以后Observer的实例,不可枚举     def(value, "__ob__", this, false);    // 判断是数组还是对象    if (Array.isArray(value)) {      // 是数组,就将这个数组的原型指向arrayMethods      Object.setPrototypeOf(value, arrayMethods);      // 晚期实现是这样      // value.__proto__ = arrayMethods;            // observe数组      this.observeArray(value);    } else {      this.walk(value);    }  }  // 遍历  walk(value) {    for (let k in value) {      defineReactive(value, k)    }  }  // 数组的遍历形式,侦测数组中的每一项  observeArray(arr) {    for (let i = 0, l = arr.length; i < l; i++) {      // 逐项进行observe      observe(arr[i]);    }  }  }

自此数组的侦测曾经实现。

上面进行收集依赖和watcher的解说。
新建一个Dep类,Dep类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等,新建文件Dep.js,Dep类中蕴含addSub,depend,notify办法。watcher则是中转站,依赖的变动须要告诉watcher。

新建dep类

let uid = 0;/** * Dep类专门帮忙咱们治理依赖,能够收集依赖,删除依赖,向依赖发送告诉等 */export default class Dep {  constructor() {    console.log("Dep结构器", this);    this.id = uid++;    // 用数组存储本人的订阅者,放的是Watcher的实例    this.subs = [];  }  // 增加订阅  addSub(sub) {    this.subs.push(sub);  }  // 删除订阅  removeSub(sub) {    remove(this.subs, sub);  }  // 增加依赖  depend() {    // Dep.target 是一个咱们指定的全局的地位,用window.target也行,只有是全局惟一,没有歧义就行    if (Dep.target) {      this.addSub(Dep.target);    }  }  // 告诉更新  notify() {    console.log("告诉更新notify");    // 浅拷贝一份    const subs = this.subs.slice();    // 遍历    for (let i = 0, l = subs.length; i < l; i++) {      subs[i].update();    }  }}/** * 从arr数组中删除元素item * @param {*} arr * @param {*} item * @returns */function remove(arr, item) {  if (arr.length) {    const index = arr.indexOf(item);    if (index > -1) {      return arr.splice(index, 1);    }  }}

那么收集依赖应该在哪里收集呢,答案很显然是在Object.defineProperty中,因为Object.defineProperty天生能够进行数据劫持,故咱们在Object.defineProperty中创立Dep数组。在getter中判断以后是否处于依赖收集阶段(Dep.target为true),还须要对子元素进行判断。

// defineReactive.js// 能够了解为所有的依赖收集工作都有Dep实现,而后在告诉watcherimport Dep from "./Dep";import observe from "./observe";/** * 给对象data的属性key定义监听 * @param {*} data 传入的数据 * @param {*} key 监听的属性 * @param {*} value 闭包环境提供的周转变量 */export default function defineReactive(data, key, value) {  console.log('执行defineReactive()', key)    // 每个数据都要保护一个属于本人的数组,用来寄存依赖本人的watcher  const dep = new Dep();  if (arguments.length === 2) {    value = data[key];  }  // 子元素要进行observe,造成递归  let childOb = observe(value);  Object.defineProperty(data, key, {    // 可枚举 能够for-in    enumerable: true,    // 可被配置,比方能够被delete    configurable: true,    // getter  收集依赖    get() {      console.log(`getter试图拜访${key}属性 侦测中 `);      // 收集依赖 Dep.target就是以后的wather实例      if (Dep.target) {        dep.depend();        // 判断有没有子元素        if (childOb) {          // 数组收集依赖          childOb.dep.depend();        }      }      return value;    },    // setter 触发依赖    set(newValue) {      console.log(`setter试图扭转${key}属性 侦测中`, newValue);      if (value === newValue) return;      value = newValue;      // 当设置了新值,新值也要被observe      childOb = observe(newValue);      // 触发依赖      // 公布订阅模式,告诉dep      dep.notify();    },  });}

最初咱们还差一个watcher类,用来承受dep的音讯,并更新。

// watcher.jsimport Dep from "./Dep";let uid = 0;/** * Watcher是一个中介的角色,数据发生变化时告诉它,而后它再告诉其余中央 */export default class Watcher {  constructor(target, expression, callback) {    console.log("Watcher结构器");    this.id = uid++;    this.target = target;    // 按点拆分  执行this.getter()就能够读取data.a.b.c的内容    this.getter = parsePath(expression);    this.callback = callback;    this.value = this.get();  }  get() {    // 进入依赖收集阶段。    // 让全局的Dep.target设置为Watcher自身    Dep.target = this;    const obj = this.target;    var value;    // 只有能找就始终找    try {      value = this.getter(obj);    } finally {      Dep.target = null;    }    return value;  }  update() {    this.run();  }   run() {    this.getAndInvoke(this.callback);  }  getAndInvoke(callback) {    const value = this.get();    if (value !== this.value || typeof value === "object") {      const oldValue = this.value;      this.value = value;      callback.call(this.target, value, oldValue);    }  }}/** * 将str用.宰割成数组segments,而后循环数组,一层一层去读取数据,最初拿到的obj就是str中想要读的数据 * @param {*} str * @returns */function parsePath(str) {  let segments = str.split(".");  return function (obj) {    for (let key of segments) {      if (!obj) return;      obj = obj[key];    }    return obj;  };}

Watcher类中须要留神的是结构器中的getter,getter接管parsePath函数的返回值,parsePath函数的次要作用是用.宰割成数组segments,而后循环数组,一层一层去读取数据,最初拿到的obj就是str中想要读的数据,对于以后例子来说就是当咱们对obj在data中定义时,内层obj.b.c.d : 4(初始值),这里的拿到的就是obj.b.c.d的初始值4。

到此响应式原理根本实现。如图

结尾处画了一张便于了解的图。

心愿能够帮忙到大家,也心愿大家踊跃留言点赞,欢送交换,前端小白,请各位大佬多点容纳~~ 一起提高

参考文件:https://www.bilibili.com/vide...