Vue实例初始化选项初合并 & 选项规范化校验
(本文2005字,大概会花费25-35min)
合并之后会挂载到实例的$options
属性中,可以在实例中通过this.$options
访问
var uid$3 = 0; function initMixin (Vue) { Vue.prototype._init = function (options) { var vm = this; // a uid 记录实例化多少个vue对象 vm._uid = uid$3++; ... // a flag to avoid this being observed vm._isVue = true; // merge options - 选项合并,将合并值赋给实例的$options属性 if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // 内部组件选项需要特殊处理。 - internal component options needs special treatment. initInternalComponent(vm, options); } else { // Vue实例不是组件,会执行mergeOptions方法 // 将两个选项对象合并为一个新对象。 // 用于实例化和继承的核心实用程序。 vm.$options = mergeOptions( // 返回Vue构造函数自身的配置项 resolveConstructorOptions(vm.constructor), options || {}, vm ); } ... }; }
通过研究resolveConstructorOptions
函数,发现:
function resolveConstructorOptions (Ctor) { var options = Ctor.options; // 有super属性,说明Vue.extend()构建的子类(下文会介绍) if (Ctor.super) { var superOptions = resolveConstructorOptions(Ctor.super); // Vue构造函数上的options,如directives,filters,... var cachedSuperOptions = Ctor.superOptions; if (superOptions !== cachedSuperOptions) { // super option changed, // need to resolve new options. Ctor.superOptions = superOptions; // check if there are any late-modified/attached options (#4976) var modifiedOptions = resolveModifiedOptions(Ctor); // update base extend options if (modifiedOptions) { // 对象合并 extend(Ctor.extendOptions, modifiedOptions); } // 将两个选项对象合并为一个新对象。 options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions); if (options.name) { options.components[options.name] = Ctor; } } } return options }
注意:有super属性,说明Vue.extend()
构建的子类,会为Ctor
添加一个super属性,指向其父类构造器
Vue.extend = function (extendOptions: Object): Function { ... Sub['super'] = Super ... }
其中,Ctor
是基础Vue
构造器,通过打印superOtions
,结构为下:
{ "components": {}, "directives": {}, "filters": {}, "beforeCreate": [], "beforeMount": [], "beforeDestroy": [], "destroyed": [] }
我们看mergeOptions
的部分:
/** * Merge two option objects into a new one. * Core utility used in both instantiation and inheritance. */ function mergeOptions ( parent, child, vm ) { { // components校验 checkComponents(child); } if (typeof child === 'function') { child = child.options; } // 规范化 props,inject,directives的校验和规范化 normalizeProps(child, vm); normalizeInject(child, vm); normalizeDirectives(child); // Apply extends and mixins on the child options, // but only if it is a raw options object that isn't // the result of another mergeOptions call. // Only merged options has the _base property. if (!child._base) { // 当传入的options里有mixin/extends属性时,再次调用mergeOptions方法合并mixins和extends里的内容到实例的构造函数options上(即parent options)比如下文 例1的情况 if (child.extends) { parent = mergeOptions(parent, child.extends, vm); } if (child.mixins) { for (var i = 0, l = child.mixins.length; i < l; i++) { parent = mergeOptions(parent, child.mixins[i], vm); } } } // 核心部分 var options = {}; var key; for (key in parent) { mergeField(key); } for (key in child) { if (!hasOwn(parent, key)) { mergeField(key); } } function mergeField (key) { // 拿到各个选择指定的选项配置,如果没有则用默认的配置 var strat = strats[key] || defaultStrat; // 执行合并策略 options[key] = strat(parent[key], child[key], vm, key); } return options }
例1:会把传入的mounted, created钩子处理函数,还有methods方法提出来和parent options做合并处理。
const childComponent = Vue.component('child', { ... mixins: [myMixin], extends: myComponent ... }) const myMixin = { created: function () { this.hello() }, methods: { hello: function () { console.log('hello from mixin') } } } const myComponent = { mounted: function () { this.yes() }, methods: { yes: function () { console.log('yes from mixin') } } }
为了更近异步理解,引用某大佬的合并流程图说明:
**选项合并中存在不清楚使用者传递了哪些配置选项,是否符合规范达到合并配置的要求。因此每个选项的书写规则需要严格限定,原则上不允许用户脱离规则外来传递选项。**所以在合并选项之前,很大的一部分工作是对选项的校验。其中components,prop,inject,directive
等都是检验的重点。
checkComponents
)代码:
/** * unicode letters used for parsing html tags, component names and property paths. * using https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname * skipping \u10000-\uEFFFF due to it freezing up PhantomJS */ // 用于解析 html 标签、组件名称和属性路径的 unicode 字母。使用 https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname,跳过 \u10000-\uEFFFF 因为它冻结了 PhantomJS // ps:这里的PhantomJS是一个基于webkit的JavaScript API。它使用QtWebKit作为它核心浏览器的功能,使用webkit来编译解释执行JavaScript代码。亦称为“无头浏览器” var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/; /** * Validate component names */ function checkComponents (options) { for (var key in options.components) { validateComponentName(key); } } function validateComponentName (name) { if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'should conform to valid custom element name in html5 specification.' ); } // 主要是防止自定义组件使用 HTML 内置,如svg等标签,及Vue自身定义的组件名,如slot, component, if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ); } }
normalizeProps
)引用Vue2.x
官方原话:
类型:
Array<string> | Object
详细:
props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值。
来自:https://cn.vuejs.org/v2/api/#props
代码:
/** * Ensure all props option syntax are normalized into the * Object-based format. */ function normalizeProps (options, vm) { var props = options.props; if (!props) { return } var res = {}; var i, val, name; // Array<string> | Object if (Array.isArray(props)) { // 数组 i = props.length; while (i--) { val = props[i]; // 如果数组的某一项为字符串的话 if (typeof val === 'string') { // 名字会被规范处理(驼峰式) name = camelize(val); // res添加属性,type默认为null res[name] = { type: null }; // eg:如果传入['first-prop','second-prop'],会被转化为: {firstProp: {type: null}, secondProp: {type: null}} } else { warn('props must be strings when using array syntax.'); } } } else if (isPlainObject(props)) { // 对象 for (var key in props) { // val赋值 val = props[key]; // 驼峰名字格式化 name = camelize(key); /* * 传入 props: { name: String } => { name: { type: String }} * 传入props: { name: { type: String,required: true} } => * { name: { type: String, reuired: true } } */ 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; }
综上,通过此规范化校验,
props
Array<string> | Object
经过名称驼峰规范化等一系列处理,最终都会被转换对象的形式,尤大佬yyds = = !
normalizeInject
)引用Vue2.x
官方原话:
provide/inject
依赖注入
类型:
- provide:
Object | () => Object
- inject:
Array<string> | { [key: string]: string | Symbol | Object }
详细:
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property
inject
选项应该是
- 一个字符串数组,或
- 一个对象,对象的 key 是本地的绑定名,value 是:
- 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
- 一个对象,该对象的:
from
property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
property 是降级情况下使用的 value来自:https://cn.vuejs.org/v2/api/#provide-inject
如果不太了解provide/inject的用法,可访问https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E4%BE%9D%E8%B5%96%E6%B3%A8%E5%85%A5
/** * Normalize all injections into Object-based format */ function normalizeInject (options, vm) { // 缓存inject对象,做中间变量 var inject = options.inject; if (!inject) { return } // 将其置空,且保证normalized与options.inject指向同一个对象 var normalized = options.inject = {}; if (Array.isArray(inject)) { for (var i = 0; i < inject.length; i++) { normalized[inject[i]] = { from: inject[i] }; } } else if (isPlainObject(inject)) { for (var key in inject) { var val = inject[key]; normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; } } else { warn( "Invalid value for option \"inject\": expected an Array or an Object, " + "but got " + (toRawType(inject)) + ".", vm ); } }
综上,通过此规范化校验,
inject
最终被转化为对象的形式与props
类似
normalizeDirectives
)类型: Object
代码:
/** * Normalize raw function directives into object format. */ function normalizeDirectives (options) { var dirs = options.directives; if (dirs) { for (var key in dirs) { var def$$1 = dirs[key]; if (typeof def$$1 === 'function') { dirs[key] = { bind: def$$1, update: def$$1 }; } } } }
通读代码,Vue
允许我们使用自定义指令,并提供以下5类钩子
我们可以使用以下的方式去校验:
{ directives: { 'demo': function (el,binding) { console.log(binding.value.color) // => "white" } } }
通过结果,对
directives
进行规范化时,针对函数的写法会将行为赋予bind,update
钩子,并回调
Thanks♪(・ω・)ノ