教你從源碼看Vue的響應式原理

前段時間把 vue源碼抽時間看了一遍,耐心點看再結合網上各種分析文章還是比較容易看明白的,沒太大問題,唯一的問題就是

看完即忘

當然了,也不是說啥都不記得了,大概流程以及架構這些東西還是能留下個印象的,對於 Vue的構建算是有了個整體認知,只是具體到代碼級別的細節很難記住多少,不過也情有可原嘛,又不是背代碼誰能記住那麼多邏輯繞來繞去的東西?

想來想去,響應式這個東西幾年前就已經被列入《三年前端,五年面試》考試大綱,那就它吧

初始化

首先找入口,vue源碼的src目錄下,存放的就是未打包前的代碼,這個目錄下又分出幾個目錄:

compiler跟模板編譯相關,將模板編譯成語法樹,再將 ast編譯成瀏覽器可識別的 js代碼,用於生成 DOM

core就是 Vue的核心代碼了,包括內置組件(slottransition等),內置 api的封裝(nextTickset等)、生命週期、observervdom

platforms跟跨平臺相關,vue目前可以運行在webweex上,這個目錄裏存在的文件用於抹平平臺間的 api差異,賦予開發者無感知的開發體驗

server存放跟服務器渲染(SSR)相關的邏輯

sfc,縮寫來自於 Single File Components,即 單文件組件,用於配合 webpack解析 .vue文件,由於我們一般會將單個組件的 templatescriptstyle,以及自定義的 customBlocks寫在一個單 .vue文件中,而這四個都是不同的東西,肯定需要在解析的時候分別抽離出來,交給對應的處理器處理成瀏覽器可執行的 js文件

share定義一些客戶端和服務器端公用的工具方法以及常量,例如生命週期的名稱、必須的 polyfill

//在此我向大家推薦一個前端全棧開發交流圈:582735936 突破技術瓶頸,提升思維能力

其他的就廢話不多說了,直接進入主題,數據的響應式肯定是跟 data 以及 props有關,所以直接從 data以及 props的初始化開始

node_modules\vue\src\core\instance\state.js文件中的 initState方法用於對 propsdatamethods等的初始化工作,在 new vue的時候,會調用 _init方法,此方法位於 Vue的原型 Vue.prototype上,這個方法就會調用 initState

// node_modules\vue\src\core\instance\index.js
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// 往 Vue構造函數的 prototyp上掛載 _init方法
initMixin(Vue)
  //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力
// node_modules\vue\src\core\instance\init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // ...
    // 初始化 props  data  watch 等
    initState(vm)
    // ...
  }
  

initState方法如下:

// node_modules\vue\src\core\instance\state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
  

可見,在此方法中,分別調用了 initPropsinitMethodsinitDatainitComputedinitWatch方法,這些方法中對 propsmethodsdatacomputedwatch進行了初始化過程,本文只是分析響應式,所以其他拋開不談,只看 initPropsinitData

initProps中,主要是使用了一個 for...inprops進行遍歷,調用 defineReactive方法將每個 props值變成響應式的值defineReactive正是 vue響應式的核心方法,放到後面再說;

並且又調用 proxy方法把這些 props值代理到 vue上,這樣做的目的是能夠讓直接訪問 vm.xxx 得到和訪問 vm._props.xxx同樣的效果(也就是代理了)

上面的意思具體點就是,你定義在 props中的東西(比如:props: { a; 1 }),首先會被附加到 vm._props對象的屬性上(即 vm._props.a),然後遍歷 vm._props,對其上的屬性進行響應式處理(對 a響應式處理),但是我們一般訪問 props並沒有看到過什麼 this._props.a的代碼,而是直接 this.a就取到了,原因就在於 vue內部已經爲我們進行了一層代理

首先附加在 vm._props上的目的是方便 vue內部的處理,只要是掛載 vm._props上的數據就都是 props而不是 datawatch什麼的,而代理到 vm上則是方便開發者書寫

// node_modules\vue\src\core\instance\state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
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)
}
 //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力 

proxy方法的原理其實就是使用 Object.definePropertygetset方法代理了屬性的訪問

最後,這裏面還有個 toggleObserving方法,這個方法是 vue內部對邏輯的一個優化,如果當前組件是根組件,那麼根組件是不應該有 props的,但是呢,你給根組件加個 propsvue也不會報錯,子組件的 props可以由父元素改變,但是根組件是沒有父組件的,所以很顯然根組件的 props肯定是不會改變的,也就沒必要對這種 props進行依賴收集了

