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

207 阅读5分钟

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

前言: 本人只是一名撸码仔,平时没事在扣扣别人实现的逻辑,这个系列也仅限于我对该学习的一项总结,以供大家来参考,如果有大佬指点一下那求之不得

相对于传统的 jQuery 一把梭子撸到底的开发模式,组件化可以帮助我们实现 视图 和 逻辑 的复用,并且可以对每个部分进行单独的思考。对于一个大型的 Vue.js 应用,通常是由一个个组件组合而成

整个初始化的流程我简单画了一下 未命名文件.png

1、vue项目的入口

git clone https://github.com/vuejs/core

拉取下来的项目结构,先大体看下结构

image-20230221174231416.png

然后打开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 天,点击查看活动详情