vue使用render函数和template模板的渲染对比

在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.defaultundefined,所以会报错:
在这里插入图片描述
这里可以做个判断,利用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的怀抱。
以上。

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值