vuex工作原理詳解

前言

vuex作為vue官方出品的狀態(tài)管理框架,以及其簡(jiǎn)單API設(shè)計(jì)、便捷的開(kāi)發(fā)工具支持,在中大型的vue項(xiàng)目中得到很好的應(yīng)用。作為flux架構(gòu)的后起之秀,吸收了前輩redux的各種優(yōu)點(diǎn),完美的結(jié)合了vue響應(yīng)式數(shù)據(jù),個(gè)人認(rèn)為開(kāi)發(fā)體驗(yàn)已經(jīng)超過(guò)了React + Redux這對(duì)基友。

在項(xiàng)目啟動(dòng)vue開(kāi)發(fā)后的這幾個(gè)月中,越發(fā)對(duì)vuex的原理感到好奇,今天將這幾日的所學(xué)總結(jié)成文,希望能幫到對(duì)vuex好奇的童鞋們。

理解computed

使用vuex中store中的數(shù)據(jù),基本上離不開(kāi)vue中一個(gè)常用的屬性computed。官方一個(gè)最簡(jiǎn)單的例子如下

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計(jì)算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實(shí)例
      return this.message.split('').reverse().join()
    }
  }
})

不知大家有沒(méi)有思考過(guò),vue的computed是如何更新的,為什么當(dāng)vm.message發(fā)生變化時(shí),vm.reversedMessage也會(huì)自動(dòng)發(fā)生變化?

我們來(lái)看看vue中data屬性和computed相關(guān)的源代碼。

// src/core/instance/state.js
// 初始化組件的state
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)
  // 當(dāng)組件存在data屬性
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  // 當(dāng)組件存在 computed屬性
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initState方法當(dāng)組件實(shí)例化時(shí)會(huì)自動(dòng)觸發(fā),該方法主要完成了初始化data,methods,props,computed,watch這些我們常用的屬性,我們來(lái)看看我們需要關(guān)注的initDatainitComputed(為了節(jié)省時(shí)間,去除了不太相關(guān)的代碼)

先看看initData這條線(xiàn)

// src/core/instance/state.js
function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // .....省略無(wú)關(guān)代碼
  
  // 將vue的data傳入observe方法
  observe(data, true /* asRootData */)
}

// src/core/observer/index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  // ...省略無(wú)關(guān)代碼
  ob = new Observer(value)
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}


在初始化的時(shí)候observe方法本質(zhì)上是實(shí)例化了一個(gè)Observer對(duì)象,這個(gè)對(duì)象的類(lèi)是這樣的

// src/core/observer/index.js
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
    // 關(guān)鍵代碼 new Dep對(duì)象
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    // ...省略無(wú)關(guān)代碼
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      // 給data的所有屬性調(diào)用defineReactive
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

在對(duì)象的構(gòu)造函數(shù)中,最后調(diào)用了walk方法,該方法即遍歷data中的所有屬性,并調(diào)用defineReactive方法,defineReactive方法是vue實(shí)現(xiàn) MDV(Model-Driven-View)的基礎(chǔ),本質(zhì)上就是代理了數(shù)據(jù)的set,get方法,當(dāng)數(shù)據(jù)修改或獲取的時(shí)候,能夠感知(當(dāng)然vue還要考慮數(shù)組,Object中嵌套Object等各種情況,本文不在分析)。我們具體看看defineReactive的源代碼

// src/core/observer/index.js
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 重點(diǎn),在給具體屬性調(diào)用該方法時(shí),都會(huì)為該屬性生成唯一的dep對(duì)象
  const dep = new Dep()

  // 獲取該屬性的描述對(duì)象
  // 該方法會(huì)返回對(duì)象中某個(gè)屬性的具體描述
  // api地址https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // 如果該描述不能被更改,直接返回,因?yàn)椴荒芨?,那么就無(wú)法代理set和get方法,無(wú)法做到響應(yīng)式
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set

  let childOb = !shallow && observe(val)
  // 重新定義data當(dāng)中的屬性,對(duì)get和set進(jìn)行代理。
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 收集依賴(lài), reversedMessage為什么會(huì)跟著message變化的原因
      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
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      // 通知依賴(lài)進(jìn)行更新
      dep.notify()
    }
  })
}

