banner
NEWS LETTER

Vue

Scroll down

Vue官方文档笔记

Vue语法

数据绑定

对于所有的数据绑定,vue提供了JavaScript表达式的支持,例如v-bind{{}}语法,只支持单表达式,js语句是无效的

VUE绑定html元素的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div id="root">
<p>{{name}}</p>
<!-- 可以访问到Vue实例上任意一个属性 -->
<div>{{$emit}}</div>
</div>
<script>
// 第一种方式
const v1 = new Vue({
el: '#root',
data() {
return {name: 'something'}
}
})
// 第二种方式
const v2 = new Vue({
data() {
retutn {name: 'otherthing'}
}
});
v2.$mounted('#root');
</script>

MVVM模型

v-for列表渲染

1
v-for="item in items"

V-for可用于遍历一个对象

1
v-for="(value,name,index)in object"

如果数据项的顺序改变,Vue并不会移动Dom元素顺序,而是就地更新每个元素

数组更新检测

Vue对数组的一些方法进行的重写,调用这些方法时也会触发视图的更新,这些方法包括:

1
2
3
4
5
6
7
push()
pop()
shift()
unshift()
splice()
sort()
reverse()

v-for接受一个整数时,索引将从1开始

显示过滤排序后的结果

可通过创建一个计算属性来返回过滤或排序后的结果,如果计算属性不适用,可以使用一个方法:

1
2
3
4
5
6
v-for="num in even(set)"
methods:{
even:function(){
return this.set.filter(function(){})
}
}

不推荐在同一个元素上使用v-if和v-for,原因:在vue2中v-for的优先级高于v-if,在每次v-for循环都会做一次v-if的判断,可能导致性能问题。而在vue3中v-if优先级高于 v-for,v-if无法访问v-for作用域声明的变量

key属性的作用

用于在新旧虚拟dom做differ对比时,将虚拟dom中有两个有同样key的元素比较,相同的元素直接复用,同一个key不同的元素的会重新渲染到正式dom中


事件处理

v-on可以接受一个方法名作为参数,也就是不加括号的情况(不加括号意味着不需要传入自定义参数),这个时候如果是原生的dom事件,会自动将event作为方法的第一个参数。v-on接受的是无括号的方法名,但是在方法声明的时候又使用了参数,那么这个参数将会是event对象。

除了直接绑定一个方法,v-on也可以传入一个js语句,也就是有括号的情况。这种情况可以接受自定义参数,如果需要访问原始的dom事件,可以用特殊变量$event占位传入方法

vue提供了v-on的事件修饰符

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
<template>
<!-- 阻止事件继续传播 -->
<a v-on:click.stop="function"></a>

<!-- 提交事件不再重载页面,阻止默认行为 -->
<a v-on:submit.prevent="function"></a>

<!-- 事情监听器使用事件捕获模式(从外到具体触发),即内部元素的触发的事件在当前元素先处理,再交给内部元素处理 -->
<a v-on:click.capture="function"></a>

<!-- 仅当event.target是当前元素时触发,即事件不是内部元素触发的 -->
<a v-on:click.self="function"></a>

<!-- 点击事件只会触发一次,once修饰符还可以用到自定义的组件事件上 -->
<a v-on:click.once="function"></a>
</template>
<script>
/*
.passive修饰符,事件的默认行为会立即执行,而不是等待回调事件执行完毕再执行默认行为
比如@wheel事件,默认行为是滚动条向下滚动一截,不加passive时,会先执行函数的行为,再滚动滚动条
注:不是所有事件都会后执行默认行为

@scroll // 是滚动条滚动触发
@wheel // 鼠标滚轮滚动一次触发
修饰符可以串联使用,而且修饰符的顺序很重要
*/
</script>

按键修饰符

1
2
<input v-on:keyup.enter="function">
<input v-on:keyup.page-down="onPageDown">

系统修饰符

1
2
3
4
.ctrl
.alt
.shift
.meta
1
2
请注意修饰键与常规按键不同,在和 keyup 事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl 的情况下释放其它按键,才能触发 keyup.ctrl。而单单释放 ctrl 也不会触发事件。如果你想要这样的行为,请为 ctrl 换用 keyCode:keyup.17。
ctrl+click:<input v-on:click.ctrl="link">
  • .exact修饰符可精准控制系统修饰符组合触发的事件
    1
    2
    3
    4
    5
    6
    没有exact修饰符的,有其他无关按键按下时也可以触发,比如上面的ctrl+click事件,按下ctrl和click的同时有alt或者shift按下也可以触发事件。而使用.exact修饰符修饰的事件,有且只有给定按键按下的时候才会触发。比如:

    有且只有ctrl按下时才会触发:
    <button v-on:click.ctrl.exact="link">
    click
    </button>

鼠标按钮修饰符

  • .left
  • .right
  • .Middle

表单输入绑定

v-model会忽略所有表单元素的value, checked, selected属性的初始值,而总是将传入的数据作为数据来源。

对不同的元素,v-model语法糖使用不同的property和抛出不同的事件(注意不是绑定值):

  1. texttextarea元素使用value属性和input事件
  2. checkboxradio元素使用checked属性和change事件,单个checkbox复选框绑定布尔值,多个checkbox复选框绑定同一数组;多个radio单选框绑定同一个布尔值
  3. select元素使用value属性和change事件

v-model的绑定值通常是字符串类型,也可以通过v-bind实现动态传入


vue组件

在定义一个组件时,data选项必须是一个函数,这是为了每个该组件的实例都可以维护唯一一份它自己的对象拷贝。


vue组件注册

vue组件局部注册

首先可以使用普通js对象来定义一个组件,在另一个组件中注册这个组件:

