從Object.defineProperty到proxy掰開揉碎了講vue響應式原理

響應式原理

vue 官網這樣定義 vuejs:漸進式的javascript框架: 如何理解漸進式:若是你有一個現成的服務端應用,你能夠將 vue 做爲應用的一部分嵌入到應用中以帶來更加豐富的業務體驗,若是你想要更多的交互邏輯放在前端實現,vue的核心庫以及生態也能夠知足需求javascript

1. 什麼是響應式

通俗的講響應式就是視圖層與數據層的雙向綁定。響應式有兩個核心要素:視圖改變,更新數據;數據改變,變化視圖。這意味着咱們在開發的時候只須要進行數據管理,不須要頻繁操做dom元素。html

視圖變化更新數據,其實比較容易實現,事件監聽便可實現,好比input標籤監聽 'input' 事件。可是當數據改變的時候,如何更新視圖呢?這就須要知道什麼時候數據發生變化,當知道數據發生變化,咱們就能夠對視圖進行更新。前端

2. 實現:

vue2.0是用 Object.defineProperty() 方法中setter與getter的觀察者模式。vue

vue3.0是基於 Proxy 代理對屬性的攔截,實現雙向綁定。java

3. Object.defineProperty() 與雙向綁定(vue2.x雙向綁定實現)

Object.defineProperty() 會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。 這個函數接受三個參數,一個參數是obj,表示要定義屬性的對象,一個參數是 prop ,是要定義或者更改的屬性名字,另外是 descriptor 描述符,來定義屬性的具體描述。Object.defineProperty(obj, prop, descriptor)react

3.1 對象與屬性

javascript中一共有三種屬性:git

普通屬性: 即常規的數據屬性,這種屬性是用戶來添加修改等,把它設置成什麼樣,就返回出來什麼樣,不會作些額外的事情。 咱們一般使用點或者方括號對普通屬性進行賦值和訪問es6

let obj = {};
obj.x = 1;
obj['y']= 12;
console.log(obj.x);  // 1
console.log(obj.y);  // 12
let proprrty = Object.keys(obj);
console.log(proprrty) // ["x", "y"]
delete obj.x;
console.log(obj.x);  // undefined
複製代碼

內部屬性: 好比數組arr的length屬性,函數的prototype屬性,DOM節點的innerHTML屬性,用戶賦完值之後,取值的時候,不必定按預期,有時候還會額外的作一些事情,也難以改變他們的行爲。 好比說某一數組,它的長度爲10, 當咱們設置它爲11時,它就會增長一個undefined元素,再設置爲9時,就會從後面刪掉兩個元素。(後文數組監聽能夠觀察到此變化) 函數的prototype若是被改變,至關於將其父類改變了,會new不一樣類型的實例。 DOM的innerHTML,咱們賦值時是一個字符串,再取出時,這字符串可能會與原來的不同, 而且在原元素上生成了不同的子節點。github

訪問器屬性: 即經過 Object.defineProperty() 定義的屬性,用戶在賦值或取值都通過預先設定的函數,從而實現內部屬性的那一種特殊效果,。數組

對於普通屬性,對象的屬性能夠修改能夠刪除也能夠枚舉,可是經過 Object.defineProperty() 定義屬性,經過描述符的設置能夠進行更精準的實現對控制對象屬性的控制。

3.2 descriptor屬性描述符

屬性描述符分爲兩種:數據描述符與存取描述符。 數據描述符是一個具備值的屬性,該值能夠是可寫的,也能夠是不可寫的。(value, writable, configurable, enumerable). 存取描述符是由 getter 函數和 setter 函數所描述的屬性。(get, set, configurable, enumerable). 布爾值的鍵configurable、enumerable 和 writable 的默認值都是 false。 屬性值和函數的鍵 value、get 和 set 字段的默認值爲 undefined。

數據描述符

const obj = {};

// x屬性 value不可修改 Writable Enumerable 不可修改
Object.defineProperty(obj, 'x', {
  value: 1,   
  // 能否修改 默認false
  writable: false,  
  // 能否被刪除,以及除 value 和 writable 特性外的其餘特性是否能夠被修改
  configurable: false, 
  // 能否在for...in循環和Object.keys() 中被枚舉
  enumerable: false, 
});

obj.x = 77; 
console.log(obj.x);    // 1
delete obj.x 
let property = Object.keys(obj);
console.log(property) //[]
Object.defineProperty(obj, 'x', {configurable: true}) // throw error

