render函数~

我心飞翔 分类:javascript
new Vue({
  el: '#app',
  render: h => h(App)
})
 

2021年了不会还有人对上面的代码不熟悉吧,通过脚手架构建的项目,main.js中都会有上面这些代码。

el是挂载的DOM元素,可以是DOM对象、选择器;

Render就是今天的大哥,如上是一个箭头函数,形参h也是一个函数,传入一个组件对象,将这个组件挂在到el上

createElement函数

这个函数就是上面Render函数的参数,运行后会返回一个虚拟DOM对象(Virtual Dom),最多传三个参数

createElement (el, ?description, ?VNode)
el 必选: { String | Object | Function }
一个HTML标签、组件选项或一个函数(用于函数化组件),例:"div"
description 可选: { Object }
一个对应属性的数据对象,作用于el参数上
具体选项如下:
{
// 动态绑定class, :class = { 'active': true }
'class': {
active: true
},
// 动态绑定style, :style = { 'color': 'red' }
'style': {
color: 'red'
},
// DOM元素的attribute属性, id = 'szgg', name = 'szgg'
'attrs': {
id: 'szgg',
name: 'szgg'
},
// 组件接收到的prop属性, 给子组件传值时使用
{
props: {
count: '0'
}
},
// DOM元素的property属性
domProps: {
innerHTML: '优雅永不过时'
},
// 事件监听器,修饰符不可用,需自己实现 @click=clickFn1
on: {
click: this.clickFn1
},
// 监听组件的原生事件,仅用于组件上,实际上还是使用$emit发送的事件 @click.native=clickFn2
nativeOn: {
click: this.clickFn2
},
// 自定义指令, v-my-directives:szgg.bar="1 + 1"
directives: [
{
name: 'my-directives', // 指令名
value: 2, // 指令的绑定值
expression: '1 + 1', // 指令的表达式
arg: 'szgg', // 指令的参数
modifiers: { // 指令的修饰符
'bar': true
}
}
],
// 作用域插槽, { name: props => VNode | Array<VNode> } 
/**
<template #default="scope">
<span>{{scope.text}}</span>
</template>
*/
scopedSlots: {
default: props => createElement('span', props.text)
},
// 如果组件是其它组件的子组件,需为插槽指定名称
slot: 'szgg',
// 其它特殊顶层 property
key: 'myKey',
// ref属性
ref: 'myRef',
// ref与v-for同时使用时,添加此属性,$refs.myRef会变成一个数组
refInFor: true  
}
VNode 可选: { String | Array }
子节点,多个子节点就放入数组中传入
例:'西内' | ['无情铁手', '致残打击']

实现一个点击功能,点击文字变色

Snipaste_2021-04-19_10-40-28.png
点击后=>
Snipaste_2021-04-19_10-40-16.png

// 挂载Vue实例,使用全局组件
<div id="app">
<ele></ele>
</div>
...
.active {
color: pink;
}
// template属性写法
<script>
Vue.component('ele', {
template: `
<div id="szgg" :class="{'active': isActive}" ref="szgg" @click="clickFn">{{message}}</div>
`,
data(){
return {
isActive: true,
message: 'SZGG'
}
},
methods: {
clickFn () {
this.isActive = !this.isActive
}
}
});
new Vue({
el:'#app',
data:{
message:'HelloWorld'
}
})
</script>
// render函数改写
<script>
Vue.component('ele', {
data(){
return {
isActive: true
}
},
render(createElement) {
return createElement('div', {
domProps: {
id: 'szgg'
},
class: {
'active': this.isActive
},
ref: 'szgg',
on: {
click: this.clickFn
}
}, [createElement('p', 'SZGG')])
},
methods: {
clickFn () {
this.isActive = !this.isActive
}
}
});
new Vue({
el:'#app'
})
</script>

上面只是Render函数的初体验,我们可以使用它实现页面的逻辑,同时也可以用js模板实现v-if,v-for,v-model

v-if,v-for,v-model

我们使用逻辑实现以上三个指令,而不是写自定义的指令实现,有兴趣的可以自己写写

v-if在render函数中的实现

// v-if 的实现,就是if/else重写
<script>
Vue.component('ele', {
data(){
return {
isActive: true
}
},
render(createElement) {
if (this.isActive) {
return createElement('div', {
domProps: {
id: 'szgg'
},
class: {
'active': this.isActive
},
ref: 'szgg',
on: {
click: this.clickFn
}
}, [createElement('p', 'SZGG')])
} else {
return createElement('div', {
on: {
click: this.clickFn
}
}, '啥也没有')
}
},
methods: {
clickFn () {
this.isActive = !this.isActive
}
}
});
new Vue({
el:'#app'
})
</script>

v-for在render函数中的实现

// v-for 的实现,就是数组的map方法重写
<script>
Vue.component('ele', {
data(){
return {
list: [1, 2, 3, 4, 5]
}
},
render(createElement) {
if (this.list.length) {
return createElement('ul', [this.list.map(i => createElement('li', i))])
} else {
return createElement('p', '你咋没数据呢')
}
}
});
new Vue({
el: '#app'
})
</script>

v-model在render函数中的实现

<script>
Vue.component('ele', {
data(){
return {
value: '来了'
}
},
render(createElement) {
// 保存一下当前的this对象,即组件对象ele
let _this = this;
return createElement('input', {
domProps: {
value: _this.value
},
on: {
input(event){
_this.value = event.target.value
}
}
})
}
});
new Vue({
el: '#app'
})
</script>

