使用-HooX-管理-React-状态的若干个好处

HooX  是一个基于 hook 的轻量级的 React 状态管理工具。使用它可方便的管理 React 应用的全局状态,概念简单,完美支持 TS。1. 更拥抱函数式组件从 React@16.8 的 hook 到 vue@3 的composition-api,基本可以断定,函数式组件是未来趋势。HooX提供了函数式组件下的状态管理方案,以及完全基于函数式写法的一系列 API,让用户更加的拥抱函数式组件,走向未来更进一步。 2. 简化纯 hook 写法带来的繁杂代码写过 hook 的同学肯定知道,hook 带来的逻辑抽象能力,让我们的代码变得更有条件。但是: useCallback/useMemo 真的是写的非常非常多由于作用域问题,一些方法内的 state 经常不知道到底对不对实际举个例子吧,比如一个列表,点击加载下一页,如果纯 hook 书写,会怎么样呢? import { useState, useEffect } from 'react'const fetchList = (...args) => fetch('./list-data', ...args)export default function SomeList() { const [list, setList] = useState([]) const [pageNav, setPageNav] = useState({ page: 1, size: 10 }) const { page, size } = pageNav // 初始化请求 useEffect(() => { fetchList(pageNav).then(data => { setList(data) }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 获取下一页内容 const nextPage = () => { const newPageNav = { page: page + 1, size } fetchList(newPageNav).then(data => { setList(data) setPageNav(newPageNav) }) } return ( <div> <div className="list"> {list.map((item, key) => ( <div className="item" key={key}> ... </div> ))} </div> <div className="nav"> {page}/{size} <div className="next" onClick={nextPage}> 下一页 </div> </div> </div> )}很常规的操作。现在,我希望给“下一页”这个方法,加个防抖,那应该怎么样呢?是这样吗? ...

November 5, 2019 · 3 min · jiezi

基于Proxy的小程序状态管理

微信小程序的市场在进一步的扩大,而背后的技术社区仍在摸索着最好的实践方案。我在帮助Nike,沃尔玛以及一些创业公司开发小程序后,依旧认为使用小程序原生框架是一个更高效,稳定的选择,而使用原生框架唯独缺少一个好的状态管理库,如果不引入状态管理则会让我们在模块化,项目结构以及单元测试上都有些捉襟见肘。 目前相对比较稳健的做法是针对redux或者mobx做一个adaptor应用到小程序中,但这样需要自己想办法打包引入外部库,还要想怎么去写这个adaptor,总显得有些麻烦。于是我迸发出一个想法去写一个专用于小程序的状态管理库,它使用起来足够简单并且可以通过小程序自己的npm机制安装。 目前我已经用这个开源库开发了两个电商小程序,在提高我开发效率的同时亦保证了程序的性能,所以接下来我想谈谈这背后的理念以启发更多开发者尝试新的解决方案。 基于Proxy的状态管理实现Proxy在小程序中已经得到了足够好的支持,目前并没有发现在任何iPhone或者Android上不能使用Proxy的情况。而基于Proxy的状态管理其实也就是订阅监听的模式,一方面监听数据的变化,另一方面将这些变化传达给订阅的小程序页面。 举一个比较常见的例子,当一个用户从自己的主页进入用户编辑页面,然后更改了自己的用户名点击保存后,用户主页和用户编辑页上的用户名这时候都应该被更新。这背后的程序逻辑则是:更新这个行为将触发Proxy去通知状态管理库,然后状态管理库负责检查此时还在页面栈中的所有页面,更新订阅了用户名这个数据的页面,如下图: Part1: 监听数据变化监听数据变化其实就是监听各个Store的属性变化,实现上就是在各个Store前面加了一层Proxy,用更直观的图片来表示就是这样: 当一个Store被观察以后,它的属性就都变成了Proxy实例,当这个属性值是Object或者Array的时候,它内部的值也会被包装成Proxy实例,这样无论多深层的数据变动都能被监听到。而在Proxy的后面,Store的属性其实是被另一套数据(紫色部分)所维护,这套数据不负责监听,它就是纯数据,针对属性的任何变动最后都会应用到这套数据上来,它的作用是维护和返回最新的数据。 实现细节: https://github.com/wwayne/min... Part2: 页面数据绑定因为小程序每个页面的js都是向Page中传递一个对象,这就让我们有机会包装这个对象,从而实现: 进入页面后,将页面保存在页面栈中将来自状态管理库的数据映射到这个页面的data上来页面退出时,将页面从页面栈中移除实现细节: https://github.com/wwayne/min... Part3: 页面订阅更新当数据被监听到变化后,我们需要依次做两件事,先是找到所有存储在页面栈里的页面,然后根据各个页面订阅的数据来检查变化,如果有变化就通知这些页面,从而让它们去触发setData更新页面。 实现细节:https://github.com/wwayne/min... 使用状态管理的例子有了状态管理库,现在我们就来实现一开始举例的更新用户信息的操作,我们的文件路径如下: stores/ user.jspages/ userEdit/ index.js index.wxml1.首先我们创建一个Store保存用户的信息,并且监听它的变化: // stores/user.jsimport { observe } from 'minii'Class UserStore { constructor () { this.name = 'bob' } changeName (name) { this.name = name }}export default observe(new UserStore(), 'user')2.接着在我们的小程序页面订阅Store的信息 // pages/userEdit/index.jsimport { mapToData } from 'minii'import userStore from '../../stores/user'const connect = mapToData(state => (({ myName: state.user.name}))Page(connect({ updateNameToJames () { userStore. changeName('james') }}))3.完成,现在可以在页面中使用和更新数据了 ...

June 14, 2019 · 1 min · jiezi

Vuex源码学习(八)模块的context如何被创建以及它的作用

你不知道action与mutation怎么被调用的?赶紧回去看啊Vuex源码学习(七)action和mutation如何被调用的(调用篇))上两个小节已经讲述了commit与dispatch如何调用mutation与action的,但是action中有几个参数感觉涉及到了一些我们遗漏(故意不讲)的点。模块的context在installModule的时候 给每个模块绑定了一个属性context。通过makeLocalContext函数创建的,在注册action、mutation和getters都有使用。这个context是什么呢?makeLocalContext函数创建了一个什么东西返回值local对象 由两个方法、两个属性构成的。这个目的是什么?创建局部模块的dispatch、commit、getters、state也就是这个东西我们按照类型分析dispatch与commit// 查看全名,如果没有全名 可能是根模块或者没有设置命名空间const noNamespace = namespace === ‘’;// 如果没有全名 就使用全局(store)上的disptach// 有全名的话 构建一个新的dispatch // 这个新的dispatch仍然接收三个参数(与store上的dispatch一样)// dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => { //unifyObjectStyle 对额外传入的_options没有任何处理 只是确定一下位置 const args = unifyObjectStyle(_type, _payload, _options) // options 值没有发生变化 const { payload, options } = args let { type } = args // 在disptach的时候是否指定选择root(根) // 如果options设置为{root : true} 那么就会跳过下面 if (!options || !options.root) { // 拼接真正的名字 type = namespace + type if (process.env.NODE_ENV !== ‘production’ && !store._actions[type]) { console.error([vuex] unknown local action type: ${args.type}, global type: ${type}) return } } // 调用(补全名字后)的action return store.dispatch(type, payload) },这段代码我们可以看出来,local对象(也就是模块的context属性)中的dispacth会在未指定使用根模块名字时,会把dispatch调用的名字强行加上这个模块的全名,用这个dispatch调用的action都会变成你这个模块下的action所以local中的dispatch与store中的disptach有什么不同通俗的讲我们想要调用A模块(有命名空间的某个action B)需要做的是this.$store.dispatch(‘A模块的全名/B的名字’); 在A模块的action中想要使用dispatch来做一些事情。actions.jsexport const ajaxGetUserName = ({dispatch})=>{ // 这个时候用dispatch调用自己模块内的其余的action不需要加上全名 dispatch(‘ajaxGetUserAge’); // 想要变成和根模块一样的dispatch 需要加上一个options注明{root : true} // 这个时候dispatch就会变成全局的 不会主动帮你拼接全名了}export const ajaxGetUserAge = () => { // do something}同理local对象下的commit也是做了同样的事情,这里就不多加解释了,相信聪明的你早就可以举一反三了。两个方法说完了,下面该讲两个属性了getters与state这两个属性就是我们的getters与state,但是这是我们local对象中,也就是局部模块下的getters与state。getters与state如何创建的getters首先判断全名是不是为空,为空就返回store对象的getters,有的话就创建局部getters。与其说是创建不如说是代理如何创建局部的getters? 代理的方式makeLocalGetters源码function makeLocalGetters (store, namespace) { // 设计思想 //其实我们并不需要创建一套getters, // 只要我们在local中通过getters来获取一些局部模块的值的时候, // 可以被代理到真正存放这些getters的地方。 // 创建代理对象 const gettersProxy = {} // 找到切割点 const splitPos = namespace.length Object.keys(store.getters).forEach(type => { // skip if the target getter is not match this namespace // 得去getters里面找一下有没有这个namespace为前缀的getter。 // 没有就找不到了 if (type.slice(0, splitPos) !== namespace) return // extract local getter type // 拿到模块内注册的那个局部的getter名字 // 全名是set/getName // localType就是getName const localType = type.slice(splitPos) // Add a port to the getters proxy. // Define as getter property because // we do not want to evaluate the getters in this time. // 完成代理任务, // 在查询局部名字是被代理到对应的store.getters中的(全名)getter Object.defineProperty(gettersProxy, localType, { get: () => store.getters[type], enumerable: true }) }) //返回代理对象 return gettersProxy}创建局部的getters就是一个代理的过程,在使用模块内使用(没有加上命名空间的)getters的名字,会被代理到,store实例上那个真正的(全名的)getters。state这个相对来说就简单很多了与代理类似,只是state只需要代理到state中对应那个模块的state,这个就比较简单了。创建完毕context是如何被创建的大家已经比较了解了。context的作用是什么?(local就是contenxt)之前说过注册mutation、action、getters都用到了local。用他们干什么?一一介绍1. 注册mutation我们注册的mutation在被commit调用时,使用的state是局部的state,当前模块内的state,所以不用特殊方式mutation无法更新父(祖先)模块和兄弟模块的内容。2. 注册dispatchdispatch是可以调用到模块内的mutation、disptach,也就是说它有更新模块内数据的能力,但是只给了dispatch传入了store的getters与state(虽然有了这俩你想要什么放在vuex的数据都能得到),并没有给store的dispatch与mutation。这就说名dispatch可以查看store中的所有数据,你放在vuex里面的数据我都可以看,但是你想改不使用特殊手段,不好意思只能改自己模块的。3. 注册gettersgetters并没有改变数据的能力,你愿意怎么操作数据都可以,模块内的数据,全模块的数据都可以给你,你愿意怎么计算都可以。在注册中我们可以看到,vuex对这个改变数据的权限控制的很严格,但是查看数据控制的很松,改只能改自己模块的,查你愿意怎么看都OK。总结context(也是local)是经过一个makeLocalContext的函数创建的,里面有局部的dispatch、commit方法和getters、state属性。局部的方法属性都是只能访问局部模块内的,除非在使用时额外传入options({root:true})来解开局部模块的限制。局部的getters是通过makeLocalGetters来实现的,主要思想是依靠代理的方式,把局部的名字的getter代理到store的getters中那个全名的getter。context 的作用可以帮助dispatch与commit控制更新数据的权限,帮助模块内getters拿到局部与全模块的数据。这个章节结束,我们所有和模块有关的内容就已经完结了。对于模块做一个小的总结。模块的意义模块与模块链接把Vuex初始化传入的内容,整理成一个方便处理的模块树(方便)模块让action、mutation、getters、state都有了自己的全名(设置namespaced为true),起名字不再被约束,减少了命名冲突。模块还给action、mutation、getters提供了局部上下文(context)让模块内的这些方法和属性,可以方便的修改模块内的数据以及获取全模块与模块内的数据。dispatch与commit也对模块进行了全力的支持(不支持不白做了吗),所以模块为Vuex提供了很多方便,方便的去获取数据、修改数据。那么Vuex真正的数据仓库在哪里?数据都存储在哪里?我们下一章见我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

