好得很程序员自学网

<tfoot draggable='sEl'></tfoot>

Vue3组件挂载之创建组件实例详解

前情提要

上文我们讲解了执行 createApp(App).mount('#root') 中的 mount函数 ,我们分析了 创建虚拟节点 的几个方法,以及 setRef的执行机制 、本文我们继续讲解 mountComponent ,挂载组件的流程。

本文主要内容

createComponentInstance 发生了什么? 如何标准化组件定义的 props、emits ? 为什么 provide 提供的值子组件都能访问到? 组件 的 v-model 实现原理、组件 v-model修饰符 实现原理。 组件 emit 实现原理。 once修饰符 实现原理。 几乎 所有组件实例属性 的详细讲解。

mountComponent

这个方法主要用于 挂载组件 ,根据传递的虚拟节点创建组件实例,调用 setupComponent 函数进行初始化组件实例的 插槽 和 props ,如果是有状态组件还需要处理 setup的返回值 。最后调用 setupRenderEffect 绑定 副作用更新函数 。这个函数比较简单,我们将重心放到 createComponentInstance、setupComponent、setupRenderEffect 当中、后文也是围绕这三个方法进行依次讲解。

const mountComponent = (
    initialVNode,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    isSVG,
    optimized
  ) => {
    //创建组件实例
    const instance = (initialVNode测试数据ponent = createComponentInstance(
      initialVNode,
      parentComponent,
      parentSuspense
    ));
    setupComponent(instance);
    if (instance.asyncDep) {
      //处理异步逻辑,suspense的时候在进行讲解
    }
    setupRenderEffect(
      instance,
      initialVNode,
      container,
      anchor,
      parentSuspense,
      isSVG,
      optimized
    );
  };

创建组件实例

createComponentInstance : 根据虚拟节点创建组件实例的方法,简单的说就是创建一个 instance 对象,这个上面有许多的 属性 用来 描述当前这个组件 以及 存储需要后续使用到的属性 ,所以我们 主要讲解各个属性的作用 ,我们先来看看这个函数的源码。 uid : 标识当前实例的 id ,这是一个 从0开始的递增值 ,同时他也是 Vue调度器 的 优先级值 , 越小的值优先级越高 ,试想一下,父组件先挂载,然后才会挂载子节点,子节点如果包含组件,那么 uid 的值将会比 父组件 的 uid 值大,当父组件挂载完毕,父组件的兄弟组件开始挂载,那么 uid 将会 更大 ,这样也符合流程更新逻辑,关于调度器我们之后单独开一章节来进行讲解。 vnode : 实例的 虚拟节点 。 parent : 当前组件实例的父组件实例。 appContext : createApp的时候注入的一个上下文 ,我们通过 app测试数据ponent,app.mixin 调用后缓存的 component 和 mixin 都放在这里面,如果没有 parent 表示当前是 根组件 ,那么就是这个 appContext ,如果有 parent 则继承 parent的appContext 。我们可以通过这里发现所有的组件的 appContext 都会是唯一的一个 context 。因此所有组件实例都将能访问到全局注册的 component、mixin、directive 等。我们来回顾一下它的结构 (省略了部分属性) 。

{
  app: null,
  config: {
    globalProperties: {},
    optionMergeStrategies: {},
    compilerOptions: {},
  },
  mixins: [],
  components: {}, 
  directives: {}, 
};

