Vue2数据响应式原理

2,402 阅读18分钟

一、 前言

1. 概述

数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

使用Vue时,我们只需要修改数据(state),视图就能够获得相应的更新,这就是响应式系统。要实现一个自己的响应式系统,我们首先要明白要做什么事情:

  1. 数据劫持:当数据变化时,我们可以做一些特定的事情
  2. 依赖收集:我们要知道那些视图层的内容(DOM)依赖了哪些数据(state)
  3. 派发更新:数据变化后,如何通知依赖这些数据的DOM

接下来,我们将一步步地实现一个自己的玩具响应式系统

2. 前置知识

Vue会监视data中所有层次的数据

  • 如何监测对象中的数据?

通过setter实现监视,且要在new Vue 时就传入要监测的数据

  1. 对象中后追加的属性,Vue默认不做响应式处理
  2. 如需给后添加的属性做响应式,需要使用如下API
Vue.set(target, propertyName/index, value)
vm.$set(target, propertyName/index, value)
  • 如何检测数组中的数据?

通过包裹数组更新元素的方法实现,本质就是做了两件事:

  1. 调用原生数组对应的方法对数组进行更新
  2. 重新解析模板,进而更新页面

在Vue修改数组中的某个元素一定要用如下方法:

API: push() pop() shift() unshift() splice() sort() reverse()
Vue.set() 或 vm.$set

注意:Vue.set() 和 vm.$set() 不能给vm 或 vm的根数据对象添加属性

Object.defineProperty()

juejin.cn/post/699508…

  • gettersetter方法可以对数据进行监听,访问和设置都会被监听捕获
  • 读取数据的时候会触发getter,而修改数据的时候会触发setter

二、 数据劫持

几乎所有的文章和教程,在讲解Vue响应式系统时都会先讲:Vue使用Object.defineProperty来进行数据劫持。那么,我们也从数据劫持讲起,大家可能会对劫持这个概念有些迷茫,没有关系,看完下面的内容,你一定会明白。

Object.defineProperty的用法在此不多做介绍,不明白的同学可在MDN上查阅。下面,我们为obj定义一个a属性

const obj = {}

let val = 1
Object.defineProperty(obj, a, {
  get() { // 下文中该方法统称为getter
    console.log('get property a')
    return val
  },
  set(newVal) { // 下文中该方法统称为setter
    if (val === newVal) return
    console.log(`set property a -> ${newVal}`)
    val = newVal
  }
})

这样,当我们访问obj.a时,打印get property a并返回1,obj.a = 2设置新的值时,打印set property a -> 2。这相当于我们自定义了obj.a取值和赋值的行为,使用自定义的gettersetter来重写了原有的行为,这也就是数据劫持的含义。

很多同学可能会有疑问,为什么这里要用一个val,而不是在get和set函数中直接return obj.aobj.a = val呢?

  如果我们直接在get函数中return obj.a的话,这里的obj.a同时也会调用一次get函数,这样的话会陷入一个死循环;set函数也是同样的道理,因此我们通过一个第三方的变量val来防止死循环。

  但是如果我们需要代理更多的属性,不可能给每一个属性定义一个第三方的变量,可以通过闭包来解决

1. 使用defineReactive 函数

所以我们就自己定义一个函数,对defineProperty进行封装,来实现数据劫持,使用defineReactive 函数不需要设置临时变量了,而是用闭包

// value使用了参数默认值
function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    get: function reactiveGetter() {
      return value
    },
    set: function reactiveSetter(newValue) {
      if (newValue === value) return
      value = newValue
    }
  })
}

defineReactive(obj, "a", 1)

2. 对象与数组的响应式处理

流程大概是这样:observe -> Observer -> defineReactive -> observe -> Observer -> defineReactive 递归

  • Observer:观察者对象,对对象或数组进行响应式处理的地方
  • defineReactive:拦截对象上每一个keyget与set函数的地方
  • observe:响应式处理的入口
