开始叠甲~
背景:闲来无事,需求不多,想着自己能不能看看Vue3的源码,尝试自己去看看能不能串起来Vue3的一个流程。也是自己学习的一个过程
不是教学,只是自己做一个记录,希望也能帮助到有需要的人,有错误之处,欢迎指正~
我也是自己一点点摸索的~ 菜勿喷~
叠甲完毕~
vue 版本 3.5.8
createApp想必每个开发Vue的程序员都不陌生,所以我们先从它入手,但是我一开始并没有找到createApp方法的一个清晰的引入路径,虽然我知道它在 @vue/runtime-dom 这个包下,所以第一步我想找到一个清晰的路径来展示createApp的来龙去脉!
1. 熟悉代码目录
已知vue3的代码都在packages目录下,我本来是想找一个入口的index的文件的,但是我在Vue文件中的找了半天,并没有发现一个直达createApp方法的目录(其实在vue的src/index中,是从runtime中都导入导出了,所以在index中也是可以直接用createApp方法的,但是后面我看了vue-compat这个目录,在这里目录中有更清晰的路径导入,所以我们直接从vue-compat目录的src的index开始本章教学
话不多话 直接上代码 (我们只关注核心代码,一些其他的警告以及环境判断就分析了)
2. 源码分析
packages/vue-compat/src/index.ts - compileToFunction
index中compileToFunction这个方法其实并没有真正的在这里执行,vue-compat这里的代码和vue中的有一点不同但是代码是99%相同的,所以我们从这开始问题不大, 也不影响我们先分析一波
import { createCompatVue } from './createCompatVue'
import {
type CompilerError,
type CompilerOptions,
compile,
} from '@vue/compiler-dom'
import {
type CompatVue,
type RenderFunction,
registerRuntimeCompiler,
warn,
} from '@vue/runtime-dom'
import { NOOP, extend, generateCodeFrame, isString } from '@vue/shared'
import type { InternalRenderFunction } from 'packages/runtime-core/src/component'
import * as runtimeDom from '@vue/runtime-dom'
import {
DeprecationTypes,
warnDeprecation,
} from '../../runtime-core/src/compat/compatConfig'
// 创建了一个对象 叫编译缓存
const compileCache: Record<string, RenderFunction> = Object.create(null)
// template是字符串或者HTML元素
// options的类型CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions,
): RenderFunction {
// 模板不是字符串并且是HTML元素 取出innerHTML
if (!isString(template)) {
if (template.nodeType) {
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
// NOOP是函数返回一个空对象
return NOOP
}
}
const key = template
// 从compileCache对象中查找模板
const cached = compileCache[key]
// 缓存中找到就直接返回模板,找不到就在下面赋值
if (cached) {
return cached
}
// 到这template是一个字符串,因为如果不是字符串在上面已经判断赋值了
// 如果template[0]第一个字符是以#开头,就获取它的HTML结构,将template赋值
if (template[0] === '#') {
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
template = el ? el.innerHTML : ``
}
// 这里是个警告,我就不深入去了解了
if (__DEV__ && !__TEST__ && (!options || !options.whitespace)) {
warnDeprecation(DeprecationTypes.CONFIG_WHITESPACE, null)
}
// TODO 这里是通过compile方法去编译返回一个虚拟DOM
// compile这个方法里面有很多工作,但是不是本章重点,所以先不展开,知道是返回虚拟DOM对象即可
const { code } = compile(
template,
extend(
{
hoistStatic: true,
whitespace: 'preserve',
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP,
} as CompilerOptions,
options,
),
)
// 声明一个报错的方法
function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset,
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}
// 创建一个render函数,__GLOBAL__判断是否是在Node环境下,如果是在浏览器环境下,runtimeDom里面所有的方法会作为参数传进去
const render = (
__GLOBAL__ ? new Function(code)() : new Function('Vue', code)(runtimeDom)
) as RenderFunction
// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true
// 给compileCache缓存对象设置一个key = render的缓存,方便后续直接获取而不用从新赋值了
return (compileCache[key] = render)
}
// 执行registerRuntimeCompiler这个函数
registerRuntimeCompiler(compileToFunction)
// 关键方法 createCompatVue
const Vue: CompatVue = createCompatVue()
Vue.compile = compileToFunction
export default Vue
packages/vue-compat/src/createCompatVue.ts - createCompatVue()
我们从上面的代码知道了vue = createCompatVue()的返回值,所以我们看看createCompatVue()执行了哪些,返回值具体是什么
import { initDev } from './dev'
import {
type CompatVue,
DeprecationTypes,
KeepAlive,
Transition,
TransitionGroup,
compatUtils,
createApp,
vModelDynamic,
vShow,
} from '@vue/runtime-dom'
import { extend } from '@vue/shared'
// 如果是dev环境执行initDev(),我们暂时不考虑深入环境相关内容
if (__DEV__) {
initDev()
}
// 从runtime-dom中导入所有方法命名空间名称为runtimeDom
import * as runtimeDom from '@vue/runtime-dom'
// 看起来像是处理兼容V2和V3的一些内容,我们暂时先不去管它,继续看createCompatVue
function wrappedCreateApp(...args: any[]) {
// @ts-expect-error
const app = createApp(...args)
if (compatUtils.isCompatEnabled(DeprecationTypes.RENDER_FUNCTION, null)) {
// register built-in components so that they can be resolved via strings
// in the legacy h() call. The __compat__ prefix is to ensure that v3 h()
// doesn't get affected.
app.component('__compat__transition', Transition)
app.component('__compat__transition-group', TransitionGroup)
app.component('__compat__keep-alive', KeepAlive)
// built-in directives. No need for prefix since there's no render fn API
// for resolving directives via string in v3.
app._context.directives.show = vShow
app._context.directives.model = vModelDynamic
}
return app
}
// 调用了compatUtils.createCompatVue传入了createApp和处理兼容的wrappedCreateApp方法
export function createCompatVue(): CompatVue {
// compatUtils.createCompatVue这个方法深入到里面最终还是执行createApp这个方法
// 只不过包含了很多兼容v2的方法
const Vue = compatUtils.createCompatVue(createApp, wrappedCreateApp)
extend(Vue, runtimeDom)
return Vue
}
所以到这我们算是发现了createApp方法的来源是来自 @vue/runtime-dom 这个包,那我们就去这个包里看看createApp方法具体做了哪些吧。
由于我们是分析V3的代码,compatUtils.createCompatVue这个里包含了很多兼容V2的处理,最终也是会调用createApp这个方法,所以我这里就不深入去分析和学习了,有兴趣的伙伴可以自己去了解了解。
packages/runtime-dom/src/index.ts - createApp()
到了这一步,createApp只是一个入口,要分析的方法就会很多,让我们一步一步拆解吧
createApp
export const createApp = ((...args) => {
// 这一步执行 相当于下面两步的执行结果调用createApp,关键在于baseCreateRenderer的执行
// createRenderer(rendererOptions) =>
// baseCreateRenderer(options).createApp(...args)
const app = ensureRenderer().createApp(...args)
// 环境判断 先跳过
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
// 从app中获取mount方法
const { mount } = app
// TODO 暂时没有继续分析
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__ && container.nodeType === 1) {
for (let i = 0; i < (container as Element).attributes.length; i++) {
const attr = (container as Element).attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null,
)
break
}
}
}
}
// clear content before mounting
if (container.nodeType === 1) {
container.textContent = ''
}
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
return app
}) as CreateAppFunction<Element>
createRenderer
let renderer: Renderer<Element | ShadowRoot> | HydrationRenderer;
// patchProp 用于更新 DOM 属性和事件的一个函数,处理的内容包括class style等
// nodeOps 是一个对象,包括很多方法,插入 移除 创建Element节点等
const rendererOptions = /*@__PURE__*/ extend({ patchProp }, nodeOps);
// 返回了createRenderer()执行之后的返回值,参数是rendererOptions
function ensureRenderer() {
// 初始renderer是undefine,所以renderer = createRenderer(rendererOptions))
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
createRenderer
packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>): Renderer<HostElement> {
return baseCreateRenderer<HostNode, HostElement>(options)
}
baseCreateRenderer
packages/runtime-core/src/renderer.ts
重头戏来了 这里面有2000多行代码还有和SSR相关的一些代码。本期我们只考虑和createApp相关代码 其余暂时用不到的代码我就先删除,不然2000多行代码 太长了, 影响阅读,后面到创建DOM的时候再分析相应
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions,
): any {
...内容暂时省略
因为代码很多都是render渲染相关的,创建DOM以及DOM的增删改查,我觉得应该放到render那一期
我们只关心返回了createApp方法,createApp方法是createAppAPI执行的结果
// render函数,参数有虚拟DOM,容器和命名空间
const render: RootRenderFunction = (vnode, container, namespace) => {
// 如果没有传入虚拟DOM,那会执行卸载逻辑
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 执行比较方法,放到render渲染和DOM DIFF中讲
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
container._vnode = vnode
// 判断是否刷新逻辑
if (!isFlushing) {
isFlushing = true
flushPreFlushCbs()
flushPostFlushCbs()
isFlushing = false
}
}
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate), // hydrate SSR方法暂时不考虑
}
}
createAppAPI
packages/runtime-core/src/apiCreateApp.ts 终于到了createApp这个方法的尽头
createApp 官网的API
// 已知参数render是渲染方法,第二个参数不考虑
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction,
): CreateAppFunction<HostElement> {
// 是不是不知道rootComponent 和 rootProps是什么了?让我们看看官网API
// rootComponent是根组件, rootProps它是要传递给根组件的 props。
return function createApp(rootComponent, rootProps = null) {
// 如果不是一个函数,那就是一个对象, 就创建一个新的组件构造函数
// 目的是为了方便扩展,且继承原有的rootComponent
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
// 判断rootProps如果有有值 并且 不是对象 抛出警告错误 赋值为null
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 创建一个context上下文,方法具体返回值放到了最下面
// 创建上下文,为了方便阅读,我把这个方法复制到这里了
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {},
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null),
optionsCache: new WeakMap(),
propsCache: new WeakMap(),
emitsCache: new WeakMap(),
}
}
const context = createAppContext()
const installedPlugins = new WeakSet()
const pluginCleanupFns: Array<() => any> = []
let isMounted = false
// 创建一个vue对象
const app: App = (context.app = {
// 唯一ID
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
_instance: null,
version,
// 获取上下文配置
get config() {
return context.config
},
// 禁止修改上下文配置
set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`,
)
}
},
// 提供一个use方法 第一个参数应是插件本身,可选的第二个参数是要传递给插件的选项。
use(plugin: Plugin, ...options: any[]) {
// 如果有 并且是测试环境 就警告,反正我理解就是执行一次 重复安装也就一次
if (installedPlugins.has(plugin)) {
__DEV__ && warn(`Plugin has already been applied to target app.`)
} else if (plugin && isFunction(plugin.install)) {
// 如果没有 并且有插件和install方法,就加到weakset中。执行install方法
installedPlugins.add(plugin)
plugin.install(app, ...options)
} else if (isFunction(plugin)) {
// plugin是函数,执行plugin,传入app和options
installedPlugins.add(plugin)
plugin(app, ...options)
} else if (__DEV__) {
warn(
`A plugin must either be a function or an object with an "install" ` +
`function.`,
)
}
return app
},
mixin(mixin: ComponentOptions) {
if (__FEATURE_OPTIONS_API__) {
if (!context.mixins.includes(mixin)) {
context.mixins.push(mixin)
} else if (__DEV__) {
warn(
'Mixin has already been applied to target app' +
(mixin.name ? `: ${mixin.name}` : ''),
)
}
} else if (__DEV__) {
warn('Mixins are only available in builds supporting Options API')
}
return app
},
component(name: string, component?: Component): any {
if (__DEV__) {
validateComponentName(name, context.config)
}
if (!component) {
return context.components[name]
}
if (__DEV__ && context.components[name]) {
warn(`Component "${name}" has already been registered in target app.`)
}
context.components[name] = component
return app
},
directive(name: string, directive?: Directive) {
if (__DEV__) {
validateDirectiveName(name)
}
if (!directive) {
return context.directives[name] as any
}
if (__DEV__ && context.directives[name]) {
warn(`Directive "${name}" has already been registered in target app.`)
}
context.directives[name] = directive
return app
},
// 关键mount挂载了
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
// 初始默认是是false
if (!isMounted) {
// 跳过
if (__DEV__ && (rootContainer as any).__vue_app__) {
warn(
`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`,
)
}
// 执行createVNode 创建虚拟DOM 并且返回虚拟DOM
const vnode = app._ceVNode || createVNode(rootComponent, rootProps)
// 挂载上下文
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// 跳过
if (__DEV__) {
context.reload = () => {
// casting to ElementNamespace because TS doesn't guarantee type narrowing
// over function boundaries
render(
cloneVNode(vnode),
rootContainer,
namespace as ElementNamespace,
)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 执行render函数,渲染DOM
render(vnode, rootContainer, namespace)
}
// 改成true 下次就不进来了
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = vnode.component
devtoolsInitApp(app, version)
}
// 用公共实例
return getComponentPublicInstance(vnode.component!)
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``,
)
}
},
onUnmount(cleanupFn: () => void) {
if (__DEV__ && typeof cleanupFn !== 'function') {
warn(
`Expected function as first argument to app.onUnmount(), ` +
`but got ${typeof cleanupFn}`,
)
}
pluginCleanupFns.push(cleanupFn)
},
unmount() {
if (isMounted) {
callWithAsyncErrorHandling(
pluginCleanupFns,
app._instance,
ErrorCodes.APP_UNMOUNT_CLEANUP,
)
render(null, app._container)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
app._instance = null
devtoolsUnmountApp(app)
}
delete app._container.__vue_app__
} else if (__DEV__) {
warn(`Cannot unmount an app that is not mounted.`)
}
},
provide(key, value) {
if (__DEV__ && (key as string | symbol) in context.provides) {
warn(
`App already provides property with key "${String(key)}". ` +
`It will be overwritten with the new value.`,
)
}
context.provides[key as string | symbol] = value
return app
},
runWithContext(fn) {
const lastApp = currentApp
currentApp = app
try {
return fn()
} finally {
currentApp = lastApp
}
},
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
// 返回实例
return app
}
}
总结
其实createApp这个方法核心功能就是创建了一个app实例(也叫上下文),并且提供了以下方法
- use
- mixin
- component
- directive
- mount
- onUnmount
- unmount
- runWithContext
最重要的方法就是mount,负责创建虚拟DOM,然后调用render函数进行渲染。
createApp就分析到这里了。下一章 我们去分析 createVNode 看看创建虚拟DOM以及如何渲染DOM到页面。欢迎点赞收藏 ~