// y屬性 value不可修改 Writable Enumerable 可修改
Object.defineProperty(obj, 'y', {
  value: 1,   
  writable: false,  
  configurable: true, 
  enumerable: false, 
});

obj.y = 12;
console.log(obj.y);    // 1
// configurable true 可修改 writable enumerable
Object.defineProperty(obj, 'y', {writable: true});
obj.y = 12;
console.log(obj.y);    // 12
property = Object.keys(obj);
console.log(property) //[]
Object.defineProperty(obj, 'y', {enumerable: true});
property = Object.keys(obj);
console.log(property) //["y"]
複製代碼
存取描述符

const obj = {};
let val;
Object.defineProperty(obj, 'x', {
  get() { 
    console.log('觸發get')
    return val; 
  },
  set(newValue) {
    console.log('觸發set')
    val = newValue; 
  },
})
obj.x = 12; // 觸發set
console.log(obj.x)  // 觸發get 12 

複製代碼

3.2 經過Object.defineProperty()實現簡單雙向綁定

實現代碼:

<div>
    <input type="text" id="value"/>
    <span id="bindValue"></span>
</div>
複製代碼
// 視圖交互控制
let inputEle = document.getElementById('value');
let spanEle = document.getElementById('bindValue');
const MessageObj = {};
Object.defineProperty(MessageObj, 'msg', {
  set: function (newVal) {
    inputEle.value = newVal;
    spanEle.innerText = newVal
  }
})
// 監聽input輸入 視圖變化 更新數據
inputEle.addEventListener('keyup', function (event) {
  MessageObj.msg = event.target.value
})
複製代碼

實現效果:

3.3 觀察者模式

vue.js 則是採用數據劫持結合觀察者模式(發佈者-訂閱者模式)的方式,經過 Object.defineProperty() 來劫持各個屬性的 setter,getter,在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。 經過上文,咱們已經對Object.defineProperty()有了必定了解,那麼咱們繼續看什麼是觀察者模式。 概念

**觀察者模式(Observer)**一般又被稱爲 發佈-訂閱者模式消息機制,它定義了對象間的一種一對多的依賴關係,只要當一個對象的狀態發生改變時,全部依賴於它的對象都獲得通知並被自動更新,解決了主體對象與觀察者之間功能的耦合,即一個對象狀態改變給其餘對象通知的問題。

上文的定義比較官方,總而言之,訂閱者模式分爲註冊和發佈兩個環節,咱們能夠用生活中的場景更加通俗的理解。

故事性的通俗理解

假設咱們去銀行辦業務,由於辦業務的人比較多,咱們會被要求在取號機上去一個號碼,這就是註冊環節。銀行職員與辦理業務的人員就是一對多的依賴關係,銀行職員對號牌任務一個一個進行處理,沒處理完一個任務,都須要通知全部依賴對象(辦業務人員)當前狀態的改變,銀行辦公大廳的led顯示牌會時刻顯示當前正在辦業務的號碼以及下一位將要辦理業務的號碼,銀行處理業務的職員就至關於發佈者,led顯示屏維護了一個觀察者列表,時刻關注led顯示屏的咱們就是訂閱者。當輪到咱們的號牌辦理業務時,咱們就會被通知到指定櫃檯辦理業務,這就是發佈環節。

簡單的觀察者模型
function observer () {
  this.dep = [];

  this.register = (fn) => {
    this.dep.push(fn);
  }

  this.notify = () => {
    this.dep.forEach(item => item())
  }
}

let bankObserver = new observer();
bankObserver.register( () => {console.log("取現金")} );
bankObserver.register( () => {console.log("辦卡")} );

bankObserver.notify();
複製代碼

經過上面例子咱們能夠提煉出觀察者模式的三個要素:發佈者、訂閱者、緩存列表。簡而言之觀察者模式就是一個對象(發佈者)維護一個依賴他的對象(緩存列表)列表,當自身狀態發生變化時,自動通知全部訂閱者。當某個對象不須要得到通知時,能夠從對象列表中刪除掉。

javascript中的觀察模式

JavaScript是一個事件驅動型語言,觀察模式的實現主要依靠事件模型,例如經常使用的一些onclick、 attachEvent 、addEventListener 都是觀察者模式的具體應用。

// html
<span id="span-click">點擊</span>
// javascript
let spanEle = document.getElementById('span-click');
spanEle.addEventListener('click',function(){
    alert('我是一個觀察者,你一點擊,我就知道了');
});
複製代碼

3.4 觀察者模式與Object.defineProperty()結合的雙向綁定實現

