前提准备
Vue3 的版本3.4.30
源代码:https://cdn.bootcdn.net/ajax/libs/vue/3.4.30/vue.global.js
本文分析的问题
1、组件描述对象中data和setup返回的数据项是怎么在组件中起作用的
2、在methods定义的方法里面为什么可以用this访问data 和 setup函数返回的数据
例子代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!--
<script src="https://unpkg.com/vue@3.4.30/dist/vue.global.js"></script>
-->
<script src="./vue/vuetest.global.js"></script>
</head>
<body>
<div id="app">
</div>
</body>
<script type="module">
var refdata=Vue.ref(10);
window.refdata=refdata;
const app = Vue.createApp({
name:"root-component",
data:function(){
return {
msg:'data-hello vue3',
name:"data-shanghai",
refdata:refdata
}
},
methods: {
testdata:function(){
//这个点击事件函数时怎么注册到按钮上的触发调用的第一个函数是?
//refdata.value=Math.random();
console.log("refdata.value= "+refdata.value);
console.log(this.msg);
}
},
computed:{
cptdata1:function(){
return "computed1: "+refdata.value+Math.random();
},
cptdata2:function(){
return "computed2: "+refdata.value+Math.random();
}
},
setup:function(props,context){
///执行setup函数时, 组件已经实例化了, 此时可以访问到实例对象
//此context其实一个代理,把组件的props, attrs, slots, emit 和 expose 代理到了context上
console.log("root-setup");
return {msg:"setup俄乌战争",weapon:"setup无人机创造历史"};
},
//用到了template,需要引入的vue中有编译功能的版本
template:`<div>
<div>msg: {{msg}}</div>
<div>name: {{name}}</div>
<div>refdata: {{refdata}}</div>
<div>weapon: {{weapon}}</div>
<div>{{cptdata1}}</div>
<div>{{cptdata2}}</div>
<div> <button @click="testdata">点击</button></div>
</div>`
},{});
app.mount('#app');
</script>
</html>
例子浏览器运行结果如下:点击了一次上面的按钮,触发testdata方法执行,控制台输出一些内容
看上面输出结果来看,msg在data 和 setup 两个函数返回中都用,从上面结果看是取的setup中的,不重名的其他数据有从data中获取的,说明setup的返回结果优先级别要大于data 返回的结果,等下从源码分析下,看vue3是怎么实现这个的。
通过分析源代码,data数据项目和methods里面定义的方法都是在function applyOptions(instance) 方法中进行处理的,通过加入断点调试具体看下里面的代码结果如下:
注意上图右边的调用栈信息,整个流程都处在组件初始化的阶段。具体的applyOptions方法源码如下:
function applyOptions(instance) {
const options = resolveMergedOptions(instance);
//proxy 是ctx的代理,具体代码在: function setupStatefulComponent(instance, isSSR) 方法中, 具体是: instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
const publicThis = instance.proxy;
const ctx = instance.ctx;
shouldCacheAccess = false;
if (options.beforeCreate) {
callHook$1(options.beforeCreate, instance, "bc");
}
const {
// state
data: dataOptions,
computed: computedOptions,
methods,
watch: watchOptions,
provide: provideOptions,
inject: injectOptions,
// lifecycle
created,
beforeMount,
mounted,
beforeUpdate,
updated,
activated,
deactivated,
beforeDestroy,
beforeUnmount,
destroyed,
unmounted,
render,
renderTracked,
renderTriggered,
errorCaptured,
serverPrefetch,
// public API
expose,
inheritAttrs,
// assets
components,
directives,
filters
} = options;
const checkDuplicateProperties = createDuplicateChecker() ;
{
const [propsOptions] = instance.propsOptions;
if (propsOptions) {
for (const key in propsOptions) {
checkDuplicateProperties("Props" /* PROPS */, key);
}
}
}
if (injectOptions) {
resolveInjections(injectOptions, ctx, checkDuplicateProperties);
}
if (methods) {
for (const key in methods) {
const methodHandler = methods[key];
if (isFunction(methodHandler)) {
{
///这里看 methods中定义的方法也都是扩展到了instance.ctx中,并且methods中的方法执行的this 都是指向instance.proxy的,而proxy要是ctx的代理,所以最终还是要到ctx中去取数据
Object.defineProperty(ctx, key, {
value: methodHandler.bind(publicThis),
configurable: true,
enumerable: true,
writable: true
});
}
{
checkDuplicateProperties("Methods" /* METHODS */, key);
}
} else {
warn$1(
`Method "${key}" has type "${typeof methodHandler}" in the component definition. Did you reference the function correctly?`
);
}
}
}
///dataOptions 就是组件定义中的data 函数
if (dataOptions) {
if (!isFunction(dataOptions)) {
warn$1(
`The data option must be a function. Plain object usage is no longer supported.`
);
}
//直接调用了data函数,并指定了方法的this为instance.proxy,这里的data变量就是dataOptions方法执行的结果了
const data = dataOptions.call(publicThis, publicThis);
if (isPromise(data)) {
warn$1(
`data() returned a Promise - note data() cannot be async; If you intend to perform data fetching before component renders, use async setup() + <Suspense>.`
);
}
if (!isObject(data)) {
///要求data返回的应该是一个object
warn$1(`data() should return an object.`);
} else {
///在这里 把data重新用reactive封装成响应式数据
instance.data = reactive(data);
{
///然后遍历返回结果的第一层属性
for (const key in data) {
checkDuplicateProperties("Data" /* DATA */, key);
///跳过一些特殊的属性const isReservedPrefix = (key) => key === "_" || key === "$";
if (!isReservedPrefix(key[0])) {
///把对应的key属性全部扩展到 instance.ctx中去。
Object.defineProperty(ctx, key, {
configurable: true,
enumerable: true,
get: () => data[key],
set: NOOP
});
}
}
}
}
}
shouldCacheAccess = true;
///把组件中定义的计算属性,用ComputedRefImpl进行封装,
if (computedOptions) {
for (const key in computedOptions) {
const opt = computedOptions[key];
const get = isFunction(opt) ? opt.bind(publicThis, publicThis) : isFunction(opt.get) ? opt.get.bind(publicThis, publicThis) : NOOP;
if (get === NOOP) {
warn$1(`Computed property "${key}" has no getter.`);
}
const set = !isFunction(opt) && isFunction(opt.set) ? opt.set.bind(publicThis) : () => {
warn$1(
`Write operation failed: computed property "${key}" is readonly.`
);
} ;
const c = computed({
get,
set
});
///把计算属性扩展到instance.ctx中去,本文中就是把cptdata1和cptdata2扩展到ctx中去
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => c.value,
set: (v) => c.value = v
});
{
checkDuplicateProperties("Computed" /* COMPUTED */, key);
}
}
}
if (watchOptions) {
for (const key in watchOptions) {
createWatcher(watchOptions[key], ctx, publicThis, key);
}
}
if (provideOptions) {
const provides = isFunction(provideOptions) ? provideOptions.call(publicThis) : provideOptions;
Reflect.ownKeys(provides).forEach((key) => {
provide(key, provides[key]);
});
}
if (created) {
callHook$1(created, instance, "c");
}
function registerLifecycleHook(register, hook) {
if (isArray(hook)) {
hook.forEach((_hook) => register(_hook.bind(publicThis)));
} else if (hook) {
register(hook.bind(publicThis));
}
}
registerLifecycleHook(onBeforeMount, beforeMount);
registerLifecycleHook(onMounted, mounted);
registerLifecycleHook(onBeforeUpdate, beforeUpdate);
registerLifecycleHook(onUpdated, updated);
registerLifecycleHook(onActivated, activated);
registerLifecycleHook(onDeactivated, deactivated);
registerLifecycleHook(onErrorCaptured, errorCaptured);
registerLifecycleHook(onRenderTracked, renderTracked);
registerLifecycleHook(onRenderTriggered, renderTriggered);
registerLifecycleHook(onBeforeUnmount, beforeUnmount);
registerLifecycleHook(onUnmounted, unmounted);
registerLifecycleHook(onServerPrefetch, serverPrefetch);
if (isArray(expose)) {
if (expose.length) {
const exposed = instance.exposed || (instance.exposed = {});
expose.forEach((key) => {
Object.defineProperty(exposed, key, {
get: () => publicThis[key],
set: (val) => publicThis[key] = val
});
});
} else if (!instance.exposed) {
instance.exposed = {};
}
}
if (render && instance.render === NOOP) {
instance.render = render;
}
if (inheritAttrs != null) {
instance.inheritAttrs = inheritAttrs;
}
if (components) instance.components = components;
if (directives) instance.directives = directives;
}
通过断点调试,看看instance.ctx 对象在执行applyOptions 方法前后的区别
执行前:
执行到此函数底部:
从上面结果看,执行完成applyOptions方法后 instance.ctx 多了很多属性,就是把data,methods,computed中的数据都扩展到ctx中去了。其中执行前就有的msg 和 weapon属性时从setup方法返回结果中获取的,具体代码如下:
///把实例中的setupState 的setup返回结构暴露到ctx中
function exposeSetupStateOnRenderContext(instance) {
//setupState 是setup方法返回的结果
const { ctx, setupState } = instance;
Object.keys(toRaw(setupState)).forEach((key) => {
if (!setupState.__isScriptSetup) {
if (isReservedPrefix(key[0])) {
warn$1(
`setup() return property ${JSON.stringify(
key
)} should not start with "$" or "_" which are reserved prefixes for Vue internals.`
);
return;
}
///把setup返回的结果都扩展到ctx中去。
Object.defineProperty(ctx, key, {
enumerable: true,
configurable: true,
get: () => setupState[key],
set: NOOP
});
}
});
}
看下断点具体执行栈信息
现在回答第二个问题
为什么在testdata方法中可以用this 访问data,setup方法返回的数据?
从上面的代码看,在处理methods时,里面的方法执行时上下文的this 是instance.proxy,也就是ctx,而data 和 setup方法返回的结果都扩展到了ctx上,所有可以直接用this.msg 访问这个属性。
Object.defineProperty(ctx, key, {
value: methodHandler.bind(publicThis),
configurable: true,
enumerable: true,
writable: true
});
哪现在又有个新问题就是:为什么this.msg的结果 是setup方法返回的而不是data 函数返回的呢?
现在我们知道testdata方法的this是instance.proxy,那么我们具体看下这个代理的具体实现,就应该知道原因了。先看:上面的注释proxy 是ctx的代理,具体代码在: function setupStatefulComponent(instance, isSSR) 方法中, 具体是: instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers); 现在看看PublicInstanceProxyHandlers代码如下,我只把get 的拦截展现出来了
const PublicInstanceProxyHandlers = {
///{ _: instance } 因为get 第一个参数一般是target 而这个target 在上下文中是instance.proxy(对应的ctx,
//这个对象中有一个属性_ 是instance) 当调用this.msg属性时首先拦截get传入了参数instance.proxy 但是由于get
//的参数列表是一个{} 解构化对象,那么js会自动解构instance.proxy 类似 { _: instance }=instance.proxy ,这样就自动把 "_" 属性传进来了
///在JavaScript中,函数参数的解构赋值允许你将一个对象解构为函数的参数。当你看到 {data: msg} 这样的语法时,
///这意味着函数期望接收一个对象,并且这个对象应该有一个名为 data 的属性。解构赋值允许你将这个 data 属性的值赋给函数体内的一个变量 msg
get({ _: instance }, key) {
if (key === "__v_skip") {
return true;
}
const { ctx, setupState, data, props, accessCache, type, appContext } = instance;
if (key === "__isVue") {
return true;
}
let normalizedProps;
if (key[0] !== "$") {
///accessCache 用于缓存这个key是什么类型的数据,下次访问提高效率,从下面的代码看,
//获取数据的优先级别是: setupState(就是setup方法返回的结果),data,ctx,props。
///从这里的代码看 vue3把data,setup 的结果扩展到ctx中,但是最后获取数据还是从具体的data,setupstate中获取的,估计把数据扩展到ctx应该可以起到写代码this.xxx更方便
const n = accessCache[key];
if (n !== void 0) {
switch (n) {
case 1 /* SETUP */:
return setupState[key];
case 2 /* DATA */:
return data[key];
case 4 /* CONTEXT */:
return ctx[key];
case 3 /* PROPS */:
return props[key];
}
} else if (hasSetupBinding(setupState, key)) {
accessCache[key] = 1 /* SETUP */;
return setupState[key];
} else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
accessCache[key] = 2 /* 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 /* PROPS */;
return props[key];
} else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
accessCache[key] = 4 /* CONTEXT */;
return ctx[key];
} else if (shouldCacheAccess) {
accessCache[key] = 0 /* OTHER */;
}
}
const publicGetter = publicPropertiesMap[key];
let cssModule, globalProperties;
if (publicGetter) {
if (key === "$attrs") {
track(instance.attrs, "get", "");
markAttrsAccessed();
} else if (key === "$slots") {
track(instance, "get", key);
}
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)) {
accessCache[key] = 4 /* CONTEXT */;
return ctx[key];
} else if (
// global properties
globalProperties = appContext.config.globalProperties, hasOwn(globalProperties, key)
) {
{
return globalProperties[key];
}
} else if (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$1(
`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$1(
`Property ${JSON.stringify(key)} was accessed during render but is not defined on instance.`
);
}
}
}
其他代码省略
}
现在回答第一个问题
data和setup返回的数据,都是通过 Object.defineProperty 方法,把他们的返回结果扩展到ctx上,然后通过instance.proxy的代理来实现具体的访问,而代理PublicInstanceProxyHandlers.get拦截访问之后要去setupState data ctx props 几个instance数据对象中访问的。
而ctx上的数据在渲染组件页面数据时要是怎么访问的,我们来看下上面的模版template编译之后的render函数
上面用到了with() 代码块作用域,而这个_ctx要是什么?,看下调用栈信息
可以看到渲染函数中的属性 都是从instance.withProxy 对象中获取的,它要是ctx的另外的代理,它创建的过程是如下:因为我这个例子用到了模版template,所以需要编译模块
最后访问要回到了和instance.proxy一样的方式。