likes
comments
collection
share

Vue3源码分析(7)-组件挂载之其他配置处理与生命周期函数注册

作者站长头像
站长
· 阅读数 26

前情提要

  • 上文我们主要分析了,在完成组件实例的创建之后对slotsprops的初始化。了解到了对于父组件传递给子组件的参数那些改分配到实例的props中、那些改分配到实例的attrs当中。
  • 我们还详细讲解了编译后的插槽内容满足标准形式,并对这个标准形式下了四个定义
  • 然后我们还讲解了如果用户自己写了render函数,且这个函数中包含插槽内容,该如何将其标准化

本文主要内容

  • Vue3如何兼容Vue2选项式api
  • 对组件定义的mixins和extends进行合并的合并策略。
  • setup函数何时调用? beforeCreate调用后做了什么?何时调用created
  • 为什么模板中可以直接使用setup返回值、以及定义的data、methods等属性?
  • 如何将data、methods、props等属性代理到ctx上?
  • methods、computed、lifeCycle中访问的this到底是什么?
  • instance.ctxinstance.proxy有什么关系?分别在Vue中扮演了怎样的角色。

初始化有状态组件

  • 我们先来回顾一下初始化组件的函数setupComponent,其中上文主要讲解的就是initPropsinitSlots。接下来判断当前实例是否是有状态组件生成的实例,简单说一下有状态组件无状态组件无状态组件是一个函数,而有状态组件是对象。本文主要任务就是讲解setupStatefulComponent的执行流程。
function setupComponent(instance) {
  const { props, children } = instance.vnode;
  const isStateful = isStatefulComponent(instance);
  initProps(instance, props, isStateful);
  initSlots(instance, children);
  const setupResult = isStateful ? 
    setupStatefulComponent(instance) : undefined;
  return setupResult;
}

setupStatefulComponent

  • 这个函数我们分成三个部分进行讲解。
  1. 第一部分的代码主要是对组件的name、directives、components属性进行监测是否合法。例如:export default {name:"slot"},对于组件名称来说为slots或component将是不合法的,同时,声明的components中的所有子组件同样也不能以slots、components命名。如果你还声明了自定义指令,他同样不可以和已有的自定义指令重名。具体检测的代码非常简单,如果你感兴趣可以查看源码,文章不做讲解。
function setupStatefulComponent(instance) {
  var _a;
  const Component = instance.type;
  //检验组件名称是否合法(不能是slot component)
  if (Component.name) {
    validateComponentName(Component.name, instance.appContext.config);
  }
  //检测接受的组件是否合法(对每一个component检测)
  if (Component.components) {
    const names = Object.keys(Component.components);
    for (let i = 0; i < names.length; i++) {
      validateComponentName(names[i], instance.appContext.config);
    }
  }
  //检测自定义指令是否合法不能是
  //bind,cloak,else-if,else,for,html,if,
  //model,on,once,pre,show,slot,text,memo
  if (Component.directives) {
    const names = Object.keys(Component.directives);
    for (let i = 0; i < names.length; i++) {
      validateDirectiveName(names[i]);
    }
  }
  //省略第二部分代码...
}
  1. 这里对一个重要的对象instance.proxy赋值了。他对instance.ctx属性做了一层代理。然后将props代理到了ctx上。这里是可以在模板中直接写props属性的关键。如果用户传递了setup函数,包裹一层错误处理,然后再调用它,显然,setup函数中的this指向undefined。实际就是包裹一层try catct。我们理解成setup(..args)就行了。这里还做了一个小优化,如果你不传递setup(props,ctx){}中的第二个参数ctx,那么就不会创建这个上下文。
  • 为什么调用setup要停止依赖收集? 假设目前处于父组件挂载流程当中并且父组件已经创建了更新副作用,同时子组件的挂载流程也在这个副作用当中,而此时子组件(当前挂载组件)的更新副作用还没有创建,但是setup中用户是可以创建响应式数据并进行访问的,那么子组件就会收集到父组件的更新副作用,所以这里必须停止依赖的收集。
function setupStatefulComponent(instance) {
  //省略第一部分代码...
  //设置accessCache
  instance.accessCache = Object.create(null);
  /*
  代理instance.ctx
  通过拦截publicPropertiesMap上的一些方法
  可以通过key直接访问到data、ctx、props、setupState、
  globalProperties、type.__cssModule等
  中的值并且访问$attrs会收集依赖
  同时在set中限制了publicPropertiesMap中的方法不可更改
  */
  instance.proxy = reactivity.markRaw(
    new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  );
  //将props暴露到ctx上,例如:props中有msg
  //可以通过ctx.msg直接访问到
  exposePropsOnRenderContext(instance);
  //获取setup函数
  const { setup } = Component;
  if (setup) {
    //创建setup函数的上下文
    //setup函数第二个参数才是ctx,如果长度<=1则不需要创建这个
    //上下文
    const setupContext = (instance.setupContext =
    setup.length > 1 ? createSetupContext(instance) : null);
    //调用setup的时候设置组件实例
    //你可以在setup中调用Vue的方法
    //getCurrentInstance访问到当前
    //实例
    setCurrentInstance(instance);
    //停止依赖收集,防止收集到父组件的
    //副作用,setup中可以创建新的响应式
    //对象
    reactivity.pauseTracking();
    //调用setup函数可能会错误,包裹一层错误处理
    //这里获取的setup的返回值
    const setupResult = callWithErrorHandling(
      setup,//需要错误包裹处理的函数
      instance,//报错提示用户是哪个实例错误
      0,//错误代码
      //传递进入setup的参数
      [reactivity.shallowReadonly(instance.props), setupContext]
    );
    //继续开始依赖收集
    reactivity.resetTracking();
    unsetCurrentInstance();
    //省略第三部分代码...
  }
}
  • PublicInstanceProxyHandlers这个代理对象比较复杂,我们在本文下面部分进行单独讲解,先对比较容易解释的函数进行解释。
  • exposePropsOnRenderContext: 用于将instance.props代理到ctx上,这个函数之后,你可以直接通过ctx访问到instance.props上的属性。且这个属性不可通过ctx更改
