声明
🔊 本文是开始学习 Vue
源码的第二篇笔记,当前的版本是 2.6.14
。如果对你有一点点帮助,请点赞鼓励一下,如果有错误或者遗漏,请在评论区指出,非常感谢各位大佬。
🔊 代码基本上是逐行注释,由于本人的能力有限,很多基础知识也进行了注释和讲解。由于源码过长,文章不会贴出完整代码,所以基本上都是贴出部分伪代码然后进行分析。
🔊 从本篇文章开始,可能会出现暂时看不懂的地方,是因为还没有学习前置知识,不必惊慌,只需知道存在这样一个知识点,接着向下看,看完了前置知识,回过头来再看这里就一目了然了。
初始化
构造函数
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<Component>) {
/**
* 给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
const opts = 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<Component>) {
//从实例构造函数上获取配置 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<Component>): ?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
选项中,以提高代码的执行效率。
⭐ 如果是根组件,对 options
进行合并,vue
会将相关的属性和方法都统一放到 vm.$options
中。vm.$options
的属性来自两个方面,一个是 Vue
的构造函数 vm.constructor
预先定义的,一个是 new Vue
时传入的入参对象。
第三部分
⭐ initProxy / vm._renderProxy 在非生产环境下执行了 initProxy
函数,参数是实例;在生产环境下设置了实例的 _renderProxy
属性为实例自身。
⭐ 设置了实例的 _self
属性为实例自身。
⭐ initLifecycle 初始化组件实例关系属性 , 比如 $parent
、$children
、$root
、$refs
等 (不是组件生命周期 mounted
, created
…)
⭐ initEvents 初始化自定义事件。
⭐ initRender 初始化插槽 , 获取 this.slots
, 定义 this._c
, 也就是 createElement
方法 , 平时使用的 h
函数。
⭐ callHook 执行 beforeCreate
生命周期函数。
⭐ initInjections 初始化 inject
选项
⭐ initState 响应式原理的核心 , 处理 props
、methods
、computed
、data
、watch
等。
⭐ initProvide 解析组件配置项上的 provide
对象,将其挂载到 vm._provided
属性上。
⭐ callHook 执行 created
生命周期函数。
第四部分
⭐ 如果有 el
属性,则调用 vm.$mount
方法挂载 vm
,挂载的目标就是把模板渲染成最终的 DOM
。
⭐ 不存在 el
的时候不挂载 , 需要手动挂载。
最后我们用一张思维导图总结一下
参考
本文由 李永宁 教程结合自己的想法整理而来,在此特别感谢前辈。