“Yeah It’s on. ”
前序
vue api 查询 https://vuejs.org/api/
Reflect
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
首先我们要了解一下,为什么会新添加这么一个全局对象?如果你看过Reflect的一些函数,你就会发现,这个对象上的方法基本上都可以从Object上面找到,找不到的那些,也是可以通过对对象命令式的操作去实现的;那么为什么还要新添加一个呢?
-
Reflect上面的一些方法并不是专门为对象设计的,比如Reflect.apply方法,它的参数是一个函数,如果使用Object.apply(func)会让人感觉很奇怪。
-
用一个单一的全局对象去存储这些方法,能够保持其它的JavaScript代码的整洁、干净。不然的话,这些方法可能是全局的,或者要通过原型来调用。
-
将一些命令式的操作如delete,in等使用函数来替代,这样做的目的是为了让代码更加好维护,更容易向下兼容;也避免出现更多的保留字。
Reflect.apply
Reflect.construct
Reflect.defineProperty
Reflect.deleteProperty
Reflect.enumerate // 废弃的
Reflect.get
Reflect.getOwnPropertyDescriptor
Reflect.getPrototypeOf
Reflect.has
Reflect.isExtensible
Reflect.ownKeys
Reflect.preventExtensions
Reflect.set
Reflect.setPrototypeOf
Proxy基础
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
正文
而 Vue3 所使用的 Proxy,则是这样拦截的:
new Proxy(data, {
get(key) { },
set(key, value) { },
})
可以看到,根本不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。
所以,不管是已有的 key 还是新增的 key,都逃不过它的魔爪。
但是 Proxy 更加强大的地方还在于 Proxy 除了 get 和 set,还可以拦截更多的操作符。
先最小化的讲解一下响应式的原理,其实就是在 Proxy 第二个参数 handler
也就是陷阱操作符中,拦截各种取值、赋值操作,依托 track
和 trigger
两个函数进行依赖收集和派发更新。
track
用来在读取时收集依赖。
trigger
用来在更新时触发依赖。
track
function track(target: object, type: TrackOpTypes, key: unknown) {
const depsMap = targetMap.get(target);
// 收集依赖时 通过 key 建立一个 set
let dep = new Set()
targetMap.set(ITERATE_KEY, dep)
// 这个 effect 可以先理解为更新函数 存放在 dep 里
dep.add(effect)
}
target
是原对象。
type
是本次收集的类型,也就是收集依赖的时候用来标识是什么类型的操作,比如上文依赖中的类型就是 get
,这个后续会详细讲解。
key` 是指本次访问的是数据中的哪个 key,比如上文例子中收集依赖的 key 就是 `count
首先全局会存在一个 targetMap
,它用来建立 数据 -> 依赖
的映射,它是一个 WeakMap 数据结构。
而 targetMap
通过数据 target
,可以获取到 depsMap
,它用来存放这个数据对应的所有响应式依赖。
depsMap
的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数。
是不是有点绕?我们用一个具体的例子来举例吧。
const target = { count: 1}
const data = reactive(target)
const effection = effect(() => {
console.log(data.count)
})
对于这个例子的依赖关系,
- 全局的
targetMap
是:
targetMap: {
{ count: 1 }: dep
}
- dep 则是
dep: {
count: Set { effection }
}
这样一层层的下去,就可以通过 target
找到 count
对应的更新函数 effection
了。
trigger
这里是最小化的实现,仅仅为了便于理解原理,实际上要复杂很多,
其实 type
的作用很关键,先记住,后面会详细讲。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
) {
// 简化来说 就是通过 key 找到所有更新函数 依次执行
const dep = targetMap.get(target)
dep.get(key).forEach(effect => effect())
}
应用程序初始化
// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')
const createApp = ((...args) => {
// 创建 app 对象
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写 mount 方法
app.mount = (containerOrSelector) => {
// ...
}
return app
})
diff
https://mp.weixin.qq.com/s/jW3-icYQaJVITCYi730iIw
// packages/runtime-core/src/renderer.ts
function getSequence(arr: number[]): number[] {
const p = arr.slice() // 拷贝一个数组 p
const result = [0]
let i, j, u, v, c
const len = arr.length
for (i = 0; i < len; i++) {
const arrI = arr[i]
// 排除等于 0 的情况
if (arrI !== 0) {
j = result[result.length - 1]
// 与最后一项进行比较
if (arr[j] < arrI) {
p[i] = j // 最后一项与 p 对应的索引进行对应
result.push(i)
continue
}
// arrI 比 arr[j] 小,使用二分查找找到后替换它
// 定义二分查找区间
u = 0
v = result.length - 1
// 开启二分查找
while (u < v) {
// 取整得到当前位置
c = ((u + v) / 2) | 0
if (arr[result[c]] < arrI) {
u = c + 1
} else {
v = c
}
}
// 比较 => 替换
if (arrI < arr[result[u]]) {
if (u > 0) {
p[i] = result[u - 1] // 正确的结果
}
result[u] = i // 有可能替换会导致结果不正确,需要一个新数组 p 记录正确的结果
}
}
}
u = result.length
v = result[u - 1]
// 倒叙回溯 用 p 覆盖 result 进而找到最终正确的索引
while (u-- > 0) {
result[u] = v
v = p[v]
}
return result
}
贪心 + 二分查找法
getSequence
的作用就是找到那些不需要移动的元素,在遍历的过程中,我们可以直接跳过不进行其他操作。
Vue3 通过拷贝一个数组,用来存储正确的结果,然后通过回溯赋值的方式解决了贪心 + 二分查找替换方式可能造成的值不正确的问题。
@vue/reactivity
响应式的功能被封装在 @vue/reactivity
模块中,该模块为我们提供了一个 reactive
函数来创建响应式对象。
// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers,
mutableCollectionHandlers
)
}
在 reactive
函数内部,会继续调用 createReactiveObject
函数来创建响应式对象,该函数也是被定义在 reactive.ts
文件中,该函数的的具体实现如下:
// packages/reactivity/src/reactive.ts
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 省略部分代码
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
在 createReactiveObject
函数内部,我们终于见到了期待已久的 Proxy
对象。当 target
对象不是集合类型的对象,比如 Map、Set、WeakMap 和 WeakSet 时,在创建 Proxy 对象时,使用的是 baseHandlers
,该 handler
对象定义了以下 5 种捕获器:
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
其中 get
和 set
捕获器是分别用于收集 effect 函数和触发 effect 函数的执行。
Vue 3 的 $emit
与 Vue 2 的 $emit
https://juejin.cn/post/6938176673292484615?utm_source=gold_browser_extension
在 Vue 2 中 $emit
方法是 Vue.prototype
对象上的属性,而 Vue 3 上的 $emit
是组件实例上的一个属性,instance.emit = emit.bind(null, instance)
。
watch vs watchEffect
https://www.thisdot.co/blog/vue-3-composition-api-watch-and-watcheffect
ref vs reactive
reactive()
only takes objects, NOT JS primitives (String, Boolean, Number, BigInt, Symbol, null, undefined)ref()
is callingreactive()
behind the scenes- Since
reactive()
works for objects andref()
callsreactive()
, objects work for both - BUT,
ref()
has a.value
property for reassigning,reactive()
does not have this and therefore CANNOT be reassigned
优化
Block Tree 和 PatchFlags
https://zhuanlan.zhihu.com/p/150732926
vue3.0它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。
Block 配合 PatchFlags 做到靶向更新
v-for 的元素作为 Block
不仅 v-if
会让 DOM
结构不稳定,v-for
也会,但是 v-for
的情况稍微复杂一些。思考如下模板:
<div>
<p v-for="item in list"></p>
<i></i>
<i></i>
</div>
假设 list 值由 [1 ,2]
变为 [1]
,按照之前的思路,最外层的 <div>
标签作为一个 Block
,那么它更新前后对应的 Block Tree
应该是:
// 前
const prevBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: 1, 1 /* TEXT */ },
{ tag: 'p', children: 2, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
// 后
const nextBlock = {
tag: 'div',
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
prevBlcok
中有四个动态节点,nextBlock
中有三个动态节点。这时候要如何进行 Diff
?有的同学可能会说拿 dynamicChildren
进行传统 Diff
,这是不对的,因为传统 Diff
的一个前置条件是同层级节点间的 Diff
,但是 dynamicChildren
内的节点未必是同层级的,这一点我们之前就提到过。
实际上我们只需要让 v-for
的元素也作为一个 Block
就可以了。这样无论 v-for
怎么变化,它始终都是一个 Block
,这保证了结构稳定,无论 v-for
怎么变化,这颗 Block Tree
看上去都是:
const block = {
tag: 'div',
dynamicChildren: [
// 这是一个 Block 哦,它有 dynamicChildren
{ tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }
{ tag: 'i', children: ctx.foo, 1 /* TEXT */ },
{ tag: 'i', children: ctx.bar, 1 /* TEXT */ },
]
}
不稳定的 Fragment
刚刚我们使用一个 Fragment
并让它充当 Block
的角色解决了 v-for
元素所在层级的结构稳定,但我们来看一下这个 Fragment
本身:
{ tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }
对于如下这样的模板:
<p v-for="item in list"></p>
在 list 由 [1, 2]
变成 [1]
的前后,Fragment
这个 Block
看上去应该是:
// 前
const prevBlock = {
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ },
{ tag: 'p', children: item, 2 /* TEXT */ }
]
}
// 后
const prevBlock = {
tag: Fragment,
dynamicChildren: [
{ tag: 'p', children: item, 1 /* TEXT */ }
]
}
我们发现,Fragment
这个 Block
仍然面临结构不稳定的情况,所谓结构不稳定从结果上看指的是更新前后一个 block
的 dynamicChildren
中收集的动态节点数量或顺序的不一致。这种不一致会导致我们没有办法直接进行靶向 Diff
,怎么办呢?其实对于这种情况是没有办法的,我们只能抛弃 dynamicChildren
的 Diff
,并回退到传统 Diff
:即 Diff
Fragment
的 children
而非 dynamicChildren
。
但需要注意的是 Fragment
的子节点(children
)仍然可以是 Block
:
const block = {
tag: Fragment,
children: [
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
{ tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
]
}
这样,对于 <p>
标签及其子代节点的 Diff
将恢复 Block Tree
的 Diff
模式。
静态提升
Vue3
的 Compiler
如果开启了 hoistStatic
选项则会提升静态节点,或静态的属性,这可以减少创建 VNode
的消耗,如下模板所示:
<div>
<p>text</p>
</div>
在没有被提升的情况下其渲染函数相当于:
function render() {
return (openBlock(), createBlock('div', null, [
createVNode('p', null, 'text')
]))
}
很明显,p
标签是静态的,它不会改变。但是如上渲染函数的问题也很明显,如果组件内存在动态的内容,当渲染函数重新执行时,即使 p
标签是静态的,那么它对应的 VNode
也会重新创建。当开启静态提升后,其渲染函数如下:
const hoist1 = createVNode('p', null, 'text')
function render() {
return (openBlock(), createBlock('div', null, [
hoist1
]))
}
使用自定义指令的元素
实际上一个元素如果使用除 v-pre/v-cloak
之外的所有 Vue
原生提供的指令,都不会被提升,使用自定义指令也不会被提升,例如:
<p v-custom></p>
和使用 key
一样,会为这段模板对应的 VNode
打上 NEED_PATCH
标志。顺便讲一下手写渲染函数时如何应用自定义指令,自定义指令是一种运行时指令,与组件的生命周期类似,一个 VNode
对象也有它自己生命周期
手写高性能渲染函数
几个需要记住的小点:
- 一个
Block
就是一个特殊的VNode
,可以理解为它只是比普通VNode
多了一个dynamicChildren
属性 createBlock()
函数和createVNode()
函数的调用签名几乎相同,实际上createBlock()
函数内部就是封装了createVNode()
,这再次证明Block
就是VNode
。- 在调用
createBlock()
创建Block
前要先调用openBlock()
函数,通常这两个函数配合逗号运算符一同出现:
render() {
return (openBlock(), createBlock('div'))
}
Slot hint
我们在“稳定的 Fragment”一节中提到了 slot hint
,当我们为组件编写插槽内容时,为了告诉 runtime:“我们已经能够保证插槽内容的结构稳定”,则需要使用 slot hint
:
render() {
return (openBlock(), createBlock(Comp, null, {
default: () => [
refVal.value
? (openBlock(), createBlock('p', ...))
? (openBlock(), createBlock('div', ...))
],
// slot hint
_: 1
}))
}
当然如果你不能保证这一点,或者觉得心智负担大,那么就不要写 hint
了。
EffectScope
副作用生效的作用域
// effect, computed, watch, watchEffect created inside the scope will be collected
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// to dispose all effects in the scope
scope.stop()
补充
vue3 放弃了 Object.defineProperty
Object.defineProperty存在的问题
- 不能监听数组索引和长度的变更
- 无法监听 属性的添加和删除
- 必须遍历对象的每个属性, 必须深层遍历嵌套的对象。
Object.defineProperty
是对对象属性的操作,所以需要对对象进行深度遍历去对属性进行操作。
为什么我们在使用vue2的时候,一样可以监听数组索引和长度的变更?
我们只是说Object.defineProperty不能,并没说vue2.0不能。并不能说因为vue2.0使Object.defineProperty,vue2.0就不能监听数组内部属性的变化了。而vue2之所以能监听,是vue2.0对数组相关的方法或其他进行了重写。当然vue2.0中还是存在无法监听直接修改数组中某一项值和数组长度,如ar[0]=1, arr.length=12是无法监听的
vue-cli-plugin-commitlint
vue-cli-plugin-commitlint
是以 vue
插件的形式写的,可以执行 vue add commitlint
直接使用,如果不是 vue
的项目也可以根据下面的配置自行配置。
- 自动检测
commit
是否规范,不规范不允许提交 - 自动提示
commit
填写格式。不怕忘记规范怎么写 - 集成
git add && git commit
不需要执行两个命令 - 自动生成
changelog
Ref vs Reactive
https://vuejsdevelopers.com/2022/06/01/ref-vs-reactive/
Unlike ref
, reactive
can only be initialized with an object. Each property of the object can be a different reactive variable, however.
const data = reactive({
count: 0,
name: 'Peter Griffin',
flag: false
})
One advantage of reactive
is that it doesn’t use a value
property so it may be a little easier to read.
data.name === 'Peter Griffin' // true
data.name.value === 'Peter Griffin' // false
ref
advantages:
- Much easier to pass single variables around your app
- Avoids destructuring pitfall
reactive
advantages:
- Can be less verbose if declaring lots of reactive variables
- Consistency between JavaScript and template
- Similar to Vue 2’s data object
I normally use ref
as I find it more flexible.
vue-tsc
https://www.npmjs.com/package/vue-tsc
Usage: vue-tsc --noEmit && vite build
Vue 3 command line Type-Checking tool base on IDE plugin Volar.
用来做Type检查的