雙向綁定過程:
  1. 組件初始化時,遍歷 data 的每一個屬性,並使用 Object.defineProperty() 給每一個屬性都註冊 setter 和 getter ,也就是 reactice 化。
  2. 每一個組件實例都對應一個 watcher 實例,它會在組件渲染的過程當中把「接觸」過的數據屬性記錄爲依賴。以後當依賴項的 setter 觸發時,會通知 watcher,從而使它關聯的組件從新渲染。
什麼是依賴?

依賴就是一個個的大括號表達式和指令表達式

// v-model="value" 是一種依賴 v-指令(非事件) 都是依賴
<input type="text" id="value" v-model="value"/> 
// {{bindValue}} 也是依賴 大括號表達式都是一個依賴
<span>{{bindValue}}</span>  
複製代碼

很顯然,每個依賴都對應着data中的數據,所以依賴能夠簡單理解爲視圖層對數據層的關係:視圖層顯示依賴於數據層的數據-依賴關係。依賴收集的目的就是爲了應對未來數據的變化,從而當依賴的數據發生變化時可以準確的批量的更新依賴所在地的顯示

數據流程圖

代碼實現以下:

// 定義觀察者 一個數據監聽器 若是要對全部屬性都進行監聽的話,那麼能夠經過遞歸方法遍歷全部屬性值,並對其進行Object.defineProperty () 處理
  function observe(data){
    if (!data || typeof data !== 'object') {
      return;
    }
    Object.keys(data).forEach((key)=>{
      defineReactive(data, key, data[key])
    })
  }
  function defineReactive (obj, key, value){
      let dep = new Dep();
      observe(value);
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        set: (newValue) => {
            console.log('值被設置')
          if( newValue !== value ){
              value = newValue;
              console.log(dep,'dep')
              //更新視圖
              dep.notify();
            }
        },
        get: () => {
            console.log('值被獲取',Dep)
          if(Dep.target) {
              dep.addSub(Dep.target);
          }
          return value;
        },
      })
  }
  // 依賴收集 訂閱者列表
  // 主要負責收集訂閱者,而後再屬性變化的時候執行對應訂閱者的更新函數
  function Dep () {
      this.subs = [];
  }
  Dep.prototype = {
      addSub: function(sub) {
          this.subs.push(sub);
      },
      notify: function() {
          console.log(this.subs,'this.subs')
          this.subs.forEach(function(sub) {
              sub.update();
          });
      }
  };
  // 訂閱者
  function Watcher(data, key,cb) {
      this.cb = cb;
      this.data = data;
      this.key = key;
      // 此處爲了觸發屬性的getter,從而在dep添加本身,結合Observer更易理解
      this.value = this.get(); 
  }
  Watcher.prototype = {
      update: function() {
          this.run(); // 屬性值變化收到通知
      },
      run: function() {
          var value = this.get(); // 取到最新值
          console.log(value,'取到最新值')
          var oldVal = this.value;
          if (value !== oldVal) {
              this.value = value;
              this.cb(); // 執行Compile中綁定的回調,更新視圖
          }
      },
      get: function() {
          Dep.target = this;  // 將當前訂閱者指向本身
          var value = this.data[this.key];   // 觸發getter,添加本身到屬性訂閱器中
          Dep.target = null;  // 添加完畢,重置
          return value;
      }
  };
  function SelfVue (data, key, cb) {
      this.data = data;
      observe(data);
      cb(); // 初始化模板數據的值
      new Watcher(this.data, key,cb);
      return this;
  }
複製代碼
<script src="./vue/vue.js"></script>
<div id="box">
  <span id="app"></span> 
  <span id="add" style="margin-left: 10px;display: inline-block;width: 10px;height: 10px;cursor: pointer;">+</span>
</div>
<script> let ele = document.getElementById('app'); let btn = document.getElementById('add'); let o = { number: 1, } var selfVue = new SelfVue(o , 'number', ()=>{ ele.innerHTML = o.number} ); btn.addEventListener('click', () => { o.number += 1; },false) </script>
複製代碼

效果實現以下:

4. Proxy 與雙向綁定(vue3.0雙向綁定實現)

defineProperty()與觀察者模式是vue2.0實現雙向綁定的方式,而vue3.0使用es6的新語法中的代理內建工具Proxy和發射工具reflect來實現,接下來咱們看下如何用Proxy實現一樣的功能。

4.1 認識Proxy

proxy的概念

Proxy 對象用於定義基本操做的自定義行爲(如屬性查找、賦值、枚舉、函數調用等)。

