vue observer 源碼學習

1、版本:2.5.9

2、建議

      vue最重要的應該就是響應式更新了,剛開始接觸vue或多或少都能從官方文檔或者其餘地方知道vue響應式更新依賴於Object.defineProperty()方法,這個方法在MDN上有詳細講解,不過,若是是初學者的話,直接去看響應式更新源碼還有點難度的,最好是先用項目練一遍,對vue有個相對熟悉的瞭解,而後能夠去各大熱門講解的博客上看看人家的講解,這樣彙總一番有點底子了再去看源碼實現相對輕鬆點。 最低級別的監聽能夠看我這個庫:https://github.com/lizhongzhen11/obj 參考:https://segmentfault.com/a/1190000009054946 https://segmentfault.com/a/1190000004384515vue

3、閱讀

      從github上把vueclone下來,或者直接在github上看也行。       別的先無論,直接去src/core/observer文件夾,這個明顯就是vue響應式更新源碼精華所在,內部共有array.js,dep.js,index.js,scheduler.js,traverse.js,watcher.js6個文件,先看哪個呢?第一次看沒有頭緒的話就先看index.js。       index.js開頭import了很多文件,先不用管,往下看須要用到時再去查找不遲。而第一步就用到了arrayMethods,該對象來自array.js,下面同時列出array.js中的相關代碼:java

// index.js
import { arrayMethods } from './array'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
// array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
複製代碼

      如上所示,arrayMethods實際上是一個Array.prototype的實例,只不過中間通過arrayProto過渡,一開始我還在糾結下方的代碼(對數組push等方法遍歷添加到剛剛建立的實例arrayMethods中,這裏沒有列出來),由於沒看到下方代碼有export,感受很奇怪,並且他代碼是下面這樣的,[]前有個;,感受很奇怪,vue做者是不寫;的,這裏出現一個;感受很突兀。PS:後來問了前輩,前輩解釋說:在js文件合併的時候,防止前一個js文件沒有;結尾致使的錯誤react

;['push','pop','shift','unshift','splice','sort','reverse']
複製代碼

      接下來,go on!定義了一個「觀察狀態」變量,內部有一個是否能夠覆蓋的布爾屬性。註釋裏面說不想強制覆蓋凍結數據結構下的嵌套值,以免優化失敗git

export const observerState = {
  shouldConvert: true
}
複製代碼

      繼續往下看,來到了重頭戲:Observer類,註釋中也說的明白:該類屬於每一個被觀察的對象,observer在目標對象的屬性的getter/setters覆蓋鍵同時蒐集依賴以及分發更新。es6

import Dep from './dep'
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }
  /** * Walk through each property and convert them into * getter/setters. This method should only be called when * value type is Object. */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
  /** * Observe a list of Array items. */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製代碼

      構造函數裏面第二步this.dep = new Dep(),這個Dep來自dep.js,這時候,得須要去看看dep.js裏面相關的代碼了:github

let uid = 0
/** * A dep is an observable that can have multiple * directives subscribing to it. * dep是可觀察的,能夠有多個指令訂閱它 */
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() // 更新 Watcher 數組中的數據
    }
  }
}
複製代碼

      Dep內部用到了Watcher,而Watcher又來自watcher.js。先說Dep,內部主要對Watcher類型的數組進行增長刪除以及更新維護,本身內部沒有什麼太多複雜的邏輯,主要仍是在watcher.js中。接下來列出watcher.js相關代碼:vue-cli

let uid = 0
/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
export default class Watcher {
  // 先看構造函數,內部變量不列出來了,太多了
  constructor (vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this // 直接在vue 頁面裏打印 this 能夠找到_watcher屬性
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep // 這裏多是怕萬一 options 對象裏沒有 deep 等屬性,因此用了 !! 來強轉成布爾型
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set() // es6語法,相似java Set集合,不會添加劇複數據
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = function () {}
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy ? undefined : this.get()
  }
複製代碼

      上面構造函數第一個參數vm是什麼?若是一直用vue-cli構建工具開發的話,可能沒怎麼注意過,**其實vm就是vue的一個實例!!!**第二個參數expOrFn暫時還不清楚,若是是函數的話直接賦給this.getter,不然this.getter直接指向一個空函數,同時還發出警報,須要傳遞一個函數。最後,判斷this.lazy,爲true的話調用this.get()方法:express

import Dep, { pushTarget, popTarget } from './dep'
/** * Evaluate the getter, and re-collect dependencies. * 對 getter 求值,並從新收集依賴 */
  get () {
    pushTarget(this) // 至關於 Dep.target = 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() // 清理deps,爲了依賴收集
    }
    return value
  }
  // dep.js
  export function pushTarget (_target: Watcher) {
    if (Dep.target) targetStack.push(Dep.target)
    Dep.target = _target
  }
  export function popTarget () {
    Dep.target = targetStack.pop()
  }
