在开发过程当中,咱们对这两个属性已经很是熟悉了:computed
和watch
。可是究其实现原理,或者说两者到底有何区别,以及何时使用计算属性,何时使用侦听属性,相信有很多朋友们仍存在疑惑。下面就一块儿来探讨一下:react
关于计算属性的使用,见以下代码。express
export default {
data() {
return {
msg : {a : 1},
count : 1
}
}
methods : {
changeA() {
this.count++;
}
}
computed: {
newMsg() {
if (this.count < 3) return this.count;
return 5;
}
}
}
复制代码
在初始化时(initState
),判断若是opts.computed
存在(用户定义了computed
属性),执行initComputed
。数组
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (!isSSR) {
// create internal watcher for the computed property.
// self-notes : it won't evaluate value immediately
watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
}
if (!(key in vm)) {
defineComputed(vm, key, userDef)
} else {
// throw a warn(The computed property id already defined in data / prop
}
}
}
复制代码
在initComputed
中,首先获取用户定义的computed
(能够是函数,也能够是对象)。若是不是服务端渲染,就调用new Watcher()
实例化一个 Watcher(咱们称之为computed Watcher
),实例化的具体过程咱们稍后讨论。紧接着,判断若是当前 vm 对象上找不到computed
对象的 key 值,调用defineComputed
,而该函数就完成了将 key 值变成了 vm 的属性。函数
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export 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)
}
复制代码
除此以外,在该函数中还作了另外一件更重要的事情,设置了computed
的 getter 和 setter,将createComputedGetter
函数的返回值做为 getter,而 setter 并不经常使用,能够暂且不关注。oop
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
}
}
}
复制代码
该函数返回一个函数,函数中进行了computed
值的计算和依赖收集。那何时会触发这个 getter 呢?一样是在执行render
时,会访问到咱们定义的相关数据。以上就是计算属性初始化的大体过程。ui
可是,若是顺着以上思路走,在调试源码时会发现一个问题,在走到if(!(key in vm))
时,条件是不成立的,这样就没法执行defineComputed
,还谈何初始化。那这是为何呢?其实,查找源码会发现,在Vue.extend
(也就是处理子组件的构造器时)中已经对计算属性作了一些处理。下面一块儿来看一下。this
Vue.extend = function (extendOptions: Object): Function {
if (Sub.options.computed) {
initComputed(Sub)
}
}
function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}
复制代码
所以总体逻辑是这样的。在最外层组件的initState
时,判断opts.computed
为undefined
,继续走到Vue.extend
(生成子组件的构造器),此时Sub.options.computed
是存在的,调用initComputed
(函数内调用defineComputed
),设置计算属性的 getter 为createComputedGetter
函数的返回值(是一个函数)。lua
当再次走到子组件的initState
时,此时opts.computed
是存在的,调用initComputed()
(实例化computed Watcher
,值为undefined
),此时if(!(key in vm))
条件不成立,计算属性完成的初始化。spa
因为设置了 getter,在render
过程当中访问到计算属性时,就会调用其 getter,在该函数中调用watcher.evaluate()
计算出计算属性的值【调用watcher.get()
-> watcher.getter()
(即用户定义的计算属性的 getter,在过程当中会访问到计算属性依赖的其余数据(即执行数据的 getter,同时进行依赖收集),将订阅该数据变化的computed Watcher
存入new Dep().subs
中)】,最后调用watcher.depend()
(再次依赖收集,将render Watcher
存入new Dep().subs
中)。此时就完成了计算属性的初次计算以及依赖收集。当咱们更改数据时,就进入了派发更新的过程。prototype
set: function reactiveSetter(newVal) {
var value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
复制代码
当咱们更改计算属性依赖的数据时,会触发它的 setter,若是值发生变化,会调用dep.notify()
。在该函数中,会遍历依赖数组分别执行update
(subs
中有两个 Watcher(computed Watcher
和render Watcher
)),当执行到render Watcher
的update
时,调用queueWatcher
(在过程当中从新计算计算属性的值,从新渲染页面)。
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
Watcher.prototype.update = function update() {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true;
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}
复制代码
以上就是关于计算属性的内容,接下来看侦听属性。
关于侦听属性的使用,见以下代码。
export default {
data() {
return {
msg: { a: 1 },
count: 1,
nested: {
a: {
b: 1
}
}
};
},
methods: {
change() {
this.count++;
this.nested.a.b++;
}
},
computed: {
newMsg() {
if (this.count < 4) {
return this.count;
}
return 5;
}
},
watch: {
newMsg(newVal) {
console.log("newMsg : " + newVal);
},
count: {
immediate: true,
handler(newVal) {
console.log("count : " + newVal);
}
},
nested: {
deep: true,
sync: true,
handler(newVal) {
console.log("nested : " + newVal.a.b);
}
}
}
}
复制代码
同计算属性同样,在initState
中,判断若是opts.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) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}
复制代码
初始的流程也很是简单,大体通过了initWatch
-> createWatcher
-> vm.$watch
,在vm.$watch
中,建立了user Watcher
,且该函数返回一个函数,能够用来销毁user Watcher
。接下来一块儿探讨user Watcher
的建立过程。
// new Watcher(vm, expOrFn, cb, options)
export default class Watcher {
constructor(vm, exporFn, cb, options){
this.deep = !!options.deep
this.user = !!options.user
this.sync = !!options.sync
this.getter = parsePath(exporFn)
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
}
export function parsePath (path: string): any {
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
obj = obj[segments[i]]
}
return obj
}
}
复制代码
在user Watcher
的建立过程当中,因为传入的getter
是一个字符串,须要通过parsePath
函数处理,该函数返回一个函数,赋值给watcher.getter
。与计算属性不一样,侦听属性会当即调用watcher.get()
进行求值(过程当中执行watcher.getter
会访问到相关数据,触发数据的 getter,进行依赖收集)。user Watcher
建立完毕后,判断options.immediate
若是为 true,则当即执行用户定义的回调函数。 当数据发生更改时,调用dep.notify()
进行派发更新,总体更新过程仍是相似以前,此处就再也不进行详细解释。 总结一下,计算属性的本质是computed Watcher
,侦听属性的本质是user Watcher
,那究竟什么时候使用计算属性,什么时候使用侦听属性呢?通常来讲,计算属性适合使用在模板渲染中,某个值是依赖了某些响应式对象而计算得来的;而侦听属性适用于观测某个值的变化去完成一段逻辑。