這裏調用 toggleObserving就是禁止掉根組件 props的依賴收集

initData裏做的事情跟 initProps差不多,首先,會把 data值取出放到 vm._data上,由於data的類型可以是一個對象也可以是一個函數,所以這裏會判斷下,如果是函數則調用 getData方法獲取 data對象,否則直接取 data的值即可,不傳 data的話,默認 data值是空對象 {}

let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}
  

這個 getData其實就是執行了傳入的 function類型的data,得到的值就是對象類型的 data

export function getData (data: Function, vm: Component): any {
  // ...
  // 使用 call執行 function類型的data,得到對象類型的data
  return data.call(vm, vm)
  // ...
}
  

另外,initData並沒有直接對 data進行遍歷以將 data中的值都變成是響應式的,而是另外調用 observe方法來做這件事,observe最終也調用了 defineReactive,但是在調用之前,還進行了額外的處理,這裏暫時不說太多,放到後面和 defineReactive一起說;除此之外,initData也調用了 proxy進行數據代理,作用和 props調用 proxy差不多,只不過其是對 data數據進行代理

構建 Observe

現在回到上面沒說的 observedefineReactive,由於 observe最終還是會調用 defineReactive,所以就直接從 observe說起

observe,字面意思就是觀察、觀測,其主要功能就是用於檢測數據的變化,由於其屬於響應式,算是 vue的一個關鍵核心,所以其專門有一個文件夾,用於存放相關邏輯文件

// node_modules\vue\src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  /// ...
  {
    ob = new Observer(value)
  }
  /// ...
}
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力  

observe方法中,主要是這一句 ob = new Observer(value),這個 Observer是一個 class

// node_modules\vue\src\core\observer\index.js
export class Observer {
  // ...
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  // ...
}
  

在其 constructor中,做了一些事情,這裏的 new Dep()Dep也是跟響應式相關的一個東西,後面再說,然後調用了 def,這個方法很簡單,就是調用 Object.defineProperty將當前實例(this)添加到value__ob__屬性上:

// node_modules\vue\src\core\util\lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
  

vue裏很多地方都用到了 Object.defineProperty,可以看出這個東西對於 vue來說還是很重要的,少了它會很麻煩,而 IE8卻不支持 Object.defineProperty,所以 Vue不兼容 IE8也是有道理的

在前面的 observe方法中,也出現過 __ob__這個東西:

if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  ob = value.__ob__
}
  

可以看到,__ob__在這裏用於做重複校驗,如果當前數據對戲 value上已經有了 __ob__屬性並且此屬性是由 Observer構造而來,則直接返回這個值,避免重複創建

回到 Observer類,接下里會判斷 value是不是數組,如果是數組,再判斷 hasProto是否爲 truth值,這個 hasProto就是用於檢測當前瀏覽器是否支持使用 __proto__的:

// node_modules\vue\src\core\util\env.js
// can we use __proto__?
export const hasProto = '__proto__' in {}
  

如果是就調用 protoAugment,否則調用 copyAugment,後者可以看做是前者兼容 __proto__的一個 polyfill,這兩個方法的目的是一樣的,都是用於改寫 Array.prototype上的數組方法,以便讓數組類型的數據也具備響應式的能力

換句話說,數組爲什麼對數組的修改,也能觸發響應式呢?原因就在於 vue內部對一些常用的數組方法進行了一層代理,對這些數組方法進行了修改,關鍵點在於,在調用這些數組方法的時候,會同時調用 notify方法:

// node_modules\vue\src\core\observer\array.js
// notify change
ob.dep.notify()
  

ob就是 __ob__,即數據對象上掛載的自身的觀察者,notify就是觀察者的通知事件,這個後面放到 defineReactive一起說,這裏調用 notify告訴 vue數據發生變化,就觸發了頁面的重渲染,也就相當於是數組也有了響應式的能力

完了之後,繼續調用 observeArray進行深層便利,以保證所有嵌套數據都是響應式的

接上面,如果是對象的話就無需那麼麻煩,直接調用 this.walk方法:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}
  

walk方法會對傳入的對象進行遍歷,然後對每一個遍歷到的數據調用 defineReactive方法,終於到這個方法了,無論是 props的初始化還是 data的初始化最後都會調用這個方法,前面那些都是一些差異性的分別處理