我們可以看到,在所代理的屬性get方法中,當(dāng)dep.Target存在的時(shí)候會(huì)調(diào)用dep.depend()方法,這個(gè)方法非常的簡(jiǎn)單,不過(guò)在說(shuō)這個(gè)方法之前,我們要認(rèn)識(shí)一個(gè)新的類(lèi)Dep

Dep 是 vue 實(shí)現(xiàn)的一個(gè)處理依賴(lài)關(guān)系的對(duì)象,
主要起到一個(gè)紐帶的作用,就是連接 reactive data 與 watcher,代碼非常的簡(jiǎn)單

// src/core/observer/dep.js
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 () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      // 更新 watcher 的值,與 watcher.evaluate() 類(lèi)似,
      // 但 update 是給依賴(lài)變化時(shí)使用的,包含對(duì) watch 的處理
      subs[i].update()
    }
  }
}

// 當(dāng)首次計(jì)算 computed 屬性的值時(shí),Dep 將會(huì)在計(jì)算期間對(duì)依賴(lài)進(jìn)行收集
Dep.target = null
const targetStack = []

export function pushTarget (_target: Watcher) {
  // 在一次依賴(lài)收集期間,如果有其他依賴(lài)收集任務(wù)開(kāi)始(比如:當(dāng)前 computed 計(jì)算屬性嵌套其他 computed 計(jì)算屬性),
  // 那么將會(huì)把當(dāng)前 target 暫存到 targetStack,先進(jìn)行其他 target 的依賴(lài)收集,
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  // 當(dāng)嵌套的依賴(lài)收集任務(wù)完成后,將 target 恢復(fù)為上一層的 Watcher,并繼續(xù)做依賴(lài)收集
  Dep.target = targetStack.pop()
}

代碼非常的簡(jiǎn)單,回到調(diào)用dep.depend()方法的時(shí)候,當(dāng)Dep.Target存在,就會(huì)調(diào)用,而depend方法則是將該dep加入watchernewDeps中,同時(shí),將所訪(fǎng)問(wèn)當(dāng)前屬性dep對(duì)象中的subs插入當(dāng)前Dep.target的watcher.看起來(lái)有點(diǎn)繞,不過(guò)沒(méi)關(guān)系,我們一會(huì)跟著例子講解一下就清楚了。

講完了代理的get,方法,我們講一下代理的set方法,set方法的最后調(diào)用了dep.notify(),當(dāng)設(shè)置data中具體屬性值的時(shí)候,就會(huì)調(diào)用該屬性下面的dep.notify()方法,通過(guò)class Dep了解到,notify方法即將加入該dep的watcher全部更新,也就是說(shuō),當(dāng)你修改data中某個(gè)屬性值時(shí),會(huì)同時(shí)調(diào)用dep.notify()來(lái)更新依賴(lài)該值的所有watcher。

介紹完了initData這條線(xiàn),我們繼續(xù)來(lái)介紹initComputed這條線(xiàn),這條線(xiàn)主要解決了什么時(shí)候去設(shè)置Dep.target的問(wèn)題(如果沒(méi)有設(shè)置該值,就不會(huì)調(diào)用dep.depend(), 即無(wú)法獲取依賴(lài))。

// src/core/instance/state.js
const computedWatcherOptions = { lazy: true }
function initComputed (vm: Component, computed: Object) {
  // 初始化watchers列表
  const watchers = vm._computedWatchers = Object.create(null)
  const isSSR = isServerRendering()

  for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    if (!isSSR) {
      // 關(guān)注點(diǎn)1,給所有屬性生成自己的watcher, 可以在this._computedWatchers下看到
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      // 關(guān)注點(diǎn)2
      defineComputed(vm, key, userDef)
    }
  }
}

