前言

此篇次要手写 Vue2.0 源码-模板编译原理

上一篇咱们次要介绍了 Vue 数据的响应式原理 对于中高级前端来说 响应式原理根本是面试 Vue 必考的源码根底类 如果不是很分明的话根本就被 pass 了 那么明天咱们手写的模板编译原理也是 Vue 面试比拟频繁的一个点 而且复杂程度是高于响应式原理的 外面次要波及到 ast 以及大量正则匹配 大家学习完能够看着思维导图一起手写一遍加深印象哈

适用人群: 没工夫去看官网源码或者看源码看的比拟懵而不想去看的同学


注释

// Vue实例化new Vue({  el: "#app",  data() {    return {      a: 111,    };  },  // render(h) {  //   return h('div',{id:'a'},'hello')  // },  // template:`<div id="a">hello</div>`});

下面这段代码 大家肯定不生疏 依照官网给出的生命周期图 咱们传入的 options 选项外面能够手动配置 template 或者是 render

留神一:平时开发中 咱们应用的是不带编译版本的 Vue 版本(runtime-only)间接在 options 传入 template 选项 在开发环境报错

留神二:这里传入的 template 选项不要和.vue 文件外面的<template>模板搞混同了 vue 单文件组件的 template 是须要 vue-loader 进行解决的

咱们传入的 el 或者 template 选项最初都会被解析成 render 函数 这样能力放弃模板解析的一致性

1.模板编译入口

// src/init.jsimport { initState } from "./state";import { compileToFunctions } from "./compiler/index";export function initMixin(Vue) {  Vue.prototype._init = function (options) {    const vm = this;    // 这里的this代表调用_init办法的对象(实例对象)    //  this.$options就是用户new Vue的时候传入的属性    vm.$options = options;    // 初始化状态    initState(vm);    // 如果有el属性 进行模板渲染    if (vm.$options.el) {      vm.$mount(vm.$options.el);    }  };  // 这块代码在源码外面的地位其实是放在entry-runtime-with-compiler.js外面  // 代表的是Vue源码外面蕴含了compile编译性能 这个和runtime-only版本须要辨别开  Vue.prototype.$mount = function (el) {    const vm = this;    const options = vm.$options;    el = document.querySelector(el);    // 如果不存在render属性    if (!options.render) {      // 如果存在template属性      let template = options.template;      if (!template && el) {        // 如果不存在render和template 然而存在el属性 间接将模板赋值到el所在的外层html构造(就是el自身 并不是父元素)        template = el.outerHTML;      }      // 最终须要把tempalte模板转化成render函数      if (template) {        const render = compileToFunctions(template);        options.render = render;      }    }  };}

咱们次要关怀$mount 办法 最终将解决好的 template 模板转成 render 函数

相干vue源码视频解说:进入学习

2.模板转化外围办法 compileToFunctions

// src/compiler/index.jsimport { parse } from "./parse";import { generate } from "./codegen";export function compileToFunctions(template) {  // 咱们须要把html字符串变成render函数  // 1.把html代码转成ast语法树  ast用来形容代码自身造成树结构 不仅能够形容html 也能形容css以及js语法  // 很多库都使用到了ast 比方 webpack babel eslint等等  let ast = parse(template);  // 2.优化动态节点  // 这个有趣味的能够去看源码  不影响外围性能就不实现了  //   if (options.optimize !== false) {  //     optimize(ast, options);  //   }  // 3.通过ast 从新生成代码  // 咱们最初生成的代码须要和render函数一样  // 相似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))  // _c代表创立元素 _v代表创立文本 _s代表文Json.stringify--把对象解析成文本  let code = generate(ast);  //   应用with语法扭转作用域为this  之后调用render函数能够应用call扭转this 不便code外面的变量取值  let renderFn = new Function(`with(this){return ${code}}`);  return renderFn;}

新建 compiler 文件夹 示意编译相干性能 外围导出 compileToFunctions 函数 次要有三个步骤 1.生成 ast 2.优化动态节点 3.依据 ast 生成 render 函数

3.解析 html 并生成 ast

