Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析

這篇文章主要介紹“Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析”,在日常操作中,相信很多人在Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析問題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析”的疑惑有所幫助!接下來,請(qǐng)跟著小編一起來學(xué)習(xí)吧!

成都創(chuàng)新互聯(lián)公司自2013年創(chuàng)立以來,先為隴川等服務(wù)建站,隴川等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為隴川企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問題。

初始化 Vue 實(shí)例

在閱讀源碼時(shí),因?yàn)槲募倍?,引用?fù)雜往往使我們不容易抓住重點(diǎn),這里我們需要找到一個(gè)入口文件,從 Vue 構(gòu)造函數(shù)開始,拋開其他無關(guān)因素,一步步理解響應(yīng)式數(shù)據(jù)的實(shí)現(xiàn)原理。首先我們找到 Vue 構(gòu)造函數(shù):

// 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)
}
// src/core/instance/init.js
Vue.prototype._init = function (options) {
 ...
 // a flag to avoid this being observed
 vm._isVue = true
 // merge options
 // 初始化vm實(shí)例的$options
 if (options && options._isComponent) {
  initInternalComponent(vm, options)
 } else {
  vm.$options = mergeOptions(
   resolveConstructorOptions(vm.constructor),
   options || {},
   vm
  )
 }
 ...
 initLifecycle(vm) // 梳理實(shí)例的parent、root、children和refs,并初始化一些與生命周期相關(guān)的實(shí)例屬性
 initEvents(vm) // 初始化實(shí)例的listeners
 initRender(vm) // 初始化插槽,綁定createElement函數(shù)的vm實(shí)例
 callHook(vm, 'beforeCreate')
 initInjections(vm) // resolve injections before data/props
 initState(vm)
 initProvide(vm) // resolve provide after data/props
 callHook(vm, 'created')
 
 if (vm.$options.el) {
  vm.$mount(vm.$options.el) // 掛載組件到節(jié)點(diǎn)
 }
}

為了方便閱讀,我們?nèi)コ?flow 類型檢查和部分無關(guān)代碼??梢钥吹?,在實(shí)例化Vue組件時(shí),會(huì)調(diào)用 Vue.prototype._init ,而在方法內(nèi)部,數(shù)據(jù)的初始化操作主要在 initState (這里的 initInjectionsinitProvideinitProps 類似,在理解了 initState 原理后自然明白),因此我們重點(diǎn)來關(guān)注 initState 。

// src/core/instance/state.js
export function initState (vm) {
 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)
 }
}

首先初始化了一個(gè) _watchers 數(shù)組,用來存放 watcher ,之后根據(jù)實(shí)例的 vm.$options ,相繼調(diào)用 initProps 、 initMethods 、 initDatainitComputedinitWatch 方法。

initProps

function initProps (vm, propsOptions) {
 const propsData = vm.$options.propsData || {}
 const props = vm._props = {}
 // cache prop keys so that future props updates can iterate using Array
 // instead of dynamic object key enumeration.
 const keys = vm.$options._propKeys = []
 const isRoot = !vm.$parent
 // root instance props should be converted
 if (!isRoot) {
 toggleObserving(false)
 }
 for (const key in propsOptions) {
 keys.push(key)
 const value = validateProp(key, propsOptions, propsData, vm)
 ...
 defineReactive(props, key, value)
 if (!(key in vm)) {
  proxy(vm, '_props', key)
 }
 }
 toggleObserving(true)
}

