欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Vue源碼學(xué)習(xí)之響應(yīng)式是如何實(shí)現(xiàn)的

 更新時(shí)間:2021年10月15日 09:51:45   作者:慧嚯  
最近接觸了vue.js,一度非常好奇vue.js是如何監(jiān)測(cè)數(shù)據(jù)更新并且重新渲染頁(yè)面,這篇文章主要給大家介紹了關(guān)于Vue源碼學(xué)習(xí)之響應(yīng)式是如何實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下

前言

作為前端開發(fā),我們的日常工作就是將數(shù)據(jù)渲染到頁(yè)面➕處理用戶交互。在 Vue 中,數(shù)據(jù)變化時(shí)頁(yè)面會(huì)重新渲染,比如我們?cè)陧?yè)面上顯示一個(gè)數(shù)字,旁邊有一個(gè)點(diǎn)擊按鈕,每次點(diǎn)擊一下按鈕,頁(yè)面上所顯示的數(shù)字會(huì)加一,這要怎么去實(shí)現(xiàn)呢?
按照原生 JS 的邏輯想一想,我們應(yīng)該做三件事:監(jiān)聽點(diǎn)擊事件,在事件處理函數(shù)中修改數(shù)據(jù),然后手動(dòng)去修改 DOM 重新渲染,這和我們使用 Vue 的最大區(qū)別在于多了一步【手動(dòng)去修改DOM重新渲染】,這一步看起來(lái)簡(jiǎn)單,但我們得考慮幾個(gè)問(wèn)題:

  • 需要修改哪個(gè) DOM ?
  • 數(shù)據(jù)每變化一次就需要去修改一次 DOM 嗎?
  • 怎么去保證修改 DOM 的性能?

所以要實(shí)現(xiàn)一個(gè)響應(yīng)式系統(tǒng)并不簡(jiǎn)單🍳,來(lái)結(jié)合 Vue 源碼學(xué)習(xí)一下 Vue 中優(yōu)秀的思想叭~

一、一個(gè)響應(yīng)式系統(tǒng)的關(guān)鍵要素

1、如何監(jiān)聽數(shù)據(jù)變化

顯然通過(guò)監(jiān)聽所有用戶交互事件來(lái)獲取數(shù)據(jù)變化是非常繁瑣的,且有些數(shù)據(jù)的變動(dòng)也不一定是用戶觸發(fā)的,那Vue是怎么監(jiān)聽數(shù)據(jù)變化的呢?—— Object.defineProperty

Object.defineProperty 方法為什么能監(jiān)聽數(shù)據(jù)變化?該方法可以直接在一個(gè)對(duì)象上定義一個(gè)新屬性,或者修改一個(gè)對(duì)象的現(xiàn)有屬性, 并返回這個(gè)對(duì)象,先來(lái)看一下它的語(yǔ)法:

Object.defineProperty(obj, prop, descriptor)
// obj是傳入的對(duì)象,prop是要定義或修改的屬性,descriptor是屬性描述符

這里比較核心的是 descriptor,它有很多可選鍵值。這里我們最關(guān)心的是 get 和 set,其中 get 是一個(gè)給屬性提供的 getter 方法,當(dāng)我們?cè)L問(wèn)了該屬性的時(shí)候會(huì)觸發(fā) getter 方法;set 是一個(gè)給屬性提供的 setter 方法,當(dāng)我們對(duì)該屬性做修改的時(shí)候會(huì)觸發(fā) setter 方法。

簡(jiǎn)言之,一旦一個(gè)數(shù)據(jù)對(duì)象擁有了 getter 和 setter,我們就可以輕松監(jiān)聽它的變化了,并可將其稱之為響應(yīng)式對(duì)象。具體怎么做呢?

function observe(data) {
  if (isObject(data)) {
    Object.keys(data).forEach(key => {
      defineReactive(data, key)
    })
  }
}

