前言
本篇通过注释源码的方式,不说没用的废话。带你一字一句的学习Vue2版本源码。有过Vue2项目经验,然后在搭配Vue的源码一起阅读最好。
源码模块中的重要方法
1.initMixin()
js
function initMixin(Vue: typeof Component) {
// 在vue实例中添加_init方法,每个 Vue 实例在被创建时都会调用的方法 !!!
// Record<Keys, Type> 工具类型
Vue.prototype._init = function (options?: Record<string, any>) {
const vm: Component = this
// 计数器
vm._uid = uid++
let startTag, endTag
// 简单理解为测试性能的即可,先不要过度纠结
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// 定义这个属性的作用是为了在后面判断一个对象是不是Vue实例,起到一个标记的作用
vm._isVue = true
// 定义这个属性用来防止 Vue 实例被转换成响应式对象。 因为Vue是让别的对象响应式的,而不是把自己响应式。
// 设置为true,就代表这是一个不需要被响应式的对象。
vm.__v_skip = true
// 创建一个副作用的实例
vm._scope = new EffectScope(true /* detached */)
// vm._scope 是一个 EffectScope 实例,它用来管理和组织 Vue 实例中的副作用。_vm 属性是一个标记,
// 表示这个 EffectScope 实例是属于一个 Vue 实例的。
/**
* 目的:
* 这样做的目的是为了在后续的代码中,可以通过检查 EffectScope 实例的 _vm 属性,
* 来判断这个 EffectScope 实例是否属于一个 Vue 实例。
* 这在某些情况下,例如在处理嵌套组件或者组件的销毁过程中,会非常有用。
*/
vm._scope._vm = true
/**
* 如果 options 存在,并且 options._isComponent 为 true,
* 调用 initInternalComponent 初始化内部组件。(这个函数在下面有讲解)
* 否则,调用 mergeOptions 和 resolveConstructorOptions 函数来合并和解析选项,
* 然后将结果赋值给 $options 属性。
* 这个 $options 属性包含了 Vue 实例的终极版本,
* 是用户提供的选项和默认选项的合并结果!!!
* 这么解释应该通俗吧。读到这的时候脑补一下你们写的代码。
*/
if (options && options._isComponent) {
initInternalComponent(vm, options as any)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor as any),
options || {},
vm
)
}
/**
* 如果在开发模式下(__DEV__ 为 true),
* 会调用 initProxy 函数来创建一个代理对象,并将这个代理对象赋值给 _renderProxy。
* 这个代理对象用来在渲染函数中捕获对未定义属性的访问,以便在开发者访问未定义的属性时发出警告。
* (这个警告由 Vue 在运行时发出的,它会在浏览器的控制台中显示。
* 当你在开发模式下运行 Vue 应用,并且试图访问或设置一个不存在的属性时,
* Vue 会通过代理对象捕获这个操作,并在浏览器的控制台中发出一个警告,告诉你这个属性是未定义的。)
* 否则 生产模式下,不需要这个代理对象,所以直接将 Vue 实例本身赋值给 _renderProxy。
* 总结一下就是,都在生产环境中了,我还给你提示干啥?提示了,你是能改还是咋滴??
*/
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// 这个也是吧实例赋值。既然赋值了,后面肯定会用到,不解释了怕刹不住
vm._self = vm
// 用来初始化 Vue 实例的生命周期相关的属性,例如 $parent、$children ...
initLifecycle(vm)
// 用来初始化 Vue 实例的事件系统,例如 设置父组件传递的事件监听器...
initEvents(vm)
// 用来初始化 Vue 实例的渲染函数,创建 createElement()
initRender(vm)
// 触发 beforeCreate 生命周期钩子。这就是我们所熟知的 此钩子会在 Vue 实例创建后,在数据观察和事件/观察者设置之前被调用。
callHook(vm, 'beforeCreate', undefined, false /* setContext */)
// 用来解析并设置 inject 选项。inject 选项用来从祖先组件接收数据。
initInjections(vm)
// 初始化 Vue 实例的状态,例如 props、methods、data ....
initState(vm)
// 解析并设置 provide 选项。provide 选项用来向子孙组件提供数据。
initProvide(vm)
// 触发 created 生命周期钩子。这个钩子会在 Vue 实例创建完成后被调用,
// 此时,实例已经完成了以下的配置:数据观察(data observer),属性和方法的运算,watch/event 事件回调。
// 然而,挂载阶段还没开始,$el 属性目前不可见。
callHook(vm, 'created')
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 如果 vm.$options.el 存在,那么会调用 vm.$mount(vm.$options.el) 来挂载 Vue 实例。
// 当然了,如果不存在,就需要手动挂载了
/**
* 举个例子,说一下什么叫自动挂载和手动挂载
* 自动挂载: 创建 Vue 实例时,如果提供了 el 选项,Vue 会在内部自动调用 vm.$mount 方法来挂载实例
* new Vue({
el: '#app',
render: h => h(App)
})
手动挂载:建 Vue 实例时,如果没有提供 el 选项,那么 Vue 实例会处于未挂载状态,需要我们手动调用 vm.$mount 方法来挂载
new Vue({
render: h => h(App),
}).$mount('#app')
所以,我们使用脚手架做vue项目的时候,一般用手动挂载。
*/
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
2.initInternalComponent
js
function initInternalComponent( vm: Component, options: InternalComponentOptions ) {
// 先介绍一下这句代码的意思,省流:做个浅拷贝。
// 1.给 vm.$options 赋值。赋值的时候会创建一个新的对象,
// 这个新的对象的原型是 vm.constructor.options,然后将这个新的对象赋值给 vm.$options
// 2.然后在把vm.$options 赋值给opts常量。ok,我在这个函数中操作opts就相当于操作vm.$options了。
// 知识点:由于 JavaScript 的变量查找是从内到外的,所以使用局部变量 opts 可以比直接使用vm.$options更快
const opts = (vm.$options = Object.create((vm.constructor as any).options))
const parentVnode = options._parentVnode
opts.parent = options.parent // 父组件实例
opts._parentVnode = parentVnode// 父vnode
const vnodeComponentOptions = parentVnode.componentOptions!
opts.propsData = vnodeComponentOptions.propsData // 组件传递的 props 数据
opts._parentListeners = vnodeComponentOptions.listeners // 父组件过来的事件监听器
opts._renderChildren = vnodeComponentOptions.children // 渲染的子节点
opts._componentTag = vnodeComponentOptions.tag // 组件标签名
// 如果 options 参数中包含 render 属性,那么将 render 和 staticRenderFns 属性赋值给新的 $options 对象
if (options.render) {
opts.render = options.render
// options.staticRenderFns 是啥?
// 如果模板中有一些静态的部分,例如一些不会改变的文本或者标签,
// 那么这些静态的部分会被提取出来,生成一些静态的渲染函数,这些函数就被存放在 options.staticRenderFns 中
// 后续的渲染只需要复用第一次渲染的结果,不需要再次生成。这可以提高渲染的性能。
opts.staticRenderFns = options.staticRenderFns
}
}
3.resolveConstructorOptions
js
function resolveConstructorOptions(Ctor: typeof Component) {
let options = Ctor.options
if (Ctor.super) {
const superOptions = resolveConstructorOptions(Ctor.super)
const cachedSuperOptions = Ctor.superOptions
// 判断是父组件的选项是否有变化。如果有变化,那么就需要更新子组件的选项,没变化啥也不干
if (superOptions !== cachedSuperOptions) {
// 更新子组件的superOptions属性,使其指向新的父组件选项
Ctor.superOptions = superOptions
// 获取子组件的选项中与父组件选项不同的部分
const modifiedOptions = resolveModifiedOptions(Ctor)
// 如果存在不同的部分,那么就将这部分选项合并到子组件的extendOptions属性中
if (modifiedOptions) {
extend(Ctor.extendOptions, modifiedOptions)
}
// 将父组件的选项和子组件的extendOptions合并,然后将结果赋值给子组件的options属性。
options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions)
// 如果子组件有名字,那么就将子组件添加到选项的components属性中,方便在模板中使用
if (options.name) {
options.components[options.name] = Ctor
}
}
}
// 返回 options选项
return options
}