Props 的实现
开发者接口
首先,我们来实现props。 让我们先考虑最终的开发者接口。 我们考虑通过setup函数的第一个参数来传递props。
const MyComponent = {
props: { message: { type: String } },
setup(props) {
return () => h('div', { id: 'my-app' }, [`message: ${props.message}`])
},
}
const app = createApp({
setup() {
const state = reactive({ message: 'hello' })
const changeMessage = () => {
state.message += '!'
}
return () =>
h('div', { id: 'my-app' }, [
h(MyComponent, { message: state.message }, []),
])
},
})
实现
基于这个接口,我们考虑需要在ComponentInternalInstance中添加什么信息。 我们需要一个属性来存储类似props: { message: { type: String } }
的props定义,以及一个存储props实际值的属性,因此添加如下内容:
export type Data = Record<string, unknown>
export interface ComponentInternalInstance {
// .
// .
// .
propsOptions: Props // 保存类似`props: { message: { type: String } }`的对象
props: Data // 保存从父组件传递的实际数据 (在这个例子中,就是 `{ message: "hello" }` 这样的对象)
}
然后创建一个新文件~/packages/runtime-core/componentProps.ts
,内容如下:
export type Props = Record<string, PropOptions | null>
export interface PropOptions<T = any> {
type?: PropType<T> | true | null
required?: boolean
default?: null | undefined | object
}
export type PropType<T> = { new (...args: any[]): T & {} }
同时在用户组件选项中添加props:
export type ComponentOptions = {
props?: Record<string, any> // 添加
setup?: () => Function
render?: Function
}
在createComponentInstance创建实例时,将从选项传入的props定义设置到propsOptions中:
export function createComponentInstance(
vnode: VNode
): ComponentInternalInstance {
const type = vnode.type as Component;
const instance: ComponentInternalInstance = {
// .
// .
// .
propsOptions: type.props || {},
props: {},
关于instance.props的形成,我们在组件挂载时根据propsOptions过滤vnode持有的props。 然后通过reactive函数将过滤后的对象转化为响应式对象,并设置到instance.props中。
我们在componentProps.ts中实现一系列这样的流程,称为initProps
函数:
export function initProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
) {
const props: Data = {}
setFullProps(instance, rawProps, props)
instance.props = reactive(props)
}
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
) {
const options = instance.propsOptions
if (rawProps) {
for (let key in rawProps) {
const value = rawProps[key]
if (options && options.hasOwnProperty(key)) {
props[key] = value
}
}
}
}
现在在挂载时执行initProps,并将props传递给setup函数:
const mountComponent = (initialVNode: VNode, container: RendererElement) => {
const instance: ComponentInternalInstance = (initialVNode.component =
createComponentInstance(initialVNode));
// init props
const { props } = instance.vnode;
initProps(instance, props);
const component = initialVNode.type as Component;
if (component.setup) {
instance.render = component.setup(
instance.props // 传递给setup
) as InternalRenderFunction;
}
// .
// .
// .
}
export type ComponentOptions = {
props?: Record<string, any>
setup?: (props: Record<string, any>) => Function // 修改为接收props
render?: Function
}
此时应该已经可以将props传递给子组件了,让我们在playground中验证一下:
const MyComponent = {
props: { message: { type: String } },
setup(props: { message: string }) {
return () => h('div', { id: 'my-app' }, [`message: ${props.message}`])
},
}
const app = createApp({
setup() {
const state = reactive({ message: 'hello' })
return () =>
h('div', { id: 'my-app' }, [
h(MyComponent, { message: state.message }, []),
])
},
})
然而,这样还不够,当props改变时视图不会更新。
const MyComponent = {
props: { message: { type: String } },
setup(props: { message: string }) {
return () => h('div', { id: 'my-app' }, [`message: ${props.message}`])
},
}
const app = createApp({
setup() {
const state = reactive({ message: 'hello' })
const changeMessage = () => {
state.message += '!'
}
return () =>
h('div', { id: 'my-app' }, [
h(MyComponent, { message: state.message }, []),
h('button', { onClick: changeMessage }, ['change message']),
])
},
})
为了让这样的组件能够正常工作,我们在componentProps.ts中实现updateProps
函数,在组件更新时执行它:
~/packages/runtime-core/componentProps.ts
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
) {
const { props } = instance
Object.assign(props, rawProps)
}
~/packages/runtime-core/renderer.ts
const setupRenderEffect = (
instance: ComponentInternalInstance,
initialVNode: VNode,
container: RendererElement
) => {
const componentUpdateFn = () => {
const { render } = instance;
if (!instance.isMounted) {
const subTree = (instance.subTree = normalizeVNode(render()));
patch(null, subTree, container);
initialVNode.el = subTree.el;
instance.isMounted = true;
} else {
let { next, vnode } = instance;
if (next) {
next.el = vnode.el;
next.component = instance;
instance.vnode = next;
instance.next = null;
updateProps(instance, next.props); // 这里
这样就可以更新屏幕了。 现在,通过使用props,我们可以向组件传递数据了!成功!
到目前为止的源代码:
chibivue (GitHub)
顺便说一下,原版Vue可以使用kebab-case接收props,所以我们也来实现这个功能。 在这里,我们创建一个新的目录~/packages/shared
,并创建general.ts
文件。 这个地方是定义不限于runtime-core或runtime-dom的通用函数的地方。 此时创建这个目录并没有特别的意义,只是为了跟原版Vue保持一致。 现在,我们实现hasOwn
和camelize
函数:
~/packages/shared/general.ts
const hasOwnProperty = Object.prototype.hasOwnProperty
export const hasOwn = (
val: object,
key: string | symbol,
): key is keyof typeof val => hasOwnProperty.call(val, key)
const camelizeRE = /-(\w)/g
export const camelize = (str: string): string => {
return str.replace(camelizeRE, (_, c) => (c ? c.toUpperCase() : ''))
}
在componentProps.ts中使用camelize:
export function updateProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
) {
const { props } = instance
// -------------------------------------------------------------- 这里
Object.entries(rawProps ?? {}).forEach(([key, value]) => {
props[camelize(key)] = value
})
}
function setFullProps(
instance: ComponentInternalInstance,
rawProps: Data | null,
props: Data,
) {
const options = instance.propsOptions
if (rawProps) {
for (let key in rawProps) {
const value = rawProps[key]
// -------------------------------------------------------------- 这里
// kebab -> camel
let camelKey
if (options && hasOwn(options, (camelKey = camelize(key)))) {
props[camelKey] = value
}
}
}
}
这样就可以处理kebab-case了。让我们在playground中验证:
const MyComponent = {
props: { someMessage: { type: String } },
setup(props: { someMessage: string }) {
return () => h('div', {}, [`someMessage: ${props.someMessage}`])
},
}
const app = createApp({
setup() {
const state = reactive({ message: 'hello' })
const changeMessage = () => {
state.message += '!'
}
return () =>
h('div', { id: 'my-app' }, [
h(MyComponent, { 'some-message': state.message }, []),
h('button', { onClick: changeMessage }, ['change message']),
])
},
})