Vue 源码解析 - 主线流程

[TOC]

  • Vue 学习笔记
  • Vue 源码解析 - 主线流程
  • Vue 源码解析 - 模板编译
  • Vue 源码解析 - 组件挂载
  • Vue 源码解析 - 数据驱动与响应式原理

前言

前面我们对 Vue 的基本使用进行了归纳:Vue 学习笔记,接下来我们对 Vue 的源码进行解读。

:本文所使用的 Vue 源码版本为:Vue 2.6.10

源码调试环境构建

在进行源码阅读之前,可以先搭建下 Vue 的源码调试环境:

  • Vue 调试环境搭建:搭建 Vue 调试环境,可以让我们在浏览器中对 Vue 源码进行单步调试,方便阅读理解源码,具体搭建步骤如下:
  1. 下载 Vue 源码,这里使用当前最新源码:Vue 2.6.10
  2. 在源码package.json中的dev脚本最后添加--sourcemap,用于生成.map文件:
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",
  1. 在源码根目录下,执行npm install,安装package.json指定的依赖。
  2. 执行npm run dev, 这一步 将在dist目录下生成vue.jsvue.js.map两个文件。
  3. 找到样例项目,随便打开一个项目,比如:examples/commits/index.html,将vue.min.js修改为vue.js

  1. 浏览器打开examples/commits/index.html,运行项目。
  2. F12 打开浏览器控制台,点击Sources标签,跳转到源码界面。
  3. Vue 源码的入口位置为:src/core/instance/index.js,随意在该文件内打上一个断点,刷新即可进入调试模式,如下图所示:
Vue 源码解析 - 主线流程_第1张图片
src/core/instance/index.js

Vue 源码目录设计

Vue 的源码都存放在src目录下:

vue/src
├─compiler
│  ├─codegen
│  ├─directives
│  └─parser
├─core
│  ├─components
│  ├─global-api
│  ├─instance
│  │  └─render-helpers
│  ├─observer
│  ├─util
│  └─vdom
│      ├─helpers
│      └─modules
├─platforms
│  ├─web
│  │  ├─compiler
│  │  │  ├─directives
│  │  │  └─modules
│  │  ├─runtime
│  │  │  ├─components
│  │  │  ├─directives
│  │  │  └─modules
│  │  ├─server
│  │  │  ├─directives
│  │  │  └─modules
│  │  └─util
│  └─weex
│      ├─compiler
│      │  ├─directives
│      │  └─modules
│      │      └─recycle-list
│      ├─runtime
│      │  ├─components
│      │  ├─directives
│      │  ├─modules
│      │  └─recycle-list
│      └─util
├─server
│  ├─bundle-renderer
│  ├─optimizing-compiler
│  ├─template-renderer
│  └─webpack-plugin
├─sfc
└─shared

src目录主要包含 6 大类功能代码:

目录 描述
compiler 模板编译(即template转化成render函数)
core 存放通用的,平台无关的运行时代码(核心代码)
server 服务端渲染代码
platforms 平台相关代码
sfc 单文件组件(*.vue)编译解析代码
shared 通用的工具方法

Vue 源码构建

Vue 源码是基于 Rollup 进行构建的,下面我们对其构建过程进行分析。

:Rollup 和 Webpack 都是前端构建工具,Webpack 功能相对更强大,但 Rollup 更轻量。Vue 之所以采用 Rollup 构建发布文件,据尤雨溪本人的回答,是因为最终打包出来的文件会更小,且初始化速度更快。

首先,查看下package.json设置的构建命令:

"scripts": {
    ...
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    ...
}

可以看到,当我们执行构建命令npm run build的时候,其实就是执行node scripts/build.js文件,因此,我们来看下构建脚本scripts/build.js的具体内容:

...
// 读取构建配置文件
let builds = require('./config').getAllBuilds()

// 过滤构建版本
// filter builds via command line arg
if (process.argv[2]) {
  const filters = process.argv[2].split(',')
  builds = builds.filter(b => {
    return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
  })
} else {
  // filter out weex builds by default
  builds = builds.filter(b => {
    return b.output.file.indexOf('weex') === -1
  })
}

// 进行构建
build(builds)

function build (builds) {
    ...
    buildEntry(builds[built])
    ...
}

