Vue2响应式原理四:computed

文章详细介绍了Vue框架中计算属性的实现机制,包括依赖收集、缓存、惰性求值等关键特性。通过Watcher和Dep类的交互,解释了如何跟踪依赖变化以及何时重新计算。此外,还讨论了一个可能导致bug的情况及其解决方案,以及如何让侦听器监听计算属性和手动触发计算属性的重新求值。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

计算属性

计算属性会根据依赖项进行计算,返回一个新的值,有两个特点:

  1. 缓存:依赖性不变,不会进行计算,直接返回结果
  2. 惰性求值:依赖项改变时,如果不进行读取,也不会进行计算

实现

实现思路:

  1. 依赖收集:由于计算属性是根据多个依赖项得出,任何一个依赖项改变都要进行重新求值,所以每一个依赖项都需要收集它的 watcher。具体实现可以在实例化 watcher时调用计算属性的函数,函数内会读取所有的依赖项。
  2. 缓存:调用函数的时候保存结果,下次读取如果没有改变依赖项直接返回结果。
  3. 惰性求值:依赖项改变时,不直接调用函数,而是修改 watcher的属性,表示需要重新计算。

Watcher

新增 options参数默认为空对象,新增 lazy、dirty、value属性。如果 lazy为true,表示惰性求值,读取的时候再求值。如果 dirty为 true,表示依赖项改变,需要重新计算。value用来保存计算的结果。

class Watcher {
  constructor(vm, key, cb, options = {}) {
	// 略 ...
    this.lazy = !!options.lazy
    this.dirty = this.lazy
    this.value = undefined

    // lazy为true,读取时再进行求值
    if (!this.lazy) {
      this.get()
    }
  }

  get() {
    Dep.target = this
    if (typeof this.key === 'function') {
      // 如果是函数,说明是计算属性,对函数进行调用,函数内部再对依赖项进行读取
      this.value = this.key.call(this.vm)
    } else {
      this.value = this.vm[this.key]
    }
    Dep.target = null
  }

  update() {
    if (this.lazy) {
      // 依赖性更新时,将 dirty设置为 true,下次读取时重新求值
      this.dirty = true
    } else {
      this.run()
    }
  }

// ...
}

class Dep {
  // ...
  
  notify() {
    for (const watcher of this.subs) {
      // 派发任务时,调用 update函数
      watcher.update()
    }
  }
}

initComputed

初始化计算顺序,并将其挂载到 Vue实例上。

class Vue {
  constructor(options) {
    const data = options.data
    this.$options = options
    this._data = typeof data === 'function' ? data() : data

    this.initData()
    this.initComputed() // 在 initWatch之前调用,因为 watch可以监听 computed
    this.initWatch()
  }

  // 略 ...

  initComputed() {
    const computed = this.$options.computed
    if (!computed) return

    const keys = Object.keys(computed)
    for (const key of keys) {
      // 为计算属性创建 watcher
      const watcher = new Watcher(this, computed[key], () => {}, { lazy: true })
      // 将计算属性挂载到 Vue实例上
      Object.defineProperty(this, key, {
        // dirty为 true,则重新计算结果,否则直接返回缓存的值
        get() {
          if (watcher.dirty) {
            watcher.get()
            watcher.dirty = false
          }

          return watcher.value
        }
      })
    }
  }
}

监听计算属性

此时,计算属性的功能基本完成,但还有一个问题就是侦听器还不能监听计算属性。计算属性并不像普通属性有自己的 Dep实例来收集 watcher,它依赖其他属性计算得出,任何一个依赖项改变都需要触发它的回调。
实现思路:记录计算属性的watcher被那些属性收集,再让这些属性对 watch也收集一次。

Watcher、Dep相互收集

class Watcher {
  deps = [] // 记录收集了该实例的 dep

  // ...
  
  // 记录 dep,并调用 addSub收集该 Watcher实例
  addDep(dep) {
    if (this.deps.includes(dep)) return

    this.deps.push(dep)
    dep.addSub(this)
  }
}

class Dep {
  subs = []

  // 收集 watcher时先通知 watcher记录被哪个 dep收集
  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 收集watcher
  addSub(watcher) {
    this.subs.push(watcher)
  }
  
  // ...
}

一个bug

class Watcher {
  // 略 ...
  get() {
    Dep.target = this
    if (typeof this.key === 'function') {
      this.value = this.key.call(this.vm)
    } else {
      this.value = this.vm[this.key]
    }
    Dep.target = null
  }
}

此处代码存在一个bug,当侦听器监听计算属性时,此时 Dep.target指向侦听器的 watcher,当读取属性时,会触发侦听器的 getter函数,watcher.dirty为 true,又会调用 watcher.get函数,Dep.target被重新赋值为计算属性的 watcher,导致侦听器的 watcher丢失。
解决办法:定义一个数组来收集 watcher,收集完成后删除 watcher,如果数组中含有数据,则将其取出,赋值给 Dep.target。

const targetStack = [] 

class Watcher {
  // 略 ...
  get() {
    // 收集 watcher
    targetStack.push(this)
    Dep.target = this
    if (typeof this.key === 'function') {
      this.value = this.key.call(this.vm)
    } else {
      this.value = this.vm[this.key]
    }
    // 收集完后删除最后添加的 watcher
    targetStack.pop()
    // 如果数组中还有 watcher,则取出最后一个赋值给 Dep.target
    Dep.target = targetStack.length ? targetStack.at(-1) : null
  }
}

