浏览代码

Merge branch 'master' of https://github.com/HcySunYang/vue-design

HcySunYang 7 年之前
父节点
当前提交
4288e4540b

+ 12 - 12
docs/art/81vue-lexical-analysis.md

@@ -480,7 +480,7 @@ let index = 0
 let last, lastTag
 ```
 
-第一个常量是 `stack`,它被初始化为一个空数组,在 `while` 循环中处理 `html` 字符流的时候每当遇到一个**非一元标签**,都会将该开始标签 `push` 到该数组。那么它的作用是什么呢?大家思考一个问题:在一个 `html` 字符串中,如何判断一个非一元标签是否缺少结束标签?
+第一个常量是 `stack`,它被初始化为一个空数组,在 `while` 循环中处理 `html` 字符流的时候每当遇到一个 **非一元标签**,都会将该开始标签 `push` 到该数组。那么它的作用是什么呢?大家思考一个问题:在一个 `html` 字符串中,如何判断一个非一元标签是否缺少结束标签?
 
 假设我们有如下 `html` 字符串:
 
@@ -561,7 +561,7 @@ if (!lastTag || !isPlainTextElement(lastTag)) {
 lastTag && isPlainTextElement(lastTag)
 ```
 
-取反后的条件就好理解多了,我们知道 `lastTag` 存储着 `stack` 栈顶的元素,而 `stack` 栈顶的元素应该就是**最近一次遇到的非一元标签的开始标签**,所以以上条件为真等价于:**最近一次遇到的非一元标签是纯文本标签(即:script,style,textarea 标签)**。也就是说:**当前我们正在处理的是纯文本标签里面的内容**。那么现在就清晰多了,当处理纯文本标签里面的内容时,就会执行 `else` 分支,其他情况将执行 `if` 分支。
+取反后的条件就好理解多了,我们知道 `lastTag` 存储着 `stack` 栈顶的元素,而 `stack` 栈顶的元素应该就是 **最近一次遇到的非一元标签的开始标签**,所以以上条件为真等价于:**最近一次遇到的非一元标签是纯文本标签(即:script,style,textarea 标签)**。也就是说:**当前我们正在处理的是纯文本标签里面的内容**。那么现在就清晰多了,当处理纯文本标签里面的内容时,就会执行 `else` 分支,其他情况将执行 `if` 分支。
 
 接下来我们就先从 `if` 分支开始说起,下面的代码是对 `if` 语句块的简化:
 
