Vue源码笔记-1.选项合并策略

497 阅读11分钟

1.Vue的引入

1.1构造器

  • Vue的==本质是一个构造器==
    • 遵循UMD规范
  • function Vue内部做了两件事
    • 禁止直接调用,只能通过new的形式创建实例
    • 调用 this._init(options)方法初始化
function Vue (options) {
    // 保证了无法直接通过Vue()去调用,只能通过new的方式去创建实例
    if (!(this instanceof Vue)
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }

1.2 定义原型上的属性方法(简介)

initMixin

  • 内部给Vue原型添加 _init 方法,在_init内部定义初始化时vue内部的初始化操作

stateMixin

  • 在其内部完成与数据相关的属性方法,如this.$data等代理数据访问方法

eventsMixin

  • 定义事件相关方法,如vm.$on
  • 共实现了4个事件方法
    • $on,绑定事件和处理函数
    • $off,取消绑定函数
    • $once,一次性的事件绑定
    • $emit,发射,相当于手动触发事件

lifecycleMixin

  • 定义生命周期相关方法
  • 共实现了3个方法
    • _update更新
    • $forceUpdate强制更新
    • $destroy销毁

rednerMixin

  • 定义与渲染相关的方法
  • $nextTick 将回调延迟到下一轮循环
  • _render 渲染

1.3静态属性和方法的定义

  • 在initGlobalAPI中定义了丰富的全局API方法
    • vue.config的读取
    • 工具类(如warn)配置(不暴露给公共使用)
    • vue.set vue.del vue.nextTick
    • Vue.components,Vue.directive,Vue.filter的定义,这些是默认的资源选项构造函数options选项
    • Vue.use Vue.mixin Vue.extend

2.构造器的默认选项

  • 为components提供内置组件 KeepAlive、transition、transitionGroup
  • 为directives提供内置指令 v-model、v-show
  • filter配置项(没有默认值)
  • 构造器自身 _base

extend方法

  • 功能:对象合并
  • 参数:对象1 to、对象2 _from
  • 操作:将_from合并到to,若其中要相同属性,则覆盖to的属性
  • 返回值:to
// 将_from对象合并到to对象,属性相同时,则覆盖to对象的属性
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}

  • Vue中构造的上面那些内置组件、指令通过extend 方法合并到components和directives中

总结:Vue构造器的默认配置

Vue.options = {
  components: {
    KeepAlive: {}
    Transition: {}
    TransitionGroup: {}
  },
  directives: {
    model: {inserted: ƒ, componentUpdated: ƒ}
    show: {bind: ƒ, update: ƒ, unbind: ƒ}
  },
  filters: {},
  _base
}

3.合并前的选项校验

  • 初始化的第一步就是选项合并,将默认配置和用户输入的配置合并起来,并挂在到$options上(vue实例中可以访问该变量)

  • 而合并的第一步就是校验。因为用户的输入是不可控的,所以要设置严格的规范限制,并在合并前检查用户是否遵从了这些规范

  • 检查的重点在 props、inject、directives

    // props,inject,directives的校验和规范化
    normalizeProps(child, vm);
    normalizeInject(child, vm);
    normalizeDirectives(child);
  • 除此以外还有其他选项需要校验

components规范校验

  • 封装在方法checkComponents中,在合并选项方法mergeOptions中调用

  • 功能:遍历每个components对象的属性值,对其进行校验

  for (var key in options.components) {
    validateComponentName(key);
  }

validateComponentName方法

  • 功能:校验标签合法性
    • 命名是否符合规则,如不能使用数字开头
    • 不能使用vue预制的组件名或html的保留字

props规范校验

props的形式

  • props有两种书写形式

    • 数组形式:props:['a','b']
    • 对象形式:props:{a:{type:'String',default:''},b:{...}}
  • 实际最终都会在内部统一成对象形式

props校验:normalizeProps

一些用到的工具方法

  • camelize:统一命名形式为驼峰形式,即将a-b改为aB
  • isPlainObject:判断是否为简单对象,即使用{}或new Object创建

校验流程

  1. 判断是数组形式还是对象形式:Array.isArray判断数组,isPlainObject判断对象
  2. 是数组
    1. 倒序遍历,拿出每个数组元素,判断是否为string类型。数组形式的传值必须为string
    2. 是string类型,调用camelize统一形式,然后改写成对象形式,保存在res中
    3. 不是string,弹出错误警告
  3. 是对象
    1. 遍历props中的每个对象
    2. 将其key规范化
    3. 将type规范化
    4. 保存到res中
  4. 不是数组也不是对象:弹出错误警告

inject规范校验

