前言
之前在通过官方文档学习 vue 的时候,关于《渲染函数&JSX》
章节的内容,只是简单看看就一笔带过了,也没有真正的使用 render 函数去做一些 demo,练练手。
最近温习了一遍官方文档,读到这个章节决定使用 render 函数写几个 demo 试试手。不上手觉得没有什么难的,一上手人傻了。大脑就跟短路一样,不知道如何该往下写了。
写下这篇文章,梳理一下逻辑,也给初学者排一排逻辑认知上的错误(笔者也是菜鸟)。正式内容开始前,笔者假设各位看官已经知道了 vue 中的普通语法也能够简单的使用。
正文
首先,我们都知道 render 函数位于 vue 配置项的顶层,优先级大于 template。该函数中的 createElement 参数(本文中简称 h)是其中的关键。h 函数的第一个参数接收一个 HTML 标签字符串 或者是注册组件的自定义元素名 或者是单文件组件对象(单文件组件的情况下)。
js
import single from "./xxxx.vue"
export default {
render(h){
h("div") // 普通的 HTML 标签
h("available") // 已经注册的组件名
return h(single) // 引入的单文件组件
},
components:{
"available":{
template:`<div>custom-element</div>`,
name:"unavailable"
}
}
}
第二个参数是一个数据对象,第三个参数是第一个参数元素或组件内的子节点,可以是文本内容、另一个HTML元素、另一个组件。
js
render(h){
return h("div", [ "html inner value" , h("h1", "title inner value") , h(some)] )
// 第一个子节点 第二个子节点 第三个子节点
}
其中较为重要的便是第二个参数,一个数据对象。其内的数据项并不是所有情况下都可以使用的,我就是在写 demo 的时候没有意识到这一点,导致"大脑短路"。
这里分为两种情况去解释遇到的问题。第一种情况便是 render 是作为从组件供别的组件来使用的,这种情况下,数据对象中的数据用到的就很少。你完全就可以像写普通组件那样写,只是普通组件中的 template 内容需要通过 render 来实现。
第二种情况便是 render 作为主组件去使用别的组件,h 函数的第二个参数中的大部分就是为了这种情况而准备的。以下通过几个例子展示两种情况下如何实现普通组件的功能。
v-bind
js
// v-bind
// 从组件(子组件)
{
props:["propsData"],
render(h){
return h("div",[this.propsData]) // template:`<div>{{ propsData }}</div>`
}
}
// 主组件(父组件)
import compo from "./xxx.vue"
{
data(){
return {
dataValue:"principal"
}
},render(h){
return h("div",{class:[this.dataValue},[h(compo,{props:{propsData:this.dataValue})])
// <div class="principal"><compo v-bind:propsData="principal"></compo></div>
}
}
v-model
js
// v-model
// 从组件
export default {
props:["outerValue"],
render(h){ // <input :value="outerValue" @input="$emit('input',$event.target.value)">
return h("input",{
on:{
input:(e)=>{
this.$emit("input",e.target.value);
}
},domProps:{ // 此处替换为 attrs 也同样有效。
value:this.outerValue
}
})
}
}
// 主组件
import compo from './components/some.vue'
export default {
data() {
return {
innerValue: ""
}
}, render(h) {
// <compo v-bind:outerValue="innerValue" @input="innerValue = $event"></compo>
// <compo v-model:outerValue="innerValue"></compo>
return h(compo, {
props: { outerValue: this.innerValue }, on: {
input: (value) => {
this.innerValue = value
}
}
})
}
}
非组件中双向数据绑定.lazy
修饰符,在普通组件中的实现为主组件配置对象中的model配置项。在 render 模式下,使用上面的例子从组件需要将监听事件以及抛出事件更改为 change,父组件监听的从组件事件也需要改为 change。其他修饰符就需要自己在从组件中的事件抛出文件做处理了。
v-model 的例子中我们便遇到了第一个只有在主组件 的情况下才可以使用的配置项nativeOn
。主组件若是想要直接监听从组件 的原生事件,而不是抛出的事件,就可以在nativeOn
配置项中填入指定的处理函数,其效果就类似于<base-input @focus.native="handler">
。
若从组件 中 input 并不是根元素,那么主组件 的 nativeOn 配置项中的事件监听函数便会失效。解决办法便是原生事件的监听函数不要放到nativeOn
而放到on
配置项中,然后从组件 在on
配置项中注册这个事件,其值为 this.$listeners.focus
。
js
// 主组件
{
// ...
return h(compo,{
on:{
focus(){}
}
})
// ...
}
// 从组件
{
// ...
return h("div",[ h("input",{ on:{focus:this.$listeners.focus} }) ])
// ...
}
slot
js
// 从组件
{
render(h) {
let slot_default = this.$slots.default
let slot_header = this.$slots.header
return h('div', [slot_default, slot_header])
// <div><slot name="default"></slot><slot name="header"></slot></div>
}
// 主组件
import compo from './components/some.vue'
export default{
render(h){
return h(compo, [
h("div", { class: "default", slot: 'default' }, ["default"]),
h("div", { class: "header", slot: 'header' }, ["header"]),
])
// <compo><template>default</template><template v-slot:header></template></compo>
}
}
需要注意的是,主组件 中渲染从组件 的渲染函数中的子节点指定{slot:"default"}
有效,若是子节点下的节点指定了 slot 便不会生效。
js
// 主组件
return h(compo,[
h("div",{slot:"default"},[h("div",{slot:"header"},"header")]),
// <div>
// <div> <-- 该元素位与 default 插槽内。header 插槽内没有内容,因为组件直接子元素下并没有声明为 slot:header 的内容
// <div>
// header
// </div>
// </div>
// </div>
])
scopedSlot
插槽作用域的写法与普通插槽的写法非常相似,slot
替换为scopedSlot
,插槽名从对象内的属性变为对象内的方法,将数据通过参数传递进去。
主组件的实现规则与 slot 写法中主组件实现一样,也可以通过指定slot:default
将组件的直接子元素内容渲染到某个插槽内,只是无法访问到插槽数据。
若要访问到插槽数据,还是需要使用scopedSlot:{default(props){return h()}}
这种方式。
js
// 从组件
{
render(h) {
let scope_slot_default = this.$scopedSlots.default(this.defaultValue)
let scope_slot_header = this.$scopedSlots.header(this.headerValue)
return h('div', [scope_slot_default, scope_slot_header])
}, data() {
return {
defaultValue: {slot_name:"default"},
headerValue: {slot_name:"header"}
}
}
}
// 主组件
import compo from './components/some.vue'
export default {
render(h) {
return h(compo, {
scopedSlots: {
default(props) {
return h("p", [props.slot_name])
},
header(props) {
return h("p", [props.slot_name])
}
}
})
}
// render(h) { 通过 slot 的方式将内容填入了指定的作用域插槽内。
// return h(compo, [
// h("p"),
// h("p",{slot:"header"})
// ])
// }
}
作用域插槽不像普通插槽,从组件中声明了主组件可以不去使用。从组件声明了作用域插槽,主组件必须使用对应的插槽,无论是通过 scopedSlot 还是 slot 的方式。
最后再次提醒各位读者,插槽的内容必须要在主组件渲染从组件时,在其从组件的直接子元素列表中指定。
结语
好记性不如烂笔头,各位读者在学习的时候一定要动手多练,不要抱有侥幸心理也不要对自己的记忆力抱有太大的希望(因人而异,笔者的记忆力不是特别好。)
最后,笔者是一个菜鸟,这是我的第二篇笔记,各位看官若是觉得有用不妨点个赞,让笔者码了一天的字觉得没有白费。点个赞,给笔者一点鼓励,有动力好产出更多文章。