function buildEntry (config) {
    ...
    return rollup.rollup(config)
    ...
    return write(file, code)
    ...
}

function write (dest, code, zip) {
  return new Promise((resolve, reject) => {
      ...
    fs.writeFile(dest, code, err => {
        ...
    })
...
}

scripts/build.js构建脚本主要做了 3 件事:

  1. 读取配置文件:具体代码为:
// build.js
let builds = require('./config').getAllBuilds()

// config.js
exports.getAllBuilds = () => Object.keys(builds).map(genConfig) 

通过config.js提供的getAllBuilds函数,即可获取到配置信息。而getAllBuilds获取的信息来自builds,我们看下builds的具体内容:

const builds = {
  // Runtime only (CommonJS). Used by bundlers e.g. Webpack & Browserify
  'web-runtime-cjs-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.dev.js'),
    format: 'cjs',
    env: 'development',
    banner
  },
  'web-runtime-cjs-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.common.prod.js'),
    format: 'cjs',
    env: 'production',
    banner
  },
  // Runtime+compiler CommonJS build (CommonJS)
  'web-full-cjs-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.dev.js'),
      ...
  },
  'web-full-cjs-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.common.prod.js'),
      ...
  },
  // Runtime only ES modules build (for bundlers)
  'web-runtime-esm': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.esm.js'),
      ...
  },
  // Runtime+compiler ES modules build (for bundlers)
  'web-full-esm': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.js'),
      ...
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.js'),
      ...
  },
  // Runtime+compiler ES modules build (for direct import in browser)
  'web-full-esm-browser-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.esm.browser.min.js'),
      ...
  },
  // runtime-only build (Browser)
  'web-runtime-dev': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.js'),
      ...
  },
  // runtime-only production build (Browser)
  'web-runtime-prod': {
    entry: resolve('web/entry-runtime.js'),
    dest: resolve('dist/vue.runtime.min.js'),
      ...
  },
  // Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
      ...
  },
  // Runtime+compiler production build  (Browser)
  'web-full-prod': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.min.js'),
      ...
  },
  // Web compiler (CommonJS).
  'web-compiler': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/build.js'),
      ...
  },
  // Web compiler (UMD for in-browser use).
  'web-compiler-browser': {
    entry: resolve('web/entry-compiler.js'),
    dest: resolve('packages/vue-template-compiler/browser.js'),
      ...
  },
  // Web server renderer (CommonJS).
  'web-server-renderer-dev': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.dev.js'),
      ...
  },
  'web-server-renderer-prod': {
    entry: resolve('web/entry-server-renderer.js'),
    dest: resolve('packages/vue-server-renderer/build.prod.js'),
      ...
  },
  'web-server-renderer-basic': {
    entry: resolve('web/entry-server-basic-renderer.js'),
    dest: resolve('packages/vue-server-renderer/basic.js'),
      ...
  },
  'web-server-renderer-webpack-server-plugin': {
    entry: resolve('server/webpack-plugin/server.js'),
    dest: resolve('packages/vue-server-renderer/server-plugin.js'),
      ...
  },
  'web-server-renderer-webpack-client-plugin': {
    entry: resolve('server/webpack-plugin/client.js'),
    dest: resolve('packages/vue-server-renderer/client-plugin.js'),
      ...
  },
  // Weex runtime factory
  'weex-factory': {
    weex: true,
    entry: resolve('weex/entry-runtime-factory.js'),
    dest: resolve('packages/weex-vue-framework/factory.js'),
      ...
  },
  // Weex runtime framework (CommonJS).
  'weex-framework': {
    weex: true,
    entry: resolve('weex/entry-framework.js'),
    dest: resolve('packages/weex-vue-framework/index.js'),
      ...
  },
  // Weex compiler (CommonJS). Used by Weex's Webpack loader.
  'weex-compiler': {
    weex: true,
    entry: resolve('weex/entry-compiler.js'),
    dest: resolve('packages/weex-template-compiler/build.js'),
      ...
  }
}

可以看到,builds提供了所有版本的构建信息,而genConfig函数只是对builds提供的构建信息转换成 Rollup 所需的参数格式而已。

