Vuex 原始碼解析

染陌同學發表於2017-10-30

寫在前面

因為對Vue.js很感興趣,而且平時工作的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js原始碼,並做了總結與輸出。

文章的原地址:github.com/answershuto…

在學習過程中,為Vue加上了中文的註釋github.com/answershuto…以及Vuex的註釋github.com/answershuto…,希望可以對其他想學習原始碼的小夥伴有所幫助。

可能會有理解存在偏差的地方,歡迎提issue指出,共同學習,共同進步。

Vuex

我們在使用Vue.js開發複雜的應用時,經常會遇到多個元件共享同一個狀態,亦或是多個元件會去更新同一個狀態,在應用程式碼量較少的時候,我們可以元件間通訊去維護修改資料,或者是通過事件匯流排來進行資料的傳遞以及修改。但是當應用逐漸龐大以後,程式碼就會變得難以維護,從父元件開始通過prop傳遞多層巢狀的資料由於層級過深而顯得異常脆弱,而事件匯流排也會因為元件的增多、程式碼量的增大而顯得互動錯綜複雜,難以捋清其中的傳遞關係。

那麼為什麼我們不能將資料層與元件層抽離開來呢?把資料層放到全域性形成一個單一的Store,元件層變得更薄,專門用來進行資料的展示及操作。所有資料的變更都需要經過全域性的Store來進行,形成一個單向資料流,使資料變化變得“可預測”。

Vuex是一個專門為Vue.js框架設計的、用於對Vue.js應用程式進行狀態管理的庫,它借鑑了Flux、redux的基本思想,將共享的資料抽離到全域性,以一個單例存放,同時利用Vue.js的響應式機制來進行高效的狀態管理與更新。正是因為Vuex使用了Vue.js內部的“響應式機制”,所以Vuex是一個專門為Vue.js設計並與之高度契合的框架(優點是更加簡潔高效,缺點是隻能跟Vue.js搭配使用)。具體使用方法及API可以參考Vuex的官網

先來看一下這張Vuex的資料流程圖,熟悉Vuex使用的同學應該已經有所瞭解。

Vuex實現了一個單向資料流,在全域性擁有一個State存放資料,所有修改State的操作必須通過Mutation進行,Mutation的同時提供了訂閱者模式供外部外掛呼叫獲取State資料的更新。所有非同步介面需要走Action,常見於呼叫後端介面非同步獲取更新資料,而Action也是無法直接修改State的,還是需要通過Mutation來修改State的資料。最後,根據State的變化,渲染到檢視上。Vuex執行依賴Vue內部資料雙向繫結機制,需要new一個Vue物件來實現“響應式化”,所以Vuex是一個專門為Vue.js設計的狀態管理庫。

安裝

使用過Vuex的朋友一定知道,Vuex的安裝十分簡單,只需要提供一個store,然後執行下面兩句程式碼即完成的Vuex的引入。

Vue.use(Vuex);

/*將store放入Vue建立時的option中*/
new Vue({
    el: '#app',
    store
});複製程式碼

那麼問題來了,Vuex是怎樣把store注入到Vue例項中去的呢?

Vue.js提供了Vue.use方法用來給Vue.js安裝外掛,內部通過呼叫外掛的install方法(當外掛是一個物件的時候)來進行外掛的安裝。

我們來看一下Vuex的install實現。

/*暴露給外部的外掛install方法,供Vue.use呼叫安裝外掛*/
export function install (_Vue) {
  if (Vue) {
    /*避免重複安裝(Vue.use內部也會檢測一次是否重複安裝同一個外掛)*/
    if (process.env.NODE_ENV !== 'production') {
      console.error(
        '[vuex] already installed. Vue.use(Vuex) should be called only once.'
      )
    }
    return
  }
  /*儲存Vue,同時用於檢測是否重複安裝*/
  Vue = _Vue
  /*將vuexInit混淆進Vue的beforeCreate(Vue2.0)或_init方法(Vue1.0)*/
  applyMixin(Vue)
}複製程式碼