provide/inject简介

  • 作用:用于父组件向【后代】组件传递数据
  • 特点:
    • 后代不局限于子代,所有后代都可以使用其提供的数据。后代不需要知道数据来自哪里
    • 非响应式:即之后这些值的修改不会在后代中被实时更新

改为响应式

官方文档说明:刻意设计成不可响应式,但如果传入可监听对象,那么还是可响应的

  • 方法1
    • 将值转为函数,要用箭头函数保证this指向,在return中使用this.val传值
    • 使用时变成调用函数的形式
    • **缺点:函数调用频繁。**比如下面代码,使用了两个地方用到了color,就会调用两次此函数
//提供
provide(){
    return {
        color:()=>{
            return this.color
        }
    }
}


//使用
<template>
  <div :style="{'color':color()}">传下来的颜色{{color()}}</div>
</template>

  • 方法2
    • 改为传递Vue实例
    • 后代中实际获得的是实例对象,要通过 .xxx的形式获取该属性
    • 这是很多UI组件库会用到的解决方法
    • 更进一步可以直接在root上设置provide
//提供
provide(){
    return {
        color:this
    }
}

//使用
<template>
  <div :style="{'color':color.color}">传下来的颜色{{color.color}}</div>
</template>

链式查找

  • provide像原型链一样,是会向上查找,找到就停止的
  • 即:祖父提供color,父亲提供color,子孙组件只会找到父亲提供的color

内存泄漏

子组件图表可以利用provide提供的方法一直保持在内存中,导致内存占用,页面卡顿

inject校验:normalizeInject

  • inject和props一样有对象和数组两种形式,且基本的书写规范也是一致的,所以校验流程也是一样的。详见上面props

directive规范校验

directive简介

  • 功能:自定义指令
  • 特点:五个钩子函数bind, inserted, update, componentUpdated, unbind
    • 钩子函数用于在对应的时间节点触发,调用绑定在其上的函数方法

用法

定义可以分为两种版本,完整版和简写版

  • 完整版:手动指定在何时触发方法
directives: {
  focus: {
    // 指令的定义
    inserted: function (el) {
      el.focus()
    }
  }
}
  • 简写版:方法自动被绑定在bind和update上
  directives: {
    'focus': function(el, binding) {
        el.style.backgroundColor = binding.value
    }
  }

directives校验:normalizeDirectives

  • 流程:遍历对象,将简写版改成对象形式

4.子类构造器

  • 子类构造器即Vue的extend
  • 作用:生成一个Vue实例的子类
  • 通常用在生成组件类,在挂载全局组件和设置了components属性的时候会使用到。在生成DOM的时候会new 实例化挂载

合并规则

  • 重复的,子类覆盖父类
  • 缺少的,使用父类上的

extend的实现

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 // 返回子类构造函数
};

5.合并策略总览

  1. Vue针对每个规定的选项都有定义好的合并策略,例如data,component,mounted等。如果合并的子父配置都具有相同的选项,则只需要按照规定好的策略进行选项合并即可。
  2. 由于Vue传递的选项是开放式的,所有也存在传递的选项没有自定义选项的情况,这时候由于选项不存在默认的合并策略,所以处理的原则是有子类配置选项则默认使用子类配置选项,没有则选择父类配置选项。
  • 在完成上面讲过的规则校验后,合并方法mergeOptions的后续逻辑

实现方法

  function mergeField (key) {
    // 如果有自定义选项策略,则使用自定义选项策略,否则选择使用默认策略,每个策略都一个函数。
    var strat = strats[key] || defaultStrat; 
    options[key] = strat(parent[key], child[key], vm, key);
  }

调用

//parent和child是mergeOptions方法的参数,别忘了这些代码都在mergeOptions内
var options = {};
  var key;
//以自定义选项优先,没有则使用默认策略
  for (key in parent) {
    mergeField(key);
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key);
    }
  }

默认策略defaultStrat

  • 子存在则用子,不存在则用父
// 用户自定义选项策略
var defaultStrat = function (parentVal, childVal) {
  // 子不存在则用父,子存在则用子配置
  return childVal === undefined
    ? parentVal
    : childVal
};

6.具体合并I:常规选项的合并

(1)el的合并

  • el是用于将vue实例挂载在已有的dom上,因此==子类和子组件上不能设定el==

  • 合并策略

    • 判断不是子类或子组件
    • 使用默认策略进行合并
strats.el = function (parent, child, vm, key) {
  if (!vm) {  // 只允许vue实例才拥有el属性,其他子类构造器不允许有el属性
    warn(
      "option \"" + key + "\" can only be used during instance " +
      'creation with the `new` keyword.'
    );
  }
  // 默认策略
  return defaultStrat(parent, child)
};

(2)data的合并

  • vue的data合并可以分三层来看
  • data的合并方法有三个参数
    • parentVal----父类值
    • childVal----子类值
    • vm----实例

