vue源码学习15:computed的实现原理

880 阅读5分钟

前言前言

vue源码学习14:watch的实现原理中学习到:watch的实现是一个watcher,源码中通过标注一个用户自定义的tag,来执行watch后面定义的函数。

今天要学习的computed也是基于watcher。它与watch不同的是如下几点:

  • computed计算出来的属性本身没有Deps,不维护watcher
  • computed依赖的属性,有两个watcher
    1. computed的watcher
    2. 渲染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函数

  1. computed是一个对象,首先要对其进行遍历。基于其用法的两种不同形式(函数和对象),会对其进行判断。分别生成不同的watcher对象。

  2. 在这里需要将每个computed的属性生成的watcher维护一个watchers,并且放在vm实例上。这样做的用处是,在创建computed getter(参照前文特性4)的时候,可以顺利的获取到它的dirty属性。

  3. 然后将计算后的属性,定义到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的实现原理是比较复杂的,需要结合之前学习的内容加以揣摩,才能弄得明白。

后续还需要多思考,多总结。继续加油。

同时也感谢你的阅读!