应用

应用实例

应用通过 createApp 函数创建应用实例

1
2
3
4
5
import { createApp } from "vue";

const app = createApp({
// 根组件选项
});

根组件

1
2
3
import App from "./app.vue";

const app = createApp(App);

挂载应用

1
<div id="app"></div>
1
app.mount("#app");

.mount方法返回根组件实例而非应用实例

dom 中的根组件模版

根组件的模版也是组件本身的一部分,但也可以直接通过在挂载容器内编写模板来单独提供

当根组件没有设置 template 选项时,vue 将自动使用容器的 innerHTML 作为模版

dom 内模版通常用于无构建步骤的 vue 应用,其根模版可由服务端生成

应用配置

应用实例暴露一个 .config 对象,允许配置一些应用级的选项

1
2
3
4
5
6
7
// 定义错误处理器,捕获子组件上的错误
app.config.errorHandler = error => {
// 处理错误
};

// 注册应用范围内的组件
app.component("TodoDeleteButton", TodoDeleteButton);

多个应用实例

应用实例不限制个数。可以在同一个页面创建多个 vue 应用,每个应用拥有自己的作用域

1
2
3
4
5
6
7
8
9
10
11
const app1 = createApp({
// 配置
});

app1.mount("#container-1");

const app2 = createApp({
// 配置
});

app2.mount("#container-2");

模版语法

文本插件

1
2
<!-- message 被替换为实例中的 message 的值 -->
<span>Message: {{ message }}</span>

原始 html

指令 {{ }} 会被解释为纯文本,而不是 html.v-html 指令将解析 html

渲染 html 容易造成 XSS 漏洞

Attribute 绑定

v-bind 指令可以响应式地绑定一个 attribute,如果绑定的值是 nullundefined,那么 attribute 将会从渲染的元素上移除

1
2
3
4
5
6
7
8
9
10
11
12
13
<div v-bind:id="id"></div>

<!-- v-bind的简写 -->
<div :id="id"></div>

<!-- v-bind 同名简写 vue3.4版本及其以上版本适用 -->
<div :id></div>
<!-- 等价于 -->
<div :id="id"></div>
<!-- 等价于 -->
<div v-bind:id></div>
<!-- 等价于 -->
<div v-bind:id="id"></div>

布尔型 Attribute

1
2
3
4
5
6
7
8
<!-- 取决于 disabled的值,如果是真值 则为真 -->
<button :disabled="disabled">按钮</button>
<!-- 这种也是 disabled态 -->
<button disabled="">按钮</button>

<button :disabled="true">按钮</button>
<!-- 等价于 -->
<button disabled>按钮</button>

动态绑定多个值

1
2
3
4
5
6
7
8
9
10
11
12
13
<div v-bind="attribute"></div>
// 等价于
<div
:id="attribute.id"
:class="attribute.class"
:style="attribute.style"
></div>

const attribute = {
id: "container",
class: "wrapper",
style: "background-color: red"
}

表达式

  1. 用于插值中
  2. 用于指令中
  3. 只支持表达式,代码片段是无效的
  4. 可绑定函数
1
2
3
4
5
6
7
8
9
10
11
{{ number + 1 }}

{{ ok ? "yes" : "no" }}

<div :id="`list-${id}`"></div>

<time :title="formatTitle(date)"></time>

<!-- error -->

{{ var a = 1 }}

受限制的全局访问

  1. 模版中的表达式会被沙盒化,仅能访问到有限的全局对象列表
  2. 没有显式包含在列表中的全局对象将不能在模板内表达式中访问,例如用户附加在 window 上的属性
  3. 可自行在 app.config.globalProperties 上显式的添加它们,以便于在组件中使用
    • 注意这个过程是在执行 mount 之前进行

指令 Directives

指令是带有 v- 前缀的特殊 attribute 例如 v-bind v-html v-for v-if v-on v-slot v-show

参数 Arguments

某些指令需要一个参数在指令后通过冒号隔开作为标识

1
2
3
4
5
<a v-bind:href="url">跳转</a>
<a :href="url">跳转</a>

<a v-on:click="clickEvent">跳转</a>
<a @click="clickEvent">跳转</a>

动态参数

指令需要的参数也可以使用表达式 需要包含到 []