在初始化computed時(shí),有2個(gè)地方需要去關(guān)注

  1. 對(duì)每一個(gè)屬性都生成了一個(gè)屬于自己的Watcher實(shí)例,并將 { lazy: true }作為options傳入
  2. 對(duì)每一個(gè)屬性調(diào)用了defineComputed方法(本質(zhì)和data一樣,代理了自己的set和get方法,我們重點(diǎn)關(guān)注代理的get方法)

我們看看Watcher的構(gòu)造函數(shù)

// src/core/observer/watcher.js
constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    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 // 如果初始化lazy=true時(shí)(暗示是computed屬性),那么dirty也是true,需要等待更新
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.getter = expOrFn // 在computed實(shí)例化時(shí),將具體的屬性值放入this.getter中
    // 省略不相關(guān)的代碼
    this.value = this.lazy
      ? undefined
      : this.get()
  }

除了日常的初始化外,還有2行重要的代碼

this.dirty = this.lazy
this.getter = expOrFn

computed生成的watcher,會(huì)將watcher的lazy設(shè)置為true,以減少計(jì)算量。因此,實(shí)例化時(shí),this.dirty也是true,標(biāo)明數(shù)據(jù)需要更新操作。我們先記住現(xiàn)在computed中初始化對(duì)各個(gè)屬性生成的watcher的dirty和lazy都設(shè)置為了true。同時(shí),將computed傳入的屬性值(一般為funtion),放入watchergetter中保存起來(lái)。

我們?cè)趤?lái)看看第二個(gè)關(guān)注點(diǎn)defineComputed所代理屬性的get方法是什么

// src/core/instance/state.js
function createComputedGetter (key) {
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    // 如果找到了該屬性的watcher
    if (watcher) {
      // 和上文對(duì)應(yīng),初始化時(shí),該dirty為true,也就是說(shuō),當(dāng)?shù)谝淮卧L(fǎng)問(wèn)computed中的屬性的時(shí)候,會(huì)調(diào)用 watcher.evaluate()方法;
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

當(dāng)第一次訪(fǎng)問(wèn)computed中的值時(shí),會(huì)因?yàn)槌跏蓟?code>watcher.dirty = watcher.lazy的原因,從而調(diào)用evalute()方法,evalute()方法很簡(jiǎn)單,就是調(diào)用了watcher實(shí)例中的get方法以及設(shè)置dirty = false,我們將這兩個(gè)方法放在一起

// src/core/instance/state.js
evaluate () {
  this.value = this.get()
  this.dirty = false
}
  
get () {  
// 重點(diǎn)1,將當(dāng)前watcher放入Dep.target對(duì)象
  pushTarget(this)
  let value
  const vm = this.vm
  try {
    // 重點(diǎn)2,當(dāng)調(diào)用用戶(hù)傳入的方法時(shí),會(huì)觸發(fā)什么?
    value = this.getter.call(vm, vm)
  } catch (e) {
  } finally {
    popTarget()
    // 去除不相關(guān)代碼
  }
  return value
}

在get方法中中,第一行就調(diào)用了pushTarget方法,其作用就是將Dep.target設(shè)置為所傳入的watcher,即所訪(fǎng)問(wèn)的computed中屬性的watcher,
然后調(diào)用了value = this.getter.call(vm, vm)方法,想一想,調(diào)用這個(gè)方法會(huì)發(fā)生什么?

this.getter 在Watcher構(gòu)建函數(shù)中提到,本質(zhì)就是用戶(hù)傳入的方法,也就是說(shuō),this.getter.call(vm, vm)就會(huì)調(diào)用用戶(hù)自己聲明的方法,那么如果方法里面用到了 this.data中的值或者其他被用defineReactive包裝過(guò)的對(duì)象,那么,訪(fǎng)問(wèn)this.data.或者其他被defineReactive包裝過(guò)的屬性,是不是就會(huì)訪(fǎng)問(wèn)被代理的該屬性的get方法。我們?cè)诨仡^看看
get方法是什么樣子的。