const { arrayMethods } = require('./array')
const obj = {
  a: 1,
  b: {
    c: 2
  }
}

observe(obj)

function observe(value) {
    // 如果传进来的是对象或者数组,则进行响应式处理
    if (Object.prototype.toString.call(value) === '[object Object]' || Array.isArray(value)) {
        return new Observer(value)
    }
}

// 观察者对象,使用es6的class来构建会比较方便 -- 将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
class Observer {
    constructor(value) {
        // 给传进来的value对象或者数组设置一个__ob__对象
        // 这个__ob__对象大有用处,如果value上有这个__ob__,则说明value已经做了响应式处理
        Object.defineProperty(value, '__ob__', {
            value: this, // 值为this,也就是new出来的Observer实例
            enumerable: false, // 不可被枚举
            writable: true, // 可用赋值运算符改写__ob__
            configurable: true // 可改写可删除
        })

        // 判断value是函数还是对象
        if(Array.isArray(value)) {  
            // 是数组,就将这个数组的原型指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods); 
            // 早期实现是这样  value.__proto__ = arrayMethods;
            
            // 如果数组里面还包含数组 需要递归判断
            this.observeArray(value)
        } else {
            // 如果是对象,则执行walk函数对对象进行响应式处理
            this.walk(value)
        }
    }

    walk(data) {
          // 获取data对象的所有key
          //let keys = Object.keys(data)
          // 遍历所有key,对每个key的值进行响应式处理
          // for(let i = 0; i < keys.length; i++) {
          //    const key = keys[i]
          //    const value = data[key]
          // 传入data对象,key,以及value
          //defineReactive(data, key, value)
         //}
      Object.keys(data).forEach((key) => {
        defineReactive(data, key, data[key]);
      });
    }

    observeArray(items) {
        // 遍历传进来的数组,如果数组里面还包含着数组,则递归处理
        for(let i = 0; i < items.length; i++) {
            observe(items[i])
        }
    }
}

function defineReactive(data, key, value) {
    // 递归重要步骤
    // 因为对象里可能有对象或者数组,所以需要递归
    observe(value)

    // 核心
    // 拦截对象里每个key的get和set属性,进行读写监听
    // 从而实现了读写都能捕捉到,响应式的底层原理
    Object.defineProperty(data, key, {
        get() {
            console.log('获取值')
            return value
        },
        set(newVal) {
            if (newVal === value) return
            console.log('设置值')
            value = newVal
            observe(newVal)
        }
    })
}

因为对数组下标的拦截太浪费性能 对 Observer 构造函数传入的数据参数增加了数组的判断

// src/obserber/index.js
class Observer {
  // 观测值
  constructor(value) {
    Object.defineProperty(value, "__ob__", {
      //  值指代的就是Observer的实例
      value: this,
      //  不可枚举
      enumerable: false,
      writable: true,
      configurable: true,
    });
  }
}

// 抽离成def函数:
  function def(obj, key, value, enumerable) {
    Object.defineProperty(obj, key, {
      value,
      enumerable,
      writable: true,
      configurable: true,
    });
  }
// 则可以写成:
class Observer {
  // 观测值
  constructor(value) {
    def(value, "__ob__", this, false);
  }
}

对数组原型重写之前咱们先要理解这段代码 这段代码的意思就是给每个响应式数据增加了一个不可枚举的__ob__属性 并且指向了 Observer 实例 那么我们首先可以根据这个属性来防止已经被响应式观察的数据反复被观测 其次 响应式数据可以使用__ob__来获取 Observer 实例的相关方法 这对数组很关键

Vue是通过改写数组的七个方法(可以改变数组自身内容的方法)来实现对数组的响应式处理

这些方法分别是:push、pop、shift、unshift、splice、sort、reverse

20210426152129897.png

这七个方法都是定义在Array.prototype上,要保留方法的功能,同时增加数据劫持的代码

