摸鱼间隙来实现一个 Vuex

开篇图

前言

若是你用过 Vue,那么 Vuex 一定是你使用过程当中没法绕过的一道坎。javascript

用过之后你有没有想过,前端

他的内部原理到底是怎么样的呢?vue

今天咱们就经过对其简单的实现,java

一块儿来探究它内部的原理。git

这里就默认你们已经用过 Vuex 而且对它的操做还比较熟悉了,github

若是还不熟悉的同窗能够异步 官网教程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 的骚操做,

另外一方面也能够学习精妙的写法,对本身的编程风格有所启发。

谢谢大渣!

-- 完 --

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

不按期更新前端内容。

参考资料

相关文章
相关标签/搜索