proxy英文原意是代理的意思,在ES6中,能夠翻譯爲"代理器"。 它主要用於改變某些操做的默認行爲,proxy在目標對象的外層搭建了一層攔截,外界對目標對象的某些操做,必須經過這層攔截。 此方法接受兩個參數target — 要使用 Proxy 包裝(處理)的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。handler — 一般以函數做爲屬性的對象,各屬性中的函數分別定義了在執行各類操做時代理 p 的行爲。

基本用法

數據攔截:支持13 種的攔截,相對Object.defineProperty更加豐富。 示例代碼以下: (1) Object數據攔截

let target = {};
let handler = {
  // 攔截對象屬性的讀取,好比proxy.a和proxy['a']
  get (target, key) {
    console.info(`Get on property "${key}"`);
    return target[key];
  },
  // 攔截對象屬性的設置,好比proxy.a = 1 和proxy['a'] = 1
  set (target, key, value) {
    console.info(`Set on property "${key}" new value "${value}"`);
    target[key] = value;
  },
  // 攔截propKey in proxy 返回布爾值
  has (target, key) {
    console.log(`is "${key}" in object ?`);
    // 隱藏 某些屬性
    if( key === 'a'){
      return false;
    }else{
      return key in target;
    }
    // return key in target;
  },
  // delete 操做符的捕捉器 刪除前攔截 進行處理 返回布爾值
  deleteProperty(target, key){
    console.log(`delete key: "${key}"`);
    if(key === 'a'){
        delete target[key];
    }else{
        return target[key];
    }
  },
  // 攔截對象自身屬性的讀取操做,具體攔截如下操做:Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols object.keys() 
  ownKeys (target) {
    console.log(`key in target`)
    // (1) 正常返回
    // return Reflect.ownKeys(target);
    // (2)error 'ownKeys' on proxy: trap result did not include 'd'
    // enumerable 的屬性必須在數組中返回
    // return ['b'] 
    // (3) 
    return ['b','d']
  },

  // 攔截Object.setPrototypeOf方法
  setPrototypeOf (target, newProto) {
    console.log(`new prototype set object`);
    return true
  }
  // 當讀取代理對象的原型時,該方法就會被調用。 返回值必須是一個對象或者null 
  // Object.getPrototypeOf .instanceof 
  // .__proto__ object.prototype.isPrototypeOf() 
  // object.prototype.__proto__
  getPrototypeOf (target) {
    console.log(`prototype in object`);
    // return Object.getPrototypeOf(target);
    return null;
  },

  // 攔截Object.preventExtensions(),且必須在內部調用此方法
  preventExtensions(target) {
    console.log("called_pre");
    Object.preventExtensions(target);
    return true;
  },
  // 攔截Object.isExtensible 默認狀況下,對象是可擴展的:便可覺得他們添加新的屬性。
  //以及它們的 __proto__ 屬性能夠被更改。
  //Object.preventExtensions,Object.seal 或 Object.freeze 方法均可以標記一個對象爲不可擴展(non-extensible)
  // 它的返回值必須與目標對象的isExtensible屬性保持一致
  // Object.isExtensible(proxy) === Object.isExtensible(target)
  isExtensible: function(target) {
    console.log("called");
    return Object.isExtensible(target);;
  },

  // 攔截Object.getOwnPropertyDescriptor(),返回一個屬性描述對象或者undefined。
  getOwnPropertyDescriptor(target, key){
    if (key === 'b') {
      // return; // error 'b' which exists in the non-extensible proxy target
      return Object.getOwnPropertyDescriptor(target, key);
    }
    return Object.getOwnPropertyDescriptor(target, key);  
  },


  // 攔截 Object.defineProperty() 返回布爾值 表示操做成功與否
  defineProperty(target, key, descriptor){
    console.log(`Object.defineProperty property ${key}`);
    Object.defineProperty(target, key, descriptor)
    return true;
  }
}
let proxy = new Proxy(target, handler);

proxy.a = 1;         // Set on property "a" new value "1"
proxy.b = 2;         // Set on property "b" new value "2"

console.log(proxy.a);// Get on property "a" // 1

console.log("a" in proxy );// is "a" in object ? // false
console.log("b" in proxy );// is "b" in object ? // true

delete proxy.a; // delete key: "a"
console.log(proxy.a);// Get on property "a" // undefined
delete proxy.b; // delete key: "b"
console.log(proxy.b);// Get on property "b" // 2