思路就是 以Array.prototype为原型,创建一个新对象arrayMthods, 然后在新对象arrayMthods上定义(改写)这些方法, 最后用Object.setPrototypeOf(object,arrayMthods)方法定义 数组的原型指向 arrayMthods

这就相当于用一个拦截器覆盖Array.prototype,每当使用Array原型上的方法操作数组时,其实执行的是拦截器中提供的方法。在拦截器中使用原生Array的原型方法去操作数组。

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,有的话就劫持
      if (inserted) {
        ob.observeArray(inserted);
      }

      return result;
    },
    false
  );
});

3. 为什么对象和数组要分开处理呢

  • 对象的属性通常比较少,对每一个属性都劫持set和get,并不会消耗很多性能
  • 数组有可能有成千上万个元素,如果每一个元素都劫持set和get,无疑消耗太多性能了
  • 所以对象通过defineProperty进行正常的劫持set和get
  • 数组则通过修改数组原型上的部分方法,来实现修改数组触发响应式

可以看出,上面三个函数的调用关系如下:

三个函数相互调用从而形成了递归,与普通的递归有所不同。 有些同学可能会想,只要在setter中调用一下渲染函数来重新渲染页面,不就能完成在数据变化时更新页面了吗?确实可以,但是这样做的代价就是:任何一个数据的变化,都会导致这个页面的重新渲染,代价未免太大了吧。我们想做的效果是:数据变化时,只更新与这个数据有关的DOM结构,那就涉及到下文的内容了:依赖

三、发布-订阅模式与观察者模式

1. 定性区别

首先,观察者是经典软件设计模式中的一种,但发布订阅只是软件架构中的一种消息范式。所以不要再被“观察者模式和发布订阅模式xxx”这样的问题误导。

2. 组成区别

其次,就是实现二者所需的角色数量有着明显的区别。观察者模式本身只需要2个角色便可成型,即观察者被观察者,其中被观察者是重点。而发布订阅需要至少3个角色来组成,包括发布者订阅者发布订阅中心,其中发布订阅中心是重点。

观察者模式发布订阅
2个角色3个角色
重点是被观察者重点是发布订阅中心

3. 观察者模式(Observer Pattern)

观察者模式定义了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新。观察者模式属于行为型模式,行为型模式关注的是对象之间的通讯,观察者模式就是观察者和被观察者之间的通讯。

观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。比如当你订阅了一份报纸,每天都会有一份最新的报纸送到你手上,有多少人订阅报纸,报社就会发多少份报纸,报社和订报纸的客户就是上面文章开头所说的“一对多”的依赖关系。

手写观察者模式

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      class Notifier {
        //通知者
        constructor() {
          this.observerList = []; //观察者列表
        }
        add(obj) {
          this.observerList.push(obj); //添加观察者
        }
        remove(obj) {
          this.observerList = this.observerList.filter((item) => item !== obj);
        }
        notify() {
          //通知每个观察者
          this.observerList.forEach((obj) => {
            obj.update();
          });
        }
      }

      class Observer {
        //观察者
        constructor(name) {
          this.name = name;
        }
        update() {
          console.log(this.name, "收到通知");
        }
      }

      let notifier = new Notifier();
      let observer1 = new Observer("张三");
      let observer2 = new Observer("李四");
      let observer3 = new Observer("王五");
      notifier.add(observer1);
      notifier.add(observer2);
      notifier.add(observer3);
      notifier.remove(observer1); //测试删除
      notifier.notify();
      console.log(notifier);
    </script>
  </body>
</html>

4. 发布订阅模式(Pub-Sub Pattern)

其实24种基本的设计模式中并没有发布订阅模式,上面也说了,他只是观察者模式的一个别称。

但是经过时间的沉淀,似乎他已经强大了起来,已经独立于观察者模式,成为另外一种不同的设计模式。

在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为消息代理或调度中心或中间件,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。

举一个例子,你在微博上关注了A,同时其他很多人也关注了A,那么当A发布动态的时候,微博就会为你们推送这条动态。A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接的消息往来的,全是通过微博来协调的(你的关注,A的发布动态)。

