本文分析 Vue 对 props 的处理更新。
_init 中 mergeOptions 里会先规范化 props: normalizeProps
props:['size','color-default']
{
size: {type: null},
colorDefault: {type: null}
}
props: {
size:{type: String},
'color-default': String
}
{
size:{type: String},
colorDefault: {type: String}
}
initProps
props 是用来将外部数据传给组件。
1、属性值的校验 validateProp
2、对于根组件会将对象深度响应,子组件属性不会(外面传来的数据可能就是响应式的)
3、代理 proxy(vm, '_props', key)
let Child = {
template: `
<div>
child
</div>
`,
props: {
size: { type: Boolean}
}
}
var vm = new Vue({
el: '#app',
template: `
<div>
<child />
</div>
`,
components: {
Child
}
})
validateProp
Boolean 的处理
| 组件使用 | key (属性名) | propOptions (组件 options) | propsData (传入的 props 对象) | value |
|---|---|---|---|---|
<child /> | size | { size: { type: Boolean } } | {} | false |
<child size/> | size | { size: { type: Boolean } } | { size: '' } | true |
<child size="size"/> | size | { size: { type: Boolean } } | { size: 'size' } | true |
<child size/> | size | { size: { type: [Boolean, String] } } | { size: '' } | true |
// 规范化后的 prop.key 的值 {type: xxx, default: xx, required: xx, validator: xx}
const prop = propOptions[key]
// prop 没传 为 true
const absent = !hasOwn(propsData, key)
// 使用时传递的 prop 的值
let value = propsData[key]
// boolean 场景
const booleanIndex = getTypeIndex(Boolean, prop.type)
if (booleanIndex > -1) {
// 组件没有传递属性,且属性定义没有 default ,置为 false
if (absent && !hasOwn(prop, 'default')) {
value = false
} else if (value === '' || value === hyphenate(key)) {
// 仅传来属性 key 或者 key="key",若 [Boolean, String] / Boolean 值为 true
const stringIndex = getTypeIndex(String, prop.type)
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true
}
}
}
// 上面没满足,value 没定义时
if (value === undefined) {
// 获取 default 的值
value = getPropDefaultValue(vm, prop, key)
const prevShouldObserve = shouldObserve
toggleObserving(true)
// 将值变成响应式
observe(value)
toggleObserving(prevShouldObserve)
}
// 校验 prop
assertProp(prop, key, value, vm, absent)
获取默认值的逻辑
getPropDefaultValue
if (!hasOwn(prop, 'default')) {
return undefined
}
const def = prop.default
// 默认值 Object & Array 要函数返回 防止多个实例数据共享
if (process.env.NODE_ENV !== 'production' && isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
)
}
// prop 发生在组件更新时, 组件上次没有传值,上次有默认值,本次属性也没
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
// 返回上一次的默认值
return vm._props[key]
}
// 默认值是函数类型且定义的 type 不是函数,执行函数,否则是函数
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
组件更新的例子
let Child = {
template: `
<div>
child {{size}}
</div>
`,
props: {
'size': {
type: Object,
default() {
return { a: 1 }
}
},
'other': {
type: Number
}
}
}
var vm = new Vue({
el: '#app',
template: `
<div @click="change">
<child :other="other"/>
</div>
`,
data() {
return {
other: 1
}
},
methods: {
change() {
this.other = Math.random();
}
},
components: {
Child
}
})
流程:
首先页面导入 Vue,我们就可以使用 Vue 静态属性方法和原型方法;
new Vue(options) 时,执行 _init ,传入的 options 会先合并处理,然后对合并后的 $options 处理。因为 el有值,所有执行 $mount,而我们没有 render 函数(可以自己定义传,或者编译器处理),这里先将 templat 转为 render 函数。执行 mountComponent,这里创建渲染 Watcher,执行vm._update(vm._render(), hydrating)。
_render最终生成 Vnode。这边组件生成 Vnoe 时,_createElement会调用createComponent(Ctor, data, Vue, undefined, 'child'),Ctor 其实是 $options.componets.Child,就是子组件返回对象,通过 Vue.extend将子组件 options 传入构建子组件构造函数 VueComponent 。
_update中调用 patch ,oldVnode 一开始是 div#app 元素,用它创建空的 divVnode,处理 children childVnode,
进入 createComponent,通过 init hook 将 childVnode,和父实例 Vue 传给子组件,子组件 _init 时,options:{_isComponent: true, _parentVnode: vnode=childVnode, parent=Vue}。
子组件 initProps:
// {other: 1}
const propsData = vm.$options.propsData || {}
const props = vm._props = {}
const keys = vm.$options._propKeys = []
// $parent 是在 initLifecycle 中建立关系 为 false
const isRoot = !vm.$parent
//
if (!isRoot) {
toggleObserving(false)
}
for (const key in propsOptions) {
keys.push(key)
//
const value = validateProp(key, propsOptions, propsData, vm)
defineReactive(props, key, value)
// 这边逻辑不会进,因为 child 组件在创建其 VueComponent 处理了,将 props 挂到 VueComponent.prototype
if (!(key in vm)) {
proxy(vm, `_props`, key)
}
}
toggleObserving(true)
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
初始 不会进入
执行子组件 $mount,和上面流程一样生成 Vnode,创建子组件 DOM ;
createComponent 中上面子组件弄完,childVnode.componentInstance 有值了,执行 insert,将子组件 DOM 添加到 div 中,invokeCreateHooks ,然后 div 添加到 body 中,执行 invokeInsertHook。
最终父子实例的关系:
点击更新
重新渲染,VueComponent 之前创建过会从缓存中拿到
patch 中 oldVnode 有值,Vnode 新生成,进入 patchVnode
到 childVnode 时,进入 prepatch -> updateChildComponent
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
size 会返回上次的默认值,然后赋值,不会触发更新。若没这段逻辑,每次默认值返回新的对象,虽然内容没改变,还是触发更新。
这边遗留 diff 算法具体更新,后面分析。