function exposePropsOnRenderContext(instance) {
  const {
    ctx,
    propsOptions: [propsOptions],
  } = instance;
  if (propsOptions) {
    Object.keys(propsOptions).forEach((key) => {
      Object.defineProperty(ctx, key, {
        enumerable: true,
        configurable: true,
        get: () => instance.props[key],
        //不可更改 这是一个空函数
        set: shared.NOOP,
      });
    });
  }
}
  • createSetupContext: 用于创建setup中的上下文,这个和instance.ctx没有关系,需要区分。 这个方法返回的对象相信大家就很熟悉了吧!就是setup中第二个参数可以访问到的四个方法,官网也详细讲解了这四个方法。
function createSetupContext(instance) {
  const expose = (exposed) => {
    if (instance.exposed) {
      //expose()只能在setup之前被调用一次
      warn(`expose() should be called only once per setup().`);
    }
    //这里给exposed进行了赋值,如果在setup中
    //调用了这个方法,那么父组件将只能访问到
    //传递的exposed中的属性
    instance.exposed = exposed || {};
  };
  let attrs;
  //这就是传递给setup的第二个参数ctx,不可修改
  //并对attrs进行了代理
  return Object.freeze({
    get attrs() {
      //经过代理的attrs同样不可修改,且不可删除
      return attrs || (attrs = createAttrsProxy(instance));
    },
    //slots只读的
    get slots() {
      return reactivity.shallowReadonly(instance.slots);
    },
    //你可以在setup中通过this.emit或ctx.emit
    //发射事件。setup中的this访问到的是instance
    get emit() {
      return (event, ...args) => instance.emit(event, ...args);
    },
    //可以通过ctx.expose()设置想暴露的属性
    expose,
  });
}
  • createAttrsProxy: 设置attrs不可修改、不可删除,同时收集依赖。这里笔者也不是很清楚为什么一定要对attrs收集依赖。如果读者有想法的请在评论区留言。
function createAttrsProxy(instance) {
  //还是ctx.$attrs访问
  //都需要收集依赖
  return new Proxy(instance.attrs, {
    get(target, key) {
      markAttrsAccessed();
      reactivity.track(instance, "get", "$attrs");
      return target[key];
    },
    //不可修改
    set() {
      warn(`setupContext.attrs is readonly.`);
      return false;
    },
    //不可删除
    deleteProperty() {
      warn(`setupContext.attrs is readonly.`);
      return false;
    },
  });
}
  1. 第三部分代码,主要是对调用setup函数的返回值进行处理,
  • 如果是使用的异步setup也就是async setup(){}那么返回值一定是一个promise,将这个结果缓存到instance.async上。
  • 如果你想了解instance上有哪些属性,请阅读Vue3源码分析(4)这里详细讲解了instance的所有属性。
  • 如果没有使用async修饰符,调用handleSetupResult处理返回结果。
  • 如果用户没有传递setup函数,也就不需要调用setup。直接调用finishComponentSetup函数。
function setupStatefulComponent(instance) {
  //省略第一部分代码...
  if (setup) {
    //省略第二部分代码...
    //如果setup使用了async修饰符
    if (shared.isPromise(setupResult)) {
      //无论如何都要设置当前实例为null
      setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
      //赋值;
      instance.asyncDep = setupResult;
      if (!instance.suspense) {
        //警告用户,必须在suspense中使用异步setup
      }
    }
    //如果没有个setup设置async修饰符
    else {
      //设置setup的返回值为setupState
      //调用finishComponentSetup
      handleSetupResult(instance, setupResult);
    }
  }
  else {
    //处理render函数
    finishComponentSetup(instance);
  }
}

handleSetupResult(处理setup函数返回值)

  • handleSetupResult: 处理setup返回的值。
  1. 如果返回的是函数,那么将返回的函数作为渲染函数,并且优先级比template编译的优先级更高,换句话说,如果你同时在.vue文件中写了template并且在setup中返回了函数,那么setup返回的函数返回值将会被渲染到页面上。
  2. 如果返回的是对象,判断一下是否是VNode,例如直接返回createVNode("div"),这是不合法的。同时设置返回值到instance.setupState上。最后调用exposeSetupStateOnRenderContextsetupState代理到instance.ctx上。
  3. 同时setupResult不能为非函数和对象形式之外的形式,比如你返回字符串、数字等都是不合法的,警告用户。
  4. 调用finishComponentSetup做最后的处理。
function handleSetupResult(instance, setupResult, isSSR) {
  //返回值为函数,设置到render上,如果不写template
  //也可以返回render函数进行渲染(自己写渲染函数)
  if (shared.isFunction(setupResult)) {
    //省略了ssr相关代码
    instance.render = setupResult;
  }
  //如果返回的是对象
  else if (shared.isObject(setupResult)) {
    //setup不能直接返回VNode,应该返回渲染函数
    if (isVNode(setupResult)) {
      warn(
        `setup() should not return VNodes directly - ` +
          `return a render function instead.`
      );
    }
    //给开发者工具设置原始的setupState
    instance.devtoolsRawSetupState = setupResult;
    //setupState中可能有ref返回,对于ref,我们想通过键直接
    //访问到值,做一个代理,当访问ref的时候直接访问.value
    instance.setupState = reactivity.proxyRefs(setupResult);
    //将setupState暴露到ctx中(dev only)
    exposeSetupStateOnRenderContext(instance);
  }
  //返回非函数和对象形式的数据是不接受的
  else if (setupResult !== undefined) {
    warn(
      `setup() should return an object. Received: ${
        setupResult === null ? "null" : typeof setupResult
      }`
    );
  }

  finishComponentSetup(instance, isSSR);
}