1
2
3
4
<a v-bind:[attributeName]="url">跳转</a>
<a :[attributeName]="url">跳转</a>
<!-- js -->
const attributeName = "href";
  1. 动态参数值的限制

    • 动态参数中的表达式应该是一个字符串或者 null
    • null 意为显式移除该绑定
    • 其他非字符串的值会触发警告
  2. 动态参数语法的限制

    • 动态参数表达式因为某些字符的缘故有一些语法限制,比如空格和引号,在 HTML attribute 名称中都是不合法的
    • 使用 dom 内嵌模版,避免在名称中使用大写字母,因为浏览器会强制将它转化为小写
    1
    2
    3
    4
    5
    <!-- 触发一个编译警告 -->
    <a :['foo' + bar]="value">jump</a>

    <!-- 在内嵌模版中 somAttr 会转为 someattr -->
    <a :[someAttr]="value">jump</a>

修饰符

修饰符是以 . 开头的特殊后缀,表明指令需要以一些特殊的方式绑定。例如 v-on 指令对触发的事件调用 event.preventDefault

1
<form @submit.prevent="onSubmit">...</form>

响应式原理

proxy

1
let proxy = new Proxy(target, handler);
  1. target 要包装的对象,可以是任何东西,包括函数

  2. handler 代理配置:带有 捕捉器

    • get 捕捉器用于读取 target 属性,set 捕捉器用于写入 target 的属性
    • 没有捕捉器时,所有 proxy 的操作都直接转发给 target
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let target = {};
    let proxy = new Proxy(target, {});
    proxy.test = 5;
    console.log(target.test); // 5
    console.log(proxy.test); // 5

    for (let key in proxy) {
    console.log(key); // test
    }
  3. proxy 是一种特殊的奇异对象,它没有自己的属性,如果 handler 为空,则透明地将操作转发为 target

  4. 对于大多数操作 js 规范中有一个所谓的内部方法,它描述最底层的工作方式。例如 [[Get]],用于读取属性的内部方法,[[Set]] 用于写入属性的内部方法。这些方法仅在规范中使用,不能直接通过方法名调用它们。proxy 捕捉器会拦截这些方法的调用

内部方法Handler 方法何时触发
[[Get]]get读取属性
[[Set]]set写入属性
[[HasProperty]]hasin操作符
[[Delete]]deletePropertydelete操作符
[[Call]]apply函数调用
[[Construct]]constructnew操作符
[[GetPrototypeOf]]getPrototypeOfObject.getPrototypeOf
[[SetPrototypeOf]]setPrototypeOfObject.setPrototypeOf
[[IsExtensible]]isExtensibleObject.isExtensible
[[PreventExtensions]]preventExtensionsObject.preventExtensions
[[DefineOwnProperty]]definePropertyObject.defineProperty Object.defineProperties
[[GetOwnProperty]]getOwnPropertyDescriptorObject.getOwnPropertyDescriptor for...in Object.keys/values/entries
[[OwnPropertyKeys]]ownKeysObject.getOwnPropertyNames Object.getOwnPropertySymbols for..in Object.keys/values/enties

不变量

js 强制 执行某些不变量

  1. 其中大多数用于返回值
    • [[Set]] 如果值已成功写入,则返回 true 否则返回 false
    • [[Delete]] 如果已完成删除该值,则返回 true 否则返回 false
    • ...
  2. 对于一些不变量
    • 应用代理 proxy 对象的 [[GetPrototypeOf]] 必须返回与应用于被代理对象的 [[GetPrototypeOf]] 相同的值。读取代理对象的原型必须返回始终返回被代理对象的原型

捕捉器可以拦截这些操作,但是也必须遵循上面的规则

get 捕捉器

  1. handler 必须有 get(target, property, receiver) 方法
    • target 目标对象,该对象被作为第一个参数传递给 new Proxy
    • property 目标属性名
    • receiver 如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所在的 this 对象。通常就是 proxy 对象本身
  2. 代理应该在所有地方都完全代替目标对象,目标对象被代理后,不应该在引用目标对象
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
let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return NaN;
}
}
});

console.log(numbers[2]); // 2
console.log(numbers[3]); // NaN

let dict = {
"Hello": "Hola",
"Bye": "Adiós"
};

dict = new Proxy(dict, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return prop;
}
}
});

console.log(dict["Hello"]); // Hola
console.log(dict["welcome"]); // welcome

