摸鱼间隙来实现一个 Vuex

246 阅读2分钟

开篇图

前言

如果你用过 Vue,那么 Vuex 必定是你使用过程中无法绕过的一道坎。

用过以后你有没有想过,

他的内部原理究竟是怎么样的呢?

今天我们就通过对其简单的实现,

一起来探究它内部的原理。

这里就默认大家已经用过 Vuex 并且对它的操作还比较熟悉了,

如果还不熟悉的同学可以异步 官网教程

先说说双向绑定

用过的同学应该都会对 Vuex 强大的数据管理能力印象深刻,

那么操作 Vuex 中的数据是怎么让渲染视图实时更新的呢?

没错,这时候老大哥 Vue 就要出场了。

出场

编程是门黑魔法,下面这种操作不知道在你的代码中有没有出现过,

(如果你不知道,权当开个眼界嘿嘿。

// 创建一个全新的 Vue 实例
const bus = new Vue()

// 将其挂载到当前项目实例
this.$bus = bus

// 进行通信
this.$bus.$emit('call', '呼叫')

this.$bus.$on('call', () => alert('收到'))

这是一个使用 Vue 实例进行全局通信的例子,其优点和缺点都非常的明显,

但是今天的重点并不是要讲这个。

不知道大家有没有注意到我们在实现通信的过程中实际使用了 Vue 中的 $emit$on 的方法,

这给了我们启发,

我们能不能让 Vue 中已有的双向绑定为我们所用呢?

说干就干!

开始表演

Vuex 功能测试准备

在开始搭建我们自己的 Vuex 前,我们先预先设定好像要实现的功能,

方便我们在开发过程中随时进行测试。

最后的效果如下:

测试界面

左边按钮中的数字依赖于仓库中的 count 变量,且每次点击都加1。

接着我们编写好调用 Vuex 的代码,使用方法同官方库:

import Vue from 'vue'
import MyVuex from './myVuex'

Vue.use(MyVuex)

const store = new MyVuex.Store({
    state: {
        count: 1
    },
    getters: {
        getCount(state) {
            return state.count
        },
        getOne(state) {
            return 1
        }
    },
    mutations: {
        doCount(state, data) {
            state.count = data
        }
    },
    actions: {
        doCount({ commit }, data) {
            commit('doCount', data)
        },
        doCountDouble({ state, commit }) {
            commit('doCount', state.count * 2)
        }
    }
})

export default store

万事具备之后就可以正式开始开发了!

Vuex 的核心主要有那么四部分:

  • state
  • getters
  • mutations
  • actions

下面我们来一一对这些部分进行剖析吧

使用 Vue 双向绑定构建 state

上面我们提到过,数据处理的核心其实还是利用了 Vue 的双向绑定,

遵从着这个思路我们可以搭出整个库的雏形:

export class Store {
    constructor(options = {}, Vue) {
        // 没有 Vue 时先装上
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        // 获取配置
        const { state = {} } = options
        // 新建 Vue 实例响应式存储
        resetStoreVM(this, state)
    }
    get state() {
        return  this._vm._data.?state
    }
}

// 新建 Vue 实例
function resetStoreVM (store, state) {
    // 先看有没有旧实例
    const oldVm = store._vm
    
    if (oldVm) {
        Vue.destroy(oldVm)
    }
    // store.getters = {}

    store._vm = new Vue({
        data: {
          ?state: state
        },
    })
}

这时候我们把 Store 的实例打印出来,就能看到我们的 state 已经被加载好了。

state的值

实现 getters

上面我们已经成功加载好了 state

但是一般而言并不推荐直接取值,而是最好通过 getters 进行值的获取,方便进行二次加工。

在实现 getters 之前,我们先来看看文档中的说明。

getters文档说明

文档中说明 getters 的值是有缓存优化策略的,但是我们这里为了方便就直接每次都使用 新计算 的值,

如果有感兴趣的同学可在源码搜索 store._makeLocalGettersCache 的相关代码。

现在我们的代码变成了这个样子:

export class Store {
    constructor(options = {}, Vue) {
        // 没有 Vue 时先装上
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        // 获取配置
        const { state = {}, getters = {}, } = options

        this.getters = Object.create(null)

        // 装载 getters
        forEachValue(getters, (fn, type) => {
            registerGetter(store, type, fn)
        })

        // 新建 Vue 实例响应式存储
        resetStoreVM(this, state)
    }
    get state() {
        return  this._vm._data.?state
    }
}
// 注册 getter 函数
function registerGetter (store, type, fn) {
    Object.defineProperty(store.getters, type, {
        get() {
            return fn(store._state)
        }
    })
}

function forEachValue (obj, fn) {
    Object.keys(obj).forEach(key => fn(obj[key], key))
}

利用了 ES5 的 Object.defineProperty 进行拦截,每次调用取值都返回函数运行的结果,

::: tip 也可以使用 Proxy 完成拦截,感兴趣的同学可以自己实现一下 :::

我们测试图例的按钮使用 getters 进行取值,进行到这里已经能在按钮上看到这个值了!

getters效果

mutations 和 actions 实现

单纯的数据获取是苍白的,接下来我们就来实现数据变化的黑魔法。

因为简单版本的 mutationsactions 实现大同小异,

所以我们这里就放在一起进行实现了。

需要注意的是这里的 mutation 必须使用 commit 进行调用,这里使用 _committing 对其加锁。

class Store {
    constructor(options = {}, Vue) {
        this._committing = false
        ...
    }

    ....

    // 执行函数并加锁
    _withCommit (fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
    }
}

这里我们梳理一下 mutationsactions 的建构流程,

  • 循环配置中的对应函数加载到 Store 的对应位置
  • 定义好 commitdispatch 方法使其指向我们存储处理函数的位置
  • 处理好调用时的 this 指向

清晰了流程之后我们最后的实现代码就是下面这样的:

export class Store {
    constructor(options = {}, Vue) {
        if (!Vue && typeof window !== 'undefined' && window.Vue) {
            install(window.Vue)
        }
        
        const { state = {}, getters = {}, mutations = {}, actions = {} } = options
        
        // 初始化
        this._committing = false
        this._state = state
        this._actions = Object.create(null)
        this._mutations = Object.create(null)
        this.getters = Object.create(null)

        const { dispatch, commit } = this
        const store = this

        // 装载 getters
        forEachValue(getters, (fn, type) => {
            registerGetter(store, type, fn)
        })

        // 装载 mutations 和 actions
        forEachValue(mutations, (fn, type) => {
            registerMutation(store, type, fn)
        })

        forEachValue(actions, (fn, type) => {
            registerAction(store, type, fn)
        })

        this.dispatch = function boundDispatch (type, payload) {
            return dispatch.call(store, type, payload)
        }

        this.commit = function boundCommit (type, payload) {
            return commit.call(store, type, payload)
        }
        
        // 新建 Vue 实例响应式存储
        resetStoreVM(this, state)
    }

    get state() {
        return  this._vm._data.?state
    }

    // 禁止再赋值
    set state (v) {
        throw new Error('不允许赋值!!!')
    }

    // commit
    commit(type, payload) {
        const entry = this._mutations[type]

        if (!entry) {
            console.error(`[vuex] unknown mutation type: ${type}`)
            return
        }
        // 执行对应处理函数
        this._withCommit(() => {
            entry(payload)
        })
    }

    // dispatch
    dispatch(type, payload) {
        const entry = this._actions[type]

        if (!entry) {
            console.error(`[vuex] unknown action type: ${type}`)
            return
        }
        
        entry (payload)
    }

    // 执行函数并加锁
    _withCommit (fn) {
        const committing = this._committing
        this._committing = true
        fn()
        this._committing = committing
    }

}

看到这里有没有长呼一口气的感觉~~

先别松懈,我们还有最后一个问题,

为了模仿原库中 Vue.use() 的安装方式,

我们还需要提供一个 install 函数

实现入口加载函数

这部分的内容其实就只有两件事情要做:

  • 取到 Store 实例并将其挂载到 this.$store
  • 将其混入我们的项目中

这部分的源码非常好理解,

所以这里我就直接对源码进行搬运了~~~

// 安装方法
export function install (_Vue) {
    if (Vue && _Vue === Vue) {
      if (process.env.NODE_ENV !== 'production') {
        console.error(
          '[vuex] already installed. Vue.use(Vuex) should be called only once.'
        )
      }
      return
    }
    Vue = _Vue
    // 取得 Vue 实例后混入
    Vue.mixin({ beforeCreate: vuexInit })   
}

/**
 * Vuex init hook, injected into each instances init hooks list.
 * 初始化 Vuex
 */
function vuexInit () {
    const options = this.$options
    
    if (options.store) {
      // 组件内部有 store,则优先使用原有的store  
      this.$store = typeof options.store === 'function'
        ? options.store()
        : options.store
    } else if (options.parent && options.parent.$store) {
      // 组件没有 store 则继承根节点的 $store
      this.$store = options.parent.$store
    }
} 

实现效果

实现效果

完整代码戳这里

觉得有用的记得 star 一下哦~~

结语

知其然也要知其所以然,

阅读源码一方面让我们了解到框架内部的实现原理,遏制住会产生 bug 的骚操作,

另一方面也可以学习精妙的写法,对自己的编程风格有所启发。

谢谢大渣!

-- 完 --

欢迎关注我的个人网站啦啦啦~

不定期更新前端内容。

参考资料