finishComponentSetup

  • finishComponentSetup: 处理render函数。并且调用applyOptions,这个函数将是组件初始化的最后流程,这是一个相当重要的函数,包括生命周期函数的注册,beforeCreat、create的调用、对其他配置的合并都是在这里进行的处理。(这里的其他配置是指除了props和slots之外的组件定义的属性,例如Vue2中的mounted、beforeUpdate、watch、computed等等,合并是指,你还定义了mixins或extends,那么需要对这些配置全部进行合并)
  1. 如果返回的setupResult是一个函数那么instance.render已经是函数了,这里将会被跳过
  2. 如果你没有写render函数,那么会编译template属性为render函数。
  3. 如果你写了render函数或者你使用了Vue自带的编译系统,那么会自动帮你完成render函数编译并且赋值给Component,这种情况是最常见的,就将这个已经处理好的render函数交给instance.render就可以了。
  4. 调用applyOptions。如果这个时候还没有render函数就需要警告用户了。
  5. 优先级:显然setup返回值为函数优先级是最高的、其次是自己书写了render函数或在.vue文件中写了template会自动帮你编译成render函数的形式、然后是在组件内定义template属性、最后则是在mixins或extends中声明了render函数(这个是在applyOptions中实现的,因为目前还没有进行配置的合并)
function finishComponentSetup(instance) {
  const Component = instance.type;
  //如果不存在渲染函数,去编译template属性的内容
  if (!instance.render) {
    //源码是通过调用registerRuntimeCompiler(如果用户调用了)
    //注册compile函数,然后可以编译instance.template
    //为render函数
    if (compile && !Component.render) {
      //如果写了template属性,那么将会编译
      //template为render赋值给Component
      //Component.render = compile(Component.template)
    }
    instance.render = Component.render || shared.NOOP;
  }

  setCurrentInstance(instance);
  reactivity.pauseTracking();
  //合并组件的所有配置,注册生命周期函数
  //调用beforeCreat、created钩子等
  applyOptions(instance);
  reactivity.resetTracking();
  unsetCurrentInstance();

  //报错提示
  if (!Component.render && instance.render === shared.NOOP) {
    if (!compile && Component.template) {
      //警告用户:写了template但是
      //我没有编译器去处理这个template
    } 
    else {
     //警告用户:没有写template也没有写render函数
      warn(`Component is missing template or render function.`);
    }
  }
}

applyOptions

  • applyOptions: 合并所有的mixinsextends中的配置,调用beforeCreate、create生命周期钩子,注册其他生命周期钩子、将watch、computed、data等属性全部代理到instance.ctx上。
  • 这个方法相当的复杂,我们将会分成9个部分进行讲解,请耐心阅读。

1.合并所有的配置

  • 通过resolveMergeOptions合并配置,并获取合并后的配置。调用beforeCreate生命周期钩子。
function applyOptions(instance) {
  //合并全局的mixins 组件的extends mixins
  const options = resolveMergedOptions(instance);
  //获取代理后的ctx=>proxy
  const publicThis = instance.proxy;
  //获取未代理的ctx
  const ctx = instance.ctx;
  setShouldCacheAccess(false);
  //如果传递了beforeCreate钩子,调用
  if (options.beforeCreate) {
    callHook(options.beforeCreate, instance, "bc");
  }
  const {
    //state
    data: dataOptions,
    computed: computedOptions,
    methods,
    watch: watchOptions,
    provide: provideOptions,
    inject: injectOptions,
    //生命周期部分
    created,
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeDestroy,
    beforeUnmount,
    destroyed,
    unmounted,
    render,
    renderTracked,
    renderTriggered,
    errorCaptured,
    serverPrefetch,
    //公共API
    expose,
    inheritAttrs,
    //静态资源部分
    components,
    directives,
    filters,
  } = options;
  //省略第二部分代码...
}
  • resolveMergedOptions: 用于合并全局和自身的mixins、extends。首先重baseappContext上获取自身和全局的mixinsextends
  • 判断缓存中是否存在已经合并过的配置对象,如果存在则读取缓存就可以了。
  • 用户可能没有传递mixinsextends,这种情况就不需要进行合并,返回本身的配置就可以了。
  • 如果存在mixinsextends且没有缓存,那么就必须要进行合并了。先对全局设置的mixins进行合并,再对组件自身的mixins进行合并。所以组件本身的mixinsextends优先级将会更高。
function resolveMergedOptions(instance) {
  const base = instance.type;
  //获取组件本身的mixins和extends
  const { mixins, extends: extendsOptions } = base;
  //获取全局定义的mixins
  //optionMergeStrategies用于自定义合并逻辑
  const {
    mixins: globalMixins,
    optionsCache: cache,
    config: { optionMergeStrategies },
  } = instance.appContext;
  //获取缓存
  const cached = cache.get(base);
  let resolved;
  //读取缓存防止无用合并
  if (cached) {
    resolved = cached;
  }
  //没有全局的mixins,组件的mixins,组件的extends
  //则不需要合并
  else if (!globalMixins.length && !mixins && !extendsOptions) {
    resolved = base;
  }
  //进行合并
  else {
    resolved = {};
    if (globalMixins.length) {
      //先混合全局的
      globalMixins.forEach((m) =>
        mergeOptions(resolved, m, optionMergeStrategies, true)
      );
    }
    mergeOptions(resolved, base, optionMergeStrategies);
  }
  //设置缓存
  if (shared.isObject(base)) {
    cache.set(base, resolved);
  }
  return resolved;
}
  • mergeOptions: 真正执行合并逻辑的函数。
  • 获取from中的获取需要合并的mixins、entends。首先合并extends,其次合并extends。这表明extends优先级低于mixins
  • 因为可以递归书写mixins所以merge的操作也将会是递归的。存在extendsmixins则递归调用mergeOptions
  • 合并本身。首先寻找合并策略,Vue内部自带了自身属性的合并策略,如果你想合并非data、props、emits、methods、computed、beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、components、directives、watch、provide、inject等属性那么就需要自定义合并策略,下面的注释中有官方给出的示例。当然如果没有寻找到合并策略就会将需要覆盖的值直接赋值而不是合并
/**
 * to 需要被覆盖的
 * from 覆盖的元素
 * strats 自定义组件选项的合并策略的对象。
 */