set 捕捉器

  1. handler 必须有一个 set(target, property, value, receiver) 函数
    • target 目标对象 该对象被作为第一个参数传递给 new Proxy
    • property 目标属性名称
    • value 目标属性值
    • receiverget 捕捉器类型,仅与 setter 访问器属性相关
  2. 如果写入操作成功,set 捕捉器应该返回 true,否则返回 false (触发 TypeError)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let numbers = [];
numbers = new Proxy(numbers, {
set(target, prop, value) {
if (typeof value === "number") {
target[prop] = value;
return true;
} else {
return false;
}
}
});

/*
数组的内建方法依然有效,值被使用 `push` 方法添加到数组

不必重写 `push` `unshift` 等添加元素的数组方法 在内部它们使用代理所拦截的 [[Set]] 操作。
*/
numbers.push(1);
numbers.push(2);

console.log(numbers.length, "length");

numbers.push("test"); // TypeError 返回 false;

ownkeysgetOwnPropertyDescriptor 进行迭代

  1. Object.keys for...in 循环和大多数其他遍历对象属性的方法都使用内部方法 [[OwnPropertyKeys]]ownKeys 捕捉器拦截来获取属性列表
  2. 下面方法在细节上有所不同
    • Object.getOwnPropertyNames(obj) 返回非 symbol
    • Object.getOwnPropertySymbols(obj) 返回 symbol
    • Object.keys/values() 返回带有 enumerable 标志的非 symbol 键/值
    • for...in 循环遍历所有带有 enumerable 标志的非 symbol 键,以及原型对象的键
  3. 带有 enumerable 标志的键,如果返回一个对象不存在的键,那么为了检查它,方法会对每个属性调用内部方法 [[GetOwnProperty]] 来获取它的描述符,通过拦截 getOwnPropertyDescriptor 来返回 enumerable: 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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
let user = {
name: "suxi",
age: 30,
_password: "***"
};

user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith("_"));
}
});

for (let key in user) {
console.log(key); // name age
}

console.log(Object.keys(user)); // name age
console.log(Object.values(user)); // suxi 30

let pig = {
name: "peiqi",
age: 3
};

pig = new Proxy(pig, {
ownKeys(target) {
// 返回一个对象不存在的键
return ["a", "b", "c"];
}
});

console.log(Object.keys(pig)); // empty

let person = {
name: "Mark"
};

person = new Proxy(person, {
ownKeys(target) {
return ["a", "b"];
},
getOwnPropertyDescriptor(target, prop) {
return {
enumerable: true,
configurable: true
}
}
});

console.log(Object.keys(person)); // a b

deleteProperty 和其他捕捉器的受保护属性

  1. 一个普遍的约定,以下划线 _ 开头的属性和方法是内部的,不应从对象外部访问它们
    • get 读取属性时抛出异常
    • set 写入属性时抛出异常
    • deleteProperty 删除属性时抛出异常
    • ownKeys 在使用 for...in 和像 Object.keys 这样的方法时排除以 _ 开头的属性
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
let student = {
name: "Aki",
_password: "secret",
checkPassword(value) {
return value === this._password;
}
};

console.log(student._password);

student = new proxy(student, {
get(target, prop) {
if (prop.startsWith("_")) {
throw new Error("Access denied");
}
let value = target[prop];
/*
如果读取的是一个函数,需要修正它的this,下面是一个简单方案
*/
return (typeof value === "function" ? value.bind(target) : value);
},
set(target, prop, value) {
if (prop.startsWith("_")) {
throw new Error("Access denied");
};
target[prop] = value;
return true;
},
deleteProperty(target, prop) {
if (prop.startsWith("_")) {
throw new Error("Access denied");
};
delete target[prop];
return true;
},
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith("_"));
}
});

try {
console.log(student._password);
} catch (e) {
console.error(e);
}

try {
student._password = "abs";
} catch (e) {
console.error(e);
}

try {
delete student._password;
} catch(e) {
console.error(e);
}

console.log(Object.keys(student)); // name checkPassword

类的私有属性

类的私有属性以 # 为前缀,它们无需代理,并且它们有其自身的问题,特别是,它们是不可继承的

带有 has 捕捉器的 in range

  1. 使用 in 操作符来检查一个数字是否在 range 范围内
  2. has 捕捉器会拦截 in 调用
  3. has(target, property)
    • target 目标对象
    • property 属性
