從源碼解析vue響應式原理

寫在前面

(距離上一次寫文章已通過去了四個月,羞愧...)這幾個月對vue的使用很多,可是自覺地始終停留在比較粗淺的層面,一直沒法提高,因此嘗試着開始閱讀源碼。 文中內容僅表明我的理解,若是錯誤,歡迎指正html

Vue中一個顯著特性是數據響應式系統:當數據被修改時,視圖會相應更新。從而方便的完成狀態管理。官方文檔中對此進行了簡要的描述,本文將結合vuejs的源碼,作出進一步的解析。vue

基本概念

首先簡單介紹一些在響應式系統中重要的概念。react

data

vue實例中的數據項git

observer

數據屬性的觀察者,監控對象的讀寫操做。github

dep

(dependence的縮寫),字面意思是「依賴」,扮演角色是消息訂閱器,擁有收集訂閱者、發佈更新的功能。express

watcher

消息訂閱者,能夠訂閱dep,以後接受dep發佈的更新並執行對應視圖或者表達式的更新。npm

dep和watcher

depwatcher的關係,能夠理解爲:dep是報紙,watcher是訂閱了報紙的人,若是他們創建了訂閱 的關係,那麼每當報紙有更新的時候,就會通知對應的訂閱者們。api

view

暫且認爲就是在瀏覽器中顯示的dom(關於virtual dom的內容暫時不在本文討論)數組

收集依賴

watcher在自身屬性中添加dep的行爲,後面會詳細介紹瀏覽器

收集訂閱者

dep在自身屬性中添加watcher的行爲,後面會詳細介紹

流程簡介

首先給出官方文檔的流程圖
圖片描述

在此基礎上,咱們根據源碼更細一步劃分出watcher和data之間的部分,即Depobserver
圖片描述

總的來講,vue的數據響應式實現主要分紅2個部分:

  1. 把數據轉化爲getter和setter
  2. 創建watcher並收集依賴

第一部分是上圖中dataobserverdep之間聯繫的創建過程,第二部分是watcherdep的關係創建

源碼解析

本文中採用的源碼是vuejs 2.5.0,Git地址

PS:簡單的代碼直接添加中文註釋,因此在關鍵代碼部分作<數字>標記,在後文詳細介紹

首先咱們在源碼中找到vue進行數據處理的方法initData

/* 源碼目錄 src/core/instance/state.js */
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)) {
      //<1>data屬性代理
      proxy(vm, `_data`, key)
    }
  }
  // observe data
   //對data調用observe
  observe(data, true /* asRootData */)
}

這一段代碼主要作2件事:

  • 代碼<1>while循環內調用proxy函數把data的屬性代理到vue實例上。完成以後能夠經過vm.key直接訪問data.key
  • 以後對data調用了observe方法,在這裏說明一下,若是是在實例化以前添加的數據,由於被observe過,因此會變成響應式數據,而在實例化以後使用vm.newKey = newVal這樣設置新屬性,是不會自動響應的。解決方法是:

    - 若是你知道你會在晚些時候須要一個屬性,可是一開始它爲空或不存在,那麼你僅須要設置一些初始值
    - 使用`vm.$data`等一些api進行數據操做

接下來來看對應代碼:

/* 源碼目錄 src/core/observer/index.js */
export function observe (value: any, asRootData: ?boolean): Observer | void {
 if (!isObject(value) || value instanceof VNode) {
   return
 }
 let ob: Observer | void
 //檢測當前數據是否被observe過,若是是則沒必要重複綁定
 if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
   ob = value.__ob__
 } else if (
   //<1>檢測當前的數據是不是對象或者數組,若是是,則生成對應的Observer
   observerState.shouldConvert &&
   !isServerRendering() &&
   (Array.isArray(value) || isPlainObject(value)) &&
   Object.isExtensible(value) &&
   !value._isVue
 ) {
   ob = new Observer(value)
 }
 if (asRootData && ob) {
   ob.vmCount++
 }
 return ob
}
  • 在本段代碼中,代碼<1>處,對傳入的數據對象進行了判斷,只對對象和數組類型生成Observer實例,而後看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
    // 生成了一個消息訂閱器dep實例 關於dep的結構稍後詳細介紹 
    this.dep = new Dep()
    this.vmCount = 0
    //def函數給當前數據添加不可枚舉的__ob__屬性,表示該數據已經被observe過
    def(value, '__ob__', this)
    //<1>對數組類型的數據 調用observeArray方法;對對象類型的數據,調用walk方法
    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.
   */
   /* 循環遍歷數據對象的每一個屬性,調用defineReactive方法 只對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. 
   */
   /* observe數組類型數據的每一個值, */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