1
2
3
4
5
6
7
8
9
10
11
12
var component0={
data:function(){}
template:"";
methods:{}
......
};
var component2={
components:{
"c0":component0,
...
}
}

基础组件的全局自动化注册

对于一些简单又常用的基础组件,可能需要频繁地引入,每添加一个组件就要注册一次,比较麻烦,因此可以使用require.context来自动化全局注册这些基础组件。我们不必在每一个需要的地方注册,而是在根组件中注册,根组件是全局注册,注册后可在根组件的任意子组件中使用。在main.js中,用require.context来匹配和获取这些基础组件,再使用Vue.component来全局注册。

注意:全局注册的行为必须在根Vue实例创建之前发生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//在应用的入口文件比如main.js
import
const requireComponent=require.context(
// 组件位置相对路径
"./component.vue",
// 是否查询其子目录
false,
// 匹配基础组件名的的正则表达式
/base/,
);
requireCompoent.keys().forEach(filename=>{
// 获取组件配置
const componentConfig=requireComponent(filename);
// 为组件命名
const componentName="xxx";
// 全局注册组件
Vue.component(
componentName,
componentConfig.default
)
})

prop属性

即使传入给prop的对象是静态的,也要用v-bind,否则会被认为是字符串。

如果要将整个对象的内容都作为prop传入,可使用无参数的v-bind,即obj={a:1,b:2};,它等价于<component :a=1 :b=2></>

父组件prop的更新会向下流到子组件,反向则不行,当父组件的prop发生改变时,子组件中所有prop都会更新,因此不应该在子组件内更改prop的值
可以向prop传一个对象表示验证需求,比如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
props:{
prop0:Number,
prop1: {
type: Object,
required: true,
//如果类型是对象或者数组必须从一个工厂函数获取默认值
default:function(){
return {message:'hello'};
}
// 自定义验证函数
validator:function(value){
return ['apple','banana','orange'].indexOf(value)!==-1;
}
},
}

当prop验证失败,vue将发出警告。

组件在遇到未在props里定义的属性时,这些attribute也会添加到组件的根元素上,而对于已有的属性,大部分属性在遇到外部提供的值时会替换掉,而class和style则会将这些值合并而不是替换。

如果不希望组件的根元素继承传入的attribute,可以在组件选项中设置inheritAttrs: false,配合$attrs属性使用,可以决定属性是赋予到组件的根元素还是其他元素

vue中的$attrs记录了父作用域中不被prop识别或获取的attribute

例子:

1
2
3
4
5
6
7
8
9
<div>
<test :test1="test1" test2="test2" class="container"></test>
</div>
<script>
Vue.component("test",{
props:['test1'],
template:`<div>{{this.$attrs}}</div>`
})
</script>

比如像上面的例子,组件的props中定义了test1属性,但是在test实例中传入了test2属性,test2属性既不是html标签共有的属性,也不是props中声明 的属性,这时,$attrs将会是{test2:test2}。而且,像test2这样的非prop属性会被当成字符串串联到html文档中,即上面的例子组件会被解析成:

1
2
3
4
5
<div>
<div test2="test2" class="container">
"test2":"test2"
</div>
</div>

test2属性存在,即使它可能没有什么用。

Attribute禁用继承


自定义事件

html中是不区分大小写的,官方推荐事件名使用kebab-case命名方式
【注】:通过v-on@传递给组件的自定义事件必须是一个函数,不能是js语句

自定义组件的v-model

一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,但是像单选框、复选框等类型的输入控件可能会将 value attribute 用于不同的目的model 选项可以用来避免这样的冲突

默认情况下,自定义组件的v-model等同于v-bind:value=""v-on:event="",但对于checkbox这样的表单内容并不合理,checkbox绑定的是checked属性和change事件,因此必须在prop设置的model属性中修改绑定的属性和事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.component("myCheckbox",{
model:{
prop:'checked',
event:'change',
},
// 仍然要在props属性里声明checked
props:{
checked:Boolean,
},
template:'<input type='checkbox' :checked='checked' @change=$emit('change',$event.target.checked)'
})
//使用myCheckbox组件
<myCheckbox v-model="mod"></myCheckbox>
// 其中mod

原生事件绑定到组件

如果要在在一个自定义组件上监听一个原生事件比如focus,可以使用.native修饰符,但是很多情况下自定义组件内的根元素并不是我们想要监听的,比如,定义了一个组件<base-input>

1
2
3
<label>
<input></input>
</label>

使用组件时:

1
<base-input @focus.native="onfocus"></base-input>

这时候,父组件的.native将失败,focus函数将不会被如期调用。为此,vue提供了一个**$listeners对象**,包含了所有作用在这个组件上的监听器。通过$listeners对象,配合v-on="$listeners",将所有的事件监听器指向此组件的任意一个需要的子元素。

对于类似 <input> 的你希望它也可以配合 v-model 工作的组件来说,为这些监听器创建一个类似下述 inputListeners 的计算属性通常是非常有用的:

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
> Vue.component('base-input', {
> inheritAttrs: false,
> props: ['label', 'value'],
> computed: {
> inputListeners: function () {
> var vm = this
> // `Object.assign` 将所有的对象合并为一个新对象
> return Object.assign({},
> // 我们从父级添加所有的监听器
> this.$listeners,
> // 然后我们添加自定义监听器,
> // 或覆写一些监听器的行为
> {
> // 这里确保组件配合 `v-model` 的工作
> input: function (event) {
> vm.$emit('input', event.target.value)
> }
> }
> )
> }
> },
> template: `
> <label>
> {{ label }}
> <input
> v-bind="$attrs"
> v-bind:value="value"
> v-on="inputListeners"
> >
> </label>
> `
> })

.sync修饰符

.sync修饰符时vue编译时的一个语法糖,使用了.sync修饰符的会被扩展为一个自动更新父组件属性的v-on监听器。.sync的功能是,当一个子组件改变了一个prop的值,这个变化也会同步到父组件。