@@ -590,7 +590,7 @@ if (!lastTag || !isPlainTextElement(lastTag)) {
 }
 ```
 
-简化后的代码看上去结构非常清晰,在 `if` 语句块的一开始定义了 `textEnd` 变量,它的值是**html 字符串中左尖括号(<)第一次出现的位置**,接着开始了对 `textEnd` 变量的一系列判断:
+简化后的代码看上去结构非常清晰,在 `if` 语句块的一开始定义了 `textEnd` 变量,它的值是 **html 字符串中左尖括号(<)第一次出现的位置**,接着开始了对 `textEnd` 变量的一系列判断:
 
 ```js
 if (textEnd === 0) {
@@ -672,7 +672,7 @@ if (comment.test(html)) {
 
 对于注释节点的判断方法是使用正则常量 `comment` 进行判断,即:`comment.test(html)`,对于 `comment` 正则常量我们在前面分析正则的部分已经讲过了,当时我提醒过大家一件事情,即这些正则常量有一个共同的特点:**都是从字符串的开头位置开始匹配的**,也就是说只有当 `html` 字符串的第一个字符是左尖括号(`<`)时才有意义。而现在我们分析的情况恰好是当 `textEnd === 0`,也就是说 `html` 字符串的第一个字符确实是左尖括号(`<`)。
 
-所以如果 `comment.test(html)` 条件为真,则说明**可能是**注释节点,大家要注意关键字:**可能是**,为什么这么说呢?大家知道完整的注释节点不仅仅要以 `<!--` 开头,还要以 `-->` 结尾,如果只以 `<!--` 开头而没有 `-->` 结尾,那显然不是一个注释节点,所以首先要检查 `html` 字符串中 `-->` 的位置:
+所以如果 `comment.test(html)` 条件为真,则说明 **可能是** 注释节点,大家要注意关键字:**可能是**,为什么这么说呢?大家知道完整的注释节点不仅仅要以 `<!--` 开头,还要以 `-->` 结尾,如果只以 `<!--` 开头而没有 `-->` 结尾,那显然不是一个注释节点,所以首先要检查 `html` 字符串中 `-->` 的位置:
 
 ```js
 const commentEnd = html.indexOf('-->')
@@ -749,7 +749,7 @@ if (conditionalComment.test(html)) {
 }
 ```
 
-类似对注释节点的判断一样,对于条件注释节点使用 `conditionalComment` 正则常量。但是如果条件 `conditionalComment.test(html)` 为真,只能说明**可能是**条件注释节点,因为条件注释节点除了要以 `<![` 开头还必须以 `]>` 结尾,所以在 `if` 语句块内第一句代码就是查找字符串 `]>` 的位置:
+类似对注释节点的判断一样,对于条件注释节点使用 `conditionalComment` 正则常量。但是如果条件 `conditionalComment.test(html)` 为真,只能说明 **可能是** 条件注释节点,因为条件注释节点除了要以 `<![` 开头还必须以 `]>` 结尾,所以在 `if` 语句块内第一句代码就是查找字符串 `]>` 的位置:
 
 ```js
 const conditionalEnd = html.indexOf(']>')
@@ -907,7 +907,7 @@ advance(attr[0].length)
 match.attrs.push(attr)
 ```
 
-这样一次循环就结束了,将会开始下一次循环,直到**匹配到开始标签的结束部分**或者**匹配不到属性**的时候循环才会停止。`parseStartTag` 函数 `if` 语句块内的最后一段代码如下:
+这样一次循环就结束了,将会开始下一次循环,直到 **匹配到开始标签的结束部分** 或者 **匹配不到属性** 的时候循环才会停止。`parseStartTag` 函数 `if` 语句块内的最后一段代码如下:
 
 ```js
 if (start) {
@@ -1298,7 +1298,7 @@ function parseEndTag (tagName, start, end) {
 
 可以明确地告诉大家,在 `Vue` 的 `html-parser` 中 `parseEndTag` 函数的使用方式有三种:
 
-* 第一种是处理普通的结束标签,此时**三个参数都传递**
+* 第一种是处理普通的结束标签,此时 **三个参数都传递**
 * 第二种是只传递第一个参数:
 
 ```js
@@ -1307,7 +1307,7 @@ parseEndTag(lastTag)
 
 只传递一个参数的情况我们前面遇到过,就是在 `handleStartTag` 函数中
 
-* 第三种是**不传递参数**,当不传递参数的时候,就是我们讲过的,这是在处理 `stack` 栈剩余未处理的标签。
+* 第三种是 **不传递参数**,当不传递参数的时候,就是我们讲过的,这是在处理 `stack` 栈剩余未处理的标签。
 
 接下来我们就逐步分析 `parseEndTag` 函数的代码,从而明白 `parseEndTag` 函数是如何完成这些事情的。
 
@@ -1338,13 +1338,13 @@ if (expectHTML) {
 lastTag === 'p' && isNonPhrasingTag(tagName)
 ```
 
-的意思是最近一次遇到的开始标签是 `p` 标签,并且当前正在解析的开始标签必须不能是**段落式内容(`Phrasing content`)**模型,这时候 `if` 语句块的代码才会执行,即调用 `parseEndTag(lastTag)`。首先大家要知道每一个 `html` 元素都拥有一个或多个内容模型(`content model`),其中 `p` 标签本身的内容模型是**流式内容(`Flow content`)**,并且 `p` 标签的特性是只允许包含**段落式内容(`Phrasing content`)**。所以条件成立的情况如下:
+的意思是最近一次遇到的开始标签是 `p` 标签,并且当前正在解析的开始标签必须不能是 **段落式内容(`Phrasing content`)** 模型,这时候 `if` 语句块的代码才会执行,即调用 `parseEndTag(lastTag)`。首先大家要知道每一个 `html` 元素都拥有一个或多个内容模型(`content model`),其中 `p` 标签本身的内容模型是 **流式内容(`Flow content`)**,并且 `p` 标签的特性是只允许包含 **段落式内容(`Phrasing content`)**。所以条件成立的情况如下:
 
 ```html
 <p><h2></h2></p>
 ```
 
-在解析上面这段 `html` 字符串的时候,首先遇到 `p` 标签的开始标签,此时 `lastTag` 被设置为 `p`,紧接着会遇到 `h2` 标签的开始标签,由于 `h2` 标签的内容模型属于非**段落式内容(`Phrasing content`)**模型,所以会立即调用 `parseEndTag(lastTag)` 函数闭合 `p` 标签,此时由于强行插入了 `</p>` 标签,所以解析后的字符串将变为如下内容:
+在解析上面这段 `html` 字符串的时候,首先遇到 `p` 标签的开始标签,此时 `lastTag` 被设置为 `p`,紧接着会遇到 `h2` 标签的开始标签,由于 `h2` 标签的内容模型属于非 **段落式内容(`Phrasing content`)** 模型,所以会立即调用 `parseEndTag(lastTag)` 函数闭合 `p` 标签,此时由于强行插入了 `</p>` 标签,所以解析后的字符串将变为如下内容:
 
 ```html
 <p></p><h2></h2></p>
@@ -1473,7 +1473,7 @@ if (tagName) {
 
 可以发现,如果 `tagName` 不存在,那么 `pos` 将始终等于 `0`,这样 `pos >= 0` 将永远成立,所以要想使得 `pos < 0` 成立,那么 `tagName` 参数是必然存在的。也就是说 `pos` 要想小于 `0`,那么必须要执行 `for` 循环,可以发现:**当 `tagName` 没有在 `stack` 栈中找到对应的开始标签时,`pos` 为 `-1`**。这样 `pos >= 0` 的条件就不成立了,此时就会判断 `else if` 分支。
 
-现在我们还需要思考一个问题,**当 `tagName` 没有在 `stack` 栈中找到对应的开始标签时**说明什么问题?我们知道 `tagName` 是当前正在解析的结束标签,结束标签竟然没有找到对应的开始标签?那么也就是说,只写了结束标签而没写开始标签。并且我们可以发现这两个 `else if` 分支的判断条件分别是:
+现在我们还需要思考一个问题,**当 `tagName` 没有在 `stack` 栈中找到对应的开始标签时** 说明什么问题?我们知道 `tagName` 是当前正在解析的结束标签,结束标签竟然没有找到对应的开始标签?那么也就是说,只写了结束标签而没写开始标签。并且我们可以发现这两个 `else if` 分支的判断条件分别是:
 
 ```js
 else if (lowerCasedTagName === 'br')
@@ -1662,7 +1662,7 @@ while (html) {
 
 在这个 `while` 循环内有一个 `if...else` 语句块,代码被该 `if...else` 语句块分为两部分处理,前面我们所讲的都是 `if` 语句块内的代码,我们知道 `else` 语句块的代码只有当 `lastTag` 存在并且 `lastTag` 为纯文本标签时才会被执行,所以可想而知 `else` 语句块的代码就是用来处理纯文本标签内的内容的,什么是纯文本标签呢?根据 `isPlainTextElement` 函数可知纯文本标签包括 `script` 标签、`style` 标签以及 `textarea` 标签。
 
-下面我们就看一下它是如何处理纯文本标签的内容的,首先我们要明确的一点是 `else` 分支的代码处理的是纯文本标签的**内容**,并不是纯文本标签。假设我们的 `html` 字符串如下:
+下面我们就看一下它是如何处理纯文本标签的内容的,首先我们要明确的一点是 `else` 分支的代码处理的是纯文本标签的 **内容**,并不是纯文本标签。假设我们的 `html` 字符串如下:
 
 ```js
 html = '<textarea>aaaabbbb</textarea>'

+ 13 - 13
docs/art/82vue-parsing.md

@@ -1157,10 +1157,10 @@ parseHTML(template, {
 
 我们在 [词法分析 - 为生成AST做准备](./81vue-lexical-analysis.md) 一章中讲解 `parseHTML` 函数时已经顺带分析了所有选项的作用。其中对于构建 `AST` 来说最关键的选项就是四个钩子函数选项:
 
-* 1、`start` 钩子函数,在解析 `html` 字符串时每次遇到**开始标签**时就会调用该函数
-* 2、`end` 钩子函数,在解析 `html` 字符串时每次遇到**结束标签**时就会调用该函数
-* 3、`chars` 钩子函数,在解析 `html` 字符串时每次遇到**纯文本**时就会调用该函数
-* 4、`comment` 钩子函数,在解析 `html` 字符串时每次遇到**注释节点**时就会调用该函数
+* 1、`start` 钩子函数,在解析 `html` 字符串时每次遇到 **开始标签** 时就会调用该函数
+* 2、`end` 钩子函数,在解析 `html` 字符串时每次遇到 **结束标签** 时就会调用该函数
+* 3、`chars` 钩子函数,在解析 `html` 字符串时每次遇到 **纯文本** 时就会调用该函数
+* 4、`comment` 钩子函数,在解析 `html` 字符串时每次遇到 **注释节点** 时就会调用该函数
 
 下面我们就从 `start` 钩子函数开始说起,为什么从 `start` 钩子函数开始呢?因为正常情况下,解析一段 `html` 字符串时必然最先遇到的就是开始标签。所以我们从 `start` 钩子函数开始讲解,在讲解的过程中为了说明某些问题我们会逐个举例。
 
@@ -1182,7 +1182,7 @@ start (tag, attrs, unary) {
 const ns = (currentParent && currentParent.ns) || platformGetTagNamespace(tag)
 ```
 
-为了让大家更好的理解,我们这里规定一些事情,比如既然我们讲解的是 `start` 钩子函数,那么当前的解析必然处于遇到一个开始标签的阶段,我们把当前解析所遇到的开始标签称为:**当前元素**,另外我们把**当前元素**的父标签称为:**父级元素**。
+为了让大家更好的理解,我们这里规定一些事情,比如既然我们讲解的是 `start` 钩子函数,那么当前的解析必然处于遇到一个开始标签的阶段,我们把当前解析所遇到的开始标签称为:**当前元素**,另外我们把 **当前元素** 的父标签称为:**父级元素**。
 
 如上这句代码定义了 `ns` 常量,它的值为标签的命名空间,如何获取当前元素的命名空间呢?首先检测 `currentParent` 变量是否存在,我们知道 `currentParent` 变量为当前元素的父级元素描述对象,如果当前元素存在父级并且父级元素存在命名空间,则使用父级的命名空间作为当前元素的命名空间。如果父级元素不存在或父级元素没有命名空间,那么会通过调用 `platformGetTagNamespace(tag)` 函数获取当前元素的命名空间。举个例子,假设我们解析的模板字符串为:
 
@@ -1389,7 +1389,7 @@ if (inVPre) {
 
 可以看到这段代码开始大量调用 `process*` 系列的函数,前面说过了,这其实就是在对当前元素描述对象做额外的处理,使得该元素描述对象能更好的描述一个标签。简单点说就是在元素描述对象上添加各种各样的具有标识作用的属性,就比如之前遇到的 `ns` 属性和 `forbidden` 属性,它们都能够对标签起到描述作用。
 
-不过我们本节主要总结**解析一个开始标签需要做的事情**,所以暂时不具体去看上面这些代码的实现。我们继续往下走,接下来定义了一个叫做 `checkRootConstraints` 的函数:
+不过我们本节主要总结 **解析一个开始标签需要做的事情**,所以暂时不具体去看上面这些代码的实现。我们继续往下走,接下来定义了一个叫做 `checkRootConstraints` 的函数:
 
 ```js
 function checkRootConstraints (el) {
@@ -1410,7 +1410,7 @@ function checkRootConstraints (el) {
 }
 ```
 
-该函数的作用是什么呢?它的作用是用来检测模板根元素是否符合要求,我们知道在编写 `Vue` 模板的时候会受到两种约束,首先模板必须有且仅有一个被渲染的根元素,第二不能使用 `slot` 标签和 `template` 标签作为模板的根元素,对于第一点为什么模板必须有且仅有一个被渲染的根元素,我们会在代码生成的部分为大家讲解,对于第二点为什么不能使用 `slot` 和 `template` 标签作为模板根元素,这是因为 `slot` 作为插槽,它的内容是由外界决定的,而插槽的内容很有可能渲染多个节点,`template` 元素的内容虽然不是由外界决定的,但它本身作为抽象组件是不会渲染任何内容到页面的,而其又可能包含多个子节点,所以也不允许使用 `template` 标签作为根节点。总之这些限制都是出于**必须有且仅有一个根元素**考虑的。
+该函数的作用是什么呢?它的作用是用来检测模板根元素是否符合要求,我们知道在编写 `Vue` 模板的时候会受到两种约束,首先模板必须有且仅有一个被渲染的根元素,第二不能使用 `slot` 标签和 `template` 标签作为模板的根元素,对于第一点为什么模板必须有且仅有一个被渲染的根元素,我们会在代码生成的部分为大家讲解,对于第二点为什么不能使用 `slot` 和 `template` 标签作为模板根元素,这是因为 `slot` 作为插槽,它的内容是由外界决定的,而插槽的内容很有可能渲染多个节点,`template` 元素的内容虽然不是由外界决定的,但它本身作为抽象组件是不会渲染任何内容到页面的,而其又可能包含多个子节点,所以也不允许使用 `template` 标签作为根节点。总之这些限制都是出于 **必须有且仅有一个根元素** 考虑的。
 
 可以看到在 `checkRootConstraints` 函数内部首先通过判断 `el.tag === 'slot' || el.tag === 'template'` 来判断根元素是否是 `slot` 标签或 `template` 标签,如果是则打印警告信息。接着又判断当前元素是否使用了 `v-for` 指令,因为 `v-for` 指令会渲染多个节点所以根元素是不允许使用 `v-for` 指令的。另外大家注意在 `checkRootConstraints` 函数内部打印警告信息时使用的是 `warnOnce` 函数而非 `warn` 函数,也就是说如果第一个 `warnOnce` 函数执行并打印了警告信息那么第二个 `warnOnce` 函数就不会再次打印警告信息,这么做的目的是每次只提示一个编译错误给用户,避免多次打印不同错误给用户造成迷惑,这是出于对开发者解决问题友好的考虑。
 
@@ -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` 特性,如下:
 
@@ -1728,9 +1728,9 @@ function findPrevElement (children: Array<any>): ASTElement | void {
 </div>
 ```
 
-当解析器遇到带有 `v-else-if` 指令的 `p` 标签时,那么此时它的前一个元素节点应该是带有 `v-if` 指令的 `div` 标签,如何找到该 `div` 标签呢?由于当前正在解析的标签为 `p`,此时 `p` 标签的元素描述对象还没有被添加到父级元素描述对象的 `children` 数组中,所以此时父级元素描述对象的 `children` 数组中最后一个元素节点就应该是 `div` 元素。注意我们说的是**最后一个元素节点**,而不是**最后一个节点**。所以要想得到 `div` 标签,我们只要找到父级元素描述对象的 `children` 数组最后一个元素节点即可。
+当解析器遇到带有 `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` 标签。
+当解析器遇到带有 `v-else` 指令的 `span` 标签时,大家思考一下此时 `span` 标签的前一个 **元素节点** 是什么?答案还是 `div` 标签,而不是 `p` 标签,这是因为 `p` 标签的元素描述对象没有被添加到父级元素描述对象的 `children` 数组中,而是被添加到 `div` 标签元素描述对象的 `ifConditions` 数组中了。所以对于 `span` 标签来讲,它的前一个元素节点仍然是 `div` 标签。
 
 总之我们发现 `findPrevElement` 函数只需要找到父级元素描述对象的最后一个元素节点即可,如下:
 
@@ -1967,7 +1967,7 @@ if (!inVPre) {
 }
 ```
 
-高亮的代码判断了元素对象的 `.pre` 属性是否为真,我们知道假如一个标签使用了 `v-pre` 指令,那么经过 `processPre` 函数处理之后,该元素描述对象的 `.pre` 属性值为 `true`,这时会将 `inVPre` 变量的值也设置为 `true`。当 `inVPre` 变量为真时,意味着**后续的所有解析工作都处于 `v-pre` 环境下**,编译器会跳过拥有 `v-pre` 指令元素以及其子元素的编译过程,所以后续的编译逻辑需要 `inVPre` 变量作为标识才行。
+高亮的代码判断了元素对象的 `.pre` 属性是否为真,我们知道假如一个标签使用了 `v-pre` 指令,那么经过 `processPre` 函数处理之后,该元素描述对象的 `.pre` 属性值为 `true`,这时会将 `inVPre` 变量的值也设置为 `true`。当 `inVPre` 变量为真时,意味着 **后续的所有解析工作都处于 `v-pre` 环境下**,编译器会跳过拥有 `v-pre` 指令元素以及其子元素的编译过程,所以后续的编译逻辑需要 `inVPre` 变量作为标识才行。
 
 另外如上代码中我们要注意判断条件:`if (!inVPre)`,该条件保证了如果当前解析工作已经处于 `v-pre` 环境下了,则不需要再次执行该 `if` 语句块内的代码。
 
@@ -2477,7 +2477,7 @@ function processIf (el) {
 <div v-if="a && b"></div>
 ```
 
-则该元素描述对象的 `el.if` 的值为字符串 `'a && b'`。在设置完 `el.if` 属性之后,紧接着调用了 `addIfCondition` 函数,可以看到第一个参数就是当前元素描述对象本身,所以如果一个元素使用了 `v-if` 指令,那么它会把自身作为一个**条件对象**添加到自身元素描述对象的 `ifConditions` 数组中,补充一下这里所说的**条件对象**指的是形如 `addIfCondition` 函数第二个参数的对象结构:
+则该元素描述对象的 `el.if` 的值为字符串 `'a && b'`。在设置完 `el.if` 属性之后,紧接着调用了 `addIfCondition` 函数,可以看到第一个参数就是当前元素描述对象本身,所以如果一个元素使用了 `v-if` 指令,那么它会把自身作为一个 **条件对象** 添加到自身元素描述对象的 `ifConditions` 数组中,补充一下这里所说的 **条件对象** 指的是形如 `addIfCondition` 函数第二个参数的对象结构:
 
 ```js
 {
@@ -2792,7 +2792,7 @@ if (dynamicValue != null) {
 
 如上代码所示,绑定属性 `key` 的属性值是一个表达式,该表达式里的 `||` 符号代表的是逻辑或运算符,而逻辑或运算符是由两个管道符 `|` 组成的,所以我们不能把这两个管道符中的任何一个作为过滤器的分界线。
 
-实际上除了以上五种情况之外,管道符存在歧义的地方还有**按位或**运算符,它是位运算中的一个运算符,该运算符就是由一个管道符组成,所以它与过滤器的分界线完全一样,这时我们必须做出选择:既然你希望管道符用来作为过滤器的分界线那就抛弃它按位或运算符的意义。有的同学会说,这不是得不到完全的语言能力了吗?实际上问题一点都不大,因为任何绑定属性的值理论上你都可以通过计算属性实现,而不是直接将表达式写在属性值的位置。话虽然这么说但是我们还是应该做一些基本的处理,比如以上列出的五种管道符存在歧义的地方我们是有能力处理的。
+实际上除了以上五种情况之外,管道符存在歧义的地方还有 **按位或** 运算符,它是位运算中的一个运算符,该运算符就是由一个管道符组成,所以它与过滤器的分界线完全一样,这时我们必须做出选择:既然你希望管道符用来作为过滤器的分界线那就抛弃它按位或运算符的意义。有的同学会说,这不是得不到完全的语言能力了吗?实际上问题一点都不大,因为任何绑定属性的值理论上你都可以通过计算属性实现,而不是直接将表达式写在属性值的位置。话虽然这么说但是我们还是应该做一些基本的处理,比如以上列出的五种管道符存在歧义的地方我们是有能力处理的。
 
 接下来我们思考一下应该如何判断一个管道符到底是不是表达式与过滤器的分界线,我们依据五种情况逐个分析,首先对于单引号中的管道符:
 

+ 20 - 21
docs/art/83vue-parsing-2.md

@@ -605,7 +605,7 @@ isProp || (!el.component && platformMustUseProp(el.tag, el.attrsMap.type, name))
 !el.component && platformMustUseProp(el.tag, el.attrsMap.type, name)
 ```
 
-首先 `el.component` 必须为假,这个条件能够保证什么呢?我们知道 `el.component` 属性保存的是标签 `is` 属性的值,如果 `el.component` 属性为假就能够保证标签没有使用 `is` 属性。那么为什么需要这个保证呢?这是因为后边的 [platformMustUseProp](../appendix/web-util.html#mustuseprop) 函数,该函数的讲解可以在附录中查看,总结如下:
+首先 `el.component` 必须为假,这个条件能够保证什么呢?我们知道 `el.component` 属性保存的是标签 `is` 属性的值,如果 `el.component` 属性为假就能够保证标签没有使用 `is` 属性。那么为什么需要这个保证呢?这是因为后边的 [platformMustUseProp](../appendix/web-util.md#mustuseprop) 函数,该函数的讲解可以在附录中查看,总结如下:
 
 * `input,textarea,option,select,progress` 这些标签的 `value` 属性都应该使用元素对象的原生的 `prop` 绑定(除了 `type === 'button'` 之外)
 * `option` 标签的 `selected` 属性应该使用元素对象的原生的 `prop` 绑定
@@ -1061,7 +1061,7 @@ export function addDirective (
 }
 ```
 
-可以看到 `addDirective` 函数接收六个参数,在 `addDirective` 函数体内,首先判断了元素描述对象的 `el.directives` 是否存在,如果不存在则先将其初始化一个空数组,然后再使用 `push` 方法添加一个指令信息对象到 `el.directives` 数组中,如果 `el.directives` 属性已经存在,则直接使用 `push` 方法将指令信息对象添加到 `el.directives` 数组中。我们一直说的**指令信息对象**实际上指的就是如上代码中传递给 `push` 方法的参数:
+可以看到 `addDirective` 函数接收六个参数,在 `addDirective` 函数体内,首先判断了元素描述对象的 `el.directives` 是否存在,如果不存在则先将其初始化一个空数组,然后再使用 `push` 方法添加一个指令信息对象到 `el.directives` 数组中,如果 `el.directives` 属性已经存在,则直接使用 `push` 方法将指令信息对象添加到 `el.directives` 数组中。我们一直说的 **指令信息对象** 实际上指的就是如上代码中传递给 `push` 方法的参数:
 
 ```js
 { name, rawName, value, arg, modifiers }
@@ -1287,7 +1287,7 @@ if (!el.component &&
 }
 ```
 
-实际上元素描述对象的 `el.attrs` 数组中所存储的任何属性都会在由虚拟DOM创建真实DOM的过程中使用 `setAttribute` 方法将属性添加到真实DOM元素上,而在火狐浏览器中存在无法通过DOM元素的 `setAttribute` 方法为 `video` 标签添加 `muted` 属性的问题,所以如上代码就是为了解决该问题的,其方案是如果一个属性的名字是 `muted` 并且该标签满足 [platformMustUseProp](../appendix/web-util.html#mustuseprop) 函数(`video` 标签满足),则会额外调用 `addProp` 函数将属性添加到元素描述对象的 `el.props` 数组中。为什么这么做呢?这是因为元素描述对象的 `el.props` 数组中所存储的任何属性都会在由虚拟DOM创建真实DOM的过程中直接使用真实DOM对象添加,也就是说对于 `<video>` 标签的 `muted` 属性的添加方式为:`videoEl.muted = true`。另外如上代码的注释中已经提供了相应的 `issue` 号:`#6887`,感兴趣的同学可以去看一下。
+实际上元素描述对象的 `el.attrs` 数组中所存储的任何属性都会在由虚拟DOM创建真实DOM的过程中使用 `setAttribute` 方法将属性添加到真实DOM元素上,而在火狐浏览器中存在无法通过DOM元素的 `setAttribute` 方法为 `video` 标签添加 `muted` 属性的问题,所以如上代码就是为了解决该问题的,其方案是如果一个属性的名字是 `muted` 并且该标签满足 [platformMustUseProp](../appendix/web-util.md#mustuseprop) 函数(`video` 标签满足),则会额外调用 `addProp` 函数将属性添加到元素描述对象的 `el.props` 数组中。为什么这么做呢?这是因为元素描述对象的 `el.props` 数组中所存储的任何属性都会在由虚拟DOM创建真实DOM的过程中直接使用真实DOM对象添加,也就是说对于 `<video>` 标签的 `muted` 属性的添加方式为:`videoEl.muted = true`。另外如上代码的注释中已经提供了相应的 `issue` 号:`#6887`,感兴趣的同学可以去看一下。
 
 ## preTransformNode 前置处理
 
@@ -1334,7 +1334,7 @@ preTransforms = pluckModuleFunction(options.modules, 'preTransformNode')
 
 由上代码可知 `preTransforms` 变量的值是使用 `pluckModuleFunction` 函数从 `options.modules` 编译器选项中读取 `preTransformNode` 字段筛选出来的。具体的筛选过程在前面的章节中我们已经讲解过了,这里就不再细说。
 
-我来说一说编译器选项中的 `modules`,在 [理解编译器代码的组织方式](./art/80vue-compiler-start.md#理解编译器代码的组织方式) 一节中我们知道编译器的选项来自于两部分,一部分是创建编译器时传递的基本选项(`baseOptions`),另一部分则是在使用编辑器编译模板时传递的选项参数。如下是创建编译器时的基本选项:
+我来说一说编译器选项中的 `modules`,在 [理解编译器代码的组织方式](./80vue-compiler-start.md#理解编译器代码的组织方式) 一节中我们知道编译器的选项来自于两部分,一部分是创建编译器时传递的基本选项(`baseOptions`),另一部分则是在使用编辑器编译模板时传递的选项参数。如下是创建编译器时的基本选项:
 
 ```js
 import { baseOptions } from './options'
@@ -1343,7 +1343,7 @@ import { createCompiler } from 'compiler/index'
 const { compile, compileToFunctions } = createCompiler(baseOptions)
 ```
 
-如上代码来自 `src/platforms/web/compiler/index.js` 文件,可以看到 `baseOptions` 导入自 `src/platforms/web/compiler/options.js` 文件,对于基本选项的解析我们在 [compile 的作用](./art/80vue-compiler-start.html#compile-的作用) 一节中做了详细的讲解,并且整理了 [附录/编译器选项](../appendix/compiler-options.html),如果大家忘记了可以回头查看。
+如上代码来自 `src/platforms/web/compiler/index.js` 文件,可以看到 `baseOptions` 导入自 `src/platforms/web/compiler/options.js` 文件,对于基本选项的解析我们在 [compile 的作用](./80vue-compiler-start.md#compile-的作用) 一节中做了详细的讲解,并且整理了 [附录/编译器选项](../appendix/compiler-options.md),如果大家忘记了可以回头查看。
 
 最终我们了解到编译器选项的 `modules` 选项来 `src/platforms/web/compiler/modules/index.js` 文件导出的一个数组,如下:
 
@@ -1432,7 +1432,7 @@ if (!map['v-model']) {
 }
 ```
 
-如果该 `input` 标签没有使用 `v-model` 属性,则函数直接返回,什么都不做。所以我们可以说 `preTransformNode` 函数要预处理的是**使用了 `v-model` 属性的 `input` 标签**,不过还没完,我们继续看如下代码
+如果该 `input` 标签没有使用 `v-model` 属性,则函数直接返回,什么都不做。所以我们可以说 `preTransformNode` 函数要预处理的是 **使用了 `v-model` 属性的 `input` 标签**,不过还没完,我们继续看如下代码
 
 ```js {9}
 let typeBinding
@@ -2021,7 +2021,7 @@ export const parseStyleText = cached(function (cssText) {
 })
 ```
 
-由上代码可以 `parseStyleText` 函数是由 `cached` 函数创建的高阶函数,`parseStyleText` 接收内联样式字符串作为参数并返回解析后的对象。在 `parseStyleText` 函数内部首先定义了 `res` 常量,该常量就会作为 `parseStyleText` 函数的返回值,其初始值是一个空对象,接着定义了两个正则常量 `listDelimiter` 和 `propertyDelimiter`,其实把一个内样式字符串解析为对象的思路很简单,首先我们要找到样式字符串的规则,如下:
+由以上代码可知 `parseStyleText` 函数是由 `cached` 函数创建的高阶函数,`parseStyleText` 接收内联样式字符串作为参数并返回解析后的对象。在 `parseStyleText` 函数内部首先定义了 `res` 常量,该常量就会作为 `parseStyleText` 函数的返回值,其初始值是一个空对象,接着定义了两个正则常量 `listDelimiter` 和 `propertyDelimiter`,其实把一个内样式字符串解析为对象的思路很简单,首先我们要找到样式字符串的规则,如下:
 
 ```js
 <div style="color: red; background: green;"></div>
@@ -2046,13 +2046,13 @@ export const parseStyleText = cached(function (cssText) {
 const listDelimiter = /;(?![^(]*\))/g
 ```
 
-该正则表达式使用了**正向否定查找(`(?!`)**,什么是正向否定查找呢?举个例子,正则表达式 `/a(?!b)/`用来匹配后面没有跟字符 `'b'` 的字符 `'a'`。所以如上正则表达式用来全局匹配字符串中的分号(`;`),但是该分号必须满足一个条件,即**该分号的后面不能跟左圆括号(`)`),除非有一个相应的右圆括号(`(`)存在**,说起来有点抽象,我们还是举例说明,如下模板所示:
+该正则表达式使用了 **正向否定查找(`(?!`)**,什么是正向否定查找呢?举个例子,正则表达式 `/a(?!b)/`用来匹配后面没有跟字符 `'b'` 的字符 `'a'`。所以如上正则表达式用来全局匹配字符串中的分号(`;`),但是该分号必须满足一个条件,即 **该分号的后面不能跟左圆括号(`)`),除非有一个相应的右圆括号(`(`)存在**,说起来有点抽象,我们还是举例说明,如下模板所示:
 
 ```html
 <div style="color: red; background: url(www.xxx.com?a=1&amp;copy=3);"></div>
 ```
 
-大家仔细观察如上 `div` 标签的 `style` 属性值中存在几个分号?答案是三个分号,但只有其中两个分号才是真正的样式规则分割,而字符串 `'url(www.xxx.com?a=1&amp;copy=3)'` 中的分号则是不能作为样式规则分割的,正则常量 `listDelimiter` 正是为了实现这个功能而设计的。有的同学可能会问为什么 `url` 中会带有分号(`;`),实际上正如上面的例子所示,我们知道内联样式是写在 `html` 文件中的,而在 `html` 规范中存在一个叫做 `html` 实体的概念,我们来看如下这段 `html` 模板:
+大家仔细观察如上 `div` 标签的 `style` 属性值中存在几个分号?答案是三个分号,但只有其中两个分号才是真正的样式规则分割,而字符串 `'url(www.xxx.com?a=1&amp;copy=3)'` 中的分号则是不能作为样式规则分割的,正则常量 `listDelimiter` 正是为了实现这个功能而设计的。有的同学可能会问为什么 `url` 中会带有分号(`;`),实际上正如上面的例子所示,我们知道内联样式是写在 `html` 文件中的,而在 `html` 规范中存在一个叫做 `html实体` 的概念,我们来看如下这段 `html` 模板:
 
 ```html
 <a href="foo.cgi?chapter=1&copy=3">link</a>
@@ -2071,7 +2071,6 @@ if (styleBinding) {
 
 与处理绑定的 `class` 属性类似,使用 `getBindingAttr` 函数获取到绑定的 `style` 属性值后,如果值存在则直接将其赋值给元素描述对象的 `el.styleBinding` 属性。
 
-
 以上就是中置处理对于 `style` 属性的处理方式,我们做一个简短的总结:
 
 * 非绑定的 `style` 属性值保存在元素描述对象的 `el.staticStyle` 属性中,假设有如下模板:
@@ -2101,7 +2100,7 @@ el.staticStyle = JSON.stringify({
 el.styleBinding = "{ fontSize: fontSize + 'px' }"
 ```
 
-现在前置处理(`preTransformNode`)和中置处理(`transformNode`)我们都讲完了,还剩下后置处理(`postTransformsNode`)没有,每当遇到非一元标签的结束标签或遇到一元标签时则会应用后置处理,我们回到 `src/compiler/parser/index.js` 文件,如下高亮的代码所示:
+现在前置处理(`preTransformNode`)和中置处理(`transformNode`)我们都讲完了,还剩下后置处理(`postTransformsNode`)没有,每当遇到非一元标签的结束标签或遇到一元标签时则会应用后置处理,我们回到 `src/compiler/parser/index.js` 文件,如下高亮的代码所示:
 
 ```js {10-12}
 function closeElement (element) {
@@ -2156,7 +2155,7 @@ if (!currentParent) {
 }
 ```
 
-这段代码是连续的几个 `if` 条件语句,首先判断了 `currentParent` 变量是否存在,我们知道 `currentParent` 变量指向的是当前节点的父节点,如果父节点不存在才会执行该 `if` 条件语句里面的代码。大家思考一下,如果 `currentParent` 变量不存在说明什么问题?我们知道如果代码执行到了这里,那么当前节点必然是文本节点,并且该文本节点没有父级节点。什么情况下出现一个文本节点没有父级节点呢?有两种情况:
+这段代码是连续的几个 `if` 条件语句,首先判断了 `currentParent` 变量是否存在,我们知道 `currentParent` 变量指向的是当前节点的父节点,如果父节点不存在才会执行该 `if` 条件语句里面的代码。大家思考一下,如果 `currentParent` 变量不存在说明什么问题?我们知道如果代码执行到了这里,那么当前节点必然是文本节点,并且该文本节点没有父级节点。什么情况下出现一个文本节点没有父级节点呢?有两种情况:
 
 * 第一:模板中只有文本节点
 
@@ -2209,7 +2208,7 @@ if (isIE &&
 </div>
 ```
 
-如上 `html` 片段存在一个 `<textarea>` 标签,该标签拥有 `placeholder` 属性,但却没有真实的文本内容,假如我们使用如下代码获取字符串内容:
+如上 `html` 片段存在一个 `<textarea>` 标签,该标签拥有 `placeholder` 属性,但却没有真实的文本内容,假如我们使用如下代码获取字符串内容:
 
 ```js
 document.getElementById('box').innerHTML
@@ -2285,13 +2284,13 @@ text = inPre || text.trim()
 text = preserveWhitespace && children.length ? ' ' : ''
 ```
 
-首先我们要明确的是当条件 `inPre || text.trim()` 为假时代表什么,我们对该条件取反:`!inPre && !text.trim()`,取反后的条件很容易理解,用一句话描述就是**不存在于 `<pre>` 标签的空白符**,有的同学可能会有疑问,此时 `text` 一定是空白符吗?难道不可能是空字符串吗?当然不可能是空字符串,因为如果 `text` 是空字符串则代码是不会执行 `chars` 钩子函数的。那么对于不存在于 `<pre>` 标签内的空白符要如何处理呢?我们来看如下代码:
+首先我们要明确的是当条件 `inPre || text.trim()` 为假时代表什么,我们对该条件取反:`!inPre && !text.trim()`,取反后的条件很容易理解,用一句话描述就是 **不存在于 `<pre>` 标签的空白符**,有的同学可能会有疑问,此时 `text` 一定是空白符吗?难道不可能是空字符串吗?当然不可能是空字符串,因为如果 `text` 是空字符串则代码是不会执行 `chars` 钩子函数的。那么对于不存在于 `<pre>` 标签内的空白符要如何处理呢?我们来看如下代码:
 
 ```js
 text = preserveWhitespace && children.length ? ' ' : ''
 ```
 
-如上代码是一个三元运算符,如果 `preserveWhitespace` 常量为真并且当前文本节点的父节点有子元素存在,则将 `text` 变量设置为空格字符(`' '`),否则将 `text` 变量设置为空字符串。其中 `preserveWhitespace` 常量是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。但即使 `preserveWhitespace` 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,换句话说,编译器只会保留那些**不存在于开始标签之后的空格**。而这也体现在了编译器源码的注释中,如下:
+如上代码是一个三元运算符,如果 `preserveWhitespace` 常量为真并且当前文本节点的父节点有子元素存在,则将 `text` 变量设置为空格字符(`' '`),否则将 `text` 变量设置为空字符串。其中 `preserveWhitespace` 常量是一个布尔值代表着是否保留空格,只有它为真的情况下才会保留空格。但即使 `preserveWhitespace` 常量的值为真,如果当前节点的父节点没有子元素则也不会保留空格,换句话说,编译器只会保留那些 **不存在于开始标签之后的空格**。而这也体现在了编译器源码的注释中,如下:
 
 ```js {3}
 text = inPre || text.trim()
@@ -2304,7 +2303,7 @@ text = inPre || text.trim()
 
 我们来做一下总结:
 
-* 1、如果文本节点是非空白符,无论其在不在 `<pre>` 标签之内,只要其不在文本标签内就会对文本进行解码,否则不会解码。
+* 1、如果文本节点是非空白符,无论其在不在 `<pre>` 标签之内,只要其不在文本标签内就会对文本进行解码,否则不会解码。
 * 2、如果文本节点是空白符
   * 2.1、空白符存在于 `<pre>` 标签之内,则完全保留
   * 2.2、空白符不存在于 `<pre>` 标签之内,则根据编译器选项配置来决定是否保留空白,并且只会保留那些不存在于开始标签之后的空白符。
@@ -2410,15 +2409,15 @@ else if (text !== ' ' || !children.length || children[children.length - 1].text
 <p>我的名字叫:{{name}}</p>
 ```
 
-如上 `<p>` 标签内的文本在解析阶段会被当做一个普通的文本节点,是该文本节点却包含了 `Vue` 的模板语法,所以需要使用 `parseText` 对其进行解析,为了让大家更好理解 `parseText` 函数的作用,我们需要先了解 `parseText` 函数的最终目的。我们知道模板最终会被编译器编译为渲染函数,而如上文本节点被编译后将以如下表达式存在于渲染函数中:
+如上 `<p>` 标签内的文本在解析阶段会被当做一个普通的文本节点,是该文本节点却包含了 `Vue` 的模板语法,所以需要使用 `parseText` 对其进行解析,为了让大家更好理解 `parseText` 函数的作用,我们需要先了解 `parseText` 函数的最终目的。我们知道模板最终会被编译器编译为渲染函数,而如上文本节点被编译后将以如下表达式存在于渲染函数中:
 
 ```js
 "我的名字叫:"+_s(name)
 ```
 
-可以看到编译的结果分为两部分,第一部分是普通文本:`"我的名字叫:"`,另外一部分是把字面量表达式中的表达式提取出来并作为 `_s` 函数的参数,这里大家暂时把 `_s` 函数理解成与 `toString` 函数的功能类似即可,并没有什么特别之处。看到这里相信你已经明白 `parseText` 函数的作用了,没错它的作用就是用来识别一段文本节点内容中的普通文本和字面量表达式并把他们按顺序拼接起来
+可以看到编译的结果分为两部分,第一部分是普通文本:`"我的名字叫:"`,另外一部分是把字面量表达式中的表达式提取出来并作为 `_s` 函数的参数,这里大家暂时把 `_s` 函数理解成与 `toString` 函数的功能类似即可,并没有什么特别之处。看到这里相信你已经明白 `parseText` 函数的作用了,没错它的作用就是用来识别一段文本节点内容中的普通文本和字面量表达式并把他们按顺序拼接起来。
 
-接下来我们打开 `src/compiler/parser/text-parser.js` 文件,可以看到该文件导出了一个 `parseText` 函数,所以这个文件的所有内容都服务于 `parseText` 函数,既然 `parseText` 函数会识别字面量表达式,那么自然需要一种识别机制,最容易想到的办法就是使用正则表达式,我们在 `src/compiler/parser/text-parser.js` 文件中能够看到如下正则常量:
+接下来我们打开 `src/compiler/parser/text-parser.js` 文件,可以看到该文件导出了一个 `parseText` 函数,所以这个文件的所有内容都服务于 `parseText` 函数,既然 `parseText` 函数会识别字面量表达式,那么自然需要一种识别机制,最容易想到的办法就是使用正则表达式,我们在 `src/compiler/parser/text-parser.js` 文件中能够看到如下正则常量:
 
 ```js
 const defaultTagRE = /\{\{((?:.|\n)+?)\}\}/g
@@ -2541,7 +2540,7 @@ while ((match = tagRE.exec(text))) {
 match = ['{{name}}', 'name']
 ```
 
-但 `match` 并不是一个普通的数组,它还包含 `match.index` 属性,该属性的值代表着匹配的字符串在整个字符串中的位置,假设我们有这样一段文本:`'abc{{name}}'`,则匹配成功后 `match.index` 的值为 `3`,因为第一个花括号(`{`)在整个字符串中的索引是 `3`。明白了这些我们就可以继续看 `while` 循环内的代码了,在 `while` 循环内的开头是如下这段代码:
+但 `match` 并不是一个普通的数组,它还包含 `match.index` 属性,该属性的值代表着匹配的字符串在整个字符串中的位置,假设我们有这样一段文本:`'abc{{name}}'`,则匹配成功后 `match.index` 的值为 `3`,因为第一个花括号(`{`)在整个字符串中的索引是 `3`。明白了这些我们就可以继续看 `while` 循环内的代码了,在 `while` 循环内的开头是如下这段代码:
 
 ```js
 index = match.index
@@ -2776,5 +2775,5 @@ comment (text: string) {
 
 大家需要注意的是,普通文本节点与注释节点的元素描述对象的类型是一样的,都是 `3`,不同的是注释节点的元素描述对象拥有 `isComment` 属性,并且该属性的值为 `true`,目的就是用来与普通文本节点做区分的。
 
-至此,对于解析器相关的内容我们就全部讲解完毕了,最终解析器把 `Vue` 的模板解析为抽象语法树(`AST`),强烈建议读完本节的同学能够仔细阅读以下附录 [Vue 模板 AST 详解](./appendix/ast.html),相信你一定会有更多的收货
+至此,对于解析器相关的内容我们就全部讲解完毕了,最终解析器把 `Vue` 的模板解析为抽象语法树(`AST`),强烈建议读完本节的同学能够仔细阅读以下附录 [Vue 模板 AST 详解](../appendix/ast.md),相信你一定会有更多的收获
 

+ 10 - 12
docs/art/8vue-reactive-dep-watch.md

@@ -2033,7 +2033,7 @@ export function initState (vm: Component) {
 }
 ```
 
-如上高亮代码所示,在这个 `if` 条件语句块中,调用 `initWatch` 函数,这个函数用来初始化 `watch` 选项,至于判断条件我们就不多讲了,前面讲解中我们已经讲解过类似的判断条件。至于 `initWatch` 函数,它就定义在 `createWatcher` 函数的上方,如下是其全部代码:
+如上高亮代码所示,在这个 `if` 条件语句块中,调用 `initWatch` 函数,这个函数用来初始化 `watch` 选项,至于判断条件我们就不多讲了,前面讲解中我们已经讲解过类似的判断条件。至于 `initWatch` 函数,它就定义在 `createWatcher` 函数的上方,如下是其全部代码:
 
 ```js
 function initWatch (vm: Component, watch: Object) {
@@ -2226,7 +2226,7 @@ if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
 }
 ```
 
-这段代码是对参数 `val` 的检查,后面我们统一称 `val` 为**被观察属性的值**,我们知道既然是深度观测,所以被观察属性的值要么是一个对象要么是一个数组,并且该值不能是冻结的,同时也不应该是 `VNode` 实例(这是Vue单独做的限制)。只有当被观察属性的值满足这些条件时,才会对其进行深度观测,只要有一项不满足 `_traverse` 就会 `return` 结束执行。所以上面这段 `if` 语句可以理解为是在检测被观察属性的值能否进行深度观测,一旦能够深度观测将会继续执行之后的代码,如下:
+这段代码是对参数 `val` 的检查,后面我们统一称 `val` 为 **被观察属性的值**,我们知道既然是深度观测,所以被观察属性的值要么是一个对象要么是一个数组,并且该值不能是冻结的,同时也不应该是 `VNode` 实例(这是Vue单独做的限制)。只有当被观察属性的值满足这些条件时,才会对其进行深度观测,只要有一项不满足 `_traverse` 就会 `return` 结束执行。所以上面这段 `if` 语句可以理解为是在检测被观察属性的值能否进行深度观测,一旦能够深度观测将会继续执行之后的代码,如下:
 
 ```js
 if (isA) {
@@ -2253,7 +2253,7 @@ if (val.__ob__) {
 }
 ```
 
-这段代码的作用不容忽视,它解决了循环引用导致死循环的问题,为了更好说明问题我们举个例子,如下:
+这段代码的作用不容忽视,它解决了循环引用导致死循环的问题,为了更好说明问题我们举个例子,如下:
 
 ```js
 const obj1 = {}
@@ -2475,7 +2475,7 @@ if (!isSSR) {
 }
 ```
 
-只有在非服务端渲染时才会执行 `if` 语句块内的代码,因为服务端渲染中计算属性的实现本质上和使用 `methods` 选项差不多。这里我们着重讲解非服务端渲染的实现,这时 `if` 语句块内的代码会被执行,可以看到在 `if` 语句块内创建了一个观察者实例对象,我们称之为**计算属性的观察者**,同时会把计算属性的观察者添加到 `watchers` 常量对象中,键值是对应计算属性的名字,注意由于 `watchers` 常量与 `vm._computedWatchers` 属性具有相同的引用,所以对 `watchers` 常量的修改相当于对 `vm._computedWatchers` 属性的修改,现在你应该知道了,`vm._computedWatchers` 对象是用来存储计算属性观察者的。
+只有在非服务端渲染时才会执行 `if` 语句块内的代码,因为服务端渲染中计算属性的实现本质上和使用 `methods` 选项差不多。这里我们着重讲解非服务端渲染的实现,这时 `if` 语句块内的代码会被执行,可以看到在 `if` 语句块内创建了一个观察者实例对象,我们称之为 **计算属性的观察者**,同时会把计算属性的观察者添加到 `watchers` 常量对象中,键值是对应计算属性的名字,注意由于 `watchers` 常量与 `vm._computedWatchers` 属性具有相同的引用,所以对 `watchers` 常量的修改相当于对 `vm._computedWatchers` 属性的修改,现在你应该知道了,`vm._computedWatchers` 对象是用来存储计算属性观察者的。
 
 另外有几点需要注意,首先创建计算属性观察者时所传递的第二个参数是 `getter` 函数,也就是说计算属性观察者的求值对象是 `getter` 函数。传递的第四个参数是 `computedWatcherOptions` 常量,它是一个对象,定义在 `initComputed` 函数的上方:
 
@@ -2711,7 +2711,7 @@ depend () {
 }
 ```
 
-此时我们已经知道了 `this.dep` 属性是一个 `Dep` 实例对象,所以 `this.dep.depend()` 这句代码的作用就是用来收集依赖。那么它收集到的东西是什么呢?这就要看 `Dep.target` 属性的值是什么了,我们回想一下整个过程:首先渲染函数的执行会读取计算属性 `compA` 的值,从而触发计算属性 `compA` 的 `get` 拦截器函数,最终调用了 `this.dep.depend()` 方法收集依赖。这个过程中的关键一步就是渲染函数的执行,我们知道在渲染函数执行之前 `Dep.target` 的值必然是**渲染函数的观察者对象**。所以计算属性观察者对象的 `this.dep` 属性中所收集的就是渲染函数的观察者对象。
+此时我们已经知道了 `this.dep` 属性是一个 `Dep` 实例对象,所以 `this.dep.depend()` 这句代码的作用就是用来收集依赖。那么它收集到的东西是什么呢?这就要看 `Dep.target` 属性的值是什么了,我们回想一下整个过程:首先渲染函数的执行会读取计算属性 `compA` 的值,从而触发计算属性 `compA` 的 `get` 拦截器函数,最终调用了 `this.dep.depend()` 方法收集依赖。这个过程中的关键一步就是渲染函数的执行,我们知道在渲染函数执行之前 `Dep.target` 的值必然是 **渲染函数的观察者对象**。所以计算属性观察者对象的 `this.dep` 属性中所收集的就是渲染函数的观察者对象。
 
 记得此时计算属性观察者对象的 `this.dep` 中所收集的是渲染函数观察者对象,假设我们把渲染函数观察者对象称为 `renderWatcher`,那么:
 
@@ -2802,7 +2802,7 @@ compA () {
 
 大家想一想这个函数的执行会发生什么事情?我们知道数据对象的 `a` 属性是响应式的,所以如上函数的执行将会触发属性 `a` 的 `get` 拦截器函数。所以这会导致属性 `a` 将会收集到一个依赖,这个依赖实际上就是计算属性的观察者对象。
 
-现在思路大概明朗了,如果计算属性 `compA` 依赖了数据对象的 `a` 属性,那么属性 `a` 将收集计算属性 `compA` 的**计算属性观察者对象**,而**计算属性观察者对象**将收集**渲染函数观察者对象**,整个路线是这样的:
+现在思路大概明朗了,如果计算属性 `compA` 依赖了数据对象的 `a` 属性,那么属性 `a` 将收集计算属性 `compA` 的 **计算属性观察者对象**,而 **计算属性观察者对象** 将收集 **渲染函数观察者对象**,整个路线是这样的:
 
 ![](http://7xlolm.com1.z0.glb.clouddn.com/2018-06-10-074626.png)
 
@@ -2845,7 +2845,7 @@ update () {
 
 ## 同步执行观察者
 
-通常情况下当数据状态发生改变时,所有 `Watcher` 都为异步执行,这么做的目的是于对性能的考虑。但在某些场景下我们仍需要同步执行的观察者,我们可以使用 `sync` 选项定义同步执行的观察者,如下:
+通常情况下当数据状态发生改变时,所有 `Watcher` 都为异步执行,这么做的目的是于对性能的考虑。但在某些场景下我们仍需要同步执行的观察者,我们可以使用 `sync` 选项定义同步执行的观察者,如下:
 
 ```js
 new Vue({
@@ -2860,7 +2860,7 @@ new Vue({
 
 如上代码所示,我们在定义一个观察者时使用 `sync` 选项,并将其设置为 `true`,此时当数据状态发生变化时该观察者将以同步的方式执行。这么做当然没有问题,因为我们仅仅定义了一个观察者而已。
 
-`Vue` 官方推出了 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 测试工具库,这个库的一个特点是,当你使用它去辅助测试 `Vue` 单文件组件时,数据变更将会以同步的方式触发组件变更,这对于测试而言会提供很大帮助。大家思考一下 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 库是如何实现这个功能的?我们知道开发者在开发组件的时候基本不太可能手动指定一个观察者为同步的,所以 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 库需要有能力拿到组件的定义并人为把组件中定义的所有观察者都转换为同步的,这是一个繁琐并容易引起 `bug` 的工作,为了解决这个问题,`Vue` 提供了 `Vue.config.async` 全局配置,它的默认值为 `true`,我们可以在 `src/core/config.js` 文件中看到这样一句代码,如下:
+`Vue` 官方推出了 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 测试工具库,这个库的一个特点是,当你使用它去辅助测试 `Vue` 单文件组件时,数据变更将会以同步的方式触发组件变更,这对于测试而言会提供很大帮助。大家思考一下 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 库是如何实现这个功能的?我们知道开发者在开发组件的时候基本不太可能手动指定一个观察者为同步的,所以 [vue-test-utils](https://github.com/vuejs/vue-test-utils) 库需要有能力拿到组件的定义并人为把组件中定义的所有观察者都转换为同步的,这是一个繁琐并容易引起 `bug` 的工作,为了解决这个问题,`Vue` 提供了 `Vue.config.async` 全局配置,它的默认值为 `true`,我们可以在 `src/core/config.js` 文件中看到这样一句代码,如下:
 
 ```js {8}
 export default ({
@@ -2897,7 +2897,7 @@ export function queueWatcher (watcher: Watcher) {
 }
 ```
 
-如上高亮代码所示,在非生产环境下如 `!config.async` 为真,则说明开发者配置了 `Vue.config.async = false`,这意味着所有观察者需要同步执行,所以只需要把原本通过 `nextTick` 包装的 `flushSchedulerQueue` 函数单独拿出来执行即可。另外通过如上高亮的代码我们也能够明白一件事儿,那就是 `Vue.config.async` 这个配置项只会在非生产环境生效。
+如上高亮代码所示,在非生产环境下如 `!config.async` 为真,则说明开发者配置了 `Vue.config.async = false`,这意味着所有观察者需要同步执行,所以只需要把原本通过 `nextTick` 包装的 `flushSchedulerQueue` 函数单独拿出来执行即可。另外通过如上高亮的代码我们也能够明白一件事儿,那就是 `Vue.config.async` 这个配置项只会在非生产环境生效。
 
 为了实现同步执行的观察者,除了把 `flushSchedulerQueue` 函数从 `nextTick` 中提取出来之外,还需要做一件事儿,我们打开 `src/core/observer/dep.js` 文件,找到 `notify` 方法,如下:
 
@@ -2917,6 +2917,4 @@ notify () {
 }
 ```
 
-在异步执行观察者的时候,当数据状态方式改变时,会通过如上 `notify` 函数通知变化,从而把执行所有观察者的 `update` 方法,在 `update` 方法内会将所有即将被执行的观察者都添加到观察者队列中,并在 `flushSchedulerQueue` 函数内对观察者回调的执行顺序进行排序。但是当同步执行的观察者时,由于 `flushSchedulerQueue` 函数是立即执行的,它不会等待所有观察者入队之后再去执行,这就没有办法保证观察者回调的正确更新顺序,这时就需要如上高亮的代码,其实现方式是在执行观察者对象的 `update` 更新方法之前就对观察者进行排序,从而保证正确的更新顺序。
-
-
+在异步执行观察者的时候,当数据状态方式改变时,会通过如上 `notify` 函数通知变化,从而执行所有观察者的 `update` 方法,在 `update` 方法内会将所有即将被执行的观察者都添加到观察者队列中,并在 `flushSchedulerQueue` 函数内对观察者回调的执行顺序进行排序。但是当同步执行的观察者时,由于 `flushSchedulerQueue` 函数是立即执行的,它不会等待所有观察者入队之后再去执行,这就没有办法保证观察者回调的正确更新顺序,这时就需要如上高亮的代码,其实现方式是在执行观察者对象的 `update` 更新方法之前就对观察者进行排序,从而保证正确的更新顺序。

+ 9 - 9
docs/art/9vue-state-init.md

@@ -218,7 +218,7 @@ const value = validateProp(key, propsOptions, propsData, vm)
 
 首先将 `prop` 的名字(`key`)添加到 `keys` 数组中,我们知道常量 `keys` 与 `vm.$options._propKeys` 属性具有相同的引用,所以这等价于将 `key` 添加到 `vm.$options._propKeys` 属性中,至于为什么添加到 `vm.$options._propKeys` 属性,我们会在后面讲到。
 
-接着定义了 `value` 常量,该常量的值为 `validateProp` 函数的返回值。一句话概括 `validateProp` 函数的作用:用来校验名字给定的 `prop` 数据是否符合预期的类型,并返回相应 `prop` 的值(或默认值)。至于 `validateProp` 函数的具体实现我们放到后面讲,现在大家只需要知道 `validateProp` 函数会返回给定名字的 `prop` 的值即可,也就是说常量 `value` 中保存着 `prop` 的值。
+接着定义了 `value` 常量,该常量的值为 `validateProp` 函数的返回值。一句话概括 `validateProp` 函数的作用:用来校验名字(`key`)给定的 `prop` 数据是否符合预期的类型,并返回相应 `prop` 的值(或默认值)。至于 `validateProp` 函数的具体实现我们放到后面讲,现在大家只需要知道 `validateProp` 函数会返回给定名字的 `prop` 的值即可,也就是说常量 `value` 中保存着 `prop` 的值。
 
 接着是一个 `if...else` 语句块:
 
@@ -554,7 +554,7 @@ someData instanceof Array
 
 这种方式的问题就在于,不同 `iframes` 之间的 `Array` 构造函数本身都是不相等的。所以以上判断方法只适用于在同一个 `iframes` 环境下。
 
-同理,为了做到更严谨判断,我们需要使用 `getType` 函数,如下:
+同理,为了做到更严谨判断,我们需要使用 `getType` 函数,如下:
 
 ```js
 function getType (fn) {
@@ -710,7 +710,7 @@ stringIndex < 0 || booleanIndex < stringIndex
 <some-comp prop1="" />
 <!-- 等价于 -->
 <some-comp prop1 />
-``` 
+```
 
 最后我们再来回顾一下 `validateProp` 函数中的这段代码:
 
@@ -841,7 +841,7 @@ if (vm && vm.$options.propsData &&
 vm.$options.propsData[key] === undefined
 ```
 
-大家别忘了我们目前讲解的代码是 `getPropDefaultValue` 函数中的代码,代码既然已经执行到了 `getPropDefaultValue` 函数那么说明外界没有向组件传递该 `prop` 数据,那也就是说 `vm.$options.propsData[key]` 很显然的应该是 `undefined`。为什么还需要如上判断呢?实际上事情并非像我们想象的那样。这是因为**组件第一次创建与后续的更新走的是两套不太一致的逻辑**。为了证明这一点,我们需要打开 `src/core/instance/lifecycle.js` 文件找到 `updateChildComponent` 函数,大家现在只需要知道组件的更新是由 `updateChildComponent` 函数来完成的即可,在 `updateChildComponent` 函数内有这样一段代码:
+大家别忘了我们目前讲解的代码是 `getPropDefaultValue` 函数中的代码,代码既然已经执行到了 `getPropDefaultValue` 函数那么说明外界没有向组件传递该 `prop` 数据,那也就是说 `vm.$options.propsData[key]` 很显然的应该是 `undefined`。为什么还需要如上判断呢?实际上事情并非像我们想象的那样。这是因为 **组件第一次创建与后续的更新走的是两套不太一致的逻辑**。为了证明这一点,我们需要打开 `src/core/instance/lifecycle.js` 文件找到 `updateChildComponent` 函数,大家现在只需要知道组件的更新是由 `updateChildComponent` 函数来完成的即可,在 `updateChildComponent` 函数内有这样一段代码:
 
 ```js {8}
 if (propsData && vm.$options.props) {
@@ -1170,7 +1170,7 @@ if (!valid && t === 'object')
 * 1、期望的类型是这五种类型之一:`'String'`、`'Number'`、`'Boolean'`、`'Function'` 以及 `'Symbol'`
 * 2、并且通过 `typeof` 操作符取到的该 `prop` 值的类型为 `object`
 
-这时我们能够否定 `prop` 的值不符合预期吗?答案是不能的,因为在 `javascript` 有个概念叫做**基本包装类型**,比如可以这样定义一个字符串:
+这时我们能够否定 `prop` 的值不符合预期吗?答案是不能的,因为在 `javascript` 有个概念叫做 **基本包装类型**,比如可以这样定义一个字符串:
 
 ```js
 const str = new String('基本包装类型')
@@ -1288,7 +1288,7 @@ if (process.env.NODE_ENV !== 'production') {
 }
 ```
 
-这段代码用来检测该方法是否真正的有定义,如果没有定义则打印警告信息,提示开发者是否正确引用了函数。
+这段代码用来检测该方法是否真正的有定义,如果没有定义则打印警告信息,提示开发者是否正确引用了函数。
 
 接着是如下这段检测代码:
 
@@ -1334,7 +1334,7 @@ methods: {
 
 ## provide 选项的初始化及实现
 
-再往下我们将研究最后两个选项的初始化工作,即 `provide` 选项以及 `inject` 选项。在这之前我们来回顾一下这两个选项的作用,实际上 `Vue` 的官方文档已经明确告诉我们这两个选项主要是用来辅助测试的,在真正的业务代码中是不推荐使用的,一般情况下我们也不需要使用这两个选项。
+再往下我们将研究最后两个选项的初始化工作,即 `provide` 选项 `inject` 选项。在这之前我们来回顾一下这两个选项的作用,实际上 `Vue` 的官方文档已经明确告诉我们这两个选项主要是用来辅助测试的,在真正的业务代码中是不推荐使用的,一般情况下我们也不需要使用这两个选项。
 
 如果一个组件使用了 `provide` 选项,那么该选项指定的数据将会被注入到该组件的所有后代组件中,在后代组件中可以使用 `inject` 选项选择性注入,这样后代组件就拿到了祖先组件提供的数据,这么做的好处是方便了为高阶组件提供数据并测试。
 
@@ -1490,7 +1490,7 @@ const keys = hasSymbol
   : Object.keys(inject)
 ```
 
-现在我们知道 `keys` 常量中保存 `inject` 选项对象的每一个键名,但我们注意到这里有一个对 [hasSymbol](../appendix/core-util.html#hassymbol) 的判断,其目的是保证 `Symbol` 类型与 `Reflect.ownKeys` 可用且为宿主环境原生提供,如果 `hasSymbol` 为真,则说明可用,此时会使用 `Reflect.ownKeys` 获取 `inject` 对象中所有可枚举的键名,否则使用 `Object.keys` 作为降级处理。实际上 `Reflect.ownKeys` 配合可枚举过滤等价于 `Object.keys` 与 `Object.getOwnPropertySymbols` 配合可枚举过滤之和,其好处是支持 `Symbol` 类型作为键名,当然了这一切都建立在宿主环境的支持之上,所以 `Vue` 官网中提到了**`inject` 选项对象的属性可以使用 `ES2015 Symbols` 作为 `key`,但是只在原生支持 `Symbol` 和 `Reflect.ownKeys` 的环境下可工作**。
+现在我们知道 `keys` 常量中保存 `inject` 选项对象的每一个键名,但我们注意到这里有一个对 [hasSymbol](../appendix/core-util.md#hassymbol) 的判断,其目的是保证 `Symbol` 类型与 `Reflect.ownKeys` 可用且为宿主环境原生提供,如果 `hasSymbol` 为真,则说明可用,此时会使用 `Reflect.ownKeys` 获取 `inject` 对象中所有可枚举的键名,否则使用 `Object.keys` 作为降级处理。实际上 `Reflect.ownKeys` 配合可枚举过滤等价于 `Object.keys` 与 `Object.getOwnPropertySymbols` 配合可枚举过滤之和,其好处是支持 `Symbol` 类型作为键名,当然了这一切都建立在宿主环境的支持之上,所以 `Vue` 官网中提到了**`inject` 选项对象的属性可以使用 `ES2015 Symbols` 作为 `key`,但是只在原生支持 `Symbol` 和 `Reflect.ownKeys` 的环境下可工作**。
 
 回过头来继续看 `resolveInject` 函数的代码,接下来的代码使用 `for` 循环,用来遍历刚刚获取到的 `keys` 数组:
 
@@ -1592,6 +1592,6 @@ toggleObserving(true)
 
 另外大家也注意到了在使用 `defineReactive` 函数为组件实例对象定义属性之前,调用了 `toggleObserving(false)` 函数关闭了响应式定义的开关,之后又将开关开启:`toggleObserving(true)`。前面我们已经讲到了类似的情况,这么做将会导致使用 `defineReactive` 定义属性时不会将该属性的值转换为响应式的,所以 `Vue` 文档中提到了:
 
->提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
+> 提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
 
 当然啦,如果父代组件提供的数据本身就是响应式的,即使 `defineReactive` 不转,那么最终这个数据也还是响应式的。