vue原始碼解析-prop機制

fengruiabc發表於2017-06-29

元件化開發,子父元件的通訊肯定是要越直觀越簡單越好。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選項。會在子元件例項化的時候建立自身的響應式。這是最核心的。大家可以細細體會。哎。突然很忙。沒法很詳細講了。不好意思。改天再來補充

相關文章