vue源碼解析-prop機制

組件化開發,子父組件的通訊確定是要越直觀越簡單越好。vue身爲一個優秀的mvvm框架裏面的子父通訊必須簡單明瞭。相比於vue1。vue2刪除了dispatch,emit等等子父通訊方式,大大提高了vue的性能。實在太複雜的邏輯就交給vuex把。此次咱們來看看咱們熟悉又陌生的prop。
在vue中。咱們常常須要從父組件往子組件裏傳遞某些數據到子組件中供子組件使用。咱們先來看看下面一個最簡單的例子:javascript

<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用的函數html

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時。該組件天然而然也會被再次實例化。實例化調用了下面函數。這裏是核心:vue

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。你們能夠好好看看。java

至於這裏。不是對象時。而是一個簡單的賦值。若是父組件該了這個test的值。父組件便會進入re-render。此時會進行patch環節。比較新舊vnode。差別化更新。當碰到該組件時。會進入以下函數:node

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週期中。這樣子組件就能本身異步更新。父組件先無論子組件,本身飯回來更新下面的節點。vuex

總結:總的來講。最核心的部分是從父組件傳給子組件的prop選項。會在子組件實例化的時候建立自身的響應式。這是最核心的。你們能夠細細體會。哎。忽然很忙。無法很詳細講了。很差意思。改天再來補充api

相關文章
相關標籤/搜索