vue.js - Vuex Action vs Mutations
(12)
En Vuex, ¿cuál es la lógica de tener tanto "acciones" como "mutaciones"?
Entiendo la lógica de los componentes que no pueden modificar el estado (lo que parece inteligente), pero tener ambas acciones y mutaciones parece que está escribiendo una función para activar otra función, para luego alterar el estado.
¿Cuál es la diferencia entre "acciones" y "mutaciones", cómo funcionan juntas, y más aún, tengo curiosidad por qué los desarrolladores de Vuex decidieron hacerlo de esta manera?
¡Porque no hay estado sin mutaciones! Cuando se compromete, se ejecuta una pieza de lógica que cambia el estado de manera previsible. Las mutaciones son la única forma de establecer o cambiar el estado (por lo que no hay cambios directos) y, además, deben ser sincrónicas. Esta solución maneja una funcionalidad muy importante: las mutaciones están iniciando sesión en devtools. ¡Y eso le proporciona una gran legibilidad y previsibilidad!
Una cosa más: acciones. Como se ha dicho, las acciones cometen mutaciones. Por lo tanto, no cambian la tienda, y no hay necesidad de que estos sean sincrónicos. ¡Pero pueden manejar una pieza adicional de lógica asincrónica!
1.De los docs :
Las acciones son similares a las mutaciones, las diferencias son que:
- En lugar de mutar el estado, las acciones cometen mutaciones.
- Las acciones pueden contener operaciones asincrónicas arbitrarias.
Las acciones pueden contener operaciones asincrónicas, pero la mutación no.
2. Invocamos la mutación, podemos cambiar el estado directamente. y también podemos en la acción de cambiar estados de esta manera:
actions: {
increment (store) {
// do whatever ... then change the state
store.dispatch(''MUTATION_NAME'')
}
}
las Acciones están diseñadas para manejar otras cosas más, podemos hacer muchas cosas allí (podemos usar operaciones asincrónicas) y luego cambiar el estado enviando la mutación allí.
Creo que la respuesta TLDR es que las mutaciones están destinadas a ser sincrónicas / transaccionales. Entonces, si necesita ejecutar una llamada Ajax, o hacer cualquier otro código asincrónico, debe hacerlo en una Acción, y luego cometer una mutación después, para establecer el nuevo estado.
Creo que tener una comprensión de las motivaciones detrás de las mutaciones y las acciones le permite a uno juzgar mejor cuándo usar qué y cómo. También libera al programador de la carga de la incertidumbre en situaciones donde las "reglas" se vuelven confusas. Después de razonar un poco sobre sus respectivos propósitos, llegué a la conclusión de que, aunque definitivamente puede haber formas incorrectas de usar Acciones y Mutaciones, no creo que haya un enfoque canónico.
Primero tratemos de entender por qué incluso pasamos por mutaciones o acciones.
¿Por qué pasar por la repetitiva en primer lugar? ¿Por qué no cambiar el estado directamente en los componentes?
Estrictamente hablando, podría cambiar el
state
directamente desde sus componentes.
El
state
es solo un objeto de JavaScript y no hay nada mágico que revierta los cambios que realice en él.
// Yes, you can!
this.$store.state[''products''].push(product)
Sin embargo, al hacer esto, está dispersando sus mutaciones de estado por todo el lugar. Pierde la capacidad de simplemente abrir un solo módulo que aloja el estado y de un vistazo ver qué tipo de operaciones se pueden aplicar a él. Tener mutaciones centralizadas resuelve esto, aunque a costa de algunas repeticiones.
// so we go from this
this.$store.state[''products''].push(product)
// to this
this.$store.commit(''addProduct'', {product})
...
// and in store
addProduct(state, {product}){
state.products.push(product)
}
...
Creo que si reemplaza algo corto con repetitivo, querrá que el repetitivo también sea pequeño. Por lo tanto, supongo que las mutaciones deben ser envoltorios muy delgados alrededor de las operaciones nativas en el estado, casi sin lógica comercial. En otras palabras, las mutaciones están destinadas a ser utilizadas principalmente como setters.
Ahora que ha centralizado sus mutaciones, tiene una mejor visión general de sus cambios de estado y dado que sus herramientas (vue-devtools) también conocen esa ubicación, facilita la depuración. También vale la pena tener en cuenta que muchos complementos de Vuex no miran el estado directamente para rastrear los cambios, sino que dependen de mutaciones para eso. Los cambios "fuera de límite" al estado son invisibles para ellos.
Entonces,
mutations
,actions
, ¿cuál es la diferencia de todos modos?
Las acciones, como las mutaciones, también residen en el módulo de la tienda y pueden recibir el objeto de
state
.
Lo que implica que también
podrían
mutarlo directamente.
Entonces, ¿qué sentido tiene tener ambos?
Si razonamos que las mutaciones tienen que mantenerse pequeñas y simples, implica que necesitamos un medio alternativo para albergar una lógica comercial más elaborada.
Las acciones son los medios para hacer esto.
Y como hemos establecido anteriormente, vue-devtools y los complementos son conscientes de los cambios a través de las mutaciones, para mantener la coherencia debemos seguir usando las mutaciones de nuestras acciones.
Además, dado que las acciones están destinadas a abarcar todo y que la lógica que encapsulan puede ser asíncrona, tiene sentido que las acciones también se vuelvan asincrónicas desde el principio.
A menudo se enfatiza que las acciones pueden ser asíncronas, mientras que las mutaciones generalmente no lo son. Puede decidir ver la distinción como una indicación de que las mutaciones deben usarse para cualquier cosa sincrónica (y acciones para cualquier cosa asincrónica); sin embargo, tendría algunas dificultades si, por ejemplo, necesitara cometer más de una mutación (sincrónicamente), o si necesitara trabajar con un Getter de sus mutaciones, ya que las funciones de mutación no reciben Getters ni Mutations como argumentos ...
... lo que lleva a una pregunta interesante.
¿Por qué las mutaciones no reciben Getters?
Todavía no he encontrado una respuesta satisfactoria a esta pregunta. He visto alguna explicación del equipo central de que encontré discutible en el mejor de los casos. Si resumo su uso, los Getters están destinados a ser extensiones calculadas (y a menudo en caché) del estado. En otras palabras, básicamente siguen siendo el estado, aunque eso requiere un cálculo inicial y normalmente son de solo lectura. Al menos así es como se les anima a ser utilizados.
Por lo tanto, evitar que las mutaciones accedan directamente a Getters significa que ahora es necesaria una de tres cosas, si necesitamos acceder desde la primera parte de la funcionalidad ofrecida por la última: (1) o bien los cálculos de estado proporcionados por Getter se duplican en algún lugar que sea accesible a la Mutación (mal olor), o (2) el valor calculado (o el Getter correspondiente) se transmite como un argumento explícito a la Mutación (funky), o (3) la lógica del Getter se duplica directamente dentro de la Mutación , sin el beneficio adicional de almacenamiento en caché como lo proporciona Getter (hedor).
El siguiente es un ejemplo de (2), que en la mayoría de los escenarios que he encontrado parece la opción "menos mala".
state:{
shoppingCart: {
products: []
}
},
getters:{
hasProduct(state){
return function(product) { ... }
}
}
actions: {
addProduct({state, getters, commit, dispatch}, {product}){
// all kinds of business logic goes here
// then pull out some computed state
const hasProduct = getters.hasProduct(product)
// and pass it to the mutation
commit(''addProduct'', {product, hasProduct})
}
}
mutations: {
addProduct(state, {product, hasProduct}){
if (hasProduct){
// mutate the state one way
} else {
// mutate the state another way
}
}
}
Para mí, lo anterior no solo parece un poco complicado, sino también algo "permeable", ya que parte del código presente en la Acción está claramente exudando de la lógica interna de la Mutación.
En mi opinión, esto es una indicación de un compromiso. Creo que permitir que Mutaciones reciba Getters automáticamente presenta algunos desafíos. Puede ser tanto para el diseño de Vuex en sí mismo, como para las herramientas (vue-devtools et al), o para mantener cierta compatibilidad con versiones anteriores, o alguna combinación de todas las posibilidades establecidas.
Lo que no creo es que pasar Getters a tus mutaciones tú mismo sea necesariamente una señal de que estás haciendo algo mal. Lo veo simplemente como "parchear" una de las deficiencias del marco.
De acuerdo con los
docs
Las acciones son similares a las mutaciones , las diferencias son que:
- En lugar de mutar el estado, las acciones cometen mutaciones.
- Las acciones pueden contener operaciones asincrónicas arbitrarias.
Considere el siguiente fragmento.
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
increment (state) {
state.count++ //Mutating the state. Must be synchronous
}
},
actions: {
increment (context) {
context.commit(''increment'') //Committing the mutations. Can be asynchronous.
}
}
})
Los controladores de acción ( incremento ) reciben un objeto de contexto que expone el mismo conjunto de métodos / propiedades en la instancia de la tienda, por lo que puede llamar a context.commit para confirmar una mutación o acceder al estado y a los captadores a través de context.state y context.getters
Descargo de responsabilidad: acabo de comenzar a usar vuejs, así que solo soy yo extrapolando la intención del diseño.
La depuración de la máquina del tiempo utiliza instantáneas del estado y muestra una línea de tiempo de acciones y mutaciones.
En teoría, podríamos haber tenido solo
actions
junto con una grabación de establecedores de estado y captadores para describir sincrónicamente la mutación.
Pero entonces:
-
Tendríamos entradas impuras (resultados asíncronos) que causaron los setters y getters.
Esto sería difícil de seguir lógicamente y los diferentes configuradores y captadores asíncronos pueden interactuar sorprendentemente.
Eso todavía puede suceder con las transacciones de
mutations
, pero luego podemos decir que la transacción debe mejorarse en lugar de ser una condición de carrera en las acciones. Las mutaciones anónimas dentro de una acción podrían resurgir más fácilmente este tipo de errores porque la programación asincrónica es frágil y difícil. - El registro de transacciones sería difícil de leer porque no habría nombre para los cambios de estado. Sería mucho más parecido a un código y menos inglés, sin las agrupaciones lógicas de mutaciones.
- Puede ser más complicado y menos eficaz para el instrumento registrar cualquier mutación en un objeto de datos, a diferencia de ahora, donde hay puntos de diferencia definidos sincrónicamente, antes y después de la llamada a la función de mutación. No estoy seguro de qué tan grande es el problema.
Compare el siguiente registro de transacciones con mutaciones con nombre.
Action: FetchNewsStories
Mutation: SetFetchingNewsStories
Action: FetchNewsStories [continuation]
Mutation: DoneFetchingNewsStories([...])
Con un registro de transacciones que no tiene mutaciones con nombre:
Action: FetchNewsStories
Mutation: state.isFetching = true;
Action: FetchNewsStories [continuation]
Mutation: state.isFetching = false;
Mutation: state.listOfStories = [...]
Espero que pueda extrapolar de ese ejemplo la posible complejidad añadida en la mutación asincrónica y anónima dentro de las acciones.
https://vuex.vuejs.org/en/mutations.html
Ahora imagine que estamos depurando la aplicación y mirando los registros de mutación de la herramienta de desarrollo. Para cada mutación registrada, el devtool deberá capturar instantáneas del estado "antes" y "después". Sin embargo, la devolución de llamada asincrónica dentro de la mutación de ejemplo anterior hace que sea imposible: la devolución de llamada aún no se llama cuando se confirma la mutación, y no hay forma de que la herramienta devtool sepa cuándo se llamará realmente la devolución de llamada: cualquier mutación de estado realizada en la devolución de llamada es esencialmente no rastreable!
Esto también me confundió, así que hice una demostración simple.
componente.vue
<template>
<div id="app">
<h6>Logging with Action vs Mutation</h6>
<p>{{count}}</p>
<p>
<button @click="mutateCountWithAsyncDelay()">Mutate Count directly with delay</button>
</p>
<p>
<button @click="updateCountViaAsyncAction()">Update Count via action, but with delay</button>
</p>
<p>Note that when the mutation handles the asynchronous action, the "log" in console is broken.</p>
<p>When mutations are separated to only update data while the action handles the asynchronous business
logic, the log works the log works</p>
</div>
</template>
<script>
export default {
name: ''app'',
methods: {
//WRONG
mutateCountWithAsyncDelay(){
this.$store.commit(''mutateCountWithAsyncDelay'');
},
//RIGHT
updateCountViaAsyncAction(){
this.$store.dispatch(''updateCountAsync'')
}
},
computed: {
count: function(){
return this.$store.state.count;
},
}
}
</script>
store.js
import ''es6-promise/auto''
import Vuex from ''vuex''
import Vue from ''vue'';
Vue.use(Vuex);
const myStore = new Vuex.Store({
state: {
count: 0,
},
mutations: {
//The WRONG way
mutateCountWithAsyncDelay (state) {
var log1;
var log2;
//Capture Before Value
log1 = state.count;
//Simulate delay from a fetch or something
setTimeout(() => {
state.count++
}, 1000);
//Capture After Value
log2 = state.count;
//Async in mutation screws up the log
console.log(`Starting Count: ${log1}`); //NRHG
console.log(`Ending Count: ${log2}`); //NRHG
},
//The RIGHT way
mutateCount (state) {
var log1;
var log2;
//Capture Before Value
log1 = state.count;
//Mutation does nothing but update data
state.count++;
//Capture After Value
log2 = state.count;
//Changes logged correctly
console.log(`Starting Count: ${log1}`); //NRHG
console.log(`Ending Count: ${log2}`); //NRHG
}
},
actions: {
//This action performs its async work then commits the RIGHT mutation
updateCountAsync(context){
setTimeout(() => {
context.commit(''mutateCount'');
}, 1000);
}
},
});
export default myStore;
Después de investigar esto, la conclusión a la que llegué es que las mutaciones son una convención centrada solo en cambiar los datos para separar mejor las preocupaciones y mejorar el registro antes y después de los datos actualizados. Mientras que las acciones son una capa de abstracción que maneja la lógica de nivel superior y luego llama a las mutaciones adecuadamente
Las mutaciones son sincrónicas, mientras que las acciones pueden ser asincrónicas.
Para decirlo de otra manera: no necesita acciones si sus operaciones son sincrónicas, de lo contrario, impleméntelas.
Las principales diferencias entre acciones y mutaciones:
- Dentro de las acciones puede ejecutar código asincrónico pero no en mutaciones. Por lo tanto, use acciones para el código asincrónico; de lo contrario, use mutaciones.
- Dentro de las acciones puedes acceder a getters, estados, mutaciones (cometerlos), acciones (despacharlas) en mutaciones puedes acceder al estado. Entonces, si desea acceder solo al estado, use mutaciones; de lo contrario, use acciones.
Puede parecer innecesario tener una capa adicional de
actions
solo para llamar a las
mutations
, por ejemplo:
const actions = {
logout: ({ commit }) => {
commit("setToken", null);
}
};
const mutations = {
setToken: (state, token) => {
state.token = token;
}
};
Entonces, si llamar a
actions
llama a
logout
, ¿por qué no llamar a la mutación en sí?
La idea completa de una acción es llamar a múltiples mutaciones desde dentro de una acción o hacer una solicitud de Ajax o cualquier tipo de lógica asincrónica que puedas imaginar.
Eventualmente podríamos tener acciones que hagan múltiples solicitudes de red y eventualmente llamen a muchas mutaciones diferentes.
Por lo tanto, tratamos de rellenar tanta complejidad de nuestro
Vuex.Store()
como sea posible en nuestras
actions
y esto deja nuestras
mutations
,
state
y
getters
más limpios y directos y se alinea con el tipo de modularidad que hace que bibliotecas como Vue y React sean populares.
Mutaciones:
Can update the state. (Having the Authorization to change the state).
Comportamiento:
Actions are used to tell "which mutation should be triggered"
En forma de Redux
Mutations are Reducers Actions are Actions
¿Por qué ambos?
Cuando la aplicación crezca, la codificación y las líneas aumentarán, esa vez debe manejar la lógica en las acciones, no en las mutaciones porque las mutaciones son la única autoridad para cambiar el estado, debe estar lo más limpio posible.
Pregunta 1 : ¿Por qué los desarrolladores de Vuejs decidieron hacerlo de esta manera?
Responder:
- Cuando su aplicación se vuelve grande, y cuando hay múltiples desarrolladores trabajando en este proyecto, encontrará que la "gestión de estado" (especialmente el "estado global") se volverá cada vez más complicada.
- La forma vuex (al igual que Redux en react.js ) ofrece un nuevo mecanismo para administrar el estado, mantener el estado y "guardar y rastrear" (eso significa que cada acción que modifica el estado puede ser rastreada por la herramienta de depuración: vue-devtools )
Pregunta 2 : ¿Cuál es la diferencia entre "acción" y "mutación"?
Veamos primero la explicación oficial:
Mutaciones:
Las mutaciones de Vuex son esencialmente eventos: cada mutación tiene un nombre y un controlador.
import Vuex from ''vuex'' const store = new Vuex.Store({ state: { count: 1 }, mutations: { INCREMENT (state) { // mutate state state.count++ } } })
Acciones: las acciones son solo funciones que despachan mutaciones.
// the simplest action function increment (store) { store.dispatch(''INCREMENT'') } // a action with additional arguments // with ES2015 argument destructuring function incrementBy ({ dispatch }, amount) { dispatch(''INCREMENT'', amount) }
Aquí está mi explicación de lo anterior:
- la mutación es la única forma de modificar el estado
- la mutación no se preocupa por la lógica de negocios, solo se preocupa por el "estado"
- la acción es lógica de negocios
- La acción puede enviar más de 1 mutación a la vez, solo implementa la lógica de negocios, no le importa el cambio de datos (que se administra por mutación)