Vue.js 源碼分析(十三) 基礎篇 組件 props屬性詳解

父組件經過props屬性向子組件傳遞數據,定義組件的時候能夠定義一個props屬性,值能夠是一個字符串數組或一個對象。html

例如:vue

<!DOCTYPE html>  
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
    <div id="app"><child :title="message"></child></div>        
    <script>
        Vue.component('child',{
            template:'<h1>{{title}}</h1>',props:['title']       //這裏props是一個字符串數組
        })
        var app = new Vue({
            el:'#app',data:{message:'Hello World'}
        })  
    </script>
</body>
</html>

這裏咱們給child這個組件定義了名爲title的props,父組件經過title特性傳遞給子組件,渲染爲:node

props除了數組,也能夠是一個對象,此時對象的鍵對應的props的名稱,值又是一個對象,能夠包含以下屬性:npm

         type:        ;類型,能夠設置爲:String、Number、Boolean、Array、Object、Date等等             ;若是隻設置type而未設置其餘選項,則值能夠直接用類型,例如:props:{title:Object}
        default      ;默認值
        required    ;布爾類型,表示是否必填項目
        validator    ;自定義驗證函數    數組

例如:app

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
    <title>Document</title>    
</head>
<body>
    <div id="app"><child></child></div>        
    <script>
        Vue.component('child',{
            template:'<h1>{{title}}</h1>',props:{title:{default:'Hello World'}}                 //這裏咱們定義的title是個對象,含有默認值
        })
        var app = new Vue({
            el:'#app'
        })  
    </script>
</body>
</html>

這裏父組件app沒有給子組件child傳遞數據,子組件使用了默認值Hello World,渲染的結果和第一個例子是同樣的。函數

 

 源碼分析工具


 以上面的例1爲例,Vue.component()註冊組件的時候會調用Vue.extend()生成一個Vue基礎構造器,內部會調用mergeOptions函數合併屬性, mergeOptions又會調用normalizeProps對props的屬性進行一些規範化的修飾,以下:oop

function normalizeProps (options, vm) {       //第1361行 規範化props屬性
  var props = options.props;                        //嘗試獲取props屬性
  if (!props) { return }
  var res = {};
  var i, val, name;
  if (Array.isArray(props)) {                       //若是props是個數組       ;這是props的數組用法的分支
    i = props.length;
    while (i--) {                                     //遍歷props
      val = props[i];
      if (typeof val === 'string') {                  //若是值是一個字符串
        name = camelize(val);
        res[name] = { type: null };                       //保存到res裏面                  ;例如:{ title: {type: null} }
      } else {
        warn('props must be strings when using array syntax.');
      }
    }
  } else if (isPlainObject(props)) {                //若是props是個對象         ;這是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;
}

 通過normalizeProps規範後,props被修飾爲一個對象格式,例子裏的執行到這裏等於:源碼分析

接下來_render函數執行遇到該組件時會執行createComponent函數,該函數又會執行extractPropsFromVNodeData(data, Ctor, tag)函數,以下:

function extractPropsFromVNodeData (      //第2109行 獲取原始值
  data,
  Ctor,
  tag
) {
  // we are only extracting raw values here.
  // validation and default values are handled in the child
  // component itself.
  var propOptions = Ctor.options.props;                  //獲取組件的定義的props對象,例如:{message: {type: null}}
  if (isUndef(propOptions)) {
    return
  }
  var res = {};
  var attrs = data.attrs;                                   //獲取data的attrs屬性,例如:{title: "Hello Vue"}
  var props = data.props;                                   //獲取data的props屬性,這應該是創建父子組件時的關係
  if (isDef(attrs) || isDef(props)) {                       //若是data有定義了attrs或者props屬性
    for (var key in propOptions) {                            //遍歷組件的props屬性
      var altKey = hyphenate(key);
      {   
        var keyInLowerCase = key.toLowerCase();                 //hyphenate:若是key是是駝峯字符串,則轉換爲-格式
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)                  //轉換爲小寫格式
        ) {
          tip(
            "Prop \"" + keyInLowerCase + "\" is passed to component " +
            (formatComponentName(tag || Ctor)) + ", but the declared prop name is" +
            " \"" + key + "\". " +
            "Note that HTML attributes are case-insensitive and camelCased " +
            "props need to use their kebab-case equivalents when using in-DOM " +
            "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"."
          );
        }
      }
      checkProp(res, props, key, altKey, true) ||           //調用checkProp優先從props裏拿對應的屬性,其次從attrs裏拿(對於attrs的話第五個參數爲false,即會刪除對應的attrs裏的屬性)
      checkProp(res, attrs, key, altKey, false);
    }
  }
  return res
}

checkProp是檢測props或attrs是否含有key對應的值,以下:

