# props

在初始化 Vue 的时候,我们知道initState()会初始化props,methods,data等内容,这就是响应式原理的入口:

// instance/state.js
export function initState(vm: Component) {
  vm._watchers = [];
  const opts = vm.$options;
  // 把props中的数据转化为响应式数据,并且注入到实例
  if (opts.props) initProps(vm, opts.props);
  if (opts.methods) initMethods(vm, opts.methods);
  if (opts.data) {
    initData(vm);
  } else {
    // 将对象转化成响应式对象
    observe((vm._data = {}), true /* asRootData */);
  }
  // 创建顺序computed watcher -> watch watcher -> render watcher
  if (opts.computed) initComputed(vm, opts.computed);
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

上面的代码会依次处理用户传入的props,methods,data,computed,watch,我们依次来看内部是怎么处理的。

在初始化 Vue 的时候,会对 options 进行合并,在这里需要对 props 进行一定的序列化

// instance/init.js
Vue.prototype._init = function (options?: Object) {
  const vm: Component = this;

  // ...
  vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
  );

  // ...
};

mergeOptions定义的地方是在core/util/options.js

export function mergeOptions(
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  // ...
  normalizeProps(child, vm);
}
//  序列化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);
        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);
      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;
}

normalizeProps其实做了几个数据类型的判断:

  • 数组: 会做一个倒序的遍历,如果是 string 则会把 key 转成驼峰类型,同时赋值 type=null,如果不是 string,会报 warn
export default {
  props: ["name", "fisrt-name"],
};

// 转化后
export default {
  props: {
    name:{
      type:null
    },
    firstName:{
      type:null
    }
  }
}
  • 对象: 会遍历该对象,对 key 也会做一个驼峰处理,如果 val 是一个对象,则直接返回,否则将 val 作为 type
export default {
  props: {
    name: {
      value: "world",
      type: String,
    },
    age:Number,
    gender: '1'
  },
};

// 转化后
export default {
  props: {
    name: {
      value: "world",
      type: String,
    },
    age:{
      type:Number
    },
    gender:{
      type:'1'
    }
  },
};

完成序列化后,就是对props的初始化initProps

// core/instance/state.js
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);
  }
  for (const key in propsOptions) {
    keys.push(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) {
          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 {
      // 设置setter和getter
      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)) {
      // 注入到vm里,目的为了使用更加方便
      // vm.key === vm._props.key
      proxy(vm, `_props`, key);
    }
  }
  toggleObserving(true);
}

这里有一个函数toggleObserving,一开始设置为false,在defineReactive后设置为true,当设置为false的时候,禁止当前 对象变为响应式对象。

export function toggleObserving(value: boolean) {
  shouldObserve = value;
}

只有在子组件的时候,才会这样处理,因为子组件的 props 来自父级组件,父组件props变化会自动触发子组件渲染,不需要对于对象进行递归的响应式处理或者数组变异处理,所以可以省略observe这个过程,直接defineReactive就可以。

之后会对props进行校验

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;
      }
    }
  }
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key);
    const prevShouldObserve = shouldObserve;
    toggleObserving(true);
    observe(value);
    toggleObserving(prevShouldObserve);
  }
  if (
    process.env.NODE_ENV !== "production" &&
    !(__WEEX__ && isObject(value) && "@binding" in value)
  ) {
    assertProp(prop, key, value, vm, absent);
  }
  return value;
}
function getPropDefaultValue(vm, prop, key) {
  // no default, return undefined
  if (!hasOwn(prop, "default")) {
    return undefined;
  }
  var def = prop.default;
  // warn against non-factory defaults for Object & Array
  if (isObject(def)) {
    warn(
      'Invalid default value for prop "' +
        key +
        '": ' +
        "Props with type Object/Array must use a factory function " +
        "to return the default value.",
      vm
    );
  }
  if (
    vm &&
    vm.$options.propsData &&
    vm.$options.propsData[key] === undefined &&
    vm._props[key] !== undefined
  ) {
    return vm._props[key];
  }
  return typeof def === "function" && getType(prop.type) !== "Function"
    ? def.call(vm)
    : def;
}

主要做了三件事情:

  • 处理Boolean类型的数据: 首先通过getTypeIndex(Boolean, prop.type)判断prop是否是Boolean类型,返回对应的索引。如果是一个Boolean,而且没有设置default同时父级也没有传递数据,则设置为false。第二种情况比较复杂,主要是处理在父组件中没写属性的值的情况或者同名的属性值情况,则设置为true
  • 处理默认值: 1.如果没有传default,则直接返回undefined。2.default必须返回函数。3.上一次组件渲染父组件传递的 prop的值是 undefined,则直接返回 上一次的默认值 vm._props[key],这样可以避免触发不必要的 watcher的更新。4.判断def类型并返回对应的逻辑。

最后就是defineReactive进行响应式的处理,并且代理proxy(vm,_props, key)到实例下。