阅读 1962

Vue 源码解析(实例化前) - 初始化全局API(一)

前言

之前,我们在网上,可以看到很多有关vue部分功能的实现原理,尤其是数据双向绑定那一块的,文章很多,但是都是按照同样的思想去实现的一个数据双向绑定的功能,但不是vue的源码。

今天,我在一行一行的去看vue的所有代码,并挨个作出解释,这个时候我们可以发现,vue的细节,很值得我们去学习。

大家觉得写的有用的话,帮忙点个关注,点点赞,有问题可以评论,只要我看到,我会第一时间回复。

话不多说,直接开始了。

正文

初始化

initGlobalAPI(Vue);
复制代码

这个时候,初始化调用initGlobalAPI,传入Vue构造函数。这里是在Vue构造函数实例化之前要做的事情,所以这里先不讲Vue对象里面做了什么,先讲实例化之前做了什么。

function initGlobalAPI (Vue) {
  // config
  var configDef = {};
  configDef.get = function () { return config; };
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = function () {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      );
    };
  }
  Object.defineProperty(Vue, 'config', configDef);

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
  };

  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;

  Vue.options = Object.create(null);
  ASSET_TYPES.forEach(function (type) {
    Vue.options[type + 's'] = Object.create(null);
  });

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue;

  extend(Vue.options.components, builtInComponents);

  initUse(Vue);
  initMixin$1(Vue);
  initExtend(Vue);
  initAssetRegisters(Vue);
}
复制代码

这是initGlobalAPI方法的所有代码,行数不多,但是知识点很多。

var configDef = {};
复制代码

这个函数声明了一个configDef得空对象;

configDef.get = function () { return config; };
复制代码

然后在给configDef添加了一个get属性,这个属性返回得是一个config对象,这个cofig对象里面,有n个属性,下面来一一解释一下:

config对象

var config = ({
  optionMergeStrategies: Object.create(null),
  silent: false,
  productionTip: process.env.NODE_ENV !== 'production',
  devtools: process.env.NODE_ENV !== 'production',
  performance: false,
  errorHandler: null,
  warnHandler: null,
  ignoredElements: [],
  keyCodes: Object.create(null),
  isReservedTag: no,
  isReservedAttr: no,
  isUnknownElement: no,
  getTagNamespace: noop,
  parsePlatformTagName: identity,
  mustUseProp: no,
  _lifecycleHooks: LIFECYCLE_HOOKS
})
复制代码

optionMergeStrategies:选项合并,用于合并core / util / options

默认值:object.creart(null)

注:object.creart(null)去创建的一个是原子,什么是原子呢,就是它是对象,但是不继承Object() ,这里对原子的概念不做深究,大家如果感兴趣,可以百度去查“js元系统”,aimingoo对这方面有做过详细的说明。

silent:是否取消警告

默认值:false

productionTip:项目启动时,是否显示提示信息

默认值:process.env.NODE_ENV !== 'production'

如果是开发环境,则是true,表示显示提示信息,在生产环境则不显示

devtools:是否启用devtools

默认值:同productionTip

performance:是否记录性能

默认值:false

errorHandler:观察程序错误的错误处理程序

默认值:null

warnHandler:观察程序警告的警告处理程序

默认值:null

ignoredElements:忽略某些自定义元素

默认值:[]

keyCodes:v - on的自定义用户keyCode

默认值:object.creart(null)

isReservedTag:检查是否保留了标记,以便它不能注册为组件。这取决于平台,可能会被覆盖

var no = function (a, b, c) { return false; };
复制代码

默认值:一个名为no的function,这个function接收三个参数,但是结果永远返回的是false

isReservedAttr:检查属性是否被保留,以便不能用作组件道具。这取决于平台,可能会被覆盖

默认值:同上

isUnknownElement:检查标记是否为未知元素。取决于平台

默认值:同上

getTagNamespace:获取元素的命名空间

