天天看点

Object 的变化侦测

是什么

是指在数据状态发生变化时,让对应的重新DOM渲染。这里我们首先了解对象(Object)变化时,怎么通知到外界的。

(1)简单来看,第一次渲染的时候数据传递就是这样一个过程

Object 的变化侦测

这里的外界,指的是要渲染的模板,或者要暴露出去的watch监听方法等。

(2)当数据发生变化时,要让外界知道数据的变化。所以数据和外界的关系应当如下。

Object 的变化侦测

(3)然后就是怎么通知的问题了,这就是我们这次要将的Object变化侦测,就是要监听数据的变化,并且通知到外界。这个监听器我们暂且定义为watcher。watcher监听着数据的变化,并负责通知外界。

Object 的变化侦测

(4)接下来,就是研究怎么去监听数据的变化了。

Vue2.0中使用的监听方法是,使用Object.defineProperty()给对象设置setter和getter方法,getter和setter方法监听数据的变化。

Object 的变化侦测

简单实现简单监听的Demo:

function defindReactive(data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log('get data')
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
    }
  });
}


let numData = {num: 1};
let key = 'num';

// 设置监听
defindReactive(numData, key, 2);

numData.num = 3
console.log(numData.num)

// 输出
set data
get data
3
           

Object.defineProperty(obj, prop, descriptor);

具体参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

可以看出在设置好监听后,我们再对对象设置或读取时,都可以触发自定义的操作。

(5)能监听到数据变化后,就要在数据变化时通知到使用该数据的地方,如某个对象的name字段值被修改了,就需要通知页面上展示字段name的地方修改值。这里展示name的地方也可以称为该数据的依赖。所以我们在监听到数据变化后,要做的主要工作就有:1. 收集依赖。 2. 通知依赖变化。

为了减少耦合,我们单独定义一个依赖收集器Dep。

Object 的变化侦测

我们首先定义一个依赖收集器:

// 依赖收集器
class Dep {
  // 创建对象时,初始化收集器的数组
  constructor() {
    this.subs = []
  }
  
  addSub(sub) {
    this.subs.push(sub)
  }
  
  removeSub(sub) {
    remove(this.subs, sub);
  }
  
  // 依赖
  depend() {
    if (window.target) {
      this.addSub(window.target)
    }
  }
  
  // 通知
  notify() {
    // 复制数组
    const subs = this.subs.slice()
    for (let i = 0; i < subs.length; i++) {
      subs[i].update()
    }
  }
}

function remove(arr, item) {
  if (arr.length) {
    const index = arr.indexOf(item)
    if(index > -1) {
      arr.splice(index, 1)
    }
  }
}
           

然后在数据getter时收集依赖,在setter时触发依赖:

// 数据监听和依赖收集器结合
function defindReactive(data, key, val) {
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend()
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
      // 通知依赖发生变化
      dep.notify();
    }
  });
}
           

(6)对于Object,我们希望把对象所有的属性以及后代属性都监听到,所以我们还需要定义一个类,把数据内所有的属性的都转换成getter和setter形式。我们把它定义为Observer。

Object 的变化侦测
class Observer {
  constructor(value) {
    this.value = value
    
    if (!Array.isArray(value)) {
      this.walk(value);
    }
  }
  
  walk(obj) {
    for (let key in obj) {
      defineReactive(obj, key, obj[key]);
      console.log('value', obj[key]);
      console.log('key', key);
    }
  }
}
           

简单测试一下:

let user = {
  name: 'hello',
  age: 223
}

let ob = new Observer(user)

// 打印输出
value hello
key name
value 223
key age
           

再改造一下defineReactive:

// 数据监听和依赖收集器结合
function defindReactive(data, key, val) {
  // 新增点
  if (typeof val === 'object) {
      new Observer(val)
  }
  let dep = new Dep();
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      // 收集依赖
      dep.depend()
      return val;
    },
    set: function (newVal) {
      console.log('set data')
      if (val === newVal) {
        return
      }
      val = newVal
      // 通知依赖发生变化
      dep.notify();
    }
  });
}
           

(7)最后实现Watcher的工作:

const bailRE = /[^\w.$]/

// 匹配path是否满足 a.b.c这样的格式。返回一个函数
export default function parsePath(path) {
  if (bailRE.test(path)) {
    return
  }
  const segments = path.split('.')
  
  return function(obj) {
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
  }
}
           
class watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }
  
  get() {
    window.target = this
    let value = this.getter.call(this.vm, this.vm);
    window.target = undefined
    return value
  }
  
  set() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}
           

代码中的window.target用来表示当前依赖。

Object侦测的简单逻辑图

Object 的变化侦测