vue源码学习(-):vue的初始化
前言: 本人只是一名撸码仔,平时没事在扣扣别人实现的逻辑,这个系列也仅限于我对该学习的一项总结,以供大家来参考,如果有大佬指点一下那求之不得
相对于传统的 jQuery 一把梭子撸到底的开发模式,组件化可以帮助我们实现 视图 和 逻辑 的复用,并且可以对每个部分进行单独的思考。对于一个大型的 Vue.js 应用,通常是由一个个组件组合而成
整个初始化的流程我简单画了一下
1、vue项目的入口
git clone https://github.com/vuejs/core
拉取下来的项目结构,先大体看下结构
然后打开packages的目录看下具体
├── packages
│ ├── compiler-core # 与平台无关的编译器实现的核心函数包
│ ├── compiler-dom # 浏览器相关的编译器上层内容
│ ├── compiler-sfc # 单文件组件的编译器
│ ├── compiler-ssr # 服务端渲染相关的编译器实现
│ ├── global.d.ts # ts 相关一些声明文件
│ ├── reactivity # 响应式核心包
│ ├── runtime-core # 与平台无关的渲染器相关的核心包
│ ├── runtime-dom # 浏览器相关的渲染器部分
│ ├── runtime-test # 渲染器测试相关代码
│ ├── server-renderer # 服务端渲染相关的包
│ ├── sfc-playground # 单文件组件演练场
│ ├── shared # 工具库相关
│ ├── size-check # 检测代码体积相关
│ ├── template-explorer # 演示模板编译成渲染函数相关的包
│ └── vue # 包含编译时和运行时的发布包
我们大体看了目录结构,但是我们应该从何开始看呢?
我这里以我个人的习惯喜欢看package.json这个文件去找入口,接下来我简单说一下这个文件
{
"private": true,
"version": "3.2.45",
"packageManager": "pnpm@7.1.0",
"scripts": {
"dev": "node scripts/dev.js",
"build": "node scripts/build.js",
"size": "run-s size-global size-baseline",
....
}
}
我大概抄了一些内容过来,我们看到scipts中的调试命令:"dev": "node scripts/dev.js",这行命令就是以用node执行 scripts/dev.js这个文件,这里再大概插一嘴,vue源码使用的rollup打包工具,所以我们简单看下这个文件
build({
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
outfile,
bundle: true,
external,
sourcemap: true,
format: outputFormat,
globalName: pkg.buildOptions?.name,
platform: format === 'cjs' ? 'node' : 'browser',
plugins:
format === 'cjs' || pkg.buildOptions?.enableNonBrowserBranches
? [nodePolyfills.default()]
: undefined,
define: {
__COMMIT__: `"dev"`,
__VERSION__: `"${pkg.version}"`,
__DEV__: `true`,
__TEST__: `false`,
__BROWSER__: String(
format !== 'cjs' && !pkg.buildOptions?.enableNonBrowserBranches
),
__GLOBAL__: String(format === 'global'),
__ESM_BUNDLER__: String(format.includes('esm-bundler')),
__ESM_BROWSER__: String(format.includes('esm-browser')),
__NODE_JS__: String(format === 'cjs'),
__SSR__: String(format === 'cjs' || format.includes('esm-bundler')),
__COMPAT__: String(target === 'vue-compat'),
__FEATURE_SUSPENSE__: `true`,
__FEATURE_OPTIONS_API__: `true`,
__FEATURE_PROD_DEVTOOLS__: `false`
},
watch: {
onRebuild(error) {
if (!error) console.log(`rebuilt: ${relativeOutfile}`)
}
}
}).then(() => {
console.log(`watching: ${relativeOutfile}`)
})
不管是webpack还是rollup都有个入口文件属性叫 entry这个字段。接下来我们看下这个属性
/**这里target默认值是vue这个文件夹**/
entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
所以 大概的目录入口找到 了packages/vue/src/index.ts,接下来我们进去看到一句
export * from '@vue/runtime-dom'
从这里可以看到这里终于进入了核心区域
2、初始化一个 Vue 3 应用
我们先来简单初始化一个 Vue 3 的应用:
# 安装 vue cli
$ yarn global add @vue/cli
# 创建 vue3 的基础脚手架 一路回车
$ vue create vue3-demo
接下来,打开项目,可以看到Vue.js 的入口文件 main.js 的内容如下:
<template>
<div class="helloWorld">
hello world
</div>
</template>
<script>
export default {
setup() {
// ...
}
}
</script>
现在我们只需要知道 <script>
中的对象内容最终会和编译后的模板内容一起,生成一个 App
对象传入 createApp
函数中:
{
render(_ctx, _cache, $props, $setup, $data, $options) {
// ...
},
setup() {
// ...
}
}
接着回到 main.js
的入口文件,整个初始化的过程只剩下如下部分了:
createApp(App).mount('#app')
接下来我们来到 runtime-dom文件夹看下这个函数createApp
export const createApp = (...args) => {
console.log(...args);
// 返回的app 中有个mount函数 挂载到根目录上
const app = ensureRenderer().createApp(...args)
if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}
...
return app
}
可以看出 上述的app会返回一个mount的挂载函数,我们在自习观察下app是如何生成的,是通过ensureRenderer().createApp(...args)
ensureRenderer().createApp(...args)
这个链式函数执行完成后肯定返回了 mount
函数,ensureRenderer
就是构造了一个带有 createApp
函数的渲染器 renderer 对象 :
// 输出renderer对象
function ensureRenderer() {
// 判断如果有renderer就输出 没有就创建renderer
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
// renderOptions 包含以下函数:
const renderOptions = {
createElement,
createText,
setText,
setElementText,
patchProp,
insert,
remove,
}
再来看一下 createRenderer
返回的对象:
// packages/runtime-core/src/renderer.ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
export function baseCreateRenderer(options) {
// ...
// 源码里面这里包含了很多函数 比如patch 、 render等等后面 介绍
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
}
由此可见这里就是ensureRenderer() 返回一个renderer的对象
{
render,
hydrate,
createApp: createAppAPI(render, hydrate),
}
其中就包含了createApp的函数,接下来看下createAppAPI
export function createAppAPI<HostElement>(
render: RootRenderFunction<HostElement>,
hydrate?: RootHydrateFunction
): CreateAppFunction<HostElement> {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = { ...rootComponent }
}
if (rootProps != null && !isObject(rootProps)) {
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
// 创建初始化上下文
const context = createAppContext()
const installedPlugins = new Set()
let isMounted = false
const app: App = (context.app = {
_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(plugin: Plugin, ...options: any[]) {
...
},
mixin(mixin: ComponentOptions) {
...
},
component(name: string, component?: Component): any {
...
},
directive(name: string, directive?: Directive) {
...
},
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
console.log(rootContainer);
if (!isMounted) {
// #5571
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.`
)
}
// 创建虚拟dom
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
vnode.appContext = context
// HMR root reload
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
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)
}
console.log( vnode);
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} 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)\``
)
}
},
unmount() {
...
},
provide(key, value) {
...
return app
}
})
if (__COMPAT__) {
installAppCompatProperties(app, context, render)
}
return app
}
}
这个函数是一个高阶函数,并且返回的是一个函数,这个函数的入参第一个值rootComponent就是我们上面<App />
组件作为根组件 ,返回了一个包含 mount
方法的 app
对象。
我们仔细看下mount的实现方式
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
console.log(rootContainer);
if (!isMounted) {
// 创建虚拟dom
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
if (isHydrate && hydrate) {
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
render(vnode, rootContainer, isSVG)
}
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)
}
console.log( vnode);
return getExposeProxy(vnode.component!) || vnode.component!.proxy
} 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)\``
)
}
},
它是先判断是否进行挂载过 isMounted,然后创建虚拟dom,再然后将context挂在到虚拟dom的appContext,具体这里面有什么内容并且是怎么生成出来的,大家可以打印出来调试,最后通过render函数将其进行渲染
本次初始化先讲到这里,后续我也会继续更新自己的文章
开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 4 天,点击查看活动详情