0%

试用vue3

vue 3.0 在 2020.4.17 发布了 beta 版本,可以说离正式发布已经很近了,是时候学习一波新姿势了。

composition api

目前 vue 3.0 的文档还未更新,可以先看这个了解 vue3.0 的新语法 vue composition api rfc
以下是我的学习笔记。

核心概念 响应性

响应性 vue 3.0 最重要的概念,vue3.0 使用 proxy 赋予对象响应性,一个对象具有响应性,那么当这个对象发生改变的时候,就可以自动触发视图的更新或者 watchEffect 中的副作用。

proxy

概念

Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。所以可以实现 Object.defineProperty 做不到的对于对象新增属性,数组下标的响应式操作。

构造

1
var proxy = new Proxy(target, handler);

要使得 proxy 起作用,必须针对 Proxy 实例(上例中是 proxy)进行操作,而不是对目标对象 target 进行操作,如果 handler 没有设置任何拦截,那么等同于直接通向原对象

proxy 实例也可以作为其他对象的原型对象

1
2
3
4
5
6
7
8
9
10
11
var proxy = new Proxy(
{},
{
get: function (target, propKey) {
return 35;
},
}
);

let obj = Object.create(proxy);
obj.time; // 35

proxy 对象是 obj 对象的原型,obj 对象本身没有 time 属性,所以根据原型链,会在 proxy 对象上读取该属性,导致被拦截

支持的操作

几乎可以支持全部对于对象的访问操作,甚至包括 函数的调用,constructor 的调用,具体看文档就好。

setup

回到 Vue3 这里

setup 相当于原来的 options,可以接受两个参数

第一个参数 props,props 是响应性的,可以触发 watchEffect,注意 props 不能被解构,否则会失去响应性。

第二个参数 context 是一个不可变对象。它有以下属性,其实也都对应了 2.0 中的 api。

  • context.attrs 就是组件上的 attrs,等同于 2.0 的 $attrs
  • context.slots 可访问通过组件插槽分发的的内容,等同于 2.0 的 $slots
  • context.emit 就是 2.0 的 $emit 方法

setup 中的 this

setup 中不支持使用 this

reactive

赋予一个对象响应性,相当于 2.0 中的 Vue.observable()

ref

接受一个非引用类型值,返回一个 reactive 的可变 ref 对象,对象有一个唯一的 value 属性

当其作为一个 reactive 对象的属性的时候,会自动 unwrap 内部的值,使它的行为与普通属性一致

toRefs

接受一个 reactive 对象,返回一个对象,对象中每个属性都是 ref 对象
这在函数返回一个对象的时候很有用,返回值可以被解构而不失响应特性

computed

接受一个 getter 函数,返回一个不可变的响应 ref 对象

readonly

接受一个对象(reactive 或者 朴素的)或者一个 ref,返回一个 readonly 的 proxy

watchEffect

立即运行一个函数,同时动态地跟踪它的依赖项,并在依赖项发生变化时重新运行它。

当 watchEffect 在 setup 中或者生命周期钩子中被调用,watcher 会被链接到组件的生命周期中,会在组件卸载的时候自动停止

watchEffect 也会返回一个 stop 函数,可以手动调用

side effect

有时在 watchEffect 中需要执行一些异步的方法,当再次触发 watchEffect 但是异步操作还未完成,就需要清理一些操作。

watchEffect 可以接受一个 onInvalidate 参数,大致形式如下

1
2
3
4
5
6
7
8
watchEffect((onInvalidate) => {
const token = performAsyncOperation(id.value);
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel();
});
});

不像是 react 中 useEffect 返回一个函数的原因是,异步函数的返回值是很重要的。
vue 会缓冲无效的副作用,当在一个 tick 中多次发生副作用的时候,避免不必要的重复调用。一个组件的 update 钩子也是一个副作用。watchEffect 会在 update 钩子触发前触发。触发的顺序是 beforeUpdate=》watchEffect=》onUpdate

生命周期

Vue 3.0 提供了在 setup 中使用的生命周期的钩子函数,与 2.0 的对应关系如下

beforeCreate -> use setup()
created -> use setup()
beforeMount -> onBeforeMount
mounted -> onMounted
beforeUpdate -> onBeforeUpdate
updated -> onUpdated
beforeDestroy -> onBeforeUnmount
destroyed -> onUnmounted
errorCaptured -> onErrorCaptured

新提供了一下供调试使用的钩子
onRenderTracked
onRenderTriggered

1
2
3
4
5
6
export default {
onRenderTriggered(e) {
debugger;
// inspect which dependency is causing the component to re-render
},
};

inject provide

在 vue 3.0 中的使用方式如下,provide 可以注入普通数据,也可以注入响应性数据。官方的例子,provide 的键值用的是 Symbol,当然也可以用字符串。

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
// Parent.vue
<template>
<div>
<div>parent</div>
<slot></slot>
</div>
</template>

<script>
import { provide, ref } from "vue";
export default {
setup() {
const themeRef = ref("dark");
provide("ThemeSymbol", themeRef);
},
};
</script>

// Son.vue
<template>
<div>
<div>son</div>
<div>{{ theme }}</div>
</div>
</template>

<script>
import { watchEffect, inject, ref } from "vue";
export default {
setup() {
const theme = inject("ThemeSymbol", ref("light"));
watchEffect(() => {
console.log(`theme set to: ${theme.value}`);
});
return {
theme,
};
},
};
</script>

template Refs

