虽然vue3已经出来很久了,但我觉得vue.js的源码还是非常值得去学习一下。vue.js里面封装的很多工具类在我们平时工作项目中也会经常用到。所以我近期会对vue.js的源码进行解读,分享值得去学习的代码片段,这篇文章将会持续更新。
一、1200~2400代码有哪些内容?:
1.一系列合并策略:
①选项 el、propsData 的合并策略
strats.el ,strats.propsData
②选项data的合并策略
strats.data
③生命周期钩子选项的合并策略
mergeHoo
④资源(assets)选项的合并策略
mergeAssets
⑤选项 watch 的合并策略
strats.watch
⑥选项 props、methods、inject、computed 的合并策略
strats.props,strats.methods ,strats.inject , strats.computed,都是使用相同的合并策略函数
⑦provide 的合并策略,与data的合并策略相同
strats.provide
⑧合并策略函数
mergeOptions
2.一些属性的校验和规范化
①validateComponentName:组件名的校验
②normalizeProps:对props的属性规范化
③normalizeInject:对inject的属性规范化
④normalizeDirectives:对directives的属性规范化
3.一些警告信息,异常捕获,异常处理等
二.1200~2400行代码的重点:
nextTick核心源码
1.nextTick会优先使用microTask(微任务), 其次是macroTask(宏任务),MutationObserver是nextTick的核心,MutationObserver接口提供了监视对DOM树所做更改的能力,MutationObserver也是微任务,所以面试的时候如果面试官问有哪些微任务,如果能答到MutationObserver是不是非常加分呢?
2.vue项目中如果需要获取修改后的dom信息,需要通过nextTick在dom更新任务之后创建一个异步任务,也就是官网所说的:nextTick会在下次 DOM 更新循环结束之后执行延迟回调.
三、1200~2400行的代码解读:
/**
* 删除属性并在必要时触发更改
*/
function del (target, key) {
//判断数据 是否是undefined或者null
// 判断数据类型是否是string,number,symbol,boolean
if (isUndef(target) || isPrimitive(target)
) {
warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
}
//判断是否是数组,并是否是有效的数组索引
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 像数组尾部添加一个新数据,相当于push
target.splice(key, 1);
return
}
// 声明一个对象ob 值为该target对象中的原型上面的所有方法和属性,表明该数据加入过观察者中
var ob = (target).__ob__;
// 如果是vue或者检测vue被实例化的次数vmCount
if (target._isVue || (ob && ob.vmCount)) {
warn(
'Avoid deleting properties on a Vue instance or its root $data ' +
'- just set it to null.'
);
return
}
//检查对象是否具有该属性
if (!hasOwn(target, key)) {
return
}
delete target[key];
if (!ob) {
return
}
// 触发通知更新,通知订阅者obj.value更新数据
ob.dep.notify();
}
// dependArray函数,其实就是遍历数组,对数组每个元素触发dep.depend收集依赖,因为不能直接对数组进行收集依赖。
// 在接触数组时收集对数组元素的依赖关系,因为我们不能像属性getter那样拦截数组元素访问。
function dependArray (value) {
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
e = value[i];
// 判断是否存在__ob__实例,并且每个都调用depend添加wathcer管理
e && e.__ob__ && e.__ob__.dep.depend();
if (Array.isArray(e)) {
// 递归完数组所有内容,直到不是数组,跳出递归
dependArray(e);
}
}
}
//选项 el、propsData 的合并策略
// 选项覆盖策略是处理如何合并父选项值和子选项值转换为最终值。
// config.optionMergeStrategies定义一个合并策略,其实就是vue的mixins属性
//propsData 用在全局扩展时进行传递数据
var strats = config.optionMergeStrategies
{
// 在非生产环境下在 strats 策略对象上添加两个策略分别是el和propsData,两个属性值都是函数
// 这两个策略函数是用来合并 el 选项和 propsData 选项的
strats.el = strats.propsData = function (parent, child, vm, key) {
//如果没有传递
if (!vm) {
warn(
"option \"" + key + "\" can only be used during instance " +
'creation with the `new` keyword.'
);
}
//在生产环境将直接使用默认的策略函数 defaultStrat 来处理 el 和 propsData 这两个选项
return defaultStrat(parent, child)
};
}
//通过_init方法可以看出,策略函数中的 vm 来自于 mergeOptions 函数的第三个参数,
//mergeOptions函数传第三个参数,策略中就拿不到vm参数,除了_init方法中调用了mergeOptions函数,其他很多地方都调用了
//Vue.extend方法中也调用了mergeoptions函数
//此时在Vue.extend方法中调用了mergeOptions函数,但是没有传第三个参数vm,所以在策略中无法拿到vm,
//就能得出mergeOptions函数是在实例化时使用new操作符走_init方法还是继承时走的Vue.extend方法
//子组件是通过实例化子类完成的,子类是通过Vue.extend方法创造出来的,
//综上得出可以通过if(!vm)判断是否是子组件
//递归合并两个数据对象,to 对应的是 childVal 产生的纯对象,from 对应 parentVal 产生的纯对象
function mergeData (to, from) {
//如果没有form直接返回参数to
if (!from) { return to }
var key, toVal, fromVal;
//如果是Symbol类型
var keys = hasSymbol
? Reflect.ownKeys(from)//返回from的所以属性key组成的数组
: Object.keys(from);//返回from的属性key,但不包括不可枚举的属性
//循环from属性key的数组
for (var i = 0; i < keys.length; i++) {
key = keys[i];
// 如果当前key为object类型,则继续
if (key === '__ob__') { continue }
toVal = to[key];
fromVal = from[key];
//检查对象是否具有该属性,如果有的话就给to[key]设置fromVal属性
if (!hasOwn(to, key)) {
set(to, key, fromVal);
} else if (
//判断toVal,fromVal是否是通过"{}"或"new Object"创建,并且toVal!=fromVal
//这个方法的作用是为了跟其他的 JavaScript对象如 null,数组,宿主对象(documents),
//DOM 等作区分,因为这些用 typeof 都会返回object
toVal !== fromVal &&
isPlainObject(toVal) &&
isPlainObject(fromVal)
) {
mergeData(toVal, fromVal);
}
}
return to
}
//mergeDataOrFn函数永远返回一个函数
function mergeDataOrFn (
parentVal,
childVal,
vm
) {
//如果没有vm,则说明是子组件选项
if (!vm) {
if (!childVal) {
return parentVal
}
if (!parentVal) {
return childVal
}
// 当parentVal和childVal都存在时,我们需要返回一个函数来返回两个函数的合并结果,
// 不用检查parentVal是否是函数,因为它必须是传递先前合并的函数。
return function mergedDataFn () {
return mergeData(
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
)
}
} else {
// 当合并处理的是非子组件的选项时 `data` 函数为 `mergedInstanceDataFn` 函数
return function mergedInstanceDataFn () {
// instance merge
var instanceData = typeof childVal === 'function'
? childVal.call(vm, vm)
: childVal;
var defaultData = typeof parentVal === 'function'
? parentVal.call(vm, vm)
: parentVal;
if (instanceData) {
return mergeData(instanceData, defaultData)
} else {
return defaultData
}
}
}
}
//选项data的合并策略,在 strats 策略对象上添加 data 策略函数,用来合并处理 data 选项
strats.data = function (
parentVal,
childVal,
vm
) {
//当没有 vm 参数时,说明处理的是子组件的选项
if (!vm) {
//判断childVal是不是函数
if (childVal && typeof childVal !== 'function') {
warn(
'The "data" option should be a function ' +
'that returns a per-instance value in component ' +
'definitions.',
vm
);
//如果不是函数直接返回 parentVal
return parentVal
}
//无论是否为子组件选项都会调用mergeDataOrFn方法,但是如果是子组件选项会传第三个参数vm,
// 由此可在mergeDataOrFn函数内判断是否为子组件选项
return mergeDataOrFn(parentVal, childVal)
}
return mergeDataOrFn(parentVal, childVal, vm)
};
// 生命周期钩子选项的合并策略
function mergeHook (
parentVal,
childVal
) {
//三目运算符:判断是否有childVal参数,如果有的话判断是否有parentVal参数,
// 如果有parentVal参数就将childVal与parentVal合并,
//如果没有 parentVal 则判断 childVal 是不是一个数组
//如果是数组类型,则直接返回,若不是则将childVal转成数组,说明生命周期钩子是可以写成数组的并会按照数组顺序依次执行
var res = childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal;
return res
? dedupeHooks(res)
: 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
}
// LIFECYCLE_HOOKS 常量实际上是由与生命周期钩子同名的字符串组成的数组
LIFECYCLE_HOOKS.forEach(function (hook) {
strats[hook] = mergeHook;
});
// 资源(assets)选项的合并策略
function mergeAssets (
parentVal,
childVal,
vm,
key
) {
创建parentVal新对象
var res = Object.create(parentVal || null);
// 判断是否有childVal参数
if (childVal) {
//主要就是为了判断childVal是否为存粹的对象
assertObjectType(key, childVal, vm);
return extend(res, childVal)
} else {
return res
}
}
//用来遍历 ASSET_TYPES 常量(component、directive、filter)
ASSET_TYPES.forEach(function (type) {
strats[type + 's'] = mergeAssets;
});
// 选项 watch 的合并策略,合并处理 watch 选项的
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// 在 Firefox 浏览器中 Object.prototype 拥有原生的 watch 函数,容易造成迷惑,
// 所以当发现组件选项是浏览器原生的 watch 时,那说明用户并没有提供 Vue 的 watch 选项,直接重置为 undefined
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
//如果没有childVal(即组件选项是否有 watch 选项),则返回parentVal新对象
if (!childVal) { return Object.create(parentVal || null) }
{
//判断childVal是否为存粹的对象
assertObjectType(key, childVal, vm);
}
//parentVal,则直接返回childVal,即直接使用组件选项的 watch
if (!parentVal) { return childVal }
var ret = {};
//将parentVal插入ret对象
extend(ret, parentVal);
//parentVal,childVal都存在,就需要做合并处理
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
//如果parent存在,则将child与parent合并,如果 parent 不存在,直接将 child 转为数组返回
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};
/**
* 选项 props、methods、inject、computed 的合并策略
*/
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal,
childVal,
vm,
key
) {
//如果有childVal参数,并且不是生产环境,则需要判断childVal是否为存粹的对象
if (childVal && "development" !== 'production') {
assertObjectType(key, childVal, vm);
}
//如果没有parentVal参数,则直接返回childVal,即直接使用对应的组件选项
if (!parentVal) { return childVal }
var ret = Object.create(null);
extend(ret, parentVal);
if (childVal) { extend(ret, childVal); }
return ret
};
//选项 provide 的合并策略,与data相同
strats.provide = mergeDataOrFn;
/*对于 el、propsData 选项使用默认的合并策略 defaultStrat。
对于 data 选项,使用 mergeDataOrFn 函数进行处理,最终结果是 data 选项将变成一个函数,且该函数的执行结果为真正的数据对象。
对于 生命周期钩子 选项,将合并成数组,使得父子选项中的钩子函数都能够被执行
对于 directives、filters 以及 components 等资源选项,父子选项将以原型链的形式被处理,正是因为这样我们才能够在任何地方都使用内置组件、指令等。
对于 watch 选项的合并处理,类似于生命周期钩子,如果父子选项都有相同的观测字段,将被合并为数组,这样观察者都将被执行。
对于 props、methods、inject、computed 选项,父选项始终可用,但是子选项会覆盖同名的父选项字段。
对于 provide 选项,其合并策略使用与 data 选项相同的 mergeDataOrFn 函数。
以上没有提及到的选项都将使默认选项 defaultStrat。*/
var defaultStrat = function (parentVal, childVal) {
return childVal === undefined
? parentVal
: childVal
};
// checkComponents函数,校验child对象中组件参数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.'
);
}
if (isBuiltInTag(name) || config.isReservedTag(name)) {
warn(
'Do not use built-in or reserved HTML elements as component ' +
'id: ' + name
);
}
}
// 对props的属性进行一些规范化
function normalizeProps (options, vm) {
var props = options.props;
if (!props) { return }
var res = {};
var i, val, name;
//如果props是数组,则将遍历props,将props转成对象,例如:{ title: {type: null} }
if (Array.isArray(props)) {
i = props.length;
while (i--) {
val = props[i];
if (typeof val === 'string') {
name = camelize(val);
res[name] = { type: null };
} else {
warn('props must be strings when using array syntax.');
}
}
//判断props是否为纯粹的对象
} else if (isPlainObject(props)) {
for (var key in props) {
val = props[key];
name = camelize(key);//将key驼峰化
res[name] = isPlainObject(val)
? val
: { type: val };//例如:props: {type: String}
}
} else {
warn(
"Invalid value for option \"props\": expected an Array or an Object, " +
"but got " + (toRawType(props)) + ".",
vm
);
}
options.props = res;
}
// 对inject的属性进行一些规范化,与normalizeProps差不多
function normalizeInject (options, vm) {
var inject = options.inject;
if (!inject) { return }
var normalized = options.inject = {};
//如果inject是数组,则将循环inject,将inject转成对象,例如:{ title: {form: null} }
if (Array.isArray(inject)) {
for (var i = 0; i < inject.length; i++) {
normalized[inject[i]] = { from: inject[i] };
}
//如果inject是纯粹的对象,则循环inject
} else if (isPlainObject(inject)) {
for (var key in inject) {
var val = inject[key];
normalized[key] = isPlainObject(val)
? extend({ from: key }, val)
: { from: val };//例如inject: {'title': {from: 'title'} }
}
} else {
warn(
"Invalid value for option \"inject\": expected an Array or an Object, " +
"but got " + (toRawType(inject)) + ".",
vm
);
}
}
// 对directives的属性进行一些规范化,例如directives: {change:{bind(){...},update(){...}}
function normalizeDirectives (options) {
var dirs = options.directives;
if (dirs) {
for (var key in dirs) {
var def$$1 = dirs[key];
//def$$1必须为函数
if (typeof def$$1 === 'function') {
dirs[key] = { bind: def$$1, update: def$$1 };
}
}
}
}
//判断是否为纯粹的对象
function assertObjectType (name, value, vm) {
if (!isPlainObject(value)) {
warn(
"Invalid value for option \"" + name + "\": expected an Object, " +
"but got " + (toRawType(value)) + ".",
vm
);
}
}
// 合并策略函数,参数选项合并参数;parent:父实例参数;child:子实例参数;vm:vue实例参数,实例化过程中被调用.
function mergeOptions (
parent,
child,
vm
) {
{
//校验child对象中组件参数components中的组件名是否符合规范
checkComponents(child);
}
//判断child是否为函数
if (typeof child === 'function') {
child = child.options;
}
// Props,Inject,Directives的规范化
normalizeProps(child, vm);
normalize(child, vm);
normalizeDirectives(child);
//只有合并过的选项会带有_base属性,判断child是否存在_base属性,如果存在则说明已经被合并处理过
if (!child._base) {
//将extend和mixins再通过mergeOptions函数与parent合并
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;
//遍历parent执行mergeField
for (key in parent) {
mergeField(key);
}
// 遍历child,当parent没有key的时候执行mergeField
// 如果有key属性,就不需要合并,因为上一步已经合并到options
for (key in child) {
if (!hasOwn(parent, key)) {
mergeField(key);
}
}
// 该函数主要是通过key获取到对应的合并策略函数,然后执行合并,赋值给options[key]
function mergeField (key) {
var strat = strats[key] || defaultStrat;
options[key] = strat(parent[key], child[key], vm, key);
}
return options
}
//用于获取Vue.extend里面定义的构造函数
function resolveAsset (
options,
type,
id,
warnMissing
) {
if (typeof id !== 'string') {
return
}
var assets = options[type];
// 检查assets中是否包含id键,如果有的话直接返回
if (hasOwn(assets, id)) { return assets[id] }
//规范键的驼峰命名
var camelizedId = camelize(id);
// 如果上一个条件不成立,那么将键转成驼峰后再判断
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
//将驼峰后的键的首字母大写
var PascalCaseId = capitalize(camelizedId);
// 如果以上条件不成立,那么将键转成驼峰后再将首字母转成大写,再次判断
if (hasOwn(assets, PascalCaseId)) { return assets[PascalCaseId] }
var res = assets[id] || assets[camelizedId] || assets[PascalCaseId];
if (warnMissing && !res) {
warn(
'Failed to resolve ' + type.slice(0, -1) + ': ' + id,
options
);
}
return res
}
//检查一下我们传递的数据是否满足 prop的定义规范
function validateProp (
key,
propOptions,
propsData,
vm
) {
var prop = propOptions[key];//获取propOptions中对应的值
var absent = !hasOwn(propsData, key);//如果propsData没有这个key,则absent为true
var value = propsData[key];//获取propsData中对应的值
//判断prop.type是否为布尔类型
var booleanIndex = getTypeIndex(Boolean, prop.type);
if (booleanIndex > -1) {
//如果absent为true,并且prop中不包含'default'
if (absent && !hasOwn(prop, 'default')) {
value = false;
// 如果value为空字符串,获取value与转成驼峰后的key相等
} else if (value === '' || value === hyphenate(key)) {
// 判断prop.type是否为String类型
var stringIndex = getTypeIndex(String, prop.type);
// 如果为stringIndex类型,或者booleanIndex的优先级比stringIndex的高
if (stringIndex < 0 || booleanIndex < stringIndex) {
value = true;
}
}
}
// 当value全等于undefined时,调用getPropDefaultValue方法取prop的默认值
if (value === undefined) {
value = getPropDefaultValue(vm, prop, key);
//shouldObserve默认值为true
var prevShouldObserve = shouldObserve;
//改变shouldObserve的值为true
toggleObserving(true);
//判断value是否为一个对象
observe(value);
//改变shouldObserve的值为prevShouldObserve的值
toggleObserving(prevShouldObserve);
}
{
assertProp(prop, key, value, vm, absent);
}
return value
}
//获取prop的默认值
function getPropDefaultValue (vm, prop, key) {
//如果prop中不包含'default'属性,则直接返回undefined
if (!hasOwn(prop, 'default')) {
return undefined
}
var def = prop.default;
//如果def为object类型,则发出警告
if (isObject(def)) {
warn(
'Invalid default value for prop "' + key + '": ' +
'Props with type Object/Array must use a factory function ' +
'to return the default value.',
vm
);
}
// vue实例存在,组件实例上的props数据存在,父组件传进来的props中没有该key值的属性,
// 如果vm实例上_props有,就返回_props上的属性,_props是之前通过proxy代理的
//是vue做的一点优化,由于函数每次返回的对象都是一个新的引用,当组件更新的时候,
//为了避免不必要watcher update而设置
if (vm && vm.$options.propsData &&
vm.$options.propsData[key] === undefined &&
vm._props[key] !== undefined
) {
return vm._props[key]
}
//如果是function 则调用def.call(vm),否则就返回default属性对应的值
return typeof def === 'function' && getType(prop.type) !== 'Function'
? def.call(vm)
: def
}
// 校验是否通过,先验证required再验证type
function assertProp (
prop,
name,
value,
vm,
absent
) {
//如果必传,但若当前为undefined时,则发出警告
if (prop.required && absent) {
warn(
'Missing required prop: "' + name + '"',
vm
);
return
}
//如果非必传时并且value为null,则直接返回
if (value == null && !prop.required) {
return
}
var type = prop.type;
var valid = !type || type === true;
var expectedTypes = [];
//type必须为数组类型
if (type) {
if (!Array.isArray(type)) {
type = [type];
}
for (var i = 0; i < type.length && !valid; i++) {
var assertedType = assertType(value, type[i], vm);
expectedTypes.push(assertedType.expectedType || '');
valid = assertedType.valid;
}
}
var haveExpectedTypes = expectedTypes.some(function (t) { return t; });
//如果valid为false,并且expectedTypes有值,则prop 的值 value 与 prop 定义的类型都不匹配,则输出警告
if (!valid && haveExpectedTypes) {
warn(
getInvalidTypeMessage(name, value, expectedTypes),
vm
);
return
}
// 判断有没有自定义的校验器,如果有则执行,并在校验器返回false时触发warn
var validator = prop.validator;
if (validator) {
if (!validator(value)) {
warn(
'Invalid prop: custom validator check failed for prop "' + name + '".',
vm
);
}
}
}
//props类型检查
var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol|BigInt)$/;
// 获取断言的结果,直到遍历完成或者是 valid 为 true 的时候跳出循环
function assertType (value, type, vm) {
var valid;
// 获取 prop 期望的类型 expectedType,然后对比prop的值value是否和expectedType 匹配
var expectedType = getType(type);
if (simpleCheckRE.test(expectedType)) {
var t = typeof value;
valid = t === expectedType.toLowerCase();
// value 的类型必须是 type 数组里的其中之一
if (!valid && t === 'object') {
valid = value instanceof type;
}
//判断value是否为纯粹的对象
} else if (expectedType === 'Object') {
valid = isPlainObject(value);
} else if (expectedType === 'Array') {
//判断value是否为数组
valid = Array.isArray(value);
} else {
try {
//判断value是否为数组
valid = value instanceof type;
} catch (e) {
warn('Invalid prop type: "' + String(type) + '" is not a constructor', vm);
valid = false;
}
}
return {
valid: valid,
expectedType: expectedType
}
}
//函数检查
var functionTypeCheckRE = /^\s*function (\w+)/;
//将fn参数转成字符串,然后在字符串内查找function
function getType (fn) {
var match = fn && fn.toString().match(functionTypeCheckRE);
return match ? match[1] : ''
}
// 判断是否是同一个类型
function isSameType (a, b) {
return getType(a) === getType(b)
}
//找到 type 和 expectedTypes 匹配的索引并返回
function getTypeIndex (type, expectedTypes) {
if (!Array.isArray(expectedTypes)) {
return isSameType(expectedTypes, type) ? 0 : -1
}
for (var i = 0, len = expectedTypes.length; i < len; i++) {
if (isSameType(expectedTypes[i], type)) {
return i
}
}
return -1
}
//生成警告信息
function getInvalidTypeMessage (name, value, expectedTypes) {
var message = "Invalid prop: type check failed for prop \"" + name + "\"." +
" Expected " + (expectedTypes.map(capitalize).join(', '));
var expectedType = expectedTypes[0];
var receivedType = toRawType(value);
// 检查是否需要指定接收值
if (
expectedTypes.length === 1 &&
isExplicable(expectedType) &&
isExplicable(typeof value) &&
!isBoolean(expectedType, receivedType)
) {
message += " with value " + (styleValue(value, expectedType));
}
message += ", got " + receivedType + " ";
// 检查是否需要指定接收值
if (isExplicable(receivedType)) {
message += "with value " + (styleValue(value, receivedType)) + ".";
}
return message
}
function styleValue (value, type) {
if (type === 'String') {
return ("\"" + value + "\"")
} else if (type === 'Number') {
return ("" + (Number(value)))
} else {
return ("" + value)
}
}
var EXPLICABLE_TYPES = ['string', 'number', 'boolean'];
function isExplicable (value) {
//将value转为小写,然后判断value是否是'string', 'number', 'boolean'的其中一种类型
return EXPLICABLE_TYPES.some(function (elem) { return value.toLowerCase() === elem; })
}
//判断是否为boolean类型
function isBoolean () {
var args = [], len = arguments.length;
while ( len-- ) args[ len ] = arguments[ len ];
return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; })
}
/* 捕捉异常 */
// 首先获取到报错的组件,之后递归查找当前组件的父组件,依次调用errorCaptured 方法。
// 在遍历调用完所有 errorCaptured 方法、或 errorCaptured 方法有报错时,调用 globalHandleError 方法
function handleError (err, vm, info) {
// Deactivate deps tracking while processing error handler to avoid possible infinite rendering.
// See: https://github.com/vuejs/vuex/issues/1505
pushTarget();
try {
// vm指当前报错的组件实例
if (vm) {
var cur = vm;
// 递归查找当前组件的父组件,依次调用errorCaptured 方法。
// 在遍历调用完所有 errorCaptured 方法、或 errorCaptured 方法有报错时,调用 globalHandleError 方法
while ((cur = cur.$parent)) {
var hooks = cur.$options.errorCaptured;
// 判断是否存在errorCaptured钩子函数
if (hooks) {
for (var i = 0; i < hooks.length; i++) {
try {
var capture = hooks[i].call(cur, err, vm, info) === false;
if (capture) { return }
} catch (e) {
globalHandleError(e, cur, 'errorCaptured hook');
}
}
}
}
}
globalHandleError(err, vm, info);
} finally {
popTarget();
}
}
// 异步错误处理
function invokeWithErrorHandling (
handler,
context,
args,
vm,
info
) {
var res;
try {
//根据不同参数选择不同的处理函数
res = args ? handler.apply(context, args) : handler.call(context);
if (res && !res._isVue && isPromise(res) && !res._handled) {
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
res._handled = true;
}
} catch (e) {
handleError(e, vm, info);
}
return res
}
// 调用全局的 errorHandler 方法,生产环境下会使用 console.error 在控制台中输出
function globalHandleError (err, vm, info) {
// 获取全局配置,判断是否设置处理函数
if (config.errorHandler) {
try {
// 执行设置的全局错误处理函数
return config.errorHandler.call(null, err, vm, info)
} catch (e) {
// 如果开发者在errorHandler函数中手动抛出同样错误信息throw err判断err信息是否相等,避免log两次
// 如果抛出新的错误信息将会一起log输出
if (e !== err) {
logError(e, null, 'config.errorHandler');
}
}
}
logError(err, vm, info);
}
// 判断环境,选择不同的抛错方式。浏览器或微信环境,并且console的类型为undefied时,打印error
function logError (err, vm, info) {
{
warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);
}
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
console.error(err);
} else {
throw err
}
}
//接下来是nextTick的核心代码
// 使用 MicroTask(微任务) 的标识符
var isUsingMicroTask = false;
var callbacks = [];
var pending = false;
//依次同步执行callbacks中回调
function flushCallbacks () {
pending = false;
var copies = callbacks.slice(0);
callbacks.length = 0;
for (var i = 0; i < copies.length; i++) {
copies[i]();
}
}
/*在vue2.5之前的版本中,nextTick基本上基于 micro task 来实现的,但是在某些情况下 micro task 具有太高的优先级,
并且可能在连续顺序事件之间(例如#4521,#6690)或者甚至在同一事件的事件冒泡过程中之间触发(#6566)。
但是如果全部都改成 macro task,对一些有重绘和动画的场景也会有性能影响,如 issue #6813。
vue2.5之后版本提供的解决办法是默认使用 micro task,但在需要时(例如在v-on附加的事件处理程序中)强制使用 macro task*/
var timerFunc;
//nextTick行为利用了可以访问的微任务队列
//通过本机Promise.then或MutationObserver。
//MutationObserver得到了更广泛的支持,但是它在
//iOS中的UIWebView>=9.3.3,当touch事件处理程序中触发时,几次后会完全停止工作
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
var p = Promise.resolve();
timerFunc = function () {
p.then(flushCallbacks);
//在有问题的UIWebViews中,Promise.then并没有完全崩溃,但是
//它可能会陷入一种奇怪的状态,即回调被推到
//微任务队列,但在浏览器
//需要做一些其他工作,例如处理计时器。所以我们可以
//通过添加计时器“强制”刷新微任务队列。
if (isIOS) { setTimeout(noop); }
};
isUsingMicroTask = true;
// MutationObserver的作用通过它创建一个观察者对象,这个对象会监听某个DOM元素,并在它的DOM树发生变化时执行我们提供的回调函数
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
// Use MutationObserver where native Promise is not available,
// e.g. PhantomJS, iOS7, Android 4.4
// (#6466 MutationObserver is unreliable in IE11)
var counter = 1;
// 声明 MO 和回调函数
var observer = new MutationObserver(flushCallbacks);
var textNode = document.createTextNode(String(counter));
// 监听 textNode 这个文本节点,一旦文本改变则触发回调函数 nextTickHandler
observer.observe(textNode, {
characterData: true
});
// 每次执行 timeFunc 都会让文本在 1 和 0 间切换
timerFunc = function () {
counter = (counter + 1) % 2;
//把该数据值赋值到data属性上面去,如果data属性发生改变了,就会重新渲染页面
textNode.data = String(counter);
};
isUsingMicroTask = true;
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// setImmediate使用宏任务
timerFunc = function () {
setImmediate(flushCallbacks);
};
} else {
//如果不支持MutationObserver,则使用settimeout
timerFunc = function () {
setTimeout(flushCallbacks, 0);
};
}
function nextTick (cb, ctx) {
var _resolve;
// 传入的cb是否是函数,ctx参数是否是一个对象,如果cb是一个函数的话,使用cb.call(ctx)
callbacks.push(function () {
if (cb) {
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});
//如果pending为true,表明本轮事件循环中已经执行过 timerFunc(nextTickHandler, 0)
if (!pending) {
pending = true;
timerFunc();
}
//判断是否有Promise对象
if (!cb && typeof Promise !== 'undefined') {
return new Promise(function (resolve) {
_resolve = resolve;
})
}
}
/* */
var mark;
var measure;
// 此方法是获取标签在浏览器加载的时间快照
{
//在浏览器环境
var perf = inBrowser && window.performance;
//performance.mark方法在浏览器的性能条目缓冲区中创建一个具有给定名称的缓冲区,
//performance.measure在浏览器的两个指定标记(分别称为起始标记和结束标记)之间的性能条目缓冲区中创建一个命名
//performance.measure从浏览器的performance entry 缓存中移除声明的标记
if (
perf &&
perf.mark &&
perf.measure &&
perf.clearMarks &&
perf.clearMeasures
) {
mark = function (tag) { return perf.mark(tag); };
measure = function (name, startTag, endTag) {
perf.measure(name, startTag, endTag);
perf.clearMarks(startTag);
perf.clearMarks(endTag);
// perf.clearMeasures(name)
};
}
}
/* not type checking this file because flow doesn't play well with Proxy */
//初始化代理
var initProxy;
{
//makeMap 方法将字符串切割,放到map中,用于校验其中的某个字符串是否存在(区分大小写)于map中
var allowedGlobals = makeMap(
'Infinity,undefined,NaN,isFinite,isNaN,' +
'parseFloat,parseInt,decodeURI,decodeURIComponent,encodeURI,encodeURIComponent,' +
'Math,Number,Date,Array,Object,Boolean,String,RegExp,Map,Set,JSON,Intl,BigInt,' +
'require' // for Webpack/Browserify
);
// 不存在,未定义的属性,方法被使用给出警告
var warnNonPresent = function (target, key) {
warn(
"Property or method \"" + key + "\" is not defined on the instance but " +
'referenced during render. Make sure that this property is reactive, ' +
'either in the data option, or for class-based components, by ' +
'initializing the property. ' +
'See: https://vuejs.org/v2/guide/reactivity.html#Declaring-Reactive-Properties.',
target
);
};
//用于检测属性 key 的声明方法,是否是 $ 或者 _ 开头的,如果是,会给出警告
var warnReservedPrefix = function (target, key) {
warn(
"Property \"" + key + "\" must be accessed with \"$data." + key + "\" because " +
'properties starting with "$" or "_" are not proxied in the Vue instance to ' +
'prevent conflicts with Vue internals. ' +
'See: https://vuejs.org/v2/api/#data',
target
);
};
var hasProxy =
typeof Proxy !== 'undefined' && isNative(Proxy);
//当前环境中的proxy可用
if (hasProxy) {
//某个字符串是否存在map中
var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');
//config.keyCodes自定义按键修饰符
config.keyCodes = new Proxy(config.keyCodes, {
set: function set (target, key, value) {
//key是否存在map中
if (isBuiltInModifier(key)) {
warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key));
return false
} else {
target[key] = value;
return true
}
}
});
}
//这个函数主要是提示错误信息,在开发者错误的调用vm属性时,提供提示作用
var hasHandler = {
has: function has (target, key) {
var has = key in target;
var isAllowed = allowedGlobals(key) ||
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
if (!has && !isAllowed) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return has || !isAllowed
}
};
// getHandler方法主要是针对读取代理对象的某个属性时进行的操作。
// 当访问的属性不是string类型或者属性值在被代理的对象上不存在,则抛出错误提示,否则就返回该属性值
var getHandler = {
get: function get (target, key) {
if (typeof key === 'string' && !(key in target)) {
if (key in target.$data) { warnReservedPrefix(target, key); }
else { warnNonPresent(target, key); }
}
return target[key]
}
};
//初始化代理
initProxy = function initProxy (vm) {
// 通过判断hasProxy,来执行不同的处理逻辑
if (hasProxy) {
// 如果options上存在render属性,且render属性上存在_withStripped属性,
// 则proxy的traps(traps其实也就是自定义方法)采用getHandler方法,否则采用hasHandler方法
var options = vm.$options;
var handlers = options.render && options.render._withStripped
? getHandler
: hasHandler;
vm._renderProxy = new Proxy(vm, handlers);
} else {
vm._renderProxy = vm;
}
};
}
var seenObjects = new _Set();
// 递归遍历对象以调用所有已转换的getter,以便对象中的每个嵌套属性,作为'deep'依赖项收集
//这个函数主要用来进行深度监听
function traverse (val) {
_traverse(val, seenObjects);
seenObjects.clear();
}
function _traverse (val, seen) {
var i, keys;
var isA = Array.isArray(val);
// val是否是数组 或者是已经冻结对象或者是VNode实例
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 只有object和array才有__ob__属性
if (val.__ob__) {
var depId = val.__ob__.dep.id;//手动依赖收集器的id
if (seen.has(depId)) {
return
}
seen.add(depId);
}
if (isA) {
i = val.length;
// 递归触发每一项的get进行依赖收集
while (i--) { _traverse(val[i], seen); }
} else {
keys = Object.keys(val);
i = keys.length;
// 递归触发子属性的get进行依赖收集
while (i--) { _traverse(val[keys[i]], seen); }
}
}
// 主要用于将传入的带有特殊前缀的事件修饰符分解为具有特定值的事件对象
// name属性:事件名称
// once属性:是否一次性执行
// capture属性:是否捕获事件
// passive属性:是否使用被动模式
var normalizeEvent = cached(function (name) {
var passive = name.charAt(0) === '&';
name = passive ? name.slice(1) : name;
var once$$1 = name.charAt(0) === '~';
name = once$$1 ? name.slice(1) : name;
var capture = name.charAt(0) === '!';
name = capture ? name.slice(1) : name;
return {
name: name,
once: once$$1,
capture: capture,
passive: passive
}
});
//与normalizeEvent一起完成更新监听器的实现,是真正执行事件处理器调用的过程
function createFnInvoker (fns, vm) {
function invoker () {
var arguments$1 = arguments;
// invoker.fns用来存放所传入的处理器
var fns = invoker.fns;
if (Array.isArray(fns)) {
var cloned = fns.slice();
for (var i = 0; i < cloned.length; i++) {
//处理器数组的调用
invokeWithErrorHandling(cloned[i], null, arguments$1, vm, "v-on handler");
}
} else {
// 单个处理器的调用
return invokeWithErrorHandling(fns, null, arguments, vm, "v-on handler")
}
}
invoker.fns = fns;
return invoker
}
//这个函数主要是修改监听配置,遍历on事件对新节点事件绑定注册事件,对旧节点移除事件监听
function updateListeners (
on,
oldOn,
add,
remove$$1,
createOnceHandler,
vm
) {
var name, def$$1, cur, old, event;
// 遍历on事件对新节点事件绑定注册事件
for (name in on) {
def$$1 = cur = on[name];
old = oldOn[name];
event = normalizeEvent(name);
if (isUndef(cur)) {
warn(
"Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
vm
);
} else if (isUndef(old)) {
if (isUndef(cur.fns)) {
cur = on[name] = createFnInvoker(cur, vm);
}
if (isTrue(event.once)) {
cur = on[name] = createOnceHandler(event.name, cur, event.capture);
}
//执行真正注册事件的执行函数,add的实现原理利用了原生 DOM 的 addEventListener
add(event.name, cur, event.capture, event.passive, event.params);
} else if (cur !== old) {
old.fns = cur;
on[name] = old;
}
}
//遍历旧节点,对旧节点移除事件监听
for (name in oldOn) {
if (isUndef(on[name])) {
event = normalizeEvent(name);
remove$$1(event.name, oldOn[name], event.capture);
}
}
}
// 把insert作为一个hooks属性保存到对应的Vnode的data上面,当该Vnode插入到父节点后会调用该hooks
function mergeVNodeHook (def, hookKey, hook) {
if (def instanceof VNode) {
def = def.data.hook || (def.data.hook = {});
}
var invoker;
var oldHook = def[hookKey];
function wrappedHook () {
hook.apply(this, arguments);
// 删除合并的钩子以确保它只被调用一次并防止内存泄漏
remove(invoker.fns, wrappedHook);
}
if (isUndef(oldHook)) {
// 没有已经存在的hook
invoker = createFnInvoker([wrappedHook]);
} else {
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
// 已经有一个invoker(已经是合并的调用程序)
invoker = oldHook;
invoker.fns.push(wrappedHook);
} else {
invoker = createFnInvoker([oldHook, wrappedHook]);
}
}
invoker.merged = true;
def[hookKey] = invoker;
}
// 获取原始值
function extractPropsFromVNodeData (
data,
Ctor,
tag
) {
// 获取组件的定义的props对象,这里只提取原始值。验证和默认值在子组件本身中处理。
var propOptions = Ctor.options.props;
if (isUndef(propOptions)) {
return
}
var res = {};
var attrs = data.attrs;//获取data的attrs属性
var props = data.props;//获取data的props属性
// 如果data有定义了attrs或者props属性
if (isDef(attrs) || isDef(props)) {
//遍历组件的props属性
for (var key in propOptions) {
// 如果key是是驼峰字符串,则转换为-格式
var altKey = hyphenate(key);
{
//转换为小写格式
var keyInLowerCase = key.toLowerCase();
if (
key !== keyInLowerCase &&
attrs && hasOwn(attrs, keyInLowerCase)
) {
tip(
"Prop \"" + keyInLowerCase + "\" is passed to component " +
(formatComponentName(tag || Ctor)) + ", but the declared prop name is" +
" \"" + key + "\". " +
"Note that HTML attributes are case-insensitive and camelCased " +
"props need to use their kebab-case equivalents when using in-DOM " +
"templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"."
);
}
}
// 调用checkProp从props或attrs里拿对应的属性
checkProp(res, props, key, altKey, true) ||
checkProp(res, attrs, key, altKey, false);
}
}
return res
}
网友评论