一、模板和渲染函数
1. Vue 的工作原理
Vue 实现了 MVVM 模式,它维护了数据层、视图层和(不可见的)视图模型层,它将监听数据层和视图模型层的变化,数据驱动视图、视图改变数据。
作为 Vue 应用开发者,应该向 Vue 提供数据和”视图样板”,以便 Vue 进行页面的渲染。
对于 “视图样板” 的提供,Vue 有模板和渲染函数两种方式。
2. 模板和渲染函数
Vue 更推荐使用模板绘制 “视图样板”。
但在某些场景下,使用模板会导致代码冗长繁琐且重复,因此 Vue 也提供了渲染函数。
3. 模板也会被编译为渲染函数
Vue 的模板实际上被编译成了渲染函数。
4. 示例
假设要在页面上渲染一个每秒递增的计数器。
(1) 模板
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <h1>{{ timer }}</h1> </template>
<script> export default { name: "demo", data() { return { timer: 0 } }, mounted() { setInterval(() => { this.timer++ }, 1000) } } </script>
|
(2) 渲染函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <script> export default { name: "demo", render(createElement, context) { return createElement('h1', this.timer) }, data() { return { timer: 0 } }, mounted() { setInterval(() => { this.timer++ }, 1000) } } </script>
|
二、模板功能的替代
1. 元素
(1) createElement()
createElement()
方法用于创建 dom 元素。
需要注意的是:
createElement()
方法并不等同于 document.createElement()
createElement()
方法和模板中的标签一样,只是告诉 Vue “视图样板” 中应该包含的 dom 元素,具体的渲染将会交给 Vue 处理
createElement()
方法将会返回一个对象,这个对象被称为 VNode,即 “虚拟 Node”
- 一般将
h
作为 createElement
的别称
(2) 使用
1
| createElement([HTML标签名/组件选项对象], [可选的属性对象], [字符串或子节点数组])
|
1 2 3 4 5 6 7 8 9 10 11
| <script> export default { name: "IFormRow", render(createElement, context) { return createElement('h1', [ "Hello", createElement('h3', '小张') ]) } } </script>
|
(3) 属性对象
属性对象是可选的,它和模板中的 attribute 相对应。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| { // 与 `v-bind:class` 的 API 相同, // 接受一个字符串、对象或字符串和对象组成的数组 'class': { foo: true, bar: false }, // 与 `v-bind:style` 的 API 相同, // 接受一个字符串、对象,或对象组成的数组 style: { color: 'red', fontSize: '14px' }, // 普通的 HTML attribute attrs: { id: 'foo' }, // 组件 prop props: { myProp: 'bar' }, // DOM property domProps: { innerHTML: 'baz' }, // 事件监听器在 `on` 内, // 但不再支持如 `v-on:keyup.enter` 这样的修饰器。 // 需要在处理函数中手动检查 keyCode。 on: { click: this.clickHandler }, // 仅用于组件,用于监听原生事件,而不是组件内部使用 // `vm.$emit` 触发的事件。 nativeOn: { click: this.nativeClickHandler }, // 自定义指令。注意,你无法对 `binding` 中的 `oldValue` // 赋值,因为 Vue 已经自动为你进行了同步。 directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], // 作用域插槽的格式为 // { name: props => VNode | Array<VNode> } scopedSlots: { default: props => createElement('span', props.text) }, // 如果组件是其它组件的子组件,需为插槽指定名称 slot: 'name-of-slot', // 其它特殊顶层 property key: 'myKey', ref: 'myRef', // 如果你在渲染函数中给多个元素都应用了相同的 ref 名, // 那么 `$refs.myRef` 会变成一个数组。 refInFor: true }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <script> export default { name: "IFormRow", render(createElement, context) { return createElement('h1', [ "Hello", createElement('h4', { style: { color: 'red', fontSize: '14px' } }, '小张') ]) } } </script>
|
(4) 注意事项
createElement()
方法返回的 VNode 对象必须是唯一的,因此保存 createElement()
方法的返回值,并将其重复返回的做法是错误的,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script> export default { name: "demo", render(createElement, context) { var VNode = createElement('p', 'test') return createElement('div', [ VNode, VNode, VNode ]) } } </script>
|
正确的做法是重复生成:
1 2 3 4 5 6 7 8 9 10
| <script> export default { name: "demo", render(createElement, context) { var VNode = createElement('p', 'test') let VNodeList = Array.apply(null, { length: 20 }).map(() => createElement('p', 'hi')) return createElement('div', VNodeList) } } </script>
|
2. v-if、v-else
通过 JavaScript 中的 if else 替代。
3. v-for
通过 JavaScript 中的 for 或 map 替代。
4. v-model
渲染函数中没有 v-model 的直接对应,需要分开实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <script> export default { name: "demo", render(createElement, context) { var that = this return createElement('input', { domProps: { value: that.value }, on: { input: function (event) { that.value = event.target.value } } }) }, data() { return { value: '' } } } </script>
|
5. 事件修饰符
对于 .passive
、.capture
和 .once
这些事件修饰符,Vue 提供了相应的前缀可以用于 on
:
修饰符 |
前缀 |
.passive |
& |
.capture |
! |
.once |
~ |
.capture.once 或 .once.capture |
~! |
例如:
1 2 3 4 5
| on: { '!click': this.doThisInCapturingMode, '~keyup': this.doThisOnce, '~!mouseover': this.doThisOnceInCapturingMode }
|
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 |
处理函数中的等价操作 |
.stop |
event.stopPropagation() |
.prevent |
event.preventDefault() |
.self |
if (event.target !== event.currentTarget) return |
按键: .enter , .13 |
if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码) |
修饰键: .ctrl , .alt , .shift , .meta |
if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey 、shiftKey 或者 metaKey ) |
这里是一个使用所有修饰符的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| on: { keyup: function (event) { if (event.target !== event.currentTarget) return if (!event.shiftKey || event.keyCode !== 13) return event.stopPropagation() event.preventDefault() } }
|
6. 插槽
(1) 区别
模板中的插槽:
渲染函数中的插槽:
- 子组件中:同样需要显式定义,通过
this.$slots.插槽名
或 this.$scopedSlots.插槽名({...})
获取插入插槽的内容并放置到目标位置
- 插入时:需要通过属性对象指定要插入的插槽
(2) 方法
在子组件中,通过 this.$slots
访问插入插槽中的内容
具名插槽的访问方式:this.$slots.插槽名
默认插槽的访问方式:this.$slots.default
在子组件中,如果想向插槽中放置数据(作用域插槽),可以通过 this.$scopedSlots.插槽名({...})
方法访问插入作用域插槽中的内容,并通过为 this.$scopedSlots.插槽名({...})
方法填入对象参数来为作用域插槽增加参数
1 2 3 4 5 6 7
| render: function (createElement) { return createElement('div', [ this.$scopedSlots.default({ text: this.message }) ]) }
|
等效于:
1 2 3
| <div> <slot :text="message"></slot> </div>
|
在向子组件中插入内容时,通过属性对象中的 scopedSlots
获取作用域插槽
(2) 简单示例
调用子组件时,将内容插入到子组件之中。
模板写法:
app.vue:
1 2 3 4 5 6 7 8 9
| <template> <div id="app"> <Children> <template slot="name"> <p>小张</p> </template> </Children> </div> </template>
|
Children.vue:
1 2 3 4 5 6 7 8
| <template> <div> <p>Children</p> <div style="background: red"> <slot name="name"></slot> </div> </div> </template>
|
渲染函数写法:
app.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script> export default { name: 'App', components: { Children }, render(createElement, context) { return createElement( 'div', { id: 'app' }, [ createElement(Children, [ createElement('p', {slot: 'nickname'}, '小张') ]) ] ) } } </script>
|
Children.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| <script> export default { name: "Children", render(createElement, context) { return createElement('div', [ createElement('p', 'Children'), createElement( 'div', { style: { background: 'red' } }, this.$slots.nickname) ]) } } </script>
|
(4) 作用域插槽示例
调用子组件时,将内容插入到子组件之中,但内容来自子组件内部。
模板写法:
app.vue:
1 2 3 4 5 6 7 8 9
| <template> <div id="app"> <Children> <template slot="nickname" slot-scope="scope"> <p>{{ scope.nickname }}</p> </template> </Children> </div> </template>
|
Children.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| <template> <div> <p>Children</p> <div style="background: red"> <slot name="nickname" :nickname="nickname"></slot> </div> </div> </template>
<script> export default { name: "Children", data() { return { nickname: '不记得' } } } </script>
|
渲染函数写法:
app.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script> export default { name: 'App', components: { Children }, render(createElement, context) { return createElement( 'div', { id: 'app' }, [ createElement(Children, { scopedSlots: { nickname: function(props) { return createElement("p", props.nickname) } } }) ] ) } } </script>
|
Children.vue:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <script> export default { name: "Children", render(createElement, context) { return createElement('div', [ createElement('p', 'Children'), createElement( 'div', { style: { background: 'red' } }, this.$scopedSlots.nickname({ nickname: this.nickname })) ]) }, data() { return { nickname: '不记得' } } } </script>
|
三、JSX
JSX 语法可以用于简化渲染函数的书写,利用 JSX 语法在可以在 js 中书写更接近于模板的代码。
示例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| <script> export default { name: 'App', components: { Children }, render(h) { return h( <div>Hello World!</div> ) } } </script>
|
需要注意的是:
参考