混入

混入提供了一种非常灵活的方式,来分发 `vue` 组件中的可复用功能,一个混入对象可以包含任意组件选项,当组件使用混入对象时,所有混入对象的选项将被混入到组件本身的选项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 定义一个混入对象
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}

// 定义一个使用混入对象的组件
var Component = Vue.extend({ //使用Vue.extend方法创建一个组件构造器
mixins: [myMixin]
})

//将组件构造器实例化
var component = new Component() // => "hello from mixin!"
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
<!-- 这里介绍一下 Vue.extend({}) -->
<!--
Vue.extend({}) 其实是一个vue的构造器,继承自vue

使用这个构造器可以创建一个组件子类,参数是一个包含组件选项的对象,注意data必须是一个函数才可以在extend中使用

当然也可以这样 new Vue.extend()({el: '#app', data(){}}) 但是不推荐这么写 new Vue({}) 不香嘛~
-->

<div id="app"></div>

<script>
var Component = Vue.extend({
template: '<p>{{name}}</p>',
// data: {name: '苏西'},
data(){
// 这里一定要是函数
return {
name: '苏西'
}
},
mounted(){
console.log('佩奇')
}
})

// 创建一个vue组件实例 vm 挂载在app上
new Component().$mount('#app')
</script>

选项合并

当组件和混入对象含有同名选项时,这些选项将会以合适的方式进行合并,默认合并策略如下:

  1. 数据对象在内部进行递归合并,如果发生冲突以组件数据优先
  2. 同名钩子函数合并为一个数组,都会被调用,但混入对象的钩子函数会在组件钩子函数之前调用
  3. 值为对象的选项,如 methodscomponentsdirectives,将会被合并为一个对象,如果键名发生冲突,取组件对象的键值对
  4. Vue.extend()使用同样的策略进行合并
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
<div id="app">
{{name}}
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
// 定义一个混入对象
var myMixin = {
data(){
name: '佩奇' // 不会覆盖组件中的name
},
created: function () {
this.hello(); // 1. 先执行混入的钩子 methods中的hello被组件中的hello所覆盖
},
mounted(){
console.log('混入的钩子函数'); // 3. 执行混入的钩子方法
},
methods: {
hello: function () {
console.log('hello from mixin!')
},
hi(){
console.log('hi mixin 混入')
}
}
}

new Vue({
el: '#app',
mixins: [myMixin],
data(){
return {
name: '苏西'
}
},
created(){
this.hi(); // 2. 执行混入的hi
},
mounted(){
console.log('组件自身的钩子函数'); // 4. 执行组件中的钩子
},
methods: {
hello(){
console.log('hello from component');
}
}
})
</script>

全局混入

混入可以进行全局注册,使用全局混入,它将影响每一个之后创建的 vue 实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
created: function () {
// this.$options 获取当前实例配置项(配置对象,例如 el,data,methods等)
var myOption = this.$options.myOption
if (myOption) {
console.log(myOption)
}
}
})

new Vue({
myOption: 'hello!'
})
// hello

请谨慎使用全局混入,因为它会影响每个单独创建的 vue 实例 (包括第三方组件)。大多数情况下,只应当应用于自定义选项,推荐将其作为插件发布,以避免重复应用混入

自定义选项合并策略

自定义选项将使用默认策略,如果想让自定义选项以自定义逻辑合并,可以向 Vue.config.optionMergeStrategies 添加一个函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 假设组件中有一个属性 myOption 混入中也有一个属性 myOption 下面定义myOption的合并策略
Vue.config.optionMergeStrategies.myOption = function(toVal, fromVal){
// 返回合并后的值
console.log(toVal, fromVal);

if(!toVal){
return fromVal;
}

if(!fromVal){
return toVal;
}

return toVal + fromVal;
}

