目錄
大綱預覽
#狀態與元件的誕生
需要狀態管理嗎?
Vuex
建立 store
單一資料來源(state)
状态更新方式(mutation)
同步更新
异步更新
状态模块化(module)
命名空间
模块化的槽点
为什么吐槽
下一步
首頁 web前端 Vue.js 以Vuex為例,揭開狀態管理的神秘面紗

以Vuex為例,揭開狀態管理的神秘面紗

Dec 16, 2021 pm 03:46 PM
vue.js vuex 前端

#以Vuex 為引,一窺狀態管理全貌

眾所周知,Vuex 是Vue 官方的狀態管理方案。

Vuex 的用法和 API 不難,官網介紹也簡潔明了。得益於此,將 Vuex 快速整合到專案中非常容易。然而正因為用法靈活,許多同學在 Vuex 的設計和使用上反而有些混亂。

其實在使用前,我們不妨暫停一下,思考幾個問題:

  • 什麼是狀態管理?
  • 我為什麼要用 Vuex?
  • 元件內部狀態和 Vuex 狀態如何分配?
  • 使用 Vuex 會有哪些潛在問題?

如果你對這些問題模稜兩可,那麼恭喜你,這篇文章可能是你需要的。

下面請和我一起,從起源開始,以 Vuex 為例,共同揭開狀態管理的神秘面紗。

大綱預覽

本文介紹的內容包含以下面向:

  • 狀態與元件的誕生
  • 需要狀態管理嗎?
  • 單一資料來源
  • 狀態更新方式
  • #非同步更新?
  • 狀態模組化
  • 模組化的槽點
  • 下一步

#狀態與元件的誕生

自三大框架誕生起,它們共有的兩個能力徹底暴擊了Jquery。這兩個能力分別是:

  1. 資料驅動視圖
  2. #元件化

資料驅動視圖,使我們告別了只能依靠操作DOM 更新頁面的時代。我們不再需要每次更新頁面時,透過層層 find 找到 DOM 然後修改它的屬性和內容,可以透過操作資料來實現這些事情。

當然了在我們前端的眼裡,資料基本上可以理解為儲存各種資料類型的 變數。在 資料驅動 這個概念出現之後,一部分變數也被賦予了特殊的意義。

首先是普通變量,和 JQ 時代沒差,只用來儲存資料。除此之外還有一類變量,它們有響應式的作用,這些變量與視圖綁定,當變量改變時,綁定了這些變量的視圖也會觸發對應的更新,這類變量我稱之為狀態變數

所謂資料驅動視圖,嚴格說就是狀態變數在驅動視圖。隨著 Vue,React 的大力普及之下,前端開發們的工作重心逐漸從操作 DOM 轉移到了操作數據,狀態變數成為了核心。

狀態變量,現在大家似乎更願意稱之為狀態。我們常常詞不離口的狀態,狀態管理,其實這個狀態就是指狀態變數。下文提到的狀態同樣也是指狀態變數。

有了狀態之後,元件也來了。

JQ 時代的前端一個頁面就是一個 html,沒有「元件」的概念,對於頁面中的公共部分,想要優雅的實現復用簡直不要太難。所幸三大框架帶來了非常成熟的組件設計,可以輕鬆的抽取一個 DOM 片段作為組件,而且組件內部可以維護自己的狀態,獨立性更高。

元件的一個重要特性,就是內部的這些狀態是對外隔離的。父元件無法存取到子元件內部的狀態,但是子元件可以存取父元件顯示傳過來的狀態(Props),並且根據變更自動回應。

這個特性可以理解為狀態被模組化了。這樣的好處是,不需要考慮目前設定的狀態會影響到其他元件。當然了組件狀態徹底隔離也是不切實際的,必然會有多個組件共享狀態的需求,這種情況的方案就是將狀態提取到離這些組件最近的父組件,透過 Props 向下傳遞。

