vue2源码浅读(四):computed和watch的原理与实现
computed的原理与实现
- 在初始化
initState
方法中,如果用户传入的配置有computed,调用initComputed
方法。
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
- 为key创建watcher后,调用
defineComputed
方法,代理到vm实例上。如下:
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
class Watcher {
constructor(vm,expOrFn,cb,options) {
this.vm = vm
this.expOrFn = expOrFn
this.cb = cb
if (options) {
this.lazy = !!options.lazy
}
this.dirty = this.lazy
this.deps = [];
this.set = {}
this.id = id++
if (typeof expOrFn === 'function') {
this.getters = expOrFn
}
this.value = this.lazy? undefined : this.get();
}
get() {
pushTarget(this)
let value = this.getters.call(this.vm, this.vm);
popTarget()
return value
}
run() {
let newValue = this.get()
}
addDep(dep) {
let id = dep.id
if (!this.set[id]) {
this.deps.push(dep)
dep.addSub(this)
this.set[id] = true;
}
}
upDate() {
if (this.lazy) {
this.dirty = true
}
}
evaluate() {
this.value = this.get();
this.dirty = false
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
watch的原理与实现
- 在初始化
initState
方法中,如果用户传入的配置有watch,调用initWatch
方法。
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}
class Watcher {
constructor(vm,expOrFn,cb,options) {
this.vm = vm
this.expOrFn = expOrFn
this.cb = cb
if (options) {
this.user = !!options.user
this.deep = !!options.deep
}
this.deps = [];
this.set = {}
this.id = id++
function parsePath(path) {
path = path.split('.')
return function (obj) {
path.forEach((key) => {
obj = obj[key]
})
return obj
}
}
if (typeof expOrFn === 'function') {
this.getters = expOrFn
} else {
this.getters = parsePath(this.expOrFn)
}
this.value = this.get();
}
get() {
pushTarget(this)
let value = this.getters.call(this.vm, this.vm);
if (this.deep) {
traverse(value)
}
popTarget()
return value
}
run() {
let newValue = this.get()
const oldValue = this.value
this.value = newValue
if (this.user) {
this.cb.call(this.vm, newValue, oldValue)
}
}
addDep(dep) {
let id = dep.id
if (!this.set[id]) {
this.deps.push(dep)
dep.addSub(this)
this.set[id] = true;
}
}
upDate() {
this.run()
}
teardown() {
if (this.active) {
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
function traverse(val) {
let isA = Array.isArray(val);
if (!isA) {
let key = Object.keys(val);
let i = key.length
while (i--) {
traverse(val[key])
}
}
}