在這里, vm.$options.propsData 是通過父組件傳給子組件實(shí)例的數(shù)據(jù)對(duì)象,如 <my-element :item="false"></my-element> 中的 {item: false} ,然后初始化 vm._propsvm.$options._propKeys 分別用來保存實(shí)例的 props 數(shù)據(jù)和 keys ,因?yàn)樽咏M件中使用的是通過 proxy 引用的 _props 里的數(shù)據(jù),而不是父組件傳遞的 propsData ,所以這里緩存了 _propKeys ,用來 updateChildComponent 時(shí)能更新 vm._props 。接著根據(jù) isRoot 是否是根組件來判斷是否需要調(diào)用 toggleObserving(false) ,這是一個(gè)全局的開關(guān),來控制是否需要給對(duì)象添加 __ob__ 屬性。這個(gè)相信大家都不陌生,一般的組件的 data 等數(shù)據(jù)都包含這個(gè)屬性,這里先不深究,等之后和 defineReactive 時(shí)一起講解。因?yàn)?props 是通過父?jìng)鹘o子的數(shù)據(jù),在父元素 initState 時(shí)已經(jīng)把 __ob__ 添加上了,所以在不是實(shí)例化根組件時(shí)關(guān)閉了這個(gè)全局開關(guān),待調(diào)用結(jié)束前在通過 toggleObserving(true) 開啟。

之后是一個(gè) for 循環(huán),根據(jù)組件中定義的 propsOptions 對(duì)象來設(shè)置 vm._props ,這里的 propsOptions 就是我們常寫的

export default {
  ...
  props: {
    item: {
      type: Object,
      default: () => ({})
    }
  }
}

循環(huán)體內(nèi),首先

const value = validateProp(key, propsOptions, propsData, vm)

validateProp 方法主要是校驗(yàn)數(shù)據(jù)是否符合我們定義的 type ,以及在 propsData 里未找到 key 時(shí),獲取默認(rèn)值并在對(duì)象上定義 __ob__ ,最后返回相應(yīng)的值,在這里不做展開。

這里我們先跳過 defineReactive ,看最后

if (!(key in vm)) {
 proxy(vm, '_props', key)
}

其中 proxy 方法:

function proxy (target, sourceKey, key) {
 sharedPropertyDefinition.get = function proxyGetter () {
  return this[sourceKey][key]
 }
 sharedPropertyDefinition.set = function proxySetter (val) {
  this[sourceKey][key] = val
 }
 Object.defineProperty(target, key, sharedPropertyDefinition)
}

vm 不存在 key 屬性時(shí),通過 Object.defineProperty 使得我們能通過 vm[key] 訪問到 vm._props[key] 。

defineReactive

initProps 中,我們了解到其首先根據(jù)用戶定義的 vm.$options.props 對(duì)象,通過對(duì)父組件設(shè)置的傳值對(duì)象 vm.$options.propsData 進(jìn)行數(shù)據(jù)校驗(yàn),返回有效值并保存到 vm._props ,同時(shí)保存相應(yīng)的 keyvm.$options._propKeys 以便進(jìn)行子組件的 props 數(shù)據(jù)更新,最后利用 getter/setter 存取器屬性,將 vm[key] 指向?qū)?vm._props[key] 的操作。但其中跳過了最重要的 defineReactive ,現(xiàn)在我們將通過閱讀 defineReactive 源碼,了解響應(yīng)式數(shù)據(jù)背后的實(shí)現(xiàn)原理。

// src/core/observer/index.js
export function defineReactive (
 obj,
 key,
 val,
 customSetter,
 shallow
) {
 const dep = new Dep()

 const property = Object.getOwnPropertyDescriptor(obj, key)
 if (property && property.configurable === false) {
  return
 }

 // cater for pre-defined getter/setters
 const getter = property && property.get
 const setter = property && property.set
 if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
 }

 let childOb = !shallow && observe(val)
 ...
}

首先 const dep = new Dep() 實(shí)例化了一個(gè) dep ,在這里利用閉包來定義一個(gè)依賴項(xiàng),用以與特定的 key 相對(duì)應(yīng)。因?yàn)槠渫ㄟ^ Object.defineProperty 重寫 target[key]getter/setter 來實(shí)現(xiàn)數(shù)據(jù)的響應(yīng)式,因此需要先判斷對(duì)象 keyconfigurable 屬性。接著

if ((!getter || setter) && arguments.length === 2) {
  val = obj[key]
}

arguments.length === 2 意味著調(diào)用 defineReactive 時(shí)未傳遞 val 值,此時(shí) valundefined ,而 !getter || setter 判斷條件則表示如果在 property 存在 getter 且不存在 setter 的情況下,不會(huì)獲取 key 的數(shù)據(jù)對(duì)象,此時(shí) valundefined ,之后調(diào)用 observe 時(shí)將不對(duì)其進(jìn)行深度觀察。正如之后的 setter 訪問器中的:

if (getter && !setter) return

此時(shí)數(shù)據(jù)將是只讀狀態(tài),既然是只讀狀態(tài),則不存在數(shù)據(jù)修改問題,繼而無須深度觀察數(shù)據(jù)以便在數(shù)據(jù)變化時(shí)調(diào)用觀察者注冊(cè)的方法。

Observe

defineReactive 里,我們先獲取了 target[key]descriptor ,并緩存了對(duì)應(yīng)的 gettersetter ,之后根據(jù)判斷選擇是否獲取 target[key] 對(duì)應(yīng)的 val ,接著是

let childOb = !shallow && observe(val)

根據(jù) shallow 標(biāo)志來確定是否調(diào)用 observe ,我們來看下 observe 函數(shù):

// src/core/observer/index.js
export function observe (value, asRootData) {
 if (!isObject(value) || value instanceof VNode) {
  return
 }
 let ob
 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
  ob = value.__ob__
 } else if (
  shouldObserve &&
  !isServerRendering() &&
  (Array.isArray(value) || isPlainObject(value)) &&
  Object.isExtensible(value) &&
  !value._isVue
 ) {
  ob = new Observer(value)
 }
 if (asRootData && ob) {
  ob.vmCount++
 }
 return ob
}

首先判斷需要觀察的數(shù)據(jù)是否為對(duì)象以便通過 Object.defineProperty 定義 __ob__ 屬性,同時(shí)需要 value 不屬于 VNode 的實(shí)例( VNode 實(shí)例通過 Diff 補(bǔ)丁算法來實(shí)現(xiàn)實(shí)例對(duì)比并更新)。接著判斷 value 是否已有 __ob__ ,如果沒有則進(jìn)行后續(xù)判斷:

  •  shouldObserve:全局開關(guān)標(biāo)志,通過toggleObserving來修改。

  • !isServerRendering():判斷是否服務(wù)端渲染。

  • (Array.isArray(value) || isPlainObject(value)):數(shù)組和純對(duì)象時(shí)才允許添加__ob__進(jìn)行觀察。

  • Object.isExtensible(value):判斷value是否可擴(kuò)展。

  • !value._isVue:避免Vue實(shí)例被觀察

滿足以上五個(gè)條件時(shí),才會(huì)調(diào)用 ob = new Observer(value) ,接下來我們要看下 Observer 類里做了哪些工作

// src/core/observer/index.js
export class Observer {
 constructor (value) {
  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)
  }
 }

 /**
  * Walk through all properties and convert them into
  * getter/setters. This method should only be called when
  * value type is Object.
  */
 walk (obj) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
   defineReactive(obj, keys[i])
  }
 }

 /**
  * Observe a list of Array items.
  */
 observeArray (items) {
  for (let i = 0, l = items.length; i < l; i++) {
   observe(items[i])
  }
 }
}

構(gòu)造函數(shù)里初始化了 value 、 dep 和 vmCount 三個(gè)屬性,為 this.value 添加 __ob__ 對(duì)象并指向自己,即 value.__ob__.value === value ,這樣就可以通過 value 或 __ob__ 對(duì)象取到 dep 和 value 。 vmCount 的作用主要是用來區(qū)分是否為 Vue 實(shí)例的根 data , dep 的作用這里先不介紹,待與 getter/setter 里的 dep 一起解釋。

接著根據(jù) value 是數(shù)組還是純對(duì)象來分別調(diào)用相應(yīng)的方法,對(duì) value 進(jìn)行遞歸操作。當(dāng) value 為純對(duì)象時(shí),調(diào)用 walk 方法,遞歸調(diào)用 defineReactive 。當(dāng) value 是數(shù)組類型時(shí),首先判斷是否有 __proto__ ,有就使用 __proto__ 實(shí)現(xiàn)原型鏈繼承,否則用 Object.defineProperty 實(shí)現(xiàn)拷貝繼承。其中繼承的基類 arrayMethods 來自 src/core/observer/array.js :

