响应式 DOM
其实很久以前就看到过 Binding.scala,当时虽然自己也有学过 scala,但是因为自己已经会了 React,觉得 React 生态十分好,Binding.scala 想打败它几乎是一件不可能的事情,尤其 scala 还是非常难的一门语言,虽然 Binding.scala 并不要求完全掌握整个的 scala。但最终还是没有深入去看。
之后自己想了解 rxjs,受群友的推荐,可以先学学 flyd,更简单易用。然后学会使用它,也在 github 搜索它时发现了 react-flyd。。。。
然后,我立即就被它的使用方式吸引了。。。
其实我并不在乎一个库是否保持更新,如果它完善的完成了它的功能,api 也足够好的话,不更新也没关系。呵,这有点类似当初 Go 语言没有包管理的哲学。
看下它的使用方式:
/** @jsx h */
import { render } from 'react-dom';
import { h } from 'react-flyd';
import { stream, scan, merge} from 'flyd';
function Counter() {
const plus$ = stream();
const minus$ = stream();
const action$ = merge(
plus$.map(() => 1),
minus$.map(() => -1)
);
const count$ = scan((x, y) => x + y, 0, action$);
return (
<div>
<div>
<button id="plus" onClick={ stream(plus$) }>+</button>
<button id="minus" onClick={ stream(minus$) }>-</button>
</div>
<div>
Count: { count$ }
</div>
</div>
);
}
render(<Counter />, document.getElementById('root'));
plus$ 和 minus$
其实都没必要看,无非就是把 click 事件转为响应的 stream。。。。
重点是 count$
。我们可以看到 count$
是一个 stream<number>
,它竟然可以直接作为 jsx 的 children 部分,然后在渲染完后会发现 count$
的内容被包裹在一个 span 标签内。。。
其实看到这里,就很容易明白 react-flyd 到底做了什么。它 hack 进 jsx 的 h 语法,然后查看 children 里的元素,如果 children 里的 child 是一个 stream,就特殊处理它。
比如创建一个 reactive component,在 didMount 的时候 subscribe 这个 stream,在 willUnmount 时就 unsubscribe。。。。
非常简单,但行之有效的逻辑。这种逻辑给我们带来了 —- 最小化更新单位。
最小化更新单位出来了,那性能优化的极致就出现了。。。至少它带来了可能。
当然,react-flyd
也有许多逻辑不够正确,例如擅自给 count$ 加了个 span 包裹,会影响 DOM 结构,不过它这擅自给 count$ 加个 span 倒是让我有了点启发。。。
我不满它加了 span 包裹,于是自己 fork 了一份,修改的过程中发现了两种概念: self control component 和 parent control component…
因为 react 组件 render 方法只能返回 vnode/null,不得返回 string/number,于是,如果想不加 span 包裹的话,就必须得把 count$ 的父元素作为 reactive component。count$ 有新值时,得它的父元素,整个 div 都得刷新,这就是 parent control component。。。
如果一个 stream 的值是 vnode 的话,则可以作为 self control component,stream 有新值时,只刷新自己。
另外,它还有 bug 是 flyd 并非 lazy observable,它的这种使用,容易造成内存泄漏,具体情况这里不详说。
回到最小化更新单位,其实,如果我们再仔细想一下的话,这里的“最小化更新单位”真的可以就是单个 html 元素,而不是单个 react component,整个应用完全可以不需要一个 react component 就可以。
可以把上面的写成下面这样都是可以的。
var counter = (
<div>
<div>
<button id="plus" onClick={ stream(plus$) }>+</button>
<button id="minus" onClick={ stream(minus$) }>-</button>
</div>
<div>
Count: { count$ }
</div>
</div>
)
render(counter, document.getElementById('root'));
然后,我们能不能再激进一点点呢?既然“最小化更新单位”真的可以就是单个 html 元素
,那,我们能不能就真的让 stream 在有新值的时候直接修改真实 DOM,而不是创建 VDOM,然后 diff,然后修改呢。。。
当然可以。