function noop (a, b, c) {}
复制代码

默认值:一个名为noop的函数,里面什么都没有做

parsePlatformTagName:解析特定平台的真实标签名称

var identity = function (_) { return _; };
复制代码

默认值:一个名为identity的函数,输入的什么就输出的什么

mustUseProp:检查是否必须使用属性(例如值)绑定属性。这个取决于平台

默认值:一个名为no的function

_lifecycleHooks:生命周期钩子数组

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured'
];
复制代码

默认值:一个数组,里面有所有生命周期的方法名

以上就是config里面所有的属性

config.set

if (process.env.NODE_ENV !== 'production') {
    configDef.set = function () {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      );
    };
  }
复制代码

做了一个判断是否是生产环境,如果不是生产环境,给configDef添加一个set方法

Object.defineProperty(Vue, 'config', configDef);
复制代码

在这里,为Vue的构造函数,添加一个要通过Object.defineProperty监听的属性config,获取的时候,获取到的是上面描述的那个config对象,如果对这个config对象直接做变更,就会提示“不要替换vue.config对象,而是设置单个字段”,说明,作者不希望我们直接去替换和变更整个config对象,如果有需要,希望去直接修改我们需要修改的值

公开util

Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive
};
复制代码

在这里,设置了一个公开的util对象,但是它不是公共的api,避免依赖,除非你意识到了风险,下面来介绍一下它的属性:

warn:警示
var warn = noop;
var generateComponentTrace = (noop);
if (process.env.NODE_ENV !== 'production') {
    warn = function (msg, vm) {
        var trace = vm ? generateComponentTrace(vm) : '';
        if (config.warnHandler) {
          config.warnHandler.call(null, msg, vm, trace);
        } else if (hasConsole && (!config.silent)) {
          console.error(("[Vue warn]: " + msg + trace));
        }
  };
}
复制代码

warn是一个function,初始化的时候,只定义了一个noop方法,如果在开发环境,这个warn是可以接收两个参数,一个是msg,一个是vm,msg不用说,大家都知道这里是一个提示信息,vm就是实例化的vue对象,或者是实例化的vue对象的某一个属性。

接下来是一个三元表达式trace,用来判断调用warn方法时,是否有传入了vm,如果没有,返回的是空,如果有,那么就返回generateComponentTrace这个function,这个方法初始化的值也是noop,什么都没有做,目的是解决流量检查问题

如果config.warnHandler被使用者变更成了值,不在是null,那么就把config.warnHandler的this指向null,其实就是指向的window,再把msg, vm, trace传给config.warnHandler

否则判断当前环境使用支持conosle并且开启了警告,如果开启了,那就把警告提示信息打印出来

extend:继承
function extend (to, _from) {
  for (var key in _from) {
    to[key] = _from[key];
  }
  return to
}
复制代码

这个方法是用于做继承操作的,接收两个值to, _from,将属性_from混合到目标对象to中,如果to存在_from中的属性,则直接覆盖,最后返回新的to

