vue源碼-深入響應(yīng)式原理

前言

隨著前后端分離成為Web開發(fā)的常態(tài),Mvvm框架越來(lái)越普及。讓前端開發(fā)從關(guān)注Dom,變?yōu)殛P(guān)注數(shù)據(jù),提高了開發(fā)效率,降低了學(xué)習(xí)成本。同時(shí)也能有效避免低級(jí)的Dom操作錯(cuò)誤。

在享受Mvvm框架帶來(lái)的便利的同時(shí),我們也會(huì)對(duì)它的具體實(shí)現(xiàn)產(chǎn)生興趣。筆者認(rèn)為Mvvm框架重要的有兩個(gè)部分

  1. 數(shù)據(jù)變化的捕獲,通知與響應(yīng)

  2. vDom對(duì)通知產(chǎn)生響應(yīng),并對(duì)Dom進(jìn)行相應(yīng)的操作

今天我們先來(lái)看一下變化的捕獲,通知與響應(yīng),分為下面四個(gè)部分

  1. 數(shù)據(jù)變化的捕獲

  2. 監(jiān)聽器的創(chuàng)建

  3. 數(shù)據(jù)變化與監(jiān)聽器的關(guān)聯(lián)

  4. 變化的響應(yīng)

一,數(shù)據(jù)變化的捕獲

日常項(xiàng)目中,我們常用的與數(shù)據(jù)變化相關(guān)的,有以下三個(gè):

  • Data: 包括定義數(shù)據(jù)模型設(shè)置的初始值,Prop傳遞的值

  • Watch:監(jiān)聽某一個(gè)值的變化進(jìn)行后續(xù)業(yè)務(wù)處理

  • Computed:頁(yè)面展示的值是多個(gè)值的組合變化

image.png

除了上述三個(gè),其實(shí)vue框架本身,還有一個(gè)組件層面的變化,比如路由變化會(huì)重新渲染組件。雖然都是變化,但這四者既有聯(lián)系也有區(qū)別,關(guān)系如下圖

image

數(shù)據(jù)變化會(huì)觸發(fā)監(jiān)聽器,會(huì)觸發(fā)組件渲染,而組件渲染的時(shí)候,會(huì)重新計(jì)算屬性。那么該如何監(jiān)聽數(shù)據(jù)變化呢?

監(jiān)聽數(shù)據(jù)變化
JavaScript中監(jiān)聽數(shù)據(jù)變化API:Getter和Setter,先看一個(gè)簡(jiǎn)單的示例:

var user = {}
var name;
Object.defineProperty(user, 'current', {
  get: function(){
      console.log('獲取名稱')
      return name
  },
  set:function(val){
      console.log('設(shè)置名稱')
      name = val
  }
})

user.current = '張三';
console.log(user.current);

控制臺(tái)輸出:

設(shè)置名稱

獲取名稱

張三

API getter和setter就是數(shù)據(jù)劫持的基礎(chǔ),通過(guò)這個(gè)例子我們看到,在設(shè)置數(shù)據(jù)或獲取數(shù)據(jù)的時(shí)候,我們都可以加入自己的處理邏輯。從而達(dá)到我們監(jiān)聽數(shù)據(jù)變化的目的。接下來(lái)我們?cè)賹?duì)比一下vue的代碼實(shí)現(xiàn)。

Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })

同樣的,Vue也是通過(guò)這種方式劫持?jǐn)?shù)據(jù),然后攔截到變化后,去通知訂閱者。Vue捕獲變化并發(fā)送通知的流程圖如下(放大查看):

image.png
  • 當(dāng)監(jiān)聽到數(shù)據(jù)變化時(shí),我們通過(guò)Watcher來(lái)發(fā)送通知

  • 當(dāng)獲取數(shù)據(jù)的時(shí)候,我們把Watcher加入到通知列表

    什么是Watcher呢?

二,監(jiān)聽器的創(chuàng)建

Watcher的分類

image

Watcher什么時(shí)候創(chuàng)建的呢?先看下面這段熟悉的代碼:

new Vue({
 el: '#app',
 router, 
 components: { App }, 
 template: '<App/>’
})

上面這塊代碼簡(jiǎn)單來(lái)說(shuō)就是創(chuàng)建了一個(gè)Vue的實(shí)例。聲明了一個(gè)組件App,渲染綁定的節(jié)點(diǎn)是#app,還有路由。

Vue實(shí)例化的處理流程(本文無(wú)關(guān)的部分略過(guò))。

image

和變化相關(guān)的有三個(gè)方法:

  • InitRender階段,綁定組件的渲染方法

  • InitState階段,創(chuàng)建數(shù)據(jù)模型,監(jiān)聽器,計(jì)算屬性的Watcher

  • $mount階段,創(chuàng)建組件Watcher

我們分別看一下三種監(jiān)聽器的創(chuàng)建

監(jiān)聽器Watcher

Vue.prototype.$watch = function (
  expOrFn: string | Function,
  cb: any,
  options?: Object
): Function {
  const vm: Component = this
  if (isPlainObject(cb)) {
    return createWatcher(vm, expOrFn, cb, options)
  }
  options = options || {}
  options.user = true
  const watcher = new Watcher(vm, expOrFn, cb, options)
  if (options.immediate) {
    try {
      cb.call(vm, watcher.value)
    } catch (error) {
      handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
    }
  }
  return function unwatchFn () {
    watcher.teardown()
  }
}

注意這一行:const watcher = new Watcher(vm, expOrFn, cb, options)

$watch方法也是Vue對(duì)外提供的API

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

我們結(jié)合Vue官網(wǎng)watch例子來(lái)看new Wacher的參數(shù):

vm:Vue實(shí)例本身

expOrFn:firstName

cb:對(duì)應(yīng)的函數(shù)

option:額外的參數(shù),從上面我們也看到,有個(gè)immediate屬性,如果為true就先調(diào)用一次

計(jì)算屬性Watcher

計(jì)算屬性也是和上面類似,但有個(gè)重要的參數(shù),lazy為true。這就代表著,在創(chuàng)建的時(shí)候,并不會(huì)立即執(zhí)行。

const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  ……略
  for (const key in computed) {
     ……略

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    }
     ……略
  }
 ……略
}

function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

而調(diào)用的時(shí)機(jī)是在渲染組件的時(shí)候觸發(fā),然后watcher.evaluate()

組件Watcher


export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  ………略
  let updateComponent
  if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
    updateComponent = () => {
      ………略
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  ………略
  return vm
}

當(dāng)組件發(fā)生變化的時(shí)候就會(huì)觸發(fā)vm._update(vm._render(), hydrating),在創(chuàng)建的時(shí)候也會(huì)執(zhí)行一次,進(jìn)行第一次渲染。具體見下圖:

image.png
  1. Init方法中,會(huì)進(jìn)行各種Watcher的創(chuàng)建

  2. $mount中會(huì)創(chuàng)建組件Watcher并執(zhí)行

  3. 組件Watcher觸發(fā)渲染

  4. 渲染過(guò)程發(fā)現(xiàn)有子組件,對(duì)子組件再走一遍上面的流程

注意:上面我們說(shuō)了三種Watcher的創(chuàng)建,計(jì)算屬性的Watcher不會(huì)立即執(zhí)行,而其他兩個(gè)都會(huì)立即執(zhí)行一次。

三,數(shù)據(jù)變化與監(jiān)聽器的關(guān)聯(lián)

到目前為止,我們解決了變化的監(jiān)聽,以及觀察者的創(chuàng)建,那么兩者又是如何聯(lián)系起來(lái)的呢?
再來(lái)看一下數(shù)據(jù)劫持的getter方法,我們發(fā)現(xiàn)只有在Dep.target(Watcher)存在的時(shí)候才建立關(guān)聯(lián)

get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    if (Dep.target) {
      dep.depend()
      if (childOb) {
        childOb.dep.depend()
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
    }
    return value
  },

