深入浅出 Vue :变化侦测

2023-05-17 14:45:55 浏览数 (1)

# Object 的变化侦测

# 变化侦测及实现

Vue.js 自动通过状态生成 DOM,并将其显示到页面,这个过程叫渲染。

在运行时应用内部的状态会不断发生变化,需要不停地渲染。确定状态发生了什么变化通过“变化侦测”实现,一般分“推”和“拉”两种。

Vue.js 的变化侦测属于“推”(push),相对于“拉”,“推”知道的信息更多,可以更细粒度的更新。当然,粒度越细,意味每个状态绑定的依赖就越多,依赖追踪在内存上的开销就会越大。所以,在后来的 Vue.js 2.0 开始,引入了虚拟 DOM,将粒度调整为中等粒度,即一个状态所绑定的依赖不再是具体 DOM,而是一个组件(状态变化后会通知组件,组件内部再使用虚拟 DOM 进行比对)。

JavaScript 中有两种方法可以侦测到变化:Object.definePropertyProxy。在 Vue.js 3.0 之前都是使用第一种方法。

代码语言:javascript复制
/**
 * 对 Object.defineProperty 的封装
 * @param {*} data 
 * @param {*} key 
 * @param {*} val 
 */
function defineReactive (data, key, val) {
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log('get value')
      return val
    },
    set: function (newVal) {
      console.log('change value')
      if (newVal === val) {
        return
      }
      val = newVal
    }
  })
}

const a = { name: 'default' }

defineReactive(a, 'name', a.name)

console.log(a.name) // get value
// default

a.name = 'new name' // change value
console.log(a.name) // get value 
// new name

a.name = 'other name' // change value
console.log(a.name) // get value
// other nam

# 收集依赖

侦测数据变化的目的是在数据变化时,及时地通知使用到数据的地方,进行对应的操作。

Vue.js 的实现方法是,将用到数据的地方都收集起来,等到数据变化时,对所有依赖触发一次通知。简单说,即在 getter 中收集依赖,在 setter 中触发依赖。

代码语言:javascript复制
function defineReactive (data, key, val) {
  const dep = []
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      console.log('get value')
      dep.push(window.target) // 收集依赖
      return val
    },
    set: function (newVal) {
      console.log('set value')
      if (newVal === val) {
        return
      }
      // 通知依赖
      for (let i = 0; i < dep.length; i  ) {
        dep[i](newVal, val)
      }
      val = newVal
    }
  })
}

window.target = (newVal, oldVal) => {
  console.log('change value', oldVal, '->', newVal)
}

const a = { name: 'default' }

defineReactive(a, 'name', a.name)

console.log(a.name) // 触发 getter 进行依赖收集
// get value
// default

a.name = 'new name' // 触发 setter 进行依赖通知
// set value
// change value default -> new name

console.log(a.name)
// get value
// new name

可以将依赖收集的逻辑封装成 Dep 类,专门用于管理依赖。

代码语言:javascript复制
class Dep {
  constructor () {
    this.subs = []
  }

  addSub (sub) {
    this.subs.push(sub)
  }

  removeSub (sub) {
    remove(this.subs, sub)
  }

  depend () {
    if (dataConsumer) {
      this.addSub(dataConsumer)
    }
  }

  notify () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i  ) {
      subs[i].update()
    }
  }
}

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

function defineReactive (data, key, val) {
  const dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend()
      return val
    },
    set: function (newVal) {
      if (newVal === val) {
        return
      }
      val = newVal
      dep.notify()
    }
  })
}

const dataConsumer = {
  data: null,
  update: function () {
    console.log('update')
  }
}

const a = { name: 'default' }

defineReactive(a, 'name', a.name)

dataConsumer.data = a.name

a.name = 'new name' // update

# 依赖是谁

收集依赖,是在收集什么?收集的是用到数据的地方,用于在数据发生变化时及时通知使用方。用到数据的地方可能是模板,也可能是用户写的一个 watch.

为了方便集中处理这些情况,抽象出一个类,在收集阶段只收集封装好的这个类的实例进来,通知时也直接通知它,然后由它去通知其他地方。

# Watcher

Watcher 作为一个中介角色,数据发生变化时通知它,它再通知其他地方。

代码语言:javascript复制
class Watcher {
  constructor (vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn) // 执行 this.getter 可以读取到 data 中的值
    this.cb = cb
    this.value = this.get()
  }

  get () {
    dataConsumer = this
    let value = this.getter.call(this.vm, this.vm) // 触发依赖收集
    dataConsumer = undefined
    return value
  }

  update () {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

const bailRE = /[^w.$]/
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]]
    }
    return obj
  }
}

# 递归侦测所有的 key

之前的实现中只侦测数据中的一个属性,需要将所有属性都侦测,可以封装一个 Observer 类。作用是将一个数据内的所有属性转换成 getter/setter 的形式,然后去追踪变化。

# Array 的变化侦测

# 变化侦测相关 API 的实现

0 人点赞