Vue源码---watch实现原理
创始人
2025-06-01 10:38:38

watch的基础了解

基础知识

首先,在深入了解其原理之前,要先知道它要怎么用,有啥作用。

  • 在vue中watch用来监听数据的变化,一旦发生变化可以执行一些其他操作

  • watch就是当值第一次绑定的时候,是不会执行监听函数的,只有值诞生改变才会执行。如果需要在第一次绑定的时候也执行函数,则需要用到immediate属性

  • handler方法:immediate表示在watch中首次绑定的时候,是否执行handler,值为true则表示在watch中声明的时候,就立即执行handler方法,值为false,则和一般使用watch一样,在数据发生变化时才执行

  • deep:当需要监听一个对象的变化时,普通的watch方法无法监听带对象内部属性的变化,只有data中的数据才能够监听到变化,此时需要deep属性进行深度监听,设置deep:true,当对象的属性较多是,每个属性的变化都会执行handler

  • 可以只监听对象的其中一个属性

  • watch中不要使用箭头函数,即不要用箭头函数来定义watcher函数

声明形式

第一种,我们可以采用字符串声明

var vm = new Vue({el: '#example',data: {message: 'Hello'},watch: {message: 'handler'},methods: {handler (newVal, oldVal) { /* ... */ }}
})

第二种,函数声明

var vm = new Vue({el: '#example',data: {message: 'Hello'},watch: {message: function (newVal, oldVal) { /* ... */ }}
})

第三种,对象声明

var vm = new Vue({el: '#example',data: {peopel: {name: 'jojo',age: 15}},watch: {// 字段可使用点操作符 监听对象的某个属性'people.name': {handler: function (newVal, oldVal) { /* ... */ }}}
})
watch: {people: {handler: function (newVal, oldVal) { /* ... */ },// 回调会在监听开始之后被立即调用immediate: true,// 对象深度监听  对象内任意一个属性改变都会触发回调deep: true}
}

第四种,数组声明

var vm = new Vue({el: '#example',data: {peopel: {name: 'jojo',age: 15}},// 传入回调数组,它们会被逐一调用watch: {'people.name': ['handle',function handle2 (newVal, oldVal) { /* ... */ },],  },methods: {handler (newVal, oldVal) { /* ... */ }}
})

源码理解

首先,vue的初始化入口在 src/core/instance/index.js

_init:

接着,我们跳进initMixin(Vue)里面

initMixin

export function initMixin (Vue: Class) {Vue.prototype._init = function (options?: Object) {const vm: Component = this// a uidvm._uid = uid++let startTag, endTag/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {startTag = `vue-perf-start:${vm._uid}`endTag = `vue-perf-end:${vm._uid}`mark(startTag)}// a flag to avoid this being observedvm._isVue = true// merge optionsif (options && options._isComponent) {// optimize internal component instantiation// since dynamic options merging is pretty slow, and none of the// internal component options needs special treatment.initInternalComponent(vm, options)} else {vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)}/* istanbul ignore else */if (process.env.NODE_ENV !== 'production') {initProxy(vm)} else {vm._renderProxy = vm}// expose real selfvm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')/* istanbul ignore if */if (process.env.NODE_ENV !== 'production' && config.performance && mark) {vm._name = formatComponentName(vm, false)mark(endTag)measure(`vue ${vm._name} init`, startTag, endTag)}if (vm.$options.el) {vm.$mount(vm.$options.el)}}
}
这里代码很多,但我们只需要关注我们需要关注的就行了,也就是 initState(vm),有关于data、methods、props、watch的都在这里面

src/core/instance/state.js :

initState

initWatch:

对于initWatch的两个参数,一个是vm实例对象,一个是watch对象,也就是我们要监听的东西
接着它会有一个判断,如果它要监听的对象是一个数组的话,我们循环遍历并且创建监听,否则,我们直接创建即可

createWatcher:

对于 createWatcher 而言,有四个参数,vm实例对象,expOrFn订阅对象,handler回调函数,options选项

然后我们先看第一个if判断:

如果handler是一个对象的话,采用options来保存这个对象,而handler取这个对象的真正回调。

一开始理解这个,感觉有点绕,其实就是上面写到的声明形式中的对象声明

所以执行完以后,handler就等于我们声明中的那个 function,而options就等于最外侧的对象watch:{}

再看第二个if判断:如果是字符串声明的watch,我们直接取实例上的方法

然后再往下看: return vm.$watch

$watch

我们继续看一下这个 $watch ,可以发现它是vue原型链上的一个方法,这个cb其实就是callback回调,也就是上面的那个handler

这里其实一开始还防了一手特殊情况,那就是 我们上面提到 对象声明的watch,用options保存该对象,handler取它真正的handler回调,那如果对象的handler依旧是一个对象呢? 这里就再判断一下
然后再往下,关于 options.user = true,似乎是因为,vue把watch按照使用场景分为3类,分别是 computed watcher用于执行计算属性更新,user watcher用于用户注册,render watcher用于视图更新。
我们使用时是用户自己定义的watch,所以将options.user设为true
所以再下一行的new Watcher就不难理解了,创建一个Watcher进行依赖收集
if(options.immediate)就是我们如果设置为true,立即调用回调
unwatchFn返回的函数可以用于取消watch监听

现在,重要就是研究这个Watcher

Watcher

constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}vm._watchers.push(this)// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // for lazy watchersthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noopprocess.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}this.value = this.lazy ? undefined : this.get()}

对于前一段的代码,没什么好讲的,就是将传进来的一些东西进行保存嘛

重点可以放在这一段上面

if (typeof expOrFn === 'function') {this.getter = expOrFn
} else {this.getter = parsePath(expOrFn)
}
传进来的expOrFn是watch的键值,而键值就有可能是 obj.a.b的形式,所以需要调用parsePath进行解析,这一步就是依赖收集的关键,它返回的是一个函数,也就是 this.getter是一个函数

对于parsePath的代码,如下

const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {if (bailRE.test(path)) {return}const segments = path.split('.')return function (obj) {for (let i = 0; i < segments.length; i++) {if (!obj) returnobj = obj[segments[i]]}return obj}
}

这里就是先将传进来的path进行字符串分割,按照 "."分成数组segments,最后返回一个函数

举个例子就是, 比如键值的形式是 obj.a.b.c,那它传进来的不就是 a.b.c嘛,我们将它划分为数组,并循环访问。首先是 访问 obj.a 赋值给obj,然后就是 obj.b(这里就是相当于 obj.a.b了),最后访问obj.a.b.c并返回

再往下走,还剩一句代码

this.value = this.lazy ? undefined : this.get()

这里就是对lazy进行判断,看看是不是写了lazy。 而lazy在computed中可以看到是为true的,这里lazy为true,就不会去获取它的值。(也就是不会进行依赖初始化,因为computed有缓存机制,只有在真正使用到这个值的时候才会进行依赖收集),而其他时候会调用this.get

所以接下来会调用this.get()

get () {pushTarget(this)let valueconst vm = this.vmtry {value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value)}popTarget()this.cleanupDeps()}return value}