// 对于多数值为对象的选项,可以使用与 methods 相同的合并策略
var strategies = Vue.config.optionMergeStrategies
strategies.myOption = strategies.methods
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
// 自定义混入策略
Vue.config.optionMergeStrategies.myOption = function(toVal, fromVal){
// 返回合并后的值
console.log(toVal, fromVal);

if(!toVal){
return fromVal;
}

if(!fromVal){
return toVal;
}

return toVal + fromVal;
}

var myMixin = {
myOption: 'suxi'
}

new Vue({
el: '#app',
myOption: 'peiqi',
mixins: [myMixin],
mounted(){
console.log(this.$options.myOption); // suxipeiqi
}
})

自定义指令

除了核心功能默认内置的指令(v-modelv-show),vue 允许注册自定义指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 举个栗子
// 注册一个全局自定义指令 v-focus
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时的回调
inserted: function (el) {
// 聚焦元素
el.focus()
}
})

// 局部注册指令,组件也接受一个 directives 的选项
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus()
}
}
}

// 使用的时候 <input v-focus>

钩子函数

一个指令定义对象可以提供下面几个钩子函数(可选)

  1. bind:只调用一次,指令第一次绑定到元素时调用,这里可以进行一次性的初始化设置
  2. inserted:被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入到文档中)
  3. update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前,指令的值可能发生了改变,也可能没有,但是可以通过比较更新前后的值来忽略不必要的模板更新
  4. componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用
  5. unbind:只调用一次,指令与元素解绑时调用

钩子函数的参数

指令钩子函数会被传入以下参数

  1. el:指令所绑定的元素,可以直接操作 dom
  2. binding:一个对象,格式如下
    • name:指令名,不包括 v- 前缀
    • value:指令的绑定值,例如 v-my-directive="1 + 1",绑定值为 2
    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用,无论是否改变都可用
    • expression:字符串形式的指令表达式,例如 v-my-directive="1 + 1",表达式为 "1 + 1"
    • arg:传给指令的参数,可选,例如 v-my-directive:foo 中,参数是 foo
    • modifiers:一个包含修饰符的对象,例如 v-my-directive.foo.bar 中,修饰符对象为 {foo: true, bar: true}
  3. vnodevue 编译生成的虚拟节点
  4. oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用

除了 el 之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset 来进行

1
<div id="app" v-demo:foo.a.b="message"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Vue.directive('demo', {
bind: function (el, binding, vnode) {
var s = JSON.stringify
el.innerHTML =
'name: ' + s(binding.name) + '<br>' + // demo
'value: ' + s(binding.value) + '<br>' + // hello
'expression: ' + s(binding.expression) + '<br>' + // message
'argument: ' + s(binding.arg) + '<br>' + // foo
'modifiers: ' + s(binding.modifiers) + '<br>' + // {a: true, b: true}
'vnode keys: ' + Object.keys(vnode).join(', ') // 虚拟dom的所有key
}
})

new Vue({
el: '#app',
data: {
message: 'hello!'
}
})

动态指令参数

指令的参数可以是动态的,例如,在 v-mydirective:[argument] = "value" 中,可以根据组件实例数据进行更新

1
2
3
4
<div id="app">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.directive('pin', {
bind: function (el, binding, vnode) {
el.style.position = 'fixed'
var s = (binding.arg == 'left' ? 'left' : 'top')
el.style[s] = binding.value + 'px'
}
})

new Vue({
el: '#app',
data: function () {
return {
direction: 'left'
}
}
})

函数简写

如果需要在 bindupdate 触发相同的行为,而不关心其他钩子,可以写成下面这样

1
2
3
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})

对象字面量

如果指令需要多个值,可以传入一个 javascript 对象字面量,指令函数可以接收合法的 js 表达式

1
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
1
2
3
4
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})

渲染函数 & JSX

vue 推荐在绝大多数情况下使用模板来创建 html,然而在一些场景中,需要 js 的完全编程能力,可以使用渲染函数

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
<!-- 一个组件模板 -->