上述共享狀態的方案,在通常情況下是沒有問題的,也是一種官方建議的最佳實踐。

但是如果你的頁面複雜,你會發現還是有力不從心的地方。例如:

  • 元件層級太深,需要共享狀態,此時狀態要層層傳遞。
  • 子元件更新一個狀態,可能有多個父元件,兄弟元件共用,實現困難。

這種情況下繼續使用 “提取狀態到父元件” 的方法你會發現很複雜。而且隨著組件增多,嵌套層級加深,這個複雜度也越來越高。因為關聯的狀態多,傳遞複雜,很容易出現像某個組件莫名其妙的更新,某個組件死活不更新這樣的問題,異常排查也會困難重重。

有鑑於此,我們需要一個更優雅到方案,專門去處理這種複雜狀況下的狀態。

需要狀態管理嗎?

上一節我們說到,隨著頁面的複雜,我們在跨元件共享狀態的實作上遇到了棘手的問題。

那麼有沒有解決方案呢?當然有的,得益於社區大佬們的努力,方案不只一個。但這些方案都有一個共同的名字,就是我們在兩年前討論非常激烈的 ——— 狀態管理

狀態管理,其實可以理解為全域狀態管理,這裡的狀態不同於元件內部的狀態,它是獨立於元件單獨維護的,然後再透過某種方式與需要該狀態的組件關聯起來。

狀態管理各有各的實現方案。 Vue 有 Vuex,React 有 Redux,Mobx,當然還有其他方案。但是它們解決的都是一個問題,就是跨元件狀態共享的問題

我記得前兩年因為 「狀態管理」 這個概念的火熱,好像成了應用開發不可或缺的一部分。以 Vue 為例,建立一個專案必然會引入 Vuex 做狀態管理。但很多人不知道為什麼用,什麼時候用,怎麼用狀態管理,只是盲目跟風,於是後來出現了非常多濫用狀態管理的例子。

看到這裡,你應該知道狀態管理不是必須的。它為什麼會出現,以及它要解決什麼問題,上面基本上都說明白了。如果你還沒明白,請暫停,從開頭再讀一次。不要覺得一個技術方案誕生的背景不重要,如果你不明白它的出現是為了解決什麼問題,那麼你就無法真正發揮它的作用。

Redux 作者有一句名言:如果你不知道是否需要 Redux(狀態管理),那就是不需要它

好了,如果你在用狀態管理,或需要使用狀態管理幫你解決問題,那我們繼續往下看。

Vuex

Vue 在國內的應用非常廣泛,尤其是中小團隊,因此大多人接觸到的第一個狀態管理方案應該是 Vuex。

那麼 Vuex 是如何解決跨元件狀態共享的問題的呢?我們一起來探索一下。

建立 store

我們上面說到,對於一般的元件共享狀態,官方建議「提取狀態到最近的父元件」。 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 實例。如果不掛載,直接導入使用也是一樣的。

單一資料來源(state)

上一步我們用建構子 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
    })
  }
}
登入後複製

状态更新方式(mutation)

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 的,这个会在下一篇说。

状态模块化(module)

前面讲过,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中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

PHP與Vue:完美搭檔的前端開發利器 PHP與Vue:完美搭檔的前端開發利器 Mar 16, 2024 pm 12:09 PM

PHP與Vue:完美搭檔的前端開發利器在當今網路快速發展的時代,前端開發變得愈發重要。隨著使用者對網站和應用的體驗要求越來越高,前端開發人員需要使用更有效率和靈活的工具來創建響應式和互動式的介面。 PHP和Vue.js作為前端開發領域的兩個重要技術,搭配起來可以稱得上是完美的利器。本文將探討PHP和Vue的結合,以及詳細的程式碼範例,幫助讀者更好地理解和應用這兩

C#開發經驗分享:前端與後端協同開發技巧 C#開發經驗分享:前端與後端協同開發技巧 Nov 23, 2023 am 10:13 AM