注意:我講了其他被用defineReactive,這個(gè)和后面的vuex有關(guān)系,我們后面在提

get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      // 這個(gè)時(shí)候,有值了
      if (Dep.target) {
        // computed的watcher依賴(lài)了this.data的dep
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
        }
        if (Array.isArray(value)) {
          dependArray(value)
        }
      }
      return value
    }

代碼注釋已經(jīng)寫(xiě)明了,就不在解釋了,這個(gè)時(shí)候我們走完了一個(gè)依賴(lài)收集流程,知道了computed是如何知道依賴(lài)了誰(shuí)。最后根據(jù)this.data所代理的set方法中調(diào)用的notify,就可以改變this.data的值,去更新所有依賴(lài)this.data值的computed屬性value了。

那么,我們根據(jù)下面的代碼,來(lái)簡(jiǎn)易拆解獲取依賴(lài)并更新的過(guò)程

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // 計(jì)算屬性的 getter
    reversedMessage: function () {
      // `this` 指向 vm 實(shí)例
      return this.message.split('').reverse().join()
    }
  }
})
vm.reversedMessage // =>  olleH
vm.message = 'World' // 
vm.reversedMessage // =>  dlroW
  1. 初始化 data和computed,分別代理其set以及get方法, 對(duì)data中的所有屬性生成唯一的dep實(shí)例。
  2. 對(duì)computed中的reversedMessage生成唯一watcher,并保存在vm._computedWatchers中
  3. 訪(fǎng)問(wèn) reversedMessage,設(shè)置Dep.target指向reversedMessage的watcher,調(diào)用該屬性具體方法reversedMessage。
  4. 方法中訪(fǎng)問(wèn)this.message,即會(huì)調(diào)用this.message代理的get方法,將this.message的dep加入reversedMessage的watcher,同時(shí)該dep中的subs添加這個(gè)watcher
  5. 設(shè)置vm.message = 'World',調(diào)用message代理的set方法觸發(fā)dep的notify方法'
  6. 因?yàn)槭莄omputed屬性,只是將watcher中的dirty設(shè)置為true
  7. 最后一步vm.reversedMessage,訪(fǎng)問(wèn)其get方法時(shí),得知reversedMessagewatcher.dirty為true,調(diào)用watcher.evaluate()方法獲取新的值。

這樣,也可以解釋了為什么有些時(shí)候當(dāng)computed沒(méi)有被訪(fǎng)問(wèn)(或者沒(méi)有被模板依賴(lài)),當(dāng)修改了this.data值后,通過(guò)vue-tools發(fā)現(xiàn)其computed中的值沒(méi)有變化的原因,因?yàn)闆](méi)有觸發(fā)到其get方法。

vuex插件

有了上文作為鋪墊,我們就可以很輕松的來(lái)解釋vuex的原理了。

我們知道,vuex僅僅是作為vue的一個(gè)插件而存在,不像Redux,MobX等庫(kù)可以應(yīng)用于所有框架,vuex只能使用在vue上,很大的程度是因?yàn)槠涓叨纫蕾?lài)于vue的computed依賴(lài)檢測(cè)系統(tǒng)以及其插件系統(tǒng),

通過(guò)官方文檔我們知道,每一個(gè)vue插件都需要有一個(gè)公開(kāi)的install方法,vuex也不例外。其代碼比較簡(jiǎn)單,調(diào)用了一下applyMixin方法,該方法主要作用就是在所有組件的beforeCreate生命周期注入了設(shè)置this.$store這樣一個(gè)對(duì)象,因?yàn)楸容^簡(jiǎn)單,這里不再詳細(xì)介紹代碼了,大家自己讀一讀編能很容易理解。