next :组件更新有两种形式, 一种 是组件内部的状态发生了改变,引起了组件自身更新; 另外一种 是父组件传递了 props , props改变 ,父组件更新、导致子组件引起了更新。 第一种next 是 null 。如果是 第二种 ,那么需要给 next 赋值为当前子组件最新的 VNode(虚拟节点) 。而 next 将会用于更新组件的 props 以及 slots 等属性。组件自身状态改变引起的更新是不需要更新 props 的。 subTree : 调用 render 函数之后返回的组件要渲染的虚拟根节点,可以通过这个属性读取到组件需要渲染的所有虚拟节点。 effect : 在 setupRenderEffect 中创建的 reactiveEffect ,我们在 Vue3源码分析(2) 中讲解了 reactiveEffect ,不了解的可以在阅读一下,目前简单理解为在调用 render函数的时候收集依赖 ,如果 响应式的值 发生了 改变 会重新调用 update函数 执行 更新流程 。我们将会在讲解 setupRenderEffect 中详细讲解这一部分。 update : 组件更新函数 ,只要调用了这个函数,组件就会强制更新。同时响应式发生改变调用的也是这个函数。 render : 渲染函数 。可以通过编译 <template></template> 得到,也可以通过写 template属性 获得,也可以通过自己写 render 函数。 exposed : 组件指定的暴露到外部的属性。 provides : 用户在组件中设置了 provide ,子代组件可以通过 inject 收到传递的值, provides:parent ? parent.provides : Object.create(appContext.provides) 通过这段代码分析我们可以知道, provides 中至少含有 全局提供的provides ,如果当前组件提供了 provide ,后面会将其混合,并且 继承 自 父组件 的 provides ,这也就解释了为什么 provides可以向下传递 ,因为每一层都可以收到 本组件的provides 和 父组件的provides 并进行 合并 。 components : 如果你想在 template 中使用组件,需要在这里 注册 ,对于使用了 <script setup> 语法糖的会将其 编译 到 components属性 当中,当然这个属性就是 用来存放这些注册的组件的 。 directives : 存放自定义指令。 propsOptions : 合并了mixins和extends后的props ,这里的属性为什么是 propsOptions 而不是 props 呢?这个 VNode 中的 props 又有什么关系呢?实际上 propsOptions 代表的是用户在组件内定义的 props ,而 VNode 中的 props 是 外部传递给组件的props 。这一点要加以区分。同时这里调用了 normalizePropsOptions 来对 propsOptions 进行标准化。接下来我们分析一下 normalizePropsOptions 函数。这个函数比较长我们分成三个部分来分析。 首先从 appContext 中获取 props 缓存,避免处理过了组件 props重复处理 。如果 comp 不是一个函数,这个判断是因为, Vue3 中允许 函数组件 的存在,但是函数组件是 无状态组件 也没有props,所以不做处理。那么剩下的都是有状态组件了,这里会处理全局注册的 mixins ,组件本身的 mixins 和 extends 。我们可以发现全局的最先处理,所以全局注册的 mixins 的 优先级 将会是 最低 的,其次是 extends ,显然优先级最高的则是 组件自身的mixins ,因为他最后执行,那么 shared.extend 将会最后覆盖之前的 props 。我们还可以发现 extends 属性和 mixins 属性在实现上没有任何区别,只是 mixins 可以是数组,而 extends 不能是数组。最后说一下 asMixin ,我们知道 全局的mixins 只需要合并 一次 ,但是 normalizePropsOptions 会调用 多次 ,为了避免 全局属性混合 的多次执行,设置了 asMixin 这个参数。当 asMixin为true 的时候表示 不需要 在合并全局的 mixins 了。 特别提示:shared.extend就是Object.assign 。

function normalizePropsOptions(comp, appContext, asMixin = false) {
  //获取props的缓存
  const cache = appContext.propsCache;
  const cached = cache.get(comp);
  //这个缓存是一个type对应一个[normalized, needCastKeys]
  //normalized表示合并了mixins和extends后的props
  if (cached) {
    return cached;
  }
  const raw = comp.props;
  const normalized = {};
  const needCastKeys = [];
  let hasExtends = false;
  if (!shared.isFunction(comp)) {
    //用于合并props的函数,因为extends和mixins
    //中还可以写mixins和extends所以需要递归合并
    /**
     * 例如const mixins = [{
     *    extends:{},
     *    mixins:[{props}],
     *    props
     * }]
     */
    const extendProps = (raw) => {
      hasExtends = true;
      const [props, keys] = normalizePropsOptions(raw, appContext, true);
      //normalized为合并后的props
      shared.extend(normalized, props);
      if (keys) needCastKeys.push(...keys);
    };
    //首先合并全局注册的mixins中的props属性(最先合并的优先级最低)
    if (!asMixin && appContext.mixins.length) {
      appContext.mixins.forEach(extendProps);
    }
    //然后合并extends属性(中间合并的优先级居中)
    //(extends功能与mixins几乎一样)但是更注重于继承
    //并且extends不能是数组
    if (comp.extends) {
      extendProps(comp.extends);
    }
    //最后合并组件自身的mixins(最后合并的优先级最高)
    if (comp.mixins) {
      comp.mixins.forEach(extendProps);
    }
  }
  //省略第二部分的代码...
}

我们知道这个函数主要是对 propsOptions 进行标准化,简单的说就是将各式各样的 propsOptions 统一成 唯一标准 。当传递的 props:['msg'] 数组形式的时候,我们需要将其转化为 props:{msg:{}} 。 validatePropName 用于检测 key 是否合法, Vue 中,组件传递参数不能以 $ 开头定义数据。例如 $msg 就是 不合法 的。

function normalizePropsOptions(comp, appContext, asMixin = false) {
  //省略第一部分的代码...
  //如果没有props且没有全局的mixins
  //组件本身的mixins、extends则设置
  //当前实例的props缓存为空
  if (!raw && !hasExtends) {
    if (shared.isObject(comp)) {
      cache.set(comp, shared.EMPTY_ARR);
    }
    return shared.EMPTY_ARR;
  }
  //处理这种类型props:['msg','hello']
  if (shared.isArray(raw)) {
    for (let i = 0; i < raw.length; i++) {
      if (!shared.isString(raw[i])) {
        console.warn(
          `props must be strings when using array syntax. ${raw[i]}`
        );
      }
      //将v-data-xxx转化为驼峰式vDataXxx
      //但是不能以$开头
      const normalizedKey = shared.camelize(raw[i]);
      if (validatePropName(normalizedKey)) {
        //将其变为props:{"msg":{}}
        normalized[normalizedKey] = shared.EMPTY_OBJ;
      }
    }
  }
  //省略第三部分的代码...
 }

在上面的代码中频繁出现 needCastKeys ,这个代表的是需要特殊处理的key,例如: props:{msg:{default:"msg"}} 含有 default ,那么理论上我们应当判断传递的属性值是否存在,然后在决定是否使用 default 的值,但是这里我们仅进行标准化,所以对于含有 default 属性的我们需要单独放入 needCastKeys 中,便于后面对 props 中的处理。再比如说 <Comp yes></Comp> 传递了yes属性,在 propsOptions 中 props:{yes:{type:Boolean}} 这样的 key=>"yes" 也是需要处理的, yes 的值应该为 true ,所以对于 type 中含有 Boolean 的也需要放入 needCastKeys 中。

export function normalizePropsOptions(comp, appContext, asMixin = false) {
  //省略第二部分代码...
  if (shared.isArray(raw)) {
    //省略...
  }
  //处理props:{msg:String}
  else if (raw) {
    if (!shared.isObject(raw)) {
      warn(`invalid props options`, raw);
    }
    //循环遍历所有的key
    for (const key in raw) {
      //"v-data-xxx"=>"vDataXxx"变为小驼峰式
      const normalizedKey = shared.camelize(key);
      //检验key是否合法
      if (validatePropName(normalizedKey)) {
        const opt = raw[key]; //获取value
        //如果获取的value是数组或函数转化则为{type:opt}
        //props:{"msg":[]||function(){}}=>
        //props:{"msg":{type:msg的值}}
        const prop = (normalized[normalizedKey] =
          shared.isArray(opt) || shared.isFunction(opt) ? { type: opt } : opt);
        if (prop) {
          //找到Boolean在prop.type中的位置 Boolean,["Boolean"]
          const booleanIndex = getTypeIndex(Boolean, prop.type);
          //找到String在prop.type中的位置
          const stringIndex = getTypeIndex(String, prop.type);
          prop[0] = booleanIndex > -1; //type中是否包含Boolean
          //type中不包含String或者Boolean的位置在String前面
          //例如:"msg":{type:["Boolean","String"]}
          prop[1] = stringIndex < 0 || booleanIndex < stringIndex;
          //如果有default属性,或者type中包含Boolean放入needCastKeys中
          if (booleanIndex > -1 || shared.hasOwn(prop, "default")) {
            needCastKeys.push(normalizedKey);
          }
        }
      }
    }
  }
  const res = [normalized, needCastKeys];
  //设置缓存
  if (shared.isObject(comp)) {
    cache.set(comp, res);
  }
  return res; //返回合并后的normalized
}

