Gahing's blog Gahing's blog
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Gahing / francecil

To be best
首页
知识体系
  • 前端基础
  • 应用框架
  • 工程能力
  • 应用基础
  • 专业领域
  • 业务场景
  • 前端晋升 (opens new window)
  • Git
  • 网络基础
  • 算法
  • 数据结构
  • 编程范式
  • 编解码
  • Linux
  • AIGC
  • 其他领域

    • 客户端
    • 服务端
    • 产品设计
软素质
  • 面试经验
  • 人生总结
  • 个人简历
  • 知识卡片
  • 灵感记录
  • 实用技巧
  • 知识科普
  • 友情链接
  • 美食推荐 (opens new window)
  • 收藏夹

    • 优质前端信息源 (opens new window)
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • 前端基础

  • 应用框架

    • UI 框架

      • Angular

      • React

      • Solid

      • Svelte

      • Vue

        • Vue 项目中的 data-v-xxx 是怎么生成的
        • Vue之从零编写一个ContextMenu(右键菜单)插件
        • Vue 第一个组件,浏览器后退无法触发beforeRouteLeave的问题与解决
        • Vue问题记录
        • vue 中 updated 的执行时机
        • 源码解析

          • Vue 源码解析-0.前置准备
          • Vue实例化过程
          • Vue响应式原理
          • Vue响应式之data
          • Vue响应式之props
            • 前言
            • 源码分析
            • 总结
          • Vue响应式之computed
          • Vue响应式之watch
          • Vue响应式之异步更新与nextTick
          • Vue响应式之组件更新
      • 框架本质

    • 开发框架

    • 组件库

  • 工程能力

  • 应用基础

  • 专业领域

  • 业务场景

  • 大前端
  • 应用框架
  • UI 框架
  • Vue
  • 源码解析
gahing
2019-11-13
目录

Vue响应式之props

# 前言

props 与 data 的响应式处理类似,源码分析主要分析与 data 处理的不同点,最后指出一些处理细节

在分析之前,我们先提出几个问题:

  1. props 支持多种类型,是在哪进行参数校验的
  2. Vue 实例和 Vue 组件在 prop 上的处理有哪些不同
  3. 内部对 prop 的修改会影响到外部么
  4. 同一组件多次实例化, prop 属性会进行多次响应式设置么

# 源码分析

首先是 props 参数校验,传入的 props 选项经过转换被挂载到 vm.$options 上

无论是实例还是组件,最后都是调用的该方法

// src\core\util\options.js

/**
 * 确保所有的 props 选项被转换为基于对象的格式
 */
function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name
  // 数组形式的处理,选项只支持字符串
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        // 转为驼峰式命名
        name = camelize(val)
        // 默认类型为 null
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    // 若为纯对象,遍历键值对
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      // 可以看到,这里不会去判断 val 是否合法
      // val 非纯对象时会被设置为类型
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    // 只支持数组和对象
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

对于 Vue 实例来说,该方法在执行 _init 时 mergeOptions 方法中调用,props 最终挂载在 vm.$options 上

对于 Vue 组件,也是在实例化的时候处理的么?

试想一下,组件一旦定义完成, props 应该是不会再变了,不随实例化处理,所以最好应该定义在定义构造函数时就处理好,并把 props 挂载在原型

我们验证一下,找到全局组件的构造函数(根据 1.Vue 实例化过程)

// src\core\global-api\extend.js

