theydy / notebook

记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
0 stars 0 forks source link

props 原理 #37

Open theydy opened 3 years ago

theydy commented 3 years ago

例子如下:

var A = {
  template: `
  <div>
    <p>{{ name }}</p>
    <p>info:</p>
    <ul>
      <li>{{ info.aa }}</li>
      <li>{{ info.bb }}</li>
    </ul>
  </div>`,
  props: {
    name: String,
    info: Object,
  },
}
var vm = new Vue({
  el: '#app',
  components: {
    A
  },
  template: `<div id="app"><A :name="name" :info="info" /></div>`,
  data () {
    return {
      name: 'test',
      info: {
        aa: 11,
        bb: 22,
      }
    }
  },
})

Props 的规范化过程

这里的规范化指的是对于子组件,规范 props 的定义。

mergeOptions → normalizeProps

  function normalizeProps (options, vm) {
    debugger
    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 {
          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 {
      warn(
        "Invalid value for option \"props\": expected an Array or an Object, " +
        "but got " + (toRawType(props)) + ".",
        vm
      );
    }
    options.props = res;
  }

规范化后的格式,props 属性驼峰化,并且至少会有一个 type。

options.props = {
  xxYyy: {
    type,
  }
}

注意一点,因为这是对于子组件 props 定义的规范化,所以 type 才是必须的,而不一定有值,如果 props 属性写法如下:

{
  props: {
    test: {
      type: Number,
      default: 5,
    }
  }
}

对于这种写法 res[name] = isPlainObject(val) ? val : { type: val }; 最后会直接赋给 res['test']

Props 的初始化过程

这里要分两步,一步是父组件如何把绑定的值做为 props 传给子组件;一步是子组件如何获取父组件传过来的值做初始化。

父组件传值

首先,vm 的 render 函数如下,attrs 上保存着传下来的 props 值。

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("A", { attrs: { name: _vm.name, info: _vm.info } }),
    ],
    1
  )
}

接着执行 A 组件 vnode 的生成。

createElement → createComponent ,和 props 相关的关键逻辑大致如下:

  function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
  ) {
    // ...
    var baseCtor = context.$options._base;
    // ...
    // 生成子组件构造函数,会走 props 的规范化、props 代理。
    Ctor = baseCtor.extend(Ctor);
    // 这里拿到父组件传下来的 props 值,{ attr: { name, info } }
    data = data || {};

    // ...
    // 最终保存在 vnode 上的 props 信息,{ name, info }
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);

    // ...
    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    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 },
      asyncFactory
    );
    // vnode.componentOptions.propsData 保存着父组件传下来的 props 值
    return vnode
  }

createComponent 中先生成 A 组件的构造函数,此时会执行 props 的规范化过程;然后执行 extractPropsFromVNodeData 得到 propsData,这是最后保存在 vnode 上关于 props 的信息;最后返回 A 组件的 vnode,vnode.componentOptions.propsData 保存着父组件传下来的 props 值。

extractPropsFromVNodeData 函数如下,大致流程:

  function extractPropsFromVNodeData (
    data,
    Ctor,
    tag
  ) {

    var propOptions = Ctor.options.props;
    if (isUndef(propOptions)) {
      return
    }
    var res = {};
    var attrs = data.attrs;
    var props = data.props;
    if (isDef(attrs) || isDef(props)) {
      for (var key in propOptions) {
        var altKey = hyphenate(key);
        {
          var keyInLowerCase = key.toLowerCase();
          // ...
        }
        checkProp(res, props, key, altKey, true) ||
        checkProp(res, attrs, key, altKey, false);
      }
    }
    return res
  }

子组件获取值初始化

现在我们知道子组件 vnode 上 componentOptions.propsData 是可以拿到父组件传下来的值的,而子组件 props 的初始化主要在 initProps 函数中。()

initState → initProps

主要流程如下:

  function initProps (vm, propsOptions) {
    var propsData = vm.$options.propsData || {};
    var props = vm._props = {};
    // ...
    var isRoot = !vm.$parent;
    // root instance props should be converted
    if (!isRoot) {
      toggleObserving(false);
    }
    var loop = function ( key ) {
      keys.push(key);
      var value = validateProp(key, propsOptions, propsData, vm);

      {
        // ...
        defineReactive$$1(props, key, value, function () {
          // ...
        });
      }
      // ...
      if (!(key in vm)) {
        proxy(vm, "_props", key);
      }
    };

    for (var key in propsOptions) loop( key );
    toggleObserving(true);
  }

这一步最重要的就是 validatePropdefineReactive 这两步,响应式暂时不看,只看 validateProp

  function validateProp (
    key,
    propOptions,
    propsData,
    vm
  ) {
    var prop = propOptions[key];
    var absent = !hasOwn(propsData, key);
    var value = propsData[key];
    // boolean ...
    // 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);
    }
    {
      assertProp(prop, key, value, vm, absent);
    }
    return value
  }

props 的初始化结束。

Props 的更新过程

子组件 props 更新。props 数据的值在父组件中发生变化,触发父组件的 render

patch 过程中执行 patchVnode 函数。

vnode hook prepatch 函数 → updateChildComponent 函数。

  function updateChildComponent (
    vm,
    propsData, // 父组件更新后的 props 数据
    listeners,
    parentVnode,
    renderChildren
  ) {
    // ...
    // 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);
      }
      toggleObserving(true);
      // keep a copy of raw propsData
      vm.$options.propsData = propsData;
    }
    // ...
  }

可以看到只是重新执行 validateProp 赋值即可,这里又有两种情况,一种是父组件传的值是普通类型,更新后 validateProp 返回赋值,会触发 prop 的 setter 触发子组件渲染 watcher 更新;

一种是父组件传的值是引用类型,此时可能只是对象中的某一项被修改,validateProp 返回的还是同一个引用,不会触发 setter,但是子组件中访问过这个 prop,子组件渲染 watcher 会被收集到这个 prop 的依赖中,父组件中修改 props 时触发 setter,也就会通知子组件渲染 watcher 更新。