:上述配置信息build中的entry字段表示构建入口的JS文件路径,dest字段表示构建完成的JS文件路径,字段format表示构建文件的格式,其值有如下可选:

  • cjs:表示构建出来的文件遵循 CommonJS 规范。
  • es:表示构建文件遵循 ES Module 规范。
  • umd:表示构建文件遵循 UMD 规范。
  1. 过滤构建文件:可以通过命令行传入参数指定构建版本,否则默认构建除weex以外的所有版本。
  2. 进行构建:使用 Rollup 进行构建,最终构建的版本存放于dist目录和package目录中。
Vue 源码解析 - 主线流程_第2张图片
npm run build

以上,就是 Vue 的整个构建过程。

Vue 源码入口文件

在 Vue 构建完成后,会生成两种 Vue.js 版本:Runtime OnlyRuntime + Compiler

  • Runtime Only:在编译阶段,将.vue等文件编译成.js文件的时候,通常借助如 webpack 的vue-loader工具进行操作,因此,Runtime Only 版本的 Vue.js 无须包含编译部分代码,其体积会更轻量。

  • Runtime + Compiler:如果没有对模板进行预编译,但代码中又使用如template等需要进行编译的模板,则需要在运行时进行编译(在线编译)。因此,Runtime + Compiler 版本的 Vue.js 不仅包含了必须的运行时代码,也包含了编译代码。

// 需要编译器的版本:Runtime + Compiler
new Vue({
    template: '
{{ hi }}
' }) // 不需要运行时编译:Runtime Only new Vue({ render (h) { return h('div', this.hi) } })

以下我们的分析采用的是 Runtime + Compiler 版本。

在上面讲解 Vue 的构建过程中,可以知道,对于 Web 应用,其 Runtime + Compiler 构建出来的 Vue.js 的入口文件为:src/platforms/web/entry-runtime-with-compiler.js,具体代码如下:

// scripts/alias.js
const path = require('path')

const resolve = p => path.resolve(__dirname, '../', p)

module.exports = {
  vue: resolve('src/platforms/web/entry-runtime-with-compiler'),
  compiler: resolve('src/compiler'),
  core: resolve('src/core'),
  shared: resolve('src/shared'),
  web: resolve('src/platforms/web'),
  weex: resolve('src/platforms/weex'),
  server: resolve('src/server'),
  sfc: resolve('src/sfc')
}

// scripts/config.js
const aliases = require('./alias')
const resolve = p => {
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1))
  } else {
    return path.resolve(__dirname, '../', p)
  }
}
const builds = {
    ...
    // Runtime+compiler development build (Browser)
    'web-full-dev': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.js'),
        format: 'umd',
        env: 'development',
        alias: { he: './entity-decoder' },
        banner
    },
    // Runtime+compiler production build  (Browser)
    'web-full-prod': {
        entry: resolve('web/entry-runtime-with-compiler.js'),
        dest: resolve('dist/vue.min.js'),
        format: 'umd',
        env: 'production',
        alias: { he: './entity-decoder' },
        banner
    },
    ...
}

scripts/alias.js文件对一些路径进行了映射,比如web对应真实路径为src/platforms/web,这样在其他文件中可简化路径书写。

下面来看下src/platforms/web/entry-runtime-with-compiler.js源码内容:

// src/platforms/web/entry-runtime-with-compiler.js
import Vue from './runtime/index'
import { compileToFunctions } from './compiler/index'

...
// 缓存 Vue 原型上的 mount 函数
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    ...
    // 对模板进行编译
    const { render, staticRenderFns } = compileToFunctions(template, {...}
    ...
    return mount.call(this, el, hydrating)
}

function getOuterHTML (el: Element): string {
    ...
}

Vue.compile = compileToFunctions

export default Vue

entry-runtime-with-compiler.js主要做了以下三件事:

  • 导入Vue
  • Vue原型上重新定义了一个$mount函数,用于对模板进行编译,最后会重新调用原先定义的$mount函数,进行组件挂载操作。
  • Vue.compile指向函数compileToFunctions

下面依次对上述事件进行阐述。

Vue 源码整体流程

entry-runtime-with-compiler.js首先会导入Vue实例:

import Vue from './runtime/index'

我们循着代码进入./runtime/index.js进行查看:

import Vue from 'core/index'
...
// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
    ...
    if (config.devtools) {
      if (devtools) {
          ...
}

export default Vue

代码很清晰,主要就是做了以下几件事:

  • 安装一些平台相关的工具函数
  • 安装一些平台相关的运行时指令和组件
  • 为浏览器安装patch函数
  • Vue定义挂载函数:这里是第一次在Vue的原型上定义$mount函数,其功能就是进行组件挂载mountComponent。该函数会在entry-runtime-with-compiler.js中被重新定义,增加模板编译功能,成功编译出render函数后,再经由该函数进行组件挂载。
  • 浏览器环境下hook测试工具devtools

./runtime/index.js最重要的功能就是定义了$mount函数进行组件挂载功能:mountComponent,其具体详情参见:Vue 源码解析 - 组件挂载

回到开头,我们可以看到,Vue实例由core/index导入而来,其具体路径为src/core/index.js,我们进入src/core/index.js进行查看:

import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

initGlobalAPI(Vue)

Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

这里主要是对Vue进行了一些全局初始化操作:主要包含以下两方面初始化内容:

  • Vue定义全局静态方法:查看initGlobalAPI源码即可看到定义的静态方法,这里就不深入源码,只列举出initGlobalAPI总共定义的静态方法:
    Vue.util = {
        warn,
        extend,
        mergeOptions,
        defineReactive
    }
    Vue.set = set
    Vue.delete = del
    Vue.nextTick = nextTick
    Vue.observable = (obj: T): T => {...}
    Vue.use = function (plugin: Function | Object) {...}
    Vue.mixin = function (mixin: Object) {...}
    Vue.extend = function (extendOptions: Object): Function {..}
    
  • Vue原型上定义了$isServer$ssrContextFunctionalRenderContext等属性。

再次回到开头,可以看到src/core/index.js./instance/index导入Vue,那我们继续查看./instance/index文件:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

直到这里,我们才终于寻找到Vue的源码定义,并且./instance/index中除了定义Vue之外,还做了很多其他事情:initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)。这些方法都以Mixin结尾,表明其都是通过 Mixin 方式为Vue添加扩展功能(这也是为什么Vue采用函数定义,而不是class的原因),其实质就是在Vue的原型上添加扩展方法,如下所示:

// initMixin(Vue)
Vue.prototype._init = function (...) {...}
// stateMixin(Vue)
Vue.prototype.$set = set
Vue.prototype.$delete = del
Vue.prototype.$watch = function (...){...}
// eventsMixin(Vue)
Vue.prototype.$on = function (...): Component {...}
Vue.prototype.$once = function (...): Component {...}
Vue.prototype.$off = function (...): Component {...}
Vue.prototype.$emit = function (...): Component {...}
// lifecycleMixin(Vue)
Vue.prototype._update = function (...) {...}
Vue.prototype.$forceUpdate = function () {...}
Vue.prototype.$destroy = function () {...}
// renderMixin(Vue)
Vue.prototype.$nextTick = function (...) {...}
Vue.prototype._render = function (): VNode {...}

到这里,Vue源码的粗略完整流程已经分析完毕。其总体流程如下图所示:

Vue 源码解析 - 主线流程_第3张图片
Vue 总体流程

以下将对 Vue 整体流程中相对重要的模块依序进行源码分析。

Vue 实例创建流程

当我们使用new Vue()时,我们来看下整个Vue实例的创建过程:

// src/core/instance/index.js
function Vue(options) {
    ...
    this._init(options)
}

new Vue()时,Vue 内部就只进行了this._init(options)操作,前面我们分析过,this._init函数是在initMixin(Vue)中定义的:

