推薦:《5個vue.js影片教學精選》
推薦:《5個vue.js影片教學精選》
#以Vuex 為引,一窺狀態管理全貌
眾所周知,Vuex 是Vue 官方的狀態管理方案。
Vuex 的用法和 API 不難,官網介紹也簡潔明了。得益於此,將 Vuex 快速整合到專案中非常容易。然而正因為用法靈活,許多同學在 Vuex 的設計和使用上反而有些混亂。
其實在使用前,我們不妨暫停一下,思考幾個問題:
如果你對這些問題模稜兩可,那麼恭喜你,這篇文章可能是你需要的。
下面請和我一起,從起源開始,以 Vuex 為例,共同揭開狀態管理的神秘面紗。
本文介紹的內容包含以下面向:
自三大框架誕生起,它們共有的兩個能力徹底暴擊了Jquery。這兩個能力分別是:
資料驅動視圖,使我們告別了只能依靠操作DOM 更新頁面的時代。我們不再需要每次更新頁面時,透過層層 find 找到 DOM 然後修改它的屬性和內容,可以透過操作資料來實現這些事情。
當然了在我們前端的眼裡,資料基本上可以理解為儲存各種資料類型的 變數
。在 資料驅動
這個概念出現之後,一部分變數也被賦予了特殊的意義。
首先是普通變量,和 JQ 時代沒差,只用來儲存資料。除此之外還有一類變量,它們有響應式的作用,這些變量與視圖綁定,當變量改變時,綁定了這些變量的視圖也會觸發對應的更新,這類變量我稱之為狀態變數。
所謂資料驅動視圖,嚴格說就是狀態變數在驅動視圖。隨著 Vue,React 的大力普及之下,前端開發們的工作重心逐漸從操作 DOM 轉移到了操作數據,狀態變數成為了核心。
狀態變量,現在大家似乎更願意稱之為狀態。我們常常詞不離口的狀態,狀態管理,其實這個狀態就是指狀態變數。下文提到的狀態同樣也是指狀態變數。
有了狀態之後,元件也來了。
JQ 時代的前端一個頁面就是一個 html,沒有「元件」的概念,對於頁面中的公共部分,想要優雅的實現復用簡直不要太難。所幸三大框架帶來了非常成熟的組件設計,可以輕鬆的抽取一個 DOM 片段作為組件,而且組件內部可以維護自己的狀態,獨立性更高。
元件的一個重要特性,就是內部的這些狀態是對外隔離的。父元件無法存取到子元件內部的狀態,但是子元件可以存取父元件顯示傳過來的狀態(Props),並且根據變更自動回應。
這個特性可以理解為狀態被模組化了。這樣的好處是,不需要考慮目前設定的狀態會影響到其他元件。當然了組件狀態徹底隔離也是不切實際的,必然會有多個組件共享狀態的需求,這種情況的方案就是將狀態提取到離這些組件最近的父組件,透過 Props 向下傳遞。
上述共享狀態的方案,在通常情況下是沒有問題的,也是一種官方建議的最佳實踐。
但是如果你的頁面複雜,你會發現還是有力不從心的地方。例如:
這種情況下繼續使用 “提取狀態到父元件” 的方法你會發現很複雜。而且隨著組件增多,嵌套層級加深,這個複雜度也越來越高。因為關聯的狀態多,傳遞複雜,很容易出現像某個組件莫名其妙的更新,某個組件死活不更新這樣的問題,異常排查也會困難重重。
有鑑於此,我們需要一個更優雅到方案,專門去處理這種複雜狀況下的狀態。
上一節我們說到,隨著頁面的複雜,我們在跨元件共享狀態的實作上遇到了棘手的問題。
那麼有沒有解決方案呢?當然有的,得益於社區大佬們的努力,方案不只一個。但這些方案都有一個共同的名字,就是我們在兩年前討論非常激烈的 ——— 狀態管理。
狀態管理,其實可以理解為全域狀態管理,這裡的狀態不同於元件內部的狀態,它是獨立於元件單獨維護的,然後再透過某種方式與需要該狀態的組件關聯起來。
狀態管理各有各的實現方案。 Vue 有 Vuex,React 有 Redux,Mobx,當然還有其他方案。但是它們解決的都是一個問題,就是跨元件狀態共享的問題。
我記得前兩年因為 「狀態管理」 這個概念的火熱,好像成了應用開發不可或缺的一部分。以 Vue 為例,建立一個專案必然會引入 Vuex 做狀態管理。但很多人不知道為什麼用,什麼時候用,怎麼用狀態管理,只是盲目跟風,於是後來出現了非常多濫用狀態管理的例子。
看到這裡,你應該知道狀態管理不是必須的。它為什麼會出現,以及它要解決什麼問題,上面基本上都說明白了。如果你還沒明白,請暫停,從開頭再讀一次。不要覺得一個技術方案誕生的背景不重要,如果你不明白它的出現是為了解決什麼問題,那麼你就無法真正發揮它的作用。
Redux 作者有一句名言:如果你不知道是否需要 Redux(狀態管理),那就是不需要它。
好了,如果你在用狀態管理,或需要使用狀態管理幫你解決問題,那我們繼續往下看。
Vue 在國內的應用非常廣泛,尤其是中小團隊,因此大多人接觸到的第一個狀態管理方案應該是 Vuex。
那麼 Vuex 是如何解決跨元件狀態共享的問題的呢?我們一起來探索一下。
我們上面說到,對於一般的元件共享狀態,官方建議「提取狀態到最近的父元件」。 Vuex 則是更高一步,將所有狀態提取到了根元件,這樣任何元件都能存取。
也許你會問:這樣做不是把狀態暴露到全域了嗎?不就徹底消除模組化的優勢了嗎?
其實不然。 Vuex 這麼做的主要目的是為了讓所有元件都可以存取這些狀態,徹底避免子元件狀態存取不了的情況。 Vuex 把所有狀態資料放在一個物件上,遵循單一資料來源的原則。但這並不代表狀態是堆砌的,Vuex 在這顆單一狀態樹上實現了自己的模組化方案。
別急,我們一步一步來,先看看如何使用 Vuex。
Vuex 是作為Vue 的插件存在的,首先npm 安裝:
$ npm install --save vuex
安裝之後,我們新建src/store
資料夾,在這裡放所有Vuex 相關的代碼。
新 index.js
並寫入以下程式碼。這段程式碼主要的作用就是用 Vue.use
方法載入 Vuex 這個插件,然後將配置好的 Vuex.Store
實例匯出。
import Vue from 'vue' import Vuex from 'vuex' // 安装插件 Vue.use(Vuex) export default new Vuex.Store({ state: {}, mutations: {}, actions: {}, modules: {} })
上面匯出的實例我們通常稱之為 store
。一個 store 包含了儲存的狀態(state
)和修改狀態的函數(mutation
)等,所有狀態和相關操作都在這裡定義。
最後一步,在入口檔案將上面匯出的 store 實例掛載到 Vue 上:
import store from './store' new Vue({ el: '#app', store: store })
注意:掛載這一步不是必須的。掛載這一步驟的功能只是為了方便在 .vue 元件中透過 this.$store
存取我們匯出的 store 實例。如果不掛載,直接導入使用也是一樣的。
上一步我們用建構子 Vuex.Store
建立了 store 實例,大家至少知道該怎麼用 Vuex 了。這一步我們來看看 Vuex.Store 建構函數的具體配置。
首先是 state
配置,他的值是一個對象,用來儲存狀態。 Vuex 使用 單一狀態樹
原則,將所有的狀態都放在這個物件上,以便於後續的狀態定位和除錯。
比如說我們有一個初始狀態app_version
表示版本,如下:
new Vuex.Store({ state: { app_version: '0.1.1' } }
現在要在元件中取得,可以這樣:
this.$store.state.app_version
但這不是唯一的取得方式,也可以這樣:
import store from '@/store' // @ 表示 src 目录 store.state.app_version
為什麼要強調這一點呢?因為很多小夥伴以為 Vuex 只能透過 this.$store
操作。到了非元件內,例如在請求函數中要設定某一個 Vuex 的狀態,就不知道該怎麼辦了。
事實上元件中取得狀態還有更優雅的方法,例如 mapState
函數,它讓取得多狀態變得更簡單。
import { mapState } from 'vuex' export default { computed: { ... // 其他计算属性 ...mapState({ version: state => state.app_version }) } }
Vuex 中的状态与组件中的状态不同,不能直接用 state.app_version='xx'
这种方式修改。Vuex 规定修改状态的唯一方法是提交 mutation
。
Mutation 是一个函数,第一个参数为 state,它的作用就是更改 state 的状态。
下面定义一个名叫 increment 的 mutation,在函数内更新 count 这个状态:
new Vuex.Store({ state: { count: 1 }, mutations: { increment(state, count) { // 变更状态 state.count += count } } })
然后在 .vue 组件中触发 increment:
this.$store.commit('increment', 2)
这样绑定了 count 的视图就会自动更新。
虽然 mutation 是更新状态的唯一方式,但实际上它还有一个限制:必须是同步更新。
为什么必须是同步更新?因为在开发过程中,我们常常会追踪状态的变化。常用的手段就是在浏览器控制台中调试。而在 mutation 中使用异步更新状态,虽然也会使状态正常更新,但是会导致开发者工具有时无法追踪到状态的变化,调试起来就会很困难。
再有 Vuex 给 mutation 的定位就是更改状态,只是更改状态,别的不要参与。所谓专人干专事儿,这样也帮助我们避免把更改状态和自己的业务逻辑混起来,同时也规范了函数功能。
那如果确实需要异步更新,该怎么办呢?
异步更新状态是一个非常常见的场景,比如接口请求回来的数据要存储,那就是异步更新。
Vuex 提供了 action
用于异步更新状态。与 mutation 不同的是,action 不直接更新状态,而是通过触发 mutation 间接更新状态。因此即便使用 action 也不违背 “修改状态的唯一方法是提交 mutation” 的原则。
Action 允许在实际更新状态前做一些副作用的操作,比如上面说的异步,还有数据处理,按条件提交不同的 mutation 等等。看一个例子:
new Vuex.Store({ state: { count: 1 }, mutations: { add(state) { state.count++ }, reduce(state) { state.count-- } }, actions: { increment(context, data) { axios.get('**').then(res => { if (data.iscan) { context.commit('add') } else { context.commit('reduce') } }) } } })
在组件中触发 action:
this.$store.dispatch('increment', { iscan: true })
这些就是 action 的使用方法。其实 action 最主要的作用就是请求接口,拿到需要的数据,然后触发 mutation 修改状态。
其实这一步在组件中也可以实现。我看过一些方案,常见的是在组件内写一个请求方法,当请求成功,直接通过 this.$store.commit
方法触发 mutation 来更新状态,完全用不到 action。
难道 action 可有可无吗?
也不是,在特定场景下确实需要 action 的,这个会在下一篇说。
前面讲过,Vuex 是单一状态树,所有状态存放在一个对象上。同时 Vuex 有自己的模块化方案
,可以避免状态堆砌到一起,变的臃肿。
Vuex 允许我们将 store 分割成模块(module),每个模块拥有自己的 state、mutation、action。虽然状态注册在根组件,但是支持模块分割,相当于做到了与页面组件平级的“状态组件”。
为了区分,我们将被分割的模块称为子模块,暴露在全局的称为全局模块。
我们来看基础用法:
new Vuex.Store({ modules: { user: { state: { uname: 'ruims' }, mutation: { setName(state, name) { state.name = name } } } } })
上面定义了 user
模块,包含了一个 state 和一个 mutation。在组件中使用方法如下:
// 访问状态 this.$store.state.user.uname // 更新状态 this.$store.commit('setName')
大家发现了,访问子模块的 state 要通过 this.$store.state.[模块名称]
这种方式去访问,触发 mutation 则与全局模块一样,没有区别。
action 与 mutation 原理一致,不细说。
上面说到,子模块触发 mutation 和 action 与全局模块一致,那么假设全局模块和子模块中都有一个名为 setName
的 mutation。在组件中触发,哪个 mutation 会执行呢?
经过试验,都会执行。官方的说法是:为了多个模块能够对同一 mutation 或 action 作出响应。
其实官方做的这个兼容,我一直没遇到实际的应用场景,反而因为同名 mutation 导致误触发带来了不少的麻烦。可能官方也意识到了这个问题,索引后来也为 mutation 和 action 做了模块处理方案。
这个方案,就是命名空间。
命名空间也很简单,在子模块中加一个 namespaced: true
的配置即可开启,如:
new Vuex.Store({ modules: { user: { namespaced: true, state: {} } } })
开启命名空间后,触发 mutation 就变成了:
this.$store.commit('user/setName')
可见提交参数由 '[mutation]'
变成了 '[模块名称]/[mutation]'
。
上面我们介绍了 Vuex 的模块化方案,将单一状态树 store 分割成多个 module,各自负责本模块状态的存储和更新。
模块化是必要的,但是这个模块的方案,用起来总觉得有点别扭。
比如,总体的设计是将 store 先分模块,模块下在包含 state,mutation,action。
那么按照正常理解,访问 user 模块下 state 应该是这样的:
this.$store.user.state.uname
但是实际 API 却是这样的:
this.$store.state.user.uname
这个 API 仿佛是在 state 中又各自分了模块。我没看过源码,但从使用体验上来说,这是别扭一。
除 state 外,mutation,action 默认注册在全局的设计,也很别扭。
首先,官方说的多个模块对同一 mutation 或 action 作出响应,这个功能暂无找到应用场景。并且未配 namespace 时还要保证命名唯一,否则会导致误触发。
其次,用 namespace 后,触发 mutation 是这样的:
this.$store.commit('user/setName')
这个明显是将参数单独处理了,为什么不是这样:
this.$store.user.commit('setName')
总体感受就是 Vuex 模块化做的还不够彻底。
上面说的槽点,并不是为了吐槽而吐槽。主要是感觉还有优化空间。
比如 this.$store.commit
函数可以触发任何 mutation 来更改状态。如果一个组件复杂,需要操作多个子模块的状态,那么就很难快速的找出当前组件操作了哪些子模块,当然也不好做权限规定。
我希望的是,比如在 A 组件要用到 b, c
两个子模块的状态,不允许操作其他子模块,那么就可以先将要用到模块导入,比如这样写:
import { a, b } from this.$store export default { methods: { test() { alert(a.state.uname) // 访问状态 a.commit('setName')// 修改状态 } } }
这样按照模块导入,查询和使用都比较清晰。
前面我们详细介绍了状态管理的背景以及 Vuex 的使用,分享了关于官方 API 的思考。相信看到这里,你已经对状态管理和 Vuex 有了更深刻的认识和理解。
然而本篇我们只介绍了 Vuex 这一个方案,状态管理的其他方案,以及上面我们的吐槽点,能不能找到更优的实现方法,这些都等着我们去尝试。
下一篇文章我们继续深挖状态管理,对比 Vuex 和 React,Fluter 在状态管理实现上的差异,然后在 Vue 上集成 Mobx,打造我们优雅的应用。
以上是以Vuex為例,揭開狀態管理的神秘面紗的詳細內容。更多資訊請關注PHP中文網其他相關文章!