Vue3探秘系列— Props:初始化与更新流程(十)

102 阅读12分钟

前言

Vue3探秘系列文章链接:

不止响应式:Vue3探秘系列— 虚拟结点vnode的页面挂载之旅(一)

不止响应式:Vue3探秘系列— 组件更新会发生什么(二)

不止响应式:Vue3探秘系列— diff算法的完整过程(三)

不止响应式:Vue3探秘系列— 组件的初始化过程(四)

终于轮到你了:Vue3探秘系列— 响应式设计(五)

计算属性:Vue3探秘系列— computed的实现原理(六)

侦听属性:Vue3探秘系列— watch的实现原理(七)

生命周期:Vue3探秘系列— 钩子函数的执行过程(八)

依赖注入:Vue3探秘系列— provide 与 inject 的实现原理(九)

Vue3探秘系列— Props:初始化与更新流程(十)

Vue3探秘系列— directive:指令的实现原理(十一)

Hello~大家好。我是秋天的一阵风

在构建现代前端应用时,组件化开发已成为主流趋势,而在Vue.js这类框架中,组件之间的通信至关重要, props作为父组件向子组件传递数据的主要方式之一,其重要性不言而喻。

Props 在 Vue.js 中扮演着关键角色。它们允许数据从父组件向下流动到子组件,从而实现了组件间的通信。无论是简单的文本还是复杂的对象结构,props 都可以轻松地将这些数据传递给子组件,并确保数据流的单一方向性,这有助于维护应用的状态一致性。

本文将深入探讨 props 的初始化和更新流程,以及这些流程背后的机制。

一、案例代码

为了让你更直观地理解,我们来举个例子,假设有这样一个 BlogPost 组件,它是这样定义的:

<div class="blog-post">
  <h1>{{title}}</h1>
  <p>author: {{author}}</p>
</div>
<script>
  export default {
    props: {
      title: String,
      author: String
    }
  }
</script>

然后我们在父组件使用这个 BlogPost 组件的时候,可以给它传递一些 Props 数据:

<blog-post title="Vue3 publish" author="yyx"></blog-post>

从最终结果来看,BlogPost 组件会渲染传递的 titleauthor 数据。

二、初始化

不知道同学们还记不记得,在之前介绍 setup初始化流程时有一个setupComponent函数,在这个函数里面会调用 initProps 方法。

function setupComponent(instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode;
  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & 4;
  // 初始化 props
  initProps(instance, props, isStateful, isSSR);
  // 初始化插槽
  initSlots(instance, children);
  // 设置有状态的组件实例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined;
  return setupResult;
}

Props 的初始化,就是通过 initProps 方法来完成的,我们来看一下它的实现:

function initProps(instance, rawProps, isStateful, isSSR = false) {
  const props = {};
  const attrs = {};
  def(attrs, InternalObjectKey, 1);
  // 设置 props 的值
  setFullProps(instance, rawProps, props, attrs);
  // 验证 props 合法
  if (process.env.NODE_ENV !== "production") {
    validateProps(props, instance.type);
  }
  if (isStateful) {
    // 有状态组件,响应式处理
    instance.props = isSSR ? props : shallowReactive(props);
  } else {
    // 函数式组件处理
    if (!instance.type.props) {
      instance.props = attrs;
    } else {
      instance.props = props;
    }
  }
  // 普通属性赋值
  instance.attrs = attrs;
}

从注释中可以看到,initProps中主要做了四件事,分别是:设置 props 的值,验证 props 是否合法,把 props 变成响应式,以及添加到实例 instance.props 上。

(1)设置 props

function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置
  const [options, needCastKeys] = normalizePropsOptions(instance.type);
  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key];
      // 一些保留的 prop 比如 ref、key 是不会传递的
      if (isReservedProp(key)) {
        continue;
      }
      // 连字符形式的 props 也转成驼峰形式
      let camelKey;
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value;
      } else if (!isEmitListener(instance.type, key)) {
        // 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
        attrs[key] = value;
      }
    }
  }
  if (needCastKeys) {
    // 需要做转换的 props
    const rawCurrentProps = toRaw(props);
    for (let i = 0; i < needCastKeys.length; i++) {
      const key = needCastKeys[i];
      props[key] = resolvePropValue(
        options,
        rawCurrentProps,
        key,
        rawCurrentProps[key]
      );
    }
  }
}