emitsOptions :合并了 mixins 和 extends 后的 emits 属性。并且对 emits 进行了标准化。主要是调用了 normalizeEmitsOptions 进行处理,这个函数的逻辑和 normlizePropsOptions 非常 相似 ,如果你看懂了上述的解释,这个函数相信你很容易就能看懂,这里就不在做多余的解释了。

export function normalizeEmitsOptions(comp, appContext, asMixin = false) {
  //获取appContext中的缓存
  const cache = appContext.emitsCache;
  const cached = cache.get(comp);
  //如果已经读取过返回缓存
  if (cached !== undefined) {
    return cached;
  }
  //获取组件的emits属性{emits:['']}
  //还可以对象写法{emits:{'':null||function(){}}}
  const raw = comp.emits;
  //最终的合并对象
  let normalized = {};
  let hasExtends = false;
  if (!shared.isFunction(comp)) {
    //合并emits的方法
    const extendEmits = (raw) => {
      const normalizedFromExtend = normalizeEmitsOptions(raw, appContext, true);
      if (normalizedFromExtend) {
        hasExtends = true;
        shared.extend(normalized, normalizedFromExtend);
      }
    };
    //最先合并appContext中的(优先级最低)
    if (!asMixin && appContext.mixins.length) {
      appContext.mixins.forEach(extendEmits);
    }
    //然后合并实例的extends(优先级居中)
    if (comp.extends) {
      extendEmits(comp.extends);
    }
    //最后合并组件自身的(优先级最高)
    if (comp.mixins) {
      comp.mixins.forEach(extendEmits);
    }
  }
  if (!raw && !hasExtends) {
    //设置缓存
    if (shared.isObject(comp)) {
      cache.set(comp, null);
    }
    return null;
  }
  //即使emits:[]是数组最终也会被转化为对象
  //emits:['m']=>emits:{'m':null}
  if (shared.isArray(raw)) {
    raw.forEach((key) => (normalized[key] = null));
  } else {
    //合并
    shared.extend(normalized, raw);
  }
  if (shared.isObject(comp)) {
    cache.set(comp, normalized);
  }
  return normalized;
}

attrs : 如果你给组件传递了没有在组件 内部声明的属性 ,那么将会放入 attrs 中。例如:

<Comp id="a"></Comp>
//Comp组件 没有对id的声明,那么会放入attrs中
export default {
  props:{}
}

setupState : setup 的返回值。 ctx : 当前组件的上下文 。 通过 createDevRenderContext 函数创建。在这个函数当中,对 ctx 对象进行了 代理 ,可以通过 ctx._ 访问到组件的实例,同时将 publicPropertiesMap 上所有的属性代理到了 ctx 上,简单的说就是之前需要通过 publicPropertiesMap 访问属性,现在在 ctx 上同样能访问到。这样的代理方式在 Vue 中被大量采用, setupState methods data computed watch props 当中所有的属性都将会采用这种方式被代理到 ctx 上,当然会出现 重名问题 ,所以保证上述的这些属性中,应当 避免重名 ,否则会有 警告 提示。当然这些属性是是在哪里被代理到 ctx 上的,我们后面都会讲到。

function createDevRenderContext(instance) {
  const target = {};
  //可通过_访问实例对象
  Object.defineProperty(target, `_`, {
    configurable: true,
    enumerable: false,
    get: () => instance,
  });
  Object.keys(publicPropertiesMap).forEach((key) => {
    Object.defineProperty(target, key, {
      configurable: true,
      enumerable: false,
      get: () => publicPropertiesMap[key](instance),
      set: shared.NOOP,
    });
  });
  return target;
}

不知道这个这个对象上的方法你是否很熟悉呢?没错,这就是 Vue 官方展示的 组合式Api ,你可以在 setup 中访问 this 调用到这些方法,不妨大胆猜测一下,为什么在 setup 中访问 this 能读取的这些方法?其实很简单。 setup.call(instance.ctx,其他参数) ,实际上只要能访问到 this 的地方,基本上都是访问的 instance.ctx 、当然有些地方的 this 绑定的是 instance.proxy 。这两者的区别我们在后面在进行讲解。其中的 i 代表的是 组件实例 。

