vue3與vue2的區別之數據響應——手寫vue3的reactive,理解vue3數據響應式原理

1、 數據響應式

首先請大家認真的思考一個問題:什么是數據響應式

答:數據變化是可偵測的,并且和數據相關的內容可以更新。

?這里一定要明確一個概念,數據響應式和視圖更新是沒有關系的!數據響應式是一種機制,一種數據變化的偵測機制。而實現數據響應式這種機制的方法不唯一。
那么,vue是如何實現數據響應式的?vue2和vue3的數據響應式有什么區別?

2、vue如何實現數據響應式?

要知道,vue3.x實現數據響應的方案跟vue2.x是不一樣的,所以在這里我將vue2.xvue3.x分別說說。這也是理解vue2.xvue3.x區別的時候,可以指出來的一個巨大的區別。

2.1 vue2.x的實現方案

我貼上一個vue2.x源碼-Object的變化偵測解讀的鏈接,方便大家理解和后續關于vue2.x的學習需要。
(特別是還沒閱讀過vue源碼的同學,可以獨自過一遍這個文檔,能對vue有一個更深的認識)

在下面vue2的源碼中可以看到,Observer類會通過遞歸的方式把一個對象的所有屬性都轉化成可觀測對象,所以我們可以知道vue2需要遍歷對象的所有的key。其實現數據響應式的核心思想就是通過defineProperty,去定義getset等方法。從而能夠攔截到對象屬性的訪問和變更。

/**
 * Observer類會通過遞歸的方式把一個對象的所有屬性都轉化成可觀測對象
 */
export class Observer {
  constructor (value) {
    this.value = value
    // 給value新增一個__ob__屬性,值為該value的Observer實例
    // 相當于為value打上標記,表示它已經被轉化成響應式了,避免重復操作
    def(value,'__ob__',this)
    if (Array.isArray(value)) {
      // 當value為數組時的邏輯
      // ...
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
/**
 * 使一個對象轉化成可觀測對象
 * @param { Object } obj 對象
 * @param { String } key 對象的key
 * @param { Any } val 對象的某個key的值
 */
function defineReactive (obj,key,val) {
  // 如果只傳了obj和key,那么val = obj[key]
  if (arguments.length === 2) {
    val = obj[key]
  }
  if(typeof val === 'object'){
      new Observer(val)
  }
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get(){
      console.log(`${key}屬性被讀取了`);
      return val;
    },
    set(newVal){
      if(val === newVal){
          return
      }
      console.log(`${key}屬性被修改了`);
      val = newVal;
    }
  })
}

在日常開發中,產品經理總是會跟我們說,我們做了xxxx就是為了解決客戶的xxxx痛點。
那么,在繼續往下閱讀的時候,可以先思考一下vue2這樣的實現方案的痛點有什么?或者說缺點有什么?
因為作為客戶(使用vue開發的前端同學)的我們需要知道,vue3是否解決了我們的痛點?

vue2的缺點:(僅僅是關于數據響應造成的缺點哦!)

  • 1、影響初始化速度、數據過大時的資源問題
    (在源碼的Observer方法上,對象的每一個屬性都要被攔截。所有的key都要有一次循環和遞歸)
  • 2、數組的特殊處理,導致其修改數據不能使用索引
    (原因在于defineProperty不支持數組,參考vue源碼-Array的變化偵測
  • 3、動態添加或刪除對象屬性無法被偵測
    defineProperty哭著對我說:臣妾的的setter函數辦不到呀)

對于沒閱讀過vue源碼的前端開發來說,應該也遇到過修改了數組,或者修改對象后發現,啥變化也沒有,一頭霧水,拍桌子直呼:vue真垃圾,有bug。
其實這些霧水大都是上面的2、3兩點引發的,vue也都提供了解決方案:$set$delete,我都整理好了,需要理解的直接移步深入響應式原理
但是,這就體驗極差

??小故事一則:去年還沒閱讀源碼的時候,公司一個大版本的發布后,出現了一個不是很嚴重,卻影響使用范圍很廣的一個bug,我們從凌晨2點修到4點,最后還是一個大牛搞了幾輪實驗發現了問題,說vue有bug,某某地方賦值需要用$set。沒錯,就是上面痛點里的第3點。原因還是我們太菜呀,沒有閱讀相關源碼。

2.2 vue3.x的實現方案

文章開頭我就強調了:數據響應式是一種機制,一種數據變化的偵測機制。而實現數據響應式這種機制的方法不唯一。于是乎,vue3.x來了,他帶著vue2.x痛點的解決方案來了!

解決方案其實一點也不神秘,在ES6之后,出現了一個新的特性:ProxyVue3.x在使用了Proxy之后,痛點們一下子就全都解決了。Proxy是怎么解決的呢?請聽下回...請繼續往下看哈看完手寫reactive之后,就全都明白啦。
順便給個Proxy的MDN地址: Proxy MDN傳松門

3、手寫reactive

在vue3.x中,定義響應式對象的方法如下:

const obj = reactive({
  name: 'chenjing',
  age: 18
})

3.1 測試Proxy是否生效

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      console.log('target, key', target, key, target[key])
      return target[key]
    })
}

proxy-get.png

ok,生效。在簡易版的reactive,我們要添加基本的屬性getsetdeleteProperty。同時,在上面代碼的get里直接return target[key],一來不太優雅、二來可能報錯。我們先來看看vue3是怎么處理的:
vue3源碼圖1.png