1
2
3
4
5
6
7
8
9
10
11
12
13
let range = {
start: 1,
end: 10
};

range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});

console.log(5 in range); // true
console.log(33 in range); // false

包装函数 apply

  1. 也可以将代理包装在函数周围
  2. apply(target, thisArg, args) 捕捉器能使代理以函数的方式被调用
    • target 是目标对象
    • thisArgthis 的值
    • args 是参数列表
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
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}

function sayHi(name) {
console.log(`Hello, ${name}`);
}


const sayHiF = delay(sayHi, 3000);

sayHiF("suxi");

/**
* 上面这种方式,大多数情况下是可行的,但是包装函数不会转发属性读取/写入操作或者任何其他操作。进行包装后,就失去了对原始函数属性的访问,例如 `name` `length` 和其他属性
*/

console.log(sayHi.length); // 1 函数的length标识函数声明时的参数的个数
console.log(sayHiF.length); // 0 丢失了

// 但是 proxy 的功能要强大的多,它可以将所有东西都转发到目标对象

function delay1(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
})
}

function sayHi1(name) {
console.log(`Hello, ${name}`);
}

const sayHi2 = delay(sayHi1, 3000);
console.log(sayHi2.length); // 1
sayHi2("suxi");

Reflect

Reflect 是一个内建对象,可简化 Proxy 的创建,对象的内部方法 [[Get]] [[Set]] 等,都只是规范性的,不能直接调用,Reflect 对象使调用这些内部方法成为了可能。

操作Reflect调用内部方法
obj[prop]Reflect.get(obj, prop)[[Get]]
obj[prop] = valueReflect.set(obj, prop, value)[[Set]]
delete obj[prop]Reflect.deleteProperty(obj, prop)[[Delete]]
new F(value)Reflect.construct(F, value)[[Construct]]
.........
1
2
3
let user = {};
Reflect.set(user, "name", "suxi");
console.log(user); // {"name": "suxi"}

Reflect 允许将操作符 new delete ... 作为函数 (Reflect.construct Reflect.deleteProperty ...) 执行调用。

对于每个可被 Proxy 捕获的内部方法,在 Reflect 中都有对应的方法,其名称和参数与 Proxy 捕获器相同

Reflect.get 读取一个对象属性
Reflect.set 写入一个对象属性,如果写入成功则返回 true 否则返回 false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let person = {
name: "aki"
};

