Vue3.2 vDOM diff流程分析之一:props和attrs的初始化和更新

1,706 阅读21分钟

EE-1MmWUUAALOhV.jpg

前言

从这篇文章开始,让我们一起分析vue3 DOM diff,分析内部的工作流程。本篇文章的主要是分析propsattrs的初始化和更新,vue是如何区分propsattrs,并且为什么可以在模板中使用$attrs$props去访问propsattrs

工具函数

image.png

parseName方法用于通过正则解析vue的事件修饰符中的oncepassivecapture。最后返回的是数组中包含事件名称和事件修饰符对象。其他的事件修饰符在runtime-dom/src/directives/vOn.ts中。里面的有兼容v2的事件修饰符和键盘事件修饰符。

image.png

vue添加了事件并不是直接调用,而是会创建一个事件调用者invoker,为了能更好的控制事件的触发。invoker是一个函数,内部是带错误处理的执行绑定的函数。invoker上存储着当前调用者应该执行的函数(value属性)和创建的时间(attrached属性),(edge 异步在触发click事件的时候会触发更新事件,解决方法通过时间戳对比)

image.png

image.png

addEventListenrremoveEventListener方法是通过原生方法实现的。

image.png

一个事件绑定了多个函数,vue会将它们转换成一个函数数组,遍历执行

image.png 找到type配置指定的原生构造函数在那个位置,不存在返回-1,type不是数组,如果是预期类型返回0,不是返回-1

image.png

vue的props功能选项的type仅支持原生构造函数,vue分辨是那个构造函数的原理是:构造函数通过toString()转换成字符串,在通过正则匹配就可以得到这是什么类型。比如:Number.toString(),得到function Number() { [native code] }

propsattrs的初始化

在vue组件中,每一个组件的数据都存在一个独立的作用域中,这样意味着组件不能够直接使用父组件中的数据。而为了能够让组件能够使用父组件中的数据,vue允许使用v-bind绑定一个参数将数据传递给子组件,且子组件中需要显式的在props功能选项中显示的声明这个参数,这就是Props, 如果没有在功能选项中显示的声明,vue就会将其归到attrs中,这也是attrsprops区分的唯一依据。

前面也提到了,只有在组件的props功能选项中显式的声明才会划分到props中,这说明props的初始化在组件的解析流程中。

第一次处理props是在创建组件实例的时候,使用了normalizePropsOptions对进行了标准化处理,产生处理配置,这对后面的处理会有很大影响,位置在runtime-core/src/component.ts,(normalizePropsOptions位置在runtime-core/src/componentProps.ts)

image.png

原因很简单,组件接受的props不全是有父组件的所传递进来的,Mixinextends也可以往当前组件传递。

image.png

propsCache 是上下文中的所有组件的props标准化后配置的缓存,如果存在可直接返回。raw是组件的props功能选项的数据(可能没有),normalized是进行标准化完毕的数据,needCastKeys是需要转换的,具体怎么转换要到后面才说。

image.png

这一部分是就是开始处理MixinextendsextendProps函数是为了能够让组件继承扩展的标准化之后props,处理顺序是:全局的Mixin => 组件的extends => 组件的Mixin。等处理到这些,如果还是啥没有,可以直接结束了。

下面就开始处理父组件传递的props,因为props支持两种格式:数组和对象,这也就有两种处理方式。

  • 数组格式

image.png 书写方式:props: ['key1', 'key2', ...],每一项都必须是字符串,propskey会进行驼峰化(studentinfo => studentInfo),因为没有给任何限制,默认给一个空对象。

  • 对象格式

code.png

书写方式:

props: {
// 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证)
    propA: Number,
    propB: {
        type: String
    }
}

对象格式的处理就比较复杂了,在标准化流程中,只会处理 只是不是一个对象,不是也会将其转换成对象,和数组一样,key一样会进行驼峰化,但是不同的是,最后赋值不是一个空对象,而是一个具有prop配置的对象,里面有一些如default的配置项。

有一些会propBoolean类型或者没有默认值,会进行转换,相比于正常的prop配置对象,添加有两个key01,这和后面是否需要转换值有关系,01分别代表如何转换值,转换成true或者是false,产生的依据就是type配置项中的类型限定,看如下代码:

image.png

image.png

