1.3 选项检验

介绍完Vue自身拥有的选项后,我们回过头来看看,实例化Vue的阶段发生了什么。从构造器的定义我们很容易发现,实例化Vue做的核心操作便是执行_init方法进行初始化。初始化操作会经过选项合并配置,初始化生命周期,初始化事件中心,乃至构建数据响应式系统等。而关键的第一步就是对选项的合并。合并后的选项会挂载到实例的$options属性中。(你可以先在实例中通过this.$options访问最终的选项)

  1. function initMixin (Vue) {
  2. Vue.prototype._init = function (options) {
  3. var vm = this;
  4. // a uid
  5. // 记录实例化多少个vue对象
  6. vm._uid = uid$3++;
  7. // 选项合并,将合并后的选项赋值给实例的$options属性
  8. vm.$options = mergeOptions(
  9. resolveConstructorOptions(vm.constructor), // 返回Vue构造函数自身的配置项
  10. options || {},
  11. vm
  12. );
  13. };
  14. }

从代码中可以看到,选项合并的重点是将用户自身传递的options选项和Vue构造函数自身的选项配置合并。我们看看mergeOptions函数的实现。

  1. function mergeOptions (parent,child,vm) {
  2. {
  3. checkComponents(child);
  4. }
  5. if (typeof child === 'function') {
  6. child = child.options;
  7. }
  8. // props,inject,directives的校验和规范化
  9. normalizeProps(child, vm);
  10. normalizeInject(child, vm);
  11. normalizeDirectives(child);
  12. // 针对extends扩展的子类构造器
  13. if (!child._base) {
  14. // extends
  15. if (child.extends) {
  16. parent = mergeOptions(parent, child.extends, vm);
  17. }
  18. // mixins
  19. if (child.mixins) {
  20. for (var i = 0, l = child.mixins.length; i < l; i++) {
  21. parent = mergeOptions(parent, child.mixins[i], vm);
  22. }
  23. }
  24. }
  25. var options = {};
  26. var key;
  27. for (key in parent) {
  28. mergeField(key);
  29. }
  30. for (key in child) {
  31. if (!hasOwn(parent, key)) {
  32. mergeField(key);
  33. }
  34. }
  35. function mergeField (key) {
  36. // 拿到各个选择指定的选项配置,如果没有则用默认的配置
  37. var strat = strats[key] || defaultStrat;
  38. // 执行各自的合并策略
  39. options[key] = strat(parent[key], child[key], vm, key);
  40. }
  41. // console.log(options)
  42. return options
  43. }

选项合并过程中更多的不可控在于不知道用户传递了哪些配置选项,这些配置是否符合规范,是否达到合并配置的要求。因此每个选项的书写规则需要严格限定,原则上不允许用户脱离规则外来传递选项。因此在合并选项之前,很大的一部分工作是对选项的校验。其中components,prop,inject,directive等都是检验的重点。

1.3.1 components规范检验

如果项目中需要使用到组件,我们会在vue实例化时传入组件选项以此来注册组件。因此,组件命名需要遵守很多规范,比如组件名不能用html保留的标签(如:img,p),也不能包含非法的字符等。这些都会在validateComponentName函数做校验。

  1. // components规范检查函数
  2. function checkComponents (options) {
  3. // 遍历components对象,对每个属性值校验。
  4. for (var key in options.components) {
  5. validateComponentName(key);
  6. }
  7. }
  8. function validateComponentName (name) {
  9. if (!new RegExp(("^[a-zA-Z][\\-\\.0-9_" + (unicodeRegExp.source) + "]*$")).test(name)) {
  10. // 正则判断检测是否为非法的标签,例如数字开头
  11. warn(
  12. 'Invalid component name: "' + name + '". Component names ' +
  13. 'should conform to valid custom element name in html5 specification.'
  14. );
  15. }
  16. // 不能使用Vue自身自定义的组件名,如slot, component,不能使用html的保留标签,如 h1, svg等
  17. if (isBuiltInTag(name) || config.isReservedTag(name)) {
  18. warn(
  19. 'Do not use built-in or reserved HTML elements as component ' +
  20. 'id: ' + name
  21. );
  22. }
  23. }

1.3.2 props规范检验

