前言
这是vue3系列源码的第八章,使用的vue3版本是3.2.45
。
推荐
背景
先看上这么一个例子 在线样例
你发现了什么? ,porps中的值被修改了,页面也更新了!!!
在前的文章中,我们主要看了页面的渲染和简单的更新,还看了响应式的实现原理。
这一章的主要内容本来是props的实现,但是无意间发现了对props修改的一个方法。
这里我们将借助这个案例来分析一下porps的实现。
前置
和案例上的dmeo差不多,我们需要准备一个App.vue
组件和一个HellowWorld.vue
子组件。
js
// App.vue
<template>
<HelloWorld :msg="aa" />
</template>
<script setup>
import { ref } from 'vue'
import HelloWorld from './HellowWorld.vue';
const aa = ref('小识')
</script>
js
// HelloWorld.vue
<template>
<button @click="msg = '谭记'">点击</button>
<div>
{{ $props.msg }}{{msg}}{{props.msg}}
</div>
</template>
<script setup>
import { defineProps } from 'vue'
const props = defineProps({msg: String})
</script>
这里子组件中放了这么多msg, 是为了在后面比较他们的差异。
props属性的生成
这里我们先看一下源码中都是如何生成props属性的,又做了哪些处理。
这里我们直接进入子组件的解析中。
在setupComponent 函数中,我们提到过执行了setup 中的内容,但是在这之前,还执行了initProps。
initProps
js
function initProps(instance, rawProps, isStateful, isSSR = false) {
const props = {};
const attrs = {};
def(attrs, InternalObjectKey, 1);
instance.propsDefaults = Object.create(null);
setFullProps(instance, rawProps, props, attrs);
// ensure all declared prop keys are present
for (const key in instance.propsOptions[0]) {
if (!(key in props)) {
props[key] = undefined;
}
}
if (isStateful) {
// stateful
instance.props = isSSR ? props : shallowReactive(props);
}
else {
if (!instance.type.props) {
// functional w/ optional props, props === attrs
instance.props = attrs;
}
else {
// functional w/ declared props
instance.props = props;
}
}
instance.attrs = attrs;
}
首先看一下传入的参数:
- instance,
HelloWorld
组件的实例 - rawProps, 父组件传进来的props对象
{msg: '小识'}
- isStateful, 4
这里先创建了一个props对象,这个对象将挂载到组件上。
接着在setFullProps 函数中,把父组件中的props对象rowProps 中的值,根据我们在defineProps 函数中申明的key ,做了一个浅拷贝。此时的props 是{msg: '小识'}
。
然后对申明了,但是没有传入值的key 做了undefined的处理。
接着调用了shallowReactive 函数,对props 做了proxy代理,变成了响应式的对象。
我们可以总结以下几点:
- instance.props是一个被代理的对象,也就是说,如果对这个对象上的值进行修改了,是可能触发页面上的更新的
- instance.props并没有做只读的限制
- instance.props实际上是对父组件传入值的浅拷贝,所以如果传入复杂数据类型的时候,在子组件里对值的修改会影响到父组件
initProps 函数结束后,我们看一下setup的执行
setupStatefulComponent
js
function setupStatefulComponent(instance, isSSR) {
var _a;
const Component = instance.type;
// 0. create render proxy property access cache
instance.accessCache = Object.create(null);
// 1. create public instance / render proxy
// also mark it raw so it's never observed
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
if ((process.env.NODE_ENV !== 'production')) {
exposePropsOnRenderContext(instance);
}
// 2. call setup()
const { setup } = Component;
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
setCurrentInstance(instance);
pauseTracking();
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [(process.env.NODE_ENV !== 'production') ? shallowReadonly(instance.props) : instance.props, setupContext]);
resetTracking();
unsetCurrentInstance();
if (isPromise(setupResult)) {
setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
if (isSSR) {
// return the promise so server-renderer can wait on it
return setupResult
.then((resolvedResult) => {
handleSetupResult(instance, resolvedResult, isSSR);
})
.catch(e => {
handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
});
}
else {
// async setup returned Promise.
// bail here and wait for re-entry.
instance.asyncDep = setupResult;
if ((process.env.NODE_ENV !== 'production') && !instance.suspense) {
const name = (_a = Component.name) !== null && _a !== void 0 ? _a : 'Anonymous';
warn(`Component <${name}>: setup function returned a promise, but no ` +
`<Suspense> boundary was found in the parent component tree. ` +
`A component with async setup() must be nested in a <Suspense> ` +
`in order to be rendered.`);
}
}
}
else {
handleSetupResult(instance, setupResult, isSSR);
}
}
else {
finishComponentSetup(instance, isSSR);
}
}
这里有一句
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
这里给instance.ctx 做了代理,那么这个PublicInstanceProxyHandlers的内容是啥呢:
js
const PublicInstanceProxyHandlers = {
get({ _: instance }, key) {
const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
// for internal formatters to know that this is a Vue instance
if ((process.env.NODE_ENV !== 'production') && key === '__isVue') {
return true;
}
// data / props / ctx
// This getter gets called for every property access on the render context
// during render and is a major hotspot. The most expensive part of this
// is the multiple hasOwn() calls. It's much faster to do a simple property
// access on a plain object, so we use an accessCache object (with null
// prototype) to memoize what access type a key corresponds to.
let normalizedProps;
if (key[0] !== '$') {
const n = accessCache[key];
if (n !== undefined) {
switch (n) {
case 1 /* AccessTypes.SETUP */:
return setupState[key];
case 2 /* AccessTypes.DATA */:
return data[key];
case 4 /* AccessTypes.CONTEXT */:
return ctx[key];
case 3 /* AccessTypes.PROPS */:
return props[key];
// default: just fallthrough
}
}
else if (hasSetupBinding(setupState, key)) {
accessCache[key] = 1 /* AccessTypes.SETUP */;
return setupState[key];
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 2 /* AccessTypes.DATA */;
return data[key];
}
else if (
// only cache other properties when instance has declared (thus stable)
// props
(normalizedProps = instance.propsOptions[0]) &&
hasOwn(normalizedProps, key)) {
accessCache[key] = 3 /* AccessTypes.PROPS */;
return props[key];
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 4 /* AccessTypes.CONTEXT */;
return ctx[key];
}
else if (!__VUE_OPTIONS_API__ || shouldCacheAccess) {
accessCache[key] = 0 /* AccessTypes.OTHER */;
}
}
const publicGetter = publicPropertiesMap[key];
let cssModule, globalProperties;
// public $xxx properties
if (publicGetter) {
if (key === '$attrs') {
track(instance, "get" /* TrackOpTypes.GET */, key);
(process.env.NODE_ENV !== 'production') && markAttrsAccessed();
}
return publicGetter(instance);
}
else if (
// css module (injected by vue-loader)
(cssModule = type.__cssModules) &&
(cssModule = cssModule[key])) {
return cssModule;
}
else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
// user may set custom properties to `this` that start with `$`
accessCache[key] = 4 /* AccessTypes.CONTEXT */;
return ctx[key];
}
else if (
// global properties
((globalProperties = appContext.config.globalProperties),
hasOwn(globalProperties, key))) {
{
return globalProperties[key];
}
}
else if ((process.env.NODE_ENV !== 'production') &&
currentRenderingInstance &&
(!isString(key) ||
// #1091 avoid internal isRef/isVNode checks on component instance leading
// to infinite warning loop
key.indexOf('__v') !== 0)) {
if (data !== EMPTY_OBJ && isReservedPrefix(key[0]) && hasOwn(data, key)) {
warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
`character ("$" or "_") and is not proxied on the render context.`);
}
else if (instance === currentRenderingInstance) {
warn(`Property ${JSON.stringify(key)} was accessed during render ` +
`but is not defined on instance.`);
}
}
},
set({ _: instance }, key, value) {
const { data, setupState, ctx } = instance;
if (hasSetupBinding(setupState, key)) {
setupState[key] = value;
return true;
}
else if ((process.env.NODE_ENV !== 'production') &&
setupState.__isScriptSetup &&
hasOwn(setupState, key)) {
warn(`Cannot mutate <script setup> binding "${key}" from Options API.`);
return false;
}
else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
data[key] = value;
return true;
}
else if (hasOwn(instance.props, key)) {
(process.env.NODE_ENV !== 'production') && warn(`Attempting to mutate prop "${key}". Props are readonly.`);
return false;
}
if (key[0] === '$' && key.slice(1) in instance) {
(process.env.NODE_ENV !== 'production') &&
warn(`Attempting to mutate public property "${key}". ` +
`Properties starting with $ are reserved and readonly.`);
return false;
}
else {
if ((process.env.NODE_ENV !== 'production') && key in instance.appContext.config.globalProperties) {
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
value
});
}
else {
ctx[key] = value;
}
}
return true;
},
has({ _: { data, setupState, accessCache, ctx, appContext, propsOptions } }, key) {
let normalizedProps;
return (!!accessCache[key] ||
(data !== EMPTY_OBJ && hasOwn(data, key)) ||
hasSetupBinding(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key) ||
hasOwn(publicPropertiesMap, key) ||
hasOwn(appContext.config.globalProperties, key));
},
defineProperty(target, key, descriptor) {
if (descriptor.get != null) {
// invalidate key cache of a getter based property #5417
target._.accessCache[key] = 0;
}
else if (hasOwn(descriptor, 'value')) {
this.set(target, key, descriptor.value, null);
}
return Reflect.defineProperty(target, key, descriptor);
}
};
代码很长,我们简单分析一下:
- 首先对于key 要分两种,一种是
$
开头的,一种不以他开头 - 对于普通的key ,get 和set 的时候,都会按照setupState -> data -> props -> ctx的这样一种顺序来进行操作。
- 对于
$
开头的属性,get 的时候会到pblicPropertiesMap 中找对应的publicGetter ,set的时候会报错 - 在set 里面有一条,如果是instance.props的属性,那么也会报错,因为只读
js
else if (hasOwn(instance.props, key)) {
(process.env.NODE_ENV !== 'production') && warn(`Attempting to mutate prop "${key}". Props are readonly.`);
return false;
}
所以我们总结一下:
这里对instance.ctx 做了代理,对其set 和get操作做了设置。
回到我们今天的主题proxy 上,无论是$props
还是通过ctx 设置instance.props的值,都会报错。
那么说完了对initProps 和ctx 的处理之后,我们就该看set的执行了
setup
js
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */,
[(process.env.NODE_ENV !== 'production') ? shallowReadonly(instance.props) : instance.props, setupContext]);
这里对传进去的instance.props 做了一层包装,shallowReadonly
js
function shallowReadonly(target) {
return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);
}
在createReactiveObject中,实际执行的是下面这行代码:
js
const proxy = new Proxy(target, shallowReadonlyHandlers);
return proxy
shallowReadonlyHandlers 是继承自readonlyHandlers
js
const readonlyHandlers = {
get: readonlyGet,
set(target, key) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
}
return true;
},
deleteProperty(target, key) {
if ((process.env.NODE_ENV !== 'production')) {
warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
}
return true;
}
};
简单的说,就是加了一层代理,把instance.props 的所有set操作禁止了。
所以我们在setup 里面做任何对props 的set都会报错。
这是setup执行的结果:
到了这里,我们其实弄清楚了文章的其中一个目的:
那就是组件的props是怎么生成的。
但是我们的重点是,props 的set 为什么能成功。到这里,并没有解开我们的困惑,因为我们的set 操作不是在setup中执行的,我们写在了模板上。
下面进入render 的call阶段
render中的props
我们进入子组件的setupRenderEffect 中的componentUpdateFn 中的renderComponentRoot中
下面的render 函数将解析template ,而他传入的参数都来自于instance
js
const { type: Component, vnode, proxy, withProxy, props,
propsOptions: [propsOptions], slots, attrs, emit, render,
renderCache, data, setupState, ctx, inheritAttrs } = instance;
...
result = normalizeVNode(render.call(proxyToUse, proxyToUse, renderCache, props, setupState, data, ctx));
而render函数中的参数是这么设置的:
这里我们看一下传入的props
这里的props 就是initProps 中生成的,是没有readonly的。
我们先看一下<button @click="msg = '谭记'">点击</button>
解析出来的vnode
他解析成了$props.msg = '谭记'
,而我们看见了render 函数中的参数,$props 其实就是我们传入的props
接下来我们看下面一句的解析:
js
<div>
{{ $props.msg }}{{msg}}{{props.msg}}
</div>
在createDevRenderContext 函数中,我们看到了这个时候的instance.ctx
此时的ctx 中竟然多了一个msg 属性,并且指向的是instance.props
$props.msg
{{ $props.msg}}
会被解析成 _ctx.$props.msg
其中 $props
触发了我们上文中提到的 PublicInstanceProxyHandlers 的 get ,得到 $props 的publicGetter
js
// i 就是instance
$props: i => ((process.env.NODE_ENV !== 'production') ? shallowReadonly(i.props) : i.props)
最终返回了instance.props
然后msg 又会触发instance.props 的get,最终得到值
而且因为这里包了shallowReadonly ,所以这里的get 也不会触发track函数,即不会触发副作用函数的收集
js
// createGeter
if (!isReadonly) {
track(tarck, "get", key)
}
msg
{{ msg }}
会被解析成$props.msg
而这里的 $props 就是我们传入的instance.props
所以直接触发了props 的createGetter ,返回了instance.props.msg的值
前面说过instance.props 是被代理的对象,并且没有做readonly 的限制, 所以这里触发了对msg 的get
并且触发了track,收集了副作用,这也就是后面能够修改值,并且改了值,页面也会刷新的原因。
props.msg
{{ props.msg}}
会被解析成$setup.props.msg
,这个就是我们在setup 中定义的那个props。
所以先触发了setup 中对props 的get
然后又触发了对props 中对msg 的get
这一步也没有触发track
总结一下:
同一个对象的值,最终都是同一个props.msg,但是不同的写法,会有很大的区别。
这么灵活的写法主要归功于PublicInstanceProxyHandlers ,他让我们可以直接写key,然后它自己根据规则去找对应的对象。
总结
那么到这里,我们其实差不多也搞明白了,为啥这里的props最终可以被改变。
因为msg = '谭记'"
这种写法绕过了ctx 和setup 中对props 的限制,没有触发readonly的报错。
并且在get 的时候触发了track,所以页面也发生了更新。
而且也只有@click="msg = '谭记'"
这种写法能绕过这些限制,换一种写法,首先在set那一步就会报错,更别说页面的更新了。