Vue2.x源碼解析系列四:數據響應之Observer

寫在前面的話:關於做者言川

筆名言川, 前端工程師,精通 Vue/Webpack/Git等,熟悉Node/React等,涉獵普遍,對算法/後端/人工智能/linux等都有必定研究。開源愛好者,github上目前總計5000+ Starhtml

  • 個人github主頁:https://github.com/lihongxun945
  • 個人博客地址:https://github.com/lihongxun945/myblog
  • 個人掘金主頁:https://juejin.im/user/5756771b1532bc0064a2b024/posts
  • 個人知乎專欄:https://zhuanlan.zhihu.com/c_1007281871281090560

此博客原地址:https://github.com/lihongxun945/myblog/issues/25前端

若是你以前看過個人這一篇文章 Vue1.0源碼解析系列:實現數據響應化 ,那麼你能夠很輕鬆看懂 Vue2.x版本中的響應化,由於基本思路以及大部分代碼其實都沒有變化。固然沒看過也不要緊,不用去看,由於這裏我會講的很是詳細。vue

數據響應我會分兩章來說,本章講 Observer 相關,下一章講 Watcherreact

從data開始

state 的初始化是從 initState 函數開始的,下面是 initState 的完整代碼:linux

core/instance/state.jsgit

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)
  }
}
複製代碼

這裏包括了四個部分:props, methods, datawatch,爲了方便起見,讓咱們從最簡單的,可是也能完整揭示數據響應化原理的 data 做爲切入點。爲何選它呢,由於 props 還涉及到如何從模板中解析,而另外兩個實際上是函數。github

讓咱們先看一下 initData 的完整代碼:面試

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
複製代碼

看起來並不算短,不過咱們能夠先把開發模式下的一些友好警告給忽略掉,畢竟對咱們分析源碼來講這些警告不是很重要,其中有三段警告,讓咱們分別看看:算法

if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
複製代碼

上面這段的意思是,若是發現 data 居然不是一個平凡對象,那麼就打印一段警告,告訴你必須應該返回一個對象。後端

if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
複製代碼

大的循環體都是在循環 data 上的 key,上面這一段是說,若是發現 methods 中有和 data 上定義重複的key,那麼就打印一個警告。

if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    }
複製代碼

上面這一段是說,若是發現 props 中發現了重複的 key,那麼也會打印一段警告。固然上述兩種警告都只有在開發模式下才有的。弄懂了這兩段警告的意思,讓咱們把它刪了,而後在看看代碼變成這樣了:

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
  // proxy data on instance
  const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
複製代碼

是否是簡單了不少,咱們把上面這段代碼拆成三段來分別看看。其中最上面的一段代碼是:

let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
  }
複製代碼

首先把 vm.$options.data 取個別名,省得後面這樣寫太長了,而後判斷了它的類型,若是是函數,就經過 getData 獲取函數的返回值。而後還有一個操做就是把 data 放到了 this._data 上,至於爲何這麼作,下一段代碼咱們就會明白。

這裏你們會有另外一個疑問了,爲何不是直接調用函數得到返回值,而是須要一個 getData 呢,它除了調用函數確定還作了別的事,讓咱們看看 getData 的源碼:

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}
複製代碼