x1 基本判断

  • 众所周知data在组件中必须以函数return的形式存在

  • 所以data合并的第一步就是判断输入的是否为Vue实例,根据vm是否存在来判断

    • 不是,则再判断是否为函数形式
      • 是,进行合并
      • 否,警告,返回父类值
    • 是,直接进行合并
// strats合并策略中data的合并策略
strats.data = function (parentVal, childVal, vm) {
  // vm代表是否为Vue创建的实例,vm不存在则说明是子父类的关系
  if (!vm) {
    if (childVal && typeof childVal !== 'function') { // 必须保证子类的data类型是一个函数而不是一个对象
      warn('The "data" option should be a function ' + 'that returns a per-instance value in component ' + 'definitions.',vm);
      return parentVal
    }
    return mergeDataOrFn(parentVal, childVal)
  }
  return mergeDataOrFn(parentVal, childVal, vm); // 是vue实例时需要传递vm作为函数的第三个参数
};

x2 mergeDataOrFn

  • 01-三个参数

    • parentVal
    • childVal
    • vm
  • 02-流程:判断是否是实例,仍然通过参数的vm是否被传入来判断

    • 不是实例,则判断父子类是否都存在

      • 子类不存在,则return父类parentVal

      • 父类不存在,则return子类childVal

      • 父子都存在,则return一个函数mergedDataFn。函数参数为父子类的data。(判断parentVal和childVal是否是函数形式,是则调用函数获取return的data;否则说明其就是data,直接传入)

      • //mergeData实现在后面讲,这里只需要知道它是用来合并的就好
        return mergeData(
                typeof childVal === 'function' ? childVal.call(this, this) : childVal,
                typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
              )
        
    • 是Vue实例

      • 同上判断实例传入的data形式获取实际data
      • 获取vm构造函数默认data选项
      • 若实例未传data,则直接return默认data
      • 若实例传data,则return mergeData(instanceData,defaultData)
        • mergeData的实现在后面讲
  • 03-注意!上述流程不是直接在mergeDataOrFn中进行的

    • 在此函数中只做了一件事:判断传入的是否是实例,根据情况返回不同的函数闭包。
      • 不是则return mergedDataFn
      • 是则return mergedInstanceDataFn
    • 前面02中的实现都是在这两个函数中,return也是指这两个函数的return而不是mergeDataOrFn的return
  • 04-结构示意

function mergeDataOrFn (parentVal,childVal,vm){
	if(!vm){ //是父子类
		if(){ //子类data选项不存在
        	return parentVal 
    	}
        if(){ //父类data选项不存在
			return childVal
        }
        //上面都不符合,即父子都有data,需要合并
        return function mergedDataFn(){
            return mergeData(...)
        }
    	
	}else{ //是Vue实例
		var instanceData=vm实例配置的data选项
        var defaultData=vm构造函数默认的data选项
        if(){ //vm实例有配置data,与默认data合并
        	 return mergeData(...)
        }else{ //vm实例没有配置data,返回默认data
            return defaultData
        }
	}
}
  • ps:provide的默认策略也是这个,inject的则在后面讲

x3 mergeData

  • 01-功能:合并data。mergeData才是实际进行合并的地方

  • 02-参数

    • to:子类data(实例data)
    • from:父类data(默认data)
  • 03-流程:从父类data拿出key,按照key遍历父子data选项

    • 将父类中有而子类中没有的值添加到子类
    if (!hasOwn(to, key)) {
          // 
          set(to, key, fromVal); 
        }
    
    • 判断父子类是否为复杂数据类型,是则要递归调用mergeData实现深度拷贝
    • 最后处理完的子类data(参数to)即为合并结果,return to
  • 04-补充

    • 获得key的方法
    var keys = hasSymbol
          ? Reflect.ownKeys(from)
          : Object.keys(from);
    
  • 总结:mergeData的策略是:将父类整合到子类,其中父子都有则用子,子没有则用父

7.具体合并2:自带资源选项合并

  • 01-资源选项和默认合并策略
// 资源选项
var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

// 定义资源合并的策略
ASSET_TYPES.forEach(function (type) {
  strats[type + 's'] = mergeAssets; // 定义默认策略
});
  • 02-mergeAssets的实现
    • 功能:资源选项合并
    • 流程
      • 以父类资源选项为原型创建空对象
      • 若子类的资源选项存在,则将其合并到该空对象上,将结果return(合并前还要判断保证资源选项是一个对象的形式)
      • 子类没有资源选项,则return该空对象
