浅析new Vue()时发生了什么

  在项目中我们引入了 Vue:import Vue from 'vue'。那么问题是vue到底从哪里来的?从node_modules中来。在node_modules路径下存在vue文件夹,vue文件夹中存在一个package.json文件。在这个文件中存在两个配置字段,它们都是程序的主入口文件。

"main": "dist/vue.runtime.common.js",
"module": "dist/vue.runtime.esm.js",

  其中module的优先级大于main的优先级。在module不存在时,main对应的配置项就是主入口文件。可以看到 dist/vue.runtime.esm.js 才是主入口文件。

  为了方便,我们可以去GitHub上下载vue的源代码到本地查看 https://github.com/vuejs/vue,下载完成后,我们在编辑器打开,它的目录结构如下:

  其中 Vue.js 的源码都在 src 目录下,源码的目录结构如下:

src 
├── compiler              # 编译相关 
├── core                  # 核⼼代码 
├── platforms             # 不同平台的⽀持 
├── server                # 服务端渲染 
├── sfc                   # .vue ⽂件解析 
├── shared                # 共享代码

一、new Vue 过程解析

1、Vue 构造函数

  当 new Vue(options) 时调用的是 src/core/instance/index.js 文件中的Vue函数,源码如下:

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) { //vue 函数
  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实例时候,会判断如果当前的环境不是生产环境,并且如果在调用Vue的时候,没有用new操作符,就会调用warn函数,抛出一个警告,告诉你Vue是一个构造函数,需要用new操作符去调用。这个warn函数并不是单纯的console.warn。

  接下来,把 options 作为参数调用 _init 方法。options 就是调用 new Vue时候传入的参数。可以看到Vue构造函数的核心代码只有一行:this._init(options)

  在Vue构造函数后边,还有几句代码:

function Vue (options) {
  ...
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

  在Vue的构造函数定义之后,有一系列方法会被立即调用。这些方法主要用来给Vue函数添加一些原型属性和方法的。然后我们去看一下在构造函数中调用的 _init()

2、Vue.prototype._init

  上边我们可以看到 Vue 只能通过 new 关键字初始化,然后会调⽤this._init⽅法, 该方法在 src/core/instance/init.js 中定义。

export function initMixin (Vue: Class<Component>) {
    Vue.prototype._init = function (options?: Object) {
       // 首先缓存当前的上下文到 vm变量中,方便之后调用
       const vm: Component = this
       // 然后设置_uid属性。_uid属性是唯一的
       // 当触发init方法,新建Vue实例时(当渲染组件时也会触发)uid都会递增
       vm._uid = uid++
       let startTag, endTag
       
       if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
         startTag = `vue-perf-start:${vm._uid}`
         endTag = `vue-perf-end:${vm._uid}`
         mark(startTag)
       }
       // a flag to avoid this being observed
       vm._isVue = true
       // merge options
       // 有子组件时,options._isComponent才会为true
       if (options && options._isComponent) {
         // optimize internal component instantiation(实例)
         // since dynamic options merging is pretty slow, and none of the
         // internal component options needs special treatment.
         // 优化组件实例,因为动态选项合并很慢,并且也没有组件的选项需要特殊对待
         // 优化components属性
         initInternalComponent(vm, options)
       } else {
         // 传入的options和vue自身的options进行合并
         vm.$options = mergeOptions(
           resolveConstructorOptions(vm.constructor),
           options || {},
           vm
         )
       }
       /* istanbul ignore else */
       if (process.env.NODE_ENV !== 'production') {
         initProxy(vm)
       } else {
         vm._renderProxy = vm
       }
       // expose real self
       vm._self = vm
       initLifecycle(vm) // 初始化一些和生命周期相关的内容
       initEvents(vm)  // 初始化事件相关属性,当有父组件的方法绑定在子组件时候,供子组件调用
       initRender(vm) // 添加slot属性
       callHook(vm, 'beforeCreate') // 调用beforeCreate钩子
       initState(vm) // 初始化数据,进行双向绑定 state/props
       initProvide(vm) // resolve provide after data/props 注入provider的值到子组件中
       callHook(vm, 'created') // 调用created钩子
    
       /* istanbul ignore if */
       // 计算覆盖率时忽略下列代码
       if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
         vm._name = formatComponentName(vm, false)
         mark(endTag)
         measure(`vue ${vm._name} init`, startTag, endTag)
       }
       if (vm.$options.el) {
         vm.$mount(vm.$options.el) // 把模板转换成render函数
       }
     }
}

  可以看到 _init 方法中主要做了下边几件事情:

(1)合并配置:_init 的参数 options 就是 new Vue(options) 时传入的选项,在这里通过 mergeOptions 方法把 options 合并到 vm.$options 中。合并配置项,比如路由,状态管理,渲染函数

new Vue({
  store: store,
  router: router,
  render: h => h(App),
}).$mount('#app')

 

(2)调用 initLifecycle(vm)、initEvents(vm)、initRender(vm)、initState(vm) 函数进行生命周期的初始化、事件中心的初始化、渲染的初始化、data、props、computed、watch的初始化等等。

  其中 initState 就是将vue实例中的data、props、computed、watch等数据项做进一步得处理,其实就是做代理以及转化成可观测对象。

(3)另外,我们可以看到在 initState(vm) 执行之前,我们执行了 beforeCreate 方法,在 initState(vm) 执行之后,我们执行了 created 方法。因此在 beforeCreate 方法中,我们无法直接引用data,method,computed,watch等在 initState(vm) 中才开始存在的属性。

(4)检测是否有el属性,有的话就调用 vm.$mount(vm.$options.el) 进行挂载渲染成真实dom。

3、initState方法如下:

  从源码可以看出,initState就是将vue实例中的data,method,computed,watch等数据项做进一步得处理,其实就是做代理以及转化成可观测对象。

  数据处理完成之后就将数据挂载到指定的钩子上:vm.$mount(vm.$options.el);

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

二、小结

1、vue只能通过new关键字初始化,new Vue() 时做了什么呢?

2、合并传入的options

3、初始化生命周期

4、初始化事件中心

5、初始化渲染

6、执行 beforeCreate 钩子

7、初始化data

8、初始化props

9、初始化computed

10、初始化watch

11、执行 created 钩子

12、最后检测是否有el属性,有的话就把上面渲染好的代码挂载到页面上

  放一张很流行解说vue数据响应式的图儿

  那么 new Vue() 实际上就是做的:new Vue() -- init -- $mount 间的过程

posted @ 2018-06-18 16:07  古兰精  阅读(12354)  评论(0编辑  收藏  举报