<!-- 包含了大量的判断和插槽 -->
<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>

<script>
Vue.component('anchored-heading', {
template: '#anchored-heading-template',
props: {
level: {
type: Number,
required: true
}
}
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
// 使用 render函数来重写
Vue.component('anchored-heading', {
render(createElement){
// 这里是默认的插槽
return createElement('h' + this.level, this.$slots.default);
},
props: {
level: {
type: Number,
required: true
}
}
})

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
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 下面介绍 render函数

// render :() => VNode 接收一个函数 返回一个VNode

// createElement函数 可以接收三个参数 其中两个参数可选
// @returns {VNode}
createElement(
// {String | Object | Function}
// 一个 HTML 标签字符串,组件选项对象,或者解析上述任何一种的一个 async 异步函数。必需参数。
'div',

// {Object} 数据对象
// 一个包含模板相关属性的数据对象
// 你可以在 template 中使用这些特性。可选参数。
{

},

// {String | Array}
// 子虚拟节点 (VNodes),由 `createElement()` 构建而成,
// 也可以使用字符串来生成“文本虚拟节点”。可选参数。
[
'先写一些文字',
createElement('h1', '一则头条'),
createElement(MyComponent, {
props: {
someProp: 'foobar'
}
})
]
)

// 第二个参数的数据对象,该参数可选,可不写
{
// 和`v-bind:class`一样的 API
// 接收一个字符串、对象或字符串和对象组成的数组
class: {
foo: true,
bar: false
},
// 和`v-bind:style`一样的 API
// 接收一个字符串、对象或对象组成的数组
style: {
color: 'red',
fontSize: '14px'
},
// 正常的 HTML 特性
attrs: {
id: 'foo'
},
// 组件 props
props: {
myProp: 'bar'
},
// DOM 属性
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',
// 其他特殊顶层属性
key: 'myKey',
// 如果你在渲染函数中给多个元素都应用了相同的 ref 名,
// 那么 `$refs.myRef` 会变成一个数组。
ref: 'myRef',
// 注意,如果需要使用相同的ref,必须带有下方这个属性,否则$refs不是一个数组,而是一个dom
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
<!-- 一个栗子 -->
<div id="app">
<my-demo :level="1"><template v-slot:live><a href="www.baidu.com">百度一下</a></template></my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
let com = Vue.component('MyComponent', {
template: '<h3>{{name}}</h3>',
props: ['name']
})

console.log(com)

Vue.component('my-demo', {
render(createElement){
return createElement('h' + this.level, {
class: {
foo: true,
}
},['wode', createElement('h2', {class: {too: true}},'一个标题'), createElement(com, {props: {name: '苏西'}}), ...this.$slots.live]);
},
props: {
level: {
type: Number,
required: true
}
},
mounted(){
console.log(this.$slots.live)
}
})

new Vue({
el: '#app'
})
</script>

节点、树以及虚拟 dom

1
2
3
4
5
<div>
<h1>My title</h1>
Some text content
<!-- TODO: Add tagline -->
</div>

上面的 html 对应的 dom 节点树如下图

每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点

高效地更新所有这些节点会是比较困难的,然而 vue 可以在模板中或渲染函数自动保持页面的更新,即便数据发生变化

1
<h1>{{ blogTitle }}</h1>
1
2
3
render: function (createElement) {
return createElement('h1', this.blogTitle)
}

组件树中的所有 VNode 必须是唯一的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 不合法的操作
render: function (createElement) {
var myParagraphVNode = createElement('p', 'hi')
return createElement('div', [
// 错误 - 重复的 VNode
myParagraphVNode, myParagraphVNode
])
}

// 如果需要重复渲染,使用以下方式
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Array.apply(this的指向, 一个数组或一个类数组对象)

// Array(size) 也可以创建一个长度为size的数组,但不能使用 map等函数

let arr1 = Array(5);
let arr2 = Array.apply(null, {length: 5});

console.log(arr1); // [空 ×5] 可以看到和普通的undefined又不一样 和自己赋值的不太一样 并没有赋值只是有个length 当使用的时候才会赋值为 undefined
console.log(arr2); // [undefined, undefined, undefined, undefined, undefined]

console.log(arr1[0]); // undefined
console.log([...arr1]); // [undefined, undefined, undefined, undefined, undefined]


let arr3 = Array.apply(null, {'0': 1, '1': 2, length: 2});
console.log(arr3); // [1, 2]

使用 javascript 代替模板功能

v-ifv-for

1
2
3
4
5
<!-- 在模板中使用 v-if 和 v-for -->
<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
11
12
13
// 如果用渲染函数
Vue.component('my-demo', {
props: ['items'],
render(createElement){
if(this.items.length){
return createElement('ul', this.items.map((item) => createElement('li', item.name)))
}else{
return createElement('p', 'No items found');
}
}
})

// 是不是回到了react的感觉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<!-- 完整示例如下 -->
<div id="app">
<my-demo :items="items"></my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('my-demo', {
props: ['items'],
render(h){
if(this.items.length){
return h('ul', this.items.map(item => h('li', item)));
}else{
return h('p', '为空')
}
}
})

new Vue({
el: '#app',
data: {
items: [1, 2, 3, 4, 5]
}
})
</script>

v-model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 需要自己手动实现 v-model的逻辑
data(){
return {value: '苏西'}
},
render(h){
var self = this;
return h('input', {
domProps:{
// input的value属性
value: self.value
},
on: {
// 绑定input事件
input(event){
// 触发input事件
self.value = event.target.value;
}
}
})
}
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
<!-- 完整示例如下 -->
<div id="app">
<my-demo></my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('my-demo', {
// 组件data必须是函数
data(){
return {
value: '苏西'
}
},
render(h){
var self = this;
return h('div', [
h('input', {
domProps: {
value: self.value
},
on: {
input(e){
self.value = e.target.value
}
}
}),
h('span', self.value)
])
}
})

new Vue().$mount('#app')
</script>



<!-- 当然了 vue 基础已经告诉我们 v-model 等价于 v-bind:value 和 v-on:input="value = $event" (type不同处理逻辑不同) 也可以使用下面的方式 -->

<div id="app">
<my-demo :value="value" @input="value = $event"></my-demo>
<span>{{value}}</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('my-demo', {
props: ['value'],
render(h){
var self = this;
return h('input', {
domProps: {
value: self.value
},
on: {
input(e){
self.$emit('input', e.target.value)
}
}
})
}
})

new Vue({data: {value: '苏西'}}).$mount('#app')
</script>

事件 & 按键修饰符

对于 .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
}

对于所有其他的修饰符,私有前缀不是必须的,需要在事件处理函数中进行处理

修饰符前缀
.stopevent.stopPropagation()
.preventevent.preventDefault()
.selfif(event.target !== event.currentTarget) return
按键 .enter.13if(event.keyCode !== 13) return
修饰键.ctrl .alt .shift .metaif(!event.ctrlKey)return
1
2
3
4
5
6
7
8
9
10
11
on: {
keyup: function(event){
// 如果触发的不是事件绑定的元素,不做任何处理
if(event.target !== event.currentTarget) return;

if(!event.shiftKye || event.keyCode !== 13) return;

event.preventDefault();
event.stopPropagation();
}
}

插槽

通过 this.$slots 访问静态插槽的内容,每个插槽都是一个 VNode 数组

1
2
3
4
5
6
7
8
9
render: function(h){
// <div><slot></slot></div>
return h('div', this.$slots.default);
}

render: function(h){
// <div><slot name="sh"></slot></div>
return h('div', this.$slots.sh);
}
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
<!-- 一个栗子 -->

<div id="app">
<my-demo>
<template>
<div style="background-color: red;">
hello
</div>
</template>

<template v-slot:sh>
<div style="background-color: skyblue;">
world
</div>
</template>
</my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('my-demo', {
render(h){
var self = this;
return h('div', [...this.$slots.default, ...this.$slots.sh]);
// return h('div', [this.$slots.default, this.$slots.sh]); 允许这样写 暂时不知道为 ?
}
})

new Vue({data: {value: '苏西'}}).$mount('#app')
</script>

也可以通过 this.$scopedSlots 访问作用域插槽,每个作用域插槽都是返回若干 VNode 的函数

1
2
3
4
5
6
7
8
9
props: ['message'],
render: function (createElement) {
// `<div><slot :text="message"></slot></div>`
return createElement('div', [
this.$scopedSlots.default({
text: this.message
})
])
}
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
<!-- 一个栗子 -->
<div id="app">
<my-demo>
<template v-slot:default="{text}">
<div style="background-color: red;">
{{text}}
</div>
</template>

<template v-slot:sh="{sh}">
<div style="background-color: skyblue;">
{{sh}}
</div>
</template>
</my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('my-demo', {
data(){
return {text: 'suxi', sh: 'peiqi'}
},
render(h){
var self = this;
return h('div', [this.$scopedSlots.default({text: self.text}), this.$scopedSlots.sh({sh: self.sh})])
}
})

new Vue({data: {value: '苏西'}}).$mount('#app')
</script>

如果需要用渲染函数向子组件中传递作用域插槽,可以利用 VNode 数据对象中的 scopedSlots 属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
render: function (createElement) {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return createElement('div', [
createElement('child', {
// 在数据对象中传递 `scopedSlots`
// 格式为 { name: props => VNode | Array<VNode> }
scopedSlots: {
default: function (props) {
return createElement('span', props.text)
}
}
})
])
}
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
<!-- 举个栗子 -->
<div id="app">
<my-demo></my-demo>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('child', {
data(){
return {text: '苏西'}
},
render(h){
let self = this;
return h('div', [this.$scopedSlots.default({
text: self.text
})])
}
})


Vue.component('my-demo', {
data(){
return {text: 'suxi', sh: 'peiqi'}
},
render(h){
var self = this;
return h('div', [h('child', {
scopedSlots: {
default: prop => h('span', prop.text)
}
})])
}
})

new Vue({data: {value: '苏西'}}).$mount('#app')
</script>

<!-- 是不是看起来很绕 很麻烦 -->

JSX

1
2
3
4
5
6
7
8
9
10
11
12
// 写太多的 createElement是一件很痛苦的事请,尤其当文档结构复杂的时候

createElement(
'anchored-heading', {
props: {
level: 1
}
}, [
createElement('span', 'Hello'),
' world!'
]
)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 是不是更像模板语法了
import AnchoredHeading from './AnchoredHeading.vue'

new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
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
<!-- 下面是一个 渲染函数 和 JSX 的对比 -->

<!-- 注意这里不再使用模板 #app 仅用作挂载元素 -->
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('child', {
data(){
return {text: '苏西'}
},
render(h){
let self = this;
return h('div', [this.$scopedSlots.default({
text: self.text
})])
}
})


Vue.component('my-demo', {
props: ['name'],
render(h){
var self = this;
return h('div', [h('child', {
scopedSlots: {
default: prop => h('span', prop.text)
}
}), h('span',{style: {color: 'red'}} self.name)])
}
})

new Vue({
render(h){
return h('my-demo');
}
}).$mount('#app')
</script>
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
// 下面是 JSX 语法 
Vue.component('child', {
data(){
return {text: '苏西'}
},
render(){
return (
<div>
{this.$scopedSlots.default({text: this.text})}
</div>
)
}
})


Vue.component('my-demo', {
props: ['name'],
render(){
return (
<div>
<Child
scopedSlots={{default: props => <span>{props.text}</span>}}
/>
<span style={{color: 'red'}}>{this.name}</span>
</div>
)
}
})

new Vue({
render(){
return <MyDemo name={'佩奇'} />;
}
}).$mount('#app')

// 看起来是不是好多了(如果你熟悉react的话)

当然浏览器是无法直接运行 jsx 的所以还需要使用 babel 和对应的插件进行编译之后运行,下面给出一个栗子

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
64
65
66
67
68
69
// vue create app 创建vue
├── src
│ ├── components
│ │ ├── Child.jsx
│ │ └── MyDemo.jsx
│ ├── App.jsx
└── └── main.js

// 注意 创建vue2 的工程已经配置好了 vue jsx babel插件,无需修改 babel.config.js


// Child.jsx

export default {
data(){
return {text: '苏西'}
},
render(){
return (
<div>
{this.$scopedSlots.default({text: this.text})}
</div>
)
}
}

// MyDemo.js

import Child from './Child';
export default {
props: ['name'],
render(){
return (
<div>
<Child
scopedSlots={{
default: props => <span>{props.text}</span>
}}
/>
<span style={{color: 'red'}}>{this.name}</span>
</div>
)
}
}

// App.jsx

import MyDemo from './components/MyDemo'

export default {
render(){
return <MyDemo name={'佩奇'} />
}
}


// main.js

import Vue from 'vue'
import App from './App'

Vue.config.productionTip = false

new Vue({
render: h => h(App),
}).$mount('#app')


// JSX 省去了组件注册的过程,引入即可使用

函数式组件

使用函数组件,将组件标记为 functional,这也意味着它是无状态的,也没有实例,一个函数组件就像这样

1
2
3
4
5
6
7
8
9
10
11
12
Vue.component('my-component', {
functional: true,
// props是可选的
props: {
// ...
},
// 为了弥补缺少的实例
// 提供第二个参数作为上下文
render(createElement, context){
// ...
}
})

2.3.0 之前的版本,如果一个函数式组件要接收 prop,则 props 选项是必须的,在 2.3.0 或以上的版本,可以省略 props 选项,所有组件上的 attribute 都会被自动隐式解析为 prop,当使用函数式组件时,该引用将会是 HTMLElement 因为它们是无状态的也是无实例的

1
2
3
4
<!-- 如果使用单文件组件 基于模板的函数式组件这样声明 -->
<template functional>

</template>

组件需要的一切都是通过 context 参数传递,它是一个包括以下字段的对象

  1. props:提供所有 prop 的对象
  2. childrenVNode 子节点数组
  3. slots:一个函数,返回包含所有插槽的对象,
  4. scopedSlots:一个暴露传入的作用域插槽对象,也可以以函数形式暴露普通插槽
  5. data:传递给组件的整个数据对象,作为 createElement 的第二个参数传入组件
  6. parent:对父组件的引用
  7. listeners:包含所有父组件当前组件注册的事件监听器的对象,这是 data.on 的一个别名
  8. injections:如果使用 inject 选项,则该对象包含了应当被注入的 property

函数式组件只是函数,所以渲染开销也会低很多,在作为包装组件时它们也同样非常有用,比如

  1. 程序化地在多个组件中选择一个来代为渲染
  2. 在将 childrenpropsdata 传递给子组件之前操作它们
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
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
)
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 一个简单的栗子 -->
<div id="app"></div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
<script>
Vue.component('child', {
functional: true,
render(h, context){
console.log(context)
return h('div', context.data, context.children)
}
})

new Vue({
render(h){
return h('child',{style: {color: 'red'}} '苏西');
}
}).$mount('#app')
</script>

向子组件或子元素传递属性和事件

在普通组件中,没有被定义为 prop 的属性会自动添加到组件的根元素上,将已有的同名属性进行替换或与其进行智能合并

1
2
3
4
5
6
7
8
9
Vue.component('my-functional-button', {
functional: true,
render: function (createElement, context) {
// 完全透传任何 attribute、事件监听器、子节点等。
return createElement('button', context.data, context.children)
}
})

// context.data实际上就是使用组件时传入的 props class style on 等等属性封装的数据对象

通过向 createElement 传入 context.data 作为第二个参数,我们就把 my-functional-button 上面所有的 attribute 和事件监听器都传递下去了。事实上这是非常透明的,以至于那些事件甚至并不要求 .native 修饰符

如果你使用基于模板的函数式组件,那么你还需要手动添加 attribute 和监听器。因为我们可以访问到其独立的上下文内容,所以我们可以使用 data.attrs 传递任何 HTML attribute,也可以使用 listeners (即 data.on 的别名) 传递任何事件监听器

1
2
3
4
5
6
7
8
9
<template functional>
<button
class="btn btn-primary"
v-bind="data.attrs"
v-on="listeners"
>
<slot/>
</button>
</template>

slots()children 对比

slots 是一个函数,返回所有插槽,children 只表示组件内部的元素

1
2
3
4
5
6
<my-functional-component>
<p v-slot:foo>
first
</p>
<p>second</p>
</my-functional-component>

对于上面的组件,children 会给你两个段落标签,而 slots().default 只会传递第二个匿名段落标签,slots().foo 会传递第一个具名段落标签,可以选择性让组件感知某个插槽机制,也可以简单的传递 children 移交给其他组件进行处理

插件

插件通常用来为 vue 添加全局功能,例如:

  1. 添加全局方法或属性
  2. 添加全局资源
  3. 通过全局混入来添加一些组件选项
  4. 添加 vue 实例方法,通过把它们添加到 Vue.prototype 上实现
  5. 一个库,提供自己的 API 同时提供上面的一个或几个功能

使用插件

1
2
3
4
5
6
7
8
9
10
11
// 通过全局方法 Vue.use() 使用插件,它需要在你调用 new Vue()启动应用之前完成

// 调用 `MyPlugin.install(Vue)`
Vue.use(MyPlugin)

new Vue({
// ...组件选项
})

// 也可以传入一个可选的选项对象
Vue.use(MyPlugin, { someOption: true })

Vue.use 会自动阻止多次注册相同插件,即使多次调用,也会只注册一次

开发插件

vue 的插件应该暴露一个 install 方法,这个方法的第一个参数是 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
26
27
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}

