对于大部分的前端开发人员来说,熟练使用vue
作项目是第一步,但当进阶后遇到一些特殊场景,解决棘手问题时,了解vue
框架的设计思想和实现思路即是基础须要。本专题将深刻vue
框架源码,一步步挖掘框架设计理念和思想,并尽量利用语言将实现思路讲清楚。但愿您是在熟练使用vue
的前提下阅读此系列文章,也但愿您阅读后能留下宝贵建议,以便后续文章改进。
<div id="app"></div> <script src="https://cdn.jsdelivr.net/npm/vue@2.6.8/dist/vue.js"></script> var vm = new Vue({ el: '#app', data: { message: '选项合并' }, components: { 'components': {} } })
从最简单的使用入手,new
一个Vue
实例对象是使用vue
的第一步,在这一步中,咱们须要传递一些基础的选项配置,Vue
会根据系统的默认选项和用户自定选项进行合并选项配置的过程。本系列将从这一过程展开,在这一节中咱们研究的核心在于各类数据选项在vue
系统中是如何进行合并的(忽略过程当中的响应式系统构建,后面专题讲解)。html
// Vue 构造函数 function Vue (options) { if (!(this instanceof Vue) ) { // 规定vue只能经过new实例化建立,不然抛出异常 warn('Vue is a constructor and should be called with the `new` keyword'); } this._init(options); } // 在引进Vue时,会执行initMixin方法,该方法会在Vue的原型上定义数据初始化init方法,方法只在实例化Vue时执行。 initMixin(Vue); // 暂时忽略其余初始化过程。。。 ···
接下来,咱们将围绕vue数据的初始化展开解析。前端
var ASSET_TYPES = [ 'component', 'directive', 'filter' ]; Vue.options = Object.create(null); // 原型上建立了一个指向为空对象的options属性 ASSET_TYPES.forEach(function (type) { Vue.options[type + 's'] = Object.create(null); }); Vue.options._base = Vue;
Vue构造函数自身有四个默认配置选项,分别是component,directive, filter
以及返回自身构造器的_base
(这里先不展开对每一个属性内容的介绍)。这四个属性挂载在构造函数的options
属性上。vue
咱们抓取_init
方法合并选项的核心部分代码以下:html5
function initMixin (Vue) { Vue.prototype._init = function (options) { var vm = this; // a uid // 记录实例化多少个vue对象 vm._uid = uid$3++; // 选项合并,将合并后的选项赋值给实例的$options属性 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), // 返回Vue构造函数自身的配置项 options || {}, vm ); }; }
从代码中能够看到,选项合并的重点是将用户自身传递的options
选项和Vue
构造函数自身的选项配置合并,并将合并结果挂载到实例对象的$options
属性上。算法
选项合并过程咱们更多的不可控在于不知道用户传了哪些配置选项,这些配置是否符合规范,因此每一个选项的规范须要严格定义好,不容许用户按照规范外的标准来传递选项。所以在合并选项以前,很大的一部分工做是对选项的校验。其中components,prop,inject,directive
等都是检验的重点。下面只会列举components
和props
的校验讲解,其余的如inject, directive
校验相似,请自行对着源码解析。npm
function mergeOptions ( parent, child, vm ) { { checkComponents(child); // 合并前对选项components进行规范检测 } if (typeof child === 'function') { child = child.options; } normalizeProps(child, vm); // 校验props选项 normalizeInject(child, vm); // 校验inject选项 normalizeDirectives(child); // 校验directive选项 if (!child._base) { 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 }
咱们能够在vue
实例化时传入组件选项以此来注册组件。所以,组件命名须要遵照不少规范,好比组件名不能用html
保留的标签(如:img,p
),只能以字母开头等。所以在选项合并以前,须要对规范进行检查。api
// components规范检查函数 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.' ); } // 不能使用Vue自身自定义的组件名,如slot, component,不能使用html的保留标签,如 h1, svg等 if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( 'Do not use built-in or reserved HTML elements as component ' + 'id: ' + name ); } }
从vue
的使用文档看,props
选项的形式有两种,一种是['a', 'b', 'c']
的数组形式,一种是{ a: { type: 'String', default: 'hahah' }}
带有校验规则的形式。从源码上看,两种形式最终都会转换成对象的形式。数组
// props规范校验 function normalizeProps (options, vm) { var props = options.props; if (!props) { return } var res = {}; var i, val, name; // props选项数据有两种形式,一种是['a', 'b', 'c'],一种是{ a: { type: 'String', default: 'hahah' }} if (Array.isArray(props)) { i = props.length; while (i--) { val = props[i]; if (typeof val === 'string') { name = camelize(val); res[name] = { type: null }; // 默认将数组形式的props转换为对象形式。 } else { // 保证是字符串 warn('props must be strings when using array syntax.'); } } } else if (isPlainObject(props)) { for (var key in props) { val = props[key]; name = camelize(key); res[name] = isPlainObject(val) ? val : { type: val }; } } else { // 非数组,非对象则断定props选项传递非法 warn( "Invalid value for option \"props\": expected an Array or an Object, " + "but got " + (toRawType(props)) + ".", vm ); } options.props = res; }
在读到props
规范检验时,我发现了一段函数优化的代码,他将每次执行函数后的值缓存起来,下次重复执行的时候调用缓存的数据,以此提升前端性能,这是典型的偏函数应用,能够参考我另外一篇文章打造属于本身的underscore系列(五)- 偏函数和函数柯里化缓存
function cached (fn) { var cache = Object.create(null); // 建立空对象做为缓存对象 return (function cachedFn (str) { var hit = cache[str]; return hit || (cache[str] = fn(str)) // 每次执行时缓存对象有值则不须要执行函数方法,没有则执行并缓存起来 }) } var camelize = cached(function (str) { // 将诸如 'a-b'的写法统一处理成驼峰写法'aB' return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; }) });
选项校验介绍完后,在正式进入合并策略以前,还须要先了解一个东西,子类构造器。在vue
的应用实例中,咱们经过Vue.extend({ template: '<div></div>', data: function() {} })
建立一个子类,这个子类和Vue
实例建立的父类同样,能够经过建立实例并挂载到具体的一个元素上。具体用法详见Vue官方文档,而具体实现以下所示(只简单抽取部分代码):app
Vue.extend = function (extendOptions) { extendOptions = extendOptions || {}; var Super = this; var name = extendOptions.name || Super.options.name; if (name) { validateComponentName(name); // 校验子类的名称是否符合规范 } var Sub = function VueComponent (options) { // 子类构造器 this._init(options); }; Sub.prototype = Object.create(Super.prototype); // 子类继承于父类 Sub.prototype.constructor = Sub; Sub.cid = cid++; // 子类和父类构造器的配置选项进行合并 Sub.options = mergeOptions( Super.options, extendOptions ); return Sub // 返回子类构造函数 };
为何要先介绍子类构造器的概念呢,缘由是在选项合并的代码中,除了须要合并Vue实例和Vue构造器自身的配置,还须要合并子类构造器和父类构造器选项的场景。
合并策略之因此是难点,其中一个是合并选项类型繁多,大致能够分为如下三类:Vue自定义策略, 父类自身配置, 子类自身策略(用户配置)。如何理解?
Vue
自定义策略,vue
在选项合并的时候对一些特殊的选项有自身定义好的合并策略,例如data
的合并,el
的合并,而每个的合并规则都不同,所以须要对每个规定选项进行特殊的合并处理Vue
构造函数自身的options
属于父类自身配置,咱们须要将实例传递的配置和Vue.options
进行合并。再者前面提到的var P = Vue.extends(); var C = P.extends()
,P做为C的父类,在合并选项时一样须要考虑进去。new
实例传递的options
选项在Vue
源码中,如何处理好这三个选项的合并,思路是这样的:
function mergeOptions ( parent, child, 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 }
喜欢本系列的朋友欢迎关注公众号 假前端,有源码解析和算法精选哦