美文网首页前端
Vue2.0 源码分析笔记(六)props、methods、pr

Vue2.0 源码分析笔记(六)props、methods、pr

作者: 若年 | 来源:发表于2021-08-17 14:57 被阅读0次

    props初始化过程

    initprops 方法定义在src/instance/state.js中

    function initProps (vm: Component, propsOptions: Object) {
      const propsData = vm.$options.propsData || {}
      const props = vm._props = {}
      // cache prop keys so that future props updates can iterate using Array
      // instead of dynamic object key enumeration.
      const keys = vm.$options._propKeys = []
      const isRoot = !vm.$parent
      // root instance props should be converted
      if (!isRoot) {
        toggleObserving(false)
      }
      for (const key in propsOptions) {
        keys.push(key)
        const value = validateProp(key, propsOptions, propsData, vm)
        /* istanbul ignore else */
        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) {
              warn(
                `Avoid mutating a prop directly since the value will be ` +
                `overwritten whenever the parent component re-renders. ` +
                `Instead, use a data or computed property based on the prop's ` +
                `value. Prop being mutated: "${key}"`,
                vm
              )
            }
          })
        } else {
          defineReactive(props, key, value)
        }
        // static props are already proxied on the component's prototype
        // during Vue.extend(). We only need to proxy props defined at
        // instantiation here.
        if (!(key in vm)) {
          proxy(vm, `_props`, key)
        }
      }
      toggleObserving(true)
    }
    

    以上代码简写为

    if (!isRoot) {
      toggleObserving(false)
    }
    for (const key in propsOptions) {
      // 省略...
      if (process.env.NODE_ENV !== 'production') {
        // 省略...
      } else {
        defineReactive(props, key, value)
      }
      // 省略...
    }
    toggleObserving(true)
    

    为了搞清楚其目的,我们需要找到 defineReactive 函数,注意如下高亮的代码:

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 省略...
    //==============高亮=========
      let childOb = !shallow && observe(val)  
    //==============高亮=========
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          // 省略...
        },
        set: function reactiveSetter (newVal) {
          // 省略...
        }
      })
    }
    

    如上那句高亮的代码所示,在使用 defineReactive 函数定义属性时,会调用 observe 函数对值继续进行观测。但由于之前使用了 toggleObserving(false) 函数关闭了开关,所以上面高亮代码中调用 observe 函数是一个无效调用。所以我们可以得出一个结论:在定义 props 数据时,不将 prop 值转换为响应式数据,这里要注意的是:由于 props 本身是通过 defineReactive 定义的,所以 props 本身是响应式的,但没有对值进行深度定义。为什么这样做呢?很简单,我们知道 props 是来自外界的数据,或者更具体一点的说,props 是来自父组件的数据,这个数据如果是一个对象(包括纯对象和数组),那么它本身可能已经是响应式的了,所以不再需要重复定义。另外在定义 props 数据之后,又调用 toggleObserving(true) 函数将开关开启,这么做的目的是不影响后续代码的功能,因为这个开关是全局的。
    最后大家还要注意一点,如下:

    if (!isRoot) {
      toggleObserving(false)
    }
    

    这段代码说明,只有当不是根组件的时候才会关闭开关,这说明如果当前组件实例是根组件的话,那么定义的 props 的值也会被定义为响应式数据。

    props 的校验

    const value = validateProp(key, propsOptions, propsData, vm)
    
    
    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 casting
      const booleanIndex = getTypeIndex(Boolean, prop.type)
      if (booleanIndex > -1) {
        if (absent && !hasOwn(prop, 'default')) {
          value = false
        } else if (value === '' || value === hyphenate(key)) {
          // only cast empty string / same name to boolean if
          // boolean has higher priority
          const stringIndex = getTypeIndex(String, prop.type)
          if (stringIndex < 0 || booleanIndex < stringIndex) {
            value = true
          }
        }
      }
      // check default value
      if (value === undefined) {
        value = getPropDefaultValue(vm, prop, key)
        // since the default value is a fresh copy,
        // make sure to observe it.
        const prevShouldObserve = shouldObserve
        toggleObserving(true)
        observe(value)
        toggleObserving(prevShouldObserve)
      }
      if (
        process.env.NODE_ENV !== 'production' &&
        // skip validation for weex recycle-list child component props
        !(__WEEX__ && isObject(value) && ('@binding' in value))
      ) {
        assertProp(prop, key, value, vm, absent)
      }
      return value
    }
    
    

    validateProp 一开始并没有对 props 的类型做校验,首先如果一个 prop 的类型是布尔类型,则为其设置合理的布尔值,其次又调用了 getPropDefaultValue 函数获取 prop 的默认值,而如上这段代码才是真正用来对 props 的类型做校验的。通过如上 if 语句的条件可知,仅在非生产环境下才会对 props 做类型校验,另外还有一个条件是用来跳过 weex 环境下某种条件的判断的,我们不做讲解。总之真正的校验工作是由 assertProp 函数完成的。

    methods 选项的初始化及实现

    methods 选项实现要简单的多,打开 src/core/instance/state.js 文件找到 initMethods 函数,如下:

    function initMethods (vm: Component, methods: Object) {
      const props = vm.$options.props
      for (const key in methods) {
        if (process.env.NODE_ENV !== 'production') {
          if (methods[key] == null) {
            warn(
              `Method "${key}" has an undefined value in the component definition. ` +
              `Did you reference the function correctly?`,
              vm
            )
          }
          if (props && hasOwn(props, key)) {
            warn(
              `Method "${key}" has already been defined as a prop.`,
              vm
            )
          }
          if ((key in vm) && isReserved(key)) {
            warn(
              `Method "${key}" conflicts with an existing Vue instance method. ` +
              `Avoid defining component methods that start with _ or $.`
            )
          }
        }
        vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
      }
    }
    

    这样一来可以很清晰的看到 methods 选项是如何实现的,就是通过 for...in 循环遍历 methods 选项对象,其中 key 就是每个方法的名字。最关键的是循环的最后一句代码:

    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
    

    通过这句代码可知,之所以能够通过组件实例对象访问 methods 选项中定义的方法,就是因为在组件实例对象上定义了与 methods 选项中所定义的同名方法,当然了在定义到组件实例对象之前要检测该方法是否真正的有定义:methods[key] == null,如果没有则添加一个空函数到组件实例对象上。

    provide 选项的初始化及实现

    Vue.prototype._init 方法中的一段用来完成初始化工作的代码:

    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')
    

    可以发现 initInjections 函数在 initProvide 函数之前被调用,这说明对于任何一个组件来讲,总是要优先初始化 inject 选项,再初始化 provide 选项,这么做是有原因的,我们后面会提到。但是我们知道 inject 选项的数据需要从父代组件中的 provide 获取,所以我们优先来了解 provide 选项的实现,然后再查看 inject 选项的实现。
    打开 src/core/instance/inject.js 文件,找到 initProvide 函数,如下:

    export function initProvide (vm: Component) {
      const provide = vm.$options.provide
      if (provide) {
        vm._provided = typeof provide === 'function'
          ? provide.call(vm)
          : provide
      }
    }
    

    如上是 initProvide 函数的全部代码,它接收组件实例对象作为参数。在 initProvide 函数内部首先定义了 provide 常量,它的值是 vm.$options.provide 选项的引用,接着是一个 if 条件语句,只有在 provide 选项存在的情况下才会执行 if 语句块内的代码,我们知道 provide 选项可以是对象,也可以是一个返回对象的函数。所以在 if 语句块内使用 typeof 操作符检测 provide 常量的类型,如果是函数则执行该函数获取数据,否则直接将 provide 本身作为数据。最后将数据复制给组件实例对象的 vm._provided 属性,后面我们可以看到当组件初始化 inject 选项时,其注入的数据就是从父代组件实例的 vm._provided 属性中获取的。

    以上就是 provide 选项的初始化及实现,它本质上就是在组件实例对象上添加了 vm._provided 属性,并保存了用于子代组件的数据。

    inject 选项的初始化及实现

    看完了 provide 选项的初始化及实现,接下来我们研究一下 inject 选项的初始化及实现。找到 initInjections 函数,它也定义在 src/core/instance/inject.js 文件,如下是 initInjections 函数的整体结构:

    export function initInjections (vm: Component) {
      const result = resolveInject(vm.$options.inject, vm)
      if (result) {
        // 省略...
      }
    }
    

    找到 resolveInject 函数,它定义在 initInjections 函数的下方,如下是其函数签名:

    export function resolveInject (inject: any, vm: Component): ?Object {
      if (inject) {
        // inject is :any because flow is not smart enough to figure out cached
        const result = Object.create(null)
        const keys = hasSymbol
          ? Reflect.ownKeys(inject)
          : Object.keys(inject)
    
        for (let i = 0; i < keys.length; i++) {
          const key = keys[i]
          // #6574 in case the inject object is observed...
          if (key === '__ob__') continue
          const provideKey = inject[key].from
          let source = vm
          while (source) {
            if (source._provided && hasOwn(source._provided, provideKey)) {
              result[key] = source._provided[provideKey]
              break
            }
            source = source.$parent
          }
          if (!source) {
            if ('default' in inject[key]) {
              const provideDefault = inject[key].default
              result[key] = typeof provideDefault === 'function'
                ? provideDefault.call(vm)
                : provideDefault
            } else if (process.env.NODE_ENV !== 'production') {
              warn(`Injection "${key}" not found`, vm)
            }
          }
        }
        return result
      }
    }
    

    keys 常量中保存 inject 选项对象的每一个键名,接下来的代码使用 for 循环,用来遍历刚刚获取到的 keys 数组,其中 key 常量就是 keys 数组中的每一个值,即 inject 选项的每一个键值,provideKey 常量保存的是每一个 inject 选项内所定义的注入对象的 from 属性的值,我们知道 from 属性的值代表着 vm._provided 数据中的每个数据的键名,所以 provideKey 常量将用来查找所注入的数据。最后定义了 source 变量,它的初始值是当前组件实例对象。接下来将开启一个 while 循环,用来查找注入数据的工作。
    我们知道 source 是当前组件实例对象,在循环内部有一个 if 条件语句,如下:

    if (source._provided && hasOwn(source._provided, provideKey))
    

    该条件检测了 source._provided 属性是否存在,并且 source._provided 对象自身是否拥有 provideKey 键,如果有则说明找到了注入的数据:source._provided[provideKey],并将它赋值给 result 对象的同名属性。有的同学会问:“source 变量的初始值为当前组件实例对象,那么如果在当前对象下找到了通过 provide 选项提供的值,那岂不是自身给自身注入数据?”。大家不要忘了 inject 选项的初始化是在 provide 选项初始化之前的,也就是说即使该组件通过 provide 选项提供的数据中的确存在 inject 选项注入的数据,也不会有任何影响,因为在 inject 选项查找数据时 provide 提供的数据还没有被初始化,所以当一个组件使用 provide 提供数据时,该数据只有子代组件可用。
    那么如果 if 判断条件为假怎么办?没关系,注意 while 循环的最后一句代码:

    source = source.$parent
    

    重新赋值 source 变量,使其引用父组件,以及类推就完成了向父代组件查找数据的需求,直到找到数据为止。
    但是如果一直找到了根组件,但依然没有找到数据怎么办?
    们知道根组件实例对象的 vm.$parent 属性为 null,所以如上 if 条件语句的判断条件如果成立,说明一直寻找到根组件也没有找到要的数据,此时需要查看 inject[key] 对象中是否定义了 default 选项,如果定义了 default 选项则使用 default 选项提供的数据作为注入的数据,否则在非生产环境下会提示开发者未找到注入的数据。

    最后如果查询到了数据,resolveInject 函数会将 result 作为返回值返回,并且 result 对象的键就是注入数据的名字,result 对象每个键的值就是注入的数据。
    此时我们已经通过 resolveInject 函数取得了注入的数据,并赋值给 result 常量,我们知道 result 常量的值有可能是不存在的,所以需要一个 if 条件语句对 result 进行判断,当条件为真时说明成功取得注入的数据,此时会执行 if 语句块内的代码。在 if 语句块内所做的事情其实很简单:

    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
    

    就是通过遍历 result 常量并调用 defineReactive 函数在当前组件实例对象 vm 上定义与注入名称相同的变量,并赋予取得的值。这里有一个对环境的判断,在非生产环境下调用 defineReactive 函数时会多传递一个参数,即 customSetter,当你尝试设置注入的数据时会提示你不要这么做。

    另外大家也注意到了在使用 defineReactive 函数为组件实例对象定义属性之前,调用了 toggleObserving(false) 函数关闭了响应式定义的开关,之后又将开关开启:toggleObserving(true)。前面我们已经讲到了类似的情况,这么做将会导致使用 defineReactive 定义属性时不会将该属性的值转换为响应式的,所以 Vue 文档中提到了:

    提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

    当然啦,如果父代组件提供的数据本身就是响应式的,即使 defineReactive 不转,那么最终这个数据也还是响应式的。

    相关文章

      网友评论

        本文标题:Vue2.0 源码分析笔记(六)props、methods、pr

        本文链接:https://www.haomeiwen.com/subject/vuplmltx.html