• 开始时间:2019-04-08
  • 目标主要版本:3.x
  • 引用 issue:N/A
  • 实现的 PR:N/A

摘要

  • h 现在被全局导入,而不是作为参数传递给渲染函数。
  • 改变了渲染函数的参数,并使其在有状态组件和函数式组件之间保持一致。
  • VNodes 现在有一个扁平的 props 结构。

基本范例

  1. // globally imported `h`
  2. import { h } from 'vue'
  3. export default {
  4. render() {
  5. return h(
  6. 'div',
  7. // flat data structure
  8. {
  9. id: 'app',
  10. onClick() {
  11. console.log('hello')
  12. }
  13. },
  14. [
  15. h('span', 'child')
  16. ]
  17. )
  18. }
  19. }

动机

在 2.x 中,VNodes 是特定于上下文的 —— 这意味着创建每个 VNode 都会被绑定到创建它的组件实例(上下文)。这是因为我们必须支持以下用例(h 是 createElement 的别名):

  1. // looking up a component based on a string ID
  2. h('some-component')
  3. h('div', {
  4. directives: [
  5. {
  6. name: 'foo', // looking up a directive by string ID
  7. // ...
  8. }
  9. ]
  10. })

为了查找局部或者全局注册的组件和指令,我们需要知道 “拥有” VNodes 的上下文组件实例。这就是为什么在 Vue2.x 中 h 被作为一个参数传入,因为传入的每个渲染函数 h是一个预先绑定到上下文实例的 curried 版本(正如 this.$createElement)。

这会造成一些不便,例如,当试图将部分逻辑提取到一个单独的渲染函数时,需要将 h 传递:

  1. function renderSomething(h) {
  2. return h('div')
  3. }
  4. export default {
  5. render(h) {
  6. return renderSomething(h)
  7. }
  8. }

当使用 JSX 时,这会特别麻烦,因为 h 是隐式使用,在用户代码中不需要。我们使用 JSX 插件必须执行自动的 h 注入,以缓解这种问题,但是这种逻辑是复杂和脆弱的。

在 3.0 我们找到了是 VNodes 不需要上下文的方法。它们可以在任何地方使用全局导入的 h 函数来创建,所以它只需要在任何文件中导入一次。


2.x 的渲染函数 API 的另一个问题是嵌套的 VNode 数据结构:

  1. h('div', {
  2. class: ['foo', 'bar'],
  3. style: { }
  4. attrs: { id: 'foo' },
  5. domProps: { innerHTML: '' },
  6. on: { click: foo }
  7. })

这个结构继承自 Snabbdom,也就是 Vue2.x 最初的虚拟 DOM 实现的基础。这样设计的原因是为了让 derring 逻辑可以模块化:一个单独的模块(例如,class 模块)只需要在 class 属性上工作。它也更加明确了每个绑定为什么将被处理。

然而,随着时间的推移,我们已经注意到,与扁平的结构相比,嵌套结构由一些缺陷:

  • 编写的更加冗余;
  • classstyle 的特殊情况有些不一致
  • 更多的内存使用(分配更多对象)
  • diff 速度慢(每个嵌套对象都需要自己的迭代循环)
  • 克隆 / 合并 / 传递更复杂和昂贵
  • 在使用 JSX 时需要更多的特殊规则或者隐式转换

在 3.x 中,我们正在想扁平化的 VNode 数据结构发展,以解决这些问题。

具体设计

全局导入的 h 函数

h 现在是全局导入的:

  1. import { h } from 'vue'
  2. export default {
  3. render() {
  4. return h('div')
  5. }
  6. }

渲染函数签名改变

由于不再需要 h 作为参数,现在的 render 将不再需要接收任何参数。事实上,在 3.0 中 render 的选项将主要被用作模版编译器产生的渲染函数的集成点。对于手动渲染函数,建议从 setup() 函数中返回它:

  1. import { h, reactive } from 'vue'
  2. export default {
  3. setup(props, { slots, attrs, emit }) {
  4. const state = reactive({
  5. count: 0
  6. })
  7. function increment() {
  8. state.count++
  9. }
  10. // return the render function
  11. return () => {
  12. return h('div', {
  13. onClick: increment
  14. }, state.count)
  15. }
  16. }
  17. }

从 setup() 返回的 render 函数自然可以访问作用域内声明的 reactive 状态和函数,还要加上传递给 setup 的参数:

这里的 props、slots 和 attrs 对象是代理,所以在渲染函数中使用时,它将始终指向最新的值。

关于 setup() 如何工作的细节,请参考 Composition API RFC

函数式组件的签名

请注意,现在的函数式组件和渲染函数也会有相同的签名,这使得它在有状态组件和函数式组件中都是一致的。

  1. const FunctionalComp = (props, { slots, attrs, emit }) => {
  2. // ...
  3. }