function defineReactive(obj, prop) {
  let val = obj[prop]
  let dep = new Dep() // 用來(lái)收集依賴
  Object.defineProperty(obj, prop, {
    get() {
      // 訪問(wèn)對(duì)象屬性了,說(shuō)明依賴當(dāng)前對(duì)象屬性,把依賴收集起來(lái)
      dep.depend()
      return val
    }
    set(newVal) {
      if (newVal === val) return
      // 數(shù)據(jù)被修改了,該通知相關(guān)人員更新相應(yīng)的視圖了
      val = newVal
      dep.notify()     
    }
  }) 
  // 深層監(jiān)聽
  if (isObject(val)) {
    observe(val)
  }
  return obj
}

這里我們需要一個(gè) Dep 類(dependency)來(lái)做依賴收集🎭

PS:Object.defineProperty 只能監(jiān)聽已存在的屬性,對(duì)于新增的屬性就無(wú)能為力了,同時(shí)無(wú)法監(jiān)聽數(shù)組的變化(Vue2中通過(guò)重寫數(shù)組原型上的方法解決這一問(wèn)題),所以在 Vue3 中將其換成了功能更強(qiáng)大的Proxy。

2、如何進(jìn)行依賴收集——實(shí)現(xiàn) Dep 類

基于構(gòu)造函數(shù)實(shí)現(xiàn):

function Dep() {
  // 用deps數(shù)組來(lái)存儲(chǔ)各項(xiàng)依賴
  this.deps = []
}
// Dep.target用來(lái)記錄正在運(yùn)行的watcher實(shí)例,這是一個(gè)全局唯一的 Watcher 
// 這是一個(gè)非常巧妙的設(shè)計(jì),因?yàn)镴S是單線程的,在同一時(shí)間只能有一個(gè)全局的 Watcher 被計(jì)算
Dep.target = null

// 在原型上定義depend方法,每個(gè)實(shí)例都能訪問(wèn)
Dep.prototype.depend = function() {
  if (Dep.target) {
    this.deps.push(Dep.target)
  }
}
// 在原型上定義notify方法,用于通知watcher更新
Dep.prototype.notify = function() {
  this.deps.forEach(watcher => {
    watcher.update()
  })
}
// Vue中會(huì)有嵌套的邏輯,比如組件嵌套,所以利用棧來(lái)記錄嵌套的watcher 
// 棧,先入后出 
const targetStack = [] 
function pushTarget(_target) { 
  if (Dep.target) targetStack.push(Dep.target) 
  Dep.target = _target 
} 
function popTarget() { 
  Dep.target = targetStack.pop() 
}

這里主要理解原型上的兩個(gè)方法:depend 和 notify,一個(gè)用于添加依賴,一個(gè)用于通知更新。我們說(shuō)收集“依賴”,那 this.deps 數(shù)組里到底存的是啥東西啊?Vue 設(shè)置了 Watcher 的概念用作依賴表示,即 this.deps 里收集的是一個(gè)個(gè) Watcher。

3、數(shù)據(jù)變化時(shí)如何更新——實(shí)現(xiàn) Watcher 類

Watcher,在Vue中有三種類型,分別用于頁(yè)面渲染以及computed和watch這兩個(gè)API,為了區(qū)分,將不同用處的 Watcher 分別稱為 renderWatcher、computedWatcher 和 watchWatcher。

用 class 實(shí)現(xiàn)一下:

class Watcher {
  constructor(expOrFn) {
    // 這里傳入?yún)?shù)不是函數(shù)時(shí)需要解析,parsePath略
    this.getter = typeof expOrFn === 'function' ? expOrFn : parsePath(expOrFn)
    this.get()
  }
  // class中定義函數(shù)不需要寫function
  get() {
    // 執(zhí)行到這時(shí),this是當(dāng)前的watcher實(shí)例,也是Dep.target
    pushTarget(this)
    this.value = this.getter()
    popTarget()
  }
  update() {
    this.get()
  }
}