// 2. 添加全局资源 自定义指令
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})

// 3. 注入组件选项 全局混入
Vue.mixin({
created: function () {
// 逻辑...
}
...
})

// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}

过滤器

vue 允许自定义过滤器,可被用于一些常见的文本格式化,过滤器可以用在两个地方:双花括号插值和 v-bind 表达式,过滤器应该被添加在 js 表达式的尾部,有管道(|)符号指示

1
2
3
4
5
<!-- 在双花括号中 -->
{{ message | capitalize }}

<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>

在组件中定义本地过滤器

1
2
3
4
5
6
7
filters: {
capitalize(value){
if(!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
}
}

或者在创建 vue 实例之前全局定义过滤器

1
2
3
4
5
Vue.filter('capitalize', function(value){
if(!value) return '';
value = value.toString();
return value.charAt(0).toUpperCase() + value.slice(1);
})

当全局过滤器和局部过滤器重名时,会采用局部过滤器,过滤器函数总接收表达式的值作为第一个参数,在上例中,capitalize 会接收 message 的值作为第一个参数,并且过滤器可以串联

1
{{message | filterA | filterB}}

filterA 被定义为接收单个参数的过滤器函数,表达式 message 的值将作为参数传入到函数,然后继续调用同样被定义为接收单个参数的过滤器函数 filterB,将 filterA 的结果传递到 filterB 中。过滤器是 JS 函数,因此可以接收参数

1
2
3
{{ message | filterA('arg1', arg2) }}

<!-- message作为第一个参数,普通字符串 `arg1` 作为第二个参数,表达式 arg2 作为第三个参数 -->