我是如何看Vue源碼的

希沃ENOW大前端javascript

公司官網:CVTE(廣州視源股份)html

團隊:CVTE旗下將來教育希沃軟件平臺中心enow團隊前端

本文做者:vue

彭康名片.png

前言

關於vue響應式的文章其實已經挺多了,不過大多都在淺嘗輒止,基本就是簡單介紹一下Object.defineProperty,覆蓋一下setter作個小demo就算解決,好一點的會幫你引入observe、watcher、dep的概念,以及加入對Array的特殊處理,因此本篇除了上述之外,更多的重心將放在setter引起render的機制與流程上,而後結合這個這個響應式機制解析vue中的watchcomputed語法實現java

文章分爲兩部分,第一部分會簡單介紹vue實例構建流程,第二部分則深刻探究響應式實現。react

建議對照源碼閱讀文章,由於不少本文不少地方會直接指出文件路徑,同時將省略部分代碼而直述功能express

版本信息:數組

  • vue: 2.6.12

1、尋找vue

直入主題markdown

真正的vue實例在core/instance/index中能夠找到數據結構

function Vue (options) {
  ....
  this._init(options) // 這個方法在initMixin中定義
}

initMixin(Vue)  // 掛載_init()
stateMixin(Vue)  // 掛載狀態處理方法(掛載data,methods等)
eventsMixin(Vue)  // 掛載 事件 的方法($on,$off等)
lifecycleMixin(Vue) // 掛載 生命週期方法(update,destory)
renderMixin(Vue)  // 掛載與渲染有關的方法($nextTick,_render)
複製代碼

每一個方法能夠按照代碼邏輯來看,實現對應功能,這裏拿initMixin舉例

篇幅有限,全部此處僅解釋initMixin邏輯,剩餘幾個方法你們能夠本身探索哦

initMixin

initMixin中僅僅掛載了_init()方法,在_init中,初始化了整個vue的狀態:

function _init(option) {
  ...
  vm._uid = uid++ // 即component id
  ...
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm)
  callHook(vm, 'created')
  ...
  if (vm.$options.el) {
    vm.$mount(vm.$options.el) // 開始掛載
  }
}
複製代碼

這裏咱們能夠看到幾個beforeCreate,createdMount關鍵字,大概就可以猜到vue實例的部分生命週期方法就是在這裏進行了掛載,再結合 vue官方文檔的圖示

init.png

關於初始化整個vue的狀態,能夠舉例來講,例如initLifecycle中就賦值了parent,children,以及一些isMounted,isDestroy的標識符。initRender中就將attrs,listeners響應化,等等,諸如此類。

initMixin=>initState=>initData,即可以看到掛載props,methods,data,computed,watch了,

能夠看到,此處先掛載了props,methods,而後是data的順序,其實再往下探究邏輯就能夠知道,若是存在變量重名,優先級是props>methods>data的,這也就解釋了爲何初始化的順序是這樣安排的

initData中,先是獲取了data數據,判斷props,methods變量重名問題,而後是走了一個代理,將變量名代理到vue實例上,這樣的話你的vue實例中,使用this.x指向就能夠訪問到this.data.x,這類代理也用在了propsmethods

initData獲取數據中能夠看到一個判斷typeof data === 'function' ? getData(data, vm) : data || {}, 支持兩種方式獲取,實際上若是是本身寫這樣一個邏輯是會藏有隱患的,若是你的data是直接使用對象,而js的複雜數據類型是地址引用,這意味着,你實例化了兩個vue對象,實際上他們的data引用地址是同一個地址,對其中一個vue data的修改會觸發另外一個vue數據的變更,帶來的問題是巨大的

export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
複製代碼

這個邏輯處理的設計也是很是巧妙,他覆蓋了實例中對該key的訪問,使用settergetter將實際訪問指向了this.data[key]