再看一下Watcher類的get方法(這個(gè)就是一個(gè)普通的方法名稱,不要和getter混淆)

get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    if (this.user) {
      handleError(e, vm, `getter for watcher "${this.expression}"`)
    } else {
      throw e
    }
  } finally {
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}

pushTarget(this):將當(dāng)前watcher壓入棧,同時(shí)將this賦值給Dep.target

popTarget():出棧

哪這個(gè)棧又是個(gè)什么東東呢?

Watcher Stack
組件是按樹形的結(jié)構(gòu)遞歸解析。如果不考慮出棧的情況,那么整個(gè)棧的情況如圖所示:

image

而在實(shí)際過(guò)程中當(dāng)關(guān)聯(lián)設(shè)置結(jié)束后,會(huì)進(jìn)行出棧操作。整個(gè)解析過(guò)程按照從根節(jié)點(diǎn)到子節(jié)點(diǎn),也就是監(jiān)聽先壓入棧,然后解析的時(shí)候發(fā)現(xiàn)棧里有監(jiān)聽,就會(huì)綁定。

是不是有點(diǎn)亂?沒關(guān)系,我們?cè)俎垡槐椤?/p>

  1. new Watcher的時(shí)候調(diào)用其內(nèi)部get方法,在這個(gè)方法中會(huì)將當(dāng)前監(jiān)聽壓入棧,并賦值給target。

  2. 繼續(xù)向下執(zhí)行,解析組件時(shí)第一次必然獲取數(shù)據(jù),這個(gè)時(shí)候就會(huì)觸發(fā)數(shù)據(jù)劫持的getter,在getter里判斷當(dāng)前target是否有值,有值就把當(dāng)前數(shù)據(jù)和Watcher進(jìn)行關(guān)聯(lián),沒有就忽略繼續(xù)向下

  3. 出棧并清空target

結(jié)合上面的文字,再具體看一下這三個(gè)Watcher關(guān)聯(lián)的流程

組件Watcher關(guān)聯(lián)

image.png

監(jiān)聽器Watcher關(guān)聯(lián)

image.png

組件Watcher和監(jiān)聽器Watcher的區(qū)別,是組件Watcher要進(jìn)行渲染。這當(dāng)然也比較好理解,監(jiān)聽的目的,歸根結(jié)底是要渲染到頁(yè)面用戶才能看到變化。比如vue-router,就是利用組件Watcher進(jìn)行的觸發(fā)。

計(jì)算屬性Watcher關(guān)聯(lián)

計(jì)算屬性和前面兩個(gè)不同,它在創(chuàng)建watcher的時(shí)候,并不會(huì)觸發(fā)get。

在初始化的時(shí)候創(chuàng)建好Watcher,渲染的時(shí)候才會(huì)觸發(fā),同時(shí)把組件Watcher也追加進(jìn)訂閱

image

四,變化的響應(yīng)

變化劫持,通知Watcher,Watcher響應(yīng)具體的動(dòng)作。這部分內(nèi)容相對(duì)就比較簡(jiǎn)單了。唯一需要注意的是,計(jì)算屬性因?yàn)闆]有入棧,所以它的響應(yīng)會(huì)被丟棄。

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true //不執(zhí)行具體動(dòng)作
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

通過(guò)代碼可以看到,當(dāng)是lazy的時(shí)候,設(shè)置dirty=true,但并沒有進(jìn)行具體的操作。

我們最后再整體回顧一下開始的關(guān)系圖:

image

結(jié)語(yǔ)

數(shù)據(jù)響應(yīng)式可以說(shuō)是Mvvm框架的精髓,希望通過(guò)本文的描述,可以讓大家更好的理解它的實(shí)現(xiàn)原理,只是通過(guò)文章,依然不能完全的描述透徹,細(xì)節(jié)部分還是需要去閱讀源碼,對(duì)照分析和研究。前端水越來(lái)越深,一起共勉。本文都是作者自己的理解,有不當(dāng)之處歡迎批評(píng)指正。關(guān)于vDom的渲染部分,會(huì)在下篇文章中分享。


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容