function mergeOptions(to, from, strats, asMixin = false) {
  const { mixins, extends: extendsOptions } = from;
  //在合并组件的extends(优先级其次)
  if (extendsOptions) {
    mergeOptions(to, extendsOptions, strats, true);
  }
  //最后合并组件自身的mixins(优先级最高)
  if (mixins) {
    mixins.forEach((m) => mergeOptions(to, m, strats, true));
  }
  for (const key in from) {
    //expose不能声明在mixins和extends中只能在组件
    //中声明
    if (asMixin && key === "expose") {
      warn(
        `"expose" option is ignored when declared in mixins or extends. ` +
          `It should only be declared in the base component itself.`
      );
    }
    //strat用于自定义合并策略函数传递父value与子value
    /**
     * 官方给出的示例:
     * const app = createApp({
     *   msg:"Vue",
     *   mixins:[{msg:"Hello"}]
     *   mounted(){
     *     console.log(this.$options.msg)
     *   }
     * })
     * app.config.optionMergeStrategies.msg = (parent, child) => {
     *   return (parent || '') + (child || '')
     * }
     */
    else {
      const strat = internalOptionMergeStrats[key] || (strats && strats[key]);
      //不存在strat和internalOptionMergeStrats则完全覆盖
      to[key] = strat ? strat(to[key], from[key]) : from[key];
    }
  }
  return to;
}
  • internalOptionMergeStrats: Vue内部合并策略。对于生命周期来说,可能同一个钩子含有多个生命周期函数,因为组件本身存在生命周期函数而mixins中也存在的话,他们都会被调用,但是因为先合并extends在合并mixins最后合并自身,如果调用的mergeAsArray进行合并的话,会先调用extends中的在调用mixins中的最后调用自身的。
const internalOptionMergeStrats = {
  data: mergeDataFn,
  //对象类
  props: mergeObjectOptions,
  emits: mergeObjectOptions,
  methods: mergeObjectOptions,
  computed: mergeObjectOptions,
  //生命周期类
  beforeCreate: mergeAsArray,
  created: mergeAsArray,
  //省略其他生命周期钩子...
  components: mergeObjectOptions,
  directives: mergeObjectOptions,
  watch: mergeWatchOptions,
  provide: mergeDataFn,
  inject: mergeInject,
};
  • mergeDataFn: data的合并策略。因为data必须书写函数,数据的合并必要等到调用这个函数之后才能合并。所以这是一个高阶函数,保证在调用函数后才进行合并。
//to:需要被合并的 from:需要合并的
function mergeDataFn(to, from) {
  //没有from不合并
  if (!from) {
    return to;
  }
  //没有to直接返回from
  if (!to) {
    return from;
  }
  return function mergedDataFn() {
    return shared.extend(
      shared.isFunction(to) ? to.call(this, this) : to,
      shared.isFunction(from) ? from.call(this, this) : from
    );
  };
}
  • mergeObjectFn: 合并对象的策略。通过Object.assign进行合并。
function mergeObjectOptions(to, from) {
  //shared.extends === Object.assign
  return to
    ? shared.extend(
       shared.extend(Object.create(null), to), 
       from
      )
    : from;
}
  • mergeAsArray: 合并生命周期的策略。先调用extends中的再调用mixins中的最后调用自身的。特别提示: mergeWatchOptions中也调用了mergeAsArray所以执行顺序和这个一样
/*
  export default {
    beforeCreate(){
      console.log(111)
    },
    mixins:[{
      beforeCreate(){
       console.log(222)
      }
    }],
    extends:{
      beforeCreate(){
        console.log(333)
      }
    }
  }
  //log:333 222 111
*/
function mergeAsArray(to, from) {
  //去重
  return to ? 
    [...new Set([].concat(to, from))]:
    from;
}
  • mergeWatchOptions: 合并watch的策略。watch中的写法有三种形式,将所有的属性当中数组进行合并。例如同时存在对象写法和函数写法的监听将会变成[{},()=>{}],后续调用createWatcher进行创建的时候只需要遍历创建就可以了。
/**
 * watch:{
 *   count(){}//函数形式
 *   count:[()=>{},()=>{}]//数组形式
 *   //对象形式
 *   count:{
 *     handler(){},
 *     immediate:true,
 *     deep:true,
 *     flush:'post'
 *   }
 * }
 */
function mergeWatchOptions(to, from) {
  if (!to) return from;
  if (!from) return to;
  const merged = shared.extend(Object.create(null), to);
  //合并所有的监听函数
  for (const key in from) {
    merged[key] = mergeAsArray(to[key], from[key]);
  }
  return merged;
}
  • mergeInject: 合并inject的策略。由于inject可能写的是数组,那么需要对其进行标准化,将其变成对象。然后再当作对象合并就行了。
function mergeInject(to, from) {
  //首先标准化to、from
  //主要讲数组形式的inject转化为对象形式
  return mergeObjectOptions(normalizeInject(to), normalizeInject(from));
}
//inject可能是数组也可能是对象
//这里将数组转化为对象
function normalizeInject(raw) {
  //如果当前是数组
  //["foo"]=>{foo:"foo"}
  if (shared.isArray(raw)) {
    const res = {};
    for (let i = 0; i < raw.length; i++) {
      res[raw[i]] = raw[i];
    }
    return res;
  }
  return raw;
}

2.代理inject属性到ctx上

  • 因为props在创建组件实例的时候已经处理过了,所以这里已经不需要再处理了我们仅需要判断属性是否重名即可。
  • resolveInjections: 处理inject
//省略第一部分代码...
const checkDuplicateProperties = createDuplicateChecker();
  const [propsOptions] = instance.propsOptions;
  if (propsOptions) {
    for (const key in propsOptions) {
      //不能有重复的key
      checkDuplicateProperties("Props", key);
    }
  }
  if (injectOptions) {
    resolveInjections(
     injectOptions,
     ctx,
     checkDuplicateProperties,
     //provides注入响应式值的时候需要设置
     //provide() {
     //  return {
     //  显式提供一个计算属性
     //  message: computed(() => this.message)
     //  }
     //}
     instance.appContext.config.unwrapInjectedRef
    );
  }
