乐趣区

关于javascript:8分钟学会-Vuejs-原理一template-字符串编译为抽象语法树-AST

《8 分钟学会 Vue.js 原理》:一、template 字符串编译为形象语法树 AST

Vue.js 并没有什么神秘的魔法,模板渲染、虚构 DOM diff,也都是一行行代码基于 API 实现的。

本文将用几分钟工夫,分章节讲清楚 Vue.js 2.0 的 <template>渲染为 HTML DOM 的原理。

有任何疑难,欢送通过评论交换~~

本节指标

将 Vue.js 的字符串模板 template 编译为形象语法树 AST;

残缺示例:DEMO -《8 分钟学会 Vue.js 原理》:一、template 字符串编译为形象语法树 AST – JSBin

极其简略的外围逻辑

实现「字符串模板 <template> 编译为 render() 函数」的外围逻辑极其简略,只有 2 局部:

  1. String.prototype.match()

首先用 .match() 办法,提取字符串中的关键词,例如标签名 div,Mustache 标签对应的变量msg 等。

用 1 行代码就能说明确:

'<div>{{msg}}</div>'.match(/\{\{((?:.|\r?\n)+?)\}\}/)
// ["{{msg}}", "msg"]

这个示例用 /\{\{((?:.|\r?\n)+?)\}\}/ 正则表达式,提取了字符串'<div>{{msg}}</div>' 中的Mustache 标签

临时不必了解该正则的含意,会用即可。

如果想要了解正则,能够试一试正则在线调试工具:Vue.js 开始标签正则,可能可视化的查看上述正则表达式

取得了 "msg" 标签对应变量的名称,咱们就能在后续拼接出渲染 DOM 所须要的_vm.msg

即从咱们申明的实例 new Vue({data() {return {msg: 'hi'}}}) 中提取出msg: 'hi',渲染为 DOM 节点。

  1. 遍历字符串并删除已遍历后果

其次,因为 <template> 实质上是一段有大量 HTML 标签的字符串,通常内容较长,为了不脱漏地获取到其中的所有标签、属性,咱们须要遍历。

实现形式也很简略,用 while(html) 循环,一直的 html.match() 提取模板中的信息。(html 变量即 template 字符串)

每提取一段,再用 html = html.substring(n) 删除掉 n 个曾经遍历过的字符。

直到 html 字符串为空,示意咱们曾经遍历、提取了全副的template

html = `<div`   // 仅遍历一遍,提取开始标签

const advance = (n) => {html = html.substring(n)
}

while (html) {match = html.match(/^<([a-zA-Z_]*)/)
    // ["<div", "div"]
    if (match) {advance(match[0].length)
        // html = '' 跳出循环         
    }
}

了解了这 2 局部逻辑,就了解了字符串模板 template 编译为 render() 函数的原理,就是这么简略!

具体步骤

0. 基于 class 语法封装

咱们用 JS 的 class 语法对代码进行简略的封装、模块化,具体来说就是申明 3 个类:

// 将 Vue 实例的字符串模板 template 编译为 AST
class HTMLParser {}

// 基于 AST 生成渲染函数;class VueCompiler {HTMLParser = new HTMLParser()
}

// 基于渲染函数生成虚构节点和实在 DOM
class Vue {compiler = new VueCompiler()
}

问:为什么要生成 AST?间接把 template 字符串模板编译为实在 DOM 有什么问题?

答:没有问题,技术上也能够实现,
Vue.js 以及泛滥编译器都采纳 AST 做为编译的中间状态,集体了解是为了编译过程中做「转化」(Transform),
例如 v-if 属性转化为 JS 的 if-else 判断,有了 AST 做为中间状态,有助于更便捷的实现 v-ifif-else

1. 开始遍历 template 字符串模板

基于咱们上述提到的 while()html = html.substring(n)

咱们能够实现一套一边解析模板字符串、一边删除已解析局部,直到全副解析实现的逻辑。

很简略,只有几行代码,

咱们为 class HTMLParser 减少一个 parseHTML(html, options) 办法:

parseHTML(html, options) {const advance = (n) => {html = html.substring(n)
  }

  while(html) {const startTag = parseStartTag()  // TODO 下一步实现 parseStartTag
    if (startTag) {advance(startTag[0].length)
      continue
    }
  }
}