// src/core/observer/array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
 'push',
 'pop',
 'shift',
 'unshift',
 'splice',
 'sort',
 'reverse'
]

methodsToPatch.forEach(function (method) {
 // cache original method
 const original = arrayProto[method]
 def(arrayMethods, method, function mutator (...args) {
  const result = original.apply(this, args)
  const ob = this.__ob__
  let inserted
  switch (method) {
   case 'push':
   case 'unshift':
    inserted = args
    break
   case 'splice':
    inserted = args.slice(2)
    break
  }
  if (inserted) ob.observeArray(inserted)
  // notify change
  ob.dep.notify()
  return result
 })
})

這里為什么要對(duì)數(shù)組的實(shí)例方法進(jìn)行重寫呢?代碼里的 methodsToPatch 這些方法并不會(huì)返回新的數(shù)組,導(dǎo)致無法觸發(fā) setter ,因而不會(huì)調(diào)用觀察者的方法。所以重寫了這些變異方法,使得在調(diào)用的時(shí)候,利用 observeArray 對(duì)新插入的數(shù)組元素添加 __ob__ ,并能夠通過 ob.dep.notify 手動(dòng)通知對(duì)應(yīng)的被觀察者執(zhí)行注冊(cè)的方法,實(shí)現(xiàn)數(shù)組元素的響應(yīng)式。

if (asRootData && ob) {
  ob.vmCount++
}

最后添加這個(gè) if 判斷,在 Vue 實(shí)例的根 data 對(duì)象上,執(zhí)行 ob.vmCount++ ,這里主要為了后面根據(jù) ob.vmCount 來區(qū)分是否為根數(shù)據(jù),從而在其上執(zhí)行 Vue.set 和 Vue.delete 。

getter/setter

在對(duì) val 進(jìn)行遞歸操作后(假如需要的話),將 obj[key] 的數(shù)據(jù)對(duì)象封裝成了一個(gè)被觀察者,使得能夠被觀察者觀察,并在需要的時(shí)候調(diào)用觀察者的方法。這里通過 Object.defineProperty 重寫了 obj[key] 的訪問器屬性,對(duì) getter/setter 操作做了攔截處理, defineReactive 剩余的代碼具體如下:

...
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) {
  ...
  childOb = !shallow && observe(newVal)
  dep.notify()
 }
})

首先在 getter 調(diào)用時(shí),判斷 Dep.target 是否存在,若存在則調(diào)用 dep.depend 。我們先不深究 Dep.target ,只當(dāng)它是一個(gè)觀察者,比如我們常用的某個(gè)計(jì)算屬性,調(diào)用 dep.depend 會(huì)將 dep 當(dāng)做計(jì)算屬性的依賴項(xiàng)存入其依賴列表,并把這個(gè)計(jì)算屬性注冊(cè)到這個(gè) dep 。這里為什么需要互相引用呢?這是因?yàn)橐粋€(gè) target[key] 可以充當(dāng)多個(gè)觀察者的依賴項(xiàng),同時(shí)一個(gè)觀察者可以有多個(gè)依賴項(xiàng),他們之間屬于多對(duì)多的關(guān)系。這樣當(dāng)某個(gè)依賴項(xiàng)改變時(shí),我們可以根據(jù) dep 里維護(hù)的觀察者,調(diào)用他們的注冊(cè)方法?,F(xiàn)在我們回過頭來看 Dep :

// src/core/observer/dep.js
export default class Dep {
 static target: ?Watcher;
 id: number;
 subs: Array<Watcher>;

 constructor () {
  this.id = uid++
  this.subs = []
 }

 addSub (sub: Watcher) {
  this.subs.push(sub)
 }

 removeSub (sub: Watcher) {
  remove(this.subs, sub)
 }

 depend () {
  if (Dep.target) {
   Dep.target.addDep(this)
  }
 }