请注意:instance 表示组件实例;rawProps 表示原始的 props 值,也就是创建 vnode 过程中传入的 props 数据;props 用于存储解析后的 props 数据;attrs 用于存储解析后的普通属性数据。

normalizePropsOptions: 将props处理成标准格式

function normalizePropsOptions(comp) {
  // comp.__props 用于缓存标准化的结果,有缓存,则直接返回
  if (comp.__props) {
    return comp.__props;
  }
  const raw = comp.props;
  const normalized = {};
  const needCastKeys = [];
  // 处理 mixins 和 extends 这些 props
  let hasExtends = false;
  if (!shared.isFunction(comp)) {
    const extendProps = (raw) => {
      const [props, keys] = normalizePropsOptions(raw);
      shared.extend(normalized, props);
      if (keys) needCastKeys.push(...keys);
    };
    if (comp.extends) {
      hasExtends = true;
      extendProps(comp.extends);
    }
    if (comp.mixins) {
      hasExtends = true;
      comp.mixins.forEach(extendProps);
    }
  }
  if (!raw && !hasExtends) {
    return (comp.__props = shared.EMPTY_ARR);
  }
  // 数组形式的 props 定义
  if (shared.isArray(raw)) {
    for (let i = 0; i < raw.length; i++) {
      if (!shared.isString(raw[i])) {
        warn(`props must be strings when using array syntax.`, raw[i]);
      }
      const normalizedKey = shared.camelize(raw[i]);
      if (validatePropName(normalizedKey)) {
        normalized[normalizedKey] = shared.EMPTY_OBJ;
      }
    }
  } else if (raw) {
    if (!shared.isObject(raw)) {
      warn(`invalid props options`, raw);
    }
    for (const key in raw) {
      const normalizedKey = shared.camelize(key);
      if (validatePropName(normalizedKey)) {
        const opt = raw[key];
        // 标准化 prop 的定义格式
        const prop = (normalized[normalizedKey] =
          shared.isArray(opt) || shared.isFunction(opt) ? { type: opt } : opt);
        if (prop) {
          const booleanIndex = getTypeIndex(Boolean, prop.type);
          const stringIndex = getTypeIndex(String, prop.type);
          prop[0 /* shouldCast */] = booleanIndex > -1;
          prop[1 /* shouldCastTrue */] =
            stringIndex < 0 || booleanIndex < stringIndex;
          // 布尔类型和有默认值的 prop 都需要转换
          if (booleanIndex > -1 || shared.hasOwn(prop, "default")) {
            needCastKeys.push(normalizedKey);
          }
        }
      }
    }
  }
  const normalizedEntry = [normalized, needCastKeys];
  comp.__props = normalizedEntry;
  return normalizedEntry;
}
  1. 首先会处理 mixinsextends 这两个特殊的属性,因为它们的作用都是扩展组件的定义,所以需要对它们定义中的 props 递归执行 normalizePropsOptions

  2. 接着处理数组形式的props,数组形式的props必须是字符串,然后将字符串变成驼峰形式的key,并为这些key创建一个EMPTY_OBJ空对象

  3. 处理对象形式的props

  4. 对含有布尔类型和有默认值的prop转换

  5. 最后,返回标准化结果 normalizedEntry,它包含标准化后的 props 定义 normalized,以及需要转换的 props key needCastKeys,并且用 comp.\_\_props 缓存这个标准化结果,如果对同一个组件重复执行 normalizePropsOptions,直接返回这个标准化结果即可。

遍历 props 数据求值

function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置
  if (rawProps) {
    for (const key in rawProps) {
      const value = rawProps[key];
      // 一些保留的 prop 比如 ref、key 是不会传递的
      if (isReservedProp(key)) {
        continue;
      }
      // 连字符形式的 props 也转成驼峰形式
      let camelKey;
      if (options && hasOwn(options, (camelKey = camelize(key)))) {
        props[camelKey] = value;
      } else if (!isEmitListener(instance.type, key)) {
        // 非事件派发相关的,且不在 props 中定义的普通属性用 attrs 保留
        attrs[key] = value;
      }
    }
  }

  // 转换需要转换的 props
}

如果 rawProps 中的 prop 在配置中定义了,那么把它的值赋值到props对象中。