如果要对一个prop属性进行双向绑定,推荐以update:[propname]的模式触发事件,例子:假设一个组件包含一个title prop属性:

1
<comp :foo.sync='bar'></comp>

会被扩展为:

1
<comp :foo="bar" @update:foo="(val)=>{bar=val}"></comp>

更新foo的值的时候,它会显性的触发一个事件

1
this.$emit("update:foo",newvalue)

.sync修饰符也可以与v-bind配合使用,需要注意的是:v-bind.sync不能用在一个字面量对象上,比如v-bind.sync=“{foo: bar}”是无效的

1
<comp v-bind.sync="bar"></comp>

父/子/兄弟组件之间传值

参考文档:vue中的数据传递

  • 父传子 prop
  • 子传父 $emit 发布订阅,在子组件订阅自定义方法,子组件方法func中通过$emit触发自定义方法,绑定触发func方法
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
// 子组件改变父组件的值
let vm=new Vue({
el:'#father',
data:{
count:20
},
methods:{
change(val){
this.count = val
},
},
components:{
'child':{
data(){
return{
}
},
props:['cou'],
methods:{
func(){
this.$emit('changeCount',30)
}
},
template:`<button @click='func'>[count+1]</button>`
};
}
})
1
2
3
4
<div id="father">
{{count}}
<child :cou='count' @changeCount='change'></child>
</div>
  • 兄弟之间传递,eventbus,引入第三个new vue作为eventbus

插槽的使用

组件内使用<slot></slot>标签对进行占位
组件使用处在标签内容写要插入的内容

作用域

父级模板里的所有内容都是在父级作用域中编译的;
子模板里的所有内容都是在子作用域中编译的;

具名插槽

在需要多个插槽的情况,可以给每个插槽起名字,<slot>元素可设置name属性,传入插槽内容时,在<template>元素上使用v-slot指令(注:v-slot指令只能用在template元素上)

vue >= 2.6.0版本

1
2
3
4
5
6
7
8
9
10
11
12
组件内:
<div>
<slot name="head"></slot>
<slot name="foot"></slot>
</div>

引用组件处:
<com>
<template v-slot:head>
<p>hahaha</p>
</template>
</com>

作用域插槽

有时需要在父级的插槽内容能够访问到子组件的数据,可以使用作用域插槽。在子组件内的<slot>元素传递 attribute,绑定在slot元素上的attribute被称为插槽prop。
理解:在子组件内把子组件的数据传给slot块,父组件内正是这个slot块的内容,设置一个变量来接收,可以访问到这个数据

1
2
3
4
5
6
7
8
9
10
11
12
<div>
<slot v-bind:user="user">
{{ user.lastName }}
</slot>
</div>

# 父组件引用处
<com>
<template v-slot:head="slotProps">
{{ slotProps.user.name }}
</template>
</com>

当被提供的内容只有默认插槽时(有且只有一个插槽,且是默认插槽),组件的标签才可以被当做插槽的模板来用,这种情况可以把v-slot指令直接用在组件标签上

1
2
3
<component v-slot="slotProps">
{{ slotProps.user.name }}
</component>

这也是唯一一种v-slot使用在非template元素上的情况

作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里。
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。所以在支持的环境下 (单文件组件现代浏览器),你也可以使用 ES2015 解构来传入具体的插槽 prop。这可以使模板更简洁

1
2
3
<component v-slot="{user}">
{{ user.name }}
</component>

关于vue.use()

参考文档:Vue.use详解 浅谈Vue.use

当组件需要用Vue.use方法来引用时,组件内部会编写一个install方法,使用Vue.use方法是将自动调用install方法。在使用Vue.use时可以传入参数,参数会作为install方法

vue响应式原理

  • vue2数据劫持Object.defineProperty/vue3数据代理proxy
    在vue2中,使用Object.defineProperty方法来为数据添加响应式,具体实现:vue为每一个需要响应式的数据创建一个访问器属性getter/setter,如果是对象,对象的每个属性都会创建一个对应的getter/setter,属性仍然是对象,递归处理;如果是数组,出于性能的考虑,不会为数组的每个元素创建getter/setter,而是重写了数组的7个方法,通过这7个方法改变数组都会触发响应式,通过索引改变数组或修改数组长度则不会触发响应式,除非使用$set api手动触发
    与vue2使用Object.defineProperty 对象属性层面上的数据劫持不同,vue3使用proxy实现数据代理,能够拦截更多的变化

  • get捕获器,用于依赖收集

  • set捕获器,用于触发更新

  • deleteProperty捕获器,拦截属性删除

  • 拦截其他(如has, ownKeys捕获器)
    通过reactive()创建响应式数据,内部使用Proxy包装对象,

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    functio reactive(target) {
    return new Proxy(target, {
    get(target, key, receiver) {
    track(target, key);// 依赖收集,记录副作用函数
    return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);// 先执行赋值操作
    trigger(target, key);// 触发更新
    return result;
    },
    // ... 其他捕获器
    })
    }

    对于有多层嵌套的对象来说,在对其创建响应式时,只有第一层会用Proxy包装,内部嵌套的对象则是在第一次访问时才被转换为Proxy代理

  • 依赖收集
    在getter中收集依赖,用到了此数据说明依赖此数据;在setter中通知依赖去更新;为每个数据都创建一个依赖管理器,在vue中称为Dep订阅器,用于收集订阅者,主要用于存放watcher观察者对象,watcher观察者对象会收集、删除依赖和向依赖发送消息。数据变化时,会通知watcher,watcher观察者对象作为一个中介,向所有的订阅者发送消息
    依赖收集发生在 render 阶段,在 Vue 实例进行 $mount 的时候进行

  • 更新实体(发布/订阅模式)

