基础
Vue 推荐在绝大多数情况下使用模板来创建 HTML, 然而在一些场景中,真的需要 JavaScript 的完全编程的能力。这时可以用渲染函数,它比模板更接近编译器。
例子:
生成一些带锚点的标题
1 2 3 4 5
| <h1> <a name="hello-world" href="#hello-world"> Hello world! </a> </h1>
|
组件接口
1
| <anchored-heading :level="1">Hello world!</anchored-heading>
|
通过 level prop 动态生成标题的组件时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| <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>
|
1 2 3 4 5 6 7 8 9
| Vue.component('anchored-heading', { template: '#anchored-heading-template', props: { level: { type: Number, required: true, }, }, })
|
使用 render 函数重写
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| Vue.component('anchored-heading', { render(createElement) { return createElement( 'h' + this.level, this.$slots.default ) }, props: { level: { type: Number, required: true, }, }, })
|
节点、树以及虚拟 DOM
当浏览器读到这些代码时,它会建立一个“DOM 节点”树来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
1 2 3 4 5
| <div> <h1>My title</h1> Some text content </div>
|
!['DOM']()
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
虚拟 DOM
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM
1
| return createElement('h1', this.blogTitle)
|
createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
createElement 参数
如何在 createElement 函数中使用模板中的那些功能。这里是 createElement 接受的参数:
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
| createElement( 'div',
{ },
[ '先写一些文字', createElement('h1', '一则头条'), createElement(MyComponent, { props: { someProp: 'foobar', }, }), ] )
|
深入数据对象
如 v-bind:class 和 v-bind:style 在模板语法中会被特别对待一样,它们在 VNode 数据对象中也有对应的顶层字段。该对象也允许你绑定普通的 HTML attribute,也允许绑定如 innerHTML 这样的 DOM 属性 (这会覆盖 v-html 指令)。
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
| { 'class': { foo: true, bar: false }, style: { color: 'red', fontSize: '14px' }, attrs: { id: 'foo' }, props: { myProp: 'bar' }, domProps: { innerHTML: 'baz' }, on: { click: this.clickHandler }, nativeOn: { click: this.nativeClickHandler }, directives: [ { name: 'my-custom-directive', value: '2', expression: '1 + 1', arg: 'foo', modifiers: { bar: true } } ], scopedSlots: { default: props => createElement('span', props.text) }, slot: 'name-of-slot', key: 'myKey', ref: 'myRef', refInFor: true }
|
完整示例
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
| 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) { 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, }, }, })
|
VNode 必须唯一 组件树中的所有 VNode 必须是唯一的。
使用 JavaScript 代替模板功能
v-if 和 v-for
1 2 3 4
| <ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li> </ul> <p v-else>No items found.</p>
|
1 2 3 4 5 6 7 8 9 10
| props: ['items'], render: function (createElement) { if (this.items.length) { return createElement('ul', this.items.map(function (item) { return createElement('li', item.name) })) } else { return createElement('p', 'No items found.') } }
|
v-model
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| props: ['value'], render: function (createElement) { var self = this return createElement('input', { domProps: { value: self.value }, on: { input: function (event) { self.$emit('input', event.target.value) } } }) }
|
事件 & 按键修饰符
对于 .passive、.capture 和 .once 这些事件修饰符, Vue 提供了相应的前缀可以用于 on:
- 修饰符
.passive -> 前缀 &
- 修饰符
.capture -> 前缀 !
- 修饰符
.once -> 前缀 ~
- 修饰符
.capture.once 或 .once.capture -> 前缀 &
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
- 修饰符
.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() } }
|
插槽
可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
1 2 3 4
| render(createElement) { return createElement('div', this.$slots.default) }
|
也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是一个返回若干 VNode 的函数:
1 2 3 4 5 6 7 8 9
| props: ['message'], render: function (createElement) { return createElement('div', [ this.$scopedSlots.default({ text: this.message }) ]) }
|
如果要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 字段:
1 2 3 4 5 6 7 8 9 10 11 12 13
| render: function (createElement) { return createElement('div', [ createElement('child', { scopedSlots: { default: function (props) { return createElement('span', props.text) } } }) ]) }
|
JSX
有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
1 2 3 4 5 6 7 8 9 10 11 12
| import AnchoredHeading from './AnchoredHeading.vue'
new Vue({ el: '#demo', render: function (h) { return ( <AnchoredHeading level={1}> <span>Hello</span> world! </AnchoredHeading> ) }, })
|
将 h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例,实际上也是 JSX 所要求的。从 Vue 的 Babel 插件的 3.4.0 版本开始,我们会在以 ES2015 语法声明的含有 JSX 的任何方法和 getter 中 (不是函数或箭头函数中) 自动注入 const h = this.$createElement,这样你就可以去掉 (h) 参数了。对于更早版本的插件,如果 h 在当前作用域中不可用,应用会抛错。
函数式组件
1 2 3 4 5 6 7 8 9 10 11 12
| Vue.component('my-component', { functional: true, props: { }, render: function (createElement, context) { }, })
|
在 2.5.0 及以上版本中,如果你使用了单文件组件,那么基于模板的函数式组件可以这样声明:
1
| <template functional> </template>
|
组件需要的一切都是通过 context 参数传递,它是一个包括如下字段的对象:
- props:提供所有 prop 的对象
- children: VNode 子节点的数组
- slots: 一个函数,返回了包含所有插槽的对象
- scopedSlots: (2.6.0+) 一个暴露传入的作用域插槽的对象。也以函数形式暴露普通插- 槽。
- data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
- parent:对父组件的引用
- listeners: (2.3.0+) 一个包含了所有父组件为当前组件注册的事件监听器的对象。这是 data.on 的一个别名。
- injections: (2.3.0+) 如果使用了 inject 选项,则该对象包含了应当被注入的属性。
在添加 functional: true 之后,需要更新我们的锚点标题组件的渲染函数,为其增加 context 参数,并将 this.$slots.default 更新为 context.children,然后将 this.level 更新为 context.props.level。
下面是一个 smart-list 组件的例子,它能根据传入 prop 的值来代为渲染更具体的组件:
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
| 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 ) }, })
|
向子元素或子组件传递 attribute 和事件
在普通组件中,没有被定义为 prop 的 attribute 会自动添加到组件的根元素上,将已有的同名 attribute 进行替换或与其进行智能合并。
然而函数式组件要求你显式定义该行为:
1 2 3 4 5 6 7
| Vue.component('my-functional-button', { functional: true, render: function (createElement, context) { return createElement('button', context.data, context.children) }, })
|
通过向 createElement 传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的 attribute 和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native 修饰符。
如果你使用基于模板的函数式组件,那么你还需要手动添加 attribute 和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs 传递任何 HTML attribute,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器。
1 2 3 4 5
| <template functional> <button class="btn btn-primary" v-bind="data.attrs" v-on="listeners"> <slot /> </button> </template>
|
slots() 和 children 对比
对于这个组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签,slots().foo 会传递第一个具名段落标签。同时拥有 children 和 slots(),因此你可以选择让组件感知某个插槽机制,还是简单地通过传递 children,移交给其它组件去处理。
1 2 3 4 5 6
| <my-functional-component> <p v-slot:foo> first </p> <p>second</p> </my-functional-component>
|