在 vue2.0 中,通过 ref 可以获取到 虚拟 dom 的引用。3.0 中也是如此,但是有一些变化。
下面的代码中,我们在 setep 创建了一个名叫 root 的 ref,同时在 template 的 div 的 ref 属性值为 root,那么在初次渲染后,root 的值将会指向虚拟 dom 引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div ref="root"></div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const root = ref(null)

onMounted(() => {
// the DOM element will be assigned to the ref after initial render
console.log(root.value) // <div/>
})

return {
root
}
}
}
</script>

Reactivity Utilities

unref

如果参数是一个 ref,返回它的内部值,否则返回参数自身,实际上是 val = isRef(val) ? val.value : val 的语法糖

toRef

被用来从 reactive 对象中的属性中 创建一个 ref

1
2
3
4
5
6
const state = reactive({
foo: 1,
bar: 2,
});

const fooRef = toRef(state, "foo");

toRefs 这个之前已经写了

将一个 reactive 对象转换为 普通对象,对象的每一个属性都是一个 ref 对象

isRef && isProxy && isReactive && isReadonly

都是一些顾名思义的方法了,不赘述了

Advanced Reactivity APIs

customRef

平时应该很少用到,知道有这个方法就好了。允许你定义自己的 ref 方法,可以加入额外的操作

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
function useDebouncedRef(value, delay = 200) {
let timeout;
return customRef((track, trigger) => {
return {
get() {
track();
return value;
},
set(newValue) {
clearTimeout(timeout);
timeout = setTimeout(() => {
value = newValue;
trigger();
}, delay);
},
};
});
}

export default {
setup() {
return {
text: useDebouncedRef("hello"),
};
},
};

markRaw

标记一个对象,这个对象不会被添加响应性

1
2
3
4
5
6
const foo = markRaw({});
console.log(isReactive(reactive(foo))); // false

// also works when nested inside other reactive objects
const bar = reactive({ foo });
console.log(isReactive(bar.foo)); // false

shallowReactive

创建一个只追踪它自己的属性的响应性的对象(不会深度追踪嵌套的对象)

1
2
3
4
5
6
7
8
9
10
11
12
const state = shallowReactive({
foo: 1,
nested: {
bar: 2,
},
});

// mutating state's own properties is reactive
state.foo++;
// ...but does not convert nested objects
isReactive(state.nested); // false
state.nested.bar++; // non-reactive

shallowReadonly

创建一个只使得它自己的属性是只读的对象(不会深度使嵌套的对象只读)

1
2
3
4
5
6
7
8
9
10
11
12
const state = shallowReadonly({
foo: 1,
nested: {
bar: 2,
},
});

// mutating state's own properties will fail
state.foo++;
// ...but works on nested objects
isReadonly(state.nested); // false
state.nested.bar++; // works

shallowRef

创建一个 ref,只使得它自身的 value 属性是响应性的

1
2
3
4
5
const foo = shallowRef({});
// mutating the ref's value is reactive
foo.value = {};
// but the value will not be converted.
isReactive(foo.value); // false

toRaw

可以获得 reactive 或者 readonly 的原始对象,允许你对原始对象进行操作。这是一个你没有其他方法时的逃生舱,可以被用来临时读取或者写入而不会触发响应追踪。这个方式是不推荐使用的,当你使用这个方法的时候,会给与警告。

vuex

vue3.0 搭配的是 vuex 的 4.0 版本,大部分 api 的用法都没有改变。

创建 store 的方法现在使用 creatStore。
vue3.0 的写法套路如下,首先需要用 useStore() 方法(和 react hooks 一模一样啊)获取 store 对象,如果返回 store 中的属性,用 computed 处理,如果返回 action,需要用 store 手动 dispatch。

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
<template>
<div>
Clicked: times, count is {{ counter.count }}.
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="incrementSync">-</button>
</div>
</template>

<script>
import { computed, watchEffect, onUpdated, onBeforeUpdate } from "vue";
import { useStore } from "vuex";

export default {
setup() {
const store = useStore();
return {
counter:computed(() => store.state.counter)
increment: () => store.dispatch("counter/increment"),
decrement: () => store.dispatch("counter/decrement"),
incrementSync: () => store.dispatch("counter/incrementSync"),
};
},
};
</script>

mapState,mapGetters,mapActions 这些辅助方法在 Vue3.0 写法下是无法使用的。因为 setup 中无法使用 this ,而这些辅助方法在 2.0 中是需要访问 this.$store 的。当然我们完全可以用 2.0 的写法用这些辅助方法。

vue-router

主要的破坏性更新是

  • 使用历史模式 从 mode:’history’ 改为 history: createWebHistory()
  • 捕获所有路由的写法现在应该是 /:catchAll(.*)
  • router.match 和 router.resolve 方法被合并到 router.resolve
  • router.getMathedComponents 这个方法被移除了
    可以通过以下代码实现这个方法
1
2
3
router.currentRoute.value.matched
.map((record) => Object.values(record.components))
.flat();

目前合并了四个 rfc

动态路由 实现了以下 API

  • router.addRoute(route: RouteRecord) Add a new route
  • router.removeRoute(name: string | symbol) Remove an existing route
  • router.hasRoute(name: string | symbol): boolean Check if a route exists
  • router.getRoutes(): RouteRecord[] Get the current list of routes

移除了 router-link 的 tag 和 event prop,不再自动给内部 a 元素添加点击事件,增加了 scoped-slot api