這段install程式碼做了兩件事情,一件是防止Vuex被重複安裝,另一件是執行applyMixin,目的是執行vuexInit方法初始化Vuex。Vuex針對Vue1.0與2.0分別進行了不同的處理,如果是Vue1.0,Vuex會將vuexInit方法放入Vue的_init方法中,而對於Vue2.0,則會將vuexinit混淆進Vue的beforeCreacte鉤子中。來看一下vuexInit的程式碼。

 /*Vuex的init鉤子,會存入每一個Vue例項等鉤子列表*/
  function vuexInit () {
    const options = this.$options
    // store injection
    if (options.store) {
      /*存在store其實代表的就是Root節點,直接執行store(function時)或者使用store(非function)*/
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      /*子元件直接從父元件中獲取$store,這樣就保證了所有元件都公用了全域性的同一份store*/
      this.$store = options.parent.$store
    }
  }複製程式碼

vuexInit會嘗試從options中獲取store,如果當前元件是根元件(Root節點),則options中會存在store,直接獲取賦值給$store即可。如果當前元件非根元件,則通過options中的parent獲取父元件的$store引用。這樣一來,所有的元件都獲取到了同一份記憶體地址的Store例項,於是我們可以在每一個元件中通過this.$store愉快地訪問全域性的Store例項了。

那麼,什麼是Store例項?

Store

我們傳入到根元件到store,就是Store例項,用Vuex提供到Store方法構造。

export default new Vuex.Store({
    strict: true,
    modules: {
        moduleA,
        moduleB
    }
});複製程式碼

我們來看一下Store的實現。首先是建構函式。

constructor (options = {}) {
    // Auto install if it is not done yet and `window` has `Vue`.
    // To allow users to avoid auto-installation in some cases,
    // this code should be placed here. See #731
    /*
      在瀏覽器環境下,如果外掛還未安裝(!Vue即判斷是否未安裝),則它會自動安裝。
      它允許使用者在某些情況下避免自動安裝。
    */
    if (!Vue && typeof window !== 'undefined' && window.Vue) {
      install(window.Vue)
    }

    if (process.env.NODE_ENV !== 'production') {
      assert(Vue, `must call Vue.use(Vuex) before creating a store instance.`)
      assert(typeof Promise !== 'undefined', `vuex requires a Promise polyfill in this browser.`)
      assert(this instanceof Store, `Store must be called with the new operator.`)
    }

    const {
      /*一個陣列,包含應用在 store 上的外掛方法。這些外掛直接接收 store 作為唯一引數,可以監聽 mutation(用於外部地資料持久化、記錄或除錯)或者提交 mutation (用於內部資料,例如 websocket 或 某些觀察者)*/
      plugins = [],
      /*使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函式以外修改 Vuex state 都會丟擲錯誤。*/
      strict = false
    } = options

    /*從option中取出state,如果state是function則執行,最終得到一個物件*/
    let {
      state = {}
    } = options
    if (typeof state === 'function') {
      state = state()
    }

    // store internal state
    /* 用來判斷嚴格模式下是否是用mutation修改state的 */
    this._committing = false
    /* 存放action */
    this._actions = Object.create(null)
    /* 存放mutation */
    this._mutations = Object.create(null)
    /* 存放getter */
    this._wrappedGetters = Object.create(null)
    /* module收集器 */
    this._modules = new ModuleCollection(options)
    /* 根據namespace存放module */
    this._modulesNamespaceMap = Object.create(null)
    /* 存放訂閱者 */
    this._subscribers = []
    /* 用以實現Watch的Vue例項 */
    this._watcherVM = new Vue()

    // bind commit and dispatch to self
    /*將dispatch與commit呼叫的this繫結為store物件本身,否則在元件內部this.dispatch時的this會指向元件的vm*/
    const store = this
    const { dispatch, commit } = this
    /* 為dispatch與commit繫結this(Store例項本身) */
    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
    /*嚴格模式(使 Vuex store 進入嚴格模式,在嚴格模式下,任何 mutation 處理函式以外修改 Vuex state 都會丟擲錯誤)*/
    this.strict = strict

    // init root module.
    // this also recursively registers all sub-modules
    // and collects all module getters inside this._wrappedGetters
    /*初始化根module,這也同時遞迴註冊了所有子modle,收集所有module的getter到_wrappedGetters中去,this._modules.root代表根module才獨有儲存的Module物件*/
    installModule(this, state, [], this._modules.root)

    // initialize the store vm, which is responsible for the reactivity
    // (also registers _wrappedGetters as computed properties)
    /* 通過vm重設store,新建Vue物件使用Vue內部的響應式實現註冊state以及computed */
    resetStoreVM(this, state)

    // apply plugins
    /* 呼叫外掛 */
    plugins.forEach(plugin => plugin(this))

    /* devtool外掛 */
    if (Vue.config.devtools) {
      devtoolPlugin(this)
    }
  }複製程式碼

