乐趣区

关于javascript:Vue-props源码解析

main.js(入口文件):

import Vue from "vue";
import App from "./app.vue";

new Vue({
  el: "#app",
  data() {
    return {
      person: {
        sex: "male",
        name: "Jack",
      },
    };
  },
  template:
    "<div><button @click='test'> 测试 </button><App :person='person'/></div>",
  components: {App},
  methods: {test() {
      this.person = {name: "Mike",};
    },
  },
});

app.vue:

<template>
  <div>{{person.name}}<button @click="test"> 测试 </button></div>
</template>

<script>
  // app 组件选项
  export default {
    name: "app",
    props: ["person"],
    methods: {test() {this.person.name = "Rose";},
    },
  };
</script>

1. 组件选项中的 props 存在多种格局,最终都会调用 normalizeProps 格式化 props

// 格式化 props
function normalizeProps(options, vm) {
  var props = options.props;
  if (!props) {return;}
  var res = {};
  var 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 (var 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;
}

2.main.js 中的根组件渲染页面时,先调用 render 函数生成组件内标签节点(VNode 实例)。生成 app 组件标签节点时,读取根组件的数据 person 赋值给 props 属性personprops 数据保留在组件标签节点上。

读取 person 属性时触发属性的 get 办法,收集组件更新的观察者。当批改 person 属性时会触发属性的 set 办法,告诉组件更新页面。

// main.js 中根组件的 template 生成的 render 函数
function render() {
  return _c(
    "div",
    [_c("button", { on: { click: test} }, [_v("测试")]),
      _c(
        "App",
        {attrs: { person: person} } // app 组件标签上的 props 数据
      ),
    ],
    1
  );
}
var vnode = new VNode("vue-component-" + Ctor.cid + (name ? "-" + name : ""),
  data,
  undefined,
  undefined,
  undefined,
  context,
  {
    Ctor: Ctor,
    propsData: propsData,
    listeners: listeners,
    tag: tag,
    children: children,
  }, // propsData 保留了 props 数据
  asyncFactory
);

3. 当渲染 app 组件标签时,会依据组件标签节点创立组件,并初始化组件数据。调用 initProps 初始化 props,遍历组件选项中的 props 属性。依据 app 组件标签上的 props 数据 (propsData),调用validateProp,获取属性对应的值。而后应用 defineProperty 定义 props 对象上的属性,增加getset办法,设置 props 属性响应式,同时监听属性变动,当 props 上的属性批改时输入正告提醒。最初组件代理 props 属性,能够在组件上间接拜访 props 属性。

initProps中定义了 props 对象上的属性,未定义属性值中的对象的属性。批改 props 对象属性的值时(例:this.person = {name: 'Rose'}),有正告提醒,但批改值中的对象属性时(例:this.person.name = 'Peter'),没有正告提醒。然而页面同样会更新,因为属性值来自于父组件中的 data,父组件中的 data 设置了响应式。

// 初始化 props,设置 props 属性值
function initProps(vm, propsOptions) {
  // propsOptions 组件选项上的 props
  var propsData = vm.$options.propsData || {}; // 组件标签上 props 数据
  var props = (vm._props = {});
  var keys = (vm.$options._propKeys = []);
  var isRoot = !vm.$parent;
  if (!isRoot) {
    // 不是根组件
    toggleObserving(false); // 更改设置响应式标识(设置 props 对象属性响应式,不设置对象属性值响应式(对象属性值的响应式可能来自于父组件 data 数据)}
  var loop = function (key) {keys.push(key);
    var value = validateProp(key, propsOptions, propsData, vm); // 依据组件标签上的 prop 获取对应的值
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {var hyphenatedKey = hyphenate(key);
      if (isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          '"' +
            hyphenatedKey +
            '"is a reserved attribute and cannot be used as component prop.',
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        // 定义 props 上的属性,设置 get,set 办法。if (!isRoot && !isUpdatingChildComponent) {
          warn(
            // 如果更改了 props 对象属性的值正告
            "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$$1(props, key, value);
    }
    if (!(key in vm)) {proxy(vm, "_props", key); // props 挂载到组件上
    }
  };

  for (var key in propsOptions) loop(key);
  toggleObserving(true);
}
/**
 * 获取 props 属性值
 */
function validateProp(
  key, // 组件属性字段
  propOptions, // 组件属性配置
  propsData, // 组件 props 属性值
  vm
) {var prop = propOptions[key];
  var absent = !hasOwn(propsData, key);
  var value = propsData[key];
  // boolean casting
  var booleanIndex = getTypeIndex(Boolean, prop.type);
  if (booleanIndex > -1) {if (absent && !hasOwn(prop, "default")) {value = false; // Boolean 类型未赋值且没有默认值,则默认为 false} else if (value === "" || value === hyphenate(key)) {
      // 值为空或者和属性名雷同
      // only cast empty string / same name to boolean if
      // boolean has higher priority  Boolean 类型领有较高的优先级
      var stringIndex = getTypeIndex(String, prop.type);
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        // 类型不包含字符串或者 Boolean 类型靠前(优先)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.
    var prevShouldObserve = shouldObserve;
    toggleObserving(true);
    observe(value); // 对默认值设置响应式
    toggleObserving(prevShouldObserve);
  }
  if (
    process.env.NODE_ENV !== "production" &&
    // skip validation for weex recycle-list child component props
    !false
  ) {assertProp(prop, key, value, vm, absent);
  }
  return value;
}

4. 初始化 dataprops 等 app 组件数据后,开始渲染页面。调用 render 时创立标签节点,顺次读取了 props 属性上的 personname 属性。依据创立的标签节点生成 DOM 元素,增加到页面中。

读取 person 属性时,app 组件 props 中的 person 属性会收集 app 组件更新的观察者。读取 name 属性时,根组件 data 中的 name 属性会收集 app 组件更新的观察者。

// app 组件 template 生成的 render 函数
function render() {
  var _vm = this;
  var _h = _vm.$createElement;
  var _c = _vm._self._c || _h;
  return _c("div", [_vm._v(_vm._s(_vm.person.name)), // 顺次读取了 props 属性上的 person 和 name 属性
    _c("button", { on: { click: _vm.test} }, [_vm._v("测试")]),
  ]);
}

5. 点击根组件中的按钮触发 test 函数,批改 person 时,将会触发根组件页面更新,更新过程中调用 updateChildComponent 更新子组件标签上的数据,批改 props 的属性 person,触发 set 办法,告诉 app 组件也更新页面,app 组件将依据批改后的 props 属性值渲染页面。

function updateChildComponent (
  vm,
  propsData,
  listeners,
  parentVnode,
  renderChildren
) {if (process.env.NODE_ENV !== 'production') {isUpdatingChildComponent = true;// 标识阐明正在更新组件}
    ...
  vm.$attrs = parentVnode.data.attrs || emptyObject;
  vm.$listeners = listeners || emptyObject;

  // update props
  if (propsData && vm.$options.props) {toggleObserving(false);
    var props = vm._props;
    var propKeys = vm.$options._propKeys || [];
    for (var i = 0; i < propKeys.length; i++) {var key = propKeys[i];
      var propOptions = vm.$options.props; // wtf flow?
      props[key] = validateProp(key, propOptions, propsData, vm);// 更新 props 属性值
    }
    toggleObserving(true);
    // keep a copy of raw propsData
    vm.$options.propsData = propsData;
  }
    ...
  if (process.env.NODE_ENV !== 'production') {isUpdatingChildComponent = false;}
}

6. 点击 app 组件中的按钮触发 test 函数,批改 person 上的 name 属性,触发 name 属性上的 set 办法,告诉 app 组件更新页面。

在根组件内批改 personname 属性,和在 app 组件内批改 personname 属性是一样的,都会触发 app 组件页面更新。

退出移动版