Vue.extend = function (extendOptions: Object): Function {
  const Super = this
  // VueComponent 继承 Vue
  const Sub = function VueComponent (options) {
    this._init(options)
  }
  Sub.prototype = Object.create(Super.prototype)
  Sub.prototype.constructor = Sub
  // 合并参数
  Sub.options = mergeOptions(
    Super.options,
    extendOptions
  )
  Sub['super'] = Super

  // 一些优化,可以跳过

  // 对于 props 和 computed 属性,在拓展的原型上定义 proxy getters
  // 避免为每个创建的实例调用 Object.defineProperty
  if (Sub.options.props) {
    initProps(Sub)
  }
  if (Sub.options.computed) {
    initComputed(Sub)
  }
  // Sub 静态方法复用 Vue 的
  Sub.extend = Super.extend
  // ...
  
  return Sub
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

可以看到,当我们进行组件定义的时候,如

Vue.component('select2', {
  props: ['options', 'value'],
  template: '#select2-template',
})
1
2
3
4

会调用 mergeOptions 方法进行 options 合并和设置

Sub.options = mergeOptions(
  Super.options,
  extendOptions
)
1
2
3
4

所以 props 会被挂载到 VueComponent 原型的 options 上


因此第一个问题 -- props 支持多种类型,是在哪进行参数校验的? -- 我们解答了一半,props 定义会进行转换,其在 normalizeProps 中执行,我们稍后再看下传入 props 时会进行怎样的校验。 ``

同时我们也初步解决了问题2和问题4,对于 Vue 组件,我们将 props 定义在原型上,可以避免每个实例调用 Object.defineProperty 进行响应式设置,具体的稍后再分析。


无论 Vue 还是 Vue 组件,实例化的时候都会执行 this._init(options)

对于 Vue , options 值由我们显式传入,

对于 Vue 组件, options 值由解析模块解析后生成

// src\core\vdom\create-component.js

export function createComponentInstanceForVnode (
  vnode: any, // we know it's MountedComponentVNode but flow doesn't
  parent: any, // activeInstance in lifecycle state
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // check inline-template render functions
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

也就是说,参数有

parent: {...}
_isComponent: true
_parentVnode: {...}
1
2
3

不进行展开分析,只知道 Vue 组件实例的 $options 在 initInternalComponent 方法中设置

export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  const opts = vm.$options = Object.create(vm.constructor.options)
  // doing this because it's faster than dynamic enumeration.
  const parentVnode = options._parentVnode
  opts.parent = options.parent
  opts._parentVnode = parentVnode

  const vnodeComponentOptions = parentVnode.componentOptions
  opts.propsData = vnodeComponentOptions.propsData
  opts._parentListeners = vnodeComponentOptions.listeners
  opts._renderChildren = vnodeComponentOptions.children
  opts._componentTag = vnodeComponentOptions.tag

  if (options.render) {
    opts.render = options.render
    opts.staticRenderFns = options.staticRenderFns
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

两者都会调用 initProps

// src\core\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)
    
    // ... 省略开发模式的一些处理
    defineReactive(props, key, value)

    // 对 vm[key] 的访问和修改是作用到 vm['_props'][key] 上
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  toggleObserving(true)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

propsData 为该实例 props 的值, 由 options 选项传入(Vue 实例) 或组件实例化时父组件传入

isRoot 用来判断是 Vue 实例还是 Vue 组件

若为 Vue 组件,会执行 toggleObserving(false) ,作用是 observe(value) 不进行处理

继续往下分析, 对 propsOptions( props 定义,其值为 $options.props ) 进行处理

validateProp 会在没有传入值的情况下返回默认值

export function validateProp (
  // prop 属性
  key: string,
  // props 类型定义和其他选项
  propOptions: Object,
  // prop 值
  propsData: Object,
  vm?: Component
): any {
  // prop 定义
  const prop = propOptions[key]
  // 判断 propsData 是否传入 key 相应的值
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]

  const booleanIndex = getTypeIndex(Boolean, prop.type)
  // prop 定义的类型中有 Boolean 类型
  if (booleanIndex > -1) {
    // propOptions 未传入且未定义默认值,取值为 false
    if (absent && !hasOwn(prop, 'default')) {
      value = false
      // propOptions 传入了空字符串或 value 为 key 的连字格式 a-b-c === ABC
    } else if (value === '' || value === hyphenate(key)) {
      
      // 如果布尔值类型优先级更高(索引更小),则转为布尔值
      const stringIndex = getTypeIndex(String, prop.type)
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true
      }
    }
  }
  // 若 propsData[key] 值未定义且不支持布尔类型
  if (value === undefined) {
    // 设置为 default 属性对应的值,
    // 对于 default 值为方法的会进行调用并返回值
    value = getPropDefaultValue(vm, prop, key)
    
    // 因为默认值每次都是重新拷贝的,需要进行 observe 处理
    const prevShouldObserve = shouldObserve
    toggleObserving(true)
    observe(value)
    toggleObserving(prevShouldObserve)
  }
  // 开发模式下会通过 type 和 validator 校验 props
  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
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

注意几点:

  1. 迭代是根据定义来的,也就是说, 传入了 props 未定义的元素,是不会进行处理的
  2. 设置值:propsData 没传入的且定义支持 Boolean 类型且没有定义 default 会设置值为 false ; propsData 传入了某种字符串且定义支持 Boolean 类型且 Boolean 类型优先级更高会设置值为 true ; propsData 没传入的其他情况会使用 default 参数设置值
  3. 通过 default 设置的值 value 为对象时,执行 observe(value)
  4. value 存在值时,不会进行 observe
  5. 通过 assertProp 校验 props
  6. 连字格式的特殊处理,暂时不知道为什么会有这个处理,这里 mark 一下
  props: {
    'a-b-c':{
      type:[Boolean]
    }
  },
  propsData:{
    'a-b-c': 111
  },
  // 'a-b-c' 属性变转为驼峰式 aBC, $props['aBC'] = false
1
2
3
4
5
6
7
8
9

其中第5点回答了问题1:在开发模式下通过定义的 type 和 validator 进行 prop 校验,但是生产模式不会进行处理


回到 initProps , 继续执行 defineReactive(props, key, value) , 对 prop 进行响应式处理

以下面这个为例子分析不同情况的处理:

props: {
  info:{
    default:function(){
      return { name: "gahing" }
    }
  },
  user:null,
}
propsData: {
  user:{
    name: "test"
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

info 值通过 default 生成,于是会执行 observe({name:'gahing'}) ;
之后调用 defineReactive(props, 'info', { name: "gahing" }) 定义了 info 的取值赋值,并挂载到 props 上;
childOb = observe({ name: "gahing" }) , 由于前面对该 val 进行响应式设置,因此这里返回的是 val.__ob__

结论:通过 default 生成的对象,会进行深度的响应式处理

user 值由 propsData 传入,validateProp 中不会执行 observe
之后调用 defineReactive(props, 'user', { name: "test" }) 定义了 user 的取值赋值,并挂载到 props 上;
childOb = observe({ name: "test" }) , 对于 vue 组件,由于 shouldObserve = false 因此不会进行 childOb 的处理: childOb == undefined ,对于 vue 实例,则会进行深度的响应式处理

这其中的奥妙在于组件的 props 依赖于外部元素传入,而外部元素已经进行深度的响应式处理了
当 prop 选项子孙元素值变更,会通知所有用到该 prop 子元素的观察者进行更新
当 prop 选项值变更(包括使用默认值到赋值),会通过 defineReactive 执行时定义的闭包 dep 去通知观察者更新


于是,这也就回答问题4 -- 同一组件多次实例化, prop 属性会进行多次响应式设置么

答案是不一定,通过默认值生成的,会进行响应式设置,其他通过外部传入值的,不会进行设置,依赖于该值一开始的设置


那么 props 的执行过程大概就这样,我们最后分析下这个问题:内部对 prop 的修改会影响到外部么?

这个问题我们应该经常遇到过,文档告诉我们在组件中 prop 只能用来取值,不能进行赋值,那源码中是如何处理的呢?

还记得响应式设置的时候有个 customSetter 参数么?

// src\core\observer\index.js

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // ... 省略部分代码
  Object.defineProperty(obj, key, {
    // ... 省略部分代码
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      // ... 省略部分代码
      
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }

      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

开发模式时会调用 customSetter 方法

这里说个小技巧,怎么确定是 customSetter 进行处理的呢?开发模式下对 props 进行赋值,看控制台报错信息的执行路径就知道了

想起来刚刚分析 initProps 被我忽略的一个代码处理

// src\core\instance\state.js - initProps

if (process.env.NODE_ENV !== 'production') {
  // 不能定义为 key,ref,slot,slot-scope,is
  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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

可以看到,在开发模式下, !isRoot && !isUpdatingChildComponent 的条件下会进行警告:避免修改 props 的值防止父组件重渲染

Vue 实例上的 props 修改不会有提示
isUpdatingChildComponent 在更新子组件时也不会提示,暂未知道其中原因

不过,不管什么模式什么情况下,值的修改都是会成功的

# 总结

我们在分析源码的时候已经对几个问题进行了解答,其中第二点关于 Vue 实例和 Vue 组件在 props 上处理的不同,也说出了几点,但还不够完全,主要是关于 Vue 组件化这块还不是特别清晰,以及组件在参数合并下有什么性能优化的地方,我们将在之后的 Vue 组件化文章中进行补充

最后,在抛一个问题,在上面的分析中, isRoot 我们说是 Vue 实例,反之为 Vue 组件,这个说法正确么?

编辑 (opens new window)
上次更新: 2024/09/01, 23:56:56
Vue响应式之data
Vue响应式之computed

← Vue响应式之data Vue响应式之computed→

最近更新
01
浅谈代码质量与量化指标
08-27
02
快速理解 JS 装饰器
08-26
03
Vue 项目中的 data-v-xxx 是怎么生成的
09-19
更多文章>
Theme by Vdoing | Copyright © 2016-2024 Gahing | 闽ICP备19024221号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式