vue原理学习系列(四):状态管理

462 阅读4分钟

前言

vue原理学习系列,本期为主要学习下vue的state-management(状态管理)的方式,重点在实例共享这一方面,实例共享是vuex实现的核心。

vue原理学习系列往期文章:

「内容速览」

  • state-management(状态管理)介绍
  • Props传递
  • 对象共享
  • 实例共享
  • mutations
  • 函数式编程

下面的内容是基于vue2来进行展开的

state-management(状态管理)介绍

什么是状态?

需要记住的“事物/信息”称为“状态”。

状态是程序用来完成某些任务的信息,它在是程序的整个运行过程中被更改或操纵的数据或信息。程序在给定时间的“状态”是指程序当前正在查看或分析以执行下一步的数据。

什么是状态管理?

Vue、React、Angular最大的优势之一是状态已经透明连接到视图中,但这些带来了问题就是状态很容易传播到任何地方,并在任何地方进行修改。状态零散地分布在许多组件和组件之间的交互中,随着我们应用的增长和复杂化,跟踪存储状态的位置和状态改变的位置就变得愈发困难,应用会变得越来越不可测。

简单来说,前端状态管理是一种解决代码如何存放和状态如何集中管理和分发问题的解决方法,其中Flux就是第一个关于单向数据流的思想,它规范了数据在Web应用中的流动方式,引入了前端状态管理的概念。

下面3小节为梳理下vue中数据共享方式

Props传递

当我们有两个子组件需要共享一个状态时,将共享状态提取到父组件中,通过props传递给子组件。

// 例如我们想要三个counter组件共享count这个数据
<div id="app">
  <counter :count="count"></counter>
  <counter :count="count"></counter>
  <counter :count="count"></counter>
  <button @click="count++">increment</button>
</div>

<script>
new Vue({
  el: '#app',
  data: {
    count: 0
  },
  components: {
    Counter: {
      props: {
        count: Number 
      },
      template: `<div>{{ count }}</div>`
    }
  }
})
</script>

对象共享

data通过共享对象来保存我们的公共状态。

在vue中data一般情况下必须是一个function,原因是大多数时候我们希望每个组件实例都有自己独立的数据,而不是所有同类组件共享相同的数据。

<div id="app">
  <counter></counter>
  <counter></counter>
  <counter></counter>
  <button @click="inc">increment</button>
</div>

<script>
// state是共享状态
const state = {
  count: 0
}
const Counter = {
  data() {
    // 返回共享状态
    return state;
  },
  render(h) {
    return h('div', this.count);
  }
}
new Vue({
  el: '#app',
  components: {
    Counter
  },
  methods: {
    inc() {
      state.count++;
    }
  }
})
</script>

实例共享

利用Vue实例作为响应性数据存储或模型,在vue实例中进行数据的变更,vue实例状态对象本身默认是响应性的,因此我们可以在其他组件中直接引用,而不需要在data中注入。

实例共享的好处在于我们在集中管理状态的同时隐藏了状态变更的细节,将状态变更细节同样集中放在了实例当中。这种基于中心模型的状态管理和Vuex非常接近,Vuex核心是基于vue实例来实现响应性的

<div id="app">
  <counter></counter>
  <counter></counter>
  <counter></counter>
  <button @click="inc">increment</button>
</div>

<script>
// Vue共享实例
const state = new Vue({
  data: {
    count: 0
  },
  methods: {
    inc() {
      this.count++;
    }
  }
})

const Counter = {
  render: h => h('div', state.count)
}
new Vue({
  el: '#app',
  components: {
    Counter
  },
  methods: {
    inc() {
      state.inc();
    }
  }
})
</script>

mutations

在上面实例共享中,我们的代码有这么一段:

const state = new Vue({
    ...
    methods: {
      inc() {
        this.count++;
      }
    }
  })

Vuex中关于函数的概念成为mutations和actions,mutations和actions的区别在于,mutations是用来改变state的状态,它必须是同步的,devtools需要捕捉到前一状态和后一状态的快照(比较前后差异),而异步函数状态改变是不可追踪的。

而actions可以包含任意的异步操作,但需要改变状态时提交mutation,而不是直接变更状态。

actions和mutations本质上是为了把异步代码和状态更改代码分开。

// 实现目标:
// 创建createStore函数,这个函数模拟一个简单版本的vuex
<div id="app">
  <counter></counter>
  <counter></counter>
  <counter></counter>
  <button @click="inc">increment</button>
</div>

<script>
function createStore({ state, mutations }) {
  return new Vue({
    data: {
      state
    },
    methods: {
      // 通过commit来调用mutations的方法
      commit(mutation) {
        if(!mutations.hasOwnProperty(mutation)) {
          throw new Error('Unknown mutation');
        }
        mutations[mutation](this.state);
      }
    }
  })
}

const store = createStore({
  state: { count: 0 },
  mutations: {
    inc (state) {
      state.count++;
    }
  }
})

const Counter = {
  render: h => h('div', store.state.count)
}

new Vue({
  el: '#app',
  components: {
    Counter
  },
  methods: {
    inc() {
      store.commit('inc');
    }
  }
})
</script>

函数式编程

函数式编程是一种编程范式

关于javascript的函数式编程,有很多的知识和概念,JavaScript函数式编程这篇文章说的很详细。

其中我们需要重点了解下纯函数: 纯函数的定义是,对于相同的输入,永远会得到相同的输出,没有任何可观察的副作用,也不依赖外部环境的状态。

也就是说纯函数定义有三个特点:

  • 确定性:对于相同的输入,永远会得到相同的输出
  • 无副作用:不引起任何可观察的副作用,可见副作用的例子包括修改全局对象或通过引用传递的参数。
  • 不依赖外部环境的状态

了解React和Redux的同学就会知道,Redux的前提:应用状态是不可变的,相较于Vuex接受不同mutations,Redux采用了actionType。

Redux的核心是Reducer,reducer接受状态和执行操作类型,执行相应的操作,然后返回一个全新的state副本。

// 例如利用reducer来处理加减
const reducer = (prevState, actionType) => {
  switch(actionType) {
    case 'inc':
      return { count: prevState.count + 1; }
    case 'del':
      return { count: prevState.count - 1; }
    default:
      return prevState;
  }
}
// 实现目标:
// 创建函数app,接受el,model,view,actions,实现加减改按钮变视图
<div id="app"></div>

<script>
function app({ el, model, view, actions }) {
  // actions执行时需要传入model参数,因此我们需要包装下actions,传入vue实例model
  const wrappedActions = {}
  Object.keys(actions).forEach(key => {
    const originalAction = actions[key];
    wrappedActions[key] = () => {
      // 接受下一model并更新原来的model
      const nextModel = originalAction(vm.model);
      vm.model = nextModel;
    }
  })

  const vm = new Vue({
    el,
    data: {
      model
    },
    render(h) {
      return view(h, this.data, wrappedActions)
    },
    methods: actions
  })
}

app({
  el: '#app',
  model: {
    count: 0
  },
  actions: {
    inc: ({ count }) => ({ count: count + 1 }),
    del: ({ count }) => ({ count: count - 1 })
  },
  view: (h, model, actions) => h('div', { attrs: { id: 'app' }}, [
    model.count, ' ',
    ('button', { on: { click: actions.inc }}, '+'),
    ('button', { on: { click: actions.del }}, '-')
  ])
})
</script>

以上都是基于个人的理解,欢迎大家提出意见。

自己前端博客:前端学习笔记