参考文档:vue响应式原理

vue的属性初始化顺序:

参考文档:vue数据响应式原理

  • Props
    1
    //判断
  • methods
  • data
    判断data是否为一个函数,是则执行,不是则直接赋值到vm上的_data
  • computed
  • watch

event bus

空。

用key管理可复用的元素

对以下代码,当切换loginType时,由于两个模板使用了相同的元素,因此input元素不会被替换掉,仅仅是替换掉了placeholder属性,Vue会复用这些元素。

1
2
3
4
5
6
7
8
<template v-if="loginType==='username'">
<label>Username</label>
<input placeholder="Enter Username">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter Email">
</template>

如果要将这两个元素是独立的,不要复用它们,需要添加具有唯一性的key属性。这样在切换loginType时,输入框元素将被重新渲染,但是<label>元素仍然会被复用,因为它们没有添加key属性

1
2
3
4
5
6
7
8
<template v-if="loginType==='username'">
<label>Username</label>
<input placeholder="Enter Username" key="username-input">
</template>
<template v-else>
<label>Email</label>
<input placeholder="Enter Email" key="email-input">
</template>
1
2
/deep/换成:deep() 编辑器中使用正则替换
/deep/\s*(\S+) => :deep($1)

生命周期钩子函数

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
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta charset="UTF-8">
<script type="text/javascript" src="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"></script>
</head>
<body>

<div id="app">
<p>{{ message }}</p>
<button @click="change()">change改变数据</button>
<button @click="destr()">destroy销毁实例</button>
<hr>
<button @click="getDom()">获取实例dom</button>
<button @click="getData()">获取实例data</button>
<button @click="getMethods()">获取实例methods</button>
</div>

<script type="text/javascript">
var obj = {"el: ": this.$el, "data: ":this.data}

function Life(name,el,data,methods){
this['生命周期钩子函数'] = name;
this['el (实例挂载的dom)'] = el;
this['data (实例的数据)'] = data;
this['methods (实例中的方法)'] = methods;
}

var app = new Vue({
el: '#app',
data: {
message: 66
},
beforeCreate: function () {
var obj = new Life('beforeCreate',this.$el,this.message,this.test)
console.table({'创建前':obj})
},
created: function () {
var obj = new Life('created',this.$el,this.message,this.test)
console.table({'创建后':obj})
},
beforeMount: function () {
var obj = new Life('beforeMount',this.$el,this.message,this.test)
console.table({'挂载前':obj})

alert('当前阶段处于 beforeMount ,此时虚拟 dom 已经创建完成,即将开始渲染。')
},
mounted: function () {
// 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。
var obj = new Life('mounted',this.$el,this.message,this.test)
console.table({'挂载后':obj})
},
beforeUpdate: function () {
// 数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
var obj = new Life('beforeUpdate',this.$el,this.message,this.test)
console.table({'数据更新,DOM 重新渲染前':obj})

alert('当前阶段处于 beforeUpdate ,此时实例中的数据已更新,但 dom 上的数据还未更新。')
},
updated: function () {
// 当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之。
var obj = new Life('updated',this.$el,this.message,this.test)
console.table({'数据更新,DOM 重新渲染后':obj})

// 注意 updated 不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated 里使用 vm.$nextTick:

// 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。
// 它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
this.$nextTick(function () {
// DOM 现在更新了
// `this` 绑定到当前实例
console.log('这是 update 中的回调')
})

},
beforeDestroy: function () {
this.change()
// 实例销毁之前调用。在这一步,实例仍然完全可用。
var obj = new Life('beforeDestroy',this.$el,this.message,this.test)
console.table({'实例销毁前,此时实例还完全可用':obj})
},
destroyed: function () {
this.message = 123
// 实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
var obj = new Life('destroyed',this.$el,this.message,this.test)
console.table({'实例销毁后':obj})

this.test()
},
methods: {
test() {
console.log('666')
},
change() {
this.message++;
console.log(this.message)
},
destr() {
this.$destroy();
},
getDom() {
console.log(this.$el)
},
getData() {
console.log(this.message)
},
getMethods() {
console.log(this.test)
}
}
})
</script>
</body>
</html>

生命周期钩子函数 实例阶段 描述 能否获取到el 能否获取到data 能否使用methods
beforeCreate 创建前 实例已经初始化,但数据观测,watch/event事件回调还未配置 不能 不能 不能
created 创建后 已完成:数据观测(data observer)、property和方法的运算,watch/event事件回调 不能
beforeMount 挂载前 dom已初始化,但未挂载和渲染
mounted 挂载后 dom已完成挂载和渲染
beforeUpdate 更新前 数据已改变,但dom未更新
updated 更新后 dom已更新
beforeDestroy 销毁实例前 销毁实例前,实例认可用
destroyed 销毁实例后 实例已销毁,所有指令解绑,子实例都被销毁
对于一个Vue实例,在编译阶段会把vue模板编译成渲染函数,通过new Vue创建一个vue实例后,初始化组件的生命周期钩子函数 - 调用beforeCreate - 设置响应式数据 - 调用creted钩子函数。然后是调用渲染函数,得到虚拟dom - beforeMount钩子调用,再根据虚拟dom生成对应的真实dom节点,挂载到页面的指定节点 - mounted钩子调用;到这一步vue实例挂载完成,
当数据发生变化,虚拟dom会相应改变,然后补丁到真实dom上
如果要销毁一个vue组件,先调用beforeDestroy,然后销毁watch监听器,销毁子组件,事件监听
vue lifecycle

vue过渡 & 动画

Vue 在插入、更新或者移除 DOM 时,提供多种不同方式的应用过渡效果。包括以下工具:

  1. 在 CSS 过渡和动画中自动应用 class
  2. 可以配合使用第三方 CSS 动画库,如 Animate.css
  3. 在过渡钩子函数中使用 JavaScript 直接操作 DOM
  4. 可以配合使用第三方 JavaScript 动画库,如 Velocity.js
    适用于以下情形:
  5. v-if
  6. v-show
  7. 动态组件
  8. 组件的根节点

使用方法:

1
2
3
<transition name="fade">
<p v-show="show">something</p>
<transition>

当插入或删除包含在transition组件内容的元素时,做以下处理:

  1. 自动嗅探目标元素是否应用了 CSS 过渡或动画,如果是,在恰当的时机添加/删除 CSS 类名。
  2. 如果过渡组件提供了 JavaScript 钩子函数,这些钩子函数将在恰当的时机被调用。
  3. 如果没有找到 JavaScript 钩子并且也没有检测到 CSS 过渡/动画,DOM 操作 (插入/删除) 在下一帧中立即执行。(注意:此指浏览器逐帧动画机制,和 Vue 的 nextTick 概念不同)

有6个类名,分别是:

  • v-enter:在进入过渡的开始生效,元素插入的下一帧失效
  • v-enter-to: 进入过渡的结束状态,v-enter失效后紧接着v-enter-to生效
  • v-enter-active:整个进入过渡过程中生效,动画完成后移除,这个类可以用来定义进入过渡的过程时间、延迟和曲线函数
  • v-leave: 离开过渡的开始生效,
  • v-leave-to:离开过渡结束时生效,v-leave失效后紧接着v-leave-to生效
  • v-leave-active:整个离开过渡过程中生效,动画完成后移除,可以用来定义过渡的过程时间、延迟和曲线函数
    vue transition class
    对没有指定name属性的<transition>组件来说,这6个class会以v-开头,否则会以指定的name开头

css过渡和css动画的区别在于:在动画中v-enter类名在节点插入dom时不会立即删除,而是在animationed事件触发时删除

可以在<transition>组件指定自定义过渡类名,这在使用第三方动画库时非常有用。有以下attribute:

  • enter-class
  • enter-to-class
  • enter-active-class
  • leave-class
  • leave-to-class
  • leave-active-class
  • 可以在<transition>组件指定自定义js钩子
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <transition
    :enter-class=""
    :enter-to-class=""
    :enter-active-class=""
    :leave-class=""
    :leave-to-class=""
    :leave-active-class=""
    v-on:before-enter="beforeEnter"
    v-on:enter="enter"
    v-on:after-enter="afterEnter"
    v-on:enter-cancelled="enterCancelled"
    v-on:before-leave="beforeLeave"
    v-on:leave="leave"
    v-on:after-leave="afterLeave"
    v-on:leave-cancelled="leaveCancelled"
    >
    <!-- ... -->
    </transition>

<transition>组件可以设置appear属性来设置节点在初始渲染的过渡,同样包含自定义class和自定义js钩子

1
2
3
4
5
6
7
8
9
10
11
12
<transition  
appear
appear-class="custom-appear-class"
appear-to-class="custom-appear-to-class" (2.1.8+)
appear-active-class="custom-appear-active-class"
v-on:before-appear="customBeforeAppearHook"
v-on:appear="customAppearHook"
v-on:after-appear="customAfterAppearHook"
v-on:appear-cancelled="customAppearCancelledHook"
>
<!-- ... -->
</transition>

transition 组件默认情况下,进入和离开会同时发生,使用mode属性可以修改这过渡属性

  • in-out:新元素先进行过渡,完成之后当前元素过渡离开。
  • out-in:当前元素先进行过渡,完成之后新元素过渡进入。
    多个元素的过渡可以在这些元素上加上key属性,让vue区分它们,并且替换时不会复用
    多个组件的过渡可以使用动态组件

FLIP动画

keep-alive

默认情况下,一个组件实例在被替换掉后会被销毁,这会导致它丢失其中所有已变化的状态——当这个组件再一次被显示时,会创建一个只带有初始状态的新实例。keep-alive是一个内部组件,默认情况下他会缓存内部的所有实例,可以使用 includeexclude props定制哪些组件需要缓存,它会根据组件的name选项进行匹配,所以需要在组件内显式地声明name选项。max prop则可以限制缓存的最大组件实例数,达到最大数后会使用类似LRU算法来重新配置内容


Vue生态

vue-cli

创建一个vue项目的最快方法是用vue-cli脚手架,

vue-cli项目如何引入全局css样式

在main.js文件中可以直接import对应的css、less、scss文件,需要配置对应的loader
自动化导入,可以使用style-resources-loader,下面是官方给的一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vue.config.js
const path = require('path');
function addStyleResource(rule) {
rule.use('style-resource')
.loader('style-resources-loader')
.options({
patterns: [
path.resolve(__dirname, './src/styles/imports.styl')
]
})
}
modules.exports = {
chainWebpack: config => {
const types = ['vue-modules','vue','normal-modules','normal'];
types.forEach(type => {
addStyleResource(config.module.rule('stylus').oneOf(type))
})
}
}

vue-router

基于vue.js及其插件机制,封装了一个全局混入,定义了两个挂载在全局Vue原型上的变量$route$router、注册了两个组件<router-link> <router-view>

==如何用vue router配置一个404页==
在route配置项的最后面配置一个通配符*,路由到404组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import Vue from 'vue';
import Router from 'vue-router';
import page404 from ' 404.vue';

const routes = [
{
path: '/',
component: import('home.vue')
},
{
path: '*',
component: page404
}
];
export default new Router({
routes,
mode: 'history'
});

pinia

vite