Vue的官方文档规定了props选项的书写形式有两种,分别是

  1. 数组形式 { props: ['a', 'b', 'c'] },
  2. 带校验规则的对象形式 { props: { a: { type: 'String', default: 'prop校验' } }}从源码上看,两种形式最终都会转换成对象的形式。
  1. // props规范校验
  2. function normalizeProps (options, vm) {
  3. var props = options.props;
  4. if (!props) { return }
  5. var res = {};
  6. var i, val, name;
  7. // props选项数据有两种形式,一种是['a', 'b', 'c'],一种是{ a: { type: 'String', default: 'hahah' }}
  8. // 数组
  9. if (Array.isArray(props)) {
  10. i = props.length;
  11. while (i--) {
  12. val = props[i];
  13. if (typeof val === 'string') {
  14. name = camelize(val);
  15. // 默认将数组形式的props转换为对象形式。
  16. res[name] = { type: null };
  17. } else {
  18. // 规则:保证是字符串
  19. warn('props must be strings when using array syntax.');
  20. }
  21. }
  22. } else if (isPlainObject(props)) {
  23. for (var key in props) {
  24. val = props[key];
  25. name = camelize(key);
  26. res[name] = isPlainObject(val)
  27. ? val
  28. : { type: val };
  29. }
  30. } else {
  31. // 非数组,非对象则判定props选项传递非法
  32. warn(
  33. "Invalid value for option \"props\": expected an Array or an Object, " +
  34. "but got " + (toRawType(props)) + ".",
  35. vm
  36. );
  37. }
  38. options.props = res;
  39. }

1.3.3 inject的规范校验

provide/inject这对组合在我们日常开发中可能使用得比较少,当我们需要在父组件中提供数据或者方法给后代组件使用时可以用到provide/inject,注意关键是后代,而不单纯指子代,这是有别于props的使用场景。官方把它被称为依赖注入,依赖注入使得组件后代都能访问到父代注入的数据/方法,且后代不需要知道数据的来源。重要的一点,依赖提供的数据是非响应式的。

基本的使用如下:

  1. // 父组件
  2. var Provider = {
  3. provide: {
  4. foo: 'bar'
  5. },
  6. // ...
  7. }
  8. // 后代组件
  9. var Child = {
  10. // 数组写法
  11. inject: ['foo'],
  12. // 对象写法
  13. inject: {
  14. foo: {
  15. from: 'foo',
  16. default: 'bardefault'
  17. }
  18. }
  19. }

inject选项有两种写法,数组的方式以及对象的方式,和props的校验规则一致,最终inject都会转换为对象的形式存在。

  1. // inject的规范化
  2. function normalizeInject (options, vm) {
  3. var inject = options.inject;
  4. if (!inject) { return }
  5. var normalized = options.inject = {};
  6. //数组的形式
  7. if (Array.isArray(inject)) {
  8. for (var i = 0; i < inject.length; i++) {
  9. // from: 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
  10. normalized[inject[i]] = { from: inject[i] };
  11. }
  12. } else if (isPlainObject(inject)) {
  13. // 对象的处理
  14. for (var key in inject) {
  15. var val = inject[key];
  16. normalized[key] = isPlainObject(val)
  17. ? extend({ from: key }, val)
  18. : { from: val };
  19. }
  20. } else {
  21. // 非法规则
  22. warn(
  23. "Invalid value for option \"inject\": expected an Array or an Object, " +
  24. "but got " + (toRawType(inject)) + ".",
  25. vm
  26. );
  27. }
  28. }

1.3.4 directive的规范校验

我们先看看指令选项的用法,Vue允许我们自定义指令,并且它提供了五个钩子函数bind, inserted, update, componentUpdated, unbind,具体的用法可以参考官方-自定义指令文档,而除了可以以对象的形式去定义钩子函数外,官方还提供了一种函数的简写,例如:

  1. {
  2. directives: {
  3. 'color-swatch': function(el, binding) {
  4. el.style.backgroundColor = binding.value
  5. }
  6. }
  7. }

函数的写法会在bind,update钩子中触发相同的行为,并且不关心其他钩子。这个行为就是定义的函数。因此在对directives进行规范化时,针对函数的写法会将行为赋予bind,update钩子。

  1. function normalizeDirectives (options) {
  2. var dirs = options.directives;
  3. if (dirs) {
  4. for (var key in dirs) {
  5. var def###1 = dirs[key];
  6. // 函数简写同样会转换成对象的形式
  7. if (typeof def###1 === 'function') {
  8. dirs[key] = { bind: def###1, update: def###1 };
  9. }
  10. }
  11. }
  12. }

1.3.5 函数缓存

这个内容跟选项的规范化无关,当读到上面规范检测的代码时,笔者发现有一段函数优化的代码值得我们学习。它将每次执行函数后的值进行缓存,当再次执行的时候直接调用缓存的数据而不是重复执行函数,以此提高前端性能,这是典型的用空间换时间的优化,也是经典的偏函数应用。

  1. function cached (fn) {
  2. var cache = Object.create(null); // 创建空对象作为缓存对象
  3. return (function cachedFn (str) {
  4. var hit = cache[str];
  5. return hit || (cache[str] = fn(str)) // 每次执行时缓存对象有值则不需要执行函数方法,没有则执行并缓存起来
  6. })
  7. }
  8. var camelizeRE = /-(\w)/g;
  9. // 缓存会保存每次进行驼峰转换的结果
  10. var camelize = cached(function (str) {
  11. // 将诸如 'a-b'的写法统一处理成驼峰写法'aB'
  12. return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
  13. });