一不小心,我竟然修改了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那一步就会报错,更别说页面的更新了。

相关推荐
专注API从业者4 小时前
Python + 淘宝 API 开发:自动化采集商品数据的完整流程
大数据·运维·前端·数据挖掘·自动化
烛阴4 小时前
TypeScript高手密技:解密类型断言、非空断言与 `const` 断言
前端·javascript·typescript
样子20185 小时前
Uniapp 之renderjs解决swiper+多个video卡顿问题
前端·javascript·css·uni-app·html
Nicholas685 小时前
flutterAppBar之SystemUiOverlayStyle源码解析(一)
前端
黑客飓风6 小时前
JavaScript 性能优化实战大纲
前端·javascript·性能优化
emojiwoo7 小时前
【前端基础知识系列六】React 项目基本框架及常见文件夹作用总结(图文版)
前端·react.js·前端框架
张人玉8 小时前
XML 序列化与操作详解笔记
xml·前端·笔记
杨荧8 小时前
基于Python的宠物服务管理系统 Python+Django+Vue.js
大数据·前端·vue.js·爬虫·python·信息可视化
YeeWang8 小时前
🎉 Eficy 让你的 Cherry Studio 直接生成可预览的 React 页面
前端·javascript
gnip8 小时前
Jenkins部署前端项目实战方案
前端·javascript·架构