//省略第三部分代码...
  • createDuplicateChecker:用于检测属性是否重名。因为后续我们需要将props、computed、data等数据的访问都代理到instnace.ctx上,如果出现重名的话,将会无法访问,届时我们需要警告用户。
function createDuplicateChecker() {
  //创建一个缓存,已经存在的属性
  //放入这个缓存当中
  const cache = Object.create(null);
  return (type, key) => {
    //再次出现这个属性则警告用户
    if (cache[key]) {
      console.warn(
        `${type} property "${key}" is already defined in ${cache[key]}.`
      );
    } else {
      cache[key] = type;
    }
  };
}
  • resolveInjections: 首先标准化inject,转化数组形式为对象形式。inject的对象形式含有多种写法这里需要详细了解才能知道源码在做什么处理:点我了解inject
  • 转化后injectOptions可能存在的形式:inject:{foo:"foo",fun:{...属性}},所以转化后opt有两种形式。一种是字符串形式一种是对象形式。最终调用injectparent.provides中寻找这个值。
  • 选项式api-provide中,你可以提供响应式的值例如:provide:{msg:ref("msg")},但是我们在子代组件进行访问的时候你可能需要通过.value来进行访问,你可以通过设置app.config.unwrapInjectedReftrue以此直接通过msg进行访问而不是msg.value。其中unwrapRef传递的就是这个值,如果你设置了这个值,会通过Object.definePropertymsg代理到ctx上,并且返回的值是msg.value,这样就不需要用户自己去通过.value访问了。如果没有设置则不会做这一层代理。
  • 最后调用checkDuplicateProperties判断是否重名。