proxy.c = 3;
Object.defineProperty(proxy,'d',{
    value:'4',
    enumerable: false,
})
// (1) 過濾掉三類屬性 
//目標對象上不存在的屬性 屬性名爲 Symbol 值 不可遍歷(enumerable)的屬性
// console.log(Object.keys(proxy)); // ['b', 'c']
//(3) 返回攔截 只返回b
console.log(Object.keys(proxy)); // ['b']


let newProxy = {};
Object.setPrototypeOf(proxy,newProxy);// new prototype set object
console.log(Object.getPrototypeOf(proxy));// prototype get object // {}

let flag = Object.isExtensible(proxy);// called
console.log(flag); // true
flag = Object.preventExtensions(proxy)// called_pre
console.log(flag); // Proxy {b: 2, c: 3, d: "4"}
flag = Object.isExtensible(proxy);// called
console.log(flag);//false

console.log("------------");

console.log(proxy) // Proxy {b: 2, c: 3, d: "4"}
flag = Object.getOwnPropertyDescriptor(proxy, 'a');
console.log(flag) // undefined
flag = Object.getOwnPropertyDescriptor(proxy, 'c');
console.log(flag) // {value: 3, writable: true, enumerable: true, configurable: true}
flag = Object.getOwnPropertyDescriptor(proxy, 'b'); // 不可擴展必須返回
console.log(flag) // {value: 2, writable: true, enumerable: true, configurable: true}

console.log("+++++++++++++++");
// Object.defineProperty(proxy, 'name', {
// value: 'proxy',
// }); // error the non-extensible proxy target 對象不可擴展了 前面代碼執行的結果
Object.defineProperty(proxy, 'c', {
    value: 'proxy',
});
console.log(proxy) // Proxy {b: 2, c: "proxy", d: "4"}

複製代碼

(2)函數調用攔截 apply construct

let handler1 = {
  //攔截函數調用、call、apply操做 三個參數分別是:
  //1.目標對象 2.目標對象的上下文對象(this) 3.目標對象的參數數組
  apply (target, thisArg, argumentsList) {
    console.log(`Calculate sum: ${argumentsList}`);
    return 'Proxy ok'
  },
  // 用於攔截new命令 返回的必須是一個對象
  construct (target, args, newTarget) {
    console.log(`args is ${args}`)
    return new target('修改的小明');
  }
}

function sum (a,b){
  return a+b;
}
let proxy1 = new Proxy(sum, handler1)
console.log(sum(1,2)); // 3
console.log(proxy1(1,2));// Calculate sum: 1,2 // Proxy ok

function person (name) {
  this.name = name;
}
let proxy2 = new Proxy(person, handler1)
let person1 = new person();
console.log(person1);  // person {name: "小明"}
let person2 = new proxy2();
console.log(person2); // person {name: "修改的小明"}
複製代碼

4.2 Proxy與雙向綁定實現

示例效果:

示例代碼:

<div>
    <input type="text" id="value"/>
    <span id="bindValue"></span>
</div>
複製代碼
let inputEle = document.getElementById('value');
let spanEle = document.getElementById('bindValue');
const MessageObj = {};
let basehandler = {
    set(target, key, newVal){
        target[key] = newVal
        spanEle.innerText = newVal
    }
}
let proxy = new Proxy(MessageObj, basehandler)
// 監聽input輸入
inputEle.addEventListener('keyup', function (event) {
    proxy.msg = event.target.value
})
複製代碼

4.3 認識Reflect

Reflect對象與Proxy對象同樣,也是 ES6 爲了操做對象而提供的新 API。 Proxy代理經過攔截修改某些方法,而Reflect是將一些方法移植到該對象上,使某些方法變爲更加合理。現階段,某些方法同時在Object和Reflect對象上部署,將來的新方法將只部署在Reflect對象上。也就是說,從Reflect對象上能夠拿到語言內部的方法。 示例代碼以下:

// ES5寫法
try{
  Object.defineProperty(target,property,attributes);
   //success
} catch(e){
  //failure
}

//ES6寫法
if(Reflect.defineProperty(target,property,attributes)){
  //success
} else{
  //failure
}

// ES5寫法
Function.prototype.apply.call(Math.floor,undefined,[1.75]) //1

//ES6寫法
Reflect.apply(Math.floor,undefined,[1.75]) //1
複製代碼