Store的構造類除了初始化一些內部變數以外,主要執行了installModule(初始化module)以及resetStoreVM(通過VM使store“響應式”)。

installModule

installModule的作用主要是用為module加上namespace名字空間(如果有)後,註冊mutation、action以及getter,同時遞迴安裝所有子module。

/*初始化module*/
function installModule (store, rootState, path, module, hot) {
  /* 是否是根module */
  const isRoot = !path.length
  /* 獲取module的namespace */
  const namespace = store._modules.getNamespace(path)

  // register in namespace map
  /* 如果有namespace則在_modulesNamespaceMap中註冊 */
  if (module.namespaced) {
    store._modulesNamespaceMap[namespace] = module
  }

  // set state
  if (!isRoot && !hot) {
    /* 獲取父級的state */
    const parentState = getNestedState(rootState, path.slice(0, -1))
    /* module的name */
    const moduleName = path[path.length - 1]
    store.`_withCommit`(() => {
      /* 將子module設定稱響應式的 */
      Vue.set(parentState, moduleName, module.state)
    })
  }

  const local = module.context = makeLocalContext(store, namespace, path)

  /* 遍歷註冊mutation */
  module.forEachMutation((mutation, key) => {
    const namespacedType = namespace + key
    registerMutation(store, namespacedType, mutation, local)
  })

  /* 遍歷註冊action */
  module.forEachAction((action, key) => {
    const namespacedType = namespace + key
    registerAction(store, namespacedType, action, local)
  })

  /* 遍歷註冊getter */
  module.forEachGetter((getter, key) => {
    const namespacedType = namespace + key
    registerGetter(store, namespacedType, getter, local)
  })

  /* 遞迴安裝mudule */
  module.forEachChild((child, key) => {
    installModule(store, rootState, path.concat(key), child, hot)
  })
}複製程式碼

resetStoreVM

在說resetStoreVM之前,先來看一個小demo。

let globalData = {
    d: 'hello world'
};
new Vue({
    data () {
        return {
            $$state: {
                globalData
            }
        }
    }
});

/* modify */
setTimeout(() => {
    globalData.d = 'hi~';
}, 1000);

Vue.prototype.globalData = globalData;

/* 任意模板中 */
<div>{{globalData.d}}</div>複製程式碼

上述程式碼在全域性有一個globalData,它被傳入一個Vue物件的data中,之後在任意Vue模板中對該變數進行展示,因為此時globalData已經在Vue的prototype上了所以直接通過this.prototype訪問,也就是在模板中的{{prototype.d}}。此時,setTimeout在1s之後將globalData.d進行修改,我們發現模板中的globalData.d發生了變化。其實上述部分就是Vuex依賴Vue核心實現資料的“響應式化”。

不熟悉Vue.js響應式原理的同學可以通過筆者另一篇文章響應式原理瞭解Vue.js是如何進行資料雙向繫結的。