function resolveInjections(
  injectOptions, //inject选项
  ctx, //instance.ctx
  checkDuplicateProperties = shared.NOOP, //监测是否重名的函数
  unwrapRef = false
) {
  //将数组写法的inject转化为对象
  if (shared.isArray(injectOptions)) {
    injectOptions = normalizeInject(injectOptions);
  }
  for (const key in injectOptions) {
    /**
     * inject:{customMsg:{from:"msg",default:"msg"}}
     * 转化前:inject:['foo']
     * 转化后:inject:{foo:"foo"}
     * 所以opt可能是字符串也可能是对象
     */
    const opt = injectOptions[key];
    let injected;
    if (shared.isObject(opt)) {
      //如果有默认值
      if ("default" in opt) {
        //如果有from key作为别名
        injected = inject(opt.from || key, opt.default, true);
      } else {
        injected = inject(opt.from || key);
      }
    } else {
      //{foo:"foo"}opt就是key
      injected = inject(opt);
    }
    if (reactivity.isRef(injected)) {
      if (unwrapRef) {
        //将inject暴露到ctx中并不需要.value
        Object.defineProperty(ctx, key, {
          enumerable: true,
          configurable: true,
          get: () => injected.value,
          set: (v) => (injected.value = v),
        });
      } else {
        {
         //警告用户:provide中提供了响应式的值,
         //但是没有设置app.config.unwrapInjectedRef
         //所以你需要.value访问
        }
        //需要使用.value
        ctx[key] = injected;
      }
    }
    //不是ref正常赋值
    else {
      ctx[key] = injected;
    }
    //监测是否重名
    checkDuplicateProperties("Inject", key);
  }
}
  • inject: 从当前实例的父实例的provides属性上找到注入的值并返回。
  • 如果组件没有父组件表示现在是顶层组件,访问提供的全局provides,否则访问当前组件的父组件的provides。为什么要访问父组件的provides呢?我们设想一下,我在父组件中使用了provide,那么这个provide将会和父组件之前的probvides进行合并,所以对于子代组件来说并不是访问自身的provides属性而是访问父亲的provides属性才能找到对应的值。
  • 如果想要默认值为非原始数据类型需要使用工厂函数就像这样inject:{msg:{default:()=>[]}。将这个函数的this指向instance.proxy,这个实例属性是ctx的代理属性我们将会在文章末尾详细讲解。
function inject(key, defaultValue, treatDefaultAsFactory = false) {
  //获取当前实例
  const instance = getCurrentInstance() || getCurrentRenderingInstance();
  if (instance) {
    //当前如果是根组件(没有父组件)获取appContext中的provides
    //否则获取父实例中的provides
    const provides =
      instance.parent == null
        ? instance.vnode.appContext && 
          instance.vnode.appContext.provides
        : instance.parent.provides;
    //如果当前的provides中有需要的值返回
    if (provides && key in provides) {
      return provides[key];
    }
    /*
    如果想要默认值为非原始数据类型需要使用
    工厂函数
    const Child = { 
      inject: { 
        foo: { 
          from: 'bar', 
          default: () => [1, 2, 3]}}}
    */
    else if (arguments.length > 1) {
      return treatDefaultAsFactory && 
        shared.isFunction(defaultValue)?
        defaultValue.call(instance.proxy):
        defaultValue;
    } else {
      warn(`injection "${String(key)}" not found.`);
    }
  } else {
    warn(`inject() can only be used inside setup() or functional components.`);
  }
}

3.代理methods到ctx上

  • methods中的方法this指向是instance.proxy
  • methods中所有属性代理到ctx上。判断获取的方法是否是一个函数,如果不是函数需要警告用户。
//省略第二部分代码...
if (methods) {
  for (const key in methods) {
    //获取methods中的方法
    const methodHandler = methods[key];
    //将method方法代理到ctx上并且绑定this为instance.proxy
    if (shared.isFunction(methodHandler)) {
      Object.defineProperty(ctx, key, {
        value: methodHandler.bind(publicThis),
        configurable: true,
        enumerable: true,
        writable: true,
      });
      //检查属性的方法名中的方法名是否重名
      checkDuplicateProperties("Methods", key);
    } else {
      //警告用户:mthods中的属性必须是函数
      }
    }
  }
//省略第四部分代码...

4.代理data到ctx上

  • data中绑定的this同样是this.proxy
  • 判断当前的data是否是一个函数,如果不是则警告用户。调用这个函数,判断返回值是否是对象,如果不是同样警告用户,然后将使用响应式api代理这个对象。最后把data返回的所有属性全部代理到ctx上。目前你可以直接通过ctx访问到data、methods、inject、props、setupResult属性。当然不能重名,否则会发生覆盖。
//省略第三部分代码...
if (dataOptions) {
    //data必须是函数
    //data(){return {}}
    if (!shared.isFunction(dataOptions)) {
      //警告用户:data必须是一个函数,对象写法
      //已经不在支持
    }
    //获取data返回的数据,绑定this为instance.proxy
    const data = dataOptions.call(publicThis, publicThis);
    //如果async data(){}警告
    if (shared.isPromise(data)) {
      //警告用户:data的返回值不能够是一个promise
    }
    //返回的data必须是对象
    if (!shared.isObject(data)) {
      warn(`data() should return an object.`);
    }
    //代理这个data放到instance上
    else {
      //所有的data保证是响应式的
      instance.data = reactivity.reactive(data);
      //遍历data
      for (const key in data) {
        //检查data中的key是否重名
        checkDuplicateProperties("Data", key);
        //不是_或$开头代理到ctx上
        if (!isReservedPrefix(key[0])) {
          Object.defineProperty(ctx, key, {
            configurable: true,
            enumerable: true,
            get: () => data[key],
            set: shared.NOOP,
          });
        }
       }
   }
 }
//省略第五部分代码...

5.代理computed到ctx上

  • computed有两种写法:函数写法、对象写法
computed:{
  count() {
    return a + b;
  },
  count: {
    get() {},
    set() {},
  },
};
  • 因为计算属性的写法有两种,所以首先需要判断是对象写法还是函数写法,如果是对象获取get属性作为计算函数,如果是函数写法则这个函数就作为计算函数
  • 获取设置函数set,如果是函数写法那么计算属性将会是不可更改的。强制修改将会触发警告。
  • 调用reactivity库自带的api完成计算属性的代理。如果你想要了解computed的实现你可以阅读Vue3源码分析(2)
  • 将计算属性代理到ctx上。并检测是否重名。
//省略第四部分代码...
if (computedOptions) {
  for (const key in computedOptions) {
    const opt = computedOptions[key];
    //如果opt直接就是函数则作为get,否则
    //获取opt的get属性作为get
    const get = shared.isFunction(opt)
      ? opt.bind(publicThis, publicThis)
      : shared.isFunction(opt.get)
      ? opt.get.bind(publicThis, publicThis)
      : shared.NOOP;
    if (get === shared.NOOP) {
      warn(`Computed property "${key}" has no getter.`);
    }
    //如果不给set,则不能为计算属性设置值
    const set =
      !shared.isFunction(opt) && shared.isFunction(opt.set)
        ? opt.set.bind(publicThis)
        : () => {
            //警告用户不能写入,计算属性是只读的
          };
    const c = reactivity.computed({
      get, //获取值的时候进行计算
      set, //设置值的时候拦截做一些操作
    });
    //代理到ctx上
    Object.defineProperty(ctx, key, {
      enumerable: true,
      configurable: true,
      get: () => c.value,
      set: (v) => (c.value = v),
    });
    checkDuplicateProperties("Computed", key);
  }
}
//省略第六部分代码...

6.处理watch

  • 这里的处理比较复杂,watch的实现就在createWatcher当中,还有Vue的调度器,这个的内容我们将会在下一小节中进行讲解。这里就先跳过了。简单理解为创建了watch
//省略第五部分代码...
if (watchOptions) {
  for (const key in watchOptions) {
    createWatcher(watchOptions[key], ctx, publicThis, key);
  }
}
//省略第七部分代码...

7.处理provide

  • 由于provide可以写函数形式,这样的形式可以让你获取this上的属性,这里的this指向的是instance.proxy。最终调用provide将需要合并的值传递到instance.provides上。同时这个provide也是组合式api中调用的provide
//省略第六部分代码...
if (provideOptions) {
  /**
   * provide:{
   *   "msg":"foo"
   *   访问this写法
   *   hello(){
   *     return this.hello
   *   }
   * }
   */
  const provides = shared.isFunction(provideOptions)
    ? provideOptions.call(publicThis)
    : provideOptions;
  Reflect.ownKeys(provides).forEach((key) => {
    provide(key, provides[key]);
  });
}
//省略第八部分代码...
  • provide: 合并提供的key、value到当前实例的provides属性上。子组件如果使用了inject,那么会访问父组件的instance.provides寻找这个值,并代理到ctx(上面inject中已经讲述过了),这样就能够访问到上层组件的属性了。
  • 你可能不了解为什么parentProvides === provides,在createComponentInstnace的时候赋值就是赋值的parent.provides,所以他们是相等的。
  • 为什么要赋值为parent.provides: 这是为了继承上层组件注入的属性,想要让子代组件通过访问父组件实例的provides属性访问到爷组件调用provide注入的值,就需要每一层都继承父组件的provides在合并自身调用的provide。假设我们定义自身的provides为空,那么继承就失效了,就只能访问到父代组件注入的属性了。
function provide(key, value) {
  const currentInstance = getCurrentInstance();
  if (!currentInstance) {
    {
      warn(`provide() can only be used inside setup().`);
    }
  } else {
    //获取组件实例的provide
    let provides = currentInstance.provides;
    const parentProvides =
      currentInstance.parent && currentInstance.parent.provides;
    //父provide与子provide一样则将父provide放到原型链中
    //在给子provide赋值
    if (parentProvides === provides) {
      provides = currentInstance.provides = Object.create(parentProvides);
    }
    provides[key] = value;
  }
}

8.注册生命周期函数

  • 到目前为止,已经完成了创建组件实例、初始化props、slots以及选项式api的合并。
  • 调用create生命周期钩子,标志着组件实例创建的完成。
  • 注册其他生命周期钩子。
//省略第七部分代码...
//调用created生命钩子
if (created) {
  callHook(created, instance, "c");
}
function registerLifecycleHook(register, hook) {
  //省略实现...
}
//将生命周期钩子挂载到实例对应的属性上
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);
//省略第九部分代码...
  • registerLifecycleHook: 注册生命周期钩子,将合并后的生命周期函数放到实例上。
  • 由于合并后的生命周期钩子可能是一个数组,所以对于数组来说需要分别注册。同时生命周期函数的this都指向instance.proxy
  • 这里采用这种方式进行注册是为了与组合式api兼容。你可以在setup中调用onBeforeMount进行生命周期函数的注册,他同样会合并到实例上。
