一萬字助你不折不扣弄清vue的雙向綁定和鏈路追蹤

前言:

從vue問世到如今,全部的vue開發者應該對vue的底層原理都模模糊糊的有一些瞭解,Object.defineProperty getter/setter,發佈訂閱之類的概念都能說出一二來,可問題是大部分的coder就真的只是模模糊糊的瞭解一二,練習vue兩年半,到頭來仍是隻能說出個雙向綁定來。vue

衆所周知vue是一個很是好用的mvvm框架,咱們所關注的雙向綁定發生在viewviewModel的交互過程當中,支持這個過程就是vue自己的數據劫持模式,粗略的描述就是vue經過Object.defineProperty方法重寫了一個普通js對象的get,set方法,使其在被訪問時收集依賴,在被設置時執行回調。同時vue將遍歷全部的dom節點,解析指令,在這個過程當中因爲掛載在this上的屬性已是響應式數據(reactive)這些變量在Compile的過程當中被訪問時便會新生成一個watcher實例放入這個屬性特有的dep實例中(閉包),當該屬性被set的時候便會遍歷(notify)這個屬性的dep實例,執行裏面每一個watcher實例中保存的回調方法。react

幾乎每一個vue coder對這個過程都是滾瓜爛熟,可在實際開發中發現並無什麼卵用,由於這只是一個很粗糲的描述和理解,裏面還有不少的細節和彼此間關聯是大部分coder都影影綽綽模模糊糊的,只有對這些細節大部分都有所瞭解了,才能在實際開發中對項目有總體的把控感。git

watcher

之因此從watcher對象開始,是由於在以前的概述中能夠發現watcher對象實際上是鏈接Observer和Compile的關鍵,從watch,computed屬性,再到vuex能夠說vue的一整套數據監聽和鏈路追蹤系統都和watcher相關,github

主要源碼端解析vuex

構造部分 主要構造參數都經過註釋有所說明,須要注意的是this.getter的值能夠看到:express

this.getter = expOrFn
複製代碼

對於初始化用來渲染視圖的watcher來講,expOrFn就是render方法,對於computed來講就是表達式,對於watch纔是key,因此這邊須要判斷是字符串仍是函數,若是是函數則返回函數,若是是字符串則返回parsePath(expOrFn)方法,因此無論傳入的expOrFn是什麼最終返回的都是一個方法,這樣子作的目的是爲了方便watcher內的get()方法調用緩存

構造部分源碼閉包

constructor (
    vm: Component, // 組件實例對象
    expOrFn: string | Function, // 要觀察的表達式
    cb: Function, // 當觀察的表達式值變化時候執行的回調
    options?: ?Object, // 給當前觀察者對象的選項
    isRenderWatcher?: boolean // 標識該觀察者實例是不是渲染函數的觀察者
  ) {
    // 每個觀察者實例對象都有一個 vm 實例屬性,該屬性指明瞭這個觀察者是屬於哪個組件的
    this.vm = vm
    if (isRenderWatcher) {
      // 只有在 mountComponent 函數中建立渲染函數觀察者時這個參數爲真
      // 組件實例的 _watcher 屬性的值引用着該組件的渲染函數觀察者
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    // deep: 當前觀察者實例對象是不是深度觀測
    // 平時在使用 Vue 的 watch 選項或者 vm.$watch 函數去觀測某個數據時,
    // 能夠經過設置 deep 選項的值爲 true 來深度觀測該數據。
    // user: 用來標識當前觀察者實例對象是 開發者定義的 仍是 內部定義的
    // 不管是 Vue 的 watch 選項仍是 vm.$watch 函數,他們的實現都是經過實例化 Watcher 類完成的
    // sync: 告訴觀察者當數據變化時是否同步求值並執行回調
    // before: 能夠理解爲 Watcher 實例的鉤子,當數據變化以後,觸發更新以前,
    // 調用在建立渲染函數的觀察者實例對象時傳遞的 before 選項。
     if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    // cb: 回調
    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()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // 檢測了 expOrFn 的類型
    // this.getter 函數終將會是一個函數
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        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()
  }

    .....

複製代碼

主要方法app

get():框架

主要是經過訪問屬性來達到收集依賴的目的,在咱們進行依賴收集時,data已經完成響應式數據的過程。咱們知道在屬性被訪問時觸發該屬性的get方法將全局變量Dep.target放入該屬性獨有的deps對象中,因此不論是在哪一種場景下進行依賴收集時都會新建一個watcher實例。其中pushTarget(this) 將該實例賦值到全局變量Dep.target this.getter.call(vm, vm) 用來訪問該屬性觸發該屬性的get方法將該watcher實例收集進其deps

/** * 求值: 收集依賴 * 求值的目的有兩個 * 第一個是可以觸發訪問器屬性的 get 攔截器函數 * 第二個是可以得到被觀察目標的值 */
  get () {
    // 推送當前Watcher實例到Dep.target
    pushTarget(this)
    let value
    // 緩存vm
    const vm = this.vm
    try {
      // 獲取value,this.getter被處理成了方法
      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()
    }
    return value
  }

複製代碼

addDep:

將自身添加進屬性所屬的deps實例,newDepIds的存在是爲了不重複收集依賴

/** * 記錄本身都訂閱過哪些Dep */
  addDep (dep: Dep) {
    const id = dep.id
    // newDepIds: 避免在一次求值的過程當中收集重複的依賴
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id) // 記錄當前watch訂閱這個dep
      this.newDeps.push(dep) // 記錄本身訂閱了哪些dep
      if (!this.depIds.has(id)) {
        // 把本身訂閱到dep
        dep.addSub(this)
      }
    }
  }

