国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Vue編程三部曲之將template編譯成AST示例詳解

3403771864 / 495人閱讀

  知道嗎?Vue.js 有 2 個(gè)版本,一個(gè)是Runtime + Compiler版本,另一個(gè)是Runtime only版本。Runtime + Compiler版本是包含編譯代碼的,簡單來說就是Runtime only版本不包含編譯代碼的,在運(yùn)行時(shí)候,需要借助 webpack 的 vue-loader 事先把模板編譯成 render 函數(shù)。

  假如在你需要在客戶端編譯模板 (比如傳入一個(gè)字符串給 template 選項(xiàng),或掛載到一個(gè)元素上并以其 DOM 內(nèi)部的 HTML 作為模板),就將需要加上編譯器,即完整版:

  // 需要編譯器
  new Vue({
  template: '<div>{{ hi }}</div>'
  })
  // 不需要編譯器
  new Vue({
  render (h) {
  return h('div', this.hi)
  }
  })

  當(dāng)使用 vue-loader 或 vueify 的時(shí)候,*.vue 文件內(nèi)部的模板會(huì)在構(gòu)建時(shí)預(yù)編譯成 JavaScript。其實(shí)要知道并不需要編譯器的,只用繼續(xù)運(yùn)行就可以。因?yàn)檫\(yùn)行時(shí)版本相比完整版體積要小大約 30%,所以應(yīng)該盡可能使用這個(gè)版本。

  在 Vue 的整個(gè)編譯過程中,會(huì)做三件事:

  解析模板parse,生成 AST

  優(yōu)化 ASToptimize

  生成代碼generate

  對(duì)編譯過程的了解會(huì)讓我們對(duì) Vue 的指令、內(nèi)置組件等有更好的理解。不過由于編譯的過程是一個(gè)相對(duì)復(fù)雜的過程,我們只要求理解整體的流程、輸入和輸出即可,對(duì)于細(xì)節(jié)我們不必?fù)柑?xì)。由于篇幅較長,這里會(huì)用三篇文章來講這三件事。這是第一篇, 模板解析,template -> AST

  注:全文源碼來源,Vue(2.6.11),Runtime + Compiler 的 Vue.js

  編譯準(zhǔn)備

  這里先做一個(gè)準(zhǔn)備工作,編譯之前有一個(gè)嵌套的函數(shù)調(diào)用,看似非常的復(fù)雜,但是卻有玄機(jī)。有什么玄機(jī)?接著往下看。

  源碼編譯鏈?zhǔn)秸{(diào)用

  compileToFunctions

  在源碼走了一遭,發(fā)現(xiàn)經(jīng)過一系列的調(diào)用,最后createCompiler函數(shù)返回的compileToFunctions函數(shù) 對(duì)應(yīng)的就是$mount函數(shù)調(diào)用的compileToFunctions方法,它是調(diào)用createCompileToFunctionFn方法的返回值。

  // 偽代碼
  function createCompilerCreator (baseCompile) {
  return function createCompiler (baseOptions) {
  function compile (
  template,
  options
  ) {
  ...
  return compiled
  }
  return {
  compile: compile,
  compileToFunctions: createCompileToFunctionFn(compile)
  }
  }
  }
  function createCompileToFunctionFn (compile) {
  var cache = Object.create(null);
  return function compileToFunctions (
  template,
  options,
  vm
  ) {
  ...
  }
  }

  方法接受三個(gè)參數(shù)。

  編譯模板 template

  編譯配置 options

  Vue 的實(shí)例

  這個(gè)方法編譯的核心代碼就一行。

  // compile
  var compiled = compile(template, options);

  而 compile 方法的核心代碼也就一行。

  const compiled = baseCompile(template, finalOptions)

  并且baseCompile方法是在執(zhí)行createCompilerCreator方法執(zhí)行的時(shí)候傳入的。

  var createCompiler = createCompilerCreator(function baseCompile (
  template,
  options
  ) {
  var ast = parse(template.trim(), options);
  if (options.optimize !== false) {
  optimize(ast, options);
  }
  var code = generate(ast, options);
  return {
  ast: ast,
  render: code.render,
  staticRenderFns: code.staticRenderFns
  }
  });

  baseCompile會(huì)做三件事情。

  現(xiàn)在我們回到createCompilerCreator傳入的函數(shù)。

  這是為什么?就是因?yàn)閂ue 本身是支持多平臺(tái)的編譯,在不同平臺(tái)下的編譯會(huì)有所有不同,但是在同一平臺(tái)編譯是相同的,所以在使用createCompiler(baseOptions)時(shí),baseOptions 會(huì)有所有不同。

  在 Vue 中利用函數(shù)柯里化的思想,將baseOptions的配置參數(shù)進(jìn)行了保存。并且在調(diào)用鏈中,不斷的進(jìn)行函數(shù)調(diào)用并返回函數(shù)。

  這其實(shí)也是利用了函數(shù)柯里化的思想把很多基礎(chǔ)的函數(shù)抽離出來, 通過 createCompilerCreator(baseCompile) 的方式把真正編譯的過程和其它邏輯如對(duì)編譯配置處理、緩存處理等剝離開,這樣的設(shè)計(jì)還是非常巧妙的。

  編譯準(zhǔn)備已經(jīng)做完,我們接下來看看 Vue 是如何做parse的。

  parse

  parse要做的事情就是對(duì) template 做解析,生成 AST 抽象語法樹。

  抽象語法樹(Abstract Syntax Tree,AST),或簡稱語法樹(Syntax tree),是源代碼語法結(jié)構(gòu)的一種抽象表示。它以樹狀的形式表現(xiàn)編程語言的語法結(jié)構(gòu),樹上的每個(gè)節(jié)點(diǎn)都表示源代碼中的一種結(jié)構(gòu)。


  例如現(xiàn)在有這樣一段代碼:

  <body>
  <div id="app"></div>
  <script>
  new Vue({
  el: '#app',
  template: `
  <ul>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  <li>1</li>
  </ul>
  `
  });
  </script>
  </body>

  經(jīng)過parse,就變成了一個(gè)嵌套的樹狀結(jié)構(gòu)的對(duì)象。

  在 AST 中,每一個(gè)樹節(jié)點(diǎn)都是一個(gè) element,并且維護(hù)了上下文關(guān)系(父子關(guān)系)。

  解析 template

  parse的過程核心就是parseHTML函數(shù),這個(gè)函數(shù)的作用就是解析 template 模板。下面將解析過程中一些重要的點(diǎn)進(jìn)行一個(gè)抽象解讀。

  function parseHTML (html, options) {
  var stack = [];
  ...
  // 遍歷模板字符串
  while (html) {
  ...
  }
  // 清除所有剩余的標(biāo)簽
  parseEndTag();
  // 將 html 字符串的指針前移
  function advance (n) {
  ...
  }
  // 解析開始標(biāo)簽
  function parseStartTag () {
  ...
  }
  // 處理解析的開始標(biāo)簽的結(jié)果
  function handleStartTag (match) {
  ...
  }
  // 解析結(jié)束標(biāo)簽
  function parseEndTag (tagName, start, end) {
  ...
  }
  }

  標(biāo)簽匹配相關(guān)的正則

  接下來就是關(guān)于指令匹配相關(guān)的正則。相信很多人都有測試過。

  // 識(shí)別合法的xml標(biāo)簽
  var ncname = '[a-zA-Z_][\w\-\.]*';
  // 復(fù)用拼接,這在我們項(xiàng)目中完成可以學(xué)起來
  var qnameCapture = "((?:" + ncname + "\:)?" + ncname + ")";
  // 匹配注釋
  var comment =/^<!--/;
  // 匹配<!DOCTYPE> 聲明標(biāo)簽
  var doctype = /^<!DOCTYPE [^>]+>/i;
  // 匹配條件注釋
  var conditionalComment =/^<![/;
  // 匹配開始標(biāo)簽
  var startTagOpen = new RegExp(("^<" + qnameCapture));
  // 匹配解說標(biāo)簽
  var endTag = new RegExp(("^<\/" + qnameCapture + "[^>]*>"));
  // 匹配單標(biāo)簽
  var startTagClose = /^\s*(/?)>/;
  // 匹配屬性,例如 id、class
  var attribute = /^\s*([^\s"'<>/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
  // 匹配動(dòng)態(tài)屬性,例如 v-if、v-else
  var dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)[[^=]+][^\s"'<>/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;

  stack

  變量stack,它定義一個(gè)棧,作用是存儲(chǔ)開始標(biāo)簽。例如我有一個(gè)這樣的簡單模板:

  <div>
  <ul>
  <li>1</li>
  </ul>
  </div>

  當(dāng)在 while 循環(huán)時(shí),如果遇到一個(gè)非單標(biāo)簽,就會(huì)將開始標(biāo)簽 push 到數(shù)組中,遇到閉合標(biāo)簽就開始元素出棧,這樣可以檢測我們寫的 template 是否符合嵌套、開閉規(guī)范,這也是檢測 html 字符串中是否缺少閉合標(biāo)簽的原理。

  advance

  advance函數(shù)貫穿這個(gè) template 的解析流程。當(dāng)我們在解析 template 字符串的時(shí)候,需要對(duì)字符串逐一掃描,直到結(jié)束。advance 函數(shù)的作用就是移動(dòng)指針。例如匹配<字符,指針移動(dòng) 1,匹配到<!--字符指針移動(dòng) 4。在整個(gè)解析過程中,貫穿著指針的移動(dòng),因?yàn)橐虢馕鐾瓿删捅仨毎涯0迦烤幾g完。

  function advance (n) {
  index += n;
  html = html.substring(n);
  }

  while

  template 的 while 循環(huán)是解析中最重要的一環(huán),也是這一小節(jié)的重點(diǎn)。

  循環(huán)的終止條件是 html 字符串為空,即 html 字符串全部編譯完畢。

  循環(huán)時(shí),第一個(gè)判斷是判斷內(nèi)容是否在存純文本標(biāo)簽中。判斷的作用是: 確保我們沒有像腳本/樣式這樣的純文本內(nèi)容元素。

  當(dāng)內(nèi)容不在純文本標(biāo)簽,判斷 template 字符串的第一個(gè)<字符位置,來進(jìn)行不同的操作。

  var textEnd = html.indexOf('<');

  當(dāng)前 template 第一個(gè)字符是 <

  在這種場景下, template 會(huì)出現(xiàn)以下幾種情況,重點(diǎn)是解析開始標(biāo)簽和結(jié)束標(biāo)簽。

  <!--開頭的注釋:會(huì)找到注釋的結(jié)尾,將注釋截取出來,移動(dòng)指針,并將注釋當(dāng)做當(dāng)前父節(jié)點(diǎn)的一個(gè)子元素存儲(chǔ)到 children 中。

  <![開頭的 條件注釋:如果是條件注釋,會(huì)直接移動(dòng)指針,不做任何其他操作。

  <!DOCTYPE開頭的 doctype:如果是 doctype,會(huì)直接移動(dòng)指針,不做任何其他操作。

  <開頭的開始標(biāo)簽

  <開頭的結(jié)束標(biāo)簽 接下來重點(diǎn)講講如何解析開始標(biāo)簽和結(jié)束標(biāo)簽。

  解析開始標(biāo)簽

  ①,通過正則匹配到開始標(biāo)簽,如果匹配到就會(huì)返回一個(gè) match 的匹配結(jié)果。例如:

  <div id="test-id" class="test-calss" v-show='show'></div>

  template 中有一個(gè) div,當(dāng)匹配到開始標(biāo)簽(結(jié)束標(biāo)簽類似)時(shí),會(huì)返回這樣數(shù)組結(jié)果。

  0: "<div"

  1: "div"

  groups: undefined

  index: 0

  input: "<div>\n <ul>\n <li>1</li>\n </ul>\n </div>"

  length: 2

  ②,接下來: 定義了 match 變量,它是一個(gè)對(duì)象,初始狀態(tài)下?lián)碛腥齻€(gè)屬性:

  tagName:存儲(chǔ)標(biāo)簽的名稱。div。

  attrs :用來存儲(chǔ)將來被匹配到的屬性,例如:id、class、v-if 這些屬性。

  start:初始值為 index,是當(dāng)前字符流讀入位置在整個(gè) html 字符串中的相對(duì)位置。 ③,然后通過advance函數(shù)移動(dòng)指針。

  ④,如果沒有匹配到開始標(biāo)簽的結(jié)束部分,并且存在屬性,就會(huì)遍歷找出所有屬性和動(dòng)態(tài)屬性。保存在 match 的 attrs 中。

  ⑤,上一步獲取了標(biāo)簽的屬性和動(dòng)態(tài)屬性,但是即使這樣并不能說明這是一個(gè)完整的標(biāo)簽,只有當(dāng)匹配到開始標(biāo)記的結(jié)束標(biāo)記時(shí),才能證明這是一個(gè)完整的標(biāo)簽,所以才會(huì)有這一步的判斷。varstartTagClose= /^\s*(/?)>/;并且標(biāo)記unarySlash屬性。

  ⑥,假設(shè)正常匹配了,有匹配結(jié)果,也返回了 match (結(jié)構(gòu)如上),就會(huì)走到handleStartTag這個(gè)函數(shù)的作用就是用來處理開始標(biāo)簽的解析結(jié)果,所以它接收 parseStartTag 函數(shù)的返回值作為參數(shù)。

  handleStartTag 的核心邏輯很簡單,先判斷開始標(biāo)簽是否是一元標(biāo)簽,類似<img />、<br/>這樣,接著對(duì) match.attrs 遍歷并做了一些處理,最后判斷如果非一元標(biāo)簽,則往 stack 里 push 一個(gè)對(duì)象,并且把 tagName 賦值給 lastTag。

  function parseStartTag () {
  // ①
  var start = html.match(startTagOpen);
  if (start) {
  // ②
  var match = {
  tagName: start[1],
  attrs: [],
  start: index
  };
  // ③
  advance(start[0].length);
  var end, attr;
  // ④
  while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) {
  ...
  }
  // ⑤
  if (end) {
  match.unarySlash = end[1];
  advance(end[0].length);
  match.end = index;
  return match
  }
  }
  }
  // Start tag:
  var startTagMatch = parseStartTag();
  if (startTagMatch) {
  handleStartTag(startTagMatch);
  ...
  }
  // ⑥
  function handleStartTag (match) {
  ...
  }

  解析結(jié)束標(biāo)簽

  有解析開始標(biāo)簽就會(huì)解析結(jié)束標(biāo)簽。所以接下來我們來看看如何解析結(jié)束標(biāo)簽。

  ①,正則匹配結(jié)束標(biāo)簽(具體的正則看前面)。

  ②,匹配到結(jié)束標(biāo)簽,進(jìn)行解析處理,獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置,開始進(jìn)行解析操作。

  ③,查找同一類型的最近打開的標(biāo)記,并記錄位置。

  ④,如果存在同一類型的標(biāo)記,就將 stack 中匹配的標(biāo)記彈出。

  ⑤,如果沒有同一類型的標(biāo)記,分別處理</br>、</p>標(biāo)簽。這是為了和瀏覽器保持同樣的行為。舉個(gè)例子:在代碼中,分別寫了</br>、</p>的結(jié)束標(biāo)簽,但注意我們并沒有寫起始標(biāo)簽,但是瀏覽器是能夠正常解析他們的,其中</br>標(biāo)簽被正常解析為<br>標(biāo)簽,而</p>標(biāo)簽被正常解析為<p></p>。除了br與p其他任何標(biāo)簽如果你只寫了結(jié)束標(biāo)簽?zāi)敲礊g覽器都將會(huì)忽略。所以為了與瀏覽器的行為相同,parseEndTag 函數(shù)也需要專門處理br與p的結(jié)束標(biāo)簽,即:</br> 和</p>。

  <div>
  </br>
  </p>
  </div>

  // ①
  var endTagMatch = html.match(endTag);
  if (endTagMatch) {
  var curIndex = index;
  advance(endTagMatch[0].length);
  // ②
  // 獲取到結(jié)束標(biāo)簽的標(biāo)簽名稱、開始位置和結(jié)束位置
  parseEndTag(endTagMatch[1], curIndex, index);
  continue
  }
  function parseEndTag (tagName, start, end) {
  ...
  // ③
  if (tagName) {
  lowerCasedTagName = tagName.toLowerCase();
  for (pos = stack.length - 1; pos >= 0; pos--) {
  if (stack[pos].lowerCasedTag === lowerCasedTagName) {
  break
  }
  }
  } else {
  pos = 0;
  }
  // ④
  if (pos >= 0) {
  ...
  stack.length = pos;
  lastTag = pos && stack[pos - 1].tag;
  // ⑤
  } else if (lowerCasedTagName === 'br') {
  if (options.start) {
  options.start(tagName, [], true, start, end);
  }
  } else if (lowerCasedTagName === 'p') {
  if (options.start) {
  options.start(tagName, [], false, start, end);
  }
  if (options.end) {
  options.end(tagName, start, end);
  }
  }
  }

  到這里結(jié)束標(biāo)簽頁解析完成,但是在 Vue 中對(duì)開始標(biāo)簽和結(jié)束標(biāo)簽的解析遠(yuǎn)不止這樣,因?yàn)闉榱藶g覽器行為保持一下在解析的過程中還會(huì)對(duì)一些特殊標(biāo)簽特殊處理,典型的就是p、br標(biāo)簽,我會(huì)在后面出一篇文章來詳細(xì)講講 Vue 是如何處理它們的。

  當(dāng)前 template 不存在 <

  當(dāng)解析到的 template 中不存在 < 時(shí),這認(rèn)為是一個(gè)文本。操作很簡單就是移動(dòng)指針。

  并且這里在源碼中發(fā)現(xiàn)初始化變量的時(shí)候,都是這樣寫的 var text =(void0), rest =(void0), next =(void0);而不是直接 var xx = undefined,這樣做就是為了更加安全。

  JavaScript void 運(yùn)算符

  if (textEnd < 0) {
  text = html;
  }
  if (text) {
  advance(text.length);
  }

  當(dāng)前 template < 不在第一個(gè)字符串

  這里的判斷處理就是為了處理我們在一些純文本中也會(huì)寫<標(biāo)記的場景。例如:

  <div>1<2</div>

  現(xiàn)在有這樣一段模塊,<div>被解析之后,還剩1<2,這時(shí)解析到存在<標(biāo)記但是位置不在第一個(gè)。就循環(huán)找出包含<的這一段文本,并將這一段當(dāng)成一個(gè)純文本處理。

  if (textEnd >= 0) {
  rest = html.slice(textEnd);
  while (
  !endTag.test(rest) &&
  !startTagOpen.test(rest) &&
  !comment.test(rest) &&
  !conditionalComment.test(rest)
  ) {
  next = rest.indexOf('<', 1);
  if (next < 0) { break }
  textEnd += next;
  rest = html.slice(textEnd);
  }
  text = html.substring(0, textEnd);
  }

  處理 stack 棧中剩余未處理的標(biāo)簽

  當(dāng) while 循環(huán)解析了一遍 template 之后,會(huì)再調(diào)用一次parseEndTag,這樣做的目的是為了處理 stack 棧中剩余未處理的標(biāo)簽。當(dāng)調(diào)用時(shí),沒有傳遞任何參數(shù),也意味著tagName, start, end都是空的,這時(shí) pos 為 0 ,所以 i >= pos 始終成立,這個(gè)時(shí)候 stack 棧中如果有剩余未處理的標(biāo)簽,則會(huì)逐個(gè)警告缺少閉合標(biāo)簽,并調(diào)用 options.end 將其閉合。

  // Clean up any remaining tags
  parseEndTag();
  function parseEndTag (tagName, start, end) {
  if (tagName) {
  ...
  } else {
  pos = 0;
  }
  if (pos >= 0) {
  for (var i = stack.length - 1; i >= pos; i--) {
  if (i > pos || !tagName && options.warn) {
  options.warn(
  ("tag <" + (stack[i].tag) + "> has no matching end tag."),
  { start: stack[i].start, end: stack[i].end }
  );
  }
  }
  ...
  }
  }

  到這里解析 template 的重點(diǎn)過程都基本結(jié)束了,整個(gè)過程就是遍歷 template 字符串,然后通過正則一點(diǎn)一點(diǎn)的匹配解析字符串,直到整個(gè)字符串被解析完成。

  生成 AST

  當(dāng)然解析完 template 目的是生成 AST,經(jīng)過上面的一些列操作,只是解析完 template 字符串,并沒有生成一顆 AST 抽象語法樹。正常的來說抽象語法樹應(yīng)該是如下這樣的,節(jié)點(diǎn)與節(jié)點(diǎn)之間通過 parent 和 children 建立聯(lián)系,每個(gè)節(jié)點(diǎn)的 type 屬性用來標(biāo)識(shí)該節(jié)點(diǎn)的類別,比如 type 為 1 代表該節(jié)點(diǎn)為元素節(jié)點(diǎn),type 為 3 代表該節(jié)點(diǎn)為文本節(jié)點(diǎn)。

  生成 AST 的主要步驟是在解析的過程中,會(huì)調(diào)用對(duì)應(yīng)的鉤子函數(shù)。解析到開始標(biāo)簽,就調(diào)用開始的鉤子函數(shù),解析到結(jié)束標(biāo)簽就調(diào)用結(jié)束的鉤子函數(shù),解析到文本就會(huì)調(diào)用文本的鉤子,解析到注釋就調(diào)用注釋的鉤子函數(shù)。這些鉤子函數(shù)就會(huì)將所有的節(jié)點(diǎn)串聯(lián)起來,并生成 AST 樹的結(jié)構(gòu)。

  start 鉤子函數(shù)

  這個(gè)鉤子函數(shù)會(huì)在解析到開始標(biāo)簽的時(shí)候被調(diào)用。為了更加清楚解析過程,我們引入如下一個(gè)模板,如下:

  <div><span></span><p></p></div>

  解析 <div>

  ①,解析到<div>會(huì)調(diào)用 start 鉤子函數(shù)。

  ②,創(chuàng)建一個(gè)基礎(chǔ)元素對(duì)象。

  {
  type: 1,
  tag:"div",
  parent: null,
  children: [],
  attrsList: []
  }
  function createASTElement (
  tag,
  attrs,
  parent
  ) {
  return {
  type: 1,
  tag: tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  rawAttrsMap: {},
  parent: parent,
  children: []
  }
  }

  ③,接著判斷 root 是否存在,如果不存在則直接將 element 賦值給 root 。root 是一個(gè)記錄值,也就是最后解析返回的整個(gè) AST。

  ④,如果當(dāng)前標(biāo)簽不是一元標(biāo)簽時(shí),會(huì)將當(dāng)前的element賦值給currentParent目的是為建立父子元素的關(guān)系。

  ⑤,將元素入棧,入棧的目的是為了做回退操作,這里先不講為什么需要做回退,后面在講。此時(shí)stack = [{ tag : "div"... }]。


  // parseHTML函數(shù) 解析到開始標(biāo)簽
  function handleStartTag (match) {
  if (options.start) {
  // ①
  options.start(tagName, attrs, unary, match.start, match.end);
  }
  }
  // start 鉤子函數(shù)
  start: {
  // ②
  var element = createASTElement(tag, attrs, currentParent);
  // element:
  // {
  // type: 1,
  // tag:"div",
  // parent: null,
  // children: [],
  // attrsList: []
  // }
  // ③
  if (!root) {
  root = element;
  }
  // ④
  if (!unary) {
  currentParent = element;
  // currentParent:
  // {
  // type: 1,
  // tag:"div",
  // parent: null,
  // children: [],
  // attrsList: []
  // }
  // ⑤
  stack.push(element);
  }
  }

  解析<span>

  接著解析到<span>。此時(shí) root 已經(jīng)存在,currentParent 也存在,所以會(huì)將 span 元素的描述對(duì)象添加到 currentParent 的 children 數(shù)組中作為子節(jié)點(diǎn),并將自己的 parent 元素進(jìn)行標(biāo)記。所以最終生成的描述對(duì)象為:

  {
  type: 1,
  tag:"div",
  parent: {/*div 元素的描述*/},
  attrsList: []
  children: [{
  type: 1,
  tag:"span",
  parent: div,
  attrsList: [],
  children:[]
  }],
  }

  此時(shí) stack = [{ tag : "div"... }, {tag : "span"...}]。

  end 鉤子函數(shù)

  當(dāng)解析到結(jié)束標(biāo)簽就會(huì)調(diào)用結(jié)束標(biāo)簽的鉤子函數(shù),還是這段模板代碼,解析完<div><span>后遇到了</span>。

  1

  <div><span></span><p></p></div>

  解析

  ①,首先就是保存最后一個(gè)元素,將 stack 的最后一個(gè)元素刪除,也就是變成 stack = [{tag: "div" ...}],這就是做了一個(gè)回退操作 。

  ②,設(shè)置 currentParent 為 stack 的最后一個(gè)元素。

 

 end: function end (tag, start, end$1) {
  // ①
  var element = stack[stack.length - 1];
  stack.length -= 1;
  // ②
  currentParet = stack[stack.length - 1];
  ...
  },

  為什么回退?

  解析 <p>

  當(dāng)再次解析到開始標(biāo)簽時(shí),就會(huì)再次調(diào)用 start 鉤子函數(shù),這里重點(diǎn)是在解析 p 的開始標(biāo)簽時(shí):stack = [{tag:"div"...},{tag:"p"...}] ,由于在解析到上一個(gè)</span>標(biāo)簽時(shí)做了一個(gè)回退操作, 這就能保證在解析 p 開始標(biāo)簽的時(shí)候,stack 中存儲(chǔ)的是 p 標(biāo)簽父級(jí)元素的描述對(duì)象。

  解析 </p>

  解析結(jié)束標(biāo)簽,做回退操作。

  遇到開始標(biāo)簽就生成元素,勾勒上下文關(guān)系 parent、children 等,每當(dāng)遇到一個(gè)非一元標(biāo)簽的結(jié)束標(biāo)簽時(shí),都會(huì)回退 currentParent 變量的值為之前的值,這樣就修正了當(dāng)前正在解析的元素的父級(jí)元素。

  chars 鉤子函數(shù)

  當(dāng)然在我們的代碼中肯定不止是開始和結(jié)束標(biāo)簽,還會(huì)有文本。當(dāng)遇到文本時(shí),就會(huì)調(diào)用 chars 鉤子函數(shù)。

  ①,首先判斷 currentParent(指向的是當(dāng)前節(jié)點(diǎn)的父節(jié)點(diǎn)) 變量是否存在,不存在就說明,說明 1:只有文本節(jié)點(diǎn)。2:文本在根元素之外。這兩種情況都會(huì)警告 ?? 提醒,接觸后面的操作。

  ②,第二個(gè)判斷主要是解決 ie textarea 占位符的問題。issue

  ③,判斷當(dāng)前元素未使用 v-pre 指令,text 不為空,使用 parseText 函數(shù)成功解析當(dāng)前文本節(jié)點(diǎn)的內(nèi)容。這里的重點(diǎn)在于 parseText 函數(shù),parseText 函數(shù)的作用就是用來解析如果我們的文本包含了字面量表達(dá)式。例如:

  <div>1111: {{ text }}</div>

  這樣的文本就會(huì)解析成如下的一個(gè)描述對(duì)象, 包含 expression 、tokens (包含原始的文本)。

  解析完之后會(huì)生成一個(gè) type = 2 的描述對(duì)象:

  child = {
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
  };

  ④,如果使用了 v-pre || test 為空 || parseText 解析失敗,那么就會(huì)生成一個(gè) type = 3 的存文本描述對(duì)象。

  child = {
  type: 1,
  text: text
  };

  ⑤,最后將解析到描述對(duì)象,添加到當(dāng)前父元素的 children 列表中,注意:這里之前說明過因?yàn)槲覀兊恼麄€(gè) template 是不能是純文本的,必須由根元素,所以如果是文本節(jié)點(diǎn),一點(diǎn)是會(huì)有父元素的。

 

 chars: function chars (text, start, end) {
  // ①
  if (!currentParent) {
  {
  if (text === template) {
  ...警告
  } else if ((text = text.trim())) {
  ...警告
  }
  }
  return
  }
  // ②
  if (isIE &&
  currentParent.tag === 'textarea' &&
  currentParent.attrsMap.placeholder === text
  ) {
  return
  }
  var children = currentParent.children;
  ...
  if (text) {
  ...
  // ③
  if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
  child = {
  type: 2,
  expression: res.expression,
  tokens: res.tokens,
  text: text
  };
  // ④
  } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
  child = {
  type: 3,
  text: text
  };
  }
  // ⑤
  if (child) {
  ...
  children.push(child);
  }
  }
  },

  到這里文本節(jié)點(diǎn)的解析完成。接下來看看注釋解析的鉤子函數(shù)。

  commit 鉤子函數(shù)

  當(dāng)我們配置了 options.comments = true ,也就意味著我們需要保留我們的注釋,這個(gè)配置需要我們手動(dòng)開啟,開啟后就會(huì)在頁面渲染后保留注釋。

  注意:如果開啟了保留注釋匹配后,瀏覽器會(huì)保留注釋。但是可能對(duì)布局產(chǎn)生影響,尤其是對(duì)行內(nèi)元素的影響。為了消除這些影響帶來的問題,好的做法是將它們?nèi)サ簟?/p>

  注釋的解析比較簡單,就是創(chuàng)建注釋節(jié)點(diǎn),然后添加當(dāng)前父元素的子階段列表中。要注意的是純文本節(jié)點(diǎn)和注釋節(jié)點(diǎn)的描述對(duì)象的 type 都是 3,不同的是注釋節(jié)點(diǎn)的元素描述對(duì)象擁有 isComment 屬性,并且該屬性的值為 true,目的就是用來與普通文本節(jié)點(diǎn)作區(qū)分的。

  shouldKeepComment: options.comments,
  if (textEnd === 0) {
  ...
  if (options.shouldKeepComment) {
  options.comment(html.substring(4, commentEnd), index, index + commentEnd + 3);
  }
  ...
  }
  comment: function comment (text, start, end) {
  if (currentParent) {
  var child = {
  type: 3,
  text: text,
  isComment: true
  };
  ...
  currentParent.children.push(child);
  }
  }

  到這里在生成 AST 過程中的 四個(gè)鉤子函數(shù)已經(jīng)全部講完。但是 Vue 本身在對(duì)元素做處理的時(shí)候的時(shí)候肯定不會(huì)是這么簡單的,因?yàn)檫@處理的過程中還要處理一元標(biāo)簽、靜態(tài)屬性、動(dòng)態(tài)屬性等。

  番外(可跳過)

  這一小節(jié)注意是看看在生成 AST 過程中的一些重要的工具函數(shù)。

  createASTElement 函數(shù)

  創(chuàng)建元素的描述對(duì)象。

  function createASTElement (
  tag,
  attrs,
  parent
  ) {
  return {
  type: 1,
  tag: tag,
  attrsList: attrs,
  attrsMap: makeAttrsMap(attrs),
  rawAttrsMap: {},
  parent: parent,
  children: []
  }
  }

  指令解析相關(guān)的正則

  前面也講到關(guān)于一些標(biāo)簽匹配相關(guān)的正則。其實(shí)這些正則大家在平時(shí)的項(xiàng)目中有涉及也可以用起來,畢竟這些正則是經(jīng)過千萬人測試的。

  var onRE = /^@|^v-on:/;
  var dirRE = /^v-|^@|^:|^#/;
  var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;
  var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/;
  var stripParensRE = /^(|)$/g;
  var dynamicArgRE = /^[.*]$/;
  var argRE = /:(.*)$/;
  var bindRE = /^:|^.|^v-bind:/;
  var modifierRE = /.[^.]]+(?=[^]]*$)/g;

  onRE

  匹配已字符@或者v-on開頭的字符串,檢測標(biāo)簽屬性是否是監(jiān)聽事件的指令。

  var onRE = /^@|^v-on:/;

  dirRE

  匹配v-、@、:、#開頭的字符串,檢測屬性名是否是指令。v-開頭的屬性統(tǒng)統(tǒng)都認(rèn)為是指令。@字符是 v-on 的縮寫。:是 v-bind 的縮寫。#是 v-slot 的縮寫。

  var dirRE = /^v-|^@|^:|^#/;

  forAliasRE

  匹配v-for屬性的值,目的是捕獲 in 或者 of 前后的字符串。

  var forAliasRE = /([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/;

  forIteratorRE

  這個(gè)也是用來匹配v-for屬性的,不同的是,這里是匹配遍歷時(shí)的 value 、 key 、index 。

  var forIteratorRE = /,([^,}]]*)(?:,([^,}]]*))?$/;

  stripParensRE

  匹配以字符(開頭、)結(jié)尾的字符串。作用是配合上面的正則對(duì)字符進(jìn)行處理(、)。

  var stripParensRE = /^(|)$/g;

  argRE

  匹配指令中的參數(shù)。作用是捕獲指令中的參數(shù)。常見的就是指令中的修飾符。

  var argRE = /:(.*)$/;

  bindRE

  匹配:、.、v-bind:開頭的字符串。作用是檢查屬性是否是綁定。

  var bindRE = /^:|^.|^v-bind:/;

  modifierRE

  匹配修飾符。主要作用是判斷是否有修飾符。

  var modifierRE = /.[^.]]+(?=[^]]*$)/g;

  parseText 函數(shù)

  這個(gè)函數(shù)的作用是解析 text,在上面講 chars 鉤子函數(shù)的時(shí)候也說到這個(gè)函數(shù)。函數(shù)有兩個(gè)參數(shù)text、delimiters。delimiters參數(shù)作用就是:改變純文本插入分隔符。例如:

  delimiters: ['${', '}'],
  // 模板
  <div>{{ text }}</div>

  模板會(huì)被編譯成這樣。

  在 parseText 函數(shù)中,重點(diǎn)邏輯是開啟一個(gè) while 循環(huán),使用 tagRE 正則匹配文本內(nèi)容,并將匹配結(jié)果保存在 match 變量中,直到匹配失敗循環(huán)才會(huì)終止,這時(shí)意味著所有的字面量表達(dá)式都已經(jīng)處理完畢了。

  while ((match = tagRE.exec(text))) {
  index = match.index;
  if (index > lastIndex) {
  rawTokens.push(tokenValue = text.slice(lastIndex, index));
  tokens.push(JSON.stringify(tokenValue));
  }
  var exp = parseFilters(match[1].trim());
  tokens.push(("_s(" + exp + ")"));
  rawTokens.push({ '@binding': exp });
  lastIndex = index + match[0].length;
  }

  例如有一段這樣的 template:

  // text: '小白',
  // message: '好久不見'
  <div>hello, {{ text }},{{ message }}</div>

  會(huì)被解析成如下 AST:

  closeElement 函數(shù)

  這個(gè)函數(shù)會(huì)在解析非一元開始標(biāo)簽和解析結(jié)束標(biāo)簽的時(shí)候調(diào)用,主要作用有兩個(gè):

  對(duì)數(shù)據(jù)狀態(tài)進(jìn)行還原,

  調(diào)用后置處理轉(zhuǎn)換鉤子函數(shù)。

  整體流程

  Vue 編譯三部曲第一步parse的整個(gè)流程已經(jīng)講述完畢,我們看著源代碼可能決定很相似,但假如只是抽離主流程的話,還是比較簡單的。parse的目的是將開發(fā)者寫的template模板字符串轉(zhuǎn)換成抽象語法樹 AST ,AST 就這里來說就是一個(gè)樹狀結(jié)構(gòu)的 JavaScript 對(duì)象,整個(gè)內(nèi)容就是描述上下關(guān)系。那么整個(gè)parse的過程是利用很多正則表達(dá)式順序解析模板,當(dāng)解析到開始標(biāo)簽、閉合標(biāo)簽、文本的時(shí)候都會(huì)分別執(zhí)行對(duì)應(yīng)的回調(diào)函數(shù),來達(dá)到構(gòu)造 AST 樹的目的。



文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請(qǐng)注明本文地址:http://m.specialneedsforspecialkids.com/yun/127821.html

相關(guān)文章

  • Vue編程部曲之模型樹優(yōu)化實(shí)戰(zhàn)代碼

      實(shí)踐是所有展示最好的方法,因此我覺得可以不必十分細(xì)致的,但我們的展示卻是整體的流程、輸入和輸出。現(xiàn)在我們就看看Vue 的指令、內(nèi)置組件等。也就是第二篇,模型樹優(yōu)化。  分析了 Vue 編譯三部曲的第一步,「如何將 template 編譯成 AST ?」上一篇已經(jīng)介紹,但我們還是來總結(jié)回顧下,parse 的目的是將開發(fā)者寫的 template 模板字符串轉(zhuǎn)換成抽象語法樹 AST ,AST 就這里...

    3403771864 評(píng)論0 收藏0
  • 聊聊Vue.js的template編譯

    摘要:具體可以查看抽象語法樹。而則是帶緩存的編譯器,同時(shí)以及函數(shù)會(huì)被轉(zhuǎn)換成對(duì)象。會(huì)用正則等方式解析模板中的指令等數(shù)據(jù),形成語法樹。是將語法樹轉(zhuǎn)化成字符串的過程,得到結(jié)果是的字符串以及字符串。里面的節(jié)點(diǎn)與父節(jié)點(diǎn)的結(jié)構(gòu)類似,層層往下形成一棵語法樹。 寫在前面 因?yàn)閷?duì)Vue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。 文...

    gnehc 評(píng)論0 收藏0
  • vue分析之template模板解析AST

    摘要:注意看注釋很粗很簡單,我就是一程序員姓名,年齡,請(qǐng)聯(lián)系我吧是否保留注釋定義分隔符,默認(rèn)為對(duì)于轉(zhuǎn)成,則需要先獲取,對(duì)于這部分內(nèi)容,做一個(gè)簡單的分析,具體的請(qǐng)自行查看源碼。其中的負(fù)責(zé)修改以及截取剩余模板字符串。 通過查看vue源碼,可以知道Vue源碼中使用了虛擬DOM(Virtual Dom),虛擬DOM構(gòu)建經(jīng)歷 template編譯成AST語法樹 -> 再轉(zhuǎn)換為render函數(shù) 最終返回...

    2bdenny 評(píng)論0 收藏0
  • JS每日一題:簡述一下Vue.js的template編譯過程?

    摘要:問簡述一下的編譯過程先上一張圖大致看一下整個(gè)流程從上圖中我們可以看到是從后開始進(jìn)行中整體邏輯分為三個(gè)部分解析器將模板字符串轉(zhuǎn)換成優(yōu)化器對(duì)進(jìn)行靜態(tài)節(jié)點(diǎn)標(biāo)記,主要用來做虛擬的渲染優(yōu)化代碼生成器使用生成函數(shù)代碼字符串開始前先解釋一下抽象 20190215問 簡述一下Vue.js的template編譯過程? 先上一張圖大致看一下整個(gè)流程showImg(https://image-static....

    NicolasHe 評(píng)論0 收藏0
  • Vue編譯AST抽象語法樹源碼分析

     直接進(jìn)入核心現(xiàn)在說說baseCompile核心代碼:  //`createCompilerCreator`allowscreatingcompilersthatusealternative   //parser/optimizer/codegen,e.gtheSSRoptimizingcompiler.   //Herewejustexportadefaultcompilerusingthede...

    3403771864 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<