April 2, 2019 · 2 min · jiezi

Vuex源码学习(七)action和mutation如何被调用的(调用篇)

前置篇不会那可不行!Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)在前置准备篇我们已经知道被处理好的action与mutation都被集中放置在哪里了。下面就要看dispacth和commit如何去调用它们。dispatch与commit的实现commit:首先呢我们要校正参数,把传入的参数整理下主要是处理这种形式// 接收一个对象this.$store.commit({type : ‘setName’,name : ‘xLemon’});this.$store.commit(‘setName’,{name : ‘xLemon’});这两个基本等价。只是第一种方式mutation接收的payload会比第二种多一个type属性,整理的部分并不关键type是我们要找的mutation的名字,如何找到mutation呢?通过 this._mutations[type] 找到要执行的mutation所以type一定要是mutation的全名所以我们通过commit找mutation的时候有命名空间的时候就要输入全名,(那种带很多/的)。没有这个名字的mutation容错处理,然后在withCommit函数的包裹下,完成了mutation的执行(所有mutation啊,全名相同的mutation都会被执行)。然后呢遍历_subscribers里面的函数进行执行。_subscribers这是什么?在一开始我们可以注册一些内容(函数),在commit完成时被通知执行。(观察者模式)如何注册在这一章就不多讲了!后面章节会统一讲述。这就是commit做的事情。dispatch呢?与commit大同小异,也有一个_actionSubscribers的属性,在dispatch执行之前全部调用。对于dispatch Vuex推荐的是放置异步任务,在注册action的时候已经被强制promise化了,所以有多个同名action会通过Promise.all来处理。在action的前后都有对应的钩子函数执行。固定disptach与commit的this指向//在vue组件内一个方法如果多次使用dispatch和commit,就会很麻烦。this.$store.dispatch(‘xxx’,payload);this.$store.commit(‘xxx’,payload);const {dispatch,commit} = this.$store;//这相当于创建变量,然后把this.$store的dispatch与commit赋值给它们。//有经验的都应该知道,函数dispatch和commit的this指向在严格模式下指向undefined。// 非严格模式下指向window,// 刚才的源码中我们也看到了,dispatch和commit都依赖于Store实例。怎么办??解决方法如下:dispatch和commit是Store原型链上的方法,在constructor内注册了构造函数内的方法,把原型上的dispatch和commit进行了一个this指向的强制绑定,通过call让两个方法永远指向Store的实例上,保证了dispatch和commit不会因为赋值给其余变量等操作导致this指向改变从而发生问题action与mutation函数的参数都有哪些?怎么来的?看一个简单的mutation:export const setName = function(state,payload) { state.name = payload;};这个时候不经意间有了一个疑惑?state哪里来的。这就要从mutation被注册的函数内找原因了handle是我们要被注册的一个mutation,entry是这个同名mutation的容器(存储所有的这个名字的mutation,一般只有一个)在吧handle放入entry的过程中,我们发现,entry是被一个函数包裹起来,然后将local.store和payload绑定到这个handle的参数上,然后把handle的this指向锁定到了Store实例上,所以mutation在被commit调用的时候只传入了一个参数payload,但是mutation函数执行的时候就有了两个参数。下面看一下action:按照刚才的分析action在被dispatch调用的时候会接收一个参数,但是action执行的时候会接收两个参数,第一个参数是个对象里面有六项,真的是包罗万象啊。。。我们看一下这个对象的六项{ dispatch : local.dispatch, commit:local.commit, getter: local.getters, state: local.state, rootGetters:store.getters, rootState:store.state}分为两种一种是local的、一种是store的。mutation中好像也有使用local,那么local的意义是什么呢?我们下一节会讲述local的含义以及makeLocalContext、makeLocalGetters两个函数的作用。还是要给个小线索,在模块树的层级很高的时候,我们在使用state的时候要一层一层找寻吗?总结dispatch与commit在执行的时候,都是根据传入的全名action、mutation去Store实例的_actions、_mutations中找到对应的方法,进行执行的。dispatch和commit中都使用了this(指向Store实例),为了防止this的指向改变从而出现问题,就把原型上的dispatch与commit在constructor中处理到了实例本身,这个过程做了this指向的绑定(call)。action和mutation在执行的时候,参数都是在注册的时候就绑定了一部分,所以action与mutation在使用的时候可以访问到state等很多内容。下一章;离开action与mutation 来讨论一下local的含义以及makeLocalContext、makeLocalGetters两个函数的作用我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~

