[译] 从头为 Vue.js 3 实现 Vuex

简介: [译] 从头为 Vue.js 3 实现 Vuex

原文:medium.com/@lachlanmil…

Vuex icon by Ergenekon Yiğit

Vue 3 的 alpha 版本已经放出有些日子了,但是大多数核心库都还没赶上趟 -- 说得就是 Vuex 和 Vue Router 了。让我们来使用 Vue 3 新的反应式 API 实现自己的罢。

为了让事情变得简单,我们将只实现顶级的 stateactionsgettersmutations - 暂时没有 namespaced modules,尽管我也会留一条如何实现的线索。

本文中的源码和测试可以在 这里 找到。在线的 demo 可以在 这里 看到。

规范

简单起见,我们的实现将不支持整个 API — 只是一个子集。我们将编码以使下面的代码可用:


const store = new Vuex.Store({
  state: {
    count: 0
  },
  mutations: {
    INCREMENT(state, payload) {
      state.count += payload
    }
  },
  actions: {
    increment(context, payload) {
      context.commit('INCREMENT', payload)
    }
  },
  getters: {
    triple(state) {
      return state.count * 3
    }
  }
})

唯一的问题是在本文撰写之时,尚无办法将 $store 附加到 Vue.prototype,所以我们将用 window.store 代替 this.$store

由于 Vue 3 从其组件和模版系统中单独暴露出了反应式 API,所以我们就可以用诸如 reactivecomputed 等函数来构建一个 Vuex store,并且单元测试也甚至完全无需加载一个组件。这对于我们足够好了,因为 Vue Test Utils 还不支持 Vue 3。

准备开始

我们将采用 TDD 的方式完成本次开发。需要安装的只有两样:vuejest 。 通过 yarn add vue@3.0.0-alpha.1 babel-jest @babel/core @babel/preset-env 安装它们。需要做一些基础的配置 - 按其文档配置即可。

反应式状态

第一个测试是关于 state 的:


test('reactive state', () => {
  const store = new Vuex.Store({
    state: {
      count: 0
    }
  })
  expect(store.state).toEqual({ count: 0 })
  store.state.count++
  expect(store.state).toEqual({ count: 1 })
})

毫无悬念地失败了 — Vuex 为 undefined。让我们定义它:


class Store {
}
const Vuex = {
  Store
}

现在我们得到的是 Expected: {"count": 0}, Received: undefined。让我们从 vue 中提取 reactive 并让测试通过吧!


import { reactive } from 'vue'
class Store {
  constructor(options) {
    this.state = reactive(options.state)
  }
}

Vue 的 reactive 函数真是 so easy。我们在测试中直接修改了 store.state - 这不太理想,所以来添加一个 mutation 作为替代。

实现 mutations 和 commit

像上面一样,还是先来编写测试:


test('commits a mutation', () => {
  const store = new Vuex.Store({
    state: {
      count: 0
    },
    mutations: {
      INCREMENT(state, payload) {
        state.count += payload
      }
    }
  })
  store.commit('INCREMENT', 1)
  expect(store.state).toEqual({ count: 1 })
})

测试失败,理所当然。报错信息为 TypeError: store.commit is not a function。让我们来实现 commit 方法,同时也要把 options.mutations  赋值给 this.mutations,这样才能在 commit 中访问到:


class Store {
  constructor(options) {
    this.state = reactive(options.state)
    this.mutations = options.mutations
  }
  commit(handle, payload) {
    const mutation = this.mutations[handle]
    if (!mutation) {
      throw Error(`[Hackex]: ${handle} is not defined`)
    }
    mutation(this.state, payload)
  }
}

因为 mutations 只是一个将函数映射为其属性的对象,所以我们用 handle 参数就能取出对应的函数,并传入 this.state 调用它。我们也能为 mutation 未被定义的情况编写一个测试:


test('throws an error for a missing mutation', () => {
  const store = new Vuex.Store({ state: {}, mutations: {} })
  expect(() => store.commit('INCREMENT', 1))
    .toThrow('[Hackex]: INCREMENT is not defined')
})

分发一个 action

dispatch 很类似于 commit - 两者的首个参数都是一个函数调用名的字符串,以及一个 payload 作为第二个参数。

