Vue 3.0 Props的初始化和更新流程的细节分析

2021-08-27  本文已影响0人  chonglingliu

Vue.js可以让组件的使用者在组件外部传递props参数,组件拿到这些props的值来实现各种各样的功能。本文我们就来探讨下组件props的初始化和更新流程。

在前一篇文章中,我们知道setup函数的第一个参数是props,本文我们就来了解下props是如何初始化和更新的。

在开始之前我们先弄清两个概念:

Props配置:就是编写组件时写的props属性,描述一个组件的Props的数据类型和默认值等信息。例如组件定义时:props: ['msg']

Props数据:是在使用组件时给组件传递的数据。例如组件使用时:<HelloWorld msg="你好" />

Props的初始化流程

normalizePropsOptions进行Props标准化配置

挂载组件的第一步是调用createComponentInstance来创建组件实例对象,初始化的过程中就实现了标准化props的配置normalizePropsOptions方法:

const instance: ComponentInternalInstance = {
    // 省略...
    propsOptions: normalizePropsOptions(type, appContext),
    // 省略...
  return instance
}

接下来我们就来看看标准化Props配置normalizePropsOptions方法的具体逻辑。

export function normalizePropsOptions(
  comp: ConcreteComponent,
  appContext: AppContext,
  asMixin = false
): NormalizedPropsOptions {
  // 1. 
  const cache = appContext.propsCache
  const cached = cache.get(comp)
  if (cached) {
    return cached
  }

  const raw = comp.props
  const normalized: NormalizedPropsOptions[0] = {}
  const needCastKeys: NormalizedPropsOptions[1] = []

  // 2. 
  let hasExtends = false
  if (__FEATURE_OPTIONS_API__ && !isFunction(comp)) {
    const extendProps = (raw: ComponentOptions) => {
      if (__COMPAT__ && isFunction(raw)) {
        raw = raw.options
      }
      hasExtends = true
      const [props, keys] = normalizePropsOptions(raw, appContext, true)
      extend(normalized, props)
      if (keys) needCastKeys.push(...keys)
    }
    if (!asMixin && appContext.mixins.length) {
      appContext.mixins.forEach(extendProps)
    }
    if (comp.extends) {
      extendProps(comp.extends)
    }
    if (comp.mixins) {
      comp.mixins.forEach(extendProps)
    }
  }

  if (!raw && !hasExtends) {
    cache.set(comp, EMPTY_ARR as any)
    return EMPTY_ARR as any
  }

  // 3
  if (isArray(raw)) {
    for (let i = 0; i < raw.length; i++) {
      const normalizedKey = camelize(raw[I])
      if (validatePropName(normalizedKey)) {
        normalized[normalizedKey] = EMPTY_OBJ
      }
    }
  } else if (raw) {
    // 4
    for (const key in raw) {
      const normalizedKey = camelize(key)
      if (validatePropName(normalizedKey)) {
        const opt = raw[key]
        const prop: NormalizedProp = (normalized[normalizedKey] =
          isArray(opt) || isFunction(opt) ? { type: opt } : opt)
        if (prop) {
          const booleanIndex = getTypeIndex(Boolean, prop.type)
          const stringIndex = getTypeIndex(String, prop.type)
          prop[BooleanFlags.shouldCast] = booleanIndex > -1
          prop[BooleanFlags.shouldCastTrue] =
            stringIndex < 0 || booleanIndex < stringIndex
          // if the prop needs boolean casting or default value
          if (booleanIndex > -1 || hasOwn(prop, 'default')) {
            needCastKeys.push(normalizedKey)
          }
        }
      }
    }
  }

  // 5
  const res: NormalizedPropsOptions = [normalized, needCastKeys]
  cache.set(comp, res)
  return res
}

标准化Props配置的代码解释:

  1. 先从appContext.propsCache中去获取组件对象为key的配置缓存,如果取到了直接返回缓存结果;
  2. 再处理extendsmixins中的Props属性,他们二者的作用是扩展组件的定义,所以需要递归他们定义的Props执行normalizePropsOptions方法,然后将结果放在组件的存储结果中。(从处理来看我们知道extends只能有一个并被优先处理,mixins可以有多个)
  3. 如果Props配置是数组且每个元素是个字符串,则将字符串改为驼峰命名,并我每个key创建一个空对象.
