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

内容预览:
  • 它内部的实现机制值得让我们深究,比如obServer的实现原理,为什么vue能...~
  • 生成的ast属性上多了两个个key, alias 和 for ,alias是别名的意思,fo...~
  • 后续有空会新增更多关于开发的知识分享~

写在前面

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

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

  说明

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

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

      

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

Vue.js的架构一

  

 

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

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

  

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

  

  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函数:

    

    注释上的意思,这里将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 = /&#10;/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(
/<!--([sS]*?)-->/g, '$1')
.replace(
/<![CDATA[([sS]*?)]]>/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:" +
"nn" + template + "nn" +
"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:nn' + 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
}
}

View Code

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

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

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

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

{

tag:
"div",
attrs:[],
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>”,{}) 会发生什么呢?

报错提示我们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的出现确实改变了前端的开发方式,使得学习前端变成了一门有着深入学问的课题。在我们日常开发中应该不断地学习,归纳,总结,寻找新的思想,对原有的代码有好的补充和好的改进。

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

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

    

以上就是:【vuejs深入二】vue源码解析之一,基础源码结构和htmlParse解析器 的全部内容。

本站部分内容来源于互联网和用户投稿,如有侵权请联系我们删除,谢谢。
Email:[email protected]


0 条回复 A 作者 M 管理员
    所有的伟大,都源于一个勇敢的开始!
欢迎您,新朋友,感谢参与互动!欢迎您 {{author}},您在本站有{{commentsCount}}条评论