在vue中我们通常使用template进行模板的设计,但是在某些时候无法使用template进行渲染,此时需要使用render函数。实际在vue源码中,template会被转化成render函数。
1、render函数的简单用法
render函数
接收1个参数(createElement函数
),返回值是createElement函数
的返回值(实际就是一个虚拟DOM)。createElement函数
接收3个参数,分别代表HTML标签名(也可以是组件对象或者resolve 了上述任何一种的一个 async 函数)、数据对象和子级虚拟节点(由 createElement()
构建而成,也可以使用字符串来生成文本虚拟节点
)。其中数据对象和子级虚拟节点是可选项。
首先看个小例子,假如需要渲染下面的HTML:
<div>hello, world!</div>
使用template
如下:
new Vue({
template: `<div>hello, world!</div>`
})
如果使用render函数
:
new Vue({
render(h) {
// h 是 createElement 函数的简写,h 是 hyper 的首字母
// 类似于 HTML(超文本标记语言)和 http(超文本传输协议)
// 中的 h。
return h("div", "hello, world!"); // "hello, world!"会被转换为文本虚拟节点
}
})
稍微复杂一点:
<div>
<p>hello, world!</p>
</div>
使用template
:
new Vue({
template: `<div><p>hello, world!</p></div>`
})
使用render
:
new Vue({
render(h) {
return h("div", [
h("p", "hello, world!") // 注意这里再次使用了h函数,并且放在了一个数组中,实际是生成了虚拟DOM,因为子节点通常不止一个,所以需要一个数组。而文本节点不需要使用h函数是因为vue内部做了处理
]);
}
})
再复杂点,给标签添加一些属性:
<div id="app">
hello, world!
</div>
使用template
:
new Vue({
template: `<div id="app">hello, world!</div>`
})
使用render
:
new Vue({
render(h) {
return h("div", {
attrs: { // 数据对象中的 attrs 表示 attributes,也就是我们常见的html标签属性,如 id,class,style等,但是class和style在数据对象中单独有一个字段描述,具体请看vue官方文档 https://cn.vuejs.org/v2/guide/render-function.html
id: "app"
}
}, "hello, world!");
}
})
上述代码表明,数据对象其实就是一个用来描述标签的对象,包含attributes、props、class、style、slots、scopedSlots等,实际就是我们在vue项目中常见的那些值。
2、使用render函数渲染插槽
要使用render
函数渲染插槽,首先需要知道vue
给我们提供的两个API
:$slots
和$scopedSlots
,分别用来获取插槽和作用域插槽。
例如:
<div id="app">
<Com>
<p>我是默认插槽</p>
<template v-slot:footer>我是具名插槽</template>
</Com>
</div>
const log = console.log;
const dir = console.dir;
const Com = {
template: `<p><slot></slot><slot name="footer"></slot></p>`,
mounted() {
log(this.$slots) // 打印插槽内容
}
};
const vm = new Vue({
el: "#app",
components: {
Com
}
});
打印输出如下:
可以发现返回结果是一个对象,属性名是插槽的名字,属性值是一个数组,数组的每个元素就是插槽的内容的转化后的虚拟DOM。
作用域插槽的用法类似,不同的是属性值是一个函数,函数返回值是一个数组,数组的每个元素就是插槽的内容的转化后的虚拟DOM
<div id="app">
<Com>
<template v-slot="scoped"><p>{{ scoped }}</p></template>
<template v-slot:footer="scoped">{{ scoped }}</template>
</Com>
</div>
const log = console.log;
const dir = console.dir;
const Com = {
template: `<p><slot :msg="msg"></slot><slot name="footer" :footer="footer"></slot></p>`,
data() {
return {
msg: "hello, world",
footer: "footer name"
};
},
mounted() {
const scopedSlots = this.$scopedSlots;
for (const key in scopedSlots) {
log(scopedSlots[key]());
}
}
};
const vm = new Vue({
el: "#app",
components: {
Com
}
});
打印输出如下:
那么怎样使用render函数
达到这样的效果?首先看匿名插槽:
<div>
<slot></slot>
</div>
通过前面对$slots
的描述知道,其返回的对象的属性值是一个含有虚拟DOM的数组,正好是我们需要的。
render(h) {
return h("div", this.$slots.default);
}
具名插槽类似:
<div>
<slot name="footer"></slot>
</div>
render(h) {
return h("div", this.$slots.footer);
}
作用域插槽也类似,只不过需要多一次函数调用:
<div id="app">
<Com>
<template v-slot="scoped"><p>{{ scoped }}</p></template>
<template v-slot:footer="scoped">{{ scoped }}</template>
</Com>
</div>
const Com = {
template: `<p><slot :msg="msg"></slot><slot name="footer" :footer="footer"></slot></p>`,
data() {
return {
msg: "hello, world",
footer: "footer name"
};
}
};
const Com = {
data() {
return {
msg: "hello, world",
footer: "footer name"
};
},
render(h) {
return h("p", [
this.$scopedSlots.default({
msg: this.msg
}), // 注意这里传入函数的对象其实就是上面的scoped的值
this.$scopedSlots.footer({
footer: this.footer
})
]);
}
};
由于render函数
相对抽象,再举几个例子,比如说要渲染下面的子组件:
// 子组件 child
<script type="x-template" id="child">
<div>
<p>
<slot :text="text"></slot>
</p>
</div>
</script>
const child = {
template: "#child",
data() {
return {
text: "hello, world!"
};
}
};
使用render函数
如下:
const child = {
data() {
text: "hello, world!"
},
render(h) {
return h("div", [
h("p", this.$slots.default)
]);
}
};
为了便于理解,可以将this.$slots.default
理解成<slot></slot>
,将this.$slots.footer
理解成<slot name="footer"></slot>
,然后放置插槽的位置就和template
一样的思路即可。例如想要在p标签
放置默认插槽,则将this.$slots.default
当做p标签
的子元素即可。具名插槽和作用域插槽类似。
但是要注意的是,由于this.$scopedSlots
的属性值是函数,如果调用不存在的函数则会报错,如下:
<com></com>
const Com = {
data() {
return {
msg: "hello, world",
footer: "footer name"
};
},
render(h) {
return h("ul", [
h("li", this.$scopedSlots.default({
msg: this.msg
})),
]);
}
};
const vm = new Vue({
el: "#app",
components: {
Com
}
});
上面代码中,由于并未使用作用域插槽,所以this.$scopedSlots.default
是undefined
,所以会报错:
这里可以做个判断,利用es6
的新语法?.
即可。?.
的用法很简单,就是判断对象是否存在,如果存在则取出.
号后面的属性对应的值。
例如,过去我们这么写:
const obj = {
a: "123",
b: "456"
};
const c = obj && obj.c;
现在可以这样写:
const c = obj?.c;
还可以连续使用:
const cc = obj?.c?.cc;
因此上述render
函数可以改成:
render(h) {
return h("ul", [
h("li", this.$scopedSlots.default?.call(this, {
msg: this.msg
})),
]);
}
利用这个方法,也可以给插槽设置默认值:
render(h) {
return h("div", this.$slots.default ?? "我是默认值"); // ?? 也是新语法,用于替代 || ,它限定当 ?? 前为 null 或者 undefined 时返回 ?? 后面的值,否则一律返回 ?? 前面的值。相比 || 更加精确
}
通过上面的描述,使用render函数
实现大部分的插槽已经不是问题了。那么假如有一个child
组件,现在需要在child
组件中使用插槽该怎么办呢?
例如:
// 子组件 child
<script type="x-template" id="child">
<div>
<p>
<slot :text="text"></slot>
</p>
<p>
<slot name="footer" :msg="msg"></slot>
</p>
</div>
</script>
const child = {
template: "#child",
data() {
return {
text: "hello, world!",
msg: "你好,世界!"
};
}
};
默认插槽和具名插槽和前面一样,不过是子元素换成组件,并且需要给数据对象里面的slot
选项指定名字,表明插入的值是放在哪个插槽中。
render(h) {
return h("div", [
h(child, [h("header", { slot: "default" }, "我是从父组件传入子组件默认插槽的内容")]),
h(child, [h("footer", { slot: "footer" }, "我是从父组件传入子组件具名插槽的内容")]),
]);
}
效果等价于:
<div>
<child>
<template>
<header>我是从父组件传入子组件默认插槽的内容</header>
</template>
</child>
<child>
<template v-slot:footer>
<footer>我是从父组件传入子组件具名插槽的内容</footer>
</template>
</child>
</div>
我们发现使用这种方式没办法同时给具名插槽和默认插槽赋值。相信从前面已经发现,所有使用$slots
的均可以使用$scopedSlots
替代,且这也是官方推荐的。因为函数易于扩展,且vue3
已经全部改成函数形式了,利于代码更新迭代。给子组件的插槽赋值也类似。
使用作用域插槽如下:
render(h) {
return h(child, {
scopedSlots: {
default(props) {
// 这里 props 是一个对象,包含子组件里面插槽上绑定的属性值
return h("header", "我是从父组件传入子组件默认插槽的内容");
},
footer(props) {
return h("footer", "我是从父组件传入子组件具名插槽的内容");
}
}
});
}
效果等价于:
<div>
<child>
<template>
<header>我是从父组件传入子组件默认插槽的内容</header>
</template>
<template v-slot:footer>
<footer>我是从父组件传入子组件具名插槽的内容</footer>
</template>
</child>
</div>
对于作用域插槽,只要使用对应的作用域插槽函数传递的属性对象即可:
render(h) {
return h(child, {
scopedSlots: {
default(props) {
// 这里 props 是一个对象,包含子组件里面插槽上绑定的属性值,属性名
// 是绑定的属性名,属性值是绑定的属性值
return h("header", props.text);
},
footer(props) {
return h("footer", props.msg);
}
}
});
}
等价于:
<div>
<child>
<template>
<header>hello, world!</header>
</template>
<template v-slot:footer>
<footer>你好,世界!</footer>
</template>
</child>
</div>
到这块render
函数的基本功能已经介绍完毕了,总结就是尽量使用作用域插槽的方式设置插槽的值,vue2
迟早过时,最终我们都将投向vue3
的怀抱。
以上。