|
|
// Vue.js v2.6.12 版本声明
|
|
|
// 版权所有 (c) 2014 - 2020 Evan You
|
|
|
// 基于 MIT 许可证发布
|
|
|
// 立即执行函数表达式,用于将 Vue 库暴露给不同的模块系统
|
|
|
// 参数 global 代表全局对象,factory 是一个函数,用于创建 Vue 实例
|
|
|
(function (global, factory) {
|
|
|
// 判断是否为 CommonJS 模块环境
|
|
|
// 如果 exports 是对象且 module 不是 undefined,则使用 module.exports 导出 factory 函数的返回值
|
|
|
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
|
|
|
// 判断是否为 AMD 模块环境
|
|
|
// 如果 define 是函数且支持 AMD 规范,则使用 define 函数定义模块
|
|
|
typeof define === 'function' && define.amd ? define(factory) :
|
|
|
// 既不是 CommonJS 也不是 AMD 环境,则将 factory 函数的返回值赋值给全局对象的 Vue 属性
|
|
|
(global = global || self, global.Vue = factory());
|
|
|
// 传入当前全局对象 this 和 factory 函数
|
|
|
}(this, function () { 'use strict';
|
|
|
|
|
|
// 创建一个冻结的空对象,用于避免不必要的对象创建和修改
|
|
|
var emptyObject = Object.freeze({});
|
|
|
|
|
|
// 这些辅助函数由于其明确性和函数内联,在 JS 引擎中能产生更好的虚拟机代码
|
|
|
// 检查一个值是否为 undefined 或 null
|
|
|
function isUndef (v) {
|
|
|
return v === undefined || v === null
|
|
|
}
|
|
|
|
|
|
// 检查一个值是否既不是 undefined 也不是 null
|
|
|
function isDef (v) {
|
|
|
return v !== undefined && v !== null
|
|
|
}
|
|
|
|
|
|
// 检查一个值是否严格等于 true
|
|
|
function isTrue (v) {
|
|
|
return v === true
|
|
|
}
|
|
|
|
|
|
// 检查一个值是否严格等于 false
|
|
|
function isFalse (v) {
|
|
|
return v === false
|
|
|
}
|
|
|
|
|
|
// 检查值是否为原始类型
|
|
|
function isPrimitive (value) {
|
|
|
// 检查值是否为字符串类型
|
|
|
return typeof value === 'string' ||
|
|
|
// 检查值是否为数字类型
|
|
|
typeof value === 'number' ||
|
|
|
// 忽略 Flow 类型检查,检查值是否为符号类型
|
|
|
typeof value === 'symbol' ||
|
|
|
// 检查值是否为布尔类型
|
|
|
typeof value === 'boolean'
|
|
|
}
|
|
|
|
|
|
// 快速检查对象 - 当我们知道值是符合 JSON 类型时,主要用于区分对象和原始值
|
|
|
function isObject (obj) {
|
|
|
// 检查对象是否不为 null 且类型为 object
|
|
|
return obj !== null && typeof obj === 'object'
|
|
|
}
|
|
|
|
|
|
// 获取值的原始类型字符串,例如 [object Object]
|
|
|
var _toString = Object.prototype.toString;
|
|
|
|
|
|
function toRawType (value) {
|
|
|
// 调用 Object.prototype.toString 方法获取值的类型字符串,并截取关键部分
|
|
|
return _toString.call(value).slice(8, -1)
|
|
|
}
|
|
|
|
|
|
// 严格的对象类型检查。仅当为纯 JavaScript 对象时返回 true
|
|
|
function isPlainObject (obj) {
|
|
|
// 检查对象的类型字符串是否为 [object Object]
|
|
|
return _toString.call(obj) === '[object Object]'
|
|
|
}
|
|
|
|
|
|
// 检查一个值是否为正则表达式对象
|
|
|
function isRegExp (v) {
|
|
|
return _toString.call(v) === '[object RegExp]'
|
|
|
}
|
|
|
|
|
|
// 检查 val 是否为有效的数组索引
|
|
|
function isValidArrayIndex (val) {
|
|
|
// 将值转换为浮点数
|
|
|
var n = parseFloat(String(val));
|
|
|
// 检查是否为非负整数且为有限值
|
|
|
return n >= 0 && Math.floor(n) === n && isFinite(val)
|
|
|
}
|
|
|
|
|
|
// 检查一个值是否为 Promise 对象
|
|
|
function isPromise (val) {
|
|
|
// 检查值是否已定义
|
|
|
return isDef(val) &&
|
|
|
// 检查值是否有 then 方法
|
|
|
typeof val.then === 'function' &&
|
|
|
// 检查值是否有 catch 方法
|
|
|
typeof val.catch === 'function'
|
|
|
}
|
|
|
|
|
|
// 将值转换为实际渲染的字符串
|
|
|
function toString (val) {
|
|
|
// 如果值为 null 或 undefined,返回空字符串
|
|
|
return val == null ? '' :
|
|
|
// 检查值是否为数组或纯对象且 toString 方法为默认方法
|
|
|
Array.isArray(val) || (isPlainObject(val) && val.toString === _toString) ?
|
|
|
// 如果是,使用 JSON.stringify 转换为格式化的字符串
|
|
|
JSON.stringify(val, null, 2) :
|
|
|
// 否则,直接转换为字符串
|
|
|
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;
|
|
|
}
|
|
|
// 如果期望小写,则返回一个函数,将输入转换为小写后检查是否在映射中
|
|
|
return expectsLowerCase ? function (val) { return map[val.toLowerCase()]; } :
|
|
|
// 否则,直接检查输入是否在映射中
|
|
|
function (val) { return map[val]; }
|
|
|
}
|
|
|
|
|
|
// 检查标签是否为内置标签
|
|
|
var isBuiltInTag = makeMap('slot,component', true);
|
|
|
|
|
|
// 检查属性是否为保留属性
|
|
|
var isReservedAttribute = makeMap('key,ref,slot,slot-scope,is');
|
|
|
|
|
|
// 从数组中移除一个元素
|
|
|
function remove (arr, item) {
|
|
|
// 检查数组长度是否大于 0
|
|
|
if (arr.length) {
|
|
|
// 查找元素在数组中的索引
|
|
|
var index = arr.indexOf(item);
|
|
|
// 如果索引大于 -1,表示元素存在于数组中
|
|
|
if (index > -1) {
|
|
|
// 使用 splice 方法移除元素并返回被移除的元素数组
|
|
|
return arr.splice(index, 1)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查对象是否具有某个属性
|
|
|
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 方法的环境提供简单的 bind 方法填充
|
|
|
// 例如 PhantomJS 1.x。从技术上讲,由于现在大多数浏览器中本地 bind 方法性能已经足够好,我们不再需要这个方法
|
|
|
// 但移除它会导致在 PhantomJS 1.x 中运行的代码中断,因此为了向后兼容必须保留
|
|
|
function polyfillBind (fn, ctx) {
|
|
|
function boundFn (a) {
|
|
|
// 获取参数长度
|
|
|
var l = arguments.length;
|
|
|
return l ?
|
|
|
// 如果有参数
|
|
|
l > 1 ?
|
|
|
// 如果参数数量大于 1,使用 apply 方法调用原函数
|
|
|
fn.apply(ctx, arguments) :
|
|
|
// 如果参数数量为 1,使用 call 方法调用原函数
|
|
|
fn.call(ctx, a) :
|
|
|
// 如果没有参数,使用 call 方法调用原函数
|
|
|
fn.call(ctx)
|
|
|
}
|
|
|
|
|
|
// 绑定函数的长度设置为原函数的长度
|
|
|
boundFn._length = fn.length;
|
|
|
return boundFn
|
|
|
}
|
|
|
|
|
|
// 使用原生的 bind 方法
|
|
|
function nativeBind (fn, ctx) {
|
|
|
return fn.bind(ctx)
|
|
|
}
|
|
|
|
|
|
// 如果 Function.prototype.bind 存在,则使用原生 bind 方法,否则使用填充方法
|
|
|
var bind = Function.prototype.bind ? nativeBind : polyfillBind;
|
|
|
|
|
|
// 将类似数组的对象转换为真正的数组
|
|
|
function toArray (list, start) {
|
|
|
// 如果 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
|
|
|
}
|
|
|
|
|
|
// 不执行任何操作。
|
|
|
// 为了让 Flow 满意而保留参数,同时避免留下无用的转译代码
|
|
|
// (https://flow.org/blog/2017/05/07/Strict-Function-Call-Arity/)
|
|
|
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])
|
|
|
})
|
|
|
} else {
|
|
|
return false
|
|
|
}
|
|
|
} catch (e) {
|
|
|
return false
|
|
|
}
|
|
|
} else if (!isObjectA && !isObjectB) {
|
|
|
// 如果都不是对象,将它们转换为字符串并比较
|
|
|
return String(a) === String(b)
|
|
|
} else {
|
|
|
return false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 返回在数组中可以找到宽松相等值的第一个索引(如果值是纯对象,数组必须包含相同结构的对象),如果不存在则返回 -1
|
|
|
function looseIndexOf (arr, val) {
|
|
|
// 遍历数组
|
|
|
for (var i = 0; i < arr.length; i++) {
|
|
|
// 检查当前元素是否与目标值宽松相等
|
|
|
if (looseEqual(arr[i], val)) { return i }
|
|
|
}
|
|
|
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 = ({
|
|
|
// 选项合并策略(在 core/util/options 中使用)
|
|
|
// 忽略 Flow 类型检查
|
|
|
optionMergeStrategies: Object.create(null),
|
|
|
// 是否抑制警告
|
|
|
silent: false,
|
|
|
// 在启动时显示生产模式提示消息?
|
|
|
productionTip: "development" !== 'production',
|
|
|
// 是否启用开发者工具
|
|
|
devtools: "development" !== 'production',
|
|
|
// 是否记录性能
|
|
|
performance: false,
|
|
|
// 监听器错误的错误处理函数
|
|
|
errorHandler: null,
|
|
|
// 监听器警告的警告处理函数
|
|
|
warnHandler: null,
|
|
|
// 忽略某些自定义元素
|
|
|
ignoredElements: [],
|
|
|
// v-on 的自定义用户键别名
|
|
|
// 忽略 Flow 类型检查
|
|
|
keyCodes: Object.create(null),
|
|
|
// 检查标签是否为保留标签,以便不能将其注册为组件。这依赖于平台,可能会被覆盖
|
|
|
isReservedTag: no,
|
|
|
// 检查属性是否为保留属性,以便不能将其用作组件的 prop。这依赖于平台,可能会被覆盖
|
|
|
isReservedAttr: no,
|
|
|
// 检查标签是否为未知元素。依赖于平台
|
|
|
isUnknownElement: no,
|
|
|
// 获取元素的命名空间
|
|
|
getTagNamespace: noop,
|
|
|
// 解析特定平台的真实标签名
|
|
|
parsePlatformTagName: identity,
|
|
|
// 检查属性是否必须使用属性绑定,例如 value。依赖于平台
|
|
|
mustUseProp: no,
|
|
|
// 异步执行更新。供 Vue Test Utils 使用。如果设置为 false,性能将显著降低
|
|
|
async: true,
|
|
|
// 出于遗留原因暴露
|
|
|
_lifecycleHooks: LIFECYCLE_HOOKS
|
|
|
});
|
|
|
|
|
|
// 用于解析 HTML 标签、组件名称和属性路径的 Unicode 字母。
|
|
|
// 使用 https://www.w3.org/TR/html53/semantics-scripting.html#potentialcustomelementname
|
|
|
// 跳过 \u10000 - \uEFFFF 以避免 PhantomJS 冻结
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
// 能否使用 __proto__ 属性?
|
|
|
var hasProto = '__proto__' in {};
|
|
|
|
|
|
// 浏览器环境嗅探
|
|
|
// 判断是否处于浏览器环境
|
|
|
var inBrowser = typeof window !== 'undefined';
|
|
|
// 判断是否处于 Weex 环境
|
|
|
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);
|
|
|
// 判断是否为 IE 9 浏览器
|
|
|
var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
|
|
|
// 判断是否为 Edge 浏览器
|
|
|
var isEdge = UA && UA.indexOf('edge/') > 0;
|
|
|
// 判断是否为安卓设备
|
|
|
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 {
|
|
|
// 创建一个空对象用于测试
|
|
|
var opts = {};
|
|
|
// 定义一个属性 "passive",当访问该属性时将 supportsPassive 设为 true
|
|
|
Object.defineProperty(opts, 'passive', ({
|
|
|
get: function get () {
|
|
|
supportsPassive = true;
|
|
|
}
|
|
|
}));
|
|
|
// 尝试添加一个测试事件监听器,触发对 "passive" 属性的访问
|
|
|
window.addEventListener('test-passive', null, opts);
|
|
|
} catch (e) {}
|
|
|
}
|
|
|
|
|
|
// 这个变量需要延迟求值,因为 vue 可能在 vue-server-renderer 设置 VUE_ENV 之前被引入
|
|
|
var _isServer;
|
|
|
// 检查是否处于服务器渲染环境
|
|
|
var isServerRendering = function () {
|
|
|
if (_isServer === undefined) {
|
|
|
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
|
|
|
};
|
|
|
|
|
|
// 检测开发者工具是否可用
|
|
|
var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__;
|
|
|
|
|
|
// 判断一个构造函数是否为原生函数
|
|
|
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;
|
|
|
// 如果原生支持 Set,则使用原生 Set
|
|
|
if (typeof Set !== 'undefined' && isNative(Set)) {
|
|
|
_Set = Set;
|
|
|
} else {
|
|
|
// 否则使用自定义的 Set 填充
|
|
|
_Set = (function () {
|
|
|
function Set () {
|
|
|
// 创建一个空对象用于存储键
|
|
|
this.set = Object.create(null);
|
|
|
}
|
|
|
// 检查集合中是否存在某个键
|
|
|
Set.prototype.has = function has (key) {
|
|
|
return this.set[key] === true
|
|
|
};
|
|
|
// 向集合中添加一个键
|
|
|
Set.prototype.add = function add (key) {
|
|
|
this.set[key] = true;
|
|
|
};
|
|
|
// 清空集合
|
|
|
Set.prototype.clear = function clear () {
|
|
|
this.set = Object.create(null);
|
|
|
};
|
|
|
|
|
|
return Set;
|
|
|
}());
|
|
|
}
|
|
|
|
|
|
// 定义警告函数,初始为空函数
|
|
|
var warn = noop;
|
|
|
// 定义提示函数,初始为空函数
|
|
|
var tip = noop;
|
|
|
// 定义生成组件调用栈的函数,初始为空函数
|
|
|
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.error(("[Vue warn]: " + msg + trace));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 定义提示函数
|
|
|
tip = function (msg, vm) {
|
|
|
if (hasConsole && (!config.silent)) {
|
|
|
// 在控制台输出提示信息
|
|
|
console.warn("[Vue tip]: " + msg + (
|
|
|
vm ? generateComponentTrace(vm) : ''
|
|
|
));
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 格式化组件名称
|
|
|
formatComponentName = function (vm, includeFile) {
|
|
|
// 检查当前组件实例是否为根组件实例
|
|
|
if (vm.$root === vm) {
|
|
|
// 如果是根组件实例,返回 '<Root>' 字符串
|
|
|
return '<Root>'
|
|
|
}
|
|
|
// 根据传入的 vm 类型和属性获取组件的选项对象
|
|
|
var options = typeof vm === 'function' && vm.cid != null
|
|
|
// 如果 vm 是函数且有 cid 属性,说明是组件构造函数,取其 options 属性
|
|
|
? vm.options
|
|
|
// 如果 vm 是 Vue 实例,取其 $options 属性,如果没有则取构造函数的 options 属性
|
|
|
: vm._isVue
|
|
|
? vm.$options || vm.constructor.options
|
|
|
// 否则直接将 vm 作为选项对象
|
|
|
: vm;
|
|
|
// 从选项对象中获取组件的名称,如果没有则取 _componentTag 属性
|
|
|
var name = options.name || options._componentTag;
|
|
|
// 从选项对象中获取组件对应的文件路径
|
|
|
var file = options.__file;
|
|
|
// 如果组件名称为空且存在文件路径
|
|
|
if (!name && file) {
|
|
|
// 使用正则表达式匹配文件路径中的文件名部分
|
|
|
var match = file.match(/([^/\\]+)\.vue$/);
|
|
|
// 如果匹配成功,将匹配到的文件名赋值给 name
|
|
|
name = match && match[1];
|
|
|
}
|
|
|
|
|
|
// 生成格式化后的组件名称字符串
|
|
|
return (
|
|
|
// 如果有组件名称,将其转换为驼峰命名并包裹在 < > 中,否则返回 '<Anonymous>'
|
|
|
(name ? ("<" + (classify(name)) + ">") : "<Anonymous>") +
|
|
|
// 如果存在文件路径且 includeFile 不为 false,添加文件路径信息
|
|
|
(file && includeFile !== false ? (" at " + file) : '')
|
|
|
)
|
|
|
};
|
|
|
|
|
|
// 重复字符串指定次数
|
|
|
var repeat = function (str, n) {
|
|
|
// 初始化结果字符串为空
|
|
|
var res = '';
|
|
|
// 开始循环,直到 n 变为 0
|
|
|
while (n) {
|
|
|
// 如果 n 是奇数,将当前字符串 str 追加到结果字符串 res 中
|
|
|
if (n % 2 === 1) { res += str; }
|
|
|
// 如果 n 大于 1,将字符串 str 自身拼接,相当于将字符串长度翻倍
|
|
|
if (n > 1) { str += str; }
|
|
|
// 将 n 右移一位,相当于将 n 除以 2 并向下取整
|
|
|
n >>= 1;
|
|
|
}
|
|
|
// 返回重复后的字符串
|
|
|
return res
|
|
|
};
|
|
|
// 生成组件调用栈信息
|
|
|
// 重新定义 generateComponentTrace 函数,用于生成组件调用链的追踪信息
|
|
|
generateComponentTrace = function (vm) {
|
|
|
// 检查传入的 vm 是否为 Vue 实例,并且是否存在父组件
|
|
|
if (vm._isVue && vm.$parent) {
|
|
|
// 初始化一个数组 tree,用于存储组件调用链中的每个组件实例
|
|
|
var tree = [];
|
|
|
// 初始化一个变量 currentRecursiveSequence,用于记录当前组件的递归调用次数
|
|
|
var currentRecursiveSequence = 0;
|
|
|
// 开始循环遍历组件的父组件链,直到没有父组件为止
|
|
|
while (vm) {
|
|
|
// 检查 tree 数组中是否已经有组件实例
|
|
|
if (tree.length > 0) {
|
|
|
// 获取 tree 数组中的最后一个组件实例
|
|
|
var last = tree[tree.length - 1];
|
|
|
// 检查最后一个组件实例的构造函数是否和当前组件实例的构造函数相同
|
|
|
if (last.constructor === vm.constructor) {
|
|
|
// 如果相同,说明是递归调用,递归次数加 1
|
|
|
currentRecursiveSequence++;
|
|
|
// 将 vm 指向其父组件,继续循环
|
|
|
vm = vm.$parent;
|
|
|
// 跳过本次循环的后续代码,继续下一次循环
|
|
|
continue
|
|
|
}
|
|
|
// 如果递归次数大于 0,说明之前有递归调用
|
|
|
else if (currentRecursiveSequence > 0) {
|
|
|
// 将 tree 数组的最后一个元素替换为一个数组,包含最后一个组件实例和递归次数
|
|
|
tree[tree.length - 1] = [last, currentRecursiveSequence];
|
|
|
// 重置递归次数为 0
|
|
|
currentRecursiveSequence = 0;
|
|
|
}
|
|
|
}
|
|
|
// 将当前组件实例添加到 tree 数组中
|
|
|
tree.push(vm);
|
|
|
// 将 vm 指向其父组件,继续循环
|
|
|
vm = vm.$parent;
|
|
|
}
|
|
|
// 生成组件调用链的追踪信息字符串
|
|
|
return '\n\nfound in\n\n' + tree
|
|
|
// 对 tree 数组中的每个元素进行映射处理
|
|
|
.map(function (vm, i) {
|
|
|
// 根据索引 i 判断是否为第一个元素
|
|
|
return ("" + (i === 0 ? '---> ' : repeat(' ', 5 + i * 2)) +
|
|
|
// 检查当前元素是否为数组
|
|
|
(Array.isArray(vm)
|
|
|
// 如果是数组,说明有递归调用,格式化输出组件名和递归次数
|
|
|
? ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)")
|
|
|
// 如果不是数组,直接格式化输出组件名
|
|
|
: formatComponentName(vm)));
|
|
|
})
|
|
|
// 将映射后的数组元素用换行符连接成一个字符串
|
|
|
.join('\n')
|
|
|
}
|
|
|
// 如果传入的 vm 不是 Vue 实例或者没有父组件
|
|
|
else {
|
|
|
// 直接生成包含组件名的追踪信息字符串
|
|
|
return ("\n\n(found in " + (formatComponentName(vm)) + ")")
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 定义唯一标识符
|
|
|
var uid = 0;
|
|
|
|
|
|
// Dep 类,用于实现依赖收集和发布更新
|
|
|
var Dep = function Dep () {
|
|
|
// 为每个 Dep 实例分配一个唯一的 ID
|
|
|
this.id = uid++;
|
|
|
// 存储订阅者的数组
|
|
|
this.subs = [];
|
|
|
};
|
|
|
|
|
|
// 向订阅者列表中添加一个订阅者
|
|
|
Dep.prototype.addSub = function addSub (sub) {
|
|
|
this.subs.push(sub);
|
|
|
};
|
|
|
|
|
|
// 从订阅者列表中移除一个订阅者
|
|
|
Dep.prototype.removeSub = function removeSub (sub) {
|
|
|
remove(this.subs, sub);
|
|
|
};
|
|
|
|
|
|
// 触发依赖收集
|
|
|
Dep.prototype.depend = function depend () {
|
|
|
if (Dep.target) {
|
|
|
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 节点
|
|
|
// 定义 VNode 构造函数,用于创建虚拟 DOM 节点对象
|
|
|
var VNode = function VNode (
|
|
|
// 节点的标签名,例如 'div'、'span' 等
|
|
|
tag,
|
|
|
// 节点的数据对象,包含节点的属性、事件等信息
|
|
|
data,
|
|
|
// 节点的子节点数组
|
|
|
children,
|
|
|
// 节点的文本内容
|
|
|
text,
|
|
|
// 与虚拟节点对应的真实 DOM 元素
|
|
|
elm,
|
|
|
// 节点所在的上下文,通常是 Vue 实例
|
|
|
context,
|
|
|
// 组件的选项对象,包含组件的配置信息
|
|
|
componentOptions,
|
|
|
// 异步组件的工厂函数
|
|
|
asyncFactory
|
|
|
) {
|
|
|
// 将传入的标签名赋值给实例的 tag 属性
|
|
|
this.tag = tag;
|
|
|
// 将传入的数据对象赋值给实例的 data 属性
|
|
|
this.data = data;
|
|
|
// 将传入的子节点数组赋值给实例的 children 属性
|
|
|
this.children = children;
|
|
|
// 将传入的文本内容赋值给实例的 text 属性
|
|
|
this.text = text;
|
|
|
// 将传入的真实 DOM 元素赋值给实例的 elm 属性
|
|
|
this.elm = elm;
|
|
|
// 命名空间,初始化为 undefined
|
|
|
this.ns = undefined;
|
|
|
// 将传入的上下文赋值给实例的 context 属性
|
|
|
this.context = context;
|
|
|
// 函数式组件的上下文,初始化为 undefined
|
|
|
this.fnContext = undefined;
|
|
|
// 函数式组件的选项,初始化为 undefined
|
|
|
this.fnOptions = undefined;
|
|
|
// 函数式组件的作用域 ID,初始化为 undefined
|
|
|
this.fnScopeId = undefined;
|
|
|
// 如果 data 存在且包含 key 属性,则将其赋值给实例的 key 属性,否则为 undefined
|
|
|
this.key = data && data.key;
|
|
|
// 将传入的组件选项对象赋值给实例的 componentOptions 属性
|
|
|
this.componentOptions = componentOptions;
|
|
|
// 组件实例,初始化为 undefined
|
|
|
this.componentInstance = undefined;
|
|
|
// 父节点,初始化为 undefined
|
|
|
this.parent = undefined;
|
|
|
// 标记节点是否为原始的、未处理的节点,初始化为 false
|
|
|
this.raw = false;
|
|
|
// 标记节点是否为静态节点,初始化为 false
|
|
|
this.isStatic = false;
|
|
|
// 标记节点是否是根插入节点,初始化为 true
|
|
|
this.isRootInsert = true;
|
|
|
// 标记节点是否为注释节点,初始化为 false
|
|
|
this.isComment = false;
|
|
|
// 标记节点是否为克隆节点,初始化为 false
|
|
|
this.isCloned = false;
|
|
|
// 标记节点是否只渲染一次,初始化为 false
|
|
|
this.isOnce = false;
|
|
|
// 将传入的异步组件工厂函数赋值给实例的 asyncFactory 属性
|
|
|
this.asyncFactory = asyncFactory;
|
|
|
// 异步组件的元数据,初始化为 undefined
|
|
|
this.asyncMeta = undefined;
|
|
|
// 标记节点是否为异步占位符节点,初始化为 false
|
|
|
this.isAsyncPlaceholder = false;
|
|
|
};
|
|
|
|
|
|
// 定义 VNode 原型上的访问器属性
|
|
|
var prototypeAccessors = { child: { configurable: true } };
|
|
|
|
|
|
// 废弃:为了向后兼容,将 child 作为 componentInstance 的别名
|
|
|
prototypeAccessors.child.get = function () {
|
|
|
return this.componentInstance
|
|
|
};
|
|
|
|
|
|
// 将访问器属性定义到 VNode 原型上
|
|
|
Object.defineProperties( VNode.prototype, prototypeAccessors );
|
|
|
|
|
|
// 创建一个空的虚拟节点
|
|
|
var createEmptyVNode = function (text) {
|
|
|
if ( text === void 0 ) text = '';
|
|
|
|
|
|
var node = new VNode();
|
|
|
node.text = text;
|
|
|
node.isComment = true;
|
|
|
return node
|
|
|
};
|
|
|
|
|
|
// 创建一个文本虚拟节点
|
|
|
function createTextVNode (val) {
|
|
|
return new VNode(undefined, undefined, undefined, String(val))
|
|
|
}
|
|
|
|
|
|
// 浅克隆一个虚拟节点
|
|
|
// 定义一个函数 cloneVNode,用于克隆一个虚拟节点(VNode)
|
|
|
function cloneVNode (vnode) {
|
|
|
// 创建一个新的 VNode 实例,将原 VNode 的部分属性传递给新实例
|
|
|
var cloned = new VNode(
|
|
|
// 克隆原 VNode 的标签名
|
|
|
vnode.tag,
|
|
|
// 克隆原 VNode 的数据对象
|
|
|
vnode.data,
|
|
|
// 克隆原 VNode 的子节点数组,如果存在子节点,则创建一个新的数组副本
|
|
|
vnode.children && vnode.children.slice(),
|
|
|
// 克隆原 VNode 的文本内容
|
|
|
vnode.text,
|
|
|
// 克隆原 VNode 对应的真实 DOM 元素
|
|
|
vnode.elm,
|
|
|
// 克隆原 VNode 的上下文
|
|
|
vnode.context,
|
|
|
// 克隆原 VNode 的组件选项对象
|
|
|
vnode.componentOptions,
|
|
|
// 克隆原 VNode 的异步组件工厂函数
|
|
|
vnode.asyncFactory
|
|
|
);
|
|
|
// 将原 VNode 的命名空间赋值给克隆节点
|
|
|
cloned.ns = vnode.ns;
|
|
|
// 将原 VNode 是否为静态节点的标记赋值给克隆节点
|
|
|
cloned.isStatic = vnode.isStatic;
|
|
|
// 将原 VNode 的 key 值赋值给克隆节点
|
|
|
cloned.key = vnode.key;
|
|
|
// 将原 VNode 是否为注释节点的标记赋值给克隆节点
|
|
|
cloned.isComment = vnode.isComment;
|
|
|
// 将原 VNode 的函数式组件上下文赋值给克隆节点
|
|
|
cloned.fnContext = vnode.fnContext;
|
|
|
// 将原 VNode 的函数式组件选项赋值给克隆节点
|
|
|
cloned.fnOptions = vnode.fnOptions;
|
|
|
// 将原 VNode 的函数式组件作用域 ID 赋值给克隆节点
|
|
|
cloned.fnScopeId = vnode.fnScopeId;
|
|
|
// 将原 VNode 的异步组件元数据赋值给克隆节点
|
|
|
cloned.asyncMeta = vnode.asyncMeta;
|
|
|
// 标记克隆节点为已克隆状态
|
|
|
cloned.isCloned = true;
|
|
|
// 返回克隆后的 VNode 节点
|
|
|
return cloned
|
|
|
}
|
|
|
|
|
|
// 获取数组的原型
|
|
|
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];
|
|
|
// 定义变异方法
|
|
|
// 调用 def 函数,为 arrayMethods 对象定义一个新的方法,该方法名由 method 变量指定
|
|
|
// 第三个参数是一个名为 mutator 的函数,它会覆盖原数组方法的行为
|
|
|
def(arrayMethods, method, function mutator () {
|
|
|
// 初始化一个空数组 args,用于存储传递给当前函数的参数
|
|
|
var args = [], len = arguments.length;
|
|
|
// 使用 while 循环将传递给函数的参数逆序存储到 args 数组中
|
|
|
while ( len-- ) args[ len ] = arguments[ len ];
|
|
|
|
|
|
// 调用原始的数组方法(original 是之前缓存的数组原型上的原始方法)
|
|
|
// 使用 apply 方法将当前数组实例作为 this 上下文,并传递参数 args
|
|
|
var result = original.apply(this, args);
|
|
|
// 获取当前数组实例的 Observer 实例
|
|
|
// __ob__ 是在对象被观测时添加的一个属性,指向该对象的 Observer 实例
|
|
|
var ob = this.__ob__;
|
|
|
// 定义一个变量 inserted,用于存储新插入到数组中的元素
|
|
|
var inserted;
|
|
|
// 根据不同的数组方法进行不同的处理
|
|
|
switch (method) {
|
|
|
// 如果是 push 或 unshift 方法
|
|
|
case 'push':
|
|
|
case 'unshift':
|
|
|
// 将传递给 push 或 unshift 方法的所有参数赋值给 inserted 变量
|
|
|
// 因为 push 和 unshift 方法会将参数添加到数组中,这些参数就是新插入的元素
|
|
|
inserted = args;
|
|
|
// 跳出 switch 语句
|
|
|
break
|
|
|
// 如果是 splice 方法
|
|
|
case 'splice':
|
|
|
// splice 方法的参数格式为 (start, deleteCount, item1, item2, ...)
|
|
|
// 从索引 2 开始截取参数,这些参数就是 splice 方法插入到数组中的新元素
|
|
|
inserted = args.slice(2);
|
|
|
// 跳出 switch 语句
|
|
|
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 类,用于将对象转换为可观测对象
|
|
|
var booleanIndex = getTypeIndex(Boolean, prop.type);
|
|
|
// 如果属性类型包含布尔类型
|
|
|
if (booleanIndex > -1) {
|
|
|
// 如果属性不存在且没有默认值,则将值设为false
|
|
|
if (absent &&!hasOwn(prop, 'default')) {
|
|
|
value = false;
|
|
|
} else if (value === '' || value === hyphenate(key)) {
|
|
|
// 只有当布尔类型的优先级高于字符串类型时,才将空字符串或同名的值转换为布尔类型
|
|
|
var Observer = function Observer (value) {
|
|
|
this.value = value;
|
|
|
this.dep = new Dep();
|
|
|
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
|
|
|
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) {
|
|
|
target.__proto__ = src;
|
|
|
}
|
|
|
|
|
|
// 通过定义隐藏属性增强目标对象或数组
|
|
|
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) {
|
|
|
// 检查传入的值是否不是对象,或者是否为 VNode 实例
|
|
|
// 如果满足条件,则直接返回,不进行观察
|
|
|
if (!isObject(value) || value instanceof VNode) {
|
|
|
return
|
|
|
}
|
|
|
// 声明一个变量 ob,用于存储观察者实例
|
|
|
var ob;
|
|
|
// 检查传入的值是否已经有 __ob__ 属性,并且该属性对应的是 Observer 实例
|
|
|
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
|
|
|
// 如果已经有观察者实例,则直接将其赋值给 ob 变量
|
|
|
ob = value.__ob__;
|
|
|
}
|
|
|
// 如果值还没有被观察,并且满足以下条件
|
|
|
else if (
|
|
|
// 全局变量 shouldObserve 为 true,表示应该进行观察
|
|
|
shouldObserve &&
|
|
|
// 当前不是服务器渲染环境
|
|
|
!isServerRendering() &&
|
|
|
// 传入的值是数组或者是普通对象
|
|
|
(Array.isArray(value) || isPlainObject(value)) &&
|
|
|
// 传入的值是可扩展的,即可以添加新的属性
|
|
|
Object.isExtensible(value) &&
|
|
|
// 传入的值不是 Vue 实例
|
|
|
!value._isVue
|
|
|
) {
|
|
|
// 创建一个新的 Observer 实例,并将其赋值给 ob 变量
|
|
|
ob = new Observer(value);
|
|
|
}
|
|
|
// 如果 asRootData 为 true,并且已经成功创建了观察者实例
|
|
|
if (asRootData && ob) {
|
|
|
// 增加观察者实例的 vmCount 属性,该属性用于记录该对象作为根数据的次数
|
|
|
ob.vmCount++;
|
|
|
}
|
|
|
// 返回观察者实例,如果没有创建则返回 undefined
|
|
|
return ob
|
|
|
}
|
|
|
// Define a reactive property on an Object.
|
|
|
// 在一个对象上定义一个响应式属性
|
|
|
function defineReactive$$1 (
|
|
|
// 要定义响应式属性的对象
|
|
|
obj,
|
|
|
// 属性名
|
|
|
key,
|
|
|
// 属性的初始值
|
|
|
val,
|
|
|
// 自定义的 setter 函数
|
|
|
customSetter,
|
|
|
// 是否进行浅观察
|
|
|
shallow
|
|
|
) {
|
|
|
// 创建一个 Dep 实例,用于依赖收集和派发更新
|
|
|
var dep = new Dep();
|
|
|
|
|
|
// 获取对象上该属性的描述符
|
|
|
var property = Object.getOwnPropertyDescriptor(obj, key);
|
|
|
// 如果属性存在且不可配置,则直接返回
|
|
|
if (property && property.configurable === false) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// cater for pre-defined getter/setters
|
|
|
// 处理预定义的 getter/setter
|
|
|
// 获取属性的 getter 函数
|
|
|
var getter = property && property.get;
|
|
|
// 获取属性的 setter 函数
|
|
|
var setter = property && property.set;
|
|
|
// 如果没有 getter 或者有 setter,并且只传入了两个参数,则获取对象上该属性的值
|
|
|
if ((!getter || setter) && arguments.length === 2) {
|
|
|
val = obj[key];
|
|
|
}
|
|
|
|
|
|
// 如果不是浅观察,则尝试为属性值创建观察者实例
|
|
|
var childOb = !shallow && observe(val);
|
|
|
// 使用 Object.defineProperty 定义属性的 getter 和 setter
|
|
|
Object.defineProperty(obj, key, {
|
|
|
// 属性可枚举
|
|
|
enumerable: true,
|
|
|
// 属性可配置
|
|
|
configurable: true,
|
|
|
// 定义 getter 函数
|
|
|
get: function reactiveGetter () {
|
|
|
// 如果有预定义的 getter,则调用该 getter 获取值,否则使用传入的初始值
|
|
|
var value = getter ? getter.call(obj) : val;
|
|
|
// 如果存在 Dep.target(即有正在计算的依赖)
|
|
|
if (Dep.target) {
|
|
|
// 收集依赖
|
|
|
dep.depend();
|
|
|
// 如果存在子观察者
|
|
|
if (childOb) {
|
|
|
// 子观察者收集依赖
|
|
|
childOb.dep.depend();
|
|
|
// 如果值是数组
|
|
|
if (Array.isArray(value)) {
|
|
|
// 对数组中的每个元素收集依赖
|
|
|
dependArray(value);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 返回属性值
|
|
|
return value
|
|
|
},
|
|
|
// 定义 setter 函数
|
|
|
set: function reactiveSetter (newVal) {
|
|
|
// 如果有预定义的 getter,则调用该 getter 获取旧值,否则使用传入的初始值
|
|
|
var value = getter ? getter.call(obj) : val;
|
|
|
// 检查新值和旧值是否相等,如果相等则直接返回
|
|
|
/* eslint-disable no-self-compare */
|
|
|
if (newVal === value || (newVal !== newVal && value !== value)) {
|
|
|
return
|
|
|
}
|
|
|
/* eslint-enable no-self-compare */
|
|
|
// 如果有自定义的 setter 函数,则调用该函数
|
|
|
if (customSetter) {
|
|
|
customSetter();
|
|
|
}
|
|
|
// 如果有 getter 但没有 setter,则直接返回
|
|
|
// #7981: for accessor properties without setter
|
|
|
if (getter && !setter) { return }
|
|
|
// 如果有预定义的 setter,则调用该 setter 设置新值
|
|
|
if (setter) {
|
|
|
setter.call(obj, newVal);
|
|
|
} else {
|
|
|
// 否则直接更新值
|
|
|
val = newVal;
|
|
|
}
|
|
|
// 如果不是浅观察,则尝试为新值创建观察者实例
|
|
|
childOb = !shallow && observe(newVal);
|
|
|
// 通知所有依赖更新
|
|
|
dep.notify();
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// Set a property on an object. Adds the new property and
|
|
|
// triggers change notification if the property doesn't
|
|
|
// already exist.
|
|
|
// 在对象上设置一个属性。如果属性不存在,则添加该属性并触发变更通知
|
|
|
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);
|
|
|
// 使用 splice 方法替换数组中的元素
|
|
|
target.splice(key, 1, val);
|
|
|
// 返回设置的值
|
|
|
return val
|
|
|
}
|
|
|
// 如果键已经存在于目标对象中且不是原型链上的属性
|
|
|
if (key in target && !(key in Object.prototype)) {
|
|
|
// 直接设置属性值
|
|
|
target[key] = val;
|
|
|
// 返回设置的值
|
|
|
return val
|
|
|
}
|
|
|
// 获取目标对象的观察者实例
|
|
|
var ob = (target).__ob__;
|
|
|
// 如果目标对象是 Vue 实例或者是根数据对象,则发出警告
|
|
|
if (target._isVue || (ob && ob.vmCount)) {
|
|
|
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
|
|
|
}
|
|
|
|
|
|
// Delete a property and trigger change if necessary.
|
|
|
// 删除一个属性,并在必要时触发变更通知
|
|
|
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)) {
|
|
|
// 使用 splice 方法删除数组中的元素
|
|
|
target.splice(key, 1);
|
|
|
return
|
|
|
}
|
|
|
// 获取目标对象的观察者实例
|
|
|
var ob = (target).__ob__;
|
|
|
// 如果目标对象是 Vue 实例或者是根数据对象,则发出警告
|
|
|
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
|
|
|
}
|
|
|
// 通知依赖更新
|
|
|
ob.dep.notify();
|
|
|
}
|
|
|
|
|
|
// Collect dependencies on array elements when the array is touched, since
|
|
|
// we cannot intercept array element access like property getters.
|
|
|
// 当数组被访问时,收集数组元素的依赖,因为我们无法像拦截属性 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(e);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Option overwriting strategies are functions that handle
|
|
|
// how to merge a parent option value and a child option
|
|
|
// value into the final value.
|
|
|
// 选项覆盖策略是处理如何将父选项值和子选项值合并为最终值的函数
|
|
|
var strats = config.optionMergeStrategies;
|
|
|
|
|
|
// Options with restrictions
|
|
|
// 有约束的选项
|
|
|
{
|
|
|
// 定义 el 和 propsData 选项的合并策略
|
|
|
strats.el = strats.propsData = function (
|
|
|
// 父选项值
|
|
|
parent,
|
|
|
// 子选项值
|
|
|
child,
|
|
|
// Vue 实例
|
|
|
vm,
|
|
|
// 选项名
|
|
|
key
|
|
|
) {
|
|
|
// 如果没有 Vue 实例,则发出警告
|
|
|
if (!vm) {
|
|
|
warn(
|
|
|
"option \"" + key + "\" can only be used during instance " +
|
|
|
'creation with the `new` keyword.'
|
|
|
);
|
|
|
}
|
|
|
// 使用默认的合并策略
|
|
|
return defaultStrat(parent, child)
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// Helper that recursively merges two data objects together.
|
|
|
// 递归合并两个数据对象的辅助函数
|
|
|
function mergeData (
|
|
|
// 目标对象
|
|
|
to,
|
|
|
// 源对象
|
|
|
from
|
|
|
) {
|
|
|
// 如果源对象不存在,则返回目标对象
|
|
|
if (!from) { return to }
|
|
|
// 定义变量,用于存储键、目标对象的值和源对象的值
|
|
|
var key, toVal, fromVal;
|
|
|
|
|
|
// 获取源对象的所有键
|
|
|
var keys = hasSymbol
|
|
|
? Reflect.ownKeys(from)
|
|
|
: Object.keys(from);
|
|
|
|
|
|
// 遍历源对象的所有键
|
|
|
for (var i = 0; i < keys.length; i++) {
|
|
|
// 获取当前键
|
|
|
key = keys[i];
|
|
|
// 如果键是 __ob__,则跳过
|
|
|
// in case the object is already observed...
|
|
|
if (key === '__ob__') { continue }
|
|
|
// 获取目标对象上该键的值
|
|
|
toVal = to[key];
|
|
|
// 获取源对象上该键的值
|
|
|
fromVal = from[key];
|
|
|
// 如果目标对象没有该键
|
|
|
if (!hasOwn(to, key)) {
|
|
|
// 在目标对象上设置该键的值
|
|
|
set(to, key, fromVal);
|
|
|
} else if (
|
|
|
// 如果目标对象和源对象上该键的值不相等
|
|
|
toVal !== fromVal &&
|
|
|
// 目标对象上该键的值是纯对象
|
|
|
isPlainObject(toVal) &&
|
|
|
// 源对象上该键的值是纯对象
|
|
|
isPlainObject(fromVal)
|
|
|
) {
|
|
|
// 递归合并两个对象
|
|
|
mergeData(toVal, fromVal);
|
|
|
}
|
|
|
}
|
|
|
// 返回合并后的目标对象
|
|
|
return to
|
|
|
}
|
|
|
|
|
|
// Data
|
|
|
// 数据合并函数
|
|
|
function mergeDataOrFn (
|
|
|
// 父数据值
|
|
|
parentVal,
|
|
|
// 子数据值
|
|
|
childVal,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
// 如果没有 Vue 实例
|
|
|
if (!vm) {
|
|
|
// 在 Vue.extend 合并时,两者都应该是函数
|
|
|
// 如果子数据值不存在,则返回父数据值
|
|
|
if (!childVal) {
|
|
|
return parentVal
|
|
|
}
|
|
|
// 如果父数据值不存在,则返回子数据值
|
|
|
if (!parentVal) {
|
|
|
return childVal
|
|
|
}
|
|
|
// 当父数据值和子数据值都存在时,我们需要返回一个函数,该函数返回两个函数合并后的结果
|
|
|
// 这里不需要检查父数据值是否是函数,因为它必须是函数才能通过之前的合并
|
|
|
return function mergedDataFn () {
|
|
|
return mergeData(
|
|
|
// 如果子数据值是函数,则调用该函数并传入 this 上下文,否则直接使用子数据值
|
|
|
typeof childVal === 'function' ? childVal.call(this, this) : childVal,
|
|
|
// 如果父数据值是函数,则调用该函数并传入 this 上下文,否则直接使用父数据值
|
|
|
typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal
|
|
|
)
|
|
|
}
|
|
|
} else {
|
|
|
return function mergedInstanceDataFn () {
|
|
|
// 实例合并
|
|
|
// 如果子数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用子数据值
|
|
|
var instanceData = typeof childVal === 'function'
|
|
|
? childVal.call(vm, vm)
|
|
|
: childVal;
|
|
|
// 如果父数据值是函数,则调用该函数并传入 Vue 实例,否则直接使用父数据值
|
|
|
var defaultData = typeof parentVal === 'function'
|
|
|
? parentVal.call(vm, vm)
|
|
|
: parentVal;
|
|
|
// 如果实例数据存在
|
|
|
if (instanceData) {
|
|
|
// 合并实例数据和默认数据
|
|
|
return mergeData(instanceData, defaultData)
|
|
|
} else {
|
|
|
// 否则返回默认数据
|
|
|
return defaultData
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 定义 data 选项的合并策略
|
|
|
strats.data = function (
|
|
|
// 父选项值
|
|
|
parentVal,
|
|
|
// 子选项值
|
|
|
childVal,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
// 如果没有 Vue 实例
|
|
|
if (!vm) {
|
|
|
// 如果子选项值存在且不是函数,则发出警告
|
|
|
if (childVal && typeof childVal !== 'function') {
|
|
|
warn(
|
|
|
'The "data" option should be a function ' +
|
|
|
'that returns a per-instance value in component ' +
|
|
|
'definitions.',
|
|
|
vm
|
|
|
);
|
|
|
|
|
|
return parentVal
|
|
|
}
|
|
|
// 使用 mergeDataOrFn 函数进行合并
|
|
|
return mergeDataOrFn(parentVal, childVal)
|
|
|
}
|
|
|
|
|
|
// 使用 mergeDataOrFn 函数进行合并,并传入 Vue 实例
|
|
|
return mergeDataOrFn(parentVal, childVal, vm)
|
|
|
};
|
|
|
|
|
|
// Hooks and props are merged as arrays.
|
|
|
// 钩子和 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;
|
|
|
});
|
|
|
|
|
|
// Assets
|
|
|
// 当存在 Vue 实例(实例创建时),我们需要对构造函数选项、实例选项和父选项进行三方合并
|
|
|
// 资产(组件、指令、过滤器)的合并函数
|
|
|
function mergeAssets (
|
|
|
// 父选项值
|
|
|
parentVal,
|
|
|
// 子选项值
|
|
|
childVal,
|
|
|
// Vue 实例
|
|
|
vm,
|
|
|
// 选项名
|
|
|
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;
|
|
|
});
|
|
|
|
|
|
// Watchers.
|
|
|
// 监听器的合并策略
|
|
|
// Watchers hashes should not overwrite one
|
|
|
// another, so we merge them as arrays.
|
|
|
// 监听器哈希不应该相互覆盖,所以我们将它们合并为数组
|
|
|
strats.watch = function (
|
|
|
// 父选项值
|
|
|
parentVal,
|
|
|
// 子选项值
|
|
|
childVal,
|
|
|
// Vue 实例
|
|
|
vm,
|
|
|
// 选项名
|
|
|
key
|
|
|
) {
|
|
|
// work around Firefox's Object.prototype.watch...
|
|
|
// 处理 Firefox 的 Object.prototype.watch
|
|
|
if (parentVal === nativeWatch) { parentVal = undefined; }
|
|
|
if (childVal === nativeWatch) { childVal = undefined; }
|
|
|
/* istanbul ignore if */
|
|
|
// 如果子选项值不存在,则返回一个继承自父选项值的对象
|
|
|
if (!childVal) { return Object.create(parentVal || null) }
|
|
|
{
|
|
|
// 检查子选项值是否为对象类型
|
|
|
assertObjectType(key, childVal, vm);
|
|
|
}
|
|
|
// 如果父选项值不存在,则返回子选项值
|
|
|
if (!parentVal) { return childVal }
|
|
|
// 初始化合并结果
|
|
|
var ret = {};
|
|
|
// 将父选项值合并到结果对象中
|
|
|
extend(ret, parentVal);
|
|
|
// 遍历子选项值的所有键
|
|
|
for (var key$1 in childVal) {
|
|
|
// 获取父选项值中该键的值
|
|
|
var parent = ret[key$1];
|
|
|
// 获取子选项值中该键的值
|
|
|
var child = childVal[key$1];
|
|
|
// 如果父选项值中该键的值存在且不是数组,则将其包装成数组
|
|
|
if (parent && !Array.isArray(parent)) {
|
|
|
parent = [parent];
|
|
|
}
|
|
|
// 将子选项值中该键的值合并到父选项值中
|
|
|
ret[key$1] = parent
|
|
|
? parent.concat(child)
|
|
|
: Array.isArray(child) ? child : [child];
|
|
|
}
|
|
|
// 返回合并结果
|
|
|
return ret
|
|
|
};
|
|
|
|
|
|
// Other object hashes.
|
|
|
// 其他对象哈希的合并策略
|
|
|
strats.props =
|
|
|
strats.methods =
|
|
|
strats.inject =
|
|
|
strats.computed = function (
|
|
|
// 父选项值
|
|
|
parentVal,
|
|
|
// 子选项值
|
|
|
childVal,
|
|
|
// Vue 实例
|
|
|
vm,
|
|
|
// 选项名
|
|
|
key
|
|
|
) {
|
|
|
// 如果子选项值存在且不是生产环境,则检查子选项值是否为对象类型
|
|
|
if (childVal && "development" !== 'production') {
|
|
|
assertObjectType(key, childVal, vm);
|
|
|
}
|
|
|
// 如果父选项值不存在,则返回子选项值
|
|
|
if (!parentVal) { return childVal }
|
|
|
// 初始化合并结果
|
|
|
var ret = Object.create(null);
|
|
|
// 将父选项值合并到结果对象中
|
|
|
extend(ret, parentVal);
|
|
|
// 如果子选项值存在,则将其合并到结果对象中
|
|
|
if (childVal) { extend(ret, childVal); }
|
|
|
// 返回合并结果
|
|
|
return ret
|
|
|
};
|
|
|
// provide 选项的合并策略使用 mergeDataOrFn 函数
|
|
|
strats.provide = mergeDataOrFn;
|
|
|
|
|
|
// Default strategy.
|
|
|
// 默认合并策略
|
|
|
var defaultStrat = function (
|
|
|
// 父选项值
|
|
|
parentVal,
|
|
|
// 子选项值
|
|
|
childVal
|
|
|
) {
|
|
|
// 如果子选项值为 undefined,则返回父选项值,否则返回子选项值
|
|
|
return childVal === undefined
|
|
|
? parentVal
|
|
|
: childVal
|
|
|
};
|
|
|
|
|
|
// Validate component names
|
|
|
// 验证组件名称
|
|
|
function checkComponents (
|
|
|
// 选项对象
|
|
|
options
|
|
|
) {
|
|
|
// 遍历选项对象中的 components 属性
|
|
|
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
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Ensure all props option syntax are normalized into the
|
|
|
// Object-based format.
|
|
|
// 确保所有 props 选项的语法都被规范化为基于对象的格式
|
|
|
function normalizeProps (
|
|
|
// 选项对象
|
|
|
options,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
// 获取选项对象中的 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);
|
|
|
// 在规范化后的 props 对象中添加该属性,并设置类型为 null
|
|
|
res[name] = { type: null };
|
|
|
} else {
|
|
|
// 如果当前元素不是字符串,则发出警告
|
|
|
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 对象赋值给选项对象的 props 属性
|
|
|
options.props = res;
|
|
|
}
|
|
|
|
|
|
// Normalize all injections into Object-based format
|
|
|
// 将所有注入项规范化为基于对象的格式
|
|
|
function normalizeInject (
|
|
|
// 选项对象
|
|
|
options,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
// 获取选项对象中的 inject 属性
|
|
|
var inject = options.inject;
|
|
|
// 如果 inject 属性不存在,则直接返回
|
|
|
if (!inject) { return }
|
|
|
// 初始化规范化后的 inject 对象
|
|
|
var normalized = options.inject = {};
|
|
|
// 如果 inject 属性是数组
|
|
|
if (Array.isArray(inject)) {
|
|
|
// 遍历数组
|
|
|
for (var i = 0; i < inject.length; i++) {
|
|
|
// 在规范化后的 inject 对象中添加该属性,并设置 from 属性为当前元素
|
|
|
normalized[inject[i]] = { from: inject[i] };
|
|
|
}
|
|
|
} else if (isPlainObject(inject)) {
|
|
|
// 如果 inject 属性是纯对象
|
|
|
// 遍历对象的所有键
|
|
|
for (var key in inject) {
|
|
|
// 获取当前键对应的值
|
|
|
var val = inject[key];
|
|
|
// 如果值是纯对象,则将 from 属性合并到该值中,否则将其包装成一个包含 from 属性的对象
|
|
|
normalized[key] = isPlainObject(val)
|
|
|
? extend({ from: key }, val)
|
|
|
: { from: val };
|
|
|
}
|
|
|
} else {
|
|
|
// 如果 inject 属性既不是数组也不是纯对象,则发出警告
|
|
|
warn(
|
|
|
"Invalid value for option \"inject\": expected an Array or an Object, " +
|
|
|
"but got " + (toRawType(inject)) + ".",
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Normalize raw function directives into object format.
|
|
|
// 将原始的函数指令规范化为对象格式
|
|
|
function normalizeDirectives (
|
|
|
// 选项对象
|
|
|
options
|
|
|
) {
|
|
|
// 获取选项对象中的 directives 属性
|
|
|
var dirs = options.directives;
|
|
|
// 如果 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,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
// 如果值不是纯对象,则发出警告
|
|
|
if (!isPlainObject(value)) {
|
|
|
warn(
|
|
|
"Invalid value for option \"" + name + "\": expected an Object, " +
|
|
|
"but got " + (toRawType(value)) + ".",
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Merge two option objects into a new one.
|
|
|
// Core utility used in both instantiation and inheritance.
|
|
|
// 将两个选项对象合并为一个新的对象
|
|
|
// 实例化和继承中使用的核心工具
|
|
|
function mergeOptions (
|
|
|
// 父选项对象
|
|
|
parent,
|
|
|
// 子选项对象
|
|
|
child,
|
|
|
// Vue 实例
|
|
|
vm
|
|
|
) {
|
|
|
{
|
|
|
// 检查子选项对象中的组件名称是否合法
|
|
|
checkComponents(child);
|
|
|
}
|
|
|
|
|
|
// 如果子选项对象是一个函数,则获取其 options 属性
|
|
|
if (typeof child === 'function') {
|
|
|
child = child.options;
|
|
|
}
|
|
|
|
|
|
// 规范化子选项对象中的 props 属性
|
|
|
normalizeProps(child, vm);
|
|
|
// 规范化子选项对象中的 inject 属性
|
|
|
normalizeInject(child, vm);
|
|
|
// 规范化子选项对象中的 directives 属性
|
|
|
normalizeDirectives(child);
|
|
|
|
|
|
// Apply extends and mixins on the child options,
|
|
|
// but only if it is a raw options object that isn't
|
|
|
// the result of another mergeOptions call.
|
|
|
// Only merged options has the _base property.
|
|
|
// 如果子选项对象不是另一次 mergeOptions 调用的结果(即没有 _base 属性)
|
|
|
if (!child._base) {
|
|
|
// 如果子选项对象有 extends 属性
|
|
|
if (child.extends) {
|
|
|
// 递归合并父选项对象和子选项对象的 extends 属性
|
|
|
parent = mergeOptions(parent, child.extends, vm);
|
|
|
}
|
|
|
// 如果子选项对象有 mixins 属性
|
|
|
if (child.mixins) {
|
|
|
// 遍历 mixins 数组
|
|
|
for (var i = 0, l = child.mixins.length; i < l; i++) {
|
|
|
// 递归合并父选项对象和 mixins 数组中的每个选项对象
|
|
|
parent = mergeOptions(parent, child.mixins[i], vm);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 定义一个空对象options
|
|
|
var options = {};
|
|
|
// 定义变量key
|
|
|
var key;
|
|
|
// 遍历parent对象的属性
|
|
|
for (key in parent) {
|
|
|
// 调用mergeField函数处理属性
|
|
|
mergeField(key);
|
|
|
}
|
|
|
// 遍历child对象的属性
|
|
|
for (key in child) {
|
|
|
// 如果parent对象不包含当前属性
|
|
|
if (!hasOwn(parent, key)) {
|
|
|
// 调用mergeField函数处理属性
|
|
|
mergeField(key);
|
|
|
}
|
|
|
}
|
|
|
// 定义mergeField函数,用于合并属性
|
|
|
function mergeField (key) {
|
|
|
// 获取属性的合并策略,如果没有则使用默认策略
|
|
|
var strat = strats[key] || defaultStrat;
|
|
|
// 根据合并策略合并parent和child中对应属性的值,并将结果存储在options中
|
|
|
options[key] = strat(parent[key], child[key], vm, key);
|
|
|
}
|
|
|
// 返回合并后的options对象
|
|
|
return options
|
|
|
}
|
|
|
//解析一个资源。
|
|
|
// 这个函数被使用是因为子实例需要访问在其祖先链中定义的资源。
|
|
|
// 定义resolveAsset函数,用于解析资源
|
|
|
function resolveAsset (
|
|
|
options,
|
|
|
type,
|
|
|
id,
|
|
|
warnMissing
|
|
|
) {
|
|
|
// 如果id不是字符串类型,则返回
|
|
|
/* istanbul ignore if */
|
|
|
if (typeof id!== 'string') {
|
|
|
return
|
|
|
}
|
|
|
// 获取指定类型的资源
|
|
|
var assets = options[type];
|
|
|
// 首先检查本地注册的变体
|
|
|
if (hasOwn(assets, id)) { return assets[id] }
|
|
|
// 将id转换为驼峰命名
|
|
|
var camelizedId = camelize(id);
|
|
|
// 如果驼峰命名的id存在于资源中,则返回对应的值
|
|
|
if (hasOwn(assets, camelizedId)) { return assets[camelizedId] }
|
|
|
// 将驼峰命名的id转换为首字母大写的形式
|
|
|
var PascalCaseId = capitalize(camelizedId);
|
|
|
// 如果首字母大写的id存在于资源中,则返回对应的值
|
|
|
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
|
|
|
}
|
|
|
|
|
|
/* */
|
|
|
|
|
|
// 定义validateProp函数,用于验证属性
|
|
|
function validateProp (
|
|
|
key,
|
|
|
propOptions,
|
|
|
propsData,
|
|
|
vm
|
|
|
) {
|
|
|
// 获取属性的选项
|
|
|
var prop = propOptions[key];
|
|
|
// 判断属性是否不存在于propsData中
|
|
|
var absent =!hasOwn(propsData, key);
|
|
|
// 获取属性的值
|
|
|
var value = propsData[key];
|
|
|
// 布尔类型转换
|
|
|
var stringIndex = getTypeIndex(String, prop.type);
|
|
|
if (stringIndex < 0 || booleanIndex < stringIndex) {
|
|
|
value = true;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 检查默认值
|
|
|
if (value === undefined) {
|
|
|
// 获取属性的默认值
|
|
|
value = getPropDefaultValue(vm, prop, key);
|
|
|
// 因为默认值是一个新的副本,所以确保对其进行观察
|
|
|
var prevShouldObserve = shouldObserve;
|
|
|
toggleObserving(true);
|
|
|
observe(value);
|
|
|
toggleObserving(prevShouldObserve);
|
|
|
}
|
|
|
{
|
|
|
// 断言属性是否有效
|
|
|
assertProp(prop, key, value, vm, absent);
|
|
|
}
|
|
|
// 返回验证后的属性值
|
|
|
return value
|
|
|
}
|
|
|
|
|
|
// 获取属性的默认值。
|
|
|
// 定义getPropDefaultValue函数,用于获取属性的默认值
|
|
|
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
|
|
|
);
|
|
|
}
|
|
|
// 如果vm存在,且propsData中属性值为undefined,且vm._props中属性值不为undefined,则返回vm._props中的值
|
|
|
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
|
|
|
}
|
|
|
|
|
|
// 断言一个属性是否有效。
|
|
|
// 定义assertProp函数,用于断言属性是否有效
|
|
|
function assertProp (
|
|
|
prop,
|
|
|
name,
|
|
|
value,
|
|
|
vm,
|
|
|
absent
|
|
|
) {
|
|
|
// 如果属性是必需的且不存在,则发出警告
|
|
|
if (prop.required && absent) {
|
|
|
warn(
|
|
|
'Missing required prop: "' + name + '"',
|
|
|
vm
|
|
|
);
|
|
|
return
|
|
|
}
|
|
|
// 如果属性值为null且属性不是必需的,则返回
|
|
|
if (value == null &&!prop.required) {
|
|
|
return
|
|
|
}
|
|
|
// 获取属性的类型
|
|
|
var type = prop.type;
|
|
|
// 初始化有效性为true,如果没有指定类型或者类型为true
|
|
|
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)$/;
|
|
|
|
|
|
// 定义assertType函数,用于检查属性值的类型是否符合预期
|
|
|
function assertType (value, type) {
|
|
|
// 初始化有效性为false
|
|
|
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中运行时会失败。
|
|
|
// 定义getType函数,用于获取函数的类型名称
|
|
|
function getType (fn) {
|
|
|
// 匹配函数的字符串表示中的函数名
|
|
|
var match = fn && fn.toString().match(/^\s*function (\w+)/);
|
|
|
// 返回匹配到的函数名,否则返回空字符串
|
|
|
return match? match[1] : ''
|
|
|
}
|
|
|
|
|
|
// 定义isSameType函数,用于判断两个类型是否相同
|
|
|
function isSameType (a, b) {
|
|
|
// 通过比较两个类型的名称是否相同来判断
|
|
|
return getType(a) === getType(b)
|
|
|
}
|
|
|
|
|
|
// 定义getTypeIndex函数,用于获取类型在预期类型数组中的索引
|
|
|
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
|
|
|
}
|
|
|
}
|
|
|
// 如果没有找到,则返回-1
|
|
|
return -1
|
|
|
}
|
|
|
|
|
|
// 定义getInvalidTypeMessage函数,用于获取无效类型的错误消息
|
|
|
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
|
|
|
}
|
|
|
|
|
|
// 定义styleValue函数,用于格式化值的显示形式
|
|
|
function styleValue (value, type) {
|
|
|
// 如果类型是字符串,则用双引号包裹值
|
|
|
if (type === 'String') {
|
|
|
return ("\"" + value + "\"")
|
|
|
} else if (type === 'Number') {
|
|
|
// 如果类型是数字,则将值转换为字符串
|
|
|
return ("" + (Number(value)))
|
|
|
} else {
|
|
|
// 否则直接将值转换为字符串
|
|
|
return ("" + value)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 定义isExplicable函数,用于判断类型是否是可解释的类型(字符串、数字、布尔)
|
|
|
function isExplicable (value) {
|
|
|
// 定义可解释的类型数组
|
|
|
var explicitTypes = ['string', 'number', 'boolean'];
|
|
|
// 检查值的类型是否在可解释的类型数组中
|
|
|
return explicitTypes.some(function (elem) { return value.toLowerCase() === elem; })
|
|
|
}
|
|
|
|
|
|
// 定义isBoolean函数,用于判断参数中是否包含布尔类型
|
|
|
function isBoolean () {
|
|
|
// 将参数转换为数组
|
|
|
var args = [], len = arguments.length;
|
|
|
while ( len-- ) args[ len ] = arguments[ len ];
|
|
|
|
|
|
// 检查数组中是否有元素的类型为布尔类型
|
|
|
return args.some(function (elem) { return elem.toLowerCase() === 'boolean'; })
|
|
|
}
|
|
|
|
|
|
|
|
|
// 定义handleError函数,用于处理错误
|
|
|
function handleError (err, vm, info) {
|
|
|
// 在处理错误处理程序时停用依赖跟踪,以避免可能的无限渲染。
|
|
|
// 参考:https://github.com/vuejs/vuex/issues/1505
|
|
|
pushTarget();
|
|
|
try {
|
|
|
// 如果有vm实例
|
|
|
if (vm) {
|
|
|
var cur = vm;
|
|
|
// 向上遍历vm的父级实例
|
|
|
while ((cur = cur.$parent)) {
|
|
|
// 获取父级实例的errorCaptured钩子函数数组
|
|
|
var hooks = cur.$options.errorCaptured;
|
|
|
// 如果有errorCaptured钩子函数
|
|
|
if (hooks) {
|
|
|
// 遍历钩子函数数组
|
|
|
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();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 定义invokeWithErrorHandling函数,用于在错误处理下调用处理程序
|
|
|
function invokeWithErrorHandling (
|
|
|
handler,
|
|
|
context,
|
|
|
args,
|
|
|
vm,
|
|
|
info
|
|
|
) {
|
|
|
// 定义变量res用于存储处理程序的返回值
|
|
|
var res;
|
|
|
try {
|
|
|
// 调用处理程序
|
|
|
res = args? handler.apply(context, args) : handler.call(context);
|
|
|
// 如果返回值是Promise且未被处理,则添加错误处理
|
|
|
if (res &&!res._isVue && isPromise(res) &&!res._handled) {
|
|
|
res.catch(function (e) { return handleError(e, vm, info + " (Promise/async)"); });
|
|
|
// 标记Promise已被处理,避免多次捕获
|
|
|
res._handled = true;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
// 如果处理程序调用时出错,则调用错误处理函数
|
|
|
handleError(e, vm, info);
|
|
|
}
|
|
|
// 返回处理程序的返回值
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 定义globalHandleError函数,用于全局处理错误
|
|
|
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函数记录错误
|
|
|
logError(err, vm, info);
|
|
|
}
|
|
|
|
|
|
// 定义logError函数,用于记录错误
|
|
|
function logError (err, vm, info) {
|
|
|
{
|
|
|
// 发出警告
|
|
|
warn(("Error in " + info + ": \"" + (err.toString()) + "\""), vm);
|
|
|
}
|
|
|
/* istanbul ignore else */
|
|
|
// 如果在浏览器或Weex环境中且console可用,则在控制台输出错误
|
|
|
if ((inBrowser || inWeex) && typeof console!== 'undefined') {
|
|
|
console.error(err);
|
|
|
} else {
|
|
|
// 否则抛出错误
|
|
|
throw err
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
// 定义变量isUsingMicroTask表示是否使用微任务,初始值为false
|
|
|
var isUsingMicroTask = false;
|
|
|
|
|
|
// 定义callbacks数组用于存储回调函数
|
|
|
var callbacks = [];
|
|
|
// 定义变量pending表示是否有挂起的回调,初始值为false
|
|
|
var pending = false;
|
|
|
|
|
|
// 定义flushCallbacks函数,用于刷新回调函数队列
|
|
|
function flushCallbacks () {
|
|
|
// 将pending设为false
|
|
|
pending = false;
|
|
|
// 获取callbacks数组的副本
|
|
|
var copies = callbacks.slice(0);
|
|
|
// 清空callbacks数组
|
|
|
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 可用,我们将使用它
|
|
|
/* istanbul ignore next, $flow-disable-line */
|
|
|
if (typeof Promise!== 'undefined' && isNative(Promise)) {
|
|
|
// 创建一个已解决的 Promise 实例
|
|
|
var p = Promise.resolve();
|
|
|
// 定义 timerFunc 函数,用于在 Promise 的 then 方法中调用 flushCallbacks 函数
|
|
|
timerFunc = function () {
|
|
|
p.then(flushCallbacks);
|
|
|
// 在有问题的 UIWebView 中,Promise.then 不会完全失效,但
|
|
|
// 它可能会陷入一种奇怪的状态,即回调被推入微任务队列,但队列没有被刷新,直到浏览器需要执行其他工作,例如处理定时器
|
|
|
// 因此,我们可以通过添加一个空定时器来“强制”刷新微任务队列
|
|
|
if (isIOS) { setTimeout(noop); }
|
|
|
};
|
|
|
// 标记正在使用微任务
|
|
|
isUsingMicroTask = true;
|
|
|
// 如果不是 IE 浏览器,且支持 MutationObserver
|
|
|
} else if (!isIE && typeof MutationObserver!== 'undefined' && (
|
|
|
isNative(MutationObserver) ||
|
|
|
// PhantomJS 和 iOS 7.x
|
|
|
MutationObserver.toString() === '[object MutationObserverConstructor]'
|
|
|
)) {
|
|
|
// 在原生 Promise 不可用时使用 MutationObserver
|
|
|
// 例如 PhantomJS、iOS 7、Android 4.4
|
|
|
// (问题 #6466:MutationObserver 在 IE11 中不可靠)
|
|
|
var counter = 1;
|
|
|
// 创建一个 MutationObserver 实例,当观察到变化时调用 flushCallbacks 函数
|
|
|
var observer = new MutationObserver(flushCallbacks);
|
|
|
// 创建一个文本节点
|
|
|
var textNode = document.createTextNode(String(counter));
|
|
|
// 开始观察文本节点的字符数据变化
|
|
|
observer.observe(textNode, {
|
|
|
characterData: true
|
|
|
});
|
|
|
// 定义 timerFunc 函数,通过改变文本节点的数据来触发 MutationObserver
|
|
|
timerFunc = function () {
|
|
|
counter = (counter + 1) % 2;
|
|
|
textNode.data = String(counter);
|
|
|
};
|
|
|
// 标记正在使用微任务
|
|
|
isUsingMicroTask = true;
|
|
|
// 如果支持 setImmediate 且是原生的
|
|
|
} else if (typeof setImmediate!== 'undefined' && isNative(setImmediate)) {
|
|
|
// 回退到使用 setImmediate
|
|
|
// 从技术上讲,它利用了(宏)任务队列,但它仍然比 setTimeout 是更好的选择
|
|
|
timerFunc = function () {
|
|
|
setImmediate(flushCallbacks);
|
|
|
};
|
|
|
} else {
|
|
|
// 回退到使用 setTimeout
|
|
|
timerFunc = function () {
|
|
|
setTimeout(flushCallbacks, 0);
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 定义 nextTick 函数,用于将回调函数添加到队列中,并在合适的时机执行
|
|
|
function nextTick (cb, ctx) {
|
|
|
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);
|
|
|
}
|
|
|
});
|
|
|
// 如果没有待处理的任务,则标记为有待处理任务,并调用 timerFunc 函数
|
|
|
if (!pending) {
|
|
|
pending = true;
|
|
|
timerFunc();
|
|
|
}
|
|
|
// $flow-disable-line
|
|
|
// 如果没有提供回调函数且支持 Promise,则返回一个 Promise 对象
|
|
|
if (!cb && typeof Promise!== 'undefined') {
|
|
|
return new Promise(function (resolve) {
|
|
|
_resolve = resolve;
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
|
|
|
// 定义 mark 和 measure 变量
|
|
|
var mark;
|
|
|
var measure;
|
|
|
|
|
|
{
|
|
|
// 检查是否在浏览器环境且支持 window.performance
|
|
|
var perf = inBrowser && window.performance;
|
|
|
/* istanbul ignore if */
|
|
|
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)
|
|
|
};
|
|
|
}
|
|
|
} |