// src/core/instance/init.js
export function initMixin (Vue: Class) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    ...
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) { // 组件对象
      initInternalComponent(vm, options)
    } else { // Vue 实例
        // 将传递进来的选项和 Vue 自带的系统相关的选项进行合并
      vm.$options = mergeOptions(
          // Vue 的内置选项,定义于 web/runtime/index.js 中,Vue.options.directives,Vue.options.components
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    ...
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件处理 
    initEvents(vm)
    // 初始化 render
    initRender(vm)
    // 触发 beforeCreate 钩子函数
    callHook(vm, 'beforeCreate')
    // 解析 options.inject 注入
    initInjections(vm) // resolve injections before data/props
    // 初始化 props、methods、data、computed 与 watch
    initState(vm)
    // 解析 options.provide
    initProvide(vm) // resolve provide after data/props
    // 触发 created 钩子函数
    callHook(vm, 'created')
    ...
    // 如果有传入 el,则进行挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

可以看到,_init函数内部主要是对传递进来的Options对象和Vue自带的指令directives和组件components(这些指令和组件的定义位于web/runtime/index.js中)进行合并,以及很多的初始化操作与钩子函数触发,最后还进行了挂载操作。

_init函数做了很多的事情,我们主要对以下事件进行分析:

  1. initLifecycle(vm):见名知意,该函数用于初始化Vue的生命周期,其源码如下所示:
// core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
    // 获取合并后的选项
    const options = vm.$options

    // locate first non-abstract parent
    let parent = options.parent
    if (parent && !options.abstract) {
      while (parent.$options.abstract && parent.$parent) {
        parent = parent.$parent
      }
      parent.$children.push(vm)
    }

    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm

    vm.$children = []
    vm.$refs = {}

    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false
}

可以看到,initLifecycle函数就是对Vue$parent$root$children$refs_watcher_inactive_directInactive_isMounted_isDestroyed_isBeingDestroyed属性进行了复位操作。

  1. initEvents(vm):见名知意,该函数用于初始化Vue的事件,其源码如下所示:
// core/instance/event.js
export function initEvents (vm: Component) {
    vm._events = Object.create(null)
        vm._hasHookEvent = false
        // init parent attached events
        const listeners = vm.$options._parentListeners
        if (listeners) {
            updateComponentListeners(vm, listeners)
        }
}

主要是对Vue组件的_events_hasHookEvent$options._parentListeners进行复位操作。

  1. initRender(vm):初始化Vue组件的渲染功能。其源码如下所示:
export function initRender (vm: Component) {
  ...
  // 将子元素解析到一个 slot 对象中
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  ...
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  ...
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ...
}

可以看到,initRender函数主要是为Vue组件设置了_c$createElement函数,可用于创建虚拟节点,其中:

  • vm._c是内部版本,主要用于渲染模板代码。
  • vm.$createElement是公有版本,用于用户自定义渲染函数。

这两个函数的底层实现均为createElement函数,该函数主要用于创建虚拟节点,关于该函数相关内容,请参考:Vue 源码解析 - 组件挂载

  1. callHook(vm, 'beforeCreate'):见名知意,该函数会触发beforeCreate钩子。其源码如下所示:
// core/instance/lifecycle.js
export function callHook(vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget();
  const handlers = vm.$options[hook];
  const info = `${hook} hook`;
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info);
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit("hook:" + hook);
  }
  popTarget();
    popTarget()
}

// core/util/error.js
export function invokeWithErrorHandling(
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res;
  try {
    res = args ? handler.apply(context, args) : handler.call(context);
    ...
}

从源码中可以看到,callHook(vm,"beforeCreate")就是通过vm.$option["beforeCreate"]取出我们设置的beforeCreate钩子函数,最后在invokeWithErrorHandling中进行回调,这样,我们的钩子函数就生效了。

  1. initInjections(vm):初始化注入inject事件。其源码如下:
// core/instance/inject.js
export function initInjections(vm: Component) {
  const result = resolveInject(vm.$options.inject, vm);
  if (result) {
    toggleObserving(false);
    Object.keys(result).forEach(key => {
      ...
      defineReactive(vm, key, result[key]);
    });
    toggleObserving(true);
  }
}

export function resolveInject (inject: any, vm: Component): ?Object {
  if (inject) {
      ...
    for (let i = 0; i < keys.length; i++) {
      const key = keys[i]
      ...
      let source = vm
      while (source) {
          ...
          result[key] = source._provided[provideKey]
          ...
        }
        source = source.$parent
      }
      ...
    }
    return result
  }
}

Vue 提供provide/inject事件,允许祖先组件向其子孙组件传递数据,无论子孙组件嵌套多深,都能进行传递。而initInjections就是完成这个功能的。