// propA的配置项
props: {
    propA: {
        type: [Boolean, String]
    }
}

booleanIndex是原生构造函数Booleantype配置项中的位置索引,stringIndex是原生构造函数Stringtype配置项中的位置索引,都是通过工具函数getTypeIndex获取,当然这些都是在type配置项是数组类型,不是数组默认都是-1

最后将处理完成的数据返回,并进行缓存,在标准化的过程中,如果有相同的key,先处理的会被后处理的覆盖,这就是优先级的高地,父组件传递的props优先级最高。

第二次就是在安装组件的开头就会进行初始化props。源码的位置在:runtime-core/src/component.tssetupComponent函数中

image.png

调用initProps函数进行初始化,函数的位置在:runtime-core/src/componentPorps.ts中。

image.png

image.png

先看看开头,开头是先定义了propsattrs两个对象,其中会给attrs内部会定义的一个标识__vInternal,意思为内部的,其含义应该是:“vue认为attrs是自己本身的,props是外部传递进来的”。

instance.propsDefault这是一个缓存,可能会出现一种情况,组件的props的功能选项显示的声明一个参数,但是没有传递,vue允许使用定义默认值default配置项,default除了可以是一个值以外,还可以定义为一个函数或者构造函数,这也存在一个问题,除了初始化的时候执行一次,那是不是每次后面更新也需要执行? 答案是并不会,在初始化的过程中,vue会将执行结果缓存到instance.propsDefault中,以后可以直接拿出来使用,

image.png

做好了准备工作,就可以开始处理传递的数据,setFullProps函数开始执行,作用是将传递的数据,区分和处理成attrsprops

image.png

options是标准化完毕的props功能选项数据,neeCastKeys是符合条件应该转换的proprawCastValues是用户传递的原始数据。hasAttrsChanged是在指是否有更新attrs,这到后面会详细说明,这和初始化props没有任何关系。

image.png

只有用户传递了数据才会往下执行。有一些特殊一点的key是不会往下传递的,比如:动态的keyref,以及一些钩子函数。这些都是只能在组件本身使用,不能传递给子组件。下面是兼容v2的hookEventinline-template

image.png

处理传递的数据,前面也说到了,只有在props功能选项中声明才会归到props中,其他的通通归到attrs中,首先的就是判断key或者是驼峰化之后的key是否存在options(也就是props功能选项)。

  • props的处理

排除那些应该要转换的prop,将传递进来的值保存到后面会进行处理,剩下的就放入props对象中就行了。

  • attrs的处理

排除不是emit派发事件的key,这些不属于attrsprops,剩下的放入attrs对象中就可以了,至于那个判断,因为setFullProps在更新时也会被使用到。(那个v2兼容,看到我也是很迷惑,有知道的大佬可以说说吗)

image.png

剩下通过resolvePropsValue函数进行单独处理,使用默认值和进行转换

image.png

opt 是子组件接受prop时声明的配置项,没有这个配置项,直接返回就不需要任何处理了。

  • 使用默认值

image.png

默认值存在且值等于undefined(没有传递值也是undefined),如果defualt只是一个值,直接赋值即可。如果是一个工厂函数或者是一个函数,会先从缓存中找,没有才会执行传递的默认值函数,这里兼容了v2(允许默认值函数中访问this),在v3中,组件接受的prop会作为参数传递给默认参数,且在函数内部可以使用inject API。结果返回时会将其缓存,方便下一次使用。

  • 转换值

image.png

前面normalizePropsOptions中我们已经得到需要转换数组neetCastKeys。在定义了一个类型为Booleanprop时,且没有默认值情况下,他就会转换为false。 另一种转换:当定义了一个类型可以是String也可以Booleanprop时(Boolean要在String前面),也没有默认值,在值没有传递时一样转换为false,但值如果是空字符串或者是连字符格式的prop名称就会转换成true。举个例子:

props: {
    propA: {
        type: [Boolean, String]
    }
}

// 如果没有给propA传递值,就会转换成false
// 给propA传递了'' 或 prop-a他就会转换成true

最后将值返回出去,存储在props对象中,setFullProps函数执行完毕。

image.png

最后的流程就很简单了,用户所有声明的props都有值可以被获取,不会报错,在开发模式才会去类型检查(类型检查到后面再讲)。

