Vue響應式原理

 

在這裏插入圖片描述

vue實現數據響應式,是經過數據劫持偵測數據變化,發佈訂閱模式進行依賴收集與視圖更新,換句話說是Observe,Watcher以及Compile三者相互配合。javascript

  • Observe實現數據劫持,遞歸給對象屬性,綁定setter和getter函數,屬性改變時,通知訂閱者
  • Compile解析模板,把模板中變量換成數據,綁定更新函數,添加訂閱者,收到通知就執行更新函數
  • Watcher做爲Observe和Compile中間的橋樑,訂閱Observe屬性變化的消息,觸發Compile更新函數

數據劫持/代理 Observer

實現響應式的第一步就是能偵測數r據的變化,在Vue2.x是經過ES5的方法Object.defineProperty()實現對象屬性的偵聽,在Vue3.x中使用了ES6提供的Proxy對對象進行代理。html

Object.definePropertyvue

function observe(obj) {
  if (!obj || typeof obj !== "object") {
    return;
  }
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key]);
  });
  function defineReactive(obj, key, value) {
    //遞歸子屬性
    observe(value);
    //訂閱器
    const dp = new Dep();
    Object.defineProperty(obj, key, {
      configurable: true, //可刪除
      enumerable: true, //可枚舉遍歷
      get: function () {
        /* 將Dep.target(即當前的Watcher對象存入dep的subs中) */
        dp.addSub(Dep.target);
        return value;
      },
      set: function (newValue) {
        //遞歸新的子屬性
        observe(newValue);
        if (value !== newValue) {
          value = newValue;
          /* 在set的時候觸發dep的notify來通知全部的Watcher對象更新視圖 */
          dp.notify();
        }
      },
    });
  }
}

Proxy實現代理java

let target = { name: " xiao" };

let handler = {
  get(target, key) {
    if (typeof target[key] === "object" && target[key] !== "null") {
      return new Proxy(target[key], handler);
    }
    return target[key];
  },
  set: function (target, key, value) {
    target[key] = value;
  },
};

target = new Proxy(target, handler);

依賴收集Dep

//Dep訂閱者,依賴收集器
class Dep {
  constructor() {
    /* 用來存放Watcher對象的數組 */
    this.subs = [];
  }
  /* 在subs中添加一個Watcher對象 */
  addSub(sub) {
    this.subs.push(sub);
  }
  /* 在subs中添加一個Watcher對象 */
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}
//用 addSub 方法能夠在目前的 Dep 對象中增長一個 Watcher 的訂閱操做;
//用 notify 方法通知目前 Dep 對象的 subs 中的全部 Watcher 對象觸發更新操做。

Watcher訂閱者

class Watcher {
  constructor(obj, key, cb) {
    /* 在new一個Watcher對象時將該對象賦值給Dep.target,在observe get中會用到 */
    Dep.target = this;
    this.obj = obj;
    this.key = key;
    this.cb = cb;
    //觸發getter,依賴收集
    this.value = obj[key];
    //收集完置空Dep.target,防止重複收集
    Dep.target = null;
  }
  update() {
    //得到新值
    this.value = obj[this.key];
    console.log("視圖更新");
  }
}

Compile模板編譯

  • 正則匹配解析vue指令、表達式
  • 把變量替換成數據初始化渲染
  • 建立Watcher訂閱更新函數
//指令處理類
const compileUtile = {
    getVal(expr,vm){
        //reduce用的好啊
        return expr.split('.').reduce((data,curentval)=>{
            return data[curentval];
        },vm.$data)
    },
    html(node,expr,vm){
        new Watcher(vm,expr,(newVal)=>{
            this.updater.htmlUpdate(node,newVal);
        })
        const value = this.getVal(expr,vm);
        this.updater.htmlUpdate(node,value);
    },
  
    //更新函數
    updater:{
        htmlUpdate(node,value){
            node.innerHTML= value;
        },
    }
}
//Compile指令解析器
class Compile{
//各類正則匹配vue指令和表達式,替換數據
}

