vue源码解析-prop机制

组件化开发,子父组件的通信肯定是要越直观越简单越好。vue身为一个优秀的mvvm框架里面的子父通信必须简单明了。相比于vue1。vue2删除了dispatch,emit等等子父通信方式,大大提升了vue的性能。实在太复杂的逻辑就交给vuex把。这次我们来看看我们熟悉又陌生的prop。
在vue中。我们经常需要从父组件往子组件里传递某些数据到子组件中供子组件使用。我们先来看看下面一个最简单的例子:

<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div id="app">
  <my-component :test="heihei">
  </my-component>
  <div>{{a}}</div>
</div>
</body>
<script src="vue.js"></script>
<script type="text/javascript">
Vue.component('my-component', {
  name: 'my-component',
  props: ['test'],
  template: '<div>A custom component!{{test}}</div>',
  created(){
    console.log(this);
  },
  mounted(){
    console.log(this);
  }
})
  new Vue({
  el: '#app',
  data: function () {
    return {
      heihei:3333
    }
  },
  created(){
  },
  methods: {
  }
})
</script>
</html>

上面是一个最简单的prop传值的问题,父组件把自身的heihei传进去。我们还是一起来看看在vue内部发生了什么。在这之前。建议大家先去看一下vue的响应式原理和vue是如何巧妙的递归构建组件。下面我们只关注vue生命周期中关于prop的部分。首先开始创建vue实例。在compile生成AST的时候自然而然会被当成attr的一个属性。在创建虚拟dom的时候。我们看看组件创建虚拟dom用的函数

function createComponent (
  Ctor,
  data,
  context,
  children,            //在render的时候如果遇到组建选项。用该函数创建组建初始化前所需要的组建参数。包括提取组建构造函数,策略合并组建自定义参数
  tag
) {
  if (isUndef(Ctor)) {
    return
  }

  var baseCtor = context.$options._base;//获取根vue构造器

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor);//若局部组建,之前未注册的组件开始注册
  }

  // if at this stage it's not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== 'function') {
    {
      warn(("Invalid Component definition: " + (String(Ctor))), context);
    }
    return
  }

  // async component
  if (isUndef(Ctor.cid)) {
    Ctor = resolveAsyncComponent(Ctor, baseCtor, context);//异步组件获取响应构造函数
    if (Ctor === undefined) {
      // return nothing if this is indeed an async component
      // wait for the callback to trigger parent update.
      return
    }
  }

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor);//再次继承一下vue根构造器上的属性方法,比如vue.mixin会改变构造器的options

  data = data || {};

  // transform component v-model data into props & events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data);
  }

  // extract props
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);//抽取相应的从父组件上的prop。这里即为({test:333})

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  var listeners = data.on;//将挂在组件上的原先的事件都放在listeners上。后续组建实例化的时候。调用$on方法
  // replace with listeners with .native modifier
  data.on = data.nativeOn;//将组件上有native修饰符的事件放在最终的data.on。后续像一般html元素一样。调用el.addeventlisten  api

  if (isTrue(Ctor.options.abstract)) {
    // abstract components do not keep anything
    // other than props & listeners
    data = {};
  }

  // merge component management hooks onto the placeholder node
  mergeHooks(data);//如果该虚拟dom是组件,则挂上相应的组件的初始化和更新函数在它的data上

  // 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 }//创建组建的虚拟dom
  );
  return vnode
}

创建虚拟dom的时候。父组件上的prop被放在了vnode中的componentOptions中的propsdata选项。在创建真实dom时。该组件自然而然也会被再次实例化。实例化调用了下面函数。这里是核心:

function createComponentInstanceForVnode (
  vnode, // we know it's MountedComponentVNode but flow doesn't
  parent, // activeInstance in lifecycle state
  parentElm,
  refElm
) {
  var vnodeComponentOptions = vnode.componentOptions;
  var options = {
    _isComponent: true,
    parent: parent,
    propsData: vnodeComponentOptions.propsData,//父组件来的props相关内容到了初始化子组件的options中。供子组件实例化时调用
    _componentTag: vnodeComponentOptions.tag,
    _parentVnode: vnode,
    _parentListeners: vnodeComponentOptions.listeners,//组建上的非.native修饰的事件
    _renderChildren: vnodeComponentOptions.children,//组建之间的子元素。用于后续实例化后传给$slots
    _parentElm: parentElm || null,
    _refElm: refElm || null
  };
  // check inline-template render functions
  var inlineTemplate = vnode.data.inlineTemplate;
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render;
    options.staticRenderFns = inlineTemplate.staticRenderFns;
  }
  return new vnodeComponentOptions.Ctor(options)//实例化组件
}


//紧接着看看初始化该组件发生了什么,子组件初始化很自然的进入了子组件的生命周期。没错。很自然。就这样又会调用initState,没错这其中包括了if (opts.props) { initProps(vm, opts.props); }。我们看看initprops发生了什么,这是最关键的

function initProps (vm, propsOptions) {
  var propsData = vm.$options.propsData || {};//取到props相关数据
  var props = vm._props = {};
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  var keys = vm.$options._propKeys = [];
  var isRoot = !vm.$parent;
  // root instance props should be converted
  observerState.shouldConvert = isRoot;
  var loop = function ( key ) {//
    keys.push(key);
    var value = validateProp(key, propsOptions, propsData, vm);//重点看看该函数。校验参数,并且建立响应式
    /* istanbul ignore else */
    {
      if (isReservedProp[key] || config.isReservedAttr(key)) {
        warn(
          ("\"" + key + "\" is a reserved attribute and cannot be used as component prop."),
          vm
        );
      }
      defineReactive$$1(props, key, value, function () {
        if (vm.$parent && !observerState.isSettingProps) {
          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
          );
        }
      });
    }
    // 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)) {
      proxy(vm, "_props", key);
    }
  };

  for (var key in propsOptions) loop( key );//遍历props数据校验的同时。创建自身响应式
  observerState.shouldConvert = true;
}


