vue中的數據劫持

數據雙向綁定作為 Vue 核心功能之一,其實現原理主要分為兩部分:

1.數據劫持
2.發(fā)布訂閱模式

數據劫持

對于 Object 類型蕴掏,主要劫持其屬性的讀取與設置操作障般。在 JavaScript 中對象的屬性主要由一個字符串類型的“名稱”以及一個“屬性描述符”組成,屬性描述符包括以下選項:
value: 該屬性的值盛杰;
writable: 僅當值為 true 時表示該屬性可以被改變挽荡;
get: getter (讀取器);
set: setter (設置器)即供;
configurable: 僅當值為 true 時定拟,該屬性可以被刪除以及屬性描述符可以被改變;
enumerable: 僅當值為 true 時逗嫡,該屬性可以被枚舉青自。
??上述 setter 和 getter 方法就是供開發(fā)者自定義屬性的讀取與設置操作株依,而設置對象屬性的描述符則少不了 Object.defineProperty() 方法:

function defineReactive (obj, key) {
  let val = obj[key]
  Object.defineProperty(obj, key, {
    get () {
      console.log(' === 收集依賴 === ')
      console.log(' 當前值為:' + val)
      return val
    },
    set (newValue) {
      console.log(' === 通知變更 === ')
      console.log(' 當前值為:' + newValue)
      val = newValue
    }
  })
}

const student = {
  name: 'xiaoming'
}

defineReactive(student, 'name') // 劫持 name 屬性的讀取和設置操作

上述代碼通過 Object.defineProperty() 方法設置屬性的 setter 與 getter 方法,從而達到劫持 student 對象中的 name 屬性的讀取和設置操作的目的性穿。
讀者可以發(fā)現勺三,該方法每次只能設置一個屬性,那么就需要遍歷對象來完成其屬性的配置:

 Object.keys(student).forEach(key => defineReactive(student, key))

另外還必須是一個具體的屬性需曾,這也非常的致命吗坚。

假如后續(xù)需要擴展該對象,那么就必須手動為新屬性設置 setter 和 getter 方法呆万,**這就是為什么不在 data 中聲明的屬性無法自動擁有雙向綁定效果的原因 **商源。(這時需要調用 Vue.set() 手動設置)
以上便是對象劫持的核心實現,但是還有以下重要的細節(jié)需要注意:

1谋减、屬性描述符 - configurable

在 JavaScript 中牡彻,對象通過字面量創(chuàng)建時,其屬性描述符默認如下

const foo = {
  name: '123'
}
Object.getOwnPropertyDescriptor(foo, 'name') // { value: '123', writable: true, enumerable: true, configurable: true }

前面也提到了 configurable 的值如果為 false出爹,則無法再修改該屬性的描述符庄吼,所以在設置 setter 和 getter 方法時,需要注意 configurable 選項的取值严就,否則在使用 Object.defineProperty() 方法時會拋出異常:

// 部分重復代碼 這里就不再羅列了总寻。
function defineReactive (obj, key) {
  // ...

  const desc = Object.getOwnPropertyDescriptor(obj, key)

  if (desc && desc.configurable === false) {
    return
  }

  // ...
}

而在 JavaScript 中,導致 configurable 值為 false 的情況還是很多的:
1.可能該屬性在此之前已經通過 Object.defineProperty() 方法設置了 configurable 的值梢为;
2.通過 Object.seal() 方法設置該對象為密封對象渐行,只能修改該屬性的值并且不能刪除該屬性以及修改屬性的描述符;
3.通過 Object.freeze() 方法凍結該對象铸董,相比較 Object.seal() 方法祟印,它更為嚴格之處體現在不允許修改屬性的值。

2粟害、默認 getter 和 setter 方法

另外蕴忆,開發(fā)者可能已經為對象的屬性設置了 getter 和 setter 方法,對于這種情況悲幅,Vue 當然不能破壞開發(fā)者定義的方法孽文,所以 Vue 中還要保護默認的 getter 和 setter 方法:

// 部分重復代碼 這里就不再羅列了
function defineReactive (obj, key) {
  let val = obj[key]

  //....

  // 默認 getter setter
  const getter = desc && desc.get
  const setter = desc && desc.set

  Object.defineProperty(obj, key, {
    get () {
      const value = getter ? getter.call(obj) : val // 優(yōu)先執(zhí)行默認的 getter
      return value
    },
    set (newValue) {
      const value = getter ? getter.call(obj) : val
      // 如果值相同則沒必要更新 === 的坑點 NaN!!!!
      if (newValue === value || (value !== value && newValue !== newValue)) {
        return
      }

      if (getter && !setter) {
        // 用戶未設置 setter
        return
      }

      if (setter) {
        // 調用默認的 setter 方法
        setter.call(obj, newValue)
      } else {
        val = newValue
      }
    }
  })
}

3、遞歸屬性值

最后一種比較重要的情況就是屬性的值可能也是一個對象夺艰,那么在處理對象的屬性時芋哭,需要遞歸處理其屬性值:

function defineReactive (obj, key) {
  let val = obj[key]

  // ...

  // 遞歸處理其屬性值
  const childObj = observe(val)

  // ...
}

遞歸循環(huán)引用對象很容易出現遞歸爆棧問題,對于這種情況郁副,Vue 通過定義 ob 對象記錄已經被設置過 getter 和 setter 方法的對象减牺,從而避免遞歸爆棧的問題。

function isObject (val) {
  const type = val
  return val !== null && (type === 'object' || type === 'function')
}

function observe (value) {
  if (!isObject(value)) {
    return
  }

  let ob
  // 避免循環(huán)引用造成的遞歸爆棧問題
  if (value.hasOwnProperty('__ob__') && value.__obj__ instanceof Observer) {
    ob = value.__ob__
  } else if (Object.isExtensible(value)) {
    // 后續(xù)需要定義諸如 __ob__ 這樣的屬性,所以需要能夠擴展
    ob = new Observer(value)
  }

  return ob
}

上述代碼中提到了對象的可擴展性拔疚,在 JavaScript 中所有對象默認都是可擴展的肥隆,但同時也提供了相應的方法允許對象不可擴展:

const obj = { name: 'xiaoming' }
Object.preventExtensions(obj)
obj.age = 20
console.log(obj.age) // undefined

除了上述方法,還有前面提到的 Object.seal() 和 Object.freeze() 方法

針對 Array 類型的劫持

數組是一種特殊的對象稚失,其下標實際上就是對象的屬性栋艳,所以理論上是可以采用 Object.defineProperty() 方法處理數組對象
但是 Vue 并沒有采用上述方法劫持數組對象句各,筆者猜測主要由于以下兩點:(讀者有更好的見解吸占,歡迎留言。)

1凿宾、特殊的 length 屬性

數組對象的 length 屬性的描述符天生獨特:

const arr = [1, 2, 3]

Object.getOwnPropertyDescriptor(arr, 'length').configurable // false

這就意味著無法通過 Object.defineProperty() 方法劫持 length 屬性的讀取和設置方法矾屯。

相比較對象的屬性,數組下標變化地相對頻繁初厚,并且改變數組長度的方法也比較靈活件蚕,一旦數組的長度發(fā)生變化,那么在無法自動感知的情況下产禾,開發(fā)者只能手動更新新增的數組下標排作,這可是一個很繁瑣的工作。

2亚情、數組的操作場景

數組主要的操作場景還是遍歷妄痪,而對于每一個元素都掛載一個 get 和 set 方法,恐怕也是不小的性能負擔势似。

3、數組方法的劫持

最終 Vue 選擇劫持一些常用的數組操作方法僧著,從而知曉數組的變化情況:

const methods = [
  'push',
  'pop',
  'shift',
  'unshift',
  'sort',
  'reverse',
  'splice'
]

數組方法的劫持涉及到原型相關的知識履因,首先數組實例大部分方法都是來源于 Array.prototype 對象。

但是這里不能直接篡改 Array.prototype 對象盹愚,這樣會影響所有的數組實例栅迄,為了避免這種情況,需要采用原型繼承得到一個新的原型對象:

const arrayProto = Array.prototype
const injackingPrototype = Object.create(arrayProto)

拿到新的原型對象之后皆怕,再重寫這些常用的操作方法:

methods.forEach(method => {
  const originArrayMethod = arrayProto[method]
  injackingPrototype[method] = function (...args) {
    const result = originArrayMethod.apply(this, args)
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) {
      // 對于新增的元素毅舆,繼續(xù)劫持
      // ob.observeArray(inserted)
    }
    // 通知變化
    return result
  }
})

最后,更新劫持數組實例的原型愈腾,在 ES6 之前憋活,可以通過瀏覽器私有屬性 proto 指定原型,之后虱黄,便可以采用如下方法:

Object.setPrototypeOf(arr, injackingPrototype)

順便提一下悦即,采用 Vue.set() 方法設置數組元素時,Vue 內部實際上是調用劫持后的 splice() 方法來觸發(fā)更新

總結

由上述內容可知,Vue 中的數據劫持分為兩大部分:
1.針對 Object 類型辜梳,采用 Object.defineProperty() 方法劫持屬性的讀取和設置方法粱甫;
2.針對 Array 類型,采用原型相關的知識劫持常用的函數作瞄,從而知曉當前數組發(fā)生變化茶宵。
并且 Object.defineProperty() 方法存在以下缺陷:
1.每次只能設置一個具體的屬性,導致需要遍歷對象來設置屬性宗挥,同時也導致了無法探測新增屬性乌庶;
2.屬性描述符 configurable 對其的影響是致命的。

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子弧械,更是在濱河造成了極大的恐慌韩肝,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凄杯,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機拙泽,發(fā)現死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來裸燎,“玉大人顾瞻,你說我怎么就攤上這事〉侣蹋” “怎么了荷荤?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長移稳。 經常有香客問我蕴纳,道長,這世上最難降的妖魔是什么个粱? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任古毛,我火速辦了婚禮,結果婚禮上都许,老公的妹妹穿的比我還像新娘稻薇。我一直安慰自己,他們只是感情好胶征,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布塞椎。 她就那樣靜靜地躺著,像睡著了一般睛低。 火紅的嫁衣襯著肌膚如雪忱屑。 梳的紋絲不亂的頭發(fā)上蹬敲,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天,我揣著相機與錄音莺戒,去河邊找鬼伴嗡。 笑死,一個胖子當著我的面吹牛从铲,可吹牛的內容都是我干的瘪校。 我是一名探鬼主播,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼名段,長吁一口氣:“原來是場噩夢啊……” “哼阱扬!你這毒婦竟也來了?” 一聲冷哼從身側響起伸辟,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤麻惶,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后信夫,有當地人在樹林里發(fā)現了一具尸體窃蹋,經...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年静稻,在試婚紗的時候發(fā)現自己被綠了警没。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡振湾,死狀恐怖杀迹,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情押搪,我是刑警寧澤树酪,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站大州,受9級特大地震影響续语,放射性物質發(fā)生泄漏。R本人自食惡果不足惜摧茴,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一绵载、第九天 我趴在偏房一處隱蔽的房頂上張望埂陆。 院中可真熱鬧苛白,春花似錦、人聲如沸焚虱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹃栽。三九已至躏率,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背薇芝。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工蓬抄, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人夯到。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓嚷缭,卻偏偏與公主長得像,于是被迫代替她去往敵國和親耍贾。 傳聞我的和親對象是個殘疾皇子阅爽,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

推薦閱讀更多精彩內容