# Vue2.x 的 render
前言
介绍 Vue2.x render 函数及其应用场景
# 1.案例
# 2.约束
所有的组件树中,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须是唯一。以下两个示例是错误的。
const Child = {
render: (h) => {
return h("p", "text")
},
}
export default {
render: (h) => {
const ChildNode = h(Child)
return h("div", [ChildNode, ChildNode])
},
}
2
3
4
5
6
7
8
9
10
11
{
render: (h) => {
return h("div", [this.$slots.default, this.$slots.default])
}
}
2
3
4
5
重复渲染多个组件或元素,可以通过一个循环和工厂函数来解决:
const Child = {
render: (h) => {
return h("p", "text")
},
}
export default {
render: (h) => {
const children = Array.apply(null, {
length: 5,
}).map(() => {
return h(Child)
})
return h("div", children)
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
对于函数有组件的 slot,复用比较复杂,需要将 slot 的每个子节点克隆一份,例如:
{
render: (h) => {
function cloneVNode(vnode) {
// 递归遍历所有子节点,并克隆
const clonedChildren =
vnode.children && vnode.children.map((vnode) => cloneVNode(vnode))
const cloned = h(vnode.tag, vnode.data, clonedChildren)
cloned.text = vnode.text
cloned.isComment = vnode.isComment
cloned.componentOptions = vnode.componentOptions
cloned.elm = vnode.elm
cloned.context = vnode.context
cloned.ns = vnode.ns
cloned.isStatic = vnode.isStatic
cloned.key = vnode.key
return cloned
}
const vNodes = this.$slots.default === undefined ? [] : this.$slots.default
const clonedVNodes =
this.$slots.default === undefined
? []
: vNodes.map((vnode) => cloneVNode(vnode))
return h("div", [vNodes, clonedVNodes])
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
在 Render 函数里创建一个 cloneVNode 的工厂函数,通过递归将 slot 所有子节点都克隆一份,并对 VNode 的关键属性也进行了复制。
深度克隆 slot 并非 Vue.js 内置方法,也没有得到推荐,属于黑科技,在一些特殊的场景才会使用到,正常业务几乎是用不到的。比如 iView 组件库的穿梭框组件 Transfer,就用到这种方法。
它的使用方法是:
<template>
<Transfer
:data="data"
:target-keys="targetKeys"
:render-format="renderFormat"
>
<div :style="{ float: 'right', margin: '5px' }">
<Button @click="reloadMockData">Refresh</Button>
</div>
</Transfer>
</template>
2
3
4
5
6
7
8
9
10
11
示例中的默认 slot 是一个 Refresh 按钮,使用者只写了一遍,但在 Transfer 组件中,是通过克隆 VNode 的方法,显示了两遍。如果不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完全一样的,使用者就要写两个一模一样的 slot,非常不友好。
# 3.使用场景
- 1.在 template 中,不允许使用两个相同 slot
<template>
<div>
<slot></slot>
<slot></slot>
</div>
</template>
2
3
4
5
6
解决方案就是上文中讲到的约束,使用一个深度克隆 VNode 节点的方法。
2.在 SSR 环境(服务端渲染),如果不是常规的 template 写法,比如通过 Vue.extend 和 new Vue 构造来生成的组件实例,是编译不过的。如 Alert 组件
// 正确写法
import Alert from "./alert.vue"
import Vue from "vue"
Alert.newInstance = (properties) => {
const props = properties || {}
const Instance = new Vue({
data: props,
render(h) {
return h(Alert, {
props: props,
})
},
})
const component = Instance.$mount()
document.body.appendChild(component.$el)
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在 SSR 下报错的写法
import Alert from "./alert.vue"
import Vue from "vue"
Alert.newInstance = (properties) => {
const props = properties || {}
const div = document.createElement("div")
div.innerHTML = `<Alert ${props}></Alert>`
document.body.appendChild(div)
const Instance = new Vue({
el: div,
data: props,
components: { Alert },
})
const alert = Instance.$children[0]
return {
add(noticeProps) {
alert.add(noticeProps)
},
remove(name) {
alert.remove(name)
},
}
}
export default Alert
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
3.在 runtime 版本的 Vue.js 中,如果使用 Vue.extend 手动构造一个实例,使用 template 选项是会报错的,把 template 改成 Render 就可以了。需要注意的是,在开发独立组件时,可以通过配置 Vue.js 版本来使 template 选项可用,但这是在自己的环境,无法保证使用者的 Vue.js 版本,所以对于听过给他人用的组件,是需要考虑兼容 runtime 版本和 SSR 环境的。
4.一个 Vue.js 组件,有一部分内容需要从父级传递来显示,如果是文本之类的,直接通过
props
就可以了,如果这个内容带有样式或复杂一点的 html 结构,可以使用v-html
指令来渲染,父级传递的仍然是一个 HTML Element 字符串,不过它仅仅是能解析正常的 html 节点且有 XSS 风险。当需要最大化程度自定义显示内容是,就需要Render
函数,它可以渲染一个完整的 Vue.js 组件。你可能会说,用 slot 不就好了。的确,slot 作用就是做内容分发的,但在一些特殊的组件中,可能 slot 也不行。比如一个表格组件Table
,它接收两个 props:列配置 columns 和行数据 data,不过某一列的单元格,不是只将数据显示出来那边简单,可能带有一些复杂的操作,这种场景只用 slot 是不行的,没办法确定是那一列的 slot。这种场景有两种解决方案,其一就是 Render 函数,另一种就是作用域 slot。
# 4.functional + render
# 4.1 render.js
export default {
functional: true,
props: {
render: Function,
},
render: (h, ctx) => {
return ctx.props.render(h)
},
}
2
3
4
5
6
7
8
9
# 4.2 创建组件
<template>
<div>
<Render :render="render"></Render>
</div>
</template>
<script>
import Render form './render.js';
export default {
components: { Render },
props: {
render: Function
}
}
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 4.3 案例
总结
通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响