源码系列 - Vue2.0模版解析

115 阅读11分钟

整体构建流程

image.png

不同构建版本的解释

其实我一开始存在一个误区,觉得vue就是需要编译的。其实不是,真正需要编译的是不存在render的Vue实例

// 需要编译器
new Vue({
    template: '<div>{{ hi }}</div>'
})

// 不需要编译器
new Vue({
    render (h) {
        return h('div', this.hi)
    }
})

库版本解释

运行时+编译器

渲染目标的区别

el

类型:string | Element

提供一个在页面上已存在的 DOM 元素作为 Vue 实例的挂载目标。可以是 CSS 选择器,也可以是一个 HTMLElement 实例。 特性:

  1. 存在el时,实例自动编译,无需手动调用$mount编译
  2. 如果不存在template和render,挂载元素的html会被提取出来当作模版
  3. 所有的挂载元素会被 Vue 生成的 DOM 替换。因此不推荐挂载 root 实例到 或者 上
  4. 作为挂载元素,实例挂载后可以用vm.$el访问到挂载目标
import Vue from 'vue/dist/vue.common.js'
new Vue({
  el: '#app',
  // 这里为什么可以用对象而不是函数呢?
  data: {
    name: '坚果'
  },
  mounted(){
    console.log(this.$el)
  }
})
<html>
  <body>
    <div id="app">
      我叫{{name}}
    </div>
  </body>
</html>

image.png

template

类型:string

一个字符串模板 特性:

  1. 模版将替换挂载元素
import Vue from 'vue/dist/vue.common.js'
new Vue({
  el: '#app',
  template: '<div class="a">vue {{name}}</div>',
  data: {
    name: '坚果'
  },
})

image.png

  1. 如果值以 # 开始,则它将被用作选择符,并使用匹配元素的 innerHTML 作为模板(只取匹配到的id内的元素替换整个模版)
import Vue from 'vue/dist/vue.common.js'
new Vue({
  el: '#app',
  template: '#app',
  data: {
    name: '坚果'
  },
})
<html lang="">
  <body>
    <div id="app">
      // 注意这里
      <div class="new-app">html {{name}}</div>
    </div>
  </body>
</html>

image.png

render(无需经历编译)

字符串模板的代替方案,允许你发挥 JavaScript 最大的编程能力。该渲染函数接收一个 createElement 方法作为第一个参数用来创建 VNode

  1. 设置了render就不会再取template选择或者从el中提取模版渲染了
import Vue from 'vue/dist/vue.common.js'
new Vue({
  el: '#app',
  template: '<div class="a">vue {{name}}</div>',
  data: {
    name: '坚果'
  },
  render: function(createElement) {
    return createElement('div', 
      {
        class: 'name',
        style: 'color: red'
      },
      [
        createElement('div', this.name),
        createElement('div', { class: 'age'}, '11111')
      ])
  }
})

image.png

寻找模版

首先想编译,就需要找到模版。之前介绍api的时候已经提过了,就是el和template,所以我们先来看看是怎么获取到模版的,并且如何实现之前总结的一些特性。 总体来说分个步骤

  1. 根据传入的el获取挂载元素
  2. 没有传入render的情况下获取template
  3. 将template编译为render

image.png

$mount

上文提到的el特性1,存在el时,实例自动调用mount编译,不存在则需要手动调用mount编译

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    var vm = this;
    // a uid
    vm._uid = uid++;
    // 先暂时把$option假定为new Vue的参数
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    );
    // ....删掉了一大堆代码(initState,生命周期hook等),直接看我们这一期的核心代码
    
    // 存在el自动调用$mount
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
}

获取el

寻找模版这一节后续所有代码都包含在$mount内 首先el可能传入Element也可能传入字符串,所以需要用query抹平差异,最后都返回Element

// 首先在Vue构造函数的原型链上添加$mount,所以_init内能用this.$mount调用
Vue.prototype.$mount = function (
  el,
  hydrating
) {
  // el存在就抹平string和DOM的差异,最终得到DOM
  el = el && query(el);
}

function query (el) {
  // string(#id),获取Element
  if (typeof el === 'string') {
    var selected = document.querySelector(el);
    if (!selected) {
      warn(
        'Cannot find element: ' + el
      );
      // 没有匹配到Element,直接创建一个空div兜底
      return document.createElement('div')
    }
    return selected
  } else {
    // 返回Element
    return el
  }
}

所有的挂载元素会被 Vue 生成的 DOM 替换。因此不推荐挂载 root 实例到 或者 上

// 不允许挂载到body或者html上,因为会被Vue生成的dom替换(符合el特性4)
if (el === document.body || el === document.documentElement) {
  warn(
    "Do not mount Vue to <html> or <body> - mount to normal elements instead."
  );
  return this
}