Object.defineProperty與Proxy的區別?node

  • Proxy能夠直接監聽對象,而非屬性,能夠監聽屬性的增長
  • Proxy能夠監聽數組
  • Proxy有不少Object.defineProperty不具有的攔截方法
  • Proxy返回一個新對象,能夠直接操做新對象達到目的,Object.defineProperty只能遍歷對象屬性修改

爲何要依賴收集?api

數據劫持的目的是在屬性變化的時候觸發視圖更新,依賴收集能夠收集到哪些地方使用到了相關屬性,屬性變化時,就能夠通知到全部的地方去更新視圖,對於沒有使用的屬性,也能夠避免無用的數據比對更新數組

Dep和Watcher的關係(多對多)app

  • data中一個key對應一個Dep實例, 一個Dep實例對應多個Watcher實例(一個屬性在多個表達式中使用)
  • 一個表達式對應一個Watcher實例,一個Watcher對用多個Dep實例(一個表達式中有多個屬性)

watcher和Dep什麼時候建立ide

  • Dep在初始化data的屬性進行數據劫持時建立的
  • Watcher是在初始化時解析大括號表達式/通常指令時建立

如何實現對數組的監聽

由於Object.defineProperty不能監聽數組長度變化,因此Vue使用了函數劫持的方式,重寫了數組的方法,Vue將data中的數組進行了原型鏈重寫,指向了本身定義的數組原型方法。這樣當調用數組api時,能夠通知依賴更新。若是數組中包含着引用類型,會對數組中的引用類型再次遞歸遍歷進行監控。這樣就實現了監測數組變化。函數

1 // src/core/observer/array.js
 2 
 3 // 獲取數組的原型Array.prototype,上面有咱們經常使用的數組方法
 4 const arrayProto = Array.prototype
 5 // 建立一個空對象arrayMethods,並將arrayMethods的原型指向Array.prototype
 6 export const arrayMethods = Object.create(arrayProto)
 7 
 8 // 列出須要重寫的數組方法名
 9 const methodsToPatch = [
10   'push',
11   'pop',
12   'shift',
13   'unshift',
14   'splice',
15   'sort',
16   'reverse'
17 ]
18 // 遍歷上述數組方法名,依次將上述重寫後的數組方法添加到arrayMethods對象上
19 methodsToPatch.forEach(function (method) {
20   // 保存一份當前的方法名對應的數組原始方法
21   const original = arrayProto[method]
22   // 將重寫後的方法定義到arrayMethods對象上,function mutator() {}就是重寫後的方法
23   def(arrayMethods, method, function mutator (...args) {
24     // 調用數組原始方法,並傳入參數args,並將執行結果賦給result
25     const result = original.apply(this, args)
26     // 當數組調用重寫後的方法時,this指向該數組,當該數組爲響應式時,就能夠獲取到其__ob__屬性
27     const ob = this.__ob__
28     let inserted
29     switch (method) {
30       case 'push':
31       case 'unshift':
32         inserted = args
33         break
34       case 'splice':
35         inserted = args.slice(2)
36         break
37     }
38     if (inserted) ob.observeArray(inserted)
39     // 將當前數組的變動通知給其訂閱者
40     ob.dep.notify()
41     // 最後返回執行結果result
42     return result
43   })
44 })

def就是經過Object.defineProperty重寫value,也就是自定義的幾個數組方法

function def(obj,key,val,enumble){
Object.defineProperty(obj,key,{
 enumble:!!enumble,
 configrable:true,
 writeble:true,
 val:val
 
})
}

observe方法裏面加入數組的處理,

  • 能獲取到__proto__屬性,就把__protp__屬性指向重寫的方法
  • 獲取不到__proto__屬性,就把重寫的方法定義到對象上實例上
// src/core/observer/index.js
export class Observer {
  ...
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  ...
}
function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}


在這裏插入圖片描述

相關文章
相關標籤/搜索