1.引子
在了解vue中的監(jiān)聽器的詳細(xì)知識(shí)前,我們需要先從Vue的一個(gè)實(shí)例創(chuàng)建來說起。
我們以一個(gè)例子作為引子。下面是一個(gè)vue組件的實(shí)例化:
new Vue({
? el: '#root',
? data: {
? ? name: ''
? },
? watch: {
? ? name : {
? ? handler(newName, oldName) {
? ? ? // ...
? ? },
? ? immediate: true
? ? }
? }
})
?Vue 初始化主要就干了幾件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。?
每當(dāng)我們new一個(gè)新的Vue實(shí)例時(shí),其實(shí)都調(diào)用了一個(gè)_init()函數(shù):
(入口文件地址:src/core/instance/index.js)
function Vue (options) {
? if (process.env.NODE_ENV !== 'production' &&
? ? !(this instanceof Vue)
? ) {
? ? warn('Vue is a constructor and should be called with the `new` keyword')
? }
? this._init(options)? ?//調(diào)用_init
}
_init()函數(shù)存在于init.js中:地址(src/core/instance/init.js)
// init.js部分代碼如下:
Vue.prototype._init = function (options?: Object) {?
????const vm: Component = this?
????...
????initLifecycle(vm)?
????initEvents(vm)? ?// 初始化事件相關(guān)的屬性 ?
????initRender(vm)? ?// vm添加了一些虛擬dom、slot等相關(guān)的屬性和方法
? ? callHook(vm, 'beforeCreate')? ?//鉤子函數(shù),創(chuàng)建之前
????//下面initInjections,initProvide兩個(gè)配套使用,用于將父組件_provided中定義的值,通過inject注入到子組件,且這些屬性不會(huì)被觀察
????initInjections(vm)? ?// resolve injections before data/props
????initState(vm)? ?//初始化狀態(tài),主要就是操作數(shù)據(jù)了,props、methods、data、computed、watch,從這里開始就涉及到了Observer、Dep和Watcher
????initProvide(vm)? ?// resolve provide after data/props
????callHook(vm, 'created')?? //鉤子函數(shù),創(chuàng)建完成
????...
}
可以看出,在Vue實(shí)例初始化時(shí),會(huì)調(diào)用一個(gè)初始化狀態(tài)的函數(shù)initState(vm)。
2. 數(shù)據(jù)的初始化
initState()函數(shù)存在于state.js中。到了這里,我們終于可以看到有關(guān)watch方法的相關(guān)內(nèi)容了。我們看一下state.js源碼中是如何將watch方法與使用watch方法的組件、watch所監(jiān)聽的內(nèi)容來相互聯(lián)系的。(源碼地址:vue/src/core/instance/state.js)
var nativeWatch = ({}).watch;? //這里是為了兼容火狐, Firefox has a "watch" function on Object.prototype?
export function initState(vm:Component) {
????vm._watchers = []? ?//為當(dāng)前組件創(chuàng)建了一個(gè)watchers屬性,為數(shù)組類型
????const opts = vm.$options
????if(opts.props) initProps(vm,opts.props)
????if(opts.methods) initMethods(vm,opts.methods)
????if(opts.data) {
????????initData(vm)
????}else{
????????observe(vm._data = {},? true? /*asRootData*/)
????? }
????if(opts.computed) initComputed(vm, opts.computed)
????if(opts.watch && opts.watch !== nativeWatch) {? //判斷組件有watch屬性?并沒有nativeWatch( 兼容火狐)
????????initWatch(vm, opts.watch)? ?//調(diào)用watch初始化
????}
????...
}
首先有一個(gè)初始化watch的名為initWatch的方法。其傳入兩個(gè)參數(shù):當(dāng)前使用watch的組件和watch監(jiān)聽的對(duì)象。這個(gè)init方法做了什么事呢?可以從代碼中看出,其對(duì)watch對(duì)象中的每一個(gè)屬性(也就是watch所監(jiān)聽的組件)進(jìn)行了遍歷。
再initWatch中,傳入的第二個(gè)參數(shù)watch是整個(gè)Vue實(shí)例的watch對(duì)象。這個(gè)watch對(duì)象中的屬性即為每個(gè)添加了watch對(duì)象的組件watch數(shù)組,數(shù)組中即為我們需要對(duì)象監(jiān)聽的組件的屬性。對(duì)于組件中的需要被監(jiān)聽的組件屬性,添加了一個(gè)createWatcher方法。
function initWatch ( vm: Component, watch: Object) {? ? //這里的watch:全局保存著全部watch數(shù)組的對(duì)象
????for(const key in watch) {? //遍歷全局watch對(duì)象,key即為單個(gè)組件中的watch
????????const handler = watch[key]
????????if (Array.isArray(handler)) {? ?//如果key為數(shù)組
????????????for(let i=0; i<handler.length; i++) {?? //遍歷單個(gè)組件中的watch數(shù)組, handler[i]即為watch數(shù)組中的屬性
????????????????createWatcher(vm, key, handler[i])? ?//每個(gè)需要被watch的屬性,做createWatcher() 操作,創(chuàng)建監(jiān)聽器 (數(shù)組)
? ? ? ????????}
????????}else{
????????createWatcher(vm, key, handler)?? //為屬性創(chuàng)建監(jiān)聽器 (字符串)
? ? }
? }
}
function createWatcher(? ?//為每個(gè)需要監(jiān)聽的屬性創(chuàng)建監(jiān)聽器
????vm:Component,? ?//當(dāng)前組件
????expOrFn:string|Function,? ? //觀察對(duì)象:格式可為字符串或函數(shù)
????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)? //調(diào)用組件的$watch方法
}
這里主要進(jìn)行了兩步預(yù)處理,代碼上很好理解,主要做一些解釋:
第一步,可以理解為用戶設(shè)置的 watch 有可能是一個(gè) options 對(duì)象,如果是這樣的話則取 options 中的 handler 作為回調(diào)函數(shù)。(并且將options 傳入下一步的 vm.$watch)
第二步,watch 有可能是之前定義過的 method,則獲取該方法為 handler。
第三步,調(diào)用組件的$watch方法。
3. 組件的$watch方法
Vue.prototype.$watch = function(? ?// 定義在Vue原型上的$watch
????expOrFn: string | Function,? ? // 接收數(shù)據(jù)類型(字符串/方法)
????cb:any,? // 任意類型的回調(diào)方法,也就是 createWatcher里的handler
????options?: Object
? ): Function {
????????const vm: Component = this? ?
????????if(isPlainObject(cb)) {? ? ?// 如果cb不是回調(diào)方法,那就先創(chuàng)建監(jiān)聽器
????????????return createWatcher(vm, expOrFn, cb, options)
? ? }
????options = options || {}
????options.user = true
????const watcher = new Watcher(vm, expOrFn, cb, options)? ?// 創(chuàng)建監(jiān)聽實(shí)例
????if(options.immediate) {? ? // immediate表示在watch中首次綁定的時(shí)候,是否執(zhí)行handler,值為true則表示在watch中聲明的時(shí)候,就立即執(zhí)行handler方法,值為false,則和一般使用watch一樣,在數(shù)據(jù)發(fā)生變化的時(shí)候才執(zhí)行handler
????????try{
????????????cb.call(vm, watcher.value)? ?// 首次聲明時(shí)就立即執(zhí)行回調(diào)
????????}catch(error) {
????????????handleError(error, vm,`callback for immediate watcher "${watcher.expression}"`)
????????}
????}
????return function unwatchFn() {
????????watcher.teardown()
????}
? }
初始化watch,就是為每個(gè)watch屬性創(chuàng)建一個(gè)觀察者對(duì)象,這個(gè)expOrFn解析取值表達(dá)式去取值,然后就會(huì)調(diào)用相關(guān)data/prop屬性的get方法,get方法又會(huì)在他的觀察者列表里加上該watcher,一旦這些依賴屬性值變化就會(huì)通知該watcher執(zhí)行update方法。即會(huì)執(zhí)行他的回調(diào)方法cb,也就是watch屬性的handler方法。
4. 組件的監(jiān)聽構(gòu)造函數(shù)Watcher
前面在$watch中用到的Watcher構(gòu)造函數(shù),在源碼/src/core/observer/watcher.js中:
class Watcher { // 當(dāng)使用了$watch 方法之后,不管有沒有監(jiān)聽,或者觸發(fā)監(jiān)聽,都會(huì)執(zhí)行以下方法
????constructor(vm, expOrFn, cb) {
????????this.cb = cb? //調(diào)用$watch時(shí)候傳進(jìn)來的回調(diào)
????????this.vm = vm
????????this.expOrFn = expOrFn //這里的expOrFn是你要監(jiān)聽的屬性或方法也就是$watch方法的第一個(gè)參數(shù)
????????this.value = this.get()? //調(diào)用自己的get方法,并拿到返回值
????}
????update(){? // 更新
????????this.run()
????}
????run(){? ?//這個(gè)方法并不是實(shí)例化Watcher的時(shí)候執(zhí)行的,而是監(jiān)聽的變量變化的時(shí)候才執(zhí)行的
????????const? value = this.get()
????????if(value !== this.value){
????????this.value = value
????????this.cb.call(this.vm)? ?//觸發(fā)你穿進(jìn)來的回調(diào)函數(shù) expOrFn
????}
}
get(){ //向Dep.target 賦值為 Watcher
? ? Dep.target = this? //將Dep身上的target 賦值為Watcher對(duì)象
? ? const value = this.vm._data[this.expOrFn];? ?//這里拿到你要監(jiān)聽的值,在變化之前的數(shù)值
? ? // 聲明value,使用this.vm._data進(jìn)行賦值,并且觸發(fā)_data[a]的get事件
? ? Dep.target = null
? ? return value
? }
}
5. 深度監(jiān)聽deep
設(shè)置deep: true 則可以監(jiān)聽到對(duì)象的變化,此時(shí)會(huì)給對(duì)象的所有屬性都加上這個(gè)監(jiān)聽器,當(dāng)對(duì)象屬性較多時(shí),每個(gè)屬性值的變化都會(huì)執(zhí)行handler。如果只需要監(jiān)聽對(duì)象中的一個(gè)屬性值,則可以做以下優(yōu)化:使用字符串的形式監(jiān)聽對(duì)象屬性,這樣只會(huì)給對(duì)象的某個(gè)特定的屬性加監(jiān)聽器。
watch: {
????'cityName.name': {
? ? ? handler(newName, oldName) {
? ? ? // ...
? ? ? },
? ? ? deep: true,
? ? ? immediate: true
? ? }
? }
數(shù)組(一維、多維)的變化不需要通過深度監(jiān)聽,對(duì)象數(shù)組中對(duì)象的屬性變化則需要deep深度監(jiān)聽。

參考文獻(xiàn):
vue中watch的詳細(xì)用法:https://www.cnblogs.com/shiningly/p/9471067.html
vue的源碼學(xué)習(xí)之五——2.數(shù)據(jù)驅(qū)動(dòng):? ?https://blog.csdn.net/qishuixian/article/details/84964567