Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

    • UI 框架

      • Angular

      • React

      • Solid

      • Svelte

      • Vue

        • Vue 项目中的 data-v-xxx 是怎么生成的
        • Vue之从零编写一个ContextMenu(右键菜单)插件
        • Vue 第一个组件,浏览器后退无法触发beforeRouteLeave的问题与解决
        • Vue问题记录
        • vue 中 updated 的执行时机
        • 源码解析

          • Vue 源码解析-0.前置准备
          • Vue实例化过程
            • 前言
            • 寻找 Vue 构造方法
            • Vue 的初始化
              • initMixin
              • stateMixin
              • eventsMixin
              • lifecycleMixin
              • renderMixin
            • Vue 实例化
            • 参考
          • Vue响应式原理
          • Vue响应式之data
          • Vue响应式之props
          • Vue响应式之computed
          • Vue响应式之watch
          • Vue响应式之异步更新与nextTick
          • Vue响应式之组件更新
      • 框架本质

    • 开发框架

    • 组件库

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 应用框架
  • UI 框架
  • Vue
  • 源码解析
gahing
2019-11-05
目录

Vue实例化过程

# 前言

上文讲述了 Vue 项目的文件结构以及调试相关的知识,本章将开始正式进入源码解析的环节。

我们从 Vue 实例化开始分析,即:当我们 new Vue(xx) 的时候,内部都做了什么

# 寻找 Vue 构造方法

要分析 Vue 的实例化,得先分析 Vue 在实例化前都做了什么,我们需要先找到构造函数

上文提到,进行 rollup 打包的时候,以 src/platforms/web/entry-runtime-with-compiler.js 为入口,我们从该文件入手,发现导入了 Vue

// src/platforms/web/entry-runtime-with-compiler.js

import Vue from './runtime/index'
1
2
3

点进去

// src/platforms/web/runtime/index.js

import Vue from 'core/index'
1
2
3

在 core/index 中对 Vue 方法加了一些属性,继续深入

// src/core/index.js

import Vue from './instance/index'
1
2
3

继续进入,找到了 Vue 构造函数

// src/core/instance/index.js

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

和我们前一篇结构目录分析的一样, core/instance 包含 Vue 构造函数的代码

当我们 new Vue() 的时候,实际调用的就是这个构造函数

# Vue 的初始化

当我们进行 import Vue from 'vue' 的时候,发生了什么?

在回答这个问题之前,我们需要知道的是,vue 目前有 web 和 weex 两个平台,每个平台包括 compiler, runtime 和 server 三个部分,每个部分会注入一些特定于平台的模块/实用程序到 vue 的核心实现中。

先分析核心部分,不同平台和版本最终都是引入核心实现 src/core/index.js 导出的 vue

// 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'

/**
 * 全局 API:给 Vue 挂载一些属性和方法,如
 * Vue.config
 * Vue.nextTick 等等
 * 注意,Vue.component 等一些方法没有显示定义,在 initAssetRegisters 进行定义,
 * 其效果为 this.options[type + 's'][id] = definition
 * 因此 Vue.options.components 上会新增一个全局组件
 * 
 * 可以在 https://cn.vuejs.org/v2/api/?#%E5%85%A8%E5%B1%80-API 查看详细
 */
initGlobalAPI(Vue)

// ssr 相关的属性和方法,不具体分析

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

一开始执行的 import Vue from './instance/index' ,其中定义了 Vue 构造函数,并对 Vue 进行了一些 Mixin 操作

从以下入手

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
1
2
3
4
5

# initMixin

// src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // 略
  }
}
1
2
3
4
5
6
7

作用:为 Vue 原型设置 _init 方法,该方法会在实例化的时候调用,该方法的具体操作 实例化 的时候会细讲

# stateMixin

// src/core/instance/state.js

export function stateMixin (Vue: Class<Component>) {
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = ...
}
1
2
3
4
5
6
7
8
9

作用:如上所示,挂载了一些数据响应相关的属性和方法到 Vue.prototype ,等用到的时候再具体分析

# eventsMixin

// src/core/instance/event.js

export function eventsMixin (Vue: Class<Component>) {
  Vue.prototype.$on = ...
  Vue.prototype.$once = ...
  Vue.prototype.$off = ...
  Vue.prototype.$emit = ...
}
1
2
3
4
5
6
7
8

