渲染前的准备工作你做好了吗


theme: fancy

本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

hello 大家好,🙎🏻‍♀️🙋🏻‍♀️🙆🏻‍♀️

我是一个热爱知识传递,正在学习写作的作者,ClyingDeng 凳凳!

我们已经获取到了老的vnode、新的vnode、还有容器。接着上篇文章,我们判断了当前案例节点传入的是组件类型,执行组件的挂载mountComponent这个函数。在render之前,我们需要创建一个组件的实例,用来存放用户传入的props、setupState、render、attrs等。拿到这个实例才可以去渲染出用户想要的节点。

创建组件实例

mountComponent方法中我们可以根据虚拟节点创建组件实例createComponentInstance。创建实例我们可以像源码文件结构一样,单独拎出到component.ts文件中。

const mountComponent = (initialVNode, container) => {
    // 组件的挂载
    // console.log(initialVNode, container);
    // 组件实例初始化
    const instance = initialVNode.compoment = createComponentInstance(initialVNode)
}

那么我们就来看看component.ts中是怎么去初始化一个实例的。

// 创建一个组件实例
export function createComponentInstance(vnode) {
   const type = vnode.type
   const instance = {
       vnode,// 实例对应的虚拟节点
       type,// 组件对象
       subTree: null,// 组件渲染的内容
       render: null, // 组件的渲染函数
       proxy: null,// 实例的代理对象
       exposed: null, // 组件对外暴露的方法

       // emit
       emit: null, // 事件触发

       // 默认props
       propsDefaults: EMPTY_OBJ,

       // 初始化attrs true / false
       inheritAttrs: type.inheritAttrs,
       propsOptions: type.props, // 属性选项 可能存在参数的mixin
       
       // state
       ctx: EMPTY_OBJ, // 组件上下文
       data: EMPTY_OBJ,
       props: EMPTY_OBJ, // 组件属性
       attrs: EMPTY_OBJ,// 除了props中的属性,没用到的 vue默认是attr属性
       slots: EMPTY_OBJ, // 插槽
       refs: EMPTY_OBJ,
       setupState: EMPTY_OBJ, // setup中return的内容
       setupContext: null, // setup 内容

       // 生命周期钩子
       isMounted: false, // 是否挂载完成
       isUnmounted: false,
       isDeactivated: false,
       // ...
   }
   return instance
}

这边我只是写了部分实例上的属性,真正的vue上是远远不止这些的。我们可以将它分成几类:节点本身表示相关、事件的触发、state数据相关、生命周期相关等。
大部分说明我已经在上面标识,这样便于大家理解。

EMPTY_OBJ在此就是表示空对象{}。模仿在源码中,将其提取到shared文件中,因为源码中会根据当前环境进行不同初始化(dev环境使用Object.freeze({}))。

就拿subTree来说,它表示组件渲染的内容(h函数渲染的视图)。与之前的vue2不同,组件渲染的虚拟节点使用_vnode来表示,$vnode则表示组件的虚拟节点。vue3中组件的虚拟节点就是vnode,组件渲染的的真实节点内容就用subTree来表示。

此外,我们实例的props和attrs在vue中是这样分配的:

image.png

在我们传入的对象中,如果在props中存在同样的属性,那么该属性就是props。如果没有的话,那它就会被分配到attrs中(属性赋值中会有具体实现)。

对于ctx,就是上下文对象。在返回实例之前,我们可以将我们当前的实例挂载到上下文中:instance.ctx = { _: instance }。为什么要这样呢?

那是因为用户可以通过render传入一个proxy,既可以获取当前的props、也可以获取到当前的state数据。这样我们传入一个上下文对象ctx就是为了去实现实例的代理。

image.png

这样我们组件实例的初始化就算完成啦!👏👏👏

给组件实例属性赋值

props和attrs

组件实例赋值肯定也是在mountComponent中了啊。源码通过setupComponent(instance)这个函数将初始化好的实例传入。在该函数中进行赋值操作。这个也是跟组件相关的功能,我们依旧可以将其写入到component.ts文件中。

// 组件赋值
export function setupComponent(instance) {
    console.log('instance', instance);
    const { props, children } = instance.vnode
    // 给实例上的props和attrs赋值
    initProps(instance, props) // props初始化
}

先来看下根据上面的初始化,这个实例上有哪些属性:

image.png

里面又我们的上下文对象ctx、虚拟节点vnode、还有初始化的attrs、props等,我们可以通过instance拿到实例上的虚拟节点vnode,从图片中可以看出虚拟节点中的props就是用户在createAPP中传入的对象,我们要根据上述规则,将其分别对应赋值到实例的atrrs和props上,这就是我们的initProps函数要做的事情了。

export function initProps(instance, rawProps) {
    console.log('instance:', instance, 'rawProps:', rawProps); // rawProps:用户传入的props {title: 'dy', content: '具体内容叻!'}
    const props = {}
    const attrs = {}
    // ...
}

我们打印一下需要的实例和props:

image.png

从图中可以看出,instance.propsOptions是我们自己定义的props值,而rawProps对应的是用户传过来所有的props。rawProps与实例上的propsOptions关系是这样的:。rawProps中包含propsOptions的属性,这些属性就是实例上的props,不包含的则是实例上的attrs。

分别定义两个对象,用来存储分配得到的props和attrs,最后给实例上的props和attrs赋值。

