关于vue.js:深度学习Vue源码模板编译原理

5次阅读

共计 6881 个字符,预计需要花费 18 分钟才能阅读完成。

前言

此篇次要手写 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.js

import {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.js

import {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-123
const 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 = [];
// 标识元素和文本 type
const 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.js

const 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(",")}`;
  }
}
// 递归创立生成 code
export 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 的模板编译原理曾经完结 大家能够看着思维导图本人入手写一遍外围代码哈 须要留神的是 本篇大量应用字符串拼接以及正则相干的常识 遇到不懂的中央能够多查阅材料 也欢送评论留言

正文完
 0