這裏能夠說一下computed的邏輯,實際上也是取巧使用了本來用於data的響應式邏輯,其實看到上面貼出來的proxy代碼,大概就能猜到,既然proxy可以改變一個變量讀取的指向,那麼他也能創造一個虛假變量的指向,這個創造出來的這個變量實際上就是computed所使用的變量,將每次computed函數賦給getter,再加上響應式處理,就徹底實現了computed,

走到最後,就是observe(data),也就是開始處理vue數據的雙向綁定

2、雙向綁定

不一樣於react的單向數據流,vue使用的雙向綁定,單向數據流能夠理解爲當源頭的數據發生變更則觸發行爲,固然這個變更是主動的,即你須要setState才能觸發,而雙向綁定則能夠抽象爲,每個數據旁邊都有一個監護人(一種處理邏輯),當數據發生變化,這個監護人就會響應行爲,這個流程是被動發生的,只要該數據發生變更,就會經過監護人觸發行爲。

若是你以前有過了解,大概就會知道,js每一個數據的變更都是經過Object原型鏈中的setter去改變值,而若是你在他改變值以前,去通知監護人,就可以實現上述的邏輯,這一點不少博客文章都寫的很是清楚了。

接着第一部分的initData知道最後observe(data),這裏開始正式處理響應式。

2.1 前置條件

前面一直提到,經過Object的原型鏈改變對象的默認行爲:gettersetter,首先咱們須要知道,在js中,讀取一個對象的值並非直接讀取,而是經過Object的原型鏈上的默認行爲getter拿到對應的值,而改變這種行爲其實是經過Object.defineProperty,來從新定義一個對象的gettersetter,在/src/core/observer/index.js中咱們能夠看一個defineReactive方法,他就是vue用來實現這種行爲的方法,也是這個響應式的核心

function defineReactive(obj, key, val, ... ) {
  // 此處須要保留getter、setter是由於,開發者可能本身基於defineProperty已經作過一層覆蓋,
  // 而響應式又會覆蓋一次,因此爲了保留開發者本身的行爲,此處須要兼容原有的getter、setter
  const getter = property && property.get // 拿到默認的getter、setter行爲
  const setter = property && property.set
  Object.defineProperty(obj, key, {
    enumerable: true, // 是否能夠被枚舉出來(例如Object.keys(),for in)
    configurable: true, // 是否能夠被配置,是否能夠被刪除
    get: function() {
      const value = getter ? getter.call(obj) : val
      ...
      return value
    }
    set: function(newVal) {
      ...
      setter.call(obj, newVal)
  	} 
  })
}
複製代碼

2. 2響應式

首先,咱們猜測一下,雙向綁定的行爲,數據可以響應行爲的變化,而行爲又可以操做數據的改變,雖然有部分教程會讓你站在數據的角度去理解這種行爲,實際上,咱們站在行爲的角度上去理解是更加方便的。

咱們將一種行爲定義爲一個Watcher,他有多是一個vue文件的template中的dom節點渲染行爲,也有多是computed的計算值行爲,總之,咱們從行爲的角度出發,一個行爲的發生,會伴隨着對變量的讀取(回想一下咱們在vue文件中的templatehtml標籤時,老是會使用{{obj.xxx}}來讀取某個變量並渲染),咱們想要實現,變量的改變也會帶動這個行爲的從新渲染,是否是咱們只須要在首次行爲發生的週期內,在讀取某個變量時,在這個變量內記錄這個Watcher,這樣的話,下次變量的改變時,我只要觸發我以前記錄過的Watcher就好了。因此,咱們只須要在一個Watcher發生時,將其掛載到一個公共變量上,這樣在讀取一個值的時候,記錄這個公共變量,就可以實現上述操做。

簡版.JPG

這裏先不解釋Dep的做用,能夠將其抽象理解爲一個被掛載在數據上的數組,每次這個數據被一個watch讀取時,就會將這個watch記錄下來

2.2.1 Watcher

既然說到將一種行爲定義爲一個watcher,那麼能夠在/src/core/observer/watcher.js中看到Watcher的實體類,而咱們以前一直所說的「行爲」,實際上就是構造器的第二個參數expOrFn,能夠有表達式或者函數讀取的兩種模式

