dynamic-object 只对外暴露了三个 api:observable
observe
Action
,分别是 动态化对象、 变化监听 与 懒追踪辅助函数。javascript
下面以开发角度描述实现思路,同时做为反思,若是有更优的思路,我会随时更新。java
本库包含许多抽象概念,为了简化描述,使用固定单词指代,约定以下:react
单词 | 含义 |
---|---|
observable | dynamic-object 提供的最重要功能,将对象动态化的函数 |
observe | 监听其回调函数中当前访问到的 observable 化的对象的修改,并在值变化时从新出发执行 |
observer | 指代 observe 中的回调函数 |
若是单纯的实现 observable
,使用 proxy 很简单,能够彻底监听对象的变化,难点在于如何在 observe
中执行依赖追踪,并当 observable
对象触发 set
时,触发对应 observe
中的 observer
。git
每一个 observable
对象触发 get
时,都将当前所在的 object
+ key
与当前 observer
对应关系存储起来,当其被 set
时,拿到对应的 observer
执行便可。github
咱们必须依赖持久化变量才能作到这一点,由于 observable
的 set
过程,与 observer
的 get
的过程是分开的。typescript
变量名 | 类型 | 含义 |
---|---|---|
proxies | WeakMap | 全部代理对象都存储于此,当重复执行 observable 或访问对象子属性时,若是已是 proxy 就从 proxies 中取出返回 |
observers | WeakMap | 任何对象的 key 只要被 get ,就会被记录在这里,同时记录当前的 observer ,当任意对象被 set 时,根据此 map 查询全部绑定的 observer 并执行,就达到 observe 的效果了 |
currentObserver | Observer | 当前的 observer 。当执行 observe 时,当前 observer 指向其第一个回调函数,这样当代理被访问时,保证其绑定的 observer 是其当前所在的回调函数。 |
对于 observable(obj)
,按照如下步骤分析:redux
若是传入的 obj
自己已经是 proxy
,也就是存在于 proxies
,直接返回 proxies.get(obj)
。这种状况考虑到可能将对象 observable
执行了屡次。(proxies
保存原对象与代理各一份,保证传入的是已代理的原对象,仍是代理自己,均可以被查找到)api
若是没有重复,new Proxy
生成代理返做为返回值。代理涉及到三处监听处理:get
set
deleteProperty
。缓存
get(target, key, receiver)复制代码
先判断 currentObserver
是否为空,若是为空,说明是在 observer
以外访问了对象,此时不作理会。babel
若是 currentObserver
不为空,将 object
+ key
-> currentObserver
的映射记录到 observers
对象中。同时为 currentObserver.observedKeys
添加当前的映射引用,当 unobserve
时,须要读取 observer.observedKeys
属性,将 observers
中全部此 observer
的依赖关系删除。
最后,若是 get
取的值不是对象(typeof obj !== "object"
),那么是基本类型,直接返回便可。若是是对象,那么:
proxies
存在,直接返回 proxy
引用。eg: const name = obj.name
,这时 name
变量也是一个代理,其依赖也可追踪。proxies
不存在,将这个对象从新按照如上流程处理一遍,这就是惰性代理,好比访问到 a.b.c
,那么会分别将 a
b
c
各走一遍 get 处理,这样不管其中哪一环,都是代理对象,可追踪,相反,若是 a
对象还存在其余字段,由于没有被访问到,因此不会进行处理,其值也不是代理,由于没有访问的对象也不必追踪。set(target, key, value, receiver)复制代码
若是新值与旧值不一样,或 key === "length"
时,就认为产生了变化,找到当前 object
+ key
对应的 observers
队列依次执行便可。有两个注意点:
observer
绑定关系清空:由于 observer
时会触发新一轮绑定,这样实现了条件的动态绑定。currentObserver
为当前 observer
,再执行 observer
时就能够将 set
正确绑定上。删除属性时,直接触发对应 observer
。
这些类型的特色是有明确封装方法,其实更容易设置追踪,此次不使用 proxy,而是复写这些对象的方法,在 get
set
中加上钩子。
马上执行当前回调 observer
,执行规则与 4.4 小节的 observers
队列执行机制相同。
有人会有疑惑,为何 observe
要当即执行内部回调呢?若是初始化不不输出,结果可能会好看一些:
import { observable, observe } from "dynamic-object"
const dynamicObj = observable({
a: 1
})
observe(() => {
console.log('a:', dynamicObj.a)
})
dynamicObj.a = 2复制代码
以上会输出两次,分别是 a: 1
和 a: 2
。另外,可能会以为这样与 react 结合,会不会致使初始化时增长没必要要的渲染?
这两个都是很好的问题,但结论是:初始化执行是必要的:
observer
是什么(除非作静态分析,但稍稍复杂些就不可能了)。render
函数,将初始化的 observe
绑定与后续 render
函数分离,达到首次 render 是 observe
初始化触发,后续 render 依靠依赖追踪自动触发 的效果,在 dynamic-react
章节会有深刻介绍。Action
是用于写标准 action 的装饰器,有如下两种写法:
@Action setUserName() {..}
Action(setUserName)复制代码
起做用是将回调函数中发生的变动临时存储起来,当执行完时统一触发,而且同一个 observer
的屡次 set
行为只会触发一次,而且执行时,获取到的是最终值,全部值的中间变化过程都会被忽略。
好比: 当 dynamicObj.a
初始值为 1 时,下面的代码不会触发 observer
执行:
Action(()=> {
dynamicObj.a = 2
dynamicObj.a = 1
})复制代码
要达到上面效果,须要额外定义一个持久化变量 trackingDeep
,每次 Action
执行时,这个变量先自增 1,执行 observer
时,若是 trackingDeep
不为 0,就把 observer
存储在队列中,当回调函数执行完后,深度减 1,开始执行存储的队列,一样,若是深度不为 1 就跳过,深度为 0 就执行。
咱们假象这种场景:
class Test {
@Action setUser(info) {
this.userStore.account = info.account
this.setName(info.name)
}
@Action setName(name) {
this.userStore.name = name
}
}复制代码
当调用 setUser
时,其内部又调用了 setName
,那么执行 setUser
时,trackingDeep
为 1,以后又执行到 setName
使得 trackingDeep
变成 2,内层 Action
执行完毕,trackingDeep
变回 1,此时队列不会执行,调用栈回退到 setName
后,trackingDeep
终于变成 0,队列执行,此时observer
仅触发了一次。
Tips: 这里有个优化点,当
trackingDeep
不为 0 时,终止dynamic-object
的依赖收集行为。这么作的好处是,当 react render 函数中,同步调用 action 时,不会绑定到这个 action 用到的变量。
Action
的概念存在一个严重的缺点(但不致命),同时也是 mobx
库一直没有解决的问题,那就是对于异步 action 迫不得已(除非为异步 action 分段使用 Action
,这也是 mobx 官方推荐的方式,也有 babel 插件来解决,但这样很 hack)。
咱们思考以下代码:
class Test {
@Action async getUser() {
this.isLoading = true
const result = await fetch()
this.isLoading = false
this.user = result
}
}复制代码
首先咱们不但愿它是忽略中间态的,不然初始将 isLoading
设置为 true 就没有意义了。
比较好的途径是,将这个异步 action 触发的 observer
塞入到队列中,每当遇到 await
就执行并清空队列,同时还能够支持 timeout
设定,好比设置为 100ms 时,若是 fetch 函数在 100ms 内执行完毕,就不会执行以前的队列,达到肉眼没法识别的间隔内不触发 loading 的效果。
理想很美好,惋惜难点不在如何实现如上的设定,而是咱们没办法将队列分隔开,考虑以下代码:
handleClick() {
this.props.Test.getUser()
this.props.Test.getArticle()
}复制代码
getUser
与 getArticle
都是异步的,若是咱们将缓存队列共用一个,那么 getArticle
执行到 await
时,顺便会邪恶的把 getUser
队列中 observer
给执行了,纵使 getUser
的 await
尚未结束(可能出现 loading 在数据还没加载完成就消失)。
有人说,将 getUser
与 getArticle
队列分开不就好了吗?是的,但目前 javascript 还作不到这一点,见此处讨论。不管是 defineProperty
仍是 proxy
,都没法在 set
触发时,知道本身是从哪一个闭包中被触发的。只知道触发的对象,以及被访问的 key,是没办法将 getUser
getArticle
放在不一样队列执行 observer
的。
目前个人作法与 mobx 同样,async
函数会打破 Action
的庇护,失去了收集后统一执行的特性,但保证了程序的正确运行。目前的解决方法是,为同步区域再套一层 Action
,或者干脆将异步与同步分开写!
说实话,这个问题被 redux 用概念巧妙规避了,咱们必须将这个函数拆成两个 dispatch。回头想一想,若是咱们也这么作,也彻底能够规避这个问题,拆成两个 action 便可!但我但愿有一天,能找到完美的解决方法。
另外但愿表达一点,redux 的成功在于定义了许多概念与规则,只要咱们遵照,就能写出维护性很棒的代码,其实 oo 思想也是同样!咱们在使用 oo 时,将对 fp 的耐心拿出来,同样能写出维护性很棒的代码。
dynamic-react 是 dynamic-object 在 react 上的应用,相似于 mobx-react 相比于 mobx。实现思路与 mobx-react 很接近,可是简化了许多。
dynamic-react 只暴露了两个接口 Provider
与 Connect
,分别用于 数据初始化 与 绑定更新与依赖注入
Provider 将接收到的全部参数全局透传到组件,所以实现很简单,将接收到的全部字段存在 context 中便可。
这个装饰器用于 react 组件,分别提供了绑定更新与依赖注入的功能。
因为 dynamic-react 是与 dynamic-object 结合使用的,所以会将全量 store 数据注入到 react 组件中,因为依赖追踪的特性,不会形成没必要要的渲染。
注入经过高阶组件方式,从 context 中取出 Provider 阶段注入的值,直接灌给自组件便可,注意组件自身的 props 须要覆盖注入数据:
export default function Connect(componentClass: any): any {
return class InjectWrapper extends React.Component<any, any>{
// 取 context
static contextTypes = {
dyStores: React.PropTypes.object
}
render() {
return React.createElement(componentClass, {
...this.context.dyStores,
...this.props,
})
}
}
}复制代码
见如上代码,咱们经过拿到当前子组件的实例:componentClass.prototype || componentClass
将其生命周期函数重写为,先执行自定义函数钩子,再执行其自身,并且自定义函数钩子绑定上当前 this
,能够在自定义勾子修改当前实例的任意字段,后续重写 render 也是依赖此实现的。
最重要阶段是在 willMount 生命周期完成的,由于对于 observer
来讲,只要在初始化时绑定了引用,以后更新都是从 observe
中自动触发的。
总体思路是复写 render 方法:
observe
包裹住原始 render 方法执行,所以绑定了依赖,将此时 render 结果直接返回便可。observe
自动触发的(或者 state、props 传参变化,这些无论),此时能够肯定是由数据流变更致使的刷新,所以能够调用 componentWillReact
生命周期。而后调用 forceUpdate
生命周期,由于重写了 render 的缘故,视图不会自动刷新。注意第一次调用时,不管如何会触发一次
observer
,为了忽略这次渲染,咱们设置一个是否渲染的 flag,当 observer 渲染了,普通 render 就再也不执行,由此避免observe
初始化一定执行一次带来初始渲染两次的问题。
在 componentWillUnmount
时 unobserve
掉当前组件的依赖追踪,给 shouldComponentUpdate
加上 pureRender,以及在 componentDidMount
与 componentDidUpdate
时通知 devTools 刷新,这里与 mobx-react 实现思路彻底一致。
最后给出 dynamic-object 的项目地址,欢迎提出建议和把玩。