
详解Vue中的watch和computed
前⾔
对于使⽤Vue的前端⽽⾔,watch、computed和methods三个属性相信是不陌⽣的,是⽇常开发中经常使⽤的属性。但是对于
它们的区别及使⽤场景,⼜是否清楚,本⽂我将跟⼤家⼀起通过源码来分析这三者的背后实现原理,更进⼀步地理解它们所代
表的含义。 在继续阅读本⽂之前,希望你已经具备了⼀定的Vue使⽤经验,如果想学习Vue相关知识,请移步⾄官⽹。
Watch
我们先来找到watch的初始化的代码,/src/core/instance/
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if () initProps(vm, ) // 初始化props
if (s) initMethods(vm, s) // 初始化⽅法
if () {
initData(vm) // 先初始化data 重点
} el {
obrve(vm._data = {}, true /* asRootData */)
}
if (ed) initComputed(vm, ed) // 初始化computed
if ( && !== nativeWatch) {
initWatch(vm, ) // 初始化watch
}
}
接下来我们深⼊分析⼀下initWatch的作⽤,不过在接下去之前,这⾥有⼀点是data的初始化是在computed和watch初始化之
前,这是为什么呢?⼤家可以停在这⾥想⼀下这个问题。想不通也没关系,继续接下来的源码分析,这个问题也会迎刃⽽解。
initWatch
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (y(handler)) { // 如果handler是⼀个数组
for (let i = 0; i < ; i++) { // 遍历watch的每⼀项,执⾏createWatcher
createWatcher(vm, key, handler[i])
}
} el {
createWatcher(vm, key, handler)
}
}
}
createWatcher
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) { // 判断handler是否是纯对象,对options和handler重新赋值
options = handler
handler = r
}
if (typeof handler === 'string') { // handler⽤的是methods上⾯的⽅法,具体⽤法请查看官⽹⽂档
handler = vm[handler]
}
// expOrnFn: watch的key值, handler: 回调函数 options: 可选配置
return vm.$watch(expOrFn, handler, options) // 调⽤原型上的$watch
}
ype.$watch
ype.$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 || {}
= true // ur Watcher的标⽰ options = { ur: true, ...options }
const watcher = new Watcher(vm, expOrFn, cb, options) // new Watcher ⽣成⼀个ur Watcher
if (ate) { // 如果传⼊了immediate 则直接执⾏回调cb
try {
(vm, )
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${sion}"`)
}
}
return function unwatchFn () {
wn()
}
}
}
上⾯⼏个函数调⽤的逻辑都⽐较简单,所以就在代码上写了注释。我们重点关注⼀下这个urWatcher⽣成的时候做了什么。
Watcher
⼜来到了我们⽐较常见的Watcher类的阶段了,这次我们重点关注⽣成urWatch的过程。
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
ur: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array
newDeps: Array
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
= vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._(this)
// options
if (options) { // 在 new UrWatcher的时候传⼊了options,并且 = true
= !!
= !!
= !!
= !!
=
} el {
= = = = fal
}
= cb
= ++uid // uid for batching
= true
= // for lazy watchers
= []
s = []
= new Set()
Ids = new Set()
sion = _ENV !== 'production' // ⼀个函数表达式
ng()
: ''
// par expression for getter
if (typeof expOrFn === 'function') {
= expOrFn
} el {
= parPath(expOrFn) // 进⼊这个逻辑,调⽤parPath⽅法,对getter进⾏赋值
if (!) {
= noop
_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, u a function instead.',
vm
)
}
}
=
undefined
: ()
}
}
⾸先会对这个watcher的属性进⾏⼀系列的初始化配置,接着判断expOrFn这个值,对于我们watch的key⽽⾔,不是函数所以
会执⾏parPath函数,该函数定义如下:
/**
* Par simple path.
*/
const bailRE = new RegExp(`[^${}.$_d]`)
export function parPath (path: string): any {
if ((path)) {
return
}
const gments = ('.')
return function (obj) {
for (let i = 0; i < ; i++) { // 遍历数组
if (!obj) return
obj = obj[gments[i]] // 每次把当前的key值对应的值重新赋值obj
}
return obj
}
}
⾸先会判断传⼊的path是否符合预期,如果不符合则直接return,接着讲path根据'.'字符串进⾏拆分,因为我们传⼊的watch可能
有如下⼏种形式:
watch: {
a: () {}
'formData.a': () {}
}
所以需要对path进⾏拆分,接下来遍历拆分后的数组,这⾥返回的函数的参数obj其实就是vm实例,通过vm[gments[i]],就可
以最终找到这个watch所对应的属性,最后将obj返回。
constructor () { // 初始化的最后⼀段逻辑
= // 因为为fal,所以会执⾏⽅法
undefined
: ()
}
get () {
pushTarget(this) // 将当前的watcher实例赋值给
let value
const vm =
try {
value = (vm, vm) // 这⾥的getter就是上⽂所讲parPath放回的函数,并将vm实例当做第⼀个参数传⼊
} catch (e) {
if () {
handleError(e, vm, `getter for watcher "${sion}"`) // 如果报错了会这这⼀块逻辑
} el {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if () { // 如果deep为true,则执⾏深递归
traver(value)
}
popTarget() // 将当前watch出栈
pDeps() // 清空依赖收集 这个过程也是尤为重要的,后续我会单独写⼀篇⽂章分析。
}
return value
}
对于UrWatcher的初始化过程,我们基本上就分析完了,traver函数本质就是⼀个递归函数,逻辑并不复杂,⼤家可以⾃
⾏查看。 初始化过程已经分析完,但现在我们好像并不知道watch到底是如何监听data的数据变化的。其实对于UrWatcher
的依赖收集,就发⽣在⽅法中,通过(parPath)函数,我们就访问了vm实例上的属性。因为这个时候已
经initData,所以会触发对应属性的getter函数,这也是为什么initData会放在initWatch和initComputed函数前⾯。所以当前的
UrWatcher就会被存放进对应属性Dep实例下的subs数组中,如下:
Property(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? (obj) : val
if () {
()
if (childOb) {
()
if (y(value)) {
dependArray(value)
}
}
}
return value
},
}
前⼏个篇章我们都提到renderWatcher,就是视图的初始化渲染及更新所⽤。这个renderWathcer初始化的时机是在我们执⾏
$mount⽅法的时候,这个时候⼜会对data上的数据进⾏了⼀遍依赖收集,每⼀个data的key的Dep实例都会将renderWathcer
放到⾃⼰的subs数组中。如图:
, 当我们对data上的数据进⾏修改时,就会触发对应属性的tter函数,进⽽触发(),遍历subs中的每⼀个watcher,
执⾏()函数->,renderWathcer的update⽅法我们就不深究了,不清楚的同学可以参考下我写的。 对
于我们分析的UrWatcher⽽⾔,相关代码如下:
class Watcher {
constructor () {} //..
run () {
if () { // ⽤于标⽰watcher实例有没有注销
const value = () // 执⾏get⽅法
if ( // ⽐较新旧值是否相同
value !== ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, becau the value may
// have mutated.
isObject(value) ||
) {
// t new value
const oldValue =
= value
if () { // UrWatcher
try {
(, value, oldValue) // 执⾏回调cb,并传⼊新值和旧值作为参数
} catch (e) {
handleError(e, , `callback for watcher "${sion}"`)
}
} el {
(, value, oldValue)
}
}
}
}
}
⾸先会判断这个watcher是否已经注销,如果没有则执⾏⽅法,重新获取⼀次新值,接着⽐较新值和旧值,如果相同则
不继续执⾏,若不同则执⾏在初始化时传⼊的cb回调函数,这⾥其实就是handler函数。⾄此,UrWatcher的⼯作原理就分
析完了。接下来我们来继续分析ComputedWatcher,同样的我们找到初始代码
Computed
initComputed
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
// $flow-disable-line
const watchers = vm._computedWatchers = (null) // ⽤来存放computedWatcher的map
// computed properties are just getters during SSR
const isSSR = isServerRendering()
for (const key in computed) {
const urDef = computed[key]
const getter = typeof urDef === 'function' ? urDef :
if (_ENV !== 'production' && getter == null) {
warn(
`Getter is missing for computed property "${key}".`,
vm
)
}
if (!isSSR) { // 不是服务端渲染
// create internal watcher for the computed property.
watchers[key] = new Watcher( // 执⾏new Watcher
vm,
getter || noop,
noop,
computedWatcherOptions { lazy: true }
)
}
// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
// 会在vm的原型上去查找computed对应的key值存不存在,如果不存在则执⾏defineComputed,存在的话则退出,
// 这个地⽅其实是Vue精⼼设计的
// ⽐如说⼀个组件在好⼏个⽂件中都引⽤了,如果不将computed
defineComputed(vm, key, urDef)
} el if (_ENV !== 'production') {
if (key in vm.$data) {
warn(`The computed property "${key}" is already defined in data.`, vm)
} el if (vm.$ && key in vm.$) {
warn(`The computed property "${key}" is already defined as a prop.`, vm)
}
}
}
}
defineComputed
new Watcher的逻辑我们先放⼀边,我们先关注⼀下defineComputed这个函数到底做了什么
export function defineComputed (
target: any,
key: string,
urDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof urDef === 'function') { // 分⽀1
= shouldCache
createComputedGetter(key)
: createGetterInvoker(urDef)
= noop
} el {
=
shouldCache && !== fal
createComputedGetter(key)
: createGetterInvoker()
: noop
= || noop
}
if (_ENV !== 'production' &&
=== noop) {
= function () {
warn(
`Computed property "${key}" was assigned to but it has no tter.`,
this
)
}
}
Property(target, key, sharedPropertyDefinition)
}
这个函数本质也是调⽤Property来改写computed的key值对应的getter函数和tter函数,当访问到key的时候,
就会触发其对应的getter函数,对于⼤部分情况下,我们会⾛到分⽀1,对于不是服务端渲染⽽
⾔,会被createComputedGetter(key)赋值,t会被赋值为⼀个空函数。
createComputedGetter
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key] // 就是上⽂中new Watcher()
if (watcher) {
if () {
te()
}
if () {
()
}
return
}
}
}
可以看到createComputedGetter(key)其实会返回⼀个computedGetter函数,也就是说在执⾏render函数时,访问到这个
vm[key]对应的computed的时候会触发getter函数,⽽这个getter函数就是computedGetter。
export default {
data () {
return {
a: 1,
b: 2
}
},
computed: {
message () { // 这⾥的函数名message就是所谓的key
return this.a + this.b
}
}
}
以上代码为例⼦,来⼀步步解析computedGetter函数。 ⾸先我们需要先获取到key对应的watcher.
const watcher = this._computedWatchers && this._computedWatchers[key]
⽽这⾥的watcher就是在initComputed函数中所⽣成的。
if (!isSSR) { // 不是服务端渲染
// create internal watcher for the computed property.
watchers[key] = new Watcher( // 执⾏new Watcher
vm,
getter || noop,
noop,
computedWatcherOptions { lazy: true }
)
}
我们来看看computedWatcher的初始化过程,我们还是接着来继续回顾⼀下Watcher类相关代码
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
ur: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array
newDeps: Array
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
= vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._(this)
// options
if (options) {
= !!
= !!
= !! // lazy = true
= !!
=
} el {
= = = = fal
}
= cb
= ++uid // uid for batching
= true
= // for lazy watchers = true 这⾥把设置为true
= []
s = []
= new Set()
Ids = new Set()
sion = _ENV !== 'production'
ng()
: ''
// par expression for getter
if (typeof expOrFn === 'function') { // ⾛到这⼀步
= expOrFn
} el {
// ..
}
= // ⼀开始不执⾏()函数 直接返回undefined
undefined
: ()
}
紧接着回到computedGetter函数中,执⾏剩下的逻辑
if (watcher) {
if () {
te()
}
if () {
()
}
return
}
⾸先判断watcher是否存在,如果存在则执⾏以下操作
判断是否为true,如果为true,则执⾏te
判断当前是否存在,存在则执⾏
最后返回
在computedWatcher初始化的时候,由于传⼊的为true,所以相应的也为true,当我们在执⾏render函
数的时候,访问到message,触发了computedGetter,所以会执⾏te。
evaluate () {
= () // 这⾥的get() 就是vm['message'] 返回就是this.a + this.b的和
= fal // 将dirty置为fal
}
同时这个时候由于访问vm上的a属性和b属性,所以会触发a和b的getter函数,这样就会把当前这个computedWatcher加⼊到
了a和b对应的Dpe实例下的subs数组中了。如图:
接着当前的毫⽆疑问就是renderWatcher了,并且也是存在的,所以就执⾏了()
depend () {
let i =
while (i--) {
[i].depend()
}
}
对于当前的message computedWatcher⽽⾔,其实就是a和b两个属性对应的Dep实例,接着遍历整个deps,对每⼀
个dep就进⾏depend()操作,也就是每⼀个Dep实例把当前的(renderWatcher都加⼊到各⾃的subs中,如图:
所以这个时候,⼀旦你修改了a和b的其中⼀个值,都会触发tter函数->()->,代码如下:
update () {
/* istanbul ignore el */
if () {
= true
} el if () {
()
} el {
queueWatcher(this)
}
}
总结
其实不管是watch还是computed本质上都是通过watcher来实现,只不过它们的依赖收集的时机会有所不同。就使⽤场景⽽
⾔,computed多⽤于⼀个值依赖于其他响应式数据,⽽watch主要⽤于监听响应式数据,在进⾏所需的逻辑操作!⼤家可以通
过单步调试的⽅法,⼀步步调试,能更好地加深理解。
以上就是详解Vue中的watch和computed的详细内容,更多关于Vue watch和computed的资料请关注其它相关⽂章!

本文发布于:2023-05-23 18:27:22,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/zhishi/a/168483764251173.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:详解Vue中的watch和computed.doc
本文 PDF 下载地址:详解Vue中的watch和computed.pdf
| 留言与评论(共有 0 条评论) |