刺破 Vue 的心臟之——響應式源碼分析

以前發刺破 vue 的心臟之——詳解 render function code的時候,承諾過會對 Vue 的核心過程的各個部分經過源碼解析的方式進行抽絲剝繭的探索,今天就來進入第二部分響應式原理部分的源碼解析,承諾兌現得有些晚,求輕拍html

1、先分析工做原理

仍是以前的套路,在讀源碼以前,先分析原理
前端


上圖來自 Vue 官網 深刻響應式原理,建議先看看,這裏主要說說個人理解:在初始化的時候,首先經過 Object.defineProperty 改寫 getter/setter 爲 Data 注入觀察者能力,在數據被調用的時候,getter 函數觸發,調用方(會爲調用方建立一個 Watcher)將會被加入到數據的訂閱者序列,當數據被改寫的時候,setter 函數觸發,變動將會通知到訂閱者(Watcher)序列中,並由 Watcher 觸發 re-render,後續的事情就是經過 render function code 生成虛擬 dom,進行 diff 比對,將不一樣反應到真實的 dom 中

2、源碼分析

記住一個實例

讀源碼是一件枯燥的事情,帶着問題去找答案,要更容易讀得進去vue

<template>
...
</template>
<script>
  export default {
    data () {
      return {
        name: 'hello'
      }
    },
    computed: {
      cname: function () {
        return this.name + 'world'
      }
    }
  }
</script>
<style>
...
</style>複製代碼

爲了減小干擾,例子已經剝離得只剩下兩個關鍵的要素,數據屬性 name,以及調用了該屬性的計算屬性 cname,這其中 cname 跟 name 就是訂閱者跟被訂閱者的關係。咱們如今須要帶着這樣的疑問去閱讀源碼,cname 是如何成爲 name 的訂閱者的,而且當 name 發生了變動的時候,如何通知到 cname 更新本身的數據react

初始化數據,注入觀察者能力

響應式處理的源碼在 src/core/observer 目錄下,見名之意,這使用了觀察者模式,先不用着急進入這個目錄,在 Vue 實例初始化的時候,會執行到 src/core/instance/state.js 中相關的狀態初始化邏輯,先到這個文件來看看:數組

export function initState (vm: Component) {
  ...
  if (opts.data) {
    // 初始化數據
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 初始化計算屬性
  if (opts.computed) initComputed(vm, opts.computed)
  ...
}複製代碼

咱們所關注的初始化數據和初始化計算屬性在這裏都會被執行到,先來分析下 initData, 沿着方法跟下去,發現最終 initData 要作的事情是:bash

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

調用 observe 方法爲 data 注入觀察者能力,這個時候咱們能夠正式進入 observer/index.js 文件了,在這個文件咱們能夠找到 observe 方法的定義,跟着方法讀下去,找到下一步的關鍵信號:dom

ob = new Observer(value)複製代碼

這一步經過傳入的 data,建立一個 Observer 實例,再跟到 Observer 的構造函數中會發現,構造函數會爲 data 的各個元素(當 data 爲數組的時候)或者各個屬性(當 data 爲對象的時候)遞歸的建立 Observer 對象,最終起做用的方法是 defineReactiveide

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: Function
) {
  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

  let childOb = observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 當前的 Watcher
      if (Dep.target) {
        // 將當前的 watcher 加入到該數據的訂閱者序列
        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 = observe(newVal)
      // 當數據發生變動的時候,通知訂閱方進行數據變動
      dep.notify()
    }
  })
}複製代碼

代碼貼得有點多,但着實不是爲了湊字數,由於在這裏部分省略可能會帶來一些疑惑,就沒有進行縮減,見諒。這裏主要是經過 Object.defineProperty 方法,重寫數據的 set 和 get 方法,當數據被調用時,set 方法會將當前的 Watcher Dep.target 也就是當前的調用方加入到該數據的訂閱者序列中,當數據變動,set 方法發通知到全部訂閱者,讓你們從新計算。這其中定義在 observer/dep.js 文件中的 Dep 定義了數據訂閱者的訂閱、取消訂閱等行爲,在這裏就不貼代碼了。函數

回憶一下咱們實例中的 name 和計算屬性 cname,當 cname 的方法執行的時候,name 被調用,就會觸發它的 get 方法,這個時候 cname 所對應的 watcher (computed 初始化的時候會爲每一個計算屬性建立一個 watcher)。當 name 發生了變動,set 方法被觸發,cname 所對應的 watcher 做爲訂閱者就會被通知到,從而從新計算 cname 的值oop

初始化計算屬性,建立 Watcher

回到 src/core/instance/state.js 文件的計算屬性初始化邏輯 initComputed,這個方法不負衆望的爲計算屬性建立了 Watcher 對象

// create internal watcher for the computed property.
  watchers[key] = new Watcher(vm, getter, noop, computedWatcherOptions)複製代碼

因而乎咱們的視線須要轉移到 observer/watcher.js 中,Watcher 的構造函數中最爲關鍵的是,this.get 方法的調用

this.value = this.lazy
      ? undefined
      : this.get()複製代碼

在 this.get 方法中有兩步尤其關鍵(對於計算屬性來講,會進行延遲計算,這就是 this.lazy 標誌的意義所在):

get () {
    // 將當前的調用者 watcher 置爲 Dep.target
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 調用方法,計算依賴方的值
      value = this.getter.call(vm, vm)
    } catch (e) {
      ...
    } finally {
      ...
    }
    return value
  }複製代碼
  1. pushTarget(this) 將 watcher 置爲 Dep.target,當所依賴的數據的 get 方法被調用的時候,就能夠根據 Dep.target 把當前的 watcher 加入到訂閱者序列中。這麼作的目的是,當 watcher 依賴於多個數據的時候,能夠共享 Dep.target
  2. 執行 this.getter.call(vm, vm) 方法計算值,例如計算屬性 cname 的 getter 就是它的定義函數function(){this.name + 'world'}。此時依賴方的 get 方法被觸發,整個流程就能串起來,說得通了

對於計算屬性,還有一個細節,須要將視線再轉移到 initComputed 中:

export function defineComputed (target: any, key: string, userDef: Object | Function) {
  ...
  Object.defineProperty(target, key, sharedPropertyDefinition)
}複製代碼

它所調用的 defineComputed 方法會爲計算屬性在當前的組件實例中建立一個同名的屬性,這也就是爲什麼計算屬性的定義上是方法,可是在實際的使用當中倒是屬性的緣由。只有在它所依賴的數據更新的時候,數據經過 set 方法通知到它,它纔會從新計算並把值賦給這個新建的代理屬性。計算屬性高效就高效在這裏

3、總結

寫源碼分析難以覆蓋到方方面面,畢竟不能一直貼代碼,如何在貼最少許代碼的狀況下把問題說清楚,這仍然仍是努力的方向。在達到這個目標以前,只能經過提問的方式了,有問題歡迎評論,盡力解答

此文還在公衆號菲麥前端中發佈:

相關文章
相關標籤/搜索