 notify () {
  // stabilize the subscriber list first
  const subs = this.subs.slice()
  ...
  for (let i = 0, l = subs.length; i < l; i++) {
   subs[i].update()
  }
 }
}

構(gòu)造函數(shù)里,首先添加一個(gè)自增的 uid 用以做 dep 實(shí)例的唯一性標(biāo)志,接著初始化一個(gè)觀察者列表 subs ,并定義了添加觀察者方法 addSub 和移除觀察者方法 removeSub ??梢钥吹狡湓?getter 中調(diào)用的 depend 會(huì)將當(dāng)前這個(gè) dep 實(shí)例添加到觀察者的依賴項(xiàng),在 setter 里調(diào)用的 notify 會(huì)執(zhí)行各個(gè)觀察者注冊(cè)的 update 方法, Dep.target.addDep 這個(gè)方法將在之后的 Watcher 里進(jìn)行解釋。簡(jiǎn)單來說就是會(huì)在 key 的 getter 觸發(fā)時(shí)進(jìn)行 dep 依賴收集到 watcher 并將 Dep.target 添加到當(dāng)前 dep 的觀察者列表,這樣在 key 的 setter 觸發(fā)時(shí),能夠通過觀察者列表,執(zhí)行觀察者的 update 方法。

當(dāng)然,在 getter 中還有如下幾行代碼:

if (childOb) {
  childOb.dep.depend()
  if (Array.isArray(value)) {
    dependArray(value)
  }
}

這里可能會(huì)有疑惑,既然已經(jīng)調(diào)用了 dep.depend ,為什么還要調(diào)用 childOb.dep.depend ??jī)蓚€(gè) dep 之間又有什么關(guān)系呢?

其實(shí)這兩個(gè) dep 的分工是不同的。對(duì)于數(shù)據(jù)的增、刪,利用 childOb.dep.notify 來調(diào)用觀察者方法,而對(duì)于數(shù)據(jù)的修改,則使用的 dep.notify ,這是因?yàn)?setter 訪問器無法監(jiān)聽到對(duì)象數(shù)據(jù)的添加和刪除。舉個(gè)例子:

const data = {
  arr: [{
    value: 1
  }],
}

data.a = 1; // 無法觸發(fā)setter
data.arr[1] = {value: 2}; // 無法觸發(fā)setter
data.arr.push({value: 3}); // 無法觸發(fā)setter
data.arr = [{value: 4}]; // 可以觸發(fā)setter

還記得 Observer 構(gòu)造函數(shù)里針對(duì)數(shù)組類型 value 的響應(yīng)式轉(zhuǎn)換嗎?通過重寫 value 原型鏈,使得對(duì)于新插入的數(shù)據(jù):

if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()

將其轉(zhuǎn)換為響應(yīng)式數(shù)據(jù),并通過 ob.dep.notify 來調(diào)用觀察者的方法,而這里的觀察者列表就是通過上述的 childOb.dep.depend 來收集的。同樣的,為了實(shí)現(xiàn)對(duì)象新增數(shù)據(jù)的響應(yīng)式,我們需要提供相應(yīng)的 hack 方法,而這就是我們常用的 Vue.set/Vue.delete 。

// src/core/observer/index.js
export function set (target: Array<any> | Object, key: any, val: any): any {
 ...
 if (Array.isArray(target) && isValidArrayIndex(key)) {
  target.length = Math.max(target.length, key)
  target.splice(key, 1, val)
  return val
 }
 if (key in target && !(key in Object.prototype)) {
  target[key] = val
  return val
 }
 const ob = (target: any).__ob__
 if (target._isVue || (ob && ob.vmCount)) {
  process.env.NODE_ENV !== 'production' && warn(
   'Avoid adding reactive properties to a Vue instance or its root $data ' +
   'at runtime - declare it upfront in the data option.'
  )
  return val
 }
 if (!ob) {
  target[key] = val
  return val
 }
 defineReactive(ob.value, key, val)
 ob.dep.notify()
 return val
}
  • 判斷value是否為數(shù)組,如果是,直接調(diào)用已經(jīng)hack過的splice即可。

  • 是否已存在key,有的話說明已經(jīng)是響應(yīng)式了,直接修改即可。

  • 接著判斷target.__ob__是否存在,如果沒有說明該對(duì)象無須深度觀察,設(shè)置返回當(dāng)前的值。

  • 最后,通過defineReactive來設(shè)置新增的key,并調(diào)用ob.dep.notify通知到觀察者。