我们先提出一些问题,围绕这些问题逐渐了解vite

  1. vite是什么,做什么的?与它功能类似的工具还有什么
  2. 为什么vite这么快
  3. vite中如何引入工程化的css
  4. vite如何处理静态资源
  5. vite中no-bundle的思想是如何体现的?vite预构建?
  6. vite如何处理不同模块规范的代码
  7. vite基于两大构建引擎esbuild和Rollup,他们分别承担了什么任务
  8. vite 打包分包策略 manualChunks

一、vite是什么
vite是一个前端开发与构建工具
二、vite很快
利用现代浏览器原生支持ESM;天然的按需加载;基于esbuild和rollup两大构建引擎
四、Vite如何处理静态资源
Vite在处理静态资源时,一般情况下会将它们转化成模块,而这个模块的默认导出是一个基于import.meta.url 计算出的模块路径。例如通过import导入一张图片,默认情况下会得到一个包含图片路径的对象,经过vite处理后,模块的默认导出会类似于export default 'src/image.png?import'。vite用自定义的查询参数来标识如何解析该资源
vite服务器将对png资源的引用转为模块
image.png
模块内容如下:
image.png
然后后面就有了image.png
特别的,对于svg文件,一般情况vite也是将其归类为静态资源,会按照上面通用的方法处理,这样引用的svg和图片差不多,不能修改颜色样式等。vite-svg-loader 插件增强了svg的引用方式,它会去读取svg的原始代码,处理成一个js模块,vite收到后按照js文件的处理逻辑来处理,这样我们得到的就是一个基于svg的vue/react组件
五、vite预构建
vite将项目代码划分为依赖和源代码,一般情况下依赖不会发生改变,而源代码在开发过程中会频繁发生变动。
vite能够做到no-bundle,利用的是浏览器原生支持esm,vite把模块的导入导出交给浏览器处理,浏览器对每个import的模块都会发出一个http请求。
对项目中的依赖部分来说,其中各个依赖很多,并且依赖的关系比较复杂,如果这一部分交给浏览器处理,浏览器会对每一次依赖的引入发送一个http请求,这样会导致特别多的http请求。再者,并非所有的依赖都是提供了esm实现,对这一部分还需要将其转换为esm模块。因此,vite对项目依赖部分的会进行预构建,即使用esbuild进行打包(esbuild的效率很高)。打包生成的文件相比打包之前会大大减少,并且打包后的代码都是esm。预构建后的代码会缓存到项目目录node_modules/.vite里面,并且配合文件系统缓存+http强缓存减少了这部分到dev server的请求次数。
image.png

image.png

对于项目代码的另一部分:源代码,vite使用no-bundle模式,直接交给浏览器原生esm机制去处理,如果遇到源代码中非esm格式的模块,vite还是会尝试用esbuild将其转换为esm格式。
源代码部分的请求

在生产环境下,vite会将整个项目打包,使用Rollup作为打包工具。vite2.9之后,vite默认打包策略会在index-hash.js chunk中包含依赖的部分,而不是区分index和vendor

vite.config.js提供了optimizeDeps配置项用于手动配置需要预构建的内容,避免动态import加载时,vite需要重新预构建依赖

七、esbuild和rollup承担的角色
esbuild承担了如下工作:最大的特点就是快

  1. 开发环境依赖预构建阶段的打包器
  2. ts和jsx的编译工具,做预发转译,同样是利用了它快的特性
  3. vite>2.6,使用esbuild进行生存环境的代码压缩
    rollup做的事情:
  4. 生存环境默认使用rollup进行打包并进行优化,实现css代码分割、自动预加载、异步chunk加载优化。这些是esbuild还做不到的
  5. 插件机制

八、vite 分包策略

vue源码

vue的compiler编译模块

包含四个目录

  • compiler-core 核心模块,平台无关性
  • compiler-dom 处理浏览器端vue模板的编译过程,会将模板编译成render函数
  • compiler-sfc 单文件组件
  • cimpiler-ssr 服务端渲染

vue 编译的三个阶段

  • ==parse==
  • ==transform==
  • ==codegen==

一、parse 模板解析
接收一个vue模板字符串,将其解析成AST,用于表示代码的抽象语法结构,例如,对下面这个vue组件

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="hello">
<p :title-prop="title"></p>
<h1>{{message}}</h1>
</div>
</template>
<script setup>
import { ref } from 'vue';
const title = ref('hello');
const message = 'hello world';
</script>

会被解析成下面的AST(简化后)

fold
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
{
"type": 0,
"children": [
{
"type": 1,
"tag": "template",
"props": [],
"children": [
{
"type": 1,
"tag": "p",
"props": [
{
"type": 7,
"name": "bind",
"exp": {
"type": 4,
"content": "title",
},
"arg": {
"type": 4,
"content": "title-prop",
},
"modifiers": [],
}
],
"children": [],
},
{
"type": 1,
"tag": "h1",
"props": [],
"children": [
{
"type": 5,
"content": {
"type": 4,
"content": "message",
},
}
],
}
],
},
{
"type": 1,
"tag": "script",
"props": [
{
"type": 6,
"name": "setup",
}
],
"isSelfClosing": false,
"children": [
{
"type": 2,
"content": "\n import { ref } from 'vue';\n const title = ref('hello');\n const message = 'hello world';\n",
}
],
}
],
}

服务端渲染方案

NuxtJs

eggJs

eggJs方案重点解决egg、vue、webpack技术的工程化(框架整合、开发流程、扩展性、稳定与性能等问题),进行合理的模块化和解耦设计,使得整个体系能够自由组合和扩展。egg提供了一套完整的工程化方案开箱即用
使用egg+vue ssr模式开发时,webpack会构建两个实例:node模式和web模式,其中node模式构建给服务端用于生成html,构建后文件存放在app/view目录下。web模式构建给客户端用,构建完成后放在public目录下。【注】只有在正式打包时会生成文件到磁盘,开发模式下不会有上面提到的目录文件,而是在内存中构建。
image.png
正式打包完成后,app/view目录下已有用于服务端的js bundle,public目录下已有用于客户端的js bundle。此时,服务端的html渲染器会渲染出html,再根据manifest.json文件,将js/css资源注入html中,模板渲染完成。
SSR服务端渲染失败时,自动降级为CSR客户端渲染


