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


    写在前面

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

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

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

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

        

  • 相关阅读:
    Python3 实现一个简单的TCP 客户端
    Mac 下 安装 和 使用 Go 框架 Beego
    Go 操作文件及文件夹 os.Mkdir及os.MkdirAll两者的区别
    Go gin 之 Air 实现实时加载
    Mac os 配置常用alias
    Mac 下 MAMP配置虚拟主机
    Thinkphp5 项目部署至linux服务器报403错误
    Linux 安装最新版 node.js 之坑
    Mac item2如何使用rz sz 上传下载命令
    Mac 使用 iTerm2 快捷登录远程服务器
  • 原文地址:https://www.cnblogs.com/ztfjs/p/vue2.html
Copyright © 2020-2023  润新知