現(xiàn)在我們了解了 childOb.dep.depend() 是為了將當(dāng)前 watcher 收集到 childOb.dep ,以便在增、刪數(shù)據(jù)時(shí)能通知到 watcher 。而在 childOb.dep.depend() 之后還有:

if (Array.isArray(value)) {
  dependArray(value)
}
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value: Array<any>) {
 for (let e, i = 0, l = value.length; i < l; i++) {
  e = value[i]
  e && e.__ob__ && e.__ob__.dep.depend()
  if (Array.isArray(e)) {
   dependArray(e)
  }
 }
}

在觸發(fā) target[key] 的 getter 時(shí),如果 value 的類型為數(shù)組,則遞歸將其每個(gè)元素都調(diào)用 __ob__.dep.depend ,這是因?yàn)闊o法攔截?cái)?shù)組元素的 getter ,所以將當(dāng)前 watcher 收集到數(shù)組下的所有 __ob__.dep ,這樣當(dāng)其中一個(gè)元素觸發(fā)增、刪操作時(shí)能通知到觀察者。比如:

const data = {
  list: [[{value: 0}]],
};
data.list[0].push({value: 1});

這樣在 data.list[0].__ob__.notify 時(shí),才能通知到 watcher 。

target[key] 的 getter 主要作用:

將 Dep.target 收集到閉包中 dep 的觀察者列表,以便在 target[key] 的 setter 修改數(shù)據(jù)時(shí)通知觀察者

根據(jù)情況對(duì)數(shù)據(jù)進(jìn)行遍歷添加 __ob__ ,將 Dep.target 收集到 childOb.dep 的觀察者列表,以便在增加/刪除數(shù)據(jù)時(shí)能通知到觀察者

通過 dependArray 將數(shù)組型的 value 遞歸進(jìn)行觀察者收集,在數(shù)組元素發(fā)生增、刪、改時(shí)能通知到觀察者

target[key] 的 setter 主要作用是對(duì)新數(shù)據(jù)進(jìn)行觀察,并通過閉包保存到 childOb 變量供 getter 使用,同時(shí)調(diào)用 dep.notify 通知觀察者,在此就不再展開。

Watcher

在前面的篇幅中,我們主要介紹了 defineReactive 來定義響應(yīng)式數(shù)據(jù):通過閉包保存 dep 和 childOb ,在 getter 時(shí)來進(jìn)行觀察者的收集,使得在數(shù)據(jù)修改時(shí)能觸發(fā) dep.notify 或 childOb.dep.notify 來調(diào)用觀察者的方法進(jìn)行更新。但具體是如何進(jìn)行 watcher 收集的卻未做過多解釋,現(xiàn)在我們將通過閱讀 Watcher 來了解觀察者背后的邏輯。