html参数即初始化 Vue 实例中的 template: '<div>{{msg}}</div>', 属性,

在遍历 html 过程中,咱们每解析出一个关键词,就调用advance() { html.substring(n) },删去这部分对应的字符串,

示例中咱们调用 parseStartTag()(下一步实现)解析出开始标签<div> 对应的字符串后,就从 html 删除了 <div> 这 5 个字符。

2. 解析开始标签<div>

接下来让咱们实现parseStartTag(),解析出字符串中的开始标签<div>

也非常简单,用 html.match(regularExp) 即可,咱们能够从 Vue.js 源码中的 html-parser.js 找到对应的正则表达式startTagOpen

源码的正则中非常复杂,因为要兼容生产环境的各种标签,咱们临时不思考,精简后就是:/^<([a-zA-Z_]*)/

在线调试开始标签正则

用这个正则调用 html.match(),即可失去['<div', 'div'] 这个数组。

提取出须要的信息后,就把曾经遍历的字符调用 advance(start[0].length) 删除。

const parseStartTag = () => {let start = html.match(this.startTagOpen);
  // ['<div', 'div']
  if (start) {
    const match = {tagName: start[1],
      attrs: [],}
    advance(start[0].length)
  }
}

留神,startTagOpen只匹配了开始标签的局部'<div',还须要一次正则匹配,找到开始标签的完结符号>

找到完结符号后,也要删除对应已遍历的局部。

const end = html.match(this.startTagClose)
debugger
if (end) {advance(end[0].length)
}
return match

两局部组合后,就能残缺遍历、解析出模板字符串中的 <div> 开始标签了。

3. 解析文本内容{{msg}}

下一步咱们把模板字符串中的文本内容 {{msg}} 提取进去,依然是 字符串遍历 && 正则匹配

持续补充 while 循环:

while (html) {
  debugger
  // 程序对逻辑有影响,startTag 要先于 text,endTag 要先于 startTag
  const startTag = parseStartTag()
  if (startTag) {handleStartTag(startTag)
    continue
  }

  let text
  let textEnd = html.indexOf('<')
  if (textEnd >= 0) {text = html.substring(0, textEnd)
  }

  if (text) {advance(text.length)
  }
}

因为删除了已解析局部、并且各局部有解析程序,所以咱们只有检测下一个 < 标签的地位即可取得文本内容在 html 中的完结下标:html.indexOf('<')

之后就能取得残缺的文本内容{{msg}}text = html.substring(0, textEnd)

最初,别忘了,删除曾经遍历的文本内容:advance(text.length)

4. 解析闭合标签</div>

到这一步,html 字符串曾经只剩下 '</div>' 了,咱们持续用遍历 && 正则解析:

while (html) {
  debugger
  // 程序对逻辑有影响,startTag 要先于 text,endTag 要先于 startTag
  let endTagMatch = html.match(this.endTag)
  if (endTagMatch) {advance(endTagMatch[0].length)
    continue
  }
}

咱们临时不须要从闭合标签中提取信息,所以只须要遍历、匹配后,删除它即可。

解析实现总结

到目前为止,咱们曾经根本实现了class HTMLParser {},屡次用正则解析提取出了 template 字符串中的 3 局部信息:

  • 开始标签:'<div', '>'
  • 文本内容:'{{msg}}'
  • 闭合标签:'</div>'

这部分的残缺代码,能够拜访 DEMO -《8 分钟学会 Vue.js 原理》:一、template 字符串编译为形象语法树 AST – JSBin 查看。

但为了取得 AST,咱们还须要基于这些信息,做一些简略的拼接。

5. 初始化形象语法树 AST 的根节点

咱们持续参考 Vue.js 源码中拼接 AST 的实现

欠缺 class VueCompiler,增加HTMLParser = new HTMLParser() 实例,以及 parse(template) 办法。

class VueCompiler {HTMLParser = new HTMLParser()

  constructor() {}

  parse(template) {}}

AST 是什么?

先不必去了解艰涩的概念,在 Vue.js 的实现中,AST 就是一般的 JS object,记录了标签名、父元素、子元素等属性:

createASTElement (tag, parent) {return { type: 1, tag, parent, children: [] }
}

咱们把 createASTElement 办法也增加到 class VueCompiler 中。

并减少 parse 办法中 this.HTMLParser.parseHTML() 的调用

parse(template) {
  const _this = this
  let root
  let currentParent

  this.HTMLParser.parseHTML(template, {start(tag) {},
    chars (text) {},})

  debugger
  return root
}

start(tag) {}就是咱们提取开始标签对应 AST 节点的回调,

其承受一个参数 tag,调用_this.createASTElement(tag, currentParent) 来生成 AST 节点。

 start(tag) {let element = _this.createASTElement(tag, currentParent)

    if (!root) {root = element}
    currentParent = element
  },

调用 start(tag) 的地位在 class HTMLParser 中的 parseHTML(html, options) 办法:

  const handleStartTag = (match) => {if (options.start) {options.start(match.tagName)
    }
  }

  while(html) {const startTag = parseStartTag()
    if (startTag) {handleStartTag(startTag)
      continue
    }
  }

当咱们通过 parseStartTag() 获取了{tagName: 'div'},就传给options.start(match.tagName),从而生成 AST 的根节点:

// root
'{"type":1,"tag":"div","children":[]}'

咱们把根节点保留到 root 变量中,用于最终返回整个 AST 的援用。

6. 为 AST 减少子节点

除了根节点,咱们还须要持续为 AST 这棵树增加子节点:文本内容节点

依然是用回调的模式(options.char(text)),提取出文本内容节点所需的信息,

欠缺 VueCompiler.parse() 中的 chars(text) 办法

chars(text) {
  debugger
  const res = parseText(text)
  const child = {
    type: 2,
    expression: res.expression,
    tokens: res.tokens,
    text
  }
  if (currentParent) {currentParent.children.push(child)
  }
},

parseHTML(html, options) 的循环中增加 options.chars(text) 调用:

while (html) {
  // ... 省略其余标签的解析
  let text
  let textEnd = html.indexOf('<')
  // ...

  if (options.chars && text) {options.chars(text)
  }
}

解析文本内容的 Mustache 标签 语法

options.chars(text)接管的 text 值为字符串 '{{msg}}',咱们还须要从中剔除{{}},拿到msg 字符串。

依然是用相熟的正则匹配:

  const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/
  function parseText(text) {let tokens = []
    let rawTokens = []
    const match = defaultTagRE.exec(text)

    const exp = match[1]
    tokens.push(("_s(" + exp + ")"))
    rawTokens.push({'@binding': exp})

    return {expression: tokens.join('+'),
      tokens: rawTokens
    }
  }

后果将是:

{expression: "_s(msg)",
  tokens: {@binding: "msg"}
}

临时不用理解 expression, tokens 及其内容的具体含意,后续到运行时阶段咱们会再具体介绍。

7. 遍历 template 字符串实现,返回 AST

残缺示例:DEMO -《8 分钟学会 Vue.js 原理》:一、template 字符串编译为形象语法树 AST – JSBin

通过以上步骤,咱们将 template 字符串解析后失去这样一个对象:

// root ===
{
    "type": 1,
    "tag": "div",
    "children": [
        {
            "type": 2,
            "expression": "_s(msg)",
            "tokens": [
                {"@binding": "msg"}
            ],
            "text": "{{msg}}"
        }
    ]
}

这就是 Vue.js 的 AST,实现就是这么简略,示例中的代码都间接来自 Vue.js 的源码(compiler 局部)

后续咱们将基于 AST 生成 render() 函数,并最终渲染出实在 DOM。

<hr/>

《8 分钟学会 Vue.js 原理》系列,共计 5 局部:

  • 一、template 字符串编译为形象语法树 AST
  • 二、AST 编译 render() 实现原理
  • 三、执行渲染函数 render() 生成虚构节点 vnode
  • 四、虚构节点 vnode 生成实在 DOM
  • 五、数据驱动 DOM 更新 – Watcher Observer 和 Dep

正在热气腾腾更新中,欢送交换~ 欢送催更~

退出移动版