[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

codercao發表於2019-09-11

不知不覺,Vue的作者尤雨溪公佈了Vue3.0版本的開發計劃,釋出到現在已經一年了,看來Vue3.0的釋出尚需時日,在開發計劃中,下圖這段話:Vue3.0版本中將基於Proxy來改造觀察者模式。說明Vue3.0講不再借助於ES5的Object.defineProperty,轉而使用最新的Proxy語法實現Vue最根本的響應式原理(注又名:資料劫持,下文統稱響應式原理)。

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

下文主要簡述從Object.defineProperty到proxy的實現觀察者機制探索,目前關於深入響應式原理的文章已經很多了,很多都寫的很好,本文不做過深的vue裡的原始碼解析,只是淺入探索和自己動手手寫一個簡易的Object.defineProperty實現觀察者機制,以及手寫一個簡易的由Proxy實現觀察者機制,當然最終以作者釋出為準。主要有以下幾個知識點帶大家一起進入

1、Object.defineProperty實現觀察者機制
2、Object.defineProperty的缺點
3、利用proxy實現簡易的實現觀察者機制
4、總結

一、Object.defineProperty實現觀察者機制

這裡我們照顧一下小白同學,首先我們來補充一下ES5中對Object.defineProperty() 方法的定義和一些基礎知識,然後手寫一個簡易的響應式,最後再結合vue原始碼簡析。

1.1 Object.defineProperty基礎知識

developer.mozilla.org對Object.defineProperty()定義

Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性, 並返回這個物件
語法:
Object.defineProperty(obj, prop, descriptor)
引數說明:
1、obj要在其上定義屬性的物件
2、要定義或修改的屬性的名稱
3、將被定義或修改的屬性描述符
返回值:
 被傳遞給函式的物件
複製程式碼

這裡我們重點關注一下語法中的第三個引數屬性描述符:descriptor

物件裡目前存在的屬性描述符有兩種主要形式:資料描述符存取描述符資料描述符是一個具有值的屬性,該值可能是可寫的,也可能不是可寫的。存取描述符是由getter-setter函式對描述的屬性。描述符必須是這兩種形式之一;不能同時是兩者。這裡descriptor有6個選鍵值configurable、enumerable、value、writableget、set這裡他們分別的介紹可以移步

1.2 建立物件

通常我們建立物件來一步一步瞭解這個Object.defineProperty() 方法和屬性描述符descriptor裡面鍵值的用法

【1】正常我們建立一個物件,如下,然後控制檯列印他們我們可以看到

      let vm = {
        name: '掘金'
      }
      console.log(vm)複製程式碼

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

【2】接下來我們通過Object.defineProperty建立一個物件,並設定這個物件要定義或者修改的屬性“name”

//    let vm = {
//      name: '掘金'
//    }
    let vm = Object.defineProperty({},"name",{
      get() {
        console.log("執行get");
        return "掘金"
      },
      set(newValue) {
        console.log("執行set");
        console.log("新值:" + newValue);
      }
    })
 console.log(vm)複製程式碼

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

其實這兩種都建立一個物件的方式,通過Object.defineProperty建立的物件,我們可以看到,多了上面說的兩個存取描述符鍵值方法get 和set 這樣的物件,就變得可控被觀察,也就是我們說的被劫持,當我們改變或者獲取這物件的屬性的時候,我們就可以控制到它。

下面我們通過改變vm.name = "juejin",我們通過控制檯可以看到

//    let vm = {
//      name: '掘金'
//    }
    let vm = Object.defineProperty({},"name",{
      get() {
        console.log("執行get");
        return "掘金"
      },
      set(newValue) {
        console.log("執行set");
        console.log("新值:" + newValue);
      }
    })
vm.name = "juejin";
//console.log(vm)複製程式碼

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索


1.3 實現觀察者機制,響應式物件

let vm = {
  id:"juejin",
  name:"掘金"};
let keys = Object.keys(vm);
keys.forEach(key=>{
  let value = vm[key];
  Object.defineProperty(vm, key,{
     get() {
        console.log("執行get");
        return value
     },
     set(newValue){
        console.log("執行set");
        if(newValue!=value){
           value =  newValue;
         }
       }
    })
})
vm.id = "test";
console.log(vm)複製程式碼

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

這裡主要是遍歷物件中的每一個屬性,每個屬性都是賦予get和set,讓物件中的每一個屬性的改變都會被監測到,也就是實現了響應式觀察者機制。

1.4 vue原始碼中的響應式原理簡析

上面的例子我們試一下,用陣列物件發現是不能生效的,那麼在vue裡陣列是怎麼實現響應式原理的呢,我們可以看到vue原始碼目錄src/core/observer/index.js裡,其實他是對物件進行了判斷,如果是陣列物件,就會走observeArray()方法,而且你會發現裡面還有一個arrayMethods,裡面是對陣列的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'進行了重寫,重寫過後的方法不僅能實現原有的功能,還能釋出訊息給訂閱者。其他物件,都走walk()方法。

1.5 把資料渲染到頁面上

當我們檢測到物件更新了,如何同步更新到頁面上呢?

1、首先,我們要找到作用域範圍內(vue,裡會有個 el:"#app")的節點,全部頁面內容都會渲染到這個結點裡面

2、然後遍歷結點上所有含有使用該物件的地方,也就是Mustache語法 (雙大括號) 的文字插值的地方,例如 {{ vm }}

3、繫結檢視更新


二、Object.defineProperty的缺點

2.1 無法監聽物件非已有的屬性的新增和已有屬性的刪除

只會對物件原有的全部屬性進行做資料劫持,也就是說Vue 不允許動態新增或者刪除物件已有屬性,它是不做資料劫持的,也就不能實現響應式。

舉例

<template>
  <div>
    <h1>{{ vm }}</h1>
    <button @click="addAttribute">新增屬性</button>
    <button @click="delAttribute">刪除屬性</button>
  </div>
</template>

<script>
export default {
  data() {
    return { 
      vm:{
        id:"juejin",
        name:"掘金"
      }
    }
  },
  methods: {
    addAttribute() {
      this.vm.use = "codercao"
      console.log(this.vm)
    },
    delAttribute() {
      for(let k in this.vm) {
       if(k=='id'){
         delete this.vm[k]
       }
      }
      console.log(this.vm)
   }
  },
}
</script>複製程式碼

點選新增屬性,你會發現控制檯列印的vm已經新增了use屬性,而頁面並沒有響應式改變

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

點選刪除屬性,你會發現控制檯列印的vm已經刪除了id屬性,而頁面並沒有響應式改變

[Vue響應式原理]從Object.defineProperty到proxy實現觀察者機制的探索

2.2 陣列變異

陣列物件也不能通過屬性或者索引控制陣列,比如length,index實現響應式,通過1.4 裡我們也看到vue原始碼只是對陣列的 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'進行了重寫,但是索引控制陣列是沒有辦法實現響應式的。

2.3 解決以上的辦法。 使用Vue.set(object, propertyName, value) 方法

改進上面2.1裡的新增屬性方式,你會發現頁面就實現了響應式,至於Vue.set方法介紹移步

    addAttribute() {
     //this.vm.use = "codercao"
     this.$set(this.vm,'use','codercao')
      console.log(this.vm)
    },複製程式碼


三、利用proxy實現簡易的實現觀察者機制

3.1proxy基礎知識

Proxy 物件用於定義基本操作的自定義行為(如屬性查詢,賦值,列舉,函式呼叫等)

語法
let p = new Proxy(target, handler);
引數說明
target用Proxy包裝的目標物件(可以是任何型別的物件,包括原生陣列,函式,甚至另一個代理)
handler一個物件,其屬性是當執行一個操作時定義代理的行為的函式複製程式碼


  let vm = {
      id:"juejin",
      name:"掘金"
    }
    let newVm = new Proxy(vm,{
      get(target,key){
        console.log("執行get");
        return target[key];
       },
      set(target,key,newValue){
        console.log("執行set");
        if(target[key]!==newValue)
        target[key] = newValue;
      }
    })
    newVm.use = "codercao"
    console.log(newVm)複製程式碼

你會發現用Proxy 也一樣實現了一個簡易的觀察者機制,當然深入研究的話,你還可以實現雙向繫結。

四、結尾

到這裡我們這篇文章到此就結束了,至於最終作者會怎麼用proxy來寫這個觀察者機制,待vue3.0釋出可以一看究竟,文章主要是帶大家實踐探索一下Object.defineProperty實現觀察者機制,順便提了一下,這個Object.defineProperty缺陷和處理辦法,然後引入proxy,屬於比較初級的嘗試,Vue發展到現在幾年了,其實大部分人對其應用已經遊刃有餘了,關注原始碼和實踐vue裡功能的原理,或許對每個前端人都會有一些提升。


相關文章