浏览代码

new: explain the reading of character streams

HcySunYang 7 年之前
父节点
当前提交
8e854135c8
共有 1 个文件被更改,包括 235 次插入0 次删除
  1. 235 0
      docs/art/82vue-parsing.md

+ 235 - 0
docs/art/82vue-parsing.md

@@ -2888,6 +2888,241 @@ let paren = 0
 
 以上代码中绑定属性 `key` 的属性值中包含一个管道符,但是由于该管道符存在于圆括号内,所以它不会被作为过滤器的分界线。
 
+再往下定义了如下这些变量:
+
+```js
+let lastFilterIndex = 0
+let c, prev, i, expression, filters
+```
+
+这里简单介绍一下这些变量的作用,更具体的将会在源码中讲解。`lastFilterIndex` 变量的初始值为 `0`,它的值是属性值字符串中字符的索引,将会被用来确定过滤器的位置。变量 `c` 为当前字符对应的 `ASCII` 码,我们知道在解析属性值时会以字符流的方式逐个字符读入,而变量 `c` 就是当前读入字符所对应的 `ASCII` 码。变量 `prev` 保存的则是当前字符的前一个字符所对应的 `ASCII` 码。变量 `i` 为当前读入字符的位置索引。变量 `expression` 将是 `parseFilters` 函数的返回值。变量 `filters` 将来会是一个数组,它保存着所有过滤器函数名。
+
+再往下将进入一个 `for` 循环:
+
+```js
+for (i = 0; i < exp.length; i++) {
+  // 省略...
+}
+```
+
+这个 `for` 循环是整个 `parseFilters` 函数的核心,它的作用就是将属性值字符串作为字符流读入,从第一个字符开始一直读到字符串的末尾,在 `for` 循环的开头执行的是如下两句代码:
+
+```js {2-3}
+for (i = 0; i < exp.length; i++) {
+  prev = c
+  c = exp.charCodeAt(i)
+  // 省略...
+}
+```
+
+可以看到每次循环的开始,都会将上一次读取的字符所对应的 `ASCII` 码赋值给 `prev` 变量,然后再讲变量 `c` 的值设置为当前读取字符所对应的 `ASCII` 码。所以我们说 `prev` 变量中保存的是上一个字符的 `ASCII` 码。
+
+在这两句代码的下面是一连串的 `if...elseif...else` 语句,如下:
+
+```js
+for (i = 0; i < exp.length; i++) {
+  prev = c
+  c = exp.charCodeAt(i)
+  if (inSingle) {
+    // 如果当前读取的字符存在于由单引号包裹的字符串内,则会执行这里的代码
+  } else if (inDouble) {
+    // 如果当前读取的字符存在于由双引号包裹的字符串内,则会执行这里的代码
+  } else if (inTemplateString) {
+    // 如果当前读取的字符存在于模板字符串内,则会执行这里的代码
+  } else if (inRegex) {
+    // 如果当前读取的字符存在于正则表达式内,则会执行这里的代码
+  } else if (
+    c === 0x7C && // pipe
+    exp.charCodeAt(i + 1) !== 0x7C &&
+    exp.charCodeAt(i - 1) !== 0x7C &&
+    !curly && !square && !paren
+  ) {
+    // 如果当前读取的字符是过滤器的分界线,则会执行这里的代码
+  } else {
+    // 当不满足以上条件时,执行这里的代码
+  }
+}
+```
+
+首先来看第一段 `if` 条件语句的判断:
+
+```js
+if (inSingle) {
+  if (c === 0x27 && prev !== 0x5C) inSingle = false
+}
+```
+
+该判断条件检测了 `inSingle` 变量是否为真,如果为真则说明当前读入的字符存在于由单引号包裹的字符串内,此时会执行 `if` 语句块内的代码,可以看到在 `if` 条件语句块内同样是一个 `if` 判断语句,它的判断条件为:
+
+```js
+c === 0x27 && prev !== 0x5C
+```
+
+这个判断条件是什么意思呢?可以看到如上判断条件中有两个十六进制的数字:`0x27` 和 `0x5C`,这两个十六进制的数字实际上就是字符的 `ASCII` 码,其中 `0x27` 为字符单引号(`'`)所对应的 `ASCII` 码,而 `0x5C` 则是字符反斜杠(`\`)所对应的 `ASCII` 码。所以如上判断条件翻译过来就是:当前字符是单引号(`'`),并且当前字符的前一个字符不是反斜杠(`\`),也就是说当前字符(`单引号`)就是字符串的结束。该判断条件的关键在于不仅要当前字符是单引号(`'`),同时前一个字符也一定不能是反斜杠才行,这是因为反斜杠在字符串内具有转移的作用。如果判断条件成立,则将 `inSingle` 变量的值设置为 `false`,代表接下来的解析工作已经不处于由单引号所包裹的字符串环境中了。
+
+再来看下一个 `elseif` 判断分支:
+
+```js
+else if (inDouble) {
+  if (c === 0x22 && prev !== 0x5C) inDouble = false
+}
+```
+
+与单引号的情况类似,该 `elseif` 条件语句检查了变量 `inDouble` 是否为真,如果为真则说明当前字符处于由双引号包裹的字符串中,此时会检查当前字符所对应的 `ASCII` 码是否等于 `0x22`,这里的数字 `0x22` 就是字符双引号(`"`)所对应的 `ASCII` 码。所以如上判断语句成立则等价于:当前字符是双引号,并且前一个字符不是转移字符(`\`)。这说明当前字符(`双引号`)就应该是字符串的结束,此时会将变量 `inDouble` 的值设置为 `false`,代表接下来的解析工作已经不处于由双引号所包裹的字符串环境中了。
+
+再接着是如下判断分支,它同时是一个 `elseif` 语句块:
+
+```js
+else if (inTemplateString) {
+  if (c === 0x60 && prev !== 0x5C) inTemplateString = false
+}
+```
+
+这个判断语句与前两个判断语句类似,如果该 `elseif` 语句的条件成立,则说明当前字符处在模板字符串中,此时会继续检测当前字符所对应的 `ASCII` 码是否等于 `0x60`,这里的数字 `0x60` 就是字符 `` ` `` 所对应的 `ASCII` 码。所以如上判断语句成立则等价于:当前字符是 `` ` ``,并且前一个字符不是转移字符(`\`)。这说明当前字符(`` ` ``)就应该是模板字符串的结束,此时会将变量 `inTemplateString` 的值设置为 `false`,代表接下来的解析工作已经不处于模板字符串环境中了。
+
+再来看下一个 `elseif` 条件语句块:
+
+```js
+else if (inRegex) {
+  if (c === 0x2f && prev !== 0x5C) inRegex = false
+}
+```
+
+如果该 `elseif` 语句的条件成立,则说明当前字符处在正则表达式中,此时会继续检测当前字符所对应的 `ASCII` 码是否等于 `0x2f`,这里的数字 `0x2f` 就是字符 `/` 所对应的 `ASCII` 码。所以如上判断语句成立则等价于:当前字符是 `/`,并且前一个字符不是转移字符(`\`)。这说明当前字符(`/`)就应该是正则表达式的结束,此时会将变量 `inRegex` 的值设置为 `false`,代表接下来的解析工作已经不处于正则表达式的环境中了。
+
+再往下的一个 `elseif` 条件语句的判断条件稍微复杂一些,如下:
+
+```js
+else if (
+  c === 0x7C && // pipe
+  exp.charCodeAt(i + 1) !== 0x7C &&
+  exp.charCodeAt(i - 1) !== 0x7C &&
+  !curly && !square && !paren
+)
+```
+
+如上判断条件中的数字 `0x7C` 为管道符(`|`)所对应的 `ASCII` 码,如果以上条件成立,则说明当前字符为管道符,实际上这个判断条件是用来检测当前遇到的管道符是否是过滤器的分界线。如果一个管道符是过滤器的分界线则必须满足以上条件,即:
+
+* 1、当前字符所对应的 `ASCII` 码必须是 `0x7C`,即当前字符必须是管道符。
+* 2、该字符的后一个字符不能是管道符。
+* 3、该字符的前一个字符不能是管道符。
+* 4、该字符不能处于花括号、方括号、圆括号之内
+
+如果一个字符满足以上条件,则说明该字符就是用来作为过滤器分界线的管道符。此时该 `elseif` 语句块内的代码将被执行,不过我们暂时跳过,来看最后一个 `else` 语句。
+
+当以上所有判断分支全部无效之后,代码会来到 `else` 分支,假设我们有如下代码:
+
+```html
+<div :key="'id'"></div>
+```
+
+此时传递给 `parseFilters` 函数的字符串就应该是 `'id'`,该字符串有四个字符,第一个字符为单引号,我们尝试按照 `parseFilters` 函数的执行过程对该字符串进行解析。首先读取该字符串的第一个字符,即单引号 `’`,接着会判断 `inSingle` 变量是否为真,由于 `inSingle` 变量的初始值为 `false`,所以会继续判断下一个条件分支,同样的由于 `inDouble`、`inTemplateString`、`inRegex` 等变量的初始值都为 `false`,并且该字符是单引号而不是管道符,所以接下来的任何一个 `elseif` 分支语句块内的代码都不会被执行。所以最终 `else` 语句块内的代码将被执行。
+
+在 `else` 语句块内,首先执行的是一段 `switch` 语句,如下:
+
+```js
+switch (c) {
+  case 0x22: inDouble = true; break         // "
+  case 0x27: inSingle = true; break         // '
+  case 0x60: inTemplateString = true; break // `
+  case 0x28: paren++; break                 // (
+  case 0x29: paren--; break                 // )
+  case 0x5B: square++; break                // [
+  case 0x5D: square--; break                // ]
+  case 0x7B: curly++; break                 // {
+  case 0x7D: curly--; break                 // }
+}
+```
+
+这段 `switch` 语句的作用总结如下:
+
+* 如果当前字符为双引号(`"`),则将 `inDouble` 变量的值设置为 `true`。
+* 如果当前字符为单引号(`‘`),则将 `inSingle` 变量的值设置为 `true`。
+* 如果当前字符为模板字符串的定义字符(`` ` ``),则将 `inTemplateString` 变量的值设置为 `true`。
+* 如果当前字符是左圆括号(`(`),则将 `paren` 变量的值加一。
+* 如果当前字符是右圆括号(`)`),则将 `paren` 变量的值减一。
+* 如果当前字符是左方括号(`[`),则将 `square` 变量的值加一。
+* 如果当前字符是右方括号(`]`),则将 `square` 变量的值减一。
+* 如果当前字符是左花括号(`{`),则将 `curly` 变量的值加一。
+* 如果当前字符是右花括号(`}`),则将 `curly` 变量的值减一。
+
+假设我们还是解析字符串 `'id'`,该字符串的第一个字符为单引号,我们知道当解析该字符串的第一个字符时会执行 `else` 语句块内的代码,所以如上 `switch` 语句将被执行,并且 `inSingle` 变量的值将被设置为 `true`。接着会解析第二个字符 `i`,由于此时 `inSingle` 变量的值已经为真,所以如下代码将被执行:
+
+```js
+if (inSingle) {
+  if (c === 0x27 && prev !== 0x5C) inSingle = false
+}
+```
+
+但是很显然字符 `i` 所对应的 `ASCII` 码不等于 `0x27`,所以这等于什么都没做,直接跳过解析下一个字符。下一个字符是 `d`,它的情况与字符 `i` 一样,也会被跳过。知道遇到最后一个字符 `'`,该字符同样是单引号,所以此时会将 `inSingle` 变量的值设置为 `false`,意味着由单引号包裹的字符串结束了。所以通过以上分析我们得知一件事情,即只要存在于由单引号包裹的字符串内的字符都将被跳过。这么做的目的就是为了避免误把存在于字符串中的管道符当做过滤器的分界线,如下代码所示:
+
+```html
+<div :key="'id|featId'"></div>
+```
+
+可看到绑定属性 `key` 的属性值为 `'id|featId'`,由于管道符 `|` 存在于由单引号所包裹的字符串内,所以该管道符不会被作为过滤器的分界线,这是非常合理的。
+
+同样的道理,对于存在于由双引号包裹的字符串中或模板字符串中或正则表达式中的管道符,也不会被作为过滤器的分界线。对于双引号和模板字符串的判断是很容易的,它们的原理与单引号类似。难点在于如何判断正则,或者换句话说我们应该在什么情况下才能将 `inRegex` 变量的值设置为 `true`。如下是 `else` 语句块内用来判断是否即将进入正则环境的代码:
+
+```js
+if (c === 0x2f) { // /
+  let j = i - 1
+  let p
+  // find first non-whitespace prev char
+  for (; j >= 0; j--) {
+    p = exp.charAt(j)
+    if (p !== ' ') break
+  }
+  if (!p || !validDivisionCharRE.test(p)) {
+    inRegex = true
+  }
+}
+```
+
+如上代码是一个 `if` 判断语句,它用来判断当前字符所对应的 `ASCII` 码是否等于数字 `0x2f`,其中数字 `0x2f` 就是字符 `/` 所对应的 `ASCII` 码。我们知道正则表达式就是以字符 `/` 开头的,所以当遇到字符 `/` 时,则说明该字符有可能是正则的开始。但至于到底是不是正则的开始还真不一定,前面我们已经提到过了,字符 `/` 还有除法的意义。而判断字符 `/` 到底是正则的开始还是除法却是一件不容的事情。实际上如上代码根本不足以保证所遇到的字符 `/` 就是正则表达式,但是还是那句话,这对于 `Vue` 而言已经足够了,我们没必要花大力气在收益很小的地方。
+
+那我们就来看看如上代码是如何来确定字符 `/` 是正则的开始的,首先我们要明确如果上面这段 `if` 条件语句成立,则说明当前字符为 `/`,此时 `if` 语句块内的代码将被执行,在 `if` 语句块内定义了变量 `j`,它的值为 `i - 1`,也就是说变量 `j` 是 `/` 字符的前一个字符的索引。然后又定义了变量 `p`,接着开启一个 `for` 循环,这个 `for` 循环的作用是找到 `/` 字符之前第一个不为空的字符。如果没找到则说明字符 `/` 之前的所有字符都是空格,或根本就没有字符,如下:
+
+```html
+<div :key="/a/.test('abc')"></div>      <!-- 第一个 `/` 之前就没有字符  -->
+<div :key="    /a/.test('abc')"></div>  <!-- 第一个 `/` 之前都是空格  -->
+```
+
+所以以上两种情况,第一个 `/` 都应该是正则的开始,而非除法。
+
+但是假如字符 `/` 之前有非空的字符,则只有在该字符不满足正则 `validDivisionCharRE` 的情况下,才会认为字符 `/` 为正则的开始。来看一下正则常量 `validDivisionCharRE`,该正则常量定义在 `parseFilters` 函数的前面,如下:
+
+```js
+const validDivisionCharRE = /[\w).+\-_$\]]/
+```
+
+该正则用来匹配一个字符,这个字符应该是字母、数字、`)`、`.`、`+`、`-`、`_`、`$`、`]` 之一。再来看如下高亮的代码:
+
+```js {9-12}
+if (c === 0x2f) { // /
+  let j = i - 1
+  let p
+  // find first non-whitespace prev char
+  for (; j >= 0; j--) {
+    p = exp.charAt(j)
+    if (p !== ' ') break
+  }
+  if (!p || !validDivisionCharRE.test(p)) {
+    inRegex = true
+  }
+}
+```
+
+可以看到如果条件 `!validDivisionCharRE.test(p)` 成立则也会认为当前字符 `/` 是正则的开始。条件 `!validDivisionCharRE.test(p)` 成立说明字符 `/` 之前的字符不能是正则 `validDivisionCharRE` 所匹配的任何一个字符,否则当前字符 `/` 就不被认为是正则的开始。
+
+以上是 `Vue` 的做法,但我们已经说过了,这不足以对字符 `/` 的意义做出准确的判断,但是对 `Vue` 而言足够了。其实我们可以很容易的找出返利,如下:
+
+```html
+<div :key="a + /a/.test('abc')"></div>
+```
+
+实际上在表达式 `a + /a/.test('abc')` 中出现的斜杠(`/`)的确是定义了正则,但 `Vue` 却不认为它是正则,因为第一个斜杠之前的第一个不为空的字符为加号 `+`。加号存在于正则 `validDivisionCharRE` 中,所以 `Vue` 不认为这里的斜杠是正则的定义。但实际上如上代码简直就是没有任何意义的,假如你非得这么写,那你也完全可以使用计算属性替代。
+
 ### 增强的 class
 ### 增强的 style
 ### 特殊的 model