三、深入组件挂载流程,你真的知道setup()干了什么吗?
在vue3中,组合式API结合setup函数的开发方式目前已经基本取代了选项式开发,那么vue框架在初始化组件时,setup到底做了什么?vue初始化组件又做了什么?
1. mountComponent
挂载组件流程,参数initialVnode为上一节中根据App组件使用createVnode创建的虚拟dom,
container为root真实dom, parentComponent为null。
function mountComponent(initialVNode, container, parentComponent) {
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent));
console.log(`创建组件实例:${instance.type.name}`,instance,initialVNode);
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
}
1.1 createComponentInstance
创建组件实例并且将vnode的component属性指向组件实例,回收了上一节中创建vnode后的component为null的伏笔。
创建组件实例将instance.ctx._ 指向实例本身。
- Vue 3 在内部实现中需要一个指向组件实例的引用,而这个引用需要被存储在一个特定的位置,这个位置就是
ctx属性。 ctx属性是一个特殊的属性,在 Vue 3 中,它用于存储组件实例的上下文信息。通过将_属性指向instance对象,Vue 3 可以在内部实现中轻松访问组件实例的信息。
function createComponentInstance(vnode, parent) {
const instance = {
type: vnode.type, // rootComponent
vnode, // 创建的vode
next: null,
props: {},
parent,
provides: parent ? parent.provides : {},
proxy: null,
isMounted: false,
attrs: {},
slots: {},
ctx: {},
setupState: {},
emit: () => {},
};
instance.ctx = {
_: instance,
};
instance.emit = emit.bind(null, instance);
return instance;
}
instance.emit = emit.bind(null, instance),将 emit 函数绑定到 instance 对象上,使得 instance.emit 成为一个新的函数,该函数的 this 指针指向 null,并且第一个参数始终是 instance。这种绑定方式称为“函数柯里化”(Function Currying)。
具体的emit的内部实现不在这里解答。
function emit(instance, event, ...rawArgs) {
const props = instance.props;
let handler = props[toHandlerKey(camelize(event))];
if (!handler) {
handler = props[(toHandlerKey(hyphenate(event)))];
}
if (handler) {
handler(...rawArgs);
}
}
1.2 setupComponent
这里的props,children均为空
function setupComponent(instance) {
const { props, children } = instance.vnode; // props,children均为空
initProps(instance, props);
initSlots(instance, children);
setupStatefulComponent(instance);
}
1.2.1 initProps
将vnode的props赋值给组件实例instance的props
function initProps(instance, rawProps) {
console.log("initProps");
instance.props = rawProps;
}
1.2.2 initSlots
判断vode的shapeFlag是否为32,即vnode.children的类型是否为object且不为元素节点(见上节),如果为32则进入格式化slots流程。
shapeFlag为 0x20(32):
SLOTS_CHILDREN- 表示 vnode 的子节点是插槽类型
function initSlots(instance, children) {
const { vnode } = instance;
console.log("初始化 slots");
if (vnode.shapeFlag & 32) {
normalizeObjectSlots(children, (instance.slots = {}));
}
}
// 遍历vnode.children,如果值为function则将对应的slots[key]赋值为函数,
// 函数的逻辑是接受一个props,使用value进行处理后将值转为数组
const normalizeObjectSlots = (rawSlots, slots) => {
for (const key in rawSlots) {
const value = rawSlots[key];
if (typeof value === "function") {
slots[key] = (props) => normalizeSlotValue(value(props));
}
}
};
const normalizeSlotValue = (value) => {
return Array.isArray(value) ? value : [value];
};
1.3 setupStatefulComponent
处理有状态组件函数,即shapeFlag为4
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers),对组件实例的proxy操作进行了代理,此时访问instance.proxy上的属性会被转发到PublicInstanceProxyHandlers上。
function setupStatefulComponent(instance) {
console.log("创建 proxy",instance);
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const Component = instance.type;
const { setup } = Component;
if (setup) {
setCurrentInstance(instance);
const setupContext = createSetupContext(instance);
const setupResult = setup && setup(shallowReadonly(instance.props), setupContext);
console.log("🚀 ~ setupStatefulComponent ~ setupResult:", setupResult)
setCurrentInstance(null);
handleSetupResult(instance, setupResult);
}
else {
finishComponentSetup(instance);
}
}
1.3.1 setCurrentInstance
设置全局的currentInstance为当前组件实例
function setCurrentInstance(instance) {
currentInstance = instance;
}
1.3.2 createSetupContext
创建setup上下文,即调用setup时,传入setup的第二个参数对象context
function createSetupContext(instance) {
console.log("初始化 setup context");
return {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => { },
};
}
setupContext = {
attrs: instance.attrs,
slots: instance.slots,
emit: instance.emit,
expose: () => { },
}
1.3.3 执行setup
此处即为我们在组件内部调用setup时的处理,将组件的props通过shallowReadonly包括,这也是为什么我们尝试修改props会出现报错的原因,同时第二个参数传入setup上下文。
setup(shallowReadonly(instance.props), setupContext);
1.3.4 handleSetupResult
判断setupResult的类型来处理setResult
setup 函数的返回值可以是以下两种类型之一:
- 函数:作为组件的渲染函数 (
render函数) - 对象:作为组件的状态对象 (
setupState对象)
function handleSetupResult(instance, setupResult) {
if (typeof setupResult === "function") {
instance.render = setupResult;
}
// setupResult 返回为{ } 详细请看第一节所使用的app.js
else if (typeof setupResult === "object") {
instance.setupState = proxyRefs(setupResult);
}
finishComponentSetup(instance);
}
proxyRefs
将setup返回值进行proxy代理,此时读取setupState等于读取setupResult
function proxyRefs(objectWithRefs) {
return new Proxy(objectWithRefs, shallowUnwrapHandlers);
}
const shallowUnwrapHandlers = {
get(target, key, receiver) {
return unRef(Reflect.get(target, key, receiver));
},
set(target, key, value, receiver) {
const oldValue = target[key];
// 如果老值为响应式,新值为非响应式,则仍保持响应式,返回对应的值
// 也就是说除了响应式变非响应式会被拦截,其他的会直接使用set改变对应值
if (isRef(oldValue) && !isRef(value)) {
return (target[key].value = value);
}
else {
return Reflect.set(target, key, value, receiver);
}
},
};
1.3.5 finishComponentSetup
结束组件的setup流程,接下来进入组件的compile环节,也就是编译组件的模板template,生成对应的render函数。
function finishComponentSetup(instance) {
const Component = instance.type;
// setupResult不为function,否则render会被赋值为setupResult
if (!instance.render) {
if (compile && !Component.render) {
if (Component.template) {
const template = Component.template;
Component.render = compile(template);
}
}
instance.render = Component.render;
}
}
这里的compile初始化时会被赋值为compileToFunction
function compileToFunction(template, options = {}) {
const { code } = baseCompile(template, options);
console.log("🚀 ~ compileToFunction ~ code:", code)
const render = new Function("Vue", code)(runtimeDom);
console.log("🚀 ~ compileToFunction ~ render:", render,runtimeDom)
return render;
}
const compile = compileToFunction
下一节,我们将介绍组件模板的compile流程。