Vue从入门到实战:render函数
来自CloudWiki
背景
Vue推荐在大多数情况下使用模板来构建HTML,
然后在一些场景中,你可能需要Javascript的编程能力,这时可以使用render函数,它比模板更接近编译器。
假设我们要生成一些带锚点的标题:
<h1> <a name="hello-world" href="#hello-world"> Hello world! </a> </h1>
对于上面的 HTML,你决定这样定义组件接口:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level prop 动态生成标题 (heading) 的组件时,你可能很快想到这样实现:
<script type="text/x-template" id="anchored-heading-template"> <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> </script> Vue.component('anchored-heading', { template: '#anchored-heading-template', props: { level: { type: Number, required: true } } })
虽然模板在大多数组件中都非常好用,但在本例中不太合适,模板代码冗长,且<slot>元素在每一级标题元素中都重复书写了。
render函数的引入
下面改用render函数重复上面的示例,代码精简了很多
render函数 又称渲染函数,字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script src="vue.js"></script> </head> <body> <div id="app"> <anchored-heading :level="3"> <a name="hello-world" href="#hello-world"> Hello world! </a> </anchored-heading> </div> <script type = "text/javascript"> Vue.component('anchored-heading', { render: function (createElement) { return createElement( 'h' + this.level, // 标签名称 this.$slots.default // 子节点数组 ) }, props: { level: { type: Number, required: true } } }) new Vue({ el: '#app' }) </script> </body> </html>
注:$slots用于以编程方式访问由插槽分发的内容,每个命名的插槽都有其相应的属性。default属性包含了所有未包含在命名插槽中的节点或v-slot:default的内容。
render函数的解释
createElement函数
该函数用于返回描述节点信息及其子节点子信息的一个对象,即虚拟节点(简称为VNode)
一般带有三个参数:
- 第一个参数是要创建的元素节点的名字(字符串形式)或者组件选项(对象形式);
- 第二个参数是元素的属性集合(包括普通属性、prop、事件属性、自定义指令等),以对象形式给出
- 第三个参数是子节点的信息,以数组形式给出,如果该元素只有文本子节点,那么直接以字符串形式给出即可,如果还有子元素,则继续调用createElement函数。
上述代码改进版,添加了a标签和链接:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title></title> <script src="vue.js"></script> </head> <body> <div id="app"> <anchored-heading :level="3"> Hello world! </anchored-heading> </div> <script type = "text/javascript"> var getChildrenTextContent = function (children) { return children.map(function (node) { return node.children ? getChildrenTextContent(node.children) : node.text }).join('') } Vue.component('anchored-heading', { render: function (createElement) { // 创建 kebab-case 风格的 ID var headingId = getChildrenTextContent(this.$slots.default) .toLowerCase() .replace(/\W+/g, '-') .replace(/(^-|-$)/g, '') return createElement( 'h' + this.level, [ createElement('a', { attrs: { name: headingId, href: '#' + headingId } }, this.$slots.default) ] ) }, props: { level: { type: Number, required: true } } }) new Vue({ el: '#app' }) </script> </body> </html>
注意:Vnode必须唯一
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render: function (createElement) { var myParagraphVNode = createElement('p', 'hi') return createElement('div', [ // 错误 - 重复的 VNode myParagraphVNode, myParagraphVNode ]) }
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render: function (createElement) { return createElement('div', Array.apply(null, { length: 20 }).map(function () { return createElement('p', 'hi') }) ) }