vue3與vue2的區(qū)別之?dāng)?shù)據(jù)響應(yīng)——手寫(xiě)vue3的reactive,理解vue3數(shù)據(jù)響應(yīng)式原理

1、 數(shù)據(jù)響應(yīng)式

首先請(qǐng)大家認(rèn)真的思考一個(gè)問(wèn)題:什么是數(shù)據(jù)響應(yīng)式

答:數(shù)據(jù)變化是可偵測(cè)的,并且和數(shù)據(jù)相關(guān)的內(nèi)容可以更新。

?這里一定要明確一個(gè)概念,數(shù)據(jù)響應(yīng)式和視圖更新是沒(méi)有關(guān)系的!數(shù)據(jù)響應(yīng)式是一種機(jī)制,一種數(shù)據(jù)變化的偵測(cè)機(jī)制。而實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式這種機(jī)制的方法不唯一。
那么,vue是如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式的?vue2和vue3的數(shù)據(jù)響應(yīng)式有什么區(qū)別?

2、vue如何實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式?

要知道,vue3.x實(shí)現(xiàn)數(shù)據(jù)響應(yīng)的方案跟vue2.x是不一樣的,所以在這里我將vue2.xvue3.x分別說(shuō)說(shuō)。這也是理解vue2.xvue3.x區(qū)別的時(shí)候,可以指出來(lái)的一個(gè)巨大的區(qū)別。

2.1 vue2.x的實(shí)現(xiàn)方案

我貼上一個(gè)vue2.x源碼-Object的變化偵測(cè)解讀的鏈接,方便大家理解和后續(xù)關(guān)于vue2.x的學(xué)習(xí)需要。
(特別是還沒(méi)閱讀過(guò)vue源碼的同學(xué),可以獨(dú)自過(guò)一遍這個(gè)文檔,能對(duì)vue有一個(gè)更深的認(rèn)識(shí))

在下面vue2的源碼中可以看到,Observer類(lèi)會(huì)通過(guò)遞歸的方式把一個(gè)對(duì)象的所有屬性都轉(zhuǎn)化成可觀測(cè)對(duì)象,所以我們可以知道vue2需要遍歷對(duì)象的所有的key。其實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式的核心思想就是通過(guò)defineProperty,去定義getset等方法。從而能夠攔截到對(duì)象屬性的訪問(wèn)和變更。

/**
 * Observer類(lèi)會(huì)通過(guò)遞歸的方式把一個(gè)對(duì)象的所有屬性都轉(zhuǎn)化成可觀測(cè)對(duì)象
 */
