vue的響應式原理

發佈訂閱模式

vue響應式原理的核心之一就是發佈訂閱模式。它定義的是一種依賴關係,當一個狀態發生改變的時候,全部依賴這個狀態的對象都會獲得通知。vue

比較典型的就是買東西,好比A想買一個小紅花,可是它缺貨了,因而A就留下聯繫方式,等有貨了商家就經過A的聯繫方式通知他。後來又來了B、C、D...,他們也想買小紅花,因而他們都留下了聯繫方式,商家把他們的聯繫方式都存到小紅花的通知列表,等小紅花有貨了,一併通知這些人。數組

在上面這個例子中,能夠抽象出來發佈訂閱的兩個類:緩存

  • Dep類:商家。Dep類有一個數組(小紅花的通知列表),來存放訂閱信息;還有兩個操做:添加訂閱者信息、通知訂閱者。
  • Watcher類: A、B、C、D每一個人都是一個Watcher類。Watcher類提供回調函數,也就是收到通知的要作什麼。
class Dep {
    constructor(){
        this.subs = []   //存放訂閱者信息
    }

    addSub(watcher){    //添加訂閱者
        this.subs.push(watcher) 
    }

    notify(){           //通知全部訂閱者
        this.subs.forEach((sub) => {
            sub.update()
        })
    }
}

class Watcher{
    constructor(cb){
        this.cb = cb  //訂閱者在收到通知要執行的操做
    }

    update(){
        this.cb && this.cb()
    }
}

const a = new Watcher(()=>{
    console.log('A收到,小紅花到貨了')
})
const b = new Watcher(()=>{
    console.log('B收到,小紅花到貨了')
})

const dep = new Dep()

dep.addSub(a)
dep.addSub(b)

dep.notify()複製代碼

數據劫持

在vue中,響應式數據能夠類比成上面例子中的小紅花,經過發佈訂閱的模式來監聽數據狀態的變化,通知視圖進行更新。那麼,是在什麼時候進行訂閱,什麼時候進行發佈,這就要用到數據劫持。app

vue使用Object.defineProperty()進行數據劫持。ide

let msg = "hello"
const data = {};
Object.defineProperty(data, 'msg', {
    enumerable: true,
    configurable: true,
    get() {  //讀取data.msg時會執行get函數
        console.log('get msg')
        return msg;
    },
    set(newVal) {  //爲data.msg賦值時會執行set函數
        console.log('set msg')
        msg = newVal;
    }
});
data.msg   //'get msg'
data.msg = 'hi'  //'set msg'複製代碼

經過Object.defineProperty定義的屬性,在取值和賦值的時候,咱們均可以在它的get、set方法中添加自定義邏輯。當data.msg的值更新時,每個取值data.msg的地方也須要更新,可視爲此處要訂閱data.msg,所以 在get方法中添加watcher。data.msg從新賦值時,要通知全部watcher進行相應的更新,所以 在set方法中notify全部watcher。函數

在vue中,定義在data中的數據都是響應式的,由於vue對data中的全部屬性進行了數據劫持。oop

function initData (vm) {
  var data = vm.$options.data;
  observe(data, true); 
}

function observe (value, asRootData) {
  var ob = new Observer(value);
  return ob
}

//Observer的做用就是對數據進行劫持,將數據定義成響應式的
var Observer = function Observer (value) {
  if (Array.isArray(value)) { //當數據是數組,數組劫持的方式與對象不一樣
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
  //當數據是對象,遞歸對象,將對象的每一層屬性都使用Object.defineProperty劫持,如 {a: {b: {c: 1}}}
    this.walk(value); 
  }
};複製代碼

使用vue時,data中常常會有數組,和對象不一樣,它的數據劫持不能經過Object.defineProperty來實現,下面咱們分別來簡單實現一下。測試

對象

對象的數據劫持,首先遍歷對象的全部屬性,對每個屬性使用Object.defineProperty劫持,當屬性的值也是對象時,遞歸。優化