其實它確實是調用了函數,並得到了返回值,除了一段異常處理代碼外,他在調用咱們的 data 函數前進行了一個 pushTarget 操做,而在結束後調用了一個 popTarget 操做。咱們繼續來看這兩個函數,他們在 **core/observer/dep.js`中有定義,並且異常簡單。

Dep.target = null
const targetStack = []

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

export function popTarget () {
  Dep.target = targetStack.pop()
}
複製代碼

雖然看起來代碼很簡單,就是在一個全局的 Dep.target 中把本身記錄了一下,也就是在 data 函數調用前記錄了一下,而後調用後又恢復了以前的值。這裏暫時理解起來會比較困難,由於咱們要結合本文後面講到的內容才能理解。簡單的說,在 getData 的時候,咱們調用 pushTarget 卻沒有傳參數,目的是把 Dep.target 給清空,這樣不會在獲取 data 初始值的過程當中意外的把依賴記錄下來。

咱們再回到 initState 的第二段代碼:

const keys = Object.keys(data)
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
複製代碼

就是遍歷了 data 的key,而後作了一個 proxy,咱們來看 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);
}
複製代碼

這裏target是就是咱們的 vm 也就是咱們的組件自身,sourceKey 就是 _data,也就是咱們的 data,這段代碼會把對 vm 上的數據讀寫代理到 _data 上去。哈哈,咱們這樣就明白了一個問題,爲何咱們是經過 data.msg 定義的數據,卻能夠經過 this.msg 訪問呢?原來是這裏作了一個代理。

到目前爲止雖說了這麼多,可是作的事情很簡單,除了一些異常處理以外,咱們主要作了三件事:

  1. 經過 getData 把options中傳入的data取出來,這期間作了一些 依賴 的處理
  2. this._data = data
  3. 對於每個 data 上的key,都在 vm 上作一個代理,實際操做的是 this._data

這樣結束以後,其實vm會變成這樣:

observer1

弄懂了這個以後咱們再看最後一段代碼:

observe(data, true /* asRootData */)
複製代碼

observe 是如何工做的?咱們來看看他的代碼,這是響應式的核心代碼。

深刻 Observer

observer 的定義在 core/observer/index.js 中,咱們看看 代碼:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  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
}
複製代碼

其中有一些不少if的判斷,包括對類型的判斷,是否以前已經作過監聽等。咱們暫且拋開這些,把代碼精簡一下,就只剩下兩行了:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ob = new Observer(value)
  return ob
}
複製代碼

能夠看到主要邏輯就是建立了一個 Observer 實例,那麼咱們再看看 Observer 的代碼:

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

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

這個類包括構造函數在內,總共有三個函數。

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)
    }
  }
複製代碼

構造函數代碼如上,主要作了這麼幾件事:

this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
複製代碼

這裏記錄了 value, depvmCount, 和 __ob__四個值,其中值得注意的是這兩個:

  • this.dep 是 明顯是記錄依賴的,記錄的是對這個value 的依賴,咱們在下面立刻就能看到怎麼記錄和使用的
  • __ob__ 實際上是把本身記錄一下,避免重複建立
if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {
      this.walk(value)
    }
複製代碼

這一段代碼會判斷 value 的類型,進行遞歸的 observe,對數組來講,就是對其中每一項都進行遞歸 observe:

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}
複製代碼

顯然,直到碰到數組中非數組部分後,最終就會進入 walk 函數,在看 walk 函數以前,咱們先看看這一段代碼:

const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
複製代碼

這裏我不打算詳細講解每一行,若是你看源碼其實很容易看懂。這裏的做用就是把 數組上的原生方法進行了一次劫持,所以你調用好比 push 方法的時候,其實調用的是被 劫持 一個方法,而在這個方法內部,Vue會進行 notify 操做,所以就知道了你對數組的修改了。不過這個作法無法劫持直接經過下標對數組的修改。

好,讓咱們回到 walk 函數:

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

walk 函數會對每個 key 進行 defineReactive 操做,在這個函數內部其實就會調用 getter/setter 攔截讀寫操做,實現響應化。那麼這時候可能有人會有一個疑問了,若是某個 key 的值也是一個對象呢?難道不能進行深度的依賴麼?固然能夠的,不過對對象嵌套的遞歸操做不是在這裏進行的,而是在 defineReactive 中進行了遞歸。讓咱們看看 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
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  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) {
      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)
      dep.notify()
    }
  })
}
複製代碼

終於看到了傳說中的 getter/setter,上面是完整的代碼,有些長,按照慣例咱們分別進行講解。

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
  if (!getter && arguments.length === 2) {
    val = obj[key]
  }
  const setter = property && property.set
複製代碼

這段代碼中,第一步是建立了一個 dep 來收集對當前 obj.key 的依賴,這裏可能你們又會問:以前 new Observer 的時候不是已經建立了嗎,這裏怎麼又建立一次?這是一個深度依賴的問題,爲了回答這個問題咱們還得先往下看代碼。

dep 以後是獲取了getter/setter ,比較簡單,咱們再往下看:

let childOb = !shallow && observe(val)
複製代碼

這一段代碼很是重要,若是 val 是一個對象,那麼咱們要遞歸進行監聽。也就是又回到了 new Observer 中,能夠知道,childOb 返回的是一個 observer 實例。有了這個對孩子的監聽器以後,當孩子改變的時候咱們就能知道了。讓咱們繼續往下看最重要的一段代碼getter

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
    },
複製代碼

首先,咱們自定義的 getter 中,會把須要取出的值拿出來,經過原來的 getter。而後會判斷 Dep.target 存在就進行一個 dep.depend() 操做,而且若是有孩子,也會對孩子進行 dep.depend() 操做。

dep.depend() 的代碼以下:

depend () { 
  if (Dep.target) { 
    Dep.target.addDep(this) 
  } 
} 
複製代碼

也就是把當前這個 dep 加入到 target 中。

那麼這個 target 就很是重要了,他究竟是什麼呢?咱們在 getData 的時候設置過 Dep.target ,但當時咱們目的是清空,而不是設置一個值。因此這裏咱們依然不知道 target 是什麼。代碼看到當前位置實際上是確定沒法理解 target 的做用的,不要緊,咱們能夠帶着這個疑問繼續往下看。

可是這裏我簡單說明一下,這個target實際上是一個 watcher,咱們在獲取一個數據的時候,好比 this.msg 並是不直接去 this._data.msg 上取,而是先建立一個watcher,而後經過 watcher.value來取,而watcher.value === msg.getter 因此在取值的時候,咱們就知道 watcher 是依賴於當前的 dep 的,而 dep.depend() 至關於 watcher.deps.push(dep)

若是你面試的時候被問到 Vue的原理,那麼有一個常見的考點是問你 Vue 是怎麼收集依賴的,好比 computed 中有以下代碼:

info () {
  return this.name + ',' + this.age
}
複製代碼

Vue 是如何知道 info 依賴 nameage 呢?是由於在第一次獲取 info 的值的時候,會取 nameage 的值,所以就能夠在他們的 getter 中記錄依賴。固然因爲咱們如今尚未看 Watcher 的代碼,因此這一塊並不能理解的很透徹,不要緊,讓咱們暫且繼續往下看。這裏只要記住** Vue 在第一次取值的時候收集依賴 就好了**。

再看看 setter 函數,我刪除了部分不影響總體邏輯的代碼:

set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
複製代碼

拋開一些異常狀況的處理,主要代碼其實作了兩件事,第一件事是設置值,不過這裏的 setter 是什麼呢?實際上是咱們自定義的 setter,若是咱們有自定義,那麼就調用咱們的 setter,不然就直接設置。

而後若是發現咱們設置的新值是一個對象,那麼就遞歸監聽這個對象。

最後,經過 dep.notify 來通知響應的 target 們,我更新啦。

還記得上面咱們留了一個深度依賴的問題嗎?咱們舉個栗子說明,假設咱們的 data 是這樣的:

data: {
  people: {
    name: '123'
  }
}
複製代碼

咱們對 people 進行 defineReactive 的時候,咱們固然能夠處理 this.people={} 的操做。可是若是我進行了 this.people.name='xx' 的操做的時候要怎麼辦呢?顯然咱們此時是沒法檢測到這個更新的。因此咱們會建立對 {name:123} 再建立一個 childObj ,而後咱們的 target 也依賴於這個孩子,就能檢測到他的更新了。

到這裏咱們就講完 Observer 了,總結一下,Observer就是經過 getter/setter 監聽數據讀寫,在 getter 中記錄依賴, 在 setter 中通知哪些依賴們。讓咱們把以前的一張圖完善下,變成這樣:

observer 2

下一章 咱們看看 什麼是 Watcher

下一章:Vue2.x源碼解析系列五:數據響應之Watcher

相關文章
相關標籤/搜索