Reflect對象一共有 13 個靜態方法,大部分與Object對象的同名方法的做用都是相同的,並且它與Proxy對象的方法是一一對應的,能夠說只要是Proxy對象的方法,就能在Reflect對象上找到對應的方法。這就讓Proxy對象能夠方便地調用對應的Reflect方法,完成默認行爲,做爲修改行爲的基礎。也就是說,無論Proxy怎麼修改默認行爲,你總能夠在Reflect上獲取默認行爲。 也就是說,Reflect.fn表示handler中的fn的默認行爲。 示例用法:

var obj = new Proxy({}, {
  get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    // 在瀏覽器console中,get方法會默認打印出值
    // 若是沒有Reflect.get執行默認行爲,就沒法正確打印出值,而會打印undefined
    return Reflect.get(target, key, receiver);
  },
  set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
  }
});
obj.a = 12;
console.log(obj.a)
複製代碼

4.4 vue3.0雙向綁定實現

數據流程圖

4.4.1 實現數據劫持 reactive

首先用proxy代理完成數據劫持,代碼以下:

// 將響應式與原始數據保存 防止重複代理 以下圖所示
// let obj = {
// a:1
// }
// let p = new Proxy(obj, basehandler) // 一樣數據 重複代理
// let p1 = new Proxy(obj, basehandler) // 一樣數據 重複代理
// let p2 = new Proxy(obj, basehandler) // 一樣數據 重複代理
// 原始=>響應式 存放代理後的對象
let toProxy = new WeakMap();
// 響應式=>原始 存放代理前的對象
let toRaw = new WeakMap();
// 收集依賴 動態建立依賴關係
function track(target, key){
  // 收集依賴
}
// 更新視圖
function trigger(target, key, info){
  // 更新視圖
}
// **** 觀察者 代理 與vue2 不一樣 ****
const baseHandler = {
    get (target, key) {
        console.log('收集依賴,數據初始化')
        const res = target[key];
        // 收集依賴 將effect key 對應起來 訂閱
        // @todo 
         track(target, key);
        // 判斷是否爲對象 爲對象的話繼續代理
        return typeof res === "object" ? reactive(res) : res;  
    },
    set (target, key, newVal) {
        console.log('數據變化')
        const info = {oldValue:target[key], newValue:newVal};
        // target[key] = newVal; 若是設置沒成功 這對象不能夠被設置 會報錯 Reflect設置反射 有返回值
        const res = Reflect.set(target, key, newVal );
        // 觸發私有屬性才更新 如 arr length變化 不觸發更新
        if(target.hasOwnProperty(key)){
            // 通知更新
            // @todo 
          trigger(target, key, info);
        }
        return res;
    }
}
function reactive (target) {
    console.log(target,'響應式數據')
    // 若是不是對象 則返回
    if(typeof target !== 'object' && target === null){
        return target;
    }
    // 若是代理的數據中存在 則再也不進行代理
    let proxy = toProxy.get(target)
    if(proxy){
        return proxy;   
    }
    // 若是對象已經代理過了 則返回該對象
    if(toRaw.has(target)){
        return target;   
    }
    // 建立觀察者
    const observed = new Proxy(target, baseHandler);
    toProxy.set(target, observed);  // 原對象爲鍵 代理後對象爲值
    toRaw.set(observed, target);  // 原對象爲鍵 代理後對象爲值
    console.log(observed,'響應完成')
    return observed;
}
複製代碼
4.4.2 建立響應式依賴

在上文中,咱們知道,全部響應式的數據,在vue2.0中都叫作依賴,vue3.0中叫作effect,即反作用,能夠淺顯理解爲,數據改變的反作用---->更新視圖。要想數據變化,依賴可以自動執行視圖更新的方法,就須要建立響應式依賴。代碼以下:

// let obj = {name:'aa'}
// effect(()=>{
// console.log(obj.name) // 此處觸發get 而後收集依賴 將key effect 對應起來
// })
// obj.name = 'bb'
// effect 數據變化 更新視圖的方法 默認執行一次進行數據初始化 數據變化 再執行
// 建立響應式依賴,並放在依賴的數組中
let effectStack = []  // {name:effect()} // 收集依賴對應關係
// {
// target:{
// key:[fn,fn,fn]
// }
// }
function effect (fn) {
    // 數據變化 函數執行 變成響應式函數
    let effect = createReactiveEffect(fn);
    effect(); // 默認先執行一次
    return effect;
}
function createReactiveEffect(fn){
    let effect = function(){  // 響應式的effect
        return run(effect,fn);  // fn執行 且 存在棧中
    };
    return effect;
}
function run (effect, fn) {
    try{
        effectStack.push(effect);
        return fn();
    }finally{
        effectStack.pop();
    }
}
複製代碼
4.4.3 依賴收集以及數據更新