2257600-20210808182444250-1714715156.png d1c575c13675401aa267a7c2e7de369d_tplv-k3u1fbpfcp-watermark.png

观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发(Fire Event)观察者里的事件。

发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

观察者模式和发布订阅模式最大的区别就是发布订阅模式有个事件调度中心,发布订阅这模式是对观察者模式的解耦。

观察者模式由具体目标调度,每个被订阅的目标里面都需要有对观察者的处理,这种处理方式比较直接粗暴,但是会造成代码的冗余。 而发布订阅模式中统一由调度中心进行处理,订阅者和发布者互不干扰,消除了发布者和订阅者之间的依赖。这样一方面实现了解耦, 还有就是可以实现更细粒度的一些控制。比如发布者发布了很多消息,但是不想所有的订阅者都接收到,就可以在调度中心做一些处理, 类似于权限控制之类的。还可以做一些节流操作。

手写发布订阅模式

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      // 发布订阅模式
      class EventEmitter {
        constructor() {
          // 事件对象,存放订阅的名字和事件
          this.events = {
            // money: [A, B],
            // work: [C],
            // home: [],
          };
        }
        // 订阅事件的方法
        $on(eventName, callback) {
          if (!this.events[eventName]) {
            // 如果events事件对象中不存在该事件名,则创建一个数组,并将回调函数放入其中
            this.events[eventName] = [callback];
          } else {
            // 存在则将回调函数push到指定数组的尾部保存
            this.events[eventName].push(callback);
          }
        }
        // 触发事件的方法
        $emit(eventName, ...args) {
          // 遍历执行所有订阅的事件
          this.events[eventName] &&
            this.events[eventName].forEach((cb) => cb(...args));
        }
        // 移除订阅事件
        $off(eventName, callback) {
          // 如果没有第二个参数,则整个事件删掉
          if (!callback) {
            delete this.events[eventName];
          }
          if (this.events[eventName]) {
            this.events[eventName] = this.events[eventName].filter(
              (cb) => cb != callback
            );
          }
        }
        // 只执行一次订阅的事件,然后移除
        $once(eventName, callback) {
          // 绑定的时fn, 执行的时候会触发fn函数
          let fn = (...args) => {
            callback(...args); // fn函数中调用原有的callback
            this.$off(eventName);
          };
          this.$on(eventName, fn);
        }
      }

      let em = new EventEmitter();
      let workday = 0;
      function A(...args) {
        console.log("A", args);
      }
      function B(...args) {
        console.log("B", args);
      }
      function C(...args) {
        console.log("C", args);
      }
      //   一个事件名可以订阅多个事件函数
      em.$on("money", A);
      em.$on("money", B);

      //   em.$on("work", C);
      em.$on("home", () => {
        workday++;
        console.log("work", workday);
      });

      em.$once("love", C);
      //   触发事件--如果该事件中有多个回调,依次执行
      em.$emit("money", "zgc", "wf"); //A B ['zgc', 'wf']
      em.$emit("home"); //work 1
      em.$emit("home"); //work 2
      em.$emit("love", "once");
      em.$emit("love", "once"); //第二次调用失效

      // 对整个"money"消息队列取消订阅
      //   em.$off("money");
      //   对"money"消息队列中的A回调取消订阅
      //   em.$off("money", A);

      console.log(em);
    </script>
  </body>
</html>

5. 观察者模式是不是发布订阅模式

《JavaScript设计模式与开发实践》一书中说了分辨模式的关键是意图而不是结构。如果以结构来分辨模式,发布订阅模式相比观察者模式多了一个中间件订阅器,所以发布订阅模式是不同于观察者模式的;如果以意图来分辨模式,他们都是实现了对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并自动更新,那么他们就是同一种模式,发布订阅模式是在观察者模式的基础上做的优化升级。

6. 在Vue中观察者模式API

  • 观察者(订阅者) – Watcher

    update():当事件发生时,具体要做的事情

  • 目标(发布者) – Dep

    1. subs 数组:存储所有的观察者
    2. addSub():添加观察者
    3. notify():当事件发生,调用所有观察者的 update() 方法
  • Observer: 它的作用是给对象的属性添加gettersetter,用于依赖收集和派发更新

  • Dep: 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个Dep实例(里面subsWatcher实例数组),当数据有变更时,会通过dep.notify()通知各个watcher

  • Watcher: 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系

为什么要单独拎出来一小节专门来说这个问题呢?因为大部分同学只是知道:Vue的响应式原理是通过Object.defineProperty实现的。被Object.defineProperty绑定过的对象,会变成「响应式」化。也就是改变这个对象的时候会触发getset事件。

但是对于里面具体的对象依赖关系并不是很清楚,这样也就给了面试官一种:你只是背了答案,对于响应式的内部实现细节,你并不是很清楚的印象。

关于Watcher 和 Dep 的关系这个问题,其实刚开始我也不是很清楚,在查阅了相关资料后,才逐渐对里面的具体实现有了清晰的理解。

591071973-4e5bd07ef3d85f9b_fix732.png

刚接触Dep这个词的同学都会比较懵: Dep究竟是用来做什么的呢?我们通过defineReactive方法将data中的数据进行响应式后,虽然可以监听到数据的变化了,那我们怎么处理通知视图就更新呢?

Dep就是帮我们依赖管理的。

如上图所示:一个属性可能有多个依赖,每个响应式数据都有一个Dep来管理它的依赖。 收集依赖

依赖

20210426213254533.png 之所以要劫持数据,目的是当数据的属性发生变化时,可以通知那些曾经用到的该数据的地方。

先收集依赖,把用到数据的地方收集起来,等属性改变,在之前收集好的依赖中循环触发一遍就好了~

  • 对象是在getter中收集依赖,在setter中触发依赖
  • 而数组是在getter中收集依赖,在拦截器(arrayMethods)中触发依赖

总结原理

上面说了那么多,下面我总结一下Vue响应式的核心设计思路:

当创建Vue实例时,vue会遍历data选项的属性,利用Object.defineProperty为属性添加gettersetter对数据的读取进行劫持(getter用来依赖收集,setter用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的watcher实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有computed watcher,user watcher实例),之后依赖项被改动时,setter方法会通知依赖与此datawatcher实例重新计算(派发更新),从而使它关联的组件重新渲染。