代码修改后,侦听器的watcher会在计算属性的watcher.get完成后到侦听器的watcher.get完成前的期间内挂载到 Dep.target上,可以在计算属性的getter函数中对其进行收集。

initComputed() {
  const computed = this.$options.computed
  if (!computed) return
  
  const keys = Object.keys(computed)
  for (const key of keys) {
    const watcher = new Watcher(this, computed[key], () => {}, { lazy: true })
    Object.defineProperty(this, key, {
      get() {
        if (watcher.dirty) {
          watcher.get()
          watcher.dirty = false
        }
        // 如果 Dep.target存在,则让计算属性的依赖项的dep对侦听器的watcher进行收集
        if (Dep.target) {
          watcher.deps.forEach((dep) => dep.depend(Dep.target))
        }
        
        return watcher.value
      }
    })
  }

手动读取计算属性

此时侦听器已经可以监听计算属性,但触发侦听器回调时,计算属性需要重新求值,可以在侦听器运行时进行读取,让计算属性重新计算。

class Watcher {
  // 略 ...

  run() {
    if (watcherQueue.includes(this.id)) return
    watcherQueue.push(this.id)

    Promise.resolve().then(() => {
      this.get() // 调用 get函数读取计算属性,让其重新求值
      this.cb.call(this.vm)
      watcherQueue.splice(watcherQueue.indexOf(this.id), 1)
    })
  }
}

完整代码

class Vue {
  constructor(options) {
    const data = options.data
    this.$options = options
    this._data = typeof data === 'function' ? data() : data

    this.initData()
    this.initComputed()
    this.initWatch()
  }

  initData() {
    const data = this._data
    observer(data)

    const keys = Object.keys(data)
    for (const key of keys) {
      Object.defineProperty(this, key, {
        get() {
          return data[key]
        },
        set(value) {
          data[key] = value
        }
      })
    }
  }

  initComputed() {
    const computed = this.$options.computed
    if (!computed) return

    const keys = Object.keys(computed)
    for (const key of keys) {
      const watcher = new Watcher(this, computed[key], () => {}, { lazy: true })

      Object.defineProperty(this, key, {
        get() {
          if (watcher.dirty) {
            watcher.get()
            watcher.dirty = false
          }

          if (Dep.target) {
            watcher.deps.forEach((dep) => dep.depend(Dep.target))
          }

          return watcher.value
        }
      })
    }
  }

  initWatch() {
    const watch = this.$options.watch
    if (!watch) return

    const keys = Object.keys(watch)
    for (const key of keys) {
      this.$watch(key, watch[key])
    }
  }

  $watch(key, cb) {
    new Watcher(this, key, cb)
  }

  $set(target, key, value) {
    if (Array.isArray(target)) {
      target[key] = value
      observer(value)
    } else {
      defindReactive(target, key, value)
    }
    target.__ob__.dep.notify()
  }
}

function observer(data) {
  const dataType = Object.prototype.toString.call(data)

  if (dataType !== '[object Object]' && dataType !== '[object Array]') {
    return
  }

  if (data.__ob__) return data.__ob__

  return new Observer(data)
}

function defindReactive(target, key, value) {
  const ob = observer(value)
  const dep = new Dep()

  Object.defineProperty(target, key, {
    get() {
      dep.depend()
      ob?.dep.depend()
      return value
    },
    set(val) {
      if (val === value) return
      dep.notify()
      value = val
    }
  })
}

class Observer {
  dep = new Dep()

  constructor(data) {
    if (Array.isArray(data)) {
      data.__proto__ = arrayMethods
      this.observeArray(data)
    } else {
      this.walk(data)
    }

    Object.defineProperty(data, '__ob__', {
      value: this,
      enumerable: false
    })
  }

  walk(data) {
    const kyes = Object.keys(data)

    for (const key of kyes) {
      defindReactive(data, key, data[key])
    }
  }

  observeArray(arr) {
    arr.forEach(observer)
  }
}

class Dep {
  subs = []

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    for (const watcher of this.subs) {
      watcher.update()
    }
  }

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

let watcherId = 0
const watcherQueue = []
const targetStack = []

class Watcher {
  deps = []

  constructor(vm, key, cb, options = {}) {
    this.vm = vm
    this.key = key
    this.cb = cb
    this.id = watcherId++
    this.lazy = !!options.lazy
    this.dirty = this.lazy
    this.value = undefined

    if (!this.lazy) {
      this.get()
    }
  }

  get() {
    targetStack.push(this)
    Dep.target = this
    if (typeof this.key === 'function') {
      this.value = this.key.call(this.vm)
    } else {
      this.value = this.vm[this.key]
    }
    targetStack.pop()
    Dep.target = targetStack.length ? targetStack.at(-1) : null
  }

  update() {
    if (this.lazy) {
      this.dirty = true
    } else {
      this.run()
    }
  }

  run() {
    if (watcherQueue.includes(this.id)) return
    watcherQueue.push(this.id)

    Promise.resolve().then(() => {
      this.get()
      this.cb.call(this.vm)
      watcherQueue.splice(watcherQueue.indexOf(this.id), 1)
    })
  }

  addDep(dep) {
    if (this.deps.includes(dep)) return

    this.deps.push(dep)
    dep.addSub(this)
  }
}

const methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sotr', 'splice']
const arrayMethods = methods.reduce((obj, method) => {
  obj[method] = function (...args) {
    const res = Array.prototype[method].apply(this, args)

    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
      default:
        break
    }
    inserted?.forEach(observer)
    this.__ob__.dep.notify()

    return res
  }

  return obj
}, Object.create(Array.prototype))
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值