function initComputed (vm: Component, computed: Object) {
 const watchers = vm._computedWatchers = Object.create(null)
 const isSSR = isServerRendering()

 for (const key in computed) {
  const userDef = computed[key]
  const getter = typeof userDef === 'function' ? userDef : userDef.get

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

這是 Vue 計(jì)算屬性的初始化操作,去掉了一部分不影響的代碼。首先初始化對(duì)象 vm._computedWatchers 用以存儲(chǔ)所有的計(jì)算屬性, isSSR 用以判斷是否為服務(wù)端渲染。再根據(jù)我們編寫的 computed 鍵值對(duì)循環(huán)遍歷,如果不是服務(wù)端渲染,則為每個(gè)計(jì)算屬性實(shí)例化一個(gè) Watcher ,并以鍵值對(duì)的形式保存到 vm._computedWatchers 對(duì)象,接下來我們主要看下 Watcher 這個(gè)類。

Watcher 的構(gòu)造函數(shù)

構(gòu)造函數(shù)接受5個(gè)參數(shù),其中當(dāng)前 Vue 實(shí)例 vm 、求值表達(dá)式 expOrFn (支持 Function 或者 String ,計(jì)算屬性中一般為 Function ),回調(diào)函數(shù) cb 這三個(gè)為必傳參數(shù)。設(shè)置 this.vm = vm 用以后續(xù)綁定 this.getter 的執(zhí)行環(huán)境,并將 this 推入 vm._watchers ( vm._watchers 用以維護(hù)實(shí)例 vm 中所有的觀察者),另外根據(jù)是否為渲染觀察者來賦值 vm._watcher = this (常用的 render 即為渲染觀察者)。接著根據(jù) options 進(jìn)行一系列的初始化操作。其中有幾個(gè)屬性:

  • this.lazy:設(shè)置是否懶求值,這樣能保證有多個(gè)被觀察者發(fā)生變化時(shí),能只調(diào)用求值一次。

  • this.dirty:配合this.lazy,用以標(biāo)記當(dāng)前觀察者是否需要重新求值。

  • this.deps、this.newDeps、this.depIds、this.newDepIds:用以維護(hù)被觀察對(duì)象的列表。

  • this.getter:求值函數(shù)。

  • this.value:求值函數(shù)返回的值,即為計(jì)算屬性中的值。

Watcher 的求值

因?yàn)橛?jì)算屬性是惰性求值,所以我們繼續(xù)看 initComputed 循環(huán)體:

if (!(key in vm)) {
 defineComputed(vm, key, userDef)
}

defineComputed 主要將 userDef 轉(zhuǎn)化為 getter/setter 訪問器,并通過 Object.defineProperty 將 key 設(shè)置到 vm 上,使得我們能通過 this[key] 直接訪問到計(jì)算屬性。接下來我們主要看下 userDef 轉(zhuǎn)為 getter 中的 createComputedGetter 函數(shù):

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
  }
 }
}

利用閉包保存計(jì)算屬性的 key ,在 getter 觸發(fā)時(shí),首先通過 this._computedWatchers[key] 獲取到之前保存的 watcher ,如果 watcher.dirty 為 true 時(shí)調(diào)用 watcher.evaluate (執(zhí)行 this.get() 求值操作,并將當(dāng)前 watcher 的 dirty 標(biāo)記為 false ),我們主要看下 get 操作:

get () {
 pushTarget(this)
 let value
 const vm = this.vm
 try {
  value = this.getter.call(vm, vm)
 } catch (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
}

可以看到,求值時(shí)先執(zhí)行 pushTarget(this) ,通過查閱 src/core/observer/dep.js ,我們可以看到:

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]
}

pushTarget 主要是把 watcher 實(shí)例進(jìn)棧,并賦值給 Dep.target ,而 popTarget 則相反,把 watcher 實(shí)例出棧,并將棧頂賦值給 Dep.target 。 Dep.target 這個(gè)我們之前在 getter 里見到過,其實(shí)就是當(dāng)前正在求值的觀察者。這里在求值前將 Dep.target 設(shè)置為 watcher ,使得在求值過程中獲取數(shù)據(jù)時(shí)觸發(fā) getter 訪問器,從而調(diào)用 dep.depend ,繼而執(zhí)行 watcher 的 addDep 操作:

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

先判斷 newDepIds 是否包含 dep.id ,沒有則說明尚未添加過這個(gè) dep ,此時(shí)將 dep 和 dep.id 分別加到 newDepIds 和 newDeps 。如果 depIds 不包含 dep.id ,則說明之前未添加過此 dep ,因?yàn)槭请p向添加的(將 dep 添加到 watcher 的同時(shí)也需要將 watcher 收集到 dep ),所以需要調(diào)用 dep.addSub ,將當(dāng)前 watcher 添加到新的 dep 的觀察者隊(duì)列。

if (this.deep) {
 traverse(value)
}