新的参数列表应该提供完全替代当前函数式渲染上下文:

  • propsslots 的值相等;
  • datachildren 不再需要了(只需要使用 props 和 slots);
  • listener 将被包含 attrs 中;
  • injections 可以使用新的 inject API(Composition API 的一部分)来替代: ```javascript import { inject } from ‘vue’ import { themeSymbol } from ‘./ThemeProvider’

const FunctionalComp = props => { const theme = inject(themeSymbol) return h(‘div’, Using theme ${theme}) }

  1. - parent 的访问将被移除,这是一些内部用例的逃生舱 —— 在用户代码中,`props` `injections` 应该是首选。
  2. <a name="w3o8S"></a>
  3. ## 扁平的 props 格式
  4. ```javascript
  5. // before
  6. {
  7. class: ['foo', 'bar'],
  8. style: { color: 'red' },
  9. attrs: { id: 'foo' },
  10. domProps: { innerHTML: '' },
  11. on: { click: foo },
  12. key: 'foo'
  13. }
  14. // after
  15. {
  16. class: ['foo', 'bar'],
  17. style: { color: 'red' },
  18. id: 'foo',
  19. innerHTML: '',
  20. onClick: foo,
  21. key: 'foo'
  22. }

在扁平结构中,VNode 的 props 使用以下规则处理:

  • key 和 ref 被保留
  • class 和 style 的 API 与 2.x 一致
  • 以 on 开头的 props 将被处理为 v-on 绑定,on 后面的所有内容都被转换为全小写的事件名称(下面会有更多介绍)
  • 对于其他东西:
    • 如果 key 作为 DOM 节点上一个存在的属性,它将被设置为一个 DOM 属性(property);
    • 否则,它将被设置为一个特性(attribute)。

特殊的保留 props

有两个全局的保留 props:

  • key
  • ref

此外,你可以使用保留的 onNodeXXX 前缀的钩子来处理 vnode 的声明周期。

  1. h('div', {
  2. onVnodeMounted(vnode) {
  3. /* ... */
  4. },
  5. onVnodeUpdated(vnode, prevVnode) {
  6. /* ... */
  7. }
  8. })

这些钩子也可以是自定义的指令建立的方式。因为它们也是以 on 开头,所以在模版中也能使用 v-on 声明。

  1. <div @vnodeMounted="() => { ... }">

由于扁平的结构,组件内部的 this.$attrs 现在被包含在任何没有被声明的原始 props,包括 classstyleonXXX 监听器和 vnodeXXX 的钩子。这使得编写封装组件更加容易 —— 只需要使用 v-bind="$attrs"this.$attrs 传递下去。

没有上下文的 VNodes

由于 VNodes 是没有上下文的,我们不能再使用一个字符串 ID(例如,h(‘some-component’))来隐含地检查全局注册的组件。查找指令也是如此。相反,我们需要使用一个导入的 API:

  1. import { h, resolveComponent, resolveDirective, withDirectives } from 'vue'
  2. export default {
  3. render() {
  4. const comp = resolveComponent('some-global-comp')
  5. const fooDir = resolveDirective('foo')
  6. const barDir = resolveDirective('bar')
  7. // <some-global-comp v-foo="x" v-bar="y" />
  8. return withDirectives(
  9. h(comp),
  10. [fooDir, this.x],
  11. [barDir, this.y]
  12. )
  13. }
  14. }

这将主要用于编译器生成的输出,因为手动编写的渲染函数代码通常直接导入组件和指令,并按照值使用它。

缺点

依赖 Vue Core

h 被全局导入意味着任何包含 Vue 组件的库都将包含 import { h } from "vue"(这也隐含在从模版编译的渲染函数中)。这会产生一些开销,因为它要求库作者在他们构建设置中正确配置 Vue 的外部化。

  • Vue 不应该绑定在库中;
  • 对于模块构建,导入应该被搁置,由最终的用户绑定处理;
  • 对于 UMD 或者浏览器的构建,应该先尝试全局的 Vue.h,然后再回头考虑 require 调用。

这是 React 库常见的做法,webpack 和 Rollup 都可以做到。有相当数量的 Vue 库也已经这样做了。我们只需要提供适当的文档和工具支持。

备选方案

N/A

采纳策略

  • 对于模版用户来说,这根本不会影响到他们。
  • 对于 JSX 用户来说,影响也将是最小的,但是我们确实需要重写我们的 JSX 插件。
  • 使用 h 手动编写渲染函数的用户将受到主要的迁移成本影响。这在我们的用户群中应该是一个很小的比例,但是我们确实需要提供一个适合的迁移路径。
    • 我们可以提供一个兼容插件,对渲染函数进行修补,使其暴露出与 2.x 兼容的参数,并且可以在每个组件中关闭,以实现一次性的迁移过程。
    • 也可以提供一个 codemod 来自动转换 h 调用,以使用新的 VNode 格式,因为映射是相当机械的。
  • 使用上下文的函数式组件可能需要手动迁移,但可以提供一个类似的适配器。

没有解决的问题

明确绑定类型的逃生舱

有了扁平的 VNode 数据结构,怎么在内部处理每一个属性就变得有一点隐晦了。这也造成了一些问题 —— 例如,如何显示地设置一个不存在的 DOM 属性,或者监听一个自定义元素的 CAPSCase 事件?

我们可能想通过前缀来支持显示绑定类型:

  1. h('div', {
  2. 'attr:id': 'foo',
  3. 'prop:__someCustomProperty__': { /*... */ },
  4. 'on:SomeEvent': e => { /* ... */ }
  5. })

这相当于 2.x 中的通过 attrsdomPropson 等进行嵌套。然而,这需要我们对每一个被修补的元素进行额外的检查,这导致一个非常小众用例的持续性能成本。我们可能想找到一个更好的方法来处理这个问题。