function checkProp (          //第2150行 檢測prop是否存在
  res,
  hash,
  key,
  altKey,
  preserve
) {
  if (isDef(hash)) {              //若是hash存在
    if (hasOwn(hash, key)) {            //若是hash裏面有定義了key
      res[key] = hash[key];
      if (!preserve) {
        delete hash[key];
      }
      return true
    } else if (hasOwn(hash, altKey)) {  //若是有駝峯的表示法,也找到了
      res[key] = hash[altKey];
      if (!preserve) {
        delete hash[altKey];
      }
      return true
    }
  }
  return false                          //若是在res裏未找到則返回false
}

 extractPropsFromVNodeData只是獲取值,驗證理驗證和默認值是子組件完成執行的,執行到這裏就獲取到了props的值,例子裏執行到這裏等於

整個對象會做爲propsData屬性保存到組件的VNode裏面,以下:

 當子組件實例化的時候會執行_init()函數,首先會執行initInternalComponent函數,對於props的操做以下:

function initInternalComponent (vm, options) {          //第4632行 子組件初始化子組件
  var opts = vm.$options = Object.create(vm.constructor.options);     //組件的配置信息
  // doing this because it's faster than dynamic enumeration. 
  var parentVnode = options._parentVnode;                             //該組件的佔位符VNode
  opts.parent = options.parent;
  opts._parentVnode = parentVnode;
  opts._parentElm = options._parentElm;
  opts._refElm = options._refElm;

  var vnodeComponentOptions = parentVnode.componentOptions;           //佔位符VNode初始化傳入的配置信息
  opts.propsData = vnodeComponentOptions.propsData;                   //這就是上面通過extractPropsFromVNodeData()獲得的propsData對象
  opts._parentListeners = vnodeComponentOptions.listeners;
  opts._renderChildren = vnodeComponentOptions.children;
  opts._componentTag = vnodeComponentOptions.tag;

  if (options.render) {
    opts.render = options.render;
    opts.staticRenderFns = options.staticRenderFns;
  }
}

這樣組件實例化時就獲得了propsData了,以下

而後回到_init()初始化函數,會執行initState()函數,該函數首先會判斷是否有props屬性,若是有則執行initProps初始化props,以下:

function initProps (vm, propsOptions) {     //第3319行 初始化props屬性
  var propsData = vm.$options.propsData || {};                      //獲取propsData屬性,也就是例子裏的{title:"Hello World"}
  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 = [];                            //用於保存當前組件的props裏的key   ;以便以後在父組件更新props時能夠直接使用數組迭代,而不須要動態枚舉鍵值
  var isRoot = !vm.$parent;
  // root instance props should be converted
  if (!isRoot) {
    toggleObserving(false);
  }
  var loop = function ( key ) {                                     //定義一個loop函數,一下子會循環調用它
    keys.push(key);                                                   //保存key
    var value = validateProp(key, propsOptions, propsData, vm);       //執行validateProp檢查propsData裏的key值是否符合propsOptions裏對應的要求,並將值保存到value裏面
    /* istanbul ignore else */
    {
      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(props, key, value, function () {                 //將key變成響應式,同時也定義了props的key屬性的值爲value
        if (vm.$parent && !isUpdatingChildComponent) {
          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 依次調用loop()函數
  toggleObserving(true);
}

至此整個流程跑完了,前面說了extractPropsFromVNodeData只是獲取值,而驗證理驗證和默認值就是在validateProp()函數內作的判斷,以下:

function validateProp (         //第1582行 檢查props
  key,
  propOptions,
  propsData,
  vm
) {
  var prop = propOptions[key];                    //獲取對應的值,例如:{type: null}
  var absent = !hasOwn(propsData, key);           //若是propsData沒有key這個鍵名,則absent爲true
  var value = propsData[key];                     //嘗試獲取propsData裏key這個鍵的值
  // boolean casting
  var booleanIndex = getTypeIndex(Boolean, prop.type);          //調用getTypeIndex()含糊判斷prop.type是否包含布爾類型
  if (booleanIndex > -1) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false;
    } else if (value === '' || value === hyphenate(key)) {
      // only cast empty string / same name to boolean if
      // boolean has higher priority
      var stringIndex = getTypeIndex(String, prop.type);
      if (stringIndex < 0 || booleanIndex < stringIndex) {
        value = true;
      }
    }
  }
  // check default value
  if (value === undefined) {                        //若是value未定義
    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);           //判斷Prop是否有效
  }
  return value                                      //最後返回value
}

剩下來就幾個工具函數了,比較簡單,大體如此。

注:在Vue這麼多屬性裏面,props是最有意思,最好玩的。雖然props的用法比較簡單,可是它的原理實現我以爲是最複雜的,理解了props的實現原理,能夠說是對Vue源碼算是有比較大的深刻了解了

相關文章
相關標籤/搜索