获取template

  1. 判断是否存在render,存在直接忽略el和template选项,因为编译el和template目的就是为了获取render函数
  2. 是否存在template,存在需要区分2种情况,模版字符串和id
    1. id:通过idToTemplate函数获取template
    2. string模版字符串,直接去编译
var options = this.$options;
// 解析template/el并转换为render(存在render直接跳过)
if (!options.render) {
  var template = options.template;
  // 最终拿去编译的为template
  // 存在template模版将替换挂载的元素(符合template特性1)
  if (template) {
    // 存在template,template接收字符串模版或者#选择符
    if (typeof template === 'string') {
      if (template.charAt(0) === '#') {
        // 找到id对应的innerHtml(符合template特性2)
        template = idToTemplate(template);
        if (!template) {
          warn(
            ("Template element not found or is empty: " + (options.template)),
            this
          );
        }
      }
      // 是Element类型,获取innerHTML
    } else if (template.nodeType) {
      template = template.innerHTML;
    } else {
      {
        warn('invalid template option:' + template, this);
      }
      return this
    }
  }
}


// 寻找template中id对于的Element,返回其内容(存在缓存)
var idToTemplate = cached(function (id) {
  var el = query(id);
  return el && el.innerHTML
});

// 缓存函数
function cached (fn) {
  // 存储已经通过id寻找的template
  var cache = Object.create(null);
  return (function cachedFn (str) {
    // 存在直接返回
    var hit = cache[str];
    return hit || (cache[str] = fn(str))
  })
}
  1. 不存在template就通过el获取outerHTML(包含标签本身),然后赋值给template
if (template) {
  // ...忽略
} else {
  // 不存在template和render,挂载元素的html会被提取出来当作模版(符合el特性3)
  template = getOuterHTML(el);
}

function getOuterHTML (el) {
  // outerHTML也innerHTML的区别就是包含标签本身
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    // 个人理解至少在$mount这个流程中不会走到这,因为要么不存在el,所以getOuterHTML不执行,要么el存在经过query处理后至少会返回一个<div></div>
    var container = document.createElement('div');
    container.appendChild(el.cloneNode(true));
    return container.innerHTML
  }
}

编译模版

获取到template后将其编译成render函数

if (template) {
  // 开启编译
  var ref = compileToFunctions(template, {
    outputSourceRange: true, // 输出范围,编译中每个节点的范围
    delimiters: options.delimiters, // 改变纯文本插入分隔符 ['{{', '}}']
    comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
  }, this);
  var render = ref.render;
  var staticRenderFns = ref.staticRenderFns;
  // 保存render,用于生成vNode给diff使用
  options.render = render;
  options.staticRenderFns = staticRenderFns;
}
return mount.call(this, el, hydrating)

核心编译的代码,即将来袭,第一次看比较绕,我会拆出几个部分来表达

源码代码串梳理

image.png 接着用代码梳理一下执行顺序 首先执行createCompilerCreator函数

