Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

dob - 框架实现 #23

Open
ascoders opened this issue Dec 11, 2017 · 0 comments
Open

dob - 框架实现 #23

ascoders opened this issue Dec 11, 2017 · 0 comments

Comments

@ascoders
Copy link
Owner

ascoders commented Dec 11, 2017

本系列分三部曲:《框架实现》 《框架使用》 与 《跳出框架看哲学》,这三篇是我对数据流阶段性的总结,正好补充之前过时的文章。

本篇是 《框架实现》。

1 引言

我觉得数据流与框架的关系,有点像网络与人的关系。

在网络诞生前,人与人之间连接点较少,大部分消息都是通过人与人之间传递,虽然信息整体性不强,但信息在局部非常完备:当你想开一家门面,找到经验丰富的经理人,可以一手包办完。

网络诞生后,如果想通过纯网络的方式,学习如何开门面,如果不是对网络很熟悉,一时半会也难以学习到全套流程。

数据流对框架来说,就像网络对人一样,总是存在着模块功能的完备性与项目整体性的博弈。

全局性强了,对整体性强要求的项目(频繁交互数据)友好,顺便利于测试,因为不利于测试的 UI 与数据关系被抽离开了。

局部性强了,对弱关联的项目友好,这样任何模块都能不依赖全局数据,自己完成所有功能。

对数据流的研究,大多集中于 “优化在某些框架的用法” “基于场景改良” “优化全局与局部数据流间关系” “函数式与面向对象之争” “对输入抽象” “数据格式转换” 这几方面。这里面参杂着统一与分离,类比到网络与人,也许最终只有人脑搬到网络中,才可以达到最终状态。

虚的就说这么多,本篇讲的是 《框架实现》,我们先钻到细节里。

2 精读 dob 框架实现

dob 是个类似 mobx 的框架,实现思路都很类似,如果难以读懂 mobx 的源码,可以先参考 dob 的实现原理。

抽丝剥茧,实现依赖追踪

MVVM 思路中,依赖追踪是核心。

dob 中 observe 类似 mobx 的 autorun,是使用频率最高的依赖监听工具。

写作时,已经有许多文章将 vue 源码翻来覆去研究过了,因此这里就不长篇大论 MVVM 原理了。

依赖追踪分为两部分,分别是 依赖收集触发回调,如果把这两个功能合起来,就是 observe 函数,分开的话,就是较为底层的 Reaction

Reaction 双管齐下,一边监听用到了哪些变量,另一边在这些变量改变后,执行回调函数。Observe 利用 Reaction 实现(简化版):

image

function observe(callback) {
  const reaction = new Reaction(() => {
    reaction.track(callback)
  })

  reaction.run()
}

reaction.run() 在初始化就执行 new Reaction 的回调,而这个回调又恰好执行 reaction.track(callback)。所以 callback 函数中用到的变量被记录了下来,当变量更改时,会触发 new Reaction 的回调,又重新收集一轮依赖,同时执行了 callback

这样就实现了回调函数用到的变量被改变后,重新执行这个回调函数,这就是 observe

为什么依赖追踪只支持同步函数

依赖收集无法得到触发时的环境信息。

依赖收集由 getter、setter 完成,但触发时,却无法定位触发代码位于哪个函数中,所以为了依赖追踪(即变量与函数绑定),需要定义一个全局的变量标示当前执行函数,当各依赖收集函数执行没有交叉时,可以正常运作:

image

上图右侧白色方块是函数体,getter 表示其中访问到某个变量的 getter,经由依赖收集后,变量被修改时,左侧控制器会重新调用其所在的函数。

但是,当函数嵌套函数时,就会出现异常:

image

由于采用全局变量标记法,当回调函数嵌套起来时,当内层函数执行完后,实际作用域已回到了外层,但依赖收集无法获取这个堆栈改变事件,导致后续 getter 都会误绑定到内层函数。

异步(回调)也是同理,虽然写在一个函数体内,但执行的堆栈却不同,因此无法实现正确的依赖收集。

所以需要一些办法,将嵌套的函数放在外层函数执行完毕后,再执行:

image

换成代码描述如下:

observe(()=>{
  console.log(1)
  observe(()=>{
  	console.log(2)
  })
  console.log(3)
})
// 需要输出 1,3,2

