- 开始时间:2019-04-08
- 目标主要版本:3.x
- 引用 issue:N/A
- 实现的 PR:N/A
摘要
h现在被全局导入,而不是作为参数传递给渲染函数。- 改变了渲染函数的参数,并使其在有状态组件和函数式组件之间保持一致。
- VNodes 现在有一个扁平的 props 结构。
基本范例
// globally imported `h`import { h } from 'vue'export default {render() {return h('div',// flat data structure{id: 'app',onClick() {console.log('hello')}},[h('span', 'child')])}}
动机
在 2.x 中,VNodes 是特定于上下文的 —— 这意味着创建每个 VNode 都会被绑定到创建它的组件实例(上下文)。这是因为我们必须支持以下用例(h 是 createElement 的别名):
// looking up a component based on a string IDh('some-component')h('div', {directives: [{name: 'foo', // looking up a directive by string ID// ...}]})
为了查找局部或者全局注册的组件和指令,我们需要知道 “拥有” VNodes 的上下文组件实例。这就是为什么在 Vue2.x 中 h 被作为一个参数传入,因为传入的每个渲染函数 h是一个预先绑定到上下文实例的 curried 版本(正如 this.$createElement)。
这会造成一些不便,例如,当试图将部分逻辑提取到一个单独的渲染函数时,需要将 h 传递:
function renderSomething(h) {return h('div')}export default {render(h) {return renderSomething(h)}}
当使用 JSX 时,这会特别麻烦,因为 h 是隐式使用,在用户代码中不需要。我们使用 JSX 插件必须执行自动的 h 注入,以缓解这种问题,但是这种逻辑是复杂和脆弱的。
在 3.0 我们找到了是 VNodes 不需要上下文的方法。它们可以在任何地方使用全局导入的 h 函数来创建,所以它只需要在任何文件中导入一次。
2.x 的渲染函数 API 的另一个问题是嵌套的 VNode 数据结构:
h('div', {class: ['foo', 'bar'],style: { }attrs: { id: 'foo' },domProps: { innerHTML: '' },on: { click: foo }})
这个结构继承自 Snabbdom,也就是 Vue2.x 最初的虚拟 DOM 实现的基础。这样设计的原因是为了让 derring 逻辑可以模块化:一个单独的模块(例如,class 模块)只需要在 class 属性上工作。它也更加明确了每个绑定为什么将被处理。
然而,随着时间的推移,我们已经注意到,与扁平的结构相比,嵌套结构由一些缺陷:
- 编写的更加冗余;
class和style的特殊情况有些不一致- 更多的内存使用(分配更多对象)
- diff 速度慢(每个嵌套对象都需要自己的迭代循环)
- 克隆 / 合并 / 传递更复杂和昂贵
- 在使用 JSX 时需要更多的特殊规则或者隐式转换
在 3.x 中,我们正在想扁平化的 VNode 数据结构发展,以解决这些问题。
具体设计
全局导入的 h 函数
h 现在是全局导入的:
import { h } from 'vue'export default {render() {return h('div')}}
渲染函数签名改变
由于不再需要 h 作为参数,现在的 render 将不再需要接收任何参数。事实上,在 3.0 中 render 的选项将主要被用作模版编译器产生的渲染函数的集成点。对于手动渲染函数,建议从 setup() 函数中返回它:
import { h, reactive } from 'vue'export default {setup(props, { slots, attrs, emit }) {const state = reactive({count: 0})function increment() {state.count++}// return the render functionreturn () => {return h('div', {onClick: increment}, state.count)}}}
从 setup() 返回的 render 函数自然可以访问作用域内声明的 reactive 状态和函数,还要加上传递给 setup 的参数:
props和attrs将同等于this.$props和this.$attrs—— 也可以参考 Optional Props Declaration 和 Attribute Fallthrough。slots将等同于this.$slots—— 也可以参考 Slots Unification。emit等同于this.$emit。
这里的 props、slots 和 attrs 对象是代理,所以在渲染函数中使用时,它将始终指向最新的值。
关于 setup() 如何工作的细节,请参考 Composition API RFC。
函数式组件的签名
请注意,现在的函数式组件和渲染函数也会有相同的签名,这使得它在有状态组件和函数式组件中都是一致的。
const FunctionalComp = (props, { slots, attrs, emit }) => {// ...}
新的参数列表应该提供完全替代当前函数式渲染上下文:
props和slots的值相等;data和children不再需要了(只需要使用 props 和 slots);listener将被包含attrs中;injections可以使用新的injectAPI(Composition API 的一部分)来替代: ```javascript import { inject } from ‘vue’ import { themeSymbol } from ‘./ThemeProvider’
const FunctionalComp = props => {
const theme = inject(themeSymbol)
return h(‘div’, Using theme ${theme})
}
- parent 的访问将被移除,这是一些内部用例的逃生舱 —— 在用户代码中,`props` 和 `injections` 应该是首选。<a name="w3o8S"></a>## 扁平的 props 格式```javascript// before{class: ['foo', 'bar'],style: { color: 'red' },attrs: { id: 'foo' },domProps: { innerHTML: '' },on: { click: foo },key: 'foo'}// after{class: ['foo', 'bar'],style: { color: 'red' },id: 'foo',innerHTML: '',onClick: foo,key: 'foo'}
在扁平结构中,VNode 的 props 使用以下规则处理:
- key 和 ref 被保留
- class 和 style 的 API 与 2.x 一致
- 以 on 开头的 props 将被处理为 v-on 绑定,on 后面的所有内容都被转换为全小写的事件名称(下面会有更多介绍)
- 对于其他东西:
- 如果 key 作为 DOM 节点上一个存在的属性,它将被设置为一个 DOM 属性(property);
- 否则,它将被设置为一个特性(attribute)。
特殊的保留 props
有两个全局的保留 props:
keyref
此外,你可以使用保留的 onNodeXXX 前缀的钩子来处理 vnode 的声明周期。
h('div', {onVnodeMounted(vnode) {/* ... */},onVnodeUpdated(vnode, prevVnode) {/* ... */}})
这些钩子也可以是自定义的指令建立的方式。因为它们也是以 on 开头,所以在模版中也能使用 v-on 声明。
<div @vnodeMounted="() => { ... }">
由于扁平的结构,组件内部的 this.$attrs 现在被包含在任何没有被声明的原始 props,包括 class、style、onXXX 监听器和 vnodeXXX 的钩子。这使得编写封装组件更加容易 —— 只需要使用 v-bind="$attrs" 将 this.$attrs 传递下去。
没有上下文的 VNodes
由于 VNodes 是没有上下文的,我们不能再使用一个字符串 ID(例如,h(‘some-component’))来隐含地检查全局注册的组件。查找指令也是如此。相反,我们需要使用一个导入的 API:
import { h, resolveComponent, resolveDirective, withDirectives } from 'vue'export default {render() {const comp = resolveComponent('some-global-comp')const fooDir = resolveDirective('foo')const barDir = resolveDirective('bar')// <some-global-comp v-foo="x" v-bar="y" />return withDirectives(h(comp),[fooDir, this.x],[barDir, this.y])}}
这将主要用于编译器生成的输出,因为手动编写的渲染函数代码通常直接导入组件和指令,并按照值使用它。
缺点
依赖 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 事件?
我们可能想通过前缀来支持显示绑定类型:
h('div', {'attr:id': 'foo','prop:__someCustomProperty__': { /*... */ },'on:SomeEvent': e => { /* ... */ }})
这相当于 2.x 中的通过 attrs、domProps 和 on 等进行嵌套。然而,这需要我们对每一个被修补的元素进行额外的检查,这导致一个非常小众用例的持续性能成本。我们可能想找到一个更好的方法来处理这个问题。
