本文为稀土掘金技术社区首发签约文章,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中是这样分配的:<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/09628d47b44d4a7aa25ca446d36203d3~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?" style="margin: auto" />
在我们传入的对象中,如果在props中存在同样的属性,那么该属性就是props。如果没有的话,那它就会被分配到`attrs`中(属性赋值中会有具体实现)。对于`ctx`,就是上下文对象。在返回实例之前,我们可以将我们当前的实例挂载到上下文中:`instance.ctx = { _: instance }`。为什么要这样呢?那是因为用户可以通过render传入一个`proxy`,既可以获取当前的props、也可以获取到当前的state数据。这样我们传入一个上下文对象ctx就是为了去实现实例的代理。<img src="https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/14064fc09758464d81f8be4cdbbba674~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?" style="margin: auto" />
这样我们组件实例的初始化就算完成啦!👏👏👏给组件实例属性赋值
=========
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初始化
}
先来看下根据上面的初始化,这个实例上有哪些属性:
![](https://i-blog.csdnimg.cn/blog_migrate/26a27bf669027b24d82d80ee8da4a21b.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:<img src="https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b23e0b99b9c944cba09197ed0d93300c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?" style="margin: auto" />
从图中可以看出,`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,如果没用到就是attrsconst 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中的reactiveinstance.attrs = attrs // attrs 属性 非响应
}
我们再来看看源码中是怎么实现initProps
的:
我们可以看到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的两个参数:
现在我们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) {// 调用组件的setupconst Component = instance.typeconst { setup } = Componentif (setup) {const setupContext = createSetupContext(instance) // 上下文对象let setupResult = setup(instance.props, setupContext) // setup返回值// …}
}
从实例中先获取到组件的实例,拿到组件的setup,然后传入所需要的props和上下文对象。用`setupResult`把setup的返回值存储起来。<img src="https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8eccba2e6f914f17a3dbd2e9c1da08c6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?" style="margin: auto" />
在我们当前的案例中setup的返回值是一个对象,那么返回的count和add就应该是state数据。当然我们也可以看到可以这样使用,setup 直接返回一个h函数(需要赋值给render):<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9183f5e28ea44e21baae4b58c1d78d94~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?" style="margin: auto" />
那么,我们就需要判断,当前的 setup 返回值是函数还是一个对象。```
export function setupStatefulComponent(instance) {// 调用组件的setupconst Component = instance.typeconst { setup } = Componentif (setup) {const setupContext = createSetupContext(instance)let setupResult = setup(instance.props, setupContext)// 直接将返回结果赋值给 render// 对象 就赋值给setupStateif (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:
将setup返回值改写成函数,我们打印结果可以看到:
即使这样,我们的准备工作还不能算完成,我们还需要给一个核心的proxy赋值呢!让我们再小小期待一下吧~👋👋👋
最后
为大家准备了一个前端资料包。包含54本,2.57G的前端相关电子书,《前端面试宝典(附答案和解析)》,难点、重点知识视频教程(全套)。
有需要的小伙伴,可以点击下方卡片领取,无偿分享