接下來咱們實現依賴收集以及數據更新頁面變化,即執行effect。代碼以下:

let targetMap = new WeakMap();  // 收集依賴
// 收集依賴 動態建立依賴關係
function track (target, key) {  
    let effect = effectStack[effectStack.length-1];
    if(effect){  // 有對應關係 才建立關聯
        let depMap = targetMap.get(target);
        if(depMap === undefined){
            depMap = new Map();
            targetMap.set(target, depMap);
        }
        let dep = depMap.get(key);
        if(dep === undefined) {
            dep = new Set();
            depMap.set(key, dep);
        }
        if(!dep.has(effect)){
            dep.add(effect)
        }
    }
}
// 更新視圖
function trigger (target, key, info) {
    
    const depMap = targetMap.get(target);
    
    if(!depMap){
        return;
    }
    const effects = new Set();
    if(key){
        let deps = depMap.get(key);
       
        if(deps){
            deps.forEach(effect=>{
                effects.add(effect)
            })
        }
    }
    effects.forEach(effect => effect())
}
複製代碼
4.4.4 完整代碼及demo效果
<div id="box" style="margin-left: 20px;">
    <span id="app"></span> 
    <span id="add" style="margin-left: 10px;display: inline-block;width: 10px;height: 10px;cursor: pointer;">+</span>
</div>
<script src="./vue/vue3.js"></script>
<script> let ele = document.getElementById('app'); let btn = document.getElementById('add'); let o = { number: 1, } let reactiveData = reactive(o); effect(()=>{ console.log('數據變了') ele.innerHTML = reactiveData.number }) btn.addEventListener('click', () => { reactiveData.number += 1; },false) </script>
複製代碼
// vue3.js
// 將響應式與原始數據保存 防止重複代理 以下圖所示
// let obj = {
// a:1
// }
// let p = new Proxy(obj, basehandler)
// let p1 = new Proxy(obj, basehandler)
// let p2 = new Proxy(obj, basehandler)
// 原始=>響應式 存放代理後的對象
let toProxy = new WeakMap();
// 響應式=>原始 存放代理前的對象
let toRaw = new WeakMap();

let targetMap = new WeakMap();  // 收集依賴
let effectStack = []  // {name:effect()} // 收集依賴對應關係
// {
// target:{
// key:[fn,fn,fn]
// }
// }
// 收集依賴 動態建立依賴關係
function track (target, key) {  
    let effect = effectStack[effectStack.length-1];
    if(effect){  // 有對應關係 才建立關聯
        let depMap = targetMap.get(target);
        if(depMap === undefined){
            depMap = new Map();
            targetMap.set(target, depMap);
        }
        let dep = depMap.get(key);
        if(dep === undefined) {
            dep = new Set();
            depMap.set(key, dep);
        }
        if(!dep.has(effect)){
            dep.add(effect)
        }
    }
}
// 更新視圖
function trigger (target, key, info) {
    
    const depMap = targetMap.get(target);
   
    if(!depMap){
        return;
    }
    const effects = new Set();
    if(key){
        let deps = depMap.get(key);
       
        if(deps){
            deps.forEach(effect=>{
                effects.add(effect)
            })
        }
    }
    effects.forEach(effect => effect())
}

// 觀察者 代理 與vue2 不一樣
const baseHandler = {
    get (target, key) {
        const res = target[key];
        // 收集依賴 將effect key 對應起來 訂閱
        track(target, key);
        return typeof res === "object" ? reactive(res) : res;  // 判斷是否爲對象 爲對象的話繼續代理
    },
    set (target, key, newVal) {
        const info = {oldValue:target[key], newValue:newVal};
        // target[key] = newVal; 若是設置沒成功 這對象不能夠被設置 會報錯 Reflect設置反射 有返回值
        const res = Reflect.set(target, key, newVal );
        // 通知更新
        // 觸發私有屬性才更新 如 arr length變化 不觸發更新
        if(target.hasOwnProperty(key)){
            trigger(target, key, info);
        }
        return res;
    }
}
function reactive (target) {
    // 若是不是對象 則返回
    if(typeof target !== 'object' && target === null){
        return target;
    }
    // 若是代理的數據中存在 則再也不進行代理
    let proxy = toProxy.get(target)
    if(proxy){
        return proxy;   
    }
    // 若是對象已經代理過了 則返回該對象
    if(toRaw.has(target)){
        return target;   
    }
    // 建立觀察者
    const observed = new Proxy(target, baseHandler);
    toProxy.set(target, observed);  // 原對象爲鍵 代理後對象爲值
    toRaw.set(observed, target);  // 原對象爲鍵 代理後對象爲值
    console.log(observed,'響應完成')
    return observed;
}

