写在前面
一个好的架构需要经过血与火的历练,一个好的工程师需要经过无数项目的摧残。
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 = /</g; var gtRE = />/g; var nlRE = / /g; var ampRE = /&/g; var quoteRE = /"/g; function decodeAttr (value, shouldDecodeNewlines) { if (shouldDecodeNewlines) { value = value.replace(nlRE, ' '); } 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 = /{{((?:.| )+?)}}/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: ' + 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: ' + 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:" + " " + template + " " + "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: ' + 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>",{})
这段代码时,返回的结果是:
一个带有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>",{}) 会发生什么呢?
报错提示我们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的出现确实改变了前端的开发方式,使得学习前端变成了一门有着深入学问的课题。在我们日常开发中应该不断地学习,归纳,总结,寻找新的思想,对原有的代码有好的补充和好的改进。
写的不好,谢谢大家观看。 后续有空会新增更多关于开发的知识分享。
如果你有什么疑问,你可以联系我,或者在下方评论。