Vue源码速读 | 第三章:深入组件挂载流程,你真的知道setup()干了什么吗?

267 阅读4分钟

三、深入组件挂载流程,你真的知道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流程。