首页IT科技vue挂载过程(Vue3源码分析组件挂载初始化props与slots)

vue挂载过程(Vue3源码分析组件挂载初始化props与slots)

时间2025-08-01 23:39:18分类IT科技浏览8857
导读:前情提要 上文我们分析了挂载组件主要调用了三个函数: createComponentInstance(创建组件实例 、setupComponent(初始化组件 、setupRenderEffect(更新副作用 。并且上一节中我们已经详细讲解了组件实例上的所有属性,还包括emit、provide等的实现。本文我们...

前情提要

上文我们分析了挂载组件主要调用了三个函数: createComponentInstance(创建组件实例)                、setupComponent(初始化组件)                        、setupRenderEffect(更新副作用)                。并且上一节中我们已经详细讲解了组件实例上的所有属性                ,还包括emit        、provide等的实现                        。本文我们将继续介绍组件挂载流程中的初始化组件        。

本文主要内容

初始化props和slots的主要流程            。 如何将传递给组件的属性分发给props和attrs(需要被透传的属性)                        。 用户自己实现了render函数                        ,如何对其进行标准化            。 标准的插槽需要满足哪些条件        。

初始化组件

(1).setupComponent

setupComponent: 这个函数主要用于初始化组件                        。内部主要调用了initProps            、initSlot                        、对于有状态组件还需要调用setupStatefulComponent                。
function setupComponent(instance) { //获取vnode的props(真正传递的props) const { props, children } = instance.vnode; //判断当前是否是有状态组件组件 const isStateful = isStatefulComponent(instance); //通过传递的真实props和声明的props 分离组件参数 //组件参数放入props中 其余放入instance.attrs //处理了props的default情况等 initProps(instance, props, isStateful); //初始化插槽 initSlots(instance, children); //验证名称是否合法,components中的组件名称是否 //合法,代理instance.ctx,创建setup函数的ctx,调用setup函数 //处理得到的结果 const setupResult = isStateful ? setupStatefulComponent(instance) : undefined; return setupResult; }
isStatefulComponent: 这个主要用于判断是否是有状态组件            、还记得Vue3源码分析(4)中提到的ShapeFlag吗?我们在createVNode中会判断type的类型        、然后设置shapeFlag来标识当前创建的虚拟节点类型    。因此我们只需要获取组件的vNode                        、而vNode中有shapeFlag然后判断他的值        ,就知道他是不是有状态组件了                        。
function isStatefulComponent(instance) { return instance.vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT; }

(2).initProps

initProps: 在创建组件实例中            ,我们只对propsOptions做了处理                、但是props和attrs目前都还是null    、所以我们需要区分出来那些是props那些是attrs                        ,同时有些propsOptions中设置了default属性            ,那么我们还需要判断是否传递了这个属性        ,如果没有传递那么应该用default属性中的值                        、又比如传递了 <Comp yes></Comp>并且声明了props:{yes:Boolean}                        ,那么应该将yes的值变为true                    。而这些就是在初始化props的时候完成的。
function initProps(instance, rawProps, isStateful) { //定义需要放入的 const props = {}; const attrs = {}; //attrs.__vInternal = 1 shared.def(attrs, InternalObjectKey, 1); //创建propsDefaults instance.propsDefaults = Object.create(null); //将真实传递的props分配给instance的props和attrs setFullProps(instance, rawProps, props, attrs); //遍历normalized(合并和的props) for (const key in instance.propsOptions[0]) { if (!(key in props)) { props[key] = undefined; } } //最后将分配好的props和attrs赋值到instance if (isStateful) { instance.props = reactivity.shallowReactive(props); } else { //不存在type.props则让props为attrs if (!instance.type.props) { instance.props = attrs; } else { instance.props = props; } } instance.attrs = attrs; }
setFullProps: 在Vue3源码分析(5)中我们详细讲解了propsOptions                ,如果读到这里还是不理解的小伙伴可以跳到上一章再去看看                    。首先重propsOptions中解构到options和needCastKeys(需要特殊处理的key)                        。options就是进行标准化后的组件定义的props    。 遍历真正传递给组件的props    ,拿到key去options中寻找                        ,如果找到了                    ,表示这个属性是组件需要接受的props,进一步判断是否是需要特殊处理的key如果不是就可以放入props中                。 如果是需要特殊处理的key                    ,获取他的值放入rawCastValues当中                        。如果在options中没有找到                        ,就判断一下emitsOptions中是否有    ,如果这里面也没有那就可以放入attrs中                ,attrs就是需要透传到subTree上的属性        。 最后遍历需要特殊处理的key调用resolvePropValue对props进行最后的处理            。
function setFullProps(instance, rawProps, props, attrs) { //获取通过mixins和extends合并的props const [options, needCastKeys] = instance.propsOptions; let hasAttrsChanged = false; //attrs是否发生改变 let rawCastValues; if (rawProps) { for (let key in rawProps) { //如果key是"ref" "key" "ref_for" "ref_key" //"onVnodeBeforeMount" "onVnodeMounted" //"onVnodeBeforeUpdate "onVnodeUpdated" //"onVnodeBeforeUnmount" "onVnodeUnmounted" //那么就跳过 if (shared.isReservedProp(key)) { continue; } //获取rawProps:{a:1}=>value=1 const value = rawProps[key]; let camelKey; //小驼峰式的key if ( options && shared.hasOwn(options, (camelKey = shared.camelize(key))) ) { //这个key不是含有default属性的 if (!needCastKeys || !needCastKeys.includes(camelKey)) { props[camelKey] = value; } //props:{"msg":{default:"a"}} //含有default属性的放入rawCastValues中 else { (rawCastValues || (rawCastValues = {}))[camelKey] = value; } } //判断当前的key是否是用于emits的 else if (!isEmitListener(instance.emitsOptions, key)) { //不是emit自定义事件的key也不是组件参数那么就是attrs if (!(key in attrs) || value !== attrs[key]) { attrs[key] = value; hasAttrsChanged = true; } } } } /** * * 这里涉及到四个属性instance, rawProps, props, attrs * instance:是当前组件的实例 * rawProps:真正传递的props可能含有组件参数props, * 标签属性attrs,自定义emit事件 * props:代表声明并且接受到的props * attrs:代表没有声明props也不属于emits属性的属性 * needCastKeys:代表需要特殊处理的属性 * 例如props:{msg:{default:"a"}}那么msg会被放入 * needCastKeys中 * */ if (needCastKeys) { //获取非响应式的props const rawCurrentProps = reactivity.toRaw(props); const castValues = rawCastValues || {}; for (let i = 0; i < needCastKeys.length; i++) { const key = needCastKeys[i]; //msg //对于有default的属性进行重设 //props:{msg:{default:"a"}} props[key] = resolvePropValue( options, //合并mixins和extends后的props(定义方) rawCurrentProps, //非响应式的props(接受方) key, //(含有default)的key "msg" //例如传递了"msg":1 定义了:props:{msg:{default:"a"}} //castValues[key]=1 castValues[key], instance, //实例 !shared.hasOwn(castValues, key) ); } } return hasAttrsChanged; }
resolvePropValue: 对特殊的key进行处理                        。 首先从opt中判断是否有default属性                        ,如果有default属性而且传递的value是undefined的话表示需要使用默认值        ,还需要进一步判断            ,如果传递的不是函数但是声明的是函数                        ,需要将value设置为这个函数的返回值            。例如:props:{yes:Number,default:(props)=>{}}并且没有向组件传递yes这个参数            ,那么yes的值将会是default函数的返回值        。 对于propsOptions中定义的接受值类型是Boolean的        ,但是又没有传递且没有默认值则设置这个值为false                        。 当然还有<Comp yes></Comp>并且声明了是Boolean                        ,则会设置为true                。
function resolvePropValue(options, props, key, value, instance, isAbsent) { //获取{msg:{default:"a"}}中的{default:"a"} const opt = options[key]; if (opt != null) { //判断是否有default属性 const hasDefault = shared.hasOwn(opt, "default"); //如果定义了default但是没有接受到value值 if (hasDefault && value === undefined) { const defaultValue = opt.default; //如果需要接受的类型不是函数,但是接受到了函数 //看看实例的propsDefaults是否有当前key的值 //还是没有则调用这个defaultValue函数取得值 if (opt.type !== Function && shared.isFunction(defaultValue)) { const { propsDefaults } = instance; if (key in propsDefaults) { value = propsDefaults[key]; } else { //包裹是为了在调用这个函数的时候 //获取当前实例不会出错 setCurrentInstance(instance); value = propsDefaults[key] = defaultValue.call(null, props); unsetCurrentInstance(); } } //设置为默认值 else { value = defaultValue; } } //需要接受的类型是Boolean if (opt[0]) { //没有设置默认值,也没有传递这个值则为false if (isAbsent && !hasDefault) { value = false; } //<Comp yes></Comp>并且声明了yes则设置为true else if (opt[1] && value === "") { value = true; } } } return value; }

(3).initSlots

initSlots:还记得在Vue3源码分析(4)中我们详细讲解了normalizeChildren                ,他主要用于标准化插槽    ,给vNode的shapeFlag加上ARRAY_CHILDREN或TEXT_CHILDREN或SLOTS_CHILDREN的标识                        ,但是并没有添加到实例的slots属性上    。因为那个时候还没有创建实例                    ,所以我们只能在那时候打上标记,在创建实例之后                    ,也就是现在,在去初始化slots                        。对于SLOTS_CHILDREN                    、TEXT_CHILDREN、ARRAY_CHILDREN分别是在那种情况下添加到shapeFlag上的                        ,如果你不了解可能会影响这一段代码的阅读    ,建议在看看第四小节                    。因为间隔较远                ,所以理解起来很困难                        ,这部分的文章主要是阐述整个Vue3的运行机制。我们后面的章节还会单独讲解slots的实现                    。 SLOTS_CHILDREN: 首先判断children._是否存在        ,如果是通过Vue的编译器得到的那么一定会有这个标识            ,当然                        ,用户自己书写render函数也可以自己传递这个标识符                        。但是大部分用户是不会传递的            ,所以else分支中就是为了处理这种情况        ,而对于children._存在的                        ,可以直接把children当做实例的slots属性    。_标识有三个值STABLE                    、DYNAMIC                        、FORWORD这个在第四小节也已经讲过了                ,就不在重复了                。 TEXT_CHILDREN    、ARRAY_CHILDREN: 因为children不是一个对象    ,而是数组或字符串或null                        ,那么需要将其标准化为对象形式                        。调用normalizeVNodeSlots处理        。
function initSlots(instance, children) { //判断当前实例的children是否是slots if (instance.vnode.shapeFlag & ShapeFlags.SLOTS_CHILDREN) { const type = children._; //获取shapeSlots //有"_"标识表示是通过compiler编译得到的 if (type) { //如果有SLOTS_CHILDREN标识 表示children就是slots属性 instance.slots = reactivity.toRaw(children); //将_属性变为不可枚举属性 shared.def(children, "_", type); } else { /** * render(){ * return h(Comp,null,{ * default:()=>h("div",null), * header:()=>h("div",null) * }) * } * 没有则表示用户自己写了render函数 * 这个时候用户可能不会添加"_"属性 * 所以需要对slots进行标准化 */ normalizeObjectSlots(children, (instance.slots = {})); } } else { instance.slots = {}; //如果children为字符串或者null或数组情况 if (children) { normalizeVNodeSlots(instance, children); } } //标识slots为内部属性 shared.def(instance.slots, InternalObjectKey, 1); }
我们先来看看到底要标准化成什么样子                    ,其实对于slots所有的标准化都是为了,将不标准的形式转化为正常通过编译得到的样子            。 我们主要关注createBlock的第三个参数对象                        。通过观察我们可以发现标准化的slots应该满足                    , 一个具名插槽对应一个创建好的VNode                        ,我们这个例子只有default所以children对象中只有default; 并且必须由_withCtx包裹;(确保上下文    ,禁止block追踪) 参数必须是一个函数                ,不能是数组;(提升性能) 函数的返回值必须是一个数组            。(标准化) 如果你想自己书写标准的插槽                        ,你就应当满足以上四个条件(我选择模板编译)        。
<template> <Comp> 我是插槽内容 </Comp> </template> //编译后 function render(_ctx, _cache) { const _component_Comp = _resolveComponent("Comp", true) return (_openBlock(), _createBlock(_component_Comp, null, { default: _withCtx(() => [ _createTextVNode(" 我是插槽内容 ") ]), _: 1 /* STABLE */ })) }
normalizeObjectSlots: 改造成正常编译后的样子                        。因为没有_标识        ,所以不是通过编译得到的            ,这将不能作为标准形式的slots                        ,将其标准化                。 对于key以"_"开头或key为$stable将不会进行标准化    。 判断书写的插槽模板是否是函数            ,如果是则调用noramlizeSlot        ,如果不是警告用户                        ,应该书写函数形式                ,同样标准化插槽的value然后包装成函数在返回                        。
const normalizeObjectSlots = (rawSlots, slots, instance) => { const ctx = rawSlots._ctx; for (const key in rawSlots) { //_开头或者$stable跳过 //这将允许设置不进行标准化的插槽 if (isInternalKey(key)) continue; //获取slots的值 const value = rawSlots[key]; //如果value已经是一个函数了,需要包裹withCtx执行 //进行标准化 都需要改成通过编译的样子 if (shared.isFunction(value)) { //给instance.slots赋值 slots[key] = normalizeSlot(key, value, ctx); } /** * 用户不写函数,抛出警告,使用函数的性能将会更好 * render(){ * return createVnode(Comp,null,{ * default:createVnode(div,null) * }) * } */ else if (value != null) { console.warn( `Non-function value encountered for slot "${key}". ` + `Prefer function slots for better performance.` ); //经过normalizeSlotValue处理 返回的createVnode一定通过数组包裹 const normalized = normalizeSlotValue(value); slots[key] = () => normalized; } } };
normalizeSlot: key代表的是插槽名称(具名插槽    ,默认为default)                        ,rawSlot代表返回虚拟节点的函数(rawSlot=()=>createVNode())                    ,所以这个函数本质上是调用normalizeSlotValue对虚拟节点进行标准化,然后包裹_withCtx,最后返回经过包裹的虚拟节点                    。接下来我们先看看withCtx执行了什么。
const normalizeSlot = (key, rawSlot, ctx) => { //已经经过标准化的slot不需要在进行标准化 if (rawSlot._n) { return rawSlot; } const normalized = withCtx((...args) => { if (getCurrentInstance()) { warn( `Slot "${key}" invoked outside of the render function: ` + `this will not track dependencies used in the slot. ` + `Invoke the slot function inside the render function instead.` ); } //标准化插槽的值 rawSlot=> default:()=>createVnode(div,null) return normalizeSlotValue(rawSlot(...args)); }, ctx); //表示不是经过compiler编译的,是用户自己写的render函数 normalized._c = false; return normalized; };
withCtx: 将传递的fn包裹成renderFnWithContext在返回                    。 在执行fn的时候包裹一层currentRenderInstance                    ,确保当前的实例不出错                        。 renderFnWithContext有以下三个属性: _n:如果有这个属性代表当前函数已经被包裹过了                        ,不应该被重复包裹    。 _c: 标识的是当前的插槽是通过编译得到的    ,还是用户自己写的                。 _d: 表示执行fn的时候是否需要禁止块跟踪                ,true代表禁止块跟踪                        ,false代表允许块跟踪                        。
function withCtx( fn, ctx = getCurrentRenderingInstance(), isNonScopedSlot ) { if (!ctx) return fn; if (fn._n) { return fn; } //设置currentRenderingInstance,通过闭包确保调用fn的时候 //currentRenderingInstance实例为当前实例 /** * 如果用户调用模板表达式内的插槽 * <Button> * <template> * <slot></slot> * </template> * </Button> * 可能会扰乱块跟踪,因此默认情况下,禁止块跟踪        ,当 * 调用已经编译的插槽时强制跳出(由.d标志指示)        。 * 如果渲染已编译的slot则无需执行此操作                、因此 * 我们在renderSlot中调用renderFnWithContext * 时,.d设置为false */ const renderFnWithContext = (...args) => { //禁止块追踪,将isBlockTreeEnabled设置为0将会停止追踪 if (renderFnWithContext._d) { setBlockTracking(-1); } const prevInstance = setCurrentRenderingInstance(ctx); const res = fn(...args); setCurrentRenderingInstance(prevInstance); //开启块追踪 if (renderFnWithContext._d) { setBlockTracking(1); } return res; }; //如果已经是renderFnWithContext则不需要在包装了 renderFnWithContext._n = true; //_n表示已经经过renderFnWithContext包装 renderFnWithContext._c = true; //表示经过compiler编译得到 //true代表禁止块追踪,false代表开启块追踪 renderFnWithContext._d = true; return renderFnWithContext; }
normalizeSlotValue: 目前value传递的是单个VNode或者是数组类型的VNode            ,我们还需要对返回的所有VNode进行标准化            。这里主要是为了处理                        ,比如default:()=>"asd"            ,如果是字符串        ,他显然可以这样写                        ,但是我们需要将字符串变成patch阶段能够处理的VNode                        。
function normalizeSlotValue(value){ if(shared.isArray(value)){ return value.map(normalizeVNode) } return [normalizeVNode(value)] }
normalizeVNode: 标准化虚拟节点            。 当前虚拟节点是null                        、boolean                ,这样的值不应该显示在页面当中    ,创建注释节点        。 当前虚拟节点是一个数组                        ,需要由Fragment包裹                        。例如下面的写法                。
//自己写render函数 export default { render(){ return createVNode(Comp,null,{ default:()=>([ createVNode(div,null), createVNode(div,null) ]) }) } } //如果是正常编译获得的那么应该是
如果是object                    ,判断当前节点是否挂载过,挂载过需要克隆节点再返回    。例如下面这种情况:
export default{ render(){ return createVNode(Comp,null,{ default:()=>createTextVNode(123) }) } }
如果是字符串或者number                    ,创建文本节点即可                        。例如下面这种情况:
//自己写render函数 export default { render(){ return createVNode(Comp,null,{ default:()=>123 }) } }
function normalizeVNode(child) { if (child == null || typeof child === "boolean") { //没有child或者没有实质性内容创建注释节点 return createVNode(Comment); } else if (shared.isArray(child)) { //用户直接写了一个数组,需要包裹一层Fragment return createVNode(Fragment, null, child.slice()); } //如果这个节点已经挂载过了克隆这个节点(复用节点) else if (typeof child === "object") { return cloneIfMounted(child); } //string 或者 number else { return createVNode(Text, null, String(child)); } }
到此为止我们就完成了对于对象形式的插槽标准化                        ,并放到了实例的slots属性上                    。 现在你可以通过访问slots.default访问到经过标准化后的虚拟节点了。而我们实际在项目中使用的是<slot name="default"></slot>    ,这个又是怎么渲染到页面上的呢?大胆猜测一下就是根据name属性获取到key然后到instance.slots中去找到这个虚拟节点最后挂载到页面就可以了                    。我们会在讲解slots的实现章节详细解释                ,这里就不过多讲解了                        。
render(){ return createVNode(Comp,null,{ default:createVNode(div) }) } //经过标准化后,相当于 render(){ return createVNode(Comp,null,{ default:withCtx(()=>[createVNode(div)]) }) } //其他的情况都差不多                        ,都是为了标准化为 //满足上面四个条件的样子
下面我们讲解另一个分支        ,如果用户用数组或字符串或数字作为children参数呢?createVNode(Comp,null,[])就像这样    。又或者createVNode(Comp,null,123)这样                。这就是标识为ARRAY_CHILDREN或TEXT_CHILDREN的情况了            ,显然调用了normalizeVNodeSlots进行处理                        。 normalizeVNodeSlots:这个情况我们可以把传递的第三个参数看成是调用对象形式的default函数的返回值                        ,那么我们只需要标准化第三个参数然后包装成一个函数            ,赋值给slots.default就可以啦        。
const normalizeVNodeSlots = (instance, children) => { const normalized = normalizeSlotValue(children); instance.slots.default = () => normalized; };

额外内容

在normalizeVNode函数中        ,如果传递的child是一个对象                        ,那么调用了cloneIfMounted                ,这个函数是干什么的呢?如果el有值    ,表示已经有真实的DOM了                        ,那么就一定调用了render函数                    ,也一定挂载过元素了            。我们看看他是如何克隆节点的呢?
//挂载过的vnode有el属性 function cloneIfMounted(child) { return child.el === null || child.memo ? child : cloneVNode(child); }
cloneVNode: 用于浅克隆一个VNode                        。还可以提供额外的props合并之前VNode身上的属性            。 如果提供了extraProps,调用mergeProps合并之前的props和新的props        。对key为class        、style的属性做了特殊处理                        。并且后面的props可以覆盖前面的props 当key为class的时候                    ,之前的class已经经过标准化了一定是一个字符串                        ,我们需要将新的class与之前的class合并为一个字符串                。 当key为style的时候    ,合并新旧的style对象    。 其余情况                ,让新的覆盖旧的                        。
function mergeProps(...args) { const ret = {}; for (let i = 0; i < args.length; i++) { const toMerge = args[i]; for (const key in toMerge) { //结合class if (key === "class") { if (ret.class !== toMerge.class) { ret.class = shared.normalizeClass([ret.class, toMerge.class]); } } //结合style属性 else if (key === "style") { ret.style = shared.normalizeStyle([ret.style, toMerge.style]); } else if (key !== "") { ret[key] = toMerge[key]; } } } return ret; }
将合并的新props作为新的VNode的props属性                    。如果传递了mergeRef参数                        ,表示需要合并ref        ,那么需要读取mergeProps中的ref属性进行合并            ,之前的ref可能是数组(使用了v-for加ref)                        ,将最新的ref添加到数组的后面            ,不是数组则转化为数组在合并他们两个ref到这个数组中。 对于静态节点        ,需要深度克隆children                    。
function cloneVNode(vnode, extraProps, mergeRef = false) { const { props, ref, patchFlag, children } = vnode; const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props; const cloned = { //省略了大量属性                        ,其他的属性和传递的 //vnode一样                ,这里只列举了可能被改变的 key: mergedProps && normalizeKey(mergedProps), ref: extraProps && extraProps.ref ? mergeRef && ref ? shared.isArray(ref) ? ref.concat(normalizeRef(extraProps)) : [ref, normalizeRef(extraProps)] : normalizeRef(extraProps) : ref, children: patchFlag === PatchFlags.HOISTED && shared.isArray(children) ? children.map(deepCloneVNode) : children, shapeFlag: vnode.shapeFlag, patchFlag: extraProps && vnode.type !== Fragment ? patchFlag === PatchFlags.HOISTED ? PatchFlags.FULL_PROPS : patchFlag | PatchFlags.FULL_PROPS : patchFlag, }; return cloned; } function deepCloneVNode(vnode) { const cloned = cloneVNode(vnode); if (shared.isArray(vnode.children)) { cloned.children = vnode.children.map(deepCloneVNode); } return cloned; }

总结

本文我们主要介绍了如何对生成的组件实例的props和slots属性进行初始化                        。 在初始化props中    ,根据定义组件的props和接受到的props放到instance.props中                        ,对于定义了但是没有传递                    ,又有默认值的我们需要使用默认值    。当然我们还需要设置透传属性attrs的值,如果传递了                    ,但是没有在props            、emits中定义                        ,那么会认为是透传属性    ,需要将其放入到instance.attrs中                。 然后我们详细讲解了slots的初始化                        。这一部分主要是对用户自己使用render函数来渲染的模板                ,进行标准化保证后续的执行不会出错        。 最后我们在额外内容中介绍了cloneVNode的api实现            。 下文中我们将会继续讲解                        ,对于其他组件定义的属性的初始化                        。也就是setupStatefulComponent函数        ,这里将会对watch                        、data            、computed等属性进行处理            ,调用setup函数        、beforeCreat                        ,created钩子等            。

以上就是Vue3源码分析组件挂载初始化props与slots的详细内容            ,更多关于Vue3组件挂载初始化的资料请关注本站其它相关文章!

声明:本站所有文章        ,如无特殊说明或标注                        ,均为本站原创发布        。任何个人或组织                ,在未征得本站同意时    ,禁止复制                        、盗用                、采集    、发布本站内容到任何网站                        、书籍等各类媒体平台                        。如若本站内容侵犯了原著者的合法权益                        ,可联系我们进行处理                。

创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!

展开全文READ MORE
Vue2 与 Vue3 的数据绑定原理及实现 explorer.exe进程出错(eusexe.exe进程安全吗 可以结束吗 eusexe进程信息查询)