vue源码学习(-):vue的初始化

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><divclass="helloWorld">helloworld</div></template><script>exportdefault {setup() {// ... }}</script>

现在我们只需要知道 <script> 中的对象内容最终会和编译后的模板内容一起,生成一个 App 对象传入 createApp 函数中:

{render(_ctx, _cache, $props, $setup, $data, $options) { // ... },setup() {// ... }}

以上就是vue在初始化的第一步的操作

接着回到 main.js 的入口文件,整个初始化的过程只剩下如下部分了:

createApp(App).mount(#app)

接下来我们来到 runtime-dom文件夹看下这个函数createApp

exportconstcreateApp= (...args) => {console.log(...args);// 返回的app 中有个mount函数 挂载到根目录上constapp=ensureRenderer().createApp(...args)if (__DEV__) {injectNativeTagCheck(app)injectCompilerOptionsCheck(app) }...returnapp}

可以看出 上述的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.tsexportfunctioncreateRenderer<HostNode=RendererNode,HostElement=RendererElement>(options: RendererOptions<HostNode, HostElement>) {returnbaseCreateRenderer<HostNode, HostElement>(options)}exportfunctionbaseCreateRenderer(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函数将其进行渲染

本次初始化先讲到这里,后续我也会继续更新自己的文章

举报/反馈