Vue3

Proxy对象

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)

语法:

1
const p = new Proxy(target, handler);
  • 参数target表示要使用proxy包装的目标对象,可以是任何类型,包括数组,函数,甚至另一个代理。
  • 参数handler是一个通常以函数作为属性的对象,各个函数属性分别定义在执行各种操作时的代理p的行为

使用proxy对象实现拦截对象属性读取和设置操作,

选项式API 和 组合式API

选项式api用导出一个对象来描述一个组件,包含data,methods,mounted等(就是vue2),选项定义的属性都会暴露在函数内部的this上,this指向当前组件实例

组合式api从vue依赖中导入api函数来描述组件,在单vue文件中,通常会与<script setup>搭配使用,setup是一个标识,告诉vue需要在编译时进行一些处理,经过处理后,比如在<script setup>中导入的变量或函数都能在模板中直接使用。

1
2
3
4
5
6
7
8
9
10
<script setup>
import { ref, onMounted } from 'vue';
const sum = ref(0);
function add() {
sum += 1;
}
onMounted(() => {
console.log(sum);
})
</script>

使用createApp来创建一个新的vue实例,实例会暴露一个.config对象用于配置一些应用级别的选项,例如定义一个应用级的错误处理器

1
2
3
app.config.errorHandler = (err) => {
// handler
}

注意确保在挂载实例之前完成所有应用配置

directive 自定义指令
Pasted image 20240517162416

响应式

ref VS reactive

vue3提供了两种声明响应式数据的方法,第一中是ref,
需要使用.value取值的响应式数据有:

  • ref 声明的数据
  • computed

有状态的方法

有些情况下需要动态创建一个方法函数,比如创建一个防抖的事件处理器,如果在methods里面创建:

1
2
3
4
5
export default {
methods: {
click: debounce(function() {})
}
}

由于这个预置防抖函数是有状态的,在运行时维护着一个内部状态,如果多个组件实例共享同一个预置防抖函数,它们之间会相互影响。可以改为在created钩子函数中创建预置防抖函数:

1
2
3
4
5
export default {
created () {
this.debounceClick = debounce(this.click, 400)
}
}

侦听器

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

1
2
3
4
5
6
7
import {ref, watch} from 'vue';
const x = ref(0);
const y = ref(0);

watch(x, (newVal, oldVal) => {}); //ref
watch(() => (x.value+y.value), (sum) => {}); //getter函数
watch([x, () => y.value*2], ([newX, newY]) => {}); //多个数据源

不能直接侦听响应式对象的属性值,需要写为一个getter函数,如果直接将data.count作为watch的第一个参数传入,传入的会是一个number

1
2
const data = reactive({count: 0});
watch(() => data.count, (newCount) => {})

深层侦听器

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器——该回调函数在所有嵌套的变更时都会被触发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const data = reacive({count: 0});
watch(data, (newV, oldV) = {
// newV === oldV因为都是指向同一个对象
// 只是深层侦听器能监听到里面属性值的变化
})
// 如果传入一个返回响应式对象的getter函数,则只会在data被指向别的对象时被触发
watch(() => data, (newV, oldV) => {
// newV !== oldV
// 代码仅在data被指向别的对象时被触发
})
// 通过设置watch函数的第三个参数,可以强制转成深层侦听器
// 如果传入一个返回响应式对象的getter函数,则只会在data被指向别的对象时被触发
watch(() => data, (newV, oldV) => {
// newV !== oldV
// 代码仅在data被指向别的对象时被触发
}, { deep: true })

默认情况下,侦听器回调会在父组件更新 (如有) 之后、所属组件的 DOM 更新之前被调用。如果想在侦听器回调中能访问被 Vue 更新之后的所属组件的 DOM,需要指明 {flush: 'post'}watchEffect方法替换为watchPostEffect
如果要在Vue更新前触发回调,设置选项{flush: 'sync'}watchEffect方法替换为watchSyncEffect

组合式函数、

组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。

所谓有状态的逻辑,指的是会随着时间变化的状态,对应无状态逻辑则指的是在接受输入后立即返回期望的输出,比如lodash这样的就是无状态逻辑库。使用组合式函数的场景:
将可复用的代码逻辑写到一个外部函数去,在函数内部处理状态变化,对外暴露出状态,比如一个useFetch函数,在函数内发送异步请求并处理返回结果和错误处理,返回结果和错误情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// useFetch.js
export default function(url) {
import {ref} from 'vue';
const data = ref(null);
const err = ref(null);

fetch(url)
.then(res => {data.value = res})
.catch(err => {err.value = err});

return {res, err};
}
// comp.vue
<script setup>
import useFetch from 'useFetch.js';
const {data, err} = useFetch('...');

组合式函数通常约定以use开头,使用驼峰式命名。
如果要组合式函数接受一个响应式,比如一个ref或者getter,这样可以在响应式的值变化时,再次出发组合式函数。我们需要配合watchEffecttoValue两个API来改造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// useFetch.js
import {ref, watchEffect, toValue} from 'vue';
export default function useFetch() {
const data = ref(null);
const err = ref(null);
const fet = (url) => {
fetch(toValue(url))
.then(res => {data.value = res})
.catch(err => {err.value = err});
}
watchEffect(() => {
fet(url)
});
return {data, err}
}

注意,组合式函数必须在setup中调用。

suspense 异步组件