从源码中可以看到,initInjections首先会取出options.inject字段,然后取出该字段的键值Object.get(key),依次从祖先的_provided字段中取出该键值对应的值,存储到一个新的对象中result,直至遍历结束。最后还会为这些inject的键值进行响应式设置defineReactive,如此便完成了provide/inject功能。

:响应式设置defineReactive的具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理

  1. initState(vm):对一些状态的初始化。其源码如下:
// core/instance/state.js
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  //   初始化 options.props
  if (opts.props) initProps(vm, opts.props);
  //   初始化 options.methods
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    // 初始化 options.data
    initData(vm);
  } else {
    // 没有 options.data 时,绑定为一个空对象
    observe((vm._data = {}), true /* asRootData */);
  }
  //   初始化 options.computed
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    // 初始化 options.watcher
    initWatch(vm, opts.watch);
  }
}

从源码中可以看到,initState函数主要对Vue组件的props,methods,data,computedwatch等状态进行初始化,各初始化具体内容如下:

  • props:由函数initProps进行初始化,其源码如下所示:
// src/core/instance/state.js
function initProps(vm: Component, propsOptions: Object) {
    ...
    // propsOptions 就是 Vue.$options.props
  for (const key in propsOptions) {
    ...
    // 对键进行检测,并返回其对应的值
    const value = validateProp(key, propsOptions, propsData, vm);
    ...
    defineReactive(props, key, value);
    }
    ...
}

// src/core/util/props.js
export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  ...
  // boolean casting
  // 检测是否为 Boolean 类型
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
      ...
      value = true
      ...
    }
  }
  // check default value
  if (value === undefined) {
      // 获取 default 值
    value = getPropDefaultValue(vm, prop, key)
      ...
  }
  ...
  return value
}

initProps其实就是对Vue.$options.props进行解析并设置到Vue的实例属性上,且这些属性具备响应式功能defineReactive

具体的实现步骤就是对Vue.$options.props进行遍历,获取其每个键值,对每个键值的值进行检测validateProp并返回其值,然后进行响应式设置。

  • methods:由函数initMethods进行初始化,其源码如下所示:
// core/instance/state.js
function initMethods(vm: Component, methods: Object) {
   ...
  for (const key in methods) {
    ...
    // 绑定 methods 中的函数到 vm 中
    vm[key] = typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
  }
}

// src/shared/util.js
export function noop (a?: any, b?: any, c?: any) {}

function nativeBind (fn: Function, ctx: Object): Function {
    return fn.bind(ctx)
}

export const bind = Function.prototype.bind ? nativeBind : polyfillBind

initMethods源码比较好读,就是遍历Vue.$options.methods,将每个方法都绑定到Vue实例上(如果methods内的键对应不是一个函数,就绑定到一个空函数noop,否则,就绑定到Vue上:bind(methods[key],vm))。

  • data:由函数initData进行初始化,其内部主要是对Options.data进行了代理(使得Vue实例具备与Options.data相同的键值)和对Options.data的键值进行了响应式设置,具体详情请参考:Vue 源码解析 - 数据驱动与响应式原理
  1. initProvide(vm):解析options.provide,其源码如下所示:
// src/core/instance/inject.js
export function initProvide(vm: Component) {
    const provide = vm.$options.provide
    if (provide) {
        vm._provided = typeof provide === 'function'
            ? provide.call(vm)
            : provide
    }
}

其实就是如果定义了Options.provide遍历,就将其赋值到vm._provided上。

  1. callHook(vm, 'created'):触发created钩子函数。

  2. vm.$mount(vm.$options.el):前面我们讲过,Vue 有两种版本:Runtime OnlyRuntime + Compiler

    对于不同的 Vue 版本,mount函数有不同的实现:

    • 对于 Runtime Only 版本,mount函数只提供 组件挂载 功能
    • 对于 Runtime + Compiler 版本,mount函数提供 模板编译 + 组件挂载 功能

    更多详细内容,请参考:Vue 源码解析 - 模板编译Vue 源码解析 - 组件挂载

到此,整个Vue实例的创建过程就简略

参考

  • 深入vue - 源码目录及构建过程分析
  • Vue.js 技术揭秘

你可能感兴趣的:(Vue 源码解析 - 主线流程)