function registerLifecycleHook(register, hook) {
  //合并后的生命周期钩子可能是数组
  if (shared.isArray(hook)) {
    //每一个hook都需要被注册this指向instance.proxy
    hook.forEach((_hook) => register(_hook.bind(publicThis)));
  }
  //注册一个
  else if (hook) {
    register(hook.bind(publicThis));
  }
}
  • 注册函数是作为参数传递进来的,我们以onBeforeMount为例讲解。其他生命周期注册函数的做法完全一样。
  • createHook: 根据不同的生命周期类型创建不同的生命周期注册函数。
const createHook =
  (lifecycle) =>
  (hook, target = getCurrentInstance()) =>
    injectHook(lifecycle, hook, target);
const onBeforeMount = createHook(LifecycleHooks.BEFORE_MOUNT);
  • injectHook: 将需要注册的生命周期函数放到实例上。
  • 初始化实例上对应钩子的属性值为数组,将需要调用的钩子包裹一层函数,放到实例对应的生命周期属性上。包裹的函数主要判断当前组件是否已经卸载,如果已经卸载则不需要在执行在进行注册。停止收集依赖,依赖的收集只需要在调用render函数的时候完成即可对于模板没有用到的变量不需要进行DOM更新。所以在生命周期钩子内部收集依赖是没有必要的。
function injectHook(
  type, //注入的那种钩子onBeforeMount
  hook, //要注册的函数
  target = getCurrentInstance(),
) {
  if (target) {
    //获取实例上当前类型的所有钩子
    const hooks = target[type] || (target[type] = []);
    //往实例身上挂载wrap过的生命周期钩子
    //多了一层错误收集
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args) => {
        //如果已经卸载了则不在执行生命周期函数
        if (target.isUnmounted) {
          return;
        }
        //停止收集依赖
        reactivity.pauseTracking();
        //设置当前实例
        setCurrentInstance(target);
        //执行本次hook(收集可能的错误)
        const res = callWithAsyncErrorHandling(hook, target, type, args);
        unsetCurrentInstance();
        reactivity.resetTracking();
        return res;
      });
    hooks.push(wrappedHook);
    return wrappedHook;
  } else {
    //警告用户
  }
}

9.处理expose

  • expose: 如果组件使用了ref,那么你可以访问到子组件的属性,但是如果想让父组件对子组件的属性访问做出限制就需要让子组件定义expose或者在setup中调用ctx.expose,让父组件只能访问到指定的属性。
  • 这里的处理相当于遍历expose中的key,然后将instance.proxy中的值返回,那么通过instance.exposed进行访问,就只能访问到进行限制的值了。
//省略第八部分代码...
//expose用于声明当组件实例被父组件通过
//模板引用访问时暴露的公共属性。
if (shared.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 = {};
  }
}
//此时如果还没有render,尝试使用mixins中书写的render函数
if (render && instance.render === shared.NOOP) {
  instance.render = render;
}
if (inheritAttrs != null) {
  instance.inheritAttrs = inheritAttrs;
}
//资源选项
//可用的组件和自定义指令
if (components) instance.components = components;
if (directives) instance.directives = directives;
  • 如果你写了以下代码,那么需要使用混合后的render函数作为渲染模板。所以此时需要给instance.render赋值
<script>
import { h } from 'vue'
export default{
  extends:{
    render:()=>h('div',null,123)
  }
}
</script>
  • 最后将注册的子组件和自定义指令放到实例相应的属性上。
  • 好啦!我们讲解完了整个初始化组件的流程,我们可以发现所有组件配置中的方法都会让this指向instance.proxy,而这个属性是对ctx的代理,我们最后还需要讲解一下这个instance.proxy到底是如何生成的。

instance.proxy到底是什么?

  • 我们先来看看instance.proxy是在哪里产生的吧!
//setupStatefulComponent
//上面进行组件名称和自定义指令是否合法进行
//了检测
instance.proxy = reactivity.markRaw(
  new Proxy(instance.ctx, PublicInstanceProxyHandlers)
);
//下面的代码是调用setup函数并处理结果
  • 这里的代码我们已经在上面讲解过了,但是对于PublicInstanceProxyHandlers,上文中我们并没有展开解答,所以我们就来阅读一下它是如何对ctx进行代理的吧!或者说这个代理又新添加了什么可以访问的属性呢?

PublicInstanceProxyHandlers

这个对象一共包含四个拦截方法get、set、has、deleteProperty。我们主要讲解get、set

1.get

  • 第一个参数是ctx,所以如果想要获取当前组件的实例应当访问_属性。
  1. 内部可以通过访问instance.proxy.__isVue判断是否是组建实例。并且优先返回setupState中的属性。特别提示:setupState是指setup调用的返回值