/* defineReactive的核心思想改寫數據的getter和setter */
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //<2>生成一個dep實例,注意此處的dep和前文Observer類裏直接添加的dep的區別
  const dep = new Dep()
    
  //檢驗該屬性是否容許從新定義setter和getter
  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 獲取原有的 getter/setters
  const getter = property && property.get
  const setter = property && property.set
  
  //<3>此處對val進行了observe
  let childOb = !shallow && observe(val)
  
  //<4>下面的代碼利用Object.defineProperty函數把數據轉化成getter和setter,而且在getter和setter時,進行了一些操做
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // dep.depend()其實就是dep和watcher進行了互相綁定,而Dep.target表示須要綁定的那個watcher,任什麼時候刻都最多隻有一個,後面還會解釋
        dep.depend()
        if (childOb) {
          //<5>當前對象的子對象的依賴也要被收集
          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
      }
      //<6>觀察新的val並通知訂閱者們屬性有更新
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
  • 在Observer類代碼中,首先給當前數據添加了一個dep實例,存放於對象或者數組類型數據的_![圖片描述][2]ob_屬性上,而後把_ob_掛在該數據上,它是該數據項被observe的標誌,咱們能夠在控制檯看到這個屬性,,例如:
//例子 1
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <title>vue demo</title>
</head>

<body>
  <script src="https://cdn.jsdelivr.net/npm/vue"></script>
  <div id="app">
    <div>obj:{{ obj}}</div>
  </div>
</body>
<script>
  app = new Vue({
    el: '#app',
    data: {
      str: "a",
      obj: {
        key: "val"
      }
    }
  });
  console.log(app._data)
</script>

</html>

在控制檯咱們能夠看到這樣的數據
圖片描述

能夠看到,首先,data對象上已經有_ob_屬性,這是被observe的標誌;其次,objarr屬性上有_ob_屬性,而str沒有,這個進一步證實了前文提到的:observe只對對象和數組有效

  • 隨後,對於數組和對象類型的數據作不一樣處理:對於數組類型observe裏面的每一個值,對於對象,咱們執行walk()方法,而walk()就是對於當前數據對象的每一個key,執行defineReactive()方法,因此接下來重點來看defineReactive()
  • defineReactive()中,在代碼<2>處生成了一個dep實例,並在接下來的代碼裏,把這個dep對象放在當前數據對象的key(好比上面例子1中的str)的getter裏,這個以前Observer中的dep是有區別的:

    • Observer中的dep掛在Object或者Array類型的數據的dep屬性上,能夠在控制檯直接查看;
    • 此處添加的dep掛在屬性的getter/setter上,存在於函數閉包中,不可直接查看
爲何會有2種`Dep`呢?由於對於`Object`或者`Array`類型的數據,可能會有**添加

或者刪除成員的操做而其餘類型的值只有賦值操做,賦值操做能夠在getter/setter上中檢測到。**,

  • 接下來代碼<3>處的是爲了處理嵌套的數據對象,好比例子1中,data是最頂層的Object,obj就是data下的Object,而obj裏面也能夠再繼續嵌套數據,有了此處的代碼以後,就能夠對嵌套的每一層都作observe處理。
  • 代碼<4>處是defineReactive()的核心:利用Object.defineProperty()(這個函數建議瞭解一下mdn地址)

在當前屬性的getter和setter中插入操做:

  • 在當前數據被get時,當前的watcher(也就是Dap.target)和dep之間的綁定,這裏有個注意點是在代碼<5>處,若是當前數據對象存在子對象,那麼子對象的dep也要和當前watcher進行綁定,以此類推。
  • 在setter時,咱們從新觀測當前val,而後經過dep.notify()來通知當前dep所綁定的訂閱者們數據有更新。

Dep

接下來介紹一下dep。源碼以下:

/* 源碼目錄 src/core/observer/dep.js */
let uid = 0
/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  //添加一個watcher
  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //移除一個watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //讓當前watcher收集依賴 同時Dep.target.addDep也會觸發當前dep收集watcher
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }
 //通知watcher們對應的數據有更新
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

這個類相對簡單不少,只有2個屬性:第一個是id,在每一個vue實例中都從0開始計數;另外一個是subs數組,用於存放wacther,根絕前文咱們知道,一個數據對應一個Dep,因此subs裏存放的也就是依賴該數據須要綁定的wacther

這裏有個Dep.target屬性是全局共享的,表示當前在收集依賴的那個Watcher,在每一個時刻最多隻會有一個

watcher

接下里看watcher的源碼,比較長,可是咱們只關注其中的幾個屬性和方法:

/* 源碼目錄 src/core/observer/watcher.js */
/**
 * 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.
 */
 /* watcher用來解析表達式,收集依賴,而且當表達式的值改變時觸發回調函數 
 用在$watch() api 和指令中
 */
export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: ISet;
  newDepIds: ISet;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    // options
    //這裏暫時不用關注 
    if (options) {
      this.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
    //deps和newDeps表示現有的依賴和新一輪收集的依賴
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    //<1>解析getter的表達式 
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      //<2>獲取實際對象的值
      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.lazy爲true是計算屬性的watcher,另外處理,其餘狀況調用get
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get () {
    pushTarget(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()
      //<3>清除先前的依賴
      this.cleanupDeps()
    }
    return value
  }

  /**
   * Add a dependency to this directive.
   */
   /* 給當前指令添加依賴 */
  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)
      }
    }
  }

  /**
   * 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()
    tmp = this.deps
    this.deps = this.newDeps
    this.newDeps = tmp
    this.newDeps.length = 0
  }

  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
   /* 訂閱者的接口 當依賴改變時會觸發 */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
   /* 調度接口 調度時會觸發 */
  run () {
    if (this.active) {
      //<14>從新收集依賴
      const value = this.get()
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const oldValue = this.value
        this.value = value
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue)
          } catch (e) {
            handleError(e, this.vm, `callback for watcher "${this.expression}"`)
          }
        } else {
          this.cb.call(this.vm, value, oldValue)
        }
      }
    }
  }

  /**
   * Evaluate the value of the watcher.
   * This only gets called for lazy watchers.
   */
  evaluate () {
    this.value = this.get()
    this.dirty = false
  }

  /**
   * Depend on all deps collected by this watcher.
   */
  depend () {
    let i = this.deps.length
    while (i--) {
      this.deps[i].depend()
    }
  }

  /**
   * Remove self from all dependencies' subscriber list.
   */
  teardown () {
    if (this.active) {
      // remove self from vm's watcher list
      // this is a somewhat expensive operation so we skip it
      // if the vm is being destroyed.
      if (!this.vm._isBeingDestroyed) {
        remove(this.vm._watchers, this)
      }
      let i = this.deps.length
      while (i--) {
        this.deps[i].removeSub(this)
      }
      this.active = false
    }
  }

首先看官方文檔的英文註釋可知,watcher用於watcher用來解析表達式,收集依賴,而且當表達式的值改變時觸發回調函數,用在$watch()api 和指令之中。

watcher函數主要內容是:

  • 初始化屬性的值,其中和本文相關的主要是depsnewDepsdepIdsnewDepIds,分別表示現有依賴和新一輪收集的依賴,這裏的依賴就是前文介紹的數據對應的dep
  • 設置getter屬性。<1>判斷傳入的表達式類型:多是函數,也多是表達式。若是是函數,那麼直接設置成getter,若是是表達式,因爲代碼<2>處的expOrFn只是字符串,好比例子1中的obj.key,在這裏僅僅是一個字符串,因此要用parsePath獲取到實際的值
  • 執行get()方法,在這裏主要作收集依賴,而且獲取數據的值,以後要調用代碼<3>`cleanupDeps`清除舊的依賴。這是必需要作的,由於數據更新以後可能有新的數據屬性添加進來,前一輪的依賴中沒有包含這個新數據,因此要從新收集。
  • update方法主要內容是裏面的觸發更新以後會觸發run方法(雖然這裏分了三種狀況,可是最終都是觸發run方法),而run方法調用get()首先從新收集依賴,而後使用this.cb.call更新模板或者表達式的值。

總結

在最後,咱們再總結一下這個流程:首先數據從初始化data開始,使用observe監控數據:給每一個數據屬性添加dep,而且在它的getter過程添加收集依賴操做,在setter過程添加通知更新的操做;在解析指令或者給vue實例設置watch選項或者調用$watch時,生成對應的watcher並收集依賴。以後,若是數據觸發更新,會通知watcherwacther在從新收集依賴以後,觸發模板視圖更新。這就完成了數據響應式的流程。

本文的流程圖根據源碼的過程畫出,而在官方文檔的流程圖中,沒有單獨列出depobvserver,由於這個流程最核心的思路就是將data的屬性轉化成gettersetter而後和watcher綁定

而後依然是慣例:若是這篇文章對你有幫助,但願能夠收藏和推薦,以上內容屬於我的看法,若是有不一樣意見,歡迎指出和探討。請尊重做者的版權,轉載請註明出處,如做商用,請與做者聯繫,感謝!

相關文章
相關標籤/搜索