初始化 构造函数 vue 的本质是一个 构造函数 ,我们 new Vue 的时候,肯定是通过它的构造函数,所以我们先找到它所在的目录 \vue-dev\src\core\instance\index.js 。
\vue-dev\src\core\instance\index.js
/*
- @Author: 一尾流莺
- @Description: Vue实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它。
- @Date: 2021-07-07 17:46:27
- @LastEditTime: 2021-07-09 19:08:26
- @FilePath: \vue-dev\src\core\instance\index.js */
// Vue 构造函数 Vue 只能通过 new 关键字初始化,然后会调用 this._init 方法
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
// instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
// Vue必须是new实例化出来的 es5实现class的方式(通过函数)
!(this instanceof Vue)
) {
// 如果不是Vue的实例走这里
warn('Vue is a constructor and should be called with the new keyword')
}
//Vue.prototype._init方法 该方法是在 initMixin 中定义的,其入参options就是我们定义的对象时传入的参数对象
this._init(options)
}
/**
- 执行xxxMixin方法,初始化相关的功能定义,这里仅仅是定义函数,后面实际用到再分析
- 每一个Mixin都是向Vue的原型上添加一些属性或者方法 */
//合并配置 initMixin(Vue) //初始化 data、props、computed、watcher stateMixin(Vue) //初始化事件中心 eventsMixin(Vue) //初始化生命周期,调用声明周期钩子函数 lifecycleMixin(Vue) //初始化渲染 renderMixin(Vue)
复制代码 代码解读 ⭐ Vue 实际上就是一个用 Function 实现的类,我们只能通过 new Vue 去实例化它,然后会调用 this._init 方法。 ⭐ 为何 Vue 不用 ES6 的 Class 去实现呢?可以看到构造函数的下方执行了很多 xxxMixin 的函数调用,并把 Vue 当参数传入,它们的功能都是给 Vue 的 prototype 上扩展一些方法,Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有,这种方式是用 Class 难以实现的。这么做的好处是非常方便代码的维护和管理,这种编程技巧也非常值得我们去学习。 最后我们用一张思维导图总结一下
init的过程 接下来我们看一下 this._init(options) 发生了什么,_init 方法是在 initMixin 中向 Vue 的原型中添加的。
\vue-dev\src\core\instance\init.js
initMixin / _init /**
- @description: 定义 Vue.prototype._init 方法
- @param {*} Vue Vue 构造函数 */ export function initMixin (Vue: Class) {
/**
- 给Vue的原型上挂载一个_init方法
- 负责 Vue 的初始化过程 */ Vue.prototype._init = function (options?: Object) {
// ```````````````````````````````````````````````````第一部分````````````````````````````````````````````````` // 获取 vue 实例 const vm: Component = this // 每个 vue 实例都有一个 _uid,并且是依次递增的,确保唯一性 vm._uid = uid++ // vue实例不应该是一个响应式的,做个标记 vm._isVue = true
// ```````````````````````````````````````````````````第二部分`````````````````````````````````````````````````
/**
* 处理组件配置项
* 对options进行合并,vue会将相关的属性和方法都统一放到vm.$options中,为后续的调用做准备工作。
* vm.$option的属性来自两个方面,一个是Vue的构造函数(vm.constructor)预先定义的,一个是new Vue时传入的入参对象
*/
if (options && options._isComponent) {
/**
* 如果是子组件初始化时走这里,这里只做了一些性能优化
* 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
*/
initInternalComponent(vm, options)
} else {
/**
* 合并配置项
* 如果是根组件初始化走这里,,合并 Vue 的全局配置到根组件的局部配置,比如 Vue.component 注册的全局组件会合并到 根实例的 components 选项中
* 至于每个子组件的选项合并则发生在两个地方:
* 1、Vue.component 方法注册的全局组件在注册时做了选项合并 (全局API)
* 2、{ components: { xx } } 方式注册的局部组件在执行编译器生成的 render 函数时做了选项合并,包括根组件中的 components 配置 (编译器)
*/
vm.$options = mergeOptions(
// 这里是取到之前的默认配置,组件 指令 过滤器等 也就是构造函数的options
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
// ```````````````````````````````````````````````````第三部分`````````````````````````````````````````````````
//在非生产环境下执行了initProxy函数,参数是实例;在生产环境下设置了实例的_renderProxy属性为实例自身
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
//设置了实例的_self属性为实例自身
vm._self = vm
// 初始化组件实例关系属性, 比如 $parent、$children、$root、$refs 等 不是组件生命周期mounted,created...
initLifecycle(vm)
/**
* 初始化自定义事件,这里需要注意一点,所以我们在 <comp @click="handleClick" /> 上注册的事件,监听者不是父组件,
* 而是子组件本身,也就是说事件的派发和监听者都是子组件本身,和父组件无关
*/
initEvents(vm)
// render初始化 初始化插槽, 获取 this.slots , 定义this._c ,也就是createElement方法,平时使用的 h 函数
initRender(vm)
// 调用创建之前的钩子函数 执行 beforeCreate 生命周期函数
callHook(vm, 'beforeCreate')
// 注入初始化 初始化 inject 选项 得到 {key:val} 形式的配置对象 并对解析结果做响应式处理 ,并代理每个 key 到 vm 实例
initInjections(vm) // resolve injections before data/props
// 数据初始化 响应式原理的核心,处理 props methods computed data watch 等
initState(vm)
// 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
initProvide(vm) // resolve provide after data/props
// 调用创建完成的钩子函数 执行 created 生命周期函数
callHook(vm, 'created')
//通过_init() 可以知道 beforeCreate 生命周期不可以访问数据 因为还没有初始化 但是可以拿到关系属性,插槽,自定义事件
// ```````````````````````````````````````````````````第四部分`````````````````````````````````````````````````
/**
* 判断vm.$options有没有el 如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM
* 存在el则默认挂载到el上 不存在的时候不挂载 需要手动挂载
*/
if (vm.$options.el) {
// 调用 $mount 方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
} } 复制代码 initInternalComponent /**
- @description: 性能优化 把组件传进来的一些配置赋值到vm.$options上 打平配置对象上的属性 减少运行时原型链的查找,提高执行效率
- @param {*} vm 组件实例
- @param {*} options 传递进来的配置 */
export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
//基于组件构造函数上的配置对象 创建vm.options = Object.create(vm.constructor.options)
//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∧ const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode
const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag
//```````````````把组件传进来的一些配置赋值到vm.$options上````````````````````∨
//如果有 render 函数, 将其赋值到vm.$options if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } } 复制代码 resolveConstructorOptions /**
- @description: 解析实例constructor上的options属性,并合并基类选项
- @param {*} Ctor 实例构造函数
- @return {*} options 配置选项 */
export function resolveConstructorOptions (Ctor: Class) { //从实例构造函数上获取配置 options let options = Ctor.options if (Ctor.super) { /** * Ctor.super是通过Vue.extend构造子类的时候。Vue.extend方法会为Ctor添加一个super属性,指向其父类构造器 * 如果构造函数上有super 说明Ctor是Vue.extend构建的子类 换句话说就是检查是否有父级组件 * 然后再用递归的方式获取基类上的配置选项,也就是获取所有上级的options合集 */ const superOptions = resolveConstructorOptions(Ctor.super)
// Ctor.superOptions:父级组件的options Vue构造函数上的options,如directives,filters,....
const cachedSuperOptions = Ctor.superOptions
if (superOptions !== cachedSuperOptions) {
// 如果父级组件被改变过,更新superOption
Ctor.superOptions = superOptions
// 检查 Ctor.options 上是否有任何后期修改/附加选项
const modifiedOptions = resolveModifiedOptions(Ctor)
if (modifiedOptions) {
//如果存在被修改或增加的选项,则合并两个选项
extend(Ctor.extendOptions, modifiedOptions)
}
// 选项合并,将合并结果赋值为 Ctor.options
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
if (options.name) {
options.components[options.name] = Ctor
}
}
} //当Ctor.super不存在时,如通过new关键字来新建Vue构造函数的实例 直接返回基础构造器的options return options }
复制代码 resolveModifiedOptions /**
- @description: 检查是否有任何后期修改/附加选项
- @param {*} Ctor 实例构造函数
- @return {*} modified */
function resolveModifiedOptions (Ctor: Class): ?Object { // 声明修改项 let modified // 获取构造函数选项 const latest = Ctor.options // 密封的构造函数选项,备份 const sealed = Ctor.sealedOptions // 对比两个选项,记录不一致的选项 for (const key in latest) { if (latest[key] !== sealed[key]) { if (!modified) modified = {} modified[key] = latest[key] } } //返回修改项 return modified }
复制代码 代码解读 通过代码,我们把 _init 的过程分成四个部分进行分析。这里暂时先知道干了这么些事情,具体的代码后面会详细分析。 第一部分 ⭐ 每个 vue 实例都有一个 _uid,并且是依次递增的,确保唯一性。 ⭐ vue 实例不应该是一个响应式的,做个标记。 第二部分 ⭐ 如果是子组件,将组件配置对象上的一些深层次属性放到 vm.options 中。vm.parent、root、mount 方法挂载 vm ,挂载的目标就是把模板渲染成最终的 DOM。 ⭐ 不存在 el 的时候不挂载 , 需要手动挂载。 最后我们用一张思维导图总结一下
参考 Vue.js 技术揭秘 精通 Vue 技术栈的源码原理 本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。
作者:一尾流莺 链接:juejin.cn/post/698458… 来源:掘金 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。