目录
- 前情提要
- mountComponent
- 创建组件实例
- 总结
前情提要
上文我们讲解了执行createApp(App).mount('#root')
中的mount函数
,我们分析了创建虚拟节点
的几个方法,以及setRef的执行机制
、本文我们继续讲解mountComponent
,挂载组件的流程。
本文主要内容
createComponentInstance
发生了什么?- 如何标准化组件定义的
props、emits
? - 为什么
provide
提供的值子组件都能访问到? 组件
的v-model
实现原理、组件v-model修饰符
实现原理。- 组件
emit
实现原理。 once修饰符
实现原理。- 几乎
所有组件实例属性
的详细讲解。
mountComponent
- 这个方法主要用于
挂载组件
,根据传递的虚拟节点创建组件实例,调用setupComponent
函数进行初始化组件实例的插槽
和props
,如果是有状态组件还需要处理setup的返回值
。最后调用setupRenderEffect
绑定副作用更新函数
。这个函数比较简单,我们将重心放到createComponentInstance、setupComponent、setupRenderEffect
当中、后文也是围绕这三个方法进行依次讲解。
const mountComponent = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
//创建组件实例
const instance = (initialVNode.component = createComponentInstance(
initialVNode,
parentComponent,
parentSuspense
));
setupComponent(instance);
if (instance.asyncDep) {
//处理异步逻辑,suspense的时候在进行讲解
}
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
);
};
创建组件实例
createComponentInstance
: 根据虚拟节点创建组件实例的方法,简单的说就是创建一个instance
对象,这个上面有许多的属性
用来描述当前这个组件
以及存储需要后续使用到的属性
,所以我们主要讲解各个属性的作用
,我们先来看看这个函数的源码。uid
: 标识当前实例的id
,这是一个从0开始的递增值
,同时他也是Vue调度器
的优先级值
,越小的值优先级越高
,试想一下,父组件先挂载,然后才会挂载子节点,子节点如果包含组件,那么uid
的值将会比父组件
的uid
值大,当父组件挂载完毕,父组件的兄弟组件开始挂载,那么uid
将会更大
,这样也符合流程更新逻辑,关于调度器我们之后单独开一章节来进行讲解。vnode
: 实例的虚拟节点
。parent
: 当前组件实例的父组件实例。appContext
:createApp的时候注入的一个上下文
,我们通过app.component,app.mixin
调用后缓存的component
和mixin
都放在这里面,如果没有parent
表示当前是根组件
,那么就是这个appContext
,如果有parent
则继承parent的appContext
。我们可以通过这里发现所有的组件的appContext
都会是唯一的一个context
。因此所有组件实例都将能访问到全局注册的component、mixin、directive
等。我们来回顾一下它的结构(省略了部分属性)
。
{
app: null,
config: {
globalProperties: {},
optionMergeStrategies: {},
compilerOptions: {},
},
mixins: [],
components: {},
directives: {},
};
next
:组件更新有两种形式,一种
是组件内部的状态发生了改变,引起了组件自身更新;另外一种
是父组件传递了props
,props改变
,父组件更新、导致子组件引起了更新。第一种next
是null
。如果是第二种
,那么需要给next
赋值为当前子组件最新的VNode(虚拟节点)
。而next
将会用于更新组件的props
以及slots
等属性。组件自身状态改变引起的更新是不需要更新props
的。subTree
: 调用render
函数之后返回的组件要渲染的虚拟根节点,可以通过这个属性读取到组件需要渲染的所有虚拟节点。effect
: 在setupRenderEffect
中创建的reactiveEffect
,我们在Vue3源码分析(2)
中讲解了reactiveEffect
,不了解的可以在阅读一下,目前简单理解为在调用render函数的时候收集依赖
,如果响应式的值
发生了改变
会重新调用update函数
执行更新流程
。我们将会在讲解setupRenderEffect
中详细讲解这一部分。update
:组件更新函数
,只要调用了这个函数,组件就会强制更新。同时响应式发生改变调用的也是这个函数。render
:渲染函数
。可以通过编译<template></template>
得到,也可以通过写template属性
获得,也可以通过自己写render
函数。exposed
: 组件指定的暴露到外部的属性。provides
: 用户在组件中设置了provide
,子代组件可以通过inject
收到传递的值,provides:parent ? parent.provides : Object.create(appContext.provides)
通过这段代码分析我们可以知道,provides
中至少含有全局提供的provides
,如果当前组件提供了provide
,后面会将其混合,并且继承
自父组件
的provides
,这也就解释了为什么provides可以向下传递
,因为每一层都可以收到本组件的provides
和父组件的provides
并进行合并
。components
: 如果你想在template
中使用组件,需要在这里注册
,对于使用了<script setup>
语法糖的会将其编译
到components属性
当中,当然这个属性就是用来存放这些注册的组件的
。directives
: 存放自定义指令。propsOptions
:合并了mixins和extends后的props
,这里的属性为什么是propsOptions
而不是props
呢?这个VNode
中的props
又有什么关系呢?实际上propsOptions
代表的是用户在组件内定义的props
,而VNode
中的props
是外部传递给组件的props
。这一点要加以区分。同时这里调用了normalizePropsOptions
来对propsOptions
进行标准化。接下来我们分析一下normalizePropsOptions
函数。这个函数比较长我们分成三个部分来分析。- 首先从
appContext
中获取props
缓存,避免处理过了组件props重复处理
。如果comp
不是一个函数,这个判断是因为,Vue3
中允许函数组件
的存在,但是函数组件是无状态组件
也没有props,所以不做处理。那么剩下的都是有状态组件了,这里会处理全局注册的mixins
,组件本身的mixins
和extends
。我们可以发现全局的最先处理,所以全局注册的mixins
的优先级
将会是最低
的,其次是extends
,显然优先级最高的则是组件自身的mixins
,因为他最后执行,那么shared.extend
将会最后覆盖之前的props
。我们还可以发现extends
属性和mixins
属性在实现上没有任何区别,只是mixins
可以是数组,而extends
不能是数组。最后说一下asMixin
,我们知道全局的mixins
只需要合并一次
,但是normalizePropsOptions
会调用多次
,为了避免全局属性混合
的多次执行,设置了asMixin
这个参数。当asMixin为true
的时候表示不需要
在合并全局的mixins
了。特别提示:shared.extend就是Object.assign
。
function normalizePropsOptions(comp, appContext, asMixin = false) {
//获取props的缓存
const cache = appContext.propsCache;
const cached = cache.get(comp);
//这个缓存是一个type对应一个[normalized, needCastKeys]
//normalized表示合并了mixins和extends后的props
if (cached) {
return cached;
}
const raw = comp.props;
const normalized = {};
const needCastKeys = [];
let hasExtends = false;
if (!shared.isFunction(comp)) {
//用于合并props的函数,因为extends和mixins
//中还可以写mixins和extends所以需要递归合并
/**
* 例如const mixins = [{
* extends:{},
* mixins:[{props}],
* props
* }]
*/
const extendProps = (raw) => {
hasExtends = true;
const [props, keys] = normalizePropsOptions(raw, appContext, true);
//normalized为合并后的props
shared.extend(normalized, props);
if (keys) needCastKeys.push(...keys);
};
//首先合并全局注册的mixins中的props属性(最先合并的优先级最低)
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendProps);
}
//然后合并extends属性(中间合并的优先级居中)
//(extends功能与mixins几乎一样)但是更注重于继承
//并且extends不能是数组
if (comp.extends) {
extendProps(comp.extends);
}
//最后合并组件自身的mixins(最后合并的优先级最高)
if (comp.mixins) {
comp.mixins.forEach(extendProps);
}
}
//省略第二部分的代码...
}
- 我们知道这个函数主要是对
propsOptions
进行标准化,简单的说就是将各式各样的propsOptions
统一成唯一标准
。当传递的props:['msg']
数组形式的时候,我们需要将其转化为props:{msg:{}}
。validatePropName
用于检测key
是否合法,Vue
中,组件传递参数不能以$
开头定义数据。例如$msg
就是不合法
的。
function normalizePropsOptions(comp, appContext, asMixin = false) {
//省略第一部分的代码...
//如果没有props且没有全局的mixins
//组件本身的mixins、extends则设置
//当前实例的props缓存为空
if (!raw && !hasExtends) {
if (shared.isObject(comp)) {
cache.set(comp, shared.EMPTY_ARR);
}
return shared.EMPTY_ARR;
}
//处理这种类型props:['msg','hello']
if (shared.isArray(raw)) {
for (let i = 0; i < raw.length; i++) {
if (!shared.isString(raw[i])) {
console.warn(
`props must be strings when using array syntax. ${raw[i]}`
);
}
//将v-data-xxx转化为驼峰式vDataXxx
//但是不能以$开头
const normalizedKey = shared.camelize(raw[i]);
if (validatePropName(normalizedKey)) {
//将其变为props:{"msg":{}}
normalized[normalizedKey] = shared.EMPTY_OBJ;
}
}
}
//省略第三部分的代码...
}
- 在上面的代码中频繁出现
needCastKeys
,这个代表的是需要特殊处理的key,例如:props:{msg:{default:"msg"}}
含有default
,那么理论上我们应当判断传递的属性值是否存在,然后在决定是否使用default
的值,但是这里我们仅进行标准化,所以对于含有default
属性的我们需要单独放入needCastKeys
中,便于后面对props
中的处理。再比如说<Comp yes></Comp>
传递了yes属性,在propsOptions
中props:{yes:{type:Boolean}}
这样的key=>"yes"
也是需要处理的,yes
的值应该为true
,所以对于type
中含有Boolean
的也需要放入needCastKeys
中。
export function normalizePropsOptions(comp, appContext, asMixin = false) {
//省略第二部分代码...
if (shared.isArray(raw)) {
//省略...
}
//处理props:{msg:String}
else if (raw) {
if (!shared.isObject(raw)) {
warn(`invalid props options`, raw);
}
//循环遍历所有的key
for (const key in raw) {
//"v-data-xxx"=>"vDataXxx"变为小驼峰式
const normalizedKey = shared.camelize(key);
//检验key是否合法
if (validatePropName(normalizedKey)) {
const opt = raw[key]; //获取value
//如果获取的value是数组或函数转化则为{type:opt}
//props:{"msg":[]||function(){}}=>
//props:{"msg":{type:msg的值}}
const prop = (normalized[normalizedKey] =
shared.isArray(opt) || shared.isFunction(opt) ? { type: opt } : opt);
if (prop) {
//找到Boolean在prop.type中的位置 Boolean,["Boolean"]
const booleanIndex = getTypeIndex(Boolean, prop.type);
//找到String在prop.type中的位置
const stringIndex = getTypeIndex(String, prop.type);
prop[0] = booleanIndex > -1; //type中是否包含Boolean
//type中不包含String或者Boolean的位置在String前面
//例如:"msg":{type:["Boolean","String"]}
prop[1] = stringIndex < 0 || booleanIndex < stringIndex;
//如果有default属性,或者type中包含Boolean放入needCastKeys中
if (booleanIndex > -1 || shared.hasOwn(prop, "default")) {
needCastKeys.push(normalizedKey);
}
}
}
}
}
const res = [normalized, needCastKeys];
//设置缓存
if (shared.isObject(comp)) {
cache.set(comp, res);
}
return res; //返回合并后的normalized
}
emitsOptions
:合并了mixins
和extends
后的emits
属性。并且对emits
进行了标准化。主要是调用了normalizeEmitsOptions
进行处理,这个函数的逻辑和normlizePropsOptions
非常相似
,如果你看懂了上述的解释,这个函数相信你很容易就能看懂,这里就不在做多余的解释了。
export function normalizeEmitsOptions(comp, appContext, asMixin = false) {
//获取appContext中的缓存
const cache = appContext.emitsCache;
const cached = cache.get(comp);
//如果已经读取过返回缓存
if (cached !== undefined) {
return cached;
}
//获取组件的emits属性{emits:['']}
//还可以对象写法{emits:{'':null||function(){}}}
const raw = comp.emits;
//最终的合并对象
let normalized = {};
let hasExtends = false;
if (!shared.isFunction(comp)) {
//合并emits的方法
const extendEmits = (raw) => {
const normalizedFromExtend = normalizeEmitsOptions(raw, appContext, true);
if (normalizedFromExtend) {
hasExtends = true;
shared.extend(normalized, normalizedFromExtend);
}
};
//最先合并appContext中的(优先级最低)
if (!asMixin && appContext.mixins.length) {
appContext.mixins.forEach(extendEmits);
}
//然后合并实例的extends(优先级居中)
if (comp.extends) {
extendEmits(comp.extends);
}
//最后合并组件自身的(优先级最高)
if (comp.mixins) {
comp.mixins.forEach(extendEmits);
}
}
if (!raw && !hasExtends) {
//设置缓存
if (shared.isObject(comp)) {
cache.set(comp, null);
}
return null;
}
//即使emits:[]是数组最终也会被转化为对象
//emits:['m']=>emits:{'m':null}
if (shared.isArray(raw)) {
raw.forEach((key) => (normalized[key] = null));
} else {
//合并
shared.extend(normalized, raw);
}
if (shared.isObject(comp)) {
cache.set(comp, normalized);
}
return normalized;
}
attrs
: 如果你给组件传递了没有在组件内部声明的属性
,那么将会放入attrs
中。例如:
<Comp id="a"></Comp>
//Comp组件 没有对id的声明,那么会放入attrs中
export default {
props:{}
}
setupState
:setup
的返回值。ctx
:当前组件的上下文
。- 通过
createDevRenderContext
函数创建。在这个函数当中,对ctx
对象进行了代理
,可以通过ctx._
访问到组件的实例,同时将publicPropertiesMap
上所有的属性代理到了ctx
上,简单的说就是之前需要通过publicPropertiesMap
访问属性,现在在ctx
上同样能访问到。这样的代理方式在Vue
中被大量采用,setupState methods data computed watch props
当中所有的属性都将会采用这种方式被代理到ctx
上,当然会出现重名问题
,所以保证上述的这些属性中,应当避免重名
,否则会有警告
提示。当然这些属性是是在哪里被代理到ctx
上的,我们后面都会讲到。
function createDevRenderContext(instance) {
const target = {};
//可通过_访问实例对象
Object.defineProperty(target, `_`, {
configurable: true,
enumerable: false,
get: () => instance,
});
Object.keys(publicPropertiesMap).forEach((key) => {
Object.defineProperty(target, key, {
configurable: true,
enumerable: false,
get: () => publicPropertiesMap[key](instance),
set: shared.NOOP,
});
});
return target;
}
- 不知道这个这个对象上的方法你是否很熟悉呢?没错,这就是
Vue
官方展示的组合式Api
,你可以在setup
中访问this
调用到这些方法,不妨大胆猜测一下,为什么在setup
中访问this
能读取的这些方法?其实很简单。setup.call(instance.ctx,其他参数)
,实际上只要能访问到this
的地方,基本上都是访问的instance.ctx
、当然有些地方的this
绑定的是instance.proxy
。这两者的区别我们在后面在进行讲解。其中的i
代表的是组件实例
。
const publicPropertiesMap = shared.extend(Object.create(null), {
$: (i) => i, //获取当前实例
$el: (i) => i.vnode.el,
$data: (i) => i.data, //获取实例的data
$props: (i) => reactivity.shallowReadonly(i.props), //获取props
$attrs: (i) => reactivity.shallowReadonly(i.attrs),
$slots: (i) => reactivity.shallowReadonly(i.slots),
$refs: (i) => reactivity.shallowReadonly(i.refs),
$emit: (i) => i.emit,
//获取options
$options: (i) => resolveMergedOptions(i),
//强制更新
$forceUpdate: (i) => i.f || (i.f = () => queueJob(i.update)),
// $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)),
$watch: (i) => instanceWatch.bind(i),
$parent: (i) => getPublicInstance(i.parent),
$root: (i) => getPublicInstance(i.root),
});
emit
: 触发传递给组件的函数。通过this.$emit
调用的就是这个函数。对于emit
这个方法我们同样分成三个部分
进行讲解。- 之前我们详细讲解了
emitsOptions
,他最终会被标准化。这也是标准化的作用,后续的处理都会变得很简单,而不需要兼容多种写法。如果调用了emit
方法,但是在emitsOptions
中没有这个属性,表示并没有注册,需要警告用户
,同时如果你传递的emits
是一个函数
,那么他就是一个检验函数(如果不理解请查看Vue官网对于emits的解释)
,传递的参数为调用emits
传递的剩余参数
。
function emit(instance, event, ...rawArgs) {
//已经卸载无须在执行
if (instance.isUnmounted) return;
//获取props
const props = instance.vnode.props || shared.EMPTY_OBJ;
//获取经过标准化的emits和props
//emits:{方法名:null||function(){}}
//如果为function代表的是验证函数
const {
emitsOptions,
propsOptions: [propsOptions],
} = instance;
if (emitsOptions) {
//警告用户:调用了emit 但是没有在emitOptions中找到,代表没有声明
if (!(event in emitsOptions)) {
if (!propsOptions || !(shared.toHandlerKey(event) in propsOptions)) {
warn(
`Component emitted event "${event}" but it is neither declared in ` +
`the emits option nor as an "${shared.toHandlerKey(event)}" prop.`
);
}
}
//获取验证函数
else {
const validator = emitsOptions[event];
if (shared.isFunction(validator)) {
//调用验证函数,返回false则警告
const isValid = validator(...rawArgs);
if (!isValid) {
warn(
`Invalid event arguments: event validation failed for event "${event}".`
);
}
}
}
}
//省略第二部分代码...
}
- 这里的处理可能会让你很懵逼,这个
isModelListener
是个啥?modifiersKey
又是个啥?在讲解这个之前你需要了解v-model
在组件中的用法(你可以在Vue3官网查询)
,以及他们的编译结果
。通过编译结果我们可以发现,新增了属性modelModifiers
,这就是咱们下面访问的modifiersKey
了,同时v-model
指令本质上会被编译为onUpdate:modelValue
,所以我们可以通过判断prop
的开头是否是onUpdate
来判断这是不是一个v-model
指令。有了这两点我们就能理解第二部分的代码在做什么了。首先判断
当前发射的事件是否是v-model
事件,如果是,获取修饰符
对传递的参数
进行转换
,然后再传递给发射的事件函数
。
<template>
<Comp v-model.trim.number = "a" />
</template>
//编译后
function render(_ctx, _cache) {
const _component_Comp = _resolveComponent("Comp", true)
return (_openBlock(), _createBlock(_component_Comp, {
modelValue: _ctx.a,
"onUpdate:modelValue": $event => ((_ctx.a) = $event),
modelModifiers: { trim: true,number:true }
}, null, 8, ["modelValue"]))
}
function emit(instance, event, ...rawArgs) {
//省略第一部分代码...
let args = rawArgs;
//判断是否是v-model事件 => update:modelValue
const isModelListener = event.startsWith("update:");
const modelArg = isModelListener && event.slice(7); //modelValue
if (modelArg && modelArg in props) {
//获取modifiersKey=>modelModifiers
const modifiersKey = `${
modelArg === "modelValue" ? "model" : modelArg
}Modifiers`;
//当给组件传递v-model的时候
//<Button v-model.trim="a"></Button>
//当这样传递的时候会收到modelModifiers={trim:true}
const { number, trim } = props[modifiersKey] || shared.EMPTY_OBJ;
//对emit('',...args)传递给父组件的参数执行trim()
if (trim) {
args = rawArgs.map((a) => a.trim());
}
if (number) {
args = rawArgs.map(shared.toNumber);
}
}
//省略第三部分代码...
}
- 首先通过event在props中找到需要发射的事件,如果存在执行即可。如果添加了
once
修饰符,例如<C @close-Model.once="handler"></C>
他实际接收到的属性是onCloseModelOnce
,而如果不写once
修饰符就收到的就是onCloseModel
,所以添加了once
修饰符的执行一次
后会被放入emitted
当中,以后在触发就不会在执行
了。
function emit(instance, event, ...rawArgs) {
//省略第二部分的代码...
//这里是简单写法,源码实际上对
//event这个字符串做了改造。
let handler = props[event]
//如果存在则执行
if (handler) {
handler.apply(instance,args)
}
//如果有once事件,存入emitted,并执行,以后不再执行
const onceHandler = props[handlerName + `Once`];
if (onceHandler) {
if (!instance.emitted) {
instance.emitted = {};
} else if (instance.emitted[handlerName]) {
return;
}
instance.emitted[handlerName] = true;
handler.apply(instance,args);
}
}
- 总结一下
emit
函数的作用: 如果声明的emits
中含有函数,作为检验函数
,检验不通过警告用户。然后对组件使用v-model
指令做了处理,主要是对修饰符
的实现。当然在HTML标签
中使用修饰符并不是在这里实现的,这里仅实现了组件的v-model修饰符
。最后调用传递父组件传递给子组件的函数,并且把子组件要传递的参数给了父组件。如果含有once
修饰符,放入emitted缓存
中,只执行一次。 inheritAttrs
:如果给组件传递了某些props
属性,但是没有给组件声明props、emits、或者事件监听器
、那么id
属性将会透传到subTree
节点上,例如:<Comp id= "com"></Comp>
如果subTree
是一个div
节点,那么id
将会赋值到这个div
上。当然你可以设置inheritAttrs为false
禁止透传。存放生命周期的属性
: 我们可以看到下面有一些简写的属性例如:bc、c、bm
等,这些都是生命周期钩子
,在这里需要注意的是,这些属性的值
应该是一个数组
,因为如果用户使用了mixins
或者extends
这些属性,那么同一个生命周期函数可能会包含多个
,而这些生命周期函数
都应该被调用,所以他们的值应当是一个数组
例如:
export default {
mixins:[{beforeCreate(){}}]
beforeCreate(){}
}
//那么bc:[createBefore1,createBefore2]
- 还有一些
其他的属性
,我都放在了注释
当中,因为看单词意思就能理解,就不额外讲解了,当然后续这些属性都将会用到,也许现在看这些属性还是比较片面的。但是通过后面的讲解,你将会逐渐了解这些属性的作用。
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
const appContext = (parent ? parent.appContext : vnode.appContext) || {};
const instance = {
uid: uid++, //当前实例的id
vnode, //当前实例对应的vnode
type, //当前实例对应的编译后的.vue生成的对象
parent, //当前实例的父实例
appContext, //app的上下文包含全局注入的插件,自定义指令等
root: null, //当前组件实例的根实例
//响应式触发的更新next为null,
//在更新的过程中父组件调用了子组件的
//instance.update会赋值next为最新组件vnode
next: null,
subTree: null, //调用render函数后的Vnode(处理了透传)
effect: null, //实例的ReactiveEffect
update: null, //副作用的scheduler
scope: new EffectScope(true),
//template编译结果或setup返回值为函数
//或.vue文件写的template编译为的render函数
render: null, //渲染函数
proxy: null, //代理后的ctx
exposed: null, //调用了ctx.expose()方法(限制暴露的数据)
exposeProxy: null, //调用了getExposeProxy方法后的expose
withProxy: null,
//当前组件的provides,父实例有则读取父实例的否则读取app上的
//父组件的provides后续会挂载到prototype上,重新赋值当前真实
//的provide上,这样可以通过原型链访问到所有上代组件中的provide
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null,
renderCache: [],
components: null, //当前组件的可用组件
directives: null, //当前组件的自定义指令
//合并mixins和extends中的props属性
propsOptions: normalizePropsOptions(type, appContext),
//合并mixins和extends中的emits属性
emitsOptions: normalizeEmitsOptions(type, appContext),
emit: null, //当前实例调用emit的函数
emitted: null, //含有once修饰符的,执行一次后放入这里不再执行
propsDefaults: {}, //默认props
//是否透传attrs
inheritAttrs: type.inheritAttrs,
// state
ctx: {}, //当前实例的上下文也就是this
data: {}, //data函数返回的值,被代理后才放入
props: {}, //接受到的组件属性
attrs: {}, //接受到的标签属性
slots: {}, //组件传递的插槽内容
refs: {}, //存入的refs
setupState: {}, //setup的返回值
//expose attrs slots emit
setupContext: null, //传递给setup的ctx(只有四个属性)
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null, //setup使用了async修饰符 返回的promise保存在这里
asyncResolved: false,
isMounted: false, //是否挂载
isUnmounted: false, //是否卸载
isDeactivated: false,
bc: null, //beforeCreate
c: null, //create
bm: null, //beforeMount
m: null, //mount
bu: null, //beforeUpdate
u: null, //update
um: null, //unmount
bum: null, //beforeUnmount
//若组件实例是 <KeepAlive> 缓存树的一部分,
//当组件从 DOM 中被移除时调用。deactivated
da: null,
//若组件实例是 <KeepAlive> 缓存树的一部分,
//当组件被插入到 DOM 中时调用。activated
a: null,
//在一个响应式依赖被组件触发了重新渲染之后调用。
//renderTriggered
rtg: null,
//在一个响应式依赖被组件的渲染作用追踪后调用。
//renderTracked
rtc: null,
/**
* 错误捕获钩子
* 组件渲染
* 事件处理器
* 生命周期钩子
* setup() 函数
* 侦听器
* 自定义指令钩子
* 过渡钩子
* 错误捕获钩子
*/
ec: null, //errorHandler
sp: null, //serverPrefetch
};
//创建实例的上下文
instance.ctx = createDevRenderContext(instance);
//当前实例的根实例
instance.root = parent ? parent.root : instance;
instance.emit = emit.bind(null, instance);
if (vnode.ce) {
vnode.ce(instance);
}
return instance;
}
总结
- 挂载组件一共执行了三个主要的函数:
createInstanceComponent(创建组件实例)、setupComponent(初始化组件)、setupRenderEffect(设置更新副作用)
。 - 本文我们重点讲解了创建组件实例,以及对几乎所有的属性进行了详细的讲解,当然
suspense相关属性
我们会在讲解suspense
的时候详细为大家剖析。我们还重点讲解了ctx对象
,其实观察编译结果,所有我们在template
中访问的xxx变量
都会变成ctx.xxx
,本质上就是将所有的可能用到的变量都代理到了ctx
上,代理的方式就是Object.defineProperty
。当然还有如何标准化propsOptions、emitsOptions、
同时还详细讲解了组件的emit如何实现
。以及在组件中使用v-model
和它的修饰符trim、number
的实现过程,once
修饰符的实现原理。 - 下一小节我们将继续剖析组件挂载的
第二个
流程-初始化组件
。