接著來看程式碼。

/* 通過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())
  }
}複製程式碼

resetStoreVM首先會遍歷wrappedGetters,使用Object.defineProperty方法為每一個getter繫結上get方法,這樣我們就可以在元件裡訪問this.$store.getter.test就等同於訪問store._vm.test。

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

之後Vuex採用了new一個Vue物件來實現資料的“響應式化”,運用Vue.js內部提供的資料雙向繫結功能來實現store的資料與檢視的同步更新。

store._vm = new Vue({
  data: {
    $$state: state
  },
  computed
})複製程式碼

這時候我們訪問store._vm.test也就訪問了Vue例項中的屬性。

這兩步執行完以後,我們就可以通過this.$store.getter.test訪問vm中的test屬性了。

嚴格模式

Vuex的Store構造類的option有一個strict的引數,可以控制Vuex執行嚴格模式,嚴格模式下,所有修改state的操作必須通過mutation實現,否則會丟擲錯誤。

/* 使能嚴格模式 */
function enableStrictMode (store) {
  store._vm.$watch(function () { return this._data.$$state }, () => {
    if (process.env.NODE_ENV !== 'production') {
      /* 檢測store中的_committing的值,如果是true代表不是通過mutation的方法修改的 */
      assert(store._committing, `Do not mutate vuex store state outside mutation handlers.`)
    }
  }, { deep: true, sync: true })
}複製程式碼

首先,在嚴格模式下,Vuex會利用vm的$watch方法來觀察$$state,也就是Store的state,在它被修改的時候進入回撥。我們發現,回撥中只有一句話,用assert斷言來檢測store._committing,當store._committing為false的時候會觸發斷言,丟擲異常。

我們發現,Store的commit方法中,執行mutation的語句是這樣的。

this._withCommit(() => {
  entry.forEach(function commitIterator (handler) {
    handler(payload)
  })
})複製程式碼

再來看看_withCommit的實現。

_withCommit (fn) {
  /* 呼叫withCommit修改state的值時會將store的committing值置為true,內部會有斷言檢查該值,在嚴格模式下只允許使用mutation來修改store中的值,而不允許直接修改store的數值 */
  const committing = this._committing
  this._committing = true
  fn()
  this._committing = committing
}複製程式碼

我們發現,通過commit(mutation)修改state資料的時候,會再呼叫mutation方法之前將committing置為true,接下來再通過mutation函式修改state中的資料,這時候觸發$watch中的回撥斷言committing是不會丟擲異常的(此時committing為true)。而當我們直接修改state的資料時,觸發$watch的回撥執行斷言,這時committing為false,則會丟擲異常。這就是Vuex的嚴格模式的實現。

接下來我們來看看Store提供的一些API。