export class Observer {
  constructor (value) {
    this.value = value
    // 給value新增一個(gè)__ob__屬性,值為該value的Observer實(shí)例
    // 相當(dāng)于為value打上標(biāo)記,表示它已經(jīng)被轉(zhuǎn)化成響應(yīng)式了,避免重復(fù)操作
    def(value,'__ob__',this)
    if (Array.isArray(value)) {
      // 當(dāng)value為數(shù)組時(shí)的邏輯
      // ...
    } else {
      this.walk(value)
    }
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}
/**
 * 使一個(gè)對(duì)象轉(zhuǎn)化成可觀測(cè)對(duì)象
 * @param { Object } obj 對(duì)象
 * @param { String } key 對(duì)象的key
 * @param { Any } val 對(duì)象的某個(gè)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;
    }
  })
}

在日常開(kāi)發(fā)中,產(chǎn)品經(jīng)理總是會(huì)跟我們說(shuō),我們做了xxxx就是為了解決客戶(hù)的xxxx痛點(diǎn)。
那么,在繼續(xù)往下閱讀的時(shí)候,可以先思考一下vue2這樣的實(shí)現(xiàn)方案的痛點(diǎn)有什么?或者說(shuō)缺點(diǎn)有什么?
因?yàn)樽鳛榭蛻?hù)(使用vue開(kāi)發(fā)的前端同學(xué))的我們需要知道,vue3是否解決了我們的痛點(diǎn)?

vue2的缺點(diǎn):(僅僅是關(guān)于數(shù)據(jù)響應(yīng)造成的缺點(diǎn)哦!)

  • 1、影響初始化速度、數(shù)據(jù)過(guò)大時(shí)的資源問(wèn)題
    (在源碼的Observer方法上,對(duì)象的每一個(gè)屬性都要被攔截。所有的key都要有一次循環(huán)和遞歸)
  • 2、數(shù)組的特殊處理,導(dǎo)致其修改數(shù)據(jù)不能使用索引
    (原因在于defineProperty不支持?jǐn)?shù)組,參考vue源碼-Array的變化偵測(cè)
  • 3、動(dòng)態(tài)添加或刪除對(duì)象屬性無(wú)法被偵測(cè)
    defineProperty哭著對(duì)我說(shuō):臣妾的的setter函數(shù)辦不到呀)

對(duì)于沒(méi)閱讀過(guò)vue源碼的前端開(kāi)發(fā)來(lái)說(shuō),應(yīng)該也遇到過(guò)修改了數(shù)組,或者修改對(duì)象后發(fā)現(xiàn),啥變化也沒(méi)有,一頭霧水,拍桌子直呼:vue真垃圾,有bug。
其實(shí)這些霧水大都是上面的2、3兩點(diǎn)引發(fā)的,vue也都提供了解決方案:$set$delete,我都整理好了,需要理解的直接移步深入響應(yīng)式原理
但是,這就體驗(yàn)極差

??小故事一則:去年還沒(méi)閱讀源碼的時(shí)候,公司一個(gè)大版本的發(fā)布后,出現(xiàn)了一個(gè)不是很?chē)?yán)重,卻影響使用范圍很廣的一個(gè)bug,我們從凌晨2點(diǎn)修到4點(diǎn),最后還是一個(gè)大牛搞了幾輪實(shí)驗(yàn)發(fā)現(xiàn)了問(wèn)題,說(shuō)vue有bug,某某地方賦值需要用$set。沒(méi)錯(cuò),就是上面痛點(diǎn)里的第3點(diǎn)。原因還是我們太菜呀,沒(méi)有閱讀相關(guān)源碼。

2.2 vue3.x的實(shí)現(xiàn)方案

文章開(kāi)頭我就強(qiáng)調(diào)了:數(shù)據(jù)響應(yīng)式是一種機(jī)制,一種數(shù)據(jù)變化的偵測(cè)機(jī)制。而實(shí)現(xiàn)數(shù)據(jù)響應(yīng)式這種機(jī)制的方法不唯一。于是乎,vue3.x來(lái)了,他帶著vue2.x痛點(diǎn)的解決方案來(lái)了!

解決方案其實(shí)一點(diǎn)也不神秘,在ES6之后,出現(xiàn)了一個(gè)新的特性:ProxyVue3.x在使用了Proxy之后,痛點(diǎn)們一下子就全都解決了。Proxy是怎么解決的呢?請(qǐng)聽(tīng)下回...請(qǐng)繼續(xù)往下看哈看完手寫(xiě)reactive之后,就全都明白啦。
順便給個(gè)Proxy的MDN地址: Proxy MDN傳松門(mén)

3、手寫(xiě)reactive

在vue3.x中,定義響應(yīng)式對(duì)象的方法如下:

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

3.1 測(cè)試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,生效。在簡(jiǎn)易版的reactive,我們要添加基本的屬性getsetdeleteProperty。同時(shí),在上面代碼的get里直接return target[key],一來(lái)不太優(yōu)雅、二來(lái)可能報(bào)錯(cuò)。我們先來(lái)看看vue3是怎么處理的:
vue3源碼圖1.png

再來(lái)一個(gè)傳送門(mén):Reflect - MDN

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

3.2 reactive基本形態(tài)

讓我們來(lái)學(xué)習(xí)一下vue3的寫(xiě)法后,加上了Reflect后,于是我們最基本的reactive就是下面這樣的:

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key) // 可以直接return target[key],避免報(bào)錯(cuò)和代碼的優(yōu)雅性,模仿源碼采用Reflect
      console.log('get', key)
      return (typeof res === 'object') ? reactive(res) : res // 子屬性若是對(duì)象 需要再次代理
    },
    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基本形態(tài).png

通過(guò)跑腳本后的控制臺(tái),可以看到訪問(wèn)屬性成功的觸發(fā)了get。同時(shí)新增屬性也觸發(fā)了set
到這里為止,vue2中的數(shù)據(jù)響應(yīng)式在vue3里其實(shí)已經(jīng)完全實(shí)現(xiàn)了。回過(guò)頭來(lái)想想,是不是沒(méi)那么難理解了吧。沒(méi)有vue2的循環(huán)遍歷遞歸,只是上了Proxy的車(chē)
當(dāng)然了在Vue3內(nèi)真正的實(shí)現(xiàn),肯定不是這么幾行代碼就搞定的。只是響應(yīng)式的原理就是利用了Proxy

既然要手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的reactive函數(shù),讓我們繼續(xù)往下閱讀。
目前只是想簡(jiǎn)單理解vue3數(shù)據(jù)響應(yīng)式原理,了解vue3數(shù)據(jù)響應(yīng)和vue2數(shù)據(jù)響應(yīng)的區(qū)別的同學(xué)可以直接點(diǎn)贊了哈哈,鼓勵(lì)一下互相學(xué)習(xí)進(jìn)步??

3.3 依賴(lài)的收集、觸發(fā)

既然要手寫(xiě)實(shí)現(xiàn)一個(gè)簡(jiǎn)易的reactive函數(shù),我們就繼續(xù)。
要實(shí)現(xiàn)reactive函數(shù),我們就要在get內(nèi)進(jìn)行依賴(lài)收集,在set中進(jìn)行觸發(fā)。即便是vue2也是通過(guò)類(lèi)似的發(fā)布訂閱模式體現(xiàn)。在這里,我們也是通過(guò)發(fā)布訂閱模式去完成。

首先是依賴(lài)收集:在get內(nèi),我們需要對(duì)依賴(lài)進(jìn)行收集。在依賴(lài)收集的時(shí)候,將其按照依賴(lài)關(guān)系放入map中映射。
然后就是依賴(lài)觸發(fā):在set中,需要觸發(fā)響應(yīng)式函數(shù)。即完成了發(fā)布訂閱。

下面代碼 有需要的可以直接復(fù)制粘貼,直接跑。可以自行斷點(diǎn)看看,有疑問(wèn)的歡迎交流。

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      const res = Reflect.get(target, key)
      console.log('get', key)
      // 依賴(lài)收集
      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)
      // 觸發(fā)
      trigger(target, key)
      return res
    },
    deleteProperty() {
      const res = Reflect.deleteProperty(target, key)
      console.log('deleteProperty', key)
      return res
    }
  })
}

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

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

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

// 保存映射關(guān)系的數(shù)據(jù)結(jié)構(gòu)
const targetMap = new WeakMap()

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

    // 從depMap中 獲取對(duì)應(yīng)的set
    let deps = depMap.get(key)
    if (!deps) {
      // 初始化需要?jiǎng)?chuàng)建一個(gè)Set
      deps = new Set()
      depMap.set(key, deps)
    }

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

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

  // 根據(jù)key獲取對(duì)應(yīng)的deps
  const deps = depMap.get(key)
  if (deps) {
    // 遍歷執(zhí)行他們
    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)
執(zhí)行結(jié)果.png

4. 結(jié)尾

好了,到此手寫(xiě)簡(jiǎn)易版vue3的reactive函數(shù)完成,希望可以幫助到打擊愛(ài)理解vue3數(shù)據(jù)響應(yīng)原理。

單純的理解數(shù)據(jù)響應(yīng)原理可以理解到Proxy就差不多了
后面依賴(lài)收集觸發(fā)就是具體到響應(yīng)后要做的事。

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

推薦閱讀更多精彩內(nèi)容