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

前言

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

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

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

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

今天我們先來看一下變化的捕獲,通知與響應(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:頁面展示的值是多個(gè)值的組合變化

image.png

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

image

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

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

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);

控制臺輸出:

設(shè)置名稱

獲取名稱

張三

API getter和setter就是數(shù)據(jù)劫持的基礎(chǔ)掷倔,通過這個(gè)例子我們看到,在設(shè)置數(shù)據(jù)或獲取數(shù)據(jù)的時(shí)候个绍,我們都可以加入自己的處理邏輯勒葱。從而達(dá)到我們監(jiān)聽數(shù)據(jù)變化的目的。接下來我們再對比一下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也是通過這種方式劫持?jǐn)?shù)據(jù),然后攔截到變化后广恢,去通知訂閱者凯旋。Vue捕獲變化并發(fā)送通知的流程圖如下(放大查看):

image.png
  • 當(dāng)監(jiān)聽到數(shù)據(jù)變化時(shí),我們通過Watcher來發(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/>’
})

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

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

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對外提供的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例子來看new Wacher的參數(shù):

vm:Vue實(shí)例本身

expOrFn:firstName

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

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

計(jì)算屬性Watcher

計(jì)算屬性也是和上面類似塌鸯,但有個(gè)重要的參數(shù)侍瑟,lazy為true。這就代表著丙猬,在創(chuàng)建的時(shí)候丢习,并不會立即執(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í)候就會觸發(fā)vm._update(vm._render(), hydrating),在創(chuàng)建的時(shí)候也會執(zhí)行一次揽思,進(jìn)行第一次渲染袜腥。具體見下圖:

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

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

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

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

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

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

到目前為止,我們解決了變化的監(jiān)聽福侈,以及觀察者的創(chuàng)建酒来,那么兩者又是如何聯(lián)系起來的呢?
再來看一下數(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í)際過程中當(dāng)關(guān)聯(lián)設(shè)置結(jié)束后,會進(jìn)行出棧操作戳葵。整個(gè)解析過程按照從根節(jié)點(diǎn)到子節(jié)點(diǎn)就乓,也就是監(jiān)聽先壓入棧,然后解析的時(shí)候發(fā)現(xiàn)棧里有監(jiān)聽拱烁,就會綁定生蚁。

是不是有點(diǎn)亂?沒關(guān)系戏自,我們再捋一遍邦投。

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

  2. 繼續(xù)向下執(zhí)行,解析組件時(shí)第一次必然獲取數(shù)據(jù)剂娄,這個(gè)時(shí)候就會觸發(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é)底是要渲染到頁面用戶才能看到變化怕午。比如vue-router废登,就是利用組件Watcher進(jìn)行的觸發(fā)。

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

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

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

image

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

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

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

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

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

image

結(jié)語

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


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末煌集,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子捌省,更是在濱河造成了極大的恐慌苫纤,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纲缓,死亡現(xiàn)場離奇詭異卷拘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)祝高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門栗弟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人工闺,你說我怎么就攤上這事乍赫⊥切迹” “怎么了?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵耿焊,是天一觀的道長。 經(jīng)常有香客問我遍搞,道長罗侯,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任溪猿,我火速辦了婚禮钩杰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诊县。我一直安慰自己讲弄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布依痊。 她就那樣靜靜地躺著避除,像睡著了一般。 火紅的嫁衣襯著肌膚如雪胸嘁。 梳的紋絲不亂的頭發(fā)上瓶摆,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機(jī)與錄音性宏,去河邊找鬼群井。 笑死,一個(gè)胖子當(dāng)著我的面吹牛毫胜,可吹牛的內(nèi)容都是我干的书斜。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼酵使,長吁一口氣:“原來是場噩夢啊……” “哼荐吉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起凝化,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤稍坯,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后搓劫,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瞧哟,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年枪向,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了勤揩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡秘蛔,死狀恐怖陨亡,靈堂內(nèi)的尸體忽然破棺而出傍衡,到底是詐尸還是另有隱情,我是刑警寧澤负蠕,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布蛙埂,位于F島的核電站,受9級特大地震影響遮糖,放射性物質(zhì)發(fā)生泄漏绣的。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一欲账、第九天 我趴在偏房一處隱蔽的房頂上張望屡江。 院中可真熱鬧,春花似錦赛不、人聲如沸惩嘉。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽文黎。三九已至,卻和暖如春畴椰,著一層夾襖步出監(jiān)牢的瞬間臊诊,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工斜脂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留抓艳,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓帚戳,卻偏偏與公主長得像玷或,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子片任,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354