get({ _: instance }, key) {
  const { ctx, setupState, data, props, accessCache, type, appContext } =
      instance;
    //访问ctx.__isVue返回true
    if (key === "__isVue") {
      return true;
    }
    //setupState有值返回setupState中的值
    if (
      setupState !== shared.EMPTY_OBJ &&
      setupState.__isScriptSetup &&
      shared.hasOwn(setupState, key)
    ) {
      return setupState[key];
    }
    //省略第二部分代码...
}
  1. 创建缓存,如果你已经访问过这个值了,那么可以直接重缓存中读取。因为props、data、setupState中的属性是会被经常访问的,如果每次访问都去调用props.hasOwnProperty()会造成性能浪费,并且直接通过访问cache的方式将会更快。
  • 如果是第一次访问,那么访问的顺序是setupState=>data=>props=>ctx
  • 我们可以发现他的缓存只是缓存了属性key的是来自于哪个对象。例如你访问的keysetupState中被找到那么cache将会被设置为cache:{key:AccessTypes.SETUP}当下次在访问这个key的时候就可以从cache中获取到AccessTypes.SETUP,那么我们就知道它是通过setupState获取到的,就可以靶向访问了。
//省略第一部分代码
let normalizedProps;
//访问的不是任何一个public方法
if (key[0] !== "$") {
  const n = accessCache[key];
  //能通过缓存读取到则读取缓存
  //accessCache:{num:2}
  //return data[key]
  if (n !== undefined) {
    switch (n) {
      case AccessTypes.SETUP:
        return setupState[key];
      case AccessTypes.DATA:
        return data[key];
      case AccessTypes.CONTEXT:
        return ctx[key];
      case AccessTypes.PROPS:
        return props[key];
    }
  }
  //如果setup不是空对象且有key这个属性设置缓存
  //且返回这个值
  else if (setupState !== shared.EMPTY_OBJ && shared.hasOwn(setupState, key)) {
    accessCache[key] = AccessTypes.SETUP;
    return setupState[key];
  }
  //如果data中有这个值,设置缓存并返回这个值
  else if (data !== shared.EMPTY_OBJ && shared.hasOwn(data, key)) {
    accessCache[key] = AccessTypes.DATA;
    return data[key];
  }
  //options中有这个属性设置缓存并返回这个属性
  else if (
    (normalizedProps = instance.propsOptions[0]) &&
    shared.hasOwn(normalizedProps, key)
  ) {
    accessCache[key] = AccessTypes.PROPS;
    return props[key];
  }
  //同上
  else if (ctx !== shared.EMPTY_OBJ && shared.hasOwn(ctx, key)) {
    accessCache[key] = AccessTypes.CONTEXT;
    return ctx[key];
  }
}
//省略第三部分代码...
  1. 如果你不是访问的data、props、setupState、ctx上的属性,那么还有可能会访问publicPropertiesMap上的方法,他们都是$开头的一些方法,你可以通过这些方法访问到当前组件的实例、强制组件更新等,想要了解更多你可以访问这里
  • 还记得注入的全局属性吗?你可以通过app.config.globalProperties.msg = 'xxx'注入全局属性,然后可以通过在被绑定了this指向为instance.proxy的函数中访问到设置的全局属性。而这里就是对这个效果的实现,当然如果全局注入的属性和组件定义的数据键发生了重名,肯定会优先返回组件定义的属性值
const publicGetter = publicPropertiesMap[key];
let globalProperties;
// public $xxx properties
if (publicGetter) {
  return publicGetter(instance);
}
//如果globalProperties中存在则返回
else if (
  ((globalProperties = appContext.config.globalProperties),
  shared.hasOwn(globalProperties, key))
) {
    return globalProperties[key];
}

2.set

  • 通过这个函数我们可以发现,你可以重新设置setupState、data、globalProperties、ctx当中的值;但是对于props是无法设置的,因为这是父组件传递的参数。
set({ _: instance }, key, value) {
  const { data, setupState, ctx } = instance;
  //给setupState修改值
  if (setupState !== shared.EMPTY_OBJ && shared.hasOwn(setupState, key)) {
    setupState[key] = value;
    return true;
  }
  //给data修改值
  else if (data !== shared.EMPTY_OBJ && shared.hasOwn(data, key)) {
    data[key] = value;
    return true;
  }
  //无法修改
  else if (shared.hasOwn(instance.props, key)) {
    //警告用户props是只读的
    return false;
  }
  //不能设置
  if (key[0] === "$" && key.slice(1) in instance) {
    //警告用户不能设置$开头的方法
    return false;
  } else {
    //不可修改globalProperties
    //将要修改的key代理到ctx上
    if (key in instance.appContext.config.globalProperties) {
      Object.defineProperty(ctx, key, {
        enumerable: true,
        configurable: true,
        value,
      });
    } 
    else {
      ctx[key] = value;
    }
  }
  return true;
}

总结

  • 本文我们具体探讨了如何对创建好的组件实例进行初始化,并且兼容了选项式api
  • 首先调用了setup函数,获取setup的返回值,并将其返回值全部代理到了ctx上,为了兼容Vue2的选项式写法,调用了applyOptions方法,将包括data、methods、computed、inject、props、setupState属性全部都代理到了ctx上,为了保证访问是正确的,他们中的属性不能够重名,否则会报警告。这也解释了为什么你可以在模板中直接写入返回的数据,例如:<template>{{msg}}</template>,编译后msg=>ctx.msg,这个ctx会在调用render函数的时候传递。而render函数中的ctx代表的就是instance.proxy
  • 我们又详细解释了instance.proxy到底是什么?这是绑定方法的一个指向,你可以在methodHandler、dataOptions、computedOptions、provideOptions、lifeCycle中通过this访问到这个对象。这样说可能有点抽象。我们来看一下代码。
//这里的六种方式都可以让你访问到instance.proxy
export default {
 render(publicThis){
  //这里的参数publicThis和this指向
  //都是指的instance.proxy
 },
 data(){
   console.log(this)
 },
 methods:{
   handler(){console.log(this)}  
 },
 computed:{
  count(){console.log(this)}
 },
 provide(){return {msg:this.msg}}
 beforeCreat(){console.log(this)}
}
  • instance.proxy就是对ctx进行的代理,你可以通过访问instance.proxy访问到全局注入的属性globalProperties。但是在ctx中是访问不到的。同时这个代理还实现了缓存机制。让你更快的访问到属性。
  • 下文中我们将会讲解watch的实现,以及Vue的调度器。