本文正在参加「」
本文是独立组件开发的第四篇文章。
第一篇文章: Vue独立组件开发: prop event slot。
第二篇文章:Vue独立组件开发:不一样的组件通信方式
第三篇文章: Vue独立组件开发:动态组件 Vue.extend及$mount
虚拟 Virtual DOM
一般来说,写 Vue
组件时,模板都是写在 <template>
内的,但它并不是最终呈现的内容,template
只是一种对开发者友好的语法,能够一眼看到 DOM 节点,容易维护,在 Vue
编译阶段,会解析为 Virtual DOM
。
正常的 DOM 节点在 HTML 中是这样的:
<div id="app">
<p>内容</p>
</div>
用 Virtual DOM
创建的 JavaScript
对象一般会是这样的:
const vNode = {
tag: 'div',
data: {
id: 'app'
},
children: [ // p 节点 ]
}
vNode
对象通过一些特定的选项描述了真实的 DOM
结构。
为什么一定要设计 vnode 这样的数据结构呢?它有什么优势呢?
首先是抽象。引入 vnode,可以把渲染过程抽象化,从而使得组件的抽象能力得到提升。
其次是可跨平台。因为对于 patch vnode
的过程,不同的平台可以有自己的实现,再基于 vnode 做服务端渲染、weex平台渲染 或 小程序平台的渲染就变得容易得多。
不过这里要特别注意,在浏览器端使用vnode 并不意味着不用操作 DOM 了。很多人误以为 vnode 的性能一定比手动操作原生DOM 好,这其实是不一定的。
这种基于vnode实现的 MVVM 框架,每次组件渲染生成 vnode的过程,会有一定的耗时,大组件尤其如此。
举个例子,对于一个1000行 x 10列的 table 组件,组件渲染生成 vnode 的过程会遍历1000行 x 10列去创建内部的 cell vnode,整个耗时会比较长。再加上挂载 vnode 生成 DOM 的过程也会有一定的耗时,所以当我们更新组件的时候,用户会感觉到明显的卡顿。
虽然 diff 算法在减少 DOM 操作方面足够优秀,但最终还是免不了操作DOM,因此性能并不是vnode的优势所在。
Render
函数
对于大部分场景,使用 template
足以应付,但如果想完全发挥 JavaScript
的编程能力,或在一些特定场景下,需要使用 Vue 的 Render
函数。
使用 Render 函数开发 Vue.js 组件是要比 template 困难的,原因在于 Render 函数返回的是一个 JS 对象,没有传统 DOM 的层级关系,配合上 if、else、for 等语句,将节点拆分成不同 JS 对象再组装,如果模板复杂,那一个 Render 函数是难读且难维护的。
所以,绝大部分组件开发和业务开发,直接使用 template 语法就可以了,并不需要特意使用 Render 函数,那样只会增加负担,同时也放弃了 Vue.js 最大的优势。
来看一组 template
和 Render
写法的对照:
template
写法
<template>
<div id="" class="container" style="color: yellow">
<p v-if="show">内容 1</p>
<p v-else>内容 2</p>
</div>
</template>
<script>
export default {
data() {
return {
show: false
}
}
}
</script>
Render
函数写法
export default {
data () {
return {
show: false
}
},
render: (h) => {
let childNode;
if (this.show) {
childNode = h('p', '内容 1');
} else {
childNode = h('p', '内容 2');
}
return h('div', {
attrs: {
id: 'app'
},
class: {
container: true
},
style: {
color: 'yellow'
}
}, [childNode]);
}
}
这里的 h
,即 createElement
,是 Render 函数的核心。可以看到,template 中的 v-if / v-else 等指令,都被 JS 的 if / else 替代了,那 v-for 自然也会被 for 语句替代。
h
有 3 个参数,分别是:
1、 要渲染的元素或组件,可以是一个 html 标签、组件选项或一个函数(不常用),该参数为必填项。
// html 标签
h('div');
// 组件
import Test from './component/Test.vue';
h(Test);
2、 对应属性的数据对象,比如组件的 props、元素的 class、绑定的事件、slot、自定义指令等,该参数是可选的,参考数据对象。
3、子节点,可选,String 或 Array,它同样是一个 h
。
[
'内容',
h('p', '内容'),
h(Component, {
props: {
someProp: 'foo'
}
})
]
下面通过一个例子来说明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) 的组件时,你可能很快想到这样实现:
<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>
</template>
<script>
export default {
props: {
level: Number
}
}
</script>
这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了 <slot></slot>
。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
<script>
export default {
render(createElement) {
return createElement(
'h' + this.level, // 标签名称
this.$slots.default // 子节点数组
)
},
props: {
level: {
type: Number,
required: true
}
}
}
</script>
这样代码精简很多。
Functional Render
上面创建的锚点标题组件是比较简单,没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional
,这意味它无状态 (没有响应式数据),也没有实例 (没有 this
上下文)。一个函数式组件就像这样:
Vue.component('my-component', {
functional: true,
// Props 是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render: function (createElement, context) {
// ...
}
})
如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
<template functional>
</template>
再添加 functional: true
之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context
参数,并将 this.$slots.default
更新为 context.children
,然后将 this.level
更新为 context.props.level
。
export default {
functional: true,
props: {
level: {
type: Number,
required: true
}
},
render(createElement, context) {
return createElement('h' + context.props.level, context.children)
}
}
因为函数式组件只是函数,没有状态,不需要经历数据响应式的初始化过程,所以渲染开销也低很多。
由于函数式组件无状态和无实例(this),我们就可以把它用作高阶组件。所谓高阶,就是可以生成其他组件的组件。
在作为包装组件时它们也同样非常有用。比如,当你需要做这些时:
- 程序化地在多个组件中选择一个来代为渲染;
- 在将
children
、props
、data
传递给子组件之前操作它们。
下面是一个 smart-list
组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
functional: true,
props: {
items: {
type: Array,
required: true
},
isOrdered: Boolean
},
render: function (createElement, context) {
function appropriateListComponent () {
var items = context.props.items
if (items.length === 0) return EmptyList
if (typeof items[0] === 'object') return TableList
if (context.props.isOrdered) return OrderedList
return UnorderedList
}
return createElement(
appropriateListComponent(),
context.data,
context.children
)
}
})
从这个例子中,根据传入的props
来选择渲染那个组件,smart-list
组件其实扮演了一个中间组件的角色。
下面再看一个函数式组件作为高阶组件的例子,来加深对函数式组件的理解。
当某个组件的内容需要用户自定义的时候,一般想到都是采用slot
插槽的方式来实现,其实我们也可以使用Functional Render
来实现,而且它能实现slot
不能完成的功能。
1、首先创建一个函数化组件 render.js:
// render.js
export default {
functional: true,
props: {
render: Function
},
render: (h, ctx) => {
return ctx.props.render(h);
}
};
它只定义了一个 props:render,格式为 Function,因为是 functional,所以在 render 里使用了第二个参数 ctx
来获取 props。这是一个中间文件,并且可以复用,其它组件需要这个功能时,都可以引入它。
2、创建组件:
<template>
<div>
<Render :render="render"></Render>
</div>
</template>
<script>
import Render from './render.js';
export default {
components: { Render },
props: {
render: Function
}
}
</script>
3、使用上面的 my-component 组件:
<template>
<div>
<my-component :render="render"></my-component>
</div>
</template>
<script>
import myComponent from '../components/my-component.vue';
export default {
components: { myComponent },
data () {
return {
render: (h) => {
return h('div', {
style: {
color: 'red'
}
}, '自定义内容');
}
}
}
}
</script>
这里的 render.js 因为只是把用户自定义的 Render 内容进行了中转,并无其它用处,所以用了 Functional Render
。
就此例来说,完全可以用 slot
取代 Functional Render
,那是因为只有 render
这一个 prop
。如果示例中的 <Render>
是用 v-for
生成的,也就是多个时,用一个 slot 是实现不了的,那时用 Render 函数就很方便了。
总结
本文首先介绍了虚拟DOM这个数据结构,它对渲染过程进行抽象,为后面的diff算法提供了基础。它还有一个重要的作用是可跨平台,因为它本质是对 HTML 标签的JS化,至于要运行在那个平台,直接转化成对应平台特有的标签语法就可以了。
通常我们写业务组件都是采用template
的形式来写,但是在某些高度灵活且需要用户自定义的场景下使用 Render 函数更加方便与简洁。尽管在某些场景下写slot
也能实现自定义,但是它也有很大的局限性,此时就可以使用 Render 函数了。
对于一些没有数据状态的组件,我们完全可以使用函数式组件来写,函数式组件因为没有状态,可以快速的渲染出内容。通过,可以用函数式组件作为包裹组件,即高阶组件。
本网站是一个以CSS、JavaScript、Vue、HTML为核心的前端开发技术网站。我们致力于为广大前端开发者提供专业、全面、实用的前端开发知识和技术支持。 在本网站中,您可以学习到最新的前端开发技术,了解前端开发的最新趋势和最佳实践。我们提供丰富的教程和案例,让您可以快速掌握前端开发的核心技术和流程。 本网站还提供一系列实用的工具和插件,帮助您更加高效地进行前端开发工作。我们提供的工具和插件都经过精心设计和优化,可以帮助您节省时间和精力,提升开发效率。 除此之外,本网站还拥有一个活跃的社区,您可以在社区中与其他前端开发者交流技术、分享经验、解决问题。我们相信,社区的力量可以帮助您更好地成长和进步。 在本网站中,您可以找到您需要的一切前端开发资源,让您成为一名更加优秀的前端开发者。欢迎您加入我们的大家庭,一起探索前端开发的无限可能!