大概看一眼 defineReactive這個方法,最後調用的 Object.defineProperty很顯眼,原來是在這個函數中修改了屬性的 get 以及 set,這兩個方法很重要,分別對應所謂的 依賴收集派發更新

先上個上述所有流程的簡要示意圖,有個大體印象,不然說得太多容易忘

依賴收集

先看 get

// node_modules\vue\src\core\observer\index.js
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
}
  

首先,如果當前屬性以及顯式定義了 get方法,則執行這個 get獲取到值,接着判斷 Dep.target

這裏又出現了一個新的東西: Dep,這是一個 class類,比較關鍵,是整個依賴收集的核心

// node_modules\vue\src\core\observer\dep.js

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}
  

進入 Dep的定義,此類的靜態屬性 target初始化的值是 null,但是可以通過兩個暴露出去的方法來修改這個值

另外,在 Dep.target = null的上面還有一段註釋,主要是說由於同一時間只能有一個 watcher被執行(當前執行完了再進行下一個),而這個 Dep.target的指向就是這個正在執行的 watcher,所以 Dep.target就應該是全局唯一的,這也正是爲什麼 target是個靜態屬性的原因

那麼現在由於 Dep.targetnull,不符合 if(Dep.target){},所以這個值肯定在什麼地方被修改了,而且應該是通過 pushTargetpopTarget來修改的

所以什麼地方會調用這兩個方法?

這又得回到 get了,什麼時候會調用 get?訪問這個屬性,也就是數據的時候就會調用這個數據的 get(如果有的話),什麼時候會訪問數據呢?當然是在渲染頁面的時候,肯定需要拿到數據來填充模板

那麼這就是生命週期的事了,這個過程應該發生在 beforeMountmount中間

// node_modules\vue\src\core\instance\lifecycle.js

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)
  

主要是 new Watcher這句代碼,as we all konwvue 使用觀察者模式實現響應式邏輯,前面的 Observe是監聽器,那麼這裏的 Watcher就是觀察者,數據的變化會被通知給 Watcher,由 Watcher進行視圖更新等操作

進入 Watcher方法

其構造函數 constructor的最後:

this.value = this.lazy
    ? undefined
    : this.get()
  

this.lazy是傳入的修飾符,暫時不用管,這裏可以認爲直接調用 this.get()

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 {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value)
    }
    popTarget()
    this.cleanupDeps()
  }
  return value
}
  //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

可以看到,在 Watcherget方法中,上來就調用了 pushTarget方法,所以就把當前這個 watcher pushtargetStack(位於 Dep的定義文件中)數組中去了,並且把 Dep.target的值置爲這個 watcher

所以,從這裏可以看出 targetStack數組的作用就是類似於一個棧,棧內的項就是 watcher

try...catch...finallyfinally語句中,首先根據 this.deep來決定是否觸發當前數據子屬性的 getter,這裏暫時不看,然後就是調用 popTarget,這個方法就是將當前 watcher出棧,並將 Dep.target指向上一個 watcher

然後 this.cleanupDeps()其實就是依賴清空,因爲已經實現了對當前 watcher的依賴收集,Dep.target已經指向了其他的 watcher,所以當前 watcher的訂閱就可以取消了,騰出空間給其他的依賴收集過程使用

接着執行 value = this.getter.call(vm, vm),這裏的 this.getter就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
  

_update_render都是掛載在 Vue.prototype上的方法,跟組件更新相關,vm._render方法返回一個 vnode,所以肯定涉及到數據的訪問,不然怎麼構建 vnode,既然訪問數據,那麼就會調用數據的 get方法(如果有的話)

那麼就又回到前面了:

// node_modules\vue\src\core\observer\index.js
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的構建過程,可以知道這個時候 Dep.target其實的指向已經已經被更正爲當前的 watcher了,也就是 trueth值,可以進入條件語句

首先執行 dep.depend()dep是在 defineReactive方法中 new Dep的實例,那麼看下 Depdepend方法

// node_modules\vue\src\core\observer\dep.js
depend () {
  if (Dep.target) {
    Dep.target.addDep(this)
  }
}
  

Dep.target此時條件成立,所以繼續調用 Dep.target上的 addDep方法,Dep.target指向 Watcher,所以看 WatcheraddDep方法

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}
  

首先通過 id避免重複添加同一數據,最後又調用了 dep.addSub將當前 Watcher添加到 Dep中去

