vue2核心原理(简易)-computed笔记

1,356 阅读3分钟

前言

  • 本章项目地址
  • 计算属性默认不执行Object.defineProperty => getter
  • 多次取值 如果依赖值不发生变化,就不会重新执行,主要是计算watcherdirty
  • dirty = true就是脏的, 取值时需要重新执行, 如果dirty = false就直接去取老值
  • dirty这样也可起到缓存作用, 多次调用只执行一次
  • 以下面的示例为例
  • 当在页面上直接computedName,但computedName不会去依赖渲染watcher, 因为computed没有Dep, 无法收集
  • 当调用computedName时,computedName里的firstName和lastName会收集当前的计算watcher
  • 如何让其依赖值(firstName,lastName)也收集渲染watcher
  • 在Dep的文件加了个 let stacks = [], 首先页面初次new 渲染watcher 此时的Dep.target 指向渲染watcher将其push到 stracks
  • 当执行到开始调用computedName时 此时的Dep.target 指向计算watcher也将其push到 stracks
  • 调用computedName结束时, 将其popTarget移除, 此时的依赖值(firstName,lastName)只收集了计算watcher, 但在stacks还有值, 通过watcher中的depend方法,将各个依赖值(firstName,lastName)在收集渲染watcher
  • 思维导图 以下面示例为模板 图片替换文本

示例

<div id="app">{{ computedName }}</div>

var vm = new Vue({
    data: {
        firstName: 'one',
        lastName: 'two'
    },
    /** computed 几种方式 */
    computed: {
        // computedName() {
        //     return this.firstName + '--' + this.lastName
        // },
        computedName: {
            get() {
                return this.firstName + '--' + this.lastName
            },
            set(newValue) {
                console.log(newValue);
            }
        }
    }
})

vm.$mount('#app')

setTimeout(() => {
    vm.firstName = 'testComputed'
}, 2000)

初始化 和 computed方法(重点)

export function initState(vm) {
    const opts = vm.$options
    if (opts.computed) {
        initComputed(vm, opts.computed)
    }
}

/** initComputed module */
/**
 * @description 通过computed watcher 里的dirty 判断是不是脏(true) 来取值 从而达到缓存(多次取值只执行一次 dirty = false) 如果依赖值发生变化 dirty = true
 * @description 并将所有依赖的watcher 通过watcher中的deps push去 
 */
function createComputedGetter(key) {
    return function computedGetter() {
        // 取值时 this -> vm
        let watcher = this._computedWactchers[key]

        // 看这里 根据 dirty 判断是否重新求值 实现缓存
        if (watcher.dirty) {
            watcher.evaluate()
        }
		
        // 看这里 Dep.target有值 再次收集
        if (Dep.target) {
            watcher.depend()
        }

        return watcher.value
    }
}

/**
 * @description 将computed上的属性 定义在vm Object.defineProperty -> getter
 * @description computed属性 没有Dep
 */
function defineComputed(vm, key, userDef) {
    let sharedProperty = {}
    if (typeof userDef == 'function') {
        sharedProperty.get = createComputedGetter(key)
    } else {
        sharedProperty.get = createComputedGetter(key)
        sharedProperty.set = userDef.set
    }
    Object.defineProperty(vm, key, sharedProperty)
}

/**
 * @description 初始化computed 并创建watcher
 */
function initComputed(vm, computed) {
    const watchers = vm._computedWactchers = {}
    for (const key in computed) {
        const userDef = computed[key]
        let getter = typeof userDef == 'function' ? userDef : userDef.get

        watchers[key] = new Watcher(vm, getter, ()=>{}, { lazy: true })

        // 将key 定义在vm上
        defineComputed(vm, key, userDef)
    }

}

watcher(重点)

import { pushTarget, popTarget } from './dep'
import { queueWatcher } from './scheduler'

/** 给new Watcher一个id */
let id = 0

/**
 * @description 数据劫持时 期望一个属性对应多个watcher 同时一个watcher对应多个属性
 */
class Watcher {
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm
        this.exprOrFn = exprOrFn
        this.cb = cb
        this.options = options

        
        this.id = id++
        
        this.deps = []
        this.depsId = new Set()
        
        /** 用户watcher */
        this.user = !!options.user

        /** computed watcher */
        this.lazy = !!options.lazy
        this.dirty = options.lazy

        if (typeof exprOrFn == 'string') {
            this.getter = function() {
                // vm取值 'obj.n' -> ['obj', 'n'] -> vm['obj']['n']
                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
        }

        /** 用户watcher 默认取得第一次值 */
        /** 看这里 computed watcher 默认不执行 */
        this.value = this.lazy ? undefined : this.get()
    }

    /** render生成vnode时 dep.push(watcher) 并更新视图 */
    get() {
        pushTarget(this)
        const value = this.getter.call(this.vm)
        popTarget()

        return value
    }

    update() {
    	// 更新时 将其lazy -> true
        if (this.lazy) {
            this.dirty = true
        } else {
            queueWatcher(this)
        }
    }

    run() {
        /** 考虑1 渲染组件watcher */
        /** 考虑2 用户watcher(newValue oldValue) */
        let newValue = this.get()
        let oldValue = this.value
        this.value = newValue

        /** 用户watcher */
        if (this.user) {
            this.cb.call(this.vm, newValue, oldValue)
        }
    }

    /** 存储dep 并排除生成vnode时多次调用一样属性 只存一个 dep 或 watcher */
    /** 其次当属性发生变化时将不再存储dep 和 watcher */
    addDep(dep) {
        let id = dep.id
        if (!this.depsId.has(id)) {
            this.depsId.add(id)
            this.deps.push(dep)
            dep.addSub(this)
        }
    }

    /** 看这里computed watcher */
    /** 取值时 */
    evaluate() {
        this.dirty = false
        this.value = this.get()
    }

	/** 将当前的计算watcher里的所有deps都去依赖渲染watcher */
    depend() {
        let i = this.deps.length
        while(i--) {
            this.deps[i].depend()
        }
    }

}

export default Watcher

Dep

/** 每个劫持的属性 加上唯一的标识 */
let id = 0

/**
 * @description 每个劫持的属性 new Dep
 */
class Dep {
    constructor() {
        this.id = id++
        this.subs = []
    }

    /** dep传给watcher */
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

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

    notify() {
        this.subs.forEach(watcher => watcher.update())
    }

}

Dep.target = null

/** 看这里 收集watcher */
let stack = []

/** 看这里 指向时收集 */
export function pushTarget(watcher) {
    Dep.target = watcher
    stack.push(watcher)
}

/** 看这里 结束后删除 */
export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length - 1]
}

export default Dep