【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

时间:2021-10-28 02:28:26

写在前面

  一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残。

  vuejs是一个优秀的前端mvvm框架,它的易用性和渐进式的理念可以使每一个前端开发人员感到舒服,感到easy。它内部的实现机制值得让我们深究,比如obServer的实现原理,为什么vue能够实现组件化等等,我们需要理解它内部的运行机制,代码结构,这样才能更深入的理解vue的优秀之处,能更好的贴合业务实际写出更恰当的代码。

  说明

    在展开本章之前,博主需要对自己看的源码文件进行一个简短的说明:

      博主最终选择首先阅读 Vue.js v2.1.3 这个版本的文件,什么文件呢? 不是基于es6的。 单独导入的。

      【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

    为什么大费周章要看这样的前端版本呢? 我给出的理由是,首先阅读目前浏览器中运行的js更符合渐进阅读的原则,有些es6的代码其实是js的语法糖,个人觉得语法糖吃的太多会长蛀虫,失了真正语言的精髓。当然完全使用es6博主完全没有意见,事实上博主工作中首选es6。但是在目前看来,先理解浏览器中的代码工作机制,再看es6的代码效果会更好,更贴切实际应用。并且博主会基于这个版本临摹一个简单点的vue,当理解充分后引入es6进行深入学习。

Vue.js的架构一

  【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

  一张图可以简略的了解vue的架构,当我们new Vue的时候,实际new的是Vue$3这个构造函数,紧接着,经过一系列判断处理,调用_init函数。 _init哪儿来的呢?

  通过initMinxin的调用,给当前Vue$3的prototype混入一个_init方法:

  【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

  在这个_init中经过六个步骤的操作,最终完成nw Vue()的操作:

  【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

  1.mergeOptions 合并策略对象,作用是合并父子组件options,它的作用是控制输出。即宽松输入,严格输出,通过一系列合并改装策略,将选项属性最终挂载到vm.$options上完成输出操作。

  2.initProxy初始化代理Proxy,它是为了后期的_render,使render时this指向proxy对象

  3.initLifecycle 初始化生命周期  这里很有意思

  4.initEvent 初始化事件,初始化生命周期之后,紧接着初始化事件处理,再紧接着代码里就可以看出来了,callHook调用事件。我们日常使用的beforeCreate、created分别在这调用

  5.initState 初始化data和props,我们的observer 发布订阅系统在这里实现。

  6.initRender 开始render

vue源码解析之一 htmlParse

  Virtual dom

    用过vue和react的人对虚拟dom这个概念应该大都不会太陌生了,它将真实的dom转换为AST节点,也就是转化成对象树的形式,当我们通过api操作dom时实际上是操作虚拟对象树,再由框架通过算法完成真实dom的转换,spa页面也是因为Virtual dom的出现渐渐出现在我们前端开发的选择中,今天博主暂时不过多介绍,这里我们来看看最基础的几个问题,html是怎么被解析成object的?指令的操作,变量的转换,运算符的操作。其实就是模板引擎的实现方式是怎样的?

  htmlparser

    在vue的源码中我们可以看到,vue中有一个parse函数:

    【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

    注释上的意思,这里将html格式的字符串转换为AST。

    整个vuejs文件八千行,这个parse函数功能占用了一千两百多行,可见这套转换逻辑在vue中的关键性,重要性,博主把它源代码抽了出来,可以直接调用:

    

var hasProto = '__proto__' in {};