const publicPropertiesMap = shared.extend(Object.create(null), {
  $: (i) => i, //获取当前实例
  $el: (i) => i.vnode.el,
  $data: (i) => i.data, //获取实例的data
  $props: (i) => reactivity.shallowReadonly(i.props), //获取props
  $attrs: (i) => reactivity.shallowReadonly(i.attrs),
  $slots: (i) => reactivity.shallowReadonly(i.slots),
  $refs: (i) => reactivity.shallowReadonly(i.refs),
  $emit: (i) => i.emit,
  //获取options
  $options: (i) => resolveMergedOptions(i),
  //强制更新
  $forceUpdate: (i) => i.f || (i.f = () => queueJob(i.update)),
  // $nextTick: (i) => i.n || (i.n = nextTick.bind(i.proxy)),
  $watch: (i) => instanceWatch.bind(i),
  $parent: (i) => getPublicInstance(i.parent),
  $root: (i) => getPublicInstance(i.root),
});

emit : 触发传递给组件的函数。通过 this.$emit 调用的就是这个函数。对于 emit 这个方法我们同样分成 三个部分 进行讲解。 之前我们详细讲解了 emitsOptions ,他最终会被标准化。这也是标准化的作用,后续的处理都会变得很简单,而不需要兼容多种写法。如果调用了 emit 方法,但是在 emitsOptions 中没有这个属性,表示并没有注册,需要 警告用户 ,同时如果你传递的 emits 是一个 函数 ,那么他就是一个 检验函数(如果不理解请查看Vue官网对于emits的解释) ,传递的参数为调用 emits 传递的 剩余参数 。

function emit(instance, event, ...rawArgs) {
  //已经卸载无须在执行
  if (instance.isUnmounted) return;
  //获取props
  const props = instance.vnode.props || shared.EMPTY_OBJ;
  //获取经过标准化的emits和props
  //emits:{方法名:null||function(){}}
  //如果为function代表的是验证函数
  const {
    emitsOptions,
    propsOptions: [propsOptions],
  } = instance;
  if (emitsOptions) {
    //警告用户:调用了emit 但是没有在emitOptions中找到,代表没有声明
    if (!(event in emitsOptions)) {
      if (!propsOptions || !(shared.toHandlerKey(event) in propsOptions)) {
        warn(
          `Component emitted event "${event}" but it is neither declared in ` +
            `the emits option nor as an "${shared.toHandlerKey(event)}" prop.`
        );
      }
    }
    //获取验证函数
    else {
      const validator = emitsOptions[event];
      if (shared.isFunction(validator)) {
        //调用验证函数,返回false则警告
        const isValid = validator(...rawArgs);
        if (!isValid) {
          warn(
            `Invalid event arguments: event validation failed for event "${event}".`
          );
        }
      }
    }
  }
  //省略第二部分代码...
 }

这里的处理可能会让你很懵逼,这个 isModelListener 是个啥? modifiersKey 又是个啥?在讲解这个之前你需要了解 v-model 在组件中的用法 (你可以在Vue3官网查询) ,以及他们的 编译结果 。通过编译结果我们可以发现,新增了属性 modelModifiers ,这就是咱们下面访问的 modifiersKey 了,同时 v-model 指令本质上会被编译为 onUpdate:modelValue ,所以我们可以通过判断 prop 的开头是否是 onUpdate 来判断这是不是一个 v-model 指令。有了这两点我们就能理解第二部分的代码在做什么了。首先 判断 当前发射的事件是否是 v-model 事件,如果是,获取 修饰符 对传递的 参数 进行 转换 ,然后再 传递给发射的事件函数 。

<template>
  <Comp v-model.trim.number = "a" />
</template>
//编译后
function render(_ctx, _cache) {
  const _component_Comp = _resolveComponent("Comp", true)
  return (_openBlock(), _createBlock(_component_Comp, {
    modelValue: _ctx.a,
    "onUpdate:modelValue": $event => ((_ctx.a) = $event),
    modelModifiers: { trim: true,number:true }
  }, null, 8, ["modelValue"]))
}

