Browse Source

new: explain how to parse attribute values & is and inline-template attribute processing

HcySunYang 7 years ago
parent
commit
17576f5427

+ 4 - 3
docs/.vuepress/config.js

@@ -57,9 +57,10 @@ module.exports = {
             '80vue-compiler-start',
             '81vue-lexical-analysis',
             '82vue-parsing',
-            '83vue-codegen',
-            '84vue-vdom',
-            '85vue-vdom-patch'
+            '83vue-parsing-2',
+            '84vue-codegen',
+            '85vue-vdom',
+            '86vue-vdom-patch'
           ]
         }
       ],

+ 144 - 16
docs/art/82vue-parsing.md

@@ -1,4 +1,4 @@
-# 句法分析 - 生成真正的AST
+# 句法分析 - 生成真正的AST(一)
 
 在上一章中,我们讲解了解析 `html` 字符串时词法分析的方式,本章我们将再进一步,讲解 `Vue` 是如何在词法分析的基础上构建抽象语法树(`AST`)的,即句法分析。
 
@@ -832,7 +832,7 @@ element = {
 
 以上就是 `parse` 函数之前定义的所有常量、变量以及函数的讲解,接下来我们将正式进入 `parse` 函数的实现讲解。
 
-### parse 函数创建 AST 前的准备工作
+## parse 函数创建 AST 前的准备工作
 
 本节我们主要讲解 `parse` 函数的结构以及真正开始解析之前的准备工作,我们知道 `parse` 函数中主要是通过调用 `parseHTML` 函数来辅助完成 `AST` 构建的,但是在调用 `parseHTML` 函数之前还需要做一些准备工作,比如前面提过的在 `parse` 函数的开头为平台化变量赋了值,如下是 `parse` 函数的整体结构:
 
@@ -1164,7 +1164,7 @@ parseHTML(template, {
 
 下面我们就从 `start` 钩子函数开始说起,为什么从 `start` 钩子函数开始呢?因为正常情况下,解析一段 `html` 字符串时必然最先遇到的就是开始标签。所以我们从 `start` 钩子函数开始讲解,在讲解的过程中为了说明某些问题我们会逐个举例。
 
-### 解析一个开始标签需要做的事情
+## 解析一个开始标签需要做的事情
 
 接下来我们就从 `start` 钩子函数开始,研究一下解析一个开始标签都需要做哪些事情,如下是在 `parse` 函数中调用 `parseHTML` 函数时传递的 `start` 钩子函数:
 
@@ -1799,7 +1799,7 @@ if (children[i].type === 1) {
 
 以上的总结就是 `start` 钩子函数在处理开始标签时所做的事情,实际上由于开始标签中包含了大量指令信息(如 `v-if` 等)或特性信息(如 `slot-scope` 等),所以在生产 `AST` 过程中,大部分工作都是由 `start` 函数来完成的,接下来我们将更加细致的去讲解解析过程中的每一个细节。
 
-### 处理使用了v-pre指令的元素及其子元素
+## 处理使用了v-pre指令的元素及其子元素
 
 回到 `start` 钩子函数中,我们开始对 `start` 钩子函数内的代码做细致的分析,首先找到如下这段代码:
 
@@ -1824,7 +1824,7 @@ function processPre (el) {
 
 `processPre` 函数接收元素描述对象作为参数,在 `processPre` 函数内部首先通过 `getAndRemoveAttr` 函数并使用其返回值与 `null` 做比较,如果 `getAndRemoveAttr` 函数的返回值不等于 `null` 则执行 `if` 语句块内的代码,即在元素描述对象上添加 `.pre` 属性并将其值设置为 `true`。
 
-大家猜测一下 `getAndRemoveAttr` 函数的作用是什么?根据传递给该函数的两个参数:第一个参数是元素描述对象,第二个参数是一个字符串 `'v-pre'`。我们大概可以猜测到 `getAndRemoveAttr` 函数应该能够获取给定元素的某个属性的值,那么如上代码就应该是获取给定元素的 `v-pre` 属性的值。实际上我们的猜测是正确的,不过只正确了一部分,实际 `getAndRemoveAttr` 函数还会做更多事情,`getAndRemoveAttr` 函数来自于 `src/compiler/helpers.js` 文件,如下是 其代码:
+大家猜测一下 `getAndRemoveAttr` 函数的作用是什么?根据传递给该函数的两个参数:第一个参数是元素描述对象,第二个参数是一个字符串 `'v-pre'`。我们大概可以猜测到 `getAndRemoveAttr` 函数应该能够获取给定元素的某个属性的值,那么如上代码就应该是获取给定元素的 `v-pre` 属性的值。实际上我们的猜测是正确的,不过只正确了一部分,实际 `getAndRemoveAttr` 函数还会做更多事情,`getAndRemoveAttr` 函数来自于 `src/compiler/helpers.js` 文件,如下是 其代码:
 
 ```js
 export function getAndRemoveAttr (
@@ -2108,7 +2108,7 @@ function processRawAttrs (el) {
 
 以上就是在生成 `AST` 过程中对于使用了 `v-pre` 指令标签的元素描述对象的处理。
 
-### 处理使用了v-for指令的元素
+## 处理使用了v-for指令的元素
 
 接下来我们回到如下这段代码:
 
@@ -2389,7 +2389,7 @@ export function processFor (el: ASTElement) {
 
 以上就是解析器对于使用 `v-for` 指令标签的解析过程,以及对该元素描述对象的补充。
 
-### 处理使用条件指令和v-once指令的元素
+## 处理使用条件指令和v-once指令的元素
 
 在使用 `processFor` 函数处理完元素描述对象之后,紧接着使用了 `processIf` 函数继续对元素的描述对象进行处理,如下高亮代码所示:
 
@@ -2566,7 +2566,7 @@ function processOnce (el) {
 
 首先通过 `getAndRemoveAttr` 函数获取并移除元素描述对象的 `attrsList` 数组中名字为 `v-once` 的属性值,并将获取到的属性值赋值给 `once` 常量,接着使用 `if` 条件语句,如果 `once` 常量不等于 `null`,则说明使用了 `v-once` 指令,此时会在元素描述对象上添加 `el.once` 属性并将其值设置为 `true`。
 
-### 处理使用了key属性的元素
+## 处理使用了key属性的元素
 
 再往下我们要讲解的就应该是 `processElement` 函数了,如下:
 
@@ -2632,7 +2632,7 @@ if (process.env.NODE_ENV !== 'production' && el.tag === 'template') {
 * 1、`key` 属性不能被应用到 `<template>` 标签。
 * 2、使用了 `key` 属性的标签,其元素描述对象的 `el.key` 属性保存在 `key` 属性的值。
 
-### 获取绑定的属性值以及过滤器的解析
+## 获取绑定的属性值以及过滤器的解析
 
 在讲解 `processKey` 函数时我们遇到了 `getBindingAttr` 函数,当时我们没有仔细讲解,并且让大家理解为它的作用与 `getAndRemoveAttr` 函数的作用相同。接下来我们就仔细研究一下 `getBindingAttr` 函数,如下是其源码:
 
@@ -3423,7 +3423,7 @@ el.key = '_f("featId")(id)'
 
 以上就是 `el.key` 属性的所有可能值。
 
-### 处理使用了ref属性的元素
+## 处理使用了ref属性的元素
 
 接下来我们讲解对于使用 `ref` 属性的标签是如何处理的,即 `processRef` 函数,如下高亮的代码所示:
 
@@ -3545,7 +3545,7 @@ function checkInFor (el: ASTElement): boolean {
 
 大家也许会有一个疑问,即为什么要检查 `ref` 属性是否在 `v-for` 指令之内使用呢?很简单,如果 `ref` 属性存在于 `v-for` 指令之内,我们需要创建一个组件实例或DOM节点的引用数组,而不是单一引用,这个时候就需要 `el.refInFor` 属性来区分了。这些内容会在讲解 `$ref` 属性的实现时详细阐述。
 
-### 处理(作用域)插槽
+## 处理(作用域)插槽
 
 我们下一个要讲解的将是 `processSlot` 函数,如下:
 
@@ -3827,18 +3827,146 @@ if (el.tag !== 'template' && !el.slotScope) {
 * 3、对于其他标签,会尝试获取 `slot-scope` 属性的值,并将获取到的值赋值给元素描述对象的 `el.slotScope` 属性。
 * 4、对于非 `<slot>` 标签,会尝试获取该标签的 `slot` 属性,并将获取到的值赋值给元素描述对象的 `el.slotTarget` 属性。如果一个标签使用了 `slot` 属性但却没有给定相应的值,则该标签元素描述对象的 `el.slotTarget` 属性值为字符串 `'"default"'`。
 
+## 处理使用了is或inline-template属性的元素
 
+再往下,我们将来到 `processComponent` 函数:
 
+```js {10}
+export function processElement (element: ASTElement, options: CompilerOptions) {
+  processKey(element)
+
+  // determine whether this is a plain element after
+  // removing structural attributes
+  element.plain = !element.key && !element.attrsList.length
+
+  processRef(element)
+  processSlot(element)
+  processComponent(element)
+  for (let i = 0; i < transforms.length; i++) {
+    element = transforms[i](element, options) || element
+  }
+  processAttrs(element)
+}
+```
+
+`processComponent` 函数的源码如下:
+
+```js
+function processComponent (el) {
+  let binding
+  if ((binding = getBindingAttr(el, 'is'))) {
+    el.component = binding
+  }
+  if (getAndRemoveAttr(el, 'inline-template') != null) {
+    el.inlineTemplate = true
+  }
+}
+```
+
+我们知道 `Vue` 内置了 `component` 组件,并且该组件接收两个 `prop` 分别是:`is` 和 `inline-template`。而 `processComponent` 函数就是用来处理 `is` 属性和 `inline-template` 属性的。在 `processComponent` 函数内部,首先执行的是如下这段代码:
+
+```js
+let binding
+if ((binding = getBindingAttr(el, 'is'))) {
+  el.component = binding
+}
+```
+
+定义了 `binding` 变量,它的值是通过 `getBindingAttr` 函数获取元素的 `is` 属性值得到的,如果获取成功,则会将取到的值赋值给元素描述对象的 `el.component` 属性。
+
+举一个例子:
+
+* 例子一:
+
+```html
+<div is></div>
+```
+
+上例中的 `is` 属性是非绑定的,并且没有任何值,则最终如上标签经过处理后其元素描述对象的 `el.component` 属性值为空字符串:
+
+```js
+el.component = ''
+```
+
+* 例子二:
+
+```html
+<div is="child"></div>
+```
+
+上例中的 `is` 属性是非绑定的,但是有一个字符串值,则最终如上标签经过处理后其元素描述对象的 `el.component` 属性值为:
+
+```js
+el.component = JSON.stringify('child')
+```
+
+* 例子三:
+
+```html
+<div :is="child"></div>
+```
+
+上例中的 `is` 属性是非绑定的,但是有一个字符串值,则最终如上标签经过处理后其元素描述对象的 `el.component` 属性值为:
+
+```js
+el.component = 'child'
+```
+
+接着我们再来看 `processComponent` 函数中如下的这段代码:
+
+```js
+if (getAndRemoveAttr(el, 'inline-template') != null) {
+  el.inlineTemplate = true
+}
+```
+
+这段代码用来处理 `inline-template` 属性的,首先通过 `getAndRemoveAttr` 属性获取 `inline-template` 属性的值,如果获取成功,则将元素描述对象的 `el.inlineTemplate` 属性设置为 `true`,代表着该标签使用了 `inline-template` 属性。
+
+以上就是 `processComponent` 函数所做的事情。
+
+## 前置处理、中置处理、后置处理
 
+我们回到 `processElement` 函数:
+
+```js {11-13}
+export function processElement (element: ASTElement, options: CompilerOptions) {
+  processKey(element)
+
+  // determine whether this is a plain element after
+  // removing structural attributes
+  element.plain = !element.key && !element.attrsList.length
 
+  processRef(element)
+  processSlot(element)
+  processComponent(element)
+  for (let i = 0; i < transforms.length; i++) {
+    element = transforms[i](element, options) || element
+  }
+  processAttrs(element)
+}
+```
 
+如上高亮代码所示,这段代码是一段 `for` 循环,用来遍历 `transforms` 数组,我们前面曾经遇到过对于 `preTransforms` 数组的遍历,我们当时说这是在应用“前置处理”,而 `transforms` 则可以成为“中置处理”,实际上还有“后置处理”,“后置处理”的代码存在于 `closeElement` 函数中,如下:
 
-### 增强的 class
-### 增强的 style
-### 特殊的 model
+```js {10-12}
+function closeElement (element) {
+  // check pre state
+  if (element.pre) {
+    inVPre = false
+  }
+  if (platformIsPreTag(element.tag)) {
+    inPre = false
+  }
+  // apply post-transforms
+  for (let i = 0; i < postTransforms.length; i++) {
+    postTransforms[i](element, options)
+  }
+}
+```
 
-## 生成抽象语法树(AST)
+如上高亮代码所示,`closeElement` 函数内部使用一个 `for` 循环遍历了 `postTransforms` 数组,这实际上就是在应用“后置处理”,为什么说这是“后置处理”呢?那是因为只有当遇到二元标签的结束标签或一元标签时才会调用 `closeElement` 函数。
 
-## 静态优化
+无论是前置处理,中置处理还是后置处理,这些名词都是为了让大家更好理解而“杜撰”出来的,他们的作用等价于提供了对元素描述对象处理的钩子,让外界有能力参与不同阶段的元素描述对象的处理,这对于平台化是很重要的事情,不同平台能够通过这些处理钩子去处理那些特定平台下特有的元素或元素的属性。
 
+由于这套文章只关注 `web` 平台,所以后面会详细讲解 `web` 平台下都应用了那些前置处理,中置处理和后置处理,以及处理的目的。
  

+ 103 - 0
docs/art/83vue-parsing-2.md

@@ -0,0 +1,103 @@
+# 句法分析 - 生成真正的AST(二)
+
+鉴于篇幅的原因,本章将继承上一章的内容,继续讲解 `AST` 的生成。
+
+## 彻底理解解析属性值的方式
+
+接下来我们要讲解的就是 `processElement` 函数中调用的最后一个 `process*` 函数,它就是 `processAttrs` 函数,这个函数是用来处理元素描述对象的 `el.attrsList` 数组中剩余的所有属性的。到目前为止我们已经讲解过的属性有:
+
+* `v-pre`
+* `v-for`
+* `v-if`、`v-else-if`、`v-else`
+* `v-once`
+* `key`
+* `ref`
+* `slot`、`slot-scope`、`scope`、`name`
+* `is`、`inline-template`
+
+以上这些属性的解析我们已经全部讲解过了,我们能够发现一些规律,比如在获取这些属性的值的时候,要么使用 `getAndRemoveAttr` 函数,要么就使用 `getBindingAttr` 函数,但是无论使用哪个函数,其共同的行为是:**在获取到特定属性值的同时,还会将该属性从 `el.attrsList` 数组中移除**。所以在调用 `processAttrs` 函数的时候,以上列出来的属性都已经从 `el.attrsList` 数组中移除了。但是 `el.attrsList` 数组中仍然可能存在其他属性,所以这个时候就需要使用 `processAttrs` 函数处理 `el.attrsList` 数组中剩余的属性。
+
+在讲解 `processAttrs` 函数之前,我们来回顾一下现在我们掌握的知识。以如上列出的属性为例,下表中总结了特定的属性与获取该属性值的方式:
+
+| 属性  | 获取属性值的方式 |
+| ------------- | ------------- |
+| `v-pre`  | `getAndRemoveAttr`  |
+| `v-for`  | `getAndRemoveAttr`  |
+| `v-if`、`v-else-if`、`v-else`  | `getAndRemoveAttr`  |
+| `v-once`  | `getAndRemoveAttr`  |
+| `key`  | `getBindingAttr`  |
+| `ref`  | `getBindingAttr`  |
+| `name`  | `getBindingAttr`  |
+| `slot-scope`、`scope`  | `getAndRemoveAttr`  |
+| `slot`  | `getBindingAttr`  |
+| `is`  | `getBindingAttr`  |
+| `inline-template`  | `getAndRemoveAttr`  |
+
+我们发现凡是以 `v-` 开头的属性,在获取属性值的时候都是通过 `getAndRemoveAttr` 函数获取的。而对于没有 `v-` 开头的特性,如 `key`、`ref` 等,在获取这些属性的值时,是通过 `getBindingAttr` 函数获取的,不过 `slot-scope`、`scope` 和 `inline-template` 这三个属性虽然没有以 `v-` 开头,但仍然使用 `getAndRemoveAttr` 函数获取其属性值。但这并不是关键,关键的是我们要知道使用 `getAndRemoveAttr` 和 `getBindingAttr` 这两个函数获取属性值的时候到底有什么区别。
+
+我们知道类似于 `v-for` 或 `v-if` 这类以 `v-` 开头的属性,在 `Vue` 中我们称之为指令,并且这些属性的属性值是默认情况下被当做表达式处理的,比如:
+
+```html
+<div v-if="a && b"></div>
+```
+
+如上代码在执行的时候 `a` 和 `b` 都会被当做变量,并且 `a && b` 是具有完整意义的表达式,而非普通字符串。并且在解析阶段,如上 `div` 标签的元素描述对象的 `el.attrsList` 属性将是如下数组:
+
+```js
+el.attrsList = [
+  {
+    name: 'v-if',
+    value: 'a && b'
+  }
+]
+```
+
+这时,当使用 `getAndRemoveAttr` 函数获取 `v-if` 属性值时,得到的就是字符串 `'a && b'`,但不要忘了这个字符串最终是要运行在 `new Function()` 函数中的,假设是如下代码:
+
+```js
+new Function('a && b')
+```
+
+那么这句代码等价于:
+
+```js
+function () {
+  a && b
+}
+```
+
+可以看到,此时的 `a && b` 已经不再是普通字符串了,而是表达式。
+
+这就意味着 `slot-scope`、`scope` 和 `inline-template` 这三个属性的值,最终也将会被作为表达式处理,而非普通字符串。如下:
+
+```html
+<div slot-scope="slotProps"></div>
+```
+
+如上代码是使用作用域插槽的典型例子,我们知道这里的 `slotProps` 确实是变量,而非字符串。
+
+那如果使用 `getBindingAttr` 函数获取 `slot-scope` 属性的值会产生什么效果呢?由于 `slot-scope` 没有并非 `v-bind:slot-scope` 或 `:slot-scope`,所以在使用 `getBindingAttr` 函数获取 `slot-scope` 属性值的时候,将会得到使用 `JSON.stringify` 函数处理后的结果,即:
+
+```js
+JSON.stringify('slotProps')
+```
+
+这个值就是字符串 `'"slotProps"'`,我们把这个字符串拿到 `new Function()` 中,如下:
+
+```js
+new Function('"slotProps"')
+```
+
+如上这句代码等价于:
+
+```js
+function () {
+  "slotProps"
+}
+```
+
+可以发现此时函数体内只有一个字符串 `"slotProps"`,而非变量。
+
+但并不是说使用了 `getBindingAttr` 函数获取的属性值最终都是字符串,如果该属性是绑定的属性(使用 `v-bind` 或 `:`),则该属性的值仍然具有 `javascript` 语言的能力。否则该属性的值就是一个普通的字符串。
+
+## processAttrs 处理剩余属性

+ 0 - 0
docs/art/83vue-codegen.md → docs/art/84vue-codegen.md


+ 0 - 0
docs/art/84vue-vdom.md → docs/art/85vue-vdom.md


+ 0 - 0
docs/art/85vue-vdom-patch.md → docs/art/86vue-vdom-patch.md