工作原理:监测插槽中所有异步依赖,当异步操作未完成时,处于pending状态,显示#fallback内容;异步操作完成后,切换为resolved状态,显示默认插槽内容。并且可以通过onErrorCaptured捕获错误
具体监测原理

监测层面 监测对象 实现方式
组件层面 async setup() 通过组件实例的异步状态标志
模板层面 <script setup> 中顶层的 await 编译时的代码转换,用withAsyncContext包裹,其中会修改suspense的状态
API 层面 异步组件、组合式函数 通过 onSuspense 事件

依赖注入(provideinject

依赖注入是为了解决props逐级透传问题,当需要将数据穿越多级从祖先组件到子组件,中间的组件并不需要这份数据,为避免重复不必要的数据传递,可在祖先组件使用provide,向所有它的子组件注入依赖,称为依赖提供者,而在子组件中,使用inject注入(或者理解为获取)这份依赖数据:

1
2
3
4
5
6
7
8
9
10
11
/* 祖先组件 */
<script setup>
import {provide} from "vue";
provide('注入名', '注入值'); // 值可以是一个响应式
</script>

/* 子组件 **/
<script setup>
import {inject} from 'vue';
const val = inject('注入名', '默认值');
</script>

某些场景下,inject默认值的可能需要使用函数来计算获得,可以使用工厂函数来创建默认值

1
const value = inject('注入名', () => new factory(), true);// true表示第二个参数是一个工厂函数

【注意】这里使用工厂函数而不是普通函数,是因为,工厂函数在每次调用时会创建一个新的对象实例,生产的是各自独立的对象,而通常情况下默认值也是各个子组件各自独立的对象。使用工厂函数能够防止共享对象的副作用,且具有更好的灵活性

expose

vue3的compose API写法中,在父组件引用子组件实例中的方法时,必须要在子组件内用expose暴露出去,否则调用失败。而选项式写法methods中定义的所有方法默认是暴露给父组件的

Vue3的生命周期

在vue3中,vue实例生命周期和2有些不同,
image.png
生命周期钩子:
【==创建阶段】beforeCreate==:
组件实例已经创建,但数据响应式和事件未设置,props、data、methods未初始化,此阶段还无法访问data等组件属性,通常不会做什么逻辑操作。
==【创建阶段】created:==
组件实例已创建完成,数据响应式,计算属性等都已经设置好,此时已经可以访问data等组件属性,但template还未编译,dom还未生成。通常在此执行一些网络请求、定时器设置等初始化操作

在进入beforeMount之前,vue会完成模板的编译
==【挂载阶段】beforeMount:==
挂载之前,vue实例已经完成其响应式设置,即将执行dom渲染过程。通过onBeforeMount注册一个回调,
==【挂载阶段】mounted==
组件挂载到dom完成,通过onMounted注册一个回调函数,用于执行一些必要的副作用如:发送请求,
==【更新阶段】beforeUpdate:==
重新渲染打补丁
==【更新阶段】updated:==
可以在onUpdated注册一个回调函数,它将在vue的响应式数据发生变化触发dom更新之后被调用,它能够访问到更新之后的dom
==【卸载阶段】beforeUnmount:==
通过onUnmounted注册回调函数,在组件被卸载完成后被调用,常用来清理一下副作用如:清理计时器,移除事件监听,断开和服务器的链接等
==【卸载阶段】unmounted:==

==setup函数==
接受两个参数,props和context,在组件被初始化之前执行,是所有生命周期中最早执行的函数,我们大部分的组件逻辑都放在这个函数。
由于它是最早执行的,也在beforeCreate之前,所以在setup内是无法访问到vue实例的,可以这样理解:在setup函数中我们设计了这个vue组件需要有哪些属性,方法,需要执行哪些必要的逻辑


vue2 和 vue3

不同

  1. vue3去掉了过滤器filter,要用方法代替
  2. vue3对未设置指令的template标签,会渲染成html原生的template标签

Vue的服务端渲染

==通用代码==
在服务端和客户端都会运行的代码,
在服务端,只有beforeCreate和created两个生命周期函数会被调用,因此应该避免在这两个函数内编写有副作用的代码,比如setTimeout,否则因为服务端不会有其他生命周期函数的执行,这个timer不会被销毁

在nodejs服务端,服务端代码进入进程时,会进行一次取值并停留在内存中,我们需要为每个请求创建单独的实例对象,

在服务端和客户端,会各自独立运行一套渲染逻辑,两者的运行环境,目标和实现细节均有所不同。在服务端,通常是node.js环境,不会有真实的dom也没有什么事件绑定,其产物是一个静态html字符串;而在客户端,通常是浏览器环境,有真实的可交互的dom,能够绑定事件响应变化

客户端hydration过程

服务端在从服务端返回的是一个静态的html文档,客户端收到这个html并取出页面预请求组合成的聚合变量,将静态的html转化为动态的vue应用,使其可交互,这就是hydration过程。下面按照服务端流程->客户端流程的顺序具体描述一下:

==服务端流程==
调用vue-server-rendererrenderToString方法生成静态HTML,这个HTML会有两个特征:

  1. 包含data-server-rendered属性
  2. 包含页面预请求的聚合数据,__INITIAL_STATE____NUXT__对象
    image.png
    image.png

==客户端流程==
将聚合数据挂载到window全局变量上,app.mount('#app')触发hydration过程,对于包含data-server-rendered属性的dom元素,执行hydrate操作,在hydrate函数中,遍历真实的html dom得到对应虚拟dom。然后客户端那一份文件渲染逻辑中构造的虚拟dom会和服务端真实dom转化得到的虚拟dom会做对比(比较的双方都是虚拟dom),检查是否一致,比较的内容包括元素结构,属性顺序,文本内容等

其他文章