vue中$watch源碼閱讀筆記

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

實現一個簡單的$watch:編程

const v = new Vue({
data:{
  a: 1,
  b: {
    c: 3
  }
}
})
// 實例方法$watch,監聽屬性"a"
v.$watch("a",()=>console.log("你修改了a"))
            //當Vue實例上的a變化時$watch的回調
setTimeout(()=>{
v.a = 2
// 設置定時器,修改a
},1000)

這個過程大概分爲三部分:實例化Vue、調用$watch方法、屬性變化,觸發回調數組

1、實例化Vue:面向對象的編程函數

class Vue { //Vue對象
    constructor (options) {
      this.$options=options;
      let data = this._data=this.$options.data;
      Object.keys(data).forEach(key=>this._proxy(key));
      // 拿到data以後,咱們循環data裏的全部屬性,都傳入代理函數中
      observe(data,this);
    }
    $watch(expOrFn, cb, options){  //監聽賦值方法
      new Watcher(this, expOrFn, cb);
      // 傳入的是Vue對象
    }

    _proxy(key) { //代理賦值方法
      // 當未開啓監聽的時候,屬性的賦值使用的是代理賦值的方法
      // 而其主要的做用,是當咱們訪問Vue.a的時候,也就是Vue實例的屬性時,咱們返回的是Vue.data.a的屬性而不是Vue實例上的屬性
      var self = this
      Object.defineProperty(self, key, {
        configurable: true,
        enumerable: true,
        get: function proxyGetter () {
          return self._data[key]
          // 返回 Vue實例上data的對應屬性值
        },
        set: function proxySetter (val) {
          self._data[key] = val
        }
      })
    }
  }

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

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

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

 

constructor :Vue實例的構造函數,傳入參數(options)的時候,constructor 就會被調用,讓Vue對象和參數data產生關聯,讓咱們能夠經過this.a 或者vm.a來訪問data屬性,創建關聯以後,循環data的全部鍵名,將其傳入到_proxy方法code

$watch:實例化Watcher對象server

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

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

Object.defineProperty(self, key, {
        configurable: true,
        enumerable: true,
        get: function proxyGetter () {
          return self._data[key]
          // 返回 Vue實例上data的對應屬性值
        },
        set: function proxySetter (val) {
          self._data[key] = val
        }
      })

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

例如:

var vm = Vue({
   data:{
       a:1, 
       msg:'我是Vue實例'      
     } 
})
console.log(vm.msg) //打印 '我是Vue實例'
// 理論上來講,msg和a,應該是data上的屬性,可是卻能夠經過vm.msg直接拿到

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

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

function defineReactive (obj, key, val) {//  相似_proxy方法,循環增長set和get方法,只不過增長了Dep對象和遞歸的方法
   var dep = new Dep()
    var childOb = observe(val)
    //這裏的val已是第一次傳入的對象所包含的屬性或者對象,會在observe進行篩選,決定是否繼續遞歸
    Object.defineProperty(obj, key, {//這個defineProperty方法,做用對象是每次遞歸傳入的對象,會在Observer對象中進行分化
   enumerable: true,
   configurable: true,
   get: ()=>{
        if(Dep.target){//這裏判斷是否開啓監聽模式(調用watch)
          dep.addSub(Dep.target)//調用了,則增長一個Watcher對象
        }
        return val//沒有啓用監聽,返回正常應該返回val
      },
   set:newVal=> {var value =  val
        if (newVal === value) {//新值和舊值相同的話,return
          return
        }
        val = newVal
        childOb = observe(newVal)
              //這裏增長observe方法的緣由是,當咱們給屬性賦的值也是對象的時候,一樣要遞歸增長set和get方法
        dep.notify()
              //這個方法是告訴watch,你該行動了
   }
 })
}
function observe (value, vm) {//遞歸控制函數
    if (!value || typeof value !== 'object') {//這裏判斷是否爲對象,若是不是對象,說明不須要繼續遞歸
      return
    }
  return new Observer(value)//遞歸
}

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

Dep起到鏈接的做用:

class Dep {
    constructor() {
     this.subs = []  //Watcher隊列數組
    }
    addSub(sub){
      this.subs.push(sub) //增長一個Watcher
    }
   notify(){
      this.subs.forEach(sub=>sub.update()) //觸發Watcher身上的update回調(也就是你傳進來的回調)
    }
}
Dep.target = null //增長一個空的target,用來存放Watcher

new Watcher:

class Watcher { // 當使用了$watch 方法以後,無論有沒有監聽,或者觸發監聽,都會執行如下方法
   constructor(vm, expOrFn, cb) {
     this.cb = cb  //調用$watch時候傳進來的回調
     this.vm = vm
     this.expOrFn = expOrFn //這裏的expOrFn是你要監聽的屬性或方法也就是$watch方法的第一個參數(爲了簡單起見,咱們這裏補考錄方法,只考慮單個屬性的監聽)
     this.value = this.get()//調用本身的get方法,並拿到返回值
   }
   update(){  // 還記得Dep.notify方法裏循環的update麼?
     this.run()
   }
   run(){//這個方法並非實例化Watcher的時候執行的,而是監聽的變量變化的時候才執行的
     const  value = this.get()
     if(value !==this.value){
       this.value = value
       this.cb.call(this.vm)//觸發你穿進來的回調函數,call的做用,我就不說了
     }
   }22             get(){ //向Dep.target 賦值爲 Watcher
     Dep.target = this  //將Dep身上的target 賦值爲Watcher對象
     const value = this.vm._data[this.expOrFn];//這裏拿到你要監聽的值,在變化以前的數值
     // 聲明value,使用this.vm._data進行賦值,而且觸發_data[a]的get事件
     Dep.target = null
     return value
   }
 }

  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:

function defineReactive (obj, key, val) {
    /*.......*/
    Object.defineProperty(obj, key, {
  /*.......*/
  get: ()=>{
    if(Dep.target){ //觸發這個get事件以前,咱們剛剛對Dep.target賦值爲Watcher對象
      dep.addSub(Dep.target)//這裏會把咱們剛賦值的Dep.target(也就是Watcher對象)添加到監聽隊列裏
    }
    return val
  },
  /*.......*/
 }
}

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

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

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

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

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

set:newVal=> {                 
 var value =  val
 if (newVal === value) {
   return
 }
 val = newVal
 childOb = observe(newVal)
 dep.notify()//觸發Dep.subs中全部Watcher.update方法
}

這裏造成了一個迴路,當修改了所監聽的那個值的時候,這個set方法被觸發。

相關文章
相關標籤/搜索