watch与computed原理
侦听器watch与计算属性computed对于我这样的Vue初学者,很容易就弄混淆了,感觉他俩差不多呀,近期扣了这部分原理,总结了这篇文章。
第一次写这么长的文章,有点乱,有问题希望大家帮我指出来~~
watch
分三步进行:
- initWatch
- mount
- update
initWatch
Vue在initState时,会对watch也进行初始化
// /src/core/instance/state.js
export function initState (vm: Component) {
// .....
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch) // <== 调用initWatch
}
}
watch属性可有多种类型 string | object | array | function,在initWatch函数中主要处理了数组的情况,
// /src/core/instance/state.js
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key] // 拿到对应侦听的键值,类型为 string | object | array | function
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) { // 是数组就遍历每一项
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler) // 调用createWatcher来处理watch[key]的其余情况,并创建用户watcher
}
}
}
createWatcher主要处理handler类型为object、string的情况,最后调用vm.$watch创建watcher
vm.$watch的参数:
- expOrFn:watch的键key eg: 'a' | 'a.c'
- handler:key对应的函数 eg:function(){....}
- options:一个对象,存储handler为对象的配置信息 eg:{handler: function(){}, deep: true, immediate: true}
// /src/core/instance/state.js
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) { // handler为对象,
options = handler // handler赋给options
handler = handler.handler // handler.handler取出对应的函数
}
// handler为字符串,在当前vm实例的methods查找名为handler的方法,赋给handler
if (typeof handler === 'string') {
handler = vm[handler]
}
// expOrFn:watch的健key eg: 'a' | 'a.c'
// handler:key对应的函数 eg:function(){....}
// options:一个对象,存储handler为对象的配置信息 eg:{handler: function(){}, deep: true, immediate: true}
return vm.$watch(expOrFn, handler, options)
}
vm.$watch主要就是创建一个用户watcher
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) { // 查看 cb是否为对象,是就调用createWatcher处理
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true // 标识是用户自己写的watcher
// vm.$watch('msg',()=>{}) vm 'msg' ()=>{} {deep,immediate, user}
const watcher = new Watcher(vm, expOrFn, cb, options) // 创建一个watcher,默认执行watcher.get()对msg取值
if (options.immediate) { // 需要立即执行?
try {
cb.call(vm, watcher.value) // watcher.value:msg初始值
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
}
看看Watcher构造函数做了什么
-
判断expOrFn是否为函数,侦听器的key一般为字符串表达式,所以执行
parsePath(expOrFn)
将表达式转为函数function(){return vm.xxx}
,主要作用就是取值,然后将函数赋给watcher.getter
-
执行
watcher.get()
,将当前watcher添加到全局,接着执行watcher.getter
对vm.xxx
进行取值,并让vm.xxx
的dep收集当前的用户watcher,目前vm.xxx
就一个用户watcher。接着有watcher.deep
判断vm.xxx
是否需要深度代理watcher,这种情况一般vm.xxx
是一个内嵌有多层的对象(vm.xxx:{a:{c: 1}}),或数组的情况,执行watcher.get
没有对内嵌的对象进行依赖搜集,所以用户可以配置deep来对内嵌的对象(或数组)也进行依赖搜集。 -
那看看deep原理
-
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 (val.__ob__) { // 初始化观察data属性时,data每个属性都被递归观察添加了一个__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) } }
-
当前
Dep.target
为用户watcher,并且vm.data
在initData过程中,每一个属性都被递归响应式观察了,就是说对象或数组都被递归观测了,即每个属性都会有个dep实例。重点来了,deep原理就是对当前对象或数组的每一项递归取值,在调用getter取值过程中,每一项都会进行dep的依赖搜集,所以改变vm.xxx
的内嵌对象属性时也会触发搜集的用户watcher
-
-
popTarget()
执行,在全局下清除当前watcher,最后返回value值给watcher.value
// //src/core/observer/watcher.js
export default class Watcher {
// watch vm 'xxx' ()=>{....} {immidate, deep, user}
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
this.deep = !!options.deep
this.user = !!options.user
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn) // 会将表达式转成函数 msg function(){return vm.msg}
}
this.value = this.get() // 用户watcher默认会执行get
}
get () {
pushTarget(this) // 将watcher 放到全局上 Dep.target = watcher
let value
const vm = this.vm
try { // 对msg 进行取值 {a:{a:1}}
value = this.getter.call(vm, vm) // user watcher:取值 会进行依赖收集
} catch (e) {
// ......
} finally {
if (this.deep) { // deep:true
traverse(value)
}
popTarget()
}
return value
}
}
此时用户watcher创建完成,就是对侦听的data属性进行取值和依赖(user watcher)搜集
再回到vm.$watch
里接着向下执行,查看用户是否配置了immediate
,immediate
为true时,执行侦听器的回调函数
if (options.immediate) { // 需要立即执行?
try {
cb.call(vm, watcher.value) // watcher.value:msg初始值
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
watch初始化完成,接着页面会挂载
mount
页面挂载过程中,每个组件都会被创建一个渲染watcher,然后模板在编译过程中对侦听的data属性进行取值,就会触发被侦听属性的getter,然后该data属性就会收集当前的渲染watcher,即当前该data属性至少搜集了两个watcher,用户watcher和渲染watcher
举个例子
........
<div>{{ msg }}</div>
........
var vm = new Vue({
data:{
msg: '1111'
},
watch:{
msg:function(val, oldval) {
console.log(val, oldval);
}
}
})
vm.msg
在initWatch阶段搜集了一个用户watcher。在挂载阶段,又对msg取值,触发了msg的getter,然后msg收集了第二个watcher——当前组件的渲染watcher
msg ==> dep.subs['user watcher', 'render watcher']
update
再修改vm.msg
的值
......
mounted() {
this.msg = '2222'
}
......
触发msg的setter,修改msg值并调用dep.notify
循环通知每个watcher更新,
-
首先user watcher更新,触发
user watcher
的run方法-
run () { if (this.active) { const value = this.get() if ( value !== this.value ) { const oldValue = this.value // initWatch 取得值 this.value = value if (this.user) { this.cb.call(this.vm, value, oldValue) } else { this.cb.call(this.vm, value, oldValue) } } } }
-
调用
watcher.get
取新值,接着比较新值与老值,不等就调用msg对应的侦听方法msg: function(val, oldval) { console.log(val, oldval); }
-
-
接着render watcher更新,就是更新组件了
watch基本上就这个原理了
computed
也分为三部分:
- initComputed
- mount
- update
initComputed
initState
// /src/core/instance/state.js
export function initState (vm: Component) {
// .....
if (opts.computed) initComputed(vm, opts.computed) // <== initComputed
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
initComputed主要做了两件事,其一就是创建计算属性watcher,其二调用defineComputed
将计算属性映射到vm实例上。vm._computedWatchers
存储着当前vm实例所有的computed watcher
function initComputed (vm: Component, computed: Object) {
// watchers存储计算属性watcher
const watchers = vm._computedWatchers = Object.create(null)
// computed properties are just getters during SSR
const isSSR = isServerRendering()
// 两种写法 只读 xxx:() => {} 可读可操作:xxx:{get(){...}, set(){...}}
for (const key in computed) {
const userDef = computed[key] // 获取用户定义的 方法 | 对象
const getter = typeof userDef === 'function' ? userDef : userDef.get //获取读的方法
if (!isSSR) {
watchers[key] = new Watcher( // 给每个计算属性创建一个watcher
vm,
getter || noop, // 将用户定义的方法传入 xxx:() => {....} get(){...}
noop, // ()=>{}
computedWatcherOptions // {lazy:true}
)
}
if (!(key in vm)) { // 计算属性名不在vm.data里
defineComputed(vm, key, userDef) // 将计算属性定义到实例上
} else if (process.env.NODE_ENV !== 'production') {
// ....
}
}
}
创建计算属性watcher
计算属性watcher的标识:{lazy: true}
export default class Watcher {
// computed vm|()=>{....}|()=>{}|{lazy: true}
constructor ( vm, expOrFn, cb, options, isRenderWatcher ) {
// .......
this.dirty = this.lazy // 如果是计算属性默认dirty就是true
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// .....
}
this.value = this.lazy // 计算属性默认不执行
? undefined
: this.get()
}
}
创建watcher主要做了两件事,赋予watcher.getter
函数,此函数就是用户定义的,初始化watcher.dirty
为true
defineComputed
主要处理Object.defineProperty
的第三个参数,确认当前计算属性的getter和setter。然后调用Object.defineProperty
将计算属性映射到vm实例上。
export function defineComputed ( // vm
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering() // server端不需要缓存
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key) // 创建计算属性的getter
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else { // userDef 为对象 有getter或setter 取出
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// 将计算属性映射到vm实例上
Object.defineProperty(target, key, sharedPropertyDefinition)
}
createComputedGetter
创建计算属性getter,当取计算属性的值时就会触发computedGetter
computedGetter主要做了什么?mount挂载阶段会详说
function createComputedGetter (key) {
return function computedGetter () { // 取值的时候回调用此方法
// ......
}
}
computed初始化主要就是创建计算属性watcher、将计算属性映射到vm实例、并创建getter
mount
看个例子
<div>{{ fullName }}</div>
new Vue({
data(){
return {
firstName: '111',
lastName: '222'
}
},
computed:{
fullName() {
return this.firstName + this.lastName;
}
}
})
组件的渲染watcher创建
组件挂载,首先为组件创建一个渲染watcher,添加到全局targetStack
数组中,Dep.target
指向当前的渲染watcher。
// /src/core/observer/dep.js
Dep.target = null
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
接着执行当前组件的更新方法初始化页面,会对fullName
进行取值,也就触发计算属性fullName
的getter。就是createComputedGetter
创建的computedGetter
function createComputedGetter (key) {
return function computedGetter () { // 取值的时候回调用此方法
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 做了一个dirty 实现了缓存的机制
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
从vm._computedWatchers
获取fullName
计算属性watcher,在创建watcher时,watcher.dirty
被初始化为true,所以这里需要调用watcher.evaluate
对fullName
取值,看看watcher.evaluate
方法
class Watcher {
//....
get () {
pushTarget(this) // 将watcher 放到全局上 Dep.target = watcher
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
} finally {
popTarget()
}
return value
}
evaluate () {
this.value = this.get()
this.dirty = false
}
}
调用this.get()
-
将当前的computed watcher push到
targetStack
,Dep.target
指向computed watcher
,此时targetStack
存着两个watcher-
targetStack = ['render watcher', 'computed watcher']
-
-
执行
this.getter.call(vm, vm)
,就是执行计算属性的方法-
computed:{ fullName() { return this.firstName + this.lastName; } }
-
就是执行
fullName
回调,执行过程中,又会对firstName
和lastName
取值,触发了它俩的getter,所以firstName
和lastName
的dep会搜集当前Dep.target
所指向的computed watcher
。同样firstName
和lastName
的dep也会被添加到computed watcher
中,此时fullName
计算属性watcher保留着两个dep。 -
返回
fullName
计算的值
-
-
this.getter.call(vm, vm)
将结果返回给value
。调用popTarget
,删除targetStack
末尾的computed watcher
,Dep.target
指向'render watcher'
,此时,targetStack
只有一个渲染watcher -
返回
value
到这this.get()
执行完毕,watcher.value
拿到返回的value
;接着将watcher.dirty
值改为false,表示当前已为计算属性取过值了,只要firstName
和lastName
值不变,以后就不需要重新计算取值,达到一个缓存的效果。
计算属性求值完成,接着执行其getter后面的代码
function createComputedGetter (key) {
return function computedGetter () { // 取值的时候回调用此方法
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) { // 做了一个dirty 实现了缓存的机制
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}
Dep.target
指向渲染watcher,执行watcher.depend()
,目的:给firstName
和lastName
的dep添加当前的渲染watcher,这样firstName
和lastName
值改变就会更新组件了,此时firstName
和lastName
的dep有两个watcher
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
fullName
的getter执行完了,也就是组件渲染获取fullName
完毕,在清楚点就是渲染watcherwatcher.getter
到这执行完毕。组件挂载完毕,接着执行popTarget()
,清除当前渲染watcher,此时targetStack
为空数组,Dep.target指向空
渲染watcher创建完毕
我在说什么鬼~~,自己有点晕了
updata
mounted() {
this.firstName = '33333';
}
改变firstName
的值,触发firstName
的setter,dep.notify
通知watcher更新
firstName ==> dep.subs = ['computed watcher', 'render watcher'];
-
computed watcher
-
update () { /* istanbul ignore else */ if (this.lazy) { // 计算属性 依赖的数据发生变化了 会让计算属性的watcher的dirty变成true this.dirty = true } else if (this.sync) { // 同步watcher this.run() } else { queueWatcher(this) // 将watcher放入队列 } }
-
判断当前watcher是否为计算属性watcher,是就执行
this.dirty = true
语句,表示下次计算属性要重新取值
-
-
render watcher
执行watcher.run()
,-
get () { pushTarget(this) // 将watcher 放到全局上 Dep.target = watcher let value const vm = this.vm try { value = this.getter.call(vm, vm) } catch (e) { } finally { popTarget() } return value } run () { const value = this.get() }
-
get() --> 对
fullName
取值 getter -->fullName
的计算属性watcher.dirty为true,重新计算取值 -->获取fullName
新值 --> 组件更新
-
computed的东西基本上就这些
总结
watch(user:true)
侦听的属性,必须是vm.data
的属性。在侦听的属性创建用户watcher时,默认会执行watcher.get()
对侦听属性取值,并进行依赖搜集。当侦听属性被修改后就会触发依赖更新,用户watcher主要触发侦听属性的回调;若页面引用了该属性,还会触发渲染watcher更新组件视图。
watch deep的原理:在创建用户watcher时,watcher.get()
会根据用户的配置信息,判断是否需要让当前watcher.value
内的属性也进行依赖搜集,dep == true
那就递归循环对每一项取值,取值触发getter,所以遍历的每一项都会进行依赖搜集。
computed(lazy:true)
计算的属性不允许能在data中定义过,否则会报错,但初始化会将计算属性代理到vm实例上。
创建计算属性watcher时,默认不会执行什么操作,但在组件渲染对计算属性取值时,会触发Vue自定义的计算属性getter,对计算属性取值,取值时也会对监听的vm.data
属性取值和依赖搜集,vm.data
属性的计算属性watcher是此时搜集来的,同时计算属性watcher也会对vm.data
属性的dep进行搜集(详细可以看源码dep.depend
方法和watcher.addDep
方法,主要实现依赖watcher与dep实例一多一、多对多的关系)。
vm.data
属性只搜集计算属性watcher可不行呀,还得搜集组件的渲染watcher,如何呢?计算属性取完值后,计算属性watcher会被targetStack
移除。接着调用计算属性的watcher.depend
对搜集了当前计算属性watcher的dep,循环添加Dep.target
指向的watcher,这时的watcher就是渲染watcher了。
当计算属性所依赖的vm.data
的属性变化时,该属性的dep就会循环更新搜集的watcher,计算属性watcher主要就是将watcher.dirty
修改为true,表示需要重新取值;接着渲染watcher更新,当对计算属性取值时,发现watcher.dirty
为true,然后重新取值,将watcher.dirty
为false,更新组件。
区别
1、watch监听是vm.data
的一个属性,它的getter不需另做处理,而computed监听是一个新的属性,它需要被代理到vm
实例上,并且getter需要重新创建
2、底层都是watcher:watch创建watcher内部默认会对属性取值,它要有老值嘛;computed watcher的创建就不会取值,只有组件渲染需要计算属性值时,才会取值
3、computed对计算属性的值有缓存处理,只要依赖的vm.data
属性值不变,重复取值,不会重复计算
4、watch趋向于监听某个vm.data
属性值变化后进行一些操作,而computed趋向于"我"的改变依赖其他属性值的变化
5、watch的用户watcher的更新主要是执行回调,computed的计算属性watcher更新就是将watcher.dirty
修改为true,等待重新取值时计算