function observeObject(obj){
    //遞歸終止條件
    if(!obj || typeof obj !== 'object') return
	
    Object.keys(obj).forEach((key) => {
        let value = obj[key]
       
        //遞歸對obj屬性的值進行數據劫持
        observeObject(value) 
        
        let dep = new Dep()  //每一個屬性都有一個依賴數組
        Object.defineProperty(obj,key,{
            enumerable: true,
            configurable: true,
            get(){
                dep.addSub(watcher) //僞代碼, 添加watcher
                return value
            },
            set(newVal){
                value = newVal
                
                //obj屬性從新賦值後,對新賦的值也進行數據劫持,由於新賦的值可能也是一個對象
                / **
                    let a = {
                    	b: 1
                    }
                    a.b = {c: 1}
                **/
                observeObject(value) 
                
                dep.notify()  //僞代碼, 通知全部watcher進行更新
            }
        })
    })
}複製代碼

數組

數組狀態的變化主要有兩種: 一是數組的項的變化,二是數組長度的變化。所以數組的數據劫持也是考慮這兩方面。this

  • 數組項的劫持:
function observeArr(arr){
    for(let i=0; i<arr.length; i++){   
        observe(arr[i])  //僞代碼,對每一項進行劫持
    }
}複製代碼

vue對於數組項是簡單數據類型的狀況沒有劫持,這也致使了vue數組使用的一個問題,當數組項是簡單數據類型時,修改數據項時視圖並不會更新。

<div><span v-for="item in arr">{{item}}</span></div>
<button @click="changeArr">change array</button>    <!--點擊按鈕視圖不會更新成523-->複製代碼
data:{
   arr: [1,2,3]
},
methods:{
  changeArr(){
     this.arr[0] = 5 
  }
}複製代碼
  • 數組長度變化的劫持是經過重寫7個能夠改變原數組長度的方法(push, pop, shift, unshift, splice, sort, reverse)實現的。
let arrayProto = Array.prototype;
let arrayMethods = Object.create(arrayProto); //arrayMethods繼承自Array.prototype
let methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];

methodsToPatch.forEach((method) => { //重寫這7個方法
    arrayMethods[method] = function(...args) { 
        let result = arrayProto[method].apply(this,args) //調用原有的數組方法
        
        let inserted;
        switch (method) {
            case 'push':
            case 'unshift':
              inserted = args;
              break
            case 'splice':
              inserted = args.slice(2);
              break
        }
        if (inserted) { //push、unshift、splice可能加入新的數組元素,這裏也要對新元素進行劫持
            observeArray(inserted); 
        }
        
        dep.notify(); //僞代碼, 通知全部watcher進行更新
        return result
    }
})

arr.__proto__ = arrayMethods  //arr是須要進行劫持的數組,修改它原有的原型鏈方法。複製代碼
實現一個簡單的雙向數據綁定
  • 第一步,初始化。
class Vue {
    constructor(options){
        this.$data = options.data
        this.$getterFn = options.getterFn
        observe(this.$data)  // 將定義在options.data中的數據做響應式處理
        
        //options.getterFn是一個取值函數,模擬頁面渲染時要作的取值操做
        
        new Watcher(this.$data, this.$getterFn.bind(this), key => {
            console.log(key + "已修改,視圖刷新")
        })
    }
}複製代碼
  • 第二步,實現observe方法。主要就是用到上面的發佈訂閱模式和數據劫持。
function observe(data){
    if(!data || typeof data !== 'object') return
    let ob;
    //爲數據建立observer時,會將observer添加到數據屬性,若是數據已經有observer,會直接返回該observer
    if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) { 
        ob = data.__ob__;
    }else{
        ob = new Observer(data)
    }
    return ob
}