// let obj = {name:'aa'}
// effect(()=>{
// console.log(obj.name) // 此處觸發get 而後收集依賴 將key effect 對應起來
// })
// obj.name = 'bb'
// effect 數據變化 更新視圖的方法 默認執行一次 數據變化 再執行
function effect (fn) {
    // 數據變化 函數執行 變成響應式函數
    let effect = createReactiveEffect(fn);
    effect(); // 默認先執行一次
    return effect;
}
function createReactiveEffect(fn){
    let effect = function(){  // 響應式的effect
        return run(effect,fn);  // fn執行 且 存在棧中
    };
    return effect;
}
function run (effect, fn) {
    try{
        effectStack.push(effect);
        return fn();
    }finally{
        effectStack.pop();
    }
}
複製代碼

5.總結

vue3.0 摒棄了Object.defineProperty() 而採用了 proxy ,最後咱們總結一下這二者的區別:

(1) defineProperty() 只能監聽某個屬性,不能對全對象監聽,proxy監聽整個對象,並返回一個新對象,能夠省去for in、閉包等內容來提高效率。

let obj1 = {
    a: 1,
    b: 2,
}
// proxy 寫法
let proxy1 = new Proxy(obj1, {
    set (target, key, value){
        console.log(`setting ${key}!`);
        return Reflect.set(target, key, value, receiver);
    },
    get (target, key){
        console.log(`getting ${key}!`);
        return Reflect.get(target, key, receiver);
    }
})
// Object.defineProperty() 寫法
function observe(data){
    if(!data || typeof data !== 'object') {
        return;
    }
    // 取出全部屬性遍歷
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
}
function defineReactive(data, key, val){
    observe(val); // 監聽子屬性
    Object.defineProperty(data, key, {
        enumerable: true, 
        configurable: true, 
        get: function() {
            console.log(key + '值獲取')
            return val;
        },
        set: function(newVal) {
            console.log(key + '值設置', val, ' --> ', newVal);
            val = newVal;
        }
    });
}
observe(obj1);
console.log(obj1.a);
console.log(obj1.b);
obj1.a = 12;
複製代碼

(2) 能夠監聽數組,不用再去單獨的對數組作特異性操做。

對比代碼以下:

let arr = [1,2,3];
let p = new Proxy(arr, {
  get(target, key,) {
    console.log('獲取數組屬性',target,key)
    return target[key];
  },
  set(target, key, value) {
    console.log('設置數組屬性',key,+','+target[key] + ' -->' + value )
    target[key] = value;
    return true;
  }
})
console.log(p) // Proxy {0: 1, 1: 2, 2: 3}
p.push(4);  
// 發生四步驟:(1)獲取數組屬性 (3) [1, 2, 3] push
// (2)獲取數組屬性 (3) [1, 2, 3] length
// (3)設置數組屬性 3 NaN -->4
// (4)設置數組屬性 length NaN -->4
console.log('++') // 設置數組屬性 0 NaN -->10
p[0] = 10;
console.log('-----------');
let arrObj = {
   b:1,
}
let bValue = arrObj.b;
Object.defineProperty(arrObj, "b", {
    enumerable: true, 
    configurable: true, 
    get: function() {
        let key = "b"
        console.log(key + '值獲取', bValue)
        return bValue;
    },
    set: function(newVal) {
        let key = "b"
        console.log(key + '值設置 --> ', newVal);
        bValue = newVal;
        return bValue
    }
})
console.log(arrObj.b) // 1
arrObj.b = [1,2,3];  // b值設置 --> (3) [1, 2, 3]
arrObj.b.push(4);  // b值獲取 (3) [1, 2, 3] 只獲取了舊值 設置push沒有監聽到
arrObj.b[0] = 10;  // b值獲取 (4) [1, 2, 3, 4] 只獲取了舊值 b[0]設置沒有監聽到
複製代碼

原理老是晦澀難懂的,學習過程也很痛苦,跌跌撞撞的看完,特此總結與你們共享,有表述不正確的地方,但願多多指正。


git地址:github.com/aicai0/vue3…

若有問題,歡迎探討,若是滿意,請手動點贊,謝謝!🙏

及時獲取更多姿式,請您關注!!

相關文章
相關標籤/搜索