April 1, 2019 · 1 min · jiezi

Vuex源码学习(六)action和mutation如何被调用的(前置准备篇)

module与moduleCollection你一定要会啊!Vuex源码学习(五)加工后的module在组件中使用vuex的dispatch和commit的时候,我们只要把action、mutation注册好,通过dispatch、commit调用一下方法名就可以做到。使用方式vue组件内//in vue componentthis.$store.commit(‘setName’,{name : ‘xLemon’});this.$store.commit(’list/setName’,{name : ‘xLemon’});vuex的mutation中// in mutation.jsexport const setName = function(state,payload){ state.name = payload.name;}// in list mutation.js 在名为list的模块下的mutation(有自己的命名空间)export const setName = function(state,payload){ state.name = payload.name;}我们传递的只是一个字符串,commit是如何找到注册mutation时的同名方法的呢?有命名空间的这种mutation是如何被找到并且执行的呢?上源码属性的意义_actions用于存放所有注册的action_mutations用于存放所有注册的mutation被注册的action和mutation如何被放到对应的属性中的呢?轮到installModule函数要出马了。installModule的意义是初始化根模块然后递归的初始化所有模块,并且收集模块树的所有getters、actions、mutations、以及state。看一下installModule的代码,installModule并不是在类的原型上,并不暴露出来,属于一个私有的方法,接收五个参数。store(Vuex.store的实例对象。rootState (根结点的state数据)。path 被初始化模块的path(前两张讲过path的意义)。module 被初始化的模块。hot 热更新(并不关键)function installModule (store, rootState, path, module, hot) { const isRoot = !path.length // 获取全名 const namespace = store._modules.getNamespace(path) // register in namespace map if (module.namespaced) { // 设置命名空间对照map store._modulesNamespaceMap[namespace] = module //console.log(store._modulesNamespaceMap); } // set state if (!isRoot && !hot) { const parentState = getNestedState(rootState, path.slice(0, -1)) const moduleName = path[path.length - 1] // 把子模块的state(数据)绑定到父模块上(按照层级) store._withCommit(() => { Vue.set(parentState, moduleName, module.state) }) } const local = module.context = makeLocalContext(store, namespace, path) // 使用模块暴露出来的方法来注册mutation、action、getter module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local) }) module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) }) module.forEachGetter((getter, key) => { const namespacedType = namespace + key registerGetter(store, namespacedType, getter, local) }) module.forEachChild((child, key) => { installModule(store, rootState, path.concat(key), child, hot) })}这个函数虽然只有40多行,但处于一个承上启下的关键点,这一章只会分析如何收集mutation与action其余的内容会再下一章讲述。installModule首先获取一下这个模块的命名(我称之为全名)依赖_modules(ModuleCollection实例对象)的getNamespace方法。根据模块的path,path有从根结点到当前节点这条路径上按顺序排序的所有模块名(根结点没有模块名,并没有设置在path,所以根模块注册时是个空数组,他的子模块的path就是[子模块的名字])。那么Vuex如何整理模块名的呢?效果:如果这个模块有自己的命名空间(namespaced为true)这个模块的全名就是父模块的全名+自己的模块名,如果这个模块没有自己的命名空间(namespaced为false)这个模块的全名就是父模块的全名为什么会是这样?分析一下代码getNamespace (path) { let module = this.root //根模块 return path.reduce((namespace, key) => { //根模块的path是个空数组不执行 // path的第一项是根模块的儿子模块。 // 获取儿子模块 并且将替换module (path的下一项就是儿子模块中的子模块了) // 下次累加 就是这个module(轮到子模块了)去找它(子模块)的子模块 module = module.getChild(key) // 查看儿子模块是不是设置了命名空间 //如果设置了这个模块的全名就增加自己的模块名和一个’/‘分割后面的模块名, //没有的话返回一个’’, // reduce累加可以把这个名称进行累加 return namespace + (module.namespaced ? key + ‘/’ : ‘’) }, ‘’)}获取完模块的全名了,之后我们看一下这两个函数module.forEachActionmodule.forEachMutation在上一章节module提供了遍历自己内部的action、mutation的方法。 module.forEachMutation((mutation, key) => { const namespacedType = namespace + key registerMutation(store, namespacedType, mutation, local)})module.forEachAction((action, key) => { const type = action.root ? key : namespace + key const handler = action.handler || action registerAction(store, type, handler, local) })const namespacedType = namespace + key 这句话 就是拼接出真正的mutation、action的名字模块全名+mutation/action的名字。也就是一开始我举例的list/setName是这个mutation的全名(被调用的时候用)this.$store.commit(’list/setName’,{name : ‘xLemon’});名称已经获取到了,下一步怎么办?把这些函数按照对应名字放到之前说的_actions、_mutations属性中啊看一下这个名字的mutation有没有被注册过,没有就声明一下,然后push进去。如果这个名字的mutation被注册过,就push进去。action同理小彩蛋 设置两个不同模块的同名mutation(全名一样哦)这两个mutation都会执行,action也是一样的。总结action和mutation在被dispatch和commit调用前,首先遍历模块树获取每个模块的全名。把模块内的action和mutation加上模块全名,整理成一个全新的名字放入_actions 、 _mutations属性中。dispacth和commit如何调用aciton和mutation 的将在下章讲述下一章:我们讨论action和mutation如何被调用的(调用篇)。我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 31, 2019 · 2 min · jiezi