再将已经初始化完毕的attrs对象和props对象挂到实例上,如果是有状态的组件,一定是实例上会有attrsprops两个属性。(props在SSR模式下,不会响应式处理)

在没有状态的组件,只有声明props功能选项,才会attrsprops都挂在实例上,不然就只有attrs会放在实例上(没有显示声明,一切都会放在attrs中),实例上的props放的也是attrs对象。 attrsprops的数据初始化完毕。

props的类型检查

image.png

props的类型检查通过validateProp函数进行处理,validateProps只是为了循环调用validateProp进行类型检查,并且将配置的规则传递进去。

image.png

image.png

validateProp函数,先是对必须传递的prop进行处理,没有传递直接报警告,如果不是必须传递且值为null,也会传递。

image.png

然后轮到type类型验证,type配置会统一转换成数组,方便后面使用,类型验证结果是通过先找到预期类型,在找到值的类型,两个比较得出来的。

image.png

最后才会执行自定义验证函数,函数返回的结果影响着类型检查是否通过,不通过一样会报警告。类型检查结束。

小总结

attrsprops主要通过props功能选项来区分。在初始化之前,会先进行标准化,这是因为一个组件的attrsprops的来源不仅仅是父组件,可能是混入或者是组件扩展,这可能会有重复的,需要去重,组件自己本身的优先级最高。规范化返回值是实际规范化props选项的原始数据和需要值转换的属性键数组(布尔值和默认值)。

propsattr挂载到元素上

有一部分组件的元素会使用到由父组件传递进来的数据,也就是props,这部分数据的挂载会使用patchProp函数(别名:hostPatchProp),位置在:runtime-core/src/patchProps.ts。这部分逻辑分为了很多模块,在modules目录中,绑定的东西有很多,比如事件、元素特性、自定义的属性等等。

初始化class

image.png

image.png 挂载class使用的是patchClass函数,为了能更好的:class和静态的class一起挂载,在编译阶段就会将其规范成['staticClass', dynamic]。通过removeAttributesetAttribute设置,也会直接通过className属性设置。

理论上如果直接设置class是速度最快的,但是如果元素带有transitionclassName,这不得多考虑一下,这需要带着transitionClassName一起设置。

初始化 style

绑定的内联样式有三种格式:字符串、对象、数组。字符串和数组格式在初始化之前将其转换成对象。数组格式中可以写三种格式,也可以对某一个样式设置多重值。

image.png

normalizeStyle就是负责在初始化之前标准化不同格式统一转换成对象,会有一个空对象ret,用来存储结果。

假如第一次进来是数组,需要遍历其中的每一项,判断是不是字符串格式,不是则递归调用normalizeStyle。递归调用一样的判断,对象是直接返回,再一个个键值对的方式放入ret对象中,递归调用的不可能是字符串,前面已经判断,除非第一次进来的就是字符串格式。那样一样直接返回。

image.png

在每一项的判断中,如果是字符串格式,会调用parseStringStyle将其转换对象。原理是通过正则匹配(正则这里就不解释,主要匹配的就是各种界限符,如:;:等),找到keyvalue拼成对象返回,最后还是一个个键值对放入ret对象中。最后就得到了一个styleObject

image.png

做完这些,就可以调用patchStyle函数,将样式挂在元素上了。style是元素身上的样式对象,后面操作样式都要依靠它。第一次进来,next是存在且一定是对象。会先遍历这个styleObject将样式一个个设置在元素上。然后移除在next中也不存在旧样式。完成!

样式设置

image.png

关于样式是如何设置在元素上的,主要是两个函数起了作用:setStyleautoPrefix函数。

setStyle函数先对CSS自定义值进行处理,不是那就是属于普通的样式设置,prefixed是通过autoPrefix函数转换处理带有前缀的CSS样式名称(后面解释)。 最后根据是否带有!important分开设置(正则捕获)。

image.png

在CSS样式名称转换成带有前缀的过程中,会进行缓存,缓存存在可以直接返回。在样式对象中的样式名称有些特殊,一般都是驼峰化之后,如果存在这样样式就会返回,添加前缀的方法是:先转换成首字母大写再拼上前缀,需要每一个前缀都验证是否再样式对象中,存在则缓存后返回。

初始化添加元素事件

image.png

