|
|
// 缓存样式文本解析器,将CSS字符串转换为样式对象
|
|
|
var parseStyleText = cached(function (cssText) {
|
|
|
var res = {}; // 存储解析结果的空对象
|
|
|
var listDelimiter = /;(?![^(]*\))/g; // 分号分割(排除括号内的分号)
|
|
|
var propertyDelimiter = /:(.+)/; // 冒号分隔属性名和值
|
|
|
cssText.split(listDelimiter).forEach(function (item) { // 拆分样式声明
|
|
|
if (item) { // 过滤空项
|
|
|
var tmp = item.split(propertyDelimiter); // 分割属性键值
|
|
|
tmp.length > 1 && (res[tmp[0].trim()] = tmp[1].trim()); // 存入结果对象
|
|
|
}
|
|
|
});
|
|
|
return res // 返回解析后的样式对象
|
|
|
});
|
|
|
|
|
|
// 合并静态样式和动态样式数据
|
|
|
function normalizeStyleData (data) {
|
|
|
var style = normalizeStyleBinding(data.style); // 标准化动态样式
|
|
|
return data.staticStyle // 合并静态样式(编译时处理)
|
|
|
? extend(data.staticStyle, style)
|
|
|
: style
|
|
|
}
|
|
|
|
|
|
// 标准化样式绑定值(支持数组/字符串/对象格式)
|
|
|
function normalizeStyleBinding (bindingStyle) {
|
|
|
if (Array.isArray(bindingStyle)) { // 数组转为对象合并
|
|
|
return toObject(bindingStyle)
|
|
|
}
|
|
|
if (typeof bindingStyle === 'string') { // 字符串解析为对象
|
|
|
return parseStyleText(bindingStyle)
|
|
|
}
|
|
|
return bindingStyle // 直接返回对象
|
|
|
}
|
|
|
|
|
|
// 获取层级样式(父组件样式覆盖子组件)
|
|
|
function getStyle (vnode, checkChild) {
|
|
|
var res = {}; // 结果对象
|
|
|
var styleData; // 临时存储样式数据
|
|
|
|
|
|
if (checkChild) { // 检查子组件链
|
|
|
var childNode = vnode;
|
|
|
while (childNode.componentInstance) { // 遍历子组件实例
|
|
|
childNode = childNode.componentInstance._vnode; // 获取子组件虚拟节点
|
|
|
if (childNode && childNode.data && (styleData = normalizeStyleData(childNode.data))) {
|
|
|
extend(res, styleData); // 合并子组件样式
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
if ((styleData = normalizeStyleData(vnode.data))) { // 当前节点样式
|
|
|
extend(res, styleData);
|
|
|
}
|
|
|
|
|
|
var parentNode = vnode;
|
|
|
while ((parentNode = parentNode.parent)) { // 遍历父节点链
|
|
|
if (parentNode.data && (styleData = normalizeStyleData(parentNode.data))) {
|
|
|
extend(res, styleData); // 合并父组件样式
|
|
|
}
|
|
|
}
|
|
|
return res // 返回最终合并结果
|
|
|
}
|
|
|
|
|
|
// 匹配CSS自定义属性(--开头)
|
|
|
var cssVarRE = /^--/;
|
|
|
// 匹配!important结尾
|
|
|
var importantRE = /\s*!important$/;
|
|
|
// 设置元素样式属性(处理多种情况)
|
|
|
var setProp = function (el, name, val) {
|
|
|
if (cssVarRE.test(name)) { // CSS变量处理
|
|
|
el.style.setProperty(name, val);
|
|
|
} else if (importantRE.test(val)) { // 含!important处理
|
|
|
el.style.setProperty(hyphenate(name), val.replace(importantRE, ''), 'important');
|
|
|
} else { // 常规属性处理
|
|
|
var normalizedName = normalize(name); // 标准化属性名
|
|
|
if (Array.isArray(val)) { // 数组值处理(自动前缀备选)
|
|
|
for (var i = 0, len = val.length; i < len; i++) { // 遍历数组设置属性
|
|
|
el.style[normalizedName] = val[i];
|
|
|
}
|
|
|
} else { // 单值处理
|
|
|
el.style[normalizedName] = val;
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 浏览器厂商前缀列表
|
|
|
var vendorNames = ['Webkit', 'Moz', 'ms'];
|
|
|
// 缓存空样式对象用于属性检测
|
|
|
var emptyStyle;
|
|
|
// 标准化样式属性名(自动添加前缀)
|
|
|
var normalize = cached(function (prop) {
|
|
|
emptyStyle = emptyStyle || document.createElement('div').style; // 创建临时元素
|
|
|
prop = camelize(prop); // 驼峰化属性名
|
|
|
if (prop !== 'filter' && (prop in emptyStyle)) { // 原生支持检测
|
|
|
return prop
|
|
|
}
|
|
|
var capName = prop.charAt(0).toUpperCase() + prop.slice(1); // 首字母大写
|
|
|
for (var i = 0; i < vendorNames.length; i++) { // 遍历前缀列表
|
|
|
var name = vendorNames[i] + capName; // 生成带前缀属性名
|
|
|
if (name in emptyStyle)) { // 检测是否支持
|
|
|
return name // 返回带前缀属性名
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 样式更新函数(对比新旧样式差异)
|
|
|
function updateStyle (oldVnode, vnode) {
|
|
|
var data = vnode.data; // 新节点数据
|
|
|
var oldData = oldVnode.data; // 旧节点数据
|
|
|
|
|
|
// 无样式变化直接返回
|
|
|
if (isUndef(data.staticStyle) && isUndef(data.style) &&
|
|
|
isUndef(oldData.staticStyle) && isUndef(oldData.style)
|
|
|
) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
var cur, name; // 临时变量
|
|
|
var el = vnode.elm; // 实际DOM元素
|
|
|
var oldStaticStyle = oldData.staticStyle; // 旧静态样式
|
|
|
var oldStyleBinding = oldData.normalizedStyle || oldData.style || {}; // 旧动态样式
|
|
|
|
|
|
var oldStyle = oldStaticStyle || oldStyleBinding; // 合并旧样式
|
|
|
var style = normalizeStyleBinding(vnode.data.style) || {}; // 标准化新样式
|
|
|
|
|
|
// 存储标准化后的样式用于下次对比
|
|
|
vnode.data.normalizedStyle = isDef(style.__ob__)
|
|
|
? extend({}, style) // 响应式数据需要克隆
|
|
|
: style;
|
|
|
|
|
|
var newStyle = getStyle(vnode, true); // 获取层级合并后的新样式
|
|
|
|
|
|
// 移除旧样式中不存在于新样式的属性
|
|
|
for (name in oldStyle) {
|
|
|
if (isUndef(newStyle[name])) {
|
|
|
setProp(el, name, '');
|
|
|
}
|
|
|
}
|
|
|
// 设置/更新变化的样式属性
|
|
|
for (name in newStyle) {
|
|
|
cur = newStyle[name];
|
|
|
if (cur !== oldStyle[name]) { // 值发生变化时更新
|
|
|
setProp(el, name, cur == null ? '' : cur); // null转为空字符串
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 导出样式模块(创建和更新时调用)
|
|
|
var style = {
|
|
|
create: updateStyle,
|
|
|
update: updateStyle
|
|
|
};
|
|
|
|
|
|
// 匹配空白字符的正则
|
|
|
var whitespaceRE = /\s+/;
|
|
|
|
|
|
// 添加类名(兼容SVG元素)
|
|
|
function addClass (el, cls) {
|
|
|
if (!cls || !(cls = cls.trim())) { // 空值处理
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (el.classList) { // 支持classList
|
|
|
if (cls.indexOf(' ') > -1) { // 多类名分割处理
|
|
|
cls.split(whitespaceRE).forEach(function (c) { return el.classList.add(c); });
|
|
|
} else {
|
|
|
el.classList.add(cls);
|
|
|
}
|
|
|
} else { // 兼容旧浏览器
|
|
|
var cur = " " + (el.getAttribute('class') || '') + " "; // 当前类名字符串
|
|
|
if (cur.indexOf(' ' + cls + ' ') < 0) { // 不存在时添加
|
|
|
el.setAttribute('class', (cur + cls).trim());
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 移除类名(兼容SVG元素)
|
|
|
function removeClass (el, cls) {
|
|
|
if (!cls || !(cls = cls.trim())) { // 空值处理
|
|
|
return
|
|
|
}
|
|
|
|
|
|
if (el.classList) { // 支持classList
|
|
|
if (cls.indexOf(' ') > -1) { // 多类名分割处理
|
|
|
cls.split(whitespaceRE).forEach(function (c) { return el.classList.remove(c); });
|
|
|
} else {
|
|
|
el.classList.remove(cls);
|
|
|
}
|
|
|
if (!el.classList.length) { // 无类名时移除属性
|
|
|
el.removeAttribute('class');
|
|
|
}
|
|
|
} else { // 兼容旧浏览器
|
|
|
var cur = " " + (el.getAttribute('class') || '') + " ";
|
|
|
var tar = ' ' + cls + ' ';
|
|
|
while (cur.indexOf(tar) >= 0) { // 循环替换目标类名
|
|
|
cur = cur.replace(tar, ' ');
|
|
|
}
|
|
|
cur = cur.trim(); // 清理空白
|
|
|
if (cur) {
|
|
|
el.setAttribute('class', cur);
|
|
|
} else {
|
|
|
el.removeAttribute('class');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 解析过渡配置并标准化格式
|
|
|
function resolveTransition (def$$1) {
|
|
|
if (!def$$1) { // 无配置直接返回
|
|
|
return
|
|
|
}
|
|
|
/* istanbul ignore else */
|
|
|
if (typeof def$$1 === 'object') { // 对象类型配置
|
|
|
var res = {};
|
|
|
if (def$$1.css !== false) { // 允许CSS过渡
|
|
|
extend(res, autoCssTransition(def$$1.name || 'v')); // 合并自动生成的CSS类
|
|
|
}
|
|
|
extend(res, def$$1); // 合并用户自定义配置
|
|
|
return res
|
|
|
} else if (typeof def$$1 === 'string') { // 字符串类型配置
|
|
|
return autoCssTransition(def$$1) // 生成对应CSS类配置
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 缓存自动生成的CSS过渡类配置
|
|
|
var autoCssTransition = cached(function (name) {
|
|
|
return {
|
|
|
enterClass: (name + "-enter"), // 进入开始类
|
|
|
enterToClass: (name + "-enter-to"), // 进入结束类
|
|
|
enterActiveClass: (name + "-enter-active"), // 进入激活类
|
|
|
leaveClass: (name + "-leave"), // 离开开始类
|
|
|
leaveToClass: (name + "-leave-to"), // 离开结束类
|
|
|
leaveActiveClass: (name + "-leave-active") // 离开激活类
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 浏览器环境检测(排除IE9)
|
|
|
var hasTransition = inBrowser && !isIE9;
|
|
|
var TRANSITION = 'transition'; // CSS过渡类型
|
|
|
var ANIMATION = 'animation'; // CSS动画类型
|
|
|
|
|
|
// 过渡属性/事件检测
|
|
|
var transitionProp = 'transition'; // 过渡属性名
|
|
|
var transitionEndEvent = 'transitionend'; // 过渡结束事件
|
|
|
var animationProp = 'animation'; // 动画属性名
|
|
|
var animationEndEvent = 'animationend'; // 动画结束事件
|
|
|
if (hasTransition) { // 处理浏览器前缀
|
|
|
/* istanbul ignore if */
|
|
|
if (window.ontransitionend === undefined && // 检测Webkit前缀
|
|
|
window.onwebkittransitionend !== undefined
|
|
|
) {
|
|
|
transitionProp = 'WebkitTransition'; // Webkit过渡属性
|
|
|
transitionEndEvent = 'webkitTransitionEnd'; // Webkit过渡结束事件
|
|
|
}
|
|
|
if (window.onanimationend === undefined && // 检测Webkit动画
|
|
|
window.onwebkitanimationend !== undefined
|
|
|
) {
|
|
|
animationProp = 'WebkitAnimation'; // Webkit动画属性
|
|
|
animationEndEvent = 'webkitAnimationEnd'; // Webkit动画结束事件
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 跨浏览器requestAnimationFrame实现
|
|
|
var raf = inBrowser
|
|
|
? window.requestAnimationFrame // 原生支持
|
|
|
? window.requestAnimationFrame.bind(window)
|
|
|
: setTimeout // 降级方案
|
|
|
: /* istanbul ignore next */ function (fn) { return fn(); }; // 非浏览器环境直接执行
|
|
|
|
|
|
// 下一帧执行函数(双raf保证在样式变更后执行)
|
|
|
function nextFrame (fn) {
|
|
|
raf(function () {
|
|
|
raf(fn);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 添加过渡类并记录
|
|
|
function addTransitionClass (el, cls) {
|
|
|
var transitionClasses = el._transitionClasses || (el._transitionClasses = []); // 初始化类存储
|
|
|
if (transitionClasses.indexOf(cls) < 0) { // 避免重复添加
|
|
|
transitionClasses.push(cls); // 记录类名
|
|
|
addClass(el, cls); // 添加类到元素
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 移除过渡类并清理记录
|
|
|
function removeTransitionClass (el, cls) {
|
|
|
if (el._transitionClasses) { // 存在记录时
|
|
|
remove(el._transitionClasses, cls); // 从记录中移除
|
|
|
}
|
|
|
removeClass(el, cls); // 从元素移除类
|
|
|
}
|
|
|
|
|
|
// 等待过渡结束执行回调
|
|
|
function whenTransitionEnds (
|
|
|
el,
|
|
|
expectedType,
|
|
|
cb
|
|
|
) {
|
|
|
var ref = getTransitionInfo(el, expectedType); // 获取过渡信息
|
|
|
var type = ref.type; // 过渡类型
|
|
|
var timeout = ref.timeout; // 超时时间
|
|
|
var propCount = ref.propCount; // 属性数量
|
|
|
if (!type) { return cb() } // 无过渡类型立即回调
|
|
|
var event = type === TRANSITION ? transitionEndEvent : animationEndEvent; // 确定事件类型
|
|
|
var ended = 0; // 结束计数器
|
|
|
var end = function () { // 结束处理函数
|
|
|
el.removeEventListener(event, onEnd); // 移除监听
|
|
|
cb(); // 执行回调
|
|
|
};
|
|
|
var onEnd = function (e) { // 事件回调
|
|
|
if (e.target === el) { // 确认目标元素
|
|
|
if (++ended >= propCount) { // 所有属性过渡完成
|
|
|
end();
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
setTimeout(function () { // 超时兜底
|
|
|
if (ended < propCount) {
|
|
|
end();
|
|
|
}
|
|
|
}, timeout + 1); // 增加1ms防止边界情况
|
|
|
el.addEventListener(event, onEnd); // 监听结束事件
|
|
|
}
|
|
|
|
|
|
// 匹配transform属性的正则
|
|
|
var transformRE = /\b(transform|all)(,|$)/;
|
|
|
|
|
|
// 获取元素过渡信息
|
|
|
function getTransitionInfo (el, expectedType) {
|
|
|
var styles = window.getComputedStyle(el); // 获取计算样式
|
|
|
var transitionDelays = (styles[transitionProp + 'Delay'] || '').split(', '); // 过渡延迟
|
|
|
var transitionDurations = (styles[transitionProp + 'Duration'] || '').split(', '); // 过渡持续时间
|
|
|
var transitionTimeout = getTimeout(transitionDelays, transitionDurations); // 计算总时间
|
|
|
var animationDelays = (styles[animationProp + 'Delay'] || '').split(', '); // 动画延迟
|
|
|
var animationDurations = (styles[animationProp + 'Duration'] || '').split(', '); // 动画持续时间
|
|
|
var animationTimeout = getTimeout(animationDelays, animationDurations); // 计算总时间
|
|
|
|
|
|
var type; // 过渡类型
|
|
|
var timeout = 0; // 总超时
|
|
|
var propCount = 0; // 属性数量
|
|
|
/* istanbul ignore if */
|
|
|
if (expectedType === TRANSITION) { // 预期为过渡类型
|
|
|
if (transitionTimeout > 0) {
|
|
|
type = TRANSITION;
|
|
|
timeout = transitionTimeout;
|
|
|
propCount = transitionDurations.length;
|
|
|
}
|
|
|
} else if (expectedType === ANIMATION) { // 预期为动画类型
|
|
|
if (animationTimeout > 0) {
|
|
|
type = ANIMATION;
|
|
|
timeout = animationTimeout;
|
|
|
propCount = animationDurations.length;
|
|
|
}
|
|
|
} else { // 自动判断类型
|
|
|
timeout = Math.max(transitionTimeout, animationTimeout);
|
|
|
type = timeout > 0
|
|
|
? transitionTimeout > animationTimeout
|
|
|
? TRANSITION
|
|
|
: ANIMATION
|
|
|
: null;
|
|
|
propCount = type
|
|
|
? type === TRANSITION
|
|
|
? transitionDurations.length
|
|
|
: animationDurations.length
|
|
|
: 0;
|
|
|
}
|
|
|
var hasTransform = // 检测是否包含transform属性
|
|
|
type === TRANSITION &&
|
|
|
transformRE.test(styles[transitionProp + 'Property']);
|
|
|
return { // 返回过渡信息对象
|
|
|
type: type,
|
|
|
timeout: timeout,
|
|
|
propCount: propCount,
|
|
|
hasTransform: hasTransform
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 计算最大超时时间
|
|
|
function getTimeout (delays, durations) {
|
|
|
/* istanbul ignore next */ // 填充延迟数组保证长度匹配
|
|
|
while (delays.length < durations.length) {
|
|
|
delays = delays.concat(delays);
|
|
|
}
|
|
|
|
|
|
return Math.max.apply(null, durations.map(function (d, i) { // 计算每个属性的总时间
|
|
|
return toMs(d) + toMs(delays[i])
|
|
|
}))
|
|
|
}
|
|
|
|
|
|
// 转换时间为毫秒(处理逗号小数格式)
|
|
|
function toMs (s) {
|
|
|
return Number(s.slice(0, -1).replace(',', '.')) * 1000 // 替换逗号为点
|
|
|
}
|
|
|
|
|
|
/* 进入过渡处理 */
|
|
|
|
|
|
function enter (vnode, toggleDisplay) {
|
|
|
var el = vnode.elm; // 获取DOM元素
|
|
|
|
|
|
// 立即调用旧的离开回调
|
|
|
if (isDef(el._leaveCb)) {
|
|
|
el._leaveCb.cancelled = true; // 标记取消
|
|
|
el._leaveCb(); // 执行回调
|
|
|
}
|
|
|
|
|
|
var data = resolveTransition(vnode.data.transition); // 解析过渡配置
|
|
|
if (isUndef(data)) { // 无有效配置直接返回
|
|
|
return
|
|
|
}
|
|
|
|
|
|
/* istanbul ignore if */
|
|
|
if (isDef(el._enterCb) || el.nodeType !== 1) { // 已存在进入回调或非元素节点
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 解构过渡配置项
|
|
|
var css = data.css;
|
|
|
var type = data.type;
|
|
|
var enterClass = data.enterClass;
|
|
|
var enterToClass = data.enterToClass;
|
|
|
var enterActiveClass = data.enterActiveClass;
|
|
|
var appearClass = data.appearClass;
|
|
|
var appearToClass = data.appearToClass;
|
|
|
var appearActiveClass = data.appearActiveClass;
|
|
|
var beforeEnter = data.beforeEnter;
|
|
|
var enter = data.enter;
|
|
|
var afterEnter = data.afterEnter;
|
|
|
var enterCancelled = data.enterCancelled;
|
|
|
var beforeAppear = data.beforeAppear;
|
|
|
var appear = data.appear;
|
|
|
var afterAppear = data.afterAppear;
|
|
|
var appearCancelled = data.appearCancelled;
|
|
|
var duration = data.duration;
|
|
|
|
|
|
// 获取上下文实例
|
|
|
var context = activeInstance;
|
|
|
var transitionNode = activeInstance.$vnode;
|
|
|
while (transitionNode && transitionNode.parent) { // 查找最近的transition父组件
|
|
|
context = transitionNode.context;
|
|
|
transitionNode = transitionNode.parent;
|
|
|
}
|
|
|
|
|
|
var isAppear = !context._isMounted || !vnode.isRootInsert; // 判断是否初次渲染
|
|
|
|
|
|
if (isAppear && !appear && appear !== '') { // 初次渲染但未配置appear
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 确定使用的类名
|
|
|
var startClass = isAppear && appearClass
|
|
|
? appearClass
|
|
|
: enterClass;
|
|
|
var activeClass = isAppear && appearActiveClass
|
|
|
? appearActiveClass
|
|
|
: enterActiveClass;
|
|
|
var toClass = isAppear && appearToClass
|
|
|
? appearToClass
|
|
|
: enterToClass;
|
|
|
|
|
|
// 确定使用的钩子函数
|
|
|
var beforeEnterHook = isAppear
|
|
|
? (beforeAppear || beforeEnter)
|
|
|
: beforeEnter;
|
|
|
var enterHook = isAppear
|
|
|
? (typeof appear === 'function' ? appear : enter)
|
|
|
: enter;
|
|
|
var afterEnterHook = isAppear
|
|
|
? (afterAppear || afterEnter)
|
|
|
: afterEnter;
|
|
|
var enterCancelledHook = isAppear
|
|
|
? (appearCancelled || enterCancelled)
|
|
|
: enterCancelled;
|
|
|
|
|
|
// 处理显式指定的持续时间
|
|
|
var explicitEnterDuration = toNumber(
|
|
|
isObject(duration)
|
|
|
? duration.enter
|
|
|
: duration
|
|
|
);
|
|
|
|
|
|
if (explicitEnterDuration != null) { // 验证持续时间有效性
|
|
|
checkDuration(explicitEnterDuration, 'enter', vnode);
|
|
|
}
|
|
|
|
|
|
var expectsCSS = css !== false && !isIE9; // 是否期望CSS过渡
|
|
|
var userWantsControl = getHookArgumentsLength(enterHook); // 用户是否控制过渡
|
|
|
|
|
|
var cb = el._enterCb = once(function () { // 单次执行回调
|
|
|
if (expectsCSS) { // 清理CSS类
|
|
|
removeTransitionClass(el, toClass);
|
|
|
removeTransitionClass(el, activeClass);
|
|
|
}
|
|
|
if (cb.cancelled) { // 过渡取消处理
|
|
|
if (expectsCSS) {
|
|
|
removeTransitionClass(el, startClass);
|
|
|
}
|
|
|
enterCancelledHook && enterCancelledHook(el);
|
|
|
} else { // 正常结束处理
|
|
|
afterEnterHook && afterEnterHook(el);
|
|
|
}
|
|
|
el._enterCb = null; // 清理回调引用
|
|
|
});
|
|
|
|
|
|
if (!vnode.data.show) { // 非v-show控制的元素
|
|
|
// 注入insert钩子处理待定离开元素
|
|
|
mergeVNodeHook(vnode, 'insert', function () {
|
|
|
var parent = el.parentNode;
|
|
|
var pendingNode = parent && parent._pending && parent._pending[vnode.key];
|
|
|
if (pendingNode && // 清理旧节点的离开回调
|
|
|
pendingNode.tag === vnode.tag &&
|
|
|
pendingNode.elm._leaveCb
|
|
|
) {
|
|
|
pendingNode.elm._leaveCb();
|
|
|
}
|
|
|
enterHook && enterHook(el, cb); // 执行进入钩子
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 开始进入过渡
|
|
|
beforeEnterHook && beforeEnterHook(el); // 执行前置钩子
|
|
|
if (expectsCSS) { // CSS过渡处理
|
|
|
addTransitionClass(el, startClass); // 添加起始类
|
|
|
addTransitionClass(el, activeClass); // 添加激活类
|
|
|
nextFrame(function () { // 下一帧处理
|
|
|
removeTransitionClass(el, startClass); // 移除起始类
|
|
|
if (!cb.cancelled) {
|
|
|
addTransitionClass(el, toClass); // 添加目标类
|
|
|
if (!userWantsControl) { // 用户未自定义控制
|
|
|
if (isValidDuration(explicitEnterDuration)) { // 有效持续时间
|
|
|
setTimeout(cb, explicitEnterDuration); // 定时器结束
|
|
|
} else {
|
|
|
whenTransitionEnds(el, type, cb); // 监听过渡结束
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
if (vnode.data.show) { // v-show控制的元素
|
|
|
toggleDisplay && toggleDisplay(); // 切换显示状态
|
|
|
enterHook && enterHook(el, cb); // 执行进入钩子
|
|
|
}
|
|
|
|
|
|
if (!expectsCSS && !userWantsControl) { // 无CSS过渡且用户未控制
|
|
|
cb(); // 立即回调
|
|
|
}
|
|
|
}
|
|
|
// 处理元素离开过渡
|
|
|
function leave (vnode, rm) {
|
|
|
var el = vnode.elm; // 获取DOM元素
|
|
|
|
|
|
// 立即调用可能的进入回调
|
|
|
if (isDef(el._enterCb)) {
|
|
|
el._enterCb.cancelled = true; // 标记进入回调已取消
|
|
|
el._enterCb(); // 执行进入回调清理
|
|
|
}
|
|
|
|
|
|
var data = resolveTransition(vnode.data.transition); // 解析过渡配置
|
|
|
if (isUndef(data) || el.nodeType !== 1) { // 无效配置或非元素节点
|
|
|
return rm() // 直接执行移除回调
|
|
|
}
|
|
|
|
|
|
/* istanbul ignore if */
|
|
|
if (isDef(el._leaveCb)) { // 已存在离开回调
|
|
|
return // 防止重复执行
|
|
|
}
|
|
|
|
|
|
// 解构过渡配置项
|
|
|
var css = data.css;
|
|
|
var type = data.type;
|
|
|
var leaveClass = data.leaveClass;
|
|
|
var leaveToClass = data.leaveToClass;
|
|
|
var leaveActiveClass = data.leaveActiveClass;
|
|
|
var beforeLeave = data.beforeLeave;
|
|
|
var leave = data.leave;
|
|
|
var afterLeave = data.afterLeave;
|
|
|
var leaveCancelled = data.leaveCancelled;
|
|
|
var delayLeave = data.delayLeave; // 延迟离开函数
|
|
|
var duration = data.duration;
|
|
|
|
|
|
var expectsCSS = css !== false && !isIE9; // 是否使用CSS过渡
|
|
|
var userWantsControl = getHookArgumentsLength(leave); // 用户是否控制离开过程
|
|
|
|
|
|
// 处理显式离开持续时间
|
|
|
var explicitLeaveDuration = toNumber(
|
|
|
isObject(duration)
|
|
|
? duration.leave
|
|
|
: duration
|
|
|
);
|
|
|
|
|
|
if (isDef(explicitLeaveDuration)) { // 验证持续时间有效性
|
|
|
checkDuration(explicitLeaveDuration, 'leave', vnode);
|
|
|
}
|
|
|
|
|
|
// 创建一次性离开回调
|
|
|
var cb = el._leaveCb = once(function () {
|
|
|
if (el.parentNode && el.parentNode._pending) { // 清理父节点待处理记录
|
|
|
el.parentNode._pending[vnode.key] = null;
|
|
|
}
|
|
|
if (expectsCSS) { // 清理CSS类
|
|
|
removeTransitionClass(el, leaveToClass);
|
|
|
removeTransitionClass(el, leaveActiveClass);
|
|
|
}
|
|
|
if (cb.cancelled) { // 过渡取消处理
|
|
|
if (expectsCSS) {
|
|
|
removeTransitionClass(el, leaveClass);
|
|
|
}
|
|
|
leaveCancelled && leaveCancelled(el); // 调用取消钩子
|
|
|
} else { // 正常结束
|
|
|
rm(); // 执行移除回调
|
|
|
afterLeave && afterLeave(el); // 调用结束钩子
|
|
|
}
|
|
|
el._leaveCb = null; // 清除回调引用
|
|
|
});
|
|
|
|
|
|
if (delayLeave) { // 存在延迟离开函数
|
|
|
delayLeave(performLeave); // 延迟执行离开
|
|
|
} else {
|
|
|
performLeave(); // 立即执行离开
|
|
|
}
|
|
|
|
|
|
// 实际执行离开动画的函数
|
|
|
function performLeave () {
|
|
|
if (cb.cancelled) { // 已取消则返回
|
|
|
return
|
|
|
}
|
|
|
// 记录待移除元素
|
|
|
if (!vnode.data.show && el.parentNode) {
|
|
|
(el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode;
|
|
|
}
|
|
|
beforeLeave && beforeLeave(el); // 执行前置钩子
|
|
|
if (expectsCSS) { // CSS过渡处理
|
|
|
addTransitionClass(el, leaveClass); // 添加离开起始类
|
|
|
addTransitionClass(el, leaveActiveClass); // 添加离开激活类
|
|
|
nextFrame(function () { // 下一帧处理
|
|
|
removeTransitionClass(el, leaveClass); // 移除起始类
|
|
|
if (!cb.cancelled) {
|
|
|
addTransitionClass(el, leaveToClass); // 添加离开目标类
|
|
|
if (!userWantsControl) { // 用户未自定义控制
|
|
|
if (isValidDuration(explicitLeaveDuration)) { // 有效持续时间
|
|
|
setTimeout(cb, explicitLeaveDuration); // 定时触发回调
|
|
|
} else {
|
|
|
whenTransitionEnds(el, type, cb); // 监听过渡结束
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
leave && leave(el, cb); // 执行用户提供的离开钩子
|
|
|
if (!expectsCSS && !userWantsControl) { // 无CSS且用户未控制
|
|
|
cb(); // 直接执行回调
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 开发环境校验过渡持续时间有效性
|
|
|
function checkDuration (val, name, vnode) {
|
|
|
if (typeof val !== 'number') { // 非数字类型警告
|
|
|
warn(
|
|
|
"<transition> 显式 " + name + " 持续时间无效,应为数字 - 得到 " + (JSON.stringify(val)),
|
|
|
vnode.context
|
|
|
);
|
|
|
} else if (isNaN(val)) { // NaN值警告
|
|
|
warn(
|
|
|
"<transition> 显式 " + name + " 持续时间为 NaN,可能表达式错误",
|
|
|
vnode.context
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 验证是否为有效持续时间
|
|
|
function isValidDuration (val) {
|
|
|
return typeof val === 'number' && !isNaN(val) // 数字且非NaN
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* 标准化过渡钩子的参数长度(处理合并钩子/组件方法/普通函数)
|
|
|
*/
|
|
|
function getHookArgumentsLength (fn) {
|
|
|
if (isUndef(fn)) return false // 无钩子直接返回
|
|
|
var invokerFns = fn.fns; // 处理合并的钩子数组
|
|
|
if (isDef(invokerFns)) {
|
|
|
return getHookArgumentsLength( // 递归检测第一个真实钩子
|
|
|
Array.isArray(invokerFns) ? invokerFns[0] : invokerFns
|
|
|
)
|
|
|
} else {
|
|
|
return (fn._length || fn.length) > 1 // 检测参数长度是否大于1
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 进入过渡的封装函数(用于create/activate生命周期)
|
|
|
function _enter (_, vnode) {
|
|
|
if (vnode.data.show !== true) { // 非v-show控制的元素
|
|
|
enter(vnode); // 执行进入过渡
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 浏览器环境的过渡处理模块
|
|
|
var transition = inBrowser ? {
|
|
|
create: _enter, // 元素创建时触发进入
|
|
|
activate: _enter, // 组件激活时触发进入
|
|
|
remove: function remove$$1 (vnode, rm) { // 元素移除时
|
|
|
if (vnode.data.show !== true) { // 非v-show元素
|
|
|
leave(vnode, rm); // 执行离开过渡
|
|
|
} else {
|
|
|
rm(); // 直接移除
|
|
|
}
|
|
|
}
|
|
|
} : {}; // 非浏览器环境空对象
|
|
|
|
|
|
// 平台相关模块列表(按顺序应用)
|
|
|
var platformModules = [
|
|
|
attrs, // 属性处理模块
|
|
|
klass, // class类名处理模块
|
|
|
events, // 事件处理模块
|
|
|
domProps, // DOM属性处理模块
|
|
|
style, // 样式处理模块
|
|
|
transition // 过渡处理模块
|
|
|
];
|
|
|
|
|
|
// 合并基础模块和平台模块
|
|
|
var modules = platformModules.concat(baseModules);
|
|
|
|
|
|
// 创建虚拟DOM patch函数
|
|
|
var patch = createPatchFunction({
|
|
|
nodeOps: nodeOps, // DOM节点操作方法
|
|
|
modules: modules // 使用的模块列表
|
|
|
});
|
|
|
|
|
|
/* IE9兼容处理 */
|
|
|
if (isIE9) {
|
|
|
// 监听selectionchange事件解决oninput不触发的问题
|
|
|
document.addEventListener('selectionchange', function () {
|
|
|
var el = document.activeElement; // 当前焦点元素
|
|
|
if (el && el.vmodel) { // 带有vmodel标记的元素
|
|
|
trigger(el, 'input'); // 手动触发input事件
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// v-model指令实现
|
|
|
var directive = {
|
|
|
inserted: function inserted (el, binding, vnode, oldVnode) {
|
|
|
if (vnode.tag === 'select') { // select元素处理
|
|
|
if (oldVnode.elm && !oldVnode.elm._vOptions) { // 旧节点无选项缓存
|
|
|
mergeVNodeHook(vnode, 'postpatch', function () { // 合并postpatch钩子
|
|
|
directive.componentUpdated(el, binding, vnode); // 更新后设置选中
|
|
|
});
|
|
|
} else {
|
|
|
setSelected(el, binding, vnode.context); // 直接设置选中
|
|
|
}
|
|
|
el._vOptions = [].map.call(el.options, getValue); // 缓存选项值
|
|
|
} else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { // 文本输入类元素
|
|
|
el._vModifiers = binding.modifiers; // 存储修饰符
|
|
|
if (!binding.modifiers.lazy) { // 非lazy模式
|
|
|
el.addEventListener('compositionstart', onCompositionStart); // 输入法开始
|
|
|
el.addEventListener('compositionend', onCompositionEnd); // 输入法结束
|
|
|
el.addEventListener('change', onCompositionEnd); // 处理Safari兼容
|
|
|
if (isIE9) el.vmodel = true; // IE9特殊标记
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
componentUpdated: function componentUpdated (el, binding, vnode) {
|
|
|
if (vnode.tag === 'select') { // select元素更新后
|
|
|
setSelected(el, binding, vnode.context); // 设置选中状态
|
|
|
var prevOptions = el._vOptions; // 旧选项值
|
|
|
var curOptions = el._vOptions = [].map.call(el.options, getValue); // 新选项值
|
|
|
if (curOptions.some(function (o, i) { return !looseEqual(o, prevOptions[i]); })) { // 选项变化检测
|
|
|
var needReset = el.multiple // 多选模式检测
|
|
|
? binding.value.some(hasNoMatchingOption)
|
|
|
: binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions);
|
|
|
if (needReset) trigger(el, 'change'); // 触发change事件
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 设置select元素选中状态
|
|
|
function setSelected (el, binding, vm) {
|
|
|
actuallySetSelected(el, binding, vm); // 实际设置
|
|
|
/* IE/Edge兼容处理 */
|
|
|
if (isIE || isEdge) setTimeout(() => actuallySetSelected(el, binding, vm), 0);
|
|
|
}
|
|
|
|
|
|
// 实际设置选中逻辑
|
|
|
function actuallySetSelected (el, binding, vm) {
|
|
|
var value = binding.value; // 当前绑定值
|
|
|
var isMultiple = el.multiple; // 是否多选
|
|
|
if (isMultiple && !Array.isArray(value)) { // 多选但值非数组
|
|
|
warn(
|
|
|
`<select multiple v-model="${binding.expression}"> 需要数组类型值`,
|
|
|
vm
|
|
|
);
|
|
|
return
|
|
|
}
|
|
|
var selected, option;
|
|
|
for (var i = 0; i < el.options.length; i++) { // 遍历选项
|
|
|
option = el.options[i];
|
|
|
if (isMultiple) { // 多选处理
|
|
|
selected = looseIndexOf(value, getValue(option)) > -1; // 是否在值数组中
|
|
|
if (option.selected !== selected) option.selected = selected; // 更新选中状态
|
|
|
} else { // 单选处理
|
|
|
if (looseEqual(getValue(option), value)) { // 值匹配
|
|
|
if (el.selectedIndex !== i) el.selectedIndex = i; // 设置选中索引
|
|
|
return
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if (!isMultiple) el.selectedIndex = -1; // 未匹配时取消选中
|
|
|
}
|
|
|
|
|
|
// 检测值是否无匹配选项
|
|
|
function hasNoMatchingOption (value, options) {
|
|
|
return options.every(o => !looseEqual(o, value)) // 全部不匹配返回true
|
|
|
}
|
|
|
|
|
|
// 获取选项值(优先使用_value)
|
|
|
function getValue (option) {
|
|
|
return '_value' in option ? option._value : option.value
|
|
|
}
|
|
|
|
|
|
// 输入法开始组合输入
|
|
|
function onCompositionStart (e) {
|
|
|
e.target.composing = true; // 标记组合输入状态
|
|
|
}
|
|
|
|
|
|
// 输入法结束组合输入
|
|
|
function onCompositionEnd (e) {
|
|
|
if (!e.target.composing) return // 非组合输入直接返回
|
|
|
e.target.composing = false; // 清除标记
|
|
|
trigger(e.target, 'input'); // 触发input事件
|
|
|
}
|
|
|
|
|
|
// 触发原生事件
|
|
|
function trigger (el, type) {
|
|
|
var e = document.createEvent('HTMLEvents'); // 创建事件
|
|
|
e.initEvent(type, true, true); // 初始化
|
|
|
el.dispatchEvent(e); // 派发事件
|
|
|
}
|
|
|
|
|
|
// 递归查找真实过渡节点(跳过无过渡的组件)
|
|
|
function locateNode (vnode) {
|
|
|
return (vnode.componentInstance && (!vnode.data || !vnode.data.transition))
|
|
|
? locateNode(vnode.componentInstance._vnode) // 递归组件实例
|
|
|
: vnode // 返回找到的节点
|
|
|
}
|
|
|
|
|
|
// v-show指令实现
|
|
|
var show = {
|
|
|
bind: function (el, { value }, vnode) { // 初始绑定
|
|
|
vnode = locateNode(vnode); // 查找过渡节点
|
|
|
var transition = vnode.data && vnode.data.transition; // 过渡配置
|
|
|
el.__vOriginalDisplay = el.style.display === 'none' ? '' : el.style.display; // 保存原始display
|
|
|
if (value && transition) { // 显示且有过渡
|
|
|
vnode.data.show = true; // 标记显示状态
|
|
|
enter(vnode, () => el.style.display = el.__vOriginalDisplay); // 执行进入过渡
|
|
|
} else {
|
|
|
el.style.display = value ? el.__vOriginalDisplay : 'none'; // 直接设置显示
|
|
|
}
|
|
|
},
|
|
|
|
|
|
update: function (el, { value, oldValue }, vnode) { // 更新
|
|
|
if (!!value === !!oldValue) return // 值无变化跳过
|
|
|
vnode = locateNode(vnode);
|
|
|
var transition = vnode.data && vnode.data.transition;
|
|
|
if (transition) {
|
|
|
vnode.data.show = true; // 更新显示标记
|
|
|
value
|
|
|
? enter(vnode, () => el.style.display = el.__vOriginalDisplay) // 进入过渡
|
|
|
: leave(vnode, () => el.style.display = 'none') // 离开过渡
|
|
|
} else {
|
|
|
el.style.display = value ? el.__vOriginalDisplay : 'none'; // 直接切换
|
|
|
}
|
|
|
},
|
|
|
|
|
|
unbind: function (el, binding, vnode, oldVnode, isDestroy) { // 解绑
|
|
|
if (!isDestroy) { // 非销毁阶段
|
|
|
el.style.display = el.__vOriginalDisplay; // 恢复原始display
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 导出平台指令(v-model和v-show)
|
|
|
var platformDirectives = {
|
|
|
model: directive,
|
|
|
show: show
|
|
|
};
|
|
|
|
|
|
// 定义过渡组件支持的属性列表
|
|
|
var transitionProps = {
|
|
|
name: String, // 过渡名称
|
|
|
appear: Boolean, // 是否在初始渲染时使用过渡
|
|
|
css: Boolean, // 是否使用 CSS 过渡类
|
|
|
mode: String, // 过渡模式(in-out/out-in)
|
|
|
type: String, // 过渡类型(transition/animation)
|
|
|
enterClass: String, // 进入开始类名
|
|
|
leaveClass: String, // 离开开始类名
|
|
|
enterToClass: String, // 进入结束类名
|
|
|
leaveToClass: String, // 离开结束类名
|
|
|
enterActiveClass: String, // 进入激活类名
|
|
|
leaveActiveClass: String, // 离开激活类名
|
|
|
appearClass: String, // 初始渲染开始类名
|
|
|
appearActiveClass: String, // 初始渲染激活类名
|
|
|
appearToClass: String, // 初始渲染结束类名
|
|
|
duration: [Number, String, Object] // 过渡持续时间
|
|
|
};
|
|
|
|
|
|
// 递归获取真实子组件(跳过抽象组件)
|
|
|
function getRealChild (vnode) {
|
|
|
var compOptions = vnode && vnode.componentOptions;
|
|
|
if (compOptions && compOptions.Ctor.options.abstract) { // 如果是抽象组件
|
|
|
return getRealChild(getFirstComponentChild(compOptions.children)) // 递归查找
|
|
|
} else {
|
|
|
return vnode // 返回真实子节点
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 从组件实例提取过渡数据
|
|
|
function extractTransitionData (comp) {
|
|
|
var data = {};
|
|
|
var options = comp.$options;
|
|
|
// 复制 props 数据
|
|
|
for (var key in options.propsData) {
|
|
|
data[key] = comp[key];
|
|
|
}
|
|
|
// 复制父级监听器
|
|
|
var listeners = options._parentListeners;
|
|
|
for (var key$1 in listeners) {
|
|
|
data[camelize(key$1)] = listeners[key$1]; // 驼峰化事件名
|
|
|
}
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 生成占位符元素(处理 keep-alive)
|
|
|
function placeholder (h, rawChild) {
|
|
|
if (/\d-keep-alive$/.test(rawChild.tag)) { // 匹配 keep-alive 标签
|
|
|
return h('keep-alive', { // 创建新的 keep-alive 元素
|
|
|
props: rawChild.componentOptions.propsData // 继承原始属性
|
|
|
})
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查父级是否包含过渡
|
|
|
function hasParentTransition (vnode) {
|
|
|
while ((vnode = vnode.parent)) { // 遍历父级节点
|
|
|
if (vnode.data.transition) {
|
|
|
return true
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 判断是否是相同子节点
|
|
|
function isSameChild (child, oldChild) {
|
|
|
return oldChild.key === child.key && oldChild.tag === child.tag // 比较 key 和 tag
|
|
|
}
|
|
|
|
|
|
// 过滤非文本节点的辅助函数
|
|
|
var isNotTextNode = function (c) { return c.tag || isAsyncPlaceholder(c); };
|
|
|
|
|
|
// 检测是否是 v-show 指令
|
|
|
var isVShowDirective = function (d) { return d.name === 'show'; };
|
|
|
|
|
|
// 过渡组件定义
|
|
|
var Transition = {
|
|
|
name: 'transition',
|
|
|
props: transitionProps,
|
|
|
abstract: true, // 标记为抽象组件
|
|
|
|
|
|
render: function render (h) {
|
|
|
var this$1 = this;
|
|
|
|
|
|
var children = this.$slots.default; // 获取默认插槽内容
|
|
|
if (!children) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 过滤掉文本节点(可能存在空白)
|
|
|
children = children.filter(isNotTextNode);
|
|
|
/* istanbul ignore if */
|
|
|
if (!children.length) { // 没有有效子节点时返回
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 多个子节点警告
|
|
|
if (children.length > 1) {
|
|
|
warn(
|
|
|
'<transition> 只能用于单个元素,列表请用 <transition-group>',
|
|
|
this.$parent
|
|
|
);
|
|
|
}
|
|
|
|
|
|
var mode = this.mode;
|
|
|
|
|
|
// 验证过渡模式有效性
|
|
|
if (mode && mode !== 'in-out' && mode !== 'out-in') {
|
|
|
warn(
|
|
|
'无效的过渡模式: ' + mode,
|
|
|
this.$parent
|
|
|
);
|
|
|
}
|
|
|
|
|
|
var rawChild = children[0]; // 获取第一个子节点
|
|
|
|
|
|
// 如果父级有过渡则直接返回
|
|
|
if (hasParentTransition(this.$vnode)) {
|
|
|
return rawChild
|
|
|
}
|
|
|
|
|
|
// 获取真实子节点(跳过抽象组件)
|
|
|
var child = getRealChild(rawChild);
|
|
|
/* istanbul ignore if */
|
|
|
if (!child) { // 没有真实子节点时返回原始节点
|
|
|
return rawChild
|
|
|
}
|
|
|
|
|
|
if (this._leaving) { // 处于离开状态时返回占位符
|
|
|
return placeholder(h, rawChild)
|
|
|
}
|
|
|
|
|
|
// 生成唯一 key 用于过渡管理
|
|
|
var id = "__transition-" + (this._uid) + "-";
|
|
|
child.key = child.key == null // 处理不同情况下的 key 生成
|
|
|
? child.isComment
|
|
|
? id + 'comment'
|
|
|
: id + child.tag
|
|
|
: isPrimitive(child.key)
|
|
|
? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
|
|
|
: child.key;
|
|
|
|
|
|
// 注入过渡数据到子节点
|
|
|
var data = (child.data || (child.data = {})).transition = extractTransitionData(this);
|
|
|
var oldRawChild = this._vnode; // 获取旧节点
|
|
|
var oldChild = getRealChild(oldRawChild);
|
|
|
|
|
|
// 处理 v-show 指令
|
|
|
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
|
|
|
child.data.show = true; // 标记需要显示
|
|
|
}
|
|
|
|
|
|
// 新旧子节点不同时的处理逻辑
|
|
|
if (
|
|
|
oldChild &&
|
|
|
oldChild.data &&
|
|
|
!isSameChild(child, oldChild) &&
|
|
|
!isAsyncPlaceholder(oldChild) &&
|
|
|
// 处理组件根节点是注释的情况
|
|
|
!(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
|
|
|
) {
|
|
|
var oldData = oldChild.data.transition = extend({}, data); // 复制过渡数据
|
|
|
// 处理不同过渡模式
|
|
|
if (mode === 'out-in') { // 先出后进模式
|
|
|
this._leaving = true; // 标记离开状态
|
|
|
mergeVNodeHook(oldData, 'afterLeave', function () { // 注册离开完成回调
|
|
|
this$1._leaving = false;
|
|
|
this$1.$forceUpdate(); // 强制更新
|
|
|
});
|
|
|
return placeholder(h, rawChild) // 返回占位符
|
|
|
} else if (mode === 'in-out') { // 先进后出模式
|
|
|
if (isAsyncPlaceholder(child)) { // 异步组件占位符
|
|
|
return oldRawChild
|
|
|
}
|
|
|
var delayedLeave; // 延迟离开函数
|
|
|
var performLeave = function () { delayedLeave(); }; // 执行离开函数
|
|
|
mergeVNodeHook(data, 'afterEnter', performLeave); // 进入后触发离开
|
|
|
mergeVNodeHook(data, 'enterCancelled', performLeave); // 取消进入时触发离开
|
|
|
mergeVNodeHook(oldData, 'delayLeave', function (leave) { delayedLeave = leave; }); // 保存延迟离开方法
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return rawChild // 返回原始子节点
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/* 过渡组相关代码 */
|
|
|
|
|
|
// 扩展过渡属性(删除 mode 属性)
|
|
|
var props = extend({
|
|
|
tag: String, // 外层包裹标签
|
|
|
moveClass: String // 移动过渡类名
|
|
|
}, transitionProps);
|
|
|
|
|
|
delete props.mode; // 删除 mode 属性
|
|
|
|
|
|
// 过渡组组件定义
|
|
|
var TransitionGroup = {
|
|
|
props: props,
|
|
|
|
|
|
beforeMount: function beforeMount () {
|
|
|
var this$1 = this;
|
|
|
|
|
|
var update = this._update; // 保存原始更新方法
|
|
|
// 重写 _update 方法处理过渡组更新
|
|
|
this._update = function (vnode, hydrating) {
|
|
|
var restoreActiveInstance = setActiveInstance(this$1); // 保存当前激活实例
|
|
|
// 强制移除需要删除的节点
|
|
|
this$1.__patch__(
|
|
|
this$1._vnode,
|
|
|
this$1.kept,
|
|
|
false, // 非服务端渲染
|
|
|
true // 仅移除模式
|
|
|
);
|
|
|
this$1._vnode = this$1.kept; // 更新当前 vnode
|
|
|
restoreActiveInstance(); // 恢复激活实例
|
|
|
update.call(this$1, vnode, hydrating); // 调用原始更新方法
|
|
|
};
|
|
|
},
|
|
|
|
|
|
render: function render (h) {
|
|
|
var tag = this.tag || this.$vnode.data.tag || 'span'; // 获取包裹标签
|
|
|
var map = Object.create(null); // 创建 key 映射表
|
|
|
var prevChildren = this.prevChildren = this.children; // 保存旧子节点
|
|
|
var rawChildren = this.$slots.default || []; // 获取原始子节点
|
|
|
var children = this.children = []; // 初始化当前子节点列表
|
|
|
var transitionData = extractTransitionData(this); // 提取过渡数据
|
|
|
|
|
|
// 处理原始子节点
|
|
|
for (var i = 0; i < rawChildren.length; i++) {
|
|
|
var c = rawChildren[i];
|
|
|
if (c.tag) { // 过滤有效元素
|
|
|
if (c.key != null && String(c.key).indexOf('__vlist') !== 0) { // 验证有效 key
|
|
|
children.push(c);
|
|
|
map[c.key] = c; // 记录 key 映射
|
|
|
;(c.data || (c.data = {})).transition = transitionData; // 注入过渡数据
|
|
|
} else { // 无效 key 警告
|
|
|
var opts = c.componentOptions;
|
|
|
var name = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag;
|
|
|
warn(("<transition-group> 子元素必须带 key: <" + name + ">"));
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理旧子节点
|
|
|
if (prevChildren) {
|
|
|
var kept = []; // 保留的节点
|
|
|
var removed = []; // 移除的节点
|
|
|
for (var i$1 = 0; i$1 < prevChildren.length; i$1++) {
|
|
|
var c$1 = prevChildren[i$1];
|
|
|
c$1.data.transition = transitionData; // 注入过渡数据
|
|
|
c$1.data.pos = c$1.elm.getBoundingClientRect(); // 记录位置信息
|
|
|
if (map[c$1.key]) { // 判断是否保留
|
|
|
kept.push(c$1);
|
|
|
} else {
|
|
|
removed.push(c$1);
|
|
|
}
|
|
|
}
|
|
|
this.kept = h(tag, null, kept); // 创建保留节点的 VNode
|
|
|
this.removed = removed; // 记录被移除的节点
|
|
|
}
|
|
|
|
|
|
return h(tag, null, children) // 渲染当前子节点
|
|
|
},
|
|
|
|
|
|
updated: function updated () {
|
|
|
var children = this.prevChildren; // 获取旧子节点
|
|
|
var moveClass = this.moveClass || ((this.name || 'v') + '-move'); // 移动类名
|
|
|
if (!children.length || !this.hasMove(children[0].elm, moveClass)) { // 无需移动时返回
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 分阶段处理 DOM 操作防止布局抖动
|
|
|
children.forEach(callPendingCbs); // 处理待定回调
|
|
|
children.forEach(recordPosition); // 记录新位置
|
|
|
children.forEach(applyTranslation); // 应用位置变换
|
|
|
|
|
|
// 强制重排以确保布局正确
|
|
|
this._reflow = document.body.offsetHeight; // 触发浏览器重排
|
|
|
|
|
|
// 为移动元素添加过渡效果
|
|
|
children.forEach(function (c) {
|
|
|
if (c.data.moved) { // 需要移动的元素
|
|
|
var el = c.elm;
|
|
|
var s = el.style;
|
|
|
addTransitionClass(el, moveClass); // 添加移动类
|
|
|
s.transform = s.WebkitTransform = s.transitionDuration = ''; // 重置样式
|
|
|
// 添加过渡结束监听器
|
|
|
el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
|
|
|
if (e && e.target !== el) { // 过滤无关事件
|
|
|
return
|
|
|
}
|
|
|
if (!e || /transform$/.test(e.propertyName)) { // 检查变换属性
|
|
|
el.removeEventListener(transitionEndEvent, cb); // 移除监听
|
|
|
el._moveCb = null;
|
|
|
removeTransitionClass(el, moveClass); // 移除移动类
|
|
|
}
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
},
|
|
|
|
|
|
methods: {
|
|
|
// 检测元素是否支持移动过渡
|
|
|
hasMove: function hasMove (el, moveClass) {
|
|
|
/* istanbul ignore if */
|
|
|
if (!hasTransition) { // 浏览器不支持过渡
|
|
|
return false
|
|
|
}
|
|
|
/* istanbul ignore if */
|
|
|
if (this._hasMove) { // 已缓存检测结果
|
|
|
return this._hasMove
|
|
|
}
|
|
|
// 克隆元素进行过渡检测
|
|
|
var clone = el.cloneNode();
|
|
|
if (el._transitionClasses) { // 移除其他过渡类
|
|
|
el._transitionClasses.forEach(function (cls) { removeClass(clone, cls); });
|
|
|
}
|
|
|
addClass(clone, moveClass); // 添加移动类
|
|
|
clone.style.display = 'none'; // 隐藏克隆元素
|
|
|
this.$el.appendChild(clone); // 插入 DOM
|
|
|
var info = getTransitionInfo(clone); // 获取过渡信息
|
|
|
this.$el.removeChild(clone); // 移除克隆元素
|
|
|
return (this._hasMove = info.hasTransform) // 返回是否支持变换
|
|
|
}
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 执行待定的过渡回调
|
|
|
function callPendingCbs (c) {
|
|
|
/* istanbul ignore if */
|
|
|
if (c.elm._moveCb) { // 移动回调
|
|
|
c.elm._moveCb();
|
|
|
}
|
|
|
/* istanbul ignore if */
|
|
|
if (c.elm._enterCb) { // 进入回调
|
|
|
c.elm._enterCb();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 记录元素当前位置
|
|
|
function recordPosition (c) {
|
|
|
c.data.newPos = c.elm.getBoundingClientRect(); // 获取新位置
|
|
|
}
|
|
|
|
|
|
// 应用位置变换
|
|
|
function applyTranslation (c) {
|
|
|
var oldPos = c.data.pos; // 旧位置
|
|
|
var newPos = c.data.newPos; // 新位置
|
|
|
var dx = oldPos.left - newPos.left; // X 轴位移
|
|
|
var dy = oldPos.top - newPos.top; // Y 轴位移
|
|
|
if (dx || dy) { // 需要位移时
|
|
|
c.data.moved = true; // 标记需要移动
|
|
|
var s = c.elm.style;
|
|
|
s.transform = s.WebkitTransform = "translate(" + dx + "px," + dy + "px)"; // 应用变换
|
|
|
s.transitionDuration = '0s'; // 立即生效
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 注册平台组件
|
|
|
var platformComponents = {
|
|
|
Transition: Transition,
|
|
|
TransitionGroup: TransitionGroup
|
|
|
};
|
|
|
|
|
|
/* 平台相关安装代码 */
|
|
|
|
|
|
// 安装平台特定的配置方法
|
|
|
Vue.config.mustUseProp = mustUseProp; // 必须用 prop 绑定的属性
|
|
|
Vue.config.isReservedTag = isReservedTag; // 保留标签检测
|
|
|
Vue.config.isReservedAttr = isReservedAttr; // 保留属性检测
|
|
|
Vue.config.getTagNamespace = getTagNamespace; // 获取标签命名空间
|
|
|
Vue.config.isUnknownElement = isUnknownElement; // 未知元素检测
|
|
|
|
|
|
// 安装平台指令和组件
|
|
|
extend(Vue.options.directives, platformDirectives); // 合并指令
|
|
|
extend(Vue.options.components, platformComponents); // 合并组件
|
|
|
|
|
|
// 安装 DOM patch 方法(浏览器环境)
|
|
|
Vue.prototype.__patch__ = inBrowser ? patch : noop;
|
|
|
|
|
|
// 公共挂载方法
|
|
|
Vue.prototype.$mount = function (
|
|
|
el,
|
|
|
hydrating
|
|
|
) {
|
|
|
el = el && inBrowser ? query(el) : undefined; // 标准化元素查询
|
|
|
return mountComponent(this, el, hydrating) // 执行挂载
|
|
|
};
|
|
|
|
|
|
// 开发工具初始化
|
|
|
/* istanbul ignore next */
|
|
|
if (inBrowser) { // 浏览器环境下
|
|
|
setTimeout(function () { // 延迟执行
|
|
|
if (config.devtools) { // 开发工具启用
|
|
|
if (devtools) { // 已安装开发工具
|
|
|
devtools.emit('init', Vue); // 发送初始化事件
|
|
|
} else { // 未安装提示
|
|
|
console[console.info ? 'info' : 'log'](
|
|
|
'下载 Vue Devtools 扩展以获得更好的开发体验:\n' +
|
|
|
'https://github.com/vuejs/vue-devtools'
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
// 生产环境提示
|
|
|
if (config.productionTip !== false &&
|
|
|
typeof console !== 'undefined'
|
|
|
) {
|
|
|
console[console.info ? 'info' : 'log'](
|
|
|
"您正在运行开发模式的 Vue\n" +
|
|
|
"部署生产环境时请确保开启生产模式\n" +
|
|
|
"更多提示请访问 https://vuejs.org/guide/deployment.html"
|
|
|
);
|
|
|
}
|
|
|
}, 0);
|
|
|
}
|
|
|
// 匹配双花括号插值表达式的正则(如{{content}})
|
|
|
var defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
|
|
|
|
|
|
// 匹配需要转义的正则特殊字符的正则
|
|
|
var regexEscapeRE = /[-.*+?^${}()|[\]\/\\]/g;
|
|
|
|
|
|
// 创建带缓存功能的正则生成器(根据自定义分隔符生成对应正则)
|
|
|
var buildRegex = cached(function (delimiters) {
|
|
|
// 转义起始分隔符中的特殊字符
|
|
|
var open = delimiters[0].replace(regexEscapeRE, '\\$&');
|
|
|
// 转义结束分隔符中的特殊字符
|
|
|
var close = delimiters[1].replace(regexEscapeRE, '\\$&');
|
|
|
// 构造新的动态正则表达式
|
|
|
return new RegExp(open + '((?:.|\\n)+?)' + close, 'g')
|
|
|
});
|
|
|
|
|
|
// 解析模板文本生成表达式和原始标记
|
|
|
function parseText(text, delimiters) {
|
|
|
// 选择使用自定义分隔符正则或默认正则
|
|
|
var tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE;
|
|
|
// 如果没有匹配项则直接返回
|
|
|
if (!tagRE.test(text)) {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 存储处理后的表达式片段
|
|
|
var tokens = [];
|
|
|
// 存储原始文本片段
|
|
|
var rawTokens = [];
|
|
|
// 初始化正则匹配位置
|
|
|
var lastIndex = tagRE.lastIndex = 0;
|
|
|
var match, index, tokenValue;
|
|
|
|
|
|
// 循环处理所有匹配项
|
|
|
while ((match = tagRE.exec(text))) {
|
|
|
index = match.index;
|
|
|
// 处理匹配前的普通文本
|
|
|
if (index > lastIndex) {
|
|
|
rawTokens.push(tokenValue = text.slice(lastIndex, index));
|
|
|
tokens.push(JSON.stringify(tokenValue));
|
|
|
}
|
|
|
// 处理插值表达式
|
|
|
var exp = parseFilters(match[1].trim());
|
|
|
tokens.push(("_s(" + exp + ")"));
|
|
|
rawTokens.push({ '@binding': exp });
|
|
|
// 更新最后匹配位置
|
|
|
lastIndex = index + match[0].length;
|
|
|
}
|
|
|
// 处理末尾剩余文本
|
|
|
if (lastIndex < text.length) {
|
|
|
rawTokens.push(tokenValue = text.slice(lastIndex));
|
|
|
tokens.push(JSON.stringify(tokenValue));
|
|
|
}
|
|
|
// 返回拼接后的表达式和原始标记
|
|
|
return {
|
|
|
expression: tokens.join('+'),
|
|
|
tokens: rawTokens
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/* 类名处理模块开始 */
|
|
|
|
|
|
// 转换节点中的class属性
|
|
|
function transformNode(el, options) {
|
|
|
// 获取警告方法
|
|
|
var warn = options.warn || baseWarn;
|
|
|
// 提取并移除静态class属性
|
|
|
var staticClass = getAndRemoveAttr(el, 'class');
|
|
|
|
|
|
// 处理静态class属性
|
|
|
if (staticClass) {
|
|
|
// 检查是否包含插值语法
|
|
|
var res = parseText(staticClass, options.delimiters);
|
|
|
if (res) {
|
|
|
// 发出过时语法警告
|
|
|
warn(
|
|
|
"class=\"" + staticClass + "\": " +
|
|
|
'Interpolation inside attributes has been removed. ' +
|
|
|
'Use v-bind or the colon shorthand instead. For example, ' +
|
|
|
'instead of <div class="{{ val }}">, use <div :class="val">.',
|
|
|
el.rawAttrsMap['class']
|
|
|
);
|
|
|
}
|
|
|
// 序列化静态class
|
|
|
el.staticClass = JSON.stringify(staticClass);
|
|
|
}
|
|
|
// 获取动态class绑定
|
|
|
var classBinding = getBindingAttr(el, 'class', false /* 不获取静态值 */);
|
|
|
if (classBinding) {
|
|
|
el.classBinding = classBinding;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成class相关数据字符串
|
|
|
function genData(el) {
|
|
|
var data = '';
|
|
|
if (el.staticClass) {
|
|
|
data += "staticClass:" + (el.staticClass) + ",";
|
|
|
}
|
|
|
if (el.classBinding) {
|
|
|
data += "class:" + (el.classBinding) + ",";
|
|
|
}
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 类名处理模块配置
|
|
|
var klass$1 = {
|
|
|
staticKeys: ['staticClass'], // 静态属性键名
|
|
|
transformNode: transformNode, // 节点转换方法
|
|
|
genData: genData // 数据生成方法
|
|
|
};
|
|
|
|
|
|
/* 样式处理模块开始 */
|
|
|
|
|
|
// 转换节点中的style属性
|
|
|
function transformNode$1(el, options) {
|
|
|
var warn = options.warn || baseWarn;
|
|
|
// 提取并移除静态style属性
|
|
|
var staticStyle = getAndRemoveAttr(el, 'style');
|
|
|
|
|
|
if (staticStyle) {
|
|
|
// 忽略测试覆盖的代码块
|
|
|
/* istanbul ignore if */
|
|
|
{
|
|
|
// 检查是否包含插值语法
|
|
|
var res = parseText(staticStyle, options.delimiters);
|
|
|
if (res) {
|
|
|
// 发出过时语法警告
|
|
|
warn(
|
|
|
"style=\"" + staticStyle + "\": " +
|
|
|
'Interpolation inside attributes has been removed. ' +
|
|
|
'Use v-bind or the colon shorthand instead. For example, ' +
|
|
|
'instead of <div style="{{ val }}">, use <div :style="val">.',
|
|
|
el.rawAttrsMap['style']
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
// 解析并序列化静态样式
|
|
|
el.staticStyle = JSON.stringify(parseStyleText(staticStyle));
|
|
|
}
|
|
|
// 获取动态style绑定
|
|
|
var styleBinding = getBindingAttr(el, 'style', false /* 不获取静态值 */);
|
|
|
if (styleBinding) {
|
|
|
el.styleBinding = styleBinding;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成style相关数据字符串
|
|
|
function genData$1(el) {
|
|
|
var data = '';
|
|
|
if (el.staticStyle) {
|
|
|
data += "staticStyle:" + (el.staticStyle) + ",";
|
|
|
}
|
|
|
if (el.styleBinding) {
|
|
|
data += "style:(" + (el.styleBinding) + "),";
|
|
|
}
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 样式处理模块配置
|
|
|
var style$1 = {
|
|
|
staticKeys: ['staticStyle'], // 静态属性键名
|
|
|
transformNode: transformNode$1, // 节点转换方法
|
|
|
genData: genData$1 // 数据生成方法
|
|
|
};
|
|
|
|
|
|
/* HTML解码模块开始 */
|
|
|
|
|
|
var decoder; // 用于HTML解码的临时元素
|
|
|
|
|
|
// HTML实体解码工具
|
|
|
var he = {
|
|
|
decode: function decode(html) {
|
|
|
// 惰性创建div元素
|
|
|
decoder = decoder || document.createElement('div');
|
|
|
// 通过innerHTML实现解码
|
|
|
decoder.innerHTML = html;
|
|
|
// 返回解码后的文本内容
|
|
|
return decoder.textContent
|
|
|
}
|
|
|
};
|
|
|
|
|
|
/* 标签类型判断模块开始 */
|
|
|
|
|
|
// 自闭合标签列表(如<img/>)
|
|
|
var isUnaryTag = makeMap(
|
|
|
'area,base,br,col,embed,frame,hr,img,input,isindex,keygen,' +
|
|
|
'link,meta,param,source,track,wbr'
|
|
|
);
|
|
|
|
|
|
// 可省略闭合标签列表(如<li>)
|
|
|
var canBeLeftOpenTag = makeMap(
|
|
|
'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source'
|
|
|
);
|
|
|
|
|
|
// 非短语内容标签列表(不能包含文本内容)
|
|
|
var isNonPhrasingTag = makeMap(
|
|
|
'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' +
|
|
|
'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' +
|
|
|
'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' +
|
|
|
'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' +
|
|
|
'title,tr,track'
|
|
|
);
|
|
|
|
|
|
/* HTML解析正则模块开始 */
|
|
|
|
|
|
// 属性解析正则(捕获属性名和值)
|
|
|
var attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
|
|
|
|
|
|
// 动态参数属性正则(如:[key])
|
|
|
var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
|
|
|
|
|
|
// XML名称正则(标签名规范)
|
|
|
var ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
|
|
|
|
|
|
// 限定名正则(带命名空间的标签名)
|
|
|
var qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
|
|
|
|
|
|
// 开始标签开头匹配正则(如<div>)
|
|
|
var startTagOpen = new RegExp(("^<" + qnameCapture));
|
|
|
|
|
|
// 开始标签结尾匹配正则(捕获闭合符)
|
|
|
var startTagClose = /^\s*(\/?)>/;
|
|
|
|
|
|
// 结束标签匹配正则(如</div>)
|
|
|
var endTag = new RegExp(("^<\\/" + qnameCapture + "[^>]*>"));
|
|
|
|
|
|
// 文档类型声明匹配正则
|
|
|
var doctype = /^<!DOCTYPE [^>]+>/i;
|
|
|
|
|
|
// HTML注释匹配正则(兼容性处理)
|
|
|
var comment = /^<!\--/;
|
|
|
|
|
|
// 条件注释匹配正则(IE特性)
|
|
|
var conditionalComment = /^<!\[/;
|
|
|
|
|
|
// 纯文本元素列表(script/style/textarea)
|
|
|
var isPlainTextElement = makeMap('script,style,textarea', true);
|
|
|
|
|
|
// 正则表达式缓存对象
|
|
|
var reCache = {};
|
|
|
|
|
|
// HTML实体解码映射表
|
|
|
var decodingMap = {
|
|
|
'<': '<', // 小于号
|
|
|
'>': '>', // 大于号
|
|
|
'"': '"', // 双引号
|
|
|
'&': '&', // 与符号
|
|
|
' ': '\n', // 换行符
|
|
|
'	': '\t', // 制表符
|
|
|
''': "'" // 单引号
|
|
|
};
|
|
|
// 匹配常规HTML实体编码的正则表达式(不包含换行相关实体)
|
|
|
var encodedAttr = /&(?:lt|gt|quot|amp|#39);/g;
|
|
|
|
|
|
// 匹配包含换行符的HTML实体编码的正则表达式
|
|
|
var encodedAttrWithNewLines = /&(?:lt|gt|quot|amp|#39|#10|#9);/g;
|
|
|
|
|
|
// 创建需要忽略首行换行的标签映射(pre和textarea)
|
|
|
var isIgnoreNewlineTag = makeMap('pre,textarea', true);
|
|
|
|
|
|
// 判断是否需要忽略标签首行换行符的函数
|
|
|
var shouldIgnoreFirstNewline = function (tag, html) { return tag && isIgnoreNewlineTag(tag) && html[0] === '\n'; };
|
|
|
|
|
|
// HTML属性值解码函数
|
|
|
function decodeAttr (value, shouldDecodeNewlines) {
|
|
|
// 根据需求选择匹配规则
|
|
|
var re = shouldDecodeNewlines ? encodedAttrWithNewLines : encodedAttr;
|
|
|
// 替换实体编码为对应字符
|
|
|
return value.replace(re, function (match) { return decodingMap[match]; })
|
|
|
}
|
|
|
|
|
|
// HTML解析器核心函数
|
|
|
function parseHTML (html, options) {
|
|
|
var stack = []; // 标签堆栈用于追踪嵌套关系
|
|
|
var expectHTML = options.expectHTML; // 是否期望标准HTML结构
|
|
|
var isUnaryTag$$1 = options.isUnaryTag || no; // 自闭合标签检查函数
|
|
|
var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; // 可省略闭合标签检查函数
|
|
|
var index = 0; // 当前解析位置索引
|
|
|
var last, lastTag; // 上次处理的HTML内容和标签名
|
|
|
|
|
|
// 主解析循环
|
|
|
while (html) {
|
|
|
last = html;
|
|
|
// 检查是否处于纯文本元素中(script/style/textarea)
|
|
|
if (!lastTag || !isPlainTextElement(lastTag)) {
|
|
|
// 查找最近标签起始位置
|
|
|
var textEnd = html.indexOf('<');
|
|
|
|
|
|
// 处理以'<'开头的内容(标签或注释)
|
|
|
if (textEnd === 0) {
|
|
|
// 处理HTML注释 <!-- -->
|
|
|
if (comment.test(html)) {
|
|
|
var commentEnd = html.indexOf('-->');
|
|
|
if (commentEnd >= 0) {
|
|
|
// 保留注释内容的回调处理
|
|
|
if (options.shouldKeepComment) {
|
|
|
options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
|
|
|
}
|
|
|
// 跳过注释部分
|
|
|
advance(commentEnd + 3);
|
|
|
continue
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理IE条件注释 <![if IE]>
|
|
|
if (conditionalComment.test(html)) {
|
|
|
var conditionalEnd = html.indexOf(']>');
|
|
|
if (conditionalEnd >= 0) {
|
|
|
advance(conditionalEnd + 2);
|
|
|
continue
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理DOCTYPE声明
|
|
|
var doctypeMatch = html.match(doctype);
|
|
|
if (doctypeMatch) {
|
|
|
advance(doctypeMatch[0].length);
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
// 处理结束标签 </div>
|
|
|
var endTagMatch = html.match(endTag);
|
|
|
if (endTagMatch) {
|
|
|
var curIndex = index;
|
|
|
advance(endTagMatch[0].length);
|
|
|
parseEndTag(endTagMatch[1], curIndex, index);
|
|
|
continue
|
|
|
}
|
|
|
|
|
|
// 处理开始标签 <div>
|
|
|
var startTagMatch = parseStartTag();
|
|
|
if (startTagMatch) {
|
|
|
handleStartTag(startTagMatch);
|
|
|
// 处理pre/textarea标签的首行换行
|
|
|
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
|
|
|
advance(1);
|
|
|
}
|
|
|
continue
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理文本内容
|
|
|
var text = (void 0), rest = (void 0), next = (void 0);
|
|
|
if (textEnd >= 0) {
|
|
|
rest = html.slice(textEnd);
|
|
|
// 扫描文本中的特殊字符
|
|
|
while (
|
|
|
!endTag.test(rest) &&
|
|
|
!startTagOpen.test(rest) &&
|
|
|
!comment.test(rest) &&
|
|
|
!conditionalComment.test(rest)
|
|
|
) {
|
|
|
// 处理文本中的'<'字符
|
|
|
next = rest.indexOf('<', 1);
|
|
|
if (next < 0) { break }
|
|
|
textEnd += next;
|
|
|
rest = html.slice(textEnd);
|
|
|
}
|
|
|
text = html.substring(0, textEnd);
|
|
|
}
|
|
|
|
|
|
// 处理剩余全部文本
|
|
|
if (textEnd < 0) {
|
|
|
text = html;
|
|
|
}
|
|
|
|
|
|
// 推进解析位置并处理文本内容
|
|
|
if (text) {
|
|
|
advance(text.length);
|
|
|
if (options.chars && text) {
|
|
|
options.chars(text, index - text.length, index);
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// 处理纯文本元素内部内容
|
|
|
var endTagLength = 0;
|
|
|
var stackedTag = lastTag.toLowerCase();
|
|
|
// 创建匹配结束标签的正则
|
|
|
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
|
|
|
// 提取纯文本内容
|
|
|
var rest$1 = html.replace(reStackedTag, function (all, text, endTag) {
|
|
|
endTagLength = endTag.length;
|
|
|
// 清理注释和CDATA
|
|
|
if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') {
|
|
|
text = text
|
|
|
.replace(/<!\--([\s\\S]*?)-->/g, '$1')
|
|
|
.replace(/<!\[CDATA\[([\s\\S]*?)]]>/g, '$1');
|
|
|
}
|
|
|
// 处理首行换行符
|
|
|
if (shouldIgnoreFirstNewline(stackedTag, text)) {
|
|
|
text = text.slice(1);
|
|
|
}
|
|
|
if (options.chars) {
|
|
|
options.chars(text);
|
|
|
}
|
|
|
return ''
|
|
|
});
|
|
|
// 更新解析位置
|
|
|
index += html.length - rest$1.length;
|
|
|
html = rest$1;
|
|
|
// 处理结束标签
|
|
|
parseEndTag(stackedTag, index - endTagLength, index);
|
|
|
}
|
|
|
|
|
|
// 错误处理:检测是否进入死循环
|
|
|
if (html === last) {
|
|
|
options.chars && options.chars(html);
|
|
|
if (!stack.length && options.warn) {
|
|
|
options.warn(("Mal-formatted tag at end of template: \"" + html + "\""), { start: index + html.length });
|
|
|
}
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 清理未闭合标签
|
|
|
parseEndTag();
|
|
|
|
|
|
// 推进解析位置的工具函数
|
|
|
function advance (n) {
|
|
|
index += n;
|
|
|
html = html.substring(n);
|
|
|
}
|
|
|
|
|
|
// 解析开始标签的函数
|
|
|
function parseStartTag () {
|
|
|
var start = html.match(startTagOpen);
|
|
|
if (start) {
|
|
|
var match = {
|
|
|
tagName: start[1], // 标签名
|
|
|
attrs: [], // 属性列表
|
|
|
start: index // 起始位置
|
|
|
};
|
|
|
advance(start[0].length);
|
|
|
var end, attr;
|
|
|
// 循环收集属性
|
|
|
while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute)) {
|
|
|
attr.start = index;
|
|
|
advance(attr[0].length);
|
|
|
attr.end = index;
|
|
|
match.attrs.push(attr); // 添加属性到列表
|
|
|
}
|
|
|
if (end) {
|
|
|
match.unarySlash = end[1]; // 自闭合标记
|
|
|
advance(end[0].length);
|
|
|
match.end = index;
|
|
|
return match
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理开始标签结果的函数
|
|
|
function handleStartTag (match) {
|
|
|
var tagName = match.tagName;
|
|
|
var unarySlash = match.unarySlash;
|
|
|
|
|
|
// 处理HTML预期行为
|
|
|
if (expectHTML) {
|
|
|
// 自动闭合p标签
|
|
|
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
|
|
|
parseEndTag(lastTag);
|
|
|
}
|
|
|
// 处理可省略闭合标签
|
|
|
if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {
|
|
|
parseEndTag(tagName);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 判断是否自闭合标签
|
|
|
var unary = isUnaryTag$$1(tagName) || !!unarySlash;
|
|
|
|
|
|
// 处理标签属性
|
|
|
var l = match.attrs.length;
|
|
|
var attrs = new Array(l);
|
|
|
for (var i = 0; i < l; i++) {
|
|
|
var args = match.attrs[i];
|
|
|
// 获取属性值(支持多种引号格式)
|
|
|
var value = args[3] || args[4] || args[5] || '';
|
|
|
// 判断是否解码换行符(特别处理a标签的href)
|
|
|
var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href'
|
|
|
? options.shouldDecodeNewlinesForHref
|
|
|
: options.shouldDecodeNewlines;
|
|
|
// 构建属性对象
|
|
|
attrs[i] = {
|
|
|
name: args[1],
|
|
|
value: decodeAttr(value, shouldDecodeNewlines)
|
|
|
};
|
|
|
// 记录源码位置信息
|
|
|
if (options.outputSourceRange) {
|
|
|
attrs[i].start = args.start + args[0].match(/^\s*/).length;
|
|
|
attrs[i].end = args.end;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 非自闭合标签入栈
|
|
|
if (!unary) {
|
|
|
stack.push({
|
|
|
tag: tagName,
|
|
|
lowerCasedTag: tagName.toLowerCase(),
|
|
|
attrs: attrs,
|
|
|
start: match.start,
|
|
|
end: match.end
|
|
|
});
|
|
|
lastTag = tagName;
|
|
|
}
|
|
|
|
|
|
// 触发start回调
|
|
|
if (options.start) {
|
|
|
options.start(tagName, attrs, unary, match.start, match.end);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理结束标签的函数
|
|
|
function parseEndTag (tagName, start, end) {
|
|
|
var pos, lowerCasedTagName;
|
|
|
// 处理参数默认值
|
|
|
if (start == null) { start = index; }
|
|
|
if (end == null) { end = index; }
|
|
|
|
|
|
// 查找匹配的起始标签
|
|
|
if (tagName) {
|
|
|
lowerCasedTagName = tagName.toLowerCase();
|
|
|
// 从栈顶向下查找
|
|
|
for (pos = stack.length - 1; pos >= 0; pos--) {
|
|
|
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
|
|
|
break
|
|
|
}
|
|
|
}
|
|
|
} else {
|
|
|
// 未指定标签名时清空堆栈
|
|
|
pos = 0;
|
|
|
}
|
|
|
|
|
|
if (pos >= 0) {
|
|
|
// 闭合所有未匹配标签
|
|
|
for (var i = stack.length - 1; i >= pos; i--) {
|
|
|
// 警告未匹配的标签
|
|
|
if (i > pos || !tagName && options.warn) {
|
|
|
options.warn(
|
|
|
("tag <" + (stack[i].tag) + "> has no matching end tag."),
|
|
|
{ start: stack[i].start, end: stack[i].end }
|
|
|
);
|
|
|
}
|
|
|
// 触发end回调
|
|
|
if (options.end) {
|
|
|
options.end(stack[i].tag, start, end);
|
|
|
}
|
|
|
}
|
|
|
// 更新堆栈
|
|
|
stack.length = pos;
|
|
|
lastTag = pos && stack[pos - 1].tag;
|
|
|
} else if (lowerCasedTagName === 'br') {
|
|
|
// 特殊处理<br>标签
|
|
|
if (options.start) {
|
|
|
options.start(tagName, [], true, start, end);
|
|
|
}
|
|
|
} else if (lowerCasedTagName === 'p') {
|
|
|
// 特殊处理<p>标签
|
|
|
if (options.start) {
|
|
|
options.start(tagName, [], false, start, end);
|
|
|
}
|
|
|
if (options.end) {
|
|
|
options.end(tagName, start, end);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 匹配事件绑定语法(@或v-on:开头)
|
|
|
var onRE = /^@|^v-on:/;
|
|
|
// 匹配所有Vue指令语法(v-、@、:、#开头)
|
|
|
var dirRE = /^v-|^@|^:|^#/;
|
|
|
// 解析v-for表达式的正则(匹配'item in list'结构)
|
|
|
var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
|
|
|
// 解析v-for迭代器的正则(匹配', index'等参数)
|
|
|
var forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
|
|
|
// 去除括号的正则(用于处理表达式)
|
|
|
var stripParensRE = /^\(|\)$/g;
|
|
|
// 匹配动态参数(如:[key])
|
|
|
var dynamicArgRE = /^\[.*\]$/;
|
|
|
|
|
|
// 匹配指令参数部分(:后的内容)
|
|
|
var argRE = /:(.*)$/;
|
|
|
// 匹配绑定语法(:、.、v-bind:开头)
|
|
|
var bindRE = /^:|^\.|^v-bind:/;
|
|
|
// 匹配修饰符的正则(.stop等)
|
|
|
var modifierRE = /\.[^.\]]+(?=[^\]]*$)/g;
|
|
|
|
|
|
// 匹配插槽语法(v-slot或#开头)
|
|
|
var slotRE = /^v-slot(:|$)|^#/;
|
|
|
|
|
|
// 匹配换行符
|
|
|
var lineBreakRE = /[\r\n]/;
|
|
|
// 匹配空白字符
|
|
|
var whitespaceRE$1 = /\s+/g;
|
|
|
|
|
|
// 匹配非法属性字符
|
|
|
var invalidAttributeRE = /[\s"'<>\/=]/;
|
|
|
|
|
|
// 缓存HTML解码函数
|
|
|
var decodeHTMLCached = cached(he.decode);
|
|
|
|
|
|
// 空插槽作用域标识
|
|
|
var emptySlotScopeToken = "_empty_";
|
|
|
|
|
|
// 可配置状态变量
|
|
|
var warn$2; // 警告函数
|
|
|
var delimiters; // 插值分隔符
|
|
|
var transforms; // 转换函数集合
|
|
|
var preTransforms; // 预处理函数集合
|
|
|
var postTransforms; // 后处理函数集合
|
|
|
var platformIsPreTag;// 判断pre标签的方法
|
|
|
var platformMustUseProp; // 判断必须用prop绑定的属性
|
|
|
var platformGetTagNamespace; // 获取标签命名空间
|
|
|
var maybeComponent; // 判断可能是组件的方法
|
|
|
|
|
|
// 创建AST元素节点
|
|
|
function createASTElement(tag, attrs, parent) {
|
|
|
return {
|
|
|
type: 1, // 节点类型为元素
|
|
|
tag: tag, // 标签名
|
|
|
attrsList: attrs, // 属性列表
|
|
|
attrsMap: makeAttrsMap(attrs), // 属性映射表
|
|
|
rawAttrsMap: {}, // 原始属性映射
|
|
|
parent: parent, // 父节点
|
|
|
children: [] // 子节点数组
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将HTML字符串转换为AST的主函数
|
|
|
function parse(template, options) {
|
|
|
// 初始化警告函数
|
|
|
warn$2 = options.warn || baseWarn;
|
|
|
|
|
|
// 初始化平台相关方法
|
|
|
platformIsPreTag = options.isPreTag || no;
|
|
|
platformMustUseProp = options.mustUseProp || no;
|
|
|
platformGetTagNamespace = options.getTagNamespace || no;
|
|
|
|
|
|
// 判断保留标签的方法
|
|
|
var isReservedTag = options.isReservedTag || no;
|
|
|
// 判断可能是组件的方法
|
|
|
maybeComponent = function (el) { return !!el.component || !isReservedTag(el.tag); };
|
|
|
|
|
|
// 从模块中提取转换函数
|
|
|
transforms = pluckModuleFunction(options.modules, 'transformNode');
|
|
|
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
|
|
|
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
|
|
|
|
|
|
// 设置分隔符
|
|
|
delimiters = options.delimiters;
|
|
|
|
|
|
// 初始化解析状态
|
|
|
var stack = []; // 元素节点栈
|
|
|
var preserveWhitespace = options.preserveWhitespace !== false; // 是否保留空白
|
|
|
var whitespaceOption = options.whitespace; // 空白处理选项
|
|
|
var root; // 根节点
|
|
|
var currentParent; // 当前父节点
|
|
|
var inVPre = false; // 是否在v-pre环境中
|
|
|
var inPre = false; // 是否在pre标签内
|
|
|
var warned = false; // 警告状态标记
|
|
|
|
|
|
// 单次警告函数
|
|
|
function warnOnce(msg, range) {
|
|
|
if (!warned) {
|
|
|
warned = true;
|
|
|
warn$2(msg, range);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 闭合元素处理
|
|
|
function closeElement(element) {
|
|
|
// 清理结尾空白
|
|
|
trimEndingWhitespace(element);
|
|
|
// 处理非v-pre环境的元素
|
|
|
if (!inVPre && !element.processed) {
|
|
|
element = processElement(element, options);
|
|
|
}
|
|
|
|
|
|
// 树结构管理
|
|
|
if (!stack.length && element !== root) {
|
|
|
// 处理根元素的条件判断
|
|
|
if (root.if && (element.elseif || element.else)) {
|
|
|
checkRootConstraints(element);
|
|
|
addIfCondition(root, {
|
|
|
exp: element.elseif,
|
|
|
block: element
|
|
|
});
|
|
|
} else {
|
|
|
warnOnce(
|
|
|
"模板应包含单个根元素,使用v-else-if连接多个元素",
|
|
|
{ start: element.start }
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将元素添加到父节点
|
|
|
if (currentParent && !element.forbidden) {
|
|
|
if (element.elseif || element.else) {
|
|
|
processIfConditions(element, currentParent);
|
|
|
} else {
|
|
|
// 处理作用域插槽
|
|
|
if (element.slotScope) {
|
|
|
var name = element.slotTarget || '"default"';
|
|
|
(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
|
|
|
}
|
|
|
currentParent.children.push(element);
|
|
|
element.parent = currentParent;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 清理子节点
|
|
|
element.children = element.children.filter(function (c) { return !(c).slotScope; });
|
|
|
trimEndingWhitespace(element);
|
|
|
|
|
|
// 重置预处理状态
|
|
|
if (element.pre) inVPre = false;
|
|
|
if (platformIsPreTag(element.tag)) inPre = false;
|
|
|
|
|
|
// 执行后处理
|
|
|
for (var i = 0; i < postTransforms.length; i++) {
|
|
|
postTransforms[i](element, options);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 清理结尾空白节点
|
|
|
function trimEndingWhitespace(el) {
|
|
|
if (!inPre) {
|
|
|
var lastNode;
|
|
|
while ((lastNode = el.children[el.children.length - 1]) &&
|
|
|
lastNode.type === 3 &&
|
|
|
lastNode.text === ' ') {
|
|
|
el.children.pop();
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查根元素约束
|
|
|
function checkRootConstraints(el) {
|
|
|
if (el.tag === 'slot' || el.tag === 'template') {
|
|
|
warnOnce("禁止使用slot/template作为根元素", { start: el.start });
|
|
|
}
|
|
|
if (el.attrsMap.hasOwnProperty('v-for')) {
|
|
|
warnOnce("根元素不能使用v-for", el.rawAttrsMap['v-for']);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 调用HTML解析器
|
|
|
parseHTML(template, {
|
|
|
warn: warn$2,
|
|
|
expectHTML: options.expectHTML,
|
|
|
isUnaryTag: options.isUnaryTag,
|
|
|
canBeLeftOpenTag: options.canBeLeftOpenTag,
|
|
|
shouldDecodeNewlines: options.shouldDecodeNewlines,
|
|
|
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
|
|
|
shouldKeepComment: options.comments,
|
|
|
outputSourceRange: options.outputSourceRange,
|
|
|
|
|
|
// 开始标签回调
|
|
|
start: function start(tag, attrs, unary, start$1, end) {
|
|
|
// 处理命名空间
|
|
|
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag);
|
|
|
|
|
|
// 处理IE SVG问题
|
|
|
if (isIE && ns === 'svg') attrs = guardIESVGBug(attrs);
|
|
|
|
|
|
// 创建AST元素
|
|
|
var element = createASTElement(tag, attrs, currentParent);
|
|
|
if (ns) element.ns = ns;
|
|
|
|
|
|
// 处理源码范围
|
|
|
if (options.outputSourceRange) {
|
|
|
element.start = start$1;
|
|
|
element.end = end;
|
|
|
element.rawAttrsMap = element.attrsList.reduce(function (cumulated, attr) {
|
|
|
cumulated[attr.name] = attr;
|
|
|
return cumulated
|
|
|
}, {});
|
|
|
}
|
|
|
|
|
|
// 检查非法属性
|
|
|
attrs.forEach(function (attr) {
|
|
|
if (invalidAttributeRE.test(attr.name)) {
|
|
|
warn$2("动态参数包含非法字符", {
|
|
|
start: attr.start + attr.name.indexOf("["),
|
|
|
end: attr.start + attr.name.length
|
|
|
});
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 检查禁用标签
|
|
|
if (isForbiddenTag(element) && !isServerRendering()) {
|
|
|
element.forbidden = true;
|
|
|
warn$2("模板包含禁用标签", { start: element.start });
|
|
|
}
|
|
|
|
|
|
// 执行预处理
|
|
|
for (var i = 0; i < preTransforms.length; i++) {
|
|
|
element = preTransforms[i](element, options) || element;
|
|
|
}
|
|
|
|
|
|
// 处理v-pre指令
|
|
|
if (!inVPre) {
|
|
|
processPre(element);
|
|
|
if (element.pre) inVPre = true;
|
|
|
}
|
|
|
|
|
|
// 处理pre标签状态
|
|
|
if (platformIsPreTag(element.tag)) inPre = true;
|
|
|
|
|
|
// 处理原始属性或指令
|
|
|
if (inVPre) {
|
|
|
processRawAttrs(element);
|
|
|
} else if (!element.processed) {
|
|
|
processFor(element); // 处理v-for
|
|
|
processIf(element); // 处理v-if
|
|
|
processOnce(element); // 处理v-once
|
|
|
}
|
|
|
|
|
|
// 设置根元素
|
|
|
if (!root) {
|
|
|
root = element;
|
|
|
checkRootConstraints(root);
|
|
|
}
|
|
|
|
|
|
// 管理节点栈
|
|
|
if (!unary) {
|
|
|
currentParent = element;
|
|
|
stack.push(element);
|
|
|
} else {
|
|
|
closeElement(element);
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 结束标签回调
|
|
|
end: function end(tag, start, end$1) {
|
|
|
var element = stack[stack.length - 1];
|
|
|
stack.length -= 1;
|
|
|
currentParent = stack[stack.length - 1];
|
|
|
if (options.outputSourceRange) element.end = end$1;
|
|
|
closeElement(element);
|
|
|
},
|
|
|
|
|
|
// 文本内容回调
|
|
|
chars: function chars(text, start, end) {
|
|
|
if (!currentParent) {
|
|
|
// 处理游离文本的警告
|
|
|
if (text.trim()) warnOnce("根元素外的文本将被忽略", { start: start });
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// 处理IE textarea placeholder问题
|
|
|
if (isIE && currentParent.tag === 'textarea' && currentParent.attrsMap.placeholder === text) return
|
|
|
|
|
|
var children = currentParent.children;
|
|
|
// 处理文本内容
|
|
|
text = inPre || text.trim()
|
|
|
? (isTextTag(currentParent) ? text : decodeHTMLCached(text))
|
|
|
: whitespaceOption === 'condense'
|
|
|
? (lineBreakRE.test(text) ? '' : ' ')
|
|
|
: preserveWhitespace ? ' ' : '';
|
|
|
|
|
|
if (text) {
|
|
|
// 压缩连续空白
|
|
|
if (!inPre && whitespaceOption === 'condense') {
|
|
|
text = text.replace(whitespaceRE$1, ' ');
|
|
|
}
|
|
|
|
|
|
// 创建文本节点
|
|
|
var child;
|
|
|
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
|
|
|
child = { type: 2, expression: res.expression, tokens: res.tokens, text: text };
|
|
|
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
|
|
|
child = { type: 3, text: text };
|
|
|
}
|
|
|
|
|
|
if (child) {
|
|
|
if (options.outputSourceRange) {
|
|
|
child.start = start;
|
|
|
child.end = end;
|
|
|
}
|
|
|
children.push(child);
|
|
|
}
|
|
|
}
|
|
|
},
|
|
|
|
|
|
// 注释处理回调
|
|
|
comment: function comment(text, start, end) {
|
|
|
if (currentParent) {
|
|
|
var child = { type: 3, text: text, isComment: true };
|
|
|
if (options.outputSourceRange) {
|
|
|
child.start = start;
|
|
|
child.end = end;
|
|
|
}
|
|
|
currentParent.children.push(child);
|
|
|
}
|
|
|
}
|
|
|
});
|
|
|
|
|
|
return root
|
|
|
}
|
|
|
|
|
|
// 处理v-pre指令
|
|
|
function processPre (el) {
|
|
|
// 检查是否存在v-pre属性
|
|
|
if (getAndRemoveAttr(el, 'v-pre') != null) {
|
|
|
el.pre = true; // 标记元素具有v-pre指令
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理原始属性
|
|
|
function processRawAttrs (el) {
|
|
|
var list = el.attrsList; // 获取属性列表
|
|
|
var len = list.length; // 属性数量
|
|
|
if (len) {
|
|
|
// 创建规范化属性数组
|
|
|
var attrs = el.attrs = new Array(len);
|
|
|
for (var i = 0; i < len; i++) {
|
|
|
// 构建属性对象
|
|
|
attrs[i] = {
|
|
|
name: list[i].name, // 属性名
|
|
|
value: JSON.stringify(list[i].value) // 值序列化
|
|
|
};
|
|
|
// 记录源码位置信息
|
|
|
if (list[i].start != null) {
|
|
|
attrs[i].start = list[i].start;
|
|
|
attrs[i].end = list[i].end;
|
|
|
}
|
|
|
}
|
|
|
} else if (!el.pre) {
|
|
|
// 没有属性的非根节点标记为plain
|
|
|
el.plain = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理元素节点主函数
|
|
|
function processElement (element, options) {
|
|
|
processKey(element); // 处理key属性
|
|
|
|
|
|
// 判断是否为纯元素(无关键属性)
|
|
|
element.plain = (
|
|
|
!element.key && // 无key
|
|
|
!element.scopedSlots && // 无作用域插槽
|
|
|
!element.attrsList.length // 无属性
|
|
|
);
|
|
|
|
|
|
processRef(element); // 处理ref属性
|
|
|
processSlotContent(element);// 处理插槽内容
|
|
|
processSlotOutlet(element); // 处理插槽出口
|
|
|
processComponent(element); // 处理组件
|
|
|
// 执行转换函数
|
|
|
for (var i = 0; i < transforms.length; i++) {
|
|
|
element = transforms[i](element, options) || element;
|
|
|
}
|
|
|
processAttrs(element); // 处理属性
|
|
|
return element
|
|
|
}
|
|
|
|
|
|
// 处理key属性
|
|
|
function processKey (el) {
|
|
|
var exp = getBindingAttr(el, 'key'); // 获取绑定的key
|
|
|
if (exp) {
|
|
|
// 开发环境警告检查
|
|
|
{
|
|
|
if (el.tag === 'template') {
|
|
|
warn$2( // 禁止template使用key
|
|
|
"<template>不能设置key,请设置在真实元素上",
|
|
|
getRawBindingAttr(el, 'key')
|
|
|
);
|
|
|
}
|
|
|
if (el.for) {
|
|
|
var iterator = el.iterator2 || el.iterator1;
|
|
|
var parent = el.parent;
|
|
|
// 过渡组件的key警告
|
|
|
if (iterator && iterator === exp && parent && parent.tag === 'transition-group') {
|
|
|
warn$2(
|
|
|
"不要在<transition-group>子元素使用索引作为key",
|
|
|
getRawBindingAttr(el, 'key'),
|
|
|
true
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
el.key = exp; // 设置key表达式
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理ref属性
|
|
|
function processRef (el) {
|
|
|
var ref = getBindingAttr(el, 'ref'); // 获取ref绑定
|
|
|
if (ref) {
|
|
|
el.ref = ref; // 设置ref
|
|
|
el.refInFor = checkInFor(el); // 检查是否在v-for内
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理v-for指令
|
|
|
function processFor (el) {
|
|
|
var exp;
|
|
|
if ((exp = getAndRemoveAttr(el, 'v-for'))) { // 获取并移除v-for属性
|
|
|
var res = parseFor(exp); // 解析表达式
|
|
|
if (res) {
|
|
|
extend(el, res); // 合并解析结果
|
|
|
} else {
|
|
|
warn$2( // 无效表达式警告
|
|
|
"无效的v-for表达式: " + exp,
|
|
|
el.rawAttrsMap['v-for']
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 解析v-for表达式
|
|
|
function parseFor (exp) {
|
|
|
var inMatch = exp.match(forAliasRE); // 匹配in/of语法
|
|
|
if (!inMatch) return // 无效格式直接返回
|
|
|
|
|
|
var res = {};
|
|
|
res.for = inMatch[2].trim(); // 获取列表部分
|
|
|
var alias = inMatch[1].trim().replace(stripParensRE, ''); // 去除括号
|
|
|
|
|
|
// 解析迭代参数
|
|
|
var iteratorMatch = alias.match(forIteratorRE);
|
|
|
if (iteratorMatch) {
|
|
|
res.alias = alias.replace(forIteratorRE, '').trim(); // 主迭代变量
|
|
|
res.iterator1 = iteratorMatch[1].trim(); // 索引参数
|
|
|
if (iteratorMatch[2]) {
|
|
|
res.iterator2 = iteratorMatch[2].trim(); // 第三个参数
|
|
|
}
|
|
|
} else {
|
|
|
res.alias = alias; // 无迭代参数
|
|
|
}
|
|
|
return res
|
|
|
}
|
|
|
|
|
|
// 处理v-if指令
|
|
|
function processIf (el) {
|
|
|
var exp = getAndRemoveAttr(el, 'v-if'); // 获取v-if表达式
|
|
|
if (exp) {
|
|
|
el.if = exp; // 设置条件表达式
|
|
|
addIfCondition(el, { // 添加条件块
|
|
|
exp: exp,
|
|
|
block: el
|
|
|
});
|
|
|
} else {
|
|
|
// 处理v-else
|
|
|
if (getAndRemoveAttr(el, 'v-else') != null) {
|
|
|
el.else = true;
|
|
|
}
|
|
|
// 处理v-else-if
|
|
|
var elseif = getAndRemoveAttr(el, 'v-else-if');
|
|
|
if (elseif) {
|
|
|
el.elseif = elseif;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理条件链关系
|
|
|
function processIfConditions (el, parent) {
|
|
|
var prev = findPrevElement(parent.children); // 查找前一个元素节点
|
|
|
if (prev && prev.if) {
|
|
|
addIfCondition(prev, { // 添加条件关系
|
|
|
exp: el.elseif,
|
|
|
block: el
|
|
|
});
|
|
|
} else {
|
|
|
warn$2( // 条件链错误警告
|
|
|
"v-" + (el.elseif ? 'else-if="' + el.elseif + '"' : 'else') +
|
|
|
" 缺少对应的v-if",
|
|
|
el.rawAttrsMap[el.elseif ? 'v-else-if' : 'v-else']
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 查找前一个元素节点
|
|
|
function findPrevElement (children) {
|
|
|
var i = children.length;
|
|
|
while (i--) {
|
|
|
if (children[i].type === 1) { // 元素节点
|
|
|
return children[i]
|
|
|
} else {
|
|
|
// 处理文本节点警告
|
|
|
if (children[i].text !== ' ') {
|
|
|
warn$2(
|
|
|
"v-if和v-else之间的文本内容将被忽略: " + children[i].text.trim(),
|
|
|
children[i]
|
|
|
);
|
|
|
}
|
|
|
children.pop(); // 移除干扰文本节点
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 添加条件关系
|
|
|
function addIfCondition (el, condition) {
|
|
|
if (!el.ifConditions) {
|
|
|
el.ifConditions = []; // 初始化条件数组
|
|
|
}
|
|
|
el.ifConditions.push(condition); // 添加条件项
|
|
|
}
|
|
|
|
|
|
// 处理v-once指令
|
|
|
function processOnce (el) {
|
|
|
var once$$1 = getAndRemoveAttr(el, 'v-once'); // 获取v-once属性
|
|
|
if (once$$1 != null) {
|
|
|
el.once = true; // 标记只渲染一次
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理插槽内容
|
|
|
function processSlotContent (el) {
|
|
|
var slotScope;
|
|
|
// 处理template插槽
|
|
|
if (el.tag === 'template') {
|
|
|
slotScope = getAndRemoveAttr(el, 'scope'); // 旧语法scope
|
|
|
/* istanbul ignore if */
|
|
|
if (slotScope) {
|
|
|
warn$2( // 废弃语法警告
|
|
|
"scope属性已废弃,请使用slot-scope",
|
|
|
el.rawAttrsMap['scope'],
|
|
|
true
|
|
|
);
|
|
|
}
|
|
|
el.slotScope = slotScope || getAndRemoveAttr(el, 'slot-scope'); // 新语法
|
|
|
} else if ((slotScope = getAndRemoveAttr(el, 'slot-scope'))) {
|
|
|
// 普通元素使用slot-scope警告
|
|
|
/* istanbul ignore if */
|
|
|
if (el.attrsMap['v-for']) {
|
|
|
warn$2(
|
|
|
"同时使用slot-scope和v-for可能导致歧义",
|
|
|
el.rawAttrsMap['slot-scope'],
|
|
|
true
|
|
|
);
|
|
|
}
|
|
|
el.slotScope = slotScope;
|
|
|
}
|
|
|
|
|
|
// 处理slot属性
|
|
|
var slotTarget = getBindingAttr(el, 'slot');
|
|
|
if (slotTarget) {
|
|
|
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget; // 默认插槽处理
|
|
|
el.slotTargetDynamic = !!(el.attrsMap[':slot'] || el.attrsMap['v-bind:slot']); // 动态插槽名
|
|
|
// 保留原生slot属性
|
|
|
if (el.tag !== 'template' && !el.slotScope) {
|
|
|
addAttr(el, 'slot', slotTarget, getRawBindingAttr(el, 'slot'));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理v-slot语法(2.6+)
|
|
|
{
|
|
|
if (el.tag === 'template') {
|
|
|
// template上的v-slot
|
|
|
var slotBinding = getAndRemoveAttrByRegex(el, slotRE);
|
|
|
if (slotBinding) {
|
|
|
{
|
|
|
// 语法混合警告
|
|
|
if (el.slotTarget || el.slotScope) {
|
|
|
warn$2("混用不同插槽语法", el);
|
|
|
}
|
|
|
// 作用域限制检查
|
|
|
if (el.parent && !maybeComponent(el.parent)) {
|
|
|
warn$2("v-slot只能在组件根模板使用", el);
|
|
|
}
|
|
|
}
|
|
|
var ref = getSlotName(slotBinding);
|
|
|
el.slotTarget = ref.name; // 插槽名称
|
|
|
el.slotTargetDynamic = ref.dynamic; // 是否动态
|
|
|
el.slotScope = slotBinding.value || emptySlotScopeToken; // 作用域
|
|
|
}
|
|
|
} else {
|
|
|
// 组件上的v-slot
|
|
|
var slotBinding$1 = getAndRemoveAttrByRegex(el, slotRE);
|
|
|
if (slotBinding$1) {
|
|
|
{
|
|
|
// 使用限制检查
|
|
|
if (!maybeComponent(el)) {
|
|
|
warn$2("v-slot只能用于组件或template", slotBinding$1);
|
|
|
}
|
|
|
// 语法冲突检查
|
|
|
if (el.slotScope || el.slotTarget) {
|
|
|
warn$2("混用不同插槽语法", el);
|
|
|
}
|
|
|
// 默认插槽处理提示
|
|
|
if (el.scopedSlots) {
|
|
|
warn$2("存在具名插槽时默认插槽需用template", slotBinding$1);
|
|
|
}
|
|
|
}
|
|
|
// 创建插槽容器
|
|
|
var slots = el.scopedSlots || (el.scopedSlots = {});
|
|
|
var ref$1 = getSlotName(slotBinding$1);
|
|
|
var slotContainer = slots[ref$1.name] = createASTElement('template', [], el);
|
|
|
slotContainer.slotTarget = ref$1.name; // 插槽名
|
|
|
slotContainer.slotTargetDynamic = ref$1.dynamic; // 动态标识
|
|
|
slotContainer.children = el.children.filter(function (c) { // 转移子节点
|
|
|
if (!c.slotScope) {
|
|
|
c.parent = slotContainer;
|
|
|
return true
|
|
|
}
|
|
|
});
|
|
|
slotContainer.slotScope = slotBinding$1.value || emptySlotScopeToken; // 作用域
|
|
|
el.children = []; // 清空原子节点
|
|
|
el.plain = false; // 标记非普通元素
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 解析插槽名称
|
|
|
function getSlotName (binding) {
|
|
|
var name = binding.name.replace(slotRE, ''); // 去除指令前缀
|
|
|
if (!name) { // 处理简写语法
|
|
|
if (binding.name[0] !== '#') {
|
|
|
name = 'default'; // 默认插槽
|
|
|
} else {
|
|
|
warn$2("v-slot简写语法需要指定名称", binding);
|
|
|
}
|
|
|
}
|
|
|
// 返回名称和动态标识
|
|
|
return dynamicArgRE.test(name)
|
|
|
? { name: name.slice(1, -1), dynamic: true } // 动态名称
|
|
|
: { name: ("\"" + name + "\""), dynamic: false } // 静态名称
|
|
|
}
|
|
|
|
|
|
// 处理slot出口(<slot>标签)
|
|
|
function processSlotOutlet (el) {
|
|
|
if (el.tag === 'slot') {
|
|
|
el.slotName = getBindingAttr(el, 'name'); // 获取slot名称
|
|
|
if (el.key) { // slot使用key警告
|
|
|
warn$2(
|
|
|
"slot不能使用key,请在外层元素设置",
|
|
|
getRawBindingAttr(el, 'key')
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理动态组件
|
|
|
function processComponent (el) {
|
|
|
var binding;
|
|
|
if ((binding = getBindingAttr(el, 'is'))) { // 获取is绑定
|
|
|
el.component = binding; // 设置组件名
|
|
|
}
|
|
|
if (getAndRemoveAttr(el, 'inline-template') != null) { // 内联模板
|
|
|
el.inlineTemplate = true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 处理元素属性
|
|
|
function processAttrs (el) {
|
|
|
var list = el.attrsList; // 属性列表
|
|
|
for (var i = 0, l = list.length; i < l; i++) {
|
|
|
var name = list[i].name; // 原始属性名
|
|
|
var value = list[i].value; // 属性值
|
|
|
if (dirRE.test(name)) { // 检查是否指令
|
|
|
el.hasBindings = true; // 标记有绑定
|
|
|
var modifiers = parseModifiers(name.replace(dirRE, '')); // 解析修饰符
|
|
|
if (modifiers) name = name.replace(modifierRE, ''); // 清理修饰符
|
|
|
|
|
|
if (bindRE.test(name)) { // v-bind处理
|
|
|
name = name.replace(bindRE, ''); // 获取属性名
|
|
|
value = parseFilters(value); // 解析过滤器
|
|
|
var isDynamic = dynamicArgRE.test(name); // 是否动态参数
|
|
|
if (isDynamic) name = name.slice(1, -1); // 提取动态参数名
|
|
|
|
|
|
// 空值检查
|
|
|
if (value.trim() === "") {
|
|
|
warn$2("v-bind值不能为空");
|
|
|
}
|
|
|
|
|
|
// 处理修饰符
|
|
|
if (modifiers) {
|
|
|
if (modifiers.prop && !isDynamic) { // prop绑定
|
|
|
name = camelize(name);
|
|
|
if (name === 'innerHtml') name = 'innerHTML'; // 特殊处理
|
|
|
}
|
|
|
if (modifiers.camel && !isDynamic) name = camelize(name); // 驼峰化
|
|
|
if (modifiers.sync) { // sync语法糖
|
|
|
var syncGen = genAssignmentCode(value, "$event");
|
|
|
// 添加更新处理器
|
|
|
if (!isDynamic) {
|
|
|
addHandler(el, "update:" + camelize(name), syncGen);
|
|
|
if (hyphenate(name) !== camelize(name)) {
|
|
|
addHandler(el, "update:" + hyphenate(name), syncGen);
|
|
|
}
|
|
|
} else {
|
|
|
addHandler(el, `"update:"+(${name})`, syncGen, null, false, warn$2, list[i], true);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 判断绑定方式
|
|
|
if ((modifiers && modifiers.prop) || platformMustUseProp(el.tag, el.attrsMap.type, name)) {
|
|
|
addProp(el, name, value, list[i], isDynamic); // 属性绑定
|
|
|
} else {
|
|
|
addAttr(el, name, value, list[i], isDynamic); // 特性绑定
|
|
|
}
|
|
|
} else if (onRE.test(name)) { // v-on处理
|
|
|
name = name.replace(onRE, ''); // 获取事件名
|
|
|
var isDynamic = dynamicArgRE.test(name);
|
|
|
if (isDynamic) name = name.slice(1, -1); // 动态事件名
|
|
|
addHandler(el, name, value, modifiers, false, warn$2, list[i], isDynamic); // 添加事件处理器
|
|
|
} else { // 普通指令处理
|
|
|
name = name.replace(dirRE, ''); // 获取指令名
|
|
|
var argMatch = name.match(argRE); // 解析参数
|
|
|
var arg = argMatch && argMatch[1];
|
|
|
var isDynamic = false;
|
|
|
if (arg) {
|
|
|
name = name.slice(0, -(arg.length + 1)); // 获取指令主名称
|
|
|
if (dynamicArgRE.test(arg)) { // 动态参数处理
|
|
|
arg = arg.slice(1, -1);
|
|
|
isDynamic = true;
|
|
|
}
|
|
|
}
|
|
|
addDirective(el, name, rawName, value, arg, isDynamic, modifiers, list[i]); // 添加指令
|
|
|
if (name === 'model') checkForAliasModel(el, value); // 特殊处理v-model
|
|
|
}
|
|
|
} else { // 普通属性处理
|
|
|
// 插值语法警告
|
|
|
{
|
|
|
if (parseText(value, delimiters)) {
|
|
|
warn$2("属性中的插值已废弃,请使用v-bind");
|
|
|
}
|
|
|
}
|
|
|
addAttr(el, name, JSON.stringify(value), list[i]); // 添加静态属性
|
|
|
// Firefox muted属性特殊处理
|
|
|
if (name === 'muted' && !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)) {
|
|
|
addProp(el, name, 'true', list[i]);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查元素是否在v-for循环中
|
|
|
function checkInFor (el) {
|
|
|
var parent = el;
|
|
|
while (parent) {
|
|
|
if (parent.for !== undefined) { // 发现父级存在v-for
|
|
|
return true // 返回存在标记
|
|
|
}
|
|
|
parent = parent.parent; // 向上遍历父级元素
|
|
|
}
|
|
|
return false // 未找到返回false
|
|
|
}
|
|
|
|
|
|
// 解析修饰符(如.stop/.prevent)
|
|
|
function parseModifiers (name) {
|
|
|
var match = name.match(modifierRE); // 使用正则匹配修饰符
|
|
|
if (match) {
|
|
|
var ret = {}; // 创建修饰符对象
|
|
|
match.forEach(function (m) {
|
|
|
ret[m.slice(1)] = true; // 去除点号后存入对象
|
|
|
});
|
|
|
return ret // 返回修饰符集合
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将属性数组转换为映射表
|
|
|
function makeAttrsMap (attrs) {
|
|
|
var map = {};
|
|
|
for (var i = 0, l = attrs.length; i < l; i++) {
|
|
|
// 非IE环境检查重复属性
|
|
|
if (map[attrs[i].name] && !isIE && !isEdge) {
|
|
|
warn$2('重复属性: ' + attrs[i].name, attrs[i]);
|
|
|
}
|
|
|
map[attrs[i].name] = attrs[i].value; // 存储属性值
|
|
|
}
|
|
|
return map // 返回属性映射表
|
|
|
}
|
|
|
|
|
|
// 判断是否为文本标签(script/style)
|
|
|
function isTextTag (el) {
|
|
|
return el.tag === 'script' || el.tag === 'style'
|
|
|
}
|
|
|
|
|
|
// 判断是否为禁止使用的标签
|
|
|
function isForbiddenTag (el) {
|
|
|
return (
|
|
|
el.tag === 'style' || // 禁止style标签
|
|
|
(el.tag === 'script' && ( // 无类型或JS类型的script标签
|
|
|
!el.attrsMap.type ||
|
|
|
el.attrsMap.type === 'text/javascript'
|
|
|
))
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// IE SVG命名空间bug正则
|
|
|
var ieNSBug = /^xmlns:NS\d+/; // 匹配错误命名空间属性
|
|
|
var ieNSPrefix = /^NS\d+:/; // 匹配错误命名空间前缀
|
|
|
|
|
|
// 修复IE SVG属性bug
|
|
|
function guardIESVGBug (attrs) {
|
|
|
var res = [];
|
|
|
for (var i = 0; i < attrs.length; i++) {
|
|
|
var attr = attrs[i];
|
|
|
if (!ieNSBug.test(attr.name)) { // 过滤非法属性
|
|
|
attr.name = attr.name.replace(ieNSPrefix, ''); // 修复属性名
|
|
|
res.push(attr); // 加入结果集
|
|
|
}
|
|
|
}
|
|
|
return res // 返回修复后的属性
|
|
|
}
|
|
|
|
|
|
// 检查v-model绑定到v-for迭代别名的情况
|
|
|
function checkForAliasModel (el, value) {
|
|
|
var _el = el;
|
|
|
while (_el) { // 遍历父级元素
|
|
|
if (_el.for && _el.alias === value) { // 发现v-for别名绑定
|
|
|
warn$2( // 发出警告
|
|
|
"v-model不能直接绑定到v-for的迭代别名",
|
|
|
el.rawAttrsMap['v-model']
|
|
|
);
|
|
|
}
|
|
|
_el = _el.parent; // 向上遍历
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// input元素的预处理转换
|
|
|
function preTransformNode (el, options) {
|
|
|
if (el.tag === 'input') { // 仅处理input标签
|
|
|
var map = el.attrsMap;
|
|
|
if (!map['v-model']) return // 无v-model直接返回
|
|
|
|
|
|
// 获取类型绑定表达式
|
|
|
var typeBinding;
|
|
|
if (map[':type'] || map['v-bind:type']) {
|
|
|
typeBinding = getBindingAttr(el, 'type');
|
|
|
} else if (!map.type && map['v-bind']) {
|
|
|
typeBinding = `(${map['v-bind']}).type`; // 从v-bind提取类型
|
|
|
}
|
|
|
|
|
|
if (typeBinding) { // 存在动态类型时创建分支逻辑
|
|
|
// 收集条件判断属性
|
|
|
var ifCondition = getAndRemoveAttr(el, 'v-if', true);
|
|
|
var ifConditionExtra = ifCondition ? `&&(${ifCondition})` : "";
|
|
|
var hasElse = getAndRemoveAttr(el, 'v-else', true) != null;
|
|
|
var elseIfCondition = getAndRemoveAttr(el, 'v-else-if', true);
|
|
|
|
|
|
// 创建checkbox分支
|
|
|
var branch0 = cloneASTElement(el);
|
|
|
processFor(branch0); // 处理v-for
|
|
|
addRawAttr(branch0, 'type', 'checkbox'); // 强制类型
|
|
|
processElement(branch0, options); // 处理元素
|
|
|
branch0.processed = true; // 标记已处理
|
|
|
branch0.if = `(${typeBinding})==='checkbox'${ifConditionExtra}`; // 条件表达式
|
|
|
addIfCondition(branch0, { exp: branch0.if, block: branch0 }); // 添加条件
|
|
|
|
|
|
// 创建radio分支
|
|
|
var branch1 = cloneASTElement(el);
|
|
|
getAndRemoveAttr(branch1, 'v-for', true); // 移除v-for
|
|
|
addRawAttr(branch1, 'type', 'radio'); // 设置类型
|
|
|
processElement(branch1, options);
|
|
|
addIfCondition(branch0, { // 添加条件分支
|
|
|
exp: `(${typeBinding})==='radio'${ifConditionExtra}`,
|
|
|
block: branch1
|
|
|
});
|
|
|
|
|
|
// 创建其他类型分支
|
|
|
var branch2 = cloneASTElement(el);
|
|
|
getAndRemoveAttr(branch2, 'v-for', true);
|
|
|
addRawAttr(branch2, ':type', typeBinding); // 动态绑定类型
|
|
|
processElement(branch2, options);
|
|
|
addIfCondition(branch0, { exp: ifCondition, block: branch2 });
|
|
|
|
|
|
// 处理else条件
|
|
|
if (hasElse) {
|
|
|
branch0.else = true;
|
|
|
} else if (elseIfCondition) {
|
|
|
branch0.elseif = elseIfCondition;
|
|
|
}
|
|
|
|
|
|
return branch0 // 返回转换后的分支结构
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 克隆AST元素节点
|
|
|
function cloneASTElement (el) {
|
|
|
return createASTElement(el.tag, el.attrsList.slice(), el.parent)
|
|
|
}
|
|
|
|
|
|
// 模型处理模块配置
|
|
|
var model$1 = {
|
|
|
preTransformNode: preTransformNode // 注册预处理方法
|
|
|
};
|
|
|
|
|
|
// 所有转换模块集合
|
|
|
var modules$1 = [
|
|
|
klass$1, // 类处理模块
|
|
|
style$1, // 样式处理模块
|
|
|
model$1 // 模型处理模块
|
|
|
];
|
|
|
|
|
|
// 文本指令处理
|
|
|
function text (el, dir) {
|
|
|
if (dir.value) {
|
|
|
// 将文本绑定到textContent属性
|
|
|
addProp(el, 'textContent', `_s(${dir.value})`, dir);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// HTML指令处理
|
|
|
function html (el, dir) {
|
|
|
if (dir.value) {
|
|
|
// 将HTML绑定到innerHTML属性
|
|
|
addProp(el, 'innerHTML', `_s(${dir.value})`, dir);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 指令集合
|
|
|
var directives$1 = {
|
|
|
model: model, // 双向绑定指令
|
|
|
text: text, // 文本指令
|
|
|
html: html // HTML指令
|
|
|
};
|
|
|
|
|
|
// 基础编译选项配置
|
|
|
var baseOptions = {
|
|
|
expectHTML: true, // 期望标准HTML结构
|
|
|
modules: modules$1, // 使用的转换模块
|
|
|
directives: directives$1, // 指令处理集合
|
|
|
isPreTag: isPreTag, // 判断pre标签方法
|
|
|
isUnaryTag: isUnaryTag, // 判断自闭合标签方法
|
|
|
mustUseProp: mustUseProp, // 必须用prop绑定的属性判断
|
|
|
canBeLeftOpenTag: canBeLeftOpenTag, // 可省略闭合标签
|
|
|
isReservedTag: isReservedTag,// 保留标签判断
|
|
|
getTagNamespace: getTagNamespace, // 获取标签命名空间
|
|
|
staticKeys: genStaticKeys(modules$1) // 静态属性键名集合
|
|
|
};
|
|
|
|
|
|
// 静态分析相关变量
|
|
|
var isStaticKey; // 静态属性判断函数
|
|
|
var isPlatformReservedTag; // 平台保留标签判断
|
|
|
|
|
|
// 生成静态键名的缓存函数
|
|
|
var genStaticKeysCached = cached(genStaticKeys$1);
|
|
|
|
|
|
// AST优化主函数:标记静态节点
|
|
|
function optimize (root, options) {
|
|
|
if (!root) return
|
|
|
// 初始化静态分析工具
|
|
|
isStaticKey = genStaticKeysCached(options.staticKeys || '');
|
|
|
isPlatformReservedTag = options.isReservedTag || no;
|
|
|
// 第一遍:标记静态节点
|
|
|
markStatic$1(root);
|
|
|
// 第二遍:标记静态根节点
|
|
|
markStaticRoots(root, false);
|
|
|
}
|
|
|
|
|
|
// 生成静态属性键名集合
|
|
|
function genStaticKeys$1 (keys) {
|
|
|
return makeMap( // 创建映射表
|
|
|
'type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap' +
|
|
|
(keys ? ',' + keys : '')
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// 递归标记静态节点
|
|
|
function markStatic$1 (node) {
|
|
|
node.static = isStatic(node); // 判断当前节点是否静态
|
|
|
if (node.type === 1) { // 元素节点处理
|
|
|
// 跳过组件插槽内容
|
|
|
if (
|
|
|
!isPlatformReservedTag(node.tag) &&
|
|
|
node.tag !== 'slot' &&
|
|
|
node.attrsMap['inline-template'] == null
|
|
|
) return
|
|
|
|
|
|
// 递归处理子节点
|
|
|
for (var i = 0, l = node.children.length; i < l; i++) {
|
|
|
var child = node.children[i];
|
|
|
markStatic$1(child);
|
|
|
if (!child.static) node.static = false; // 子节点非静态则当前节点非静态
|
|
|
}
|
|
|
|
|
|
// 处理条件分支中的节点
|
|
|
if (node.ifConditions) {
|
|
|
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
|
|
|
var block = node.ifConditions[i$1].block;
|
|
|
markStatic$1(block);
|
|
|
if (!block.static) node.static = false;
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 标记静态根节点
|
|
|
function markStaticRoots (node, isInFor) {
|
|
|
if (node.type === 1) { // 元素节点处理
|
|
|
// 记录静态节点是否在循环中
|
|
|
if (node.static || node.once) {
|
|
|
node.staticInFor = isInFor;
|
|
|
}
|
|
|
// 标记静态根节点条件:包含有效子节点
|
|
|
if (node.static && node.children.length &&
|
|
|
!(node.children.length === 1 && node.children[0].type === 3)) {
|
|
|
node.staticRoot = true; // 标记为静态根
|
|
|
return
|
|
|
} else {
|
|
|
node.staticRoot = false; // 不满足条件取消标记
|
|
|
}
|
|
|
// 递归处理子节点
|
|
|
if (node.children) {
|
|
|
for (var i = 0, l = node.children.length; i < l; i++) {
|
|
|
markStaticRoots(node.children[i], isInFor || !!node.for);
|
|
|
}
|
|
|
}
|
|
|
// 处理条件分支
|
|
|
if (node.ifConditions) {
|
|
|
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
|
|
|
markStaticRoots(node.ifConditions[i$1].block, isInFor);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 判断节点是否静态
|
|
|
function isStatic (node) {
|
|
|
if (node.type === 2) return false // 表达式节点非静态
|
|
|
if (node.type === 3) return true // 文本节点静态
|
|
|
return !!(node.pre || ( // 包含v-pre或满足以下条件:
|
|
|
!node.hasBindings && // 无动态绑定
|
|
|
!node.if && !node.for && // 无流程控制
|
|
|
!isBuiltInTag(node.tag) && // 非内置标签
|
|
|
isPlatformReservedTag(node.tag) && // 平台原生标签
|
|
|
!isDirectChildOfTemplateFor(node) && // 非template的直接v-for子元素
|
|
|
Object.keys(node).every(isStaticKey) // 所有属性都是静态的
|
|
|
))
|
|
|
}
|
|
|
|
|
|
// 判断是否是template标签的直接v-for子元素
|
|
|
function isDirectChildOfTemplateFor (node) {
|
|
|
while (node.parent) { // 向上遍历父节点
|
|
|
node = node.parent;
|
|
|
if (node.tag !== 'template') return false
|
|
|
if (node.for) return true // 发现template有v-for
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 匹配函数表达式(箭头函数和传统函数)
|
|
|
var fnExpRE = /^([\w$_]+|\([^)]*?\))\s*=>|^function(?:\s+[\w$]+)?\s*\(/;
|
|
|
// 匹配函数调用结尾
|
|
|
var fnInvokeRE = /\([^)]*?\);*$/;
|
|
|
// 匹配简单属性路径(obj.prop或obj['prop'])
|
|
|
var simplePathRE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/;
|
|
|
|
|
|
// 键盘事件键码映射表
|
|
|
var keyCodes = {
|
|
|
esc: 27, // ESC键
|
|
|
tab: 9, // Tab键
|
|
|
enter: 13, // Enter键
|
|
|
space: 32, // 空格键
|
|
|
up: 38, // 上箭头
|
|
|
left: 37, // 左箭头
|
|
|
right: 39, // 右箭头
|
|
|
down: 40, // 下箭头
|
|
|
'delete': [8, 46] // 删除/退格键
|
|
|
};
|
|
|
|
|
|
// 键盘事件键名映射表(兼容不同浏览器)
|
|
|
var keyNames = {
|
|
|
esc: ['Esc', 'Escape'], // IE兼容
|
|
|
tab: 'Tab', // Tab键
|
|
|
enter: 'Enter', // 回车键
|
|
|
space: [' ', 'Spacebar'], // IE兼容
|
|
|
up: ['Up', 'ArrowUp'], // 上箭头兼容
|
|
|
left: ['Left', 'ArrowLeft'], // 左箭头兼容
|
|
|
right: ['Right', 'ArrowRight'], // 右箭头兼容
|
|
|
down: ['Down', 'ArrowDown'], // 下箭头兼容
|
|
|
'delete': ['Backspace', 'Delete', 'Del'] // 删除键兼容
|
|
|
};
|
|
|
|
|
|
// 生成条件判断代码的工厂函数
|
|
|
var genGuard = function (condition) { return ("if(" + condition + ")return null;"); };
|
|
|
|
|
|
// 事件修饰符对应的代码片段
|
|
|
var modifierCode = {
|
|
|
stop: '$event.stopPropagation();', // 阻止冒泡
|
|
|
prevent: '$event.preventDefault();', // 阻止默认
|
|
|
self: genGuard("$event.target !== $event.currentTarget"), // 仅当前元素触发
|
|
|
ctrl: genGuard("!$event.ctrlKey"), // 需要ctrl键
|
|
|
shift: genGuard("!$event.shiftKey"), // 需要shift键
|
|
|
alt: genGuard("!$event.altKey"), // 需要alt键
|
|
|
meta: genGuard("!$event.metaKey"), // 需要meta键
|
|
|
left: genGuard("'button' in $event && $event.button !== 0"), // 鼠标左键
|
|
|
middle: genGuard("'button' in $event && $event.button !== 1"), // 鼠标中键
|
|
|
right: genGuard("'button' in $event && $event.button !== 2") // 鼠标右键
|
|
|
};
|
|
|
|
|
|
// 生成事件处理器代码
|
|
|
function genHandlers (events, isNative) {
|
|
|
var prefix = isNative ? 'nativeOn:' : 'on:'; // 判断原生事件
|
|
|
var staticHandlers = ""; // 静态事件集合
|
|
|
var dynamicHandlers = ""; // 动态事件集合
|
|
|
|
|
|
// 遍历所有事件
|
|
|
for (var name in events) {
|
|
|
var handlerCode = genHandler(events[name]);
|
|
|
// 分离动态/静态事件
|
|
|
if (events[name] && events[name].dynamic) {
|
|
|
dynamicHandlers += name + "," + handlerCode + ",";
|
|
|
} else {
|
|
|
staticHandlers += "\"" + name + "\":" + handlerCode + ",";
|
|
|
}
|
|
|
}
|
|
|
|
|
|
staticHandlers = "{" + (staticHandlers.slice(0, -1)) + "}"; // 格式化静态事件
|
|
|
// 组合最终代码
|
|
|
return dynamicHandlers
|
|
|
? prefix + "_d(" + staticHandlers + ",[" + dynamicHandlers.slice(0, -1) + "])"
|
|
|
: prefix + staticHandlers;
|
|
|
}
|
|
|
|
|
|
// 生成单个事件处理器
|
|
|
function genHandler (handler) {
|
|
|
if (!handler) return 'function(){}' // 空处理器
|
|
|
|
|
|
// 处理数组格式的处理器(多个处理函数)
|
|
|
if (Array.isArray(handler)) {
|
|
|
return "[" + handler.map(genHandler).join(',') + "]";
|
|
|
}
|
|
|
|
|
|
// 判断处理器类型
|
|
|
var isMethodPath = simplePathRE.test(handler.value); // 方法路径格式
|
|
|
var isFunctionExpression = fnExpRE.test(handler.value); // 函数表达式
|
|
|
var isFunctionInvocation = simplePathRE.test( // 函数调用格式
|
|
|
handler.value.replace(fnInvokeRE, '')
|
|
|
);
|
|
|
|
|
|
// 无修饰符时的处理
|
|
|
if (!handler.modifiers) {
|
|
|
if (isMethodPath || isFunctionExpression) {
|
|
|
return handler.value; // 直接返回方法名
|
|
|
}
|
|
|
return `function($event){${isFunctionInvocation ? 'return ' + handler.value : handler.value}}`;
|
|
|
}
|
|
|
// 有修饰符时的处理
|
|
|
else {
|
|
|
var code = '';
|
|
|
var genModifierCode = '';
|
|
|
var keys = [];
|
|
|
|
|
|
// 处理每个修饰符
|
|
|
for (var key in handler.modifiers) {
|
|
|
if (modifierCode[key]) { // 预定义修饰符
|
|
|
genModifierCode += modifierCode[key];
|
|
|
if (keyCodes[key]) keys.push(key); // 收集需要键码检查的修饰符
|
|
|
} else if (key === 'exact') { // 精确修饰符
|
|
|
genModifierCode += genGuard(
|
|
|
['ctrl', 'shift', 'alt', 'meta']
|
|
|
.filter(k => !handler.modifiers[k])
|
|
|
.map(k => `$event.${k}Key`)
|
|
|
.join('||')
|
|
|
);
|
|
|
} else {
|
|
|
keys.push(key); // 自定义键名修饰符
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成按键过滤代码
|
|
|
if (keys.length) code += genKeyFilter(keys);
|
|
|
// 添加修饰符代码
|
|
|
if (genModifierCode) code += genModifierCode;
|
|
|
|
|
|
// 生成处理器主体代码
|
|
|
var handlerCode = isMethodPath
|
|
|
? `return ${handler.value}($event)`
|
|
|
: isFunctionExpression
|
|
|
? `return (${handler.value})($event)`
|
|
|
: isFunctionInvocation
|
|
|
? `return ${handler.value}`
|
|
|
: handler.value;
|
|
|
|
|
|
return `function($event){${code}${handlerCode}}`;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成按键过滤代码
|
|
|
function genKeyFilter (keys) {
|
|
|
return `if(!$event.type.indexOf('key')&&${keys.map(genFilterCode).join('&&')})return null;`;
|
|
|
}
|
|
|
|
|
|
// 生成单个按键过滤条件
|
|
|
function genFilterCode (key) {
|
|
|
var keyVal = parseInt(key, 10);
|
|
|
if (keyVal) return `$event.keyCode!==${keyVal}`; // 数字键码直接比较
|
|
|
|
|
|
// 处理键名和键码别名
|
|
|
var keyCode = keyCodes[key];
|
|
|
var keyName = keyNames[key];
|
|
|
return `_k($event.keyCode,${JSON.stringify(key)},${JSON.stringify(keyCode)},$event.key,${JSON.stringify(keyName)})`;
|
|
|
}
|
|
|
|
|
|
/* 事件处理指令 */
|
|
|
function on (el, dir) {
|
|
|
if (dir.modifiers) {
|
|
|
warn("无参数的v-on不支持修饰符"); // 参数校验
|
|
|
}
|
|
|
el.wrapListeners = code => `_g(${code},${dir.value})`; // 包装监听器
|
|
|
}
|
|
|
|
|
|
/* 属性绑定指令 */
|
|
|
function bind$1 (el, dir) {
|
|
|
el.wrapData = function (code) {
|
|
|
return `_b(${code},'${el.tag}',${dir.value},${dir.modifiers?.prop ? 'true' : 'false'}${dir.modifiers?.sync ? ',true' : ''})`;
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 基础指令集合
|
|
|
var baseDirectives = {
|
|
|
on: on, // 事件绑定
|
|
|
bind: bind$1,// 属性绑定
|
|
|
cloak: noop // 空指令(SSR占位)
|
|
|
};
|
|
|
|
|
|
/* 代码生成状态管理 */
|
|
|
var CodegenState = function (options) {
|
|
|
this.options = options; // 编译选项
|
|
|
this.warn = options.warn || baseWarn; // 警告方法
|
|
|
this.transforms = pluckModuleFunction(options.modules, 'transformCode'); // 代码转换函数
|
|
|
this.dataGenFns = pluckModuleFunction(options.modules, 'genData'); // 数据生成函数
|
|
|
this.directives = extend({}, baseDirectives, options.directives); // 合并指令
|
|
|
var isReservedTag = options.isReservedTag || no;
|
|
|
this.maybeComponent = el => !!el.component || !isReservedTag(el.tag); // 组件判断
|
|
|
this.onceId = 0; // v-once计数器
|
|
|
this.staticRenderFns = []; // 静态渲染函数缓存
|
|
|
this.pre = false; // v-pre状态标记
|
|
|
};
|
|
|
|
|
|
// 生成渲染函数入口
|
|
|
function generate (ast, options) {
|
|
|
var state = new CodegenState(options); // 初始化状态
|
|
|
var code = ast ? genElement(ast, state) : '_c("div")'; // 生成组件或默认div
|
|
|
return {
|
|
|
render: `with(this){return ${code}}`, // 渲染函数
|
|
|
staticRenderFns: state.staticRenderFns // 静态渲染函数数组
|
|
|
};
|
|
|
}
|
|
|
|
|
|
// 生成元素代码
|
|
|
function genElement (el, state) {
|
|
|
if (el.parent) {
|
|
|
el.pre = el.pre || el.parent.pre; // 继承父级v-pre状态
|
|
|
}
|
|
|
|
|
|
// 静态根节点处理
|
|
|
if (el.staticRoot && !el.staticProcessed) {
|
|
|
return genStatic(el, state);
|
|
|
}
|
|
|
// v-once处理
|
|
|
else if (el.once && !el.onceProcessed) {
|
|
|
return genOnce(el, state);
|
|
|
}
|
|
|
// v-for处理
|
|
|
else if (el.for && !el.forProcessed) {
|
|
|
return genFor(el, state);
|
|
|
}
|
|
|
// v-if处理
|
|
|
else if (el.if && !el.ifProcessed) {
|
|
|
return genIf(el, state);
|
|
|
}
|
|
|
// template插槽处理
|
|
|
else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
|
|
|
return genChildren(el, state) || 'void 0';
|
|
|
}
|
|
|
// slot处理
|
|
|
else if (el.tag === 'slot') {
|
|
|
return genSlot(el, state);
|
|
|
}
|
|
|
// 普通元素/组件
|
|
|
else {
|
|
|
var code;
|
|
|
if (el.component) { // 组件处理
|
|
|
code = genComponent(el.component, el, state);
|
|
|
} else {
|
|
|
var data;
|
|
|
// 生成属性数据
|
|
|
if (!el.plain || (el.pre && state.maybeComponent(el))) {
|
|
|
data = genData$2(el, state);
|
|
|
}
|
|
|
// 生成子节点
|
|
|
var children = el.inlineTemplate ? null : genChildren(el, state, true);
|
|
|
code = `_c('${el.tag}'${data ? `,${data}` : ''}${children ? `,${children}` : ''})`;
|
|
|
}
|
|
|
// 应用代码转换
|
|
|
for (var i = 0; i < state.transforms.length; i++) {
|
|
|
code = state.transforms[i](el, code);
|
|
|
}
|
|
|
return code;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成静态节点代码
|
|
|
function genStatic (el, state) {
|
|
|
el.staticProcessed = true; // 标记已处理
|
|
|
var originalPreState = state.pre;
|
|
|
if (el.pre) state.pre = el.pre; // 保存当前v-pre状态
|
|
|
|
|
|
// 缓存静态渲染函数
|
|
|
state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`);
|
|
|
state.pre = originalPreState; // 恢复v-pre状态
|
|
|
return `_m(${state.staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`; // 返回缓存索引
|
|
|
}
|
|
|
|
|
|
// 生成v-once节点代码
|
|
|
function genOnce (el, state) {
|
|
|
el.onceProcessed = true;
|
|
|
// 优先处理v-if条件
|
|
|
if (el.if && !el.ifProcessed) return genIf(el, state);
|
|
|
|
|
|
// 处理v-for中的v-once
|
|
|
if (el.staticInFor) {
|
|
|
let parent = el.parent;
|
|
|
let key;
|
|
|
// 查找最近的v-for的key
|
|
|
while (parent) {
|
|
|
if (parent.for) {
|
|
|
key = parent.key;
|
|
|
break;
|
|
|
}
|
|
|
parent = parent.parent;
|
|
|
}
|
|
|
if (!key) { // 无key警告
|
|
|
state.warn("v-once需要用在有key的v-for内", el.rawAttrsMap['v-once']);
|
|
|
return genElement(el, state);
|
|
|
}
|
|
|
return `_o(${genElement(el, state)},${state.onceId++},${key})`; // 生成唯一标识
|
|
|
}
|
|
|
// 普通静态处理
|
|
|
else {
|
|
|
return genStatic(el, state);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成条件表达式代码
|
|
|
function genIf (el, state, altGen, altEmpty) {
|
|
|
el.ifProcessed = true; // 避免递归
|
|
|
return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty);
|
|
|
}
|
|
|
|
|
|
// 递归生成条件分支代码
|
|
|
function genIfConditions (conditions, state, altGen, altEmpty) {
|
|
|
if (!conditions.length) return altEmpty || '_e()'; // 空节点
|
|
|
|
|
|
const condition = conditions.shift();
|
|
|
// 生成三元表达式
|
|
|
return condition.exp
|
|
|
? `(${condition.exp})?${genTernaryExp(condition.block)}:${genIfConditions(conditions, state)}`
|
|
|
: genTernaryExp(condition.block);
|
|
|
|
|
|
// 生成分支代码
|
|
|
function genTernaryExp(el) {
|
|
|
return altGen
|
|
|
? altGen(el, state)
|
|
|
: el.once
|
|
|
? genOnce(el, state)
|
|
|
: genElement(el, state);
|
|
|
}
|
|
|
}
|
|
|
// 生成 v-for 指令的渲染代码
|
|
|
function genFor (
|
|
|
el,
|
|
|
state,
|
|
|
altGen,
|
|
|
altHelper
|
|
|
) {
|
|
|
// 获取循环表达式
|
|
|
var exp = el.for;
|
|
|
// 获取循环别名
|
|
|
var alias = el.alias;
|
|
|
// 生成迭代器1参数
|
|
|
var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : '';
|
|
|
// 生成迭代器2参数
|
|
|
var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : '';
|
|
|
|
|
|
// 检查组件是否需要显式key的警告
|
|
|
if (state.maybeComponent(el) &&
|
|
|
el.tag !== 'slot' &&
|
|
|
el.tag !== 'template' &&
|
|
|
!el.key
|
|
|
) {
|
|
|
// 发出缺少key的警告提示
|
|
|
state.warn(
|
|
|
"<" + (el.tag) + " v-for=\"" + alias + " in " + exp + "\">: component lists rendered with " +
|
|
|
"v-for should have explicit keys. " +
|
|
|
"See https://vuejs.org/guide/list.html#key for more info.",
|
|
|
el.rawAttrsMap['v-for'],
|
|
|
true /* tip */
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 标记已处理避免递归
|
|
|
el.forProcessed = true;
|
|
|
// 返回生成的循环函数代码
|
|
|
return (altHelper || '_l') + "((" + exp + ")," +
|
|
|
"function(" + alias + iterator1 + iterator2 + "){" +
|
|
|
"return " + ((altGen || genElement)(el, state)) +
|
|
|
'})'
|
|
|
}
|
|
|
|
|
|
// 生成元素的数据对象
|
|
|
function genData$2 (el, state) {
|
|
|
// 初始化数据对象字符串
|
|
|
var data = '{';
|
|
|
|
|
|
// 首先生成指令相关代码
|
|
|
var dirs = genDirectives(el, state);
|
|
|
// 如果有指令则添加到数据对象
|
|
|
if (dirs) { data += dirs + ','; }
|
|
|
|
|
|
// 处理key属性
|
|
|
if (el.key) {
|
|
|
data += "key:" + (el.key) + ",";
|
|
|
}
|
|
|
// 处理ref属性
|
|
|
if (el.ref) {
|
|
|
data += "ref:" + (el.ref) + ",";
|
|
|
}
|
|
|
// 处理refInFor标记
|
|
|
if (el.refInFor) {
|
|
|
data += "refInFor:true,";
|
|
|
}
|
|
|
// 处理pre标记
|
|
|
if (el.pre) {
|
|
|
data += "pre:true,";
|
|
|
}
|
|
|
// 记录组件原始标签名
|
|
|
if (el.component) {
|
|
|
data += "tag:\"" + (el.tag) + "\",";
|
|
|
}
|
|
|
// 执行模块的数据生成函数
|
|
|
for (var i = 0; i < state.dataGenFns.length; i++) {
|
|
|
data += state.dataGenFns[i](el);
|
|
|
}
|
|
|
// 处理普通属性
|
|
|
if (el.attrs) {
|
|
|
data += "attrs:" + (genProps(el.attrs)) + ",";
|
|
|
}
|
|
|
// 处理DOM属性
|
|
|
if (el.props) {
|
|
|
data += "domProps:" + (genProps(el.props)) + ",";
|
|
|
}
|
|
|
// 处理事件处理器
|
|
|
if (el.events) {
|
|
|
data += (genHandlers(el.events, false)) + ",";
|
|
|
}
|
|
|
// 处理原生事件
|
|
|
if (el.nativeEvents) {
|
|
|
data += (genHandlers(el.nativeEvents, true)) + ",";
|
|
|
}
|
|
|
// 处理非作用域的slot
|
|
|
if (el.slotTarget && !el.slotScope) {
|
|
|
data += "slot:" + (el.slotTarget) + ",";
|
|
|
}
|
|
|
// 处理作用域插槽
|
|
|
if (el.scopedSlots) {
|
|
|
data += (genScopedSlots(el, el.scopedSlots, state)) + ",";
|
|
|
}
|
|
|
// 处理组件v-model
|
|
|
if (el.model) {
|
|
|
data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";
|
|
|
}
|
|
|
// 处理内联模板
|
|
|
if (el.inlineTemplate) {
|
|
|
var inlineTemplate = genInlineTemplate(el, state);
|
|
|
if (inlineTemplate) {
|
|
|
data += inlineTemplate + ",";
|
|
|
}
|
|
|
}
|
|
|
// 清理末尾逗号并闭合对象
|
|
|
data = data.replace(/,$/, '') + '}';
|
|
|
// 处理动态绑定属性
|
|
|
if (el.dynamicAttrs) {
|
|
|
data = "_b(" + data + ",\"" + (el.tag) + "\"," + (genProps(el.dynamicAttrs)) + ")";
|
|
|
}
|
|
|
// 执行数据包装函数
|
|
|
if (el.wrapData) {
|
|
|
data = el.wrapData(data);
|
|
|
}
|
|
|
// 执行监听器包装函数
|
|
|
if (el.wrapListeners) {
|
|
|
data = el.wrapListeners(data);
|
|
|
}
|
|
|
return data
|
|
|
}
|
|
|
|
|
|
// 生成指令数组代码
|
|
|
function genDirectives (el, state) {
|
|
|
var dirs = el.directives;
|
|
|
if (!dirs) { return }
|
|
|
// 初始化指令数组
|
|
|
var res = 'directives:[';
|
|
|
var hasRuntime = false;
|
|
|
var i, l, dir, needRuntime;
|
|
|
// 遍历所有指令
|
|
|
for (i = 0, l = dirs.length; i < l; i++) {
|
|
|
dir = dirs[i];
|
|
|
needRuntime = true;
|
|
|
// 获取指令生成函数
|
|
|
var gen = state.directives[dir.name];
|
|
|
if (gen) {
|
|
|
// 执行指令编译时处理
|
|
|
needRuntime = !!gen(el, dir, state.warn);
|
|
|
}
|
|
|
if (needRuntime) {
|
|
|
hasRuntime = true;
|
|
|
// 生成指令对象代码
|
|
|
res += "{name:\"" + (dir.name) + "\",rawName:\"" + (dir.rawName) + "\"" + (dir.value ? (",value:(" + (dir.value) + "),expression:" + (JSON.stringify(dir.value))) : '') + (dir.arg ? (",arg:" + (dir.isDynamicArg ? dir.arg : ("\"" + (dir.arg) + "\""))) : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";
|
|
|
}
|
|
|
}
|
|
|
if (hasRuntime) {
|
|
|
// 返回有效的指令数组
|
|
|
return res.slice(0, -1) + ']'
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成内联模板代码
|
|
|
function genInlineTemplate (el, state) {
|
|
|
// 获取第一个子元素作为模板
|
|
|
var ast = el.children[0];
|
|
|
// 验证子元素数量
|
|
|
if (el.children.length !== 1 || ast.type !== 1) {
|
|
|
state.warn(
|
|
|
'Inline-template components must have exactly one child element.',
|
|
|
{ start: el.start }
|
|
|
);
|
|
|
}
|
|
|
if (ast && ast.type === 1) {
|
|
|
// 生成内联模板的渲染函数
|
|
|
var inlineRenderFns = generate(ast, state.options);
|
|
|
return ("inlineTemplate:{render:function(){" + (inlineRenderFns.render) + "},staticRenderFns:[" + (inlineRenderFns.staticRenderFns.map(function (code) { return ("function(){" + code + "}"); }).join(',') + "]}")
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成作用域插槽代码
|
|
|
function genScopedSlots (
|
|
|
el,
|
|
|
slots,
|
|
|
state
|
|
|
) {
|
|
|
// 判断是否需要强制更新
|
|
|
var needsForceUpdate = el.for || Object.keys(slots).some(function (key) {
|
|
|
var slot = slots[key];
|
|
|
return (
|
|
|
slot.slotTargetDynamic ||
|
|
|
slot.if ||
|
|
|
slot.for ||
|
|
|
containsSlotChild(slot)
|
|
|
});
|
|
|
|
|
|
// 判断是否需要唯一key
|
|
|
var needsKey = !!el.if;
|
|
|
|
|
|
// 检查父级作用域是否需要强制更新
|
|
|
if (!needsForceUpdate) {
|
|
|
var parent = el.parent;
|
|
|
while (parent) {
|
|
|
if (
|
|
|
(parent.slotScope && parent.slotScope !== emptySlotScopeToken) ||
|
|
|
parent.for
|
|
|
) {
|
|
|
needsForceUpdate = true;
|
|
|
break
|
|
|
}
|
|
|
if (parent.if) {
|
|
|
needsKey = true;
|
|
|
}
|
|
|
parent = parent.parent;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成所有插槽代码
|
|
|
var generatedSlots = Object.keys(slots)
|
|
|
.map(function (key) { return genScopedSlot(slots[key], state); })
|
|
|
.join(',');
|
|
|
|
|
|
// 返回统一处理函数调用
|
|
|
return ("scopedSlots:_u([" + generatedSlots + "]" + (needsForceUpdate ? ",null,true" : "") + (!needsForceUpdate && needsKey ? (",null,false," + (hash(generatedSlots))) : "") + ")")
|
|
|
}
|
|
|
|
|
|
// 生成字符串的哈希值
|
|
|
function hash(str) {
|
|
|
var hash = 5381;
|
|
|
var i = str.length;
|
|
|
while(i) {
|
|
|
hash = (hash * 33) ^ str.charCodeAt(--i);
|
|
|
}
|
|
|
return hash >>> 0
|
|
|
}
|
|
|
|
|
|
// 检查元素是否包含slot子元素
|
|
|
function containsSlotChild (el) {
|
|
|
if (el.type === 1) {
|
|
|
if (el.tag === 'slot') {
|
|
|
return true
|
|
|
}
|
|
|
return el.children.some(containsSlotChild)
|
|
|
}
|
|
|
return false
|
|
|
}
|
|
|
|
|
|
// 生成单个作用域插槽代码
|
|
|
function genScopedSlot (
|
|
|
el,
|
|
|
state
|
|
|
) {
|
|
|
// 检查旧语法格式
|
|
|
var isLegacySyntax = el.attrsMap['slot-scope'];
|
|
|
// 处理带if条件的插槽
|
|
|
if (el.if && !el.ifProcessed && !isLegacySyntax) {
|
|
|
return genIf(el, state, genScopedSlot, "null")
|
|
|
}
|
|
|
// 处理带for循环的插槽
|
|
|
if (el.for && !el.forProcessed) {
|
|
|
return genFor(el, state, genScopedSlot)
|
|
|
}
|
|
|
// 生成插槽作用域参数
|
|
|
var slotScope = el.slotScope === emptySlotScopeToken
|
|
|
? ""
|
|
|
: String(el.slotScope);
|
|
|
// 生成插槽渲染函数
|
|
|
var fn = "function(" + slotScope + "){" +
|
|
|
"return " + (el.tag === 'template'
|
|
|
? el.if && isLegacySyntax
|
|
|
? ("(" + (el.if) + ")?" + (genChildren(el, state) || 'undefined') + ":undefined")
|
|
|
: genChildren(el, state) || 'undefined'
|
|
|
: genElement(el, state)) + "}";
|
|
|
// 处理反向代理配置
|
|
|
var reverseProxy = slotScope ? "" : ",proxy:true";
|
|
|
return ("{key:" + (el.slotTarget || "\"default\"") + ",fn:" + fn + reverseProxy + "}")
|
|
|
}
|
|
|
|
|
|
// 生成子元素代码
|
|
|
function genChildren (
|
|
|
el,
|
|
|
state,
|
|
|
checkSkip,
|
|
|
altGenElement,
|
|
|
altGenNode
|
|
|
) {
|
|
|
var children = el.children;
|
|
|
if (children.length) {
|
|
|
var el$1 = children[0];
|
|
|
// 优化单个v-for子元素的情况
|
|
|
if (children.length === 1 &&
|
|
|
el$1.for &&
|
|
|
el$1.tag !== 'template' &&
|
|
|
el$1.tag !== 'slot'
|
|
|
) {
|
|
|
var normalizationType = checkSkip
|
|
|
? state.maybeComponent(el$1) ? ",1" : ",0"
|
|
|
: "";
|
|
|
return ("" + ((altGenElement || genElement)(el$1, state)) + normalizationType)
|
|
|
}
|
|
|
// 获取子元素规范化类型
|
|
|
var normalizationType$1 = checkSkip
|
|
|
? getNormalizationType(children, state.maybeComponent)
|
|
|
: 0;
|
|
|
var gen = altGenNode || genNode;
|
|
|
// 生成子元素数组代码
|
|
|
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',') + "]" +
|
|
|
// 确定子节点数组需要的规范化类型
|
|
|
// 返回值:
|
|
|
// 0: 不需要规范化
|
|
|
// 1: 需要简单规范化(可能存在一级嵌套数组)
|
|
|
// 2: 需要完全规范化
|
|
|
function getNormalizationType (
|
|
|
children, // 子节点数组
|
|
|
maybeComponent // 判断是否为组件的函数
|
|
|
) {
|
|
|
var res = 0; // 初始化规范化类型
|
|
|
for (var i = 0; i < children.length; i++) { // 遍历所有子节点
|
|
|
var el = children[i];
|
|
|
if (el.type !== 1) { // 只处理元素节点
|
|
|
continue
|
|
|
}
|
|
|
// 检查需要完全规范化的条件
|
|
|
if (needsNormalization(el) || // 元素本身需要规范化
|
|
|
(el.ifConditions && el.ifConditions.some(function (c) { return needsNormalization(c.block); }))) { // 条件分支中的元素需要规范化
|
|
|
res = 2; // 标记为完全规范化
|
|
|
break // 提前终止循环
|
|
|
}
|
|
|
// 检查需要简单规范化的条件
|
|
|
if (maybeComponent(el) || // 元素可能是组件
|
|
|
(el.ifConditions && el.ifConditions.some(function (c) { return maybeComponent(c.block); }))) { // 条件分支中的元素可能是组件
|
|
|
res = 1; // 标记为简单规范化
|
|
|
}
|
|
|
}
|
|
|
return res // 返回规范化类型
|
|
|
}
|
|
|
|
|
|
// 判断元素是否需要规范化处理
|
|
|
function needsNormalization (el) {
|
|
|
return el.for !== undefined || // 包含 v-for 指令
|
|
|
el.tag === 'template' || // template 标签
|
|
|
el.tag === 'slot' // slot 标签
|
|
|
}
|
|
|
|
|
|
// 生成节点代码的入口函数
|
|
|
function genNode (node, state) {
|
|
|
if (node.type === 1) { // 元素节点
|
|
|
return genElement(node, state)
|
|
|
} else if (node.type === 3 && node.isComment) { // 注释节点
|
|
|
return genComment(node)
|
|
|
} else { // 文本节点
|
|
|
return genText(node)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成文本节点代码
|
|
|
function genText (text) {
|
|
|
return ("_v(" + (text.type === 2 // 判断是否动态文本
|
|
|
? text.expression // 直接使用表达式(已用 _s() 包装)
|
|
|
: transformSpecialNewlines(JSON.stringify(text.text))) + ")") // 处理特殊换行符
|
|
|
}
|
|
|
|
|
|
// 生成注释节点代码
|
|
|
function genComment (comment) {
|
|
|
return ("_e(" + (JSON.stringify(comment.text)) + ")") // 使用 _e 创建空注释
|
|
|
}
|
|
|
|
|
|
// 生成插槽代码
|
|
|
function genSlot (el, state) {
|
|
|
var slotName = el.slotName || '"default"'; // 获取插槽名称
|
|
|
var children = genChildren(el, state); // 生成子节点代码
|
|
|
var res = "_t(" + slotName + (children ? ("," + children) : ''); // 基础插槽代码
|
|
|
|
|
|
// 处理插槽属性和动态属性
|
|
|
var attrs = el.attrs || el.dynamicAttrs
|
|
|
? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(function (attr) {
|
|
|
return {
|
|
|
name: camelize(attr.name), // 属性名驼峰化
|
|
|
value: attr.value, // 属性值
|
|
|
dynamic: attr.dynamic // 是否动态属性
|
|
|
};
|
|
|
}))
|
|
|
: null;
|
|
|
|
|
|
var bind$$1 = el.attrsMap['v-bind']; // 获取 v-bind 绑定
|
|
|
|
|
|
// 处理属性和绑定的参数占位
|
|
|
if ((attrs || bind$$1) && !children) {
|
|
|
res += ",null"; // 子节点参数占位
|
|
|
}
|
|
|
if (attrs) {
|
|
|
res += "," + attrs; // 添加属性参数
|
|
|
}
|
|
|
if (bind$$1) {
|
|
|
res += (attrs ? '' : ',null') + "," + bind$$1; // 添加动态绑定参数
|
|
|
}
|
|
|
return res + ')' // 闭合函数调用
|
|
|
}
|
|
|
|
|
|
// 生成组件代码(componentName 参数用于规避类型检查)
|
|
|
function genComponent (
|
|
|
componentName, // 组件名
|
|
|
el, // 元素节点
|
|
|
state // 编译状态
|
|
|
) {
|
|
|
// 生成子节点代码(内联模板时跳过)
|
|
|
var children = el.inlineTemplate ? null : genChildren(el, state, true);
|
|
|
return ("_c(" + componentName + "," + (genData$2(el, state)) + (children ? ("," + children) : '') + ")") // 生成组件创建函数
|
|
|
}
|
|
|
|
|
|
// 生成属性对象代码
|
|
|
function genProps (props) {
|
|
|
var staticProps = ""; // 静态属性字符串
|
|
|
var dynamicProps = ""; // 动态属性字符串
|
|
|
|
|
|
// 遍历所有属性
|
|
|
for (var i = 0; i < props.length; i++) {
|
|
|
var prop = props[i];
|
|
|
var value = transformSpecialNewlines(prop.value); // 处理特殊换行符
|
|
|
|
|
|
if (prop.dynamic) { // 动态属性
|
|
|
dynamicProps += (prop.name) + "," + value + ","; // 收集动态属性
|
|
|
} else { // 静态属性
|
|
|
staticProps += "\"" + (prop.name) + "\":" + value + ","; // 收集静态属性
|
|
|
}
|
|
|
}
|
|
|
|
|
|
staticProps = "{" + (staticProps.slice(0, -1)) + "}"; // 包裹静态属性为对象
|
|
|
|
|
|
if (dynamicProps) { // 存在动态属性时
|
|
|
return ("_d(" + staticProps + ",[" + (dynamicProps.slice(0, -1)) + "])") // 合并静态和动态属性
|
|
|
} else {
|
|
|
return staticProps // 直接返回静态属性
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 转换特殊换行符(解决 #3895, #4268 问题)
|
|
|
function transformSpecialNewlines (text) {
|
|
|
return text
|
|
|
.replace(/\u2028/g, '\\u2028') // 替换行分隔符
|
|
|
.replace(/\u2029/g, '\\u2029') // 替换段落分隔符
|
|
|
}
|
|
|
|
|
|
// 匹配不允许在表达式中使用的关键字
|
|
|
var prohibitedKeywordRE = new RegExp('\\b' + (
|
|
|
'do,if,for,let,new,try,var,case,else,with,await,break,catch,class,const,' +
|
|
|
'super,throw,while,yield,delete,export,import,return,switch,default,' +
|
|
|
'extends,finally,continue,debugger,function,arguments'
|
|
|
).split(',').join('\\b|\\b') + '\\b');
|
|
|
|
|
|
// 匹配不能作为属性名的一元操作符
|
|
|
var unaryOperatorsRE = new RegExp('\\b' + (
|
|
|
'delete,typeof,void'
|
|
|
).split(',').join('\\s*\\([^\\)]*\\)|\\b') + '\\s*\\([^\\)]*\\)');
|
|
|
|
|
|
// 用于移除表达式中的字符串内容
|
|
|
var stripStringRE = /'(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*"|`(?:[^`\\]|\\.)*\$\{|\}(?:[^`\\]|\\.)*`|`(?:[^`\\]|\\.)*`/g;
|
|
|
|
|
|
// 检测模板中的错误表达式
|
|
|
function detectErrors (ast, warn) {
|
|
|
if (ast) { // 当AST存在时进行检查
|
|
|
checkNode(ast, warn); // 递归检查所有节点
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 递归检查AST节点
|
|
|
function checkNode (node, warn) {
|
|
|
if (node.type === 1) { // 元素节点类型
|
|
|
for (var name in node.attrsMap) { // 遍历所有属性
|
|
|
if (dirRE.test(name)) { // 检查是否指令属性
|
|
|
var value = node.attrsMap[name];
|
|
|
if (value) {
|
|
|
var range = node.rawAttrsMap[name];
|
|
|
if (name === 'v-for') { // 处理v-for指令
|
|
|
checkFor(node, ("v-for=\"" + value + "\""), warn, range);
|
|
|
} else if (name === 'v-slot' || name[0] === '#') { // 处理插槽指令
|
|
|
checkFunctionParameterExpression(value, (name + "=\"" + value + "\""), warn, range);
|
|
|
} else if (onRE.test(name)) { // 处理事件绑定
|
|
|
checkEvent(value, (name + "=\"" + value + "\""), warn, range);
|
|
|
} else { // 其他指令表达式检查
|
|
|
checkExpression(value, (name + "=\"" + value + "\""), warn, range);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
if (node.children) { // 递归检查子节点
|
|
|
for (var i = 0; i < node.children.length; i++) {
|
|
|
checkNode(node.children[i], warn);
|
|
|
}
|
|
|
}
|
|
|
} else if (node.type === 2) { // 文本插值类型
|
|
|
checkExpression(node.expression, node.text, warn, node);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查事件表达式中的非法操作符
|
|
|
function checkEvent (exp, text, warn, range) {
|
|
|
var stripped = exp.replace(stripStringRE, ''); // 移除字符串内容
|
|
|
var keywordMatch = stripped.match(unaryOperatorsRE); // 匹配一元操作符
|
|
|
if (keywordMatch && stripped.charAt(keywordMatch.index - 1) !== '$') { // 检查$前缀
|
|
|
warn( // 发出警告
|
|
|
"avoid using JavaScript unary operator as property name: " +
|
|
|
"\"" + (keywordMatch[0]) + "\" in expression " + (text.trim()),
|
|
|
range
|
|
|
);
|
|
|
}
|
|
|
checkExpression(exp, text, warn, range); // 常规表达式检查
|
|
|
}
|
|
|
|
|
|
// 检查v-for指令相关标识符
|
|
|
function checkFor (node, text, warn, range) {
|
|
|
checkExpression(node.for || '', text, warn, range); // 检查for表达式
|
|
|
checkIdentifier(node.alias, 'v-for alias', text, warn, range); // 检查别名
|
|
|
checkIdentifier(node.iterator1, 'v-for iterator', text, warn, range); // 检查迭代器1
|
|
|
checkIdentifier(node.iterator2, 'v-for iterator', text, warn, range); // 检查迭代器2
|
|
|
}
|
|
|
|
|
|
// 验证标识符合法性
|
|
|
function checkIdentifier (
|
|
|
ident, // 标识符名称
|
|
|
type, // 标识符类型描述
|
|
|
text, // 原始文本
|
|
|
warn, // 警告函数
|
|
|
range // 源码位置范围
|
|
|
) {
|
|
|
if (typeof ident === 'string') { // 仅处理字符串类型
|
|
|
try {
|
|
|
new Function(("var " + ident + "=_")); // 通过函数构造验证合法性
|
|
|
} catch (e) {
|
|
|
warn(("invalid " + type + " \"" + ident + "\" in expression: " + (text.trim())), range);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 验证表达式合法性
|
|
|
function checkExpression (exp, text, warn, range) {
|
|
|
try {
|
|
|
new Function(("return " + exp)); // 尝试解析表达式
|
|
|
} catch (e) {
|
|
|
var keywordMatch = exp.replace(stripStringRE, '').match(prohibitedKeywordRE);
|
|
|
if (keywordMatch) { // 匹配到禁止关键字
|
|
|
warn(
|
|
|
"avoid using JavaScript keyword as property name: " +
|
|
|
"\"" + (keywordMatch[0]) + "\"\n Raw expression: " + (text.trim()),
|
|
|
range
|
|
|
);
|
|
|
} else { // 其他解析错误
|
|
|
warn(
|
|
|
"invalid expression: " + (e.message) + " in\n\n" +
|
|
|
" " + exp + "\n\n" +
|
|
|
" Raw expression: " + (text.trim()) + "\n",
|
|
|
range
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查函数参数表达式
|
|
|
function checkFunctionParameterExpression (exp, text, warn, range) {
|
|
|
try {
|
|
|
new Function(exp, ''); // 验证参数表达式
|
|
|
} catch (e) {
|
|
|
warn( // 参数表达式错误警告
|
|
|
"invalid function parameter expression: " + (e.message) + " in\n\n" +
|
|
|
" " + exp + "\n\n" +
|
|
|
" Raw expression: " + (text.trim()) + "\n",
|
|
|
range
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 生成代码错误位置标记框架
|
|
|
var range = 2; // 显示错误上下文的行数范围
|
|
|
|
|
|
function generateCodeFrame (
|
|
|
source, // 原始代码
|
|
|
start, // 错误起始位置
|
|
|
end // 错误结束位置
|
|
|
) {
|
|
|
if ( start === void 0 ) start = 0; // 默认起始位置
|
|
|
if ( end === void 0 ) end = source.length; // 默认结束位置
|
|
|
|
|
|
var lines = source.split(/\r?\n/); // 按行分割代码
|
|
|
var count = 0; // 字符累计计数器
|
|
|
var res = []; // 结果数组
|
|
|
|
|
|
// 遍历行号定位错误位置
|
|
|
for (var i = 0; i < lines.length; i++) {
|
|
|
count += lines[i].length + 1; // 累加行长度(包含换行符)
|
|
|
if (count >= start) { // 定位到错误起始行
|
|
|
// 生成错误上下文范围行
|
|
|
for (var j = i - range; j <= i + range || end > count; j++) {
|
|
|
if (j < 0 || j >= lines.length) { continue } // 跳过无效行号
|
|
|
// 添加行号和代码内容
|
|
|
res.push(("" + (j + 1) + (repeat$1(" ", 3 - String(j + 1).length)) + "| " + (lines[j]));
|
|
|
var lineLength = lines[j].length;
|
|
|
if (j === i) { // 错误所在行
|
|
|
// 生成错误位置下划线
|
|
|
var pad = start - (count - lineLength) + 1;
|
|
|
var length = end > count ? lineLength - pad : end - start;
|
|
|
res.push(" | " + repeat$1(" ", pad) + repeat$1("^", length));
|
|
|
} else if (j > i) { // 后续行处理
|
|
|
if (end > count) { // 跨行错误处理
|
|
|
var length$1 = Math.min(end - count, lineLength);
|
|
|
res.push(" | " + repeat$1("^", length$1));
|
|
|
}
|
|
|
count += lineLength + 1; // 更新字符计数器
|
|
|
}
|
|
|
}
|
|
|
break // 结束循环
|
|
|
}
|
|
|
}
|
|
|
return res.join('\n') // 拼接成字符串
|
|
|
}
|
|
|
|
|
|
// 生成重复字符串的工具函数
|
|
|
function repeat$1 (str, n) {
|
|
|
var result = '';
|
|
|
if (n > 0) { // 使用位运算优化循环
|
|
|
while (true) { // 通过右移操作实现快速重复
|
|
|
if (n & 1) { result += str; } // 奇数追加字符串
|
|
|
n >>>= 1; // 无符号右移
|
|
|
if (n <= 0) { break }
|
|
|
str += str; // 字符串翻倍
|
|
|
}
|
|
|
}
|
|
|
return result
|
|
|
}
|
|
|
|
|
|
// 创建函数并处理错误的工具函数
|
|
|
function createFunction (code, errors) {
|
|
|
try {
|
|
|
return new Function(code) // 尝试创建函数
|
|
|
} catch (err) {
|
|
|
errors.push({ err: err, code: code }); // 收集错误信息
|
|
|
return noop // 返回空函数
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建编译到函数的工厂函数
|
|
|
function createCompileToFunctionFn (compile) {
|
|
|
var cache = Object.create(null); // 创建编译缓存对象
|
|
|
|
|
|
return function compileToFunctions (
|
|
|
template, // 模板字符串
|
|
|
options, // 编译选项
|
|
|
vm // Vue实例
|
|
|
) {
|
|
|
options = extend({}, options); // 扩展选项对象
|
|
|
var warn$$1 = options.warn || warn; // 获取警告函数
|
|
|
delete options.warn; // 移除选项中的warn
|
|
|
|
|
|
/* istanbul ignore if */ // 忽略代码覆盖率
|
|
|
{
|
|
|
// 检测内容安全策略(CSP)限制
|
|
|
try {
|
|
|
new Function('return 1'); // 尝试创建简单函数
|
|
|
} catch (e) {
|
|
|
if (e.toString().match(/unsafe-eval|CSP/)) { // 匹配CSP错误
|
|
|
warn$$1( // 发出CSP警告
|
|
|
'It seems you are using the standalone build of Vue.js in an ' +
|
|
|
'environment with Content Security Policy that prohibits unsafe-eval. ' +
|
|
|
'The template compiler cannot work in this environment. Consider ' +
|
|
|
'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
|
|
|
'templates into render functions.'
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查缓存是否存在
|
|
|
var key = options.delimiters
|
|
|
? String(options.delimiters) + template // 带分隔符的缓存键
|
|
|
: template; // 普通模板缓存键
|
|
|
if (cache[key]) {
|
|
|
return cache[key] // 返回缓存结果
|
|
|
}
|
|
|
|
|
|
// 执行编译过程
|
|
|
var compiled = compile(template, options);
|
|
|
|
|
|
// 处理编译错误和提示
|
|
|
{
|
|
|
if (compiled.errors && compiled.errors.length) { // 存在错误
|
|
|
if (options.outputSourceRange) { // 支持输出源码位置
|
|
|
compiled.errors.forEach(function (e) {
|
|
|
warn$$1( // 带代码框架的错误提示
|
|
|
"Error compiling template:\n\n" + (e.msg) + "\n\n" +
|
|
|
generateCodeFrame(template, e.start, e.end),
|
|
|
vm
|
|
|
);
|
|
|
});
|
|
|
} else { // 简单错误列表提示
|
|
|
warn$$1(
|
|
|
"Error compiling template:\n\n" + template + "\n\n" +
|
|
|
compiled.errors.map(function (e) { return ("- " + e); }).join('\n') + '\n',
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
if (compiled.tips && compiled.tips.length) { // 处理提示信息
|
|
|
if (options.outputSourceRange) { // 带源码位置的提示
|
|
|
compiled.tips.forEach(function (e) { return tip(e.msg, vm); });
|
|
|
} else { // 普通提示
|
|
|
compiled.tips.forEach(function (msg) { return tip(msg, vm); });
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 将编译结果转换为函数
|
|
|
var res = {}; // 结果对象
|
|
|
var fnGenErrors = []; // 函数生成错误收集
|
|
|
res.render = createFunction(compiled.render, fnGenErrors); // 生成渲染函数
|
|
|
res.staticRenderFns = compiled.staticRenderFns.map(function (code) { // 生成静态渲染函数数组
|
|
|
return createFunction(code, fnGenErrors)
|
|
|
});
|
|
|
|
|
|
// 处理函数生成阶段的错误
|
|
|
/* istanbul ignore if */ // 忽略代码覆盖率
|
|
|
{
|
|
|
if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
|
|
|
warn$$1( // 编译器内部错误警告
|
|
|
"Failed to generate render function:\n\n" +
|
|
|
fnGenErrors.map(function (ref) {
|
|
|
var err = ref.err;
|
|
|
var code = ref.code;
|
|
|
|
|
|
return ((err.toString()) + " in\n\n" + code + "\n");
|
|
|
}).join('\n'),
|
|
|
vm
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return (cache[key] = res) // 缓存并返回结果
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建编译器生成器函数
|
|
|
function createCompilerCreator (baseCompile) {
|
|
|
// 返回创建编译器的函数
|
|
|
return function createCompiler (baseOptions) {
|
|
|
// 核心编译函数
|
|
|
function compile (
|
|
|
template,
|
|
|
options
|
|
|
) {
|
|
|
// 继承基础配置创建最终配置
|
|
|
var finalOptions = Object.create(baseOptions);
|
|
|
// 存储错误和提示的数组
|
|
|
var errors = [];
|
|
|
var tips = [];
|
|
|
|
|
|
// 基础警告函数
|
|
|
var warn = function (msg, range, tip) {
|
|
|
(tip ? tips : errors).push(msg);
|
|
|
};
|
|
|
|
|
|
// 合并用户自定义选项
|
|
|
if (options) {
|
|
|
// 处理需要源码位置信息的情况
|
|
|
if (options.outputSourceRange) {
|
|
|
// 计算模板前导空格长度
|
|
|
var leadingSpaceLength = template.match(/^\s*/)[0].length;
|
|
|
|
|
|
// 增强版警告函数(带源码位置)
|
|
|
warn = function (msg, range, tip) {
|
|
|
var data = { msg: msg };
|
|
|
if (range) { // 转换源码位置偏移量
|
|
|
if (range.start != null) {
|
|
|
data.start = range.start + leadingSpaceLength;
|
|
|
}
|
|
|
if (range.end != null) {
|
|
|
data.end = range.end + leadingSpaceLength;
|
|
|
}
|
|
|
}
|
|
|
(tip ? tips : errors).push(data);
|
|
|
};
|
|
|
}
|
|
|
// 合并自定义模块
|
|
|
if (options.modules) {
|
|
|
finalOptions.modules =
|
|
|
(baseOptions.modules || []).concat(options.modules);
|
|
|
}
|
|
|
// 合并自定义指令
|
|
|
if (options.directives) {
|
|
|
finalOptions.directives = extend(
|
|
|
Object.create(baseOptions.directives || null),
|
|
|
options.directives
|
|
|
);
|
|
|
}
|
|
|
// 复制其他选项
|
|
|
for (var key in options) {
|
|
|
if (key !== 'modules' && key !== 'directives') {
|
|
|
finalOptions[key] = options[key];
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 注入警告函数到配置
|
|
|
finalOptions.warn = warn;
|
|
|
|
|
|
// 执行基础编译流程
|
|
|
var compiled = baseCompile(template.trim(), finalOptions);
|
|
|
// 开发环境下检测AST错误
|
|
|
{
|
|
|
detectErrors(compiled.ast, warn);
|
|
|
}
|
|
|
// 附加错误和提示到编译结果
|
|
|
compiled.errors = errors;
|
|
|
compiled.tips = tips;
|
|
|
return compiled
|
|
|
}
|
|
|
|
|
|
// 返回编译器对象
|
|
|
return {
|
|
|
compile: compile,
|
|
|
compileToFunctions: createCompileToFunctionFn(compile)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 创建默认编译器(使用标准解析/优化/生成器)
|
|
|
var createCompiler = createCompilerCreator(function baseCompile (
|
|
|
template,
|
|
|
options
|
|
|
) {
|
|
|
// 解析模板生成AST
|
|
|
var ast = parse(template.trim(), options);
|
|
|
// 执行优化(默认开启)
|
|
|
if (options.optimize !== false) {
|
|
|
optimize(ast, options);
|
|
|
}
|
|
|
// 生成渲染代码
|
|
|
var code = generate(ast, options);
|
|
|
return {
|
|
|
ast: ast,
|
|
|
render: code.render, // 渲染函数代码
|
|
|
staticRenderFns: code.staticRenderFns // 静态渲染函数数组
|
|
|
}
|
|
|
});
|
|
|
|
|
|
// 创建基础编译器实例
|
|
|
var ref$1 = createCompiler(baseOptions);
|
|
|
// 解构编译方法
|
|
|
var compile = ref$1.compile;
|
|
|
var compileToFunctions = ref$1.compileToFunctions;
|
|
|
|
|
|
// 检测浏览器属性值编码行为
|
|
|
var div; // 用于DOM测试的临时元素
|
|
|
function getShouldDecode (href) {
|
|
|
div = div || document.createElement('div');
|
|
|
// 创建包含换行符的属性值测试节点
|
|
|
div.innerHTML = href ? "<a href=\"\n\"/>" : "<div a=\"\n\"/>";
|
|
|
// 检查是否编码为
|
|
|
return div.innerHTML.indexOf(' ') > 0
|
|
|
}
|
|
|
|
|
|
// 记录不同场景下的编码需求
|
|
|
var shouldDecodeNewlines = inBrowser ? getShouldDecode(false) : false; // 常规属性编码检测
|
|
|
var shouldDecodeNewlinesForHref = inBrowser ? getShouldDecode(true) : false; // href属性编码检测
|
|
|
|
|
|
// 带缓存的模板获取函数
|
|
|
var idToTemplate = cached(function (id) {
|
|
|
var el = query(id); // 查询DOM元素
|
|
|
return el && el.innerHTML // 返回元素内容
|
|
|
});
|
|
|
|
|
|
// 重写Vue的$mount方法
|
|
|
var mount = Vue.prototype.$mount;
|
|
|
Vue.prototype.$mount = function (
|
|
|
el,
|
|
|
hydrating
|
|
|
) {
|
|
|
// 标准化元素查询
|
|
|
el = el && query(el);
|
|
|
|
|
|
// 禁止挂载到body/html元素
|
|
|
/* istanbul ignore if */
|
|
|
if (el === document.body || el === document.documentElement) {
|
|
|
warn(
|
|
|
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
|
|
|
);
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
// 处理渲染函数/模板/元素关系
|
|
|
var options = this.$options;
|
|
|
if (!options.render) { // 无render函数时处理模板
|
|
|
var template = options.template;
|
|
|
if (template) { // 处理template选项
|
|
|
if (typeof template === 'string') { // 字符串模板
|
|
|
if (template.charAt(0) === '#') { // ID选择器
|
|
|
template = idToTemplate(template);
|
|
|
/* istanbul ignore if */
|
|
|
if (!template) { // 模板元素不存在警告
|
|
|
warn(
|
|
|
("Template element not found or is empty: " + (options.template)),
|
|
|
this
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
} else if (template.nodeType) { // DOM元素节点
|
|
|
template = template.innerHTML;
|
|
|
} else { // 无效模板类型
|
|
|
{
|
|
|
warn('invalid template option:' + template, this);
|
|
|
}
|
|
|
return this
|
|
|
}
|
|
|
} else if (el) { // 使用挂载元素的outerHTML
|
|
|
template = getOuterHTML(el);
|
|
|
}
|
|
|
if (template) { // 编译模板为渲染函数
|
|
|
/* istanbul ignore if */
|
|
|
if (config.performance && mark) { // 性能标记
|
|
|
mark('compile');
|
|
|
}
|
|
|
|
|
|
// 执行模板编译
|
|
|
var ref = compileToFunctions(template, {
|
|
|
outputSourceRange: "development" !== 'production', // 开发环境输出源码位置
|
|
|
shouldDecodeNewlines: shouldDecodeNewlines, // 换行编码处理
|
|
|
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
|
|
|
delimiters: options.delimiters, // 自定义分隔符
|
|
|
comments: options.comments // 保留注释
|
|
|
}, this);
|
|
|
var render = ref.render;
|
|
|
var staticRenderFns = ref.staticRenderFns;
|
|
|
// 注入渲染函数到选项
|
|
|
options.render = render;
|
|
|
options.staticRenderFns = staticRenderFns;
|
|
|
|
|
|
/* istanbul ignore if */
|
|
|
if (config.performance && mark) { // 记录编译耗时
|
|
|
mark('compile end');
|
|
|
measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
// 调用原始挂载方法
|
|
|
return mount.call(this, el, hydrating)
|
|
|
};
|
|
|
|
|
|
// 获取元素的outerHTML(兼容IE SVG)
|
|
|
function getOuterHTML (el) {
|
|
|
if (el.outerHTML) { // 标准浏览器支持
|
|
|
return el.outerHTML
|
|
|
} else { // 兼容处理
|
|
|
var container = document.createElement('div');
|
|
|
container.appendChild(el.cloneNode(true));
|
|
|
return container.innerHTML
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 暴露编译方法到Vue
|
|
|
Vue.compile = compileToFunctions;
|
|
|
|
|
|
return Vue;
|
|
|
})); |