commit(mutation

/* 呼叫mutation的commit方法 */
commit (_type, _payload, _options) {
  // check object-style commit
  /* 校驗引數 */
  const {
    type,
    payload,
    options
  } = unifyObjectStyle(_type, _payload, _options)

  const mutation = { type, payload }
  /* 取出type對應的mutation的方法 */
  const entry = this._mutations[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown mutation type: ${type}`)
    }
    return
  }
  /* 執行mutation中的所有方法 */
  this._withCommit(() => {
    entry.forEach(function commitIterator (handler) {
      handler(payload)
    })
  })
  /* 通知所有訂閱者 */
  this._subscribers.forEach(sub => sub(mutation, this.state))

  if (
    process.env.NODE_ENV !== 'production' &&
    options && options.silent
  ) {
    console.warn(
      `[vuex] mutation type: ${type}. Silent option has been removed. ` +
      'Use the filter functionality in the vue-devtools'
    )
  }
}複製程式碼

commit方法會根據type找到並呼叫_mutations中的所有type對應的mutation方法,所以當沒有namespace的時候,commit方法會觸發所有module中的mutation方法。再執行完所有的mutation之後會執行_subscribers中的所有訂閱者。我們來看一下_subscribers是什麼。

Store給外部提供了一個subscribe方法,用以註冊一個訂閱函式,會push到Store例項的_subscribers中,同時返回一個從_subscribers中登出該訂閱者的方法。

/* 註冊一個訂閱函式,返回取消訂閱的函式 */
subscribe (fn) {
  const subs = this._subscribers
  if (subs.indexOf(fn) < 0) {
    subs.push(fn)
  }
  return () => {
    const i = subs.indexOf(fn)
    if (i > -1) {
      subs.splice(i, 1)
    }
  }
}複製程式碼

在commit結束以後則會呼叫這些_subscribers中的訂閱者,這個訂閱者模式提供給外部一個監視state變化的可能。state通過mutation改變時,可以有效補獲這些變化。

dispatch(action

來看一下dispatch的實現。

/* 呼叫action的dispatch方法 */
dispatch (_type, _payload) {
  // check object-style dispatch
  const {
    type,
    payload
  } = unifyObjectStyle(_type, _payload)

  /* actions中取出type對應的ation */
  const entry = this._actions[type]
  if (!entry) {
    if (process.env.NODE_ENV !== 'production') {
      console.error(`[vuex] unknown action type: ${type}`)
    }
    return
  }

  /* 是陣列則包裝Promise形成一個新的Promise,只有一個則直接返回第0個 */
  return entry.length > 1
    ? Promise.all(entry.map(handler => handler(payload)))
    : entry[0](payload)
}複製程式碼

以及registerAction時候做的事情。

/* 遍歷註冊action */
function registerAction (store, type, handler, local) {
  /* 取出type對應的action */
  const entry = store._actions[type] || (store._actions[type] = [])
  entry.push(function wrappedActionHandler (payload, cb) {
    let res = handler.call(store, {
      dispatch: local.dispatch,
      commit: local.commit,
      getters: local.getters,
      state: local.state,
      rootGetters: store.getters,
      rootState: store.state
    }, payload, cb)
    /* 判斷是否是Promise */
    if (!isPromise(res)) {
      /* 不是Promise物件的時候轉化稱Promise物件 */
      res = Promise.resolve(res)
    }
    if (store._devtoolHook) {
      /* 存在devtool外掛的時候觸發vuex的error給devtool */
      return res.catch(err => {
        store._devtoolHook.emit('vuex:error', err)
        throw err
      })
    } else {
      return res
    }
  })
}複製程式碼

因為registerAction的時候將push進_actions的action進行了一層封裝(wrappedActionHandler),所以我們在進行dispatch的第一個引數中獲取state、commit等方法。之後,執行結果res會被進行判斷是否是Promise,不是則會進行一層封裝,將其轉化成Promise物件。dispatch時則從_actions中取出,只有一個的時候直接返回,否則用Promise.all處理再返回。

watch

/* 觀察一個getter方法 */
watch (getter, cb, options) {
  if (process.env.NODE_ENV !== 'production') {
    assert(typeof getter === 'function', `store.watch only accepts a function.`)
  }
  return this._watcherVM.$watch(() => getter(this.state, this.getters), cb, options)
}複製程式碼

熟悉Vue的朋友應該很熟悉watch這個方法。這裡採用了比較巧妙的設計,_watcherVM是一個Vue的例項,所以watch就可以直接採用了Vue內部的watch特性提供了一種觀察資料getter變動的方法。

registerModule

/* 註冊一個動態module,當業務進行非同步載入的時候,可以通過該介面進行註冊動態module */
registerModule (path, rawModule) {
  /* 轉化稱Array */
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
    assert(path.length > 0, 'cannot register the root module by using registerModule.')
  }

  /*註冊*/
  this._modules.register(path, rawModule)
  /*初始化module*/
  installModule(this, this.state, path, this._modules.get(path))
  // reset store to update getters...
  /* 通過vm重設store,新建Vue物件使用Vue內部的響應式實現註冊state以及computed */
  resetStoreVM(this, this.state)
}複製程式碼

registerModule用以註冊一個動態模組,也就是在store建立以後再註冊模組的時候用該介面。內部實現實際上也只有installModule與resetStoreVM兩個步驟,前面已經講過,這裡不再累述。

unregisterModule

 /* 登出一個動態module */
unregisterModule (path) {
  /* 轉化稱Array */
  if (typeof path === 'string') path = [path]

  if (process.env.NODE_ENV !== 'production') {
    assert(Array.isArray(path), `module path must be a string or an Array.`)
  }

  /*登出*/
  this._modules.unregister(path)
  this._withCommit(() => {
    /* 獲取父級的state */
    const parentState = getNestedState(this.state, path.slice(0, -1))
    /* 從父級中刪除 */
    Vue.delete(parentState, path[path.length - 1])
  })
  /* 重製store */
  resetStore(this)
}複製程式碼

同樣,與registerModule對應的方法unregisterModule,動態登出模組。實現方法是先從state中刪除模組,然後用resetStore來重製store。

resetStore

/* 重製store */
function resetStore (store, hot) {
  store._actions = Object.create(null)
  store._mutations = Object.create(null)
  store._wrappedGetters = Object.create(null)
  store._modulesNamespaceMap = Object.create(null)
  const state = store.state
  // init all modules
  installModule(store, state, [], store._modules.root, true)
  // reset vm
  resetStoreVM(store, state, hot)
}複製程式碼

這裡的resetStore其實也就是將store中的_actions等進行初始化以後,重新執行installModule與resetStoreVM來初始化module以及用Vue特性使其“響應式化”,這跟建構函式中的是一致的。

外掛

Vue提供了一個非常好用的外掛Vue.js devtools

/* 從window物件的__VUE_DEVTOOLS_GLOBAL_HOOK__中獲取devtool外掛 */
const devtoolHook =
  typeof window !== 'undefined' &&
  window.__VUE_DEVTOOLS_GLOBAL_HOOK__

export default function devtoolPlugin (store) {
  if (!devtoolHook) return

  /* devtoll外掛例項儲存在store的_devtoolHook上 */
  store._devtoolHook = devtoolHook

  /* 出發vuex的初始化事件,並將store的引用地址傳給deltool外掛,使外掛獲取store的例項 */
  devtoolHook.emit('vuex:init', store)

  /* 監聽travel-to-state事件 */
  devtoolHook.on('vuex:travel-to-state', targetState => {
    /* 重製state */
    store.replaceState(targetState)
  })

  /* 訂閱store的變化 */
  store.subscribe((mutation, state) => {
    devtoolHook.emit('vuex:mutation', mutation, state)
  })
}複製程式碼

如果已經安裝了該外掛,則會在windows物件上暴露一個VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的時候會觸發“vuex:init”事件通知外掛,然後通過on方法監聽“vuex:travel-to-state”事件來重置state。最後通過Store的subscribe方法來新增一個訂閱者,在觸發commit方法修改mutation資料以後,該訂閱者會被通知,從而觸發“vuex:mutation”事件。

最後

Vuex是一個非常優秀的庫,程式碼量不多且結構清晰,非常適合研究學習其內部實現。最近的一系列原始碼閱讀也使我自己受益匪淺,寫這篇文章也希望可以幫助到更多想要學習探索Vuex內部實現原理的同學。

關於

作者:染陌

Email:answershuto@gmail.com or answershuto@126.com

Github: github.com/answershuto

Blog:answershuto.github.io/

知乎主頁:www.zhihu.com/people/cao-…

知乎專欄:zhuanlan.zhihu.com/ranmo

掘金: juejin.im/user/58f87a…

osChina:my.oschina.net/u/3161824/b…

轉載請註明出處,謝謝。

歡迎關注我的公眾號

相關文章