解析vue给元素绑定的事件,可能是v-bind指令,或者是简写,最后会被编译器转换成对象,最终在给元素添加事件。(排除v-model指令派发的事件)

image.png

初始化只会执行patchEvent函数中的这一部分。vue创建了一个事件调用者,通过时间戳等可以更好的控制事件的触发。最后通过addEventListener将事件添加给元素。在移除事件函数时,也会将事件调用者销毁,更新只需要更新调用者中指定的函数即可。(真正调用函数的是这个事件调用者)

特殊及自定义属性处理

image.png

当看到这个判断条件我相信很多人都是懵的转换,这是什么鬼条件:((key = key.slice(1)), true)。其实这是逗号操作符,作用就是对每一个操作进行求值(从左到右),并返回最后一个操作的值。如果转换成if...else,或许可以看到清楚一些。

image.png

可以看到前两个判断结果已经定死了,属性在写的时候以.开头就一定通过,属性在写的时候以^开头就一定不通过,如果结合后面的执行的函数,可以发现这是有一定目的性的,如果通过就会执行patchDOMProp,会将传递进来的属性设置到元素的prototype上,不通过就会执行后面的patchAttr,将传递进来的属性设置为attr。(设置的时候会将.^去掉)

shouldSetAsProp函数的主要作用就是限制了一些属性,必须设置为attr而不是prop。比如元素的form就必须设置为attr,再比如SVG的所有属性都必须设置为attr才能正常工作,想要更加详细了解可以可以自行去查看:shouldSetAsProp函数

所以我们可以得出一个结论:属性在写的时候以.开头就一定会在元素的prototype上设置属性,属性在写的时候以^开头就一定会设置为attr,啥也没有就按照默认的规则shouldSetAsProp函数的执行结果来确定。(当然在在进行设置的时候都会进行验证,出现错误会报警告)。

操作元素上的属性

元素的attr值会元素原型上存储,比如classNameid等,patchDOMProp主要操作的就是元素的原型。

image.png

可能是通过v-html指令进行覆盖元素原本的内容,v-html指令的原理就是通过innerHTMLtextContent进行操作,在进行DOM元素内容覆盖的时候,如果当前元素存在子元素,需要正确的卸载,再去操作。

image.png

操作元素的value值,但是要排除<progress>元素,自定义元素可以使用_value,真正操作的依据是旧值和新值不同,或者元素是<option>,当然如果值是null,这不符合规范,移除。

image.png

这一部分主要针对的就是HTML的属性,比如值不符合规范或者是特殊情况,比如id值为null就会移除,某些HTML属性在渲染的过程只会保留一个键没有值:<select multiple> vue就会编译成 {multiple: ''} 这些特殊情况。

image.png

这部分不是重点,简单说一下,为了兼容v2对枚举attribute的行为,具体可以看 attribute强制行为

image.png

这是为了一部分不兼容属性而设立的,如果是属性在写的时候以.开头,也会走这部分逻辑,但前提是不会进入前面的一大部分逻辑。

image.png

patchDOMProp处理的属性大部分都是放在元素的原型上,而patchAttr将属性放到元素身上,即在HTML渲染完毕可以在结构中看到。如:<div id="app"></div> id就可以在结构中看到。

image.png

如果元素是SVG,只会操作xlink:开头的属性。通过setAttributeNSremoveAttributeNS操作

image.png

前面是为了兼容v2的枚举attribute行为。后面就是真正开始添加属性。有一些特殊的HTML属性只会有键没有值(在HTML5规范中,比如multiple),这部分属性给值true就会存在于结构中,给false就会被移除。其他的就是正常的按照key="value"设置即可。

小总结

propsattrs放到元素上的过程中,操作方式只有两种:1.是依靠原生的操作方法,比如:setAttribute, 2.有一部分的propsattrs操作其实是修改元素的原型对象,给元素绑定的自定义属性,如果可以通过都是放在元素的原型上。vue还提供了给用户一种强制设置的方式:用户可以给属性添加前缀.代表强制在元素的原型上设置,添加前缀^代表强制放到元素结构上。

有一些特殊的HTML属性,因为它们渲染完成是只会留下属性名称,vue会将用户给它们指定的值保存,如果值是假值就会移除这个属性,真值则会保留或者设置。还有一些必须设置attr才能正常工作,详细看:shouldSetAsProp函数,里面列举了很多一些属性必须要设置为attr的原因和不设置有什么后果。

