写在前面

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

  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是怎么被解析成 的?指令的操作,变量的转换,运算符的操作。其实就是模板引擎的实现方式是怎样的?

  htmlparser

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

    

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

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

    

var hasProto = '__proto__' in {};// Browser environment sniffingvar inBrowser =  typeof window !== 'undefined' &&   .prototype.toString.call(window) !== '[   ]';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 === ' ' && (      !el.attrsMap.type ||      el.attrsMap.type === 'text/ '    ))  )}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 =  .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  s for parsing tags and attributesvar 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- -names-19990114/#NT-QName// but for Vue templates we can enforce a simple charsetvar 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 is OrStyle = makeMap(' ,style', true);var hasLang = function (attr) { return attr.name === 'lang' && attr.value !== 'html'; };var isSpecialTag = function (tag, isSFC, stack) {  if (is OrStyle(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, '
');  }  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   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 !== ' ' && stackedTag !== 'style' && stackedTag !== 'no ') {          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,  , 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 (  === undefined) {        // first filter, end of          lastFilterIndex = i + 1;          = 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 (  === undefined) {      = 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++) {        = wrapFilter( , filters[i]);    }  }  return  }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  Warn (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  Pos;var  EndPos;/** * 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 =  Pos =  EndPos = 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,  Pos),    idx: val.substring( Pos + 1,  EndPos)  }}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;   Pos = index$1;  while (!eof()) {    chr = next();    if (isStringStart(chr)) {      parseString(chr);      continue    }    if (chr === 0x5B) { inBracket++; }    if (chr === 0x5D) { inBracket--; }    if (inBracket === 0) {       EndPos = 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 statevar 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 ||  Warn;  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  ;        if (!inVPre && text !== ' ' && (  = parseText(text, delimiters))) {          currentParent.children.push({            type: 2,             :  ,            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  : " + 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   = parseText(value, delimiters);        if ( ) {          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:[{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  : " + 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的出现确实改变了前端的开发方式,使得学习前端变成了一门有着深入学问的课题。在我们日常开发中应该不断地学习,归纳,总结,寻找新的思想,对原有的代码有好的补充和好的改进。

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

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

    

========================================================转载请注明出处。
收藏 打印