Vue2 props 处理分析

398 阅读2分钟

本文分析 Vue 对 props 的处理更新。

_initmergeOptions 里会先规范化 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

最终父子实例的关系:

image.png

点击更新

重新渲染,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 算法具体更新,后面分析。