写在前面
因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出。
文章的原地址:github.com/answershuto…。
在学习过程中,为Vue加上了中文的注释github.com/answershuto…以及Vuex的注释github.com/answershuto…,希望可以对其他想学习源码的小伙伴有所帮助。
可能会有理解存在偏差的地方,欢迎提issue指出,共同学习,共同进步。
Vuex
我们在使用Vue.js开发复杂的应用时,经常会遇到多个组件共享同一个状态,亦或是多个组件会去更新同一个状态,在应用代码量较少的时候,我们可以组件间通信去维护修改数据,或者是通过事件总线来进行数据的传递以及修改。但是当应用逐渐庞大以后,代码就会变得难以维护,从父组件开始通过prop传递多层嵌套的数据由于层级过深而显得异常脆弱,而事件总线也会因为组件的增多、代码量的增大而显得交互错综复杂,难以捋清其中的传递关系。
那么为什么我们不能将数据层与组件层抽离开来呢?把数据层放到全局形成一个单一的Store,组件层变得更薄,专门用来进行数据的展示及操作。所有数据的变更都需要经过全局的Store来进行,形成一个单向数据流,使数据变化变得“可预测”。
Vuex是一个专门为Vue.js框架设计的、用于对Vue.js应用程序进行状态管理的库,它借鉴了Flux、redux的基本思想,将共享的数据抽离到全局,以一个单例存放,同时利用Vue.js的响应式机制来进行高效的状态管理与更新。正是因为Vuex使用了Vue.js内部的“响应式机制”,所以Vuex是一个专门为Vue.js设计并与之高度契合的框架(优点是更加简洁高效,缺点是只能跟Vue.js搭配使用)。具体使用方法及API可以参考Vuex的官网。
先来看一下这张Vuex的数据流程图,熟悉Vuex使用的同学应该已经有所了解。
Vuex实现了一个单向数据流,在全局拥有一个State存放数据,所有修改State的操作必须通过Mutation进行,Mutation的同时提供了订阅者模式供外部插件调用获取State数据的更新。所有异步接口需要走Action,常见于调用后端接口异步获取更新数据,而Action也是无法直接修改State的,还是需要通过Mutation来修改State的数据。最后,根据State的变化,渲染到视图上。Vuex运行依赖Vue内部数据双向绑定机制,需要new一个Vue对象来实现“响应式化”,所以Vuex是一个专门为Vue.js设计的状态管理库。
安装
使用过Vuex的朋友一定知道,Vuex的安装十分简单,只需要提供一个store,然后执行下面两句代码即完成的Vuex的引入。
1 | 复制代码Vue.use(Vuex); |
那么问题来了,Vuex是怎样把store注入到Vue实例中去的呢?
Vue.js提供了Vue.use方法用来给Vue.js安装插件,内部通过调用插件的install方法(当插件是一个对象的时候)来进行插件的安装。
我们来看一下Vuex的install实现。
1 | 复制代码/*暴露给外部的插件install方法,供Vue.use调用安装插件*/ |
这段install代码做了两件事情,一件是防止Vuex被重复安装,另一件是执行applyMixin,目的是执行vuexInit方法初始化Vuex。Vuex针对Vue1.0与2.0分别进行了不同的处理,如果是Vue1.0,Vuex会将vuexInit方法放入Vue的_init方法中,而对于Vue2.0,则会将vuexinit混淆进Vue的beforeCreacte钩子中。来看一下vuexInit的代码。
1 | 复制代码 /*Vuex的init钩子,会存入每一个Vue实例等钩子列表*/ |
vuexInit会尝试从options中获取store,如果当前组件是根组件(Root节点),则options中会存在store,直接获取赋值给$store即可。如果当前组件非根组件,则通过options中的parent获取父组件的$store引用。这样一来,所有的组件都获取到了同一份内存地址的Store实例,于是我们可以在每一个组件中通过this.$store愉快地访问全局的Store实例了。
那么,什么是Store实例?
Store
我们传入到根组件到store,就是Store实例,用Vuex提供到Store方法构造。
1 | 复制代码export default new Vuex.Store({ |
我们来看一下Store的实现。首先是构造函数。
1 | 复制代码constructor (options = {}) { |
Store的构造类除了初始化一些内部变量以外,主要执行了installModule(初始化module)以及resetStoreVM(通过VM使store“响应式”)。
installModule
installModule的作用主要是用为module加上namespace名字空间(如果有)后,注册mutation、action以及getter,同时递归安装所有子module。
1 | 复制代码/*初始化module*/ |
resetStoreVM
在说resetStoreVM之前,先来看一个小demo。
1 | 复制代码let globalData = { |
上述代码在全局有一个globalData,它被传入一个Vue对象的data中,之后在任意Vue模板中对该变量进行展示,因为此时globalData已经在Vue的prototype上了所以直接通过this.prototype访问,也就是在模板中的。此时,setTimeout在1s之后将globalData.d进行修改,我们发现模板中的globalData.d发生了变化。其实上述部分就是Vuex依赖Vue核心实现数据的“响应式化”。
不熟悉Vue.js响应式原理的同学可以通过笔者另一篇文章响应式原理了解Vue.js是如何进行数据双向绑定的。
接着来看代码。
1 | 复制代码/* 通过vm重设store,新建Vue对象使用Vue内部的响应式实现注册state以及computed */ |
resetStoreVM首先会遍历wrappedGetters,使用Object.defineProperty方法为每一个getter绑定上get方法,这样我们就可以在组件里访问this.$store.getter.test就等同于访问store._vm.test。
1 | 复制代码forEachValue(wrappedGetters, (fn, key) => { |
之后Vuex采用了new一个Vue对象来实现数据的“响应式化”,运用Vue.js内部提供的数据双向绑定功能来实现store的数据与视图的同步更新。
1 | 复制代码store._vm = new Vue({ |
这时候我们访问store._vm.test也就访问了Vue实例中的属性。
这两步执行完以后,我们就可以通过this.$store.getter.test访问vm中的test属性了。
严格模式
Vuex的Store构造类的option有一个strict的参数,可以控制Vuex执行严格模式,严格模式下,所有修改state的操作必须通过mutation实现,否则会抛出错误。
1 | 复制代码/* 使能严格模式 */ |
首先,在严格模式下,Vuex会利用vm的$watch方法来观察?state,也就是Store的state,在它被修改的时候进入回调。我们发现,回调中只有一句话,用assert断言来检测store._committing,当store._committing为false的时候会触发断言,抛出异常。
我们发现,Store的commit方法中,执行mutation的语句是这样的。
1 | 复制代码this._withCommit(() => { |
再来看看_withCommit的实现。
1 | 复制代码_withCommit (fn) { |
我们发现,通过commit(mutation)修改state数据的时候,会再调用mutation方法之前将committing置为true,接下来再通过mutation函数修改state中的数据,这时候触发$watch中的回调断言committing是不会抛出异常的(此时committing为true)。而当我们直接修改state的数据时,触发$watch的回调执行断言,这时committing为false,则会抛出异常。这就是Vuex的严格模式的实现。
接下来我们来看看Store提供的一些API。
commit(mutation)
1 | 复制代码/* 调用mutation的commit方法 */ |
commit方法会根据type找到并调用_mutations中的所有type对应的mutation方法,所以当没有namespace的时候,commit方法会触发所有module中的mutation方法。再执行完所有的mutation之后会执行_subscribers中的所有订阅者。我们来看一下_subscribers是什么。
Store给外部提供了一个subscribe方法,用以注册一个订阅函数,会push到Store实例的_subscribers中,同时返回一个从_subscribers中注销该订阅者的方法。
1 | 复制代码/* 注册一个订阅函数,返回取消订阅的函数 */ |
在commit结束以后则会调用这些_subscribers中的订阅者,这个订阅者模式提供给外部一个监视state变化的可能。state通过mutation改变时,可以有效补获这些变化。
dispatch(action)
来看一下dispatch的实现。
1 | 复制代码/* 调用action的dispatch方法 */ |
以及registerAction时候做的事情。
1 | 复制代码/* 遍历注册action */ |
因为registerAction的时候将push进_actions的action进行了一层封装(wrappedActionHandler),所以我们在进行dispatch的第一个参数中获取state、commit等方法。之后,执行结果res会被进行判断是否是Promise,不是则会进行一层封装,将其转化成Promise对象。dispatch时则从_actions中取出,只有一个的时候直接返回,否则用Promise.all处理再返回。
watch
1 | 复制代码/* 观察一个getter方法 */ |
熟悉Vue的朋友应该很熟悉watch这个方法。这里采用了比较巧妙的设计,_watcherVM是一个Vue的实例,所以watch就可以直接采用了Vue内部的watch特性提供了一种观察数据getter变动的方法。
registerModule
1 | 复制代码/* 注册一个动态module,当业务进行异步加载的时候,可以通过该接口进行注册动态module */ |
registerModule用以注册一个动态模块,也就是在store创建以后再注册模块的时候用该接口。内部实现实际上也只有installModule与resetStoreVM两个步骤,前面已经讲过,这里不再累述。
unregisterModule
1 | 复制代码 /* 注销一个动态module */ |
同样,与registerModule对应的方法unregisterModule,动态注销模块。实现方法是先从state中删除模块,然后用resetStore来重制store。
resetStore
1 | 复制代码/* 重制store */ |
这里的resetStore其实也就是将store中的_actions等进行初始化以后,重新执行installModule与resetStoreVM来初始化module以及用Vue特性使其“响应式化”,这跟构造函数中的是一致的。
插件
Vue提供了一个非常好用的插件Vue.js devtools
1 | 复制代码/* 从window对象的__VUE_DEVTOOLS_GLOBAL_HOOK__中获取devtool插件 */ |
如果已经安装了该插件,则会在windows对象上暴露一个VUE_DEVTOOLS_GLOBAL_HOOK。devtoolHook用在初始化的时候会触发“vuex:init”事件通知插件,然后通过on方法监听“vuex:travel-to-state”事件来重置state。最后通过Store的subscribe方法来添加一个订阅者,在触发commit方法修改mutation数据以后,该订阅者会被通知,从而触发“vuex:mutation”事件。
最后
Vuex是一个非常优秀的库,代码量不多且结构清晰,非常适合研究学习其内部实现。最近的一系列源码阅读也使我自己受益匪浅,写这篇文章也希望可以帮助到更多想要学习探索Vuex内部实现原理的同学。
关于
作者:染陌
Email:answershuto@gmail.com or answershuto@126.com
Github: github.com/answershuto
知乎主页:www.zhihu.com/people/cao-…
osChina:my.oschina.net/u/3161824/b…
转载请注明出处,谢谢。
欢迎关注我的公众号
本文转载自: 掘金