五.render
前言
多选框组件
1.Render 函数
template 和 render 写法的对照:
<template>
<div id="main" class="container" style="color: red">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return {
show: false,
}
},
}
</script>
export default {
data() {
return {
show: false,
}
},
render: (h) => {
let childNode
if (this.show) {
childNode = h("p", "内容 1")
} else {
childNode = h("p", "内容 2")
}
return h(
"div",
{
attrs: {
id: "main",
},
class: {
container: true,
},
style: {
color: "red",
},
},
[childNode]
)
},
}
这里h
,即createElement
,是 Render 函数的核心。可以看到,template 中v-if/v-else等指令,都被 JS 的if/else替代了,那v-for自然也会被for语句替换。
h 有 3 个参数,分别是:
1.要渲染的元素或组件,可以是一个 html 标签、组件选项或一个函数(不常用),该参数为必填项。
// 1.html 标签
h("div")
// 2.组件选项
import DatePicker from "../component/data-picker.vue"
h(DatePicker)
2.对应属性的数据对象,比如组件的 props、元素的 class、绑定的事件、slot、自定义指令等,该参数是可选的。
3.子节点,可选,String 或 Array,它同样是一个 h。
2.约束
所有的组件树种,如果 vNode 是组件或含有组件的 slot,那么 vNode 必须是唯一。以下两个示例是错误的。
// 局部声明组件
const Child = {
render: (h) => {
return h("p", "text")
},
}
export default {
render: (h) => {
// 创建一个子节点,使用组件 Child
const ChildNode = h(Child)
return h("div", [ChildNode, ChildNode])
},
}
{
render: (h) => {
return h("div", [this.$slots.default, this.$slots.default])
}
}
重复渲染多个组件或元素,可以疼痛感一个循环和工厂函数来解决:
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)
},
}
对于函数有组件的 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])
}
}
在 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>
示例中的默认 slot 是一个 Refresh 按钮,使用者只写了一遍,但在 Transfer 组件中,是通过克隆 VNode 的方法,显示了两遍。如果不这样做,就要声明两个具名 slot,但是左右两个的逻辑可能是完全一样的,使用者就要写两个一模一样的 slot,非常不友好。
3.Render 函数使用场景
一般情况下是不推荐直接使用 Render 函数的,使用 template 足以,在 Vue.js 中,使用 Render 函数的场景,主要有以下 4 点:
1.使用两个相同 slot。在 template 中,Vue.js 不允许使用两个相同的 slot,比如下面的示例是错误的:
<template>
<div>
<slot></slot>
<slot></slot>
</div>
</template>
解决方案就是上文中讲到的约束,使用一个深度克隆 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
// 在 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
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
Vue.js 提供了一个function
的布尔值选项,设置为 true 可以使组件无状态和无实例,也就是没有 data 和 this 上下文。这样用 Render 函数返回虚拟节点可以更容易渲染,因为函数化组件只是一个函数,渲染开销要小很多。
使用函数化组件,Render 函数提供了第二参数 context 来提供临时上下文。组件需要的 data、props、slots、children、parent 都是通过这个上下文来传递的,比如 this.level 要改写为 context.props.level,this.$slots.default 改写为 context.children
函数化组件在业务中并不是很常见,而且也有类似的方法实现,比如某些场景可以用 is 特性来动态挂载组件。函数化组件主要适用于以下两个场景:
- 程序化地在多个组件中选一个
- 在将 children、props、data 传递给子组件之前操作它们
比如某个组件需要使用 Render 函数来自定义,而不是通过传递普通文本或 v-html 指令,这时就可以用 Function Render
- 首先创建一个函数化组件render.js
// render.js
export default {
functional: true,
props: {
render: Function,
},
render: (h, ctx) => {
return ctx.props.render(h)
},
}
它只定义了一个 props:render,格式为 Function,因为 functional,所以在 render 里使用了第二个参数ctx
来获取 props。这时一个中间文件,并且可以复用,其他组件需要这个功能时,都可以引入它。
2.创建组件:
<!-- my-component.vue -->
<template>
<div>
<Render :render="render"></Render>
</div>
</template>
<script>
import Render form './render.js';
export default {
components: { Render },
props: {
render: Function
}
}
</script>
3.使用上面的 my-component 组件
<!-- demo.vue -->
<template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from "../components/my-component.vue"
export default {
components: { myComponent },
data() {
return {
render: (h) => {
return h(
"div",
{
style: {
color: "red",
},
},
"自定义内容"
)
},
}
},
}
</script>
这里的 render.js 因为只是把 demo.vue 中 Render 内容过继,并无其他用处,所以用了 Functional Render
就此来说,完全可以用 slot 取代 Function Render,那是因为只有render
这一个 prop。如果示例中的<Render>
是用v-for
生成的,也就是多个时,用一个 slot 是实现不了的,那时用 Render 函数就很方便了。
总结
通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响