1. 程式人生 > >Vue--$watch()原始碼分析

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...