渲染前的准备工作你做好了吗
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中是这样分配的:
在我们传入的对象中,如果在props中存在同样的属性,那么该属性就是props。如果没有的话,那它就会被分配到attrs
中(属性赋值中会有具体实现)。
对于ctx
,就是上下文对象。在返回实例之前,我们可以将我们当前的实例挂载到上下文中:instance.ctx = { _: instance }
。为什么要这样呢?
那是因为用户可以通过render传入一个proxy
,既可以获取当前的props、也可以获取到当前的state数据。这样我们传入一个上下文对象ctx就是为了去实现实例的代理。
这样我们组件实例的初始化就算完成啦!👏👏👏
给组件实例属性赋值
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初始化
}
先来看下根据上面的初始化,这个实例上有哪些属性:
里面又我们的上下文对象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:
从图中可以看出,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
的:
在
initProps
里面,又有一个setFullProps
这个函数,通过该函数来实现props和attrs赋值。
最后,我们可以再回到
setupComponent
这个函数中,打印一下我们执行完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) {// 调用组件的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的返回值存储起来。
在我们当前的案例中setup的返回值是一个对象,那么返回的count和add就应该是state数据。
当然我们也可以看到可以这样使用,setup 直接返回一个h函数(需要赋值给render):
那么,我们就需要判断,当前的 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:
将setup返回值改写成函数,我们打印结果可以看到:
render中存在返回的h函数,就是setup的返回值。
这样,我们的render和setupState的赋值也完成啦🤗🤗🤗
即使这样,我们的准备工作还不能算完成,我们还需要给一个核心的proxy赋值呢!让我们再小小期待一下吧~👋👋👋
感兴趣的朋友可以关注 手写vue3系列 专栏或者点击关注作者ClyingDeng哦(●'◡'●)!。 如果不足,请多指教。
文末有惊喜
感谢大家一直以来支持与鼓励,评论送网易云黑胶周卡!🔜
更文不易,可以先点个关注🙋♀️,防止迷路!
评论区第7位、第15位、第24位、第34位幸运儿,送网易云周卡哟🥳🥳🥳
心动不如行动🏃🏃🏃♀️,截止11月30日晚8点!!!
每人仅限评论一次,重复评论算最初评论位数(周卡月底领取有效,仅限非黑胶会员领取哦)🤩🤩🤩
幸运鹅会是你嘛!🫵🫵🫵
愿 手机/电脑前的你,快乐24小时不打烊💞💞💞
转载自:https://juejin.cn/post/7171459471665790983