class Observer {
    constructor(data){
        this.dep = new Dep()   //將dep掛載到observer上,用於處理data是數組的狀況
        Object.defineProperty(data, '__ob__', {  //將observer掛載到要data上,方便經過data訪問dep屬性和walk、observeArray方法
            enumerable: false,
            configurable: false,
            value: this
        })
        if(Array.isArray(data)){  //若是是數組,重寫數組的7個方法,對數組的每一項做響應式處理
            data.__proto__ = arrayMethods  
            this.observeArray(data)
        }else{
            this.walk(data)
        }
    }

    walk(data){
        let keys = Object.keys(data)
        keys.forEach((key) => {
            defineReactive(data, key)
        })
    }

    observeArray(data){
        data.forEach((val) => {
            observe(val)
        })
    }
}

//重寫數組的7個方法
let arrayProto = Array.prototype;
let arrayMethods = Object.create(arrayProto); //arrayMethods繼承自Array.prototype
let methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
methodsToPatch.forEach((method) => { 
    arrayMethods[method] = function(...args) { //將一個不定數量的參數表示爲一個數組
       let result = arrayProto[method].apply(this,args) //調用原有的數組方法        
        let inserted;
        switch (method) {
            case 'push':
            case 'unshift':
              inserted = args;
              break
            case 'splice':
              inserted = args.slice(2);
              break
        }
        if (inserted) { //push、unshift、splice可能加入新的數組元素,這裏也要對新元素進行劫持
            this.__ob__.observeArray(inserted); 
        }
        this.__ob__.dep.notify('array')  //觸發這個數組dep的notify方法
        return result
    }
})


function defineReactive(data,key){
    let dep = new Dep()  //每一個屬性對應一個dep,來管理訂閱
    let value = data[key]
    
    //當value是數組時,不會爲數組的每一個屬性添加dep,而是爲整個數組添加一個dep。
    //當數組執行上面那7個方法時,就觸發這個dep的notify方法  this.__ob__.dep.notify('array')
    let childOb = observe(value) 
    
    Object.defineProperty(data,key,{
        enumerable: true,
        configurable: true,
        get(){
        
            //添加訂閱者。Dep.target是一個全局對象。它指向當前的watcher
            Dep.target && dep.addSub(Dep.target)
            
            if(Array.isArray(value)) {
                Dep.target && childOb.dep.addSub(Dep.target)
            }
            
            return value
        },
        set(newVal){
            if(newVal === value) return
            value = newVal
            observe(value)
            dep.notify(key)
        }
    })
}複製代碼

什麼時候觸發watcher仍是明顯的。添加watcher就有點不太明顯了。這裏對watcher的構造函數做了一些修改。

Dep.target = null
class Watcher{
    constructor(data,getterFn,cb){
        this.cb = cb
		
        Dep.target = this
        getterFn()
        Dep.target = null
    }

    update(key){
        this.cb && this.cb(key)
    }
}複製代碼

關鍵就是:

Dep.target = this
getterFn()
Dep.target = null複製代碼

在new Watcher()時,就會執行這三行代碼。Dep.target = this將當前建立的watcher賦值給Dep.target這個全局變量,執行getterFn()時,會對取vm.$data中的值,上面已經將vm.$data做了響應式處理,因此取它值的時候就會執行各屬性的get方法。

get(){ 
   //此時Dep.target指向當前的watcher,此時就將當前watcher添加到這個屬性對應的訂閱數組裏。
   Dep.target && dep.addSub(Dep.target)
            
   if(Array.isArray(value)) {
       Dep.target && childOb.dep.addSub(Dep.target)  //若是屬性對應的值是數組,就將當前watcher添加到該數組對應的訂閱數組裏。
   }
            
   return value
},複製代碼

這樣就完成了對須要訪問的屬性添加watcher的操做,而後將Dep.target還原成null。

測試代碼:(渲染視圖也是對data裏的屬性取值,如{{msg.m}},添加watcher,完成訂閱。這裏咱們就簡單訪問取值來進行模擬)