定义:props: ['age','message-id']
结果:propOptions: {age: {}, messageId: {}}
  1. 如果Props配置是对象,则标准化每个不是以$开头的prop的定义。首先把数组和函数转换成对象:{type: prop}。然后判断如果proptype属性中有定义Boolean,则标记为需要转换数据;如果proptype属性中Boolean存在,String不存在或者BooleanString前面,此时标记为需要转换成boolean类型。
定义:props: {name: String, intro: [Boolean, String]}
结果:propOptions.normalized: 
normalized
  1. 如果prop含有default或者类型包含了Boolean,则标记为key的值需要转换,放在needCastKeys中。
propOptions
  1. 将获取到的propOptions缓存到appContext.propsCache中。

initProps设置Props初始化

我们前面的章节有提到在setupComponent设置组件对象的时候会调用initProps(instance, props, isStateful, isSSR)进行Props初始化处理。我们接下来就来介绍下它:

setupComponent

initProps的代码逻辑如下:

initProps

initProps初始化的过程分为四个步骤:

  1. Props进行设值;
  2. 没有传值的Props将其值设置为undefined;
  3. 如果是开发环境,进行Props的验证,给出错误提示;
  4. props变为响应式数据赋值给组件实例对象,将attrs赋值给组件实例对象。

我们接下来逐步分析。

setFullProps设置props的值
setFullProps

为了方便理解,在此给个例子:

demo

设置的流程解释:

  1. 遍历rawProps的值,例子中就是{name: "Lan", address: "北京东城", age: "18", intro: ""};
  2. 如果propsOptions中有rawProps对应的key,如果不需要转换,就直接赋值给props,如果需要转换值,则先暂存到rawCastValues中;如果propsOptions中没有rawProps对应的key,并且不是事件相关的属性,则将其存到attrs中。
props = {address: "北京东城"};
attrs = {age: 18};
rawCastValues = {intro: "", name: "Lan"};
  1. 通过resolvePropValue进行转换:
props = {address: "北京东城",intro: true,name: "Lan"};
attrs = {age: 18};
validateProps验证props值的合法性
validateProps

验证合法性主要有如下几个规则:

  1. 如果必须传值,但是没有传值就报警告
  2. 如果不必须传值,传值为null, 有效返回
  3. 如果类型不匹配就报警告
  4. 如果validator, 传入的值验证不通过报警告
props设置成浅响应式对象和赋值
instance.props = shallowReactive(props)
instance.props = props
instance.attrs = attrs

这一步很好理解。shallowReactive表示只监测props的变化,内部属性的变化不会被监测。

留个小问题:props为什么要设置成响应式呢?

至此,prop的初始化就完成了。在后面的执行setup函数中就能将props对象传递给子组件了。

Props更新流程

更新Props触发的渲染流程

Props是在父组件定义然后传给子组件的,所以Props值的变化会父组件的重新渲染。父组件的重新渲染会触发patch,然后当更新子组件的时候触发updateComponent流程,由于props变化了,hasPropsChanged会返回true,也就是说shouldUpdateComponenttrue,此时给子组件的next设置为新的VNode,然后执行子组件的重新渲染。

const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  if (shouldUpdateComponent(n1, n2, optimized)) {
    
    // normal update
    instance.next = n2
    // in case the child component is also queued, remove it to avoid
    // double updating the same child component in the same flush.
    invalidateJob(instance.update)
    // instance.update is the reactive effect.
    instance.update()

  }
}

export function shouldUpdateComponent(
    prevVNode: VNode,
    nextVNode: VNode,
    optimized?: boolean
  ): boolean {
    const { props: prevProps, children: prevChildren, component } = prevVNode
    const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
    // 省略...
    return hasPropsChanged(prevProps, nextProps, emits)
}

当子组件重新渲染的时候会执行componentUpdateFn方法,且此时新的VNode的next有值,触发的
updateComponentPreRender流程中会调用updateProps方法更新子组件实例对象的props

