收藏好這篇,別再只說「數據劫持」了

最近接觸了一些面試者,當我問起「Vue 如何實現數據雙向綁定」時,會脫口而出「數據劫持」,而後呢?而後就沒有而後了╮(╯_╰)╭。確實,「數據劫持」是基礎,但遠不是面試官想聽到的答案,不如花個十分鐘看看本文,下次照着回答就行了

「雙向綁定」 自己

要解答問題,首先要理解問題: 數據雙向綁定 是一種模式,web語境下通常指數據從dom到JS對象之間的自動同步。DOM 與 JS 被隔離在兩個不一樣的運行時上,互相之間須要經過命令式的 DOM接口 溝通:DOM 須要正確觸發事件,將信息傳輸給JS程序;而JS也須要在狀態變動後,有意識地調用適當的接口,改變DOM內容。這種方式會引發兩個問題:javascript

  1. 狀態的管理與展示是徹底剝離開的兩套不一樣邏輯,須要刻意保持同步,這是很高的開發成本
  2. DOM 規範定義了很多接口,並且有兼容性問題,這是很高的學習成本

雙向綁定經過各類各樣的設計,將數據從 DOM 到 JS 或者從 JS 到 DOM 的同步過程,封裝在框架自己,上層代碼脫離了對底層接口的依賴,只須要關注狀態管理邏輯。vue

Object.defineProperty

咱們要討論的第一個問題是,如何檢測 JS 對象屬性發生的變動?最簡單粗暴的方法是「髒檢查」,舊版本的Angular就是用的這種方法,在各類可能引起狀態變動的事件後,啓動一次髒檢查。這種方法很直觀,但實現上須要考慮不少問題:java

  1. 髒檢查過程當中可能會觸發新的數據變動,也就進入死循環了
  2. 髒檢查的實現必須保留新舊兩份數據的副本
  3. 髒檢查必須預知全部可能觸發狀態變動的時機,也就意味着須要對部分原生接口進行包裹(包括 setTimeoutrequestNextAnimationFrame等)
  4. ...

Vue 則採用元編程接口 Object.defineProperty 實現的。 在組件初始化,會調用該接口,將對象屬性包裝爲getset函數,將代碼「埋入」屬性是「獲取」、「修改」行爲中。看個簡單例子,直觀感覺下:git

const person = {};

// 嘿嘿,誰都改不了個人名字
Object.defineProperty(person, 'name', {
    get() {
        return 'van';
    },
    set(v) {
        console.log('they want change my name');
    }
});

console.log(person.name);
// van
person.name = 'tec';
// they want change my name
console.log(person.name);
// van

依賴管理方案

Object.defineProperty 只是解決了狀態變動後,如何觸發通知的問題,那要通知誰呢?誰會關心那些屬性發生了變化呢?在 Vue 中,使用 Dep 解耦了依賴者與被依賴者之間關係的肯定過程。簡單來講:github

  • 第一步,經過 Observer 提供的接口,遍歷狀態對象,給對象的每一個屬性、子屬性都綁定了一個專用的 Dep 對象。這裏的狀態對象主要指組件當中的data屬性。
  • 第二步,建立三中類型的watcher:web

    1. 調用 initComputedcomputed 屬性轉化爲 watcher 實例
    2. 調用 initWatch 方法,將 watch 配置轉化爲 watcher 實例
    3. 調用 mountComponent 方法,爲 render 函數綁定 watcher 實例
  • 第三步,狀態變動後,觸發 dep.notify() 函數,該函數再進一步觸發 Watcher 對象 update 函數,執行watcher的從新計算。

對應下圖:面試

圖片描述

注意,Vue 組件中的 render 函數,咱們能夠單純將其視爲一種特殊的 computed 函數,在它所對應的 Watcher 對象發生變化時,觸發執行render,生成新的 virutal-dom 結構,再交由 Vue 作diff,更新視圖。編程

結語

本文到這裏就結束了,更多內容能夠嘗試看看源碼,代碼裏面的設計模式很是值得學習。設計模式

Vue 使用數據劫持做爲底層支撐,又設計了一套精妙的依賴管理方案解耦依賴。但數據劫持方案也有其難以解決的痛點:數組

  1. 只能應用於簡單對象
  2. 對數組無效,因此須要包裝數組方法
  3. 屬性劫持的出發點是「變」,因此vue沒法很好接入「immutable」模式
相關文章
相關標籤/搜索