// src/store.js
export function install (_Vue) {
  if (Vue && _Vue === Vue) {
    return
  }
  Vue = _Vue
  applyMixin(Vue)
}
// src/mixins.js
// 對(duì)應(yīng)applyMixin方法
export default function (Vue) {
  const version = Number(Vue.version.split('.')[0])

  if (version >= 2) {
    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)
    }
  }

  /**
   * Vuex init hook, injected into each instances init hooks list.
   */

  function vuexInit () {
    const options = this.$options
    // store injection
    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
    }
  }
}

我們?cè)跇I(yè)務(wù)中使用vuex需要類(lèi)似以下的寫(xiě)法

const store = new Vuex.Store({
    state,
    mutations,
    actions,
    modules
});

那么 Vuex.Store到底是什么樣的東西呢?我們先看看他的構(gòu)造函數(shù)

// src/store.js
constructor (options = {}) {
  const {
    plugins = [],
    strict = false
  } = options

  // store internal state
  this._committing = false
  this._actions = Object.create(null)
  this._actionSubscribers = []
  this._mutations = Object.create(null)
  this._wrappedGetters = Object.create(null)
  this._modules = new ModuleCollection(options)
  this._modulesNamespaceMap = Object.create(null)
  this._subscribers = []
  this._watcherVM = new Vue()

  const store = this
  const { dispatch, commit } = this
  this.dispatch = function boundDispatch (type, payload) {
    return dispatch.call(store, type, payload)
}
  this.commit = function boundCommit (type, payload, options) {
    return commit.call(store, type, payload, options)
}

  // strict mode
  this.strict = strict

  const state = this._modules.root.state

  // init root module.
  // this also recursively registers all sub-modules
  // and collects all module getters inside this._wrappedGetters
  installModule(this, state, [], this._modules.root)

  // 重點(diǎn)方法 ,重置VM
  resetStoreVM(this, state)

  // apply plugins
  plugins.forEach(plugin => plugin(this))

}

除了一堆初始化外,我們注意到了這樣一行代碼
resetStoreVM(this, state) 他就是整個(gè)vuex的關(guān)鍵

// src/store.js
function resetStoreVM (store, state, hot) {
  // 省略無(wú)關(guān)代碼
  Vue.config.silent = true
  store._vm = new Vue({
    data: {
      $$state: state
    },
    computed
  })
}

去除了一些無(wú)關(guān)代碼后我們發(fā)現(xiàn),其本質(zhì)就是將我們傳入的state作為一個(gè)隱藏的vue組件的data,也就是說(shuō),我們的commit操作,本質(zhì)上其實(shí)是修改這個(gè)組件的data值,結(jié)合上文的computed,修改被defineReactive代理的對(duì)象值后,會(huì)將其收集到的依賴(lài)的watcher中的dirty設(shè)置為true,等到下一次訪(fǎng)問(wèn)該watcher中的值后重新獲取最新值。

這樣就能解釋了為什么vuex中的state的對(duì)象屬性必須提前定義好,如果該state中途增加一個(gè)屬性,因?yàn)樵?strong>屬性沒(méi)有被defineReactive,所以其依賴(lài)系統(tǒng)沒(méi)有檢測(cè)到,自然不能更新。

由上所說(shuō),我們可以得知store._vm.$data.$$state === store.state, 我們可以在任何含有vuex框架的工程驗(yàn)證這一點(diǎn)。


總結(jié)

vuex整體思想誕生于flux,可其的實(shí)現(xiàn)方式完完全全的使用了vue自身的響應(yīng)式設(shè)計(jì),依賴(lài)監(jiān)聽(tīng)、依賴(lài)收集都屬于vue對(duì)對(duì)象Property set get方法的代理劫持。最后一句話(huà)結(jié)束vuex工作原理,vuex中的store本質(zhì)就是沒(méi)有template的隱藏著的vue組件;

(如果本文對(duì)幫助到了大家,歡迎給個(gè)贊,謝謝)~

參考文章

深入理解 Vue Computed 計(jì)算屬性

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容