上一篇:Vue2.x 源码 - 响应式原理
Vue 的组件对象支持计算属性 computed 和侦听属性 watch,上一篇只是统一的分析了整个响应式的过程,这一篇针对这个两个属性来详细说明一下;
var vm = new Vue({
data:{
name:'xx',
value:'11'
},
computed:{
str: function(){
return `${this.name}:${this.value}`;
}
}
})
1、在 Vue 实例初始化阶段的 initState
函数中,这里执行了 initComputed
方法来对 computed
进行初始化:
//空函数,什么都不做,用于初始化一些值为函数的变量
export function noop () {}
const computedWatcherOptions = { lazy: true } //缓存
//初始化computed
function initComputed (vm: Component, computed: Object) {
// 创建一个空对象 watchers 没有原型链方法
const watchers = vm._computedWatchers = Object.create(null)
//是否是服务端渲染
const isSSR = isServerRendering()
// 循环computed
for (const key in computed) {
const userDef = computed[key]
//计算属性可能是一个function,也有可能设置了get以及set的对象。
const getter = typeof userDef === 'function' ? userDef : userDef.get
if (process.env.NODE_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) {
// 为computed属性创建内部监视器 Watcher,保存在vm实例的_computedWatchers中
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}
//当前key没有重复
if (!(key in vm)) {
//将computed绑定到 vm 上
defineComputed(vm, key, userDef)
} else if (process.env.NODE_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} else if (vm.$options.props && key in vm.$options.props) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
可以看出,每一个computed
其实都是一个 Watcher
,这里有这样一句 const computedWatcherOptions = { lazy: true }
在创建 Watcher
的时候当作参数传入,这是 computed
可以缓存的重要原因;
在 Watcher
类的构造函数里面有这样一段代码:
this.value = this.lazy ? undefined : this.get()
lazy 存在说明这个时候创建的是 computed watcher
,并且在初始化的时候,不会立刻调用 this.get()
去求值;
2、在组件挂载时,会执行 render
函数来创建一个 render watcher
,当访问到 this.str
的时候,会触发计算属性的 getter
:
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
}
}
}
dirty 初始值为 true ,调用 watcher.evaluate():
evaluate () {
this.value = this.get()
this.dirty = false
}
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}
重点:
2.1、将 dirty 设置为 false ,调用 Watcher 构造函数内部的 get;
2.2、在 get 里面会执行 value = this.getter.call(vm, vm)
,这实际上就是执行了计算属性定义的 getter 函数,在我这里就是 ${this.name}:${this.value}
;
2.3、通过pushTarget(this)
处理,这个时候的 Dep.target
就是个 computed watcher
,由于 name 和 value 是响应式的,又会触发他们的 getter 方法,在 getter 中会把这个computed watcher
作为依赖收集起来,保存到 name 和 value 的 dep 中;
数据之间的依赖收集已经结束了,下面手动调用 watcher.depend()
再次收集一次 Dep.target,于是 data 又收集到 恢复了的页面watcher,这样就完成了计算属性的依赖收集。
一旦计算属性依赖的数据发生变化,就会触发对应的 setter,通知所有的订阅者更新;调用 Watcher
的 update 方法:
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
这个时候 computed watcher 在初始化的时候 lazy 参数就是 true,因此在更新时会设置 dirty 为 true,告诉计算属性:需要重新计算了;在读取计算属性的时候就会重新计算了。
1、初始化时 computed 不会立刻求值, value 为 undefined,这个时候 Dep.target 为页面 watcher;
2、挂载的时候会调取 computed 的属性,这个时候触发计算属性的 getter 方法createComputedGetter
,第一次的时候会调用watcher.evaluate()
来进行求值,这个时候会触发 Watcher 类的 get 方法,Dep.target 会被设置成对应的 computed watcher,旧的值会被缓存;
3、computed 计算会调用计算属性定义的 getter 函数,这里会读取 computed 所有依赖的 data,读取 data 的时候又会触发 data 的getter 方法,这个时候 computed watcher 会被 data 的依赖收集器 dep 收集起来,在计算完成之后释放之前的 Dep.target ;
4、求值完成之后紧接着就会手动执行 watcher.depend 让 data 再次收集一次 Dep.target,这个时候收集到的是页面 watcher ,用于数据变化之后通知页面更新;
注意:computed 中的数据不经过 Observer 监听,所以不存在 dep。只有 data 和 props 会通过 Observer 监听。
侦听属性的初始化过程,与计算属性类似,都发生在 Vue 实例初始化阶段的 initState() 函数中,其中有一个 initWatch 函数:
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
//watch的每一项是不是数组
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
) {
//handler存在,只有handler是纯正的对象才会返回true
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
//字符串,从组件实例上获取handler
if (typeof handler === 'string') {
handler = vm[handler]
}
//创建监听器
return vm.$watch(expOrFn, handler, options)
}
1、获取 watch
循环遍历每一项,然后调取 createWatcher
方法;
2、 createWatcher
方法中 key 区分对象和字符串分别处理,然后为每一个属性创建监听器;
$watch
是 Vue 原型上的⽅法,它是在执⾏ stateMixin 的时候定义的:
//设置 $watch
Vue.prototype.$watch = function (
expOrFn: string | Function, //用户手动监听
cb: any, //监听变化之后的回调函数
options?: Object //参数
): Function {
const vm: Component = this
//如果cb是对象执行createWatcher,获取cb 对象里面的handler属性继续递归执行 $watch,直到cb不是对象
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
//标记为用户 watcher,用于给data从新赋值时走watch的监听函数
options.user = true
//为expOrFn添加watcher
const watcher = new Watcher(vm, expOrFn, cb, options)
//immediate==true 立即执行cb回调
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
//取消观察函数
return function unwatchFn () {
//从所有依赖项的订阅者列表中删除当前watcher
watcher.teardown()
}
}
判断 cb 是对象执行 createWatcher 方法,然后为 expOrFn 创建 watcher 实例,一但 watch 的数据变化了就会执行 watcher 的 run 方法,执行 cb 回调函数,最后会移除 watcher;
所以本质上侦听属性也是基于 Watcher 实现的,它是⼀个 user watcher ,具体分析看下面;
从 Watcher 的参数来区分,Watcher 可以分为4种类型: deep、user、computed、sync;
通常,如果我们想对⼀下对象做深度观测的时候,需要设置这个属性为 true,考虑到这种情况:
var vm = new Vue({
data() {
a: {
b: 1
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b = 2
这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了;
⽽我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
这样就创建了⼀个 deep watcher 了,在 watcher 执⾏ get 求值的过程中有⼀段逻辑:
if (this.deep) {
traverse(value)
}
在对 watch 的表达式或者函数求值后,会调⽤ traverse 函数,它的定义在 src/core/observer/traverse.js 中:
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
traverse 的逻辑也很简单,它实际上就是对⼀个对象做深层递归遍历,因为遍历过程中就是对⼀个 ⼦对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher ,这个函数实现还有⼀个⼩的优化,遍历过程中会把⼦响应式对象通过它们的 dep id 记 录到 seenObjects
,避免以后重复访问。
在使用的时候需要注意一下,按照实际情况开启 deep 模式,因为 traverse
函数,会有⼀定的性能开销;
通过 vm.$watch 创建的 watcher 是⼀个 user watcher ,其实它的功能很简 单,在对 watcher 求值以及在执⾏回调函数的时候,会处理⼀下错误,如下:
//watcher get方法里面
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
//watcher run
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
除了对错误的处理,就是直接执行 cb 函数,这个 cb 函数就是自定义的回调函数;
computed watcher 就是为计算属性量⾝定制的,可以参考上面 computed 依赖收集和更新;
在 Watcher 的 update 方法中:
if (this.sync) {
this.run()
}
当响应式数据发送变化后,触发了 watcher.update() , 只是把这个 watcher 推送到⼀个队列中,在 nextTick 后才会真正执⾏ watcher 的回调函数。 ⽽⼀旦我们设置了 sync ,就可以在当前 Tick 中同步执⾏ watcher 的回调函数。
只有当我们需要 watch 的值的变化到执⾏ watcher 的回调函数是⼀个同步过程的时候才会去设置该 属性为 true。
1、computed
computed 是响应式的
computed 可以缓存
依赖的 data 变了,computed 才会更新
属性名不能和 data 、props重名
2、watch
可以设置 immediate 立即执行,deep 深度监听、sync 同步执行
watch值变了然后会触发对应的 data 更新
监听的属性必须是 data 、props 里面已经定义的
下一篇:Vue2.x 源码 - 生命周期