浅聊vapor
之前整理 vue 流程的时候了解到了 vapor, 所以现在来大概了解一下.
什么是 vapor
接着上篇 post 对 vue 概念的分析, 操作 dom 性能开销大, 所以有了 vdom 的概念.
用 js 对象来描述 dom 结构, 把新老 vdom 对比, 来分析数据变更导致页面变化”最小的 dom 操作”.
借鉴于 solidjs 和 svelte 的无 vdom 思路, 和 vue 的流程和可拓展性, vue 也可以尝试无 dom 模式.
(八卦: vapor 作者用 svelte 写了个爱坤组件库, 所以对 svelte 比较了解, 现在 vapor 被收到 vue 仓库中开发了)
我粗粗看了下 vapor 是如何避免 vdom 的.
如何绕开 vdom
先来回顾下 vdom 挂载与更新流程: 执行 render 函数, 获得 vnode => 根据新老 vnode 对比, 进行 dom 操作.
响应式的 effect 是包裹上述全流程的, 所以数据变化就会引起上面的步骤重新执行.
简单的思考
虽然绕开 vdom, 但有两个点是少不了的:
- 响应式: 让数据更改可以触发界面变化, 并且可以使用新数据.
- dom 操作: 最后改变页面的动作.
因为这些操作对用户是透明的, 所以选择在编译阶段处理.
sfc 编译结果
在 vue 的 sfc playground, 观察一下 vapor 的编译结果.
template
就是初始的:
1 | <script setup vapor> |
vapor 编译结果中的 render 函数是这样的:
1 | const t0 = _template("<h1> </h1>") |
这个 render 函数做了:
- 分析了
template
, 把每部分内容都通过_template()
函数生成真实 dom. 注意 dom 是空的. - 为 dom 绑定事件.
- 创建一个
renderEffect
, 更新有响应式数据绑定的 dom.
可以看到, vapor 的主要魔法在于, 在编译阶段把数据和 dom 的关系直接关联起来了.
而 vue 3 在编译阶段已经完成了静态分析, 这个方案和现在的 vue 是很契合的.
试一下多变量与数组
此时我心里有了疑问, 想看看多个变量和数组的处理.
我做了第一个小修改, 增加了个 ref
变量与一个 h2
, 在模板上新增了一个插值表达式.
编译结果中仍然只有一个 renderEffect
, 所以vapor 的更新粒度是组件, 可能是避免重走 vue 1 的粒度过小的老问题.
做了第二个小修改, 数组遍历, 模板如下:
1 | <template> |
编译结果是这样的:
1 | const t0 = _template("<div> </div>", true) |
粗看发现, renderEffect
被设在了组件内部.
但这样只能处理修改数组元素的情况, 那么如果直接对数组操作呢?
来看 _createFor()
函数, 也是用 renderEffect()
驱动的, 和以前的 diff children 差不多的逻辑.
如此这般, 这里就产生了 数组长度 + 1
数量的 watcher, 并且也需要进行 vdom 那样的 diff 算法.
这2个小功能的观察让我感觉到 vapor 的开发量是很大的, 并且应该是照着测试用例先完成功能, 再优化具体实现的.
vapor 宣传的包体积小应该是稳定的, 但性能方面, 还得后续看.
与以前流程的变化
我们现在已经知道了:
- vapor 模式在编译的时候就让 render 函数返回了真实 dom, 并且建立了可以精准修改 dom 的 effect.
- vapor 提供了一些方法来支持上面说的行为, 比如前面例子中的
template()
,setText()
,createFor()
等方法. 都是主版本 vue 没有的方法.
如果对 vue 流程熟悉的朋友, 应该已经想到, vapor 的改动远不止此, 这样的 render 函数是不能被原来的流程使用的.
vapor 的 render 函数返回了真实 dom 的数组, 而主版本的 render 函数只能返回 vnode.
并且 vapor 在 render 函数中建立了 renderEffect, 这个动作在主版本中, 是 mountComponent 做的.
再并且 vapor 的 renderEffect, 直接调用了 dom 操作的方法, 所以不会有 patch 方法, 不会有 updateComponent 的方法.
(细心的同志可能已经发现 vapor sfc 编译的 render 函数接受的参数都是不同的)
那么我们现在来理理 vapor 的运行时流程.
先从createVaporApp看起来.
1 | export const createVaporApp: CreateAppFunction<ParentNode, VaporComponent> = ( |
这里的createAppAPI
是从runtime-core
中引用的, 也就是 vapor 的 app 和主版本是一致的, 区别是mountApp
等方法.
(我们从这儿也看出来 vue 3 的分层设计很便于扩展)
那就来看mountApp
:
1 | const mountApp: AppMountFn<ParentNode> = (app, container) => { |
- mountApp: 创建组件实例, 调用 mountComponent.
- mountComponent: 调用 beforeMount => insert(instance.block) => mounted
insert()
方法很简单就是插入 dom.
所以mountApp
总结就是创建组件实例, 并插入 dom.
创建组件的过程就调用了前文提到编译后的 render 函数.
这个 render 函数执行后, 建立好 renderEffect, 并返回了 dom, 供 insert()
调用.
如果有兴趣就跟着一起看下创建组件实例:
1 | export function createComponent( |
我们关心的步骤是这几个:
- 创建一个空的 vapor 组件实例.
- 调用 setup 方法, 获取到 render 方法.
- 调用 render 方法, 获取到 dom, 赋值给组件实例的
block
属性. (之后直接作为insert()
的参数)
至于 setup 结果直接是 block 的情况和注释我没看懂, 按我理解这是”手动写 setup”的情况, 反而更可能出现在非 sfc 中, 而现在 vapor 是不支持非 sfc 的.
总结
vapor 的代码在 compiler-vapor 和 runtime-vapor, 跟主版本的区别也就在于这里.
我暂时只看到 vapor 的单独入口, 不确定现在是不是支持混合模式.
vapor 是依赖编译, 目前只能编译 sfc 和 jsx, 我感觉理论上 render 函数也能编译, 不算是绝对限制, 但太复杂了, 因为理论上vue的编译时标记也可以做, 但现在只能在sfc上. 但没关系, vapor支持混合模式.
最后, 还是想输出下自己了解了下 vapor 后的2个感受.
- vapor 的开发离上生产还有一段距离.
- 无 dom 很合适 vue 的情况, 但是不是在所有场景都比 vdom 好, 还需要努力.
- 目前可能只支持 script setup, 对于组件开发不知道有没有影响.
- 另外是不是能把 dom 操作抽出来以支持跨平台, 也挺重要的. (我觉得比支持手写 render 函数容易)
(本文完)
如果你可以 点击这个链接打赏我5毛来鼓励我, 非常感谢.
本文遵循 cc协议
你可以在注明出处和非商用的前提下任意复制及演绎