带你一行一行手写Vue2.0源码系列(二) -模板编译

前言

上一篇中我们主要讲的Vue2.0响应式的实现,其主要是利用Object.defineProperty的来对数据进行劫持。本节课接上一章,主要是讲解Vue是如何进行模板编译的,模板编译相对与响应式的实现会复杂的很多,涉及到很多正则一级AST语法树的实现。

我个人推荐的看源码的方式最好是通过视频文章自己能手写一边然后再去看,否则就会造成这样的结果。

真是卷...

本文章主要讲解的最基本的模板编译(标签,属性,mustache语法),并不包括事件、指令、过滤器等。

正文

思维导图

Vue在初始化的时候,会将我们传入的template模板解析为render函数,如果没有传入template模板就会将el对标的元素转为字符串,然后进行解析。

arduino 复制代码
new Vue({
  el: '#app',
  data:{
    name: '小明',
    age: 18,
  },
  template: '<div>{{ name }}</div>', // template的优先级会比el高
})

编译入口

ini 复制代码
// core/instance/init.js

function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options; // $表示vue中的变量
    initState(vm);

    if(options.el) {
      vm.$mount(options.el); // 实现页面挂载
    }
  }

  Vue.prototype.$mount = function(el) {
    const vm = this;
    el = document.querySelector(el);
    const opt = vm.$options;
    if(!opt.render) {
      let template;
      if(opt.template) { // 如果传入的对象中有template就将template作为模板
        template = opt.template;
      } else { // 如果没有template就将el 转为模板
        template = el.outerHTML;
      }
      if(template) {
        // 对模板进行编译
        const render = compileToFunction(template); // 最终转化为render函数
        opt.render = render;
      }
    }
  }
}

接着上一章中的通过initState函数将数据转为响应式,接下来就是进行页面挂载$mount,将模板转为render函数,其主要核心方法就是compileToFunction

模板转为AST语法树

ini 复制代码
// compiler/parse.js

const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 他匹配到标签名  <div
const startTagClose = /^\s*(\/?)>/; // 匹配单个标签  <br/>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 他匹配到结束标签 </XXXX>

export function parseHTML(html) {
  const ELEMENT_TYPE = 1; // 元素节点
  const TEXT_TYPE = 3; // 文本节点
  const stack = [];  // 栈结构
  let currentParent; // 当前父亲节点
  let root; // 根节点

  function createASTElement(tag, attrs) { // 创建节点,包括标签名、节点类型、子节点、属性,父节点
    return {
      tag: tag,
      type: ELEMENT_TYPE,
      children: [],
      attrs,
      parent: null
    }
  }

  function start(tag, attrs) { // 开始标签入栈
    let node = createASTElement(tag, attrs);
    if (!root) {
      root = node;
    }
    if (currentParent) {
      node.parent = currentParent;
      currentParent.children.push(node);
    }
    stack.push(node);
    currentParent = node;
  }

  function chars(text) { // 文本标签入栈
    text = text.replace(/\s/g, '');
    text && currentParent.children.push({
      type: TEXT_TYPE,
      text,
      parent: currentParent
    })
  }

  function end(tag) { // 出栈
    let node = stack.pop();
    currentParent = stack[stack.length - 1];
  }

  function advance(n) { // 每次已经配到就移除匹配到的内容
    html = html.substring(n);
  }

  function parseStartTag() { // 开始标签匹配函数
    const start = html.match(startTagOpen);
    if (start) {
      const match = {
        tagName: start[1],
        attrs: []
      }
      advance(start[0].length);
      let attr; // 属性
      let end; // 结束标签
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 如果没有匹配到结束标签就一直匹配,同时匹配中间的dom属性
        advance(attr[0].length);
        let name = attr[1];
        let value = attr[3] || attr[4] || attr[5];
        match.attrs.push({
          name: name,
          value: value
        })
      }
      if (end) {
        advance(end[0].length);
      }
      return match;
    }
    return false; // 不是开始标签
  }

  while (html) { // 最后循环结束条件就是模板被拆分完
    let textEnd = html.indexOf('<');
    if (textEnd === 0) { // 说明是个标签
      const startTagMatch = parseStartTag();
      if (startTagMatch) {
        start(startTagMatch.tagName, startTagMatch.attrs);
        continue;
      }
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        advance(endTagMatch[0].length);
        end(endTagMatch[1]);
        continue;
      }
    }
    if (textEnd > 0) {
      let text = html.substring(0, textEnd); // 文本内容
      if (text) {
        advance(text.length);
        chars(text);
      }
    }
  }
  return root; // 最后返回根节点
}

使用不同的正则表达式对html进行匹配,遇到开始标签就存入,在匹配到结束标签之前一直循环匹配dom属性,依次循环。同时每次匹配到一个就利用advancehtml字符串进行截取,知道字符串被截取完毕结束循环。

AST转成code代码

ini 复制代码
// compiler/codegen/index.js

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // 匹配双括号内容

function gen(node) {
  if(node.type === 1) { // 如果是元素节点
    return codegen(node); // 进行递归处理
  } else {
    // 文本节点
    let text = node.text;
    if(!defaultTagRE.test(text)) { // 文本节点中没有{{}}直接将传入原文本
      return `_v(${JSON.stringify(text)})`;
    } else { // 文本节点中存在{{}}
      let tokens = [];
      let match;
      defaultTagRE.lastIndex = 0;
      let lastIndex = 0;
      while(match = defaultTagRE.exec(text)) {
        let index = match.index; // index代表匹配到的位置
        if(index > lastIndex) { // 匹配到{{,将{{之前的文本存入tokens
          tokens.push(JSON.stringify(text.slice(lastIndex, index)));
        }
        tokens.push(`_s(${match[1].trim()})`); // 将{{ }}里面的变量存入tokens
        lastIndex = index + match[0].length; // 修改指针位置
      }
      if(lastIndex < text.length) { // 将剩余的文本存入tokens
        tokens.push(JSON.stringify(text.slice(lastIndex, index)));
      }
      return `_v(${tokens.join('+')})`;
    }
  }
}

function genChildren(children) {
  return children.map(child => {
    return gen(child);
  }).join(",");
}

function genProps(attrs) { // 对属性进行处理
  let str = '';
  for (let i = 0; i < attrs.length; i++) {
    let attr = attrs[i];
    if (attr.name === 'style') { // 如果属性是样式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)}}`;
}

export function codegen(ast) {
  let children = genChildren(ast.children);
  let code = `_c('${ast.tag}', ${ast.attrs.length > 0 ? genProps(ast.attrs) : 'null'}, ${ast.children.length ? `${children}` : ''})`;

  return code;
}

ast语法树转为code代码,最后生成

css 复制代码
<div id="app">
   <p style="color: red;">
     我的姓名是: {{ name }}
     我的年龄是: {{ age }}
   </p>
</div>

// 最终结果
// _c('div', {id:"app"}, _c('p', {style:{"color":" red"}}, _v("我的姓名是:"+_s(name)+"我的年龄是:"+_s(age))))

生成render函数

javascript 复制代码
// compiler/index.js

export function compileToFunction(template) {

  let ast = parseHTML(template); // 将template转为ast树

  let code = codegen(ast); // 将ast树转为code代码

  code = `with(this) {return ${code}}`;

  let render = new Function(code); // 根据代码生成render函数

  return render;
}

new Function()利用with来改变render函数的作用域。

小结

至此Vue2.0的模板编译原理已经完结 大家可以试着自己动手写一遍核心代码哈,本文主要实现基础的元素的编译,其中不乏出错的地方,望请见谅!

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

vue2.0 和 vue3.0系列文章(后续更新)

相关推荐
-seventy-8 分钟前
对 JavaScript 原型的理解
javascript·原型
&白帝&26 分钟前
uniapp中使用picker-view选择时间
前端·uni-app
谢尔登33 分钟前
Babel
前端·react.js·node.js
ling1s33 分钟前
C#基础(13)结构体
前端·c#
卸任40 分钟前
使用高阶组件封装路由拦截逻辑
前端·react.js
计算机学姐1 小时前
基于python+django+vue的家居全屋定制系统
开发语言·vue.js·后端·python·django·numpy·web3.py
lxcw1 小时前
npm ERR! code CERT_HAS_EXPIRED npm ERR! errno CERT_HAS_EXPIRED
前端·npm·node.js
Ripple1111 小时前
Vue源码速读 | 第二章:深入理解Vue虚拟DOM:从vnode创建到渲染
vue.js
秋沐1 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
这个需求建议不做1 小时前
vue3打包配置 vite、router、nginx配置
前端·nginx·vue