作用:如上所示,挂载了一些事件监听触发的方法到 Vue.prototype

# lifecycleMixin

// src/core/instance/lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  // 更新 vdom
  Vue.prototype._update = ...
  // 调用监听器的更新方法
  Vue.prototype.$forceUpdate = ...
  Vue.prototype.$destroy = ...
}
1
2
3
4
5
6
7
8
9

作用:如上所示,挂载了一些生命周期的方法到 Vue.prototype

# renderMixin

// src/core/instance/render.js

export function renderMixin (Vue: Class<Component>) {
  // 添加工具方法到原型中,如 Vue.prototype._n = toNumber
  installRenderHelpers(Vue.prototype)
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  // 生成 vnode
  Vue.prototype._render = ...
}
1
2
3
4
5
6
7
8
9
10
11

作用:如上所示,挂载了一些方法到 Vue.prototype


接着分析平台特定版本差异。

server 和 compiler 版本最后单独打包,这里我们就不分析了。

我们主要分析 web 的 runtime 和 runtime-with-compiler(完整版) 的差异

使用上的不同可以参看官方文档 (opens new window),简单介绍下:

  • 完整版:同时包含编译器和运行时的版本。

  • 编译器:用来将模板字符串编译成为 JavaScript 渲染函数的代码。

  • 运行时:用来创建 Vue 实例、渲染并处理虚拟 DOM 等的代码。基本上就是除去编译器的其它一切。

进行源码分析,两者都是

// src\platforms\web\entry-runtime.js
// src\platforms\web\entry-runtime-with-compiler.js

import Vue from './runtime/index'
1
2
3
4

在 ./runtime/index 进行的操作如下:

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// web 平台相关配置
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// 安装 web 平台 runtime 版本的指令:model  show
extend(Vue.options.directives, platformDirectives)
// 安装 web 平台 runtime 版本的组件:Transition 相关
extend(Vue.options.components, platformComponents)

// 安装 vdom patch(修改 vdom 更新视图) 方法
Vue.prototype.__patch__ = inBrowser ? patch : noop

// 挂载 mount 方法
Vue.prototype.$mount = function (
  // 挂载的元素
  el?: string | Element,
  // 服务端渲染相关参数
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// 省略 devtools 工具提示部分

export default Vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48

$mount 主要调用的是 src\core\instance\lifecycle.js 的 mountComponent 方法,粗略分析如下:

// src\core\instance\lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 没有 render 属性则赋值一个 创建空 VNode 的方法
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
   // ... 提示相关
  }

  // 调用 beforeMount 钩子
  callHook(vm, 'beforeMount')

  let updateComponent
  
  // ... 插件相关

  // vm._render() 创建一个 vnode
  // _update 进行 vdom 更新
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  // 实例化 Watcher
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    // 调用 mounted 钩子
    callHook(vm, 'mounted')
  }
  return vm
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

对于 rumtime 版本,直接将上述的 Vue 导出,而完整版增加了如下处理:

// src\platforms\web\entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  // 挂载的元素
  el?: string | Element,
  // 服务端渲染相关参数
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // 解析 template/el 并将其转化为 render 函数
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    // template 为 html 片段
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 设置 render 方法
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 执行原来的 $mount
  return mount.call(this, el, hydrating)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

相比 runtime 版本,完整版增加了将 template/el 转化为 render 函数等手段

注意:如果完整版提供了 render 函数,那就不会去解析 template/el

至此,Vue 的初始化告一段落。

一般我们写的项目,都是用单文件组件 .vue ,其中的 template 会被 vue-loader 解析成 render 函数.

一开始 Vue 实例化的时候应该避免传入 template 参数,如

new Vue({
  el: '#app',
  router,
  store,
  components: { App },
  template: '<App/>'
})
1
2
3
4
5
6
7

为了解析 template ,导致需要用完整版的 vue , 改成如下写法,只需要用 runtime 版本的 vue

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");
// 或 
new Vue({
  el: '#app',
  router,
  store,
  render: h => h(App)
});
1
2
3
4
5
6
7
8
9
10
11
12

# Vue 实例化

当我们执行 new Vue() 的时候,会发生什么?

