欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue.js 源码分析(十三) 基础篇 组件 props属性详解

程序员文章站 2022-07-09 20:51:08
父组件通过props属性向子组件传递数据,定义组件的时候可以定义一个props属性,值可以是一个字符串数组或一个对象。 例如: 这里我们给child这个组件定义了名为title的props,父组件通过title特性传递给子组件,渲染为: props除了数组,也可以是一个对象,此时对象的键对应的pro ......

父组件通过props属性向子组件传递数据,定义组件的时候可以定义一个props属性,值可以是一个字符串数组或一个对象。

例如:

<!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特性传递给子组件,渲染为:

Vue.js 源码分析(十三) 基础篇 组件 props属性详解

props除了数组,也可以是一个对象,此时对象的键对应的props的名称,值又是一个对象,可以包含如下属性:

         type:        ;类型,可以设置为:string、number、boolean、array、object、date等等             ;如果只设置type而未设置其他选项,则值可以直接用类型,例如:props:{title:object}
        default      ;默认值
        required    ;布尔类型,表示是否必填项目
        validator    ;自定义验证函数   

例如:

<!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的属性进行一些规范化的修饰,如下:

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被修饰为一个对象格式,例子里的执行到这里等于:

Vue.js 源码分析(十三) 基础篇 组件 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的值,例子里执行到这里等于

Vue.js 源码分析(十三) 基础篇 组件 props属性详解

整个对象会作为propsdata属性保存到组件的vnode里面,如下:

Vue.js 源码分析(十三) 基础篇 组件 props属性详解

 当子组件实例化的时候会执行_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了,如下

Vue.js 源码分析(十三) 基础篇 组件 props属性详解

然后回到_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源码算是有比较大的深入了解了