function validateProp (
  key,
  propOptions,
  propsData,
  vm
) {
  var prop = propOptions[key];
  var absent = !hasOwn(propsData, key);
  var value = propsData[key];
  // handle boolean props
  if (isType(Boolean, prop.type)) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false;
    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
      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 prevShouldConvert = observerState.shouldConvert;
    observerState.shouldConvert = true;
    observe(value);//创建自己的响应式
    observerState.shouldConvert = prevShouldConvert;
  }
  {
    assertProp(prop, key, value, vm, absent);
  }
  return value
}

因为在实例化的时候。子组件接过来的props也有了响应式。所以在渲染子组件的时候。该属性的dep(消息订制器)会将子组件的watcher push进去。当子组件自己改变当前属性时。子组件会重新re-render。而父组件的值不会改变但是当子组件接受的如果是个对象。结果就不一样了。这里可以重点看看observe(value);//创建自己的响应式。简单来说。如果props传入的是父组件的一个对象。那么这个对象中的属性的getter,setter已经在父组件中创建好了。observe(value)中会对已经创建过响应式的对象不再重复创建响应式。所以该对象中还保留着父组件的re-render函数。一旦子组件自己改变了这个值。说白了。对象就是都是在一片内存里。子组件改变了这个对象。那么引用了这个对象的全就变了。便会出发之前在父组件touch阶段推入的父组件的re-render的监听。一旦该值变了。父组件会重新re-render。大家可以好好看看。

至于这里。不是对象时。而是一个简单的赋值。如果父组件该了这个test的值。父组件便会进入re-render。此时会进行patch环节。比较新旧vnode。差异化更新。当碰到该组件时。会进入如下函数:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }
    // reuse element for static trees.
    // note we only do this if the vnode is cloned -
    // if the new node is not cloned it means the render functions have been
    // reset by the hot-reload-api and we need to do a proper re-render.
    if (isTrue(vnode.isStatic) &&
        isTrue(oldVnode.isStatic) &&
        vnode.key === oldVnode.key &&
        (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
      vnode.elm = oldVnode.elm;//关键。将旧的构造好的elm先赋值给新的vnode
      vnode.componentInstance = oldVnode.componentInstance;//关键,将旧的构造好的组件实例赋值给新的vnode
      return
    }
    var i;
    var data = vnode.data;
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {//如果是组件。会在比较前先做prepacth看下面的prepacth函数
      i(oldVnode, vnode);
    }
    var elm = vnode.elm = oldVnode.elm;
    var oldCh = oldVnode.children;
    var ch = vnode.children;
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
      if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '');
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text);
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
    }
  }




prepatch: function prepatch (oldVnode, vnode) {//在re-render的时候。patch到组件节点时。重新更新一下组件上最新的事件和props等等
    var options = vnode.componentOptions;
    var child = vnode.componentInstance = oldVnode.componentInstance;
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    );
  },



function updateChildComponent (
  vm,
  propsData,
  listeners,
  parentVnode,
  renderChildren
) {
  // determine whether component has slot children
  // we need to do this before overwriting $options._renderChildren
  var hasChildren = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    parentVnode.data.scopedSlots || // has new scoped slots
    vm.$scopedSlots !== emptyObject // has old scoped slots
  );

  vm.$options._parentVnode = parentVnode;
  vm.$vnode = parentVnode; // update vm's placeholder node without re-render
  if (vm._vnode) { // update child tree's parent
    vm._vnode.parent = parentVnode;
  }
  vm.$options._renderChildren = renderChildren;

  // update props
  if (propsData && vm.$options.props) {
    observerState.shouldConvert = false;
    {
      observerState.isSettingProps = true;
    }
    var props = vm._props;
    var propKeys = vm.$options._propKeys || [];
    for (var i = 0; i < propKeys.length; i++) {
      var key = propKeys[i];
      props[key] = validateProp(key, vm.$options.props, propsData, vm);
    }
    observerState.shouldConvert = true;
    {
      observerState.isSettingProps = false;
    }
    // keep a copy of raw propsData
    vm.$options.propsData = propsData;
  }
  // update listeners
  if (listeners) {
    var oldListeners = vm.$options._parentListeners;
    vm.$options._parentListeners = listeners;
    updateComponentListeners(vm, listeners, oldListeners);
  }
  // resolve slots + force update if has children
  if (hasChildren) {
    vm.$slots = resolveSlots(renderChildren, parentVnode.context);
    vm.$forceUpdate();
  }
}

这里在patch中如果在比较新旧两个组建时。因为如果组件有从父组件传递props。props必定会有响应式。回调就是子组件的render函数。那么这里的赋值必然会让子组件重新渲染。进入子组件自身的patch周期中。这样子组件就能自己异步更新。父组件先不管子组件,自己饭回来更新下面的节点。

总结:总的来说。最核心的部分是从父组件传给子组件的prop选项。会在子组件实例化的时候创建自身的响应式。这是最核心的。大家可以细细体会。哎。突然很忙。没法很详细讲了。不好意思。改天再来补充

    原文作者:fengruiabc
    原文地址: https://segmentfault.com/a/1190000009981658
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