如果不是,那么判断这个 key 是否为非事件派发相关,如果是那么则把它的值赋值到 attrs 对象中。另外,在遍历的过程中,遇到 keyref 这种 key,则直接跳过。

对需要转换的 props 求值

function setFullProps(instance, rawProps, props, attrs) {
  // 标准化 props 的配置

// 遍历 props 数据求值
if (needCastKeys) {
	// 需要做转换的 props
	const rawCurrentProps = toRaw(props);
     for (let i = 0; i < needCastKeys.length; i++) {
		const key = needCastKeys[i] props[key] = resolvePropValue(options, rawCurrentProps, key, rawCurrentProps[key])
	}
}
}
function resolvePropValue(options, props, key, value) {
  const opt = options[key];
  if (opt != null) {
    const hasDefault = hasOwn(opt, "default");
    // 默认值处理
    if (hasDefault && value === undefined) {
      const defaultValue = opt.default;
      value =
        opt.type !== Function && isFunction(defaultValue)
          ? defaultValue()
          : defaultValue;
    }
    // 布尔类型转换
    if (opt[0 /* shouldCast */]) {
      if (!hasOwn(props, key) && !hasDefault) {
        value = false;
      } else if (
        opt[1 /* shouldCastTrue */] &&
        (value === "" || value === hyphenate(key))
      ) {
        value = true;
      }
    }
  }
  return value;
}
  1. 遍历 needCastKeys 数组进行求值
  2. resolvePropValue 中主要是对有默认值布尔类型的情况进行处理

(2) 验证 props 是否合法 : validateProps

function initProps(instance, rawProps, isStateful, isSSR = false) {
  const props = {}
  // 设置 props 的值
// 验证 props 合法
    if ((process.env.NODE_ENV !== ‘production’)) {
        validateProps(props, instance.type)
    }
}
function validateProps(props, comp) {
  const rawValues = toRaw(props);
  const options = normalizePropsOptions(comp)[0];
  for (const key in options) {
    let opt = options[key];
    if (opt == null) continue;
    validateProp(key, rawValues[key], opt, !hasOwn(rawValues, key));
  }
}
function validateProp(name, value, prop, isAbsent) {
  const { type, required, validator } = prop;
  // 检测 required
  if (required && isAbsent) {
    warn('Missing required prop: "' + name + '"');
    return;
  }
  // 虽然没有值但也没有配置 required,直接返回
  if (value == null && !prop.required) {
    return;
  }
  // 类型检测
  if (type != null && type !== true) {
    let isValid = false;
    const types = isArray(type) ? type : [type];
    const expectedTypes = [];
    // 只要指定的类型之一匹配,值就有效
    for (let i = 0; i < types.length && !isValid; i++) {
      const { valid, expectedType } = assertType(value, types[i]);
      expectedTypes.push(expectedType || "");
      isValid = valid;
    }
    if (!isValid) {
      warn(getInvalidTypeMessage(name, value, expectedTypes));
      return;
    }
  }
  // 自定义校验器
  if (validator && !validator(value)) {
    warn(
      'Invalid prop: custom validator check failed for prop "' + name + '".'
    );
  }
}

校验主要是三个方面:是否必填type类型是否符合是否通过自定义校验器

关于自定义校验器,你可以在官网这里学习 Props

(3) 将props 变成响应式 :shallowReactive

function initProps(instance, rawProps, isStateful, isSSR = false) {
  // 设置 props 的值
  // 验证 props 合法
  if (isStateful) {
    // 有状态组件,响应式处理
    instance.props = isSSR ? props : shallowReactive(props);
  } else {
    // 函数式组件处理
    if (!instance.type.props) {
      instance.props = attrs;
    } else {
      instance.props = props;
    }
  }
  // 普通属性赋值
  instance.attrs = attrs;
}

(4) 添加到实例 instance.props

instance.props = props;

三、Props的更新

当Props数据发生改变的时候会发生什么事呢?答案很简单,就是触发组件的重新渲染,以我们之前的例子来说,不仅引用了BlogPost的父组件会重新渲染,子组件BlogPost自身也会重新渲染。

那么问题来了,父组件被重新渲染这个好理解,因为我们在父组件的模板中引用了props变量,也就是title和author。那子组件是如何被触发重新渲染的呢?

(1)updateComponent

在之前的文章中不止响应式:Vue3探秘系列— 组件更新会发生什么(二)提到,组件的重新渲染会触发patch过程,也就是根据新旧子树进行patchpatch方法里面如果又遇到了组件节点,就会进入updateComponent方法

const updateComponent = (n1, n2, parentComponent, optimized) => {
  const instance = (n2.component = n1.component);
  // 根据新旧子组件 vnode 判断是否需要更新子组件
  if (shouldUpdateComponent(n1, n2, parentComponent, optimized)) {
    // 新的子组件 vnode 赋值给 instance.next
    instance.next = n2;
    // 子组件也可能因为数据变化被添加到更新队列里了,移除它们防止对一个子组件重复更新
    invalidateJob(instance.update);
    // 执行子组件的副作用渲染函数
    instance.update();
  } else {
    // 不需要更新,只复制属性
    n2.component = n1.component;
    n2.el = n1.el;
  }
};

shouldUpdateComponenttrue 时,会将新的子组件 vnode 赋值给 instance.next,然后执行 instance.update 触发子组件的重新渲染。

但是问题又来了,子组件实例在重新渲染的时候,会拿instance.props 的数据进行渲染,如果这个数据还是旧的,那还是渲染了旧数据,这毫无意义。所以我们还要知道instance.props是如何更新的。

(2)setupRenderEffect

执行instance.update 函数,实际上是执行 componentEffect 组件副作用渲染函数:

const setupRenderEffect = (
 instance,
 initialVNode,
 container,
 anchor,
 parentSuspense,
 isSVG,
 optimized
) => {
 // 创建响应式的副作用渲染函数
 instance.update = effect(function componentEffect() {
   if (!instance.isMounted) {
     // 渲染组件
   } else {
     // 更新组件
     let { next, vnode } = instance;
     // next 表示新的组件 vnode
     if (next) {
       // 更新组件 vnode 节点信息
       updateComponentPreRender(instance, next, optimized);
     } else {
       next = vnode;
     }
     // 渲染新的子树 vnode
     const nextTree = renderComponentRoot(instance);
     // 缓存旧的子树 vnode
     const prevTree = instance.subTree;
     // 更新子树 vnode
     instance.subTree = nextTree;
     // 组件更新核心逻辑,根据新旧子树 vnode 做 patch
     patch(
       prevTree,
       nextTree,
       // 如果在 teleport 组件中父节点可能已经改变,所以容器直接找旧树 DOM 元素的父节点
       hostParentNode(prevTree.el),
       // 参考节点在 fragment 的情况可能改变,所以直接找旧树 DOM 元素的下一个节点
       getNextHostNode(prevTree),
       instance,
       parentSuspense,
       isSVG
     );
     // 缓存更新后的 DOM 节点
     next.el = nextTree.el;
   }
 }, prodEffectOptions);
};

在更新组件的时候,会判断是否有 instance.next,它代表新的组件 vnode,根据前面的逻辑next不为空,所以会执行 updateComponentPreRender 更新组件vnode 节点信息,我们来看一下它的实现:

(3)updateComponentPreRender

const updateComponentPreRender = (instance, nextVNode, optimized) => {
  nextVNode.component = instance;
  const prevProps = instance.vnode.props;
  instance.vnode = nextVNode;
  instance.next = null;
  updateProps(instance, nextVNode.props, prevProps, optimized);
  updateSlots(instance, nextVNode.children);
};

这里面 updateProps就是关键。

(4) updateProps

