对于 selectedKeys 来说,由于它是由页面路径( pathname )决定的,所以每一次 pathname 发生变化都需要重新计算 selectedKeys 的值。又因为通过 pathname 以及最基础的菜单数据 menuData 去计算 selectedKeys 是一件非常昂贵的事情(要做许多数据格式处理和计算),有没有什么办法可以优化一下这个过程呢?
Memoization 可以赋予普通函数记忆输出结果的功能,它会在每次调用函数之前检查传入的参数是否与之前执行过的参数完全相同,如果完全相同则直接返回上次计算过的结果,就像常用的缓存一样。
import memoize from 'memoize-one'; constructor(props) { super(props); this.fullPathMenuData = memoize(menuData => formatMenuPath(menuData)); this.selectedKeys = memoize((pathname, fullPathMenu) => ( getMeunMatchKeys(getFlatMenuKeys(fullPathMenu), urlToList(pathname)) )); const { pathname, menuData } = props; this.state = { openKeys: this.selectedKeys(pathname, this.fullPathMenuData(menuData)), }; }
在组件的构造器中我们可以根据当前 props 传来的 pathname 及 menuData 计算出当前的 selectedKeys 并将其当做 openKeys 的初始值初始化组件内部 state。因为 openKeys 是由用户所控制的,所以对于后续 openKeys 值的更新我们只需要配置相应的回调将其交给 Menu 组件控制即可。
import Menu from 'antd/lib/menu'; handleOpenChange = (openKeys) => { this.setState({ openKeys, }); }; <Menu style={{ padding: '16px 0', width: '100%' }} mode="inline" theme="dark" openKeys={openKeys} selectedKeys={this.selectedKeys(pathname, this.fullPathMenuData(menuData))} onOpenChange={this.handleOpenChange} > {this.renderMenu(this.fullPathMenuData(menuData))} </Menu>
这样我们就实现了对于 selectedKeys 及 openKeys 的分别管理,开发者在使用侧边栏组件时只需要将应用当前的页面路径同步到侧边栏组件中的 pathname 属性即可,侧边栏组件会自动处理相应的菜单高亮( selectedKeys )和多级菜单的打开与关闭( openKeys )。
4、知识点:正确区分 prop 与 state
上述这个场景也是一个非常经典的关于如何正确区分 prop 与 state 的例子。
selectedKeys 由传入的 pathname 决定,于是我们就可以将 selectedKeys 与 pathname 之间的转换关系封装在组件中,使用者只需要传入正确的 pathname 就可以获得相应的 selectedKeys 而不需要关心它们之间的转换是如何完成的。而 pathname 作为组件渲染所需的基础数据,组件无法从自身内部获得,所以就需要使用者通过 props 将其传入进来。
另一方面, openKeys 作为组件内部的 state,初始值可以由 pathname 计算而来,后续的更新则与组件外部的数据无关而是会根据用户的操作在组件内部完成,那么它就是一个 state,与其相关的所有逻辑都可以彻底地被封装在组件内部而不需要暴露给使用者。
简而言之,一个数据如果想成为 prop 就必须是组件内部无法获得的,而且在它成为了 prop 之后,所有可以根据它的值推导出来的数据都不再需要成为另外的 props,否则将违背 React 单一数据源的原则。对于 state 来说也是同样,如果一个数据想成为 state,那么它就不应该再能够被组件外部的值所改变,否则也会违背单一数据源的原则而导致组件的表现不可预测,产生难解的 bug。
5、组合式开发:应用菜单
严格来说,在这一小节中着重探讨的应用菜单部分的思路并不属于组合式开发思想的范畴,更多地是如何写出一个支持无限级子菜单及自动匹配当前路由的菜单组件。组件当然是可以随意插拔的,但前提是应用该组件的父级部分不依赖于组件所提供的信息。这也是我们在编写组件时所应当遵循的一个规范,即组件可以从外界获取信息并在此基础上进行组件内部的逻辑判断。但当组件向其外界抛出信息时,更多的时候应该是以回调的形式让调用者去主动触发,然后更新外部的数据再以 props 的形式传递给组件以达到更新组件的目的,而不是强制需要在外部再配置一个回调的接收函数去直接改变组件的内部状态。
从这点上来说,组合式开发与组件封装其实是有着异曲同工之妙的,关键都在于对内部状态的严格控制。不论一个模块或一个组件需要向外暴露多少接口,在它的内部都应该是解决了某一个或某几个具体问题的。就像工厂产品生产流水线上的一个环节,在经过了这一环节后产品相较于进入前一定产生了某种区别,不论是增加了某些功能还是被打上某些标签,产品一定会变得更利于下游合作者使用。更理想的情况则是即使删除掉了这一环节,原来这一环节的上下游依然可以无缝地衔接在一起继续工作,这就是我们所说的模块或者说组件的可插拔性。
六、后端路由服务的意义