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

目录
  • 前情提要
  • mountComponent
  • 创建组件实例
  • 总结

前情提要

上文我们讲解了执行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.component = 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.component,app.mixin调用后缓存的componentmixin都放在这里面,如果没有parent表示当前是根组件,那么就是这个appContext,如果有parent则继承parent的appContext。我们可以通过这里发现所有的组件的appContext都会是唯一的一个context。因此所有组件实例都将能访问到全局注册的component、mixin、directive等。我们来回顾一下它的结构(省略了部分属性)
{
  app: null,
  config: {
    globalProperties: {},
    optionMergeStrategies: {},
    compilerOptions: {},
  },
  mixins: [],
  components: {},
  directives: {},
};
  • next:组件更新有两种形式,一种是组件内部的状态发生了改变,引起了组件自身更新;另外一种是父组件传递了propsprops改变,父组件更新、导致子组件引起了更新。第一种nextnull。如果是第二种,那么需要给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,组件本身的mixinsextends。我们可以发现全局的最先处理,所以全局注册的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属性,在propsOptionsprops:{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 :合并了mixinsextends后的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 组件挂载创建实例的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue3 computed初始化获取设置值实现示例

    目录 computed 用法 computed 实现 computed 初始化 computed 获取值的实现 值的展示 缓存功能 computed 设置值实现 computed 用法 本文给大家带来的是vue3 中 computed API的实现. 大家看过vue3的官网,应该都知道,在vue3 的组合式API中,computed这个功能与以往的有所不同了. 以往vue2 的 computed 用法: export default { components: { }, computed: {

  • Vue3源码分析组件挂载初始化props与slots

    目录 前情提要 初始化组件 (1).setupComponent (2).initProps (3).initSlots 额外内容 总结 前情提要 上文我们分析了挂载组件主要调用了三个函数: createComponentInstance(创建组件实例).setupComponent(初始化组件).setupRenderEffect(更新副作用).并且上一节中我们已经详细讲解了组件实例上的所有属性,还包括emit.provide等的实现.本文我们将继续介绍组件挂载流程中的初始化组件. 本文主要内

  • Vue实例初始化为渲染函数设置检查源码剖析

    目录 引言 _renderProxy是干什么的 initProxy方法 总结 引言 之前的文章提到,Vue实例化时_init方法做了很多处理,其中就有这么一段: if (__DEV__) { initProxy(vm) } else { vm._renderProxy = vm } 在生产模式下,_renderProxy直接指向了Vue实例本身,而在开发环境下调用了initProxy方法,那么它究竟是做什么的呢? _renderProxy是干什么的 通过对_renderProxy进行全局搜索,我

  • Vue子组件props从父组件接收数据并存入data

    目录 1.不允许直接修改 2.存在异步的情况 解决思路 经过测试父组件中传递过来的数据有以下特点: 1.不允许直接修改 如果直接使用 this.xxx = action 操作的话 控制台会报下面这个错误 大概的意思是 你小子不要随便劈我瓜,我父组件的瓜岂是你能变的,父组件重新渲染时,这个值会被覆盖,你小子自个儿用计算属性或者data存一下吧 2.存在异步的情况 假如父组件该数据是后台接口获取的数据,那么会产生这种情况.子组件的生命周期都已经走完了,父组件的数据还没传过来.因为V8引擎的运行速度是

  • Vue源码学习之数据初始化

    目录 初始化数据 创建Vue实例 构造函数扩展方法 初始化状态 调用initData方法对数据进行代理 初始化数据 环境搭建:菜鸟学Vue源码第一步之rollup环境搭建步 响应式数据的核心就是,数据变化了可以监听到数据变化了,数据的取值和更改值可以监测到,首先第一步需要创建一个Vue实例 创建Vue实例 //dist/index.html //用Vue创造一个实例 const vm = new Vue({ data(){ return { name:'i东东', age:18 } } }) 创

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

    目录 前情提要 mountComponent 创建组件实例 总结 前情提要 上文我们讲解了执行createApp(App).mount('#root')中的mount函数,我们分析了创建虚拟节点的几个方法,以及setRef的执行机制.本文我们继续讲解mountComponent,挂载组件的流程. 本文主要内容 createComponentInstance发生了什么? 如何标准化组件定义的props.emits? 为什么provide提供的值子组件都能访问到? 组件的v-model实现原理.组件

  • Vuejs第十一篇组件之slot内容分发实例详解

    什么是组件? 组件(Component)是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能.在有些情况下,组件也可以是原生 HTML 元素的形式,以 is 特性扩展. Slot分发内容 ①概述: 简单来说,假如父组件需要在子组件内放一些DOM,那么这些DOM是显示.不显示.在哪个地方显示.如何显示,就是slot分发负责的活. ②默认情况下 父组件在子组件内套的内容,是不显示的. 例如代码: <

  • 微信小程序中button组件的边框设置的实例详解

    微信小程序中button组件的边框设置的实例详解 button的边框是用:after方式实现的,用户如果在button上定义边框会出现两条线,需用:after的方式去覆盖默认值. 如果设置了Button的背景色,没有用:after设置边框的颜色,则button的四个角会出现模糊的尖角.如下图所示: 如上图四个角会模糊..wxss代码如下: .clickEncryptBtn{ width:130px; border-radius: 3px; margin:20px auto; padding-to

  • vue父组件触发事件改变子组件的值的方法实例详解

    父组件向子组件通信 业务场景:现在我要在父组件点击一个回退按钮,这个回退会传进子组件中(子组件中有两步进程),子组件来处理. 解决方案 起初我是父组件通过props传值,但是发现只有组件第一次加载时才能传值,通过事件改变的父组件值并不会再通过过props传递,也就是说props只有加载组件时才会工作,并不会根据值改变动态操作 后面,我是通过操作dom的方法,this.$refs.children这样直接操作子组件 <ProgressTwo ref="progressTwo" v-

  • C/C++ 动态数组的创建的实例详解

    C/C++ 动态数组的创建的实例详解 在C++语言中,二维动态数组主要使用指针的方法建立,以建立一个整数二维数组为例: #include<iostream> #include<string> #include<malloc.h> using namespace std; int main(int argc,char **argv) { ///*int a[2][3]={{1,2,3},{4,5,6}}; //cout<<sizeof(a+1)<<

  • Symfony2创建页面实例详解

    本文实例讲述了Symfony2创建页面的方法.分享给大家供大家参考,具体如下: 在Symfony2中创建页面只需要两步: 1.创建路由:路由定义你页面的URI(如/about)并指定要执行的控制器(PHP函数).当传入的请求URL匹配该路由时,Symfony2将执行指定的控制器: 2.创建控制器:控制器是一个PHP函数,它接受传入的请求并将其转换成Symfony2的Response对象. 我们喜欢这样简单的实现,因为它符合Web的工作方式.每一个Web交互都是由HTTP请求开始,应用程序的任务就

  • Vue3中v-if和v-for优先级实例详解

    目录 在vue2中应尽量避免二者同时使用 vue3中的改变 结论 补充:注意事项 总结 在vue2中应尽量避免二者同时使用 vue 2.x官方链接 当 v-if 与 v-for 一起使用时,v-for 具有比 v-if 更高的优先级. 那么,我们举个例子说明为啥不推荐 <template> <div class="hello"> <div v-for="(item,index) in list" v-if="index ===

  • vue3更新的setup语法糖实例详解

    目录 前言 语法糖用法: 语法糖带来的体验 一.组件自动注册 二.属性及方法无需return 三.自动将文件名定义为组件的name属性 1.defineProps 2.defineEmits 3.defineExpose 总结 前言 vue3最近更新了一个setup语法糖,这两天才看到,使用起来雀食很甜,特发个帖子记录下 语法糖用法: // 将 `setup` attribute 添加到 `<script>` 代码块上 // 里面的代码会被编译成组件 `setup()` 函数的内容 // 就是

  • vue3中的watch和watchEffect实例详解

    目录 首先总结一下两者的区别: 下面是根据上面的第三点做的一些小实验: 总结 闲来无事,比较了一下 vue3 中的 watch 和 watchEffect,总觉得官方文档没咋说清楚,今天就小小实践了一下. 首先总结一下两者的区别: 1.watch 是惰性执行,而 watchEffect 不是,不考虑 watch 的第三个参数配置的情况,watch 在组件第一次执行的时候是不会执行的,只有在之后依赖项变化的时候再执行,而 watchEffect 是在程序执行到此处的时候就立即执行,而后再响应其依赖

  • Java创建表格实例详解 原创

    表格是最常用的数据统计形式之一,在 swing 中 由 JTable 类实现表格.接下来,我们看看怎么利用 JTable 创建表格. 在 JTable 类中除了默认的构造方法外,还提供了利用指定表格列名数组和表格数据数组创建表格的构造方法,代码如下: JTable(Object[][] rowDate,Object[] columnNames) 参数说明:         rowDate:封装表格数据的数组.         columnNames:封装表格列名的数组. 在使用表格时,通常将其添

随机推荐