继上一篇学习了renderer渲染器之后,这一篇继续来看看组件在Vue3源码中是怎么实现渲染的
引言
Vue3的组件本质,其实是一个对象
Vue3的组件有以下这些特点
- 必须包括一个
render函数,这部分决定了渲染内容 - 定义的数据放在
data里,也应该是一个函数,并且返回响应性数据 - 其他可选的包括
生命周期、computed计算属性、watch监听……
组件的挂载、更新和卸载
根据有无data,我们可以区分组件为有状态组件和无状态组件
通过之前h函数可以知道,Vue3里的组件渲染的模板是在render函数中返回的h函数,那我们需要做的就是把render函数里的h函数获取到并渲染
当组件更新的时候,也是将h函数中对应的部分做更新即可
无状态组件
无状态组件即没有响应式数据的组件,只有组件最基本的render方法
const component = {
render() {
return h("div", "hello this is component");
},
};
const vnode = h(component);
阅读源码可知,组件的挂载/更新由processComponent方法开始,同样是包括了新旧节点、容器、锚点这四个属性
const processComponent = (oldVNode, newVNode, container, anchor) => {
if (oldVNode == null) {
mountComponent(newVNode, container, anchor);
}
};
无状态组件的挂载
挂载组件使用了mountComponent方法,阅读源码可知,里面做了3个事情
- 创建组件实例并双向绑定(component里的
instance属性,以及instance里的component属性)- 组件实例是为了添加状态和属性,在数据更新、组件更新等时候可以判断
- 设置组件数据和属性
- 设置组件渲染的副作用(后续更新用)
const mountComponent = (initialVNode, container, anchor) => {
initialVNode.component = createComponentInstance(initialVNode);
const instance = initialVNode.component;
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container, anchor);
};
组件实例的创建
createComponentInstance方法创建组件实例,给实例添加一系列属性,例如vnode节点、type类型、render返回值(组件实际渲染的内容)、update方法(组件更新用)等等
let uid = 0;
function createComponentInstance(vnode) {
const type = vnode.type;
const instance = {
uid: uid++, // 自增,组件的唯一值
vnode,
type,
subTree: null,
effect: null,
update: null,
render: null,
};
return instance;
}
组件数据的初始化
创建完实例以后,要对组件的数据做初始化,用到了setupComponent方法
因为是无状态组件,所以没有数据,初始化的目的在于把render方法绑定到实例上面去
function setupComponent(instance) {
const setupResult = setupStatefulComponent(instance)
return setupResult
}
function setupStatefulComponent(instance) {
finishComponentSetup(instance)
}
function finishComponentSetup(instance) {
const Component = instance.type
instance.render = Component.render
}
组件渲染
最后就是设置组件渲染的setupRenderEffect方法了
effect还是用了响应式里的ReactiveEffect类,基本的响应式方法是componentUpdateFn,即组件更新方法,这里无论初次渲染还是后续更新都用的这个方法,另外还添加了一个调度器
const setupRenderEffect = (instance, initialVNode, container, anchor) => {
const componentUpdateFn = () => {
// 初次渲染
if (!instance.isMounted) {
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor);
initialVNode.el = subTree.el;
} else {
// TODO: 更新组件
}
};
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queuePreFlushCb(update)
));
const update = (instance.update = () => effect.run());
update();
};
因为组件的h函数是在render方法里的,所以在patch挂载组件前,需要拿到里面的VNode数据,这里用了一个renderComponentRoot方法
export function renderComponentRoot(instance) {
const { vnode, render } = instance;
let result;
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
result = normalizeVNode(render!());
}
} catch (err) {
console.error(err);
}
return result;
}
无状态组件的更新
通过阅读源码可以知道,无状态组件的更新本质是先卸载-后挂载,这些目前都已经实现
有状态组件
所谓的有状态组件就是那些带有响应式数据的组件,这些响应式数据放在data属性中,并以对象形式返回
const component = {
data() {
return {
msg: "hello this is component",
};
},
render() {
return h("div", this.msg);
},
};
const vnode = h(component);
有状态组件的挂载
对比之前无状态组件,有状态组件的挂载核心在于
- 获取数据并设置响应式
this指向处理
获取数据&设置数据响应式
阅读源码可以知道,响应式数据的设置封装在applyOptions方法中,并在完成组件的setup后调用
function applyOptions(instance: any) {
const { data: dataOptions } = instance.type;
if (dataOptions) {
// 因为data是通过return返回的,所以要执行方法才能拿到真正的data对象
const data = dataOptions();
if (isObject(data)) {
// 把数据变成响应式的
instance.data = reactive(data);
}
}
}
function finishComponentSetup(instance) {
......
applyOptions(instance);
}
修改this指向
组件的响应式数据一般是使用this来获取的,这里的this指向需要绑定为data
源码中这个this绑定发生在renderComponentRoot方法中,具体的是发生在**生成组件VNode**的时候
function renderComponentRoot(instance) {
const { vnode, render, data = {} } = instance;
let result;
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
result = normalizeVNode(render!.call(data));
}
} catch (err) {
console.error(err);
}
return result;
}
有状态组件的更新
我们一般用改变响应式数据的值的方式对组件进行更新
const component = {
data() {
return {
msg: "hello this is component",
};
},
render() {
return h("div", this.msg);
},
created() {
setTimeout(() => {
this.msg = "hello world";
}, 2000);
},
};
这里的更新其实本质上是监听响应式数据的变化,触发之前绑定好的effect函数,即componentUpdateFn方法
要注意把之前挂载的方法结尾,给组件实例添加一个isMounted的标记,以便后续数据变动可以进入else分支
const componentUpdateFn = () => {
if (!instance.isMounted) {
......
// 渲染完成后,更新渲染标记
instance.isMounted = true;
} else {
let { next, vnode } = instance;
// 标记下次要渲染的vnode
if (!next) {
next = vnode;
}
const nextTree = renderComponentRoot(instance);
const prevTree = instance.subTree;
instance.subTree = nextTree;
patch(prevTree, nextTree, container, anchor);
next.el = nextTree.el;
}
};
生命周期的处理
Vue中的生命周期,本质上是在特定时间执行的回调函数
beforeCreate&created
beforeCreate在实例初始化后、数据/watch等配置前被调用
created则是数据都配置完了,还没开始渲染前调用
因此,这两个生命周期在源码中都可以放在applyOptions中触发回调
function applyOptions(instance: any) {
......
// beforeCreate在数据初始化之前
if (beforeCreate) {
callHook(beforeCreate);
}
if (dataOptions) {
const data = dataOptions();
if (isObject(data)) {
instance.data = reactive(data);
}
}
// 数据初始化完成后,created执行
if (created) {
callHook(created);
}
......
}
function callHook(hook: Function) {
hook();
}
beforeMount&mounted
beforeMount在组件渲染之前调用,渲染完成再调用mounted
但是,由于我们解析生命周期的函数发生在applyOptions中,此时还没开始组件渲染,所以得想办法把这些生命周期函数存起来
这里可以存储在组件实例中,等到渲染方法调用的时候再去执行回调
Vue源码中,整个过程分成两步
- 存储
- 调用
生命周期回调函数的存储(注册)
在applyOptions中拿到生命周期函数,并用registerLifecycleHook这个注册方法,把回调方法存储在组件实例中
function applyOptions(instance: any) {
const {
......
beforeMount,
mounted,
} = instance.type;
......
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook, instance);
}
registerLifecycleHook(onBeforeMount, beforeMount);
registerLifecycleHook(onMounted, mounted);
}
创建组件实例的时候,也需要对应补充一些用来存储生命周期回调方法的属性
const enum LifecycleHooks {
BEFORE_CREATE = "bc",
CREATED = "c",
BEFORE_MOUNT = "bm",
MOUNTED = "m",
}
// 创建实例的时候,添加若干用来存储生命周期回调方法的属性
export function createComponentInstance(vnode) {
......
const instance = {
......
// 生命周期相关
isMounted: false,
bc: null,
c: null,
bm: null,
m: null,
};
return instance;
}
onBeforeMount和onMounted都是Vue源码里封装好的方法,这些方法用不同的名字标记了不同生命周期,最终的目的就是把生命周期的回调方法存储在组件实例上
const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
const onMounted = createHook(LifecycleHooks.MOUNTED);
export function injectHook(type: LifecycleHooks, hook: Function, target: any) {
if (target) {
target[type] = hook;
return hook;
}
}
export const createHook = (lifecycle: LifecycleHooks) => {
return (hook, target) => injectHook(lifecycle, hook, target);
};
生命周期调用
存储完了,就可以在对应位置触发回调函数了(这里两个生命周期都在componentUpdateFn方法中)
const componentUpdateFn = () => {
if (!instance.isMounted) {
// beforeMount和mounted生命周期
const { bm, m } = instance;
// 挂载前,触发beforeMount
bm && bm();
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor);
// 挂载完成后,触发mounted
m && m();
initialVNode.el = subTree.el;
} else {
// TODO: 组件更新
}
};
获取响应式数据
在生命周期中获取响应式数据,我们会用this.XXX来访问,那么实现的方法也很明确,就是改变this指向
因为生命周期不是即刻调用,所以我们只考虑使用bind方法改变this指向
function callHook(hook: Function, proxy: any) {
hook.bind(proxy)();
}
最后在调用生命周期函数的时候,传入data来改变this指向即可
function applyOptions(instance: any) {
......
// beforeCreate在数据初始化之前
if (beforeCreate) {
callHook(beforeCreate, instance.data);
}
......
// 数据初始化完成后,created执行
if (created) {
callHook(created, instance.data);
}
// 注册非即刻调用的生命周期,也用bind改变this指向
function registerLifecycleHook(register: Function, hook?: Function) {
register(hook?.bind(instance.data), instance);
}
registerLifecycleHook(onBeforeMount, beforeMount);
registerLifecycleHook(onMounted, mounted);
}
setup方法处理
Vue3新增了一个通过setup函数挂载响应式数据的方式
const component = {
setup() {
const obj = reactive({ msg: "hello world" });
return () => h("div", obj.msg);
},
};
const vnode = h(component);
观察发现,其实setup函数和普通的render方法区别只是
render函数是setup函数的返回值- 响应式数据分散在
setup函数中,单独声明
所以处理起来,只需要对setup做个判断,拿到这个函数的返回值塞到render中即可,其他的和之前都一样
function setupStatefulComponent(instance) {
const Component = instance.type;
// 提供了两种api:composition和setup
const { setup } = Component;
if (setup) {
// 拿到setup函数返回值作为render,多处理一层
const setupResult = setup();
handleSetupResult(instance, setupResult);
} else {
finishComponentSetup(instance);
}
}
function handleSetupResult(instance, setupResult: any) {
if (isFunction(setupResult)) {
instance.render = setupResult;
}
finishComponentSetup(instance);
}