Vue--$watch()源碼分析


  這一段時間工作上不是很忙,所以讓我有足夠的時間來研究一下VueJs還是比較開心的 (只要不加班怎么都開心),說到VueJs總是讓人想到雙向綁定,MVVM,模塊化,等牛逼酷炫的名詞,而通過近期的學習我也是發現了Vue一個很神奇的方法$watch,第一次嘗試了下,讓我十分好奇這是怎么實現的,

為什么變量賦值也會也會觸發回調?這背后又有什么奇淫巧技?懷着各種問題,我看到了一位大牛,楊川寶的文章,但是我還是比較愚笨,看了三四遍,依然心存疑惑,最終在楊大牛的GitHub又看了許久,終於有了眉目,本篇末尾,我會給上鏈接

  在正式介紹$watch方法之前,我有必要先介紹一下實現基本的$watch方法所需要的知識點,並簡單介紹一下方便理解:

    1)   Object.defineProperty ( obj, key , option) 方法

        這是一個非常神奇的方法,同樣也是$watch以及實現雙向綁定的關鍵

        總共參數有三個,其中option中包括  set(fn), get(fn), enumerable(boolean), configurable(boolean)

        set會在obj的屬性被修改的時候觸發,而get是在屬性被獲取的時候觸發,(其實屬性的每次賦值,每次取值,都是調用了函數

    2)  Es6 知識,例如Class,()=>, const, let,這些也都比較基礎,但是如果不知道的話,還是還是推薦了解一下;

    3)  面向對象編程,例如Object.keys,constructor,call,以及各種花式this指向,不過這些方法,也不是特別難理解,稍加搜索,OK的。

    下面簡單介紹一下$watch方法的使用

      其使用方法如下:

  

 1  //----/VUE.JS
 2           const v = new Vue({  3  data:{  4               a:1,  5  b:{  6                 c:3
 7  }  8  }  9  }) 10           // 實例方法$watch,監聽屬性"a"
11           v.$watch("a",()=>console.log("你修改了a")) 12                         //當Vue實例上的a變化時$watch的回調
13           setTimeout(()=>{ 14             v.a = 2
15             // 設置定時器,修改a
16           },1000)

     

    怎么樣?是不是很簡單,而且很有用?下面我來簡單的實現一下$watch這個方法;

    從實例化Vue對象開始,到調用$watch方法,再到屬性變化,觸發回調,我分為三個階段

    首先第一個階段

    new Vue(options)

   想要實現watch,當實例化Vue對象的時候,有下面三個函數,需要被調用

   

 1 class Vue { //Vue對象
 2  constructor (options) {  3       this.$options=options;  4       let data = this._data=this.$options.data;  5       Object.keys(data).forEach(key=>this._proxy(key));  6       // 拿到data之后,我們循環data里的所有屬性,都傳入代理函數中
 7       observe(data,this);  8  }  9     $watch(expOrFn, cb, options){  //監聽賦值方法
10       new Watcher(this, expOrFn, cb); 11       // 傳入的是Vue對象
12  } 13 
14     _proxy(key) { //代理賦值方法
15       // 當未開啟監聽的時候,屬性的賦值使用的是代理賦值的方法
16       // 而其主要的作用,是當我們訪問Vue.a的時候,也就是Vue實例的屬性時,我們返回的是Vue.data.a的屬性而不是Vue實例上的屬性
17       var self = this
18  Object.defineProperty(self, key, { 19         configurable: true, 20         enumerable: true, 21         get: function proxyGetter () { 22           return self._data[key] 23           // 返回 Vue實例上data的對應屬性值
24  }, 25         set: function proxySetter (val) { 26           self._data[key] = val 27  } 28  }) 29  } 30   }

 

    以上是一個Class Vue ,這個Vue類身上有三個方法,分別是constructor (實例化默認方法),$watch(也就是我們今天要實現的方法)和一個_proxy(代理)方法

    constructor

        當Vue被實例化,並傳入參數(options)的時候,constructor 就會被調用,並且接收Vue實例化的參數options,這個函數在這里做的事情,就是,對傳進來的data進行加工 第一步做的,就是讓Vue對象,和參數data,產生一個關聯,好讓你可以通過,this.a , 或者vm.a 來操作data屬性,建立關聯之后,循環data的所有鍵名,將其傳入到_proxy方法,到這里constructor方法的主要作用就結束了,什么?你說還有observe方法?這個屬於另一個方向,稍后會做出解釋;

    $watch

      這個方法你一看就會明白,只是實例化Watcher對象,而Watcher對象里還有其他的什么方法,稍后我會介紹;

    _proxy

      這個方法是一個代理方法,接收一個鍵名,作用的對象是Vue對象,具體的作用嘛,不知道大家有沒有想過,這個:    

