props

这一次,通过源码阅读,主要探索的方面包括如何初始化 Props、以及如何进行更新。

初始化

1initState(vm)
2
3function initState(vm) {
4  vm._watchers = []
5  const opts = vm.$options
6  if (opts.props)
7    initProps(vm, opts.props)
8
9  // ...
10}
11
12function initProps(vm, propsOptions) {
13  const propsData = vm.$options.propsData || {} // 获取Vue实例选项上的Props
14  const props = (vm._props = {}) // 获取挂载Vue实例上的_props
15  const keys = (vm.$options._propKeys = []) // Props的Key值组成的数组
16  // ...
17  for (const key in propsOptions) loop(key) // 循环遍历 vue 实例选项中Props,并且执行响应式处理以及挂载在对应实例上
18  // ...
19}

初始化 Props 的关键点就在于 loop 函数,让我们接着该函数做了什么事情。

1const sharedPropertyDefinition = {
2  enumerable: true,
3  configurable: true,
4  get: noop,
5  set: noop,
6}
7function proxy(target, sourceKey, key) {
8  sharedPropertyDefinition.get = function proxyGetter() {
9    return this[sourceKey][key]
10  }
11  sharedPropertyDefinition.set = function proxySetter(val) {
12    this[sourceKey][key] = val
13  }
14  Object.defineProperty(target, key, sharedPropertyDefinition)
15}
16
17const loop = function (key) {
18  keys.push(key) // 每遍历一次Props中的值,都会收集其Key
19  // ...
20  defineReactive$$1(props, key, value, () => {
21    // 将Vue实例上对象的_props中每一项属性都设置响应式
22    if (!isRoot && !isUpdatingChildComponent) {
23      // 配置的Setter函数,避免子组件直接操作Props的警告
24      warn(
25        `Avoid mutating a prop directly since the value will be `
26        + `overwritten whenever the parent component re-renders. `
27        + `Instead, use a data or computed property based on the prop's `
28        + `value. Prop being mutated: "${
29          key
30          }"`,
31        vm
32      )
33    }
34  })
35  // static props are already proxied on the component's prototype
36  // during Vue.extend(). We only need to proxy props defined at
37  // instantiation here.
38  if (!(key in vm)) {
39    // 遍历时若发现新属性时,就将新属性重新挂载到Vue实例的_props中
40    proxy(vm, '_props', key)
41  }
42}

理解上应该不会有太大的问题,loop 函数中主要做了以下几件事:

  • defineReactive

相信对这个函数应该都不会陌生,其实就是对 Vue 实例上的_props对象中每一个属性都配置成响应式的,这样一来,当父组件中传递进来的 Props 变化时,则会通知相应的子组件中更新函数进行更新;

  • 对相应的子组件配置 Props 属性的 Setter 警告函数

用过 Vue 的童鞋们应该都会遇到直接更改一个 Props 中的属性时,会抛出一个警告。而这个警告就是在 Vue 遍历 _props 对象中的值时,都会默认配置一个警告 Setter 函数;

  • proxy

在遍历的过程中,一旦发现有新的属性时,都会将新属性重新挂载到 Vue 实例的 _props 中。这里有一个很重要的知识点,当我们直接访问一个 Props 中的属性时,即上面栗子中this.name,其实是直接访问了 Vue 实例的 _props 对象中值而已。

至此,我们也知道 Vue 源码是如何实现初始化 Props 的了,那么,究竟是父组件是如何通知更新 Props 的呢?我们接着看下去。

更新

由于父组件在更新的过程中,会通知子组件也进行更新,这时候就会调取一个方法updateChildComponent,而这个方法里就会对 Props 进行更新。我们就来看看是如何处理的:

1updateChildComponent(
2  child,
3  options.propsData, // updated props
4  options.listeners, // updated listeners
5  vnode, // new parent vnode
6  options.children // new children
7)
8
9function updateChildComponent(vm, propsData, listeners, parentVnode, renderChildren) {
10  // ...
11  // update props
12  if (propsData && vm.$options.props) {
13    toggleObserving(false) // 关闭依赖监听
14    const props = vm._props // 获取Vue实例上_props对象
15    const propKeys = vm.$options._propKeys || [] // 获取保留在Vue实例上的props key值
16    for (let i = 0; i < propKeys.length; i++) {
17      // 循环遍历props key
18      const key = propKeys[i]
19      const propOptions = vm.$options.props // wtf flow?
20      props[key] = validateProp(key, propOptions, propsData, vm) // 先校验Props中定义的数据类型是否符合,符合的话就直接返回,并且直接赋值给Vue实例上_props对象中相应的属性中
21    }
22    toggleObserving(true) // 打开依赖监听
23    // keep a copy of raw propsData
24    vm.$options.propsData = propsData // 新的PropsData直接取替掉选项中旧的PropsData
25  }
26  // ...
27}

一开始看到这里代码时,我是懵逼状态的,因为很容易绕不出来 😂。。在这里面会有几个问题,分别是:

  • validateProp 作用究竟是什么?

相信用过 Props 的同学都清楚,在传递给子组件时,子组件中是有权限决定传递的值类型的,大大提高传递的规范,举个例子:

1props: {
2  name: {
3    type: String,
4    required: true
5  }
6}

代码很好理解,就是规定 name 属性的类型以及是否必传。而方法validateProp作用就是校验父组件传递给子组件的值是否按要求传递,若正确传递则直接赋值给 _props 对象上相应的属性。

  • 校验通过后,直接赋值给 _props 对象上相应的属性的用意何在?

上面提到过,_props 对象上的每一个属性都会使用 proxy 方法进行响应式挂载。那么当我直接赋值到 _props 对象上相应的属性时,就会触发到其 Setter 函数进行相应的依赖更新。因此,当父组件更新一个传递到子组件的属性时,首先会触发其 Setter 函数通知父组件进行更新,然后通过渲染函数传递到子组件后,更新子组件中的 Props。这时候,由于此时的 Props 对象中的属性收集到了子组件的依赖,更改后会通知相应的依赖进行更新。

  • toggleObserving 究竟是干嘛用的?

首先它是一个递归遍历方法,Props 在通知子组件依赖更新时,必须搞清楚的一点,就是是整个值的变化来进行通知。如何理解?

简单滴说,对于属性值为基本数据类型的,当值改变时,是可以直接通知子组件进行更新的,而对于复杂数据类型来说,在更新时,会递归遍历其对象内部的属性来通知相应的依赖进行更新。

那么当调用方法toggleObserving为 false 时,对于基础数据类型来说,当其值变化时则直接通知子组件更新,而对于其复杂数据类型来说,则不会递归下去,而只会监听整个复杂数据类型替换时,才会去通知子组件进行更新。因此在 Props 中所有属性通知完后,又会重新调用方法 toggleObserving 为 true 来打开递归开关。(真的不得不服尤大大啊,这么好的优化思路都能想出来,牛人 👍)

至此,你大概也知道整个更新流程了,但是我当时还是存在疑惑的,既然基础数据类型值更改或复杂数据类型整个值更改,可以直接通知到子组件进行更新,那么是否会有一种情况就是,复杂数据类型中属性更改时,又是如何通知子组件更新的呢??🤔

首先,我们一开始已经忽略一个方法,那就是 defineReactive$$1,这个方法真的用的秒,可以看看上面的代码,在初始化 Props 时候,会对 Props 每一项的属性进行使用该方法进行响应式的处理,包括了复杂数据类型的中属性,此时该属性不但收集了父组件依赖,还收集了子组件的依赖,这样一来,当复杂数据类型中属性变化时,会先通知父组件更新,再通知子组件进行更新。(这时候我真的不得不服到五体投地。。。)

总结

  • 当 Props 中属性为基础数据类型值更改或复杂数据类型替换时,会通过 Setter 函数通知父组件进行更新,然后通过渲染函数,传递到子组件中更新其 Props 中对象相应的值,这时候就会触发到相应值的 Setter 来通知子组件进行更新;
  • 当 Props 中属性为复杂数据类型的属性更改时,由于使用 defineReactive$$1 方法收集到了父组件依赖以及子组件的依赖,这时候会先通知父组件进行更新,再通知子组件进行更新;

参考: