1.引子
在了解vue中的監(jiān)聽器的詳細知識前泪勒,我們需要先從Vue的一個實例創(chuàng)建來說起屿岂。
我們以一個例子作為引子。下面是一個vue組件的實例化:
new Vue({
? el: '#root',
? data: {
? ? name: ''
? },
? watch: {
? ? name : {
? ? handler(newName, oldName) {
? ? ? // ...
? ? },
? ? immediate: true
? ? }
? }
})
?Vue 初始化主要就干了幾件事情燕锥,合并配置赚楚,初始化生命周期,初始化事件中心煌恢,初始化渲染骇陈,初始化 data、props瑰抵、computed你雌、watcher 等等。?
每當我們new一個新的Vue實例時二汛,其實都調(diào)用了一個_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)? ?// 初始化事件相關的屬性 ?
????initRender(vm)? ?// vm添加了一些虛擬dom婿崭、slot等相關的屬性和方法
? ? callHook(vm, 'beforeCreate')? ?//鉤子函數(shù),創(chuàng)建之前
????//下面initInjections习贫,initProvide兩個配套使用逛球,用于將父組件_provided中定義的值,通過inject注入到子組件苫昌,且這些屬性不會被觀察
????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實例初始化時官研,會調(diào)用一個初始化狀態(tài)的函數(shù)initState(vm)。
2. 數(shù)據(jù)的初始化
initState()函數(shù)存在于state.js中闯睹。到了這里戏羽,我們終于可以看到有關watch方法的相關內(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 = []? ?//為當前組件創(chuàng)建了一個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初始化
????}
????...
}
首先有一個初始化watch的名為initWatch的方法酷宵。其傳入兩個參數(shù):當前使用watch的組件和watch監(jiān)聽的對象。這個init方法做了什么事呢躬窜?可以從代碼中看出浇垦,其對watch對象中的每一個屬性(也就是watch所監(jiān)聽的組件)進行了遍歷。
再initWatch中斩披,傳入的第二個參數(shù)watch是整個Vue實例的watch對象溜族。這個watch對象中的屬性即為每個添加了watch對象的組件watch數(shù)組,數(shù)組中即為我們需要對象監(jiān)聽的組件的屬性垦沉。對于組件中的需要被監(jiān)聽的組件屬性煌抒,添加了一個createWatcher方法。
function initWatch ( vm: Component, watch: Object) {? ? //這里的watch:全局保存著全部watch數(shù)組的對象
????for(const key in watch) {? //遍歷全局watch對象厕倍,key即為單個組件中的watch
????????const handler = watch[key]
????????if (Array.isArray(handler)) {? ?//如果key為數(shù)組
????????????for(let i=0; i<handler.length; i++) {?? //遍歷單個組件中的watch數(shù)組寡壮, handler[i]即為watch數(shù)組中的屬性
????????????????createWatcher(vm, key, handler[i])? ?//每個需要被watch的屬性,做createWatcher() 操作讹弯,創(chuàng)建監(jiān)聽器 (數(shù)組)
? ? ? ????????}
????????}else{
????????createWatcher(vm, key, handler)?? //為屬性創(chuàng)建監(jiān)聽器 (字符串)
? ? }
? }
}
function createWatcher(? ?//為每個需要監(jiān)聽的屬性創(chuàng)建監(jiān)聽器
????vm:Component,? ?//當前組件
????expOrFn:string|Function,? ? //觀察對象:格式可為字符串或函數(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方法
}
這里主要進行了兩步預處理况既,代碼上很好理解,主要做一些解釋:
第一步组民,可以理解為用戶設置的 watch 有可能是一個 options 對象棒仍,如果是這樣的話則取 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)聽實例
????if(options.immediate) {? ? // immediate表示在watch中首次綁定的時候憨颠,是否執(zhí)行handler,值為true則表示在watch中聲明的時候积锅,就立即執(zhí)行handler方法爽彤,值為false养盗,則和一般使用watch一樣,在數(shù)據(jù)發(fā)生變化的時候才執(zhí)行handler
????????try{
????????????cb.call(vm, watcher.value)? ?// 首次聲明時就立即執(zhí)行回調(diào)
????????}catch(error) {
????????????handleError(error, vm,`callback for immediate watcher "${watcher.expression}"`)
????????}
????}
????return function unwatchFn() {
????????watcher.teardown()
????}
? }
初始化watch淫茵,就是為每個watch屬性創(chuàng)建一個觀察者對象爪瓜,這個expOrFn解析取值表達式去取值蹬跃,然后就會調(diào)用相關data/prop屬性的get方法匙瘪,get方法又會在他的觀察者列表里加上該watcher,一旦這些依賴屬性值變化就會通知該watcher執(zhí)行update方法蝶缀。即會執(zhí)行他的回調(diào)方法cb丹喻,也就是watch屬性的handler方法。
4. 組件的監(jiān)聽構造函數(shù)Watcher
前面在$watch中用到的Watcher構造函數(shù)翁都,在源碼/src/core/observer/watcher.js中:
class Watcher { // 當使用了$watch 方法之后碍论,不管有沒有監(jiān)聽,或者觸發(fā)監(jiān)聽柄慰,都會執(zhí)行以下方法
????constructor(vm, expOrFn, cb) {
????????this.cb = cb? //調(diào)用$watch時候傳進來的回調(diào)
????????this.vm = vm
????????this.expOrFn = expOrFn //這里的expOrFn是你要監(jiān)聽的屬性或方法也就是$watch方法的第一個參數(shù)
????????this.value = this.get()? //調(diào)用自己的get方法鳍悠,并拿到返回值
????}
????update(){? // 更新
????????this.run()
????}
????run(){? ?//這個方法并不是實例化Watcher的時候執(zhí)行的,而是監(jiān)聽的變量變化的時候才執(zhí)行的
????????const? value = this.get()
????????if(value !== this.value){
????????this.value = value
????????this.cb.call(this.vm)? ?//觸發(fā)你穿進來的回調(diào)函數(shù) expOrFn
????}
}
get(){ //向Dep.target 賦值為 Watcher
? ? Dep.target = this? //將Dep身上的target 賦值為Watcher對象
? ? const value = this.vm._data[this.expOrFn];? ?//這里拿到你要監(jiān)聽的值坐搔,在變化之前的數(shù)值
? ? // 聲明value藏研,使用this.vm._data進行賦值,并且觸發(fā)_data[a]的get事件
? ? Dep.target = null
? ? return value
? }
}
5. 深度監(jiān)聽deep
設置deep: true 則可以監(jiān)聽到對象的變化概行,此時會給對象的所有屬性都加上這個監(jiān)聽器蠢挡,當對象屬性較多時,每個屬性值的變化都會執(zhí)行handler凳忙。如果只需要監(jiān)聽對象中的一個屬性值业踏,則可以做以下優(yōu)化:使用字符串的形式監(jiān)聽對象屬性,這樣只會給對象的某個特定的屬性加監(jiān)聽器涧卵。
watch: {
????'cityName.name': {
? ? ? handler(newName, oldName) {
? ? ? // ...
? ? ? },
? ? ? deep: true,
? ? ? immediate: true
? ? }
? }
數(shù)組(一維勤家、多維)的變化不需要通過深度監(jiān)聽,對象數(shù)組中對象的屬性變化則需要deep深度監(jiān)聽柳恐。
參考文獻:
vue中watch的詳細用法:https://www.cnblogs.com/shiningly/p/9471067.html
vue的源碼學習之五——2.數(shù)據(jù)驅動:? ?https://blog.csdn.net/qishuixian/article/details/84964567