第十二章:组件的实现原理
渲染组件
组件就是特殊类型的虚拟 DOM,其 vnode 的 type 为对象。
function patch(n1, n2, container) {
if (n1 && n1.type !== n2.type) {
unmount(n1);
n1 = null;
}
const { type } = n2;
if (type === 'string') {
/**处理普通元素 */
} else if (type === Text) {
/**处理文本节点 */
} else if (type === Fragment) {
/**处理片段 */
} else if (type === 'object') {
// 作为组件去处理
if (!n1) {
// 挂载组件
mountComponent(n2, container, anchor);
} else {
// 更新组件
patchComponent(n1, n2, anchor);
}
}
}
function mountComponent(vnode, container, anchor) {
const options = vnode.type;
const { render } = options;
const subTree = render();
patch(null, subTree, container, anchor);
}
TIP
每个组件都是对自身页面内容的封装,用来描述页面内容的一部分。因此一个组件必须包含一个 render 函数,并且 render 函数的返回值必须是一个 vnode。
组件状态与自更新
function mountComponent(vnode, container, anchor) {
const queue = new Set();
const p = Promise.resolve();
let isFlushing = false;
function queueJob(job) {
queue.add(job);
if (!isFlushing) {
isFlushing = true;
p.then(() => {
try {
queue.forEach(job => job());
} finally {
isFlushing = false;
queue.clear();
}
});
}
}
const options = vnode.type;
const { render, data } = options;
const state = reactive(data()); // 将数据响应式化
// 将 render 函数添加到副作用函数中,响应式数据变化后会触发组件更新
effect(
() => {
const subTree = render.call(state, state); // 改变 this,使能够通过 this.xx 访问 data 的数据
patch(null, subTree, container, anchor);
},
{
// 每一个响应式数据变化都会触发组件更新,因此需要自定义调度器,使全部数据变化后再执行副作用函数。
scheduler: queueJob
}
);
}
组件实例与生命周期
上面的例子每次都是 patch(null, vnode)
,即每次都是重新构建,这是不合理的,因此要记录上一次的构建结果进行 patch
function mountComponent(vnode, container, anchor) {
const options = vnode.type;
const {
render,
data,
beforeCreate,
created,
beforeMount,
mounted,
beforeUpdate,
updated
} = options;
beforeCreate && beforeCreate();
const state = reactive(data()); // 将数据响应式化
const instance = {
state,
isMounted: false,
subTree: null
};
vnode.component = instance;
created && created.call(state);
// 将 render 函数添加到副作用函数中,响应式数据变化后会触发组件更新
effect(
() => {
const subTree = render.call(state, state);
// 第一次渲染
if (!instance.isMounted) {
beforeMount && beforeMount.call(state);
patch(null, subTree, container, anchor);
instance.isMounted = true;
mounted && mounted.call(state);
} else {
beforeUpdate && beforeUpdate.call(state);
// 如果不是第一次就执行 patch,传入上一次的 subTree
patch(instance.subTree, subTree, container, anchor);
updated && updated.call(state);
}
instance.subTree = subTree;
},
{
// 每一个响应式数据变化都会触发组件更新,因此需要自定义调度器,使全部数据变化后再执行副作用函数。
scheduler: queueJob
}
);
}
Props 的传递
组件的 props 需要在组件内显示定义,否则将会认为是 attrs
function mountComponent(vnode, container, anchor) {
const options = vnode.type;
const {
render,
data,
props: propsOption
/**... */
} = options;
const [props, attrs] = resolveProps(propsOption, vnode.props);
const instance = {
state,
props: shallowReactive(props)
isMounted: false,
subTree: null
};
// 传递的 props 如果在组件内声明了,就认为是 props,否则认为是 attrs
function resolveProps(options, propData) {
const props = {};
const attrs = {};
for (const key in propData) {
if (key in options) {
props[key] = propData[key];
} else {
attrs[key] = propData[key];
}
}
return [props, attrs];
}
}
为什么 Props 使用的是 shallowReactive
?
Props 引发的被动更新
props 本质上是父组件的数据,当其更新时会触发父组件的更新。 在父组件更新的过程中,渲染器发现父组件的 subTree 包含组件类型的虚拟节点,所以会调用 patchComponent 完成子组件的更新。
function patchComponent(n1, n2, container) {
// 传递 instance,将新的(n2)设置 component,否则下次更新时将无法获得实例对象
const instance = (n2.component = n1.component);
const { props } = instance;
if (hasPropsChanged(n1.props, n2.props)) {
const [nextProps] = resolveProps(n2.type.props, n2.props);
// 更新
for (const k in nextProps) {
props[k] = nextProps[k];
}
// 删除不存在的
for (const k in props) {
if (!(k in nextProps)) delete props[k];
}
}
}
/**
* 长度不同 or 任意一个值不想等,则认为更新了
*/
function hasPropsChanged(prev, next) {
const nextKeys = Object.keys(next);
if (nextKeys.length !== Object.keys(prev).length) return true;
for (const key in nextKeys) {
if (next[key] !== prev[key]) return true;
}
return false;
}
让 this 访问到 Props
由于 Props 和组件自身的数据都要传递到渲染函数,并可以通过 this 访问,因此我们需要封装一个渲染上下文。
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = instance;
if (k in state) {
return state[k];
} else if (k in props) {
return props[k];
} else {
console.error('不存在');
}
},
set(t, k, v, r) {
const { state, props } = instance;
if (k in state) {
state[k] = v;
} else if (k in props) {
console.error('props 上的数据不允许修改');
} else {
console.error('不存在');
}
}
});
const subTree = render.call(renderContext, renderContext);
setup 函数的作用与实现
setup 主要配合 Vue3 中的组合式 API,为用户提供一个地方,用于创建组合逻辑、创建响应式数据、创建通用函数、注册生命钩子等能力
在组件的整个生命周期中 setup 只会在被挂载时执行一次,其返回值有两种情况:
- 返回一个函数,组件会将其当作自己的 render 函数。(这种方式常用于组件而非模版,模版也会产生 render 函数,二者会冲突)
- 返回一个对象,该对象包含的数据将暴露给模版使用。
setup 函数接收两个参数:
const Comp = {
props: { foo: String },
setup(props, setupContext) {
props.foo; // 访问组件接收的 props
const { slots, emit, attrs, expos } = setupContext;
}
};
简单实现一下 setup:
function mountComponent(vnode, container, anchor) {
const options = vnode.type;
const {
setup
/**... */
} = options;
const [props, attrs] = resolveProps(propsOption, vnode.props);
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
};
// 用 attrs 做例子,应该还有 emit、slots 等
const setupContext = { attrs };
const setupRes = setup(shallowReadonly(instance.props), setupContext);
// setup 返回的数据
const setupState = null;
if (typeof setupRes === 'function') {
if (render) {
console.error('setup 返回 render 函数,与模版冲突');
}
render = setupRes;
} else {
setupState = setupRes;
}
}
renderContext 中增加 setupState
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props } = instance;
if (k in state) {
return state[k];
} else if (k in props) {
return props[k];
} else if (k in setupState) {
return setupState[k]; // 新增
} else {
console.error('不存在');
}
},
set(t, k, v, r) {
const { state, props } = instance;
if (k in state) {
state[k] = v;
} else if (k in props) {
console.error('props 上的数据不允许修改');
} else if (k in setupState) {
setupState[k] = v; // 新增
} else {
console.error('不存在');
}
}
});
组件事件与 emit 的实现
<template>
<MyComp @change="handler" />
</template>
上面的模版对应的虚拟 DOM 为:
const node = {
props: {
onChange: handler
}
/** ... */
};
可以看到,自定义事件 change 被编译称为 onChange,并存储到了 props 中,这其实是一个约定。
function mountComponent(vnode, container, anchor) {
const options = vnode.type;
const {
setup
/**... */
} = options;
const [props, attrs] = resolveProps(propsOption, vnode.props);
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null
};
/**
* event: 事件名
* payload: 传递给事件的参数
*/
function emit(event, ...payload) {
const eventName = `on${event[0].toUpperCase() + event.slice(0)}`;
const handler = instance.props[eventName];
if (handler) {
handler(...payload);
} else {
console.error('未找到事件');
}
}
// 用 attrs 做例子,应该还有 emit、slots 等
const setupContext = { attrs, emit };
function resolveProps(options, propData) {
const props = {};
const attrs = {};
for (const key in propData) {
// 以 on 开头的 props,无论是否显示的声明,都会添加到 props,因为可能是 emit
if (key in options || key.startsWith('on')) {
props[key] = propData[key];
} else {
attrs[key] = propData[key];
}
}
return [props, attrs];
}
}
插槽的工作原理及实现
<!-- MyComponent -->
<template>
<header><slot name="header" /></header>
<div>
<slot name="body" />
</div>
</template>
父组件调用时
<template>
<MyComponent>
<template #header>
<h1>我是标题</h1>
</template>
<template #body>
<div>我是内容</div>
</template>
</MyComponent>
</template>
父组件模版会编译成:
function render() {
return {
type: MyComponent,
// 组件的 children 会被编译成一个对象
children: {
header() {
return { type: 'h1', children: '我是标题' };
},
body() {
return { type: 'div', children: '我是内容' };
}
}
};
}
MyComponent 组件会被编译成:
function render() {
return [
{
type: 'header',
children: [this.$slots.header()]
},
{
type: 'body',
children: [this.$slots.body()]
}
];
}
在运行时的实现上,插槽依赖 setupContext 上的 slots 对象
function mountComponent(vnode, container, anchor) {
/** ... */
const slots = vnode.children || {};
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots
};
const setupContext = { attrs, emit, slots };
const renderContext = new Proxy(instance, {
get(t, k, r) {
const { state, props, slots } = instance;
// 通过劫持 $slots,使 this 可以访问到插槽对象
if (k === '$slots') return slots;
if (k in state) {
return state[k];
} else if (k in props) {
return props[k];
} else {
console.error('不存在');
}
}
});
}
注册生命周期
Vue3 中有 onMounted
这种注册生命周期的方式,那么它是怎么注册到当前实例的呢?
import { onMounted } from 'vue';
const Comp = {
setup() {
onMounted(() => {
console.log('onMounted1');
});
// 注册多次
onMounted(() => {
console.log('onMounted2');
});
}
};
Vue3 中定义了一个全局实例 currentInstance
, 每次调用 setup 前设置值。
let currentInstance = null;
function setCurrentInstance(instance) {
currentInstance = instance;
}
function mountComponent(vnode, container, anchor) {
/** ... */
const instance = {
state,
props: shallowReactive(props),
isMounted: false,
subTree: null,
slots,
// 在实例中创建数组,用来存储通过 onMounted 函数注册的回调
mounted: []
};
const setupContext = { attrs, emit, slots };
setCurrentInstance(instance);
const setupRes = setup(shallowReadonly(instance.props), setupContext);
setCurrentInstance(null);
/** ... */
effect(() => {
// 第一次渲染
if (!instance.isMounted) {
/** ... */
instance.isMounted = true;
// 执行 vnode 上的钩子
mounted && mounted.call(state);
// 执行 onMounted 注册的回调
instance.mounted.forEach(fn => fn.call(renderContext));
}
/**... */
});
}
onMounted 的实现:
function onMounted(cb) {
if (currentInstance) {
currentInstance.mounted.push(cb);
}
}