到這里,一個(gè)簡(jiǎn)單的響應(yīng)式系統(tǒng)就成形了,總結(jié)來(lái)說(shuō):Object.defineProperty 讓我們能夠知道誰(shuí)訪問(wèn)了數(shù)據(jù)以及什么時(shí)候數(shù)據(jù)發(fā)生變化,Dep 可以記錄都有哪些 DOM 和某個(gè)數(shù)據(jù)有關(guān),Watcher 可以在數(shù)據(jù)變化的時(shí)候通知 DOM 去更新。
Watcher 和 Dep 是一個(gè)非常經(jīng)典的觀察者設(shè)計(jì)模式的實(shí)現(xiàn)。

二、虛擬 DOM 和 diff

1、虛擬 DOM 是什么?

虛擬 DOM 是用 JS 中的對(duì)象來(lái)表示真實(shí)的DOM,如果有數(shù)據(jù)變動(dòng),先在虛擬 DOM 上改動(dòng),最后再去改動(dòng)真實(shí)的DOM,good idea!💡

關(guān)于虛擬 DOM 的優(yōu)勢(shì),還是聽尤大的:

在我看來(lái) Virtual DOM 真正的價(jià)值從來(lái)都不是性能,而是它 1) 為函數(shù)式的 UI 編程方式打開了大門;2) 可以渲染到 DOM 以外的 backend。

舉個(gè)例子:

<template>
  <div id="app" class="container">
    <h1>HELLO WORLD!</h1>
  </div>
</template>
// 對(duì)應(yīng)的vnode 
{ 
  tag: 'div', 
  props: { id: 'app', class: 'container' }, 
  children: { tag: 'h1', children: 'HELLO WORLD!' } 
}

我們可以這樣去定義:

function VNode(tag, data, childern, text, elm) { 
  this.tag = tag 
  this.data = data 
  this.childern = childern 
  this.text = text 
  this.elm = elm // 對(duì)真實(shí)節(jié)點(diǎn)的引用 
}

2、diff 算法——新舊節(jié)點(diǎn)對(duì)比

數(shù)據(jù)變化時(shí),會(huì)觸發(fā)渲染 watcher 的回調(diào),更新視圖。Vue 源碼中在更新視圖時(shí)用 patch 方法比較新舊節(jié)點(diǎn)的異同。

(1)判斷新舊節(jié)點(diǎn)是不是相同節(jié)點(diǎn)

function sameVNode()
function sameVnode(a, b) { 
  return a.key === b.key && 
  ( a.tag === b.tag && 
    a.isComment === b.isComment && 
    isDef(a.data) === isDef(b.data) && 
    sameInputType(a, b) 
  ) 
 }

(2)若新舊節(jié)點(diǎn)不同

替換舊節(jié)點(diǎn):創(chuàng)建新節(jié)點(diǎn) -->  刪除舊節(jié)點(diǎn)

(3)若新舊節(jié)點(diǎn)相同

  • 都沒(méi)有子節(jié)點(diǎn),好說(shuō)
  • 一個(gè)有子節(jié)點(diǎn)一個(gè)沒(méi)有,好說(shuō),要么刪除個(gè)子節(jié)點(diǎn)要么新增個(gè)子節(jié)點(diǎn)
  • 都有子節(jié)點(diǎn),這可就有點(diǎn)復(fù)雜了,執(zhí)行updateChildren:
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  // 以上是新舊Vnode的首尾指針、新舊Vnode的首尾節(jié)點(diǎn)

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 如果不滿足這個(gè)while條件,表示新舊Vnode至少有一個(gè)已經(jīng)遍歷了一遍了,就退出循環(huán)
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 比較舊的開頭和新的開頭是否是相同節(jié)點(diǎn)
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 比較舊的結(jié)尾和新的結(jié)尾是否是相同節(jié)點(diǎn)
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 比較舊的開頭和新的結(jié)尾是否是相同節(jié)點(diǎn)
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 比較舊的結(jié)尾和新的開頭是否是相同節(jié)點(diǎn)
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      // 設(shè)置key和不設(shè)置key的區(qū)別:
      // 不設(shè)key,newCh和oldCh只會(huì)進(jìn)行頭尾兩端的相互比較,設(shè)key后,除了頭尾兩端的比較外,還會(huì)從用key生成的對(duì)象oldKeyToIdx中查找匹配的節(jié)點(diǎn),所以為節(jié)點(diǎn)設(shè)置key可以更高效的利用dom。
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      // 抽取出oldVnode序列的帶有key的節(jié)點(diǎn)放在map中,然后再遍歷新的vnode序列
      // 判斷該vnode的key是否在map中,若在則找到該key對(duì)應(yīng)的oldVnode,如果此oldVnode與遍歷到的vnode是sameVnode的話,則復(fù)用dom并移動(dòng)dom節(jié)點(diǎn)位置
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