const updateComponentPreRender = (
  instance: ComponentInternalInstance,
  nextVNode: VNode,
  optimized: boolean
) => {
  nextVNode.component = instance
  const prevProps = instance.vnode.props
  instance.vnode = nextVNode
  instance.next = null
  // 更新props
  updateProps(instance, nextVNode.props, prevProps, optimized)
}

更新props后再执行对subTree VNode执行patch进行更新子组件。

字不如图

这里我们回过头来看看pros是如何更新的。

updateProps更新的具体细节

它的主要目标就是把父组件渲染时得到的新值更新到子组件的实例对象的props中。

Vue的编译阶段可以知道组件的VNodePatchFlags

这里把可略过的代码附上:

export function updateProps(
  instance: ComponentInternalInstance,
  rawProps: Data | null,
  rawPrevProps: Data | null,
  optimized: boolean
) {
  const {
    props,
    attrs,
    vnode: { patchFlag }
  } = instance
  const rawCurrentProps = toRaw(props)
  const [options] = instance.propsOptions
  let hasAttrsChanged = false

  if (
    // always force full diff in dev
    // - #1942 if hmr is enabled with sfc component
    // - vite#872 non-sfc component used by sfc component
    !(
      __DEV__ &&
      (instance.type.__hmrId ||
        (instance.parent && instance.parent.type.__hmrId))
    ) &&
    (optimized || patchFlag > 0) &&
    !(patchFlag & PatchFlags.FULL_PROPS)
  ) {
    if (patchFlag & PatchFlags.PROPS) {
      // Compiler-generated props & no keys change, just set the updated
      // the props.
      const propsToUpdate = instance.vnode.dynamicProps!
      for (let i = 0; i < propsToUpdate.length; i++) {
        let key = propsToUpdate[I]
        // PROPS flag guarantees rawProps to be non-null
        const value = rawProps![key]
        if (options) {
          // attr / props separation was done on init and will be consistent
          // in this code path, so just check if attrs have it.
          if (hasOwn(attrs, key)) {
            if (value !== attrs[key]) {
              attrs[key] = value
              hasAttrsChanged = true
            }
          } else {
            const camelizedKey = camelize(key)
            props[camelizedKey] = resolvePropValue(
              options,
              rawCurrentProps,
              camelizedKey,
              value,
              instance,
              false /* isAbsent */
            )
          }
        } else {
          if (__COMPAT__) {
            if (isOn(key) && key.endsWith('Native')) {
              key = key.slice(0, -6) // remove Native postfix
            } else if (shouldSkipAttr(key, instance)) {
              continue
            }
          }
          if (value !== attrs[key]) {
            attrs[key] = value
            hasAttrsChanged = true
          }
        }
      }
    }
  } else {
    // full props update.
    if (setFullProps(instance, rawProps, props, attrs)) {
      hasAttrsChanged = true
    }
    // in case of dynamic props, check if we need to delete keys from
    // the props object
    let kebabKey: string
    for (const key in rawCurrentProps) {
      if (
        !rawProps ||
        // for camelCase
        (!hasOwn(rawProps, key) &&
          // it's possible the original props was passed in as kebab-case
          // and converted to camelCase (#955)
          ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
      ) {
        if (options) {
          if (
            rawPrevProps &&
            // for camelCase
            (rawPrevProps[key] !== undefined ||
              // for kebab-case
              rawPrevProps[kebabKey!] !== undefined)
          ) {
            props[key] = resolvePropValue(
              options,
              rawCurrentProps,
              key,
              undefined,
              instance,
              true /* isAbsent */
            )
          }
        } else {
          delete props[key]
        }
      }
    }

    if (attrs !== rawCurrentProps) {
      for (const key in attrs) {
        if (!rawProps || !hasOwn(rawProps, key)) {
          delete attrs[key]
          hasAttrsChanged = true
        }
      }
    }
  }
}

提出一个问题

在传递动态的数据时,我们会在属性前面加上冒号:, 譬如下面的:address="address", 这个冒号:的作用是什么?我们进行标准化Props配置时候是对address做的处理时为什么没有这个冒号:做处理呢?

<HelloWorld name="Lan" :address="address" age="18" intro="" />

上一篇 下一篇

猜你喜欢

热点阅读