浅曦Vue源码-7-响应式数据-initState(1)-initState/initProps

270 阅读4分钟

「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

前面一篇文章从整体上梳理了一遍 _init 方法都做了哪些工作,总结起来就是:

  1. mergeOptions 合并 Vue.optionsoptions
  2. 设置 _renderProxy
  3. initLifetcylce 初始化 $parent、$root、$children
  4. initEvents 初始化自定义事件
  5. initRender 解析组件中插槽信息,初始化 vm._cvm.$createElement
  6. 触发 beforeCreated
  7. initInjections 处理 inject 并设置其为响应式数据
  8. initState 数据响应
  9. initProvide 初始化 Provide
  10. 触发 created
  11. 根据 options.el 有无决定是否调用 $mount

这些都说了,就剩一个 initState 没有说,这一篇算是填个大坑吧,这个 initState 可以说是第二复杂的,第一复杂的是 $mount 这一部分后面还要接着讲。

接下来请本篇小作文的主角 initState 登场!

二、initState

方法位置:src/core/instance/state.js -> initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  
  if (opts.props) initProps(vm, opts.props)
  // .... 暂时省略后面的过程
}

initState 方法作为数据响应式的入口,分别要处理:

  1. props 对应 initProps
  2. methods 对应 initMethods
  3. data 对应 initData
  4. computed 对应 initComputed 方法
  5. watch 对应 initWtach 方法

其实这里有必要说一下,我们的 test.html 中,new Vue 创建根实例的时候并没有传入 props/computed/watch/methods,所以事实上第一次执行的时候并不会执行对应的处理方法,所以这里需要更新一下 test.html 创建 Vue 实例部分的代码:

test.html 的 script 标签部分代码
<script>
  const sub = { /* sub details */ };
  debugger
  new Vue({
    el: '#app',
      data: {
        msg: 'hello vue'
      },
      props: {
        someProp: {
          type: String,
          default: function () { return { sp: ''somePropDefaultVal'' } }
        }
      },
      computed: {
        someComputed () {
         return this.msg + this.someProp
        }
      },
      watch: {
        msg (nv, ov) {
         console.log(nv, ov)
        }
      },
      hahaha: 'hahahahahha',
      provide: {
        foo: 'bar'
      },
      components: {
        someCom: sub
     }
  })
</script>

三、initProps

方法位置:src/core/instance/state.js -> initProps

该方法的主要作用:

  1. 获取或者初始化 vm.$options.propsDatavm._props 属性,值为 {}
  2. for in 循环遍历 vm.$options.propsOptions,处理 props 选项,把 props 的每个 key 都转化成响应式,并将 _props 代理到 vm
function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  
  if (!isRoot) {
    toggleObserving(false)
  }

  // 遍历 props 对象
  for (const key in propsOptions) {
    
    keys.push(key)

    // 获取 props[key] 的默认值
    const value = validateProp(key, propsOptions, propsData, vm)
   
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,vm)
      }

      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {}
      })
    } else {
      // 为 props 的每个 key 设置数据响应式
      defineReactive(props, key, value)
    }

    if (!(key in vm)) {
      // 代理 key 到 到 vm 上
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}

3.1 vm.$options.propsData

initProps 的开头,我们获取了 vm.$options.propsData 对象,在初始化根实例的时候这个对象是个无关紧要的对象,因为他是个 undefined

但是当初始化子组件实例时,经过在 _init 的时候变不会走 mergeOptions 这个 else 分支合并选项了,而是会走前面的 if 选项,进而执行 initInternalComponent(vm, options) 方法,这个方法会在 options 上挂载 propsData 这里先有个印象,因为这里讲的是 new Vue 时的 initState 中的 initProps

Vue.prototype._init = function () {
  if (options && options._isComponent) {
    // 组件时走这里
    initInternalComponent(vm, options)
  } else {
    mergeOptions(.....)
  }
}

// ....
export function initInternalComponent (vm, options) {
  // ...
  opts.propsData =vnodeComponentOptions.propsData // 初始化 组件 options.propsData
  // ...
}

3.2 const props = vm._props = {}

用于接收处理响应式数据的 prop 的对象,它里面的数据都变成了 gettersetter ,当然 set 是要抛出错误的。

3.3 for in 遍历 propsOptions

propsOptions 就是我们上面 demo 中定义好的 props 对象:

image.png

在遍历的过程中主要做了以下几件事:

  1. 缓存 propsOptions 中的 key
  2. 通过 validateProp(key, propsOptions, propsData, vm) 方法获取 key 对应的值 value
  3. key 设置到 props 变量即 vm._props,且为这个 key 设置响应式

3.3.1 validateOptions

方法位置:src/core/util/props.js -> validateOptions

方法作用:处理 prop 类型为 Boolean 的情形的默认值,另外当 propsData 中没有这个 prop 的时候,使用默认值,当然,你的默认值还不是响应式的数据类型,所以要用 observe 方法对这个默认值进行处理,简单来说 observe 就是递归调用 defineReactive 把默认值的子属性,甚至子属性的子属性都变成响应式数据。

此外,如果默认值是对象或者数组类型时,则会要求用一个工厂函数返回默认值所对应的对象或者数组,这和组件中的 data 要求用工厂函数时一个道理,防止组件间共享数据发生意外。

在我们的例子中,props 是在 new Vue 时传入的,所以并没有给 someProp 传值,所以这里就是取用的默认值;

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // 处理类型为 Boolean 的默认值
  const booleanIndex = getTypeIndex(Boolean, prop.type)
  if (booleanIndex > -1) {
    
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key) // 这里会检查对象或者数组默认值提示必须使用工厂函数返回默认值
    // 默认值是一个全新的值,所以要设置数据响应式
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  // ....
  return value
}

3.3.2 defineReactive

在一个对象上定义一个响应式的属性,其核心就是大家熟知的 Obeject.definePorperty 方法来拦截对象属性的读取和设置,当读取时收集依赖,当重新设置时派发更新,告知那些依赖方重新计算就可以得到新的值。

举个例子,我们 new Vue 传入的 props 是这样的:

new Vue({
   // 
    props: {
      someProp: {
        type: Object,
        default: function () { return { sp: 'special offer' } }
      }
    },

})

此时如果 someProp 变成了一个新值,那肯定是要派发更新的,但是如果只是 someProp.sp 这个属性发生变化,这个大家都知道也是要派发更新的,所以从这里大家就要有个认识,这里面一定是递归处理过了才能让 sp 值被读取时依赖被收集,更新时更感知到并且派发依赖。

defeineReactive 也是一个大块内容,我们下个主题再讨论他。

四、总结

本文主要回顾了一下 _init 做了哪些事儿,着重讲了 initState 中的 initProps 的概况,for in 遍历 propsOptions,获取每个 key 对应的默认值,如果这个默认值是对象或者数组,则递归的将其属性或者子项都变成数据响应式,最后通过 defineProperty 将这个 prop 代理到 vm._props 属性上实现数据响应式