get上来第一步就会进行 pushTarget(this)

export function pushTarget (target: ?Watcher) {targetStack.push(target)Dep.target = target
}
就是把全局Dep.target,也就是现在的上下文设置为当前的Watcher。 Dep.target是一个储存watcher的全局变量

然后就会 this.getter.call(vm,vm),就是对vm的属性进行层级访问,触发data中目标属性的get方法,进行依赖收集

      // "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value)}

如果设置了deep的话,就会traverse进行深度监听

export function traverse (val: any) {_traverse(val, seenObjects)seenObjects.clear()
}function _traverse (val: any, seen: SimpleSet) {let i, keysconst isA = Array.isArray(val)if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {return}if (val.__ob__) {const depId = val.__ob__.dep.idif (seen.has(depId)) {return}seen.add(depId)}if (isA) {i = val.lengthwhile (i--) _traverse(val[i], seen)} else {keys = Object.keys(val)i = keys.lengthwhile (i--) _traverse(val[keys[i]], seen)}
}
depId 是每一个被观察属性都会有的唯一标识
if(seen.has(depId)){} 是为了去重,防止相同属性重复执行逻辑
if(isA) 根据数组和对象使用不同策略,最终的目的是递归的获取每一项属性,触发它们的数据劫持get收集依赖

当data的属性发生变动时,触发属性的set方法,执行属性对应的dep.notify方法,通知收集的所有watcher,执行watcher.update方法进行更新

  update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}

queueWatcher进行异步更新

最后会调用run方法,this.get获取新值, this.cb将新旧值传入

总结:

vue中watch对数据监听的原理是:

对watch每个属性创建一个watcher,watcher在初始化时会将监听的目标值缓存到watcher.value中,因此触发 data[key]的get方法,被对应的dep进行依赖收集;当data[key]发生变动时触发set方法,执行dep.notify方法,通知所有收集的依赖watcher,触发收集的watch watcher,执行watcher.cb,也就是watch中的监听函数

相关内容

热门资讯