複製代碼

updata()

調用run方法,run方法執行回調函數完成數據更新,this.cb即爲watcher實例生成時傳入的回調函數

/** * 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) {
      const value = this.get()
      // 對比新值 value 和舊值 this.value 是否相等
      // 是對象的話即便值不變(引用不變)也須要執行回調
      // 深度觀測也要執行
      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) {
          // 意味着這個觀察者是開發者定義的,所謂開發者定義的是指那些經過 watch 選項或 $watch 函數定義的觀察者
          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)
        }
      }
    }
  }

複製代碼

watcher所扮演的角色

大體瞭解了watcher實例後,再分析watcher在不一樣場景所扮演的角色,

頁面數據更新

也就是所謂的雙向綁定,在vue遍歷dom節點時{{msg}}內的數據被訪問時生成watcher將其收集進msg的dep中當msg變動時觸發回調函數更新視圖,這一塊也是被說臭了的一塊,就不細說,網上一大把

$watch屬性

官網:watch 選項提供了一個更通用的方法,來響應數據的變化

源碼位置: vue/src/core/instance/state.js

根據咱們以前的分析要響應數據的變化那麼就須要將變化(回調函數) 這個動做看成 依賴(watcher實例)響應數據(data屬性)所收集進他的deps

經過源碼能夠看到initWatch方法遍歷option內的watch屬性,而後依次調用createWatcher方法而後經過調用

vm.$watch(expOrFn, handler, options)
複製代碼

來生成watcher實例。

例: 以一個watch屬性的一般用法爲例

data: {
    question: '',
  },
  watch: {
    // 若是 `question` 發生改變,這個函數就會運行
    question: function (newQuestion, oldQuestion) {
      回調方法 balabalabala...
    }
  },
複製代碼

原理解析

在以前的watcher實例所需的構造參數瞭解到watcher所需的必要參數,此時生成watcher實例的expOrFn爲watch屬性的key也就是例子中的question,cb爲watch屬性的handler也就是後面的回調方法,在watcher的get方法中咱們得知expOrFn將會做爲getter被訪問,

......
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      }
      .....
     value = this.getter.call(vm, vm)
     .....
複製代碼

又由於key爲data的屬性做爲響應式數據在被訪問時觸發其get方法將收集這個**依賴(當前watcher實例)**也就是Dep.target也就是當前的watcher(watcher在get方法時會將Dep.target設置成當前屬性),當該屬性被修改時調用dep的notify方法再調用每一個watcher的updata方法執行回調就可達到watch屬性的目的,因此watch屬性能夠被總結爲依賴更新觸發回調

源碼註釋

// 遍歷vue實例中的watch屬性
function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
function createWatcher ( vm: Component, expOrFn: string | Function, handler: any, options?: Object ) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}
// $watch 方法容許咱們觀察數據對象的某個屬性,當屬性變化時執行回調
// 接受三個參數: expOrFn(要觀測的屬性), cb, options(可選的配置對象)
// cb便可以是一個回調函數, 也能夠是一個純對象(這個對象要包含handle屬性。)
// options: {deep, immediate}, deep指的是深度觀測, immediate當即執行回掉
// $watch()本質仍是建立一個Watcher實例對象。

Vue.prototype.$watch = function ( expOrFn: string | Function, cb: any, options?: Object ): Function {
    // vm指向當前Vue實例對象
    const vm: Component = this
    if (isPlainObject(cb)) {
      // 若是cb是一個純對象
      return createWatcher(vm, expOrFn, cb, options)
    }
    // 獲取options
    options = options || {}
    // 設置user: true, 標示這個是由用戶本身建立的。
    options.user = true
    // 建立一個Watcher實例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      // 若是immediate爲真, 立刻執行一次回調。
      try {
        // 此時只有新值, 沒有舊值, 在上面截圖能夠看到undefined。
        // 至於這個新值爲何經過watcher.value, 看下面我貼的代碼
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回一個函數,這個函數的執行會解除當前觀察者對屬性的觀察
    return function unwatchFn () {
      // 執行teardown()
      watcher.teardown()
    }
  }


複製代碼

computed屬性

官網:你能夠像綁定普通屬性同樣在模板中綁定計算屬性。Vue 知道 vm.reversedMessage 依賴於 vm.message,所以當 vm.message 發生改變時,全部依賴 vm.reversedMessage 的綁定也會更新。並且最妙的是咱們已經以聲明的方式建立了這種依賴關係:計算屬性的 getter 函數是沒有反作用 (side effect) 的,這使它更易於測試和理解。

就像watch屬性能夠理解爲依賴變動觸發回調,computed能夠理解爲依賴變動求值,並且將該屬性掛載在當前實例上能夠經過this訪問並返回最新值。

原理解析:

依賴收集 以下源碼所示,在方法initComputed中將遍歷vue實例的computed屬性,而後爲computed內的每一個屬性都實例化一個watcher對象

....
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
      ....
複製代碼

此時computed屬性的表達式將做爲watcher實例的expFcn,以前說過expFcn將會做爲getter被訪問,

......
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      }
      .....
     value = this.getter.call(vm, vm)
     .....
複製代碼

能夠看到此時的expFcn是一個方法,當這個方法被執行時方法內若是有data屬性(響應式數據)則這些數據的get方法將會被依次觸發,此時全局屬性Dep.target仍是當前的watcher實例也就是說當前的watcher將做爲這些data屬性的依賴之一被保存在被閉包的deps屬性中

觸發更新 當computed屬性所依賴的data屬性被修改時執行dep.notify方法依次遍歷deps內的watcher實例執行各自的uptate方法, 最終在run方法內調用get方法將watcher的value屬性更新爲最新值

run () {
    if (this.active) {
      const value = this.get()
      ...
        this.value = value

   .....
    }
  }
複製代碼

又由於data屬性被修改,觸發vue的render()方法,更新視圖,訪問computed屬性,而在defineComputed方法內咱們能夠看到在被重寫了get和set方法的computed屬性都被掛載到了當前vue實例上,而此時computed屬性被訪問觸發了被重寫了的get方法,從而調用createComputedGetter方法最終返回已是最新值的watcher.value

部分相關源碼

sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)


  function createComputedGetter (key) {
    return function computedGetter () {
 ....
      return watcher.value
      .....
    }
複製代碼
function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
  // computed properties are just getters during SSR
  const isSSR = isServerRendering()
  // 遍歷vue實例中的computed屬性,此時每一個computed屬性的表達式將做爲watcher實例的expFcn
  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (process.env.NODE_ENV !== 'production' && getter == null) {
      warn(
        `Getter is missing for computed property "${key}".`,
        vm
      )
    }
    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    // 若是computed屬性不屬於vue實例,則d
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } else if (process.env.NODE_ENV !== 'production') {
      if (key in vm.$data) {
        warn(`The computed property "${key}" is already defined in data.`, vm)
      } else if (vm.$options.props && key in vm.$options.props) {
        warn(`The computed property "${key}" is already defined as a prop.`, vm)
      }
    }
  }
}
//
export function defineComputed ( target: any,// 當前vue實例 key: string,// computed屬性 userDef: Object | Function// computed表達式,如果對象則爲對象內的get方法 ) {
// 重寫get set 方法
  const shouldCache = !isServerRendering()
  if (typeof userDef === 'function') {
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef)
    sharedPropertyDefinition.set = noop
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop
    sharedPropertyDefinition.set = userDef.set || noop
  }
  if (process.env.NODE_ENV !== 'production' &&
      sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        `Computed property "${key}" was assigned to but it has no setter.`,
        this
      )
    }
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}
// 生成屬性的get方法,當computed屬性被訪問時經過watcher實例返回最新的數據
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}
複製代碼

vuex

官網:Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。Vuex 也集成到 Vue 的官方調試工具 devtools extension,提供了諸如零配置的 time-travel 調試、狀態快照導入導出等高級調試功能。

這裏不介紹vuex的各類概念官網上說了

Vuex 背後的基本思想,借鑑了 Flux、Redux 和 The Elm Architecture

  • state: 單一狀態樹
  • getter: 從state派生出的一些狀態,(本質就是computed, 無side effects的純函數)
  • action: 提交 mutation,(而不是直接change state)。
  • mutation: change state

這些概念的詳細說明,出門左拐,這裏只是基於vue數據鏈路追蹤的角度來詳細解釋vuex的相關源碼實現

先回憶一下vuex在vue項目中的使用,咱們常常是在main.js中引入配置好的vuex對象而後將其看成vue的構造參數之一傳入vue的構造方法,而後全部的vue實例都將擁有一個$store屬性指向同一狀態空間也就是咱們說的單一狀態樹

如:

new Vue({
    el: '#app',
    router,
    store,
    components: { App },
    template: '<App/>'
})
複製代碼

這個操做的支持是由於在vue實例的全局beforeCreate鉤子中混入vuex對象

Vue.mixin({ beforeCreate: vuexInit })
複製代碼
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
  // 全局混入store屬性
    Vue.mixin({ beforeCreate: vuexInit })
  } else {
    const _init = Vue.prototype._init
    Vue.prototype._init = function (options = {}) {
      options.init = options.init
        ? [vuexInit].concat(options.init)
        : vuexInit
      _init.call(this, options)
    }
  }

  function vuexInit () {
    const options = this.$options
    // 若是當前實例有傳入store對象則取當前若無則像上取,確保每一個實例都有store屬性
    if (options.store) {
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      this.$store = options.parent.$store
    }
  }
}
複製代碼

從上述源碼可知項目中的全部組件實例都擁有指向同一狀態空間的能力,那麼若是隻是作全局屬性那麼一個普通的js對象便可,用vuex的目的是由於他能夠完美的和vue自己的數據鏈路追蹤無縫切合,如同擁有了一個全局的data屬性,不管是收集依賴觸發頁面更新,監聽數據變化等data屬性能作的他都能作,那和data如此相像的就只有data自己了,因此vuex的store自己其實就是一個vue實例,在不考慮其餘因素的前提下,只作全局響應式 數據共享,那麼在vue的原型鏈上掛載一個vue實例同樣能夠作到相同的效果,使用過vuex的coder知道咱們每每須要從 store 中的 state 中派生出一些狀態,而不是簡單的直接訪問state

以官網實例來講

const store = new Vuex.Store({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos: state => {
      return state.todos.filter(todo => todo.done)
    }
  }
})
複製代碼

當咱們訪問this.$store.doneTodos時將會返回最新的todos中done爲true的數據,同理當todos發生變化時(set被觸發)doneTodos也會隨着更新依賴,這麼看是否是和咱們以前看的computed如出一轍,能夠從如下的源碼看到resetStoreVM方法中將遍歷每一個getter並生成computed做爲參數傳入到vue的構造方法中,由此能夠這樣理解當store中的state內數據被修改時至關於vue實例的data屬性被修改觸發其set方法更新依賴,當getter內的數據被訪問時相對於computed屬性被訪問返回最新的依賴數據

部分源碼

/** * forEach for object * 遍歷對象 */
  function forEachValue (obj, fn) {
    Object.keys(obj).forEach(function (key) { return fn(obj[key], key); });
  }
  // 遍歷vuex 內每一個module內的getter屬性
   module.forEachGetter(function (getter, key) {
      var namespacedType = namespace + key;
      registerGetter(store, namespacedType, getter, local);
    });

 function registerGetter (store, type, rawGetter, local) {
    if (store._wrappedGetters[type]) {
      {
        console.error(("[vuex] duplicate getter key: " + type));
      }
      return
    }
    store._wrappedGetters[type] = function wrappedGetter (store) {
      return rawGetter(
        local.state, // local state
        local.getters, // local getters
        store.state, // root state
        store.getters // root getters
      )
    };
  }

/* 經過vm重設store,新建Vue對象使用Vue內部的響應式實現註冊state以及computed */
function resetStoreVM (store, state, hot) {
  /* 存放以前的vm對象 */
  const oldVm = store._vm 

  // bind store public getters
  store.getters = {}
  const wrappedGetters = store._wrappedGetters
  const computed = {}

  /* 經過Object.defineProperty爲每個getter方法設置get方法,好比獲取this.$store.getters.test的時候獲取的是store._vm.test,也就是Vue對象的computed屬性 */
  forEachValue(wrappedGetters, (fn, key) => {
    // use computed to leverage its lazy-caching mechanism
    computed[key] = () => fn(store)
    Object.defineProperty(store.getters, key, {
      get: () => store._vm[key],
      enumerable: true // for local getters
    })
  })

  // use a Vue instance to store the state tree
  // suppress warnings just in case the user has added
  // some funky global mixins
  const silent = Vue.config.silent
  /* Vue.config.silent暫時設置爲true的目的是在new一個Vue實例的過程當中不會報出一切警告 */
  Vue.config.silent = true
  /* 這裏new了一個Vue對象,運用Vue內部的響應式實現註冊state以及computed*/
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
  Vue.config.silent = silent

  // enable strict mode for new vm
  /* 使能嚴格模式,保證修改store只能經過mutation */
  if (store.strict) {
    enableStrictMode(store)
  }

  if (oldVm) {
    /* 解除舊vm的state的引用,以及銷燬舊的Vue對象 */
    if (hot) {
      // dispatch changes in all subscribed watchers
      // to force getter re-evaluation for hot reloading.
      store._withCommit(() => {
        oldVm._data.$$state = null
      })
    }
    Vue.nextTick(() => oldVm.$destroy())
  }
}
複製代碼

咱們再經過幾個經常使用的vuex用法來鞏固理解

例1:頁面視圖更新 以下代碼所示當組件B觸發點擊事件更新 $store.count 時,組件A視圖將會隨之更新,由於在組件A compilecount做爲響應式數據將count屬性的get方法將收集用於更新頁面的watcher實例做爲依賴,當組件B修改count時觸發set方法更新依賴

component A

<div id="app">
    {{ $store.count }}
</div>
<script>
export default {

}

component B

<div id="app">
  <div @click="$store.count+=1"></div>
</div>
<script>
export default {
   methods:{
   }
}
</script>
複製代碼

實例2:監聽store數據變化 在業務開發過程當中咱們常常會遇到須要監聽store數據變化的狀況,咱們通常的作法是在這個vue實例中將須要觀測的state做爲computed屬性return出來,讓其掛載到當前this實例中,而後經過watch屬性觀測其變化

在下面的實例中能夠看到當$store.count變化時將觸發watch屬性中的回調方法,

由以前分析可知,當組件實例化中將在方法initComputed內遍歷其computed屬性,爲每個compted屬性生成一個內部watcher實例,s_count的表達式將做爲watcher的getter被訪問,又由於store.count作爲store內部vue實例的響應式數據,當其被訪問時將會收集這個watcher做爲依賴,同時s_count將被掛載到當前this實例上。同時在initWatch方法中當,將會爲watch屬性s_count生成一個內部watcher實例,this實例上的s_count屬性(被computed掛載將做爲watcher的getter被訪問,s_count將做爲該this實例生成的新屬性收集這個watcher做爲依賴,s_count的表達式將做爲回調方法(cb) 傳入watcher實例中,當store.count發生變化時觸發其set方法,調用dep.notify依次觸發依賴更新,s_count將獲取到最新值,s_count更新後觸發當前實例中的依賴再依次更新,從而觸發回調方法

能夠看到這個操做的關鍵在於當前組件實例中computed的內部watcher被vuex內的vue實例中的響應數據看成依賴被收集,這是兩個不一樣的vue實例進行數據流通的關鍵

<div id="app">
    {{ $store.count }}
</div>
<script>
export default {
   watch:{
   		s_count:{
   			callBack balabalala......
   		}
   }
   computed:{
  		s_count:{
  			return $store.count
  		}
  }
}
</script>
複製代碼

後記

拋開全部細節來講整個vue的數據鏈路追蹤系統其實就是一套簡單的觀察者模式,咱們在開發中根據業務需求建立數據間的依賴關係,vue所作的其實就是簡單的收集依賴關係而後觸發更新。很簡單的東西,可每每不少coder都理不清晰,在於網上的相關資料大都參差不齊,不少人都喜歡寫一些什麼vue雙向綁定的實現, 模仿vue實現雙向綁定之類的東西,大多就是經過Object.defineProperty()重寫了get ,set。這種文章實現起來容易,看起來也爽,五分鐘的光景感受本身就是尤大了, 其實到頭來仍是一團霧水,歸根結底仍是要閱讀源碼,配合大神們的註釋和官網解釋看效果更佳

參考

zhuanlan.zhihu.com/c_101888436…

juejin.im/user/56cc84…

github.com/answershuto…

相關文章
相關標籤/搜索