Vue3.x
快速上手
声明式渲染 Declarative Rendering => 通过模板语法,以双花括号为占位符将数据插入到节点。
响应性 Responsiveness => 通过 Vue 自动跟踪 JS 的状态变化,在其发生改变时响应式地更新 DOM。
API 风格 API Styles => 选项式 API Options API、组合式 API Composition API。
选项式 API 与组合式 API
Options API 包含数据、方法、生命周期等选项,且选项中定义的属性会暴露在函数内部的 this 上。
封装复用组件不能完全解决因业务巨大所导致的逻辑关注点的冗长,这就会导致开发中必须不断地跳转相关代码的选项块。Composition API 将同类逻辑关注点的代码汇聚于 setup 组件选项。
Composition API 常配合 setup 函数来描述组件逻辑。因 setup 在 beforeCreate 钩子之前执行,此时的组件实例还未创建,所以在 setup 函数中不能使用 this,否则会出现 undefined。此外模板中需要使用的数据和函数,应在 setup 内进行返回。
这里对 setup 中无法使用 this 再做源码说明:
- 调用 createComponentInstance 创建组件实例;
- 调用 setupComponent 初始化 component 内部的操作;
- 调用 setupStatefulComponent 初始化有状态的组件;
- 在 setupStatefulComponent 取出 setup 函数;
- 通过 callWithErrorHandling 的函数执行 setup;
上述代码可看出组件的实例 instance 肯定在执行 setup 函数之前就创建出来。
/* core/packages/runtime-core/src/errorHandling.ts */
export function callWithErrorHandling(
fn: Function,
instance: ComponentInternalInstance | null,
type: ErrorTypes,
args?: unknown[] // props, context
) {
let res
try {
res = args ? fn(...args) : fn() // 未绑定 this
} catch (err) {
handleError(err, instance, type)
}
return res
}
在单文件组件中,组合式 API 也会与 <script setup> 搭配使用。<script setup> 是一种编译时语法糖,其中的导入和顶层变量/函数都能够在模板中直接使用。setup attribute 作为 hint,告诉 Vue 在编译时进行转换,推荐在 SFCs and Composition API 场景下使用。
SFCs => Vue Single-File Components(a.k.a. *.vue files, abbreviated as SFC) => 单文件组件能获得完整的语法高亮、CommonJS 模块以及组件作用域的 CSS。
SPA => single-page application 网络应用程序或网站的模型,通过动态重写当前页面来与用户交互,而非传统的从服务器重新加载整个新页面。
非单文件组件不能保证全局定义的组件名唯一,在字符串模板中缺乏语法高亮,不支持 CSS。其构建步骤中只能使用 Html 和 ES5 JavaScript,而不能使用 webpack 和预处理器 Babel。
<template>
<div>Vue2 => 单文件组件</div>
</template>
<script>
export default { // 默认暴露
name:'kebab-case|PascalCaseComponentName', // 不写则默认暴露单文件组件名 => Xxx.vue 的 Xxx
data () { return { msg: '单文件组件' } }
}
</script>
<style></style>
Vue.component('kebab-case|PascalCaseComponentName', {
data: function () {
return { ... }
},
template: '<...>非单文件组件<... />'
})
<template>
<h1>{{ msg }}</h1>
<button @click="sayHi()">Hi Vue3</button>
</template>
<script>
export default {
name: 'HelloWorld',
setup(){
const msg = "Hello Vue3";
const sayHi = () => {
console.log("Hi Vue3")
};
return { msg, sayHi }
}
}
</script>
<template>
<button @click="increment">Count is: {{ count }}</button>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const count = ref(0) // 响应式状态
function increment() { count.value++ } // 用来修改状态、触发更新的函数
onMounted(() => { console.log(`The initial count is ${count.value}.`) }) // 生命周期钩子
</script>
setup 作为组合式 API 的入口,是一个组件选项,在组件被创建之前且 props 被解析之后执行。setup 写法中可以通过 ...toRefs 方式将响应式对象中的每个属性转变为响应式数据,简化模板中的命名对象的指定。且 setup 返回的所有内容都会暴露给组件的其余部分 (计算属性、方法、生命周期等) 以及组件的模板。
import { reactive, toRefs } from "vue";
export default {
name: "Test01",
setup(){
const data = reactive({
name:'okk', age:20, func(){ console.log('Hello Vue3') }
})
return{ ...toRefs(data) } // => {{ name }}、{{ age }}、@<event>="func"
}
}
setup 函数具有两个参数 props 和 context,props 是由上级组件所传递的属性组成的对象,context 对象包含 attrs,slots 和 emit 属性。
使用 <script setup> 时,声明的顶层绑定(声明变量、函数以及引入内容)都能在模板中直接使用,不需要返回。但若需将响应式对象中的每个属性都转换为响应式数据,那么需要借助 toRefs() 的解构。
当 <script setup> 与 <script> 标签同时存在时,后者 setup() 中定义的任何变量和方法都不能在模板进行访问。
<script setup>
import { reactive,toRefs } from "vue";
const data = reactive({ name:'okk', age:20, func(){ console.log('Hello Vue3') } })
const { name,age,func } = toRefs(data)
</script>
响应式数据
直接对变量进行字面量赋值的操作不会产生响应式的效果,所以在数据更新时不会驱动视图的更新。若要定义响应式数据,需要借助从 vue 导入的相关 function。定义响应式数据 => reactive()、ref();辅助函数 => toRef()、toRefs()。
ref 产生的响应式数据在修改和读取时应指定其 value;ref 产生的响应式数据在模板中使用时,可以省略 .value
。
- reactive => 传入对象或数组
除 null 和 undefined 的原始类型都有其相应的包装对象 => BigInt、Symbol、String、Number、Boolean
通过传入普通对象(非包装对象)作为参数创建响应式对象,若参数是字符串或数字则会报出警告,类似 React Hook 中的 useState()
和 useReducer()
。当直接从响应式数据对象中解构属性时,会造成响应式的丢失。
<template>
<div>{{ countobj.count }}-<button @click="add">clickme add</button></div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup() {
const countobj = reactive({ count: 0, });
const add = () => { countobj.count++; };
return { countobj, add, };
},
};
</script>
reactive 源码位于 .../node_modules/@vue/reactivity/dist/reactivity.d.ts,其接受类型是泛型 T 的参数 target。T extends object => target 的类型是 object 类型或继承自 object 类的子类类型。返回值类型为 UnwrapNestedRefs<T>
。
Creates a reactive copy of the original object.
The reactive conversion is "deep"—it affects all nested properties. In the ES2015 Proxy based implementation, the returned proxy is not equal to the original object. It is recommended to work exclusively with the reactive proxy and avoid relying on the original object.
A reactive object also automatically unwraps refs contained in it, so you don't need to use.value
when accessing and mutating their value:
type
关键字声明的类型 UnwrapNestedRefs<T>
通过判断 T 是否属于 Ref 或其子类,指定传入的 T 或者 UnwrapRefSimple<T>
。
// reactive 的类型声明 - Creates a reactive copy of the original object.
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;
...
// UnwrapNestedRefs<T> 类型
export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>;
reactive 方法的定义于 .../@vue/reactivity/dist/reactivity.global.js。此处是编译后的 JS 版本,需要查看 TS 版本的点此。
reactive 接受 object 类型的参数 target。若传入对象只读则返回本身,as 断言关键字表示传入的值一定为 Target 类型,ReactiveFlags.IS_READONLY
根据枚举类判断是否为只读的属性;当传递的对象是普通对象,则会执行创建响应式对象函数 createReactiveObject(...)
。
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target
}
return createReactiveObject(
target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap
)
}
...
export function isReadonly(value: unknown): boolean {
return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])
}
reactive.ts 中返回 createReactiveObject() 的函数 | 描述 |
---|---|
reactive | 创建深层响应的可读写代理对象 |
readonly | 创建深层响应的只读代理对象 |
shallowReactive | 创建浅层响应的可读写代理对象 |
shallowReadonly | 创建浅层响应的只读代理对象 |
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// only specific value types can be observed.
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
createReactiveObject(...)
大体情况下是返回通过 new Proxy(..)
构造函数构建出来的 proxy。
在使用 reactive 传递参数时,可以是对象也可以是原始值,但是后者并不会被包装成响应式数据;返回的响应式数据本质为 Proxy 对象。
返回的响应式副本与原始数据有关联,当原始对象里的数据或响应式对象里的数据发生变化时,彼此都会被相互影响,但是前者数据改变不会触发界面更新。
- shallowReactive => 只考虑对象类型最外层的响应式
相比 reactive 遍历所有层次的数据生成响应式,shallowReactive 只处理第一层。
开发中适合纵向深,但仅会更改最外层属性的数据对象。
- ref => 传入原始数据以创建含有响应式属性 value 的包装式对象
响应式对象中的 __v_isRef
属性用于区分当前对象是 ref 或是普通对象,前者在模板解析期间会直接取出 value 属性(模板中省略 fieldName.value)。
所谓响应式丢失,就是对通过 reactive 生成的响应式对象数据使用展开运算符,将代理的响应式对象数据转换为普通对象数据。此时修改对象的属性值,不会触发更新和模板渲染。ref => 不但可用于实现原始值的响应式代理,还可以用于解决响应式的丢失问题。
<template>
<div>{{ count }}-<button @click="add">clickme add</button></div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
const count = ref(0), add = () => { count.value++; };
console.log(count) // RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}
return { count, add };
},
};
</script>
传递的原始数据可以是原始值也可以是引用值,若传递原始值,则指向原始数据的值保存在返回的响应式数据对象的 .value
中;若传递引用值,则返回的响应式数据对象的 .value
属性中具有指向对应的原始数据(引用值|对象)的 Proxy 副本。
<template>
<div>
<div>count01 => {{ count01 }}</div>
<button @click="add01">Click Me</button>
<div>count02 => {{ count02 }}</div>
<button @click="add02">Click Me</button>
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
let origin01 = 0, origin02 = { val: 0 }; // 原始数据分别为原始值、引用值
let count01 = ref(origin01), count02 = ref(origin02);
console.log("count01", count01); // count01 RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: 0, _value: 0}
console.log("count02", count02); // count02 RefImpl {__v_isShallow: false, dep: undefined, __v_isRef: true, _rawValue: {…}, _value: Proxy}
function add01() { count01.value++; }
function add02() { count02.value.val++; }
return { count01, count02, add01, add02, };
}
};
</script>
ref 方法的定义于 ../node_modules/vue/dist/vue.global.js。此处是编译后的 JS 版本,需要查看 TS 版本的点此。
export function ref(value?: unknown) {
return createRef(value, false)
}
...
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
...
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
通过 ref 生成的响应式对象都是由 RefImpl 类构造的实例,而 RefImpl 实例对象中的 value
属性会据传入原始数据或引用值,分别对应原始数据或 Proxy 对象。
带泛型 T 的 RefImpl 类中定义辅助操作的私有属性,通过 getter|setter 对 value
进行操作拦截。createRef 中传入的第二个参数默认是 false,那么 _rawValue
和 _value
应根据 toRaw 和 toReactive 方法确定。
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(value: T, public readonly __v_isShallow: boolean) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
...
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
if (ref.dep) {
if (__DEV__) {
triggerEffects(ref.dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(ref.dep)
}
}
}
在 Vue2 中使用的 this.$refs.xxx 获取元素和组件在 Vue3 中已被移除。现通过定义 ref 对象,并将其绑定到元素或组件的 ref 属性上即可获得元素或组件。
<template>
<span ref="msgRef">Get the elements and components through the ref</span>
</template>
<script>
import { ref, onMounted } from "vue";
export default {
setup(){
const msgRef = ref();
onMounted(() => { console.log(msgRef.value); });
return { msgRef };
};
}
</script>
- shallowRef => 传基本数据类型时与 ref 无异,不处理对象类型的响应式
ref 传入对象类型的参数时,会创建 value 属性为 Proxy 实例的 RefImpl 对象。
shallowRef 传入对象类型的参数时,会创建 value 属性为 Object 实例的 RefImpl 对象,Object 实例是没有进行响应式处理的。
shallowRef 适合用于后续功能不会修改对象中属性的对象数据,常会通过生成新对象的方式将其替换。
-
triggerRef => 手动触发和 shallowRef 相关联的副作用
-
toRef => 将响应式对象中的某个字段单独提供给外部使用
为避免在模板中使用复杂表示的对象属性,可能会考虑到在返回时以变量接受响应式对象的具体属性,然而这并不能符合预期的获得响应式数据,而只是一个快照。
let slogan = { msg: "Hello", status: 200 }
let s = new Proxy(slogan, {
set(target, propName, value){
Reflect.set(target, propName, value)
}
})
let snapshot = s.msg // 此处无法通过更改 snapshot 达到修改源对象的目的 -> snapshot = Hello
toRef 为响应式对象上的指定字段新建一个 ref 对象,其 value 属性保持与传入对象的源字段同步。
<template>
<div>
<div>{{name}} => {{age}}</div>
<button @click="btnFunc">Click Me</button>
</div>
</template>
<script>
import {toRef, reactive} from "vue"; // 引入 toRef
export default {
setup(){
let user = reactive({ name:'Ok', age:23 }), name = toRef(user,'name'), age = toRef(user,'age'), btnFunc = () => { name.value = "Okk", age.value = 24,console.log(name) }
return {name,age,btnFunc}
}
}
</script>
ObjectRefImpl{__v_isRef: true, _defaultValue: undefined, _key: "name", _object: Proxy {name: 'Okk', age: 24}, value: "Okk"}
返回由 toRef 包裹的数据时看似可用 ref 代替,但后续操作的实际数据不再为此前定义的数据,而是由 ref 产生的新数据,所以不符合预期。
toRef 是引用源数据(利用 Getter 将 value 指向源数据),ref 是复制源数据。
<template>
<div>{{fooRef}}</div>
</template>
<script>
import { reactive, toRef } from "vue";
export default {
setup() {
const state = reactive({foo: 1, bar: 2 }); // 响应式对象 state
const fooRef = toRef(state, "foo");
fooRef.value++;
console.log(state.foo) // 2
state.foo++;
console.log(fooRef.value); // 3
return {state, fooRef}
},
};
</script>
- toRefs => 剥离响应式对象,将响应式对象中的每个字段作为响应式数据
将响应式对象中的每个属性都转换为单独的响应式数据,响应式对象转换为普通对象。该函数可以让消费组件在不丢失响应式的情况下对返回的对象进行解构与展开操作,解决了直接对响应式对象解构展开所导致的响应性丢失问题。
<template>
<div>
<div>{{name}} => {{age}}</div>
<button @click="btnFunc">Click Me</button>
</div>
</template>
<script>
import {toRefs, reactive} from "vue"; // 引入 toRef
export default {
setup(){
let user = reactive({ name:'Ok', age:23 }), ordinaryUser = toRefs(user), {name,age} = {...ordinaryUser},btnFunc = () => { name.value = "Okk", age.value = 24 }
console.log("{...user} equals ordinaryUser??? =>", {...user} == ordinaryUser) // false
console.log(user, "---", ordinaryUser); // Proxy {name: 'Ok', age: 23} '---' {name: ObjectRefImpl, age: ObjectRefImpl}
return {name,age,btnFunc}
}
}
</script>
- readonly 和 shallowReadonly
页面不更新的情况可能是数据已经改变,但并没有响应到页面,或者是被禁止修改数据。readonly 和 shallowReadonly 返回原始对象的只读代理。
前者返回的对象不论嵌套层级多深都无法更改,后者返回的对象只禁止最外层的数据修改。经两者处理的源对象允许被修改,且修改后会影响只读处理的返回值。
只读处理适用于数据并非在该组件定义,且不可修改的情况。本质上就是对所返回的只读代理对象的 setter 方法进行劫持。
- toRaw 和 markRaw
toRaw 返回由 reactive()、readonly()、shallowReactive() 或 shallowReadonly() 创建的代理对应的原始对象。
toRaw 将一个由 reactive 生成的响应式对象转换为普通对象。
用于读取响应式对象所对应的普通对象,开发中常见于深层嵌套且没有变更需求的数据。对这个普通对象的所有操作,不会引起页面的更新。
export function toRaw<T>(observed: T): T {
const raw = observed && (observed as Target)[ReactiveFlags.RAW]
return raw ? toRaw(raw) : observed
}
markRaw 所标记的对象永远不会成为响应式对象。
适用于渲染具有不可变数据源的大列表时,跳过响应式以提高性能;在响应式对象上追加第三方类库的场景。
为响应式对象追加的属性也会是响应式的,即会引起页面的变化。当追加无需修改的数据时,可先将其进行 markRaw 标记,再进行追加。
- customRef => 创建自定义的 ref,对其依赖项跟踪和更新触发显示控制
在 get 函数中调用 track 以追踪依赖的改变;在 set 函数中调用 trick 以重新触发模板的解析。
function xxxRef(value){
return customRef((track, trigger) => {
return {
get(){
track();
return value;
},
set(newValue){
value = newValue;
trigger();
}
}
})
}
上游组件通过 provide 函数提供数据,下游组件通过 inject 函数使用数据。
provide 不再是一个对象或返回对象的函数,而是接收注入键与注入值的函数。
inject 不再是一个字符串数组或对象,而是需要接收注入键与可选默认值的函数。
为增加 provide 与 inject 之间的响应性,可对 provide 传入 ref 与 reactive。
Vue3 中使用 inject 的 options API 注入,模板中需手动解包。
<template>
<inject-test></inject-test>
</template>
<script>
import { provide, ref } from "vue";
import InjectTest from "./InjectTest.vue";
export default {
components: {
InjectTest
},
setup(){
const msg = ref("Hello, provide & inject!");
provide("msg", msg);
return {
msg
};
}
}
</script>
<template>
<span>Info: {{msg}}-{{urgency}}</span>
</template>
<script>
import { inject } from "vue";
export default {
setup(){
const msg = inject("msg");
const urgency = inject("urgency", "routine");
return {
msg,
urgency
};
}
}
</script>
生命周期
-
原 beforeDestroy 与 destroyed 变为 beforeUnmount 与 unmounted。
-
setup 函数中要将生命周期加上 on 前缀的小驼峰形式。
-
同时存在 setup 里的生命周期和选项式生命周期,前者会在后者前触发。
new Vue() => app = Vue.createApp(options); app.mount(el)
计算属性与侦听器
写在 setup 中的计算属性需要传入一个回调函数作为参数,并接收此函数的返回值为计算属性的结果。
<template>
<div class="hello">{{ nickname }}</div> /*<!--Hello-->*/
</template>
<script>
import { ref, computed } from "vue";
export default {
name: "HelloWorld",
setup() {
const myname = ref("hello");
const nickname = computed(() => myname.value.substring(0, 1).toUpperCase() + myname.value.substring(1));
return {
nickname,
};
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
</style>
watch 侦听函数在引入后可传入三个参数,侦听数据源、传入新旧参数的执行函数和配置对象。
- 监视 ref 定义的响应式数据和监视多组 ref 定义的响应式数据;
watch(dataSource, (newValue, oldValue) => {...}, {immediate: true})
watch([dataSource01, dataSource02], (newValue, oldValue) => {...}, {immediate: true})
- 监视 reactive 定义的响应式数据的全部属性时,无法获取 oldValue,且默认开启深度监听,deep 配置也会失效;
watch(dataSource, (newValue, oldValue) => {...}, {deep: false}) // deep 配置失效
- reactive 响应式数据中的属性不能直接侦听,需使用返回该属性的 getter 函数作为数据源。当侦听属性是对象类型时,不会自动开启深层侦听,需要手动开启。
watch(() => xxx.dataSource, (newValue, oldValue) => {...})
watch([() => xxx.dataSource01, () => xxx.dataSource02], (newValue, oldValue) => {...})
侦测的是结构,不是具体的值,故基本数据类型的 RefImpl 对象不需要通过 .value 指定值作为数据源。
将对象数据类型传入 ref 函数所生成的响应式数据,需要通过 .value 指定才可作为数据源。因 RefImpl 中的 value 属性(Proxy 对象)不再是基本类型,只有在整个被替换时(内存中的地址改变),变化才可以被监视发现。此外开启深度监视也可
与需要显示指定依赖的 watch 不同,watchEffect 会立即执行传入的函数,并在执行过程中追踪依赖,当依赖变更时会重新运行该函数。适合依赖和逻辑强相关的场景。
const count = ref(0)
watchEffect(() => console.log(count.value)) // -> logs 0
setTimeout(() => { count.value++ }, 1000) // -> logs 1
与 watchEffect 相比,watch 允许惰性地执行副作用,即回调仅在侦听源发生更改时调用;更具体地说明应触发侦听器重新运行的状态;能访问被侦听状态的先前值和当前值。watchEffect 在监视回调中使用了什么属性,就默认监听什么属性。watchEffect 一定程度上和 computed 类似,但前者更注重回调函数的函数体,所以不需要写返回值,后者注重回调函数的返回值,所以一定得写。
// !!单一源
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
/* ... */
})
// !!多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
某些情况下需要停止侦听器,此时应调用 watch 或 watchEffect 返回的函数。
const stopWatchXxx = watchEffect(() => {
console.log("watch execution...", xxx.value);
})
const changeXxx = () => {
xxx.value++;
if(xxx.value > 6) { stopWatch(); }
};
自定义 Hooks
Hooks 本质是函数,把 setup 中使用的 composition API 进行封装,类似 mixin。
import { reactive, onMounted, onBeforeUnmount } from "vue";
export default function (){
let pointPosition = reactive({ x: 0, y: 0 });
function savePoint(event){
pointPosition.x = event.pageX
pointPosition.y = event.pageY
}
onMounted(() => { window.addEventListener('click', savePoint) })
onBeforeUnmount(() => { window.removeEventListener('click', savePoint) })
return pointPosition
}
// utils/useCounter.js
import { ref } from "vue";
export default function (){
let counter = ref(100);
const increment = () => { counter.value++; console.log(counter.value); };
const decrement = () => { counter.value--; console.log(counter.value); };
return { counter, increment, decrement };
}
// xxx.vue
export default {
setup(){
return { ...useCounter() }
}
}
import { ref, watch } from "vue";
export default function (val){
const title = ref(val);
watch(title, (newValue) => {
document.title = newValue;
}, {
immediate: true
})
return title
}
响应式数据判断
-
isRef => 检查一个值是否为 ref 对象
-
isReactive => 检查一个对象是否是由 reactive 创建的响应式代理
-
isReadonly => 检查一个对象是否是由 readonly 创建的响应式代理
-
isProxy => 检查一个对象是否是由 reactive 或 readonly 方法创建的代理
Vue Router 4.x
- 创建路由实例的方式改变
从 v3.x 中以 new VueRouter 的方式创建路由实例,到 v4.x 中改用 createRouter。
// 创建路由实例并传递 `routes` 配置
const router = VueRouter.createRouter({
history: VueRouter.createWebHashHistory(), // 使用 hash 模式
routes, // `routes: routes` 的缩写
})
确保 _use_ 路由实例使整个应用支持路由 => app.use(router)。
- 路由模式配置的改变
在 v3.x 中的路由模式是通过 mode 属性控制(值为字符串),现通过 import 引入不同函数来指定对应路由模式。mode 属性改为 history。
"history" => createWebHistory()
"hash" => createWebHashHistory()
"abstract" => createMemoryHistory()
- 4.x Composition API => 路由地址和路由实例需以 hooks 的形式调用取得
- 捕获路由与 404 Not found 路由 => pathMatch
对于未匹配到的路由,可通过编写动态路由匹配所有页面,并使用指定参数获取匹配内容。
{ path: "/:pathMatch(.*)", component: () => import("../pages/NotFound.vue") }
<span>Not Found: {{ this.$route.params.pathMatch }}</span>
/:pathMatch(.*)
和 /:pathMatch(.*)*
的区别在于结果是否进行解析。
Not Found: ["hello", "vue-router", "not-found"] // /:pathMatch(.*)*
Not Found: hello/vue-router/not-found // /:pathMatch(.*)
- 动态添加路由 => 开发中常根据用户的不同权限来注册不同的路由
v4.x 中废弃 router.addRoutes,仅存 router.addRoute。
/* 函数签名 */
addRoute([parentName: string,] route: RouteConfig): () => void
添加新路由规则时若有设置 name,会对此前同 name 的路由规则进行覆盖。
const categoryRoute = {
path: "/category", component: () => import("../pages/Category.vue")
}
router.addRoute(categoryRoute);
const hardwareTestRoute = {
path: "hardware-test",
component: () => import("../pages/HardwareTest.vue")
}
router.addRoute("hardware", hardwareTestRoute);
- 路由导航守卫
vue-router 提供的导航守卫主要用来通过跳转或取消的方式守卫导航
全局前置守卫 beforeEach 在导航触发时回调,接收即将进入和离开的 Route 路由对象 to & from。其返回值 false 表示取消当前导航,不返回或返回 undefined 则进行默认导航,也可以返回字符串类型的路由地址,或者包含路径参数的对象。
第三个可选的参数 next 不推荐使用。在 Vue2 中通过 next 函数决定如何跳转,但是在 Vue3 中应通过返回值控制,且应避免多次的调用 next。
Vuex
组件化开发中,data 定义或在 setup 里使用的数据可看作 State;模板 template 最终会被渲染成 DOM,称之 View;State 的修改联系模块内的行为事件 Actions。
- 组合式 API
调用 useStore 函数在 setup 里访问 store,与选项式 API 访问 this.$store 等效。
import { toRefs } from "vue";
import { useStore } from "vuex";
const store = useStore();
const { counter } = toRefs(store.state);
function increment(){ store.commit("increment")}
- 状态映射到组件保持响应式
import { computed, toRefs } from "vue";
import { useStore, mapState } from "vuex";
const store = useStore();
const { name, level } = mapState(["name", "level"]);
// 1.使用 bind 绑定
const responseName = computed(name.bind({$store: store}));
const responseLevel = computed(level.bind({$store: store}));
// 2.自定义封装的 bind 绑定 hook
const { responseName, responseLevel } = useResponseState(["name", "level"]);
import { computed } from "vue";
import { useStore, mapState } from "vuex";
export default function useResponseState(mapper){
const store = useStore();
const stateMapperRes = mapState(mapper);
const resState = {};
Object.keys(stateMapperRes).forEach(key => {
resState[key] = computed(stateMapperRes[key].bind({$store: store}))
})
return resState
}
// 3.toRefs
const { name: responseName, level: responseLevel } = toRefs(store.state);
内置组件
- Teleport => 将组件的 HTML 结构移动到指定位置
开发中不想让常规处于屏幕中央的弹窗结构出现在后代组件的内部,因组件树上的任何存在的定位都可能使内部定位样式受到干扰。
- Suspense(experimental) => 等待异步组件时渲染额外的内容
异步组件需借助 defineAsyncComponent 函数,import 的调用结果作为返回值。
// 动态|异步引入
import {defineAsyncComponent} from 'vue'
const Xxx = defineAsyncComponent(() => import('./components/Xxx.vue'))
// 静态引入
import Xxx from './component/Xxx.vue'
静态引入的潜在风险是当嵌套最深的组件渲染迟滞时,会拖慢其外层所有组件的渲染。动态引入的问题是当网速较慢时,嵌套的内容渲染可能会出现抖动的情况。抖动可通过无需引入的内置组件 Suspense 来解决,其底层通过插槽实现。Suspense 组件有两个仅接收一个直接子节点的插槽。
<Suspense>
<template v-slot:default>
<Xxx> // 待展示组件放入 default 插槽
</template>
<template v-slot:fallback>
<Yyy> // 备用展示内容放入 fallback 插槽
</template>
</Suspense>
setup 不能是 async 函数(异步组件除外),因其返回值不是对象,而是模板解析不了的 Promise,此时模板获取不到 return 对象中的属性。
当使用 Suspense 与 defineAsyncComponent 时,可以返回异步 Promise 实例。
Vite
在不提供块级作用域时,模块化常用社区规范 commonJS 使用函数作用域 IIFE 进行模拟(ES6 之前的 JavaScript )。
- 相较 Vue CLI,轻量级脚手架工具 Vite 默认安装的插件更少
- 考虑到开发过程中依赖增加与额外配置,在实际项目中还是推荐 Vue CLI
- Vite => 基于缓存的热更新;Vue CLI => 基于 Webpack 的热更新
- Vite 使用 ES6 的模块化加载,在开发模式中不需要打包构建就能直接运行
- esbuild 预构建依赖相比于 JavaScript 编写的 Webpack 在速度上更快
预构建依赖 => 开发服务器 DevServer 启动前对将所有代码视为原生 ES 模块(将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM),然后在分析模块的导入时会动态地应用构建过的依赖
- 搭建 Vite 项目
# 安装 vite 自定义初始化项目
npm init vite@latest | yarn create vite
# 也可以通过附加的命令行选项直接指定项目名称和想要使用的模板
# npm 7+, 需要额外的双横线:
npm init vite@latest my-vue-app -- --template vue
# yarn
yarn create vite my-vue-app --template vue
cd my-project # 切换初始化的项目目录
npm install # 安装设定包内容
npm run dev # 启动项目;package.json中有相应信息
破旧立新
Proxy - 数据劫持优化
Vue2.x 通过 defineProperty 对属性读取和修改进行拦截,即数据劫持。
Object.defineProperty(obj, prop, descriptor)
设定数据属性的 defineProperty 等价于 targetObject[propertyName] = ?
。
Object.defineProperty(targetObject, propertyName, { value: 'value' })
Object.defineProperty(targetObject, propertyName, {
enumerable: false,
configurable: false,
writable: false,
value: 'value'
})
-
数据属性:Configurable、Enumerable、Writable、Value;
-
访问器属性:Configurable、Enumerable、Set、Get
data 选项定义的属性会被递归遍历的设置 Get、Set 访问器属性描述符。故 Vue2.x 中新增、删除预先不存在的属性或直接使用下标修改数组都无法实现响应式,页面也不会正常更新。
// 操作对象解决方案 => 无法直接赋值增加或者删除对象属性
import Vue from 'vue'
Vue.set(obj,'key','value') | Vue.delete(obj,'key') // way1
this.$set(obj,'key','value') | this.$delete(obj,'key') // way2
// 操作数组解决方案 => 无法直接通过下标赋值修改或者删除对象属性
import Vue from 'vue'
Vue.set(arr,index,'value') | Vue.delete(arr,index) // way1
this.$set(arr,index,'value') | this.$delete(arr,index) // way2
this.arr.splice(0,1,'value') | this.arr.splice(0) // way3
Vue3.x 使用内置的构造函数 Proxy 创建代理,拦截属性的变化,并通过 Reflect 对被代理的对象属性进行操作。
let p = new Proxy(targetObject, {
get(target, propName) { return Reflect.get(target, propName) },
set(target, propName, value) { Reflect.set(target, propName, value) },
deleteProperty(target, propName) { return Reflect.deleteproperty(target, propName) }
})
Vue3.x 使用 Proxy 只会对真正访问到的内部属性进行惰性响应式,而 Vue2.x 对深层嵌套的对象递归遍历处理以实现响应式,无疑会造成较大的性能开销。
编译时的底层源码优化
- slot 编译优化
Sub 组件仅在被传入动态 slot 的情况下随 Sup 组件的更新而更新。
- diff 算法优化
静态标记取缔全量比较。将渲染颗粒度从组件级降低到区块级,渲染效率不再与模板大小成正相关,而是与动态节点的数量成正相关。只对比虚拟节点中带有数字枚举类型 patchFlag 值的节点,其他节点形成 block tree 稳定结构区域。
// 见 Vue Template Explorer
<div class="hello">Hello World!</div>
<div class="zs">Hello zs!</div>
<div>{{msg}}</div>
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", { class: "hello" }, "Hello World!"),
_createElementVNode("div", { class: "zs" }, "Hello zs!"),
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
通过静态提升 hoistStatic,使不参与更新的元素在每次需要渲染时仅做复用,不做新建。
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", { class: "hello" }, "Hello World!", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", { class: "zs" }, "Hello zs!", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_hoisted_2,
_createElementVNode("div", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
事件会被视作动态绑定,故每次比较都会追踪其变化。但往往事件绑定的都是相同函数,没有追踪变化的必要。可采取 cacheHandlers 进行缓存操作,等待复用。
// vue
<button @click="clickHandler">click me</button>
// 事件监听缓存之前
const _hoisted_1 = ["onClick"]
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("button", { onClick: _ctx.clickHandler }, "click me", 8 /* PROPS */, _hoisted_1))
}
// 事件监听缓存之后失去静态标记
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.clickHandler && _ctx.clickHandler(...args)))
}, "click me"))
}
- hoistStatic 通过 _createStaticVNode 将静态标签转化为字符串;
- 服务端渲染是通过 _ssrRenderAttrs 将静态标签直接转化为文本插入;
- React 是先将 JSX 转化为虚拟 DOM,再转化为 HTML;
<div class="hello">Hello World!</div>
<div class="zs">Hello zs!</div>
<div>{{msg}}</div>
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<!--[--><div${
_ssrRenderAttrs(_mergeProps({ class: "hello" }, _cssVars))
}>Hello World!</div><div${
_ssrRenderAttrs(_mergeProps({ class: "zs" }, _cssVars))
}>Hello zs!</div><div${
_ssrRenderAttrs(_cssVars)
}>${
_ssrInterpolate(_ctx.msg)
}</div><!--]-->`)
}
- tree-shaking 减少打包体积
在编译阶段标记未被引用的函数或对象,在压缩阶段删除标记的代码以实现按需打包。该优化可有效阻止构建时将引入的模块全部打包。
React's Hooks & Composition API
mixin 与组件之间存在隐式依赖:mixin 中定义的方法可能会去调用其他方法。
高阶组件采取黑盒外层包裹组件,增加了复杂度和理解成本。
Render Props 会导致代码体积过大,嵌套过深的问题。
React Hooks 会在每次组件渲染时顺序执行。不允许在循环内部、条件语句或嵌套函数中调用 Hooks => 底层是基于链表的实现,每一个 hook 的 next 会指向下一个 hook。
组合式 API 只能在 setup 钩子中使用,更改 data 会使相关函数或模板重新计算。
Ref 自动解包
- 模板中的解包是浅层的解包
常规 ref 在模板中作为顶层 property 被访问时将自动解包,不需要使用 .value。
当 ref 放入普通对象时,在模板中的使用需要 .value。
<template>
<span>{{ info.msg.value }}</span>
</template>
<script>
import { ref } from "vue";
export default {
setup(){
const msg = ref("Hello, Unpack or not!");
const info = { msg };
return { msg, info }
}
}
</script>
- ref 放入 reactive 的属性中,在模板里使用会自动解包
<template>
<span>{{ info.msg }}</span>
</template>
<script>
import { ref, reactive } from "vue";
export default {
setup(){
const msg = ref("Hello, Unpack or not!");
const info = reactive({ msg });
return { msg, info }
}
}
</script>
Vue CLI - @vue/cli 5.0.8
- main.js => 程序入口文件
Vue2 => 引入 Vue 函数,以 new 的方式创建 Vue 实例并挂载到 DOM。
Vue3 => 解构 createApp 函数,在其后链式调用方法,并挂载于 DOM。
// vue2
import Vue from 'vue';
import App from './App/vue';
import router from './router';
import store from './store';
Vue.config.productionTip = false;
new Vue({ router, store, render: h => h(App) }).$mount("#app")
// vue3
import { createApp } from 'vue';
import App from './App/vue';
import router from './router';
import store from './store';
createApp(App).use(router).use(store).mount('#app');
-
vuex => new Vuex.Store() 转变为 createStore()
-
配置文件变化 => 非 Vue3 的变化 => 常作为第三方引入组件的配置
-
@vue/cli3 启动时不会创建 vue.config.js,欲改 Webpack 配置时自行创建
vue-cli(1.x、2.x) 的后续的版本虽已内部高度集成 Webpack,但依然可以通过创建 vue.config.js 去覆盖默认的配置文件。
其他改变
- 全局 API 的转移 => Vue.xxx 调整到应用实例 app 上
| 2.x 全局 API | 3.x 实例 API |
|:------------------------:|:---------------------------:|
| Vue.config.xxx | app.config.xxx |
| Vue.config.productionTip | remove |
| Vue.component | app.component |
| Vue.directive | app.directive |
| Vue.mixin | app.mixin |
| Vue.use | app.use |
| Vue.prototype | app.config.globalproperties |
- 过渡类名更改
// v2.x
.v-enter, .v-leave-to {opacity: 0;}
.v-leave, .v-enter-to {opacity: 1;}
// v3.x
.v-enter-from, .v-leave-to {opacity: 0;}
.v-leave-from, .v-enter-to {opacity: 1;}
- 因兼容性移除 keyCode 作为 v-on 的修饰符;不再支持 config.keyCodes
<!-- Vue 2 Key Code on v-on -->
<input v-on:keyup.13="submit" />
<input v-on:keyup.8="confirmDelete" />
<!-- Vue 3 Key Modifier on v-on -->
<input v-on:keyup.enter="submit" />
<input v-on:keyup.delete="confirmDelete" />
// Vue2 存在 -> 现已移除
Vue.config.keyCodes.defineAliasButton = 13 // 定义按键别名
- Fragments
因 Vue2.x 不支持 multiple root 组件,所以需要通过将组件都包含在一个 <div> 中修复警告。Vue 3 中支持通过多根节点组件来减少层级。底层逻辑,无需操作。
-
data 选项始终被声明为函数,防止组件复用时数据关联所造成的干扰
-
移除 v-on.native 修饰符 => native 用于指明原生事件,非自定义事件
// Vue2 默认 click 是自定义事件
<Xxx @click.native="yyy" />
给组件绑定的事件若没有被声明接收,默认为原生事件;若声明接受,则表示为自定义事件。
// Sup 组件绑定事件
<my-xxx v-on:close="handleComponentEvent" v-on:click="handleNativeEvent" />
// Sub 组件声明自定义事件
<script>
export default {
emits: ["close"] // 声明则为自定义事件 -> 不声明为原生事件
}
</script>
- 移除过滤器 => 通过 methods 和 computed 也可实现
Bugs 解决
- Uncaught TypeError: app.mount(...).use is not a function
main.js 中实例的使用顺序是,先调用 createApp()
创建应用程序实例;再使用 app.use()
安装插件;最后将应用实例挂载于容器元素 app.mount()
。
const app = createApp(App)
// 重点注意链式调用顺序
app.use(store).use(router).use(ElementPlus).mount('#app');
- Error: Cannot find module 'unplugin-vue-components/resolvers'
Vue3 项目下按需引入 Vant 时出现异常,模块未找到在 node_modules 中找到。
删除 Vant 依赖后重新下载无法解决。
unplugin-vue-components/resolvers 指定库在 22-07-10 加入 npm。
# 解决方式
yarn add unplugin-vue-components
- [Vue warn]: Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered. at Xxx.
问题出现 => 在 async setup 的组件中获取数据,运行项目时页面空白。
解决方式 => 需要在父节点中加上 Suspense 组件。
- "File '?.vue.ts' is not a module" | 文件 "?.vue.ts" 不是模块。ts(2306)
异常场景描述:?.vue 组件中存在空的 <script lang="ts">
标签。
解决方式:无逻辑组件可去除 ts 的申明;有逻辑组件应该完成一定初始化步骤。
<script lang="ts" setup>
import { ref } from "vue";
let fixVueNotModuleError = ref("okk");
console.log(fixVueNotModuleError.value);
</script>
- Component name "" should always be multi-word.
异常场景描述:.vue 文件中 name 属性设置后飘红。
解决方式:.eslintrc.js 中对 rules 数组新增一项检测规则。
"vue/multi-word-component-names": "off",
- Vue+TS 页面刷新后路由匹配 Bug
守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中。
因全局前置守卫 beforeEach
的触发时机早于页面的跳转,故守卫中可通过 to
和 router.getRoutes
的打印来获取路由的信息。
在路由信息中,路径的映射没有问题,但跳转对象的 name 指向了 not-found。
由于页面刷新会重新执行 main.ts,那么应考虑执行顺序的问题:
- 注册路由执行的
use
会调用 router 中install
方法来获取当前 path - 获取到的 path 会与 router.routes 进行一轮匹配
- 若没有注册动态路由,那么会匹配 not-found
解决:动态路由的注册方法应该在路由的注册之前:调换 main.ts 中的执行顺序。
-
[Vue warn]: Property "xxx" was accessed during render but is not defined on instance. at <ComponentXXX> => 在 setup 定义的数据并未返回
-
Vue3 中不支持 Vue2 的 .sync 语法糖 => 点此
结束
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处!