function updateProps(instance, rawProps, rawPrevProps, optimized) {
  const {
    props,
    attrs,
    vnode: { patchFlag },
  } = instance;
  const rawCurrentProps = toRaw(props);
  const [options] = normalizePropsOptions(instance.type);
  if ((optimized || patchFlag > 0) && !((patchFlag & 16) /* FULL_PROPS */)) {
    if (patchFlag & 8 /* PROPS */) {
      // 只更新动态 props 节点
      const propsToUpdate = instance.vnode.dynamicProps;
      for (let i = 0; i < propsToUpdate.length; i++) {
        const key = propsToUpdate[i];
        const value = rawProps[key];
        if (options) {
          if (hasOwn(attrs, key)) {
            attrs[key] = value;
          } else {
            const camelizedKey = camelize(key);
            props[camelizedKey] = resolvePropValue(
              options,
              rawCurrentProps,
              camelizedKey,
              value
            );
          }
        } else {
          attrs[key] = value;
        }
      }
    }
  } else {
    // 全量 props 更新
    setFullProps(instance, rawProps, props, attrs);
    // 因为新的 props 是动态的,把那些不在新的 props 中但存在于旧的 props 中的值设置为 undefined
    let kebabKey;
    for (const key in rawCurrentProps) {
      if (
        !rawProps ||
        (!hasOwn(rawProps, key) &&
          ((kebabKey = hyphenate(key)) === key || !hasOwn(rawProps, kebabKey)))
      ) {
        if (options) {
          if (
            rawPrevProps &&
            (rawPrevProps[key] !== undefined ||
              rawPrevProps[kebabKey] !== undefined)
          ) {
            props[key] = resolvePropValue(
              options,
              rawProps || EMPTY_OBJ,
              key,
              undefined
            );
          }
        } else {
          delete props[key];
        }
      }
    }
  }
  if (process.env.NODE_ENV !== "production" && rawProps) {
    validateProps(props, instance.type);
  }
}

updateProps 主要的目标就是把父组件渲染时求得的 props 新值,更新到子组件实例的 instance.props 中。

在编译阶段,我们除了捕获一些动态 vnode,也捕获了动态的 props,所以我们可以只去比对动态的 props 数据更新。

当然,如果不满足优化的条件,我们也可以通过setFullProps去全量比对更新 props,并且,由于新的 props 可能是动态的,因此会把那些不在新props中但存在于旧 props 中的值设置为 undefined

四、为什么 instance.props 需要变成响应式呢?

假设有这么一个场景,定义一个父组件parent和子组件Child,子组件Child接收的一个props,变量为count。

import { ref, h, defineComponent, watchEffect } from "vue";
const count = ref(0);
let dummy;
const Parent = {
  render: () => h(Child, { count: count.value }),
};
const Child = defineComponent({
  props: { count: Number },
  setup(props) {
    watchEffect(() => {
      dummy = props.count;
    });
    return () => h("div", props.count);
  },
});
count.value++;

在子组件Child中,不仅渲染模板用到了count,还使用watchEffect注册了一个函数,函数内依赖了props.count。

假如我们想要的效果是,当count被修改时,这个回调函数也会执行,那么前提条件就是count必须是响应式数据

所以答案很简单,将instance.props设置为响应式数据就是为了让我们可以在子组件中监听props的变化

那么问题又来了,为什么要使用 shallowReactive呢?

shallowReactive 和普通的 reactive 函数的主要区别是处理器函数不同,我们来回顾 getter 的处理器函数:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    if (key === "__v_isReactive" /* IS_REACTIVE */) {
      return !isReadonly;
    } else if (key === "__v_isReadonly" /* IS_READONLY */) {
      return isReadonly;
    } else if (
      key === "__v_raw" /* RAW */ &&
      receiver ===
        (isReadonly
          ? target["__v_readonly" /* READONLY */]
          : target["__v_reactive" /* REACTIVE */])
    ) {
      return target;
    }
    const targetIsArray = isArray(target);
    if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
      return Reflect.get(arrayInstrumentations, key, receiver);
    }
    const res = Reflect.get(target, key, receiver);
    if (
      isSymbol(key)
        ? builtInSymbols.has(key)
        : key === `__proto__` || key === `__v_isRef`
    ) {
      return res;
    }
    if (!isReadonly) {
      track(target, "get" /* GET */, key);
    }
    if (shallow) {
      return res;
    }
    if (isRef(res)) {
      return targetIsArray ? res : res.value;
    }
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

shallowReactive 创建的 getter 函数,shallow 变量为 true,那么就不会执行后续的递归 reactive 逻辑。也就是说,shallowReactive 只把对象 target 的最外一层属性的访问和修改处理成响应式。

之所以可以这么做,是因为 props 在更新的过程中,只会修改最外层属性,所以用 shallowReactive 就足够了

总结

好的,到这里我们这一节的学习也要结束啦,通过这节课的学习,你应该要了解 Props 是如何被初始化的,如何被校验的,你需要区分开 Props 配置和 Props 传值这两个概念;你还应该了解 Props 是如何更新的以及实例上的 props 为什么要定义成响应式的。