這裏出現了幾個變量,newDepIdsnewDepsdepIdsdeps,這幾個變量其實就是在 Dep添加 watcher之前的一次校驗,以及方便後續移除訂閱,提升 vue的性能,算是 vue內部一種優化策略,這裏不用理會

// node_modules\vue\src\core\observer\dep.js
addSub (sub: Watcher) {
  this.subs.push(sub)
}
  

最終,在 Dep中,會把 watcher pushDepsubs數組屬性中

即,最終 propsdata的響應式數據的 watcher都將放到 Depsubs中,這就完成了一次依賴收集的過程

繼續回到 defineReactive,在調用了 dep.depend()之後,還有幾行代碼:

// node_modules\vue\src\core\observer\index.js
let childOb = !shallow && observe(val)
// ...
if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}
  

遞歸調用 observe,保證子屬性也是響應式的,如果當前值是數組,那麼保證這個數組也是響應式的

這個依賴收集過程,簡要示意圖如下:

派發更新

依賴收集的目的就是將所有響應式數據通過 watcher收集起來統一管理,當數據發生變化的時候,就通知視圖進行更新,這個更新的過程就是派發更新

繼續看 defineReactiveset方法,這個方法實現派發更新的主要邏輯

// node_modules\vue\src\core\observer\index.js
set: function reactiveSetter (newVal) {
  const value = getter ? getter.call(obj) : val
  /* eslint-disable no-self-compare */
  if (newVal === value || (newVal !== newVal && value !== value)) {
    return
  }
  /* eslint-enable no-self-compare */
  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()
}
//在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力  

首先是一系列的驗證判斷,可以不用管,然後設置數據的值爲傳入的值,這是一般 set函數都會執行的方法

然後到 childOb = !shallow && observe(newVal),一般情況下,shallow都是 trueth值,所以會調用 observe,經過上面的分析,我們知道這個 observe就是依賴收集相關的東西,這裏的意思就是對新設置的值也進行依賴收集,加入到響應式系統中來

接下來這行代碼纔是關鍵:

dep.notify()
  

看下 Dep

// node_modules\vue\src\core\observer\dep.js
notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  // ...
  for (let i = 0, l = subs.length; i < l; i++) {
    subs[i].update()
  }
}
  

notify方法中,遍歷了 subs,對每個項調用 update方法,經過前面的分析我們知道,subs的每個項其實都是依賴收集起來的 watcher,這裏也就是調用了 watcherupdate方法,通過 update來觸發對應的 watcher實現頁面更新

所以,Dep其實就是一個 watcher管理模塊,當數據變化時,會被 Observer監測到,然後由 Dep通知到 watcher

update () {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync) {
    this.run()
  } else {
    queueWatcher(this)
  }
}
  

this.lazycomputed相關,computed是惰性求值的,所以這裏只是把 this.dirty設爲 true,並沒有做什麼更新的操作;

this.syncwatch相關,如果 watch設置了這個值爲 true,則是顯式要求 watch更新需要在當前 Tick 一併執行,不必放到下一個 Tick

這兩個暫時不看,不擴充太多避免邏輯太亂,正常流程會執行 queueWatcher(this)

// node_modules\vue\src\core\observer\scheduler.js
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}
  //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

queueWatcher首先會根據 has[id]來避免同一 watcher的重複添加,接下來引入了隊列的概念,vue並不會在每次數據改變的時候就立即執行 watcher重渲染頁面,而是把這些 watcher 先推送到一個隊列裏,然後在nextTick 裏調用 flushSchedulerQueue批量執行這些 watcher,更新 DOM

這裏在 nextTick裏執行 flushSchedulerQueue的目的就是爲了要等到當前 Tick中所有的 watcher都加入到 queue中,再在下一 Tick中執行隊列中的 watcher

看下這個 flushSchedulerQueue方法,首先對隊列中的 watcher根據其 id進行排序,將 id小的 watcher放在前面(父組件 watcherid小於子組件的), 排序的目的也已經在註釋中解釋地很清楚了:

// node_modules\vue\src\core\observer\scheduler.js

// Sort queue before flush.
// This ensures that:
// 1\. Components are updated from parent to child. (because parent is always
//    created before the child)
// 2\. A component's user watchers are run before its render watcher (because
//    user watchers are created before the render watcher)
// 3\. If a component is destroyed during a parent component's watcher run,
//    its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
  

