فهرست منبع

new: explain the element description object of the tag using the v-pre directive

HcySunYang 7 سال پیش
والد
کامیت
ce94d74ea2
1فایلهای تغییر یافته به همراه210 افزوده شده و 3 حذف شده
  1. 210 3
      docs/art/82vue-parsing.md

+ 210 - 3
docs/art/82vue-parsing.md

@@ -1679,7 +1679,7 @@ function processIfConditions (el, parent) {
 
 在 `processIfConditions` 函数内部,首先通过 `findPrevElement` 函数找到当前元素的前一个元素描述对象,并将其赋值给 `prev` 常量,接着进入 `if` 条件语句,判断当前元素的前一个元素是否使用了 `v-if` 指令,我们知道对于使用了 `v-else-if` 或 `v-else` 指令的元素来讲,他们的前一个元素必然需要使用相符的 `v-if` 指令才行。如果前一个元素确实使用 `v-if` 指令,那么则会调用 `addIfCondition` 函数将当前元素描述对象添加到前一个元素的的 `ifConditions` 数组中。如果前一个元素没有使用 `v-if` 指令,那么此时将会进入 `else...if` 条件语句的判断,即如果是非生产环境下,会打印警告信息提示开发者没有相符的使用了 `v-if` 指令的元素。
 
-以上是当前元素使用了 `v-else-if` 或 `v-else` 指令时的特殊处理,由此可知**当一个元素使用了 `v-else-if` 或 `v-else` 指令时,它们是不会作为父级元素节点的**,而是会被添加到相符的使用了 `v-if` 指令的元素描述对象的 `ifConditions` 数组中。
+以上是当前元素使用了 `v-else-if` 或 `v-else` 指令时的特殊处理,由此可知**当一个元素使用了 `v-else-if` 或 `v-else` 指令时,它们是不会作为父级元素节点的**,而是会被添加到相符的使用了 `v-if` 指令的元素描述对象的 `ifConditions` 数组中。
 
 如果当前元素没有使用 `v-else-if` 或 `v-else` 指令,那么还会判断当前元素是否使用了 `slot-scope` 特性,如下:
 
@@ -1697,6 +1697,92 @@ if (element.elseif || element.else) {
 
 如上代码高亮代码所示,如果一个元素使用了 `slot-scope` 特性,那么该元素的描述对象会被添加到父级元素的 `scopedSlots` 对象下,也就是说使用了 `slot-scope` 特性的元素与使用了 `v-else-if` 或 `v-else` 指令的元素一样,他们都不会作为父级元素的子节点,对于使用了 `slot-scope` 特性的元素来讲它们将被添加到父级元素描述的 `scopedSlots` 对象下。另外由于如上代码中 `elseif` 语句块涉及 `slot-scope` 相关的处理,我们打算放到后面统一讲解。
 
+接着我们对象 `findPrevElement` 函数做一个补充讲解,`findPrevElement` 函数的作用是寻找当前元素的前一个元素节点,如下是其源码:
+
+```js
+function findPrevElement (children: Array<any>): ASTElement | void {
+  let i = children.length
+  while (i--) {
+    if (children[i].type === 1) {
+      return children[i]
+    } else {
+      if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
+        warn(
+          `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
+          `will be ignored.`
+        )
+      }
+      children.pop()
+    }
+  }
+}
+```
+
+首先 `findPrevElement` 函数只用在了 `processIfConditions` 函数中,它的作用就是当解析遇到一个带有 `v-else-if` 或 `v-if` 指令的元素时,找到该元素的前一个元素节点,假设我们解析如下 `html` 字符串:
+
+```html
+<div>
+  <div v-if="a"></div>
+  <p v-else-if="b"></p>
+  <span v-else="c"></span>
+</div>
+```
+
+当解析遇到带有 `v-else-if` 指令的 `p` 标签时,那么此时它的前一个元素节点应该是带有 `v-if` 指令的 `div` 标签,如何找到该 `div` 标签呢?由于当前正在解析的标签为 `p`,此时 `p` 标签的元素描述对象还没有被添加到父级元素描述对象的 `children` 数组中,所以此时父级元素描述对象的 `children` 数组中最后一个元素节点就应该是 `div` 元素。注意我们说的是**最后一个元素节点**,而不是**最后一个节点**。所以要想得到 `div` 标签,我们只要找到父级元素描述对象的 `children` 数组最后一个元素节点即可。
+
+当解析器遇到带有 `v-else` 指令的 `span` 标签时,大家思考一下此时 `span` 标签的前一个**元素节点**是什么?答案还是 `div` 标签,而不是 `p` 标签,这是因为 `p` 标签的元素描述对象没有被添加到父级元素描述对象的 `children` 数组中,而是被添加到 `div` 标签元素描述对象的 `ifConditions` 数组中了。所以对于 `span` 标签来讲,它的前一个元素节点仍然是 `div` 标签。
+
+总之我们发现 `findPrevElement` 函数只需要找到父级元素描述对象的最后一个元素节点即可,如下:
+
+```js
+function findPrevElement (children: Array<any>): ASTElement | void {
+  let i = children.length
+  while (i--) {
+    if (children[i].type === 1) {
+      return children[i]
+    } else {
+      if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
+        warn(
+          `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
+          `will be ignored.`
+        )
+      }
+      children.pop()
+    }
+  }
+}
+```
+
+`findPrevElement` 函数通过 `while` 循环从后向前遍历父级的子节点,并找到最后一个元素节点。理论上该节点就应该是带有 `v-if` 指令的元素,如果该元素节点没有 `v-if` 指令,会在 `processIfConditions` 函数中打印警告信息。大家注意 `while` 循环内的代码,使用 `if` 语句检测了子节点的类型是否为 `1`,即是否为元素节点,只有是元素节点时才会将该节点的描述对象作为返回值返回。如果在找到元素节点之前遇到了非元素节点,那么 `else` 分支的代码将会被执行:
+
+```js {10}
+if (children[i].type === 1) {
+  // 省略...
+} else {
+  if (process.env.NODE_ENV !== 'production' && children[i].text !== ' ') {
+    warn(
+      `text "${children[i].text.trim()}" between v-if and v-else(-if) ` +
+      `will be ignored.`
+    )
+  }
+  children.pop()
+}
+```
+
+如上高亮的那句代码所示,可以看到非元素节点被从 `children` 数组中 `pop` 出去,所以在非生产环境下如果该非元素节点的 `.text` 属性如果不为空,则打印警告信息提示开发者这部分存在于 `v-if` 指令和 `v-else(-if)` 指令之间的内容将被忽略。什么意思呢?举个例子:
+
+```html
+<div>
+  <div v-if="a"></div>
+  aaaaa
+  <p v-else-if="b"></p>
+  bbbbb
+  <span v-else="c"></span>
+</div>
+```
+
+如上代码中的文本 `aaaaa` 和 `bbbbb` 都将被忽略。
+
 到目前为止,我们大概粗略的过了一遍 `start` 钩子函数的内容,接下来我们做一些总结,以使得我们的思路更加清晰:
 
 * 1、`start` 钩子函数是当解析 `html` 字符串遇到开始标签时被调用的。
@@ -1713,7 +1799,7 @@ if (element.elseif || element.else) {
 
 以上的总结就是 `start` 钩子函数在处理开始标签时所做的事情,实际上由于开始标签中包含了大量指令信息(如 `v-if` 等)或特性信息(如 `slot-scope` 等),所以在生产 `AST` 过程中,大部分工作都是由 `start` 函数来完成的,接下来我们将更加细致的去讲解解析过程中的每一个细节。
 
-### 处理使用了 v-pre 指令的元素及其子元素
+### 处理使用了v-pre指令的元素及其子元素
 
 回到 `start` 钩子函数中,我们开始对 `start` 钩子函数内的代码做细致的分析,首先找到如下这段代码:
 
@@ -1898,10 +1984,131 @@ if (platformIsPreTag(element.tag)) {
 * 1、`<pre>` 标签会对其所包含的 `html` 字符实体进行解码
 * 2、`<pre>` 标签会保留 `html` 字符串编写时的空白
 
-更具体的实现我们会在后面的分析中讲到。
+更具体的实现我们会在后面的分析中讲到。再往下我们要看的是如下这段代码:
+
+```js
+if (inVPre) {
+  processRawAttrs(element)
+} else if (!element.processed) {
+  // structural directives
+  processFor(element)
+  processIf(element)
+  processOnce(element)
+  // element-scope stuff
+  processElement(element, options)
+}
+```
+
+这段代码是一个 `if...elseif` 语句块,其中 `if` 语句块内的代码会在判断条件 `inVPre` 为真的情况下执行,`inVPre` 为真说明当前解析环境是在 `v-pre` 环境下。我们知道使用 `v-pre` 指令的标签及其子标签的解析行为是不一致的,编译器会跳过使用了 `v-pre` 指令元素及其子元素的编译工作。具体是如何跳过的呢?通过如上代码可知如果当前元素的解析处于 `v-pre` 环境,则直接使用 `processRawAttrs` 函数对元素描述对象进行加工。同时我们注意 `elseif` 分支内的代码,可以看到如果当前元素的解析没有处于 `v-pre` 环境,那么会调用一系列 `process*` 函数来处理该元素的描述对象。
+
+现在假设我们要解析的标签使用了 `v-pre` 指令,如下:
+
+```html
+<div v-pre v-on:click="handleClick"></div>
+```
+
+当解析如上 `html` 字符串时首先会遇到 `div` 开始标签,由于该 `div` 开始标签使用了 `v-pre` 指令,所以此时 `inVPre` 的值为真,所以 `processRawAttrs` 函数将被执行,如下是 `processRawAttrs` 函数的源码:
+
+```js
+function processRawAttrs (el) {
+  const l = el.attrsList.length
+  if (l) {
+    const attrs = el.attrs = new Array(l)
+    for (let i = 0; i < l; i++) {
+      attrs[i] = {
+        name: el.attrsList[i].name,
+        value: JSON.stringify(el.attrsList[i].value)
+      }
+    }
+  } else if (!el.pre) {
+    // non root node in pre blocks with no attributes
+    el.plain = true
+  }
+}
+```
+
+`processRawAttrs` 函数接收元素描述对象作为参数,其作用是将该元素所有属性全部作为原生的属性(`arrt`)处理。在 `processRawAttrs` 函数内部首先定义了 `l` 常量,它是元素描述对象属性数组 `el.attrsList` 的长度,接着使用一个 `if` 语句判断 `l` 是否为真,如果为真说明该元素的开始标签上有属性,此时会执行 `if` 语句块内的代码,在 `if` 语句块内首先定义了 `attrs` 常量,它与 `el.attrs` 属性语句相同的引用,初始值是长度为 `l` 的数组。接着使用 `for` 循环遍历 `el.attrsList` 数组中的每一个属性,并将这些属性挪移到 `attrs` 数组中:
+
+```js {3,4}
+for (let i = 0; i < l; i++) {
+  attrs[i] = {
+    name: el.attrsList[i].name,
+    value: JSON.stringify(el.attrsList[i].value)
+  }
+}
+```
+
+可以看到 `attrs` 数组的每个元素与 `el.attrsList` 数组中的元素相同,都是一个带有 `name` 属性和 `value` 属性的对象,其中 `name` 属性存储着属性的名字,`value` 属性存储着属性的值,这里大家注意 `value` 的值:
+
+```js
+JSON.stringify(el.attrsList[i].value)
+```
+
+这里的 `JSON.stringify` 函数很重要,实际上 `el.attrsList[i].value` 本身就已经是一个字符串了,在字符串的基础上继续 `JSON.stringify`,为什么这么做呢?举个例子大家就明白了,如下是两个使用了 `new Function()` 创建函数的例子:
+
+```js
+const fn1 = new Function('console.log(1)')
+const fn2 = new Function(JSON.stringify('console.log(1)'))
+```
+
+上面代码中定义了两个函数 `fn1` 和 `fn2`,它们的区别在于 `fn2` 的参数使用 `JSON.stringify`,实际上上面的代码等价于:
+
+```js
+const fn1 = function () {
+  console.log(1)
+}
+const fn2 = function () {
+  'console.log(1)'
+}
+```
+
+可以看到 `fn1` 函数的执行能够通过 `console.log` 语句打印数字 `1`,而 `fn2` 函数体内的 `console.log` 语句是一个字符串。
+
+我们回到这段代码:
+
+```js {3}
+attrs[i] = {
+  name: el.attrsList[i].name,
+  value: JSON.stringify(el.attrsList[i].value)
+}
+```
+
+同样的,这里使用 `JSON.stringify` 实际上就是保证最终生成的代码中 `el.attrsList[i].value` 属性始终被作为普通的字符串处理。通过以上代码的讲解我们知道了,如果一个标签的解析处于 `v-pre` 环境,则会将该标签的属性全部添加到元素描述对象的 `.attrs` 数中,并且 `.attrs` 数组与 `.attrsList` 数组几乎相同,唯一不同的是在 `.attrs` 数组中每个对象的 `value` 属性值都是通过 `JSON.stringify` 处理过的。
+
+注意 `processRawAttrs` 函数还没完,如下:
+
+```js {7}
+function processRawAttrs (el) {
+  const l = el.attrsList.length
+  if (l) {
+    // 省略...
+  } else if (!el.pre) {
+    // non root node in pre blocks with no attributes
+    el.plain = true
+  }
+}
+```
+
+假如 `el.attrsList` 数组的长度为 `0`,则会进入 `else...if` 分支的判断,检查该元素是否使用了 `v-pre` 指令,如果没有使用 `v-pre` 指令才会执行 `else...if` 语句块的代码。思考一下,首先我们有一个大前提,即 `processRawAttrs` 函数的执行说明当前解析必然处于 `v-pre` 环境,要么是使用 `v-pre` 指令的标签自身,要么就是其子节点。同时 `el.attrsList` 数组的长度为 `0` 说明该元素没有任何属性,而且 `else...if` 条件的成立说明该也元素没有使用 `v-pre` 指令,这说明该元素一定是使用了 `v-pre` 指令的标签的子标签,如下:
+
+```html
+<div v-pre>
+  <span></span>
+</div>
+```
+
+如上 `html` 字符串所示,当解析 `span` 标签时,由于 `span` 标签没有任何属性,并且 `span` 标签也没有使用 `v-pre` 指令,所以此时会在 `span` 标签的元素描述对象上添加 `.plain` 属性并将其设置为 `true`,用来标识该元素是纯的,在代码生成的部分我们将看到一个被标识为 `plain` 的元素将由哪些不同。
+
+最后我们对使用了 `v-pre` 指令的标签所生成的元素描述对象做一个总结:
 
+* 1、如果标签使用了 `v-pre` 指令,则该标签的元素描述对象的 `element.pre` 属性将为 `true`。
+* 2、对于使用了 `v-pre` 指令的标签及其子代标签,它们的任何属性都将会被作为原始属性处理,即使用 `processRawAttrs` 函数处理之。
+* 3、经过 `processRawAttrs` 函数的处理,会在元素的描述对象上添加 `element.attrs` 属性,它与 `element.attrsList` 数组结构相同,不同的是 `element.attrs` 数组中每个对象的 `value` 值会经过 `JSON.stringify` 函数处理。
+* 4、如果一个标签没有任何属性,并且该标签是使用了 `v-pre` 指令标签的子代标签,那么该标签的元素描述对象将被添加 `element.plain` 属性,并且其值为 `true`。
 
+以上就是在生成 `AST` 过程中对于使用了 `v-pre` 指令标签的元素描述对象的处理。
 
+### 处理使用了v-for指令的元素
 
 ### 增强的 class
 ### 增强的 style