1 //首先我們實例化Vue對象
2 var vm = Vue({ 3  data:{ 4        a:1, 5        msg:'今天學習watch'      
6  } 7 }) 8 console.log(vm.msg) //打印 '今天學習watch'
9 // 理論上來說,msg和a,應該是data上的屬性,但是卻可以通過vm.msg直接拿到

    原因就在於,_proxy 這個方法身上,我們可以看到defineProperty方法作用的對象,是self,也就是Vue對象,而get方法里,return出來的卻是self._data[key], _data在上面的方法當中,已經和參數data相等了,所以當我們訪問Vue.a的時候,get方法返回給我們的,是Vue._data.a。

    但是,表面上,他只是為了修改取值和賦值的方法,而且就算我用Vue.data.a取值,又能怎么樣?但是實際上,叫它代理方法,不是沒有原因的,當watch事件開啟了監聽屬性的時候,變量的set和get方法當然是有watch來控制,畢竟人家還有回調要搞嘛,但是,當我不需要watch的時候,怎么辦呢?

那當然就是這個代理函數加上的set和get方法來代理沒有watch時候的取值和賦值的方法啦。

   下面我來說一下,剛才漏掉的,opserve(data,this)

   當我們在new Vue的時候,傳進去的data很可能包括子對象,例如在使用Vue.data.a = {a1:1 , a2:2 }的時候,這種情況是十分常見的,但是剛才的_proxy函數只是循環遍歷了key,如果我們要給對象的子對象增加set和get方法的時候,最好的方法就是遞歸;

   方法也很簡單,如果有屬性值 == object,那么久把他的屬性值拿出來,遍歷一次,如果還有,繼續遍歷,代碼如下:

    

 1      class  Observer{  //對象Observer
 2             constructor(value) {//value 就是Vue實例上的data
 3               this.value = value  4               this.dep = new Dep()  5               //Dep對象是聯絡Watcher對象和觸發監聽回調的對象,稍后會有描述
 6               this.walk(value)  7  }  8             //遞歸。。讓每個字屬性可以observe
 9  walk(value){ 10               Object.keys(value).forEach(key=>this.convert(key,value[key])) 11  } 12             convert(key, val){ //這里的 key value 是Vue實例data的每個鍵值對
13               defineReactive(this.value, key, val)//this.value 就是Vue實例的data
14  } 15  } 16 
17 
18 
19 
20           function defineReactive (obj, key, val) {//類似_proxy方法,循環增加set和get方法,只不過增加了Dep對象和遞歸的方法
var dep = new Dep()
21 var childOb = observe(val) 22 //這里的val已經是第一次傳入的對象所包含的屬性或者對象,會在observe進行篩選,決定是否繼續遞歸 23 Object.defineProperty(obj, key, {//這個defineProperty方法,作用對象是每次遞歸傳入的對象,會在Observer對象中進行分化 24 enumerable: true, 25 configurable: true, 26 get: ()=>{ 27 if(Dep.target){//這里判斷是否開啟監聽模式(調用watch) 28 dep.addSub(Dep.target)//調用了,則增加一個Watcher對象 29 } 30 return val//沒有啟用監聽,返回正常應該返回val 31 }, 32 set:newVal=> {var value = val 33 if (newVal === value) {//新值和舊值相同的話,return 34 return 35 } 36 val = newVal 37 childOb = observe(newVal) 38           //這里增加observe方法的原因是,當我們給屬性賦的值也是對象的時候,同樣要遞歸增加set和get方法 39 dep.notify() 40           //這個方法是告訴watch,你該行動了 41 } 42 }) 43 } 44       function observe (value, vm) {//遞歸控制函數 45        if (!value || typeof value !== 'object') {//這里判斷是否為對象,如果不是對象,說明不需要繼續遞歸 46 return 47 } 48 return new Observer(value)//遞歸 49 }

 

 

 

     這里寫的比較亂(不會寫注釋啊!!),不過沒關系,你只需要知道,Opserver對象是使用defineReactive方法循環給參數value設置set和get方法,同時順便調了observe方法做了一個遞歸判斷,看看是否要從Opserver對象開始再來一遍。就是這樣,至於dep對象,大可不必關心。

    到這里,new Vue所執行的階段就告一段落,仍然留下了一些坑,例如Dep,不過馬上就會展示出來

    因為Dep起到連接的作用,所以在new Watcher之前,有必要讓你們看一下:

    

 1       class Dep {  2  constructor() {  3               this.subs = [] //Watcher隊列數組  4  }  5  addSub(sub){  6                this.subs.push(sub) //增加一個Watcher  7  }  8  notify(){  9                this.subs.forEach(sub=>sub.update()) //觸發Watcher身上的update回調(也就是你傳進來的回調) 10  } 11  } 12       Dep.target = null //增加一個空的target,用來存放Watcher

 

      

    new Watcher

  Dep對象身上的方法和作用,大體在上面的注釋寫的比較清楚,很多涉及到Watcher對象,那么下面我就來介紹一下Watcher對象,在開始的時候,我們已經知道,Vue對象身上的一個方法,$watch,而這個方法做的事情也不是別的,正是new Watcher對象,那么上代碼:

  

 1  //-----WATCHER
 2  class Watcher { // 當使用了$watch 方法之后,不管有沒有監聽,或者觸發監聽,都會執行以下方法  3  constructor(vm, expOrFn, cb) {  4               this.cb = cb //調用$watch時候傳進來的回調  5               this.vm = vm  6               this.expOrFn = expOrFn //這里的expOrFn是你要監聽的屬性或方法也就是$watch方法的第一個參數(為了簡單起見,我們這里不考慮方法,只考慮單個屬性的監聽)  7               this.value = this.get()//調用自己的get方法,並拿到返回值  8  }  9             update(){  // 還記得Dep.notify方法里循環的update么?
10               this.run() 11  } 12  run(){//這個方法並不是實例化Watcher的時候執行的,而是監聽的變量變化的時候才執行的 13               const  value = this.get() 14               if(value !==this.value){ 15                 this.value = value 16                 this.cb.call(this.vm)//觸發你傳進來的回調函數,call的作用,我就不說了
17  } 18  }
19 get(){ 20 Dep.target = this //將Dep身上的target 賦值為Watcher對象 21 const value = this.vm._data[this.expOrFn];//這里拿到你要監聽的值,在變化之前的數值 22 // 聲明value,使用this.vm._data進行賦值,並且觸發_data[a]的get事件 23 Dep.target = null 24 return value 25 } 26 }

      class Watcher在實例化的時候,重點在於get方法,我們來分析一下,get方法首先把Watcher對象賦值給Dep.target,隨后又有一個賦值,

  const value = this.vm._data[this.exOrFn], 這個賦值的過程,是一個關鍵點,要知道,我們之前所做的都是什么,不就是修改了Vue對象的data(_data)的所有屬性的get和set事件么? 而Vue對象也作為第一個參數,傳給了Watcher對象,此時此刻,這個this.vm._data里的所有屬性,在取值的時候,都會觸發之前增加的get方法,此時,我們再來看一下get方法是什么?

 1       get: ()=>{  2                 if(Dep.target){ //觸發這個get事件之前,我們剛剛對Dep.target賦值為Watcher對象  3  dep.addSub(Dep.target)//這里會把我們剛賦值的Dep.target(也就是Watcher對象)添加到監聽隊列里  4  }  5                 return val  6  } 7           }

      在吧Watcher對象放再Dep.subs數組中之后,new Watcher對象所執行的任務就告一段落,此時我們有:

      Dep.subs數組中,已經添加了一個Watcher對象,

      Dep對象身上有notify方法,來觸發subs隊列中的Watcher的update方法,

      Watcher對象身上有update方法可以調用run方法觸發最終我們傳進去的回調,

     可是你會覺得,雖然方法都這么齊全,所謂萬事具備只欠東風,那么如何觸發Dep.notify方法,來層層回調,找到Watcher的run呢?

     答案就在set方法中的最后一行  

 1               set:newVal=> {                 
2          var value = val 3 if (newVal === value) { 4 return 5 } 6 val = newVal 7 childOb = observe(newVal) 8 dep.notify()//觸發Dep.subs中所有Watcher.update方法
9          
}

      別問我set方法怎么觸發,當然是你修改了你所監聽的那個值的時候啦,

     到這里,就是我對簡易版的$watch方法的理解,不過終歸是簡易版,除了回味代碼的思路,更感慨尤大的水平之高,在寫博客的時候,也沒有太好的思路,不知道怎么寫更容易懂(反正也沒人看),可能你看了這篇博客之后,仍然對$watch的實現還有問題,當然歡迎指正和提問,雖然是Vue1的實現方法,但是思路是不會過時的,另外推薦,楊川寶大大的文章和GitHub

   文章地址 https://segmentfault.com/a/1190000004384515

   GitHub  https://github.com/georgebbbb...

     

    以上

  2017-04-23    擊鼓賣糖


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM