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

前言

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

推荐

createApp都发生了什么

mount都发生了什么

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

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

ref reactive是怎么实现的

响应式到底是怎么实现的

页面是如何更新的

背景

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

你发现了什么?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 ,getset 的时候,都会按照setupState -> data -> props -> ctx的这样一种顺序来进行操作。
  • 对于$开头的属性,get 的时候会到pblicPropertiesMap 中找对应的publicGetterset的时候会报错
  • 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 做了代理,对其setget操作做了设置。

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

那么说完了对initPropsctx 的处理之后,我们就该看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 里面做任何对propsset都会报错。

这是setup执行的结果:

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

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

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

下面进入rendercall阶段

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 触发了我们上文中提到的 PublicInstanceProxyHandlersget ,得到 $propspublicGetter

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

最终返回了instance.props

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

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

js 复制代码
// 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那一步就会报错,更别说页面的更新了。

相关推荐
m0_7482517215 分钟前
DataOps驱动数据集成创新:Apache DolphinScheduler & SeaTunnel on Amazon Web Services
前端·apache
珊珊来吃16 分钟前
EXCEL中给某一列数据加上双引号
java·前端·excel
胡西风_foxww44 分钟前
【ES6复习笔记】Spread 扩展运算符(8)
前端·笔记·es6·扩展·运算符·spread
小林爱1 小时前
【Compose multiplatform教程08】【组件】Text组件
android·java·前端·ui·前端框架·kotlin·android studio
跨境商城搭建开发1 小时前
一个服务器可以搭建几个网站?搭建一个网站的流程介绍
运维·服务器·前端·vue.js·mysql·npm·php
hhzz1 小时前
vue前端项目中实现电子签名功能(附完整源码)
前端·javascript·vue.js
秋雨凉人心1 小时前
上传npm包加强
开发语言·前端·javascript·webpack·npm·node.js
时清云2 小时前
【算法】 课程表
前端·算法·面试
NoneCoder2 小时前
CSS系列(37)-- Overscroll Behavior详解
前端·css
Nejosi_念旧2 小时前
使用Webpack构建NPM Library
前端·webpack·npm