但与某个 mutation 函数接受 state 作为首参不同,一个 action 的第一个参数是个 context 对象,该对象暴露了 statecommitgettersdispatch

同时,dispatch 将总是返回一个 Promise - 所以 dispatch(...).then 应该是合法的。这意味着如果用户的 action 没返回一个 Promise,或调用了某些类似 axios.get 的东西,我们也需要为用户返回一个 Promise。

我们可以编写如下测试。你可能会注意到测试重复了一大段刚才 mutation 用例中的东西 — 我们稍后会用一些工厂函数来清理它。


test('dispatches an action', async () => {
  const store = new Vuex.Store({
    state: {
      count: 0
    },
    mutations: {
      INCREMENT(state, payload) {
        state.count += payload
      }
    },
    actions: {
      increment(context, payload) {
        context.commit('INCREMENT', payload)
      }
    }
  })
  store.dispatch('increment', 1).then(() => {
    expect(store.state).toEqual({ count: 1 })
  })
})

运行这段将得到报错 TypeError: store.dispatch is not a function。从前面的经验中我们得知需要在构建函数中也给 actions 赋值,所以让我们完成这两件事,并以早先调用 mutation 的相同方式调用 action


class Store
  constructor(options) {
    // ...
    this.actions = options.actions
  }
  
  // ...
  dispatch(handle, payload) {
    const action = this.actions[handle]
    const actionCall = action(this, payload)
  }
}

现在运行后得到了 TypeError: Cannot read property 'then' of undefined 报错。这当然是因为,没有返回一个 Promise - 其实我们还什么都没返回呢。

class Store {
  // ...
  dispatch(handle, payload) {
    const action = this.actions[handle]
    const actionCall = action(this, payload)
    if (!actionCall || !typeof actionCall.then) {
      return Promise.resolve(actionCall)
    }
    return actionCall
  }

这和 Vuex 的真实实现并无太大区别,在 这里 查看其源码。现在测试通过了!

通过 computed 实现 getters

实现 getters 会更有意思一点。我们同样会使用 Vue 暴露出的新 computed 方法。开始编写测试:


test('getters', () => {
  const store = new Vuex.Store({
    state: {
      count: 5
    },
    mutations: {},
    actions: {},
    getters: {
      triple(state) {
        return state.count * 3
      }
    }
  })
  expect(store.getters['triple']).toBe(15)
  store.state.count += 5
  expect(store.getters['triple']).toBe(30)
})

照例,我们得到了 TypeError: Cannot read property 'triple' of undefined 报错。不过这次我们不能只在 constructor 中写上 this.getters = getters 就草草了事啦 - 需要遍历 options.getters 并确保它们都用上搭配了响应式状态值的 computed。一种简单却不正确的方式会是这这样的:


class Store {
  constructor(options) {
    // ...
    if (!options.getters) {
      return
    }
    for (const [handle, fn] of Object.entries(options.getters)) {
      this.getters[handle] = computed(() => fn(this.state)).value
    }
  }
}

Object.entries(options.getters) 返回函数名 handle (本例中为 triple) 和回调函数 fn (getter 函数本身) 。当我们运行测试时,第一条断言 expect(store.getters['triple']).toBe(15) 通过了,因为返回了 .value;但同时也丢失了反应性 -- store.getters['triple'] 被永远赋值为一个数字了。我们本想返回调用 computed 后的值的。可以通过 Object.defineProperty 实现这一点,在对象上定义一个动态的 get 方法。这也是真实的 Vuex 所做的 - 参考 这里


class Store {
  constructor(options) {
    // ...
    for (const [handle, fn] of Object.entries(options.getters)) {
      Object.defineProperty(this.getters, handle, {
        get: () => computed(() => fn(this.state)).value,
        enumerable: true
      })
    }
  }
}

现在一切正常了。

结合 module 的嵌套 state

为了完全兼容真实的 Vuex,需要实现 module。鉴于文章的长度,我不会在这里完整的实现它。基本上,你只需要为每个 module 递归地实现以上的过程并适当创建命名空间即可。就来看看 module 中嵌套的 state 如何实现这点吧。测试看起来是这样的:


test('nested state', () => {
  const store = new Vuex.Store({
    state: {
      count: 5
    },
    mutations: {},
    actions: {},
    modules: {
      levelOne: {
        state: {},
        modules: {
          levelTwo: {
            state: { name: 'level two' }
          }
        }
      }
    }
  })
  expect(store.state.levelOne.levelTwo.name).toBe('level two')
})

我们可以用一些巧妙的递归来实现:


const registerState = (modules, state = {}) => {
  for (const [name, module] of Object.entries(modules)) {
    state[name] = module.state
    if (module.modules) {
      registerState(module.modules, state[name])
    }
  }
  return state
}

Object.entries 再次发挥了作用 - 我们取得了模块名称以及内容。如果该模块又包含 modules,则再次调用 registerState,并传入上一层模块的 state。这让我们可以任意嵌套多层。到达底层模块时,就直接返回 state

actionsmutationsgetters 稍微复杂一点。我会在之后的文章中实现它们。

升级 constructor 以使用 registerState 方法,所有测试再次通过了。这一步在 actions/mutations/getters 之前完成,这样它们就能访问传递到 reactive 中的整个状态了。


class Store {
  constructor(options) {
    let nestedState = {}
    if (options.modules) {
      nestedState = registerState(options.modules, options.state)
    }
    this.state = reactive({ ...options.state, ...nestedState })
    // ..
  }
}

改进

一些特性尚未实现 -- 比如:

  • 针对 module 的 namespaced actions/mutations/getters
  • plugin 系统
  • 对 mutations/actions 的 subscribe 能力 (主要用于 plugins)

我会在之后的文章中覆盖它们,但实现起来也不太困难。对于有兴趣的读者也是很好的实践机会。

总结

  • 通过 Vue 3 的反应式系统为 Vue 构建反应式插件很简单
  • 完全有可能构建一个和 Vue 解耦的反应式系统 — 我们一次都没有渲染组件或打开浏览器,却对插件可以在 web 和 非 web 环境中(如 Weex、NativeScript 或其它什么 Vue 社区中的风靡之物)都能正常工作很自信



相关文章
|
23天前
|
存储 JavaScript 前端开发
Vue应用瘦身秘籍:揭秘Vuex如何重塑你的应用状态,让复杂变简单!🔥
【8月更文挑战第27天】在开发Vue应用时,随着应用规模的增长,组件间通信与状态共享问题日益复杂。Vuex作为Vue官方推荐的状态管理库,提供了集中式存储仓库来管理组件的共享状态,简化状态跟踪与组件通信。Vuex的核心概念包括state(存储状态数据)、mutations(同步修改state)和actions(处理异步操作)。通过一个购物车应用示例展示了如何定义state、mutations及actions,以及如何在Vue组件中使用这些状态管理功能。掌握Vuex有助于提高应用的健壮性和可维护性。
53 0
|
1月前
|
JavaScript
Vue学习之--------VueX(2022/8/31)
这篇文章是关于VueX的基础知识介绍,涵盖了VueX中的state、mutations、getters和actions的定义和使用,以及Action提交mutation而非直接变更状态,以及mutations的同步执行特性。
Vue学习之--------VueX(2022/8/31)
|
23天前
|
存储 JavaScript
解锁Vuex高级玩法:模块化与插件共舞,让你的Vue项目状态管理如虎添翼!
【8月更文挑战第27天】Vuex是一款专为Vue.js应用程序设计的状态管理模式及库,它通过集中管理组件状态来确保状态变更的可预测性。在大型应用中,采用模块化管理可以让代码结构更加清晰,同时利用插件增强功能。模块化管理允许将store拆分为包含各自state、mutations、actions和getters的独立模块。插件则能监听状态变化,实现诸如日志记录或数据持久化等功能。本文通过具体示例介绍了如何在Vuex中实现模块化管理和插件的高级应用。
34 1
|
23天前
|
存储 缓存 监控
解锁Vuex性能优化的秘密:让大型Vue应用如丝般顺滑,紧跟技术热点,体验速度与效率的双重飞跃!
【8月更文挑战第27天】Vuex是Vue.js应用程序的状态管理解决方案,它允许开发者集中管理组件间共享的状态。然而,在大型应用中,庞大的状态树可能会影响性能。本文介绍几种优化策略:1)精简状态树,避免存储不必要的数据并通过模块化降低复杂度;2)利用getters缓存计算结果以提高效率;3)通过actions处理异步操作,确保状态更新的同步性和逻辑清晰;4)在组件级别上减少不必要的重渲染;5)使用工具如Vue Devtools进行监控和调试。这些方法有助于提升应用的整体性能和用户体验。
45 0
|
1月前
|
JavaScript
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
这篇文章通过一个实际的Vue项目案例,演示了如何在Vuex中实现多组件间共享数据。文章内容包括在Vuex的state中新增用户数组,创建Person.vue组件用于展示和添加用户信息,以及在Count组件中使用Person组件操作的数据。通过测试效果展示了组件间数据共享和状态更新的流程。
Vue学习之--------深入理解Vuex之多组件共享数据(2022/9/4)
|
1月前
|
JavaScript
Vue学习之--------深入理解Vuex之模块化编码(2022/9/4)
这篇文章详细介绍了Vuex的模块化编码和命名空间的使用,旨在让代码更易于维护并提高数据分类的明确性。内容包括模块化和命名空间的概念、如何在store中配置模块、以及如何在组件中使用模块化的数据。文章通过实战项目案例,展示了如何拆分`store/index.js`文件,创建`count.js`和`person.js`模块,并在`Count.vue`和`Person.vue`组件中使用这些模块。最后,文章还提供了测试效果和一些使用注意点。
Vue学习之--------深入理解Vuex之模块化编码(2022/9/4)
|
1月前
|
JavaScript 前端开发 开发者
Vue学习之--------深入理解Vuex、原理详解、实战应用(2022/9/1)
这篇文章详细介绍了Vuex的基本概念、使用场景、安装配置、基本用法、实际应用案例以及注意事项,通过一个数字累加器的实战示例,帮助开发者深入理解Vuex的原理和应用。
|
20天前
|
存储 JavaScript 前端开发
Vue.js + Vuex:解锁前端复杂应用的神秘钥匙,探索状态管理的新境界!
【8月更文挑战第30天】Vue.js结合Vuex状态管理,为复杂前端应用提供了解锁高效与优雅的金钥匙。Vue.js凭借简洁的API和高效虚拟DOM更新机制广受好评,但在大规模应用中,组件间状态共享变得复杂。这时,Vuex通过中心化状态存储,使状态管理清晰可见,如同为Vue.js应用增添智慧大脑。例如,在购物车应用中,Vuex通过`state`、`mutations`、`actions`和`getters`清晰管理状态,简化组件间状态同步,减少耦合,确保单一状态源,使开发更加高效有序。在Vue.js的世界里,Vuex是一位智慧管家,让前端开发不仅高效,更成为一门艺术。
10 0
|
20天前
|
存储 JavaScript 前端开发
【Vue.js的神秘力量】一键解锁:如何让Bootstrap和Vuex成为你的开发超能力?
【8月更文挑战第30天】Vue.js是一个轻量且灵活的JavaScript框架,易于上手且功能强大。为提高开发效率和应用交互性,常需集成第三方库。本文介绍如何在Vue.js项目中集成Bootstrap和Vuex,及其它常见第三方库。Bootstrap提供响应式设计和预制组件,通过安装插件和引入CSS/JS即可集成;Vuex作为官方状态管理库,通过安装并创建store来管理组件状态。此外,Vue.js还可轻松集成Axios和Vue Router等库,提升HTTP请求和页面路由功能。合理选择和集成第三方库能显著提升开发效率,但需保持代码可维护性和可读性。
16 0
|
20天前
|
前端开发 JavaScript 开发者
Vue.js 与第三方库的神秘融合:Bootstrap、Vuex 等究竟带来何种惊喜?
【8月更文挑战第30天】Vue.js 作为一款流行的前端框架,凭借其强大的功能和易用性深受开发者喜爱。它不仅能独立运作,还能无缝集成各种第三方库,显著提升开发效率。例如,通过 `vue-bootstrap`,我们可以轻松引入 Bootstrap 的响应式组件,快速打造美观的用户界面;借助 Vuex,Vue 提供的状态管理方案,能有效解决复杂应用中的状态共享难题。此外,诸如 `axios` 和 `moment` 等库也能进一步增强 Vue 的功能,使其成为构建高性能前端应用的理想选择。
29 0