var createCompiler = createCompilerCreator(function baseCompile(){...});
function createCompilerCreator (baseCompile) {
  // 执行这部分
  return function createCompiler (baseOptions) {
    function compile (
      template,
      options
    ) {
        // ...暂时忽略一些代码
    }

    return {
      compile: compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

此时得到createCompiler函数然后执行

const { compile, compileToFunctions } = createCompiler(baseOptions);

createCompiler函数内部执行createCompileToFunctionFn

function createCompileToFunctionFn (compile) {
  var cache = Object.create(null);
  // 返回这个函数
  return function compileToFunctions (
    template,
    options,
    vm
  ) {
    // ... 暂时忽略一些代码
  }
}

最后执行得到的compileToFunctions,

if (template) {
  // 开启编译
  var ref = compileToFunctions(template, {
    outputSourceRange: true, // 输出范围,编译中每个节点的范围
    delimiters: options.delimiters, // 改变纯文本插入分隔符 ['{{', '}}']
    comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们
  }, this);
  var render = ref.render;
  var staticRenderFns = ref.staticRenderFns;
  // 保存render,用于生成vNode给diff使用
  options.render = render;
  options.staticRenderFns = staticRenderFns;
}

这一节是教大家怎么梳理函数执行顺序,但是因为存在太多的闭包调用和函数传递,所以接下来这一节就抛弃这些闭包按真正的执行顺序慢慢梳理

compileToFunctions

这个函数的核心功能就两个

  1. 缓存,缓存编译过的template,避免重复的编译工作量
  2. 调用编译函数compile
  3. 通过new Function将compile得到的字符串生成render函数
function createCompileToFunctionFn (compile) {
  // 创建存储缓存的对象{key: val}
  var cache = Object.create(null);

  return function compileToFunctions (
    template, // 模版
    options, // 参数
    vm // Vue实例
  ) {
    options = extend({}, options);
    // 检查缓存是否存在
    // 默认'{{,}}'+template,避免修改了delimiters
    var key = options.delimiters
      ? String(options.delimiters) + template
      : template;
    // 存在缓存直接抛出编译结果
    if (cache[key]) {
      return cache[key]
    }

    // 编译,compile来自createCompiler内
    var compiled = compile(template, options);

    var res = {};
    var fnGenErrors = [];
    // 这里非常重要,非常重要,非常重要,先暂时放一下,后续再聊
    // 这里的render也就是compileToFunctions执行后得到的render,用于生成vNode
    res.render = new Function(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
      return new Function(code, fnGenErrors)
    });
    // 缓存,return
    return (cache[key] = res)
  }
}

createCompiler

compile来自createCompiler函数,因为省掉了一些代码,所以目前核心目前就1点

  1. 调用createCompileToFunctionFn传递compile函数
export function createCompilerCreator (baseCompile) {
  return function createCompiler (baseOptions) {
    function compile (
      template,
      options
    ) {
      // ... 省去错误收集的源码
      // 真正的编译函数
      const compiled = baseCompile(template.trim())
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

compile核心就是调用baseCompile函数(本文重点,本文重点,本文重点) baseCompile函数核心功能如下

  1. 解析template
  2. 静态标记
  3. 生成render字符串
const createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
) {
  // 解析template为AST
  const ast = parse(template.trim(), options)
  // 静态标记
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  // 生成render字符串
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

parse

解析template为AST,核心代码就是parseHTML 想将一个字符串解析为AST,我们就需要识别以下这些东西

  1. 开始标签
  2. 结束标签
  3. 文本标签
  4. 注释

parseHTML结构也如上述枚举,区分这四种情况处理

// 将HTML模板字符串转化为AST
export function parse(template, options) {
   // ...
  parseHTML(template, {
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    shouldKeepComment: options.comments,
    // 当解析到开始标签时,调用该函数
    start (tag, attrs, unary) {

    },
    // 当解析到结束标签时,调用该函数
    end () {

    },
    // 当解析到文本时,调用该函数
    chars (text) {

    },
    // 当解析到注释时,调用该函数
    comment (text) {

    }
  })
  return root
}

parseHTML就是利用循环不断的截取html,匹配不同标签,直到html为空为止

function parseHTML(html, options) {
    // 节点栈
    const stack = [];
    // 索引
    let index = 0;
    // 当前循环的html,为了匹配文本而创建的变量
    let last;
    // 栈顶的节点名
    let lastTag;
    // 循环直到html为空(每次匹配都会裁剪html)
    while (html) {
        last = html;
        if (!lastTag) {
            // 注释,开始标签,结束标签以这个判断,不是<开头为文本标签
            let textEnd = html.indexOf('<');
            if (textEnd === 0) {
              // ...解析代码都在这个位置
            }
        }
    }
}
解析注释

HTML的注释比较简单,用正则匹配开头(),如果保留注释就添加到栈里,不保留注释就直接抛弃注释内容,截取html

var comment = /^<!\--/;
// 存在注释
if (comment.test(html)) {
  // 获取注释结尾的位置
  var commentEnd = html.indexOf('-->');
  if (commentEnd >= 0) {
    // 保留注释
    if (options.shouldKeepComment) {
      // 调用注释函数
      options.comment(
        html.substring(4, commentEnd), // 传入<!--xx-->的xx内容(这里的4是因为<!--长度固定为4)
        index, // 起始位置
        index + commentEnd + 3 // 结束位置(3是因为-->长度固定为3)
      );
    }
    // 移动索引和截取html字符串
    advance(commentEnd + 3);
    continue
  }
}

advance函数作用如下

  1. 移动索引,用于记录匹配节点的起始和结束位置(或者下一个节点的起始位置)
  2. 截取html
function advance (n) {
  index += n;
  html = html.substring(n);
}

比如如下demo

<!--注释-->
<div>坚果</div>
// 解析后html会变成,起始位置为0,结束位置9
<div>坚果</div>

options.comment函数就是之前说的parseHTML.comment函数,主要是为了维护AST栈

export function parse(template, options) {
   // ...
  parseHTML(template, {
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    comment: function comment (text, start, end) {
      var child = {
        type: 3, // 文本类型
        text: text, // 注释内容
        isComment: true // 标记为注释
      };
      // 开启了统计节点范围就记录开始和结束位置
      if (options.outputSourceRange) {
        child.start = start;
        child.end = end;
      }
      // 给当前父标签添加子标签,比如{type: 1, tag: 'div', children: []}
      currentParent.children.push(child);
    }
  });
}
解析开始标签

正则匹配标签,正则匹配标签,正则匹配标签,重要的事情说三遍。 解析开始标签最重要是获取以下几个信息

  1. 标签名
  2. 属性名和属性值

既然最重要是正则表达式,那么就先分析一下正则表达式,暂时忽略unicode码

分析正则

匹配起始和标签名 image.png 匹配普通属性和值 image.png 匹配动态属性和值 image.png 开始标签的结束点 image.png

匹配起始标签
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/;
const ncname = "[a-zA-Z_][\\-\\.0-9_a-zA-Z" + (unicodeRegExp.source) + "]*";
const qnameCapture = "((?:" + ncname + "\\:)?" + ncname + ")";
const startTagOpen = new RegExp(("^<" + qnameCapture));
const startTagClose = /^\s*(\/?)>/;
// 判断是否匹配到开始标签
var startTagMatch = parseStartTag();

/*
demo
<div class="jg" style="color:red">{{name}}</div>
*/ 
function parseStartTag () {
  // start = ['<div', 'div', index: 0, input: '<div class="jg" style="color:red">{{name}}</div>', groups: undefined]
  var start = html.match(startTagOpen);
  if (start) {
    // 存储开始标签的信息
    var match = {
      tagName: start[1], // 标签名
      attrs: [], // 属性
      start: index // 开始位置
    };
    /*
    移动index和截取html,html删掉<div
    html = (空格)class="jg" style="color:red">{{name}}</div>
    */
    advance(start[0].length);
    var end, attr;
    /*
      !(end = html.match(startTagClose)) 没有匹配>或者/>时去获取属性
      attr = html.match(dynamicArgAttribute) || html.match(attribute)获取动态和普通属性
    */
    while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
      /*
        第一次循环attr = [' class="jg"', 'class', '=', 'jg']
        第二次循环attr = [' style="color:red"', 'style', '=', 'color:red']
      */
      attr.start = index; // 记录属性起始位置
      advance(attr[0].length); // 截取字符串
      attr.end = index; // 记录属性结束位置
      match.attrs.push(attr); // 存储属性
    }
    // html = >{{name}}</div>
    // 匹配到>或者/>,
    if (end) {
      // 是否自闭合,/>
      match.unarySlash = end[1]; // (end[1] === '/')
      /*
        截取字符串和移动index
        html = {{name}}</div>
      */
      advance(end[0].length);
      // 记录开始标签结束位置
      match.end = index;
      return match
    }
  }
}

获取到的开始标签信息,需要整理一下startTagMatch.attrs的内容,将其整理成{class: 'jg', start, end},并且维护标签栈信息,最后触发parseHTML.start

/*
startTagMatch = {
  attrs: [
    [' class="jg"', 'class', '=', 'jg', start: 5, end: 15],
    [' style="color:red"', 'style', '=', 'color:red',start: 16, end: 33]
  ],
  end: 34
  start: 0
  tagName: "div"
  unarySlash: ""
}
*/
if (startTagMatch) {
  handleStartTag(startTagMatch);
  continue
}

function handleStartTag(match) {
  const tagName = match.tagName;
  const unarySlash = match.unarySlash;
  // 是否要求为正确的html
  if (expectHTML) {
    // 栈顶为p标签(当前匹配标签的父标签),内部不允许包含div等等元素,直接闭合
    // <p><div>1</div></p> -> <p></p>
    if (lastTag === 'p' && isNonPhrasingTag(tagName)) {
      parseEndTag(lastTag);
    }
    // <td>1<td>2</td></td> -> <td>1</td>
    if (canBeLeftOpenTag(tagName) && lastTag === tagName) {
      parseEndTag(tagName);
    }
  }
  // 是否不需要闭合的标签(link,meta等)或者已经闭合的标签,
  const unary = isUnaryTag(tagName) || !!unarySlash;
  const l = match.attrs.length;
  const attrs = new Array(l);
  // 修改match.attrs属性
  for (let i = 0; i < l; i++) {
    const args = match.attrs[i];
    /*
      args[3]:双引号内的内容
      args[4]:单引号内的内容
      args[5]:没符号包裹的值
    */
    const value = args[3] || args[4] || args[5] || '';
    attrs[i] = {
      name: args[1],
      value: value
    };
    if (options.outputSourceRange) {
      attrs[i].start = args.start + args[0].match(/^\s*/).length;
      attrs[i].end = args.end;
    }
  }
  // 没闭合就添加到标签栈顶
  if (!unary) {
    stack.push({
      tag: tagName,
      lowerCasedTag: tagName.toLowerCase(),
      attrs: attrs,
      start: match.start,
      end: match.end
    });
    // 修改栈顶标签
    lastTag = tagName;
  }
  // 调用parseHTML.start函数
  if (options.start) {
    options.start(tagName, attrs, unary, match.start, match.end);
  }
}  

接着就是调用parseHTML.start函数

生成AST
function parse(template, options) {
  // 父标签
  let currentParent
  // 是否拥有v-pre或者在v-pre的标签下
  let inVPre = false
  parseHTML(template, {
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    /*
     tag = div
     attrs = [{"name":"class","value":"jg","start":5,"end":15}, {"name":"style","value":"color:red","start":16,"end":33}]
     unary = false
     start = 0
     end = 34
    */
    start(tag, attrs, unary, start, end) {
      /*
        创建AST节点
        element = {
          "type":1,
          "tag":"div",
          "attrsList":[{"name":"class","value":"jg","start":5,"end":15},{"name":"style","value":"color:red","start":16,"end":33}],
          "attrsMap":{"class":"jg","style":"color:red"},
          "rawAttrsMap":{},
          "children":[]
        }
      */
      let element = createASTElement(tag, attrs, currentParent);
      {
        // 记录范围
        if (options.outputSourceRange) {
          element.start = start;
          element.end = end;
          element.rawAttrsMap = element.attrsList.reduce((cumulated, attr) => {
            cumulated[attr.name] = attr;
            return cumulated;
          }, {});
        }
      }
      // ... 后续代码衔接在这
    }
  })
}
// 创建AST节点
function createASTElement(tag, attrs, parent) {
  return {
    type: 1, // 标签节点为1
    tag,
    attrsList: attrs,
    attrsMap: makeAttrsMap(attrs),
    rawAttrsMap: {},
    parent, // 存储父标签
    children: [] // 存储子标签
  };
}
处理 v-pre
function parse(template, options) {
  // 父标签
  let currentParent
  // 是否拥有v-pre或者在v-pre的标签下
  let inVPre = false
  parseHTML(template, {
    // ...省略上节代码
    start(tag, attrs, unary, start, end) {
      // ...省略上节代码
      if (!inVPre) {
        // 处理是否存在v-pre属性
        processPre(element);
        if (element.pre) {
          inVPre = true;
        }
      }
      // 存在v-pre属性
      if (inVPre) {
        processRawAttrs(element);
      }
      // 下一部分代码
    }
  })
}

// 存在将element添加pre属性
function processPre(el) {
  if (getAndRemoveAttr(el, 'v-pre') != null) {
    el.pre = true;
  }
}
// 判断是否存在某个属性,并且删除attrsList内属性
function getAndRemoveAttr(el, name, removeFromMap) {
  let val;
  if ((val = el.attrsMap[name]) != null) {
    const list = el.attrsList;
    for (let i = 0, l = list.length; i < l; i++) {
      if (list[i].name === name) {
        // 删除v-pre属性
        list.splice(i, 1);
        break;
      }
    }
  }
  if (removeFromMap) {
    delete el.attrsMap[name];
  }
  return val;
}
// 处理属性,因为v-pre为直接跳过自身和子标签的编译,所以直接将属性值转为字符串即可
function processRawAttrs(el) {
  const list = el.attrsList;
  const len = list.length;
  if (len) {
    const attrs = (el.attrs = new Array(len));
    for (let i = 0; i < len; i++) {
      attrs[i] = {
        name: list[i].name,
        value: JSON.stringify(list[i].value)
      };
      if (list[i].start != null) {
        attrs[i].start = list[i].start;
        attrs[i].end = list[i].end;
      }
    }
  }
  else if (!el.pre) {
    // 没有属性的非根节点
    el.plain = true;
  }
}
处理v-for

v-if,v-once等等都差不多,都是给element添加属性

if (inVPre) {
  processRawAttrs(element);
} else {
  processFor(element);
  processIf(element);
  processOnce(element);
}
// ...代码衔接出

function processFor(el) {
  let exp;
  // 存在v-for属性
  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
    // 处理v-for
    const res = parseFor(exp);
    // 揉合element和res数据
    if (res) {
      extend(el, res);
    }
    else {
      warn(`Invalid v-for expression: ${exp}`, el.rawAttrsMap['v-for']);
    }
  }
}

// 匹配v-for="(item, index) in list"中的(item, index) in list
const forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
// 匹配逗号后的字符 ,index)
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/;
// 去除()
const stripParensRE = /^\(|\)$/g;
function parseFor(exp) {
  // inMatch = ['(item, index) in list', '(item, index)', 'list', index: 0, input: '(item, index) in list', groups: undefined] 
  const inMatch = exp.match(forAliasRE);
  if (!inMatch)
    return;
  const res = {};
  // 获取数据源,res.for = list
  res.for = inMatch[2].trim();
  // 删除(),alias = item, index
  const alias = inMatch[1].trim().replace(stripParensRE, '');
  // iteratorMatch = [', index', ' index', undefined, index: 4, input: 'item, index', groups: undefined] 
  const iteratorMatch = alias.match(forIteratorRE);
  if (iteratorMatch) {
    // 把,后的删除,res.alias = item
    res.alias = alias.replace(forIteratorRE, '').trim();
    // 存储index
    res.iterator1 = iteratorMatch[1].trim();
    // 可能循环对象(key, value, index),所以iteratorMatch[2]存在
    if (iteratorMatch[2]) {
      res.iterator2 = iteratorMatch[2].trim();
    }
  }
  else {
    // 不存在,后的内容
    res.alias = alias;
  }
  // res = {alias: "item", for: "list", iterator1: "index"}
  return res;
}
处理其它
// 是否存在根element,不存在赋值为当前element
if (!root) {
  root = element;
}
// 是否为自闭合标签
if (!unary) {
  // 修改栈顶标签为当前element
  currentParent = element;
  // 维护AST栈
  stack.push(element);
} else {
  // 自闭合标签
  closeElement(element);
}

function closeElement(element) {
  // 处理key,v-bind,v-on,ref等等
  if (!inVPre && !element.processed) {
    element = processElement(element, options);
  }
  // 是v-pre标签,修改inVPre值
  if (element.pre) {
    inVPre = false;
  }
}
解析结束标签

和匹配开始的标签差不多,只不过变成了</开头,>结尾 image.png

/*
还是之前的demo
<div class="jg" style="color:red">{{name}}</div>
html = </div>
*/ 
const endTag = /^<\/((?:[a-zA-Z_][\-\.0-9_a-zA-Z]*\:)?[a-zA-Z_][\-\.0-9_a-zA-Z]*)[^>]*>/
// endTagMatch = ['</div>', 'div', index: 0, input: '</div>', groups: undefined]
const endTagMatch = html.match(endTag);
if (endTagMatch) {
  // 记录起始索引
  const curIndex = index;
  // 截取html,移动索引
  advance(endTagMatch[0].length);
  // 传入标签名,起始位置,结束位置
  parseEndTag(endTagMatch[1], curIndex, index);
  continue;
}

function parseEndTag(tagName, start, end) {
  let pos, lowerCasedTagName;
  if (start == null)
    start = index;
  if (end == null)
    end = index;
  /*
    找到对应的标签栈的位置,此刻pos=0,可能存在<div><span></div>,所以需要找到对应的标签名
    stack = [{"tag":"div","lowerCasedTag":"div","attrs":[{"name":"class","value":"jg","start":5,"end":15},{"name":"style","value":"color:red","start":16,"end":33}],"start":0,"end":34}]
  */
  if (tagName) {
    lowerCasedTagName = tagName.toLowerCase();
    for (pos = stack.length - 1; pos >= 0; pos--) {
      if (stack[pos].lowerCasedTag === lowerCasedTagName) {
        break;
      }
    }
  }
  else {
    // If no tag name is provided, clean shop
    pos = 0;
  }
  // 匹配到对应的栈
  if (pos >= 0) {
    /* 
      闭合该标签下所有该标签
      1. <div><span>xx</div> -> <div><span>xx</span></div>
      2. <div>xx<span></div> -> <div>xx<span></span></div>
    */
    for (let i = stack.length - 1; i >= pos; i--) {
      if ((i > pos || !tagName) && options.warn) {
        options.warn(`tag <${stack[i].tag}> has no matching end tag.`, {
          start: stack[i].start,
          end: stack[i].end
        });
      }
      // parseHTML.end,闭合标签
      if (options.end) {
        // 传入正确的栈内起始标签信息
        options.end(stack[i].tag, start, end);
      }
    }
    // 出栈
    stack.length = pos;
    // 最后标签为出栈后栈顶的内容
    lastTag = pos && stack[pos - 1].tag;
  }
  // 特殊处理</br>,当作自闭合标签处理
  else if (lowerCasedTagName === 'br') {
    if (options.start) {
      options.start(tagName, [], true, start, end);
    }
  }
  // 特殊处理p, 1111</p> ->渲染成1111<p></p>
  else if (lowerCasedTagName === 'p') {
    if (options.start) {
      options.start(tagName, [], false, start, end);
    }
    if (options.end) {
      options.end(tagName, start, end);
    }
  }
}

parseHTML.end就很简单了,维护AST栈,修改当前标签即可

function parse(template, options) {
  let currentParent
  parseHTML(template, {
    end(tag, start, end) {
      const element = stack[stack.length - 1];
      // 出栈
      stack.length -= 1;
      currentParent = stack[stack.length - 1];
      if (options.outputSourceRange) {
        element.end = end;
      }
      closeElement(element);
    }
  }
}
解析文本
/*
  demo: <div class="jg" style="color:red">{{name}}文本<文本{{age}}</div>
  经过开始标签的解析,html = {{name}}文本<文本{{age}}</div>
*/ 
function parseHTML(html, options) {
    // 最后一个节点类型
    let lastTag;
    while (html) {
        last = html;
        if (!lastTag) {
            // 注释,开始标签,结束标签以这个判断,不是<开头为文本标签
            let textEnd = html.indexOf('<');
            if (textEnd === 0) {
              // ...省略其它解析
            } else {
              // 文本解析
              let text, rest, next;
              // {{name}}文本<文本{{age}}</div>时
              // textEnd = 10
              if (textEnd >= 0) {
                // rest = <文本{{age}}</div>
                rest = html.slice(textEnd);
                // 在文本内允许存在<字符,所以需要不端循环,直到匹配到结束标签,开始标签,注释
                while (!endTag.test(rest) &&
                       !startTagOpen.test(rest) &&
                       !comment.test(rest) &&
                       !conditionalComment.test(rest)) {
                  // 跳过第一个,next = 10
                  next = rest.indexOf('<', 1);
                  if (next < 0)
                    break;
                  textEnd += next;
                  rest = html.slice(textEnd);
                }
                // text = {{name}}文本<文本{{age}}
                text = html.substring(0, textEnd);
              }
              // 没有<,全部为文本
              if (textEnd < 0) {
                text = html;
              }
              // 截取html,移动index
              if (text) {
                advance(text.length);
              }
              // 触发parseHTML.chars
              if (options.chars && text) {
                options.chars(text, index - text.length, index);
              }
            }
        }
    }

function parse(template, options) {
  let currentParent
  parseHTML(template, {
    // ...
    chars(text, start, end) {
      // 必须用标签包裹,template = '111'会报错
      if (!currentParent) {
        {
          if (text === template) {
            warnOnce('Component template requires a root element, rather than just text.', { start });
          }
          else if ((text = text.trim())) {
            warnOnce(`text "${text}" outside root element will be ignored.`, {
              start
            });
          }
        }
        return;
      }
      // 文本必然为栈顶标签的子节点
      const children = currentParent.children;
      if (text) {
        let res;
        let child;
        // 不在v-pre标签内,是否是动态文本,也就是存在{{}}
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          };
        }
        // 普通文本
        else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          };
        }
        // 添加到父标签的children里,维护AST栈
        if (child) {
          if (options.outputSourceRange) {
            child.start = start;
            child.end = end;
          }
          children.push(child);
        }
      }
    }
  })
}

// 提取文本
function parseText(text, delimiters) {
  // const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;
  const tagRE = defaultTagRE;
  // 没有匹配到{{...}}
  if (!tagRE.test(text)) {
    return;
  }
  const tokens = [];
  const rawTokens = [];
  // tagRE.lastIndex:每次正则匹配的起始位置,1234,第一次匹配1,lastIndex = 1,第二次匹配就从2开始
  let lastIndex = (tagRE.lastIndex = 0);
  let match, index, tokenValue;
  // 开始匹配{{}}
  while ((match = tagRE.exec(text))) {
    /*
     第一次匹配match = ['{{name}}', 'name', index: 0, input: '{{name}}文本<文本{{age}}', groups: undefined]
     第二次匹配match = ['{{age}}', 'age', index: 13, input: '{{name}}文本<文本{{age}}', groups: undefined]
    */
    index = match.index;
    /*
     匹配普通文本
     第一次匹配不进入这里,因为index=0,lastIndex=0
     第二次匹配index=13,lastIndex=8
    */
    if (index > lastIndex) {
      rawTokens.push((tokenValue = text.slice(lastIndex, index)));
      tokens.push(JSON.stringify(tokenValue));
    }
    const exp = match[1].trim();
    // _s()后续会转为支持的函数,目前以字符串拼接
    tokens.push(`_s(${exp})`);
    rawTokens.push({ '@binding': exp });
    lastIndex = index + match[0].length;
  }
  // {{age}}文本,这种情况会把"文本"加入rawTokens,tokens
  if (lastIndex < text.length) {
    rawTokens.push((tokenValue = text.slice(lastIndex)));
    tokens.push(JSON.stringify(tokenValue));
  }
  /*
    expression:  "_s(name)+\"文本<文本\"+_s(age)",
   tokens: [@binding: "name", "文本<文本", {@binding: 'age'}]
  */
  return {
    expression: tokens.join('+'),
    tokens: rawTokens
  };
}

到这一步我们得到的AST结构为

{
  "type": 1,
  "tag": "div",
  "attrsList": [],
  "attrsMap": {
    "class": "jg",
    "style": "color:red"
  },
  "rawAttrsMap": {
    "class": {
      "name": "class",
      "value": "jg",
      "start": 5,
      "end": 15
    },
    "style": {
      "name": "style",
      "value": "color:red",
      "start": 16,
      "end": 33
    }
  },
  "children": [
    {
      "type": 2,
      "expression": "_s(name)+\"文本<文本\"+_s(age)",
      "tokens": [
        {
          "@binding": "name"
        },
        "文本<文本",
        {
          "@binding": "age"
        }
      ],
      "text": "{{name}}文本<文本{{age}}",
      "start": 34,
      "end": 54
    }
  ],
  "start": 0,
  "end": 60,
  "plain": false,
  "staticClass": "\"jg\"",
  "staticStyle": "{\"color\":\"red\"}"
}

generate

由于篇幅问题这里不再介绍如何生成render

function generate(ast, options) {
  const state = new CodegenState(options);
  const code = ast
  ? ast.tag === 'script'
  ? 'null'
  : genElement(ast, state)
  : '_c("div")';
  return {
    // 利用with绑定this,最后调用的this指向vue实例,所以可以访问到methods或者data等
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  };
}
<div class="jg" :class="[other]" style="color:red" @click="handleClick" v-if="show">
  {{name}}文本<文本{{age}}
  <div v-for="i in list" :key="i"></div>
</div>
// 输出render-------
`with(this){
  return (c) ? 
    _c('div',{
      staticClass:"jg",
      class:[other],
      staticStyle:{"color":"red"},
      on:{"click":handleClick}},
      [
        _v(_s(name)+"文本<文本"+_s(age)),
        _l((list), function(i){return _c('div',{key:i})})
      ],2)
    : _e()
}`

再次回到compileToFunctions

function createCompileToFunctionFn (compile) {
  // 创建存储缓存的对象{key: val}
  var cache = Object.create(null);

  return function compileToFunctions (
    template, // 模版
    options, // 参数
    vm // Vue实例
  ) {
    // 。。。
    var res = {};
    // 将刚刚返回的render字符串,利用new Function生成一个函数
    res.render = new Function(compiled.render, fnGenErrors);
    res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
      return new Function(code, fnGenErrors)
    });
    // 缓存,return
    return (cache[key] = res)
  }
}