// src/compiler/parse.js// 以下为源码的正则  对正则表达式不分明的同学能够参考小编之前写的文章(前端进阶高薪必看 - 正则篇);const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; //匹配标签名 形如 abc-123const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //匹配非凡标签 形如 abc:234 后面的abc:可有可无const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配标签开始 形如 <abc-123 捕捉外面的标签名const startTagClose = /^\s*(\/?)>/; // 匹配标签完结  >const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾 如 </abc-123> 捕捉外面的标签名const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性  形如 id="app"let root, currentParent; //代表根节点 和以后父节点// 栈构造 来示意开始和完结标签let stack = [];// 标识元素和文本typeconst ELEMENT_TYPE = 1;const TEXT_TYPE = 3;// 生成ast办法function createASTElement(tagName, attrs) {  return {    tag: tagName,    type: ELEMENT_TYPE,    children: [],    attrs,    parent: null,  };}// 对开始标签进行解决function handleStartTag({ tagName, attrs }) {  let element = createASTElement(tagName, attrs);  if (!root) {    root = element;  }  currentParent = element;  stack.push(element);}// 对完结标签进行解决function handleEndTag(tagName) {  // 栈构造 []  // 比方 <div><span></span></div> 当遇到第一个完结标签</span>时 会匹配到栈顶<span>元素对应的ast 并取出来  let element = stack.pop();  // 以后父元素就是栈顶的上一个元素 在这里就相似div  currentParent = stack[stack.length - 1];  // 建设parent和children关系  if (currentParent) {    element.parent = currentParent;    currentParent.children.push(element);  }}// 对文本进行解决function handleChars(text) {  // 去掉空格  text = text.replace(/\s/g, "");  if (text) {    currentParent.children.push({      type: TEXT_TYPE,      text,    });  }}// 解析标签生成ast外围export function parse(html) {  while (html) {    // 查找<    let textEnd = html.indexOf("<");    // 如果<在第一个 那么证实接下来就是一个标签 不论是开始还是完结标签    if (textEnd === 0) {      // 如果开始标签解析有后果      const startTagMatch = parseStartTag();      if (startTagMatch) {        // 把解析好的标签名和属性解析生成ast        handleStartTag(startTagMatch);        continue;      }      // 匹配完结标签</      const endTagMatch = html.match(endTag);      if (endTagMatch) {        advance(endTagMatch[0].length);        handleEndTag(endTagMatch[1]);        continue;      }    }    let text;    // 形如 hello<div></div>    if (textEnd >= 0) {      // 获取文本      text = html.substring(0, textEnd);    }    if (text) {      advance(text.length);      handleChars(text);    }  }  // 匹配开始标签  function parseStartTag() {    const start = html.match(startTagOpen);    if (start) {      const match = {        tagName: start[1],        attrs: [],      };      //匹配到了开始标签 就截取掉      advance(start[0].length);      // 开始匹配属性      // end代表完结符号>  如果不是匹配到了完结标签      // attr 示意匹配的属性      let end, attr;      while (        !(end = html.match(startTagClose)) &&        (attr = html.match(attribute))      ) {        advance(attr[0].length);        attr = {          name: attr[1],          value: attr[3] || attr[4] || attr[5], //这里是因为正则捕捉反对双引号 单引号 和无引号的属性值        };        match.attrs.push(attr);      }      if (end) {        //   代表一个标签匹配到完结的>了 代表开始标签解析结束        advance(1);        return match;      }    }  }  //截取html字符串 每次匹配到了就往前持续匹配  function advance(n) {    html = html.substring(n);  }  //   返回生成的ast  return root;}

利用正则 匹配 html 字符串 遇到开始标签 完结标签和文本 解析结束之后生成对应的 ast 并建设相应的父子关联 一直的 advance 截取残余的字符串 直到 html 全副解析结束 咱们这里次要写了对于开始标签外面的属性的解决--parseStartTag

4.依据 ast 从新生成代码

// src/compiler/codegen.jsconst defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; //匹配花括号 {{  }} 捕捉花括号外面的内容function gen(node) {  // 判断节点类型  // 次要蕴含解决文本外围  // 源码这块蕴含了简单的解决  比方 v-once v-for v-if 自定义指令 slot等等  咱们这里只思考一般文本和变量表达式{{}}的解决  // 如果是元素类型  if (node.type == 1) {    //   递归创立    return generate(node);  } else {    //   如果是文本节点    let text = node.text;    // 不存在花括号变量表达式    if (!defaultTagRE.test(text)) {      return `_v(${JSON.stringify(text)})`;    }    // 正则是全局模式 每次须要重置正则的lastIndex属性  不然会引发匹配bug    let lastIndex = (defaultTagRE.lastIndex = 0);    let tokens = [];    let match, index;    while ((match = defaultTagRE.exec(text))) {      // index代表匹配到的地位      index = match.index;      if (index > lastIndex) {        //   匹配到的{{地位  在tokens外面放入一般文本        tokens.push(JSON.stringify(text.slice(lastIndex, index)));      }      //   放入捕捉到的变量内容      tokens.push(`_s(${match[1].trim()})`);      //   匹配指针后移      lastIndex = index + match[0].length;    }    // 如果匹配完了花括号  text外面还有残余的一般文本 那么持续push    if (lastIndex < text.length) {      tokens.push(JSON.stringify(text.slice(lastIndex)));    }    // _v示意创立文本    return `_v(${tokens.join("+")})`;  }}// 解决attrs属性function genProps(attrs) {  let str = "";  for (let i = 0; i < attrs.length; i++) {    let attr = attrs[i];    // 对attrs属性外面的style做非凡解决    if (attr.name === "style") {      let obj = {};      attr.value.split(";").forEach((item) => {        let [key, value] = item.split(":");        obj[key] = value;      });      attr.value = obj;    }    str += `${attr.name}:${JSON.stringify(attr.value)},`;  }  return `{${str.slice(0, -1)}}`;}// 生成子节点 调用gen函数进行递归创立function getChildren(el) {  const children = el.children;  if (children) {    return `${children.map((c) => gen(c)).join(",")}`;  }}// 递归创立生成codeexport function generate(el) {  let children = getChildren(el);  let code = `_c('${el.tag}',${    el.attrs.length ? `${genProps(el.attrs)}` : "undefined"  }${children ? `,${children}` : ""})`;  return code;}

拿到生成好的 ast 之后 须要把 ast 转化成相似_c('div',{id:"app"},_c('div',undefined,_v("hello"+_s(name)),_c('span',undefined,_v("world"))))这样的字符串

5.code 字符串生成 render 函数

export function compileToFunctions(template) {  let code = generate(ast);  // 应用with语法扭转作用域为this  之后调用render函数能够应用call扭转this 不便code外面的变量取值 比方 name值就变成了this.name  let renderFn = new Function(`with(this){return ${code}}`);  return renderFn;}

小结

至此 Vue 的模板编译原理曾经完结 大家能够看着思维导图本人入手写一遍外围代码哈 须要留神的是 本篇大量应用字符串拼接以及正则相干的常识 遇到不懂的中央能够多查阅材料 也欢送评论留言