7. 结合发布订阅模式实现响应式

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script>
      //Dep 被观察者类--订阅发布器,提供用来收集观察者方法和通知观察者方法;
      //   注意:每一个数据都有自己的订阅发布器,它被定义在defineReactive方法里
      class Dep {
        constructor() {
          //   保存订阅者的列表,订阅中心存储观察者
          this.subs = [];
        }
        depend() {
          // 添加订阅者的方法--添加依赖
          this.subs.push(Dep.target);
        }
        notify() {
          // 通知订阅者们让watcher更新视图
          this.subs.forEach((watcher) => {
            watcher.update();
          });
        }
      }
      //Watcher 观察者类,用来生成观察者;
      class Watcher {
        //  data,key 监视哪个数据对象,这个对象中被监视的属性
        constructor(data, key) {
          this.value = data;
          this.key = key;
          Dep.target = this;
          this.get();
          Dep.target = null;
        }
        get() {
          return this.value[this.key];
        }
        // 当收到数据变化的消息时执行该方法,更新页面模板
        update() {
          console.log(
            "监听" + this.key + "的watcher被触发了!  新的值=" + this.get()
          );
        }
      }

      // 具体实现对象属性响应式的代码
      function defineReactive(data, key, value) {
        // 递归重要步骤: 因为对象里可能有对象或者数组,所以需要递归
        let childOb = obesrve(value);
        // 因为data中每一个属性对应的数据都有一套自己的发布订阅模型,所以直接在这里给每一个数据创建一个实例
        let dep = new Dep();
        // 核心:拦截对象里每个key的get和set属性,进行读写监听,从而实现了读写都能捕捉到,响应式的底层原理
        Object.defineProperty(data, key, {
          get() {
            console.log("正在访问", key);
            // 有一个观察者在访问这个数据,所以应该将这个观察者存入订阅发布中心
            // 收集依赖
            if (Dep.target) {
              dep.depend();
              // 判断有没有子元素
              if (childOb) {
                // 数组收集依赖
                childOb.dep.depend();
              }
            }

            return value;
          },
          set(newValue) {
            console.log("正在修改", key);
            value = newValue;
            childOb = obesrve(newValue);
            // 当这个数据改变时调用dep.notify通知依赖这个数据的watcher
            dep.notify();
          },
        });
      }

      //将一个正常的object转换为每个层级的属性都是响应式(可以被侦测)的object
      class Observer {
        constructor(value) {
          // 给传进来的value对象或者数组设置一个__ob__对象
          // 这个__ob__对象大有用处,如果value上有这个__ob__,则说明value已经做了响应式处理
          def(value, "__ob__", this, false);
          this.dep = new Dep();
          // 判断value是函数还是对象
          if (Array.isArray(value)) {
            // 是数组,就将这个数组的原型指向arrayMethods
            Object.setPrototypeOf(value, arrayMethods);
            //   处理数组内部的嵌套
            this.observeArray(value);
          } else {
            // 如果是对象,则执行walk函数对对象进行响应式处理
            this.walk(value);
          }
        }
        walk(data) {
          // 对对象进行响应式处理
          Object.keys(data).forEach((key) => {
            // console.log(data, key, data[key]);
            defineReactive(data, key, data[key]);
          });
        }
        observeArray(items) {
          // 遍历传进来的数组,如果数组里面还包含着数组或对象,则递归处理
          for (let i = 0; i < items.length; i++) {
            obesrve(items[i]);
          }
        }
      }

      function obesrve(value) {
        //   如果value不是对象或者数组则直接返回,数据入口
        if (typeof value !== "object") return;
        let ob;
        if (typeof value.__ob__ !== "undefined") {
          ob = value.__ob__;
        } else {
          ob = new Observer(value);
        }
        return ob;
      }

      function def(obj, key, value, enumerable) {
        Object.defineProperty(obj, key, {
          value,
          enumerable,
          writable: true,
          configurable: true,
        });
      }
      // 改写方法数组
      const arrayPrototype = Array.prototype;

      // 以Array.prototype为原型创建arrayMethod
      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数据已经被劫持");

            // 恢复原来的功能(数组方法)
            // 用apply将original中的this指向从window改为调用数组方法的数组
            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,有的话就劫持
            if (inserted) {
              ob.observeArray(inserted);
            }
            // console.log(1, this);

            // 发布订阅模式,通知dep
            // 向依赖发送消息
            ob.dep.notify();

            return result;
          },
          false
        );
      });
      function Vue(options) {
        //  将options里的内容添加到当前对象实例上
        // console.log(Object.keys(options)); //["data"]
        Object.keys(options).forEach((key) => {
          //   console.log(typeof key);//string
          this[key] = options[key];
        });
        // 处理data数据,将他变成响应式的,可监测的
        obesrve(this.data);

        new Watcher(this, "data");
        new Watcher(this.data, "count");
        new Watcher(this.data, "user");
        new Watcher(this.data.user, "name");
        new Watcher(this.data.user, "desc");
        new Watcher(this.data, "list");
      }

      // 测试代码
      let vm = new Vue({
        data: {
          count: 123,
          user: {
            name: "zgc",
            desc: "前端开发",
          },
          list: ["YYDS", "双厨狂喜", "AWSL"],
        },
        test() {
          this.data.count = 12;
          this.data.user.name = "wf";
          this.data.list.push(1);
          console.log(this.data);
        },
      });
      vm.test();
    </script>
  </body>
</html>

参考文章:

juejin.cn/post/696166…