浅羲Vue源码-4-new Vue()那些事儿(1)

这是我参与2022首次更文挑战的第5天,活动详情查看:2022首次更文挑战

一、背景

在上一篇中,我们通过对 Vue 项目中 rollup 的打包分析了一下 vue.js 的入口问题,从而解释了 initGlobalAPI 和 new Vue 的先后关系。这个先后关系决定了,new Vue 时一些属性,比如 Vue.options 的来源。理解这些对象的来源,有利于理解这个 Vue 的执行过程。

本篇小作文立意说一说 new Vue 都发生了什么,这个问题也是一个常见的面试题。

二、代码目录结构

看下 vue 的项目结构,这个是被简化过的文件结构,因为我们只讨论源码,很多子目录都被省略掉了,比如 benchmarks,等。别紧张,只有 src 目录下是源码,其他很多内容暂时不用管。

.
├── benchmarks
├── dist
├── examples
├── flow
├── packages
├── scripts
├── 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 weex 相关,省略
│   ├── server 省略
│   ├── sfc
│   └── shared
├── test
└── types
复制代码

二、断点进入 Vue 源码

2.1 test.html 的断点

我们在 test.htmlnew Vue 之前写了一个 debugger,打开控制台,刷新,就能看到代码停在了这里:

image.png

2.2 进入到源码的入口文件

因为之前我们在第一篇的 浅羲Vue源码-1-准备工作 通过配置 rollup--sourcemap 参数生成了 sourcemap 文件,此时就派上用场了。通过进一步点击断点的按步骤执行,进入到 Vue 构造函数的所在文件:src/core/instance/index.js

image.png

三、Vue 构造函数

3.1 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) // Vue 构造函数就这一行代码
}

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

export default Vue
复制代码

从上面的代码,可以清晰发现,Vue 构造函数只有一行关键代码。但是有个神奇的事情是,并没有见到 _init 方法声明,那么这个方法是在哪里声明的呢?答案是 initMixin

3.2 src/core/instance/init.js 中的 initMixin

前面提到 initMixn 会给 Vue 的构造函数上添加 _init 方法,其具体做法是导出一个函数,该函数接收 Vue 构造函数,然后扩展 _init 方法:

// some import 
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    // .... detail of _init
  }
}
复制代码

四、_init 的逻辑

接下我们分步骤看下 _init 都做了什么事情。

4.1 声明 vm 变量

Vuevm 是一个非常重要的变量,时刻注意,vm 就是 Vue 的实例,即这里的 vm = this

// some import 
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this // vm 是 Vue 的实例
    // a uid
    vm._uid = uid++ // 每个 vue 实例都有一个 _uid,是个自增的数字
    vm._isVue = true // 一个标识符,用于防止被数据观察处理
  }
}
复制代码

4.2 处理组件或者根实例的选项合并

4.2.1 组件选项

组件选项就是我们创建组件时传递的对象,例如在 test.html 中声明的子组件,这就是个组件选项,可以理解为创建 Vue 组件所必须的配置项;

const sub = {
  template: `
    <div style="color: red;background: #5cb85c;display: inline-block">{{ someKey + foo }}</div>`,
  props: {
    someKey: {
      type: String,
      default: 'hhhhhhh'
    }
  },
inject: ['foo']
};
复制代码

4.2.2 根实例选项

new Vue() 时传给构造函数的选项就是根实例选项,例如 test.html 中创建 Vue 实例传入的对象:

new Vue({ // 这对象就是根实例选项
  el: '#app',
  data: {
    msg: 'hello vue'
  },
  hahaha: 'hahahahahha',
  provide: {
    foo: 'bar'
  },
  components: {
    someCom: sub
  }
})
复制代码

4.2.3 合并选项

选项合并时,是将合并过后的选项赋值到 vm.$options 这个属性上,这个属性在贯穿了整个源码阅读,当遇到这个属性时,请谨记本次用到这个属性添加了哪些属性,更新了哪些属性,又或者删除了哪些属性

Vue.prototype._init = function (options?: Object) {
  // ...
  if (options && options._isComponent) {
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
 // ....
}
复制代码

这里用到了几个方法,mergerOptionsresolveConstructorOptions,其中 resolveConstructorOptions 就是从构造函数自身上获取 options 等信息给到实例复用,里暂时不表。

4.3 代理实例属性 _renderProxyvm 自身

当访问到 _renderProxy 时,就是在访问 vm 自身的属性,这个属性在后面处理渲染时有用到。

Vue.prototype._init = function (options?: Object) {
  // ......
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm); // 开发环境用 ES6 的 Proxy 实现的
  } else {
    vm._renderProxy = vm
  }
  // ....
}
复制代码

4.4 执行一系列初始化

Vue.prototype._init = function (options?: Object) {
  // ....
  // 初始化如 Vue 实例上的一些关键属性,如 _inactive/_isMounted/_isDestroyed,
  // 另外还有组件间的关系:$parent/$children/$refs 等
  initLifecycle(vm)
  
  // 初始化组件的自定义事件,比如说在自定义组件上 @some-event 
  initEvents(vm)
  
  // 初始化渲染最重要的方法:vm._c 和 vm.$createElement,
  // 解析组件中的 slot,挂载到 vm.$slots 属性
  initRender(vm)
  
  // 调用 beforeCreate 生命周期钩子
  callHook(vm, 'beforeCreate')
  
  // 初始化组件上的 inject 选项,把这个东西处理成 result[key] = val 的标准形式,
  // 然后对这个 result 做数据响应式处理,代理每个 key 到 vm
  initInjections(vm) // resolve injections before data/props
  
  // 这里是处理数据响应式的重点,后面会单独说,大致是处理 props/methods/data/computed/watch
  initState(vm)
  
  // 解析组件上的 provide,这个 provide 有点像 react 的 Provider,是跨组件深层传递数据的
  // 同样,把解析结果代理到 vm._provide 上;
  // 这里多说一点,为啥先初始化 initInjections 后初始化 initProvide 呢?
  // 他之所以敢这么干是因为 inject 在子组件上,而 provide 在父组件上,而父组件先于子组件被
  // 处理所以当 inject 后初始化没问题,因为他取用的是父组件上的 provide,
  // 此时父组件的 provide 早已经初始化完成了
  initProvide(vm) // resolve provide after data/props
  
  // 调用 created 生命周期钩子
  callHook(vm, 'created')
  // .......
}
复制代码

4.5 el 和 $mount

这里如果 $options.el 属性存在,即挂载点,就执行 $mount 去挂载;

Vue.prototype._init = function (options?: Object) {
  // 如果发现配置项上有  el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,
  // 就不需要再手动调用 $mount,没有 el 就需要手动 $mount
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
复制代码

猜你喜欢

转载自juejin.im/post/7055910776611012645