// Browser environment sniffing
var inBrowser =
typeof window !== 'undefined' &&
Object.prototype.toString.call(window) !== '[object Object]'; var UA = inBrowser && window.navigator.userAgent.toLowerCase();
var isIE = UA && /msie|trident/.test(UA);
var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
var isEdge = UA && UA.indexOf('edge/') > 0;
var isAndroid = UA && UA.indexOf('android') > 0;
var isIOS = UA && /iphone|ipad|ipod|ios/.test(UA);
function makeMap(keys,expectsLowerCase){
var _map = {};
for(var i=0;i<keys.length;i++){
_map[keys[i]] = true;
}
return expectsLowerCase?
function(val){return _map[val.toLowerCase()]}:
function(val){return _map[val]}
} function makeAttrsMap (attrs) {
var map = {};
for (var i = 0, l = attrs.length; i < l; i++) {
if ("development" !== 'production' && map[attrs[i].name] && !isIE) {
warn$1('duplicate attribute: ' + attrs[i].name);
}
map[attrs[i].name] = attrs[i].value;
}
return map
} function isForbiddenTag (el) {
return (
el.tag === 'style' ||
(el.tag === 'script' && (
!el.attrsMap.type ||
el.attrsMap.type === 'text/javascript'
))
)
} function processPre (el) {
if (getAndRemoveAttr(el, 'v-pre') != null) {
el.pre = true;
}
}
/**
* Create a cached version of a pure function.
*/
function cached (fn) {
var cache = Object.create(null);
return function cachedFn (str) {
var hit = cache[str];
return hit || (cache[str] = fn(str))
}
} function decode (html) {
decoder = decoder || document.createElement('div');
decoder.innerHTML = html;
return decoder.textContent
} // Regular Expressions for parsing tags and attributes
var singleAttrIdentifier = /([^\s"'<>/=]+)/;
var singleAttrAssign = /(?:=)/;
var singleAttrValues = [
// attr value double quotes
/"([^"]*)"+/.source,
// attr value, single quotes
/'([^']*)'+/.source,
// attr value, no quotes
/([^\s"'=<>`]+)/.source
];
var attribute = new RegExp(
'^\\s*' + singleAttrIdentifier.source +
'(?:\\s*(' + singleAttrAssign.source + ')' +
'\\s*(?:' + singleAttrValues.join('|') + '))?'
); // could use https://www.w3.org/TR/1999/REC-xml-names-19990114/#NT-QName
// but for Vue templates we can enforce a simple charset
var ncname = '[a-zA-Z_][\\w\\-\\.]*';
var qnameCapture = '((?:' + ncname + '\\:)?' + ncname + ')';
var startTagOpen = new RegExp('^<' + qnameCapture);
var startTagClose = /^\s*(\/?)>/;
var endTag = new RegExp('^<\\/' + qnameCapture + '[^>]*>');
var doctype = /^<!DOCTYPE [^>]+>/i;
var comment = /^<!--/;
var conditionalComment = /^<!\[/; var IS_REGEX_CAPTURING_BROKEN = false;
'x'.replace(/x(.)?/g, function (m, g) {
IS_REGEX_CAPTURING_BROKEN = g === '';
}); // Special Elements (can contain anything)
var isScriptOrStyle = makeMap('script,style', true);
var hasLang = function (attr) { return attr.name === 'lang' && attr.value !== 'html'; };
var isSpecialTag = function (tag, isSFC, stack) {
if (isScriptOrStyle(tag)) {
return true
}
if (isSFC && stack.length === 1) {
// top-level template that has no pre-processor
if (tag === 'template' && !stack[0].attrs.some(hasLang)) {
return false
} else {
return true
}
}
return false
}; var reCache = {}; var ltRE = /&lt;/g;
var gtRE = /&gt;/g;
var nlRE = / /g;
var ampRE = /&amp;/g;
var quoteRE = /&quot;/g; function decodeAttr (value, shouldDecodeNewlines) {
if (shouldDecodeNewlines) {
value = value.replace(nlRE, '\n');
}
return value
.replace(ltRE, '<')
.replace(gtRE, '>')
.replace(ampRE, '&')
.replace(quoteRE, '"')
} function parseHTML (html, options) {
var stack = [];
var expectHTML = options.expectHTML;
var isUnaryTag$$1 = options.isUnaryTag || no;
var index = 0;
var last, lastTag;
while (html) {
last = html;
// Make sure we're not in a script or style element
if (!lastTag || !isSpecialTag(lastTag, options.sfc, stack)) {
var textEnd = html.indexOf('<');
if (textEnd === 0) {
// Comment:
if (comment.test(html)) {
var commentEnd = html.indexOf('-->'); if (commentEnd >= 0) {
advance(commentEnd + 3);
continue
}
} // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment
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
} // End tag:
var endTagMatch = html.match(endTag);
if (endTagMatch) {
var curIndex = index;
advance(endTagMatch[0].length);
parseEndTag(endTagMatch[0], endTagMatch[1], curIndex, index);
continue
} // Start tag:
var startTagMatch = parseStartTag();
if (startTagMatch) {
handleStartTag(startTagMatch);
continue
}
} var text = (void 0), rest$1 = (void 0), next = (void 0);
if (textEnd > 0) {
rest$1 = html.slice(textEnd);
while (
!endTag.test(rest$1) &&
!startTagOpen.test(rest$1) &&
!comment.test(rest$1) &&
!conditionalComment.test(rest$1)
) {
// < in plain text, be forgiving and treat it as text
next = rest$1.indexOf('<', 1);
if (next < 0) { break }
textEnd += next;
rest$1 = html.slice(textEnd);
}
text = html.substring(0, textEnd);
advance(textEnd);
} if (textEnd < 0) {
text = html;
html = '';
} if (options.chars && text) {
options.chars(text);
}
} else {
var stackedTag = lastTag.toLowerCase();
var reStackedTag = reCache[stackedTag] || (reCache[stackedTag] = new RegExp('([\\s\\S]*?)(</' + stackedTag + '[^>]*>)', 'i'));
var endTagLength = 0;
var rest = html.replace(reStackedTag, function (all, text, endTag) {
endTagLength = endTag.length;
if (stackedTag !== 'script' && stackedTag !== 'style' && stackedTag !== 'noscript') {
text = text
.replace(/<!--([\s\S]*?)-->/g, '$1')
.replace(/<!\[CDATA\[([\s\S]*?)]]>/g, '$1');
}
if (options.chars) {
options.chars(text);
}
return ''
});
index += html.length - rest.length;
html = rest;
parseEndTag('</' + stackedTag + '>', stackedTag, index - endTagLength, index);
} if (html === last && options.chars) {
options.chars(html);
break
}
} // Clean up any remaining tags
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(attribute))) {
advance(attr[0].length);
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; if (expectHTML) {
if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
parseEndTag('', lastTag);
}
if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
parseEndTag('', tagName);
}
} var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash; var l = match.attrs.length;
var attrs = new Array(l);
for (var i = 0; i < l; i++) {
var args = match.attrs[i];
// hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778
if (IS_REGEX_CAPTURING_BROKEN && args[0].indexOf('""') === -1) {
if (args[3] === '') { delete args[3]; }
if (args[4] === '') { delete args[4]; }
if (args[5] === '') { delete args[5]; }
}
var value = args[3] || args[4] || args[5] || '';
attrs[i] = {
name: args[1],
value: decodeAttr(
value,
options.shouldDecodeNewlines
)
};
} if (!unary) {
stack.push({ tag: tagName, attrs: attrs });
lastTag = tagName;
unarySlash = '';
} if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end);
}
} function parseEndTag (tag, tagName, start, end) {
var pos;
if (start == null) { start = index; }
if (end == null) { end = index; } // Find the closest opened tag of the same type
if (tagName) {
var needle = tagName.toLowerCase();
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].tag.toLowerCase() === needle) {
break
}
}
} else {
// If no tag name is provided, clean shop
pos = 0;
} if (pos >= 0) {
// Close all the open elements, up the stack
for (var i = stack.length - 1; i >= pos; i--) {
if (options.end) {
options.end(stack[i].tag, start, end);
}
} // Remove the open elements from the stack
stack.length = pos;
lastTag = pos && stack[pos - 1].tag;
} else if (tagName.toLowerCase() === 'br') {
if (options.start) {
options.start(tagName, [], true, start, end);
}
} else if (tagName.toLowerCase() === 'p') {
if (options.start) {
options.start(tagName, [], false, start, end);
}
if (options.end) {
options.end(tagName, start, end);
}
}
}
} /* */ function parseFilters (exp) {
var inSingle = false;
var inDouble = false;
var inTemplateString = false;
var inRegex = false;
var curly = 0;
var square = 0;
var paren = 0;
var lastFilterIndex = 0;
var c, prev, i, expression, filters; for (i = 0; i < exp.length; i++) {
prev = c;
c = exp.charCodeAt(i);
if (inSingle) {
if (c === 0x27 && prev !== 0x5C) { inSingle = false; }
} else if (inDouble) {
if (c === 0x22 && prev !== 0x5C) { inDouble = false; }
} else if (inTemplateString) {
if (c === 0x60 && prev !== 0x5C) { inTemplateString = false; }
} else if (inRegex) {
if (c === 0x2f && prev !== 0x5C) { inRegex = false; }
} else if (
c === 0x7C && // pipe
exp.charCodeAt(i + 1) !== 0x7C &&
exp.charCodeAt(i - 1) !== 0x7C &&
!curly && !square && !paren
) {
if (expression === undefined) {
// first filter, end of expression
lastFilterIndex = i + 1;
expression = exp.slice(0, i).trim();
} else {
pushFilter();
}
} else {
switch (c) {
case 0x22: inDouble = true; break // "
case 0x27: inSingle = true; break // '
case 0x60: inTemplateString = true; break // `
case 0x2f: inRegex = true; break // /
case 0x28: paren++; break // (
case 0x29: paren--; break // )
case 0x5B: square++; break // [
case 0x5D: square--; break // ]
case 0x7B: curly++; break // {
case 0x7D: curly--; break // }
}
}
} if (expression === undefined) {
expression = exp.slice(0, i).trim();
} else if (lastFilterIndex !== 0) {
pushFilter();
} function pushFilter () {
(filters || (filters = [])).push(exp.slice(lastFilterIndex, i).trim());
lastFilterIndex = i + 1;
} if (filters) {
for (i = 0; i < filters.length; i++) {
expression = wrapFilter(expression, filters[i]);
}
} return expression
} function wrapFilter (exp, filter) {
var i = filter.indexOf('(');
if (i < 0) {
// _f: resolveFilter
return ("_f(\"" + filter + "\")(" + exp + ")")
} else {
var name = filter.slice(0, i);
var args = filter.slice(i + 1);
return ("_f(\"" + name + "\")(" + exp + "," + args)
}
} /* */ var defaultTagRE = /\{\{((?:.|\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 lastIndex = tagRE.lastIndex = 0;
var match, index;
while ((match = tagRE.exec(text))) {
index = match.index;
// push text token
if (index > lastIndex) {
tokens.push(JSON.stringify(text.slice(lastIndex, index)));
}
// tag token
var exp = parseFilters(match[1].trim());
tokens.push(("_s(" + exp + ")"));
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)));
}
return tokens.join('+')
} /* */ function baseWarn (msg) {
console.error(("[Vue parser]: " + msg));
} function pluckModuleFunction (
modules,
key
) {
return modules
? modules.map(function (m) { return m[key]; }).filter(function (_) { return _; })
: []
} function addProp (el, name, value) {
(el.props || (el.props = [])).push({ name: name, value: value });
} function addAttr (el, name, value) {
(el.attrs || (el.attrs = [])).push({ name: name, value: value });
} function addDirective (
el,
name,
rawName,
value,
arg,
modifiers
) {
(el.directives || (el.directives = [])).push({ name: name, rawName: rawName, value: value, arg: arg, modifiers: modifiers });
} function addHandler (
el,
name,
value,
modifiers,
important
) {
// check capture modifier
if (modifiers && modifiers.capture) {
delete modifiers.capture;
name = '!' + name; // mark the event as captured
}
var events;
if (modifiers && modifiers.native) {
delete modifiers.native;
events = el.nativeEvents || (el.nativeEvents = {});
} else {
events = el.events || (el.events = {});
}
var newHandler = { value: value, modifiers: modifiers };
var handlers = events[name];
/* istanbul ignore if */
if (Array.isArray(handlers)) {
important ? handlers.unshift(newHandler) : handlers.push(newHandler);
} else if (handlers) {
events[name] = important ? [newHandler, handlers] : [handlers, newHandler];
} else {
events[name] = newHandler;
}
} function getBindingAttr (
el,
name,
getStatic
) {
var dynamicValue =
getAndRemoveAttr(el, ':' + name) ||
getAndRemoveAttr(el, 'v-bind:' + name);
if (dynamicValue != null) {
return parseFilters(dynamicValue)
} else if (getStatic !== false) {
var staticValue = getAndRemoveAttr(el, name);
if (staticValue != null) {
return JSON.stringify(staticValue)
}
}
} function getAndRemoveAttr (el, name) {
var val;
if ((val = el.attrsMap[name]) != null) {
var list = el.attrsList;
for (var i = 0, l = list.length; i < l; i++) {
if (list[i].name === name) {
list.splice(i, 1);
break
}
}
}
return val
} var len;
var str;
var chr;
var index$1;
var expressionPos;
var expressionEndPos; /**
* parse directive model to do the array update transform. a[idx] = val => $$a.splice($$idx, 1, val)
*
* for loop possible cases:
*
* - test
* - test[idx]
* - test[test1[idx]]
* - test["a"][idx]
* - xxx.test[a[a].test1[idx]]
* - test.xxx.a["asa"][test1[idx]]
*
*/ function parseModel (val) {
str = val;
len = str.length;
index$1 = expressionPos = expressionEndPos = 0; if (val.indexOf('[') < 0 || val.lastIndexOf(']') < len - 1) {
return {
exp: val,
idx: null
}
} while (!eof()) {
chr = next();
/* istanbul ignore if */
if (isStringStart(chr)) {
parseString(chr);
} else if (chr === 0x5B) {
parseBracket(chr);
}
} return {
exp: val.substring(0, expressionPos),
idx: val.substring(expressionPos + 1, expressionEndPos)
}
} function next () {
return str.charCodeAt(++index$1)
} function eof () {
return index$1 >= len
} function isStringStart (chr) {
return chr === 0x22 || chr === 0x27
} function parseBracket (chr) {
var inBracket = 1;
expressionPos = index$1;
while (!eof()) {
chr = next();
if (isStringStart(chr)) {
parseString(chr);
continue
}
if (chr === 0x5B) { inBracket++; }
if (chr === 0x5D) { inBracket--; }
if (inBracket === 0) {
expressionEndPos = index$1;
break
}
}
} function parseString (chr) {
var stringQuote = chr;
while (!eof()) {
chr = next();
if (chr === stringQuote) {
break
}
}
} /* */ var dirRE = /^v-|^@|^:/;
var forAliasRE = /(.*?)\s+(?:in|of)\s+(.*)/;
var forIteratorRE = /\((\{[^}]*\}|[^,]*),([^,]*)(?:,([^,]*))?\)/;
var bindRE = /^:|^v-bind:/;
var onRE = /^@|^v-on:/;
var argRE = /:(.*)$/;
var modifierRE = /\.[^.]+/g; var decodeHTMLCached = cached(decode); // configurable state
var warn$1;
var platformGetTagNamespace;
var platformMustUseProp;
var platformIsPreTag;
var preTransforms;
var transforms;
var postTransforms;
var delimiters;
var no = function(){
return false;
};
/**
* Convert HTML string to AST.
*/
function parse (
template,
options
) {
warn$1 = options.warn || baseWarn;
platformGetTagNamespace = options.getTagNamespace || no;
platformMustUseProp = options.mustUseProp || no;
platformIsPreTag = options.isPreTag || no;
preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
transforms = pluckModuleFunction(options.modules, 'transformNode');
postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');
delimiters = options.delimiters;
var stack = [];
var preserveWhitespace = options.preserveWhitespace !== false;
var root;
var currentParent;
var inVPre = false;
var inPre = false;
var warned = false;
parseHTML(template, {
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
start: function start (tag, attrs, unary) {
// check namespace.
// inherit parent ns if there is one
var ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag); // handle IE svg bug
/* istanbul ignore if */
if (isIE && ns === 'svg') {
attrs = guardIESVGBug(attrs);
} var element = {
type: 1,
tag: tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs),
parent: currentParent,
children: []
};
if (ns) {
element.ns = ns;
} if (isForbiddenTag(element) && !isServerRendering()) {
element.forbidden = true;
"development" !== 'production' && warn$1(
'Templates should only be responsible for mapping the state to the ' +
'UI. Avoid placing tags with side-effects in your templates, such as ' +
"<" + tag + ">."
);
} // apply pre-transforms
for (var i = 0; i < preTransforms.length; i++) {
preTransforms[i](element, options);
} if (!inVPre) {
processPre(element);
if (element.pre) {
inVPre = true;
}
}
if (platformIsPreTag(element.tag)) {
inPre = true;
}
if (inVPre) {
processRawAttrs(element);
} else {
processFor(element);
processIf(element);
processOnce(element);
processKey(element); // determine whether this is a plain element after
// removing structural attributes
element.plain = !element.key && !attrs.length; processRef(element);
processSlot(element);
processComponent(element);
for (var i$1 = 0; i$1 < transforms.length; i$1++) {
transforms[i$1](element, options);
}
processAttrs(element);
} function checkRootConstraints (el) {
if ("development" !== 'production' && !warned) {
if (el.tag === 'slot' || el.tag === 'template') {
warned = true;
warn$1(
"Cannot use <" + (el.tag) + "> as component root element because it may " +
'contain multiple nodes:\n' + template
);
}
if (el.attrsMap.hasOwnProperty('v-for')) {
warned = true;
warn$1(
'Cannot use v-for on stateful component root element because ' +
'it renders multiple elements:\n' + template
);
}
}
} // tree management
if (!root) {
root = element;
checkRootConstraints(root);
} else if (!stack.length) {
// allow root elements with v-if, v-else-if and v-else
if (root.if && (element.elseif || element.else)) {
checkRootConstraints(element);
addIfCondition(root, {
exp: element.elseif,
block: element
});
} else if ("development" !== 'production' && !warned) {
warned = true;
warn$1(
"Component template should contain exactly one root element:" +
"\n\n" + template + "\n\n" +
"If you are using v-if on multiple elements, " +
"use v-else-if to chain them instead."
);
}
}
if (currentParent && !element.forbidden) {
if (element.elseif || element.else) {
processIfConditions(element, currentParent);
} else if (element.slotScope) { // scoped slot
currentParent.plain = false;
var name = element.slotTarget || 'default';(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
} else {
currentParent.children.push(element);
element.parent = currentParent;
}
}
if (!unary) {
currentParent = element;
stack.push(element);
}
// apply post-transforms
for (var i$2 = 0; i$2 < postTransforms.length; i$2++) {
postTransforms[i$2](element, options);
}
}, end: function end () {
// remove trailing whitespace
var element = stack[stack.length - 1];
var lastNode = element.children[element.children.length - 1];
if (lastNode && lastNode.type === 3 && lastNode.text === ' ') {
element.children.pop();
}
// pop stack
stack.length -= 1;
currentParent = stack[stack.length - 1];
// check pre state
if (element.pre) {
inVPre = false;
}
if (platformIsPreTag(element.tag)) {
inPre = false;
}
}, chars: function chars (text) {
if (!currentParent) {
if ("development" !== 'production' && !warned && text === template) {
warned = true;
warn$1(
'Component template requires a root element, rather than just text:\n\n' + template
);
}
return
}
// IE textarea placeholder bug
/* istanbul ignore if */
if (isIE &&
currentParent.tag === 'textarea' &&
currentParent.attrsMap.placeholder === text) {
return
}
text = inPre || text.trim()
? decodeHTMLCached(text)
// only preserve whitespace if its not right after a starting tag
: preserveWhitespace && currentParent.children.length ? ' ' : '';
if (text) {
var expression;
if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {
currentParent.children.push({
type: 2,
expression: expression,
text: text
});
} else {
currentParent.children.push({
type: 3,
text: text
});
}
}
}
});
return root
} function processRawAttrs (el) {
var l = el.attrsList.length;
if (l) {
var attrs = el.attrs = new Array(l);
for (var i = 0; i < l; i++) {
attrs[i] = {
name: el.attrsList[i].name,
value: JSON.stringify(el.attrsList[i].value)
};
}
} else if (!el.pre) {
// non root node in pre blocks with no attributes
el.plain = true;
}
} function processKey (el) {
var exp = getBindingAttr(el, 'key');
if (exp) {
if ("development" !== 'production' && el.tag === 'template') {
warn$1("<template> cannot be keyed. Place the key on real elements instead.");
}
el.key = exp;
}
} function processRef (el) {
var ref = getBindingAttr(el, 'ref');
if (ref) {
el.ref = ref;
el.refInFor = checkInFor(el);
}
} function processFor (el) {
var exp;
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
var inMatch = exp.match(forAliasRE);
if (!inMatch) {
"development" !== 'production' && warn$1(
("Invalid v-for expression: " + exp)
);
return
}
el.for = inMatch[2].trim();
var alias = inMatch[1].trim();
var iteratorMatch = alias.match(forIteratorRE);
if (iteratorMatch) {
el.alias = iteratorMatch[1].trim();
el.iterator1 = iteratorMatch[2].trim();
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim();
}
} else {
el.alias = alias;
}
}
} function processIf (el) {
var exp = getAndRemoveAttr(el, 'v-if');
if (exp) {
el.if = exp;
addIfCondition(el, {
exp: exp,
block: el
});
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
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$1(
"v-" + (el.elseif ? ('else-if="' + el.elseif + '"') : 'else') + " " +
"used on element <" + (el.tag) + "> without corresponding v-if."
);
}
} function addIfCondition (el, condition) {
if (!el.conditions) {
el.conditions = [];
}
el.conditions.push(condition);
} function processOnce (el) {
var once = getAndRemoveAttr(el, 'v-once');
if (once != null) {
el.once = true;
}
} function processSlot (el) {
if (el.tag === 'slot') {
el.slotName = getBindingAttr(el, 'name');
if ("development" !== 'production' && el.key) {
warn$1(
"`key` does not work on <slot> because slots are abstract outlets " +
"and can possibly expand into multiple elements. " +
"Use the key on a wrapping element instead."
);
}
} else {
var slotTarget = getBindingAttr(el, 'slot');
if (slotTarget) {
el.slotTarget = slotTarget === '""' ? '"default"' : slotTarget;
}
if (el.tag === 'template') {
el.slotScope = getAndRemoveAttr(el, 'scope');
}
}
} function processComponent (el) {
var binding;
if ((binding = getBindingAttr(el, 'is'))) {
el.component = binding;
}
if (getAndRemoveAttr(el, 'inline-template') != null) {
el.inlineTemplate = true;
}
} function processAttrs (el) {
var list = el.attrsList;
var i, l, name, rawName, value, arg, modifiers, isProp;
for (i = 0, l = list.length; i < l; i++) {
name = rawName = list[i].name;
value = list[i].value;
if (dirRE.test(name)) {
// mark element as dynamic
el.hasBindings = true;
// modifiers
modifiers = parseModifiers(name);
if (modifiers) {
name = name.replace(modifierRE, '');
}
if (bindRE.test(name)) { // v-bind
name = name.replace(bindRE, '');
value = parseFilters(value);
if (modifiers) {
if (modifiers.prop) {
isProp = true;
name = camelize(name);
if (name === 'innerHtml') { name = 'innerHTML'; }
}
if (modifiers.camel) {
name = camelize(name);
}
}
if (isProp || platformMustUseProp(el.tag, name)) {
addProp(el, name, value);
} else {
addAttr(el, name, value);
}
} else if (onRE.test(name)) { // v-on
name = name.replace(onRE, '');
addHandler(el, name, value, modifiers);
} else { // normal directives
name = name.replace(dirRE, '');
// parse arg
var argMatch = name.match(argRE);
if (argMatch && (arg = argMatch[1])) {
name = name.slice(0, -(arg.length + 1));
}
addDirective(el, name, rawName, value, arg, modifiers);
if ("development" !== 'production' && name === 'model') {
checkForAliasModel(el, value);
}
}
} else {
// literal attribute
{
var expression = parseText(value, delimiters);
if (expression) {
warn$1(
name + "=\"" + value + "\": " +
'Interpolation inside attributes has been removed. ' +
'Use v-bind or the colon shorthand instead. For example, ' +
'instead of <div id="{{ val }}">, use <div :id="val">.'
);
}
}
addAttr(el, name, JSON.stringify(value));
}
}
} function checkInFor (el) {
var parent = el;
while (parent) {
if (parent.for !== undefined) {
return true
}
parent = parent.parent;
}
return false
} function parseModifiers (name) {
var match = name.match(modifierRE);
if (match) {
var ret = {};
match.forEach(function (m) { ret[m.slice(1)] = true; });
return ret
}
}

大家可以自行测试,当我们运行

parse("<div></div>",{})

这段代码时,返回的结果是:

【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

一个带有attrs和children等各种属性的ast对象。这个应该很好理解,比如我们现在可以将一个div表示成:

{
tag:"div",
attrs:[{name:"id",value:"div1"}],
children:[]
}

对应的dom就应该是:

<div id="div1"></div>

parse函数实现了这个转换的步骤,通过各种正则适配将html解析成ast对象;vue中有很多定制需求,比如代码:

function processFor (el) {
var exp;
if ((exp = getAndRemoveAttr(el, 'v-for'))) {
var inMatch = exp.match(forAliasRE);
if (!inMatch) {
"development" !== 'production' && warn$1(
("Invalid v-for expression: " + exp)
);
return
}
el.for = inMatch[2].trim();
var alias = inMatch[1].trim();
var iteratorMatch = alias.match(forIteratorRE);
if (iteratorMatch) {
el.alias = iteratorMatch[1].trim();
el.iterator1 = iteratorMatch[2].trim();
if (iteratorMatch[3]) {
el.iterator2 = iteratorMatch[3].trim();
}
} else {
el.alias = alias;
}
}
}

这段代码就是我们所用的v-for指令的基础解析了:让我们看看执行parse("<div v-for='a in b'></div>",{}) 会发生什么呢?

