写在前面
Vue3发布已经有段时间了,相比Vue2做了特别多的优化。但具体好在哪里呢,除了开发者用得爽之外,框架底层的优化需要我们通过研究源码才能有切身体会。
本文主要是通过源码层面来对比 Vue3 和 Vue2,思考与总结新的 Vue3 做了哪些优化,这些优化好在哪里。
注:文章中有些标题是带下划线的蓝色方法名,这些方法都对应设置了超链接,点击即可跳转到源码中对应文件的位置
1. 初始化
Vue3 相对 Vue2 是一次重构,改用多模块架构,分为 compiler、reactivity、runtime三大模块
- compiler-core
- compiler-dom
- runtime-core
- runtime-dom
- reactivity
将来在做 自定义渲染 时只需要基于 compiler 和 runtime 两个 core 模块进行扩展即可
于是就引申出了一个概念:renderer,意为渲染器,是应用的入口,来自 runtime
模块
在runtime
模块下我们可以扩展任意平台的渲染规则,目前我们要研究的web 平台入口是runtime-dom
,
于是初始化的方式也发生了改变,通过Vue实例上的createApp
方法创建页面应用,所以初始化阶段我们从createApp
入手
1.1. Vue 的初始化
1.1.1. createApp
-
作用
- 获取渲染器(
renderer
),通过渲染器的createApp
创建app 对象 - 扩展
mount
方法
- 获取渲染器(
-
核心源码
const createApp = ((...args) => { const app = ensureRenderer().createApp(...args); const { mount } = app; app.mount = (containerOrSelector: Element | ShadowRoot | string): any => { const container = normalizeContainer(containerOrSelector); if (!container) return; // 挂载前先清空dom的内容 container.innerHTML = ''; // 调用原本的mount方法进行挂载 const proxy = mount(container); return proxy; }; return app; }) as CreateAppFunction<Element>; function ensureRenderer() { // 返回单例renderer return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)); }
目前的
createApp
基于runtime-dom
,目的是通过ensureRenderer
创建一个基于web 平台的渲染器,之所以加上一个“目前的”,是因为还会有runtime-core
的createApp
,后面会具体整理。ps:
rendererOptions
是web 平台特有的操作dom和属性的方法。
1.1.2. createRenderer
-
作用
- 通过参数
options
创建平台的客户端渲染器
- 通过参数
-
核心源码
function createRenderer<HostNode = RendererNode, HostElement = RendererElement>( options: RendererOptions<HostNode, HostElement> ) { return baseCreateRenderer<HostNode, HostElement>(options); }
这里再调用一次
baseCreateRenderer
是为了创建客户端渲染器,可以在当前文件下面看到还有另外一个方法createHydrationRenderer
,他也调用了baseCreateRenderer
,这是创建服务端渲染器
1.1.3. baseCreateRenderer
-
作用
- 根据平台操作参数返回真正的平台渲染器
-
核心源码
function baseCreateRenderer( options: RendererOptions, createHydrationFns?: typeof createHydrationFunctions ): any { // 取出平台特有的方法,这里太占空间,暂时不贴了,主要是insert、remove、patchProp等等 const {...} = options // 接下来是很多组件渲染和diff的方法 const patch = (n1, n2, container, ...) => {...} const processElement = (n1, n2, container) => {...} const mountElement = (vnode, container, ...) => {...} const mountChildren = (children, container, ...) => {...} ... const render: RootRenderFunction = (vnode, container) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true); } } else { // 初始化和更新都走这里,类似vue2的__patch__ patch(container._vnode || null, vnode, container); } container._vnode = vnode; }; return { render, hydrate, createApp: createAppAPI(render, hydrate), }; }
渲染器包括
render
、hydrate
、createApp
三个方法,这一步非常重要,或许将来基于Vue3的跨平台开发都会类似这种形式。
通过参数
options
解构出基于平台的操作 dom 与 属性 的方法,用来创建真正的渲染和更新函数。其中需要关注的是patch
,因为他不止负责渲染、更新,将来的初始化组件也通过这个入口进入 ⭐。其中
render
方法类似于vue2的vm._update
,负责初始化和更新由于
baseCreateRenderer
是一个长达 1800 多行的方法,初始化时只关注最终返回的渲染器即可,最后的
createApp
由工厂函数createAppAPI
创建
1.1.4. createAppAPI
-
作用
- 通过参数
render
和hydrate
创建平台的createApp
方法,createApp
用于创建真正的app(Vue) 实例
- 通过参数
-
核心源码
function createAppAPI<HostElement>( render: RootRenderFunction, hydrate?: RootHydrateFunction ): CreateAppFunction<HostElement> { return function createApp(rootComponent, rootProps = null) { const app = { use(plugin: Plugin, ...options: any[]) { plugin.install(app, ...options); return app; }, mixin(mixin: ComponentOptions) { context.mixins.push(mixin); return app; }, component(name: string, component?: Component): any { context.components[name] = component; return app; }, directive(name: string, directive?: Directive) { context.directives[name] = directive; return app; }, mount(rootContainer: HostElement, isHydrate?: boolean): any { ... }, unmount() { if (isMounted) { render(null, app._container); } }, provide(key, value) { context.provides[key as string] = value; return app; }, }; return app; }; }
还记得吗,刚才有提醒,有两个
createApp
方法- 在
runtime-core
模块中:创建真正的app(Vue)实例 - 在
runtime-dom
模块中:通过runtime-core
模块创建web 平台的渲染器,利用渲染器拿到实例
createApp
内部定义了许多实例上的方法,use
、mixin
、component
、directive
、mount
、unmount
、provide
。 熟悉Vue2 的小伙伴可能会发现了,原来的静态方法现在都变成了实例方法,而且几乎每个方法返回app对象,因此可以链式调用,like thisconst app = createApp({ setup() { const state = reactive({ count: 0, }); return { state }; }, }) .use(store) .directive(transfer) .mixin(cacheStore) .mount('#app');
这一步结束后渲染器的
createApp
方法也有了,ensureRenderer
将整个渲染器返回给runtime-dom
模块。然后通过渲染器创建app 实例,对实例的mount
方法进行扩展,接下来进入实例的渲染阶段。mount
是渲染阶段的入口
核心源码如下:mount(rootContainer: HostElement, isHydrate?: boolean): any { if (!isMounted) { // 初始化虚拟dom树 const vnode = createVNode(rootComponent as ConcreteComponent, rootProps); if (isHydrate && hydrate) { // 服务端渲染 hydrate(vnode as VNode<Node, Element>, rootContainer as any); } else { // 客户端渲染 render(vnode, rootContainer); } return vnode.component!.proxy; } }
app 实例未经扩展的
mount
方法相当于Vue2的updateComponent
,做了两件事情:-
获取虚拟 dom,
-
通过
render
方法将虚拟 dom转成真实 dom-
核心源码
const render: RootRenderFunction = (vnode, container) => { if (vnode == null) { if (container._vnode) { unmount(container._vnode, null, null, true); } } else { patch(container._vnode || null, vnode, container); } flushPostFlushCbs(); container._vnode = vnode; };
render
方法特别像Vue2的vm._update
,是初始化渲染和组件更新的入口,均调用patch
方法,
由于首次渲染不存在旧的虚拟 dom,因此n1
是null
-
- 在
1.1.5. patch
-
作用
- 根据虚拟 dom的类型 进行 组件的初始化与更新,最终将虚拟 dom转成真实 dom,(ps:组件包括浏览器 host 组件和自定义组件,下文相同)
-
核心源码
const patch: PatchFn = ( n1,// 旧的虚拟dom n2,// 新的虚拟dom container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // 新节点的类型 const { type, ref, shapeFlag } = n2 switch (type) { case Text: ... break case Comment: ... break case Static: ... break case Fragment: ... break default: if (shapeFlag & ShapeFlags.ELEMENT) { ... } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } } }
之所以说组件的初始化与更新,是因为Vue3的
patch
不同于Vue2的__patch__
,__patch__
只负责渲染,因此我们可以说是组件的渲染,但Vue3的patch
在渲染阶段最终触发的函数不仅包括组件的渲染,期间还包括组件初始化阶段由于初始化时传入的新的虚拟 dom(
n2
)是开发者调用createApp
的参数,所以被判定是一个对象类型,所以会作为自定义组件处理,因此接下来执行processComponent
方法核心源码如下:
const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { ... } else { // 初始化渲染 mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized); } } else { // 组件更新 updateComponent(n1, n2, optimized); } };
由于首次渲染传进来的旧的虚拟 dom是
null
,所以执行mountComponent
方法
1.1.6. mountComponent
-
作用
- 初始化自定义组件
- 创建组件实例
- 安装组件(即组件初始化)
- 安装副作用:完成渲染,定义更新函数
- 初始化自定义组件
-
核心源码
const mountComponent: MountComponentFn = ( initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) => { // 1. 创建组件实例 const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance( initialVNode, parentComponent, parentSuspense )); // inject renderer internals for keepAlive if (isKeepAlive(initialVNode)) { (instance.ctx as KeepAliveContext).renderer = internals; } // 2. 安装组件(即组件初始化) setupComponent(instance); // setup() is async. This component relies on async logic to be resolved // before proceeding if (__FEATURE_SUSPENSE__ && instance.asyncDep) { parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect); // Give it a placeholder if this is not hydration // TODO handle self-defined fallback if (!initialVNode.el) { const placeholder = (instance.subTree = createVNode(Comment)); processCommentNode(null, placeholder, container!, anchor); } return; } // 3. 安装副作用:完成渲染,定义更新函数 setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized); };
组件的初始化包括:
createComponentInstance
: 创建组件实例setupComponent
:安装组件(组件初始化)。(类似于Vue2初始化执行的vm._init
方法)setupRenderEffect
:安装渲染函数的副作用(effect),完成组件渲染,并定义组件的更新函数。(effect替代了Vue2的Watcher)
1.2. 组件初始化
1.2.1. setupComponent
-
作用
- 安装组件(组件初始化)
mergeOptions
、- 定义实例的属性、事件、处理插槽,
- 通过
setupStatefulComponent
方法完成数据响应式
- 安装组件(组件初始化)
-
核心源码
function setupComponent(instance: ComponentInternalInstance, isSSR = false) { isInSSRComponentSetup = isSSR; const { props, children, shapeFlag } = instance.vnode; const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT; // 初始化props initProps(instance, props, isStateful, isSSR); // 初始化插槽 initSlots(instance, children); // 数据响应式 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined; isInSSRComponentSetup = false; return setupResult; }
其中
setupStatefulComponent
负责数据响应式
(1) setupStatefulComponent
-
作用
- 完成数据响应式
-
核心源码
function setupStatefulComponent(instance: ComponentInternalInstance, isSSR: boolean) { // createApp的配置对象 const Component = instance.type as ComponentOptions; // 0. create render proxy property access cache instance.accessCache = Object.create(null); // 1. render函数的上下文 instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers); // 2. 处理setup函数 const { setup } = Component; if (setup) { const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null); currentInstance = instance; pauseTracking(); const setupResult = callWithErrorHandling(setup, instance, ErrorCodes.SETUP_FUNCTION, [ __DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext, ]); resetTracking(); currentInstance = null; if (isPromise(setupResult)) { if (isSSR) { ... } else if (__FEATURE_SUSPENSE__) { // async setup returned Promise. // bail here and wait for re-entry. instance.asyncDep = setupResult; } } else { // 最终也会执行finishComponentSetup handleSetupResult(instance, setupResult, isSSR); } } else { finishComponentSetup(instance, isSSR); } }
没有设置
setup
会执行handleSetupResult
,最终依然会调用finishComponentSetup
方法
(2) finishComponentSetup
-
作用
- 确保
instance
上有render 函数 - 兼容 Vue2 options API 方式的数据响应式功能
- 确保
-
核心源码
function finishComponentSetup(instance: ComponentInternalInstance, isSSR: boolean) { const Component = instance.type as ComponentOptions; // template / render function normalization if (__NODE_JS__ && isSSR) { ... } else if (!instance.render) { // could be set from setup() if (compile && Component.template && !Component.render) { Component.render = compile(Component.template, { isCustomElement: instance.appContext.config.isCustomElement, delimiters: Component.delimiters, }); } instance.render = (Component.render || NOOP) as InternalRenderFunction; // for runtime-compiled render functions using `with` blocks, the render // proxy used needs a different `has` handler which is more performant and // also only allows a whitelist of globals to fallthrough. if (instance.render._rc) { instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers); } } // applyOptions兼容Vue2的options API if (__FEATURE_OPTIONS_API__) { currentInstance = instance; pauseTracking(); applyOptions(instance, Component); resetTracking(); currentInstance = null; } }
1.2.2. setupRenderEffect
-
作用
- 安装渲染函数的副作用
- 完成组件渲染
-
核心源码
const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized ) => { // create reactive effect for rendering instance.update = effect( function componentEffect() { if (!instance.isMounted) { let vnodeHook: VNodeHook | null | undefined; const { el, props } = initialVNode; const { bm, m, parent } = instance; // 1.首先获取当前组件的虚拟dom const subTree = (instance.subTree = renderComponentRoot(instance)); if (el && hydrateNode) { ... } else { // 初始化:递归执行patch patch(...) } } else { // updateComponent patch(...) } }, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions ); };
我对副作用的理解:如果定义了一个响应式数据,和他相关的副作用函数在数据发生变化时会重新执行
函数内部通过
instance.isMounted
判断是初始化渲染或者是组件更新于是我们发现,effect替代了Vue2的Watcher
1.3. 流程梳理
-
Vue3的入口是
createApp
方法,createApp
通过ensureRender
获取renderer
对象,调用renderer.createApp
方法返回app 对象,然后扩展$mount
方法 -
ensureRender
保证renderer
是一个单例,通过createRenderer
调用baseCreateRenderer
完成创建 -
baseCreateRenderer
是真正创建renderer
的方法,renderer
包括render
、hydrate
和createApp
,其中createApp
方法通过调用createAppAPI
创建 -
createAppAPI
是一个工厂函数,返回一个真正的createApp
方法,createApp
内部创建了**Vue(app)**的实例方法并返回 -
如果开发者调用了
mount
方法,将继续执行mount
方法,从render
到patch
,最终执行processComponent
,在这里完成数据响应式、真实 dom的挂载,至此,初始化阶段结束
1.4. 思考与总结
-
渲染器(
renderer
)是一个对象,包含三部分- render
- hydrate
- createApp
-
全局方法为何调整到实例上?
- 避免实例之间污染
- tree-shaking
- 语义化
-
初始化阶段相比较Vue2的变化
- 新增渲染器的概念,一切方法都由渲染器提供
- Vue2通过创建对象的方式创建应用;而Vue3取消了对象的概念,改用方法返回实例,实例的方法可以链式调用
- 根组件是自定义组件
- 自定义组件完成组件实例的创建、初始化、安装渲染/更新函数三件事情
2. 数据响应式与 effect
在Vue2中,数据响应式存在以下几个小缺陷:
- 对于动态添加或删除的 key 需要额外的**api(Vue.set/Vue.delete)**解决
- 数组响应式需要单独一套逻辑处理
- 初始化时深层递归,效率相对低一些
- 无法监听新的数据结构
Map
、Set
Vue3 通过重构 响应式原理解决了上述问题,不仅如此,速度更是提升一倍,内存占用减少1/2,那么一起来探究竟吧~
重构内容大致如下:
-
用
proxy
代替Object.defineProperty
-
数据懒观察
-
优化原本的发布订阅模型,去除
Observer
、Watcher
、Dep
,改用简洁的reactive
、effect
、targetMap
track
: 用于追踪依赖trigger
: 用于触发依赖targetMap
: 相当于发布订阅中心,以树结构管理对象、key 与依赖之间的关系
2.1. 从源码探究过程
定义Vue3响应式数据的核心方法是reactive
和ref
。
由于Vue3依然兼容Vue2,所以原本的options API可以继续使用,经过debug调试后发现最终在resolveData
方法中执行了reactive
;还有我在调试ref
时发现也是借助了reactive
,所以可以认为reactive
是Vue3 数据响应式的入口。
在研究reactive
之前,我们先看一下响应式数据的几种枚举类型
2.1.1. ReactiveFlag
export const enum ReactiveFlags {
SKIP = '__v_skip' /* */, // 表示不需要被代理
IS_REACTIVE = '__v_isReactive', // 响应式对象的标志,类似Vue2的__ob__
IS_READONLY = '__v_isReadonly', // 只读对象的标志,不允许被修改
RAW = '__v_raw' /* */, // 原始类型
}
特别需要注意的是IS_REACTIVE
和RAW
2.1.2. reactive
- 作用
- 创建响应式对象
- 核心源码
除了只读对象,其他都允许执行响应式处理function reactive(target: object) { // if trying to observe a readonly proxy, return the readonly version. if (target && (target as Target)[ReactiveFlags.IS_READONLY]) { return target; } return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers); }
2.1.3. createReactiveObject
-
作用
- 不重复地创建响应式对象
-
核心源码
function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // 1. 如果是 只读属性或者代理,则直接返回 if (target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE])) { return target; } // 2. 如果已经是响应式对象了,则从 缓存 中直接返回 const proxyMap = isReadonly ? readonlyMap : reactiveMap; const existingProxy = proxyMap.get(target); if (existingProxy) { return existingProxy; } // 3. 创建响应式对象 const proxy = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers ); // 4. 存入 缓存 proxyMap.set(target, proxy); return proxy; }
类型不是
Object
、Array
、Map
、Set
、Weakmap
、Weakset
均不作操作。并且根据
Set、Map
和 普通对象 区分使用handler
如果是 普通对象(包含数组) 则使用
baseHandlers
,也就是mutableHandlers
2.1.4. mutableHandlers
- 作用
- 定义响应式的拦截方法
getter
触发依赖收集和定义子元素的响应式setter
触发依赖更新
- 定义响应式的拦截方法
- 核心源码
const get = function get(target: Target, key: string | symbol, receiver: object) {
// 已经是响应式对象、只读等边缘情况的处理
...
// 1. 数组
const targetIsArray = isArray(target);
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver);
}
// 2. 对象
const res = Reflect.get(target, key, receiver);
// 3. 依赖追踪
track(target, TrackOpTypes.GET, key);
// 4. 如果是对象,则继续观察
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 reactive(res);
}
// 5. 返回
return res;
};
const set = function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
const oldValue = (target as any)[key];
// 边缘情况的判断
...
const hadKey =
isArray(target) && isIntegerKey(key) ? Number(key) < target.length : hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver);
// 如果目标是原型链中的内容则不要触发依赖更新
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;
};
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key);
const oldValue = (target as any)[key];
const result = Reflect.deleteProperty(target, key);
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue);
}
return result;
}
// in 操作符的捕捉器。
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key);
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key);
}
return result;
}
// Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
function ownKeys(target: object): (string | number | symbol)[] {
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY);
return Reflect.ownKeys(target);
}
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys,
};
getter
- 触发
track
实现依赖收集, - 向下继续懒观察
- 触发
setter
- 触发
trigger
负责触发依赖。
- 触发
在proxy中有三种方法可以拦截getter
,分别是get
、has
和ownKeys
,有两种方法可以拦截setter
,分别是set
和deleteProperty
。
与Vue2不同的是,Vue3的数据侦听改用懒执行方式,即只有调用了getter
方法才会继续向下侦听,有效减少了首次执行的时间。
2.1.5. targetMap
-
定义
const targetMap = new WeakMap<any, KeyToDepMap>();
-
作用
发布订阅中心,是一个Map 结构,以树结构管理 各个对象、对象的 key、key 对应的 effect的关系大概是这样
type targetMap = { [key: Object]: { [key: string]: Set<ReactiveEffect>; }; };
2.1.6. activeEffect
-
定义
let activeEffect: ReactiveEffect | undefined;
-
作用
是一个全局变量,用于临时保存正在执行的副作用函数,本质上是一个副作用函数。有点像Vue2的
Dep.target
2.1.7. track
-
作用
- 收集依赖
-
核心源码
export function track(target: object, type: TrackOpTypes, key: unknown) { // 源码中有暂停收集和继续收集的两个方法,这里是没有暂停的标志 // 全局变量activeEffect是在instance.update或用户手动设置的副作用函数 if (!shouldTrack || activeEffect === undefined) { return; } // 从发布订阅中心取出对象所有的key let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } // 去除对象key的所有依赖effect let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); activeEffect.deps.push(dep); } }
收集依赖是同样是双向操作,
targetMap收集副作用函数,副作用函数也需要引用当前
key
依赖的所有副作用函数,用于将来重新收集依赖使用。为什么重新收集依赖在下文会详细说明
2.1.8. trigger
-
作用
- 触发依赖
-
核心源码
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown ) { const depsMap = targetMap.get(target); if (!depsMap) { // never been tracked return; } const effects = new Set<ReactiveEffect>(); // 添加副作用函数 const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { if (effect !== activeEffect || effect.allowRecurse) { effects.add(effect); } }); } }; // 根据type和key找到需要处理的depsMap,这里处理边缘情况,最终执行ADD、DELETE、SET将depsMap中的内容添加到effects中 ... // 首次渲染和异步更新 const run = (effect: ReactiveEffect) => { if (effect.options.scheduler) { effect.options.scheduler(effect); } else { effect(); } }; // 遍历副作用函数并执行 effects.forEach(run); }
原本这里的代码有很多,但取出核心逻辑立刻就变简洁了。
创建组件更新函数时会通过
effect
会传入第二个参数,其中包含scheduler
,将来在这里的 的 run 方法中会被使用核心内容就是根据
key
和 触发依赖的类型(ADD
、DELETE
或SET
) 执行add
方法,将依赖的副作用函数放到effects
中批量执行
2.1.9. 副作用(effect)
我对副作用的理解是:如果定义了一个响应式数据,和他相关的副作用函数在数据发生变化时都会重新执行
在debug过程中,发现watchEffect
和watch
也是通过doWatch
方法最终调用effect
,所以我们可以认为effect
是创建副作用函数的入口
(1)effectStack
-
定义
const effectStack: ReactiveEffect[] = [];
-
作用
这是一个栈(其实是数组)结构,存储多个副作用函数
activeEffect
,用于处理effect嵌套的场景(这个后面详细说明)。
(2)effect
-
作用
- 创建副作用函数,执行时触发
getter
完成依赖收集
- 创建副作用函数,执行时触发
-
核心源码
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { if (isEffect(fn)) { fn = fn.raw; } const effect = createReactiveEffect(fn, options); if (!options.lazy) { effect(); } return effect; } function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { if (!effect.active) { return options.scheduler ? undefined : fn(); } if (!effectStack.includes(effect)) { cleanup(effect); try { enableTracking(); effectStack.push(effect); activeEffect = effect; return fn(); } finally { effectStack.pop(); resetTracking(); activeEffect = effectStack[effectStack.length - 1]; } } } as ReactiveEffect; // 为effect添加很多属性 ... return effect; }
真正的副作用函数在
createReactiveEffect
方法中创建,首先将副作用函数本身添加到effectStack
栈顶,然后将赋值给activeEffect
,紧接着执行fn
,fn
触发响应式数据的getter
方法进行依赖收集,将activeEffect
添加到targetMap
;当key
发生改变时会触发依赖,从targetMap
取出对应的副作用函数并执行,这便是一个副作用函数作为一个依赖的收集与触发流程或许你也会疑惑,
activeEffect
用于临时存储当前副作用函数我明白,但是为什么还要保存到effectStack
这个栈结构中呢?后来查阅Vue3的社区才发现
effect
被设计为可以嵌套使用,这里的栈结构就是为了处理嵌套场景。栈的特点是先进后出,也就是后进入的副作用函数率先执行,然后出栈,保证执行的顺序是从外到内
这和react hook极不相同,可能是我用多了react的原因,嵌套这一点我暂时还是无法接受的,不过尤大既然这样设计自然有他的想法,所依作为一个react developer没有太多发言权,还是等Vue3用多了再做评价吧~
(3)副作用为什么需要重新收集依赖
或许在读响应式原理和依赖收集的源码时你也会疑惑,为什么每次触发getter
都进入track
呢,而且还有重新收集依赖的过程?
其实这是一种边缘情况的处理,副作用函数中的某些变量有可能会在条件中读取,因此存在动态依赖的行为。
没怎么懂对吗,这里用语言描述确实晦涩了点,不如用一小段代码体会一下,
watchEffect(() => {
if (state.role === ROOT) {
notice(state.user);
}
});
state.user
在条件中读取,当满足条件时,当前副作用函数是state.user
的依赖;当不满足条件时,state.user
需要清除这个依赖。这样描述是否会清晰一些呢~
2.2. 流程概述
2.2.1. 数据响应式
- 初始化时创建响应式对象,建立
getter
、setter
拦截,getter
负责收集依赖,setter
负责触发依赖 - 渲染时调用组件级的
effect
方法,将组件的render 函数赋值给全局变量activeEffect
并执行,render 函数触发对应key
的getter
函数,完成依赖收集 - 当用户再次触发了
key
的setter方法,从targetMap
中取出对应的依赖函数,然后执行trigger
方法触发依赖 完成更新
2.2.2. effect
目前看源码所了解到**触发effect
**的方式有这些:instance.update
、watch
、watchEffect
、computed
- 当执行到
effect
时,首先调用createReactiveEffect
创建一个真正的副作用函数 - 如果是
computed
则等待响应式数据的getter
触发副作用函数执行,反之则在创建过程中执行,最终都会触发key
的getter
函数,完成依赖收集 - 考虑到嵌套问题,将副作用函数放入
effectStack
中进行管理,每次执行然后出栈,保证副作用函数的执行顺序从外到内 - 另外要考虑动态依赖的边缘情况,所以需要重新收集依赖
2.3. 思考与总结
-
Vue3的数据响应式那么多优点,有缺点吗?
新的数据响应式方案不仅效率高,还可以完成13 个 api 的拦截,但缺点是不兼容低版本浏览器
proxy
,尤其是IE,不过都1202年了,还有人用IE嘛。。。 哈哈哈开玩笑~ -
为什么要用
Reflect
?我的理解是,
Reflect
和Proxy
相辅相成,只要proxy
对象上有的方法reflect
也拥有。而使用Reflect
其实是一种是安全措施,保证操作的是原对象 -
为什么需要互相引用?
这一点和Vue2很像,Vue2的dep和Watcher也是互相引用,当删除 key时会解除二者的引用关系。
Vue3同样考虑到这一点,删除 key时需要解除副作用函数与targetMap 中 key 的依赖函数的 关系
-
effect 嵌套问题
react 的函数式组件之所以不能嵌套使用 hook,是因为 react 的设计理念和 vue 不同,react 函数式组件每次 render 都作为函数自上而下执行,通过链表管理每个 hook 的状态,这就导致如果在条件或嵌套中使用 hook,会出现 hook 混乱的结果。但 vue 只是通过触发依赖更新组件,没有重新 render 一说,所以可以嵌套使用也是合理的,只是看开发者是否习惯这种思想的转换了。
-
关于重新收集依赖
React的副作用让开发者自己决定函数的执行依赖于哪些值,而Vue3替我们做了这件事情,于是开发者便不再有负担,只需要使用即可,不过有时候使用不当可能会造成多次重新收集依赖的过程,但也无伤大雅。
3. 异步更新
还记得吗,在组件但初始化阶段执行了setupRenderEffect
,通过effect
为instance.update
赋值了更新函数,
instance.update = effect(
function componentEffect() {
if (!instance.isMounted) {
// 初始化:递归执行patch
patch(...)
} else {
// updateComponent
patch(...)
}
},
__DEV__ ? createDevEffectOptions(instance) : prodEffectOptions
);
第二个参数中一定有个关键属性{scheduler: queueJob}
。
回顾effect
函数,副作用函数的执行都通过run方法调用
核心源码如下
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
effect.options.scheduler(effect);
} else {
effect();
}
};
可以看到存在scheduler
时,通过scheduler
,也就是通过instance.update
的queueJob
方法执行异步更新
3.1. 从源码探究原理
3.1.1. queueJob
- 作用
- 不重复地为
queue
添加任务 - 调用
queueFlush
- 不重复地为
- 核心源码
export function queueJob(job: SchedulerJob) { if ( (!queue.length || !queue.includes(job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex)) && job !== currentPreFlushParentJob ) { queue.push(job); queueFlush(); } }
3.1.2. queueFlush
-
作用
- 不重复地执行异步任务
-
核心源码
function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true; currentFlushPromise = resolvedPromise.then(flushJobs); } }
熟悉Vue2 源码的小伙伴可能会发现,Vue3的异步任务变得简洁了许多,
其中,真正的异步任务完全变成了promise,即基于浏览器的微任务队列来实现异步任务
const resolvedPromise: Promise<any> = Promise.resolve();
3.1.3. nextTick
- 作用
- 在 dom 更新完成后 执行自定义方法
- 核心源码
export function nextTick(this: ComponentPublicInstance | void, fn?: () => void): Promise<void> { const p = currentFlushPromise || resolvedPromise; return fn ? p.then(this ? fn.bind(this) : fn) : p; }
currentFlushPromise
是Promise 对象,通过then
方法会继续向微任务队列添加方法
3.2. 流程梳理
- 组件初始化时在
setupRenderEffect
方法中为instance.update
赋值更新函数 - 当触发
setter
函数时会执行trigger
,取出effect 函数通过queueJob
执行 queueJob
将任务加入到queue
中,然后执行queueFlush
方法queueFlush
是真正的异步任务,会不重复地向微任务队列添加任务- 当前的同步任务执行完毕后,浏览器会一次性刷新微任务队列,从而完成异步更新
3.3. 思考与总结
- Vue3 的异步任务相比 Vue2 变得很简洁,不再兼容低版本浏览器
- 真正的异步任务是 Promise 对象的 then 方法
4. patch
在研究 patch 之前,首先我们需要了解下Vue3的编译器优化,因为这直接改变了VNode的结构,为Vue3 patching 算法的极高性能做好了基础。
4.1. 编译器带来的优化
-
4.1.1. 静态节点提升
将节点分为动态节点和静态节点,静态节点的作用域提升到 render 函数同级,like this,
const _hoisted_1 = /*#__PURE__*/ _createTextVNode(' 一个文本节点 '); const _hoisted_2 = /*#__PURE__*/ _createVNode('div', null, 'good job!', -1 /* HOISTED */); return function render(_ctx, _cache) { with (_ctx) { return _openBlock(), _createBlock('div', null, [_hoisted_1, _hoisted_2]); } };
其中
_hoisted_1
和_hoisted_2
就是被提升的静态节点,只在首次渲染时创建,在后续更新的时候会绕过这些静态节点。
-
4.1.2. 补丁标记 和 动态属性记录
当我们在 template 中使用动态属性会被记录下来,like this,
<child-comp :title="title" :foo="foo" msg="hello"/>
会被 render 函数记录下来
export function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_child_comp = _resolveComponent('child-comp'); return ( _openBlock(), _createBlock( _component_child_comp, { title: _ctx.title, foo: $setup.foo, msg: 'hello', }, null, 8 /* PROPS */, ['title', 'foo'] ) ); }
可以看到最后的两个参数,
8
是PatchFlags
中的一个类型,本质上是一个二进制数字,通过按位与运算可以做组合条件,在这里8
表示当前组件有动态变化的props
;第二个参数表示动态变化的是哪些props
。在后续diff props的时候只diff
title
和foo
。 -
4.1.3. block
如果当前节点下有动态变化的内容则作为一个 block 存储,
所以在Vue3中,你应常看到的 render 函数会是这种形式
export function render(_ctx, _cache) { return _openBlock(), _createBlock('div', null, [_hoisted_1, _hoisted_2]); }
_openBlock
打开区块,_createBlock
创建 block,所有的动态变化的子节点会存储到dynamicChildren
中,将来diff children的时候只 diff dynamicChildren。
-
4.1.4. 缓存事件处理程序
如果使用了内联函数,会被缓存到
_cache
中,在下次更新时直接从_cache
中取出来使用,不再重复创建函数,避免前后引用不同导致的不必要渲染like this,
<child-comp @click="toggle(index)"/>
会被编译成
export function render(_ctx, _cache, $props, $setup, $data, $options) { const _component_child_comp = _resolveComponent('child-comp'); return ( _openBlock(), _createBlock(_component_child_comp, { onClick: _cache[1] || (_cache[1] = $event => _ctx.toggle(_ctx.index)), }) ); }
对于内联函数,如果组件发生了重新渲染,前后两个函数的引用不同可能会导致重复更新,这一点在 react 中会很常见,使用 react 时我们会通过
useCallback
进行优化。但Vue3替我们做了这一点。接下来来看一下,patching 算法是如何处理的
4.2. patch
-
作用
- 组件渲染和更新的入口
- 通过调用
processXXX
执行对应类型的渲染/更新函数
-
核心源码
const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // 若包含patchFlag,则开启优化 if (n2.patchFlag === PatchFlags.BAIL) { optimized = false n2.dynamicChildren = null } const { type, ref, shapeFlag } = n2 // 根据VNode的type决定使用哪种patching算法 switch (type) { case Text: processText(n1, n2, container, anchor) break case Comment: processCommentNode(n1, n2, container, anchor) break case Static: if (n1 == null) { mountStaticNode(n2, container, anchor, isSVG) } else if (__DEV__) { patchStaticNode(n1, n2, container, isSVG) } break case Fragment: processFragment(...) break default: if (shapeFlag & ShapeFlags.ELEMENT) { processElement(...) } else if (shapeFlag & ShapeFlags.COMPONENT) { processComponent(...) } else if (shapeFlag & ShapeFlags.TELEPORT) { ;(type as typeof TeleportImpl).process(...) } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { ;(type as typeof SuspenseImpl).process(...) } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } // set ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2) } }
除了
Text
、Comment
、Static
、Fragment
会通过type
处理,其他情况都是由shapeFlag
来决定使用哪种 patching 算法。这几种算法基本大同小异,本文选取
processElement
进行分析
4.3. processElement
-
作用
- 组件首次渲染:调用
mountElement
- 组件更新:调用
patchElement
- 组件首次渲染:调用
-
核心源码
const processElement = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { isSVG = isSVG || (n2.type as string) === 'svg' if (n1 == null) { mountElement( n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } else { patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized) } }
n1
表示旧的虚拟 dom,n2
表示新的虚拟 dom。目前我们分析patching 算法,所以需要看patchElement
方法。
4.4. patchElement
-
作用
- 对
Element
类型的 VNode 进行 patch
- 对
-
核心源码
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { const el = (n2.el = n1.el!) let { patchFlag, dynamicChildren, dirs } = n2 // #1426 take the old vnode's patch flag into account since user may clone a // compiler-generated vnode, which de-opts to FULL_PROPS patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS const oldProps = n1.props || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ let vnodeHook: VNodeHook | undefined | null // 1. diff props if (patchFlag > 0) { if (patchFlag & PatchFlags.FULL_PROPS) { // 动态key,需要全量diff ... // 最终调用的是patchProps } else { // class是动态属性的情况 ... // 最终调用的是hostPatchProp // style是动态属性的情况 ... // 最终调用的是hostPatchProp // 处理dynamicProps中的动态属性 ... // 循环调用hostPatchProp } // 动态text ... // 最终调用hostSetElementText } else if (!optimized && dynamicChildren == null) { // 全量diff,即没有优化的情况 ... // 最终调用的是patchProps } // 2. diff children if (dynamicChildren) { // 动态子节点 patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, areChildrenSVG ) } else if (!optimized) { // 全量diff,即没有优化的情况 patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG ) } }
代码量有点多,因为还包含没有优化的情况,没有优化时就和 Vue2 基本相同,
借助
patchFlag
,可以通过hostPatchProp
实现靶向更新。借助
dynamicChildren
,可以通过patchBlockChildren
实现按需 diff 子节点, 没有patchChildren
时通过patchChildren
全量 diff。hostPatchProp
很简单,只是按照传入的参数进行更新,我们重点关注patchBlockChildren
和patchChildren
4.5. patchBlockChildren
-
作用
- 处理
block
级别的children
- 处理
-
核心源码
const patchBlockChildren: PatchBlockChildrenFn = ( oldChildren, newChildren, fallbackContainer, parentComponent, parentSuspense, isSVG ) => { for (let i = 0; i < newChildren.length; i++) { const oldVNode = oldChildren[i] const newVNode = newChildren[i] // Determine the container (parent element) for the patch. const container = // - In the case of a Fragment, we need to provide the actual parent // of the Fragment itself so it can move its children. oldVNode.type === Fragment || // - In the case of different nodes, there is going to be a replacement // which also requires the correct parent container !isSameVNodeType(oldVNode, newVNode) || // - In the case of a component, it could contain anything. oldVNode.shapeFlag & ShapeFlags.COMPONENT || oldVNode.shapeFlag & ShapeFlags.TELEPORT ? hostParentNode(oldVNode.el!)! : // In other cases, the parent container is not actually used so we // just pass the block element here to avoid a DOM parentNode call. fallbackContainer patch( oldVNode, newVNode, container, null, parentComponent, parentSuspense, isSVG, true ) } }
遍历
newChildren
,即dynamicChildren
,对同级的新旧VNode
通过patch
进行diff,以此类推不断缩小 children 的级别,调用patch
。到一定层级后调用
patch
就不再有优化选项,最终变成处理新旧两个 children
4.6. patchChildren
-
作用
- 选择子节点的patching 算法
-
核心源码
const patchChildren: PatchChildrenFn = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized = false ) => { const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const c2 = n2.children const { patchFlag, shapeFlag } = n2 // fast path if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // 有key的children patchKeyedChildren(...) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // 没有key的children patchUnkeyedChildren(...) return } } // children has 3 possibilities: text, array or no children. // children 有三种可能:文本,数组,或没有children ... }
PatchFlags.KEYED_FRAGMENT
和PatchFlags.UNKEYED_FRAGMENT
是children
是否包含key
的依据,根据是否包含key
选择patchKeyedChildren
或patchUnkeyedChildren
。其中
patchKeyedChildren
是对带有 key 的 children 的处理方法。
4.7. patchKeyedChildren
-
作用
- diff 带
key
的子节点
- diff 带
-
核心源码
const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, optimized: boolean ) => { let i = 0 const l2 = c2.length let e1 = c1.length - 1 // prev ending index let e2 = l2 - 1 // next ending index // 1. 掐头 // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch(...) } else { break } i++ } // 2. 去尾 // a (b c) // d e (b c) while (i <= e1 && i <= e2) { const n1 = c1[e1] const n2 = (c2[e2] = optimized ? cloneIfMounted(c2[e2] as VNode) : normalizeVNode(c2[e2])) if (isSameVNodeType(n1, n2)) { patch(...) } else { break } e1-- e2-- } // 3. 掐头去尾后,若新节点有剩余则新增,旧节点有剩余则删除 // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch(...) i++ } } } // 4. 旧节点有剩余则删除 // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } } // 5. 掐头去尾后,如果中间剩余的是不相同的节点,则diff // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else { ... } }
该过程在源码中分为 5 步,但可以优化提取下,分成三大步
- 首先一次性找到所有相同的开头,该过程称为掐头
- 然后一次性找到所有相同的结尾,该过程称为去尾
- 掐头去尾后的处理,该过程称为扫尾
- 3.1. 新旧节点有一个空了
- 3.1. 若新数组有剩余就新增,
- 3.2. 若旧数组有剩余就删除
- 3.2. 新旧节点都有剩余
- diff 两个新旧节点两个剩余的 children
- 3.1. 新旧节点有一个空了
举个例子或许会更形象些
-
新增子节点的情况
const oldChildren = [a, b, c, d]; const newChildren = [a, e, f, b, c, d]; // 1. 掐头 const oldChildren = [b, c, d]; const newChildren = [e, f, b, c, d]; // 2. 去尾 const oldChildren = []; const newChildren = [e, f]; // 3. 掐头去尾后,新旧节点有一个空了,则批量增加[e, f] ...
-
更改部分子节点的情况
const oldChildren = [a, g, h, b, c, d]; const newChildren = [a, e, f, b, c, d]; // 1. 掐头 const oldChildren = [g, h, b, c, d]; const newChildren = [e, f, b, c, d]; // 2. 去尾 const oldChildren = [g, h]; const newChildren = [e, f]; // 3. 掐头去尾后,新旧节点都没空,对[g, h]、[e, f]这两个children进行diff const oldChildren = [g, h]; const newChildren = [e, f];
5. 思考、总结与补充
-
Vue3 大量运用工厂模式
像 createAppAPI、createRenderer 都是通过工厂模式返回一个真正的函数,这些方法基本都在源码的core 包里面,在这里的目的是为了更好地跨平台,工厂函数只提供核心处理方法,具体的平台方法由模块声明。可以更好地跨平台开发,避免再出现开发Weex要直接修改源码这种情况。
-
渲染器的概念及应用
渲染器是一个对象,是 Vue3 源码中的一个核心概念,渲染器内部包含三个方法,
render
、hydrate
、createApp
。我们如果要做自定义渲染可以通过创建自定义渲染器来实现。
-
编译器相关
-
编译器做了哪些事情?
通过parse、transform、generate将
template
转成render 函数 -
编译发生在哪个阶段?
- 如果是webpack环境,则在开发环境下通过vue-loader完成编译工作。到了生产环境只保留vue的runtime文件
- 如果是携带了compiler的runtime版本,则组件初始化执行
mount
方法时,最终调用setupComponent
完成编译
-
编译阶段做的优化
- Block Tree
- PatchFlags
- 静态提升、静态 PROPS
- Cache Event handler
- 预字符串化
- 如何实现靶向更新
-
-
位运算
位运算是在数字底层(即表示数字的 32 个数位)进行运算的。由于位运算是低级的运算操作,所以速度往往也是最快的。
可以很快地处理组合条件,比如项目中如果有多角色、叠加权限的场景就可以使用位运算来解决。
-
diff
Vue3 真正的 diff 算法变化并不大,可以分成三大步骤
- 掐头、
- 去尾、
- 扫尾
- 批量增/删
- diff
真正让 Vue3 diff 快到飞起的原因是编译器做了很多优化。
-
关于数据响应式
不仅仅是
Object.defineProperty
到proxy
的改变,还有一个变化是子数据的懒观察。 -
composition-api 及 hook
很像React的hook,Vue3 的 hook 没有新旧值不同的心智负担,而且副作用函数可以嵌套使用,但存在一些是否应该解构的心智负担。
composition-api相比 Vue2 的options API可以更好地复用逻辑,具体的奥秘可以在官网查看~,强烈推荐!!!