let vm = new Vue({
   el: '#root',
   data:{
       msg: {
           m: "hello world"
       },
       arr: [
          {a: 1},
          {a: 2}
       ]
   },
   getterFn(){
       console.log(this.$data.msg.m)
       this.$data.arr.forEach((item) => {
           console.log(item.a)
       })
   }
})複製代碼

效果:能夠看到,getterFn訪問過的數據,在修改值時就會觸發watcher的回調函數。

vue的幾種watcher

vue裏面主要有三種watcher:

  • 渲染watcher: 當渲染用到的data數據變化時,從新渲染頁面
  • computed watcher: 當data數據變化時,更新computed的值
  • user watcher: 當要watch的數據變化時,執行watch定義的回調函數

渲染watcher

渲染watcher是在vm.$mount()方法執行時建立的。

Vue.prototype.$mount = function () {
  var updateComponent = function () {
      vm._update(vm._render(), hydrating);
  };
  //updateComponent就是進行視圖渲染的函數,對data中數據的取值的操做就是在該函數中完成
  new Watcher(vm, updateComponent, noop, options,true);
};複製代碼

Watcher的構造函數:

var Watcher = function Watcher (vm,expOrFn,cb,options,isRenderWatcher) {
  this.vm = vm;
  
  if (options) {
    ...
    this.lazy = !!options.lazy;  //主要用於computed watcher
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  
  this.cb = cb;
 
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;   //expOrFn對應上面的updateComponent方法
  } else {
    this.getter = parsePath(expOrFn);
  }
  
  //若是this.lazy爲false,就當即執行this.get()
  //因此在建立watcher的時候就會執行updateComponent方法
  this.value = this.lazy? undefined: this.get();  
};

Watcher.prototype.get = function get () {
  pushTarget(this);   //類比上面簡易版的Dep.target = this
  var value;
  var vm = this.vm;
  
  value = this.getter.call(vm, vm);  //執行取值函數,完成watcher訂閱
  
  popTarget();  //類比上面簡易版的Dep.target = null
 
  return value
};複製代碼

在渲染watcher建立的時候,就當即執行取值函數,完成響應式數據的依賴收集。能夠看出,定義在data中的數據,它們的watcher都是同一個,就是在vm.$mount()方法執行時建立的watcher。watcher的update方法:

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);   //渲染watcher會走這裏的邏輯,其實最終都會執行this.run(),只是這裏用隊列進行優化
  }
};

Watcher.prototype.run = function run () {
	var value = this.get();  //又會執行updateComponent方法
}複製代碼

定義在data中的數據,它們的watcher都是同一個,當data每一次數據中數據更新時,都會執行watcher.update()。渲染watcher的update()最終會執行updateComponent方法,若是一次性修改N個data屬性時,好比下面例子中的change,理論上會執行N次updateComponent(),很明顯,這是不科學的。

做爲優化,維護一個watcher隊列,每次執行watcher.update()就嘗試往隊列裏面添加watcher(queueWatcher(this)),若是當前watcher已經存在於隊列中,就再也不添加。最後在nextTick中一次性執行這些watcher的run方法。

這樣,若是一次性修改N個data屬性時,實際上只會執行一次updateComponent()

data:{
    msg: "hello",
    msg2: "ni hao"
}, 
methods:{
    change(){
        this.msg = "hi"
        this.msg2 = "hi"
  }
},複製代碼

computed watcher

data:{
    msg: "hello"
},
computed: {
    newMsg(){
        return this.msg + ' computed'
    }
},複製代碼
<div>{{newMsg}}</div>複製代碼

當msg更新時,newMsg也會更新。由於computed會對訪問到的data數據(這裏是msg)進行訂閱。

function initComputed (vm, computed) {
  var watchers = vm._computedWatchers = Object.create(null);

  for (var key in computed) {
    var userDef = computed[key];
    var getter = typeof userDef === 'function' ? userDef : userDef.get;
    
    watchers[key] = new Watcher(   //watcher的取值函數就是咱們在computed中定義的函數
      vm,
      getter || noop,
      noop,
      computedWatcherOptions     // { lazy: true }
   );
   
   if (!(key in vm)) {
      defineComputed(vm, key, userDef);
   }
  } 
}複製代碼

