Vue響應式系統理解

前言

以前學習 vue 的時候,一直沒刨根問底過。在看到網上這類文章比較多,良莠不齊的質量有時候看的一頭霧水。固然也有不錯的文章,可是終究是別人的理解。因而寫一篇關於本身的理解記錄下來,親身實踐才能收穫更多!vue

初階:響應式原理

在說明以前,咱們先了解一個 Object.defineProperty() 。引用 MDN 上的權威介紹 developer.mozilla.org/zh-CN/docs/…設計模式

Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,並返回此對象。緩存

語法安全

Object.defineProperty(obj, prop, descriptor)
複製代碼

參數bash

  • obj函數

    要定義屬性的對象。學習

  • propui

    要定義或修改的屬性的名稱或 Symbolspa

  • descriptor設計

    要定義或修改的屬性描述符。

返回值

被傳遞給函數的對象。

在瞭解了這個以後,咱們就能夠用它來實現一個響應式的初級樣子。

const TestObject = {
  name:"",
  age:10
}
let tempValue = '';
Object.defineProperty(TestObject,'name',{
  
  get:function (){
    console.log('我被獲取了,我能夠在這裏搞點事情!')
    return tempValue;
  },
  set:function (newValue){
    console.log('我被寫入了,我能夠在這裏搞點事情!')
    tempValue = newValue
  }
})
複製代碼

此時使用 TestObject.name 方法可讓 get 和 set 裏面的對應生效。

我想看到這裏,聰明的同窗可能會有一個疑問,爲啥要搞一個 tempValue ?我直接 TestObject.name 在 get 和 set 裏面賦值不行麼?就像這樣:

const TestObject = {
  name:"",
  age:10
}
Object.defineProperty(TestObject,'name',{
  get:function (){
    console.log('我被獲取了,我能夠在這裏搞點事情!')
    return TestObject.name;
  },
  set:function (newValue){
    console.log('我被寫入了,我能夠在這裏搞點事情!')
    TestObject.name = newValue
  }
})
複製代碼

真正運行的時候,其實會發現,陷入了死循環,這裏你們切忌要避免坑!

咱們重讀MDN上的文檔能夠發現,get 和 set 自己就是在獲取和設置的時候觸發的函數,在裏面寫了 TestObject.name ,那麼就會繼續調用 set ,而後繼續 TestObject.name ,繼續 set ,繼續.....無限循環。因此使用一個臨時變量在外面,是比較安全的作法。

中階:接管對象

在前面基礎上,咱們如今能夠開始接管整個對象了,邏輯很是簡單,套個循環,上代碼:

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    DefineObj(obj,key,obj[key])
  })
}
function DefineObj(obj,key,value){
  Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被獲取了,我能夠在這裏搞點事情!')
      return value;
    },
    set:function (newValue){
      console.log('我被寫入了,我能夠在這裏搞點事情!')
      value = newValue
    }
  })
}
const TestObject = {
  name:"",
  age:10
}
ProxyObj(TestObject)
複製代碼

這時,聰明的同窗又會有疑問了?爲何要建立兩個 function ,ProxyObj 和 DefineObj 不能堆在一個裏面麼?就像這樣

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被獲取了,我能夠在這裏搞點事情!')
      return obj[key];
    },
    set:function (newValue){
      console.log('我被寫入了,我能夠在這裏搞點事情!')
      obj[key] = newValue
    }
  })
  })
}
const TestObject = {
  name:"",
  age:10
}
ProxyObj(TestObject)
複製代碼

不能,其實緣由跟以前提到的問題同樣,存在死循環的問題。那爲何咱們拆分開來就不存在呢?由於這裏的 DefineObj 傳入的形參。用咱們瞭解到的 js 基礎知識來解釋,一個函數內的形參,至關因而函數內預設的變量,通常狀況下(也會有二般狀況)這個變量的生命週期僅在函數內。因此利用了這個特性,案例巧妙的將形參 value 拿出來傳遞和賦值,就不存在無限死循環的問題了。

劃重點:使用 Object.defineProperty 切忌不要陷入到死循環當中!

高階:收集依賴

理解需求

終於來到了收集依賴環節,這塊也是我以前一直沒有想通的地方。直到次日醒來朦朧中看着屏幕前的代碼,忽然如有所思了,話很少說,直接來看看需求,對於需求都沒有很是清晰的概念,直接上代碼有點暈。

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
 return TestObject.age>18?'成年人':'未成年人';
})
複製代碼

