Vue3中组件中定义的Data分析

前提准备

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一样的方式。

  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值