[译] VUE 和 VUEX 中的数据流

看起来在 Vue 里面困扰开发者的事情之一是如何在组件之间共享状态。对于刚刚接触响应式编程的开发者来说,像Vuex 这种库,有着繁多的新名词及其关注点分离的方式,往往令人望而生畏。特别是当你只希望分享一两个数据片段时,(这一套逻辑的复杂性)就显得有点过分了。

考虑到这一点的话,我想我应该把两个简短的演示放到一起展示出来。第一个通过使用一个简单的 JavaScript 对象,在每个新组件当中引用来实现共享状态。第二个做了和 Vuex 一样的事情,当它运行成功的时候,也是一个你绝对不应该做的事情的示例(我们将在最后看看为什么)。

你可以通过查看下面这些演示来开始:

或者获取这个仓库并在本地运行试试看!代码里很多地方是2.0版本的特性,但我接下来想讲的数据流概念在任何版本里都是相关的,并且它可以通过一些改变很轻易地向下兼容到1.0。

这些演示都是一样的功能,只是实现的方法不同。应用程序由两个独立的聊天组件实例组成。当用户在一个实例里提交一个消息的时候,它应该在两个聊天窗口都出现,因为消息状态是共享的,下面是一个截图:

用一个对象共享状态

开始前,让我们先来看看数据是如何在示例的应用程序当中流转的。

在这个演示里,我们将使用一个简单的 JavaScript 对象:var store = {...},在Client.vue组件的实例之间共享状态。下面是关键文件的重要代码部分:

index.html
<div id="app"></div>
<script>
  var store = {
    state: {
      messages: []
    },
    newMessage (msg) {
      this.state.messages.push(msg)
    }
  }
</script>

这里有两个关键的地方:

  1. 我们通过把这个对象直接添加到index.html里来让其对整个应用程序可用,也可以将它注入到应用程序里更下一层的作用链,但目前直接添加显然更快捷简单。
  2. 我们在这里保存状态,但同时也提供了一个函数来调用它。相比起分散在组件各处的函数,我们更倾向于让它们保持在一个地方(便于维护),并在任何需要它们的地方简单使用。
App.vue
<template>
  <div id="app">
    <div class="row">
      <div class="col">
        <client clientid="Client A"></client>
      </div>
      <div class="col">
        <client clientid="Client B"></client>
      </div>
    </div>
  </div>
</template>

<script>
import Client from './components/Client.vue'

export default {
  components: {
    Client
  }
}
</script>

这里我们引入了 Client 组件,并创建了两个它的实例,使用一个属性:clientid,来对每个实例进行区分。事实上,你应该更动态地去实现这些,但别忘了,目前快捷简单更重要。

注意一点,到这里我们还完全没有同步任何状态。

Client.vue
<template>
  <div>
    <h1>{{ clientid }}</h1>
    <div class="client">
      <ul>
        <li v-for="message in messages">
          <label>{{ message.sender }}:</label> {{ message.text }}
        </li>
      </ul>
      <div class="msgbox">
        <input v-model="msg" placeholder="Enter a message, then hit [enter]" @keyup.enter="trySendMessage">
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      msg: '',
      messages: store.state.messages
    }
  },
  props: ['clientid'],
  methods: {
    trySendMessage() {
      store.newMessage({
        text: this.msg,
        sender: this.clientid
      })
      this.resetMessage()
    },
    resetMessage() {
      this.msg = ''
    }
  }
}
</script>

下面是应用程序的主要内容:

  1. 在该模板里,设置一个v-for循环去遍历messages集合。
  2. 绑定在文本输入框上的v-model简单地存储了组件的本地数据对象msg
  3. 同样在数据对象里,我们创建了一个store.state.messages的引用,它将触发组件的更新。
  4. 最后,将 enter 键绑定到trySendMessage函数,这个函数包含了以下几个功能:
    1. 准备好需要存储的数据(发送者和消息的一个字典对象)。
    2. 调用定义在共享存储里的newMessage函数。
    3. 调用一个清理函数:resetMessage,重置输入框。通常你更应该在一个promise完成之后再调用它。

这就是使用对象的方法,来试一试

用 Vuex 共享状态

好了,现在来试试看用 Vuex 实现。同样的,先上图,也便于我们将 Vuex 的术语(actions,mutations等等)对应到我们刚刚完成的示例中。

正如你所看到的,Vuex 简单地形式化了我们刚刚完成的过程。使用它的时候,所做的事情其实和我们上面做过的非常像:

  1. 创建一个用来共享的存储,在这个例子中它将通过 vue/vuex 注入到组件当中。
  2. 定义组件可以调用的 actions,它们仍然是集中定义的。
  3. 定义实际接触存储状态的 mutations。我们这么做,actions 就可以形成不止一个 mutation,或者执行逻辑去决定调用哪一个 mutation。这意味着你再也不用担心组件当中的业务逻辑了,成功!
  4. 当状态更新时,任何拥有 getter,动态属性和映射到 store 的组件都会被立即更新。

同样再来看看代码:

main.js
import store from './vuex/store'

new Vue({ // eslint-disable-line no-new
  el: '#app',
  render: (h) => h(App),
  store: store
})

这次,我们用 Vuex 创建了一个存储并将其直接传入应用程序当中,替代掉了之前index.html中的 store 对象。在继续之前,先来看一下这个存储:

store.js
export default new Vuex.Store({

  state: {
    messages: []
  },

  actions: {
    newMessage ({commit}, msg) {
      commit('NEW_MESSAGE', msg)
    }
  },

  mutations: {
    NEW_MESSAGE (state, msg) {
      state.messages.push(msg)
    }
  },

  strict: debug

})

和我们自己创建的对象非常相似,但是多了一个mutations对象。

Client.vue
<div class="row">
  <div class="col">
    <client clientid="Client A"></client>
  </div>
  <div class="col">
    <client clientid="Client B"></client>
  </div>
</div>

和上次一样的配方。(惊人的相似,对吧?)

Client.vue
<script>
import { mapState, mapActions } from 'vuex'

export default {
  data() {
    return {
      msg: ''
    }
  },
  props: ['clientid'],
  computed: {
    ...mapState({
      messages: state => state.messages
    })
  },
  methods: {
    trySendMessage() {
      this.newMessage({
        text: this.msg,
        sender: this.clientid
      })
      this.resetMessage()
    },
    resetMessage() {
      this.msg = ''
    },
    ...mapActions(['newMessage'])
  }
}
</script>

模板仍然刚好一样,所以我甚至不需要费心怎么去引入它。最大的不同在于:

  1. 使用mapState来生成对共享消息集合的引用。
  2. 使用mapActions来生成创建一个新消息的动作(action)。

(注意:这些都是 Vuex 2.0特性。)

好的,做完啦!也来看一下这个演示吧。

结论

所以,正如你所希望看到的,自己进行简单的状态共享和使用 Vuex 进行共享并没有多大区别。而 Vuex 最大的优点在于它为你形式化了集中处理数据存储的过程,并提供了所有功能方法去处理那些数据。

最初,当你阅读 Vuex 的文档和示例的时候,它那些针对 mutations,actions 和 modules 的单独文档很容易让人感觉困扰。但是如果你敢于跨出那一步,简单地在store.js文件里写一些关于它们的代码来开始学习。随着这个文件的大小增加,你就将找到正确的时间移步到actions.js里,或者是把它们更进一步地分离开来。

不要着急,慢慢来,一步一个台阶。当然也可以使用vue-cli从创建一个模板开始,我使用browserify模板,并把下面的代码添加进我的package.json文件。

"dependencies": {
    "vue": "^2.0.0-rc.6",
    "vuex": "^2.0.0-rc.5"
}

还在看吗?

我知道我还说过要再讲一个“不好的”方式。再次,这个演示恰好也是一样的。不好的地方在于我利用了 Vue 2.0 里单向绑定的特性来注入回调函数,从而允许了父子模板之间顺序的双向绑定。首先,来看一下2.0文档中的这个部分,然后再来看看我这个不好的方法。

App.vue
<div class="row">
  <div class="col">
    <client clientid="Client A" :messages="messages" :callback="newMessage"></client>
  </div>
  <div class="col">
    <client clientid="Client B" :messages="messages" :callback="newMessage"></client>
  </div>
</div>

这里,我在组件上使用了一个属性将一个动态绑定传递到messages集合里。但是,我同时还传递了一个动作函数,所以可以在子组件里调用它。

Client.vue
<script>
export default {
  data() {
    return {
      msg: ''
    }
  },
  props: ['clientid', 'messages', 'callback'],
  methods: {
    trySendMessage() {
      this.callback({
        text: this.msg,
        sender: this.clientid
      })
      this.resetMessage()
    },
    resetMessage() {
      this.msg = ''
    }
  }
}
</script>

这里就是不好的做法。

要问为什么有这么不好吗?

  1. 我们正在破坏之前图中所展示的单向循环。
  2. 我们创建了一个在组件及其父组件之间的紧密耦合。
  3. 这将变得不可维护。如果你在组件里需要20个函数,你就将添加20个属性,管理它们的命名等等,然后,如果任何东西发生改变,呃!

所以为什么还要再展示这段?因为我和其他人一样很懒。有时我就会做这样的事情,仅仅想知道再继续做下去会有多么糟糕,然后我就会咒骂自己的懒惰,因为我可能要花上一小时或者一天的时间去清理它们。鉴于这种情况,我希望我可以帮助你尽早避免无谓的决定和错误,千万不要传递任何你不需要的东西。99%的情况下,一个单独的共享状态已经足够完美。(不久再详细讲讲那1%的情况)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • Vuex是什么? Vuex 是一个专为 Vue.js应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件...
    萧玄辞阅读 3,102评论 0 6
  • “春风又吹绿了花蕾,你已经也添了新岁。” 那个夏天,我带着柠檬味的羞涩和蜜桃味的期待踏进高一四班,遇到了正洋溢着青...
    百里流笙阅读 264评论 0 2
  • 昨夜熄了灯正准备入睡,一串蛙鸣清清楚楚地从远处传来,透过玻璃,来到我的耳畔。间隔一会儿再次响起,似乎不敢那么起劲,...
    滋小然阅读 707评论 34 34
  • 活动结束后,活动策划人员以为就没什么事情,终于可以休息了。事实上,还有一些收尾工作需要对活动继续跟进!这里列举一个...
    宁小南阅读 17,179评论 0 1
  • 想念是一种时时刻刻都想拥你入怀的感觉,呼吸时也会想,吃饭时也会想,以为闭上眼睛就好了,才发现脑海里都是你。那么,我...
    阳光杂货铺阅读 353评论 0 0