person = new Proxy(person, {
get(target, prop, receiver) {
console.log(`Get ${prop}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
console.log(`Set ${prop} = ${value}`);
return Reflect.set(target, prop, value, receiver);
}
});

let name = person.name;
person.name = "peiqi";
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
// 为什么Reflect更好

let pig1 = {
_name: "peiqi",
get name() {
return this._name;
}
};

let pig1Proxy = new Proxy(pig1, {
get(target, prop) {
return target[prop];
}
});

let pig2 = {
__proto__: pig1Proxy,
_name: "suxi"
};

console.log(pig2.name); // 期待是 suxi 可是是 peiqi 原因在于 pig1Proxy get使用的 target是 pig1

// 使用 Reflect

let pig2Proxy = new Proxy(pig1, {
get(target, prop, recevier) {
return Reflect.get(target, prop, recevier);
}
});

let pig3 = {
__proto__: pig2Proxy,
_name: "suxi"
};

console.log(pig3.name);

Proxy 的局限性

代理提供了一种独特的方法,可以在最底层更改或调整现有对象的行为。但是并不完美,有局限性

内建对象: 内部插槽(Internal slot)

许多内建对象 Map Set Date Promise 等都使用了内部插槽。例如 Map 对象将项目 item 存储在 [[MapData]] 中。内建方法可以直接访问它们,而不通过 [[Get]]/[[Set]] 内部方法。所以 Proxy 无法拦截它们

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
let map1 = new Map();
let proxy1 = new Proxy(map1, {});
/**
在内部,一个 Map 将所有数据存储在其 [[MapData]] 内部插槽中。代理对象没有这样的插槽。内建方法 Map.prototype.set 方法试图访问内部属性 this.[[MapData]],但由于 this=proxy,在 proxy 中无法找到它,只能失败
*/
try {
proxy1.set("test", 1);
} catch(e) {
console.error(e);
}

// 还有一种解决方法
let map2 = new Map();

let proxy2 = new Proxy(map2, {
get(target, prop, recevier) {
// 这是一种简写方式
let value = Reflect.get(...arguments);
return typeof value === "function" ? value.bind(target) : value
}
});

proxy2.set("test", 1);
console.log(proxy2.get("test")); // 1
// 这样就正常工作了

Array 没有内部插槽

内建 Array 没有使用内部插槽。由于历史原因,它出现很久以前,所以代理数组没有这种问题

私有字段

类的私有字段也会发生类似的情况

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
class User {
#name = "suxi";

getName() {
return this.#name;
}
};

let user3 = new User();

let proxyUser3 = new Proxy(user3, {});

try {
console.log(proxyUser3.getName());
} catch(e) {
console.error(e);
}

let proxyUser4 = new Proxy(user3, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value === "function" ? value.bind(target) : value;
}
});

console.log(proxyUser4.getName()); // suxi

proxy !== target

代理和原始对象是不同的对象

1
2
3
4
5
6
7
8
9
10
11
12
13
let allUsers = new Set();
class Person {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}

let person2 = new Person();
console.log(allUsers.has(person2)); // true

person2 = new Proxy(person2, {});
console.log(allUsers.has(person2)); // false 找不到了

Proxy 可以拦截许多操作符,例如 new in delete 但是没有办法拦截 === 一个对象只严格等于其自身,没有其他值

可撤销的 Proxy

一个可撤销的代理可以被禁用的代理。假设我们有一个资源,并且想随时关闭对资源的访问。可以包装成一个可撤销的代理 let {proxy, revoke} = Proxy.revocable(target, handler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let object = {
data: "value data"
}

let { proxy, revoke } = Proxy.revocable(object, {});

console.log(proxy.data);

revoke();

try {
console.log(proxy.data); // error
} catch(e) {
console.error(e)
}

revoke() 的调用会从代理中删除对目标对象的所有内部引用,因此它们之间再无链接

revokeproxy 是分开的,因此我们可以传递 proxy 同时 revoke 留在当前范围内

我们可以通过设置 proxy.revoke = revoke 来将 revoke 绑定 proxy

另一种选择是创建一个 WeakMap 其中 proxy 作为键,相应的 revoke 作为值,这样可以轻松的找到 proxy 对应的 revoke

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let revokes = new WeakMap();

let obj = {
data: "value"
};

let { proxy, revoke } = Proxy.revocable(obj, {});

revokes.set(proxy, revoke);

let revoke = revokes.get(proxy);

revoke();

try {
console.log(proxy.data); // Error
} catch(e) {
console.error(e);
}

/**
此处我们使用 WeakMap 而不是 Map,因为它不会阻止垃圾回收。如果一个代理对象变得“不可访问”(例如,没有变量再引用它),则 WeakMap 允许将其与它的 revoke 一起从内存中清除
*/

响应式基础

声明响应式状态

ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ref } from 'vue'
import type { Ref } from 'vue'
const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

// 为Ref 标注类型
// 1. 使用 Ref
const day: Ref<number> = ref(1)
// 2. 使用泛型
const year = ref<number>(2024)
const month = ref<number>() // 此处 ref 推导出来是一个联合类型 number | undefined

在组件中访问 ref,需要在 setup() 函数中声明并返回它们,在模版中使用它们是不需要 .value 的,它会自动解包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { ref } from "vue";

export default {
setup() {
const count = ref(0);
return {
count
};
}
};

<template>
<button @click="count++">{{ count }}</button>
</template>

<script setup> 中的顶层的导入、声明的变量和函数可在同一组件的模板中直接使用

深层响应式

ref 的值具有深层响应式,即便改变深层对象的属性,变化也会被检测到

DOM 更新时机

修改响应式状态时,dom 会自动更新,但是更新不是同步的,vue 在更新周期中缓存所有状态的修改,确保不论进行多少次状态修改,每个组件只更新一次

1
2
3
4
5
6
7
8
// 等待 dom 更新完之后再执行的操作 使用全局 API nextTick
import { nextTick, ref } from "vue";
const count = ref(0);
async function doSometing() {
count.value++;
await nextTick()
// dom更新后的 操作
}

reactive

reactive 用来创建响应式对象,使对象本身具有响应式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { reactive } from "vue";

const state = reactive({ count: 1 });

<template>
<button @click="state.count++">{{ state.count }}</button>
</template>

// reactive 标注类型
interface State {
count: number;
}

const state: State = reactive({ count: 1 });

代理对象和原对象是不相等的,修改原对象不会触发更新,对于同一对象的 reactive 返回相同的代理对象,这个规则对于深层对象也同样适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let state = { count: 1, b: { c: 2 } };
let proxyState = reactive(state);

proxyState === state // false
reactive(state) === proxyState // true

state.b === proxyState.b // false
reactive(state.b) === proxyState.b // true

const row = {};
proxyState.row = row;

proxyState.row === row // false
proxyState.row === reactive(row) // true

reactive 局限性

  1. 只能用于对象、数据 Map Set 等类型
  2. 不能轻易替换整个响应式对象,这样会导致失去响应式
  3. 将数据解构或传递属性时会失去响应式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const state = reactive({ count: 1 });
state = reactive({ count: 2 }); // 失去第一个响应式的链接

const { count } = state;

count++;

// 无法影响到 state.count

function fn(a) {
// ...
}

fn(state.count) // 操作无法影响到 state.count

ref 解包

作为 reactive 对象的属性

  1. 一个 ref 会在作为响应式对象的属性被访问或修改时自动解包
  2. 只有当嵌套在一个深层响应式对象内时,才会发生 ref 解包。当其作为浅层响应式对象的属性被访问时不会解包
  3. 如果将一个新的 ref 赋值给一个关联了已有 ref 的属性,那么它会替换掉旧的 ref
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const count = ref(0);
const state = reactive({
count
});

console.info(state.count); // 0
count.value++;
console.info(state.count); // 1

const count2 = ref(3);

state.count = count2;

count.value++;
console.info(count.value); // 2
console.info(state.count); // 3

const m1 = shallowReactive({ count });
console.info(m1.count.value); // 2

数组和集合

reactive 对象不同的是,当 ref 作为响应式数组或原生集合类型 (如 Map) 中的元素被访问时,它不会被解包

1
2
3
4
5
6
7
let b = reactive([ref(0)]);
console.info(b[0].value); // 0

let c = reactive(new Map([
["c", ref(1)]
]));
console.info(c.get("c").value); // 1

在模板中解包

  1. 在模板渲染上下文中,只有顶级的 ref 属性才会被解包
  2. 如果 ref 是文本插值的最终计算值 (即 {{ }} 标签),那么它将被解包
1
2
3
4
5
6
7
8
9
const fn1 = ref(0);
const fn2 = { a: ref(2) }

<template>
<div>{{ fn1 }}</div>
<div>{{ fn2.a }}</div>
<div>{{ fn2.a + 1 }}</div>
<div>{{ fn2.a.value + 1 }}</div>
</template>

计算属性

  1. computed 返回一个计算属性 ref,模版中直接解包。vue 计算属性自动追踪响应式依赖,并缓存计算值
  2. computed 在首次访问时才会计算,后续仅会在其响应式依赖更新时才重新计算
  3. computed 相对方法来说,方法会在每次 render 的时候重复执行,而 computed 只会在依赖更新时重新执行
1
2
3
4
5
6
7
8
9
10
11
12
13
import { computed, reactive } from "vue";

const author = reactive({
name: "suxi",
books: [
"111",
"222"
]
});

const publish = computed(() => {
return author.books.length;
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 执行顺序
import { computed, reactive } from "vue";

const state = reactive({
count: 0
});

const publish = computed(() => {
console.log("computed");
return state.count;
});

console.info("111111");
console.info(publish.value); // computed 0
console.info(publish.value); // 0
state.count++; // computed
console.info(publish.value); // 1
console.info("222222") // 2222

// 泛型指定 computed 类型

const publish2 = computed<number>(() => {
return state.count;
});

可写的计算属性

  1. 计算属性默认是只读的
  2. 计算属性不应该有副作用,副作用应由监听器根据响应式状态的变更来创建副作用
    • 不应改变其他状态
    • 不应做异步请求
    • 不应更改dom
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, computed } from "vue";

const firstName = ref("Pei");
const lastName = ref("Qi");

const fullName = computed({
get() {
return firstName + "-" + lastName;
},
set(value) {
let [first, last] = value.split("-");
firstName.value = first;
lastName.value = last;
}
})

类和样式绑定

绑定对象、数组、字符串

  1. class 绑定对象,如果对象 key 对应的 value 为真值,则添加值为 key 的类
  2. style 绑定对象,则对象的 key 需要是 cssProperty 形式的字符串,否则会被忽略
  3. class 绑定数组,则是数组或者对象数组则是对应值或对应 key 作为类名添加
  4. style 绑定一个包含多个样式对象的数组,这些对象会合并后渲染到元素上
1
2
3
4
5
6
7
8
const errorClass = ref("validate-error");
const activeClass = ref("active");

<template>
<div :class="{ active: true }"></div>
<div :class="[{ active: true }, errorClass]"></div>
<div :style="{ fontSize: '12px' }"></div>
</template>

组件中的 class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Component组件
<div class="fn1 fn2">ffff</div>

<Component class="fn3 fn4"></Component>

// 结果
<div class="fn1 fn2 fn3 fn4">ffff</div>

// 如果组件中有多个根元素,则需要指定哪个根元素接受这个 class

// Component组件
<div :class="$attrs.class">fff</div>
<span>11111</span>

<Component class="fn3 fn4"></Component>

<div class="fn3 fn4">fff</div>
<span>11111</span>

绑定内联样式

1
2
3
4
5
6
<template>
<div :style="{ fontSize: '12px' }">1</div>
<div :style="{ 'font-size': '22px' }">2</div>

<div :style="[ {'font-size': '12px' }, { color: 'red' } ]"></div>
</template>

自动前缀,当 style 中使用了需要浏览器特殊前缀的 css 属性时,vue 会自动添加

样式多值,可以对一个属性提供多个(不同前缀的)值,数组会渲染浏览器支持的最后一个值

1
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

条件渲染

v-if v-else

  1. 有条件的渲染一块内容
  2. 如果有多个块同时需要限制,可以将 v-if 使用在 template
  3. v-else 只能配合 v-if v-else-if 使用,不能单独出现

v-show

  1. 有条件的显示一块内容
  2. 只是切换了 cssdisplay 属性
  3. v-show 不可使用在 template 上,也不可以和 v-else 配合使用

区别

  1. v-if 是按条件渲染,如果是 false 不会做任何事,切换时,条件区块中的监听器和子组件都会销毁和重建
  2. v-show 是一开始就渲染,切换时,只是切换了 cssdisplay 属性
  3. v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销,频繁切换使用 v-show 较好,反之使用 v-if 较好

注意

  1. v-if 不推荐和 v-for 同时使用
  2. 如果同时存在,v-if 优先级更高

列表渲染

v-for

  1. v-for="item in items" v-for="(item, index) in items" 迭代渲染
  2. 也可以使用 of 替代 in
  3. v-for 遍历对象时,第一个参数时是 value 第二个参数是 key 第三个参数是 index v-for="(value, key, index) in obj" 遍历的顺序是基于 Object.values(obj) 的顺序决定的
  4. v-for 中使用范围值是从 1-nv-for="item in 10" 1 - 10 初值是 1 而非 0
  5. 当有多个块需要循环渲染时,可以在 template 上使用 v-for
  6. v-ifv-for 同时使用是不推荐的,如果同时使用 v-if 的优先级会更高,这意味着 v-if 中无法使用 v-for 中的变量 v-for="item in items" v-if="item.show"
  7. 使用 v-for 时推荐使用 key 属性,template 上使用 v-for 时,key 应该被放置在这个 template 容器上,key 绑定的应该是基础类型的值 numberstring

事件处理

  1. 内联事件处理器是在模版中直接使用表达式
  2. 方法事件处理器 v-on 一个方法
  3. v-on="fo()" 是一个内联事件处理器
  4. 在内联处理器中调用方法可以向方法传入自定义参数来代替原生事件对象
  5. 在内联事件中访问原生的 dom 事件,可以向处理器传入 $event
  6. 事件修饰符
    • .stop 阻止事件冒泡 event.stopPropagation()
    • .prevent 阻止默认行为 event.preventDefault()
    • .self 仅当 event.target 是元素本身时才会触发事件处理器
    • .capture 指向内部元素的事件,在被内部元素处理前,先被外部处理
    • .once 事件处理器只触发一次
    • .passive 首先触发默认行为,然后再触发事件处理器 所以和 .prevent 不能同时使用
  7. 按键修饰符
    • @keyup.enter="enterEvent"
    • .enter 回车事件
    • .tab tab 键事件
    • .delete 捕获 delete 和 backspace 按键
    • .esc
    • .space
    • .up
    • .down
    • .left
    • .right
  8. 系统按键修饰符
    • .ctrl
    • .alt
    • .shift
    • .meta mac 中是 command window 中是 win
  9. .exact 修饰符
    • 允许精确控制触发事件所需要的系统修饰符组合
  10. 鼠标按钮修饰符 限定为特定鼠标按键触发的事件
    • .left
    • .right
    • .middle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const count = ref(0);

<button @click="count++">Add</button>
<p>Count -> {{ count }}</p>

const add = (event) => {
count.value++;
console.info(event.target.tagName);
}
<button @click="add">Add</button>
<p>Count -> {{ count }}</p>


<button @click="count++">Add +</button>
<p>Count -> {{ count }}</p>
<button @click="add">Add -</button>
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
<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>


<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>


<!-- 仅在 `key` 为 `Enter` 时调用 `submit` -->
<input @keyup.enter="submit" />


<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />

<!-- Alt + 点击 -->
<div @click.alt="doSomething">Do something</div>

<!-- 当按下 Alt 时,即使同时按下 Ctrl 或 Shift 也会触发 -->
<button @click.alt="onClick">A</button>

<!-- 仅当按下 Alt 且未按任何其他键时才会触发 -->
<button @click.alt.exact="onCtrlClick">A</button>

<!-- 仅当没有按下任何系统按键时触发 -->
<button @click.exact="onClick">A</button>

表单输入绑定

1
2
3
4
5
6
7
8
9
<input
:value="text"
@input="text = $event.target.value"
/>

// => 简写
<input
v-model="text"
/>
  1. v-model 用于不同类型的输入,textarea select 等元素
    • text textarea 元素会绑定 value 并监听 input 事件
    • checkbox radio 元素会绑定 checked 并监听 change 事件
    • select 会绑定 value 并监听 change 事件
  2. v-model 会忽略表单元素上初始化的 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
<p>Message is: {{ message }}</p>
<input v-model="message" placeholder="edit me" />

<span>Multiline message is:</span>
<p style="white-space: pre-line;">{{ message }}</p>
<textarea v-model="message" placeholder="add multiple lines"></textarea>

<input type="checkbox" id="checkbox" v-model="checked" />
<label for="checkbox">{{ checked }}</label>

// 绑定值 (value) 为数组
const checkedNames = ref([])

<div>Checked names: {{ checkedNames }}</div>

<input type="checkbox" id="jack" value="Jack" v-model="checkedNames" />
<label for="jack">Jack</label>

<input type="checkbox" id="john" value="John" v-model="checkedNames" />
<label for="john">John</label>

<input type="checkbox" id="mike" value="Mike" v-model="checkedNames" />
<label for="mike">Mike</label>


<div>Picked: {{ picked }}</div>

<input type="radio" id="one" value="One" v-model="picked" />
<label for="one">One</label>

<input type="radio" id="two" value="Two" v-model="picked" />
<label for="two">Two</label>


// 如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件,所以建议提供一个空值的禁用选项
<div>Selected: {{ selected }}</div>

<select v-model="selected">
<option disabled value="">Please select one</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>


<div>Selected: {{ selected }}</div>

<select v-model="selected" multiple>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
1
2
3
4
5
6
7
8
9
10
<!-- `picked` 在被选择时是字符串 "a" -->
<input type="radio" v-model="picked" value="a" />

<!-- `toggle` 只会为 true 或 false -->
<input type="checkbox" v-model="toggle" />

<!-- `selected` 在第一项被选中时为字符串 "abc" -->
<select v-model="selected">
<option value="abc">ABC</option>
</select>

修饰符

  1. .lazy
    • 默认情况下,v-modelinput 事件触发后更新数据,.lazy 可以修改为 change 事件触发后更新数据
  2. .number
    • 自动转换为数字
    • 如果值无法被 parseFloat 处理会返回原始值
    • number 修饰符会在输入框有 type="number" 时自动启用
  3. .trim
    • 自动去除首尾空格

生命周期

注册生命周期钩子