簡單實現

咱們須要實現這個watcher函數,裏面能夠填入對象,而且設置關注的對象 type 屬性,此時該屬性尚未在對象身上,咱們須要賦予一下,當 age 變化之後,type 也要對應着改變一下。這就是咱們所說的依賴收集需求。

咱們這樣簡單實現一下:

function watcher(obj,key,cb){
  Object.defineProperty(obj,key,{
    get:function (){
      const val = cb();
      return val;
    },
    set:function(newValue){
      console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
    }  
  })
}

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)//未成年人
TestObject.age=19;
console.log(TestObject.type)//成年人
複製代碼

能夠看到,咱們經過這樣的方式,就簡單的實現了 TestObject 屬性上的依賴計算屬性 。

完善需求

可是問題又來了,我若是不去讀取 type 它是不會主動更新的。如何作到 age 變化之後,type 自動更新呢?

在前面的基礎上,咱們先定義一個依賴更新時候的函數 updateTodo 此時代碼變這樣:

let target = '';

function watcher(obj,key,cb){
  function updateTodo(){
    const val = cb();
    console.log('更新啦',val)
    }
  Object.defineProperty(obj,key,{
    get:function (){
      target = updateTodo;//重點代碼
      const val = cb();//重點代碼
      target = '';//重點代碼
      return val;
    },
    set:function(newValue){
      console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
    }  
  })
}

const TestObject = {
  name:"",
  age:10
}

watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';//重點代碼
})
console.log(TestObject.type)
TestObject.age = 19;
console.log(TestObject.type)
複製代碼

會發現咱們順便在全局還定義一個target,儲存當前 callback,這裏是我以爲最重要的部分,必定要認真看裏面的這段,思考一下,我單獨拿出來:

target = updateTodo;
const val = cb();
target = '';
複製代碼

callback函數長這樣:

()=>{
  return TestObject.age>18?'成年人':'未成年人';
}
複製代碼

可能你們會有疑問,target 賦一下有重置是什麼騷操做?有何意義?

注意, callback 裏面有一個 TestObject.age !這個一旦被訪問,它的 get 函數就被調用了!那麼此刻,前面target裏面儲存的 updateTodo 函數,是否是就能夠在 get 裏面取到了呢?

因此在 cb() 執行完以後,實際上裏面就有機會收集依賴了,target 就是這個做用,做爲一個臨時的 callback 緩存着,那麼咱們的需求也很好解決了,只須要在這裏進行一次依賴的收集和釋放便可!

這裏的疑問解決了之後,就開始直接上代碼吧!後面就比較好理解,跟咱們前面的接管對象章節作一個合併,直接展現完整代碼,細細品味,很是有意思:

function ProxyObj(obj){
  Object.keys(obj).forEach(key=>{
    DefineObj(obj,key,obj[key])
  })
}

function DefineObj(obj,key,value){
  let deps = [];
  Object.defineProperty(obj,key,{
    get:function (){
      console.log('我被獲取了,我能夠在這裏搞點事情!')
      //依賴收集
      if(target && !deps.includes(target)){//判斷是否存在或重複依賴
        deps.push(target)
      }
      return value;
    },
    set:function (newValue){
      console.log('我被寫入了,我能夠在這裏搞點事情!')
      value = newValue;
      deps.forEach(fn=>{//依賴釋放
        fn()
      })
    }
  })
}

function watcher(obj,key,cb){
  function updateTodo(){
    const val = cb();
    console.log('更新啦',val)
    }
  Object.defineProperty(obj,key,{
    get:function (){
      target = updateTodo;
      const val = cb();
      target='';
      return val;
    },
    set:function(newValue){
      console.log('該屬性是被用於去自動計算的哦~不要人工賦值!')
    }  
  })
}


let target = '';
const TestObject = {
  name:"",
  age:10
}

ProxyObj(TestObject)
watcher(TestObject,'type',()=>{
  return TestObject.age>18?'成年人':'未成年人';
})
console.log(TestObject.type)
TestObject.age=19;
console.log(TestObject.type)
複製代碼

固然,若是你對設計模式和一些代碼整潔有要求,看到這裏你可能會以爲很是不爽,沒有關係,嘗試着本身來封裝下吧,或者參看 Vue 內的源碼部分。

相關文章
相關標籤/搜索