function emit(instance, event, ...rawArgs) {
  //省略第一部分代码...
  let args = rawArgs;
  //判断是否是v-model事件 => update:modelValue
  const isModelListener = event.startsWith("update:");
  const modelArg = isModelListener && event.slice(7); //modelValue
  if (modelArg && modelArg in props) {
    //获取modifiersKey=>modelModifiers
    const modifiersKey = `${
      modelArg === "modelValue" ? "model" : modelArg
    }Modifiers`;
    //当给组件传递v-model的时候
    //<Button v-model.trim="a"></Button>
    //当这样传递的时候会收到modelModifiers={trim:true}
    const { number, trim } = props[modifiersKey] || shared.EMPTY_OBJ;
    //对emit('',...args)传递给父组件的参数执行trim()
    if (trim) {
      args = rawArgs.map((a) => a.trim());
    }
    if (number) {
      args = rawArgs.map(shared.toNumber);
    }
  }
  //省略第三部分代码...
}

首先通过event在props中找到需要发射的事件,如果存在执行即可。如果添加了 once 修饰符,例如 <C @close-Model.once="handler"></C> 他实际接收到的属性是 onCloseModelOnce ,而如果不写 once 修饰符就收到的就是 onCloseModel ,所以添加了 once 修饰符的执行 一次 后会被放入 emitted 当中,以后在触发就 不会在执行 了。

function emit(instance, event, ...rawArgs) {
  //省略第二部分的代码...
  //这里是简单写法,源码实际上对
  //event这个字符串做了改造。
  let handler = props[event]
  //如果存在则执行
  if (handler) {
    handler.apply(instance,args)
  }
  //如果有once事件,存入emitted,并执行,以后不再执行
  const onceHandler = props[handlerName + `Once`];
  if (onceHandler) {
    if (!instance.emitted) {
      instance.emitted = {};
    } else if (instance.emitted[handlerName]) {
      return;
    }
    instance.emitted[handlerName] = true;
    handler.apply(instance,args);
  }
}

总结一下 emit 函数的作用: 如果声明的 emits 中含有函数,作为 检验函数 ,检验不通过警告用户。然后对组件使用 v-model 指令做了处理,主要是对 修饰符 的实现。当然在 HTML标签 中使用修饰符并不是在这里实现的,这里仅实现了组件的 v-model修饰符 。最后调用传递父组件传递给子组件的函数,并且把子组件要传递的参数给了父组件。如果含有 once 修饰符,放入 emitted缓存 中,只执行一次。 inheritAttrs :如果给组件传递了某些 props 属性,但是没有给组件声明 props、emits、或者事件监听器 、那么 id 属性将会透传到 subTree 节点上,例如: <Comp id= "com"></Comp> 如果 subTree 是一个 div 节点,那么 id 将会赋值到这个 div 上。当然你可以设置 inheritAttrs为false 禁止透传。 存放生命周期的属性 : 我们可以看到下面有一些简写的属性例如: bc、c、bm 等,这些都是 生命周期钩子 ,在这里需要注意的是,这些 属性的值 应该是一个 数组 ,因为如果用户使用了 mixins 或者 extends 这些属性,那么同一个生命周期函数可能会包含 多个 ,而这些 生命周期函数 都应该被调用,所以他们的值应当是一个 数组 例如:

export default {
  mixins:[{beforeCreate(){}}]
  beforeCreate(){}
}
//那么bc:[createBefore1,createBefore2]

还有一些 其他的属性 ,我都放在了 注释 当中,因为看单词意思就能理解,就不额外讲解了,当然后续这些属性都将会用到,也许现在看这些属性还是比较片面的。但是通过后面的讲解,你将会逐渐了解这些属性的作用。

