Q: Vue3相比较于Vue2升级的地方?
-
代码库的文件夹结构变化:vue2的时候是把所有模块都放在
/src下,例如-
compile
-
core
-
platforms
-
shared
-
types
而Vue3是基于
monorepo的结构,在packages下放了多个文件夹。 -
-
类型语言的变化:
Vue2采用的是flow,Vue3采用的是TypeScript。 -
源码在响应式方面的变化:Vue2基于defineProperty,Vue3基于Proxy API。
Vue3 setup处理逻辑
组件实例的设置流程 setupComponent
function setupComponent(instance, isSSR = false) {
const { props, children } = instance.vnode
// 判断是否是一个有状态的组件
const isStateful = isStatefulComponent(instance)
// 初始化props
initProps(instance, props, isStateful, isSSR)
// 初始化插槽
initSlots(instance, children)
// 设置有状态的组件实例
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
设置有状态的组件实例 setupStatefulComponent
runtime-core/src/component.ts
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// 0. create render proxy property access cache
instance.accessCache = Object.create(null)
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
// 2. call setup()
const { setup } = Component
if (setup) {
// 如果setup函数带参数,创建一个setupContext
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
// 执行setup函数获取结果
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
// 处理setup执行结果
handleSetupResult(instance, resolvedResult, isSSR)
}
}
注意proxy 的get方法的取值优先级:
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.DATA:
return data[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
// default: just fallthrough
}
finishComponentSetup
在handleSetupResult的最后,会执行这个函数完成组件实例的设置。
当组件没有定义的时候,也会执行finishComponentSetup去完成组件实例设置。
这个函数功能:
1、标准化模板或者渲染函数
Vue的runtime-only和runtime-compile两个版本的区别就在于,是否有注册compile方法。
在Vue3中,compile方法是通过外部注册的。
在finishComponentSetup函数中,我们先看instance.render是否存在,如果不存在,则开始标准化流程,这里主要是处理三种情况:
1)compile和组件template属性存在,render方法不存在。
这时候,会由runtime-compiled版本在JavaScript运行时进行模板编译,生成render函数。
2)compile和render方法不存在,template属性存在。
这时候,由于没有compile,用的是runtime-only版本,就会报一个警告来告诉用户,想要运行时编译就得使用runtime-compiled版本的Vue.js。
3)组件既没有写render函数,又没有写template模板。
这时候,需要报一个警告,告诉用户组件缺少了render函数或者template模板。
处理完以上情况后,就要把组件的render函数赋值给instance.render。组件渲染的时候,运行instance.render函数生成组件的子树Vnode。
2、兼容Options API
Vue3之所以可以兼容Options API语法,是因为Vue3里有一个applyOptions方法,实现了Vue2的Options API的功能。
大概是一下这些步骤:
-
处理全局mixin
-
处理extend
-
处理本地mixins
-
props已经在外面处理过了
-
处理inject
-
处理methods方法
-
处理data
-
处理计算属性
-
处理watch
-
处理provide
-
处理组件
-
处理指令
-
处理生命周期option
Provide和Inject API的原理
provide其实是从当前组件实例开始,往上通过this.$parent.provide,并以此递归知道App根节点。
那么Inject的原理也就很好理解了,通过查过父组件的provide数据来获取key。
Vue3相比较于Vue2,提供了
import {provide, inject} from 'vue';
这样更加宽松的API结构,可以更好的获取数据。
转换生成AST
baseParse的实现
export function baseParse(
content: string,
options: ParserOptions = {}
): RootNode {
// 创建解析上下文
const context = createParserContext(content, options)
const start = getCursor(context)
// 解析子节点,并创建AST
return createRoot(
parseChildren(context, TextModes.DATA, []),
getSelection(context, start)
)
}
createParserContext
parseChildren
插槽:如何实现内容分发
**在父组件渲染阶段,子组件的插槽部分的DOM是不能渲染的。**需要通过某种方式保留下来,等到子组件渲染的时候再渲染。
function render(_ctx, _cache, $props, $setup, $data, $options) {
return (openBlock(), _reateBlock(component_layout, null, {
header: _withCtx(() => [
_createVNode("h1", null, _toDisplayString(_ct.header), 1 /* TEXT */)
]),
default: _withctx(() => [
createVNode("p", null, _toDisplayString(_ctx.main), 1 /* TEXT */)
]),
footer: _withCtx(() => [
_createVNode ("p", null, _toDisplayString(_ctx footer), 1 /* TEXT */)
]),
_: 1
}))
}
--javascripttypescriptbashsqljsonhtmlcssccppjavarubypythongorustmarkdown
export const initSlots = (
instance: ComponentInternalInstance,
children: VNodeNormalizedChildren
) => {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
const type = (children as RawSlots)._
if (type) {
// users can get the shallow readonly version of the slots object through `this.$slots`,
// we should avoid the proxy object polluting the slots of the internal instance
instance.slots = toRaw(children as InternalSlots)
// make compiler marker non-enumerable
def(children as InternalSlots, '_', type)
} else {
normalizeObjectSlots(
children as RawSlots,
(instance.slots = {}),
instance
)
}
} else {
instance.slots = {}
if (children) {
normalizeVNodeSlots(instance, children)
}
}
def(instance.slots, InternalObjectKey, 1)
}
initSlots的实现很简单,就是对了把插槽部分转换成函数,然后保留在instance.slots上。
在子组件中,插槽的渲染主要是通过renderSlot实现的。
插槽的实现,实际上就是一种延时渲染,把父组件中编写的插槽内容保存到一个对象上,并且把具体渲染DOM的代码用函数的方式封装,然后在子组件渲染的时候,根据插槽名在对象中找到对应的函数,然后执行这些函数做真正的渲染。