Vuex源码学习(五)加工后的module

没有看过moduleCollection那可不行!Vuex源码学习(四)module与moduleCollection代码块和截图的区别代码块部分希望大家按照我的引导一行行认真的读代码的截图是希望大家能记住图中的结构与方法,下面会对整体进行一个分析,而不会一行一行的分析。但是以后的文章会更偏向于使用代码块,希望大家喜欢。上一章我们讲述了ModuleCollection类的作用,帮助我们把伪(未加工的)模块变成真正的模块,然后把每个模块按照父子与兄弟关系链接起来。那么真正的模块相比于伪(未加工的)模块多了哪些能力呢?module提供的方法这是module暴露出来的所有方法,以及一个属性。先看一下constructorconstructor (rawModule, runtime) { this.runtime = runtime // Store some children item // 创建一个容器存放该模块所有的子模块 this._children = Object.create(null) // Store the origin module object which passed by programmer // 存放自己未被加工的模块内容。 this._rawModule = rawModule const rawState = rawModule.state // Store the origin module‘s state // 创建这个模块的数据容器 this.state = (typeof rawState === ‘function’ ? rawState() rawState) || {}}模块的初始化主要是做了以下三件事情创建_children属性用于存放子模块创建_rawModule属性存储自己模块的伪(未被加工)模块时的内容创建state属性存储自己模块的数据内容 每个模块都有自己的state。模块的初始化并没有做什么事情,模块提供的方法和属性才是它的核心,模块提供了一个namespaced的属性,以及很多方法,我将模块提供的方法分成两类。先说属性get namespaced () { // 获取模块的namespaced属性 确定这个模块有没有自己的命名空间 return !!this._rawModule.namespaced}判断是否有命名空间有什么用?在以后设置getters、mutation、actions时有很大作用,以后再讲。再说方法模块提供的所有方法都是为了给外部的调用,这些方法没有一个是让模块在自己的内部使用的。所以我把方法划分的纬度是,按照这个方法是用于构建模块树还是用于抽取模块中的内容,构建模块树的方法:1.addChild:给模块添加子模块。addChild (key, module) { this._children[key] = module}这个方法实现上很简单,它是在哪里被调用的呢?大家可以翻开上一章的moduleCollection的内容,在ModuleCollection中完成模块之间的链接,就是使用这个方法给父模块添加子模块。removeChild:移除子模块 Vuex初始化的时候未使用,但可以给你提供灵活的处理模块的能力removeChild (key) { delete this._children[key]}getChild:获取子模块 获取子模块的意义是什么?在以后配置模块的名字时,需要获取模块的是否设置了命名空间,获取命名空间的属性模块提供了,再提供一个获取子模块就都Ok了getChild (key) { return this._children[key]}updateChild:更新模块的_ra wModule属性(更新模块的未加工前的模块内容),Vuex中未使用update (rawModule) { this._rawModule.namespaced = rawModule.namespaced if (rawModule.actions) { this._rawModule.actions = rawModule.actions } if (rawModule.mutations) { this._rawModule.mutations = rawModule.mutations } if (rawModule.getters) { this._rawModule.getters = rawModule.getters }}Vuex在链接与整合模块的时候使用了其中两个方法,addChild、getChild。类ModuleCollection在链接时需要找到模块(getChild)然后给模块添加子模块(addChild)的功能,所以这两个方法是在整合模块时最重要的。抽取模块中的内容上面的一组方法,是为了更好的完成模块的链接,给散落的单一模块整理成一个模块树可以提供便捷的封装方法,下面要说的方法什么叫做抽取模块中的内容?将这些方法暴露给外面可以方便的去获取这个模块内的一些内容来使用。forEachValue是Vuex封装的工具方法,用于遍历对象的。export function forEachValue (obj, fn) { Object.keys(obj).forEach(key => fn(obj[key], key))}这四个方法作用:forEachChild : 遍历模块的子模块forEachGetter : 遍历模块中_rawModule.getters 这块就应该知道 _rawModule的作用了,我把模块未加工时会有getters属性,存放这个模块所有的getters方法(vuex的基本用法就不多讲了),然后遍历,forEachMutation : 和forEachGetter类似,只是换成了遍历mutationsforEachAction : 和forEachGetter类似,只是换成了遍历actions这四个方法就是遍历这些内容,有意义吗?意义很大,目前_rawModule上这些getters、mutations、actions属性并不会生效,只是单纯的一个属性,如何让他们可以成为那种,被dispatch、commit使用的那种方法呢?先给大家一个小提示,mutations、actions都是要被注册的,注册之前总要获取到这些内容,具体的实现方式后面的章节会详细讲述,总结加工后真正的module(我们称由Module这个类实例化出来为真正的module)只是缓存了一些内容,并且给外界提供了一堆方便高效的方法。这些方便高效的方法为之后的注册action、mutation。整合state都起了很关键的作用。所以说module这个小单元为下面的代码提供了很大便利,额外思考我们对一段内容需要频繁的处理并且处理方式大同小异的时候,是不是可以像module一样整理成一个对象,然后给外界提供一些方法。(有一种面向对象思想)下一章讲述action和mutation是如何调用的我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 27, 2019 · 1 min · jiezi

Vuex源码学习(四)module与moduleCollection

如果你还不知道Vuex是怎么安装的,请移步Vuex源码学习(三)install都做了哪些事情整合模块这一节该分析模块的是怎么被整合的,以及要整合成什么样子。在Vuex的constructor中比较靠前的位置有这么两行代码,_modules属性是ModuleCollection的实例对象,之后 _modules属性被频繁使用,这块就是对Vuex的模块进行了一次整合,整合出一个可以被使用的 _modules,而_moduleNamespaceMap是一个空对象该怎么整合模块?先看一下我们我们项目中Store的结构store/index.jsmoduleList:moduleSet:结构就是这样的在以上代码中的modules下的数据,我都称它是伪(未加工)模块,因为它还不具有模块的功能。当我们实例化Vuex.Store这个类的时候接收的参数options就会直接交给moduleCollection来处理。参数options是什么呢?就是上面图中这样结构的数据,想要处理成什么样子?下面看一下ModuleCollection是怎么处理的export default class ModuleCollection { constructor (rawRootModule) { // register root module (Vuex.Store options) // 注册模块并链接 this.register([], rawRootModule, false) } … register (path, rawModule, runtime = true) { if (process.env.NODE_ENV !== ‘production’) { // 不符合规则的模块会报错。 assertRawModule(path, rawModule) } // 创建一个模块 const newModule = new Module(rawModule, runtime) if (path.length === 0) { this.root = newModule } else { // path.slice(0,-1)就可以拿到父模块的path。 // get方法可以根据path来找到对应的模块。 const parent = this.get(path.slice(0, -1)) // 将子模块挂载到父模块上 parent.addChild(path[path.length - 1], newModule) } // register nested modules if (rawModule.modules) { // 遍历每个模块的modules(目的是获取所有子模块) forEachValue(rawModule.modules, (rawChildModule, key) => { // 为什么要path.concat(key)? // 依次注册子模块。 this.register(path.concat(key), rawChildModule, runtime) }) } }}在Vuex与vue-router的源码中,命名变量是很有规律的,在开发人员使用这两个框架的时候,传递进去的参数,在使用时命名的变量名都是raw开头的,代表是未经过加工的。将未经过加工的伪模块处理成真正可以使用的模块。在初始化的时候直接开始注册模块,moduleCollection的这个类的任务是把生成的模块合理的链接起来,而模块的生成交给了Module这个类。所以register方法就是把根模块以及所有的子模块从一个伪(未加工)模块变成一个真正的模块并且链接起来。遍历树形结构用什么方法? 递归!register都做了什么?筛选出不符合规则的模块,报错提示。将伪(未加工)模块加工成一个真正的模块。将加工好的模块挂载在它的父模块上。如果这个模块有modules属性(模块有自己的子模块)让每个子模块重复以上操作递归的出口:rawModule.modules为false(模块没有子模块) ,也就是每个模块都没有子模块需要注册了,那就代表全部加工与链接完毕。分析register的三个参数register接收三个参数,path、rawModule、hot。hot这个参数目前看来不关键。rawModule是伪(未加工)模块那path的作用是什么呢?path的作用很大,大家类比下前端页面的dom树的Xpath,如果我想知道这个节点的位置,需要知道这个父节点的位置,然后一层一层的向上知道根结点,有了Xpath就可以直接找到这个节点,Vuex也是一样的想知道某个模块的位置,只需要提供根结点到他的一个path,path按顺序存储着根模块到它本身的所有祖先模块(根模块没有名字,又不能把第一个放一个空,所以path里 面没有根模块),在每次注册的时候,这个模块有子模块,就把它的path加上(concat)子模块的名字,在子模块执行register方法时,path就比它的父模块多一个父模块的名字,所以根模块注册的时候传入path就是[](空数组)了。ModuleCollection的get方法可以根据path来获取指定的模块,在挂载的时候十分有用,,使用reduce的方法,按照数组的顺序,一层一层的找目标模块。path对以后要讲的设置命名空间也很有帮助。总结ModuleCollection这个类,主要完成了模块的链接与整合,生成模块的任务交给了Module这个类。模块的链接与整合通过递归完成。path可以让moduleCollection快速找到对应模块。下一章讲述生成的module具体可以做什么我是一个应届生,最近和朋友们维护了一个公众号,内容是我们在从应届生过渡到开发这一路所踩过的坑,已经我们一步步学习的记录,如果感兴趣的朋友可以关注一下,一同加油~ ...

March 26, 2019 · 1 min · jiezi

揭开redux,react-redux的神秘面纱

16年开始使用react-redux,迄今也已两年多。这时候再来阅读和读懂redux/react-redux源码,虽已没有当初的新鲜感,但依然觉得略有收获。把要点简单写下来,一方面供感兴趣的读者参考,另一方面也是自己做下总结。reduxreact-redux最核心的内容就是redux。内带redux,react-redux只提供了几个API来关联redux与react的组件以及react state的更新。首先,看下如何使用redux。 redux老司机可以直接滑动滚轮至下一章。 简单来说,redux有三个概念,action, reducer 和 dispatch。 action和dispatch比较好理解:动作指令和提交动作指令方法。而reducer,个人在字面上没有理解,但抽象层面上可以理解为用来生成state的函数。用一个简单案例体现这三个概念:// actionconst INCREMENT = { type: ‘INCREMENT’ }// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// dispatch// 此处开始使用reduxconst store = redux.createStore( count )console.log( store.getState() ) // 0store.dispatch( INCREMENT )console.log( store.getState() ) // 1接下来说说redux中的两大模块:store对象中间件store对象APIcreateStore会创建了一个store对象,创建的过程中它主要做了下面两件事:初始化state暴露相关接口:getState(), dispatch( action ), subscribe( listener )等。其中getState()用来获取store中的实时state, dispatch(action)根据传入的action更新state, subscribe( listener)可以监听state的变化。中间件中间件可以用来debug或提交异步动作指令. 在初始化store的时候,我们通过createStore( reducer, state, applyMiddleware( middleware1, middleware2 ) )添加多个中间件。 为了实现多个中间件,redux专门引入了函数式编程的compose()方法,简单来说,compose将多层函数调用的写法变得优雅:// 未使用compose方法a( b( c( ’d’ ) ) )// 用compose方法compose( a, b, c )(’d’)而中间件的写法比较奇特,是多级函数,在阅读源码的时候有点绕。显然中间件的写法还可以优化,尽管现在的写法方便在源码中使用,但对redux用户来说稍显复杂,可以用单层函数。function logMiddleware({ getState }) { return nextDispatch => action => { console.log( ‘before dispatch’, getState() ) const res = nextDispatch( action ) console.log( ‘after dispatch’, getState() ) return res }}react-redux了解了redux运作原理,就可以知道react-redux的大部分使用场景是如何运作。react-redux提供了几个API将redux与react相互关联。基于上一个案例展示react-redux的用法:// actionconst increment = () => ({ type: ‘INCREMENT’ })// reducerfunction count( state = 0, action ) { switch( action.type ) { case ‘INCREMENT’: return state + 1 default: return state }}// reduxconst store = Redux.createStore( count )// react-reduxconst { Provider, connect } = ReactReduxconst mapStateToProps = state => ( { count: state } )const mapDispatchToProps = dispatch => ( { increment : () => dispatch( increment() ) } )const App = connect( mapStateToProps, mapDispatchToProps )( class extends React.Component { onClick = () => { this.props.increment() } render() { return <div> <p>Count: { this.props.count }</p> <button onClick={ this.onClick }>+</button> </div> }} )ReactDOM.render( <Provider store={ store }> <App /></Provider>, document.getElementById( ‘app’ ) )点击运行案例react-redux提供最常用的两个API是:ProviderconnectProviderProvider本质上是一个react组件,通过react的context api(使一个组件可以跨多级组件传递props)挂载redux store中的state,并且当组件初始化后开始监听state。当监听到state改变,Provider会重新setState在context上的storeState,简要实现代码如下:class Provider extends Component { constructor(props) { super(props) const { store } = props this.state = { storeState: Redux.store.getState(), } } componentDidMount() { this.subscribe() } subscribe() { const { store } = this.props store.subscribe(() => { const newStoreState = store.getState() this.setState(providerState => { return { storeState: newStoreState } }) }) } render() { const Context = React.createContext(null) <Context.Provider value={this.state}> {this.props.children} </Context.Provider> }}connect()connect方法通过connectHOC(HOC: react高阶组件)将部分或所有state以及提交动作指令方法赋值给react组件的props。小结写react不用redux就像写代码不用git, 我们需要用redux来更好地管理react应用中的state。了解redux/react-redux的运作原理会消除我们在使用redux开发时的未知和疑惑,并且在脑中有一个完整的代码执行回路,让开发流程变得透明,直观。 如果本文帮助到了你,我也十分荣幸, 欢迎点赞和收藏。如果有任何疑问或者建议,都欢迎在下方评论区提出。 ...

December 18, 2018 · 2 min · jiezi

[源码阅读]纯粹极简的react状态管理组件unstated

简介unstated是一个极简的状态管理组件看它的简介:State so simple, it goes without saying对比对比redux:更加灵活(相对的缺点是缺少规则,需要使用者的自觉)redux的状态是存放在一棵树内,采用严格的单向流unstated的状态是用户自己定义,说白了就是object,可以放在一个组件的内,也可以放在多个组件内针对React,一致的APIredux必须编写reducer和action,通过dispatch(action)改变状态,它不限框架unstated改变状态的API完全与React一致,使用this.setState,当然和React的setState不同,但是它的底层也是用到了setState去更新视图功能相对简单unstated没有中间件功能,每次状态改变(不管是否相等),都会重新渲染(V2.1.1)可以自定义listener,每次更新状态时都会执行。对比React的自带state:天生将组件分割为Container(状态管理)和Component(视图管理)灵活配置共享状态或者私有状态支持promise快速了解请直接跳到总结初识3大板块和几个关键变量Provider: 注入状态实例,传递map,本质是Context.Provider,可嵌套达成链式传递Container: 状态管理类,遵循React的API,发布订阅模式,通过new生成状态管理实例Subscribe: 订阅状态组件,本质是Context.Consumer,接收Provider提供的map,视图渲染组件map: new Map(),通过类查找当前类创建的状态管理实例深入这里引入官方例子// @flowimport React from ‘react’;import { render } from ‘react-dom’;import { Provider, Subscribe, Container } from ‘unstated’;type CounterState = { count: number};// 定义一个状态管理类class CounterContainer extends Container<CounterState> { state = { count: 0 }; increment() { this.setState({ count: this.state.count + 1 }); } decrement() { this.setState({ count: this.state.count - 1 }); }}// 渲染视图组件(Context.Consumer的模式)function Counter() { return ( <Subscribe to={[CounterContainer]}> {counter => ( <div> <button onClick={() => counter.decrement()}>-</button> <span>{counter.state.count}</span> <button onClick={() => counter.increment()}>+</button> </div> )} </Subscribe> );}render( <Provider> <Counter /> </Provider>, document.getElementById(‘root’));这里Counter是我们自定义的视图组件,首先使用<Provider>包裹,接着在Counter内部,调用<Subscribe>组件,传递一个数组给props.to,这个数组内存放了Counter组件需要使用的状态管理类(此处也可传递状态管理实例)。Providerexport function Provider(props: ProviderProps) { return ( <StateContext.Consumer> {parentMap => { let childMap = new Map(parentMap); // 外部注入的状态管理实例 if (props.inject) { props.inject.forEach(instance => { childMap.set(instance.constructor, instance); }); } // 负责将childMap传递,初始为null return ( <StateContext.Provider value={childMap}> {props.children} </StateContext.Provider> ); }} </StateContext.Consumer> );}这里的模式是<Consumer> ()=>{ /* … / return <Provider>{props.children}<Provider /> }</Consumer> 有3个注意点:外层嵌套<Consumer>可以嵌套调用。<Provider value={…}> / … / <Provider value={此处继承了上面的value}> / … / </Provider>props.inject可以注入现成的状态管理实例,添加到map之中。返回值写成props.children。返回值写成props.children的意义简单一句话概括,这么写可以避免React.Context改变导致子组件的重复渲染。具体看这里:避免React Context导致的重复渲染Containerexport class Container<State: {}> { // 保存状态 默认为{} state: State; // 保存监听函数,默认为[] _listeners: Array<Listener> = []; setState( updater: $Shape<State> | ((prevState: $Shape<State>) => $Shape<State>), callback?: () => void ): Promise<void> { return Promise.resolve().then(() => { let nextState; / 利用Object.assign改变state / // 执行listener(promise) let promises = this._listeners.map(listener => listener()); // 所有Promise执行完毕 return Promise.all(promises).then(() => { // 全部listener执行完毕,执行回调 if (callback) { return callback(); } }); }); } // 增加订阅(这里默认的订阅就是React的setState空值(为了重新渲染),也可以添加自定义监听函数) subscribe(fn: Listener) { this._listeners.push(fn); } // 取消订阅 unsubscribe(fn: Listener) { this._listeners = this._listeners.filter(f => f !== fn); }}Container内部逻辑很简单,改变state,执行监听函数。其中有一个_listeners,是用于存放监听函数的。每个状态管理实例存在一个默认监听函数onUpdate,这个默认的监听函数的作用就是调用React的setState强制视图重新渲染。这里的监听函数内部返回Promise,最后通过Promise.all确保执行完毕,然后执行回调参数。因此setState在外面使用也可以使用then。例如,在官方例子中:increment() { this.setState({ count: this.state.count + 1 },()=>console.log(‘2’)) .then(()=>console.log(‘3’) ) console.log(‘1’) } // 执行顺序是 1 -> 2 ->32个注意点:setState和React API一致,第一个参数传入object或者function,第二个传入回调这里通过Promise.resolve().then模拟this.setState的异步执行关于Promise.resolve和setTimeout的区别简单的说两者都是异步调用,Promise更快执行。setTimeout(()=>{},0)会放入下一个新的任务队列Promise.resolve().then({})会放入微任务,在调用栈为空时立刻补充调用栈并执行(简单理解为当前任务队列尾部)更多详细可以看这里提供的2个视频:https://stackoverflow.com/a/38752743Subscribeexport class Subscribe<Containers: ContainersType> extends React.Component< SubscribeProps<Containers>, SubscribeState> { state = {}; // 存放传入的状态组件 instances: Array<ContainerType> = []; unmounted = false; componentWillUnmount() { this.unmounted = true; this._unsubscribe(); } _unsubscribe() { this.instances.forEach(container => { // container为当前组件的每一个状态管理实例 // 删除listeners中的this.onUpdate container.unsubscribe(this.onUpdate); }); } onUpdate: Listener = () => { return new Promise(resolve => { // 组件未被卸载 if (!this.unmounted) { // 纯粹是为了让React更新组件 this.setState(DUMMY_STATE, resolve); } else { // 已经被卸载则直接返回 resolve(); } }); }; / … /}这里的关键就是instances,用于存放当前组件的状态管理实例。当组件unmount的时候,会unsubscribe当前状态管理实例的默认监听函数,那么如果当前的状态管理实例是共享的,会不会有影响呢?不会的。往后看可以知道,当state每次更新,都会重新创建新的状态管理实例(因为props.to的值可能会发生变化,例如取消某一个状态管理实例),而每次创建时,都会先unsubscribe再subscribe,确保不会重复添加监听函数。onUpdate就是创建状态管理组件时默认传递的监听函数,用的是React的setState更新一个DUMMY_STATE(空对象{})。export class Subscribe<Containers: ContainersType> extends React.Component< SubscribeProps<Containers>, SubscribeState> { / 上面已讲 / _createInstances( map: ContainerMapType | null, containers: ContainersType ): Array<ContainerType> { // 首先全部instances解除订阅 this._unsubscribe(); // 必须存在map 必须被Provider包裹才会有map if (map === null) { throw new Error( ‘You must wrap your <Subscribe> components with a <Provider>’ ); } let safeMap = map; // 重新定义当前组件的状态管理组件(根据to传入的数组) let instances = containers.map(ContainerItem => { let instance; // 传入的是Container组件,则使用 if ( typeof ContainerItem === ‘object’ && ContainerItem instanceof Container ) { instance = ContainerItem; } else { // 传入的不是Container,可能是其他自定义组件等等(需要用new执行),尝试获取 instance = safeMap.get(ContainerItem); // 不存在则以它为key,value是新的Container组件 if (!instance) { instance = new ContainerItem(); safeMap.set(ContainerItem, instance); } } // 先解绑再绑定,避免重复订阅 instance.unsubscribe(this.onUpdate); instance.subscribe(this.onUpdate); return instance; }); this.instances = instances; return instances; } / … /}在_createInstances内部,如果检查到传入的props.to的值已经是状态管理实例(私有状态组件),那么直接使用即可,如果传入的是类class(共享状态组件),会尝试通过查询map,不存在的则通过new创建。export class Subscribe<Containers: ContainersType> extends React.Component< SubscribeProps<Containers>, SubscribeState> { / 上面已讲 / render() { return ( <StateContext.Consumer> / Provider传递的map */ {map => // children是函数 this.props.children.apply( null, // 传给子函数的参数(传进当前组件的状态管理实例) this._createInstances(map, this.props.to) ) } </StateContext.Consumer> ); }}每一次render都会创建新的状态管理实例。到此,3大板块已经阅读完毕。总结简单易用,与React一致的API,一致的书写模式,让使用者很快上手。并没有规定如何管理这些状态管理类,非常灵活。我们可以学redux将所有状态放到一个共享状态管理实例内部,例如通过Provider的inject属性注入,或者针对每一个组件创建单独的状态管理实例(可共享可独立)(unstated作者推荐),一切可以按照自己的想法,但同时也要求使用者自己定义一些规则去约束写法。仅仅是管理了状态,每次更新都是一个全新的instance集合,并没有做任何对比,需要我们在视图层自己实现。返回值写成props.children的意义。关于Promise.resolve().then({})和setTimeout(()=>{},0)的区别。导图源码阅读专栏对一些中小型热门项目进行源码阅读和分析,对其整体做出导图,以便快速了解内部关系及执行顺序。当前源码(带注释),以及更多源码阅读内容:https://github.com/stonehank/sourcecode-analysis,欢迎fork,求 ...

December 8, 2018 · 3 min · jiezi