前言
辞职后一时半会也找不到工作, 只好接着摸索摸索代码了.
距离 Vue3.5 发布也有了不少的时间, 也需要更新更新脑子里对于更新的东西的状态了.
更新
具体更新在官方README有写vuejs changelog
包含对响应式的重构和优化, SSR 的优化和BUG修复, 自定义元素(Custom Element)的API更新等等.
响应式
- 响应式不再以 Watch < - > Dep 的方式关联关系, 而是使用双向链表的形式进行关联和依赖收集, 使用版本计数的方式优化更新速度和内存.pull#10397
- 对数组的响应式优化, 减少不必要的数组的遍历.pull#9511
- 默认启用了 props 解构编译.
- 新增了 onEffectCleanup API 用来清除 Effect 副作用.issues#10173
- 新增了 watch, getCurrentWatcher 和 onWatcherCleanup API.issues#9927
- watch, effect, effectScope 等能够控制暂停监听了. pull#9651 rfc
- watch 支持 deep 深度监听的层级.issues#9572
SSR
- 新增 useId 和 app.config.idPrefix 配置issues#11404
- 对于异步组件自定义水合(hydration)策略.issues#11458
自定义元素(Custom Elements)
- 新增 useHost API.
- 新增 useShadowRoot API.issues#6113 issues#8195
- 可以在 Option API 中使用 this.$host.
- 能够将子组件样式应用于 shadow root. issues#11517
- 支持在自定义元素使用 app 实例. issues#4356 issues#4635
- 支持选择器 :root css选择器应用 css 变量(v-bind). pull#8830
- 支持自定义元素 dispathEvent CustomEvent 传递参数. issues#7605
- 支持自定义元素暴露值. issues#5540
- 支持插入样式时的 nonce 配置. issues#6530
- 支持传入自定义选项到 defineCustomElement.
- 支持设置在 defineCustomElement 中将 shadowRoot 为 false. issues#4314 issues#4404
Teleport
- 支持通过设置 defer 属性延迟挂载. pull#11387
- 支持对 Teleport 的子元素应用 transition 标签. pull#6548
杂项
- 新增 useTemplateRef API.
- 新增 app.onUnmount API. issues#4619
- 新增 app.config.throwUnhandledErrorInProduction 配置项. issues#7876
- 可信类型兼容. rfcs
- 支持在模板中使用 Symbol. issues#9027
内部内容
- CustomRefs 缓存 value. pull#11539
除了以上之外, 还有一些类型声明的更新, 以及在之后的版本的大量的BUG修复.
响应式部分
内存和速度优化
响应式部分的依赖收集原理没有变化, 触发和存储方式有所改变, 推荐以下文章了解:
数组优化
对于数组优化部分, 比较好解释: Vue 3.4 的数组方法处理:
baseHandlers.ts
typescript
// packages/reactivity/src/baseHandlers.ts:48
// vue 3.4 关于数组原生方法的处理
function createArrayInstrumentations() {
const instrumentations: Record<string, Function> = {}
// instrument identity-sensitive Array methods to account for possible reactive
// values
;(['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 + '')
}
// we run the method using the original args first (which may be reactive)
const res = arr[key](...args)
if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
return arr[key](...args.map(toRaw))
} else {
return res
}
}
})
// instrument length-altering mutation methods to avoid length being tracked
// which leads to infinite loops in some cases (#2137)
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
instrumentations[key] = function (this: unknown[], ...args: unknown[]) {
pauseTracking()
pauseScheduling()
const res = (toRaw(this) as any)[key].apply(this, args)
resetScheduling()
resetTracking()
return res
}
})
return instrumentations
}
Vue 3.5 对数组方法重写的太长了, 这里就只贴出一些关键内容:
arrayInstrumentations.ts
typescript
// packages\reactivity\src\arrayInstrumentations.ts
export const arrayInstrumentations: Record<string | symbol, Function> = <any>{
// ...
concat(...args: unknown[]) {
return reactiveReadArray(this).concat(
...args.map(x => (isArray(x) ? reactiveReadArray(x) : x)),
)
},
// ...
join(separator?: string) {
return reactiveReadArray(this).join(separator)
},
lastIndexOf(...args: unknown[]) {
return searchProxy(this, 'lastIndexOf', args)
},
// ...
values() {
return iterator(this, 'values', toReactive)
},
// ...
}
// instrument identity-sensitive methods to account for reactive proxies
function searchProxy(
self: unknown[],
method: keyof Array<any>,
args: unknown[],
) {
const arr = toRaw(self) as any
track(arr, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
// we run the method using the original args first (which may be reactive)
const res = arr[method](...args)
// if that didn't work, run it again using raw values.
if ((res === -1 || res === false) && isProxy(args[0])) {
args[0] = toRaw(args[0])
return arr[method](...args)
}
return res
}
/**
* Track array iteration and return:
* - if input is reactive: a cloned raw array with reactive values
* - if input is non-reactive or shallowReactive: the original raw array
*/
function reactiveReadArray<T>(array: T[]): T[] {
const raw = toRaw(array)
if (raw === array) return raw
track(raw, TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
return isShallow(array) ? raw : raw.map(toReactive)
}
/**
* Track array iteration and return raw array
*/
function shallowReadArray<T>(arr: T[]): T[] {
track((arr = toRaw(arr)), TrackOpTypes.ITERATE, ARRAY_ITERATE_KEY)
return arr
}
// instrument iterators to take ARRAY_ITERATE dependency
function iterator(
self: unknown[],
method: keyof Array<unknown>,
wrapValue: (value: any) => unknown,
) {
const arr = shallowReadArray(self)
const iter = (arr[method] as any)() as IterableIterator<unknown> & {
_next: IterableIterator<unknown>['next']
}
if (arr !== self && !isShallow(self)) {
iter._next = iter.next
iter.next = () => {
const result = iter._next()
if (result.value) {
result.value = wrapValue(result.value)
}
return result
}
}
return iter
}
dep.ts
typescript
// packages/reactivity/src/dep.ts
// ...
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
// ...
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
depsMap.forEach(run)
} else {
const targetIsArray = isArray(target)
const isArrayIndex = targetIsArray && isIntegerKey(key)
if (targetIsArray && key === 'length') {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (
key === 'length' ||
key === ARRAY_ITERATE_KEY ||
(!isSymbol(key) && key >= newLength)
) {
run(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
// schedule ARRAY_ITERATE for any numeric key change (length is handled above)
if (isArrayIndex) {
run(depsMap.get(ARRAY_ITERATE_KEY))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isArrayIndex) {
// new index added to array -> length changes
run(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!targetIsArray) {
run(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
run(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
run(depsMap.get(ITERATE_KEY))
}
break
}
}
}
// ...
}
可以很容易看到是如何优化的.
之前是对 indexOf 这一类不会触发对整个数组的依赖收集, 从而需要对数组每一项进行循环收集.
而现在则是通过新的 ARRAY_ITERATE_KEY, 在修改触发依赖的时候, 直接触发这个 ARRAY_ITERATE_KEY 的副作用函数, 不再对数组每一项进行循环收集.
默认启用了 Props 的解构
这是一项编译性功能, 部分人持有不应该使用的态度, 因为容易造成误解行为. 比如:
propsDestructure.ts
typescript
const { data } = defineProps<{
data: unknown[]
}>();
// 这是错误的
watch(data, () => {})
// 这是正确的
watch(() => data, () => {})
可以看到令人有些迷惑的行为, 这种行为可能导致一定的阅读成本.
可以在编译结果看到原因:
新增了 onEffectCleanup API
这就相当于 React 的 useEffect 的 return 回调.
onEffectCleanup.ts
typescript
// Vue
effect(() => {
a.value; // 依赖项
const { promise, cancel } = fetchXXX()
onEffectCleanup(cancel)
promise.then(data => {
// ...
})
})
// React
useEffect(() => {
const { promise, cancel } = fetchXXX()
promise.then(data => {
// ...
})
return cancel
}, [xxx])
这个例子可以在当依赖性发生变化的时候, 取消上一次的请求, 代码结果将执行 promise.catch 的回调.
新增了 watch, getCurrentWatcher 和 onWatcherCleanup API
新增了 watch 听起来可能很疑惑, 但是此 watch 和 3.4 及以前的 watch 略有不同.
之前的 watch 有执行时机的需要, 比如 flush: 'post', 所以是包含在 runtime-core 的包里, 因为包含 flush 参数.
而本次更新的 watch 则是在 reactive 包里, 没有 flush 参数, 变化即触发.
而 getCurrentWatcher 则是获取当前执行的 watcher. 因为 runtime-core 的内部也使用了 reactive 的 watch, 所以两个 watch 都能够被获取到.
onWatcherCleanup 则和 onEffectCleanup 类似.
推荐文章:
watch, effect, effectScope 等能够控制暂停监听了.
3.5 的 watch, effect, effectScope 导出了 pause 和 resume 方法用来控制 watch 的监听, 在 pause 期间, 监听回调不会触发.
watch 支持 deep 深度监听的层级
这个就很好理解了, watch 有个 deep 参数, 决定是否深度监听对象的值, 而现在这个值可以是数字, 即监听的层级, 以达到性能优化的目的.
SSR 部分
SSR 相关知识
新增 useId
在个别时候, 需要随机生成 ID 的逻辑, 但是服务端随机生成后和客户端的值会对不上, 这会导致水合(hydration) 出现问题.
在 React 中, 有 useId 来确保随机值一致, 现在 Vue 也有了.
推荐文章:
对于异步组件自定义水合(hydration)
增加了对于异步组件水合(hydration)时机的控制支持.
详细看 pull:
水合时机除了固定给出的:
- 显示水合(hydration)
- 媒体查询水合(hydration)
- 空闲时水合(hydration)
- 触发事件时水合(hydration)
以外, 还可以自定义水合(hydration)时机的方法.
自定义元素
这部分不太熟, 请看各自的 issues 和 pull.
Teleport
支持通过设置 defer 属性延迟挂载
如果需要挂载在同一组件的元素上, 会因为在 Teleport 渲染时, 对应的元素尚未渲染而出现问题.
所以出现了 defer 属性, 保证挂载时机在元素已经被渲染了后.
defer.vue
typescript
file not exist
支持对 Teleport 的子元素应用 transition 标签.
transition 的范围增加啦. 详看: pull#6548
杂项
新增 useTemplateRef API
这是因为歧义性问题新增的 API.
在获取组件实例或 DOM 对象的时候, 需要使用 ref 绑定, 并且需要在 setup 也声明一个同名的 ref 变量.
两个 ref 并不相同, 这造成了歧义性.
其实现很简单:
useTemplateRef.ts
typescript
// packages/runtime-core/src/helpers/useTemplateRef.ts
export function useTemplateRef<T = unknown, Keys extends string = string>(
key: Keys,
): Readonly<ShallowRef<T | null>> {
const i = getCurrentInstance()
const r = shallowRef(null)
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
let desc: PropertyDescriptor | undefined
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: val => (r.value = val),
})
}
const ret = readonly(r)
return ret
}
新增 app.onUnmount API
用于处理当整个 app 卸载的时候, 需要执行的逻辑, 应用级别的卸载钩子.
新增 app.config.throwUnhandledErrorInProduction 配置项
强制在生产模式下抛出未处理的错误.
可信类型兼容
防止 XSS 的新的安全性内容.
支持在模板中使用 Symbol
Vue 在编译模板的时候会根据白名单来决定是否对模板中的变量添加前缀.
比如 Math 属于全局对象, 如果在模板中写 Math.abs(xxx) 的时候, Vue 不会再 Math 前添加 _ctx 或者 setupContext 的前缀.
该前缀是编译时添加, 通过分析 script 的内容, 得到导出变量的来源, 并赋予模板中的变量.
相关代码:
globalsAllowList.ts
typescript
// packages/shared/src/globalsAllowList.ts
// 3.5
const GLOBALS_ALLOWED =
'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error,Symbol'
// -----------------------------------------------------------------------------------------
// 3.4
const GLOBALS_ALLOWED =
'Infinity,undefined,NaN,isFinite,isNaN,parseFloat,parseInt,decodeURI,' +
'decodeURIComponent,encodeURI,encodeURIComponent,Math,Number,Date,Array,' +
'Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,console,Error'
export const isGloballyAllowed = /*#__PURE__*/ makeMap(GLOBALS_ALLOWED)
/** @deprecated use `isGloballyAllowed` instead */
export const isGloballyWhitelisted = isGloballyAllowed
在最后部分添加了 Symbol.
内部内容
CustomRefs 缓存 value
字面上的意思, 在 3.4 及之前, 自定义 ref 每次 xxx.value 都会执行 get, 更新后将缓存结果.
总结
Vue 3.5 相比 Vue 3.4 增加了像是 onWatcherCleanup, useTemplateRef 这样业务上用得到的 API, 同时也增加了一些对组件库来说比较常用的东西, 比如 Teleport 的 defer, 对插件来说可能用得到的 app.onUnmount.
还有对于 SSR 来说比较有用的 useId 和 异步组件的自定义水合(hydration).
同时, 响应式的优化让内存效率和性能也有了较多的提升.
由于刚出来不久, 所以现在 bug 还是比较多的, 截至到 2024/11/05 两个月已经优化和修复了 12 个版本的 bug. 相比 3.4 接近一年的时间还是有比较长的时间才能进入生成环境吧.