前言
本文是vue2.x源码分析的第六篇,主要讲解编译compile过程!
调用方式
var compiled = compile(template, options);
1 分析compile
//tips:请结合断点调试,该函数位于闭包createCompiler中,有的变量是在上层函数中定义的function compile (template, options) {var finalOptions = Object.create(baseOptions);var errors = [];var tips = [];finalOptions.warn = function (msg, tip$$1) {(tip$$1 ? tips : errors).push(msg);};if (options) {//合并自定义modulesif (options.modules) {finalOptions.modules = (baseOptions.modules || []).concat(options.modules);}//合并自定义directivesif (options.directives) {finalOptions.directives = extend(Object.create(baseOptions.directives),options.directives);}// copy other optionsfor (var key in options) {if (key !== 'modules' && key !== 'directives') {finalOptions[key] = options[key];}}}/*以上都是处理finalOptions,到这里finalOptions如下:{delimiters:undefined,shouldDecodeNewlines:false,warn:function (msg, tip$$1),__proto__:Object}这个__proto__指向一个预先定义好的baseOptions对象,该对象长这样:var baseOptions = {expectHTML: true,modules: modules$1,//modules$1=[klass$1,style$1]directives: directives$1, //这里预先定义了html,text,model三个指令isPreTag: isPreTag,isUnaryTag: isUnaryTag,mustUseProp: mustUseProp,canBeLeftOpenTag: canBeLeftOpenTag,isReservedTag: isReservedTag,getTagNamespace: getTagNamespace,staticKeys: genStaticKeys(modules$1)};*/var compiled = baseCompile(template, finalOptions); //主要函数{errors.push.apply(errors, detectErrors(compiled.ast));}compiled.errors = errors;compiled.tips = tips;return compiled}
来看看baseCompile(template, finalOptions)
function baseCompile (template,options) {var ast = parse(template.trim(), options); //主要函数1optimize(ast, options);var code = generate(ast, options); //主要函数2return {ast: ast,render: code.render,staticRenderFns: code.staticRenderFns}}
2 分析 parse(template.trim(), options);
//主要是调用parseHTML(html, options)解析html,返回结果ast是含有如下属性的对象// attrs:Array// attrsList:Array// attrsMap:Object// children:Array// parent:undefined// plain:false// static:false// staticRoot:false// tag:"div"// type:1// __proto__:Objectfunction parse (template,options) {warn$2 = options.warn || baseWarn;platformGetTagNamespace = options.getTagNamespace || no;platformMustUseProp = options.mustUseProp || no;platformIsPreTag = options.isPreTag || no;//这个pluckModuleFunction函数作用就是从options.modules中取出key为'preTransformNode'的值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;function warnOnce (msg) {if (!warned) {warned = true;warn$2(msg);}}function endPre (element) {// check pre stateif (element.pre) {inVPre = false;}if (platformIsPreTag(element.tag)) {inPre = false;}}//parseHTML第二个参数里有很重要的三个函数:start,end,charsparseHTML(template, {warn: warn$2,expectHTML: options.expectHTML,isUnaryTag: options.isUnaryTag,canBeLeftOpenTag: options.canBeLeftOpenTag,shouldDecodeNewlines: options.shouldDecodeNewlines,//start和end函数负责构建节点树start: function start (tag, attrs, unary) {// check namespace.// inherit parent ns if there is onevar 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;}//不处理style和script标签if (isForbiddenTag(element) && !isServerRendering()) {element.forbidden = true;"development" !== 'production' && warn$2('Templates should only be responsible for mapping the state to the ' +'UI. Avoid placing tags with side-effects in your templates, such as ' +"<" + tag + ">" + ', as they will not be parsed.');}// 猜测:html如果用了其他的模板,如ejs等需要先转换// apply pre-transformsfor (var i = 0; i < preTransforms.length; i++) {preTransforms[i](element, options);}//处理v-pre指令if (!inVPre) {processPre(element);if (element.pre) {inVPre = true;}}if (platformIsPreTag(element.tag)) {inPre = true;}//如果含有v-pre指令,则直接调用processRawAttrs(element);处理原始属性if (inVPre) {processRawAttrs(element);} else {processFor(element);//处理v-for指令,会将v-for='xx'替换成其他字符串processIf(element);//处理v-if指令processOnce(element);//处理v-once指令processKey(element);//处理key// determine whether this is a plain element after// removing structural attributes// 移除结构性属性后判断该元素是不是plain元素element.plain = !element.key && !attrs.length;processRef(element);//处理refprocessSlot(element);//处理slotprocessComponent(element);//处理componentfor (var i$1 = 0; i$1 < transforms.length; i$1++) {transforms[i$1](element, options); //对class和style属性进行处理}//以上处理了v-for,v-if,v-once,v-pre等指令,但还有其它指令,如v-on,v-bind,//以及它们的快捷写法'@:',':',该函数就是处理这些指令以及普通元素,处理的结果就是//在element上加了一个attrs属性,存放原始属性processAttrs(element);}function checkRootConstraints (el) {{if (el.tag === 'slot' || el.tag === 'template') {warnOnce("Cannot use <" + (el.tag) + "> as component root element because it may " +'contain multiple nodes.');}if (el.attrsMap.hasOwnProperty('v-for')) {warnOnce('Cannot use v-for on stateful component root element because ' +'it renders multiple elements.');}}}// 经过上述处理后,由于可能有v-if这种会改变树结构的指令,所以需要对结构树// 进一步处理,至此第一轮while循环解析完成,接下来就是重复这个过程了if (!root) {root = element;checkRootConstraints(root); //根节点不能是slot/template元素,且不能含有v-for指令} else if (!stack.length) {// 允许根元素使用 v-if, v-else-if and v-elseif (root.if && (element.elseif || element.else)) {checkRootConstraints(element);addIfCondition(root, {exp: element.elseif,block: element});} else {warnOnce("Component template should contain exactly one root element. " +"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 slotcurrentParent.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);} else {endPre(element);}// apply post-transformsfor (var i$2 = 0; i$2 < postTransforms.length; i$2++) {postTransforms[i$2](element, options);}},end: function end () {// 删除尾随空格var element = stack[stack.length - 1];var lastNode = element.children[element.children.length - 1];if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) {element.children.pop();}// pop stackstack.length -= 1;currentParent = stack[stack.length - 1];endPre(element);},chars: function chars (text) {if (!currentParent) {{if (text === template) {warnOnce('Component template requires a root element, rather than just text.');} else if ((text = text.trim())) {warnOnce(("text \"" + text + "\" outside root element will be ignored."));}}return}// IE textarea placeholder bug/* istanbul ignore if */if (isIE &¤tParent.tag === 'textarea' &¤tParent.attrsMap.placeholder === text) {return}var children = currentParent.children;text = inPre || text.trim()? decodeHTMLCached(text)// only preserve whitespace if its not right after a starting tag: preserveWhitespace && children.length ? ' ' : '';if (text) {var expression;if (!inVPre && text !== ' ' && (expression = parseText(text, delimiters))) {children.push({type: 2,expression: expression,text: text});} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {children.push({type: 3,text: text});}}}});return root}
来看看parseHTML(template,options)
/*解析过程中最重要的函数,因此代码量较大*/function parseHTML (html, options) { //将template传给htmlvar stack = [];var expectHTML = options.expectHTML;var isUnaryTag$$1 = options.isUnaryTag || no; //是否是一元标签var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no;var index = 0;var last, lastTag;while (html) { //通过while循环一步步处理html,每处理一步就缩短html,直至html为空last = html;// 不处理script/style/textarea元素if (!lastTag || !isPlainTextElement(lastTag)) {var textEnd = html.indexOf('<');if (textEnd === 0) {// 当匹配到Comment,只对html推进,不做其他处理if (comment.test(html)) {var commentEnd = html.indexOf('-->');if (commentEnd >= 0) {advance(commentEnd + 3);continue}}//当匹配到conditionalComment,同Comment一样处理// http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_commentif (conditionalComment.test(html)) {var conditionalEnd = html.indexOf(']>')if (conditionalEnd >= 0) {advance(conditionalEnd + 2);continue}}//当匹配到doctype,同Comment一样处理,/^<!DOCTYPE [^>]+>/ivar doctypeMatch = html.match(doctype);if (doctypeMatch) {advance(doctypeMatch[0].length);continue}// 当匹配到end tag,同Comment一样处理,/^<\/((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)[^>]*>/var endTagMatch = html.match(endTag);if (endTagMatch) {var curIndex = index;advance(endTagMatch[0].length);parseEndTag(endTagMatch[1], curIndex, index);continue}// 除以上四种,就默认以下处理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 textnext = 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 (!isPlainTextElement(stackedTag) && 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, index - endTagLength, index);}if (html === last) {options.chars && options.chars(html);if ("development" !== 'production' && !stack.length && options.warn) {options.warn(("Mal-formatted tag at end of template: \"" + html + "\""));}break}}// Clean up any remaining tagsparseEndTag();function advance (n) {index += n;html = html.substring(n);}function parseStartTag () {//startTagOpen='/^<((?:[a-zA-Z_][\w\-\.]*\:)?[a-zA-Z_][\w\-\.]*)/'var start = html.match(startTagOpen);if (start) {var match = {tagName: start[1],attrs: [],start: index};advance(start[0].length);var end, attr;//开始寻找属性//startTagClose='/^\s*(\/?)>/'//attribute='/^\s*([^\s"'<>\/=]+)(?:\s*((?:=))\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/'while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {advance(attr[0].length);match.attrs.push(attr);}if (end) {match.unarySlash = end[1];//若'/'存在,则赋值给unarySlashadvance(end[0].length);match.end = index;return match //至此,parseStartTag结束,接下来执行handleStartTag(match);match此时长这样/*attrs:Array(1)end:14start:0tagName:"div"unarySlash:""__proto__:Object*/}}}function handleStartTag (match) {var tagName = match.tagName;var unarySlash = match.unarySlash;if (expectHTML) {if (lastTag === 'p' && isNonPhrasingTag(tagName)) {parseEndTag(lastTag);}if (canBeLeftOpenTag$$1(tagName) && lastTag === tagName) {parseEndTag(tagName);}}var unary = isUnaryTag$$1(tagName) || tagName === 'html' && lastTag === 'head' || !!unarySlash;var l = match.attrs.length;//新建一个attrs属性,遍历match.attrs,使得attrs=[{name:'id',value:'app'}]这种map结构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=369778if (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, lowerCasedTag: tagName.toLowerCase(), attrs: attrs });lastTag = tagName;}//这个是最重要的函数,对一些特殊的属性做特殊处理,例如指令属性v-text='message'if (options.start) {options.start(tagName, attrs, unary, match.start, match.end);//tips:返回到parse函数中看start执行过程}}function parseEndTag (tagName, start, end) {var pos, lowerCasedTagName;if (start == null) { start = index; }if (end == null) { end = index; }if (tagName) {lowerCasedTagName = tagName.toLowerCase();}// Find the closest opened tag of the same typeif (tagName) {for (pos = stack.length - 1; pos >= 0; pos--) {if (stack[pos].lowerCasedTag === lowerCasedTagName) {break}}} else {// If no tag name is provided, clean shoppos = 0;}if (pos >= 0) {// Close all the open elements, up the stackfor (var i = stack.length - 1; i >= pos; i--) {if ("development" !== 'production' &&(i > pos || !tagName) &&options.warn) {options.warn(("tag <" + (stack[i].tag) + "> has no matching end tag."));}if (options.end) {options.end(stack[i].tag, start, end);}}// Remove the open elements from the stackstack.length = pos;lastTag = pos && stack[pos - 1].tag;} else if (lowerCasedTagName === 'br') {if (options.start) {options.start(tagName, [], true, start, end);}} else if (lowerCasedTagName === 'p') {if (options.start) {options.start(tagName, [], false, start, end);}if (options.end) {options.end(tagName, start, end);}}}}
以上这个过程只是一个解析过程,将相应的属性放到相应的位置,但是还没有产生可执行代码,以下
generate函数的作用就是根据这些属性来产生相应的代码。
3 分析 generate(ast, options)
// 温故下,返回结果ast是含有如下属性的对象// ```javascript// attrs:Array //保存原始的html特性// attrsList:Array// attrsMap:Object// children:Array// parent:undefined// plain:false// static:false// staticRoot:false// tag:"div"// type:1// __proto__:Objectfunction generate (ast,options) {// save previous staticRenderFns so generate calls can be nestedvar prevStaticRenderFns = staticRenderFns;var currentStaticRenderFns = staticRenderFns = [];var prevOnceCount = onceCount;onceCount = 0;currentOptions = options;warn$3 = options.warn || baseWarn;transforms$1 = pluckModuleFunction(options.modules, 'transformCode');dataGenFns = pluckModuleFunction(options.modules, 'genData');platformDirectives$1 = options.directives || {};isPlatformReservedTag$1 = options.isReservedTag || no;var code = ast ? genElement(ast) : '_c("div")'; //主要函数,执行genElement(ast)staticRenderFns = prevStaticRenderFns;onceCount = prevOnceCount;return {render: ("with(this){return " + code + "}"),staticRenderFns: currentStaticRenderFns}}
来看看 genElement(ast)
//根据ast的属性是否有once,for,if,slot,component等属性执行不同函数,否则当普通元素处理并执行genData和genChildren函数,这两个函数都是在做字符串的拼装工作,最后返回拼装完成的code字符串function genElement (el) {if (el.staticRoot && !el.staticProcessed) { //静态节点return genStatic(el)} else if (el.once && !el.onceProcessed) { //v-once节点return genOnce(el)} else if (el.for && !el.forProcessed) { //v-for节点return genFor(el)} else if (el.if && !el.ifProcessed) { //v-if节点return genIf(el)} else if (el.tag === 'template' && !el.slotTarget) {return genChildren(el) || 'void 0'} else if (el.tag === 'slot') { //处理slotreturn genSlot(el)} else {// component or elementvar code;if (el.component) { //处理组件节点code = genComponent(el.component, el);} else {var data = el.plain ? undefined : genData(el); //处理元素节点的datavar children = el.inlineTemplate ? null : genChildren(el, true);//处理元素节点的子元素code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";}// module transformsfor (var i = 0; i < transforms$1.length; i++) {code = transforms$1[i](el, code);}return code}}
来看看genData(el)
//整个函数都是在做data字符串的拼装工作,最后返回datafunction genData (el) {var data = '{';// 首先处理指令,因为在el产生之前,指令可能会改变el的其他属性var dirs = genDirectives(el);if (dirs) { data += dirs + ','; }// 处理key属性if (el.key) {data += "key:" + (el.key) + ",";}// 处理ref属性if (el.ref) {data += "ref:" + (el.ref) + ",";}if (el.refInFor) {data += "refInFor:true,";}// 处理v-pre指令if (el.pre) {data += "pre:true,";}// record original tag name for components using "is" attributeif (el.component) { //处理组件data += "tag:\"" + (el.tag) + "\",";}// 处理class和style属性for (var i = 0; i < dataGenFns.length; i++) {data += dataGenFns[i](el);}//处理特性 attributesif (el.attrs) {data += "attrs:{" + (genProps(el.attrs)) + "},";}//处理属性 DOM propertyif (el.props) {data += "domProps:{" + (genProps(el.props)) + "},";}//处理事件if (el.events) {data += (genHandlers(el.events)) + ",";}//处理本地事件if (el.nativeEvents) {data += (genHandlers(el.nativeEvents, true)) + ",";}处理slot目标if (el.slotTarget) {data += "slot:" + (el.slotTarget) + ",";}//处理scoped的slotif (el.scopedSlots) {data += (genScopedSlots(el.scopedSlots)) + ",";}//处理v-modelif (el.model) {data += "model:{value:" + (el.model.value) + ",callback:" + (el.model.callback) + ",expression:" + (el.model.expression) + "},";}// 处理内联模板if (el.inlineTemplate) {var inlineTemplate = genInlineTemplate(el);if (inlineTemplate) {data += inlineTemplate + ",";}}data = data.replace(/,$/, '') + '}';// v-bind data wrap 处理v-bindif (el.wrapData) {data = el.wrapData(data);}return data}
来看看genDirectives(el)
function genDirectives (el) {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 = platformDirectives$1[dir.name] || baseDirectives[dir.name];if (gen) {// compile-time directive that manipulates AST.// returns true if it also needs a runtime counterpart.needRuntime = !!gen(el, dir, warn$3);}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.arg) + "\"") : '') + (dir.modifiers ? (",modifiers:" + (JSON.stringify(dir.modifiers))) : '') + "},";}}if (hasRuntime) {return res.slice(0, -1) + ']'}}
来看看genChildren
function genChildren (el, checkSkip) {var children = el.children;if (children.length) {var el$1 = children[0];// optimize single v-forif (children.length === 1 &&el$1.for &&el$1.tag !== 'template' &&el$1.tag !== 'slot') {return genElement(el$1) //只有一个子元素并且有v-for属性时,递归调用genElement}var normalizationType = checkSkip ? getNormalizationType(children) : 0;return ("[" + (children.map(genNode).join(',')) + "]" + (normalizationType ? ("," + normalizationType) : ''))}}
来看看genNode
function genNode (node) {if (node.type === 1) { //type=1,表示是元素节点,递归调用genElementreturn genElement(node)} else {return genText(node) //按照vue的用法,不是元素节点就只能是文本节点了}}/*function genText (text) {return ("_v(" + (text.type === 2? text.expression // no need for () because already wrapped in _s(): transformSpecialNewlines(JSON.stringify(text.text))) + ")")}*/
4 小结
compile过程即baseCompile过程:
- 调用parse函数对原始模板进行解析得到ast;
- 调用generate函数处理ast得到最终render函数
- 本篇偏向对编译的整体过程分析,没有对诸如指令到底是怎么编译的进行分析,后面章节将结合实例具体分析指令等编译过程,让我们先瞅瞅vue一共提供了哪些内置指令:
v-textv-htmlv-showv-ifv-elsev-else-ifv-forv-onv-bindv-modelv-prev-cloakv-once
除此之外,还有三个特殊属性key,ref,slot以及内置组件component,transition,transition-group,keep-alive,slot,接下来的章节将对这些内容进行分析
