入口函数 createApp
vue2的入口函数是一个构造函数 Vue
并且要实例化才能使用,到了vue3 入口函数变成一个单独的函数createApp
我们可以打一个断点进入到入口函数内部去看看
(文件在:vue-next3.2/packages/runtime-dom/src/index.ts) 发现其实内部调用的是另一个createApp
,是由ensureRenderer
函数调用返回的结果进行点调用,所以可以推断ensureRenderer
返回的渲染器是一个对象,且一定会有一个createApp
方法,
跳转到ensureRenderer
内部去看看 ensureRendere
函数作用是确保全局renderer
存在,存在直接返回,不存在调用createRenderer
进行创建一个新的renderer
而createRenderer
内部比较庞大(2018行),这里就不上代码了,主要是分成三部分,1.拿到平台的操作方法(renderer支持自定义) 2.定义了一系列的操作方法,有用于diif的:patch
、patchProps
、patchElement
,更新挂载DOM的:mountChildren
、mountComponent
、updateComponent
,但是最终要的还是patch
和render
方法,最后的renderer
看一下里面有啥
render
:渲染函数
createApp
:是由createAppAPI
执行之后产生的函数
hydrate
:用于服务端渲染
应用实例的产生
vue2中实例一开始就包含了很多$
开头的方法,但是在vue3中,这些方法并不是直接挂在实例上,而是挂载到了组件实例的渲染上下文中,vue3的实例都是一个代理对象,可以说,vue3非常依赖于Proxy
,所有的数据代理都由Proxy进行代理,
至于为什么会在外面也看到这些方法,其实因为vue3在创建根组件实例的时候,对所有的以$
开头的方法和属性也都进行了代理,
再往后看发现实例上有一些方法,这些方法在vue2中是静态方法的,也就是需要Vue.xxx
调用的,但是,vue3没有静态方法,全部由静态方法变为实例方法,变成app.xxx
,这样可以非常好的利用摇树优化 不会出现 dead code 使用就打包 不使用就不打包并且vue3 中的 filter 以及被移除了,还有一些属性,如:_component
用户写配置项,_container
组件挂载容器,_context
:应用程序上下文
初始化
在createAppAPI
创建的createApp
执行完毕之后,会返回一个app
实例,每一个根实例身上会有一个mount
用于挂载,但是第一次执行执行的mount
并不是实例上的mount
,而是在入口函数createApp
中,先把原本的函数存起来,然后扩展的mount
,并且这个mount
内部调用实例上的mount
这个扩展mount
接受一个选择器参数,什么选择器都行,获取容器的方式querySelector()
推荐使用id选择器(一般都是#app
),接下来就是获取模板,用户可能会直接卸载容器中,后面的就是对vue2的兼容验证了,之后就是清除模板中的内容,后面挂载需要一个空的容器,
执行实例上的mount
,接受三个参数,rootContainer
:容器 isHybrate
:服务端渲染 isSVG
是否渲染的是SVG,函数内主要分为两步,根据组件生成VNode
、将VNode
渲染在容器中
创建VNode
vue的VNode
是由createVNode
进行创建,但其实是调用_createNode
去创建,在这个函数中:主要的任务就是把用户传递进来的一些属性进行处理,如class
、style
,最主要的是对props
处理和对组件本身进行第一次标记,shapeFlag
,然后再传递给createBaseVNode
,让它再进行根据属性进行创建,在createBaseVNode
中先初始化一个公共的VNode,然后再通过外部传入的属性,进行修改,并且,VNode
的一些公共属性就是在这里进行初始化的,这些标记都十分重要,是为之后的编译过程埋下了伏笔。
appContext
:应用程序的上下文shapeFlag
:标明该VNode
是啥,如:原生节点、组件(函数式组件或者状态组件)、文本子节点等patchFlag
:记录了VNode
那些是动态的,dynamicProps
:需要监视哪些熟悉的变化dynamicChildren
:需要监视那些子节点el
:记录当前VNode
的实际真实节点
....
之后就是对子节点进行处理了,以及对块树(black tree
)的跟踪,这也是vue diff加快的原因之一,
if (needFullChildrenNormalization) {
// 标准化子节点
normalizeChildren(vnode, children)
// normalize suspense children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).normalize(vnode)
}
} else if (children) {
// compiled element vnode - if children is passed, only possible types are
// string or Array.
// 说明了子元素 只能是文本子节点或者是数组子节点
vnode.shapeFlag |= isString(children)
? ShapeFlags.TEXT_CHILDREN
: ShapeFlags.ARRAY_CHILDREN
}
// track vnode for block tree
if (
isBlockTreeEnabled > 0 &&
// avoid a block node from tracking itself
!isBlockNode &&
// has current parent block
currentBlock &&
// presence of a patch flag indicates this node needs patching on updates.
// component nodes also should always be patched, because even if the
// component doesn't need to update, it needs to persist the instance on to
// the next vnode so that it can be properly unmounted later.
(vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
// the EVENTS flag is only for hydration and if it is the only flag, the
// vnode should not be considered dynamic due to handler caching.
vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
) {
currentBlock.push(vnode)
}
复制代码
最后把创建好的VNode
返回(最后的处理是对 vue2的兼容,这里就不去赘述),相对于vue2的VNode
,去掉了tag
等一系列,反倒多了一个type
属性,里面记录着VNode
的模板以及一些配置,但是两个VNode
产生的时间节点不一样,vue3是直接创建的,而vue2是先进行编译后才会产生VNode
,最终交给patch
去渲染的的VNode
都是由生成的render
函数去生成的,
挂载解析
在mount
函数中也调用的render
,但是这个render
函数是renderer.ts中的,其作用就是将VNode
渲染到容器中,核心就是打补丁,然后将最新的vnode
存储,用于下一次对比,在代码中,有一个处理 container._vnode || null
这是为了兼容第一次初始化渲染,没有旧的vnode
,
path
函数接受很多参数,但是最重要的参数只有三个,n1
、n2
、container
,分别是旧的虚拟DOM、新的虚拟DOM、挂载容器,在patch
函数内部,会对传入的虚拟的DOM先进行处理,后进行template
的解析,生成render
函数,
核心逻辑
处理的节点类型有:文本、注释、静态节点、Fargment
(一系列没有根节点,并排排列的节点)、element
、component
、teleport
、suspense
,在第一次进入patch
函数时,type
是根组件的配置对象,所以会先执行解析组件逻辑(if(shapeFlag & ShapeFlags.COMPONENT)
),也就是会先执行processComponent
在第一次n1
肯定是null
,会走if
的逻辑,第二个if
是内置组件keepAlive
缓存的组件,初始化显然不是,也就是走else
,执行mountComponent
,
渲染函数的产生
挂载组件一共分为以下几步
- 创建组件实例
instance
是组件实例,这里不再是一些$xxx
的方法,而是一些属性,其中一些属性看过vue2源码的人会比较眼熟,可以一眼就看出来,如bc = beforeCreate
、bm = beforeMount
、bu = beforeUpdate
、 bum = beforeUnmount
,其中最终的要的就是ctx
属性,里面就是$xxx
方法,还有一些其他重要属性,如type
、vnode
、slots
、props
等
- 安装组件
export function setupComponent(
instance: ComponentInternalInstance,
isSSR = false
) {
isInSSRComponentSetup = isSSR
const { props, children } = instance.vnode
const isStateful = isStatefulComponent(instance)
// 初始化属性和插槽
initProps(instance, props, isStateful, isSSR)
initSlots(instance, children)
const setupResult = isStateful
// 安装有状态的组件
? setupStatefulComponent(instance, isSSR)
: undefined
isInSSRComponentSetup = false
return setupResult
}
复制代码
在这个函数中,主要工作就是进行,初始化属性和插槽,以及安装有状态的组件,在这个时候,setup
还没执行,props
就已经初始化了,说明props
的数据优先于组件本身的数据,之后就是调用setupStatefulComponent
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
// Component 是当前组件的配置
const Component = instance.type as ComponentOptions
if (__DEV__) {
if (Component.name) {
validateComponentName(Component.name, instance.appContext.config)
}
if (Component.components) {
const names = Object.keys(Component.components)
for (let i = 0; i < names.length; i++) {
validateComponentName(names[i], instance.appContext.config)
}
}
if (Component.directives) {
const names = Object.keys(Component.directives)
for (let i = 0; i < names.length; i++) {
validateDirectiveName(names[i])
}
}
if (Component.compilerOptions && isRuntimeOnly()) {
warn(
`"compilerOptions" is only supported when using a build of Vue that ` +
`includes the runtime compiler. Since you are using a runtime-only ` +
`build, the options should be passed via your build tool config instead.`
)
}
}
// 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))
if (__DEV__) {
exposePropsOnRenderContext(instance)
}
// 2. call setup()
//
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
setCurrentInstance(instance)
pauseTracking()
// 存在并执行setup 并往里面传递一些参数 传递的是 props 和 ctx
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
resetTracking()
unsetCurrentInstance()
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult: unknown) => {
handleSetupResult(instance, resolvedResult, isSSR)
})
.catch(e => {
handleError(e, instance, ErrorCodes.SETUP_FUNCTION)
})
} else if (__FEATURE_SUSPENSE__) {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult
} else if (__DEV__) {
warn(
`setup() returned a Promise, but the version of Vue you are using ` +
`does not support it yet.`
)
}
} else {
handleSetupResult(instance, setupResult, isSSR)
}
} else {
// 处理选项等事务
finishComponentSetup(instance, isSSR)
}
}
复制代码
这里会先代理上下文,(Component
是组件的配置)再从配置中去除setup
,带错误处理的执行,会传递一些参数,传递是props
和 ctx
如果返回的是Promise
,就代表是服务端渲染,返回以便服务器渲染器可以等待它,如果是异步服务端渲染,先存储起来,等待返回,不是,就是正常的,就进行后续的setup
返回的结果处理
有两种情况 返回的是一个对象或者是返回的是一个函数,如果是对象(数据,并且假设可以从渲染模板中得到渲染函数),直接进行代理,并设置在组件实例上,而函数,是普通的render
或者服务端渲染的ssrRender
也是存储在组件实例上,返回的对象的代理方式类似ref
,最后会调用finishComponentSetup
去处理option API
最先转换v2的是渲染函数,在v3中,v2的渲染函数已经规范为函数式组件,具体兼容过程可以去看convertLegacyRenderFn
函数 在renderer.ts, 在服务端渲染的情况下,确认配置中的render function
如果render function
不存在,全部变为NOOP函数,以便继承来自mixins/extend
的函数,在客户端渲染且实例上没有render function
(一般用户不会手写render function
,可以在setup
中设置)的情况下,进行模板编译生成渲染函数,
但是为了兼容v2的内联模板,不存在直接找整个模板,找不到模板不会进行模板编译,在编译模板之前,会进行编译的配置的收集(v2和v3都会被收集),拿到最终配置选项,执行complie
(实际上执行的是baseComplie
)进行编译,进行编译三部曲
编译三部曲
prefixIdentifiers
这个参数是为防止(将vue的mode
设置为module
的情况)在严格模式下使用with(this)
(在module
模式下,默认是严格模式,不允许使用with(this)
)
编译三部曲分为:1.将模板转换为AST
语法树 2.修饰AST
语法树 3.生成render
函数
将模板转换为AST
语法树
将模板转换为AST
语法树 (vue3编译是深度搜索优先,会优先将一个节点中的所有的子节点编译完毕,才会进行下一个节点的编译) 调用的是baseParse
,但是其核心在parseChildren
,在parseChildren
中(在vue2中,是采用大量的正则表 达式进行匹配,vue3采用的是函数式的方式),代码比较庞大,这里一部分一部分的看,在此之前先介绍几个变量,
ancestors
:存储的是按顺序的父节点的数组,而parent
希望拿到的就是离我当编译位置最近的父元素, 在后面方便校验是否编译完上一个节点中的子节点,HTML模板是双标签,分为开始和结束,编译最先拿到的是开始标 签,后面编译到结束标签也好告诉程序,这个节点编译完毕
ns
:当前节点类型
nodes
:存储编译完的节点,
delimiters
:
主要分为 插值、文本、标签, 最先判断是不是插值,通过使用parseInterpolation
解析,
然后是标签以打头的<
的内容,如果使用正则匹配为<p
、<h1
等,使用parseElement
进行解析
如果上两种方式都没有找到,就代表是普通文本,使用parseText
解析
最后进行v2空白处理,根据处理结果,进行返回,
最后的AST
语法树长这样
转换AST
语法树
单纯的依靠ast语法树无法生成render
,需要拿到一些方法和属性,如函数缓存、静态节点,以及指令转换、节点转换等工具函数,对由模板生成的 AST 进行转换
生成render
函数
得到了最终的ast语法树,就可以传入generate
函数中进行生成render
,函数中会根据ast语法树中的一些标记做优化,如静态节点提升,静态属性提升,函数缓存,内联模板等
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
// 生成上下文
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
const hasHelpers = ast.helpers.length > 0
// 是否可以使用 with(this)
const useWithBlock = !prefixIdentifiers && mode !== 'module'
const genScopeId = !__BROWSER__ && scopeId != null && mode === 'module'
const isSetupInlined = !__BROWSER__ && !!options.inline
// preambles
// in setup() inline mode, the preamble is generated in a sub context
// and returned separately.
const preambleContext = isSetupInlined
? createCodegenContext(ast, options)
: context
if (!__BROWSER__ && mode === 'module') {
genModulePreamble(ast, preambleContext, genScopeId, isSetupInlined)
} else {
// 静态提升
genFunctionPreamble(ast, preambleContext)
}
// enter render function
const functionName = ssr ? `ssrRender` : `render`
// 服务端渲染和客户端渲染
const args = ssr ? ['_ctx', '_push', '_parent', '_attrs'] : ['_ctx', '_cache']
if (!__BROWSER__ && options.bindingMetadata && !options.inline) {
// binding optimization args
args.push('$props', '$setup', '$data', '$options')
}
const signature =
!__BROWSER__ && options.isTS
? args.map(arg => `${arg}: any`).join(',')
: args.join(', ')
if (isSetupInlined) {
push(`(${signature}) => {`)
} else {
push(`function ${functionName}(${signature}) {`)
}
indent()
if (useWithBlock) {
push(`with (_ctx) {`)
indent()
// function mode const declarations should be inside with block
// also they should be renamed to avoid collision with user properties
// 重命名创建函数 一些方法
if (hasHelpers) {
push(
`const { ${ast.helpers
.map(s => `${helperNameMap[s]}: _${helperNameMap[s]}`)
.join(', ')} } = _Vue`
)
push(`\n`)
newline()
}
}
// generate asset resolution statements 生成配置资产列表,方便以后使用
if (ast.components.length) {
genAssets(ast.components, 'component', context)
if (ast.directives.length || ast.temps > 0) {
newline()
}
}
if (ast.directives.length) {
genAssets(ast.directives, 'directive', context)
if (ast.temps > 0) {
newline()
}
}
if (__COMPAT__ && ast.filters && ast.filters.length) {
newline()
genAssets(ast.filters, 'filter', context)
newline()
}
// 拿出vue中的方法
if (ast.temps > 0) {
push(`let `)
for (let i = 0; i < ast.temps; i++) {
push(`${i > 0 ? `, ` : ``}_temp${i}`)
}
}
if (ast.components.length || ast.directives.length || ast.temps) {
push(`\n`)
newline()
}
// generate the VNode tree expression
if (!ssr) {
push(`return `)
}
if (ast.codegenNode) {
genNode(ast.codegenNode, context)
} else {
push(`null`)
}
if (useWithBlock) {
deindent()
push(`}`)
}
deindent()
push(`}`)
return {
ast,
code: context.code,
preamble: isSetupInlined ? preambleContext.code : ``,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
复制代码
到最后生成的函数还是字符串,需要在后面进行转换成函数,至此,整个编译流程结束
挂载开始
安装组件
回到之前finishComponentSetup
函数执行栈,开始对v2的配置项处理,调用的是applyOptions(instance)
data
选项的处理
computed
选项处理 后面还有一些选项处理,在选项处理完毕之后,就是处理生命周期函数,约束暴露,最后执行完毕,回到mountComponent
执行栈,开始安装渲染依赖setupRenderEffect
setupRenderEffect
内部定义了一个函数componentUpdateFn
,服务于更新和挂载两个项目,在内部,会有两种调用patch方式,区别就在于,是否存在旧的VNode
,
// 初始化渲染
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// 更新渲染
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
复制代码
而新的VNode
是调用renderComponentRoot
产生的,在renderComponentRoot
内部,分为函数式组件和有状态的组件两种,但一定是得到了render
,如果用户没有设置,vue会先进行编译后进行赋值,最后把render的执行结果返回,且如果是函数组件,会将attr
、slots
、emit
传递进去,
安装依赖
在执行挂载更新的前后,会分别执行v2的生命周期函数和v3的生命周期函数,而setupRenderEffect
等同于v2中的mountComponent
,在v2中响应式依赖使用的是渲染watcher
,而v3中通过effect
,setupRenderEffect
内部执行一个effect
,effect
是将传入的fn和它内部调用的响应式数据产生一个依赖映射关系,在v3.2之后,这里就改成使用实例化一个ReactiveEffect
对象来产生依赖映射关系,在最后在把生成的update
函数默认执行,内部就会调用componentUpdateFn
,进行挂载,vue3初始化流程结束
挂载流程如下: mount() => processComponent() => mountComponent() => setupComponent()
然后 setupComponent()
分别调用setupStatefulComponent()
安装有状态的组件 setupRenderEffect()
依赖收集
最后:欢迎大佬指导和评论