身為C#開發者,我們的開發工作通常包括前端和後端的開發,而隨著技術的發展和專案的複雜性提高,前端與後端協同開發也變得越來越重要和複雜。本文將分享一些前端與後端協同開發的技巧,以幫助C#開發者更有效率地完成開發工作。確定好介面規範前後端的協同開發離不開API介面的交互。要確保前後端協同開發順利進行,最重要的是定義好介面規格。接口規範涉及到接口的命

Django是前端還是後端?一探究竟! Django是前端還是後端?一探究竟! Jan 19, 2024 am 08:37 AM

Django是一個由Python編寫的web應用框架,它強調快速開發和乾淨方法。儘管Django是web框架,但要回答Django是前端還是後端這個問題,需要深入理解前後端的概念。前端是指使用者直接和互動的介面,後端是指伺服器端的程序,他們透過HTTP協定進行資料的互動。在前端和後端分離的情況下,前後端程式可以獨立開發,分別實現業務邏輯和互動效果,資料的交

前端面試官常問的問題 前端面試官常問的問題 Mar 19, 2024 pm 02:24 PM

在前端開發面試中,常見問題涵蓋廣泛,包括HTML/CSS基礎、JavaScript基礎、框架和函式庫、專案經驗、演算法和資料結構、效能最佳化、跨域請求、前端工程化、設計模式以及新技術和趨勢。面試官的問題旨在評估候選人的技術技能、專案經驗以及對行業趨勢的理解。因此,應試者應充分準備這些方面,以展現自己的能力和專業知識。

前端怎麼實現即時通訊 前端怎麼實現即時通訊 Oct 09, 2023 pm 02:47 PM

實作即時通訊的方法有WebSocket、Long Polling、Server-Sent Events、WebRTC等等。詳細介紹:1、WebSocket,它可以在客戶端和伺服器之間建立持久連接,實現即時的雙向通信,前端可以使用WebSocket API來創建WebSocket連接,並透過發送和接收訊息來實現即時通訊;2、Long Polling,是一種模擬即時通訊的技術等等

Go語言前端技術探秘:前端開發新視野 Go語言前端技術探秘:前端開發新視野 Mar 28, 2024 pm 01:06 PM

Go語言作為一種快速、高效的程式語言,在後端開發領域廣受歡迎。然而,很少有人將Go語言與前端開發聯繫起來。事實上,使用Go語言進行前端開發不僅可以提高效率,還能為開發者帶來全新的視野。本文將探討使用Go語言進行前端開發的可能性,並提供具體的程式碼範例,幫助讀者更了解這一領域。在傳統的前端開發中,通常會使用JavaScript、HTML和CSS來建立使用者介面

Django:前端和後端開發都能搞定的神奇框架! Django:前端和後端開發都能搞定的神奇框架! Jan 19, 2024 am 08:52 AM

Django:前端和後端開發都能搞定的神奇框架! Django是一個高效、可擴展的網路應用程式框架。它能夠支援多種Web開發模式,包括MVC和MTV,可以輕鬆地開發出高品質的Web應用程式。 Django不僅支援後端開發,還能夠快速建構出前端的介面,透過模板語言,實現靈活的視圖展示。 Django把前端開發和後端開發融合成了一種無縫的整合,讓開發人員不必專門學習

Golang與前端技術結合:探討Golang如何在前端領域發揮作用 Golang與前端技術結合:探討Golang如何在前端領域發揮作用 Mar 19, 2024 pm 06:15 PM

Golang與前端技術結合:探討Golang如何在前端領域發揮作用,需要具體程式碼範例隨著互聯網和行動應用的快速發展,前端技術也愈發重要。而在這個領域中,Golang作為一門強大的後端程式語言,也可以發揮重要作用。本文將探討Golang如何與前端技術結合,以及透過具體的程式碼範例來展示其在前端領域的潛力。 Golang在前端領域的角色作為一門高效、簡潔且易於學習的

See all articles