当然这不是简单 setTimeout 异步控制就可以,因为依赖收集是同步的,我们要在同步基础上,实现函数执行顺序的变换。

我们可以逐层分解,在每一层执行时,子元素如果是 observe,就会临时放到队列里并跳过,在父 observe 执行完毕后,检查并执行队列,两层嵌套时执行逻辑如下图所示:

image

这些努力,就是为了保证在同步执行时,所有 getter 都能绑定到正确的回调函数。

如何结合 React

observe 如何到 render

observe 可以类比到 React 的 render,它们都具有相同的特征:是同步函数,同时 observe 的运行机制也符合了 render 函数的需求,不是吗?

如果将 observe 用到 react render 函数,当任何 render 函数使用到的变量发生改动,对应的 render 函数就会重新执行,实现 UI 刷新。

要实现结合,用到两个小技巧:聚合生命周期、替换 render 函数,用图才能解释清楚:

image

以上是简化版,正式版本使用 reaction 实现,可以更清晰的区分依赖收集与 rerender 阶段。

如何避免在 view 中随意修改变量

为了使用起来具有更好的可维护性,需要限制依赖追踪的功能,使值不能再随意的修改。可见,强大的功能,不代表在数据流场景的高可用性,恰当的约束反而会更好。

因此引入 Action 概念,在 Action 中执行的变量修改,不仅会将多次修改聚合成一次 render,而且不在 Action 中的变量修改会抛出异常。

Action 类似进栈出栈,当栈深度不为 0 时,进行的任何的变量修改,拦截到后就可以抛出异常了。

有层次的实现 Debug

一层一层功能逐渐冒泡。

调试功能,在依赖追踪、与 react 结合这一层都需要做,怎样分工配合才能保证功能不冗余,且具有良好的拓展性呢?

数据流框架的 Debug 分为数据层和 UI 层,顺序是 dob 核心记录 debug 信息 -> dob-devtools 读取再加工,强化 UI 信息

在 UI 层不止可以简单的将对象友好展示出来,更可以通过额外信息采集,将 Action 与 UI 元素绑定,让用户找到任意一次 Action 触发时,rerender 了哪些 UI 元素,以及每个 UI 元素分别与哪些 Action 绑定。

由于数据流需要一个 Provider 提供数据源,与 Connect 注入数据,所以可以将所有与数据流绑定的 UI 元素一一映射到 Debug UI,就像一面镜子一样映射:

image

通过 Debug UI,将 debug 信息与 UI 一一对应,实现 dob-react-devtools 的效果。

Debug 功能如何解耦

解耦还能方便许多功能拓展,比如支持 redux。

我得答案是事件。通过精心定义的一系列事件,制造出一个具有生命周期的工具库!

在所有 getter setter 节点抛出相关信息,Debug 端订阅这些事件,找到对自己有用的,记录下来。例如:

event.on("get", info => {
  // 不在调试模式
  if (!globalState.useDebug) {
    return
  }

  // 记录调用堆栈..
})

Dob 目前支持这几种事件钩子:

  • get: 任何数据发生了 getter。
  • set: 任何数据发生了 setter。
  • deleteProperty: 任何数据的 key 被移除时。
  • runInAction: 调用了 Action。
  • startBatch: 任意 Action 入栈。
  • endBatch: 任意 Action 出栈。

并且在关键生命周期节点,还要遵守调用顺序,比如以下是 Action 触发后,到触发 observe 的顺序:

startBatch -> debugInAction -> ...multiple nested startBatch and endBatch -> debugOutAction -> reaction -> observe

如果未开启 debug,执行顺序简化为:

startBatch -> ...multiple nested startBatch and endBatch -> reaction -> observe

订阅了这些事件,可以完成类似 redux-dev-tools 的功能。

3 总结

由于篇幅有限,本文介绍的《框架实现》均是一些上层设计,很少有代码讲解。因为我觉得一篇引发思考的文章不应该贴太多的代码,况且人脑处理图形的效率远远高于文字、略高于代码,所以通过一些图来展示。如果想看代码实现,可以读 dob 源码

如希望详细了解依赖注入实现流程,请看 从零开始用 proxy 实现 mobx

下一篇是 《框架使用》,会站在使用者的角度思考数据流。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant