vue响应式原理,底层源码分析,底层实例分析

501 阅读11分钟

vue响应式原理

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

image.png

本文将采用webpack环境进行编写,项目目录如下。 index.js: 入口文件 arrar.js: 数组文件 def.js: 定义一个对象属性 defineReactive.js: 给对象data的属性key定义监听。 Dep.js: Dep类专门帮助我们管理依赖,可以收集依赖,删除依赖,向watcher发送通知等。 Observe.js: 监听 value,试图创建bserver实例,如果value已经是响应式数据(根据是否具有__ob__属性判断),就不需要再创建Observer实例,直接返回已经创建的Observer实例即可。 Observer.js: 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object。 Wather.js: Watcher是一个中介的角色,数据发生变化时通知它,然后它再通知其他地方.

image.png

先从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); // 1
obj.a = 8
console.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

image.png

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

为了代码整齐,首先我们将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.js
import 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.js
import 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.js

import 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__属性已存在。

image.png

但是我们会发现在obj.b obj.c的身上没有__ob__属性。

image.png

我们写Observer的目的是为了递归侦听,现在我们对外层的元素已经完成了监测,思考下我们现在只剩下对内部的属性进行侦听了,那么该怎么做呢?

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

// index.js
import observe from "./observe";
import defineReactive from "./defineReactive";
let obj = {
  a: 1,
  b: {
    c: {
      d: 4,
    },
  },
  e: [22, 33, 44, 55],
};
// 创建observe 函数 
observe(obj)



// observe.js
import 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.js

def 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.js
import 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

image.png

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

image.png

以上递归侦听可以归纳为下图

image.png

下面,我们在已有函数的基础上将数组的响应式原理补上去。数组的响应式原理。尤雨溪老师讲数组的七个方法进行了改写("push", "pop", "shift", "unshift", "splice", "sort", "reverse",),数组的侦听可以简单的理解为如下原理

image.png

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

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

// arrar.js

import def from "./def";

const arrayPrototype = Array.prototype;

// 以Array.prototype为原型创建arrayMethod
export 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完成,然后在通知watcher

import 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.js

import 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。

到此响应式原理基本完成。如图

image.png

希望可以帮助到大家,也希望大家积极留言点赞,欢迎交流,前端小白,请各位大佬多点包容~~ 一起进步

项目地址(可以直接跑起来github):github.com/yu156456300…

参考文件:www.bilibili.com/video/BV1G5…