class Watcher {
 	constructor ( vm: Component, // vue實例 expOrFn: string | Function, // 行爲 cb: Function, // 爲watch服務 options?: ?Object, isRenderWatcher?: boolean // 判斷是否爲渲染watcher, )
}
複製代碼

接着來看一種最典型的watcher行爲,在/src/core/instance/lifecycle.js中的moundComponent方法中,能夠看到一個實例化watcher的方法

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
複製代碼

能夠看到,他將updateComponent(能夠抽象爲渲染行爲)傳給Watcher,而在Watcher的實例化中,將會執行此方法,固然在執行以前,pushTarget(this),將這個watcher掛載到公共變量上然後開始執行渲染行爲,

class Watch {
  constructor(...) {
    ....
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }
    this.get();
  }
  get() {
    pushTarget(this) // 掛載行爲至公共Target
    value = this.getter.call(vm, vm) // 開始執行行爲,之因此會有返回值是爲了computed服務
    popTarget() // 取消掛載,避免下次讀取變量時又會綁定此行爲
  }
}
複製代碼

完整版.JPG

此時,若是此行爲讀取了某個響應式變量,那麼該變量的getter將會存儲公共變量target,當行爲完成後就會取消行爲的掛載,這個時候咱們再回過頭來看前面的defineReactive的邏輯

function defineReactive(obj, key) {
  const dep = new Dep(); // 每一個數據都有一個本身的存儲列表
  const getter = property && property.get
  const setter = property && property.set
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) { // 判斷公共變量中是否掛載了行爲(watcher)
        dep.depend() // 將行爲(watcher)加入dep(即此變量的存儲行爲列表)
        ...
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return // 判斷變量沒有變化,則直接返回(後二者判斷則是由於NaN!==NaN的特性)
      }
      if (setter) {
        setter.call(obj, newVal) // 開始
      } else {
        val = newVal
      }
      dep.notify() // 通知本身這個數據的存儲列表,數據發生改變,須要從新執行行爲(watcher)
    }
   });
  }
複製代碼

這個時候就很清晰明瞭了,這就是不少博客文章所說的依賴收集,變量在get時經過公共變量Target收集依賴(也就是本文所說的行爲),在set時,即變量數據發生改變時,觸發更新notify;

2.2.2 Computed

前文有大體介紹computed的實現,實際上在介紹完Wacher以後就能夠來詳細介紹了,計算屬性computed並無實際的變量,他經過原型鏈覆蓋創造了一個變量指向(src/core/instance/state.jsinitComputed),回憶一下computed的兩種寫法

'fullName': function() {
  return this.firstName + this.secondeName;
}
'fullName': {
  get: function () {...},
  set: function() {...},
}
複製代碼

咱們再來看一下initComputed

function initComputed (vm: Component, computed: Object) {
 const watchers = vm._computedWatchers = Object.create(null)
 for (const key in computed) {
   const userDef = computed[key]
   // 對照着computed的兩種寫法,就能理解爲何這裏有這樣的判斷,
   const getter = typeof userDef === 'function' ? userDef : userDef.get
   watchers[key] = new Watcher(
    vm,
    getter || noop,
    noop,
    { lazy: true }
  )
   defineComputed(vm, key, userDef) // 經過defineProperty來創造一個掛載在vm上key(fullName)的指向
 }
}
複製代碼

能夠看到,他將computedgetter方法,做爲Watcher的行爲傳遞了進去,這樣在執行getter時,能夠將此行爲綁定至過程當中所讀取到的變量(firstName),如此,再下次firstName發生改變時,就會觸發此Watcher,從新運行getter方法,獲得一個新的fullName的值(還記得前文class Watch中的value = this.getter.call(vm, vm)嗎?這個返回值就是computed的返回值),這樣就實現了computed的邏輯

2.2.3 Watch

watch的用法,是監聽某個變量,當該變量發生變化時,執行特定的邏輯,