// 资源选项自定义合并策略
//参数:1父类资源选项,2子类资源选项,3vm实例,4要合并的值的名称(比如components,directives等)
function mergeAssets (parentVal,childVal,vm,key) {
  //create的第一个参数是要创建的对象的原型
  var res = Object.create(parentVal || null); // 创建一个空对象,其原型指向父类的资源选项。
  if (childVal) {
    assertObjectType(key, childVal, vm); // components,filters,directives选项必须为对象
    return extend(res, childVal) // 子类选项赋值给空对象
  } else {
    return res
  }
}
  • 合并结果示例:如下可见,子类必须通过原型链才能找到父类上的资源
var vm = new Vue({
  components: {
    componentA: {}
  },
  directives: {
    'v-boom': {}
  }
})

console.log(vm.$options.components)
// 根实例的选项和资源默认选项合并后的结果
{
  components: {
    componentA: {},
    __proto__: {
      KeepAlive: {}
      Transition: {}
      TransitionGroup: {}
    } 
  },
  directives: {
    'v-boom': {},
    __proto__: {
      'v-show': {},
      'v-model': {}
    }
  }
}

8.具体合并3:生命周期钩子函数合并

  • 01-生命周期钩子和默认合并策略
var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];
LIFECYCLE_HOOKS.forEach(function (hook) {
  strats[hook] = mergeHook; // 对生命周期钩子选项的合并都执行mergeHook策略
});

  • 02-mergeHook实现

    • 功能:合并父子类的钩子函数选项

    • 参数:parentVal,childVal

    • 注意,钩子函数==有先后顺序,且父类的应该在子类的前面调用。==所以钩子函数应该以数组的形式存储,按序调用

    • 流程:通过判断父子选项的情况进行合并

      • 父子都有,则用concat合并数组,返回合并结果
      • 父无子有,则保证子为一个数组,返回子
      • 父有子无,则返回父的数组
      var res = childVal ? parentVal ? parentVal.concat(childVal) : Array.isArray(childVal) ? childVal : [childVal] : parentVal
      
      • 上述判断结果并非直接返回,而是先赋值给变量res。通过对res进行去重后再返回
      return res
            ? dedupeHooks(res)
            : res
      
  • **03-去重方法dedupeHooks **

    • 功能:给钩子函数数组去重,避免多个组件的钩子函数相互影响
    • 参数:hooks钩子函数数组
    • 流程:就是简单的数组去重流程,将结果赋给一个新数组res,return res
  function dedupeHooks (hooks) {
    var res = [];
    for (var i = 0; i < hooks.length; i++) {
      if (res.indexOf(hooks[i]) === -1) {
        res.push(hooks[i]);
      }
    }
    return res
  }
  • 04-总结:合并的结果是一个数组,数组中包含父类和子类的钩子函数。在执行时先执行父类的钩子函数再执行子类的钩子函数

9.具体合并4:watch选项合并

  • 01-合并策略:watch合并和钩子函数类似,也需要先执行父类再执行子类,因此也是以数组形式存储。区别是钩子函数结果必须是函数。watch选项的结果允许是对象、函数、方法名

  • 02-流程

    • 对火狐浏览器上object自带watch的兼容(略)
    • 父有子无,return 以父为原型的空对象
    • 确保watch是一个对象
    • 子有父无,return 子
    • 父子都有
      • 声明空对象ret,将ret与父值合并
      • 以子的属性名为key进行循环,分别获取父子的该属性值
      • 获取的父属性转为数组形式
      • 若父存在,则父concat子;若不存在,则将子转为数组。结果赋值给ret对象作为该key的属性值
      • return ret
  • 03-例子:先执行父的watch,再执行子的watch

var Parent = Vue.extend({
  watch: {
    'test': function() {
      console.log('parent change')
    }
  }
})
var Child = Parent.extend({
  watch: {
    'test': {
      handler: function() {
        console.log('child change')
      }
    }
  },
  data() {
    return {
      test: 1
    }
  }
})
var vm = new Child().$mount('#app');
vm.test = 2;
// 输出结果
parent change
child change

  • 04-总结:最终结果是将父子选项合并成一个数组。数组的值可以是对象、函数、函数名

10.具体合并5:props、methods等相同合并策略选项合并

  • 属性:props、methods、inject、computed

  • 策略:

    • 子不存在用父
    • 子存在用子
    • 相同选项子优先,子覆盖父
 //四个连等,都是一个方法
  strats.props =
  strats.methods =
  strats.inject =
  strats.computed = function (
    parentVal,
    childVal,
    vm,
    key
  ) {
    if (childVal && "development" !== 'production') {
      assertObjectType(key, childVal, vm);
    }
    //父不存在则直接用子
    if (!parentVal) { return childVal }
    var ret = Object.create(null);
    extend(ret, parentVal);
    //子覆盖父
    if (childVal) { extend(ret, childVal); }
    return ret
  };