image.png render是在渲染Watcher中执行如下函数

updateComponent = () => {
  // _render执向vm,所以with内的代码的this全部指向vm
  // 又因为渲染前已经做好了事件,data,methods等等代理,所以都可以访问到
  vm._update(vm._render(), hydrating);
};

也就是会遇到_c(),_l(),_s(),这些又是什么呢? 只有一个目的,根据各种情况生成vNode!!!

function installRenderHelpers(target) {
  // 其实没有这里没有_c,为了方便看直接加一起了
    target._c = (a, b, c, d) => createElement$1(vm, a, b, c, d, false);
    target._o = markOnce;
    target._n = toNumber;
    target._s = toString;
    target._l = renderList;
    target._t = renderSlot;
    target._q = looseEqual;
    target._i = looseIndexOf;
    target._m = renderStatic;
    target._f = resolveFilter;
    target._k = checkKeyCodes;
    target._b = bindObjectProps;
    target._v = createTextVNode;
    target._e = createEmptyVNode;
    target._u = resolveScopedSlots;
    target._g = bindObjectListeners;
    target._d = bindDynamicKeys;
    target._p = prependModifier;
}
// Vue引入的时候执行
function renderMixin(Vue) {
  // 注册辅助函数
  installRenderHelpers(Vue.prototype);
}

比如createElement$1

function createElement$1(context, tag, data, children, normalizationType, alwaysNormalize) {
  return new VNode(tag, data, children, undefined, undefined, context);
}

class VNode {
  constructor(tag, data, children, text, elm, context, componentOptions, asyncFactory) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
    this.ns = undefined;
    this.context = context;
    this.fnContext = undefined;
    this.fnOptions = undefined;
    this.fnScopeId = undefined;
    this.key = data && data.key;
    this.componentOptions = componentOptions;
    this.componentInstance = undefined;
    this.parent = undefined;
    this.raw = false;
    this.isStatic = false;
    this.isRootInsert = true;
    this.isComment = false;
    this.isCloned = false;
    this.isOnce = false;
    this.asyncFactory = asyncFactory;
    this.asyncMeta = undefined;
    this.isAsyncPlaceholder = false;
  }
  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child() {
    return this.componentInstance;
  }
}

经过这一串的处理,最终将vNode传递给patch,最后渲染页面。

参考资料

Vue源码系列-Vue中文社区