mergeOptions:将两个选项对象合并为一个新对象,用于实例化和继承的核心实用程序(这是一个很重要的方法,在后面多处会用到,所以建议大家仔细看这里)
function mergeOptions (parent, child, vm) {

  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
  }

  if (typeof child === 'function') {
    child = child.options;
  }

  normalizeProps(child, vm);
  normalizeInject(child, vm);
  normalizeDirecitives(child);
  var extendsFrom = child.extends;
  if (extendsFrom) {
    parent = mergeOptions(parent, extendsFrom, 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
}
复制代码
if (process.env.NODE_ENV !== 'production') {
    checkComponents(child);
}
function checkComponents (options) {
  for (var key in options.components) {
    validateComponentName(key);
  }
}
function validateComponentName (name) {
  if (!/^[a-zA-Z][\w-]*$/.test(name)) {
    warn(
      'Invalid component name: "' + name + '". Component names ' +
      'can only contain alphanumeric characters and the hyphen, ' +
      'and must start with a letter.'
    );
  }
  if (isBuiltInTag(name) || config.isReservedTag(name)) {
    warn(
      'Do not use built-in or reserved HTML elements as component ' +
      'id: ' + name
    );
  }
}
复制代码

这个方法接收三个参数parent,child,vm,在不是生产环境的情况下,会去检测参数child中,是否存在components,如果存在该对象,遍历所有的componets,进行名称是否符合规范,这里有一个正则,是用来判断以字母开头,以0个或多个任意字母和字符“-”结尾的字符串,如果不符合这个规定的话,就会提示警告信息

if (typeof child === 'function') {
    child = child.options;
}
复制代码

如果child是一个function的话,则把child自己指向child的options属性

接下来要做的就是规范child里面的Props、Inject、Direcitives

normalizeProps(child, vm);
normalizeInject(child, vm);
normalizeDirecitives(child);
复制代码
normalizeProps:规范属性,确保所有的props的规范都是基于对象的
function normalizeProps (options, vm) {
  var props = options.props;
  if (!props) { return }
  var res = {};
  var i, val, name;
  if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== 'production') {
        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 if (process.env.NODE_ENV !== 'production') {
    warn(
      "Invalid value for option \"props\": expected an Array or an Object, " +
      "but got " + (toRawType(props)) + ".",
      vm
    );
  }
  options.props = res;
}

复制代码
var props = options.props;
if (!props) { return }
复制代码

一开始,会检查child是否存在props属性,如果不存在,直接return出去,如果存在的话则是去声明了几个变量,一个名为res的对象,还有i, val, name

if (Array.isArray(props)) {
    i = props.length;
    while (i--) {
      val = props[i];
      if (typeof val === 'string') {
        name = camelize(val);
        res[name] = { type: null };
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.');
      }
    }
  }
复制代码

检查props是数组还是对象,如果是数组的话,则是去循环它,并判断每一个数组项,是否是字符串,如果是字符串那么就去执行camelize方法。

camelize:

var camelizeRE = /-(\w)/g;
var camelize = cached(function (str) {
  return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; })
});
复制代码

把名称格式为“xx-xx”的变为“xxXx”,这里接收的是当前的props属性值,一个字符串

cached:

function cached (fn) {
  var cache = Object.create(null);
  return (function cachedFn (str) {
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
复制代码

在调用camelize方法的时候,camelize调用了cached,这是一个暂存式函数,对暂存式函数不了解的朋友,可以去看看函数式编程,在cached也是创建了一个原子cache,然后会返回一个cachedFn方法,这里会检测cache是否存在当前props属性值的属性,如果存在,直接返回,如果不存在,则是调用,调用cached的方法传过来的function,在调用cached方法的方法中返回的结果,返回到调用cached方法的方法(这句话我知道很绕口,但是我只会这么解释,哪位大佬有更好的表述方式,欢迎评论,我做修改)

然后把所有的数组项,并且是字符串的,全部都遍历一遍,做这样的处理,然后在res对象里面,去添加一个属性,它是一个对象,属性名就是遍历后的这个遍历后的值(把-转换成大写字母),属性值有一个初始化的type属性,值为null

当然不是生产环境下,并且props虽然是数组,但是数组项不是字符串的话,会警告你“使用数组语法时,props必须是字符串”

var _toString = Object.prototype.toString;
function isPlainObject (obj) {
  return _toString.call(obj) === '[object Object]'
}
else if (isPlainObject(props)) {
    for (var key in props) {
      val = props[key];
      name = camelize(key);
      res[name] = isPlainObject(val)
        ? val
        : { type: val };
    }
}
options.props = res;
复制代码

如果child的props不是数组,使用isPlainObject去判断props是否是对象,这个方法代码就一行,很简单,也比较好理解,我也就不浪费篇幅去解释了;

如果是对象的话,就去遍历它,把所有的属性名按照上面数组项的处理方式,去处理所有的数组名,并且当作res的属性名,该属性名的值需要去判断原props的该属性的值是否是对象,如果是对象,直接当作当前属性名的属性值,如果不是的话,则给当前处理后的属性名,传一个对象,type属性的值就是原props该属性名的属性值

这里,就把child里面所有的props给规范化了,最后覆盖了源child的props属性(这一个方法的内容真多,各种知识点,有没有,点波赞吧)

normalizeInject:规范Inject
function normalizeInject (options, vm) {
  var inject = options.inject;
  if (!inject) { return }
  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 if (process.env.NODE_ENV !== 'production') {
    warn(
      "Invalid value for option \"inject\": expected an Array or an Object, " +
      "but got " + (toRawType(inject)) + ".",
      vm
    );
  }
}
复制代码

和props一样,先检查是否存在,不存在直接返回;

如果存在的话,把child的inject存在一个变量inject里,把child里面的inject变成空对象,并且把该值传给一个normalized的变量;

如果inject是一个数组的话,则遍历它,normalized的每一个属性名,就是每一个inject的数组项,每一个属性值都是一个对象,对象的属性from的值,就是每一个inject的数组项

如果inject是一个对象的话,则遍历它,把每一个属性值存为变量val,normalized的key,就是inject的key,如果val是一个对象的话,则把{ from: key }和val合并,val覆盖{ from: key }

normalizeDirectives:规范Directives
function normalizeDirectives (options) {
  var dirs = options.directives;
  if (dirs) {
    for (var key in dirs) {
      var def = dirs[key];
      if (typeof def === 'function') {
        dirs[key] = { bind: def, update: def };
      }
    }
  }
}
复制代码

源码里只处理了child.directives的对象格式,如果存在的话遍历它,如果每一个属性值def都是function的话则把每一个directives的属性值改为{ bind: def, update: def };

到这里,规范化的事情就做完了,休息一下,点个关注点个赞,咱们继续。

var extendsFrom = child.extends;
if (extendsFrom) {
parent = mergeOptions(parent, extendsFrom, vm);
}
复制代码

看child是否存在extends,递归当前的mergeOptions方法,parent就是当前的parent,child就是当前child的extends的值;

if (child.mixins) {
    for (var i = 0, l = child.mixins.length; i < l; i++) {
      parent = mergeOptions(parent, child.mixins[i], vm);
    }
}
复制代码

检测child是否存在mixins,如果存在的话,递归当前的mergeOptions方法,并把最新的结果,去覆盖上一次调用mergeOptions方法的parent;

var defaultStrat = function (parentVal, childVal) {
  return childVal === undefined
    ? parentVal
    : childVal
};

var strats = config.optionMergeStrategies;//这只是初始化的值

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
复制代码

现在声明了一个options的对象,然后分别去遍历了parent和child,parent和child的key传给了一个mergeField的方法;

在mergeField中声明一个start变量,如果strats下的存在当前这个key的属性,则返回,否则就返回一个默认的defaultStrat;

defaultStrat接收两个参数,第一个参数是parent,第二个是child,如果child存在就返回child,否则就返回parent;

把mergeField接收到的key,当作之前optins的key,它的值就是前面返回的变量start方法返回的值;

最后,把整个options返回。

结束语

到这里,Vue.util的四个属性已经讲了三个了,第四个属性是一个defineReactive方法,我不打算在这一篇去讲,因为这个方法,就是实现一个数据双向绑定的核心方法,内容可能会比较多,而且这一篇的内容也已经够长了,写的再多的话,不适合学习了,所以我打算在下一篇单独去讲一下defineReactive这个方法。

这篇文章,是vue源码解析的起始篇,接下来我会持续更新该系列的文章,欢迎大家批评和点评,还是老话,多点关注,多点赞😊

谢谢大家。

关注下面的标签,发现更多相似文章
评论