function createComponentInstance(vnode, parent, suspense) {
  const type = vnode.type;
  const appContext = (parent ? parent.appContext : vnode.appContext) || {};
  const instance = {
    uid: uid++, //当前实例的id
    vnode, //当前实例对应的vnode
    type, //当前实例对应的编译后的.vue生成的对象
    parent, //当前实例的父实例
    appContext, //app的上下文包含全局注入的插件,自定义指令等
    root: null, //当前组件实例的根实例
    //响应式触发的更新next为null,
    //在更新的过程中父组件调用了子组件的
    //instance.update会赋值next为最新组件vnode
    next: null,
    subTree: null, //调用render函数后的Vnode(处理了透传)
    effect: null, //实例的ReactiveEffect
    update: null, //副作用的scheduler
    scope: new EffectScope(true),
    //template编译结果或setup返回值为函数
    //或.vue文件写的template编译为的render函数
    render: null, //渲染函数
    proxy: null, //代理后的ctx
    exposed: null, //调用了ctx.expose()方法(限制暴露的数据)
    exposeProxy: null, //调用了getExposeProxy方法后的expose
    withProxy: null,
    //当前组件的provides,父实例有则读取父实例的否则读取app上的
    //父组件的provides后续会挂载到prototype上,重新赋值当前真实
    //的provide上,这样可以通过原型链访问到所有上代组件中的provide
    provides: parent ? parent.provides : Object.create(appContext.provides),
    accessCache: null,
    renderCache: [],
    components: null, //当前组件的可用组件
    directives: null, //当前组件的自定义指令
    //合并mixins和extends中的props属性
    propsOptions: normalizePropsOptions(type, appContext),
    //合并mixins和extends中的emits属性
    emitsOptions: normalizeEmitsOptions(type, appContext),
    emit: null, //当前实例调用emit的函数
    emitted: null, //含有once修饰符的,执行一次后放入这里不再执行
    propsDefaults: {}, //默认props
    //是否透传attrs
    inheritAttrs: type.inheritAttrs,
    // state
    ctx: {}, //当前实例的上下文也就是this
    data: {}, //data函数返回的值,被代理后才放入
    props: {}, //接受到的组件属性
    attrs: {}, //接受到的标签属性
    slots: {}, //组件传递的插槽内容
    refs: {}, //存入的refs
    setupState: {}, //setup的返回值
    //expose attrs slots emit
    setupContext: null, //传递给setup的ctx(只有四个属性)
    suspense,
    suspenseId: suspense ? suspense.pendingId : 0,
    asyncDep: null, //setup使用了async修饰符 返回的promise保存在这里
    asyncResolved: false,
    isMounted: false, //是否挂载
    isUnmounted: false, //是否卸载
    isDeactivated: false,
    bc: null, //beforeCreate
    c: null, //create
    bm: null, //beforeMount
    m: null, //mount
    bu: null, //beforeUpdate
    u: null, //update
    um: null, //unmount
    bum: null, //beforeUnmount
    //若组件实例是 <KeepAlive> 缓存树的一部分,
    //当组件从 DOM 中被移除时调用。deactivated
    da: null,
    //若组件实例是 <KeepAlive> 缓存树的一部分,
    //当组件被插入到 DOM 中时调用。activated
    a: null,
    //在一个响应式依赖被组件触发了重新渲染之后调用。
    //renderTriggered
    rtg: null,
    //在一个响应式依赖被组件的渲染作用追踪后调用。
    //renderTracked
    rtc: null,
    /**
     * 错误捕获钩子
     * 组件渲染
     * 事件处理器
     * 生命周期钩子
     * setup() 函数
     * 侦听器
     * 自定义指令钩子
     * 过渡钩子
     * 错误捕获钩子
     */
    ec: null, //errorHandler
    sp: null, //serverPrefetch
  };
  //创建实例的上下文
  instance.ctx = createDevRenderContext(instance);
  //当前实例的根实例
  instance.root = parent ? parent.root : instance;
  instance.emit = emit.bind(null, instance);
  if (vnode.ce) {
    vnode.ce(instance);
  }
  return instance;
}

总结

挂载组件一共执行了三个主要的函数: createInstanceComponent(创建组件实例)、setupComponent(初始化组件)、setupRenderEffect(设置更新副作用) 。 本文我们重点讲解了创建组件实例,以及对几乎所有的属性进行了详细的讲解,当然 suspense相关属性 我们会在讲解 suspense 的时候详细为大家剖析。我们还重点讲解了 ctx对象 ,其实观察编译结果,所有我们在 template 中访问的 xxx变量 都会变成 ctx.xxx ,本质上就是将所有的可能用到的变量都代理到了 ctx 上,代理的方式就是 Object.defineProperty 。当然还有如何 标准化propsOptions、emitsOptions、 同时还详细讲解了组件的 emit如何实现 。以及在组件中使用 v-model 和它的修饰符 trim、number 的实现过程, once 修饰符的实现原理。 下一小节我们将继续剖析组件挂载的 第二个 流程- 初始化组件 。

以上就是Vue3组件挂载之创建组件实例详解的详细内容,更多关于Vue3 组件挂载创建实例的资料请关注其它相关文章!

查看更多关于Vue3组件挂载之创建组件实例详解的详细内容...

  阅读:52次