共计 5775 个字符,预计需要花费 15 分钟才能阅读完成。
对于大部分的前端开发人员来讲,熟练使用
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
系统中是如何进行合并的(忽略过程中的响应式系统构建, 后面专题讲解)。
// 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 数据的初始化展开解析。
1.1 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
属性上。
我们抓取 _init
方法合并选项的核心部分代码如下:
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
属性上。
1.2 选项校验
选项合并过程我们更多的不可控在于不知道用户传了哪些配置选项,这些配置是否符合规范,所以每个选项的规范需要严格定义好,不允许用户按照规范外的标准来传递选项。因此在合并选项之前,很大的一部分工作是对选项的校验。其中 components,prop,inject,directive
等都是检验的重点。下面只会列举 components
和props
的校验讲解,其他的如 inject, directive
校验类似,请自行对着源码解析。
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 | |
} |
1.2.1 components 规范检验
我们可以在 vue
实例化时传入组件选项以此来注册组件。因此,组件命名需要遵守很多规范,比如组件名不能用 html
保留的标签(如:img,p
), 只能以字母开头等。因此在选项合并之前,需要对规范进行检查。
// 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 | |
); | |
} | |
} |
1.2.2 props 规范检验
从 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; | |
} |
1.2.3 函数缓存
在读到 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() : ''; }) | |
}); |
1.3 子类构造器
选项校验介绍完后,在正式进入合并策略之前,还需要先了解一个东西,子类构造器。在 vue
的应用实例中,我们通过 Vue.extend({template: '<div></div>', data: function() {}})
创建一个子类,这个子类和 Vue
实例创建的父类一样,可以通过创建实例并挂载到具体的一个元素上。具体用法详见 Vue 官方文档,而具体实现如下所示(只简单抽取部分代码):
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 构造器自身的配置,还需要合并子类构造器和父类构造器选项的场景。
1.4 合并策略
合并策略之所以是难点,其中一个是合并选项类型繁多,大体可以分为以下三类:Vue 自定义策略,父类自身配置,子类自身策略(用户配置)。如何理解?
-
Vue
自定义策略,vue
在选项合并的时候对一些特殊的选项有自身定义好的合并策略,例如data
的合并,el
的合并,而每一个的合并规则都不一样,因此需要对每一个规定选项进行特殊的合并处理 - 父类自身配置,首先创建一个 vue 实例时,
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 | |
} |
喜欢本系列的朋友欢迎关注公众号 假前端,有源码解析和算法精选哦