再贴下 Vue 构造函数的代码。

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)
}
1
2
3
4
5
6
7
8

可以看到,new Vue(options) 的时候,会将传入的配置作为参数调用 _init 原型方法

上文已经说了,_init 方法是在 initMixin 时挂载的,我们进入查看

// src\core\instance\init.js

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  
  // ...
  // 合并 options
    if (options && options._isComponent) {
      // 优化内部组件实例
      // 因为合并动态选项非常慢,并且内部组件选项都不需要特殊处理。
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
  // ...

  // 挂载 $parent, $root, $children, $refs, 父组件 $children.push(vm)
  initLifecycle(vm)
  // 初始化父组件在 vm 上添加的事件
  initEvents(vm)
  // 实例上挂载 $slots, $scopedSlots, $attrs, $listeners, $createElement
  initRender(vm)
  // 调用 beforeCreate 钩子
  callHook(vm, 'beforeCreate')
  // 在 data/props 前处理 injections
  initInjections(vm)
  // 挂载 props/methods/data/computed/watcher
  initState(vm)
  // 在 data/props 后处理 provide
  initProvide(vm) 
  // 调用 created 钩子
  callHook(vm, 'created')

  // ...

  // 提供 el 参数的话,执行 $mount 方法挂载组件,上文已经分析了 $mount 方法了
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

需要注意的几点:

  • injections 在 data/props 前处理,因此 data/props 可以使用 injections 的值初始化
  • provide 在 data/props 后处理,因此 provide 中可以使用 data/props 的值
  • initState 上属性的初始化顺序为: props -> methods -> data -> computed -> watch, 只有前面的初始化完了才能进行访问,比如 data 可以访问 methods 而 props 不能
  • methods 中定义的方法绑定了 vm 上下文: bind(methods[key], vm)
  • prop 的 validator 直接调用 validator(value) 不绑定 vm, 因此其中访问不到 injections;validator 验证失败时仅进行 warn 提示

最后上一段 data 进行响应式处理的代码, 抛个开头,后面文章会讲响应式

function initData (vm: Component) {
  let data = vm.$options.data
  // 获取 data 方法返回值
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  // Object.prototype.toString.call(data) ==== '[object Object]'
  // data 不是严格意义上的对象,进行警告
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

  // ...属性冲突检测

  // data 属性代理 vm.a 等价于访问 vm._data.a
  proxy(vm, `_data`, key)

  // 观察 data 
  observe(data, true /* asRootData */)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

注意,实例化 Vue 的时候,data 是可以传对象的,而组件中,data 传 返回对象的方法

那么,组件中的 data 解析是在哪做的呢?

以 Vue.component 全局注册为例

Vue.component('select2', {
  template: '#select2-template',
  data: {},
})
1
2
3
4

前面分析过了, Vue.component 在 initAssetRegisters 中进行定义

// src\core\global-api\assets.js

// definition 为传入的第二个参数
if (type === 'component' && isPlainObject(definition)) {
  definition.name = definition.name || id
  // 断点进入该方法
  definition = this.options._base.extend(definition)
}
1
2
3
4
5
6
7
8

在 extend 中,处理如下(省略部分代码):

// src\core\global-api\extend.js

Vue.extend = function (extendOptions: Object): Function {
  const Super = this
  // VueComponent 继承 Vue
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  // 合并参数
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // 一些优化,可以跳过
  // 对于 props 和 computed 属性,在拓展的原型上定义 proxy getters
  // 避免为每个创建的实例调用 Object.defineProperty
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }
  // Sub 静态方法复用 Vue 的
  Sub.extend = Super.extend
  // ...
  
  return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

在 mergeOptions 的时候出错,因为在 mergeField("data") 会调用 strats.data 方法,其中对 data 参数进行 function 的判断

至此,分析结束。平时写的 .vue 单文件组件,会解析成一个对象,当其进行局部注册的时候和上面类似

后面会专门写一篇关于Vue组件实例化的

# 参考

  1. new Vue() 的时候发生了什么 (opens new window)
  2. Vue源码阅读 - 文件结构与运行机制 (opens new window)
编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
Vue 源码解析-0.前置准备
Vue响应式原理

← Vue 源码解析-0.前置准备 Vue响应式原理→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式