ソースを参照

new: explain about the processing of v-for

HcySunYang 7 年 前
コミット
d51734da8a
1 ファイル変更281 行追加0 行削除
  1. 281 0
      docs/art/82vue-parsing.md

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

@@ -2110,6 +2110,287 @@ function processRawAttrs (el) {
 
 ### 处理使用了v-for指令的元素
 
+接下来我们回到如下这段代码:
+
+```js
+if (inVPre) {
+  // 省略...
+} else if (!element.processed) {
+  // 省略...
+}
+```
+
+如果一个标签使用了 `v-pre` 指令,那么该标签及其子标签的解析都会有 `if` 语句块内的 `processRawAttrs` 函数来完成。反之将会执行 `eelse...if` 条件语句的判断,可以看到其判断条件为 `!element.processed`,这里要补充一下元素描述对象的 `element.processed` 属性是一个布尔值,它标识着当前元素是否已经被解析过了,或许大家会对 `element.processed` 属性有疑问,实际上 `element.processed` 属性是在元素描述对象应用 `preTransforms` 数组中的处理函数时被添加的,我们可以打开 `src/platforms/web/compiler/modules/model.js` 文件找到 `preTransformNode` 函数,该函数中有这样一段代码,如下:
+
+```js {4}
+processFor(branch0)
+addRawAttr(branch0, 'type', 'checkbox')
+processElement(branch0, options)
+branch0.processed = true // prevent it from double-processed
+```
+
+由于我们还没有对 `preTransforms` 前置处理函数进行讲解,所以大家看不明白如上代码没关系,你只需知道经过如上代码的处理之后由于元素已经被处理过了,所以这里会通过 `.processed` 做一个标识,以防止被重复处理。再回到如下这段代码:
+
+```js {5}
+if (inVPre) {
+  // 省略...
+} else if (!element.processed) {
+  // structural directives
+  processFor(element)
+  // 省略...
+}
+```
+
+如果元素没有被处理过,那么 `else...if` 语句块内的代码将被执行,可以看到对元素描述对象应用的第一个处理函数是 `processFor` 函数,接下来我们的目标就是研究 `processFor` 函数对元素描述对象做了怎样的处理。
+
+找到 `processFor` 函数,如下是其源码:
+
+```js
+export function processFor (el: ASTElement) {
+  let exp
+  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
+    const res = parseFor(exp)
+    if (res) {
+      extend(el, res)
+    } else if (process.env.NODE_ENV !== 'production') {
+      warn(
+        `Invalid v-for expression: ${exp}`
+      )
+    }
+  }
+}
+```
+
+`processFor` 函数接收元素描述对象作为参数,在 `processFor` 函数内部首先定义了 `exp` 变量,接着是一个 `if` 条件语句块。在判断条件中首先通过 `getAndRemoveAttr` 函数从元素描述对象中获取 `v-for` 属性对应的属性值,并将值赋值给 `exp` 变量,如果标签的 `v-for` 属性值存在则会执行 `if` 语句块内的代码,否则什么都不会做。
+
+对于 `getAndRemoveAttr` 函数前面我们已经讲过了这里就不做补充了。现在假如我们当前元素是一个使用了 `v-for` 指令的 `div` 标签,如下:
+
+```js
+<div v-for="obj in list"></div>
+```
+
+那么 `exp` 变量的值将是字符串 `'obj in list'`,此时 `if` 语句块内的代码将会执行,在 `if` 语句块内一上来就通过 `parseFor` 函数对 `v-for` 属性的值做解析,我们把目光转移到 `parseFor` 函数上,看一看 `parseFor` 函数是如何解析字符串 `'obj in list'` 的。
+
+`parseFor` 函数的源码如下:
+
+```js
+export function parseFor (exp: string): ?ForParseResult {
+  const inMatch = exp.match(forAliasRE)
+  if (!inMatch) return
+  const res = {}
+  res.for = inMatch[2].trim()
+  const alias = inMatch[1].trim().replace(stripParensRE, '')
+  const iteratorMatch = alias.match(forIteratorRE)
+  if (iteratorMatch) {
+    res.alias = alias.replace(forIteratorRE, '')
+    res.iterator1 = iteratorMatch[1].trim()
+    if (iteratorMatch[2]) {
+      res.iterator2 = iteratorMatch[2].trim()
+    }
+  } else {
+    res.alias = alias
+  }
+  return res
+}
+```
+
+`parseFor` 函数接收 `v-for` 指令的值作为参数,现在我们假设参数 `exp` 的值为字符串 `'obj in list'`。在 `parseFor` 函数开头首先使用字符串 `exp` 去匹配正则 `forAliasRE`,并将匹配的结果保存在 `inMatch` 常量中,该正则的作用我们在本章的开头讲过,所以这里不做过多说明,如果 `exp` 字符串为 `'obj in list'`,那么最终 `inMatch` 常量则是一个数组,如下:
+
+```js
+const inMatch = [
+  'obj in list',
+  'obj',
+  'list'
+]
+```
+
+如果匹配失败则 `inMatch` 常量的值将为 `null`。可以看到在 `parseFor` 函数内部如果匹配失败则函数直接返回 `undefined`:
+
+```js {3}
+export function parseFor (exp: string): ?ForParseResult {
+  const inMatch = exp.match(forAliasRE)
+  if (!inMatch) return
+  // 省略...
+}
+```
+
+我们可以回到 `processFor` 函数,注意如下高亮的代码:
+
+```js {4,5,8-10}
+export function processFor (el: ASTElement) {
+  let exp
+  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
+    const res = parseFor(exp)
+    if (res) {
+      extend(el, res)
+    } else if (process.env.NODE_ENV !== 'production') {
+      warn(
+        `Invalid v-for expression: ${exp}`
+      )
+    }
+  }
+}
+```
+
+可以看到在 `processFor` 函数内部定义了 `res` 常量接收 `parseFor` 函数对 `exp` 字符串的解析结果,如果解析失败则 `res` 常量的值将为 `undefined`,所以在非生产环境下会打印警告信息提示开发者所编写的 `v-for` 指令的值为无效的。
+
+再回到 `parseFor` 函数中,如果对 `exp` 字符串解析成功,则如下高亮的两句代码将被执行:
+
+```js {4,5}
+export function parseFor (exp: string): ?ForParseResult {
+  const inMatch = exp.match(forAliasRE)
+  if (!inMatch) return
+  const res = {}
+  res.for = inMatch[2].trim()
+  // 省略...
+  return res
+}
+```
+
+定义了 `res` 常量,它的初始值为一个空对象,可以看到最后 `parseFor` 函数会将 `res` 对象作为返回值返回。接着在 `res` 对象上添加 `res.for` 属性,它的值为 `inMatch` 数组的第三个元素,假如 `exp` 字符串的值为 `'obj in list'`,则 `res.for` 属性的值将是字符串 `'list'`,所以大家应该能够猜测到了 `res.for` 属性所存储的值应该是被遍历的目标变量的名字。
+
+再往下将会执行如下高亮的这两句代码:
+
+```js {4,5}
+export function parseFor (exp: string): ?ForParseResult {
+  // 省略...
+  res.for = inMatch[2].trim()
+  const alias = inMatch[1].trim().replace(stripParensRE, '')
+  const iteratorMatch = alias.match(forIteratorRE)
+  // 省略...
+  return res
+}
+```
+
+定义了 `alias` 常量,它的值比较复杂,我们一点点来看,假设字符串 `exp` 的值为 `'obj in list'`,则 `inMatch[1]` 的值应该是字符串 `'obj'`,如果 `exp` 字符串的值是 `'(obj, inde) in list'`,那么 `inMatch[1]` 的值应该是字符串 `'(obj, index)'`,当然啦如果你在编写 `v-for` 指令时存在多余的空格,比如:
+
+```html
+<div v-for="  obj in list"></div>
+```
+
+则 `exp` 字符串也会有多余的空格:`'  obj in list'`,这是就会导致 `inMatch[1]` 的值中也会包含多余的空格:`'  obj'`。理想的做法是此时我们将多余的空格去掉,然后再做下一步处理,这就是为什么 `parseFor` 函数中要对 `inMatch[1]` 字符串使用 `trim()` 函数的原因。去掉空格之后,可以看到紧接着使用该字符串的 `replace` 方法匹配正则 `stripParensRE`,并将匹配的内容替换为空字符串,最终的结果是将 `inMatch[1]` 中的左右圆括号移除,本章的开头讲解了正则 `stripParensRE` 的作用,它用来匹配字符串中的左右圆括号。
+
+如下是 `v-for` 指令的值与 `alias` 常量值的对应关系:
+
+* 1、如果 `v-for` 指令的值为 `'obj in list'`,则 `alias` 的值为字符串 `'obj'`
+* 2、如果 `v-for` 指令的值为 `'(obj, index) in list'`,则 `alias` 的值为字符串 `'obj, index'`
+* 3、如果 `v-for` 指令的值为 `'(obj, key, index) in list'`,则 `alias` 的值为字符串 `'obj, key, index'`
+
+了解了 `alias` 常量的值之后,我们再来看如下这句代码:
+
+```js
+const iteratorMatch = alias.match(forIteratorRE)
+```
+
+这里定义了 `iteratorMatch` 常量,它的值使用使用 `alias` 字符串的 `match` 方法匹配正则 `forIteratorRE` 得到的,其中正则 `forIteratorRE` 我们也以及在前面的章节中讲过了,这里总结一下对于不同的 `alias` 字符串其对应的匹配结果:
+
+* 1、如果 `alias` 字符串的值为 `'obj'`,则匹配结果 `iteratorMatch` 常量的值为 `null`
+* 2、如果 `alias` 字符串的值为 `'obj, index'`,则匹配结果 `iteratorMatch` 常量的值是一个包含两个元素的数组:`[', index', 'index']`
+* 3、如果 `alias` 字符串的值为 `'obj, key, index'`,则匹配结果 `iteratorMatch` 常量的值是一个包含三个元素的数组:`[', key, index', 'key', 'index']`
+
+明白了这些我们继续看 `parseFor` 函数的代码,接下来要看的是如下这段代码:
+
+```js {4, 11}
+export function parseFor (exp: string): ?ForParseResult {
+  // 省略...
+  const iteratorMatch = alias.match(forIteratorRE)
+  if (iteratorMatch) {
+    res.alias = alias.replace(forIteratorRE, '')
+    res.iterator1 = iteratorMatch[1].trim()
+    if (iteratorMatch[2]) {
+      res.iterator2 = iteratorMatch[2].trim()
+    }
+  } else {
+    res.alias = alias
+  }
+  return res
+}
+```
+
+如上高亮的代码所示,我们知道如果 `alias` 常量的值为字符串 `'obj'` 时,则匹配结果 `iteratorMatch` 常量的会是 `null`,所以此时 `if` 条件语句判断失败,`else` 语句块的代码将被执行,即在 `res` 对象上添加 `res.alias` 属性,其值就是 `alias` 常量的值,也就是字符串 `'obj'`。
+
+如果 `alias` 常量的值为字符串 `'obj, index'`,则匹配结果 `iteratorMatch` 常量将会是一个拥有两个元素的数组,此时 `if` 语句块内的代码将被执行,在 `if` 语句块内首先执行的是如下这句代码:
+
+```js
+res.alias = alias.replace(forIteratorRE, '')
+```
+
+使用 `alias` 字符串的 `replace` 方法去匹配正则 `forIteratorRE`,并将匹配到的内容替换为空字符串,最后将结果赋值给 `res.alias` 属性。如果字符串 `alias` 的值为 `'obj, index'`,则替换后的结果应该为字符串 `'obj'`。所以 `res.alias` 属性的值就是字符串 `'obj'`。
+
+接着执行的将是如下这句代码:
+
+```js
+res.iterator1 = iteratorMatch[1].trim()
+```
+
+在 `res` 对象上定义 `res.iterator1` 属性,它的值是匹配结果 `iteratorMatch` 数组第二个元素去前后空白之后的值。假设 `alias` 字符串为 `'obj, index'`,则 `res.iterator1` 的值应该为字符串 `'index'`。
+
+再往下会进入另外一个 `if` 条件语句:
+
+```js
+if (iteratorMatch[2]) {
+  res.iterator2 = iteratorMatch[2].trim()
+}
+```
+
+由于 `alias` 字符串的值为 `'obj, index'`,对应的匹配结果 `iteratorMatch` 数组只有两个元素,所以 `iteratorMatch[2]` 的值为 `undefined`,此时如上 `if` 语句块内的代码不会被执行。但是如果 `alias` 字符串的值为 `'obj, key, index'`,则匹配结果 `iteratorMatch[2]` 的值将会是字符串 `'index'`,此时 `if` 语句块内的代码将被执行,可以看到在 `res` 对象上定义了 `res.iterator2` 属性,其值就是字符串 `iteratorMatch[2]` 去掉前后空白后的结果。
+
+以上就是 `parseFor` 函数的全部实现,它的作用是解析 `v-for` 指令的值,并创建一个包含解析结果的对象,最后将该对象返回。我们来做一个简短的总结:
+
+* 1、如果 `v-for` 指令的值为字符串 `'obj in list'`,则 `parseFor` 函数的返回值为:
+
+```js
+{
+  for: 'list',
+  alias: 'obj'
+}
+```
+
+* 2、如果 `v-for` 指令的值为字符串 `'(obj, index) in list'`,则 `parseFor` 函数的返回值为:
+
+```js
+{
+  for: 'list',
+  alias: 'obj',
+  iterator1: 'index'
+}
+```
+
+* 2、如果 `v-for` 指令的值为字符串 `'(obj, key, index) in list'`,则 `parseFor` 函数的返回值为:
+
+```js
+{
+  for: 'list',
+  alias: 'obj',
+  iterator1: 'key',
+  iterator2: 'index'
+}
+```
+
+最后我们再回到 `processFor` 函数,来看如下高亮的代码:
+
+```js {6}
+export function processFor (el: ASTElement) {
+  let exp
+  if ((exp = getAndRemoveAttr(el, 'v-for'))) {
+    const res = parseFor(exp)
+    if (res) {
+      extend(el, res)
+    } else if (process.env.NODE_ENV !== 'production') {
+      warn(
+        `Invalid v-for expression: ${exp}`
+      )
+    }
+  }
+}
+```
+
+可以看到如果 `parseFor` 函数对 `v-for` 指令的值解析成功,则会将解析结果保存在 `res` 常量中,并使用 `extend` 函数将 `res` 常量中的属性混入当前元素的描述对象中。
+
+以上就是解析器对于使用 `v-for` 指令标签的解析过程,以及对该元素描述对象的补充。
+
+### 处理使用了v-if和v-once指令的元素
+
 ### 增强的 class
 ### 增强的 style
 ### 特殊的 model