2020年9月18号vue3.0正式发布,距离今天已经过去3个年头了,目前最新的版本是v3.3.4,从最初发布到现在一共经历过3个次要版本的迭代
版本迭代
v3.0.0 One Piece(海贼王) 2020年9月18号发布
- 内部模块解耦,新的架构提供了更好的维护性。
- 保留了vue2中对option api的支持,同时还引入入了Composition API,旨在解决大规模应用程序中使用 Vue 的痛点,支持类似于 React 钩子的逻辑组合和重用、更灵活的代码组织模式和更可靠的类型推断。
- 性能上面也做了很大的改进,初始化速度、更新、内存使用方面都有很大的减少,通过tree-shaking也能很大程度的减小最终的包大小。编译模板时也做了很多优化,静态提升、更新类型标记等
- 更好的类型支持、vue3使用TypeScript编写
- 新增实验性功能
<script setup>
语法糖和<style vars>
语法糖
typescript
// <style vars> 语法糖
<template>
<div class="text">hello</div>
</template>
<script>
export default {
data() {
return {
color: 'red'
}
}
}
</script>
<style vars="{ color }">
.text {
color: var(--color);
}
</style>
v3.1.0 Pluto(冥王星) 2021年6月8号发布
- 破坏性更新,props 中声明的 key,将一直存在。不管父组件是否传递该 key,这一直是 Vue 2 中的行为,因此被认为是一个修复。
- 性能改进,仅在实际更改时触发
$attrs
更新
v3.2.0 Quintessential Quintuplets 典型的五胞胎 2021年8月5号发布
- 实验性功能
<script setup>
语法糖和<style> v-bind
语法糖,现在被认为是稳定的
typescript
// <style> v-bind 语法糖
<script setup>
import { ref } from 'vue'
const color = ref('red')
</script>
<template>
<button @click="color = color === 'red' ? 'green' : 'red'">
Color is: {{ color }}
</button>
</template>
<style scoped>
button {
color: v-bind(color);
}
</style>
- 新增
defineCustomElement
用于创建Vue驱动的Web Components - 新增
v-memo
指令,缓存一个模板的子树,如果绑定数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过 - 新增实验性功能
Reactivity Transform(响应式转换)
,但是今年年初这个实验性功能被废弃了,详见废弃原因
typescript
// 响应式转换 编译前
<script setup>
import { $ref } from 'vue/macros'
let count = $ref(0)
console.log(count)
function increment() { count++ }
</script>
// 编译后
<script setup>
import { ref } from 'vue'
let count = ref(0)
console.log(count.value)
function increment() { count.value++ }
</script>
v3.3.0 Rurouni Kenshin(浪客剑心) 2023年5月11号发布
- 宏定义的类型支持复杂类型及导入,之前
defineProps
和defineEmits
的类型参数使用的类型仅限于本地类型(同一文件),并且只支持类型字面量和接口。这是因为 Vue 需要能够分析 props 接口上的属性,以便生成相应的运行时选项。此限制现已在3.3
中解决。编译器现在可以解析导入的类型,并支持一组有限的复杂类型
typescript
<script setup lang="ts">
import type { Props } from './foo'
// 导入 + 交叉类型
defineProps<Props & { extraProp?: string }>()
</script>
- 泛型组件,使用
<script setup>
的组件现在可以通过generic
属性接受通用类型参数
typescript
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
</script>
3.更友好的 defineEmits
类型定义
typescript
// BEFORE
const emit = defineEmits<{
(e: 'foo', id: number): void,
(e: 'bar', name: string, ...rest: any[]): void
}>()
// AFTER
const emit = defineEmits<{
foo: [id: number],
bar: [name: string, ...rest: any[]],
baz: [name: string, ...rest: [number, boolean]]
}>()
- 使用
defineSlots
定义插槽类型
typescript
<script setup lang="ts">
defineSlots<{
default?: (props: { msg: string }) => any;
item?: (props: { id: number }) => any
}>()
</script>
- 新的
defineOptions
宏允许直接在<script setup>
中声明组件选项,而不需要单独的<script>
typescript
<script setup>
defineOptions({ name: 'componentName', inheritAttrs: false })
</script>
- 实验性功能
props
支持解构
js
<script setup>
import { watchEffect } from 'vue'
const { msg = 'hello' } = defineProps(['msg'])
watchEffect(() => {
// msg 将会被试做为依赖进行追踪, 就像访问 props.msg 一样,当 msg 更新副作用函数会重新执行
console.log(`msg is: ${msg}`)
})
</script>
<template>{{ msg }}</template>
- 实验性功能
defineModel
typescript
<!-- BEFORE -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
console.log(props.modelValue)
function onInput(e) {
emit('update:modelValue', e.target.value)
}
</script>
<template>
<input :value="modelValue" @input="onInput" />
</template>
<!-- AFTER -->
<script setup>
const modelValue = defineModel()
console.log(modelValue.value)
</script>
<template>
<input v-model="modelValue" />
</template>
vue3响应式过程
vue3由三个核心模块组成:
响应式模块(Reactivity Module)
允许我们创建响应式对象并且能够观测对象的变化,当使用对象的副作用函数运行时,这些函数会被追踪(track),当对象发生变化的时候,会触发(trigger)这些函数重新执行
编译模块(Compiler Module)
将Html templates编译成渲染函数,这个过程可能发生在浏览器端,也有可能发生在构建vue的时候,这取决于你使用vue的方式
渲染模块(Renender Module)
包含在网页上渲染组件的三个个不同阶段
- 渲染阶段(Render Phase), 调用渲染函数生成虚拟DOM
- 挂载阶段(Mount Phase),根据生成的虚拟DOM,并调用DOM API来创建网页
- 补丁阶段(Patch Phase),渲染器将旧的虚拟节点和新的虚拟节点进行比较,并且只更新网页中需要变化的部分
图示过程
响应式模块
响应式转换
reactive方法介绍
typescript
/**
* @description: 创建响应式对象
* @param target 被代理的对象
* @param isReadonly 是否只读
* @param baseHandlers Object Array的捕获器
* @param collectionHandlers Map Set的捕获器
* @param proxyMap 一个weakMap 用于存储targe和Proxy的对应关系map
* @return 代理对象
*/
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
if (!isObject(target)) { // val !== null && typeof val === 'object'
// 如果不是对象, 直接返回传入的值
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 如果需要代理的对象已经是Proxy 直接返回
// 除了 在一个Proxy方法上 调用readonly方法
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 如果对象已被代理 直接返回对应的Proxy
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 只有 Object Array | Map Set WeakMap WeakSet类型的数据可以观测
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
}
这个方法基本还算是比较容易理解的, 但是最终生成代理对象的时候,有一个判断,Object Array
和 Map Set WeakMap WeakSet
用了不同的捕获器handler
对象。
baseHandlers
get 捕获器
用于拦截对象的读取属性操作
typescript
get(target: Target, key: string | symbol, receiver: object) {
// 响应式对象有两种模式,分别是readonly(只读)和shallow(浅层代理)
const isReadonly = this._isReadonly,
shallow = this._shallow
// 处理一些特殊属性的访问 这些属性主要是通过api访问
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
const res = Reflect.get(target, key, receiver)
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
return res
}
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
return res
}
if (isRef(res)) {
// ref unwrapping - skip unwrap for Array + integer key.
return targetIsArray && isIntegerKey(key) ? res : res.value
}
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
get
捕获器中第一步是处理一些特殊属性的访问
typescript
if (key === ReactiveFlags.IS_REACTIVE) {
// 调用 isReactive()
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
// 调用 isReadonly()
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
// 调用 isShallow()
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
// 调用 toRaw() 返回代理的原始对象
return target
}
get
捕获器中第二步是针对数组做特殊的处理
typescript
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
为什么要对数组上的方法includes
、indexOf
、lastIndexOf
做这些特殊处理呢,我们看一段简单的示例代码就懂了
js
const obj = {};
function myReactive(obj) {
return new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
if (typeof res === "object" && res !== null) {
return myReactive(obj);
}
return res;
},
});
}
const arr = myReactive([obj]);
console.log(arr.includes(obj)); // false
console.log(arr.indexOf(obj)); // -1
console.log(arr.lastIndexOf(obj)); // -1
代码执行后分别打印了false、-1、-1,这是为什么呢?当调用includes
、indexOf
、lastIndexOf
这些方法时,会遍历arr
,遍历arr
的过程取到的是被Proxy
代理过后的对象,如果拿这个Proxy
对象和obj
原始对象比较,肯定找不到,所以需要重写这三个方法。
push
、pop
、shift
、unshift
、splice
这些方法为什么要特殊处理呢?仔细看这几个方法的执行,都会改变数组的长度。以push
为例,我们查看ECMAScript对push
的执行流程说明:
在第二步中会读取数组的length
属性,在第六步会设置length
属性。我们知道在属性的读取过程中会进行依赖的收集,在属性的修改过程中会触发依赖执行。如果按照这样的逻辑会发生什么问题呢?我们还是以一个例子说明:
js
const arr = reactive([])
watchEffect(() => {
//立即运行, 且每次依赖更新时都会重新执行
arr.push(1)
})
当向arr
中进行push
操作,首先读取到arr.length
,将length
对应的依赖effect
收集起来,由于push
操作会设置length
,所以在设置length
的过程中会触发length
的依赖,执行watchEffect
里面的函数,又会调用arr.push
操作,这样就会造成一个死循环。
为了解决这两个问题,需要重写这几个方法。
arrayInstrumentations
:
typescript
const arrayInstrumentations = /*#__PURE__*/ createArrayInstrumentations()
// 对数组特殊处理的部分
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
// 每个索引都需要进行收集依赖
track(arr, TrackOpTypes.GET, i + '')
}
// 在原始对象上调用方法
const res = arr[key](...args)
if (res === -1 || res === false) {
// 如果没有找到,可能参数中有响应对象,将参数转为原始对象,再调用方法
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
// 暂停依赖收集
// 因为push等操作是修改数组的,所以在push过程中不进行依赖的收集是合理的,只要它能够触 发依赖就可以
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
// 恢复依赖收集
resetTracking()
return res
}
})
return instrumentations
}
我们在回到回到get
捕获器中
typescript
// 获取 取值操作的结果值
const res = Reflect.get(target, key, receiver);
if (isSymbol(key) ? builtInSymbols.has(key) : isNonTrackableKeys(key)) {
// 如果key是Symbol内置的值或者 key在 `__proto__,__v_isRef,__isVue`里面 直接返回结果值
return res;
}
if (!isReadonly) {
// 如果不是只读的, 收集依赖, 因为对于只读的响应式数据,是无法对其进行修改的,所以收集它的依赖时没有用的
track(target, TrackOpTypes.GET, key);
}
if (shallow) {
// 如果是浅层响应式,不做深层级的转换 直接返回结果值 比如使用shallowReactive方法转换的响应式对象
return res;
}
if (isRef(res)) {
// 如果是结果值是ref类型
// 如果ref是作为响应式数组中的元素直接返回ref,否则解包ref的value值
return targetIsArray && isIntegerKey(key) ? res : res.value;
}
if (isObject(res)) {
// 如果res是Object,进行深层响应式处理。
return isReadonly ? readonly(res) : reactive(res);
}
return res;
从最后的代码我们可以看出,vue3是懒惰式的创建响应式对象,只有访问对应的key,才会继续创建响应式对象,否则不用创建。
set 捕获器
typescript
set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
if (!this._shallow) {
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
set
拦截器中首先获取旧值。如果旧值是只读的ref
类型,而新的值不是ref
,则返回false
,不允许修改
js
let oldValue = (target as any)[key]
if (isReadonly(oldValue) && isRef(oldValue) && !isRef(value)) {
return false
}
// 比如这种情况
const state = reactive({a: readonly(ref(1))})
state.a = 2 // 这种操作是不允许的
接着判断是不是浅层代理
js
if (!this._shallow) {
// 新值不是浅层响应式且不是只读,新旧值取其对应的原始值
if (!isShallow(value) && !isReadonly(value)) {
oldValue = toRaw(oldValue)
value = toRaw(value)
}
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 如果target不是数组并且旧值是ref类型,新值不是ref类型,直接修改oldValue.value为value
oldValue.value = value // 相当于直接装箱 在get中有拆箱操作
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
// 如果是浅层响应式,对象按原样设置
}
为什么要取新旧的原始值呢?我猜测是避免下面这种情况触发多次watchEffect
回调
js
const obj1 = {};
const obj2 = { a: reactive(obj1) };
const state = reactive(obj2);
watchEffect(() => {
console.log("state.a", state.a);
});
state.a = obj1;
接下来就是调用Reflect.set
进行赋值。然后触发依赖。
js
// 判断操作的key是否存在
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果是原型链上面的修改,就不触发更新
if (target === toRaw(receiver)) {
if (!hadKey) {
// 新增值
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) { // Object.is(value, oldValue)
// 如果是修改值 做新旧值的比较
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
delete捕获器
用于拦截对对象属性的 delete
操作
typescript
deleteProperty(target: object, key: string | symbol): boolean {
// 目标对象是否有对应的key
const hadKey = hasOwn(target, key)
// 旧值
const oldValue = (target as any)[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
// 删除成功且目标对象否有对应的key 触发依赖收集
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
has捕获器
用于拦截in
操作符的代理方法。
typescript
has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
if (!isSymbol(key) || !builtInSymbols.has(key)) {
// key不是symbol或者不是内置的Symbol 触发依赖收集
track(target, TrackOpTypes.HAS, key)
}
return result
}
ownKeys捕获器
用于拦截Object.getOwnPropertyNames()
、Object.getOwnPropertySymbols()
、Object.keys()
等操作
typescript
ownKeys(target: object): (string | symbol)[] {
track(
target,
TrackOpTypes.ITERATE, // 迭代类型
isArray(target) ? 'length' : ITERATE_KEY
)
return Reflect.ownKeys(target)
}
collectionHandlers
为什么要对Map
等类型数据写单独的捕获器函数呢?因为proxy本身也有一定的局限性
js
const map = new Map([[1, "男"]]);
const proxyMap = new Proxy(map, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
});
console.log(proxyMap.get(1)); // 报错TypeError: Method Map.prototype.get called on incompatible receiver [object Object]
具体原因我们可以参考一下Proxy 的局限性,这跟内置对象(例如 Map、Set、Date、Promise)的内部机制有关,他们的内部所有的数据存储在一个"internal slots"中。当我们访问 Set.prototype.add 其实就是通过内部的 this 来访问该方法的,但是数据代理的时候this=proxy,但是 proxy 并没有相应的"internal slots"这个东西,所以会报错。
接下来我们看下collectionHandlers
的代码定义
typescript
// 这个是collectionHandlers定义的入口,我们看到这里只定义了get捕获器
const mutableCollectionHandlers: ProxyHandler<CollectionTypes> = {
get: createInstrumentationGetter(false, false)
}
// get捕获器的具体实现
function createInstrumentationGetter(isReadonly: boolean, shallow: boolean) {
// 和baseHandler一样,他也有两种模式isReadonly(只读)和shallow(浅层代理)
const instrumentations = shallow
? isReadonly
? shallowReadonlyInstrumentations
: shallowInstrumentations
: isReadonly
? readonlyInstrumentations
: mutableInstrumentations
return (
target: CollectionTypes,
key: string | symbol,
receiver: CollectionTypes
) => {
// 对一些内部key值判断
if (key === ReactiveFlags.IS_REACTIVE) {
return !isReadonly
} else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.RAW) {
return target
}
//如果是 get has add set delete clear forEach 的方法调用,或者是获取size,那么改为调用mutabelInstrumentations里的相关方法
return Reflect.get(
hasOwn(instrumentations, key) && key in target
? instrumentations
: target,
key,
receiver
)
}
}
看一下mutabelInstrumentations
的里面定义的方法
Map
和Set
的get
typescript
function get(
target: MapTypes,
key: unknown,
isReadonly = false,
isShallow = false
) {
/**
* 1. 针对readonly(reactive(new Map()))的情况
* target获取的是代理对象,而rawTarget的是Map对象
* 2. 针对reactive(new Map())的情况
* target和rawTarget都是指向Map对象
*/
target = (target as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 获取原始的key,这里是因为Map中也可以使用对象作为key,所以对象也可能是响应式的
const rawKey = toRaw(key)
if (!isReadonly) {
// 非只读模式
// 若key为代理对象,对key进行依赖收集
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
// 对rawKey进行依赖收集
track(rawTarget, TrackOpTypes.GET, rawKey)
}
// 获取Map原型链上的has方法用于判断获取成员是否存在于Map对象上
const { has } = getProto(rawTarget)
// 根据不同的代理模式赋值wrap,当前就是toReactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
if (has.call(rawTarget, key)) {
// 转换目标值为响应式对象
return wrap(target.get(key))
} else if (has.call(rawTarget, rawKey)) {
// 转换目标值为响应式对象
return wrap(target.get(rawKey))
} else if (target !== rawTarget) {
/**
* 针对readonly(reactive(new Map())),即使没有匹配的键值对,也要跟踪对响应式对象某键的依赖信息
* const state = reactive(new Map())
* const readonlyState = readonly(state)
*
* effect(() => {
* console.log(readonlyState.get('foo'))
* })
* // 打印 undefined
* state.set('foo', 1)
* // 打印 1
*/
target.get(key)
}
}
Map
和Set
的has
方法
typescript
function has(this: CollectionTypes, key: unknown, isReadonly = false): boolean {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 获取原始的key,这里是因为Map中也可以使用对象作为key,所以对象也可能是响应式的
const rawKey = toRaw(key)
if (!isReadonly) {
// 非只读模式
// 若key为代理对象,对key进行依赖收集
if (hasChanged(key, rawKey)) {
track(rawTarget, TrackOpTypes.GET, key)
}
// 对rawKey进行依赖收集
track(rawTarget, TrackOpTypes.GET, rawKey)
}
return key === rawKey
? target.has(key)
: target.has(key) || target.has(rawKey) // 如果key为代理对象,判断有key或者有rawKey
}
Map
和Set
的size
属性
typescript
function size(target: IterableCollections, isReadonly = false) {
target = (target as any)[ReactiveFlags.RAW]
// 跟踪ITERATE_KEY即所有修改size的操作均会触发访问size属性的副作用函数
!isReadonly && track(toRaw(target), TrackOpTypes.ITERATE, ITERATE_KEY)
return Reflect.get(target, 'size', target)
}
Set
的add
方法
typescript
function add(this: SetTypes, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
const proto = getProto(target)
const hadKey = proto.has.call(target, value)
if (!hadKey) {
// 如果不存在当前数据,说明是新增的,触发响应式依赖
trigger(target, TriggerOpTypes.ADD, value, value)
}
return this
}
Map
的set
方法
typescript
function set(this: MapTypes, key: unknown, value: unknown) {
value = toRaw(value)
const target = toRaw(this)
// 获取原型上面的方法
const { has, get } = getProto(target)
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
// 开发环境检查目标数据上面是否同时存在rawKey和key
checkIdentityKeys(target, has, key)
}
const oldValue = get.call(target, key)
target.set(key, value)
if (!hadKey) {
// 如果Map不存在对应的key,说明是新增键值数据
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
// 如果新旧值不同,代表有修改,触发响应式依赖
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
return this
}
Map
和Set
的delete方法
typescript
function deleteEntry(this: CollectionTypes, key: unknown) {
const target = toRaw(this)
// 获取原型上面的方法
const { has, get } = getProto(target)
// 检测key是否存在
let hadKey = has.call(target, key)
if (!hadKey) {
key = toRaw(key)
hadKey = has.call(target, key)
} else if (__DEV__) {
// 开发环境检查目标数据上面是否同时存在rawKey和key
checkIdentityKeys(target, has, key)
}
const oldValue = get ? get.call(target, key) : undefined
// forward the operation before queueing reactions
const result = target.delete(key)
if (hadKey) {
// 如果存在集合中key,触发响应式依赖
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
Map
和Set
的clear
方法
typescript
function clear(this: IterableCollections) {
const target = toRaw(this)
const hadItems = target.size !== 0
const oldTarget = __DEV__
? isMap(target)
? new Map(target)
: new Set(target)
: undefined
// forward the operation before queueing reactions
const result = target.clear()
if (hadItems) {
// size不为0的情况下触发响应式依赖
trigger(target, TriggerOpTypes.CLEAR, undefined, undefined, oldTarget)
}
return result
}
Map
和Set
的forEach
方法
typescript
function createForEach(isReadonly: boolean, isShallow: boolean) {
return function forEach(
this: IterableCollections,
callback: Function,
thisArg?: unknown
) {
const observed = this as any
const target = observed[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
// 根据不同的代理模式赋值wrap,当前就是toReactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 不是只读模式就收集ITERATE_KEY依赖,Map/Set对象元素个数发生变化则触发响应式依赖
!isReadonly && track(rawTarget, TrackOpTypes.ITERATE, ITERATE_KEY)
return target.forEach((value: unknown, key: unknown) => {
// 将key和value都转换为代理对象
return callback.call(thisArg, wrap(value), wrap(key), observed)
})
}
}
迭代器对象相关方法
迭代器主要是对集合中的迭代方法进行处理即: ['keys', 'values', 'entries', Symbol.iterator]
typescript
const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator]
iteratorMethods.forEach(method => {
mutableInstrumentations[method as string] = createIterableMethod(
method,
false,
false
)
function createIterableMethod(
method: string | symbol,
isReadonly: boolean,
isShallow: boolean
) {
return function (
this: IterableCollections,
...args: unknown[]
): Iterable & Iterator {
const target = (this as any)[ReactiveFlags.RAW]
const rawTarget = toRaw(target)
const targetIsMap = isMap(rawTarget)
// 如果是entries方法,或者是map的迭代方法,isPair为true
const isPair =
method === 'entries' || (method === Symbol.iterator && targetIsMap)
/**
* 当调用的是Map对象的keys方法,即副作用函数只依赖Map对象的键而没有依赖值。
* 避免修改值的时候执行了不必要的依赖更新,在trigger函数中有判断。
*/
const isKeyOnly = method === 'keys' && targetIsMap
const innerIterator = target[method](...args)
// 根据不同的代理模式赋值wrap,当前就是toReactive
const wrap = isShallow ? toShallow : isReadonly ? toReadonly : toReactive
// 不是只读模式,就收集迭代依赖依赖
!isReadonly &&
track(
rawTarget,
TrackOpTypes.ITERATE,
isKeyOnly ? MAP_KEY_ITERATE_KEY : ITERATE_KEY
)
// 返回包装过的迭代器,这个迭代器返回的值也是被代理的过的
// 迭代器的值 是触发真实的迭代器获得的
return {
// iterator protocol
next() {
const { value, done } = innerIterator.next()
return done
? { value, done }
: {
value: isPair ? [wrap(value[0]), wrap(value[1])] : wrap(value),
done
}
},
// 可迭代协议
[Symbol.iterator]() {
return this
}
}
}
}
副作用函数
前面vue3已经简单的了解到vue3是如何将普通对象转换为响应式对象的,接下来来研究下副作用函数,了解一个类ReactiveEffect
。 ReactiveEffect
对象会在这几种场景下创建:
- computed(计算属性)
- watch (侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数)
- watchEffect (立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行)
- render (页面渲染) 接下来我们看下
ReactiveEffect
的定义
typescript
// 当前递归跟踪的effectshen深度
let effectTrackDepth = 0
// 当前活跃的effect
let activeEffect: ReactiveEffect | undefined
// 一个二进制变量,表示当前 ReactivEffect 的层级
let trackOpBit = 1
// 表示是否需要收集依赖
let shouldTrack = true
/**
* 按位跟踪标记最多支持 30 级递归
* 选择该值是为了使现代 JS 引擎能够在所有平台上使用 SMI(small integer 小整数).
* 当递归深度更大时,回退到使用完全清理.
*/
const maxMarkerBits = 30
class ReactiveEffect<T = any> {
// 是否活跃
active = true
// dep 数组,在响应式对象收集依赖时也会将对应的依赖项添加到这个数组中
deps: Dep[] = []
// 父ReactiveEffect 的实例
parent: ReactiveEffect | undefined = undefined
// 创建后可能会附加的属性,如果是 computed 则指向 ComputedRefImpl
computed?: ComputedRefImpl<T>
// 是否允许递归,会被外部更改
allowRecurse?: boolean
// 延迟停止
private deferStop?: boolean
// 停止事件
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
// 参数赋值给fn
public fn: () => T,
// 参数赋值给scheduler
public scheduler: EffectScheduler | null = null,
/**
* 副作用的作用域, 可以捕获其中所创建的响应式副作用,
* 这样捕获到的副作用可以在组件卸载的时候一起处理
*/
scope?: EffectScope
) {
// 记录副作用作用域
// 参照 effectScope可以看的明白(https://cn.vuejs.org/api/reactivity-advanced.html#effectscope)
recordEffectScope(this, scope)
}
// run方法,执行目标函数
run() {
if (!this.active) {
// 如果是非激活状态,直接执行传入的fn函数并返回其结果
return this.fn()
}
// 赋值activeEffect给parent变量
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
// 如果找到parent===this 直接return,跳出函数执行
return
}
parent = parent.parent
}
try {
// 将当前活跃activeEffect 赋值给this.parent
this.parent = activeEffect
// 将当前当前活跃activeEffect指向自己
activeEffect = this
// shouldTrack设置为true
shouldTrack = true
// 定义当前的 ReactiveEffect 层级
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
// 标记所有的 dep 为 was(表示过去的依赖)
initDepMarkers(this)
} else {
// 降级方案,删除所有的依赖,再重新收集
cleanupEffect(this)
}
// 执行目标副作用函数
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
// 重置标记,删除旧的依赖
finalizeDepMarkers(this)
}
// 退出当前层级
trackOpBit = 1 << --effectTrackDepth
// 将当前活跃实例修改为this.parent
activeEffect = this.parent
shouldTrack = lastShouldTrack
// 将this.parent 赋值为undefined
this.parent = undefined
// 延时停止,这个标志是在 stop 方法中设置的
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// 延迟停止,需要执行完当前的副作用函数之后再停止
if (activeEffect === this) {
// 在 run 方法中会判断 deferStop 的值,如果为 true,就会执行 stop 方法
this.deferStop = true
} else if (this.active) {
// 清除所有的依赖追踪标记
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
// 将active设为false
this.active = false
}
}
}
看下initDepMarkers
的代码实现
typescript
const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
// 当前作用有依赖
for (let i = 0; i < deps.length; i++) {
// 标记依赖在当前深度被追踪,w代表was(过去的)
deps[i].w |= trackOpBit // set was tracked
}
}
}
看下cleanupEffect
的代码实现 zhuanlan.zhihu.com/p/461159820
typescript
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
// 在各个 dep 中,删除该effect对象
deps[i].delete(effect)
}
// 清空依赖数组
deps.length = 0
}
}
看下finalizeDepMarkers
的代码实现
typescript
// 当前深度是否是旧依赖
const wasTracked = (dep: Dep): boolean => (dep.w & trackOpBit) > 0
// 当前深度是否是新依赖
const newTracked = (dep: Dep): boolean => (dep.n & trackOpBit) > 0
const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i++) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
// 是就依赖但不是新依赖 删除掉依赖
dep.delete(effect)
} else {
// 需要保留的依赖,放到数据的较前位置,因为在最后会删除较后位置的所有依赖
deps[ptr++] = dep
}
// 清理 was 和 new 标记,将它们对应深度的 bit,置为 0
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
依赖收集
在看依赖收集函数track
前我们先了解一个vue3内部对象targetMap
targetMap
是vue3响应式一个核心的对象,主要存储了响应式对象和副作用函数的关系。
typescript
// weakMap key是响应式对象, value是一个map
const targetMap = new WeakMap<object, KeyToDepMap>()
// map key是响应式对象的key value是依赖这个key的所有effect
type KeyToDepMap = Map<any, Dep>
// set结构, 保证effect不重复
type Dep = Set<ReactiveEffect> & TrackedMarkers
type TrackedMarkers = {
/**
* wasTracked
*/
w: number // 之前被收集
/**
* newTracked // 之后被收集
*/
n: number
}
看下track
的定义
typescript
/**
*
* 检查当前正在运行哪个effect并将其记录为 dep
* 记录所有依赖响应式熟悉的effect
*
* @param target - 响应式对象.
* @param type - 定义访问响应式对象属性的类型.
* @param key - 响应式对象属性的标识符
*/
function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
// 允许依赖收集 且当前activeEffect有值
let depsMap = targetMap.get(target)
if (!depsMap) {
// 每个 target 对应一个 depsMap
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
// 每个 key 对应一个 dep 集合
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// 依赖是否需要收集
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 是否小于最大标记的位数
if (!newTracked(dep)) {
// 当前深度标记为新依赖
dep.n |= trackOpBit
// 如果是旧依赖就不需要再收集
shouldTrack = !wasTracked(dep)
}
} else {
// cleanup模式
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect!)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
派发更新
vue3派发更新的核心是通过track
函数实现的,我们来看下函数的实现
typescript
/**
* Finds all deps associated with the target (or a specific property) and
* triggers the effects stored within.
*
* @param target - 响应式对象.
* @param type - 定义触发副作用函数的操作类型 主要有四种类型:set、add、delete、clear
* @param key - 响应式对象属性的标识符
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
const depsMap = targetMap.get(target)
if (!depsMap) {
// 如果没有收集过依赖,直接退出函数
return
}
// 需要触发的依赖数组
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// 如果是map或set的clear操作,直接触发所有的副作用函数
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
// 如果是数组的length
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
// 触发length的依赖和下标大于新值的依赖
deps.push(dep)
}
})
} else {
if (key !== void 0) {
// 触发对应key的依赖
deps.push(depsMap.get(key))
}
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
// 非数组 触发依赖迭代的副作用
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 触发依赖map key的迭代依赖
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 数组添加新属性 且属性是number类型,这时会导致length变化
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
// 删除操作
if (!isArray(target)) {
// 非数组 触发依赖迭代的副作用
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
// 触发依赖map key迭代的副作用
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
// 数组情况下 删除操作 没有触发对应的依赖
break
case TriggerOpTypes.SET:
if (isMap(target)) {
// 触发不依赖map key的迭代依赖
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
const eventInfo = __DEV__
? { target, type, key, newValue, oldValue, oldTarget }
: undefined
// 触发所有要执行的副作用函数
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
}
triggerEffects
代码实现
typescript
function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
// 确保computed的副作用最先执行,避免在其他副作用执行时引用的是旧的计算结果
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
triggerEffect
代码实现
typescript
/**
* @param effect 副作用函数
* @param debuggerEventExtraInfo 调试信息
*/
function triggerEffect(
effect: ReactiveEffect,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
if (effect !== activeEffect || effect.allowRecurse) {
// 判断当前执行的activeEffect和要执行的effect是不是一个,避免死循环
// 比如避免在watchEffect里面执行count++类似的操作导致死循环
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
// 如果传入了scheduler调度函数,就执行scheduler调度函数
effect.scheduler()
} else {
// 否则直接执行effect的run方法
effect.run()
}
}
}
接下来我们看下具体的调度函数
computed
的调度函数
typescript
class ComputedRefImpl{
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
// computed 对应的调度函数
if (!this._dirty) {
// 重置_dirty标识
this._dirty = true
//
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
// 值被污染,重新计算
self._dirty = false
// 调用run方法
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
// 触发依赖它的副作用函数
triggerEffects(dep)
}
}
}
watch
的调度函数
typescript
let scheduler: EffectScheduler
if (flush === 'sync') {
// 同步执行
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 等其他副作用执行完后再执行(Vue 更新之后,可以访问更新后的dom)
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}
const effect = new ReactiveEffect(getter, scheduler)
渲染函数
的调度函数
typescript
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update), // 组件渲染的调度函数
instance.scope // 用于组件执行时访问到的effect, 方便后续一起停止,所以内部的副作用必须都是同步的,异步注册的需要自己手动停止
))
// update最终就是执行run方法
const update: SchedulerJob = (instance.update = () => effect.run())
queueJob
代码实现
typescript
// 是否正在清空
let isFlushing = false
// 清空任务待执行
let isFlushPending = false
// 任务队列
const queue: SchedulerJob[] = []
// 当前执行的任务在任务队列里面的下标
let flushIndex = 0
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
let currentFlushPromise: Promise<void> | null = null
// 将任务放到执行队列中
function queueJob(job: SchedulerJob) {
// 重复任务搜索 用Array.includes()方法,默认从当前执行的任务下标开始搜索,避免任务递归触发。
// 如果任务是watch的callback,就从当前执行的任务下标+1开始搜索,目的是允许他递归触发
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)
) {
if (job.id == null) {
// 没有id的任务放到队尾
queue.push(job)
} else {
// 否则将任务插入到合适的位置
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 清空任务队列
queueFlush()
}
}
// 使用二分查找,根据id,在目标队列里面寻找合适的插入位置,确保任务id递增
function findInsertionIndex(id: number) {
let start = flushIndex + 1
let end = queue.length
while (start < end) {
const middle = (start + end) >>> 1
const middleJobId = getId(queue[middle])
middleJobId < id ? (start = middle + 1) : (end = middle)
}
return start
}
function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 在下一个微任务里面清空任务, 记录当前微任务
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
function nextTick<T = void, R = void>(
this: T,
fn?: (this: T) => R
): Promise<Awaited<R>> {
const p = currentFlushPromise || resolvedPromise
// 确保其他的任务在当前队列清空后执行
return fn ? p.then(this ? fn.bind(this) : fn) : p
}
// 清空任务函数
function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
// 给任务任务排序
// 1. 确保任务执行先父后子
// 2. 父组件执行更新时,如果子组件已经卸载,可以跳过子组件的更新
queue.sort(comparator)
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
// 任务active标识不为false(在组件卸载的时候这个值会被设置为fasle)
// 如果任务的active
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
}
} finally {
flushIndex = 0
queue.length = 0
// 清空 需要后置执行的任务
flushPostFlushCbs(seen)
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
// 继续清空队列,直到所有任务都清空
if (queue.length || pendingPostFlushCbs.length) {
flushJobs(seen)
}
}
总结
vue2
和vue3
响应式部分共同点
- 基本实现思路差不多,都是对数据做响应式转换,然后在数据被访问的时候收集依赖,在数据变化的时候触发更新
vue2
和vue3
响应式部分不同同点 vue3
有更友好的typescript
支持,可以看到整个vue3
的代码库也是用typescript
编写vue3
优化了系统模块结构,开发人员可以只导入需要的API,从而减少最终的包体积大小vue3
使用Proxy取代Object.defineProperty做数据操作劫持,并且增加对Map和Set数据结构的响应式处理vue3
是惰性的创建响应式,深层属性只有对应的数据被使用到才会继续创建响应式代理,而vue2
在数据创建开始就递归的完成所有深层的响应式代理vue3
可以更快更准确的更新,例如在vue2
中如果对象新增属性使用this.$set
是会触发所有用到这个对象的依赖更新,而在vue3
中控制了只会触发用到具体属性的地方更新