vue observer 原始碼學習

pro-xiaoy發表於2018-06-28

一、版本:2.5.9

二、建議

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

三、閱讀

      從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中的相關程式碼:

// 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檔案沒有;結尾導致的錯誤

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

      接下來,go on!定義了一個“觀察狀態”變數,內部有一個是否可以覆蓋的布林屬性。註釋裡面說不想強制覆蓋凍結資料結構下的巢狀值,以避免優化失敗

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

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

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裡面相關的程式碼了:

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相關程式碼:

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()方法:

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移除。

/**
   * 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清空,比較騷。

      也就是說,利用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原始碼明顯比第一次看好多了,但是不斷地呼叫其它方法,理解上還是有一定的難度,這一次閱讀原始碼更多的就是做個筆記,寫得並不好,但是留個印象,方便下次再看。

相關文章