再接著根據(jù) this.deep 來調(diào)用 traverse 。 traverse 的作用主要是遞歸遍歷觸發(fā) value 的 getter ,調(diào)用所有元素的 dep.depend() 并過濾重復(fù)收集的 dep 。最后調(diào)用 popTarget() 將當(dāng)前 watcher 移出棧,并執(zhí)行 cleanupDeps :

cleanupDeps () {
 let i = this.deps.length
 while (i--) {
  const dep = this.deps[i]
  if (!this.newDepIds.has(dep.id)) {
   dep.removeSub(this)
  }
 }
 ...
}

遍歷 this.deps ,如果在 newDepIds 中不存在 dep.id ,則說明新的依賴?yán)锊话?dāng)前 dep ,需要到 dep 的觀察者列表里去移除當(dāng)前這個(gè) watcher ,之后便是 depIds 和 newDepIds 、 deps 和 newDeps 的值交換,并清空 newDepIds 和 newDeps 。到此完成了對(duì) watcher 的求值操作,同時(shí)更新了新的依賴,最后返回 value 即可。

回到 createComputedGetter 接著看:

if (Dep.target) {
 watcher.depend()
}

當(dāng)執(zhí)行計(jì)算屬性的 getter 時(shí),有可能表達(dá)式中還有別的計(jì)算屬性依賴,此時(shí)我們需要執(zhí)行 watcher.depend 將當(dāng)前 watcher 的 deps 添加到 Dep.target 即可。最后返回求得的 watcher.value 即可。

總的來說我們從 this[key] 觸發(fā) watcher 的 get 函數(shù),將當(dāng)前 watcher 入棧,通過求值表達(dá)式將所需要的依賴 dep 收集到 newDepIds 和 newDeps ,并將 watcher 添加到對(duì)應(yīng) dep 的觀察者列表,最后清除無效 dep 并返回求值結(jié)果,這樣就完成了依賴關(guān)系的收集。

Watcher 的更新

以上我們了解了 watcher 的依賴收集和 dep 的觀察者收集的基本原理,接下來我們了解下 dep 的數(shù)據(jù)更新時(shí)如何通知 watcher 進(jìn)行 update 操作。

notify () {
 // stabilize the subscriber list first
 const subs = this.subs.slice()
 for (let i = 0, l = subs.length; i < l; i++) {
  subs[i].update()
 }
}

首先在 dep.notify 時(shí),我們將 this.subs 拷貝出來,防止在 watcher 的 get 時(shí)候 subs 發(fā)生更新,之后調(diào)用 update 方法:

update () {
 /* istanbul ignore else */
 if (this.lazy) {
  this.dirty = true
 } else if (this.sync) {
  this.run()
 } else {
  queueWatcher(this)
 }
}
  • 如果是 lazy ,則將其標(biāo)記為 this.dirty = true ,使得在 this[key] 的 getter 觸發(fā)時(shí)進(jìn)行 watcher.evaluate 調(diào)用計(jì)算。

  • 如果是 sync 同步操作,則執(zhí)行 this.run ,調(diào)用 this.get 求值和執(zhí)行回調(diào)函數(shù) cb 。

  • 否則執(zhí)行 queueWatcher ,選擇合適的位置,將 watcher 加入到隊(duì)列去執(zhí)行即可,因?yàn)楹晚憫?yīng)式數(shù)據(jù)無關(guān),故不再展開。

小結(jié)

因?yàn)槠邢蓿粚?duì)數(shù)據(jù)綁定的基本原理做了基本的介紹,在這畫了一張簡(jiǎn)單的流程圖來幫助理解 Vue 的響應(yīng)式數(shù)據(jù),其中省略了一些 VNode 等不影響理解的邏輯及邊界條件,盡可能簡(jiǎn)化地讓流程更加直觀:

Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析

到此,關(guān)于“Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

文章名稱:Vue響應(yīng)式數(shù)據(jù)中的觀察者模式實(shí)例簡(jiǎn)析
文章網(wǎng)址:http://muchs.cn/article28/gdeejp.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供小程序開發(fā)、網(wǎng)站收錄、Google、品牌網(wǎng)站建設(shè)、虛擬主機(jī)營(yíng)銷型網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)