上文提到的兩種Watcher行爲都是函數行爲,可是Watcher的行爲是支持函數或者表達式的(expOrFn),因此此處的exp(expression)這裏就是能夠提現到的,咱們只須要在變量發生變化時,執行watch定義的邏輯便可,

還記得前文代碼defineReactiveset方法通知依賴更新(dep.notify()),雖然前文一直爲了方便理解,將Dep描述爲一種抽象的列表結構,僅用於依賴收集,但實際上他是一個單獨的數據結構,

let uid = 0class Dep {
  constructor() {
    this.id = uid ++; 
    this.subs = []; // 真正用於收集依賴的數據
  }
  depend () { // 依賴收集
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  notify() { // 變量值發生變化,通知更新
    // 遍歷全部收集的依賴,注意觸發更新,
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
  ...
}
Dep.target = null; // 這就是一直說的,用於掛載Watcher行爲的公共變量
function pushTarget(target){ Dep.target = target };
function popTarget() { Dep.target = null };
複製代碼

實際上這裏的靜態變量target以及pushTarget、popTarget是通過簡化的,由於渲染並非一個單一的行爲,他是層層嵌套的行爲,因此在綁定響應式時,也是須要區分該變量究竟是要綁定至哪一個行爲(不然每一個變量都綁定最頂層的行爲,一個變量的變化,將會引起整個頁面的update),所以真正的target是還有一個stack棧結構,用於掛載多個嵌套的行爲

能夠看到,每次變量更新,都會觸發watcher.update,那麼對於watch監聽的回調,就能夠放到在update中調用

class Watch {
  constructor(vm, expOrFn, cb, ...) {
    this.cb = cb // 這個cb就是watch監聽的回調
  }
  update() {
    this.run()
  }
  run() {
    ...
    this.cb.call(this.vm, ...)
	}
}
複製代碼

至此,關於watch監聽的實現邏輯大體就是如此

關於依賴收集,實際上並非在get變量時,直接將watcher綁定至Dep中,能夠看到Dep.depend(),他先通知行爲(watcher),叫他先綁定本身,而後watcher綁定完dep以後,纔會回過頭,告知DepaddSub(),這裏的邏輯像是一個圈

因此如今咱們回過頭來看,前文說了,每一個數據都有一個「監護人」,來記錄此數據所綁定的行爲,那麼這個「監護人」到底在哪裏呢? 能夠看到/src/core/observer/index.jsclass Observer中,

class Observer {
  constructor(val) {
    ...
    def(value, '__ob__', this) // 對value定義__ob__屬性,掛載此object
    ...
  }
}
複製代碼

ob.JPG 對於每一份須要響應式處理的數據,都會掛載一個Observer實例,其內subs就是用於記錄綁定此數據的Watcher,同時也能夠看到,這份數據的get、set方法已是被重寫過了,也就是前文的defineReactive中的覆蓋行爲。

2.2.4 其餘

其實對於Array的響應式是須要特殊處理的,由於他除了set、get以外,還會對數組進行增減操做(splice等),而這些操做是set沒法捕捉的,因此覆蓋get、set顯然沒法實現數組的響應式,而vue中採用的是直接覆蓋數組的原型鏈中會對數據自己改變的方法(push、shift、splice等),/src/core/observer/array.js整個文件就是對數據的特殊處理 最新的vue3中,使用了ES6proxy特性來替代這種覆蓋set、get實現響應式行爲,這種模式同時也可以處理Array

3、結尾

vue的源碼固然沒有如此簡單,不少東西文章都沒有涉及到,譬如說,經過上面的邏輯其實你能夠發現,depwatcher實際上是互相引用的,而js的垃圾回收是檢測變量引用的機制,因此若是是簡單的複製上文的邏輯,最終的這部分的內存實際上是沒法被回收的,須要你手動清除,固然vue中也作了這樣的處理(每一個vm下其實有一個watcherList,用於記錄這個示例中全部使用到的watcher,再vm.destroy時,經過遍歷watcherList,再銷燬每個watcher,而watcher中又會本身銷燬Dep),可是限於篇幅緣由沒法詳細介紹了。

相關文章
相關標籤/搜索