|
|
// Vue.js v2.6.12
|
|
|
// (c) 2014-2020 Evan You
|
|
|
// Released under the MIT License.
|
|
|
// 立即执行函数,用于创建 Vue 实例并挂载到全局对象
|
|
|
(function (global, factory) {
|
|
|
// 检查是否为 CommonJS 模块环境
|
|
|
if (typeof exports === 'object' && typeof module !== 'undefined') {
|
|
|
// 如果是 CommonJS 环境,通过 module.exports 导出 factory 函数的返回值
|
|
|
module.exports = factory();
|
|
|
}
|
|
|
// 检查是否为 AMD 模块环境
|
|
|
else if (typeof define === 'function' && define.amd) {
|
|
|
// 如果是 AMD 环境,使用 define 方法定义模块
|
|
|
define(factory);
|
|
|
}
|
|
|
// 若既不是 CommonJS 也不是 AMD 环境,则作为全局变量挂载到全局对象
|
|
|
else {
|
|
|
global = global || self;
|
|
|
// 将 factory 函数的返回值挂载到全局对象的 Vue 属性上
|
|
|
global.Vue = factory();
|
|
|
}
|
|
|
// 传入全局对象 this 和工厂函数
|
|
|
}(this, function () {
|
|
|
// 使用严格模式,增强代码的安全性和报错提示
|
|
|
'use strict';
|
|
|
|
|
|
// 创建一个冻结的空对象,防止其属性被修改,常用于默认值
|
|
|
var emptyObject = Object.freeze({});
|
|
|
|
|
|
// 检查值是否为 undefined 或 null
|
|
|
function isUndef (v) {
|
|
|
// 若值为 undefined 或者 null 则返回 true
|
|
|
return v === undefined || v === null;
|
|
|
}
|
|
|
|
|
|
// 检查值是否已定义且不为 null
|
|
|
function isDef (v) {
|
|
|
// 若值既不是 undefined 也不是 null 则返回 true
|
|
|
return v !== undefined && v !== null;
|
|
|
}
|
|
|
|
|
|
// 检查值是否为 true
|
|
|
function isTrue (v) {
|
|
|
// 若值严格等于 true 则返回 true
|
|
|
return v === true;
|
|
|
}
|
|
|
|
|
|
// 检查值是否为 false
|
|
|
function isFalse (v) {
|
|
|
// 若值严格等于 false 则返回 true
|
|
|
return v === false;
|
|
|
}
|
|
|
|
|
|
// 检查值是否为原始类型(字符串、数字、符号、布尔值)
|
|
|
function isPrimitive (value) {
|
|
|
// 判断值的类型是否为字符串、数字、符号或布尔值
|
|
|
return (
|
|
|
typeof value === 'string' ||
|
|
|
typeof value === 'number' ||
|
|
|
typeof value === 'symbol' ||
|
|
|
typeof value === 'boolean'
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 快速检查值是否为对象(非 null 且类型为 'object')
|
|
|
function isObject (obj) {
|
|
|
// 若值不为 null 且类型为 'object' 则返回 true
|
|
|
return obj !== null && typeof obj === 'object';
|
|
|
}
|
|
|
|
|
|
// 获取对象原型上的 toString 方法引用,用于后续获取对象类型字符串
|
|
|
var _toString = Object.prototype.toString;
|
|
|
|
|
|
// 获取值的原始类型字符串,去除前后的 [object ] 部分
|
|
|
function toRawType (value) {
|
|
|
// 调用 toString 方法并截取中间部分
|
|
|
return _toString.call(value).slice(8, -1);
|
|
|
}
|
|
|
|
|
|
// 严格检查对象是否为普通的 JavaScript 对象
|
|
|
function isPlainObject (obj) {
|
|
|
// 若对象的 toString 结果为 [object Object] 则返回 true
|
|
|
return _toString.call(obj) === '[object Object]';
|
|
|
}
|
|
|
|
|
|
// 检查值是否为正则表达式对象
|
|
|
function isRegExp (v) {
|
|
|
// 若对象的 toString 结果为 [object RegExp] 则返回 true
|
|
|
return _toString.call(v) === '[object RegExp]';
|
|
|
}
|
|
|
|
|
|
// 检查值是否为有效的数组索引(非负整数且为有限值)
|
|
|
function isValidArrayIndex (val) {
|
|
|
// 将值转换为浮点数
|
|
|
var n = parseFloat(String(val));
|
|
|
// 检查是否为非负整数且为有限值
|
|
|
return n >= 0 && Math.floor(n) === n && isFinite(val);
|
|
|
}
|
|
|
|
|
|
// 检查值是否为 Promise 对象(有 then 和 catch 方法)
|
|
|
function isPromise (val) {
|
|
|
// 检查值是否已定义且有 then 和 catch 方法
|
|
|
return (
|
|
|
isDef(val) &&
|
|
|
typeof val.then === 'function' &&
|
|
|
typeof val.catch === 'function'
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 将值转换为字符串,处理数组、对象等情况
|
|
|
function toString (val) {
|
|
|
// 若值为 null 或 undefined 则返回空字符串
|
|
|
if (val == null) {
|
|
|
return '';
|
|
|
}
|
|
|
// 若值为数组或普通对象且其 toString 方法为默认的则进行 JSON 序列化
|
|
|
if (Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)) {
|
|
|
return JSON.stringify(val, null, 2);
|
|
|
}
|
|
|
// 其他情况直接转换为字符串
|
|
|
return String(val);
|
|
|
}
|
|
|
|
|
|
// 将值转换为数字,若转换失败则返回原始值
|
|
|
function toNumber (val) {
|
|
|
// 将值转换为浮点数
|
|
|
var n = parseFloat(val);
|
|
|
// 若转换结果为 NaN 则返回原始值,否则返回转换后的数字
|
|
|
return isNaN(n) ? val : n;
|
|
|
}
|
|
|
|
|
|
// 创建一个映射对象,并返回一个用于检查键是否存在于映射中的函数
|
|
|
function makeMap (
|
|
|
str,
|
|
|
expectsLowerCase
|
|
|
) {
|
|
|
// 创建一个空对象作为映射
|
|
|
var map = Object.create(null);
|
|
|
// 将字符串按逗号分割成数组
|
|
|
var list = str.split(',');
|
|
|
// 遍历数组,将每个元素作为键添加到映射中
|
|
|
for (var i = 0; i < list.length; i++) {
|
|
|
map[list[i]] = true;
|
|
|
}
|
|
|
// 根据是否需要小写处理返回不同的检查函数
|
|
|
if (expectsLowerCase) {
|
|
|
return function (val) { return map[val.toLowerCase()]; };
|
|
|
}
|
|
|
return function (val) { return map[val]; };
|
|
|
}
|
|
|
|
|
|
// 创建一个检查是否为内置标签(slot、component)的函数
|
|
|
var isBuiltInTag = makeMap('slot,component', true);
|
|
|
|
|
|
// 创建一个检查是否为保留属性(key、ref 等)的函数
|
|
|
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
|
|
|
|
|
|
// 从数组中移除指定的项
|
|
|
function remove (arr, item) {
|
|
|
// 检查数组是否有元素
|
|
|
if (arr.length) {
|
|
|
// 查找项在数组中的索引
|
|
|
var index = arr.indexOf(item);
|
|
|
// 若索引存在,则移除该项
|
|
|
if (index > -1) {
|
|
|
return arr.splice(index, 1);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 获取对象原型上的 hasOwnProperty 方法引用
|
|
|
var hasOwnProperty = Object.prototype.hasOwnProperty;
|
|
|
|
|
|
// 检查对象是否自身拥有指定的属性
|
|
|
function hasOwn (obj, key) {
|
|
|
// 使用 hasOwnProperty 方法检查对象是否拥有指定属性
|
|
|
return hasOwnProperty.call(obj, key);
|
|
|
}
|
|
|
|
|
|
// 创建一个函数的缓存版本,避免重复计算
|
|
|
function cached (fn) {
|
|
|
// 创建一个空对象作为缓存
|
|
|
var cache = Object.create(null);
|
|
|
return function cachedFn (str) {
|
|
|
// 检查缓存中是否已有结果
|
|
|
var hit = cache[str];
|
|
|
// 若有则返回缓存结果,否则计算并缓存结果
|
|
|
return hit || (cache[str] = fn(str));
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 用于将连字符分隔的字符串转换为驼峰式字符串
|
|
|
var camelizeRE = /-(\w)/g;
|
|
|
|
|
|
// 创建一个缓存版本的驼峰化函数
|
|
|
var camelize = cached(function (str) {
|
|
|
// 使用正则表达式将连字符后的字母转换为大写
|
|
|
return str.replace(camelizeRE, function (_, c) { return c ? c.toUpperCase() : ''; });
|
|
|
});
|
|
|
|
|
|
// 创建一个缓存版本的首字母大写函数
|
|
|
var capitalize = cached(function (str) {
|
|
|
// 将字符串的首字母转换为大写
|
|
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
|
});
|
|
|
|
|
|
// 用于将驼峰式字符串转换为连字符分隔的字符串
|
|
|
var hyphenateRE = /\B([A-Z])/g;
|
|
|
|
|
|
// 创建一个缓存版本的连字符化函数
|
|
|
var hyphenate = cached(function (str) {
|
|
|
// 使用正则表达式将大写字母前添加连字符并转换为小写
|
|
|
return str.replace(hyphenateRE, '-$1').toLowerCase();
|
|
|
});
|
|
|
|
|
|
// 为不支持 bind 方法的环境提供的 polyfill
|
|
|
function polyfillBind (fn, ctx) {
|
|
|
function boundFn (a) {
|
|
|
var l = arguments.length;
|
|
|
// 根据参数数量调用原函数
|
|
|
if (l) {
|
|
|
if (l > 1) {
|
|
|
return fn.apply(ctx, arguments);
|
|
|
}
|
|
|
return fn.call(ctx, a);
|
|
|
}
|
|
|
return fn.call(ctx);
|
|
|
}
|
|
|
// 记录原函数的参数长度
|
|
|
boundFn._length = fn.length;
|
|
|
return boundFn;
|
|
|
}
|
|
|
|
|
|
// 使用原生的 bind 方法
|
|
|
function nativeBind (fn, ctx) {
|
|
|
return fn.bind(ctx);
|
|
|
}
|
|
|
|
|
|
// 根据环境选择使用原生 bind 或 polyfill
|
|
|
var bind = Function.prototype.bind
|
|
|
? nativeBind
|
|
|
: polyfillBind;
|
|
|
|
|
|
// 将类数组对象转换为真正的数组
|
|
|
function toArray (list, start) {
|
|
|
// 若未指定起始位置,则默认为 0
|
|
|
start = start || 0;
|
|
|
// 计算数组长度
|
|
|
var i = list.length - start;
|
|
|
// 创建一个指定长度的数组
|
|
|
var ret = new Array(i);
|
|
|
// 遍历类数组对象,将元素复制到新数组中
|
|
|
while (i--) {
|
|
|
ret[i] = list[i + start];
|
|
|
}
|
|
|
return ret;
|
|
|
}
|
|
|
|
|
|
// 将一个对象的属性复制到另一个对象上
|
|
|
function extend (to, _from) {
|
|
|
// 遍历源对象的属性
|
|
|
for (var key in _from) {
|
|
|
// 将属性复制到目标对象上
|
|
|
to[key] = _from[key];
|
|
|
}
|
|
|
return to;
|
|
|
}
|
|
|
|
|
|
// 将对象数组合并为一个对象
|
|
|
function toObject (arr) {
|
|
|
// 创建一个空对象作为结果
|
|
|
var res = {};
|
|
|
// 遍历数组
|
|
|
for (var i = 0; i < arr.length; i++) {
|
|
|
// 若数组元素存在,则将其属性合并到结果对象上
|
|
|
if (arr[i]) {
|
|
|
extend(res, arr[i]);
|
|
|
}
|
|
|
}
|
|
|
return res;
|
|
|
}
|
|
|
|
|
|
// 空函数,不执行任何操作
|
|
|
function noop (a, b, c) {}
|
|
|
|
|
|
// 始终返回 false 的函数
|
|
|
var no = function (a, b, c) { return false; };
|
|
|
|
|
|
// 返回传入参数本身的函数
|
|
|
var identity = function (_) { return _; };
|
|
|
|
|
|
// 从编译器模块数组中生成静态键字符串
|
|
|
function genStaticKeys (modules) {
|
|
|
// 使用 reduce 方法将模块的静态键合并成一个数组并转换为字符串
|
|
|
return modules.reduce(function (keys, m) {
|
|
|
return keys.concat(m.staticKeys || []);
|
|
|
}, []).join(',');
|
|
|
}
|
|
|
|
|
|
// 检查两个值是否宽松相等
|
|
|
function looseEqual (a, b) {
|
|
|
// 若严格相等则直接返回 true
|
|
|
if (a === b) {
|
|
|
return true;
|
|
|
}
|
|
|
// 检查 a 是否为对象
|
|
|
var isObjectA = isObject(a);
|
|
|
// 检查 b 是否为对象
|
|
|
var isObjectB = isObject(b);
|
|
|
if (isObjectA && isObjectB) {
|
|
|
try {
|
|
|
// 检查 a 是否为数组
|
|
|
var isArrayA = Array.isArray(a);
|
|
|
// 检查 b 是否为数组
|
|
|
var isArrayB = Array.isArray(b);
|
|
|
if (isArrayA && isArrayB) {
|
|
|
// 若都是数组,检查长度和元素是否宽松相等
|
|
|
return a.length === b.length && a.every(function (e, i) {
|
|
|
return looseEqual(e, b[i]);
|
|
|
});
|
|
|
} else if (a instanceof Date && b instanceof Date) {
|
|
|
// 若都是日期对象,检查时间戳是否相等
|
|
|
return a.getTime() === b.getTime();
|
|
|
} else if (!isArrayA && !isArrayB) {
|
|
|
// 若都不是数组,检查键和值是否宽松相等
|
|
|
var keysA = Object.keys(a);
|
|
|
var keysB = Object.keys(b);
|
|
|
return keysA.length === keysB.length && keysA.every(function (key) {
|
|
|
return looseEqual(a[key], b[key]);
|
|
|
});
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 异常情况返回 false
|
|
|
return false;
|
|
|
}
|
|
|
} else if (!isObjectA && !isObjectB) {
|
|
|
// 若都不是对象,检查字符串形式是否相等
|
|
|
return String(a) === String(b);
|
|
|
}
|
|
|
// 其他情况返回 false
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
// 在数组中查找第一个与指定值宽松相等的元素的索引
|
|
|
function looseIndexOf (arr, val) {
|
|
|
// 遍历数组
|
|
|
for (var i = 0; i < arr.length; i++) {
|
|
|
// 若找到宽松相等的元素则返回其索引
|
|
|
if (looseEqual(arr[i], val)) {
|
|
|
return i;
|
|
|
}
|
|
|
}
|
|
|
// 未找到则返回 -1
|
|
|
return -1;
|
|
|
}
|
|
|
|
|
|
// 确保函数只执行一次
|
|
|
function once (fn) {
|
|
|
var called = false;
|
|
|
return function () {
|
|
|
// 若函数未执行过
|
|
|
if (!called) {
|
|
|
// 标记为已执行
|
|
|
called = true;
|
|
|
// 执行原函数
|
|
|
fn.apply(this, arguments);
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 服务器渲染标记属性名
|
|
|
var SSR_ATTR = 'data-server-rendered';
|
|
|
|
|
|
// 资产类型数组
|
|
|
var ASSET_TYPES = [
|
|
|
'component',
|
|
|
'directive',
|
|
|
'filter'
|
|
|
];
|
|
|
|
|
|
// 生命周期钩子数组
|
|
|
var LIFECYCLE_HOOKS = [
|
|
|
'beforeCreate',
|
|
|
'created',
|
|
|
'beforeMount',
|
|
|
'mounted',
|
|
|
'beforeUpdate',
|
|
|
'updated',
|
|
|
'beforeDestroy',
|
|
|
'destroyed',
|
|
|
'activated',
|
|
|
'deactivated',
|
|
|
'errorCaptured',
|
|
|
'serverPrefetch'
|
|
|
];
|
|
|
|
|
|
// Vue 的配置对象
|
|
|
var config = {
|
|
|
// 选项合并策略对象
|
|
|
optionMergeStrategies: Object.create(null),
|
|
|
// 是否抑制警告信息
|
|
|
silent: false,
|
|
|
// 是否在开发环境显示生产模式提示
|
|
|
productionTip: "development" !== 'production',
|
|
|
// 是否启用开发者工具
|
|
|
devtools: "development" !== 'production',
|
|
|
// 是否记录性能信息
|
|
|
performance: false,
|
|
|
// 错误处理函数
|
|
|
errorHandler: null,
|
|
|
// 警告处理函数
|
|
|
warnHandler: null,
|
|
|
// 忽略的自定义元素列表
|
|
|
ignoredElements: [],
|
|
|
// 自定义按键别名对象
|
|
|
keyCodes: Object.create(null),
|
|
|
// 检查标签是否为保留标签的函数
|
|
|
isReservedTag: no,
|
|
|
// 检查属性是否为保留属性的函数
|
|
|
isReservedAttr: no,
|
|
|
// 检查标签是否为未知元素的函数
|
|
|
isUnknownElement: no,
|
|
|
// 获取元素命名空间的函数
|
|
|
getTagNamespace: noop,
|
|
|
// 解析平台标签名的函数
|
|
|
parsePlatformTagName: identity,
|
|
|
// 检查属性是否必须使用属性绑定的函数
|
|
|
mustUseProp: no,
|
|
|
// 是否异步执行更新
|
|
|
async: true,
|
|
|
// 生命周期钩子数组
|
|
|
_lifecycleHooks: LIFECYCLE_HOOKS
|
|
|
};
|
|
|
|
|
|
// 用于匹配 Unicode 字母的正则表达式
|
|
|
var unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
|
|
|
|
|
|
// 检查字符串是否以 $ 或 _ 开头
|
|
|
function isReserved (str) {
|
|
|
// 获取字符串首字符的 Unicode 编码
|
|
|
var c = (str + '').charCodeAt(0);
|
|
|
// 检查是否为 $ 或 _ 的编码
|
|
|
return c === 0x24 || c === 0x5F;
|
|
|
}
|
|
|
|
|
|
// 在对象上定义一个属性
|
|
|
function def (obj, key, val, enumerable) {
|
|
|
// 使用 Object.defineProperty 方法定义属性
|
|
|
Object.defineProperty(obj, key, {
|
|
|
// 属性的值
|
|
|
value: val,
|
|
|
// 是否可枚举
|
|
|
enumerable: !!enumerable,
|
|
|
// 是否可写
|
|
|
writable: true,
|
|
|
// 是否可配置
|
|
|
configurable: true
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 用于检查路径是否包含非法字符的正则表达式
|
|
|
var bailRE = new RegExp(("[^" + (unicodeRegExp.source) + ".$_\\d]"));
|
|
|
|
|
|
// 解析简单路径,返回一个函数用于获取对象上的属性值
|
|
|
function parsePath (path) {
|
|
|
// 检查路径是否包含非法字符
|
|
|
if (bailRE.test(path)) {
|
|
|
return;
|
|
|
}
|
|
|
// 将路径按点分割成数组
|
|
|
var segments = path.split('.');
|
|
|
return function (obj) {
|
|
|
// 遍历路径数组,逐级获取对象属性值
|
|
|
for (var i = 0; i < segments.length; i++) {
|
|
|
if (!obj) {
|
|
|
return;
|
|
|
}
|
|
|
obj = obj[segments[i]];
|
|
|
}
|
|
|
return obj;
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 返回 factory 函数的返回值
|
|
|
return {};
|
|
|
}));
|
|
|
// 空注释,可能用于代码分隔
|
|
|
// 检查当前环境是否支持使用 __proto__ 属性
|
|
|
var hasProto = '__proto__' in {};
|
|
|
|
|
|
// 进行浏览器环境嗅探,判断代码运行的环境
|
|
|
// 判断是否在浏览器环境中,通过检查 window 对象是否存在
|
|
|
var inBrowser = typeof window !== 'undefined';
|
|
|
// 判断是否在 Weex 环境中,通过检查 WXEnvironment 对象是否存在
|
|
|
var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;
|
|
|
// 获取 Weex 环境的平台信息,并转换为小写
|
|
|
var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();
|
|
|
// 获取浏览器环境下的用户代理信息,并转换为小写
|
|
|
var UA = inBrowser && window.navigator.userAgent.toLowerCase();
|
|
|
// 判断是否为 IE 浏览器
|
|
|
var isIE = UA && /msie|trident/.test(UA);
|
|
|
// 判断是否为 IE9 浏览器
|
|
|
var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
|
|
|
// 判断是否为 Edge 浏览器
|
|
|
var isEdge = UA && UA.indexOf('edge/') > 0;
|
|
|
// 判断是否为 Android 设备
|
|
|
var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');
|
|
|
// 判断是否为 iOS 设备
|
|
|
var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');
|
|
|
// 判断是否为 Chrome 浏览器
|
|
|
var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge;
|
|
|
// 判断是否为 PhantomJS 环境
|
|
|
var isPhantomJS = UA && /phantomjs/.test(UA);
|
|
|
// 判断是否为 Firefox 浏览器,并获取其版本号
|
|
|
var isFF = UA && UA.match(/firefox\/(\d+)/);
|
|
|
|
|
|
// Firefox 在 Object.prototype 上有 "watch" 函数
|
|
|
var nativeWatch = ({}).watch;
|
|
|
|
|
|
// 检查浏览器是否支持 passive 选项
|
|
|
var supportsPassive = false;
|
|
|
if (inBrowser) {
|
|
|
try {
|
|
|
// 创建一个空对象用于测试 passive 选项
|
|
|
var opts = {};
|
|
|
// 使用 Object.defineProperty 定义 passive 属性,当访问该属性时设置 supportsPassive 为 true
|
|
|
Object.defineProperty(opts, 'passive', {
|
|
|
get: function get () {
|
|
|
// 忽略此代码块的测试覆盖率检查
|
|
|
/* istanbul ignore next */
|
|
|
supportsPassive = true;
|
|
|
}
|
|
|
});
|
|
|
// 尝试添加一个测试事件监听器,使用 passive 选项
|
|
|
window.addEventListener('test-passive', null, opts);
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
|
|
|
// 用于存储是否为服务器渲染的标志,需要延迟求值
|
|
|
var _isServer;
|
|
|
// 判断是否为服务器渲染的函数
|
|
|
var isServerRendering = function () {
|
|
|
if (_isServer === undefined) {
|
|
|
// 忽略此代码块的测试覆盖率检查
|
|
|
/* istanbul ignore if */
|
|
|
if (!inBrowser && !inWeex && typeof global !== 'undefined') {
|
|
|
// 检测 vue-server-renderer 的存在,并避免 Webpack 对 process 进行填充
|
|
|
_isServer = global['process'] && global['process'].env.VUE_ENV === 'server';
|
|
|
} else {
|
|
|
_isServer = false;
|
|
|
}
|
|
|
}
|
|
|
return _isServer;
|
|
|
};
|
|
|
|
|
|
// 检测是否存在 Vue 开发者工具
|
|
|
var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
|
|
|
|
// 检查构造函数是否为原生构造函数
|
|
|
/* istanbul ignore next */
|
|
|
function isNative (Ctor) {
|
|
|
return typeof Ctor === 'function' && /native code/.test(Ctor.toString());
|
|
|
}
|
|
|
|
|
|
// 检查当前环境是否支持 Symbol 和 Reflect.ownKeys
|
|
|
var hasSymbol =
|
|
|
typeof Symbol !== 'undefined' && isNative(Symbol) &&
|
|
|
typeof Reflect !== 'undefined' && isNative(Reflect.ownKeys);
|
|
|
|
|
|
// 定义一个 Set 类型的变量
|
|
|
var _Set;
|
|
|
// 忽略此代码块的测试覆盖率检查
|
|
|
/* istanbul ignore if */
|
|
|
if (typeof Set !== 'undefined' && isNative(Set)) {
|
|
|
// 如果原生支持 Set,则使用原生 Set
|
|
|
_Set = Set;
|
|
|
} else {
|
|
|
// 实现一个非标准的 Set 填充,仅适用于原始类型的键
|
|
|
_Set = (function () {
|
|
|
// Set 构造函数
|
|
|
function Set () {
|
|
|
// 使用一个空对象来模拟 Set 的存储
|
|
|
this.set = Object.create(null);
|
|
|
}
|
|
|
// 检查 Set 中是否存在指定的键
|
|
|
Set.prototype.has = function has (key) {
|
|
|
return this.set[key] === true;
|
|
|
};
|
|
|
// 向 Set 中添加一个键
|
|
|
Set.prototype.add = function add (key) {
|
|
|
this.set[key] = true;
|
|
|
};
|
|
|
// 清空 Set
|
|
|
Set.prototype.clear = function clear () {
|
|
|
this.set = Object.create(null);
|
|
|
};
|
|
|
|
|
|
return Set;
|
|
|
}());
|
|
|
}
|
|
|
|
|
|
// 初始化警告、提示和组件跟踪相关的函数为空函数
|
|
|
var warn = noop;
|
|
|
var tip = noop;
|
|
|
// 绕过 Flow 类型检查
|
|
|
var generateComponentTrace = (noop);
|
|
|
var formatComponentName = (noop);
|
|
|
|
|
|
// 在开发环境下进行处理
|
|
|
{
|
|
|
// 检查是否存在 console 对象
|
|
|
var hasConsole = typeof console !== 'undefined';
|
|
|
// 用于将连字符或下划线分隔的字符串转换为驼峰式的正则表达式
|
|
|
var classifyRE = /(?:^|[-_])(\w)/g;
|
|
|
// 将连字符或下划线分隔的字符串转换为驼峰式的函数
|
|
|
var classify = function (str) {
|
|
|
return str
|
|
|
.replace(classifyRE, function (c) { return c.toUpperCase(); })
|
|
|
.replace(/[-_]/g, '');
|
|
|
};
|
|
|
|
|
|
// 定义警告函数
|
|
|
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 且未禁止警告,则在控制台输出警告信息
|
|
|
console.error(("[Vue warn]: " + msg + trace));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 定义提示函数
|
|
|
tip = function (msg, vm) {
|
|
|
if (hasConsole && (!config.silent)) {
|
|
|
// 如果存在 console 且未禁止警告,则在控制台输出提示信息
|
|
|
console.warn("[Vue tip]: " + msg + (
|
|
|
vm ? generateComponentTrace(vm) : ''
|
|
|
));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 格式化组件名称的函数
|
|
|
formatComponentName = function (vm, includeFile) {
|
|
|
if (vm.$root === vm) {
|
|
|
// 如果是根组件,则返回 <Root>
|
|
|
return '<Root>';
|
|
|
}
|
|
|
// 获取组件的选项对象
|
|
|
var options = typeof vm === 'function' && vm.cid != null
|
|
|
? vm.options
|
|
|
: vm._isVue
|
|
|
? vm.$options || vm.constructor.options
|
|
|
: vm;
|
|
|
// 获取组件的名称
|
|
|
var name = options.name || options._componentTag;
|
|
|
// 获取组件的文件路径
|
|
|
var file = options.__file;
|
|
|
if (!name && file) {
|
|
|
// 如果没有名称但有文件路径,则从文件路径中提取组件名称
|
|
|
var match = file.match(/([^/\\]+)\.vue$/);
|
|
|
name = match && match[1];
|
|
|
}
|
|
|
|
|
|
return (
|
|
|
// 格式化组件名称
|
|
|
(name ? ("<" + (classify(name)) + ">") : "<Anonymous>") +
|
|
|
(file && includeFile !== false ? (" at " + file) : '')
|
|
|
);
|
|
|
};
|
|
|
|
|
|
// 重复字符串的函数
|
|
|
var repeat = function (str, n) {
|
|
|
var res = '';
|
|
|
while (n) {
|
|
|
if (n % 2 === 1) {
|
|
|
// 如果 n 为奇数,则将 str 添加到结果中
|
|
|
res += str;
|
|
|
}
|
|
|
if (n > 1) {
|
|
|
// 如果 n 大于 1,则将 str 翻倍
|
|
|
str += str;
|
|
|
}
|
|
|
// 将 n 右移一位
|
|
|
n >>= 1;
|
|
|
}
|
|
|
return res;
|
|
|
};
|
|
|
|
|
|
// 生成组件跟踪信息的函数
|
|
|
generateComponentTrace = function (vm) {
|
|
|
if (vm._isVue && vm.$parent) {
|
|
|
// 如果是 Vue 组件且有父组件
|
|
|
var tree = [];
|
|
|
var currentRecursiveSequence = 0;
|
|
|
while (vm) {
|
|
|
if (tree.length > 0) {
|
|
|
var last = tree[tree.length - 1];
|
|
|
if (last.constructor === vm.constructor) {
|
|
|
// 如果当前组件和上一个组件是同一个构造函数创建的,则增加递归次数
|
|
|
currentRecursiveSequence++;
|
|
|
vm = vm.$parent;
|
|
|
continue;
|
|
|
} else if (currentRecursiveSequence > 0) {
|
|
|
// 如果递归次数大于 0,则将上一个组件和递归次数作为数组添加到 tree 中
|
|
|
tree[tree.length - 1] = [last, currentRecursiveSequence];
|
|
|
currentRecursiveSequence = 0;
|
|
|
}
|
|
|
}
|
|
|
// 将当前组件添加到 tree 中
|
|
|
tree.push(vm);
|
|
|
// 切换到父组件
|
|
|
vm = vm.$parent;
|
|
|
}
|
|
|
return '\n\nfound in\n\n' + tree
|
|
|
.map(function (vm, i) {
|
|
|
return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) + (Array.isArray(vm)
|
|
|
? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)")
|
|
|
: formatComponentName(vm)));
|
|
|
})
|
|
|
.join('\n');
|
|
|
} else {
|
|
|
// 如果不是 Vue 组件或没有父组件,则直接返回组件名称
|
|
|
return ("\n\n(found in " + (formatComponentName(vm)) + ")");
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 初始化一个唯一标识符
|
|
|
var uid = 0;
|
|
|
|
|
|
// Dep 类,用于实现依赖收集和发布更新
|
|
|
// Dep 是一个可观察对象,可以有多个指令订阅它
|
|
|
var Dep = function Dep () {
|
|
|
// 为每个 Dep 实例分配一个唯一的 ID
|
|
|
this.id = uid++;
|
|
|
// 存储订阅者的数组
|
|
|
this.subs = [];
|
|
|
};
|
|
|
|
|
|
// 向 Dep 实例中添加订阅者
|
|
|
Dep.prototype.addSub = function addSub (sub) {
|
|
|
this.subs.push(sub);
|
|
|
};
|
|
|
|
|
|
// 从 Dep 实例中移除订阅者
|
|
|
Dep.prototype.removeSub = function removeSub (sub) {
|
|
|
remove(this.subs, sub);
|
|
|
};
|
|
|
|
|
|
// 触发依赖收集
|
|
|
Dep.prototype.depend = function depend () {
|
|
|
if (Dep.target) {
|
|
|
// 如果存在当前目标观察者,则让其添加对当前 Dep 实例的依赖
|
|
|
Dep.target.addDep(this);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 通知所有订阅者进行更新
|
|
|
Dep.prototype.notify = function notify () {
|
|
|
// 先复制一份订阅者列表,避免在更新过程中修改原列表导致问题
|
|
|
var subs = this.subs.slice();
|
|
|
if (!config.async) {
|
|
|
// 如果不是异步更新,则对订阅者列表进行排序,确保按正确顺序更新
|
|
|
subs.sort(function (a, b) { return a.id - b.id; });
|
|
|
}
|
|
|
for (var i = 0, l = subs.length; i < l; i++) {
|
|
|
// 调用每个订阅者的 update 方法进行更新
|
|
|
subs[i].update();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 当前正在计算的目标观察者,全局唯一,因为同一时间只能有一个观察者在计算
|
|
|
Dep.target = null;
|
|
|
// 存储目标观察者的栈
|
|
|
var targetStack = [];
|
|
|
|
|
|
// 将目标观察者压入栈中,并设置为当前目标观察者
|
|
|
function pushTarget (target) {
|
|
|
targetStack.push(target);
|
|
|
Dep.target = target;
|
|
|
}
|
|
|
|
|
|
// 从栈中弹出目标观察者,并更新当前目标观察者
|
|
|
function popTarget () {
|
|
|
targetStack.pop();
|
|
|
Dep.target = targetStack[targetStack.length - 1];
|
|
|
}
|
|
|
|
|
|
// VNode 类,用于表示虚拟 DOM 节点
|
|
|
var VNode = function VNode (
|
|
|
tag, // 节点的标签名
|
|
|
data, // 节点的数据
|
|
|
children, // 节点的子节点
|
|
|
text, // 节点的文本内容
|
|
|
elm, // 对应的真实 DOM 元素
|
|
|
context, // 节点的上下文
|
|
|
componentOptions, // 组件选项
|
|
|
asyncFactory // 异步工厂函数
|
|
|
) {
|
|
|
this.tag = tag;
|
|
|
this.data = data;
|
|
|
this.children = children;
|
|
|
this.text = text;
|
|
|
this.elm = elm;
|
|
|
this.ns = undefined; // 命名空间
|
|
|
this.context = context;
|
|
|
this.fnContext = undefined; // 函数式组件的上下文
|
|
|
this.fnOptions = undefined; // 函数式组件的选项
|
|
|
this.fnScopeId = undefined; // 函数式组件的作用域 ID
|
|
|
this.key = data && data.key; // 节点的 key
|
|
|
this.componentOptions = componentOptions;
|
|
|
this.componentInstance = undefined; // 组件实例
|
|
|
this.parent = undefined; // 父节点
|
|
|
this.raw = false; // 是否为原始节点
|
|
|
this.isStatic = false; // 是否为静态节点
|
|
|
this.isRootInsert = true; // 是否为根插入节点
|
|
|
this.isComment = false; // 是否为注释节点
|
|
|
this.isCloned = false; // 是否为克隆节点
|
|
|
this.isOnce = false; // 是否只渲染一次
|
|
|
this.asyncFactory = asyncFactory;
|
|
|
this.asyncMeta = undefined; // 异步元数据
|
|
|
this.isAsyncPlaceholder = false; // 是否为异步占位符
|
|
|
};
|
|
|
|
|
|
// 定义 VNode 原型的访问器属性
|
|
|
var prototypeAccessors = { child: { configurable: true } };
|
|
|
|
|
|
// 为了向后兼容,定义 child 访问器,返回 componentInstance
|
|
|
/* istanbul ignore next */
|
|
|
prototypeAccessors.child.get = function () {
|
|
|
return this.componentInstance;
|
|
|
};
|
|
|
|
|
|
// 将访问器属性定义到 VNode 原型上
|
|
|
Object.defineProperties( VNode.prototype, prototypeAccessors );
|
|
|
|
|
|
// 创建一个空的 VNode 节点
|
|
|
var createEmptyVNode = function (text) {
|
|
|
if ( text === void 0 ) text = '';
|
|
|
|
|
|
var node = new VNode();
|
|
|
node.text = text;
|
|
|
node.isComment = true;
|
|
|
return node;
|
|
|
};
|
|
|
|
|
|
// 创建一个文本 VNode 节点
|
|
|
function createTextVNode (val) {
|
|
|
return new VNode(undefined, undefined, undefined, String(val));
|
|
|
}
|
|
|
|
|
|
// 优化后的浅克隆函数,用于静态节点和插槽节点,避免在 DOM 操作时因引用问题出错
|
|
|
function cloneVNode (vnode) {
|
|
|
var cloned = new VNode(
|
|
|
vnode.tag,
|
|
|
vnode.data,
|
|
|
// 克隆子节点数组,避免在克隆子节点时修改原始数组
|
|
|
vnode.children && vnode.children.slice(),
|
|
|
vnode.text,
|
|
|
vnode.elm,
|
|
|
vnode.context,
|
|
|
vnode.componentOptions,
|
|
|
vnode.asyncFactory
|
|
|
);
|
|
|
cloned.ns = vnode.ns;
|
|
|
cloned.isStatic = vnode.isStatic;
|
|
|
cloned.key = vnode.key;
|
|
|
cloned.isComment = vnode.isComment;
|
|
|
cloned.fnContext = vnode.fnContext;
|
|
|
cloned.fnOptions = vnode.fnOptions;
|
|
|
cloned.fnScopeId = vnode.fnScopeId;
|
|
|
cloned.asyncMeta = vnode.asyncMeta;
|
|
|
cloned.isCloned = true;
|
|
|
return cloned;
|
|
|
}
|
|
|
|
|
|
// 由于 Flow 对动态访问数组原型方法支持不佳,不对此文件进行类型检查
|
|
|
var arrayProto = Array.prototype;
|
|
|
// 创建一个继承自数组原型的对象,用于重写数组方法
|
|
|
var arrayMethods = Object.create(arrayProto);
|
|
|
|
|
|
// 需要重写的数组方法
|
|
|
var methodsToPatch = [
|
|
|
'push',
|
|
|
'pop',
|
|
|
'shift',
|
|
|
'unshift',
|
|
|
'splice',
|
|
|
'sort',
|
|
|
'reverse'
|
|
|
];
|
|
|
|
|
|
// 拦截数组的变异方法并触发更新事件
|
|
|
methodsToPatch.forEach(function (method) {
|
|
|
// 缓存原始的数组方法
|
|
|
var original = arrayProto[method];
|
|
|
// 在 arrayMethods 对象上定义重写后的方法
|
|
|
def(arrayMethods, method, function mutator () {
|
|
|
// 将参数转换为数组
|
|
|
var args = [], len = arguments.length;
|
|
|
while ( len-- ) args[ len ] = arguments[ len ];
|
|
|
|
|
|
// 调用原始方法并获取结果
|
|
|
var result = original.apply(this, args);
|
|
|
// 获取数组的观察者实例
|
|
|
var ob = this.__ob__;
|
|
|
var inserted;
|
|
|
switch (method) {
|
|
|
case 'push':
|
|
|
case 'unshift':
|
|
|
// 如果是 push 或 unshift 方法,插入的元素就是参数
|
|
|
inserted = args;
|
|
|
break;
|
|
|
case 'splice':
|
|
|
// 如果是 splice 方法,插入的元素是参数从索引 2 开始的部分
|
|
|
inserted = args.slice(2);
|
|
|
break;
|
|
|
}
|
|
|
if (inserted) {
|
|
|
// 对插入的元素进行观察
|
|
|
ob.observeArray(inserted);
|
|
|
}
|
|
|
// 通知依赖进行更新
|
|
|
ob.dep.notify();
|
|
|
return result;
|
|
|
});
|
|
|
});
|
|
|
|
|
|
// 获取重写后的数组方法的属性名
|
|
|
var arrayKeys = Object.getOwnPropertyNames(arrayMethods);
|
|
|
|
|
|
// 用于控制是否进行观察的标志
|
|
|
var shouldObserve = true;
|
|
|
|
|
|
// 切换观察状态的函数
|
|
|
function toggleObserving (value) {
|
|
|
shouldObserve = value;
|
|
|
}
|
|
|
|
|
|
// Observer 类,用于将对象的属性转换为响应式的
|
|
|
// 该类会将目标对象的属性键转换为 getter/setter,用于收集依赖和分发更新
|
|
|
var Observer = function Observer (value) {
|
|
|
// 保存被观察的对象
|
|
|
this.value = value;
|
|
|
// 创建一个 Dep 实例,用于依赖收集和发布更新
|
|
|
this.dep = new Dep();
|
|
|
// 记录关联的 Vue 实例数量
|
|
|
this.vmCount = 0;
|
|
|
// 在被观察的对象上定义一个不可枚举的属性 __ob__,指向当前 Observer 实例
|
|
|
def(value, '__ob__', this);
|
|
|
if (Array.isArray(value)) {
|
|
|
if (hasProto) {
|
|
|
// 如果支持 __proto__,则通过原型链增强数组
|
|
|
protoAugment(value, arrayMethods);
|
|
|
} else {
|
|
|
// 否则通过定义隐藏属性增强数组
|
|
|
copyAugment(value, arrayMethods, arrayKeys);
|
|
|
}
|
|
|
// 对数组的每个元素进行观察
|
|
|
this.observeArray(value);
|
|
|
} else {
|
|
|
// 如果是对象,则遍历其属性并转换为响应式
|
|
|
this.walk(value);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 遍历对象的所有属性并将其转换为 getter/setter
|
|
|
// 此方法仅在值类型为 Object 时调用
|
|
|
Observer.prototype.walk = function walk (obj) {
|
|
|
// 获取对象的所有属性名
|
|
|
var keys = Object.keys(obj);
|
|
|
for (var i = 0; i < keys.length; i++) {
|
|
|
// 将每个属性转换为响应式
|
|
|
defineReactive$$1(obj, keys[i]);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 观察数组中的每个元素
|
|
|
Observer.prototype.observeArray = function observeArray (items) {
|
|
|
for (var i = 0, l = items.length; i < l; i++) {
|
|
|
// 对数组中的每个元素进行观察
|
|
|
observe(items[i]);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 通过 __proto__ 拦截原型链来增强目标对象或数组
|
|
|
function protoAugment (target, src) {
|
|
|
// 禁用 no-proto 规则
|
|
|
/* eslint-disable no-proto */
|
|
|
// 将目标对象的原型指向源对象
|
|
|
target.__proto__ = src;
|
|
|
// 启用 no-proto 规则
|
|
|
/* eslint-enable no-proto */
|
|
|
}
|
|
|
|
|
|
// 通过定义隐藏属性来增强目标对象或数组
|
|
|
/* istanbul ignore next */
|
|
|
function copyAugment (target, src, keys) {
|
|
|
for (var i = 0, l = keys.length; i < l; i++) {
|
|
|
var key = keys[i];
|
|
|
// 在目标对象上定义属性,值为源对象的对应属性
|
|
|
def(target, key, src[key]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 尝试为一个值创建一个观察者实例
|
|
|
// 如果成功观察,则返回新的观察者;如果该值已经有观察者,则返回现有的观察者
|
|
|
function observe (value, asRootData) {
|
|
|
if (!isObject(value) || value instanceof VNode) {
|
|
|
// 如果值不是对象或者是 VNode 实例,则不进行观察
|
|
|
return;
|
|
|
}
|
|
|
var ob;
|
|
|
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
|
|
|
// 如果值已经有观察者,则直接使用现有的观察者
|
|
|
ob = value.__ob__;
|
|
|
} else if (
|
|
|
shouldObserve &&
|
|
|
!isServerRendering() &&
|
|
|
(Array.isArray(value) || isPlainObject(value)) &&
|
|
|
Object.isExtensible(value) &&
|
|
|
!value._isVue
|
|
|
) {
|
|
|
// 如果满足观察条件,则创建一个新的观察者
|
|
|
ob = new Observer(value);
|
|
|
}
|
|
|
if (asRootData && ob) {
|
|
|
// 如果作为根数据且有观察者,则增加关联的 Vue 实例数量
|
|
|
ob.vmCount++;
|
|
|
}
|
|
|
return ob;
|
|
|
}
|
|
|
|
|
|
// 在对象上定义一个响应式属性
|
|
|
function defineReactive$$1 (
|
|
|
obj, // 目标对象
|
|
|
key, // 属性名
|
|
|
val, // 属性值
|
|
|
customSetter, // 自定义的 setter 函数
|
|
|
shallow // 是否进行浅观察
|
|
|
) {
|
|
|
// 创建一个 Dep 实例,用于依赖收集和发布更新
|
|
|
var dep = new Dep();
|
|
|
|
|
|
// 获取对象上该属性的描述符
|
|
|
var property = Object.getOwnPropertyDescriptor(obj, key);
|
|
|
if (property && property.configurable === false) {
|
|
|
// 如果属性不可配置,则直接返回
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 获取原有的 getter 和 setter 函数
|
|
|
var getter = property && property.get;
|
|
|
var setter = property && property.set;
|
|
|
if ((!getter || setter) && arguments.length === 2) {
|
|
|
// 如果没有 getter 或者有 setter 且只传入了两个参数,则获取对象上该属性的当前值
|
|
|
val = obj[key];
|
|
|
}
|
|
|
|
|
|
// 如果不是浅观察,则对属性值进行观察
|
|
|
var childOb = !shallow && observe(val);
|
|
|
// 使用 Object.defineProperty 定义属性的 getter 和 setter
|
|
|
Object.defineProperty(obj, key, {
|
|
|
enumerable: true, // 属性可枚举
|
|
|
configurable: true, // 属性可配置
|
|
|
get: function reactiveGetter () {
|
|
|
// 如果有原有的 getter 函数,则调用它获取值,否则使用传入的 val
|
|
|
var value = getter ? getter.call(obj) : val;
|
|
|
if (Dep.target) {
|
|
|
// 如果存在当前目标观察者,则让其添加对当前 Dep 实例的依赖
|
|
|
dep.depend();
|
|
|
if (childOb) {
|
|
|
// 如果有子观察者,则让当前目标观察者添加对子观察者的依赖
|
|
|
childOb.dep.depend();
|
|
|
if (Array.isArray(value)) {
|
|
|
// 如果值是数组,则对数组元素进行依赖收集
|
|
|
dependArray(value);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
return value;
|
|
|
},
|
|
|
// 定义响应式属性的 setter 函数
|
|
|
set: function reactiveSetter (newVal) {
|
|
|
// 获取当前属性的值
|
|
|
var value = getter ? getter.call(obj) : val;
|
|
|
// 禁用 no-self-compare 规则,用于处理 NaN 的情况
|
|
|
/* eslint-disable no-self-compare */
|
|
|
// 检查新值和旧值是否相等,考虑 NaN 的情况
|
|
|
if (newVal === value || (newVal !== newVal && value !== value)) {
|
|
|
return;
|
|
|
}
|
|
|
// 启用 no-self-compare 规则
|
|
|
/* eslint-enable no-self-compare */
|
|
|
// 如果存在自定义的 setter 函数,则调用它
|
|
|
if (customSetter) {
|
|
|
customSetter();
|
|
|
}
|
|
|
// 对于只有 getter 没有 setter 的访问器属性,直接返回
|
|
|
if (getter && !setter) { return; }
|
|
|
if (setter) {
|
|
|
// 如果存在原有的 setter 函数,则调用它设置新值
|
|
|
setter.call(obj, newVal);
|
|
|
} else {
|
|
|
// 否则直接更新局部变量 val 的值
|
|
|
val = newVal;
|
|
|
}
|
|
|
// 如果不是浅观察,则对新值进行观察
|
|
|
childOb = !shallow && observe(newVal);
|
|
|
// 通知所有依赖该属性的观察者进行更新
|
|
|
dep.notify();
|
|
|
},
|
|
|
|
|
|
// 在对象上设置一个属性,如果属性不存在则添加该属性并触发变更通知
|
|
|
function set (target, key, val) {
|
|
|
// 检查目标对象是否为 undefined、null 或原始类型,如果是则发出警告
|
|
|
if (isUndef(target) || isPrimitive(target)) {
|
|
|
warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
|
|
|
}
|
|
|
if (Array.isArray(target) && isValidArrayIndex(key)) {
|
|
|
// 如果目标是数组且键是有效的数组索引,则更新数组长度并插入新值
|
|
|
target.length = Math.max(target.length, key);
|
|
|
target.splice(key, 1, val);
|
|
|
return val;
|
|
|
}
|
|
|
if (key in target && !(key in Object.prototype)) {
|
|
|
// 如果键已经存在于目标对象中且不是原型链上的属性,则直接更新该属性的值
|
|
|
target[key] = val;
|
|
|
return val;
|
|
|
}
|
|
|
// 获取目标对象的观察者实例
|
|
|
var ob = (target).__ob__;
|
|
|
if (target._isVue || (ob && ob.vmCount)) {
|
|
|
// 如果目标是 Vue 实例或其根数据,发出警告并返回值
|
|
|
warn(
|
|
|
'Avoid adding reactive properties to a Vue instance or its root $data ' +
|
|
|
'at runtime - declare it upfront in the data option.'
|
|
|
);
|
|
|
return val;
|
|
|
}
|
|
|
if (!ob) {
|
|
|
// 如果目标对象没有观察者,则直接设置属性值
|
|
|
target[key] = val;
|
|
|
return val;
|
|
|
}
|
|
|
// 定义该属性为响应式属性
|
|
|
defineReactive$$1(ob.value, key, val);
|
|
|
// 通知观察者更新
|
|
|
ob.dep.notify();
|
|
|
return val;
|
|
|
}
|
|
|
|
|
|
// 删除一个属性,如果必要则触发变更通知
|
|
|
function del (target, key) {
|
|
|
// 检查目标对象是否为 undefined、null 或原始类型,如果是则发出警告
|
|
|
if (isUndef(target) || isPrimitive(target)) {
|
|
|
warn(("Cannot delete reactive property on undefined, null, or primitive value: " + ((target))));
|
|
|
}
|
|
|
if (Array.isArray(target) && isValidArrayIndex(key)) {
|
|
|
// 如果目标是数组且键是有效的数组索引,则从数组中移除该元素
|
|
|
target.splice(key, 1);
|
|
|
return;
|
|
|
}
|
|
|
// 获取目标对象的观察者实例
|
|
|
var ob = (target).__ob__;
|
|
|
if (target._isVue || (ob && ob.vmCount)) {
|
|
|
// 如果目标是 Vue 实例或其根数据,发出警告并返回
|
|
|
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;
|
|
|
}
|
|
|
// 通知观察者更新
|
|
|
ob.dep.notify();
|
|
|
}
|
|
|
|
|
|
// 当数组被访问时,收集数组元素的依赖,因为无法像属性 getter 那样拦截数组元素的访问
|
|
|
function dependArray (value) {
|
|
|
for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
|
|
|
// 获取数组元素
|
|
|
e = value[i];
|
|
|
// 如果元素有观察者,则收集其依赖
|
|
|
e && e.__ob__ && e.__ob__.dep.depend();
|
|
|
if (Array.isArray(e)) {
|
|
|
// 如果元素是数组,则递归调用 dependArray 函数收集依赖
|
|
|
dependArray(e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 选项合并策略是处理如何将父选项值和子选项值合并为最终值的函数
|
|
|
var strats = config.optionMergeStrategies;
|
|
|
|
|
|
// 有使用限制的选项处理
|
|
|
{
|
|
|
// 处理 el 和 propsData 选项的合并策略
|
|
|
strats.el = strats.propsData = function (parent, child, vm, key) {
|
|
|
if (!vm) {
|
|
|
// 如果没有 Vue 实例,发出警告
|
|
|
warn(
|
|
|
"option \"" + key + "\" can only be used during instance " +
|
|
|
'creation with the `new` keyword.'
|
|
|
);
|
|
|
}
|
|
|
// 使用默认合并策略返回结果
|
|
|
return defaultStrat(parent, child);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 递归合并两个数据对象的辅助函数
|
|
|
function mergeData (to, from) {
|
|
|
if (!from) {
|
|
|
// 如果没有源对象,则直接返回目标对象
|
|
|
return to;
|
|
|
}
|
|
|
var key, toVal, fromVal;
|
|
|
// 根据是否支持 Symbol 获取源对象的所有键
|
|
|
var keys = hasSymbol
|
|
|
? Reflect.ownKeys(from)
|
|
|
: Object.keys(from);
|
|
|
|
|
|
for (var i = 0; i < keys.length; i++) {
|
|
|
// 获取当前键
|
|
|
key = keys[i];
|
|
|
// 跳过 __ob__ 属性
|
|
|
if (key === '__ob__') { continue; }
|
|
|
// 获取目标对象和源对象上该键对应的值
|
|
|
toVal = to[key];
|
|
|
fromVal = from[key];
|
|
|
if (!hasOwn(to, key)) {
|
|
|
// 如果目标对象没有该键,则使用 set 函数设置该键的值
|
|
|
set(to, key, fromVal);
|
|
|
} else if (
|
|
|
toVal !== fromVal &&
|
|
|
isPlainObject(toVal) &&
|
|
|
isPlainObject(fromVal)
|
|
|
) {
|
|
|
// 如果目标对象和源对象上该键的值不相等,且都是普通对象,则递归合并
|
|
|
mergeData(toVal, fromVal);
|
|
|
}
|
|
|
}
|
|
|
return to;
|
|
|
}
|
|
|
|
|
|
// 合并数据函数或数据对象
|
|
|
function mergeDataOrFn (
|
|
|
parentVal, // 父数据值
|
|
|
childVal, // 子数据值
|
|
|
vm // Vue 实例
|
|
|
) {
|
|
|
if (!vm) {
|
|
|
// 在 Vue.extend 合并中,两者都应该是函数
|
|
|
if (!childVal) {
|
|
|
// 如果没有子数据值,则返回父数据值
|
|
|
return parentVal;
|
|
|
}
|
|
|
if (!parentVal) {
|
|
|
// 如果没有父数据值,则返回子数据值
|
|
|
return childVal;
|
|
|
}
|
|
|
// 当父数据值和子数据值都存在时,返回一个函数,该函数返回两者合并的结果
|
|
|
return function mergedDataFn () {
|
|
|
return mergeData(
|
|
|
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
|
|
|
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
|
|
|
);
|
|
|
};
|
|
|
} else {
|
|
|
return function mergedInstanceDataFn () {
|
|
|
// 实例合并
|
|
|
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 = function (
|
|
|
parentVal, // 父级 data 选项值
|
|
|
childVal, // 子级 data 选项值
|
|
|
vm // Vue 实例
|
|
|
) {
|
|
|
if (!vm) {
|
|
|
if (childVal && typeof childVal !== 'function') {
|
|
|
// 如果子级 data 选项存在但不是函数,发出警告并返回父级 data 选项值
|
|
|
warn(
|
|
|
'The "data" option should be a function ' +
|
|
|
'that returns a per-instance value in component ' +
|
|
|
'definitions.',
|
|
|
vm
|
|
|
);
|
|
|
return parentVal;
|
|
|
}
|
|
|
// 合并 data 选项
|
|
|
return mergeDataOrFn(parentVal, childVal);
|
|
|
}
|
|
|
// 合并 data 选项并传入 Vue 实例
|
|
|
return mergeDataOrFn(parentVal, childVal, vm);
|
|
|
};
|
|
|
|
|
|
// 生命周期钩子和 props 以数组形式合并
|
|
|
function mergeHook (
|
|
|
parentVal, // 父级钩子值
|
|
|
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.forEach(function (hook) {
|
|
|
strats[hook] = mergeHook;
|
|
|
});
|
|
|
|
|
|
// 合并资产选项(如组件、指令、过滤器)
|
|
|
function mergeAssets (
|
|
|
parentVal, // 父级资产选项值
|
|
|
childVal, // 子级资产选项值
|
|
|
vm, // Vue 实例
|
|
|
key // 选项键名
|
|
|
) {
|
|
|
// 创建一个继承自父级资产选项的对象
|
|
|
var res = Object.create(parentVal || null);
|
|
|
if (childVal) {
|
|
|
// 验证子级资产选项是否为对象类型
|
|
|
assertObjectType(key, childVal, vm);
|
|
|
// 将子级资产选项合并到结果对象中
|
|
|
return extend(res, childVal);
|
|
|
} else {
|
|
|
// 如果没有子级资产选项,则返回结果对象
|
|
|
return res;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 为每个资产类型设置合并策略
|
|
|
ASSET_TYPES.forEach(function (type) {
|
|
|
strats[type + 's'] = mergeAssets;
|
|
|
});
|
|
|
|
|
|
// 合并 watch 选项
|
|
|
strats.watch = function (
|
|
|
parentVal, // 父级 watch 选项值
|
|
|
childVal, // 子级 watch 选项值
|
|
|
vm, // Vue 实例
|
|
|
key // 选项键名
|
|
|
) {
|
|
|
// 处理 Firefox 的 Object.prototype.watch
|
|
|
if (parentVal === nativeWatch) { parentVal = undefined; }
|
|
|
if (childVal === nativeWatch) { childVal = undefined; }
|
|
|
// 如果没有子级 watch 选项,则返回一个继承自父级 watch 选项的对象
|
|
|
if (!childVal) { return Object.create(parentVal || null); }
|
|
|
{
|
|
|
// 验证子级 watch 选项是否为对象类型
|
|
|
assertObjectType(key, childVal, vm);
|
|
|
}
|
|
|
if (!parentVal) {
|
|
|
// 如果没有父级 watch 选项,则返回子级 watch 选项
|
|
|
return childVal;
|
|
|
}
|
|
|
var ret = {};
|
|
|
// 将父级 watch 选项合并到结果对象中
|
|
|
extend(ret, parentVal);
|
|
|
for (var key$1 in childVal) {
|
|
|
var parent = ret[key$1];
|
|
|
var child = childVal[key$1];
|
|
|
if (parent && !Array.isArray(parent)) {
|
|
|
// 如果父级 watch 选项不是数组,则将其转换为数组
|
|
|
parent = [parent];
|
|
|
}
|
|
|
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, // Vue 实例
|
|
|
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;
|
|
|
}
|
|
|
// 合并 provide 选项的策略
|
|
|
strats.provide = mergeDataOrFn;
|
|
|
|
|
|
// 默认合并策略,如果子选项值未定义,则返回父选项值,否则返回子选项值
|
|
|
var defaultStrat = function (parentVal, childVal) {
|
|
|
return childVal === undefined
|
|
|
? parentVal
|
|
|
: childVal;
|
|
|
}
|
|
|
|
|
|
// 验证组件名称
|
|
|
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) {
|
|
|
// 获取 options 中的 props 选项
|
|
|
var props = options.props;
|
|
|
// 如果 props 选项不存在,直接返回
|
|
|
if (!props) { return }
|
|
|
// 用于存储规范化后的 props
|
|
|
var res = {};
|
|
|
var i, val, name;
|
|
|
// 如果 props 是数组形式
|
|
|
if (Array.isArray(props)) {
|
|
|
// 获取数组长度
|
|
|
i = props.length;
|
|
|
// 从后往前遍历数组
|
|
|
while (i--) {
|
|
|
// 获取当前元素
|
|
|
val = props[i];
|
|
|
if (typeof val === 'string') {
|
|
|
// 将字符串转换为驼峰命名
|
|
|
name = camelize(val);
|
|
|
// 规范化为对象格式,类型为 null
|
|
|
res[name] = { type: null };
|
|
|
} else {
|
|
|
// 发出警告,数组形式的 props 必须是字符串
|
|
|
warn('props must be strings when using array syntax.');
|
|
|
}
|
|
|
}
|
|
|
} else if (isPlainObject(props)) {
|
|
|
// 如果 props 是对象形式
|
|
|
for (var key in props) {
|
|
|
// 获取当前属性值
|
|
|
val = props[key];
|
|
|
// 将键名转换为驼峰命名
|
|
|
name = camelize(key);
|
|
|
res[name] = isPlainObject(val)
|
|
|
? val
|
|
|
// 如果不是对象,将其包装为对象,指定类型
|
|
|
: { type: val };
|
|
|
}
|
|
|
} else {
|
|
|
// 发出警告,props 选项的值必须是数组或对象
|
|
|
warn(
|
|
|
"Invalid value for option \"props\": expected an Array or an Object, " +
|
|
|
"but got " + (toRawType(props)) + ".",
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
// 将规范化后的 props 赋值给 options.props
|
|
|
options.props = res;
|
|
|
}
|
|
|
|
|
|
// 将所有注入项规范化为基于对象的格式。
|
|
|
function normalizeInject (options, vm) {
|
|
|
// 获取 options 中的 inject 选项
|
|
|
var inject = options.inject;
|
|
|
// 如果 inject 选项不存在,直接返回
|
|
|
if (!inject) { return }
|
|
|
// 用于存储规范化后的 inject
|
|
|
var normalized = options.inject = {};
|
|
|
if (Array.isArray(inject)) {
|
|
|
// 如果 inject 是数组形式
|
|
|
for (var i = 0; i < inject.length; i++) {
|
|
|
// 规范化为对象格式,指定 from 属性
|
|
|
normalized[inject[i]] = { from: inject[i] };
|
|
|
}
|
|
|
} else if (isPlainObject(inject)) {
|
|
|
// 如果 inject 是对象形式
|
|
|
for (var key in inject) {
|
|
|
// 获取当前属性值
|
|
|
var val = inject[key];
|
|
|
normalized[key] = isPlainObject(val)
|
|
|
// 如果是对象,合并 from 属性
|
|
|
? extend({ from: key }, val)
|
|
|
// 否则,指定 from 属性
|
|
|
: { from: val };
|
|
|
}
|
|
|
} else {
|
|
|
// 发出警告,inject 选项的值必须是数组或对象
|
|
|
warn(
|
|
|
"Invalid value for option \"inject\": expected an Array or an Object, " +
|
|
|
"but got " + (toRawType(inject)) + ".",
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将原始的函数指令规范化为对象格式。
|
|
|
function normalizeDirectives (options) {
|
|
|
// 获取 options 中的 directives 选项
|
|
|
var dirs = options.directives;
|
|
|
if (dirs) {
|
|
|
// 如果 directives 选项存在
|
|
|
for (var key in dirs) {
|
|
|
// 获取当前指令定义
|
|
|
var def$$1 = dirs[key];
|
|
|
if (typeof def$$1 === 'function') {
|
|
|
// 如果是函数,将其转换为对象格式,指定 bind 和 update 方法
|
|
|
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
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将两个选项对象合并为一个新对象。
|
|
|
// 这是实例化和继承过程中使用的核心工具函数。
|
|
|
function mergeOptions (
|
|
|
parent, // 父选项对象
|
|
|
child, // 子选项对象
|
|
|
vm // Vue 实例
|
|
|
) {
|
|
|
// 检查子选项中的组件名称是否合法
|
|
|
checkComponents(child);
|
|
|
|
|
|
if (typeof child === 'function') {
|
|
|
// 如果子选项是函数,获取其 options 属性
|
|
|
child = child.options;
|
|
|
}
|
|
|
|
|
|
// 规范化子选项中的 props
|
|
|
normalizeProps(child, vm);
|
|
|
// 规范化子选项中的 inject
|
|
|
normalizeInject(child, vm);
|
|
|
// 规范化子选项中的 directives
|
|
|
normalizeDirectives(child);
|
|
|
|
|
|
// 仅当子选项是原始选项对象(不是另一次 mergeOptions 调用的结果)时,应用 extends 和 mixins
|
|
|
// 只有合并后的选项才有 _base 属性
|
|
|
if (!child._base) {
|
|
|
if (child.extends) {
|
|
|
// 递归合并 extends 选项
|
|
|
parent = mergeOptions(parent, child.extends, vm);
|
|
|
}
|
|
|
if (child.mixins) {
|
|
|
// 遍历 mixins 数组,递归合并每个 mixin
|
|
|
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
|
|
|
}
|
|
|
|
|
|
// 解析一个资产(如组件、指令、过滤器等)。
|
|
|
// 该函数用于子实例访问其祖先链中定义的资产。
|
|
|
function resolveAsset (
|
|
|
options, // 选项对象
|
|
|
type, // 资产类型(如 'components', 'directives', 'filters')
|
|
|
id, // 资产 ID
|
|
|
warnMissing // 是否在未找到资产时发出警告
|
|
|
) {
|
|
|
if (typeof id !== 'string') {
|
|
|
// 如果 ID 不是字符串,直接返回
|
|
|
return
|
|
|
}
|
|
|
// 获取该类型的资产对象
|
|
|
var assets = options[type];
|
|
|
// 先检查本地注册的变体
|
|
|
if (hasOwn(assets, id)) { return assets[id] }
|
|
|
// 将 ID 转换为驼峰命名
|
|
|
var camelizedId = camelize(id);
|
|
|
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
|
|
|
// 将驼峰命名的 ID 首字母大写
|
|
|
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, // prop 的键名
|
|
|
propOptions, // prop 的选项定义
|
|
|
propsData, // props 的数据
|
|
|
vm // Vue 实例
|
|
|
) {
|
|
|
// 获取该 prop 的选项定义
|
|
|
var prop = propOptions[key];
|
|
|
// 判断该 prop 是否未在 propsData 中提供
|
|
|
var absent = !hasOwn(propsData, key);
|
|
|
// 获取该 prop 的值
|
|
|
var value = propsData[key];
|
|
|
// 布尔值转换
|
|
|
var booleanIndex = getTypeIndex(Boolean, prop.type);
|
|
|
if (booleanIndex > -1) {
|
|
|
if (absent && !hasOwn(prop, 'default')) {
|
|
|
// 如果未提供且没有默认值,将值设为 false
|
|
|
value = false;
|
|
|
} else if (value === '' || value === hyphenate(key)) {
|
|
|
// 仅当布尔类型优先级高于字符串类型时,将空字符串或同名值转换为布尔值
|
|
|
var stringIndex = getTypeIndex(String, prop.type);
|
|
|
if (stringIndex < 0 || booleanIndex < stringIndex) {
|
|
|
value = true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 检查默认值
|
|
|
if (value === undefined) {
|
|
|
// 获取该 prop 的默认值
|
|
|
value = getPropDefaultValue(vm, prop, key);
|
|
|
// 由于默认值是一个新副本,确保对其进行观察
|
|
|
var prevShouldObserve = shouldObserve;
|
|
|
toggleObserving(true);
|
|
|
observe(value);
|
|
|
toggleObserving(prevShouldObserve);
|
|
|
}
|
|
|
// 断言该 prop 的值是否合法
|
|
|
assertProp(prop, key, value, vm, absent);
|
|
|
return value
|
|
|
}
|
|
|
|
|
|
// 获取一个 prop 的默认值。
|
|
|
function getPropDefaultValue (vm, prop, key) {
|
|
|
// 如果没有默认值,返回 undefined
|
|
|
if (!hasOwn(prop, 'default')) {
|
|
|
return undefined
|
|
|
}
|
|
|
// 获取默认值
|
|
|
var def = prop.default;
|
|
|
// 警告对象和数组类型的非工厂函数默认值
|
|
|
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
|
|
|
);
|
|
|
}
|
|
|
// 如果之前渲染时该 prop 的原始值也是 undefined,返回之前的默认值以避免不必要的 watcher 触发
|
|
|
if (vm && vm.$options.propsData &&
|
|
|
vm.$options.propsData[key] === undefined &&
|
|
|
vm._props[key] !== undefined
|
|
|
) {
|
|
|
return vm._props[key]
|
|
|
}
|
|
|
// 对于非函数类型,调用工厂函数获取默认值
|
|
|
// 如果一个值的原型是函数,即使在不同的执行上下文中,它也是函数
|
|
|
return typeof def === 'function' && getType(prop.type) !== 'Function'
|
|
|
? def.call(vm)
|
|
|
: def
|
|
|
}
|
|
|
|
|
|
// 断言一个 prop 是否合法。
|
|
|
function assertProp (
|
|
|
prop, // prop 的选项定义
|
|
|
name, // prop 的名称
|
|
|
value, // prop 的值
|
|
|
vm, // Vue 实例
|
|
|
absent // 该 prop 是否未提供
|
|
|
) {
|
|
|
if (prop.required && absent) {
|
|
|
// 如果 prop 是必需的但未提供,发出警告
|
|
|
warn(
|
|
|
'Missing required prop: "' + name + '"',
|
|
|
vm
|
|
|
);
|
|
|
return
|
|
|
}
|
|
|
if (value == null && !prop.required) {
|
|
|
// 如果值为 null 或 undefined 且 prop 不是必需的,直接返回
|
|
|
return
|
|
|
}
|
|
|
// 获取 prop 的类型定义
|
|
|
var type = prop.type;
|
|
|
// 初始化合法性标志
|
|
|
var valid = !type || type === true;
|
|
|
// 存储期望的类型
|
|
|
var expectedTypes = [];
|
|
|
if (type) {
|
|
|
if (!Array.isArray(type)) {
|
|
|
// 如果类型不是数组,将其转换为数组
|
|
|
type = [type];
|
|
|
}
|
|
|
// 遍历类型数组,检查值是否符合类型要求
|
|
|
for (var i = 0; i < type.length && !valid; i++) {
|
|
|
var assertedType = assertType(value, type[i]);
|
|
|
// 存储期望的类型
|
|
|
expectedTypes.push(assertedType.expectedType || '');
|
|
|
// 更新合法性标志
|
|
|
valid = assertedType.valid;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if (!valid) {
|
|
|
// 如果值不合法,发出警告
|
|
|
warn(
|
|
|
getInvalidTypeMessage(name, value, expectedTypes),
|
|
|
vm
|
|
|
);
|
|
|
return
|
|
|
}
|
|
|
// 获取自定义验证器
|
|
|
var validator = prop.validator;
|
|
|
if (validator) {
|
|
|
if (!validator(value)) {
|
|
|
// 如果自定义验证器检查失败,发出警告
|
|
|
warn(
|
|
|
'Invalid prop: custom validator check failed for prop "' + name + '".',
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 简单类型检查的正则表达式
|
|
|
var simpleCheckRE = /^(String|Number|Boolean|Function|Symbol)$/;
|
|
|
|
|
|
// 断言值的类型是否符合期望类型
|
|
|
function assertType (value, type) {
|
|
|
var valid;
|
|
|
// 获取期望类型的名称
|
|
|
var expectedType = getType(type);
|
|
|
if (simpleCheckRE.test(expectedType)) {
|
|
|
// 如果是简单类型
|
|
|
var t = typeof value;
|
|
|
// 检查基本类型是否匹配
|
|
|
valid = t === expectedType.toLowerCase();
|
|
|
// 处理原始包装对象
|
|
|
if (!valid && t === 'object') {
|
|
|
valid = value instanceof type;
|
|
|
}
|
|
|
} else if (expectedType === 'Object') {
|
|
|
// 如果期望类型是对象,检查是否为纯对象
|
|
|
valid = isPlainObject(value);
|
|
|
} else if (expectedType === 'Array') {
|
|
|
// 如果期望类型是数组,检查是否为数组
|
|
|
valid = Array.isArray(value);
|
|
|
} else {
|
|
|
// 其他情况,检查是否为该类型的实例
|
|
|
valid = value instanceof type;
|
|
|
}
|
|
|
return {
|
|
|
valid: valid,
|
|
|
expectedType: expectedType
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 使用函数的字符串名称来检查内置类型,
|
|
|
// 因为在不同的 vm 或 iframe 中运行时,简单的相等检查会失败。
|
|
|
function getType (fn) {
|
|
|
// 匹配函数名
|
|
|
var match = fn && fn.toString().match(/^\s*function (\w+)/);
|
|
|
return match ? match[1] : ''
|
|
|
}
|
|
|
|
|
|
// 判断两个类型是否相同
|
|
|
function isSameType (a, b) {
|
|
|
return getType(a) === getType(b)
|
|
|
}
|
|
|
|
|
|
// 获取类型在期望类型数组中的索引
|
|
|
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);
|
|
|
var expectedValue = styleValue(value, expectedType);
|
|
|
var receivedValue = styleValue(value, receivedType);
|
|
|
// 检查是否需要指定期望的值
|
|
|
if (expectedTypes.length === 1 &&
|
|
|
isExplicable(expectedType) &&
|
|
|
!isBoolean(expectedType, receivedType)) {
|
|
|
message += " with value " + expectedValue;
|
|
|
}
|
|
|
message += ", got " + receivedType + " ";
|
|
|
// 检查是否需要指定接收到的值
|
|
|
if (isExplicable(receivedType)) {
|
|
|
message += "with value " + receivedValue + ".";
|
|
|
}
|
|
|
return message
|
|
|
}
|
|
|
|
|
|
// 格式化值以显示类型
|
|
|
function styleValue (value, type) {
|
|
|
if (type === 'String') {
|
|
|
return ("\"" + value + "\"")
|
|
|
} else if (type === 'Number') {
|
|
|
return ("" + (Number(value)))
|
|
|
} else {
|
|
|
return ("" + value)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 判断值是否可以明确表示
|
|
|
function isExplicable (value) {
|
|
|
var explicitTypes = ['string', 'number', 'boolean'];
|
|
|
return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; })
|
|
|
}
|
|
|
|
|
|
// 判断参数中是否包含布尔类型
|
|
|
function isBoolean () {
|
|
|
var args = [], len = arguments.length;
|
|
|
while ( len-- ) args[ len ] = arguments[ len ];
|
|
|
|
|
|
return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; })
|
|
|
}
|
|
|
|
|
|
// 处理错误。
|
|
|
function handleError (err, vm, info) {
|
|
|
// 在处理错误处理程序时停用依赖跟踪,以避免可能的无限渲染。
|
|
|
// 参考:https://github.com/vuejs/vuex/issues/1505
|
|
|
pushTarget();
|
|
|
try {
|
|
|
if (vm) {
|
|
|
var cur = vm;
|
|
|
// 遍历父实例链
|
|
|
while ((cur = cur.$parent)) {
|
|
|
// 获取父实例的 errorCaptured 钩子
|
|
|
var hooks = cur.$options.errorCaptured;
|
|
|
if (hooks) {
|
|
|
// 遍历 errorCaptured 钩子数组
|
|
|
for (var i = 0; i < hooks.length; i++) {
|
|
|
try {
|
|
|
// 调用钩子函数,若返回 false 则停止捕获
|
|
|
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, // Vue 实例
|
|
|
info // 错误信息
|
|
|
) {
|
|
|
var res;
|
|
|
try {
|
|
|
// 调用处理程序
|
|
|
res = args ? handler.apply(context, args) : handler.call(context);
|
|
|
if (res && !res._isVue && isPromise(res) && !res._handled) {
|
|
|
// 如果结果是 Promise 且未处理过,捕获错误并处理
|
|
|
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
|
|
|
// 标记为已处理,避免多次捕获
|
|
|
res._handled = true;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 处理调用过程中的错误
|
|
|
handleError(e, vm, info);
|
|
|
}
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 全局处理错误。
|
|
|
function globalHandleError (err, vm, info) {
|
|
|
if (config.errorHandler) {
|
|
|
try {
|
|
|
// 调用用户配置的错误处理程序
|
|
|
return config.errorHandler.call(null, err, vm, info)
|
|
|
} catch (e) {
|
|
|
// 如果用户在处理程序中故意抛出原始错误,不重复记录
|
|
|
if (e !== err) {
|
|
|
logError(e, null, 'config.errorHandler');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 记录错误
|
|
|
logError(err, vm, info);
|
|
|
}
|
|
|
|
|
|
// 记录错误。
|
|
|
function logError (err, vm, info) {
|
|
|
// 发出警告信息
|
|
|
warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);
|
|
|
if ((inBrowser || inWeex) && typeof console !== 'undefined') {
|
|
|
// 在浏览器或 Weex 环境中,使用 console.error 输出错误
|
|
|
console.error(err);
|
|
|
} else {
|
|
|
// 其他环境抛出错误
|
|
|
throw err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 标记是否使用微任务
|
|
|
var isUsingMicroTask = false;
|
|
|
|
|
|
// 存储回调函数的数组
|
|
|
var callbacks = [];
|
|
|
// 标记是否有回调函数正在等待执行
|
|
|
var pending = false;
|
|
|
|
|
|
// 执行所有回调函数
|
|
|
function flushCallbacks () {
|
|
|
// 标记为不再有回调函数等待执行
|
|
|
pending = false;
|
|
|
// 复制回调函数数组
|
|
|
var copies = callbacks.slice(0);
|
|
|
// 清空回调函数数组
|
|
|
callbacks.length = 0;
|
|
|
// 遍历并执行所有回调函数
|
|
|
for (var i = 0; i < copies.length; i++) {
|
|
|
copies[i]();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 这里我们使用微任务进行异步延迟包装。
|
|
|
// 在 2.5 版本中,我们使用(宏)任务(结合微任务)。
|
|
|
// 然而,当在重绘之前更改状态时,这会有一些微妙的问题
|
|
|
// (例如 #6813,out-in 过渡效果)。
|
|
|
// 此外,在事件处理程序中使用(宏)任务会导致一些无法避免的奇怪行为
|
|
|
// (例如 #7109、#7153、#7546、#7834、#8109)。
|
|
|
// 所以现在我们再次在所有地方使用微任务。
|
|
|
// 这种权衡的一个主要缺点是,在某些情况下
|
|
|
// 微任务的优先级太高,会在本应顺序执行的事件之间触发
|
|
|
// (例如 #4521、#6690,这些都有解决方法)
|
|
|
// 甚至在同一事件的冒泡过程中触发(#6566)。
|
|
|
var timerFunc;
|
|
|
|
|
|
// nextTick 行为利用了微任务队列,可以通过原生 Promise.then 或 MutationObserver 访问。
|
|
|
// MutationObserver 的支持范围更广,但在 iOS >= 9.3.3 的 UIWebView 中,当在触摸事件处理程序中触发时,它存在严重的 bug。
|
|
|
// 触发几次后,它会完全停止工作……所以,如果原生 Promise 可用,我们将使用它:
|
|
|
if (typeof Promise !== 'undefined' && isNative(Promise)) {
|
|
|
var p = Promise.resolve();
|
|
|
timerFunc = function () {
|
|
|
// 使用 Promise.then 异步执行回调函数
|
|
|
p.then(flushCallbacks);
|
|
|
// 在有问题的 UIWebViews 中,Promise.then 不会完全崩溃,但
|
|
|
// 它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器
|
|
|
// 需要做一些其他工作,例如处理定时器。因此,我们可以
|
|
|
// 通过添加一个空定时器来“强制”刷新微任务队列。
|
|
|
if (isIOS) { setTimeout(noop); }
|
|
|
};
|
|
|
// 标记为使用微任务
|
|
|
isUsingMicroTask = true;
|
|
|
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
|
|
|
isNative(MutationObserver) ||
|
|
|
// PhantomJS 和 iOS 7.x
|
|
|
MutationObserver.toString() === '[object MutationObserverConstructor]'
|
|
|
)) {
|
|
|
// 在原生 Promise 不可用时使用 MutationObserver,
|
|
|
// 例如 PhantomJS、iOS7、Android 4.4
|
|
|
// (#6466 MutationObserver 在 IE11 中不可靠)
|
|
|
var counter = 1;
|
|
|
var observer = new MutationObserver(flushCallbacks);
|
|
|
var textNode = document.createTextNode(String(counter));
|
|
|
observer.observe(textNode, {
|
|
|
characterData: true
|
|
|
});
|
|
|
timerFunc = function () {
|
|
|
counter = (counter + 1) % 2;
|
|
|
textNode.data = String(counter);
|
|
|
};
|
|
|
// 标记为使用微任务
|
|
|
isUsingMicroTask = true;
|
|
|
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
|
|
|
// 回退到 setImmediate。
|
|
|
// 从技术上讲,它利用了(宏)任务队列,
|
|
|
// 但它仍然比 setTimeout 更好。
|
|
|
timerFunc = function () {
|
|
|
setImmediate(flushCallbacks);
|
|
|
};
|
|
|
} else {
|
|
|
// 回退到 setTimeout。
|
|
|
timerFunc = function () {
|
|
|
setTimeout(flushCallbacks, 0);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 定义 nextTick 函数,用于在下次 DOM 更新循环结束之后执行延迟回调
|
|
|
function nextTick (cb, ctx) {
|
|
|
// 定义 _resolve 变量,用于存储 Promise 的 resolve 函数
|
|
|
var _resolve;
|
|
|
// 将回调函数添加到 callbacks 数组中
|
|
|
callbacks.push(function () {
|
|
|
// 如果传入了回调函数
|
|
|
if (cb) {
|
|
|
try {
|
|
|
// 执行回调函数,并绑定上下文
|
|
|
cb.call(ctx);
|
|
|
} catch (e) {
|
|
|
// 处理回调函数执行过程中的错误
|
|
|
handleError(e, ctx, 'nextTick');
|
|
|
}
|
|
|
} else if (_resolve) {
|
|
|
// 如果没有传入回调函数,但有 _resolve 变量,调用 _resolve 并传入上下文
|
|
|
_resolve(ctx);
|
|
|
}
|
|
|
});
|
|
|
// 如果没有正在等待执行的回调函数
|
|
|
if (!pending) {
|
|
|
// 标记为有回调函数正在等待执行
|
|
|
pending = true;
|
|
|
// 调用 timerFunc 函数,异步执行回调函数
|
|
|
timerFunc();
|
|
|
}
|
|
|
// $flow-disable-line
|
|
|
// 如果没有传入回调函数,并且支持 Promise
|
|
|
if (!cb && typeof Promise !== 'undefined') {
|
|
|
// 返回一个 Promise 对象
|
|
|
return new Promise(function (resolve) {
|
|
|
// 将 resolve 函数赋值给 _resolve 变量
|
|
|
_resolve = resolve;
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 定义 mark 和 measure 变量,用于性能标记和测量
|
|
|
var mark;
|
|
|
var measure;
|
|
|
|
|
|
// 在开发环境下执行以下代码
|
|
|
{
|
|
|
// 检查是否在浏览器环境中,并且支持 window.performance
|
|
|
var perf = inBrowser && window.performance;
|
|
|
// 如果支持性能标记和测量相关的方法
|
|
|
if (
|
|
|
perf &&
|
|
|
perf.mark &&
|
|
|
perf.measure &&
|
|
|
perf.clearMarks &&
|
|
|
perf.clearMeasures
|
|
|
) {
|
|
|
// 定义 mark 函数,用于创建性能标记
|
|
|
mark = function (tag) { return perf.mark(tag); };
|
|
|
// 定义 measure 函数,用于测量两个标记之间的性能
|
|
|
measure = function (name, startTag, endTag) {
|
|
|
// 测量两个标记之间的性能
|
|
|
perf.measure(name, startTag, endTag);
|
|
|
// 清除开始标记
|
|
|
perf.clearMarks(startTag);
|
|
|
// 清除结束标记
|
|
|
perf.clearMarks(endTag);
|
|
|
// perf.clearMeasures(name)
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 不对此文件进行类型检查,因为 Flow 与 Proxy 不兼容
|
|
|
var initProxy;
|
|
|
|
|
|
// 在开发环境下执行以下代码
|
|
|
{
|
|
|
// 定义一个允许的全局变量的映射
|
|
|
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,' +
|
|
|
'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
|
|
|
);
|
|
|
};
|
|
|
|
|
|
// 定义一个警告函数,用于提示以 $ 或 _ 开头的属性需要通过 $data 访问
|
|
|
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
|
|
|
);
|
|
|
};
|
|
|
|
|
|
// 检查是否支持 Proxy
|
|
|
var hasProxy =
|
|
|
typeof Proxy !== 'undefined' && isNative(Proxy);
|
|
|
|
|
|
// 如果支持 Proxy
|
|
|
if (hasProxy) {
|
|
|
// 定义一个内置修饰符的映射
|
|
|
var isBuiltInModifier = makeMap('stop,prevent,self,ctrl,shift,alt,meta,exact');
|
|
|
// 使用 Proxy 对 config.keyCodes 进行拦截
|
|
|
config.keyCodes = new Proxy(config.keyCodes, {
|
|
|
// 拦截设置属性的操作
|
|
|
set: function set (target, key, value) {
|
|
|
// 如果是内置修饰符
|
|
|
if (isBuiltInModifier(key)) {
|
|
|
// 发出警告,避免覆盖内置修饰符
|
|
|
warn(("Avoid overwriting built-in modifier in config.keyCodes: ." + key));
|
|
|
return false
|
|
|
} else {
|
|
|
// 设置属性值
|
|
|
target[key] = value;
|
|
|
return true
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 定义 has 拦截器的处理函数
|
|
|
var hasHandler = {
|
|
|
// 拦截 in 操作符
|
|
|
has: function has (target, key) {
|
|
|
// 检查对象是否有该属性
|
|
|
var has = key in target;
|
|
|
// 检查是否是允许的全局变量,或者是以 _ 开头且不在 $data 中的属性
|
|
|
var isAllowed = allowedGlobals(key) ||
|
|
|
(typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
|
|
|
// 如果对象没有该属性且不是允许的属性
|
|
|
if (!has && !isAllowed) {
|
|
|
// 如果该属性在 $data 中,提示需要通过 $data 访问
|
|
|
if (key in target.$data) { warnReservedPrefix(target, key); }
|
|
|
else { warnNonPresent(target, key); }
|
|
|
}
|
|
|
// 返回对象是否有该属性或是否是允许的属性
|
|
|
return has || !isAllowed
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 定义 get 拦截器的处理函数
|
|
|
var getHandler = {
|
|
|
// 拦截属性访问操作
|
|
|
get: function get (target, key) {
|
|
|
// 如果属性名是字符串且对象没有该属性
|
|
|
if (typeof key === 'string' && !(key in target)) {
|
|
|
// 如果该属性在 $data 中,提示需要通过 $data 访问
|
|
|
if (key in target.$data) { warnReservedPrefix(target, key); }
|
|
|
else { warnNonPresent(target, key); }
|
|
|
}
|
|
|
// 返回属性值
|
|
|
return target[key]
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 定义 initProxy 函数,用于初始化代理
|
|
|
initProxy = function initProxy (vm) {
|
|
|
// 如果支持 Proxy
|
|
|
if (hasProxy) {
|
|
|
// 确定使用哪个代理处理程序
|
|
|
var options = vm.$options;
|
|
|
var handlers = options.render && options.render._withStripped
|
|
|
? getHandler
|
|
|
: hasHandler;
|
|
|
// 创建代理对象
|
|
|
vm._renderProxy = new Proxy(vm, handlers);
|
|
|
} else {
|
|
|
// 如果不支持 Proxy,直接使用原对象
|
|
|
vm._renderProxy = vm;
|
|
|
}
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 定义一个 Set 对象,用于存储已访问过的对象的依赖 ID
|
|
|
var seenObjects = new _Set();
|
|
|
|
|
|
// 递归遍历一个对象,触发所有转换后的 getter,以便将对象内的每个嵌套属性收集为 "深度" 依赖
|
|
|
function traverse (val) {
|
|
|
// 调用 _traverse 函数进行遍历
|
|
|
_traverse(val, seenObjects);
|
|
|
// 清空 seenObjects 集合
|
|
|
seenObjects.clear();
|
|
|
}
|
|
|
|
|
|
// 内部递归遍历函数
|
|
|
function _traverse (val, seen) {
|
|
|
var i, keys;
|
|
|
// 判断 val 是否为数组
|
|
|
var isA = Array.isArray(val);
|
|
|
// 如果 val 既不是数组也不是对象,或者是冻结对象,或者是 VNode 实例,则直接返回
|
|
|
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
|
|
|
return
|
|
|
}
|
|
|
// 如果 val 有 __ob__ 属性(表示是响应式对象)
|
|
|
if (val.__ob__) {
|
|
|
// 获取该对象的依赖 ID
|
|
|
var depId = val.__ob__.dep.id;
|
|
|
// 如果 seen 集合中已经存在该依赖 ID,则直接返回
|
|
|
if (seen.has(depId)) {
|
|
|
return
|
|
|
}
|
|
|
// 将该依赖 ID 添加到 seen 集合中
|
|
|
seen.add(depId);
|
|
|
}
|
|
|
// 如果 val 是数组
|
|
|
if (isA) {
|
|
|
// 获取数组长度
|
|
|
i = val.length;
|
|
|
// 从后往前遍历数组元素
|
|
|
while (i--) { _traverse(val[i], seen); }
|
|
|
} else {
|
|
|
// 获取对象的所有键
|
|
|
keys = Object.keys(val);
|
|
|
// 获取键的数量
|
|
|
i = keys.length;
|
|
|
// 从后往前遍历键
|
|
|
while (i--) { _traverse(val[keys[i]], seen); }
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 缓存 normalizeEvent 函数的结果
|
|
|
var normalizeEvent = cached(function (name) {
|
|
|
// 判断是否为被动事件
|
|
|
var passive = name.charAt(0) === '&';
|
|
|
// 去除被动事件标记
|
|
|
name = passive ? name.slice(1) : name;
|
|
|
// 判断是否为一次性事件
|
|
|
var once$$1 = name.charAt(0) === '~'; // Prefixed last, checked first
|
|
|
// 去除一次性事件标记
|
|
|
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
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 创建一个函数调用器
|
|
|
function createFnInvoker (fns, vm) {
|
|
|
// 定义调用器函数
|
|
|
function invoker () {
|
|
|
var arguments$1 = arguments;
|
|
|
|
|
|
var fns = invoker.fns;
|
|
|
// 如果 fns 是数组
|
|
|
if (Array.isArray(fns)) {
|
|
|
// 复制一份 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")
|
|
|
}
|
|
|
}
|
|
|
// 将传入的 fns 赋值给调用器的 fns 属性
|
|
|
invoker.fns = fns;
|
|
|
// 返回调用器函数
|
|
|
return invoker
|
|
|
}
|
|
|
|
|
|
// 更新事件监听器
|
|
|
function updateListeners (
|
|
|
on,
|
|
|
oldOn,
|
|
|
add,
|
|
|
remove$$1,
|
|
|
createOnceHandler,
|
|
|
vm
|
|
|
) {
|
|
|
var name, def$$1, cur, old, event;
|
|
|
// 遍历新的事件监听器
|
|
|
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(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);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 合并 VNode 钩子函数
|
|
|
function mergeVNodeHook (def, hookKey, hook) {
|
|
|
// 如果 def 是 VNode 实例
|
|
|
if (def instanceof VNode) {
|
|
|
// 获取或创建 data.hook 对象
|
|
|
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)) {
|
|
|
// 创建一个新的调用器
|
|
|
invoker = createFnInvoker([wrappedHook]);
|
|
|
} else {
|
|
|
// 如果旧的钩子函数已经是合并后的调用器
|
|
|
if (isDef(oldHook.fns) && isTrue(oldHook.merged)) {
|
|
|
// 使用旧的调用器
|
|
|
invoker = oldHook;
|
|
|
// 将包装后的钩子函数添加到调用器的 fns 数组中
|
|
|
invoker.fns.push(wrappedHook);
|
|
|
} else {
|
|
|
// 如果旧的钩子函数是普通函数
|
|
|
invoker = createFnInvoker([oldHook, wrappedHook]);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 标记调用器为已合并
|
|
|
invoker.merged = true;
|
|
|
// 将调用器赋值给对应的钩子键
|
|
|
def[hookKey] = invoker;
|
|
|
}
|
|
|
|
|
|
// 从 VNode 数据中提取 props
|
|
|
function extractPropsFromVNodeData (
|
|
|
data,
|
|
|
Ctor,
|
|
|
tag
|
|
|
) {
|
|
|
// 只提取原始值,验证和默认值在子组件中处理
|
|
|
var propOptions = Ctor.options.props;
|
|
|
// 如果没有定义 props 选项,直接返回
|
|
|
if (isUndef(propOptions)) {
|
|
|
return
|
|
|
}
|
|
|
var res = {};
|
|
|
// 获取 data 中的 attrs 和 props
|
|
|
var attrs = data.attrs;
|
|
|
var props = data.props;
|
|
|
// 如果 attrs 或 props 存在
|
|
|
if (isDef(attrs) || isDef(props)) {
|
|
|
// 遍历 propOptions 中的每个 prop
|
|
|
for (var key in propOptions) {
|
|
|
// 获取 kebab-case 形式的 prop 名称
|
|
|
var altKey = hyphenate(key);
|
|
|
{
|
|
|
// 获取 prop 名称的小写形式
|
|
|
var keyInLowerCase = key.toLowerCase();
|
|
|
// 如果 prop 名称大小写不同,且 attrs 中存在小写形式的 prop
|
|
|
if (
|
|
|
key !== keyInLowerCase &&
|
|
|
attrs && hasOwn(attrs, keyInLowerCase)
|
|
|
) {
|
|
|
// 提示 prop 名称大小写不一致的问题
|
|
|
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 + "\"."
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
// 检查 props 中是否存在该 prop
|
|
|
checkProp(res, props, key, altKey, true) ||
|
|
|
// 检查 attrs 中是否存在该 prop
|
|
|
checkProp(res, attrs, key, altKey, false);
|
|
|
}
|
|
|
}
|
|
|
// 返回提取的 props
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 检查并提取 prop
|
|
|
function checkProp (
|
|
|
res,
|
|
|
hash,
|
|
|
key,
|
|
|
altKey,
|
|
|
preserve
|
|
|
) {
|
|
|
// 如果 hash 存在
|
|
|
if (isDef(hash)) {
|
|
|
// 如果 hash 中存在 key
|
|
|
if (hasOwn(hash, key)) {
|
|
|
// 将 key 对应的值赋给 res
|
|
|
res[key] = hash[key];
|
|
|
// 如果不需要保留该属性
|
|
|
if (!preserve) {
|
|
|
// 从 hash 中删除该属性
|
|
|
delete hash[key];
|
|
|
}
|
|
|
return true
|
|
|
} else if (hasOwn(hash, altKey)) {
|
|
|
// 如果 hash 中存在 altKey
|
|
|
res[key] = hash[altKey];
|
|
|
// 如果不需要保留该属性
|
|
|
if (!preserve) {
|
|
|
// 从 hash 中删除该属性
|
|
|
delete hash[altKey];
|
|
|
}
|
|
|
return true
|
|
|
}
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 模板编译器会在编译时静态分析模板,尽量减少规范化的需求。
|
|
|
// 对于纯 HTML 标记,可以完全跳过规范化,因为生成的渲染函数保证返回 Array<VNode>。但有两种情况需要额外的规范化:
|
|
|
|
|
|
// 1. 当子节点包含组件时 - 因为函数式组件可能返回一个数组而不是单个根节点。在这种情况下,只需要简单的规范化 - 如果任何子节点是数组,我们使用 Array.prototype.concat 扁平化整个数组。由于函数式组件已经对自己的子节点进行了规范化,所以保证只有一层深度。
|
|
|
function simpleNormalizeChildren (children) {
|
|
|
// 遍历子节点
|
|
|
for (var i = 0; i < children.length; i++) {
|
|
|
// 如果子节点是数组
|
|
|
if (Array.isArray(children[i])) {
|
|
|
// 使用 Array.prototype.concat 扁平化数组
|
|
|
return Array.prototype.concat.apply([], children)
|
|
|
}
|
|
|
}
|
|
|
// 如果没有数组子节点,直接返回子节点数组
|
|
|
return children
|
|
|
}
|
|
|
|
|
|
// 2. 当子节点包含总是生成嵌套数组的结构时,例如 <template>、<slot>、v-for,或者当子节点是用户手写的渲染函数 / JSX 提供时。在这种情况下,需要进行全面的规范化以适应所有可能的子节点值类型。
|
|
|
function normalizeChildren (children) {
|
|
|
// 如果子节点是原始值
|
|
|
return isPrimitive(children)
|
|
|
? [createTextVNode(children)]
|
|
|
: Array.isArray(children)
|
|
|
? normalizeArrayChildren(children)
|
|
|
: undefined
|
|
|
}
|
|
|
|
|
|
// 判断节点是否为文本节点
|
|
|
function isTextNode (node) {
|
|
|
return isDef(node) && isDef(node.text) && isFalse(node.isComment)
|
|
|
}
|
|
|
|
|
|
// 规范化数组形式的子节点
|
|
|
function normalizeArrayChildren (children, nestedIndex) {
|
|
|
var res = [];
|
|
|
var i, c, lastIndex, last;
|
|
|
// 遍历子节点数组
|
|
|
for (i = 0; i < children.length; i++) {
|
|
|
c = children[i];
|
|
|
// 如果子节点未定义或为布尔值,则跳过
|
|
|
if (isUndef(c) || typeof c === 'boolean') { continue }
|
|
|
// 获取结果数组的最后一个索引
|
|
|
lastIndex = res.length - 1;
|
|
|
// 获取结果数组的最后一个元素
|
|
|
last = res[lastIndex];
|
|
|
// 如果子节点是数组
|
|
|
if (Array.isArray(c)) {
|
|
|
if (c.length > 0) {
|
|
|
// 递归规范化嵌套数组
|
|
|
c = normalizeArrayChildren(c, ((nestedIndex || '') + "_" + i));
|
|
|
// 合并相邻的文本节点
|
|
|
if (isTextNode(c[0]) && isTextNode(last)) {
|
|
|
res[lastIndex] = createTextVNode(last.text + (c[0]).text);
|
|
|
c.shift();
|
|
|
}
|
|
|
// 将规范化后的子节点添加到结果数组中
|
|
|
res.push.apply(res, c);
|
|
|
}
|
|
|
} else if (isPrimitive(c)) {
|
|
|
// 如果子节点是原始值
|
|
|
if (isTextNode(last)) {
|
|
|
// 合并相邻的文本节点
|
|
|
res[lastIndex] = createTextVNode(last.text + c);
|
|
|
} else if (c !== '') {
|
|
|
// 将原始值转换为文本节点
|
|
|
res.push(createTextVNode(c));
|
|
|
}
|
|
|
} else {
|
|
|
// 如果子节点是其他类型
|
|
|
if (isTextNode(c) && isTextNode(last)) {
|
|
|
// 合并相邻的文本节点
|
|
|
res[lastIndex] = createTextVNode(last.text + c.text);
|
|
|
} else {
|
|
|
// 为嵌套数组子节点设置默认 key
|
|
|
if (isTrue(children._isVList) &&
|
|
|
isDef(c.tag) &&
|
|
|
isUndef(c.key) &&
|
|
|
isDef(nestedIndex)) {
|
|
|
c.key = "__vlist" + nestedIndex + "_" + i + "__";
|
|
|
}
|
|
|
// 将子节点添加到结果数组中
|
|
|
res.push(c);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 返回规范化后的子节点数组
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 初始化 provide 选项
|
|
|
function initProvide (vm) {
|
|
|
// 获取 vm 实例的 provide 选项
|
|
|
var provide = vm.$options.provide;
|
|
|
// 如果 provide 选项存在
|
|
|
if (provide) {
|
|
|
// 如果 provide 是函数,调用该函数并将结果赋值给 _provided
|
|
|
vm._provided = typeof provide === 'function'
|
|
|
? provide.call(vm)
|
|
|
: provide;
|
|
|
}
|
|
|
}
|
|
|
// 运行时辅助函数,用于将 v-bind="object" 合并到 VNode 的数据中
|
|
|
function bindObjectProps (
|
|
|
// VNode 的数据对象
|
|
|
data,
|
|
|
// 标签名
|
|
|
tag,
|
|
|
// v-bind 绑定的对象值
|
|
|
value,
|
|
|
// 是否作为 DOM 属性绑定
|
|
|
asProp,
|
|
|
// 是否为同步绑定
|
|
|
isSync
|
|
|
) {
|
|
|
// 如果绑定的值存在
|
|
|
if (value) {
|
|
|
// 如果绑定的值不是对象类型
|
|
|
if (!isObject(value)) {
|
|
|
// 发出警告,提示 v-bind 无参数时需要对象或数组值
|
|
|
warn(
|
|
|
'v-bind without argument expects an Object or Array value',
|
|
|
this
|
|
|
);
|
|
|
} else {
|
|
|
// 如果绑定的值是数组类型
|
|
|
if (Array.isArray(value)) {
|
|
|
// 将数组转换为对象
|
|
|
value = toObject(value);
|
|
|
}
|
|
|
// 用于存储属性的目标对象
|
|
|
var hash;
|
|
|
// 循环处理绑定对象的每个键
|
|
|
var loop = function ( key ) {
|
|
|
// 如果键是 'class'、'style' 或者是保留属性
|
|
|
if (
|
|
|
key === 'class' ||
|
|
|
key === 'style' ||
|
|
|
isReservedAttribute(key)
|
|
|
) {
|
|
|
// 目标对象为 data
|
|
|
hash = data;
|
|
|
} else {
|
|
|
// 获取 data 中的 type 属性
|
|
|
var type = data.attrs && data.attrs.type;
|
|
|
// 根据条件判断目标对象是 domProps 还是 attrs
|
|
|
hash = asProp || config.mustUseProp(tag, type, key)
|
|
|
? data.domProps || (data.domProps = {})
|
|
|
: data.attrs || (data.attrs = {});
|
|
|
}
|
|
|
// 将键转换为驼峰形式
|
|
|
var camelizedKey = camelize(key);
|
|
|
// 将键转换为连字符形式
|
|
|
var hyphenatedKey = hyphenate(key);
|
|
|
// 如果目标对象中不存在驼峰形式和连字符形式的键
|
|
|
if (!(camelizedKey in hash) && !(hyphenatedKey in hash)) {
|
|
|
// 将绑定对象中的值赋给目标对象
|
|
|
hash[key] = value[key];
|
|
|
|
|
|
// 如果是同步绑定
|
|
|
if (isSync) {
|
|
|
// 获取或创建 data 中的 on 对象
|
|
|
var on = data.on || (data.on = {});
|
|
|
// 为 update:key 事件添加处理函数
|
|
|
on[("update:" + key)] = function ($event) {
|
|
|
value[key] = $event;
|
|
|
};
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 遍历绑定对象的每个键
|
|
|
for (var key in value) loop( key );
|
|
|
}
|
|
|
}
|
|
|
// 返回处理后的 data 对象
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 运行时辅助函数,用于渲染静态树
|
|
|
function renderStatic (
|
|
|
// 静态树的索引
|
|
|
index,
|
|
|
// 是否在 v-for 指令内部
|
|
|
isInFor
|
|
|
) {
|
|
|
// 获取或创建静态树缓存数组
|
|
|
var cached = this._staticTrees || (this._staticTrees = []);
|
|
|
// 获取缓存中的静态树
|
|
|
var tree = cached[index];
|
|
|
// 如果已经渲染过静态树且不在 v-for 内部
|
|
|
if (tree && !isInFor) {
|
|
|
// 直接返回缓存的静态树
|
|
|
return tree
|
|
|
}
|
|
|
// 否则,重新渲染新的静态树
|
|
|
tree = cached[index] = this.$options.staticRenderFns[index].call(
|
|
|
// 渲染代理对象
|
|
|
this._renderProxy,
|
|
|
null,
|
|
|
// 用于函数式组件模板生成的渲染函数
|
|
|
this
|
|
|
);
|
|
|
// 标记静态树
|
|
|
markStatic(tree, ("__static__" + index), false);
|
|
|
// 返回渲染好的静态树
|
|
|
return tree
|
|
|
}
|
|
|
|
|
|
// 运行时辅助函数,用于 v-once 指令
|
|
|
// 实际上意味着用唯一的键将节点标记为静态
|
|
|
function markOnce (
|
|
|
// 要标记的 VNode 树
|
|
|
tree,
|
|
|
// 索引
|
|
|
index,
|
|
|
// 键
|
|
|
key
|
|
|
) {
|
|
|
// 标记静态节点
|
|
|
markStatic(tree, ("__once__" + index + (key ? ("_" + key) : "")), true);
|
|
|
// 返回标记后的树
|
|
|
return tree
|
|
|
}
|
|
|
|
|
|
// 标记静态节点或节点数组
|
|
|
function markStatic (
|
|
|
// 要标记的节点或节点数组
|
|
|
tree,
|
|
|
// 键
|
|
|
key,
|
|
|
// 是否为 v-once 节点
|
|
|
isOnce
|
|
|
) {
|
|
|
// 如果是节点数组
|
|
|
if (Array.isArray(tree)) {
|
|
|
// 遍历数组中的每个节点
|
|
|
for (var i = 0; i < tree.length; i++) {
|
|
|
// 如果节点存在且不是字符串类型
|
|
|
if (tree[i] && typeof tree[i] !== 'string') {
|
|
|
// 标记单个静态节点
|
|
|
markStaticNode(tree[i], (key + "_" + i), isOnce);
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// 标记单个静态节点
|
|
|
markStaticNode(tree, key, isOnce);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 标记单个静态节点
|
|
|
function markStaticNode (
|
|
|
// 要标记的节点
|
|
|
node,
|
|
|
// 键
|
|
|
key,
|
|
|
// 是否为 v-once 节点
|
|
|
isOnce
|
|
|
) {
|
|
|
// 标记节点为静态节点
|
|
|
node.isStatic = true;
|
|
|
// 设置节点的键
|
|
|
node.key = key;
|
|
|
// 标记节点是否为 v-once 节点
|
|
|
node.isOnce = isOnce;
|
|
|
}
|
|
|
|
|
|
// 将对象形式的监听器绑定到 VNode 的数据上
|
|
|
function bindObjectListeners (
|
|
|
// VNode 的数据对象
|
|
|
data,
|
|
|
// 监听器对象
|
|
|
value
|
|
|
) {
|
|
|
// 如果监听器对象存在
|
|
|
if (value) {
|
|
|
// 如果监听器对象不是普通对象
|
|
|
if (!isPlainObject(value)) {
|
|
|
// 发出警告,提示 v-on 无参数时需要对象值
|
|
|
warn(
|
|
|
'v-on without argument expects an Object value',
|
|
|
this
|
|
|
);
|
|
|
} else {
|
|
|
// 获取或创建 data 中的 on 对象
|
|
|
var on = data.on = data.on ? extend({}, data.on) : {};
|
|
|
// 遍历监听器对象的每个键
|
|
|
for (var key in value) {
|
|
|
// 获取已存在的监听器
|
|
|
var existing = on[key];
|
|
|
// 获取当前监听器
|
|
|
var ours = value[key];
|
|
|
// 如果已存在监听器,则将当前监听器添加到数组中,否则直接赋值
|
|
|
on[key] = existing ? [].concat(existing, ours) : ours;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 返回处理后的 data 对象
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 解析作用域插槽
|
|
|
function resolveScopedSlots (
|
|
|
// 作用域插槽函数数组
|
|
|
fns,
|
|
|
// 解析结果对象
|
|
|
res,
|
|
|
// 是否有动态键
|
|
|
hasDynamicKeys,
|
|
|
// 内容哈希键
|
|
|
contentHashKey
|
|
|
) {
|
|
|
// 如果结果对象不存在,则创建一个新的对象
|
|
|
res = res || { $stable: !hasDynamicKeys };
|
|
|
// 遍历作用域插槽函数数组
|
|
|
for (var i = 0; i < fns.length; i++) {
|
|
|
// 获取当前插槽
|
|
|
var slot = fns[i];
|
|
|
// 如果插槽是数组类型
|
|
|
if (Array.isArray(slot)) {
|
|
|
// 递归解析作用域插槽
|
|
|
resolveScopedSlots(slot, res, hasDynamicKeys);
|
|
|
} else if (slot) {
|
|
|
// 如果插槽有 proxy 标记
|
|
|
if (slot.proxy) {
|
|
|
// 设置插槽函数的 proxy 标记
|
|
|
slot.fn.proxy = true;
|
|
|
}
|
|
|
// 将插槽函数添加到结果对象中
|
|
|
res[slot.key] = slot.fn;
|
|
|
}
|
|
|
}
|
|
|
// 如果有内容哈希键
|
|
|
if (contentHashKey) {
|
|
|
// 将内容哈希键添加到结果对象中
|
|
|
(res).$key = contentHashKey;
|
|
|
}
|
|
|
// 返回解析结果对象
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 绑定动态键值对到基础对象上
|
|
|
function bindDynamicKeys (
|
|
|
// 基础对象
|
|
|
baseObj,
|
|
|
// 动态键值对数组
|
|
|
values
|
|
|
) {
|
|
|
// 遍历动态键值对数组
|
|
|
for (var i = 0; i < values.length; i += 2) {
|
|
|
// 获取键
|
|
|
var key = values[i];
|
|
|
// 如果键是字符串且不为空
|
|
|
if (typeof key === 'string' && key) {
|
|
|
// 将键值对添加到基础对象上
|
|
|
baseObj[values[i]] = values[i + 1];
|
|
|
} else if (key !== '' && key !== null) {
|
|
|
// null 是用于显式移除绑定的特殊值
|
|
|
// 发出警告,提示动态指令参数值无效
|
|
|
warn(
|
|
|
("Invalid value for dynamic directive argument (expected string or null): " + key),
|
|
|
this
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
// 返回处理后的基础对象
|
|
|
return baseObj
|
|
|
}
|
|
|
|
|
|
// 辅助函数,用于动态地将修饰符运行时标记添加到事件名称前
|
|
|
// 确保仅在值已经是字符串时添加,否则会被转换为字符串并导致类型检查失败
|
|
|
function prependModifier (
|
|
|
// 要处理的值
|
|
|
value,
|
|
|
// 修饰符符号
|
|
|
symbol
|
|
|
) {
|
|
|
// 如果值是字符串类型,则在前面添加修饰符符号,否则直接返回值
|
|
|
return typeof value === 'string' ? symbol + value : value
|
|
|
}
|
|
|
|
|
|
// 安装渲染辅助函数到目标对象上
|
|
|
function installRenderHelpers (
|
|
|
// 目标对象
|
|
|
target
|
|
|
) {
|
|
|
// 安装 markOnce 函数
|
|
|
target._o = markOnce;
|
|
|
// 安装 toNumber 函数
|
|
|
target._n = toNumber;
|
|
|
// 安装 toString 函数
|
|
|
target._s = toString;
|
|
|
// 安装 renderList 函数
|
|
|
target._l = renderList;
|
|
|
// 安装 renderSlot 函数
|
|
|
target._t = renderSlot;
|
|
|
// 安装 looseEqual 函数
|
|
|
target._q = looseEqual;
|
|
|
// 安装 looseIndexOf 函数
|
|
|
target._i = looseIndexOf;
|
|
|
// 安装 renderStatic 函数
|
|
|
target._m = renderStatic;
|
|
|
// 安装 resolveFilter 函数
|
|
|
target._f = resolveFilter;
|
|
|
// 安装 checkKeyCodes 函数
|
|
|
target._k = checkKeyCodes;
|
|
|
// 安装 bindObjectProps 函数
|
|
|
target._b = bindObjectProps;
|
|
|
// 安装 createTextVNode 函数
|
|
|
target._v = createTextVNode;
|
|
|
// 安装 createEmptyVNode 函数
|
|
|
target._e = createEmptyVNode;
|
|
|
// 安装 resolveScopedSlots 函数
|
|
|
target._u = resolveScopedSlots;
|
|
|
// 安装 bindObjectListeners 函数
|
|
|
target._g = bindObjectListeners;
|
|
|
// 安装 bindDynamicKeys 函数
|
|
|
target._d = bindDynamicKeys;
|
|
|
// 安装 prependModifier 函数
|
|
|
target._p = prependModifier;
|
|
|
}
|
|
|
|
|
|
// 函数式渲染上下文类
|
|
|
function FunctionalRenderContext (
|
|
|
// VNode 的数据对象
|
|
|
data,
|
|
|
// 组件的 props
|
|
|
props,
|
|
|
// 子节点
|
|
|
children,
|
|
|
// 父组件实例
|
|
|
parent,
|
|
|
// 组件构造函数
|
|
|
Ctor
|
|
|
) {
|
|
|
var this$1 = this;
|
|
|
|
|
|
// 获取组件的选项
|
|
|
var options = Ctor.options;
|
|
|
// 确保函数式组件中的 createElement 函数有唯一的上下文
|
|
|
// 这对于正确的命名插槽检查是必要的
|
|
|
var contextVm;
|
|
|
// 如果父组件有 _uid 属性
|
|
|
if (hasOwn(parent, '_uid')) {
|
|
|
// 创建一个继承自父组件的对象
|
|
|
contextVm = Object.create(parent);
|
|
|
// 记录原始父组件
|
|
|
contextVm._original = parent;
|
|
|
} else {
|
|
|
// 传入的上下文 vm 也是一个函数式上下文
|
|
|
// 在这种情况下,我们要确保能够获取到真实的上下文实例
|
|
|
contextVm = parent;
|
|
|
// 获取原始父组件
|
|
|
parent = parent._original;
|
|
|
}
|
|
|
// 判断组件是否是编译后的
|
|
|
var isCompiled = isTrue(options._compiled);
|
|
|
// 判断是否需要规范化
|
|
|
var needNormalization = !isCompiled;
|
|
|
|
|
|
// 保存数据对象
|
|
|
this.data = data;
|
|
|
// 保存 props
|
|
|
this.props = props;
|
|
|
// 保存子节点
|
|
|
this.children = children;
|
|
|
// 保存父组件实例
|
|
|
this.parent = parent;
|
|
|
// 保存监听器
|
|
|
this.listeners = data.on || emptyObject;
|
|
|
// 解析注入
|
|
|
this.injections = resolveInject(options.inject, parent);
|
|
|
// 定义 slots 方法
|
|
|
this.slots = function () {
|
|
|
// 如果 $slots 不存在
|
|
|
if (!this$1.$slots) {
|
|
|
// 规范化作用域插槽
|
|
|
normalizeScopedSlots(
|
|
|
data.scopedSlots,
|
|
|
this$1.$slots = resolveSlots(children, parent)
|
|
|
);
|
|
|
}
|
|
|
// 返回 $slots
|
|
|
return this$1.$slots
|
|
|
};
|
|
|
|
|
|
// 定义 scopedSlots 属性
|
|
|
Object.defineProperty(this, 'scopedSlots', ({
|
|
|
// 可枚举
|
|
|
enumerable: true,
|
|
|
// 获取作用域插槽
|
|
|
get: function get () {
|
|
|
return normalizeScopedSlots(data.scopedSlots, this.slots())
|
|
|
}
|
|
|
}));
|
|
|
|
|
|
// 支持编译后的函数式模板
|
|
|
if (isCompiled) {
|
|
|
// 暴露 $options 用于 renderStatic()
|
|
|
this.$options = options;
|
|
|
// 预解析插槽用于 renderSlot()
|
|
|
this.$slots = this.slots();
|
|
|
// 规范化作用域插槽
|
|
|
this.$scopedSlots = normalizeScopedSlots(data.scopedSlots, this.$slots);
|
|
|
}
|
|
|
|
|
|
// 如果有作用域 ID
|
|
|
if (options._scopeId) {
|
|
|
// 创建带有作用域 ID 的 VNode
|
|
|
this._c = function (a, b, c, d) {
|
|
|
var vnode = createElement(contextVm, a, b, c, d, needNormalization);
|
|
|
if (vnode && !Array.isArray(vnode)) {
|
|
|
vnode.fnScopeId = options._scopeId;
|
|
|
vnode.fnContext = parent;
|
|
|
}
|
|
|
return vnode
|
|
|
};
|
|
|
} else {
|
|
|
// 创建普通的 VNode
|
|
|
this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 安装渲染辅助函数到函数式渲染上下文的原型上
|
|
|
installRenderHelpers(FunctionalRenderContext.prototype);
|
|
|
|
|
|
// 创建函数式组件
|
|
|
function createFunctionalComponent (
|
|
|
// 组件构造函数
|
|
|
Ctor,
|
|
|
// props 数据
|
|
|
propsData,
|
|
|
// VNode 的数据对象
|
|
|
data,
|
|
|
// 上下文 vm
|
|
|
contextVm,
|
|
|
// 子节点
|
|
|
children
|
|
|
) {
|
|
|
// 获取组件的选项
|
|
|
var options = Ctor.options;
|
|
|
// 初始化 props 对象
|
|
|
var props = {};
|
|
|
// 获取组件的 props 选项
|
|
|
var propOptions = options.props;
|
|
|
// 如果 props 选项存在
|
|
|
if (isDef(propOptions)) {
|
|
|
// 遍历 props 选项的每个键
|
|
|
for (var key in propOptions) {
|
|
|
// 验证并设置 props 的值
|
|
|
props[key] = validateProp(key, propOptions, propsData || emptyObject);
|
|
|
}
|
|
|
} else {
|
|
|
// 如果 data 中有 attrs 属性,合并到 props 中
|
|
|
if (isDef(data.attrs)) { mergeProps(props, data.attrs); }
|
|
|
// 如果 data 中有 props 属性,合并到 props 中
|
|
|
if (isDef(data.props)) { mergeProps(props, data.props); }
|
|
|
}
|
|
|
|
|
|
// 创建函数式渲染上下文
|
|
|
var renderContext = new FunctionalRenderContext(
|
|
|
data,
|
|
|
props,
|
|
|
children,
|
|
|
contextVm,
|
|
|
Ctor
|
|
|
);
|
|
|
|
|
|
// 调用组件的渲染函数
|
|
|
var vnode = options.render.call(null, renderContext._c, renderContext);
|
|
|
|
|
|
// 如果渲染结果是单个 VNode
|
|
|
if (vnode instanceof VNode) {
|
|
|
// 克隆并标记函数式组件的结果
|
|
|
return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
|
|
|
} else if (Array.isArray(vnode)) {
|
|
|
// 规范化子节点
|
|
|
var vnodes = normalizeChildren(vnode) || [];
|
|
|
// 初始化结果数组
|
|
|
var res = new Array(vnodes.length);
|
|
|
// 遍历每个子节点
|
|
|
for (var i = 0; i < vnodes.length; i++) {
|
|
|
// 克隆并标记函数式组件的结果
|
|
|
res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext);
|
|
|
}
|
|
|
// 返回结果数组
|
|
|
return res
|
|
|
}
|
|
|
} |