前言
之前,我们在网上,可以看到很多有关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源码解析的起始篇,接下来我会持续更新该系列的文章,欢迎大家批评和点评,还是老话,多点关注,多点赞😊
谢谢大家。