這里主要的邏輯是:新節(jié)點(diǎn)的頭和尾與舊節(jié)點(diǎn)的頭和尾分別比較,看是不是相同節(jié)點(diǎn),如果是就直接patchVnode;否則的話,用一個(gè) Map 存儲(chǔ)舊節(jié)點(diǎn)的 key,然后遍歷新節(jié)點(diǎn)的 key 看它們是不是在舊節(jié)點(diǎn)中存在,相同 key 那就復(fù)用;這里時(shí)間復(fù)雜度是O(n),空間復(fù)雜度也是O(n),用空間換時(shí)間~

diff 算法主要是為了減少更新量,找到最小差異部分 DOM ,只更新差異部分。

三、nextTick

所謂 nextTick,即下一個(gè) tick,那 tick 是什么呢?

我們知道 JS 執(zhí)行是單線程的,它處理異步邏輯是基于事件循環(huán),主要分為以下幾步:

  1. 所有同步任務(wù)都在主線程上執(zhí)行,形成一個(gè)執(zhí)行棧(execution context stack);
  2. 主線程之外,還存在一個(gè)"任務(wù)隊(duì)列"(task queue)。只要異步任務(wù)有了運(yùn)行結(jié)果,就在"任務(wù)隊(duì)列"之中放置一個(gè)事件;
  3. 一旦"執(zhí)行棧"中的所有同步任務(wù)執(zhí)行完畢,系統(tǒng)就會(huì)讀取"任務(wù)隊(duì)列",看看里面有哪些事件。那些對(duì)應(yīng)的異步任務(wù),于是結(jié)束等待狀態(tài),進(jìn)入執(zhí)行棧,開始執(zhí)行;
  4. 主線程不斷重復(fù)上面的第三步。

主線程的執(zhí)行過(guò)程就是一個(gè) tick,而所有的異步結(jié)果都是通過(guò) “任務(wù)隊(duì)列” 來(lái)調(diào)度。 消息隊(duì)列中存放的是一個(gè)個(gè)的任務(wù)(task)。 規(guī)范中規(guī)定 task 分為兩大類,分別是 macro task 和 micro task,并且每個(gè) macro task 結(jié)束后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) { 
  // 1. Handle current MACRO-TASK 
  handleMacroTask()
  // 2. Handle all MICRO-TASK 
  for (microTask of microTaskQueue) { 
    handleMicroTask(microTask)
  } 
}

在瀏覽器環(huán)境中,常見的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate、setInterval;常見的 micro task 有 MutationObsever 和 Promise.then。

我們知道數(shù)據(jù)的變化到 DOM 的重新渲染是一個(gè)異步過(guò)程,發(fā)生在下一個(gè) tick。比如我們平時(shí)在開發(fā)的過(guò)程中,從服務(wù)端接口去獲取數(shù)據(jù)的時(shí)候,數(shù)據(jù)做了修改,如果我們的某些方法去依賴了數(shù)據(jù)修改后的 DOM 變化,我們就必須在 nextTick 后執(zhí)行。比如下面的偽代碼:

getData(res).then(() => { 
  this.xxx = res.data 
  this.$nextTick(() => { // 這里我們可以獲取變化后的 DOM }) 
})

四、總結(jié)

到此這篇關(guān)于Vue源碼學(xué)習(xí)之響應(yīng)式是如何實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)Vue響應(yīng)式實(shí)現(xiàn)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Vue項(xiàng)目中使用Base64

    Vue項(xiàng)目中使用Base64

    在vue項(xiàng)目中有時(shí)會(huì)使用到Base6464轉(zhuǎn)碼,現(xiàn)將自己使用的一種方法記錄下來(lái),對(duì)vue使用Base64相關(guān)知識(shí)感興趣的朋友一起看看吧
    2024-02-02
  • Vue項(xiàng)目引入PWA的步驟

    Vue項(xiàng)目引入PWA的步驟

    這篇文章主要介紹了Vue項(xiàng)目引入PWA的步驟,幫助大家更好的理解和學(xué)習(xí)使用vue,感興趣的朋友可以了解下
    2021-04-04
  • 詳解element-ui 組件el-autocomplete使用踩坑記錄

    詳解element-ui 組件el-autocomplete使用踩坑記錄

    最近使用了el-autocomplete組件,本文主要介紹了element-ui 組件el-autocomplete使用踩坑記錄,文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-03-03
  • Vue.js如何使用Socket.IO的示例代碼

    Vue.js如何使用Socket.IO的示例代碼

    這篇文章主要介紹了Vue.js如何使用Socket.IO的示例代碼,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-09-09
  • vue實(shí)現(xiàn)評(píng)論列表

    vue實(shí)現(xiàn)評(píng)論列表

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)評(píng)論列表,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-04-04
  • vue請(qǐng)求數(shù)據(jù)的三種方式

    vue請(qǐng)求數(shù)據(jù)的三種方式

    這篇文章主要介紹了vue請(qǐng)求數(shù)據(jù)的三種方式,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2020-03-03
  • vue動(dòng)態(tài)生成新表單并且添加驗(yàn)證校驗(yàn)規(guī)則方式

    vue動(dòng)態(tài)生成新表單并且添加驗(yàn)證校驗(yàn)規(guī)則方式

    這篇文章主要介紹了vue動(dòng)態(tài)生成新表單并且添加驗(yàn)證校驗(yàn)規(guī)則方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-10-10
  • Vue實(shí)現(xiàn)導(dǎo)航欄點(diǎn)擊當(dāng)前標(biāo)簽變色功能

    Vue實(shí)現(xiàn)導(dǎo)航欄點(diǎn)擊當(dāng)前標(biāo)簽變色功能

    這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)導(dǎo)航欄點(diǎn)擊當(dāng)前標(biāo)簽變色功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • vue2.0$nextTick監(jiān)聽數(shù)據(jù)渲染完成之后的回調(diào)函數(shù)方法

    vue2.0$nextTick監(jiān)聽數(shù)據(jù)渲染完成之后的回調(diào)函數(shù)方法

    今天小編就為大家分享一篇vue2.0$nextTick監(jiān)聽數(shù)據(jù)渲染完成之后的回調(diào)函數(shù)方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2018-09-09
  • Electron vue的使用教程圖文詳解

    Electron vue的使用教程圖文詳解

    Electron相當(dāng)于一個(gè)瀏覽器的外殼,可以把網(wǎng)頁(yè)程序嵌入到殼里面,可以運(yùn)行在桌面上的一個(gè)程序,可以把網(wǎng)頁(yè)打包成一個(gè)在桌面運(yùn)行的程序。這篇文章主要介紹了electron-vue多顯示屏下將新窗口投放是其他屏幕 ,需要的朋友可以參考下
    2019-07-07

最新評(píng)論