大概意思就是,在清空隊列之前對隊列進行排序,主要是爲了以下 3

  • 組件的更新是由父到子的(因爲父組件的創建在子組件之前),所以 watcher的創建也應該是先父後子,執行順序也應該保持先父後子

  • 用戶自定義 watcher應該在 渲染 watcher之前執行(因爲用戶自定義 watcher的創建在 渲染watcher之前)

  • 如果一個組件在父組件的 watcher 執行期間被銷燬,那麼這個子組件的 watcher 都可以被跳過

排完序之後,使用了一個 for循環遍歷隊列,執行每個 watcherrun方法,那麼就來看下這個 run方法

// node_modules\vue\src\core\observer\watcher.js
run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}
  

首先判斷 this.active,這個 this.active的初始值是 true,那麼什麼時候會變成 false呢?當 watcher從所有 Dep中移除的時候,也就是這個 watcher移除掉了,所以也就沒有什麼派發更新的事情了

// node_modules\vue\src\core\observer\watcher.js
teardown () {
  // ...
  this.active = false
}
  

接着執行 const value = this.get()獲取到當前值,調用 watcherget方法的時候會執行 watchergetter方法:

// node_modules\vue\src\core\observer\watcher.js
get () {
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    value = this.getter.call(vm, vm)
  } catch (e) {
    // ...
  }
  // ...
  return value
}
  

而這個 getter前面已經說了,其實就是:

// node_modules\vue\src\core\instance\lifecycle.js
updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
  //在此我向大家推薦一個前端全棧開發交流圈:582735936  突破技術瓶頸,提升思維能力

也就是執行了 DOM更新的操作

回到 flushSchedulerQueue,在執行完 watcher.run()之後,還有些收尾工作,主要是執行了 resetSchedulerState方法

// node_modules\vue\src\core\observer\scheduler.js
function resetSchedulerState () {
  index = queue.length = activatedChildren.length = 0
  has = {}
  if (process.env.NODE_ENV !== 'production') {
    circular = {}
  }
  waiting = flushing = false
}
  

這個方法主要是用於重置隊列狀態,比如最後將 waitingflushing置爲 false,這樣一來,當下次調用 queueWatcher的時候,就又可以往 queue隊列裏堆 watcher

回到 queueWatcher這個方法

if (!flushing) {
  queue.push(watcher)
} else {
  // if already flushing, splice the watcher based on its id
  // if already past its id, it will be run next immediately.
  let i = queue.length - 1
  while (i > index && queue[i].id > watcher.id) {
    i--
  }
  queue.splice(i + 1, 0, watcher)
}
  

flushSchedulerQueue執行,進行批量處理 watcher的時候,flushing將被置爲 true,這個時候如果再次添加新的 user watcher進來,那麼就會立即添加到 queue中去

這裏採取改變 queue的方式是原數組修改,也就是說添加進去的 watcher會立即加入到 flushSchedulerQueue批處理的進程中,因而在 flushSchedulerQueue中對 queue的循環處理中,for循環是實時獲取 queue的長度的

// node_modules\vue\src\core\observer\scheduler.js
function flushSchedulerQueue () {
  // ...
  for (index = 0; index < queue.length; index++) {
    // ...
  }
  // ...
}
  

另外,新加入的 watcher加到 queue的位置也是根據id進行排序的,契合上面所說的 watch執行先父後子的理念

大體流程示意圖如下:

總結

vue的代碼相比於 react的其實還是挺適合閱讀的,我本來還打算打斷點慢慢看,沒想到根本沒用到,這也表明了vue的輕量級確實是有原因的

少了各種模式和各種系統的堆砌,但同時又能滿足一般業務的開發需要,代碼體積小意味着會有更多的人有興趣將其接入移動端,概念少意味着小白也能快速上手,俗話說得小白者得天下,vue能與 react這種頂級大廠團伙化規模維護的框架庫分庭抗禮也不是沒有道理的

結語

感謝您的觀看,如有不足之處,歡迎批評指正。
獲取資料👈👈👈
本次給大家推薦一個免費的學習羣,裏面概括移動應用網站開發,css,html,webpack,vue node angular以及面試資源等。
對web開發技術感興趣的同學,歡迎加入Q羣:👉👉👉582735936 👈👈👈,不管你是小白還是大牛我都歡迎,還有大牛整理的一套高效率學習路線和教程與您免費分享,同時每天更新視頻資料。
最後,祝大家早日學有所成,拿到滿意offer,快速升職加薪,走上人生巔峯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章