在initComputed的時候,建立了watcher,它有個屬性lazy: ture。在watcher的constructor中,lazy: ture表示建立watcher的時候不會執行取值函數,因此,此時watcher並無加入msg的訂閱數組。

this.value = this.lazy? undefined: this.get();  
複製代碼

只有在頁面對computed進行取值{{newMsg}}的時候,watcher纔會加入msg的訂閱數組。這裏主要來看看defineComputed方法,它的大體邏輯以下:

function defineComputed (target,key,userDef) {  // target:vm, key: newMsg
 
 Object.defineProperty(target, key, {
      enumerable: true,
      configurable: true,
      get: function computedGetter () {  //當視圖對newMsg進行取值的時候會執行這裏
        var watcher = this._computedWatchers && this._computedWatchers[key];
        if (watcher) {
          if (watcher.dirty) {   //這裏要對照Watcher的構造函數來看,默認watcher.dirty = watcher.lazy,首次執行爲true
            watcher.evaluate();  //會執行watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend();
          }
          return watcher.value
        }
      },
      set: userDef.set || noop
  });
}

Watcher.prototype.evaluate = function evaluate () {
  this.value = this.get();    //執行watcher的取值函數,返回取值函數執行的結果,並將watcher添加到msg的訂閱數組
  this.dirty = false;  //this.dirty置爲false,用於緩存。
};複製代碼

computed watcher有個屬性dirty,用於標記是否執行取值函數。

一、初始化watcher時,watcher.dirty = watcher.lazy,值爲true。頁面第一次訪問newMsg時就會執行watcher.evaluate()

二、取值完成後,watcher.dirty = false。下一次頁面再取值就會直接返回以前計算獲得的值 watcher.value 。

三、若是watcher訂閱的 msg 發生變化,就會通知執行watcher的 watcher.update()。lazy屬性爲true的watcher執行update方法是watcher.dirty = true,這樣頁面取值newMsg就會從新執行取值函數,返回新的值。這樣就實現了computed的緩存功能。

Watcher.prototype.update = function update () {
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};複製代碼

user watcher

watch:{
   msg(newValue,oldValue){
      console.log(newValue,oldValue)
   }       
},複製代碼

或者這樣:

mounted(){
   this.$watch('msg',function(newValue,oldValue){
       console.log(newValue,oldValue)
   })
}複製代碼

user watcher的核心方法就是vm.$watch:

Vue.prototype.$watch = function (expOrFn,cb,options) {

    //核心就是這裏
    //expOrFn  ---> msg
    //cb  ---> 用戶本身定義的回調函數,function(oldValue,newValue){console.log(oldValue,newValue)}
    
    var watcher = new Watcher(vm, expOrFn, cb, options);
  };
}複製代碼

和渲染watcher、 computed watcher的expOrFn不一樣,user watcher 的expOrFn是個表達式。

//watcher的構造函數中
if (typeof expOrFn === 'function') {
  this.getter = expOrFn;
} else {
  this.getter = parsePath(expOrFn);
}複製代碼

建立user watcher時,會根據這個表達式完成取值操做,添加watcher到訂閱數組。

expOrFn: 'msg'   -----> vm.msg
expOrFn: 'obj.a'  -----> vm.obj ----->vm.obj.a複製代碼

deep:true時,會遞歸遍歷當前屬性對應的值,將watcher添加到全部屬性上,每一次修改某一個屬性都會執行watcher.update()。

Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  value = this.getter.call(vm, vm);
  
  if (this.deep) {
     traverse(value);  //遞歸遍歷取值,每次取值都添加該watcher到取值屬性的訂閱數組。
  }
  popTarget();
  return value
};複製代碼
相關文章
相關標籤/搜索