前言
从这篇文章开始,让我们一起分析vue3 DOM diff,分析内部的工作流程。本篇文章的主要是分析props
和attrs
的初始化和更新,vue是如何区分props
和attrs
,并且为什么可以在模板中使用$attrs
和$props
去访问props
和attrs
。
工具函数
parseName
方法用于通过正则解析vue的事件修饰符中的once
、passive
、capture
。最后返回的是数组中包含事件名称和事件修饰符对象。其他的事件修饰符在runtime-dom/src/directives/vOn.ts
中。里面的有兼容v2的事件修饰符和键盘事件修饰符。
vue添加了事件并不是直接调用,而是会创建一个事件调用者invoker
,为了能更好的控制事件的触发。invoker
是一个函数,内部是带错误处理的执行绑定的函数。invoker
上存储着当前调用者应该执行的函数(value
属性)和创建的时间(attrached
属性),(edge 异步在触发click
事件的时候会触发更新事件,解决方法通过时间戳对比)
addEventListenr
和removeEventListener
方法是通过原生方法实现的。
一个事件绑定了多个函数,vue会将它们转换成一个函数数组,遍历执行
找到
type
配置指定的原生构造函数在那个位置,不存在返回-1
,type
不是数组,如果是预期类型返回0
,不是返回-1
。
vue的props
功能选项的type
仅支持原生构造函数,vue分辨是那个构造函数的原理是:构造函数通过toString()
转换成字符串,在通过正则匹配就可以得到这是什么类型。比如:Number.toString()
,得到function Number() { [native code] }
。
props
和attrs
的初始化
在vue组件中,每一个组件的数据都存在一个独立的作用域中,这样意味着组件不能够直接使用父组件中的数据。而为了能够让组件能够使用父组件中的数据,vue允许使用v-bind
绑定一个参数将数据传递给子组件,且子组件中需要显式的在props
功能选项中显示的声明这个参数,这就是Props
, 如果没有在功能选项中显示的声明,vue就会将其归到attrs
中,这也是attrs
和props
区分的唯一依据。
前面也提到了,只有在组件的props
功能选项中显式的声明才会划分到props
中,这说明props
的初始化在组件的解析流程中。
第一次处理props
是在创建组件实例的时候,使用了normalizePropsOptions
对进行了标准化处理,产生处理配置,这对后面的处理会有很大影响,位置在runtime-core/src/component.ts
,(normalizePropsOptions
位置在runtime-core/src/componentProps.ts
)
原因很简单,组件接受的props
不全是有父组件的所传递进来的,Mixin
和extends
也可以往当前组件传递。
propsCache
是上下文中的所有组件的props标准化后配置的缓存,如果存在可直接返回。raw
是组件的props
功能选项的数据(可能没有),normalized
是进行标准化完毕的数据,needCastKeys
是需要转换的,具体怎么转换要到后面才说。
这一部分是就是开始处理Mixin
和extends
,extendProps
函数是为了能够让组件继承扩展的标准化之后props
,处理顺序是:全局的Mixin
=> 组件的extends
=> 组件的Mixin
。等处理到这些,如果还是啥没有,可以直接结束了。
下面就开始处理父组件传递的props
,因为props
支持两种格式:数组和对象,这也就有两种处理方式。
- 数组格式
书写方式:
props: ['key1', 'key2', ...]
,每一项都必须是字符串,props
的key
会进行驼峰化(studentinfo => studentInfo
),因为没有给任何限制,默认给一个空对象。
- 对象格式
书写方式:
props: {
// 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证)
propA: Number,
propB: {
type: String
}
}
对象格式的处理就比较复杂了,在标准化流程中,只会处理 只是不是一个对象,不是也会将其转换成对象,和数组一样,key
一样会进行驼峰化,但是不同的是,最后赋值不是一个空对象,而是一个具有prop
配置的对象,里面有一些如default
的配置项。
有一些会prop
是Boolean
类型或者没有默认值,会进行转换,相比于正常的prop
配置对象,添加有两个key
:0
和1
,这和后面是否需要转换值有关系,0
和1
分别代表如何转换值,转换成true
或者是false
,产生的依据就是type
配置项中的类型限定,看如下代码:
// propA的配置项
props: {
propA: {
type: [Boolean, String]
}
}
booleanIndex
是原生构造函数Boolean
在type
配置项中的位置索引,stringIndex
是原生构造函数String
在type
配置项中的位置索引,都是通过工具函数getTypeIndex
获取,当然这些都是在type
配置项是数组类型,不是数组默认都是-1
。
最后将处理完成的数据返回,并进行缓存,在标准化的过程中,如果有相同的key
,先处理的会被后处理的覆盖,这就是优先级的高地,父组件传递的props
优先级最高。
第二次就是在安装组件的开头就会进行初始化props
。源码的位置在:runtime-core/src/component.ts
的setupComponent
函数中
调用initProps
函数进行初始化,函数的位置在:runtime-core/src/componentPorps.ts
中。
先看看开头,开头是先定义了props
和attrs
两个对象,其中会给attrs
内部会定义的一个标识__vInternal
,意思为内部的,其含义应该是:“vue认为attrs
是自己本身的,props
是外部传递进来的”。
instance.propsDefault
这是一个缓存,可能会出现一种情况,组件的props
的功能选项显示的声明一个参数,但是没有传递,vue允许使用定义默认值default
配置项,default
除了可以是一个值以外,还可以定义为一个函数或者构造函数,这也存在一个问题,除了初始化的时候执行一次,那是不是每次后面更新也需要执行? 答案是并不会,在初始化的过程中,vue会将执行结果缓存到instance.propsDefault
中,以后可以直接拿出来使用,
做好了准备工作,就可以开始处理传递的数据,setFullProps
函数开始执行,作用是将传递的数据,区分和处理成attrs
和props
options
是标准化完毕的props
功能选项数据,neeCastKeys
是符合条件应该转换的prop
,rawCastValues
是用户传递的原始数据。hasAttrsChanged
是在指是否有更新attrs
,这到后面会详细说明,这和初始化props
没有任何关系。
只有用户传递了数据才会往下执行。有一些特殊一点的key
是不会往下传递的,比如:动态的key
和ref
,以及一些钩子函数。这些都是只能在组件本身使用,不能传递给子组件。下面是兼容v2的hookEvent
和inline-template
。
处理传递的数据,前面也说到了,只有在props
功能选项中声明才会归到props
中,其他的通通归到attrs
中,首先的就是判断key
或者是驼峰化之后的key
是否存在options
(也就是props
功能选项)。
props
的处理
排除那些应该要转换的prop
,将传递进来的值保存到后面会进行处理,剩下的就放入props
对象中就行了。
attrs
的处理
排除不是emit
派发事件的key
,这些不属于attrs
和props
,剩下的放入attrs
对象中就可以了,至于那个判断,因为setFullProps
在更新时也会被使用到。(那个v2兼容,看到我也是很迷惑,有知道的大佬可以说说吗)
剩下通过resolvePropsValue
函数进行单独处理,使用默认值和进行转换
opt 是子组件接受prop
时声明的配置项,没有这个配置项,直接返回就不需要任何处理了。
- 使用默认值
默认值存在且值等于undefined
(没有传递值也是undefined
),如果defualt
只是一个值,直接赋值即可。如果是一个工厂函数或者是一个函数,会先从缓存中找,没有才会执行传递的默认值函数,这里兼容了v2(允许默认值函数中访问this
),在v3中,组件接受的prop
会作为参数传递给默认参数,且在函数内部可以使用inject API
。结果返回时会将其缓存,方便下一次使用。
- 转换值
前面normalizePropsOptions
中我们已经得到需要转换数组neetCastKeys
。在定义了一个类型为Boolean
的prop
时,且没有默认值情况下,他就会转换为false
。 另一种转换:当定义了一个类型可以是String
也可以Boolean
的prop
时(Boolean
要在String
前面),也没有默认值,在值没有传递时一样转换为false
,但值如果是空字符串或者是连字符格式的prop
名称就会转换成true
。举个例子:
props: {
propA: {
type: [Boolean, String]
}
}
// 如果没有给propA传递值,就会转换成false
// 给propA传递了'' 或 prop-a他就会转换成true
最后将值返回出去,存储在props
对象中,setFullProps
函数执行完毕。
最后的流程就很简单了,用户所有声明的props
都有值可以被获取,不会报错,在开发模式才会去类型检查(类型检查到后面再讲)。
再将已经初始化完毕的attrs
对象和props
对象挂到实例上,如果是有状态的组件,一定是实例上会有attrs
和props
两个属性。(props
在SSR模式下,不会响应式处理)
在没有状态的组件,只有声明props
功能选项,才会attrs
和props
都挂在实例上,不然就只有attrs
会放在实例上(没有显示声明,一切都会放在attrs
中),实例上的props
放的也是attrs
对象。 attrs
和props
的数据初始化完毕。
props
的类型检查
props
的类型检查通过validateProp
函数进行处理,validateProps
只是为了循环调用validateProp
进行类型检查,并且将配置的规则传递进去。
validateProp
函数,先是对必须传递的prop
进行处理,没有传递直接报警告,如果不是必须传递且值为null
,也会传递。
然后轮到type
类型验证,type
配置会统一转换成数组,方便后面使用,类型验证结果是通过先找到预期类型,在找到值的类型,两个比较得出来的。
最后才会执行自定义验证函数,函数返回的结果影响着类型检查是否通过,不通过一样会报警告。类型检查结束。
小总结
attrs
和props
主要通过props
功能选项来区分。在初始化之前,会先进行标准化,这是因为一个组件的attrs
和props
的来源不仅仅是父组件,可能是混入或者是组件扩展,这可能会有重复的,需要去重,组件自己本身的优先级最高。规范化返回值是实际规范化props
选项的原始数据和需要值转换的属性键数组(布尔值和默认值)。
props
和attr
挂载到元素上
有一部分组件的元素会使用到由父组件传递进来的数据,也就是props
,这部分数据的挂载会使用patchProp
函数(别名:hostPatchProp
),位置在:runtime-core/src/patchProps.ts
。这部分逻辑分为了很多模块,在modules
目录中,绑定的东西有很多,比如事件、元素特性、自定义的属性等等。
初始化class
挂载
class
使用的是patchClass
函数,为了能更好的:class
和静态的class
一起挂载,在编译阶段就会将其规范成['staticClass', dynamic]
。通过removeAttribute
和setAttribute
设置,也会直接通过className
属性设置。
理论上如果直接设置class
是速度最快的,但是如果元素带有transition
的className
,这不得多考虑一下,这需要带着transitionClassName
一起设置。
初始化 style
绑定的内联样式有三种格式:字符串、对象、数组。字符串和数组格式在初始化之前将其转换成对象。数组格式中可以写三种格式,也可以对某一个样式设置多重值。
normalizeStyle
就是负责在初始化之前标准化不同格式统一转换成对象,会有一个空对象ret
,用来存储结果。
假如第一次进来是数组,需要遍历其中的每一项,判断是不是字符串格式,不是则递归调用normalizeStyle
。递归调用一样的判断,对象是直接返回,再一个个键值对的方式放入ret
对象中,递归调用的不可能是字符串,前面已经判断,除非第一次进来的就是字符串格式。那样一样直接返回。
在每一项的判断中,如果是字符串格式,会调用parseStringStyle
将其转换对象。原理是通过正则匹配(正则这里就不解释,主要匹配的就是各种界限符,如:;
、:
等),找到key
和value
拼成对象返回,最后还是一个个键值对放入ret
对象中。最后就得到了一个styleObject
。
做完这些,就可以调用patchStyle
函数,将样式挂在元素上了。style
是元素身上的样式对象,后面操作样式都要依靠它。第一次进来,next
是存在且一定是对象。会先遍历这个styleObject
将样式一个个设置在元素上。然后移除在next
中也不存在旧样式。完成!
样式设置
关于样式是如何设置在元素上的,主要是两个函数起了作用:setStyle
和autoPrefix
函数。
setStyle
函数先对CSS自定义值进行处理,不是那就是属于普通的样式设置,prefixed
是通过autoPrefix
函数转换处理带有前缀的CSS样式名称(后面解释)。 最后根据是否带有!important
分开设置(正则捕获)。
在CSS样式名称转换成带有前缀的过程中,会进行缓存,缓存存在可以直接返回。在样式对象中的样式名称有些特殊,一般都是驼峰化之后,如果存在这样样式就会返回,添加前缀的方法是:先转换成首字母大写再拼上前缀,需要每一个前缀都验证是否再样式对象中,存在则缓存后返回。
初始化添加元素事件
解析vue给元素绑定的事件,可能是v-bind
指令,或者是简写,最后会被编译器转换成对象,最终在给元素添加事件。(排除v-model
指令派发的事件)
初始化只会执行patchEvent
函数中的这一部分。vue创建了一个事件调用者,通过时间戳等可以更好的控制事件的触发。最后通过addEventListener
将事件添加给元素。在移除事件函数时,也会将事件调用者销毁,更新只需要更新调用者中指定的函数即可。(真正调用函数的是这个事件调用者)
特殊及自定义属性处理
当看到这个判断条件我相信很多人都是懵的转换,这是什么鬼条件:((key = key.slice(1)), true)
。其实这是逗号操作符,作用就是对每一个操作进行求值(从左到右),并返回最后一个操作的值。如果转换成if...else
,或许可以看到清楚一些。
可以看到前两个判断结果已经定死了,属性在写的时候以.
开头就一定通过,属性在写的时候以^
开头就一定不通过,如果结合后面的执行的函数,可以发现这是有一定目的性的,如果通过就会执行patchDOMProp
,会将传递进来的属性设置到元素的prototype
上,不通过就会执行后面的patchAttr
,将传递进来的属性设置为attr
。(设置的时候会将.
和^
去掉)
shouldSetAsProp
函数的主要作用就是限制了一些属性,必须设置为attr
而不是prop
。比如元素的form
就必须设置为attr
,再比如SVG
的所有属性都必须设置为attr
才能正常工作,想要更加详细了解可以可以自行去查看:shouldSetAsProp
函数
所以我们可以得出一个结论:属性在写的时候以.
开头就一定会在元素的prototype
上设置属性,属性在写的时候以^
开头就一定会设置为attr
,啥也没有就按照默认的规则shouldSetAsProp
函数的执行结果来确定。(当然在在进行设置的时候都会进行验证,出现错误会报警告)。
操作元素上的属性
元素的attr
值会元素原型上存储,比如className
、id
等,patchDOMProp
主要操作的就是元素的原型。
可能是通过v-html
指令进行覆盖元素原本的内容,v-html
指令的原理就是通过innerHTML
和textContent
进行操作,在进行DOM
元素内容覆盖的时候,如果当前元素存在子元素,需要正确的卸载,再去操作。
操作元素的value
值,但是要排除<progress>
元素,自定义元素可以使用_value
,真正操作的依据是旧值和新值不同,或者元素是<option>
,当然如果值是null,这不符合规范,移除。
这一部分主要针对的就是HTML的属性,比如值不符合规范或者是特殊情况,比如id
值为null
就会移除,某些HTML
属性在渲染的过程只会保留一个键没有值:<select multiple>
vue就会编译成 {multiple: ''}
这些特殊情况。
这部分不是重点,简单说一下,为了兼容v2对枚举attribute
的行为,具体可以看 attribute
强制行为
这是为了一部分不兼容属性而设立的,如果是属性在写的时候以.
开头,也会走这部分逻辑,但前提是不会进入前面的一大部分逻辑。
patchDOMProp
处理的属性大部分都是放在元素的原型上,而patchAttr
将属性放到元素身上,即在HTML渲染完毕可以在结构中看到。如:<div id="app"></div>
id就可以在结构中看到。
如果元素是SVG
,只会操作xlink:
开头的属性。通过setAttributeNS
和removeAttributeNS
操作
前面是为了兼容v2的枚举attribute
行为。后面就是真正开始添加属性。有一些特殊的HTML属性只会有键没有值(在HTML5规范中,比如multiple
),这部分属性给值true
就会存在于结构中,给false
就会被移除。其他的就是正常的按照key="value"
设置即可。
小总结
props
和attrs
放到元素上的过程中,操作方式只有两种:1.是依靠原生的操作方法,比如:setAttribute
, 2.有一部分的props
和attrs
操作其实是修改元素的原型对象,给元素绑定的自定义属性,如果可以通过都是放在元素的原型上。vue还提供了给用户一种强制设置的方式:用户可以给属性添加前缀.
代表强制在元素的原型上设置,添加前缀^
代表强制放到元素结构上。
有一些特殊的HTML属性,因为它们渲染完成是只会留下属性名称,vue会将用户给它们指定的值保存,如果值是假值就会移除这个属性,真值则会保留或者设置。还有一些必须设置attr
才能正常工作,详细看:shouldSetAsProp
函数,里面列举了很多一些属性必须要设置为attr
的原因和不设置有什么后果。
绑定的事件,在没有使用任何事件修饰符的情况下,vue会直接将函数作为事件函数,使用了事件修饰符,vue会使用一个函数来对事件的触发进行操作。一个事件绑定了多个函数会进行转换一个函数数组,触发的时候遍历执行。触发事件也不会直接触发,而是有一个事件调用者进行操作,可以防止事件多次调用和优化事件调用。
$attrs
和$props
的初始化
$xxx
是vue的代理属性,在使用的时候虽然是以对象的形式,但是它都是真真正正的函数。在使用的过程中,vue的实例进行代理,在获取的过程中会被拦截,传递给$xxx
函数vue实例执行,就会从实例上取出数据,所以流程就是:访问$attrs
或者$props
=> 被拦截找到对应的函数执行返回数据。
props
和attrs
的更新
props
的有两种更新情况,第一种呢,在前面将props
和attrs
放到元素上时,都走了一遍,原本属性上是没有这些属性,放上去其实也类似更新。具体流程前面初始化的过程中已经说明了,这里就不在说明。
第二种就是由父组件引起的组件的VNode
更新,新的VNode
可能会带来新的props
需要更新,或者是因为异步组件已经挂起没解析就触发更新,只需要更新props
和slots
,因为组件对渲染的依赖尚未设置。其实它们走的流程都是一样的。都是通过updatePorps
函数更新,位置在runtime-core/src/componentProps.ts
。
更新props
和attrs
除了要确定新旧数据,还要找到props
功能选项的配置。hasAttrsChanged
这个值是用来确定attrs
是否改变,以去更新插槽。
这里先要讲一下vue的patchFlags
标记,这里使用到了两个patchFlags
标记:PROPS
和FULL_PROPS
,两个的区别在于PROPS
指有动态的props
和attrs
,FULL_PROPS
指节点存在动态的key
需要diff全部的props
和attrs
。
PROPS
标记情况
在这种情况下,只会拿到vnode
上的dynamicProps
(动态props
汇总),以遍历更新动态的props
和attrs
。如果没有props
功能配置选项,那全部只会去更新attrs
对象(没有,实例上的props
和attrs
使用的都是attrs
对象),将hasAttrsChanged
改为true
。
存在props
配置项,会进行判断是否存在attrs
中,存在就更新attrs
对象并将hasAttrsChanged
改为true
代表attrs
发生了改变。不存在通过resolvePropsValue
确认最终值后放入props
中。
FULL_PROPS
情况
开头执行的setFullProps
函数,目的是为了将新的props
和attrs
放入实例上。可能新旧会存在相同的,最后会确认是否有修改attrs
。
这里需要说明一下,rawProps
只是包含props
和attrs
的数据,并不是配置,前面执行setFullPorps
就已经将attrs
和props
更新了,但是执行完setFullProps
并不代表所有的props
和attrs
都会更新,或许还有一部分没有更新。下面找出新数据力不存在的key
,如果存在props
配置,会重新确认一次这些props
的值放入,不存在就将它们一一删除。后面的attrs
也一样的更新,前提是实例上的props
和attrs
使用的不是同一个对象。
更新的最后一个步骤,如果attrs
发生了改变,说明$attrs
也一定发生了改变,去派发更新插槽。如果在开发模式还会检查一遍更新完毕的数据。props
和attrs
更新流程结束。
小总结
props
和attrs
的更新,除了更新已经放在元素上的数据,最直接的是更新实例上props
和attrs
,更新传递过来的只是数据,使用的props
配置还是之前的,setFullProps
只是将新的数据更新到实例中,如果旧的数据有些不存在新的数据中,则默认认为“缺席”,使用默认值或者转换值,没有props
配置会将这些“缺席”的一一删除。而attrs
是直接删除。最后因为attrs
更新而去更新插槽。
最后的总结
props
和attrs
是组件之间通讯的一种方式,无论以任何一种方式去传递,最后都会放在实例中的props
和attrs
属性中。在处理这些数据的过程中,需要注意一些特性HTML属性,它们存在的方式只有键没有值,在编译的过程中,一般都是编译成真假值。在更新阶段也是要特殊处理的,还有一部分是必须设置为attr
才能正常工作。
最后,props和attrs的初始化和更新流程到此,可能有些地方分析的不足,希望各路大佬能够指导和补充,谢谢。