五.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 Alert3.在 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 函数就很方便了。
总结
通过对前端组件的分析,需要重点关注组件中易变性对组件封装的影响,它会对组件的可复用性、可扩展性产生很大影响
