一不小心,我竟然修改了props

前言

这是vue3系列源码的第八章,使用的vue3版本是3.2.45

推荐

createApp都发生了什么

mount都发生了什么

页面到底是从什么时候开始渲染的

setup中的内容到底是什么时候执行的

ref reactive是怎么实现的

响应式到底是怎么实现的

页面是如何更新的

背景

先看上这么一个例子 在线样例

你发现了什么?porps中的值被修改了,页面也更新了!!!


在前的文章中,我们主要看了页面的渲染和简单的更新,还看了响应式的实现原理。

这一章的主要内容本来是props的实现,但是无意间发现了对props修改的一个方法。

这里我们将借助这个案例来分析一下porps的实现。

前置

和案例上的dmeo差不多,我们需要准备一个App.vue组件和一个HellowWorld.vue子组件。

// App.vue
<template>
  <HelloWorld :msg="aa" />
 </template>
 <script setup>
 import { ref } from 'vue'
 import HelloWorld from './HellowWorld.vue';
 
 const aa = ref('小识')
 
 </script>
// 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

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

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的内容是啥呢:

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,getset的时候,都会按照setupState -> data -> props -> ctx的这样一种顺序来进行操作。
  • 对于$开头的属性,get的时候会到pblicPropertiesMap中找对应的publicGetterset的时候会报错
  • set里面有一条,如果是instance.props的属性,那么也会报错,因为只读
 else if (hasOwn(instance.props, key)) {
(process.env.NODE_ENV !== 'production') && warn(`Attempting to mutate prop "${key}". Props are readonly.`);
return false;
}

一不小心,我竟然修改了props

所以我们总结一下:

这里对instance.ctx做了代理,对其setget操作做了设置。

回到我们今天的主题proxy上,无论是$props还是通过ctx设置instance.props的值,都会报错。

那么说完了对initPropsctx的处理之后,我们就该看set的执行了

setup

const setupResult = callWithErrorHandling(setup, instance,  0 /* ErrorCodes.SETUP_FUNCTION */, 
[(process.env.NODE_ENV !== 'production') ?  shallowReadonly(instance.props) : instance.props, setupContext]);

这里对传进去的instance.props做了一层包装,shallowReadonly

function shallowReadonly(target) {
return createReactiveObject(target, true, shallowReadonlyHandlers, shallowReadonlyCollectionHandlers, shallowReadonlyMap);
}

createReactiveObject中,实际执行的是下面这行代码:

const proxy = new Proxy(target, shallowReadonlyHandlers);
return proxy

shallowReadonlyHandlers是继承自readonlyHandlers

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里面做任何对propsset都会报错。

这是setup执行的结果:

一不小心,我竟然修改了props

到了这里,我们其实弄清楚了文章的其中一个目的:

那就是组件的props是怎么生成的。

但是我们的重点是,propsset为什么能成功。到这里,并没有解开我们的困惑,因为我们的set操作不是在setup中执行的,我们写在了模板上。

下面进入rendercall阶段

render中的props

我们进入子组件的setupRenderEffect中的componentUpdateFn中的renderComponentRoot

下面的render函数将解析template,而他传入的参数都来自于instance

 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

一不小心,我竟然修改了props

这里的props就是initProps中生成的,是没有readonly的。

我们先看一下<button @click="msg = '谭记'">点击</button>解析出来的vnode

一不小心,我竟然修改了props

他解析成了$props.msg = '谭记',而我们看见了render函数中的参数,$props其实就是我们传入的props

接下来我们看下面一句的解析:

  <div>
{{ $props.msg }}{{msg}}{{props.msg}}
</div>

createDevRenderContext函数中,我们看到了这个时候的instance.ctx

一不小心,我竟然修改了props

此时的ctx中竟然多了一个msg属性,并且指向的是instance.props

$props.msg

{{ $props.msg}} 会被解析成 _ctx.$props.msg

其中 $props 触发了我们上文中提到的 PublicInstanceProxyHandlersget,得到 $propspublicGetter

// i 就是instance
$props: i => ((process.env.NODE_ENV !== 'production') ? shallowReadonly(i.props) : i.props)

最终返回了instance.props

然后msg又会触发instance.propsget,最终得到值

而且因为这里包了shallowReadonly,所以这里的get也不会触发track函数,即不会触发副作用函数的收集

// createGeter
if (!isReadonly) {
track(tarck, "get", key)
}

msg

{{ msg }} 会被解析成$props.msg

而这里的 $props 就是我们传入的instance.props

所以直接触发了propscreateGetter,返回了instance.props.msg的值

前面说过instance.props是被代理的对象,并且没有做readonly的限制,
所以这里触发了对msgget

并且触发了track,收集了副作用,这也就是后面能够修改值,并且改了值,页面也会刷新的原因。

props.msg

{{ props.msg}} 会被解析成$setup.props.msg,这个就是我们在setup中定义的那个props

所以先触发了setup中对propsget

然后又触发了对props中对msgget

这一步也没有触发track

总结一下:

同一个对象的值,最终都是同一个props.msg,但是不同的写法,会有很大的区别。

这么灵活的写法主要归功于PublicInstanceProxyHandlers,他让我们可以直接写key,然后它自己根据规则去找对应的对象。

总结

那么到这里,我们其实差不多也搞明白了,为啥这里的props最终可以被改变。

因为msg = '谭记'"这种写法绕过了ctxsetup中对props的限制,没有触发readonly的报错。

并且在get的时候触发了track,所以页面也发生了更新。

而且也只有@click="msg = '谭记'"这种写法能绕过这些限制,换一种写法,首先在set那一步就会报错,更别说页面的更新了。

原文链接:https://juejin.cn/post/7328292293915263026 作者:小识谭记

(0)
上一篇 2024年1月27日 上午10:00
下一篇 2024年1月27日 上午10:10

相关推荐

发表回复

登录后才能评论