在vue源码学习14:watch的实现原理中学习到:watch的实现是一个watcher,源码中通过标注一个用户自定义的tag,来执行watch后面定义的函数。
今天要学习的computed也是基于watcher。它与watch不同的是如下几点:
- computed计算出来的属性本身没有Deps,不维护watcher
- computed依赖的属性,有两个watcher
- computed的watcher
- 渲染watcher
从用法说起
Vue中computed典型的用法有如下两种:
// 方式1:
computed: {
fullName() {
return this.firstName + this.lastName
}
}
// 方式2:
computed: {
fullName: {
get() {
console.log('ooo')
return this.firstName + this.lastName
},
set() {
console.log("set full name")
}
}
}
如果你对computed的用法比较熟悉的话,应该知道computed有下面这几个特性:
- 特性1:computed默认不执行
- 特性2:取值的时候,computed里面的方法会被执行
- 特性3:如果computed依赖的值
没有发生变化
,多次取值,计算函数只执行一次 - 特性4:依赖的值发生变化,函数会被执行(一个自定义的getter方法解决这个问题)
默认不执行:依靠watcher中的lazy
属性判断,如果lazy是true,则不执行函数
无变化不执行:依靠watcher中的dirty
属性判断,如果dirty是true,则重新计算,否则不计算
computed实现
Vue在初始化的时候,如果发现传入的属性是一个computed
,则会对其进行初始化处理:
export function initState(vm) {
const opts = vm.$options
if (opts.computed) {
// 对数据进行处理
initComputed(vm, opts.computed)
}
}
initComputed函数
-
computed是一个对象,首先要对其进行遍历。基于其用法的两种不同形式(函数和对象),会对其进行判断。分别生成不同的watcher对象。
-
在这里需要将每个computed的属性生成的watcher维护一个watchers,并且放在vm实例上。这样做的用处是,在创建computed getter(参照前文特性4)的时候,可以顺利的获取到它的dirty属性。
-
然后将计算后的属性,定义到vm上
代码如下:
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = {}
for (let key in computed) {
/**
* userDef 可能是函数
* 可能是对象
* 依赖的属性变化,就重新取值
**/
const userDef = computed[key]
let getter = typeof userDef === 'function' ? userDef : userDef.get
console.log('getter', getter)
// 这里有多少个getter就要有多少个watcher,每一个计算属性的本质就是一个watcher
// 将wathcer和属性做一个映射
watchers[key] = new Watcher(vm, getter, () => { }, { lazy: true }) // 默认不执行
// 将key定义在vm上
defineComputed(vm, key, userDef)
}
}
defineComputed方法
在这个方法中,维护了一个sharedProperty
对象,用来存储defineProperty的第三个参数。这个对象中的get
方法是一个自定义的get方法。即createComputedGetter(key)
之所以要用自定义的,是因为computed取值的时候,是有缓存的,如果没有变化,则不计算
从这里可以看出,computed依赖值发生变化的时候,是走createComputedGetter的方法的。
function defineComputed(vm, key, userDef) {
let sharedProperty = {}
if (typeof userDef == 'function') {
sharedProperty.get = userDef
} else {
/**
* 每一次取值都会走get,
* 如果发现是脏的,就重新获取
* 如果不是脏的,就不走get
* */
// sharedProperty.get = userDef.get
sharedProperty.get = createComputedGetter(key)
sharedProperty.set = userDef.set
}
Object.defineProperty(vm, key, sharedProperty)
}
函数式编程:createComputedGetter
一旦computed
依赖的值发生变化,就会立刻进入这个方法。
在这个方法中,根据每一个watcher实例的dirty
属性来判断是否执行计算方法。并返回计算过后的值,或者是没有发生变化的值。
需要注意的是,在computed的依赖属性的Dep上,收集了两个watcher:
- computed的watcher
- 渲染watcher
这两个watcher都会被执行。
createComputedGetter完整代码:
if (watcher.dirty) {
// 根据dirty属性来判断是否需要求值
watcher.evaluate()
}
function createComputedGetter(key) {
return function computedGetter() {
// 取计算属性的值,走的是这个函数
// 谁取值,this就是谁,所以要获取到watcher,就可以把watcher挂载到vm上
// 通过key可以拿到对应的watcher
// 这个watcher包含了getter
// 一旦属性发生变化,就执行watcher中的getter
let watcher = this._computedWatchers[key]
if (watcher.dirty) {
// 根据dirty属性来判断是否需要求值
watcher.evaluate()
}
/**
* 如果当前取完值后,Dep.target还有值,需要继续向上收集
*/
if (Dep.target) {
// 计算属性watcher内部有两个dep
console.log('dep.target', Dep.target)
watcher.depend() //watcher里面对应了多个dep
}
return watcher.value
}
}
watcher的变化
// 一个组件对应一个watcher
let id = 0;
import { popTarget, pushTarget } from './dep';
import { queueWatcher } from './scheduler';
class Watcher {
constructor(vm, exprOrFn, cb, options) {
this.vm = vm
this.exprOrFn = exprOrFn
this.user = !!options.user
++ this.lazy = !!options.lazy
// 如果是计算属性,则默认dirty是true | lazy也是true
++ this.dirty = options.lazy
this.cb = cb
this.options = options
this.id = id++
if (typeof exprOrFn == 'string') {
this.getter = function () {
let path = exprOrFn.split('.')
let obj = vm
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
return obj
}
} else {
this.getter = exprOrFn
}
this.deps = []
this.depsId = new Set()
// 默认初始化执行get
// 如果是lazy就什么都不做,这里实现了computed不取值不计算的原理
++ this.value = this.lazy ? undefined : this.get()
}
get() {
pushTarget(this)
/* 创建关联
* 每个属性都可以收集自己的watcher
* 希望一个属性可以对应多个watcher
* 一个watcher可以对应多个属性
*/
+修改 const value = this.getter.call(this.vm)
popTarget() // 这里去除Dep.target,是防止用户在js中取值产生依赖收集
return value
}
update() {
// 每次更新时,把watcher缓存下来
// 如果当前的watcher是lazy的,则说明是计算属性的watcher,每次更新的时候,把dirty变成true,下一次调用的时候就会执行计算方法
++ if (this.lazy) {
++ this.dirty = true
++ } else {
++ queueWatcher(this)
++ }
}
run() { // 后续要有其他的功能
// console.log('run')
let newValue = this.get()
let oldValue = this.value
this.value = newValue // 为了保证下一次更新的时候,这一个新值是下一个的老值
if (this.user) {
this.cb.call(this.vm, newValue, oldValue)
}
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.depsId.add(id)
this.deps.push(dep)
dep.addSub(this)
}
}
// 新增evaluate方法,返回计算的值,顺便把dirty置位false,说明已经取过值了,下次就不再重新计算了
++ evaluate() {
++ // 把dirty置位false,说明取过值了
++ this.dirty = false
++ // get就是watcher传进来的exprOrFn
++ this.value = this.get()
++ }
// 新增depend方法,循环执行deps中Dep对象的的depend方法,进行依赖收集
++ depend() {
++ let i = this.deps.length
++ console.log('i', i)
++ while (i--) {
++ this.deps[i].depend()
++ }
++ }
}
export default Watcher
Dep:在Dep中额外维护一个stack栈,解决computed的两个watcher的问题
class Dep {
//... 其他代码,参见以前的文章
}
Dep.target = null
// 用一个栈解决computer的watcher问题
++ let stack = []
export function pushTarget(watcher) {
Dep.target = watcher
++ stack.push(watcher)
}
export function popTarget() {
++ stack.pop()
++ Dep.target = stack[stack.length - 1]
}
总结
computed的实现原理是比较复杂的,需要结合之前学习的内容加以揣摩,才能弄得明白。
后续还需要多思考,多总结。继续加油。
同时也感谢你的阅读!