【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器

报错提示我们div是根节点不能添加v-for指令,恩,平时vue-for写到根节点的同学应该也有吧,报错是这里发出的。

生成的ast属性上多了两个个key, alias 和 for ,alias是别名的意思,for当然是for的对象了,最后生成的vnode节点对象会有一个context,当前作用域引用,应该会从context中调用这个对象进行for循环。

看到这里其实你可以看出,还有很多指令的实现方式都在这里完成,大家可以复制上面我分离出来的源码细细把玩,甚至你可以基于这个parse实现一个小vue,指令什么的一应具全。 例如v-if:

function processIf (el) {
var exp = getAndRemoveAttr(el, 'v-if');
if (exp) {
el.if = exp;
addIfCondition(el, {
exp: exp,
block: el
});
} else {
if (getAndRemoveAttr(el, 'v-else') != null) {
el.else = true;
}
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$1(
"v-" + (el.elseif ? ('else-if="' + el.elseif + '"') : 'else') + " " +
"used on element <" + (el.tag) + "> without corresponding v-if."
);
}
}

等等。

稍后,下一章节博主将要实现一个自己的parse,并记录我的实现逻辑,感兴趣的可以持续关注。我觉得可以通过这个出发点去统筹全局,理解vue的设计模型,在理解了observer绑定机制之后,再从parse开始解析,一直到生命周期,事件,等等。

写在后面

  mvvm框架和webpack的出现确实改变了前端的开发方式,使得学习前端变成了一门有着深入学问的课题。在我们日常开发中应该不断地学习,归纳,总结,寻找新的思想,对原有的代码有好的补充和好的改进。

写的不好,谢谢大家观看。 后续有空会新增更多关于开发的知识分享。

如果你有什么疑问,你可以联系我,或者在下方评论。