漂移是什么意思(摇杆漂移是什么... 本篇文章极速百科给大家谈谈漂移是什么意思,以及摇杆漂移是什么意思对应的知识点,希望对各位有所帮助,不...
车架号后四位是什么(车架号后四... 本篇文章极速百科给大家谈谈车架号后四位是什么,以及车架号后四位是什么在哪里看对应的知识点,希望对各位...
隐形眼镜基弧是什么意思,请问,... 隐形眼镜基弧是什么意思目录隐形眼镜基弧是什么意思请问,配隐形眼镜的时候要不要关注那个基弧?隐形眼镜基...
广西省崇左市属于什么市,祟左是... 广西省崇左市属于什么市目录广西省崇左市属于什么市祟左是地级市还是县级市崇左是南宁得直辖市吗 为什么区...
内衣尺码大小分类,内衣的型号分... 内衣尺码大小分类目录内衣尺码大小分类内衣的型号分哪几种?什么abc事什么意思?34、36是尺寸嘛?内...
几个防止卫生间反味小妙招,卫生... 几个防止卫生间反味小妙招目录几个防止卫生间反味小妙招卫生间反臭怎么办?卫生间怎么样防臭几个防止卫生间...
庄子中的成语和解释,四个出自《... 庄子中的成语和解释目录庄子中的成语和解释四个出自《庄子》的成语及解释《庄子》中的成语及解释(按篇目分...
怎么切翡翠原石(收玉石的联系方... 本篇文章极速百科给大家谈谈怎么切翡翠原石,以及收玉石的联系方式对应的知识点,希望对各位有所帮助,不要...
关于燕子的古诗,描写燕子的古诗... 关于燕子的古诗目录关于燕子的古诗描写燕子的古诗描写燕子的古诗有哪些?关于燕子的古诗关于燕子的古诗 ...
好巧不巧是什么意思,好巧不巧什... 好巧不巧是什么意思目录好巧不巧是什么意思好巧不巧什么意思?“无巧不巧”究竟何解?好巧不巧是什么意思好...
关东煮里面放什么配料啊,关东煮... 关东煮里面放什么配料啊目录关东煮里面放什么配料啊关东煮的配料关东煮需要哪些调味料呀?请问,关东煮都可...
极速进化满电出发!长安深蓝SL... 本篇文章极速百科给大家谈谈极速进化满电出发!长安深蓝SL03开启预售,以及长安蓝鲸plus新车报价对...
比亚迪f3汽车报价(比亚迪f3... 今天给各位分享比亚迪f3汽车报价的知识,其中也会对比亚迪f3价格及图片易车进行解释,如果能碰巧解决你...
两台电脑怎么共享一台打印机,两... 两台电脑怎么共享一台打印机目录两台电脑怎么共享一台打印机两台电脑如何共享一台打印机?请问一个打印机怎...
16个复韵母有哪些(16个复韵... 本篇文章极速百科给大家谈谈16个复韵母有哪些,以及16个复韵母怎么读拼音视频对应的知识点,希望对各位...
樟树有什么作用,樟树有什么作用... 樟树有什么作用目录樟树有什么作用樟树有什么作用?樟树的用途有哪些?樟树有什么作用?樟树有什么作用 ...
dazl启动子的作用,启动子和... dazl启动子的作用目录dazl启动子的作用启动子和终止子是什么作用的?dazl启动子的作用启动子的...
写字楼是干什么的 极速百科网 ... 写字楼是干什么的目录写字楼是干什么的写字楼是干什么的写字楼是干什么的 写字楼的功能介绍写字楼是干什么...
骄傲的两种解释,骄傲的意思是什... 骄傲的两种解释目录骄傲的两种解释骄傲的意思是什么?骄傲的两种解释骄傲的两种解释 “骄傲”有两个...
jp是哪个国家的缩写(jp是哪... 本篇文章极速百科给大家谈谈jp是哪个国家的缩写,以及jp是哪个国家的缩写名字对应的知识点,希望对各位...
苹果手机显示不支持此配件怎么办... 不支持此配件怎么解决 苹果iphone可能不支持此配件怎么办怎么解除不支持此配件 不支持此配件怎么解...
支付宝借呗的利息是多少,蚂蚁借... 支付宝借呗的利息是多少目录支付宝借呗的利息是多少蚂蚁借呗利息是怎么计算的蚂蚁借呗的利息是多少借呗的利...
关于兰字的词语或成语越多越好.... 关于兰字的词语或成语越多越好.目录关于兰字的词语或成语越多越好.有关兰字的成语有哪些关于兰的词语或成...
宝马m5多少钱是不是很贵呢?(... 本篇文章极速百科给大家谈谈宝马m5多少钱是不是很贵呢?,以及宝马m5li多少钱对应的知识点,希望对各...
辽宁省喀左县在哪个城市,辽宁省... 辽宁省喀左县在哪个城市目录辽宁省喀左县在哪个城市辽宁省朝阳市喀左县的邮政编码辽宁省喀左县在哪里辽宁省...
关于marcjacobs香水,... 关于marcjacobs香水目录关于marcjacobs香水marcjacobs香水(探索时尚与艺术...
四级英语考试时间分配,大学英语... 四级英语考试时间分配目录四级英语考试时间分配大学英语四级考多长时间?英语四级考试时间安排?英语四级考...
dnfbuff强化有什么用,地... dnfbuff强化有什么用目录dnfbuff强化有什么用地下城buff强化栏DNF中人物的Buff有...
幼儿园孩子新年祝福语简短,适合... 幼儿园孩子新年祝福语简短目录幼儿园孩子新年祝福语简短适合幼儿园小朋友说的新年祝福语幼儿园老师给小朋友...
正断层有哪些断层组合类型,断层... 正断层有哪些断层组合类型目录正断层有哪些断层组合类型断层的组合类型简答题 断层的类型及组合形式有哪些...