13. 实现 setup 函数
13. 实现 setup 函数
1.setup 函数作用
组件的 render 函数每次更新时都会重新执行,但是 setup 函数只会在组件挂载时执行一次。
- setup 函数是 compositionAPI 的入口
- 可以在函数内部编写逻辑,解决 vue2 中反复横跳问题
- setup 返回函数时为组件的 render 函数,返回对象时对象中的数据将暴露给模板使用
- setup 中函数的参数为 props、context({slots,emit,attrs,expose})
const My = {
props: { address: String },
render() {
return h('div', this.address);
}
};
const VueComponent = {
props: {
address: String
},
setup(props) {
const name = ref('erxiao');
return {
name,
address: props.address
};
},
render() {
return h(Text, `${this.address},${this.name}`);
}
};
render(h(VueComponent, { address: '回龙观' }), app);
对
setup
函数进行解析
export function setupComponent(instance) {
const { props, type } = instance.vnode;
initProps(instance, props);
let { setup } = type;
if (setup) {
// 对setup做相应处理
const setupContext = {};
const setupResult = setup(instance.props, setupContext);
console.log(setupResult);
if (isFunction(setupResult)) {
instance.render = setupResult;
} else if (isObject(setupResult)) {
instance.setupState = proxyRefs(setupResult); // 这里对返回值进行结构
}
}
instance.proxy = new Proxy(instance, PublicInstanceProxyHandlers);
const data = type.data;
if (data) {
if (!isFunction(data))
return console.warn('The data option must be a function.');
instance.data = reactive(data.call(instance.proxy));
}
if (!instance.render) {
instance.render = type.render;
}
}
新增取值范围
const PublicInstanceProxyHandlers = {
get(target, key) {
const { data, props, setupState } = target;
if (data && hasOwn(data, key)) {
return data[key];
} else if (hasOwn(props, key)) {
return props[key];
} else if (setupState && hasOwn(setupState, key)) {
// setup返回值做代理
return setupState[key];
}
const publicGetter = publicPropertiesMap[key];
if (publicGetter) {
return publicGetter(target);
}
},
set(target, key, value) {
const { data, props, setupState } = target;
if (data && hasOwn(data, key)) {
data[key] = value;
return true;
} else if (hasOwn(props, key)) {
console.warn(`Attempting to mutate prop "${key}". Props are readonly.`);
return false;
} else if (setupState && hasOwn(setupState, key)) {
// setup返回值做代理
setupState[key] = value;
}
return true;
}
};
2.实现 emit 方法
const VueComponent = {
setup(props, ctx) {
const handleClick = () => {
ctx.emit('myEvent');
};
return () => h('button', { onClick: handleClick }, '点我啊');
}
};
render(
h(VueComponent, {
onMyEvent: () => {
alert(1000);
}
}),
document.getElementById('app')
);
const setupContext = {
attrs: instance.attrs,
emit: (event, ...args) => {
const eventName = `on${event[0].toUpperCase() + event.slice(1)}`;
const handler = instance.vnode.props[eventName]; // 找到绑定的方法
// 触发方法执行
handler && handler(...args);
}
};
3.slot 实现
const MyComponent = {
render() {
return h(Fragment, [
h('div', [this.$slots.header()]), // 获取插槽渲染
h('div', [this.$slots.body()]),
h('div', [this.$slots.footer()])
]);
}
};
const VueComponent = {
setup() {
return () =>
h(MyComponent, null, {
// 渲染组件时传递对应的插槽属性
header: () => h('p', '头'),
body: () => h('p', '体'),
footer: () => h('p', '尾')
});
}
};
render(h(VueComponent), app);
export const createVNode = (type, props, children = null) => {
// ....
if (children) {
let type = 0;
if (Array.isArray(children)) {
type = ShapeFlags.ARRAY_CHILDREN;
} else if (isObject(children)) {
// 类型是插槽
type = ShapeFlags.SLOTS_CHILDREN;
} else {
children = String(children);
type = ShapeFlags.TEXT_CHILDREN;
}
vnode.shapeFlag |= type;
}
return vnode;
};
const publicPropertiesMap = {
$attrs: (i) => i.attrs,
$slots: (i) => i.slots
};
function initSlots(instance, children) {
if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) {
instance.slots = children;
} else {
instance.slots = {};
}
}
export function createComponentInstance(vnode) {
const instance = {
// 组件的实例
slots: null // 初始化插槽属性
};
return instance;
}
export function setupComponent(instance) {
const { props, type, children } = instance.vnode;
initProps(instance, props);
initSlots(instance, children); // 初始化插槽
}
4.插槽更新
function updateComponentPreRender(instance, next) {
instance.next = null;
instance.vnode = next;
updateProps(instance, instance.props, next.props);
Object.assign(instance.slots, next.children); // 渲染前要更新插槽
}
5.组件卸载实现
const unmount = (vnode) => {
const { shapeFlag } = vnode;
if (vnode.type === Fragment) {
return unmountChildren(vnode.children);
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 组件那么移除
return unmount(vnode.component.subTree); // 移除组件
}
hostRemove(vnode.el);
};
6.生命周期实现原理
生命周期需要让当前实例关联对应的生命周期,这样在组件构建过程中就可以调用对应的钩子
component.ts
export let currentInstance = null;
export const setCurrentInstance = (instance) => (currentInstance = instance);
export const getCurrentInstance = () => currentInstance;
export const unsetCurrentInstance = () => (currentInstance = null);
setCurrentInstance(instance); // 在调用setup的时候保存当前实例
const setupResult = setup(instance.props, setupContext);
unsetCurrentInstance();
创建生命周期钩子
apiLifecycle.ts
export const enum LifecycleHooks {
BEFORE_MOUNT = 'bm',
MOUNTED = 'm',
BEFORE_UPDATE = 'bu',
UPDATED = 'u'
}
function createHook(type) {
return (hook, target = currentInstance) => {
// 调用的时候保存当前实例
if (target) {
const hooks = target[type] || (target[type] = []);
const wrappedHook = () => {
setCurrentInstance(target); // 当生命周期调用时 保证currentInstance是正确的
hook.call(target);
setCurrentInstance(null);
};
hooks.push(wrappedHook);
}
};
}
export const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
export const onMounted = createHook(LifecycleHooks.MOUNTED);
export const onBeforeUpdate = createHook(LifecycleHooks.BEFORE_UPDATE);
export const onUpdated = createHook(LifecycleHooks.UPDATED);
钩子调用
const componentUpdateFn = () => {
if (!instance.isMounted) {
const { bm, m } = instance;
if (bm) {
// beforeMount
invokeArrayFns(bm);
}
const subTree = render.call(renderContext, renderContext);
patch(null, subTree, container, anchor);
instance.subTree = subTree;
instance.isMounted = true;
if (m) {
// mounted
invokeArrayFns(m);
}
} else {
let { next, bu, u } = instance;
if (next) {
updateComponentPreRender(instance, next);
}
if (bu) {
// beforeUpdate
invokeArrayFns(bu);
}
const subTree = render.call(renderContext, renderContext);
patch(instance.subTree, subTree, container, anchor);
if (u) {
// updated
invokeArrayFns(u);
}
instance.subTree = subTree;
}
};
shared.ts
export const invokeArrayFns = (fns) => {
for (let i = 0; i < fns.length; i++) {
fns[i](); // 调用钩子方法
}
};