既然是包含关系,那我们就可以去循环遍历用户传入的rawProps,拿instance.propsOptions上的属性与 rawProps上的属性对比,如果两者相等,就把当前属性和属性对应的值赋值给props,不是就赋值给attrs。

给实例上的props和attrs赋值时还需要注意的是,props是响应式的,而attrs则是非响应式。

具体的props初始化是这样实现的:

export function initProps(instance, rawProps) {
    const props = {}
    const attrs = {}
    //遍历实例上的props 如果在props中存在,就属于props 
    //用户的props里用到rawProps 就当前rawProps中的属性值就是props,如果没用到就是attrs
    const options = Object.keys(instance.propsOptions)
    if (rawProps) {
        for (const key in rawProps) {
            const value = rawProps[key];
            if (options.includes(key)) {
                props[key] = value;
            } else {
                attrs[key] = value;
            }
        }
    }
    instance.props = reactive(props) // 引用之前reactivity中的reactive
    instance.attrs = attrs // attrs 属性 非响应
}

我们再来看看源码中是怎么实现initProps的:
image.png
initProps里面,又有一个setFullProps这个函数,通过该函数来实现props和attrs赋值。

image.png
最后,我们可以再回到setupComponent这个函数中,打印一下我们执行完initProps后的实例:

image.png

我们可以看到attrs和props都已经赋值成功了。到此,我们的props初始化就算赋值完成啦!👏👏👏

render和setupState

对于render和setupState,我们可以在setupStatefulComponent中完成!!!

export function setupComponent(instance) {
    const { props, children } = instance.vnode
    // 给实例上的props和attrs赋值
    initProps(instance, props) // props初始化
    // initSlot(instance, children) // 插槽
    setupStatefulComponent(instance)
    console.log('instance: ', instance);
}

setupStatefulComponent的核心就是调用setup,根据返回值类型不同进行不同的赋值。如果setup返回的是一个函数,那就直接给render赋值;如果返回的是一个对象,那么对象里面的就是setupSate

顺着主要的核心,我们就需要去执行用户的setup,可是怎么执行呢?🤔🤔🤔

我们先看看vue3中的setup的两个参数:

image.png

现在我们props已经获取到了,就差上下文ctx了。我们可以看到vue中的上下文对象包含attrs、slots、emit、expose四个属性,那我们就可以照葫芦画瓢,先初始化这个上下文对象。

export function createSetupContext(instance) {
    return {
        attrs: instance.attrs,
        slots: instance.slots,
        emit: instance.emit,
        expose: (exposed) => instance.exposed = exposed || {} // expose 对外暴露的方法
    }
}

通过组件实例返回对应的上下文属性。

我们既然可以通过createSetupContext获取到上下文对象,那么接下来我们就可以这样来完成我们render和setupState赋值:

export function setupStatefulComponent(instance) {// 调用组件的setup
    const Component = instance.type
    const { setup } = Component
    if (setup) {
        const setupContext = createSetupContext(instance) // 上下文对象
        let setupResult = setup(instance.props, setupContext) // setup返回值
        // ...
    }
}

从实例中先获取到组件的实例,拿到组件的setup,然后传入所需要的props和上下文对象。用setupResult把setup的返回值存储起来。

image.png

在我们当前的案例中setup的返回值是一个对象,那么返回的count和add就应该是state数据。

当然我们也可以看到可以这样使用,setup 直接返回一个h函数(需要赋值给render):

image.png

那么,我们就需要判断,当前的 setup 返回值是函数还是一个对象。

export function setupStatefulComponent(instance) {// 调用组件的setup
    const Component = instance.type
    const { setup } = Component
    if (setup) {
        const setupContext = createSetupContext(instance)
        let setupResult = setup(instance.props, setupContext)
        // 直接将返回结果赋值给 render
        // 对象 就赋值给setupState
        if (isFunction(setupResult)) {
            instance.render = setupResult
        } else if (isObject(setupResult)) {
            instance.setupState = setupResult
        }
        if (!instance.render) { // 不存在render,就将组件本身的render赋值给render
            // 如果没有render template 就需要模版编译
            instance.render = Component.render
        }
    }
}

根据不同情况,对实例上的 render和setupState 进行赋值。

执行完我们的setupStatefulComponent,我们可以看下实例上的render和setupState:

image.png

将setup返回值改写成函数,我们打印结果可以看到:
image.png
render中存在返回的h函数,就是setup的返回值。
这样,我们的render和setupState的赋值也完成啦🤗🤗🤗

即使这样,我们的准备工作还不能算完成,我们还需要给一个核心的proxy赋值呢!让我们再小小期待一下吧~👋👋👋

感兴趣的朋友可以关注 手写vue3系列 专栏或者点击关注作者ClyingDeng哦(●’◡’●)!。 如果不足,请多指教。

文末有惊喜

感谢大家一直以来支持与鼓励,评论送网易云黑胶周卡!🔜

更文不易,可以先点个关注🙋‍♀️,防止迷路!

评论区第7位、第15位、第24位、第34位幸运儿,送网易云周卡哟🥳🥳🥳

行动不如行动🏃‍🏃‍🏃‍♀️,截止11月30日晚8点!!!

每人仅限评论一次,重复评论算最初评论位数(周卡月底领取有效,仅限非黑胶会员领取哦)🤩🤩🤩

幸运鹅会是你嘛!🫵🫵🫵

1.jpg

愿 手机/电脑前的你,快乐24小时不打烊💞💞💞

© 版权声明
THE END
喜欢就支持一下吧
点赞5 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容