Vue.js 源码分析(十三) 基础篇 组件 props属性详解
父组件通过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特性传递给子组件,渲染为:
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被修饰为一个对象格式,例子里的执行到这里等于:
接下来_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源码算是有比较大的深入了解了
推荐阅读
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解
-
Vue.js 源码分析(十二) 基础篇 组件详解
-
Vue.js 源码分析(十一) 基础篇 过滤器 filters属性详解
-
Vue.js 源码分析(十三) 基础篇 组件 props属性详解
-
Vue 2.0 深入源码分析(七) 基础篇 侦听器 watch属性详解
-
Vue 2.0 深入源码分析(五) 基础篇 methods属性详解
-
Vue 2.0 深入源码分析(六) 基础篇 computed 属性详解
-
Vue.js 源码分析(二十三) 指令篇 v-show指令详解
-
Vue 2.0 深入源码分析(三) 基础篇 模板渲染 el、emplate、render属性详解
-
Vue.js 源码分析(十四) 基础篇 组件 自定义事件详解