1. 程式人生 > >vue中$watch源碼閱讀筆記

vue中$watch源碼閱讀筆記

vue 告訴 應該 最好 notify type 十分 msg 建立

項目中使用了vue,一直在比較computed和$watch的使用場景,今天周末抽時間看了下vue中$watch的源碼部分,也查閱了一些別人的文章,暫時把自己的筆記記錄於此,供以後查閱:

實現一個簡單的$watch:

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

這個過程大概分為三部分:實例化Vue、調用$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 }
View Code

註意這裏的Object.defineProperty ( obj, key , option) 方法

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

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

constructor :Vue實例的構造函數,傳入參數(options)的時候,constructor 就會被調用,讓Vue對象和參數data產生關聯,讓我們可以通過this.a 或者vm.a來訪問data屬性,建立關聯之後,循環data的所有鍵名,將其傳入到_proxy方法

$watch:實例化Watcher對象

_proxy:這個方法是一個代理方法,接收一個鍵名,作用的對象是Vue對象,

回頭來看Object.defineProperty ( obj, key , option) 這個方法

技術分享
 1 Object.defineProperty(self, key, {
 2         configurable: true,
 3         enumerable: true,
 4         get: function proxyGetter () {
 5           return self._data[key]
 6           // 返回 Vue實例上data的對應屬性值
 7         },
 8         set: function proxySetter (val) {
 9           self._data[key] = val
10         }
11       })
View Code

這個方法的第一個Obj的參數傳入的是self,也就是Vue實例本身,而get方法裏,return出來的卻是self._data[key], _data在上面的方法當中,已經和參數data相等了,所以當我們訪問Vue.a的時候,get方法返回給我們的,是Vue._data.a。

例如:

技術分享
1 var vm = Vue({
2    data:{
3        a:1, 
4        msg:‘我是Vue實例‘      
5      } 
6 })
7 console.log(vm.msg) //打印 ‘我是Vue實例‘
8 // 理論上來說,msg和a,應該是data上的屬性,但是卻可以通過vm.msg直接拿到
View Code

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

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

技術分享
 1 function defineReactive (obj, key, val) {//  類似_proxy方法,循環增加set和get方法,只不過增加了Dep對象和遞歸的方法
 2    var dep = new Dep()
 3     var childOb = observe(val)
 4     //這裏的val已經是第一次傳入的對象所包含的屬性或者對象,會在observe進行篩選,決定是否繼續遞歸
 5     Object.defineProperty(obj, key, {//這個defineProperty方法,作用對象是每次遞歸傳入的對象,會在Observer對象中進行分化
 6    enumerable: true,
 7    configurable: true,
 8    get: ()=>{
 9         if(Dep.target){//這裏判斷是否開啟監聽模式(調用watch)
10           dep.addSub(Dep.target)//調用了,則增加一個Watcher對象
11         }
12         return val//沒有啟用監聽,返回正常應該返回val
13       },
14    set:newVal=> {var value =  val
15         if (newVal === value) {//新值和舊值相同的話,return
16           return
17         }
18         val = newVal
19         childOb = observe(newVal)
20               //這裏增加observe方法的原因是,當我們給屬性賦的值也是對象的時候,同樣要遞歸增加set和get方法
21         dep.notify()
22               //這個方法是告訴watch,你該行動了
23    }
24  })
25 }
26 function observe (value, vm) {//遞歸控制函數
27     if (!value || typeof value !== ‘object‘) {//這裏判斷是否為對象,如果不是對象,說明不需要繼續遞歸
28       return
29     }
30   return new Observer(value)//遞歸
31 }
View Code

Opserver對象是使用defineReactive方法循環給參數value設置set和get方法,同時順便調了observe方法做了一個遞歸判斷,看看是否要從Opserver對象開始再來一遍。

Dep起到連接的作用:

技術分享
 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
View Code

new Watcher:

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

class Watcher在實例化的時候,重點在於get方法,我們來分析一下,get方法首先把Watcher對象賦值給Dep.target,隨後又有一個賦值,const value = this.vm._data[this.exOrFn],之前所做的就是修改了Vue對象的data(_data)的所有屬性的get和set?,而Vue對象也作為第一個參數,傳給了Watcher對象,這個this.vm._data裏的所有屬性,在取值的時候,都會觸發之前defineReactive 方法.

回過頭來再看看get:

技術分享
 1 function defineReactive (obj, key, val) {
 2     /*.......*/
 3     Object.defineProperty(obj, key, {
 4   /*.......*/
 5   get: ()=>{
 6     if(Dep.target){ //觸發這個get事件之前,我們剛剛對Dep.target賦值為Watcher對象
 7       dep.addSub(Dep.target)//這裏會把我們剛賦值的Dep.target(也就是Watcher對象)添加到監聽隊列裏
 8     }
 9     return val
10   },
11   /*.......*/
12  }
13 }
View Code

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

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

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

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

那麽如何觸發Dep.notify方法,來層層回調,找到Watcher的run呢?

技術分享
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 }
View Code

vue中$watch源碼閱讀筆記