複製代碼

      get()中最終會判斷cthis.deep是否爲true,若是是調用traverse(value),而traverse()來自traverse.js,其目的是把dep.id加進去;popTarget()是爲了將以前pushTarget(this)target移除。segmentfault

/** * Clean up for dependency collection. */
  cleanupDeps () {
    let i = this.deps.length
    while (i--) {
      const dep = this.deps[i]
      if (!this.newDepIds.has(dep.id)) {
        dep.removeSub(this)
      }
    }
    let tmp = this.depIds
    this.depIds = this.newDepIds
    this.newDepIds = tmp
    this.newDepIds.clear() // newDepIds 是Set類型,能夠經過clear()清空
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }
複製代碼

      cleanupDeps()方法將舊的依賴編號與新的依賴集合編號進行對比,若是舊依賴數組中存在的編號,而新依賴集合編號中不存在,就須要刪除對應編號的依賴;接下來交換新舊依賴集合編號,而後清空this.newDepIds(其實此時該集合內保存的是舊有的依賴集合編號);隨後交換新舊依賴數組,而後來了一步騷操做:this.newDeps.length = 0,將this.newDeps清空,比較騷。api

      也就是說,利用get()方法求值後會清理依賴收集。       到了get()能夠先暫停回顧一下。這裏是在Watcher構造函數中調用的,也就是說,當new Watcher()時就會走遍上述代碼,包括調用get()來取值。

這時候若是繼續強行看完Watcher下面的源碼,會發現沒什麼頭緒,因此依然回到index.js中。繼續研究Observer類的構造函數。

constructor (value: any) {
  this.value = value
  this.dep = new Dep()
  this.vmCount = 0
  def(value, '__ob__', this)
  if (Array.isArray(value)) {
    const augment = hasProto ? protoAugment : copyAugment
    augment(value, arrayMethods, arrayKeys)
    this.observeArray(value)
  } else {
    this.walk(value)
  }
}
複製代碼

      構造函數中緊跟着調用了def(value, '__ob__', this),這個方法是幹嗎的?在哪裏?       經過查找發現def方法位於util/lang.js內,下面貼出源碼:

/** * Define a property. */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}
複製代碼

      def內部調用了Object.defineProperty(),結合Observer構造函數的傳參,可知這裏給每一個對象定義了一個__ob__屬性,在平常開發中,當咱們打印輸出時常常能看到__ob__。       接下來進一步判斷value是否是數組,若是不是的話調用walk(),固然要確保參數是Object,而後遍歷對象的key而且每一個調用defineReactive(obj, keys[i], obj[keys[i]])

看看defineReactive()方法內部實現:

export function defineReactive (obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean) {
  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 // 緩存對象屬性內的get方法
  const setter = property && property.set // 緩存對象屬性內的set方法
  let childOb = !shallow && observe(val)  // observe(val)嘗試返回一個 observer實例,若是 !shallow === true 那麼 childOb === ob
                                          // 其實也能夠理解爲, childOb === val.__ob__
  Object.defineProperty(obj, key, {       // 這裏開始是真正的核心所在,其實就是從新對象的get、set方法,方便監聽
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val // getter 存在的話就調用原生的 get 方法取值,不然用傳進來的值
      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
      /* 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()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal) // childOb === newVal.__ob__
      dep.notify() // 內部調用了 watcher.js 裏面的 uodate(),內部又調用了 run(),run()裏面設置值,其中還用到了watcher隊列
    }
  })
}
複製代碼

      響應式更新的重中之重就是首先得監聽到對象屬性值的改變,vue經過defineReactive()內部重寫傳入的對象屬性中的set以及get方法,其中,js原生的call()也有很大的功勞。

總結

      再一次看vue源碼明顯比第一次看好多了,可是不斷地調用其它方法,理解上仍是有必定的難度,這一次閱讀源碼更多的就是作個筆記,寫得並很差,可是留個印象,方便下次再看。

相關文章
相關標籤/搜索