绑定的事件,在没有使用任何事件修饰符的情况下,vue会直接将函数作为事件函数,使用了事件修饰符,vue会使用一个函数来对事件的触发进行操作。一个事件绑定了多个函数会进行转换一个函数数组,触发的时候遍历执行。触发事件也不会直接触发,而是有一个事件调用者进行操作,可以防止事件多次调用和优化事件调用。

$attrs$props的初始化

image.png

image.png

$xxx是vue的代理属性,在使用的时候虽然是以对象的形式,但是它都是真真正正的函数。在使用的过程中,vue的实例进行代理,在获取的过程中会被拦截,传递给$xxx函数vue实例执行,就会从实例上取出数据,所以流程就是:访问$attrs或者$props => 被拦截找到对应的函数执行返回数据。

propsattrs的更新

props的有两种更新情况,第一种呢,在前面将propsattrs放到元素上时,都走了一遍,原本属性上是没有这些属性,放上去其实也类似更新。具体流程前面初始化的过程中已经说明了,这里就不在说明。

第二种就是由父组件引起的组件的VNode更新,新的VNode可能会带来新的props需要更新,或者是因为异步组件已经挂起没解析就触发更新,只需要更新propsslots,因为组件对渲染的依赖尚未设置。其实它们走的流程都是一样的。都是通过updatePorps函数更新,位置在runtime-core/src/componentProps.ts

image.png

更新propsattrs除了要确定新旧数据,还要找到props功能选项的配置。hasAttrsChanged这个值是用来确定attrs是否改变,以去更新插槽。

这里先要讲一下vue的patchFlags标记,这里使用到了两个patchFlags标记:PROPSFULL_PROPS,两个的区别在于PROPS指有动态的propsattrsFULL_PROPS指节点存在动态的key需要diff全部的propsattrs

  • PROPS标记情况

code.png

在这种情况下,只会拿到vnode上的dynamicProps(动态props汇总),以遍历更新动态的propsattrs。如果没有props功能配置选项,那全部只会去更新attrs对象(没有,实例上的propsattrs使用的都是attrs对象),将hasAttrsChanged改为true

存在props配置项,会进行判断是否存在attrs中,存在就更新attrs对象并将hasAttrsChanged改为true代表attrs发生了改变。不存在通过resolvePropsValue确认最终值后放入props中。

  • FULL_PROPS情况

code.png

开头执行的setFullProps函数,目的是为了将新的propsattrs放入实例上。可能新旧会存在相同的,最后会确认是否有修改attrs

这里需要说明一下,rawProps只是包含propsattrs的数据,并不是配置,前面执行setFullPorps就已经将attrsprops更新了,但是执行完setFullProps并不代表所有的propsattrs都会更新,或许还有一部分没有更新。下面找出新数据力不存在的key,如果存在props配置,会重新确认一次这些props的值放入,不存在就将它们一一删除。后面的attrs也一样的更新,前提是实例上的propsattrs使用的不是同一个对象。

image.png

更新的最后一个步骤,如果attrs发生了改变,说明$attrs也一定发生了改变,去派发更新插槽。如果在开发模式还会检查一遍更新完毕的数据。propsattrs更新流程结束。

小总结

propsattrs的更新,除了更新已经放在元素上的数据,最直接的是更新实例上propsattrs,更新传递过来的只是数据,使用的props配置还是之前的,setFullProps只是将新的数据更新到实例中,如果旧的数据有些不存在新的数据中,则默认认为“缺席”,使用默认值或者转换值,没有props配置会将这些“缺席”的一一删除。而attrs是直接删除。最后因为attrs更新而去更新插槽。

最后的总结

propsattrs是组件之间通讯的一种方式,无论以任何一种方式去传递,最后都会放在实例中的propsattrs属性中。在处理这些数据的过程中,需要注意一些特性HTML属性,它们存在的方式只有键没有值,在编译的过程中,一般都是编译成真假值。在更新阶段也是要特殊处理的,还有一部分是必须设置为attr才能正常工作。

最后,props和attrs的初始化和更新流程到此,可能有些地方分析的不足,希望各路大佬能够指导和补充,谢谢。