事件修饰符和按键修饰符

表1-1 部分事件修饰符和按键修饰符及对应的句柄

修饰符 对应句柄
.stop event.stopPropagation()
.prevent event.preventDefault()
.self if (event.target !== event.currentTarget) return
.enter/.13 if (event.keyCode !== 13) return
.ctrl,.alt,.shift,.meta if (!event.ctrlKey) return

对于事件修饰符.capyure,.once,Vue提供了特殊的前缀

表1-2 事件修饰符.capture,.once的前缀

修饰符 前缀
.capture !
.once ~
.once.capture ~!

修饰符有很多,这里我们只实现一个 .enter功能,在输入框里输入文字按enter打在公屏上,如下

<script>
Vue.component('ele', {
data(){
return {
value: '',
list: []
}
},
render(createElement) {
// 保存一下当前的this对象,即组件对象ele
let _this = this;
let listNode;
if (this.list.length) {
// 获取list列表中的VNodes
listNode = createElement('ul', this.list.map(i => createElement('li', i)))
} else {
listNode = createElement('p', '你快整点优雅的句子')
}
return createElement('div', [
createElement('input', {
attrs: {
placeholder: '请输入优雅的语句'
},
domProps: {
value: _this.value
},
on: {
input(event){
_this.value = event.target.value
},
// 监听键盘按下事件,是Enter的话再执行逻辑
keydown(event){
if (event.key !== 'Enter') return;
_this.list.push(event.target.value);
_this.value = ''
}
}
}),
listNode
])
}
});
new Vue({
el: '#app'
})
</script>

render函数中的插槽

首先要先判断组件是否使用了插槽,即组件标签之间是否有内容,可以用$slots.default获取

默认插槽

<script>
Vue.component('ele', {
render(createElement) {
console.log(this.$slots.default);
if (this.$slots.default) {
return createElement('div', this.$slots.default)
} else {
return createElement('div', '你没用插槽啊')
}
}
});
new Vue({
el: '#app'
})
</script>

作用域插槽

作用域插槽的实现有两个点:

1.在slot标签上绑定数据,即<slot :text="message"></slot>

2.在父组件中使用绑定的数据,即<child v-slot="props"><span>{{ props.text }}</span></child>

代码实现

<div id="app">
<ele>
<template #default="scope">
<span>{{scope.text}}</span>
</template>
</ele>
</div>
<script>
Vue.component('ele', {
data(){
return {
message: 'HelloWorld'
}
},
render(createElement) {
// 相当于`<div><slot :text="message"></slot></div>`,通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
});
new Vue({
el: '#app'
})
</script>

上面的代码实现了第一步,通过测试我们可以在页面上显示出正确的内容,但是还是用template获取绑定的数据,下面我们再使用Render函数实现第二步。

<div id="app">
</div>
<script>
Vue.component('ele', {
data(){
return {
message: 'HelloWorld'
}
},
render(createElement) {
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
});
new Vue({
render(createElement) {
return createElement('div', [
// 相当于`<div><ele v-slot="props"><span>{{ props.text }}</span></ele></div>`
createElement('ele', {
scopedSlots: {
default: function (scope) {
return createElement('span', scope.text)
}
}
})
])
}
}).$mount('#app')
</script>

上面代码为完成代码,使用Render函数实现了作用域插槽的功能

函数化组件

了解完上述信息,我们可以尝试用Render函数去描绘一个组件,在函数中定义组件的属性、行为、样式,那么这么做的好处是什么?

文章开头说到Render函数返回的是一个VNode,更容易进行渲染,如果我们把组件用函数化,就能提高渲染的效率,减少渲染开销

Vue.js中提供了一个属性functional,设置为true时可以是组件无状态和无实例,也就是没有data和this上下文,那么我们如何获取数据呢?这时Render函数提供了第二个参数context来提供临时上下文,可以通过context来获取组件需要的data,props,slots,children,parent属性, 例如:this.message要改写为context.props.level, this.$slots.default要改写为context.children

下面看一个例子:

<div id="app">
<!--2. 给组件传入Vue实例中的数据-->
<ele :l="l"></ele>
<button @click="changeFn(1)">H1</button>
<button @click="changeFn(2)">H2</button>
<button @click="changeFn(3)">H3</button>
</div>
<script>
// 定义一个组件对项
let levelTitle = {
// 5. 数据最终传入函数化的组件
props: ['level'],
render(createElement) {
return createElement(`h${this.level}`, '我是' + this.level)
}
};
Vue.component('ele', {
// 函数化组件开启
functional: true,
render(createElement, context) {
return createElement(levelTitle, {
// 4. 拿到传入本组件的props,再传入组件ele
props: {
level: context.props.l
}
})
},
// 3. 组件中接受数据
props: {
l: {
type: Number
}
}
});
new Vue({
// 1. 定义初始化数据
data: {
l: 1
},
methods: {
changeFn(level){
this.l = level
}
}
}).$mount('#app')
</script>

相信通过上面的例子应该对函数化组件有相应的了解了,也让我们折服在Render函数的魅力之下,但相比模块化的template来说,它的代码写起来太多,很不方便,所以函数化组件主要用于以下两个场景:

程序化的在多个组件中选择一个

在children,props,data传递给子组件之前操作他们

Render函数的学习告一段落,它给我们提供了另一种描绘组件的方法,让我们感受到js的强大之处,通过这些练习,我受益良多。

借鉴:Vue官网、Vue.js实战(书籍)

回复

我来回复
  • 暂无回复内容