再來一個傳送門:Reflect - MDN

Reflect 是一個內置的對象,它提供攔截 JavaScript 操作的方法。這些方法與proxy handlers的方法相同。Reflect不是一個函數對象,因此它是不可構造的。
與大多數全局對象不同Reflect并非一個構造函數,所以不能通過new運算符對其進行調用,或者將Reflect對象作為一個函數來調用。Reflect的所有屬性和方法都是靜態的(就像Math對象)。
Reflect 對象提供了以下靜態方法,這些方法與proxy handler methods的命名相同.
其中的一些方法與 Object相同, 盡管二者之間存在 某些細微上的差別 .

3.2 reactive基本形態

讓我們來學習一下vue3的寫法后,加上了Reflect后,于是我們最基本的reactive就是下面這樣的:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key) // 可以直接return target[key],避免報錯和代碼的優雅性,模仿源碼采用Reflect
      console.log('get', key)
      return (typeof res === 'object') ? reactive(res) : res // 子屬性若是對象 需要再次代理
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      console.log('set', key)
      return res
    },
    deleteProperty() {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return res
    }
  })
}

reactive基本形態.png

通過跑腳本后的控制臺,可以看到訪問屬性成功的觸發了get。同時新增屬性也觸發了set
到這里為止,vue2中的數據響應式在vue3里其實已經完全實現了。回過頭來想想,是不是沒那么難理解了吧。沒有vue2的循環遍歷遞歸,只是上了Proxy的車
當然了在Vue3內真正的實現,肯定不是這么幾行代碼就搞定的。只是響應式的原理就是利用了Proxy

既然要手寫實現一個簡易的reactive函數,讓我們繼續往下閱讀。
目前只是想簡單理解vue3數據響應式原理,了解vue3數據響應和vue2數據響應的區別的同學可以直接點贊了哈哈,鼓勵一下互相學習進步??

3.3 依賴的收集、觸發

既然要手寫實現一個簡易的reactive函數,我們就繼續。
要實現reactive函數,我們就要在get內進行依賴收集,在set中進行觸發。即便是vue2也是通過類似的發布訂閱模式體現。在這里,我們也是通過發布訂閱模式去完成。

首先是依賴收集:在get內,我們需要對依賴進行收集。在依賴收集的時候,將其按照依賴關系放入map中映射。
然后就是依賴觸發:在set中,需要觸發響應式函數。即完成了發布訂閱。

下面代碼 有需要的可以直接復制粘貼,直接跑。可以自行斷點看看,有疑問的歡迎交流。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key)
      console.log('get', key)
      // 依賴收集
      track(target, key)
      return (typeof res === 'object') ? reactive(res) : res
    },
    set(target, key, val) {
      const res = Reflect.set(target, key, val)
      console.log('set', key)
      // 觸發
      trigger(target, key)
      return res
    },
    deleteProperty() {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return res
    }
  })
}

// 保存副作用函數
const effectStack = []
// 添加副作用函數
function effect (fn) {
  const e = createReactiveEffect(fn)

  // 立即執行
  e()
  return e
}

function createReactiveEffect(fn) {
  // 封裝fn,處理其錯誤,執行之,存放到stack
  const effect = () => {
    try {
      // 0入棧
      effectStack.push(effect)
      // 1 執行fn
      return fn()
    } finally {
      // 2 出棧
      effectStack.pop
    }
  }
  return effect
}

// 保存映射關系的數據結構
const targetMap = new WeakMap()

// 當副作用函數觸發響應式數據之后,執行track,進項依賴收集工作
// 目標是將target, key和前面effectStack中的副作用函數之間建立映射關系
function track (target, key) {
  // 1.先拿出響應函數
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    // 獲取target對應的map
    let depMap = targetMap.get(target)
    if (!depMap) {
      // 初始化的時候 depMap不存在 初始化一次
      depMap = new Map()
      targetMap.set(target, depMap)
    }

    // 從depMap中 獲取對應的set
    let deps = depMap.get(key)
    if (!deps) {
      // 初始化需要創建一個Set
      deps = new Set()
      depMap.set(key, deps)
    }

    // 將副作用函數放到集合中
    deps.add(effect)
  }
}

// 觸發響應式函數
function trigger (target, key) {
  // 從targetMap中獲取對應副作用函數集合
  // 1. 獲取target對應的map
  const depMap = targetMap.get(target)
  if (!depMap) return

  // 根據key獲取對應的deps
  const deps = depMap.get(key)
  if (deps) {
    // 遍歷執行他們
    deps.forEach(dep => dep())
  }
}
const obj = reactive({
  name: 'chenjing',
  age: 18,
  look: {
    height: '180cm'
  }
})
effect(() => {
  console.log('effect1', obj.name)
})
effect(() => {
  console.log('effect2', obj.name, obj.look.height)
})

setTimeout(() => {
  console.log('----  分割線   -----')
  obj.name = 'jay'
  obj.look.height = '178cm'
}, 1000)
執行結果.png

4. 結尾

好了,到此手寫簡易版vue3的reactive函數完成,希望可以幫助到打擊愛理解vue3數據響應原理。

單純的理解數據響應原理可以理解到Proxy就差不多了
后面依賴收集觸發就是具體到響應后要做的事。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容