# 监听一个变量的变化,需要怎么做

(1)方法1:ES5 的 Object.defineProperty

基础版本实现:

// 监视对象
function observe(obj) {
  // 遍历对象,使用 get/set 重新定义对象的每个属性值
   Object.keys(obj).map(key => {
       defineReactive(obj, key, obj[key])
   })
}

function defineReactive(obj, k, v) {
   // 递归子属性
   if (typeof(v) === 'object') observe(v)
   
   // 重定义 get/set
   Object.defineProperty(obj, k, {
       enumerable: true,
       configurable: true,
       get: function reactiveGetter() {
           console.log('get: ' + v)
           return v
       },
       // 重新设置值时,触发收集器的通知机制
       set: function reactiveSetter(newV) {
           console.log('set: ' + newV)
           v = newV
       },
   })
}

let data = {a: 1}
// 监视对象
observe(data)
data.a // get: 1
data.a = 2 // set: 2

但是以上方法存在几个缺陷:

  • IE8 及更低版本 IE 是不支持的(如果不考虑IE8以下兼容可忽略)
  • 无法检测到对象属性的新增或删除
  • 如果修改数组的 length ( Object.defineProperty 不能监听数组的长度),以及数组的 push 等变异方法是无法触发 setter

而vue中使用了函数劫持的方式,重写了数组的方法,Vue 将 data 中的数组进行了原型链重写,指向了自己定义的数组原型方法。这样当调用数组 api 时,可以通知依赖更新。如果数组中包含着引用类型,会对数组中的引用类型再次递归遍历进行监控。这样就实现了监测数组变化。实现方法如下所示:

// 获得数组原型
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// 重写以下函数
const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // 缓存原生函数
  const original = arrayProto[method]
  // 重写函数
  def(arrayMethods, method, function mutator (...args) {
  // 先调用原生函数获得结果
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    // 调用以下几个函数时,监听新数据
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // 手动派发更新
    ob.dep.notify()
    return result
  })
})

(2)ES6 的 Proxy

使用proxy会带来几个问题:

  • Proxy只会代理对象的第一层,那么又是怎样处理这个问题的呢?——我们可以判断当前 Reflect.get 的返回值是否为 Object ,如果是则再通过 reactive 方法做代理, 这样就实现了深度观测。
  • 监测数组的时候可能触发多次get/set,那么如何防止触发多次呢(因为获取push和修改length的时候也会触发)——我们可以判断是否是 hasOwProperty
const toProxy = new WeakMap(); // 存放被代理过的对象
const toRaw = new WeakMap(); // 存放已经代理过的对象

function reactive(target) {
  return createReactiveObject(target); // // 创建响应式对象
}

function isObject(target) {
  return typeof target === "object" && target !== null;
}

function hasOwn(target, key) {
  return target.hasOwnProperty(key);
}

// 创建响应式对象
function createReactiveObject(target) {
  // 如果不是对象,则直接返回
  if (!isObject(target)) {
    return target;
  }

  // toProxy用来存放被代理过的对象
  let observed = toProxy.get(target);

  // 判断是否被代理过,如果被代理过则直接返回
  if (observed) {
    return observed;
  }

  // 判断是否要重复代理
  if (toRaw.has(target)) {
    return target;
  }

  const handlers = {
    get(target, key, receiver) {
      let res = Reflect.get(target, key, receiver);
      track(target, 'get', key); // 依赖收集==
      return isObject(res) ? reactive(res) : res; // 如果是对象,则继续代理
    },
    set(target, key, value, receiver) {
      let oldValue = target[key];
      let hadKey = hasOwn(target, key);
      let result = Reflect.set(target, key, value, receiver);
      if (!hadKey) {
        trigger(target, 'add', key); // 触发添加
      } else if (oldValue !== value) {
        trigger(target, 'set', key); // 触发修改
      }
      return result;
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key);
      return result;
    }
  };

  // 开始代理
  observed = new Proxy(target, handlers);
  toProxy.set(target, observed);
  toRaw.set(observed, target); // 做映射表
  return observed;
}