Vue实现文本编译详情

目录
  • 模板编译
    • 获取template字符串
    • 解析html
    • 生成代码字符串
    • 生成render函数
    • 结语

Vue实现文本编译详情

模板编译

在数据劫持中,我们完成了Vuedata选项中数据的初始操作。这之后需要将html字符串编译为render函数,其核心逻辑如下:

render函数的情况下会直接使用传入的render函数,而在没有render函数的情况下,需要将template编译为render函数。

具体逻辑如下:

  • 获取template字符串
  • template字符串解析为ast抽象语法树
  • ast抽象语法树生成代码字符串
  • 将字符串处理为render函数赋值给vm.$options.render

获取template字符串

在进行template解析之前,会进行一系列的条件处理,得到最终的template其处理逻辑如下:

src/init.js中书写如下代码:

/**
 * 将字符串处理为dom元素
 * @param el
 * @returns {Element|*}
 */
function query (el) {
  if (typeof el === 'string') {
    return document.querySelector(el);
  }
  return el;
}

function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options;
    initState(vm);
    const { el } = options;
    // el选项存在,会将el通过vm.$mount方法进行挂载
    // el选项如果不存在,需要手动调用vm.$mount方法来进行组件的挂载
    if (el) {
      vm.$mount(el);
    }
  };
  Vue.prototype.$mount = function (el) {
    el = query(el);
    const vm = this;
    const options = vm.$options;
    if (!options.render) { // 有render函数,优先处理render函数
      let template = options.template;
      // 没有template,使用el.outerHTML作为template
      if (!template && el) {
        template = el.outerHTML;
      }
      options.render = compileToFunctions(template);
    }
  };
}

当我们得到最终的template后,需要调用compileToFunctionstemplate转换为render函数。在compileToFunctions中就是模板编译的主要逻辑。

创建src/compiler/index.js文件,其代码如下:

export function compileToFunctions (template) {
  // 将html解析为ast语法树
  const ast = parseHtml(template);
  // 通过ast语法树生成代码字符串
  const code = generate(ast);
  // 将字符串转换为函数
  return new Function(`with(this){return $[code]}`);
}

解析html

当拿到对应的html字符串后,需要通过正则来将其解析为ast抽象语法树。简单来说就是将html处理为一个树形结构,可以很好的表示每个节点的父子关系。

下面是一段html,以及表示它的ast:

<body>
<div id="app">
  hh
  <div id="aa" style="font-size: 18px;">hello {{name}} world</div>
</div>
<script>
  const vm = new Vue({
    el: '#app',
    data () {
      return {
        name: 'zs',
      };
    },
  });
</script>
</body>

const ast = {
  tag: 'div', // 标签名
  attrs: [{ name: 'id', value: 'app' }], // 属性数组
  type: 1, // type:1 是元素,type: 3 是文本
  parent: null, // 父节点
  children: [] // 孩子节点
}

html的解析逻辑如下:

  • 通过正则匹配开始标签的开始符号、匹配标签的属性、匹配开始标签结束符号、匹配文本、匹配结束标签
  • while循环html字符串,每次删除掉已经匹配的字符串,直到html为空字符串时,说明整个文本匹配完成
  • 通过栈数据结构来记录所有正在处理的标签,并且根据标签的入栈出栈顺序生成树结构

代码中通过advance函数来一点点删除被匹配的字符串,其逻辑比较简单,只是对字符串进行了截取:

// 删除匹配的字符串
function advance (length) {
  html = html.slice(length);
}

首先处理开始标签和属性。

<开头的字符串为开始标签或结束标签,通过正则匹配开始标签,可以通过分组得到标签名。之后循环匹配标签的属性,直到匹配到结尾标签。在这过程中要将匹配到的字符串通过advance进行删除。

export function parseHtml (html) {
  function parseStartTag () {
    const start = html.match(startTagOpen);
    if (start) {
      const match = { tag: start[1], attrs: [] };
      // 开始解析属性,直到标签闭合
      advance(start[0].length);
      let end = html.match(startTagClose);
      let attr = html.match(attribute);
      // 循环处理属性
      while (!end && attr) {
        match.attrs.push({
          name: attr[1],
          value: attr[3] || attr[4] || attr[5]
        });
        advance(attr[0].length);
        end = html.match(startTagClose);
        attr = html.match(attribute);
      }
      if (end) {
        advance(end[0].length);
      }
      return match;
    }
  }

  // 注意:在template中书写模板时可能开始和结束会有空白
  html = html.trim();
  while (html) {
    // 开始和结束标签都会以 < 开头
    const textEnd = html.indexOf('<');
    if (textEnd === 0) {
      // 处理开始标签
      const startTag = parseStartTag();
      if (startTag) {
        start(startTag.tag, startTag.attrs);
      }
      // some code ...
    }
    // some code...
  }
  return root;
}

在获得开始标签的标签名和属性后,通过start函数,可以生成树根以及每一个入栈标签对应ast元素并确定父子关系:

// 树 + 栈
function createASTElement (tag, attrs) {
  return {
    tag,
    type: 1,
    attrs,
    children: [],
    parent: null
  };
}

let root, currentParent;
const stack = [];

function start (tag, attrs) {
  const element = createASTElement(tag, attrs);
  if (!root) {
    root = element;
  } else {
    // 记录父子关系
    currentParent.children.push(element);
    element.parent = currentParent;
  }
  currentParent = element;
  stack.push(element);
}

以一段简单的html为例,我们画图看下其具体的出栈入栈逻辑:

<div id="app">
  <h2>
    hello world
    <span> xxx </span>
  </h2>
</div>

通过对象的引用关系,最终便能得到一个树形结构对象root

解析完开始标签后,剩余的文本起始字符串可能为:

  • 下一个开始标签
  • 文本内容
  • 结束标签

如果仍然是开始标签,会重复上述逻辑。如果是文本内容,<字符的索引会大于0,只需要将[0, textEnd)之间的文本截取出来放到父节点的children中即可:

export function parseHtml (html) {

  // 树 + 栈
  let root, currentParent;
  const stack = [];
  function char (text) {
    // 替换所有文本中的空格
    text = text.replace(/\s/g, '');
    if (currentParent && text) {
      // 将文本放到对应的父节点的children数组中,其type为3,标签type为1
      currentParent.children.push({
        type: 3,
        text,
        parent: currentParent
      });
    }
  }

  while (html) {
    // some code ...
    //  < 在之后的位置,说明要处理的是文本内容
    if (textEnd > 0) { // 处理文本内容
      let text = html.slice(0, textEnd);
      if (text) {
        char(text);
        advance(text.length);
      }
    }
  }
  return root;
}

最后来处理结束标签。

匹配到结束标签时要将stack中最后一个元素出栈,更新currentParent,直到stack中的元素为空时。就得到了完整的ast抽象语法树:

export function parseHtml (html) {
  // 树 + 栈
  let root, currentParent;
  const stack = [];

  // 每次处理好前一个,最后将所有元素作为子元素push到root节点中
  function end (tag) { // 在结尾标签匹配时可以确立父子关系
    stack.pop();
    currentParent = stack[stack.length - 1];
  }

  while (html) {
    // 开始和结束标签都会以 < 开头
    const textEnd = html.indexOf('<');
    if (textEnd === 0) {
      // some code ...
      // 处理结尾标签
      const endTagMatch = html.match(endTag);
      if (endTagMatch) {
        end(endTagMatch[1]);
        advance(endTagMatch[0].length);
      }
    }
    // some code ...
  }
  return root;
}

到这里我们拿到了一个树形结构对象ast,接下来要根据这个树形结构,递归生成代码字符串

生成代码字符串

Vue代码生成可视化编辑器

先看下面一段html字符串生成的代码字符串是什么样子的:

<body>
<div id="app">
  hh
  <div id="aa" style="color: red;">hello {{name}} world</div>
</div>
<script>
  const vm = new Vue({
    el: '#app',
    data () {
      return {
        name: 'zs',
      };
    },
  });
</script>
</body>

最终得到的代码字符串如下:

const code = `_c("div",{id:"app"},_v("hh"),_c("div"),{id:"aa",style:{color: "red"}},_v("hello"+_s(name)+"world"))`

最终会将上述代码通过new Function(with(this) { return $[code]})转换为render函数,而在render函数执行时通过call来将this指向vm 。所以代码字符串中的函数和变量都会从vm上进行查找。

下面是代码字符串中用到的函数的含义:

  • _c : 创建虚拟元素节点createVElement
  • _v : 创建虚拟文本节点createTextVNode
  • _s : stringify对传入的值执行JSON.stringify

接下来开始介绍如何将ast树形对象处理为上边介绍到code

创建src/compiler/generate.js文件,需要解析的内容如下:

  • 标签
  • 属性
  • 递归处理children
  • 文本

标签处理比较简单,直接获取ast.tag即可。

属性在代码字符串中是以对象的格式存在,而在ast中是数组的形式。这里需要遍历数组,并将其namevalue处理为对象的键和值。需要注意style属性要特殊处理

function genAttrs (attrs) {
  if (attrs.length === 0) {
    return 'undefined';
  }
  let str = '';
  for (let i = 0; i < attrs.length; i++) {
    const attr = attrs[i];
    if (attr.name === 'style') {
      const styleValues = attr.value.split(',');
      // 可以对对象使用JSON.stringify来进行处理
      attr.value = styleValues.reduce((obj, item) => {
        const [key, val] = item.split(':');
        obj[key] = val;
        return obj;
      }, {});
    }
    str += `${attr.name}:${JSON.stringify(attr.value)}`;
    if (i !== attrs.length - 1) {
      str += ',';
    }
  }
  return `{${str}}`;
}

// some code ...

export function generate (el) {
  const children = genChildren(el.children);
  return `_c("${el.tag}", ${genAttrs(el.attrs)}${children ? ',' + children : ''})`;
}

在用,拼接对象时,也可以先将每一部分放到数组中,通过数组的join方法用,来拼接为字符串。

标签和属性之后的参数都为孩子节点,要以函数参数的形式用,进行拼接,最终在生成虚拟节点时会通过...扩展运算符将其处理为一个数组:

function gen (child) {
  if (child.type === 1) {
    // 将元素处理为代码字符串并返回
    return generate(child);
  } else if (child.type === 3) {
    return genText(child.text);
  }
}

// 将children处理为代码字符串并返回
function genChildren (children) { // 将children用','拼接起来
  const result = [];
  for (let i = 0; i < children.length; i++) {
    const child = children[i];
    // 将生成结果放到数组中
    result.push(gen(child));
  }
  return result.join(',');
}
export function generate (el) {
  const children = genChildren(el.children);
  return `_c("${el.tag}", ${genAttrs(el.attrs)}${children ? ',' + children : ''})`;
}

在生成孩子节点时,需要判断每一项的类型,如果是元素会继续执行generate方法来生成元素对应的代码字符串,如果是文本,需要通过genText方法来进行处理:

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g;

function genText (text) {
  if (!defaultTagRE.test(text)) {
    return `_v(${JSON.stringify(text)})`;
  }
  // <div id="aa">hello {{name}} xx{{msg}} hh <span style="color: red" class="bb">world</span></div>
  const tokens = [];
  let lastIndex = defaultTagRE.lastIndex = 0;
  let match;
  while (match = defaultTagRE.exec(text)) {
    // 这里的先后顺序如何确定? 通过match.index和lastIndex的大小关系
    // match.index === lastIndex时,说明此时是{{}}中的内容,前边没有字符串
    if (match.index > lastIndex) {
      tokens.push(JSON.stringify(text.slice(lastIndex, match.index)));
    }
    // 然后将括号内的元素放到数组中
    tokens.push(`_s(${match[1].trim()})`);
    lastIndex = defaultTagRE.lastIndex;
  }
  if (lastIndex < text.length) {
    tokens.push(JSON.stringify(text.slice(lastIndex)));
  }
  return `_v(${tokens.join('+')})`;
}

genText中会利用lastIndex以及match.index来循环处理每一段文本。由于正则添加了g标识,每次匹配完之后,都会将lastIndex移动到下一次开始匹配的位置。最终匹配完所有的{{}} 文本后,match=null并且lastIndex=0,终止循环。

{{}}中的文本需要放到_s() 中,每段文本都会放到数组tokens中,最后将每段文本通过+拼接起来。最终在render函数执行时,会进行字符串拼接操作,然后展示到页面中。

代码中用到的lastIndexmatch.index含义分别如下:

  • lastIndex: 字符串下次开始匹配的位置对应的索引
  • match.index: 匹配到的字符串在原字符串中的索引

其匹配逻辑如下图所示:

在上边的逻辑完成后,会得到最终的code,下面需要将code处理为render函数。

生成render函数

js中,new Function可以通过字符串来创建一个函数。利用我们之前生成的字符串再结合new Function便可以得到一个函数。

而字符串中的变量最终会到vm实例上进行取值,with可以指定变量的作用域,下面是一个简单的例子:

const obj = { a: 1, b: 2 }
with (obj) {
  console.log(a) // 1
  console.log(b) // 2
}

利用new Functionwith的相关特性,可以得到如下代码:

const render = new Function(`with(this){return $[code]}`)

到这里,我们便完成了compileToFunctions函数的功能,实现了文章开始时这行代码的逻辑:

vm.$options.render = compileFunctions(template)

结语

文本中代码主要涉及的知识如下:

  • 通过栈+树这俩种数据结构,通过正则将html解析为树
  • 利用正则表达式来进行字符串的匹配实现相应的逻辑

文章中介绍到的整个逻辑,也是Vue在文本编译过程中的核心逻辑。希望小伙伴在读完本文之后,可以对Vue如何解析template有更深的理解,并可以尝试阅读其源码。

到此这篇关于Vue实现文本编译详情的文章就介绍到这了,更多相关Vue文本编译内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Vue 中使用富文本编译器wangEditor3的方法

    富文本编译器在vue中的使用 在开发的过程中由于某些特殊需求,被我们使用,今天我就简单讲一讲如何在vue中使用wangEditor3 首先讲一下简单的使用. 1.使用npm安装 npm install wangeditor --save 2.vue页面代码如下 <template> <section> <div id="div5"></div> <div id="div6"></div> <

  • Vue实现文本编译详情

    目录 模板编译 获取template字符串 解析html 生成代码字符串 生成render函数 结语 Vue实现文本编译详情 模板编译 在数据劫持中,我们完成了Vue中data选项中数据的初始操作.这之后需要将html字符串编译为render函数,其核心逻辑如下: 有render函数的情况下会直接使用传入的render函数,而在没有render函数的情况下,需要将template编译为render函数. 具体逻辑如下: 获取template字符串 将template字符串解析为ast抽象语法树

  • vue模版编译详情

    目录 1.parse 解析器 1.1 截取的规则 1.2  截取过程部分 1.3  解析器总结 2.optimize 优化器 2.1 静态节点 2.2 静态根节点 2.3 优化器总结 3.generate 代码生成器 3.1 JS的with语法 思考: html是标签语言,只有JS才能实现判断.循环,而模版有指令.插值.JS表达式,能够实现判断.循环等,故模板不是html,因此模板一定是转换为某种JS代码,这种编译又是如何进行的? 解析: 模版编译是将template编译成render函数的过程

  • vue富文本框(插入文本、图片、视频)的使用及问题小结

    今天在vue里面插入富文本遇到了一些小坑在这里提供给大家用于参考,如有错误,望多加指正. 我这里使用的是Element-ui的上传图片组件 首先引入Element-ui(这个我就不作赘述了,详情参考element中文官网) 在引入富文本组件vue-quill-editor 使用在main.js引入相应的样式 import VueQuillEditor from 'vue-quill-editor' import 'quill/dist/quill.core.css' import 'quill/

  • Vue SSR 即时编译技术的实现

    当我们在服务端渲染 Vue 应用时,无论服务器执行多少次渲染,大部分 VNode 渲染出的字符串是不变的,它们有一些来自于模板的静态 html,另一些则来自模板动态渲染的节点(虽然在客户端动态节点有可能会变化,但是在服务端它们是不变的).将这两种类型的节点提取出来,仅在服务端渲染真正动态的节点(serverPrefetch 预取数据相关联的节点),可以显著的提升服务端的渲染性能. 提取模板中静态的 html 只需在编译期对模板结构做解析,而判断动态节点在服务端渲染阶段是否为静态,需在运行时对 V

  • 解决Vue的文本编辑器 vue-quill-editor 小图标样式排布错乱问题

    假设你已经知道如何引入vue-quill-editor,并且遇到了跟我一样的问题(如上图),显示出来的图标排列不整齐,字体,文字大小选择时超出边框.你可以试试下面这种解决办法 . 在使用文本编辑器的vue页面中引入vue-quill-editor中的样式. @import "../../node_modules/quill/dist/quill.snow.css"; 然后在组件中添加class名 -- class="ql-editor". <quill-edi

  • Vue项目打包编译优化方案

    1. 不生成.map文件 默认情况下,当我们执行 npm run build 命令打包完一个项目后,会得到一个dist目录,里面有一个js目录,存放了该项目编译后的所有js文件. 我们发现每个js文件都有一个相应的 .map 文件,它们仅是用来调试代码的,可以加快打包速度,但会增大打包体积,线上我们是不需要这个代码的.这里我们需要配置不生成map文件. vue-cli2 config/index.js文件中,找到 productionSourceMap: true 这一行,将 true 改为 f

  • vue多页面配置详情

    目录 1.多页面的区别 2.SPA 与 MPA 3.Vue Cli 脚手架配置 1.多页面的区别 单页应用这个概念,是随着前几年 AngularJS.React.Ember 等这些框架的出现而出现的.在前面的前言内容里,我们在页面渲染中讲了页面的局部刷新,而单页应用则是使用了页面的局部刷新的能力,在切换页面的时候刷新页面内容,从而获取更好的体验. 2.SPA 与 MPA 单页应用(SinglePage Web Application,简称 SPA)和多页应用(MultiPage Applicat

  • vue 中使用 bimface详情

    目录 1. 安装 vue 脚手架 2. 创建项目 3. 引入 bimface 文件 3.1 运行项目 3.2 引入 bimface 文件 4. 实现页面渲染 4.1 修改 html 4.2 修改 CSS 4.3 修改 JS 整个过程分为如下几个步骤: 1.安装 vue 脚手架 2.创建项目 3.引入 bimface 响应的文件 4.修改 App.vue 文件,实现页面渲染 1. 安装 vue 脚手架 这里还是使用 Vue CLI 通过如下命令,全局安装 vue 脚手架工具 npm install

  • Vue实现通知或详情类弹窗

    本文实例为大家分享了Vue实现通知或详情类弹窗的具体代码,供大家参考,具体内容如下 效果如图所示:(整体样式模仿ant-design-vue Modal样式,同时阴影覆盖浏览器窗口,并自定义滚动条样式) ①创建弹窗组件Dialog.vue: <template>   <div class="m-dialog-mask">     <div class="m-modal">       <div class="m-m

  • Vue插件使用方法详情分享

    目录 一.应用场景 二.使用方法 1.使用自定义插件 2.使用第三方插件[elementUI] 一.应用场景 为vue添加全局功能,比如添加全局的方法和属性.混入全局组件.添加全局资源(指令.过滤器.过渡等).添加第三方的类库(element-ui等) 二.使用方法 1.使用自定义插件 <1>.创建js文件 export default {     install(Vue) {                  // 自定义全局过滤器(截取前四位A)         Vue.filter('m

随机推荐