HcySunYang 7 年 前
コミット
03c04fcbbf

+ 6 - 0
note/0前言.md

@@ -18,8 +18,14 @@
 
 #### 文章特点
 
+* 与 `Vue` 源码 `dev` 分支保持同步
+
 这套文章是分析 `Vue` 源码的文章,且会跟随 `Vue` 仓库的 `dev` 分支的源码的变化实时更新(注:有的时候 `dev` 分支的更新到文章的更新会有稍许延迟)。
 
+* 逐行级别的分析
+
+这里要解释一下,有的时候我们在讲解一个文件的代码时,你会发现,有些内容我们并没有进行讲解,那是因为这部分内容可能与本节的主题无关,但这些内容绝对不会被遗漏,它们会被放到合适的地方进行讲解
+
 #### 你应该了解的
 
 文章将会尽可能详细,且尽可能对基础的知识点进行讲解,但需要太多口舌的东西即使再基础也不会去讲,这里列出我希望你在阅读该系列文章前最好了解的东西:

+ 1 - 1
note/1了解Vue这个项目.md

@@ -71,7 +71,7 @@
 
 通过名字,我们就可以猜到,完整版比运行时版本多了一个传说中的 `compiler`,而 `compiler` 在我们介绍目录结构的时候说过,它的作用是:*编译器代码的存放目录,将 template 编译为 render 函数*。
 
-上图中只介绍了 `cjs` 与 `es` 版本的输出,对于 `umd` 模块格式的输出,同样也分为 `运行时版` 与 `完整版`,除此之外每个版本又分为 `生产环境` 与 `开发环境`,如下图:
+上图中只介绍了 `cjs` 与 `es` 版本的输出,对于 `umd` 模块格式的输出,同样也分为 `运行时版` 与 `完整版`,并且还分为 `生产环境` 与 `开发环境`,如下图:
 
 ![](http://ovjvjtt4l.bkt.clouddn.com/2017-08-31-131849.jpg)
 

+ 10 - 8
note/2Vue构造函数.md

@@ -72,7 +72,7 @@ import Vue from 'core/index'
 import Vue from './instance/index'
 ```
 
-按照之前的逻辑,继续打开 `./instance/index.js` 文件:
+按照之前的套路,继续打开 `./instance/index.js` 文件:
 
 ```js
 // 从五个文件导入五个方法(不包括 warn)
@@ -156,7 +156,7 @@ function Vue (options) {
   Object.defineProperty(Vue.prototype, '$props', propsDef)
 ```
 
-我们先看最后两句,使用 `Object.defineProperty` 在 `Vue.prototype` 上定义了两个属性,就是大家熟悉的:`$data` 和 `$props`,这两个属性的定义分别写在了 `dataDef` 以及 `propsDef` 这两个对象里,也就是这两句代码上面的代码,首先是 `get` :
+我们先看最后两句,使用 `Object.defineProperty` 在 `Vue.prototype` 上定义了两个属性,就是大家熟悉的:`$data` 和 `$props`,这两个属性的定义分别写在了 `dataDef` 以及 `propsDef` 这两个对象里,我们来仔细看一下这两个对象的定义,首先是 `get` :
 
 ```js
 const dataDef = {}
@@ -227,9 +227,9 @@ export function installRenderHelpers (target: any) {
 }
 ```
 
-以上代码就是 `installRenderHelpers` 函数的源码,可以发现,这个函数的作用就是在 `Vue.prototype` 上添加一系列方法的,那么这些方法的作用大家还不需要关心,我们后面自然会知道
+以上代码就是 `installRenderHelpers` 函数的源码,可以发现,这个函数的作用就是在 `Vue.prototype` 上添加一系列方法的,那么这些方法的作用大家还不需要关心,后面都会讲解到
 
-`renderMixin` 方法在指定完 `installRenderHelpers` 函数之后,又在 `Vue.prototype` 上添加了两个方法,分别是:`$nextTick` 和 `_render`,最终经过 `renderMixin` 之后,`Vue.prototype` 将又被添加了如下方法:
+`renderMixin` 方法在执行完 `installRenderHelpers` 函数之后,又在 `Vue.prototype` 上添加了两个方法,分别是:`$nextTick` 和 `_render`,最终经过 `renderMixin` 之后,`Vue.prototype` 将又被添加了如下方法:
 
 ```js
 // installRenderHelpers 函数中
@@ -328,7 +328,7 @@ initGlobalAPI(Vue)
   Object.defineProperty(Vue, 'config', configDef)
 ```
 
-这段代码的作用是在 `Vue` 构造函数上添加 `config` 属性,这个属性的添加方式类似我们前面看过的 `$data` 以及 `$props`,也是一个只读的属性,并且当你试图设置其值时,在非生产环境下会给你一个友好的提示,为什么说它友好呢?因为如果是我的话,我可能会提示你:`what are you fucking doing`
+这段代码的作用是在 `Vue` 构造函数上添加 `config` 属性,这个属性的添加方式类似我们前面看过的 `$data` 以及 `$props`,也是一个只读的属性,并且当你试图设置其值时,在非生产环境下会给你一个友好的提示。
 
 那 `Vue.config` 的值是什么呢?在 `src/core/global-api/index.js` 文件的开头有这样一句:
 
@@ -553,13 +553,15 @@ Vue.directive
 Vue.filter
 ```
 
+这三个静态方法大家都不陌生,分别用来全局注册组件,指令和过滤器。
+
 这样,`initGlobalAPI` 方法的全部功能我们就介绍完毕了,它的作用就像它的名字一样,是在 `Vue` 构造函数上添加全局的API,类似整理 `Vue.prototype` 上的属性和方法一样,我们同样对 `Vue` 静态属性和方法做一个整理,将他放到 [附录/Vue 构造函数整理-全局API](Vue构造函数整理-全局API.md) 中,便于以后查阅。
 
 至此,对于 `core/index.js` 文件的作用我们也大概清楚了,在这个文件里,它首先将核心的 `Vue`,也就是在 `core/instance/index.js` 文件中的 `Vue`,也可以说是原型被包装(添加属性和方法)后的 `Vue` 导出,然后使用 `initGlobalAPI` 方法给 `Vue` 添加静态方法和属性,除此之外,在这里文件里,也对原型进行了修改,为其添加了两个属性:`$isServer` 和 `$ssrContext`,最后添加了 `Vue.version` 属性并导出了 `Vue`。
 
 #### Vue 平台化的包装
 
-现在,在我们弄清 `Vue` 构造函数的过程中已经看了两个主要的文件,分别是:`core/instance/index.js` 文件以及 `core/index.js` 文件,前者是 `Vue` 构造函数的定义文件,我们一直都叫其 `Vue` 的出生文件,主要作用是定义 `Vue` 构造函数,并对其原型添加属性和方法。后者的主要作用是,为 `Vue` 添加全局的API,也就是静态的方法和属性。这两个文件有个共同点,就是它们都在 `core` 目录下,我们在介绍 `Vue` 项目目录结构的时候说过:`core` 目录存放的是平台无关的代码,所以无论是 `core/instance/index.js` 文件还是 `core/index.js` 文件,它们都在包装核心的 `Vue`,且这些包装是平台无关的。但是,`Vue` 是一个 `Multi-platform` 的项目(web和weex),不同平台可能会内置不同的组件、指令,或者一些平台特有的功能等等,那么这就需要对 `Vue` 根据不同的平台进行平台化的包装,这就是接下来我们要看的文件,这个文件也出现在我们寻找 `Vue` 构造函数的路线上,他就是:`platforms/web/runtime/index.js` 文件。
+现在,在我们弄清 `Vue` 构造函数的过程中已经看了两个主要的文件,分别是:`core/instance/index.js` 文件以及 `core/index.js` 文件,前者是 `Vue` 构造函数的定义文件,我们一直都叫其 `Vue` 的出生文件,主要作用是定义 `Vue` 构造函数,并对其原型添加属性和方法,即实例属性和实例方法。后者的主要作用是,为 `Vue` 添加全局的API,也就是静态的方法和属性。这两个文件有个共同点,就是它们都在 `core` 目录下,我们在介绍 `Vue` 项目目录结构的时候说过:`core` 目录存放的是平台无关的代码,所以无论是 `core/instance/index.js` 文件还是 `core/index.js` 文件,它们都在包装核心的 `Vue`,且这些包装是平台无关的。但是,`Vue` 是一个 `Multi-platform` 的项目(web和weex),不同平台可能会内置不同的组件、指令,或者一些平台特有的功能等等,那么这就需要对 `Vue` 根据不同的平台进行平台化的包装,这就是接下来我们要看的文件,这个文件也出现在我们寻找 `Vue` 构造函数的路线上,他就是:`platforms/web/runtime/index.js` 文件。
 
 在看这个文件之前,大家可以先打开 `platforms` 目录,可以发现有两个子目录 `web` 和 `weex`。这两个子目录的作用就是分别为相应的平台对核心的 `Vue` 进行包装的。而我们所要研究的 web 平台,就在 `web` 这个目录里。
 
@@ -754,7 +756,7 @@ Vue.prototype.$mount = function (
 
 首先在 `Vue.prototype` 上添加 `__patch__` 方法,如果在浏览器环境运行的话,这个方法的值为 `patch` 函数,否则是一个空函数 `noop`。然后又在 `Vue.prototype` 上添加了 `$mount` 方法,我们暂且不关心 `$mount` 方法的内容和作用。
 
-之后的一段代码是 `vue-devtools` 的全局钩子,它被包裹在 `setTimeout` 中,最后导出了 `Vue`。
+再往下的一段代码是 `vue-devtools` 的全局钩子,它被包裹在 `setTimeout` 中,最后导出了 `Vue`。
 
 现在我们就看完了 `platforms/web/runtime/index.js` 文件,该文件的作用是对 `Vue` 进行平台化的包装:
 
@@ -767,7 +769,7 @@ Vue.prototype.$mount = function (
 
 #### with compiler
 
-在看完 `runtime/index.js` 文件之后,其实 `运行时` 版本的 `Vue` 构造函数就以及成型了。我们可以打开 `entry-runtime.js` 这个入口文件,这个文件只有两行代码:
+在看完 `runtime/index.js` 文件之后,其实 `运行时` 版本的 `Vue` 构造函数就已经“成型了”。我们可以打开 `entry-runtime.js` 这个入口文件,这个文件只有两行代码:
 
 ```js
 import Vue from './runtime/index'

+ 4 - 4
note/3Vue的思路之以一个例子为线索.md

@@ -4,7 +4,7 @@
 
 从这一章节开始,我们将逐渐走进 `Vue`,我们采用一种由浅入深,由宽到窄的思路,一开始我们会从宏观来看 `Vue` 是如何设计的,然后在一点点“追究”进去,进而逐步搞清楚 `Vue` 为什么这么设计。
 
-而这一节,我们就致力于搞清楚:`Vue的思路`。我们将会从一个例子开始,这个例子及其简单,如下:
+而这一节,我们就致力于搞清楚:`Vue的思路`。我们将会从一个例子开始,这个例子非常简单,如下:
 
 我们有如下模板:
 
@@ -167,9 +167,9 @@ initProvide(vm) // resolve provide after data/props
 callHook(vm, 'created')
 ```
 
-上面的代码是那两段性能追踪的代码之间全部的内容,我们逐一分析,首先在 `Vue` 实例上添加 `_isVue` 属性,并设置其值为 `true`。目的是用来标识一个对象是 Vue 实例,以避免其被响应系统观测(其实在其他地方也有用到,但是宗旨都是一样的,这个属性就是用来告诉你:我不是普通的对象,我是Vue实例)。
+上面的代码是那两段性能追踪的代码之间全部的内容,我们逐一分析,首先在 `Vue` 实例上添加 `_isVue` 属性,并设置其值为 `true`。目的是用来标识一个对象是 `Vue` 实例,即如果发现一个对象拥有 `_isVue` 属性并且其值为 `true`,那么就代表该对象是 `Vue` 实例。这样可以避免该对象被响应系统观测(其实在其他地方也有用到,但是宗旨都是一样的,这个属性就是用来告诉你:我不是普通的对象,我是Vue实例)。
 
-然后是这样一段代码:
+再往下是这样一段代码:
 
 ```js
 // merge options
@@ -187,7 +187,7 @@ if (options && options._isComponent) {
 }
 ```
 
-上面的代码是一段 `if` 分支语句,条件是:`options && options._isComponent`,其中 `options` 就是我们调用 `Vue` 时传递的参数选项,但 `options._isComponent` 是什么鬼?我们知道在本节的例子中我们的 `options` 对象只有两个属性 `el` 和 `data`,并且在 `Vue` 的官方文档中你也找不到关于 `_isComponent` 这个选项的介绍,其实我相信大部分同学都已经知道,这是一个内部选项。而事实也确实是这样,这个内部选项是在 `Vue` 创建组件的时候才会有的,为了不牵涉太多内容导致大家头晕,这里暂时不介绍其相关内容,我们在这套文章的后面必然会再回来找它
+上面的代码是一段 `if` 分支语句,条件是:`options && options._isComponent`,其中 `options` 就是我们调用 `Vue` 时传递的参数选项,但 `options._isComponent` 是什么鬼?我们知道在本节的例子中我们的 `options` 对象只有两个属性 `el` 和 `data`,并且在 `Vue` 的官方文档中你也找不到关于 `_isComponent` 这个选项的介绍,其实我相信大部分同学都已经知道,这是一个内部选项。而事实也确实是这样,这个内部选项是在 `Vue` 创建组件的时候才会有的,为了不牵涉太多内容导致大家头晕,这里暂时不介绍其相关内容。
 
 根据本节的例子,上面的代码必然会走 `else` 分支,也就是这段代码:
 

+ 22 - 9
note/4Vue选项的规范化.md

@@ -2,6 +2,8 @@
 
 <p class="tip">注意:本节中当我们提到“以我们的例子为例”的时候,这里的“我们的例子”指的是《Vue的思路之以一个例子为线索》中的例子</p>
 
+#### 弄清楚传递给 mergeOptions 函数的三个参数
+
 这一小节我们继续前面的讨论,看一看 `mergeOptions` 都做了些什么。根据 `core/instance/init.js` 顶部的引用关系可知,`mergeOptions` 函数来自于 `core/util/options.js` 文件,事实上不仅仅是 `mergeOptions` 函数,整个文件所做的一切都为了一件事:选项的合并。
 
 不过在我们深入 `core/util/options.js` 文件之前,我们有必要搞清楚一件事,就是如下代码中:
@@ -172,6 +174,8 @@ vm.$options = mergeOptions(
 
 现在我们已经搞清楚传递给 `mergeOptions` 函数的三个参数分别是什么了,那么接下来我们就打开 `core/util/options.js` 文件并找到  `mergeOptions` 方法,看一看都发生了什么。
 
+#### 检查组件名称是否符合要求
+
 打开 `core/util/options.js` 文件,找到 `mergeOptions` 方法,这个方法上面有一段注释:
 
 ```js
@@ -227,9 +231,9 @@ export function validateComponentName (name: string) {
 `validateComponentName` 函数由两个 `if` 语句块组成,所以可想而知,对于组件的名字要满足这两条规则才行,这两条规则就是这两个 `if` 分支的条件语句:
 
 * ①:组件的名字要满足正则表达式:`/^[a-zA-Z][\w-]*$/`
-* ②:要满足:`isBuiltInTag(name) || config.isReservedTag(name)` 不成立
+* ②:要满足:条件 `isBuiltInTag(name) || config.isReservedTag(name)` 不成立
 
-对于第一条规则,`Vue` 限定组件的名字由普通的字符和中横线(-)组成,且必须以字开头。
+对于第一条规则,`Vue` 限定组件的名字由普通的字符和中横线(-)组成,且必须以字开头。
 
 对于第二条规则,首先将 `options.components` 对象的 `key` 小写化作为组件的名字,然后以组件的名字为参数分别调用两个方法:`isBuiltInTag` 和 `config.isReservedTag`,其中 `isBuiltInTag` 方法的作用是用来检测你所注册的组件是否是内置的标签,这个方法可以在 [shared/util.js 文件工具方法全解](/note/附录/shared-util) 中查看其实现,于是我们可知:`slot` 和 `component` 这个两个名字被 `Vue` 作为内置标签而存在的,你是不能够使用的,比如这样:
 
@@ -262,10 +266,12 @@ Vue.config.isUnknownElement = isUnknownElement
 Vue.config.isReservedTag = isReservedTag
 ```
 
-就是在给 `config.isReservedTag` 赋值,其值为来自于 `platforms/web/util/element.js` 文件的 `isReservedTag` 函数,大家可以在附录 [platforms/web/util 目录下的工具方法全解](/note/附录/web-util) 中查看该方法的作用及实现,可知在 `Vue` 中 `html` 标签和部分 `SVG` 标签被认为是保留的。所以这段代码是在保证选项被合并前的合理合法。最后大家注意一点,这些工作在非生产环境下做的,所以在非生产环境下开发者就能够发现并修正这些问题,所以在生产环境下就不需要再重复做一次校验检测了。
+就是在给 `config.isReservedTag` 赋值,其值为来自于 `platforms/web/util/element.js` 文件的 `isReservedTag` 函数,大家可以在附录 [platforms/web/util 目录下的工具方法全解](/note/附录/web-util) 中查看该方法的作用及实现,可知在 `Vue` 中 `html` 标签和部分 `SVG` 标签被认为是保留的。所以这段代码是在保证选项被合并前的合理合法。最后大家注意一点,这些工作在非生产环境下做的,所以在非生产环境下开发者就能够发现并修正这些问题,所以在生产环境下就不需要再重复做一次校验检测了。
 
 另外要说一点,我们的例子中并没有使用 `components` 选项,但是这里还是给大家顺便介绍了一下。如果按照我们的例子的话,`mergeOptions` 函数中的很多代码都不会执行,但是为了保证让大家理解整个选项合并所做的事情,这里都会有所介绍。
 
+#### 允许合并另一个实例构造者的选项
+
 我们继续看代码,接下来的一段代码同样是一个 `if` 语句块:
 
 ```js
@@ -276,6 +282,8 @@ if (typeof child === 'function') {
 
 这说明 `child` 参数除了是普通的选项对象外,还可以是一个函数,如果是函数的话就取该函数的 `options` 静态属性作为新的 `child`,我们想一想什么样的函数具有 `options` 静态属性呢?现在我们知道 `Vue` 构造函数本身就拥有这个属性,其实通过 `Vue.extend` 创造出来的子类也是拥有这个属性的。所以这就允许我们在进行选项合并的时候,去合并一个 `Vue` 实例构造者的选项了。
 
+#### 规范化 props(normalizeProps)
+
 接着看代码,接下来是三个用来规范化选项的函数调用:
 
 ```js
@@ -515,6 +523,8 @@ if (Array.isArray(props)) {
 
 在警告中使用了来自 `shared/util.js` 文件的 `toRawType` 方法获取你所传递的 `props` 的真实数据类型。
 
+#### 规范化 inject(normalizeInject)
+
 现在我们已经了解了,原来 `Vue` 底层是这样处理 `props` 选项的,下面我们再来看看第二个规范化函数:`normalizeInject`,源码如下:
 
 ```js
@@ -523,6 +533,7 @@ if (Array.isArray(props)) {
  */
 function normalizeInject (options: Object, vm: ?Component) {
   const inject = options.inject
+  if (!inject) return
   const normalized = options.inject = {}
   if (Array.isArray(inject)) {
     for (let i = 0; i < inject.length; i++) {
@@ -545,14 +556,15 @@ function normalizeInject (options: Object, vm: ?Component) {
 }
 ```
 
-首先是这句代码:
+首先是这句代码:
 
 ```js
 const inject = options.inject
+if (!inject) return
 const normalized = options.inject = {}
 ```
 
-第一句代码使用 `inject` 变量缓存了 `options.inject`,通过这句代码和函数的名字我们能够知道,这个函数是用来规范化 `inject` 选项的。然后在第二句代码中重新了 `options.inject` 的值为一个空的 `JSON` 对象,并定义了一个值同样为空 `JSON` 对象的变量 `normalized`。现在变量 `normalized` 和 `options.inject` 将拥有相同的引用,也就是说当修改 `normalized` 的时候,`options.inject` 也将受到影响。
+第一句代码使用 `inject` 变量缓存了 `options.inject`,通过这句代码和函数的名字我们能够知道,这个函数是用来规范化 `inject` 选项的。第二句代码判断是否传递了 `inject` 选项,如果没有则直接 `return`。然后在第三句代码中重写了 `options.inject` 的值为一个空的 `JSON` 对象,并定义了一个值同样为空 `JSON` 对象的变量 `normalized`。现在变量 `normalized` 和 `options.inject` 将拥有相同的引用,也就是说当修改 `normalized` 的时候,`options.inject` 也将受到影响。
 
 在这两句代码之后,同样是判断分支语句,判断 `inject` 选项是否是数组和纯对象,类似于对 `props` 的判断一样。说到这里我们需要了解一下 `inject` 选项了,这个选项是 `2.2.0` 版本新增,它要配合 `provide` 选项一同使用,具体介绍可以查看官方文档,这里我们举一个简单的例子:
 
@@ -580,7 +592,7 @@ var vm = new Vue({
 })
 ```
 
-上面的代码中,在子组件的 `created` 钩子中我们访问了 `this.data`,但是在子组件中我们并没有定义这个数据,但是我们使用了 `inject` 选项注入了这个数据,这个数据的来源就是父组件通过 `provide` 提供的。父组件通过 `provide` 选项向子组件提供数据,然后子组件中可以使用 `inject` 选项注入数据。这里我们的 `inject` 选项使用一个字符串数组,其实我们也可以写成对象的形式,如下:
+上面的代码中,在子组件的 `created` 钩子中我们访问了 `this.data`,但是在子组件中我们并没有定义这个数据,之所以在没有定义的情况下能够使用,是因为我们使用了 `inject` 选项注入了这个数据,这个数据的来源就是父组件通过 `provide` 提供的。父组件通过 `provide` 选项向子组件提供数据,然后子组件中可以使用 `inject` 选项注入数据。这里我们的 `inject` 选项使用一个字符串数组,其实我们也可以写成对象的形式,如下:
 
 ```js
 // 子组件
@@ -610,7 +622,7 @@ if (Array.isArray(inject)) {
 }
 ```
 
-使用 `for` 循环遍历数组的每一个元素,将元素的值作为 `normalized` 的 `key`,然后将 `{ from: inject[i] }` 作为整个表达式的值。大家不要忘了一件事,那就是 `normalized` 对象和 `options.inject` 拥有相同的引用,所以 `normalized` 的改变就意味着 `options.inject` 的改变。
+使用 `for` 循环遍历数组的每一个元素,将元素的值作为 `key`,然后将 `{ from: inject[i] }` 作为值。大家不要忘了一件事,那就是 `normalized` 对象和 `options.inject` 拥有相同的引用,所以 `normalized` 的改变就意味着 `options.inject` 的改变。
 
 也就是说如果你的 `inject` 选项是这样写的:
 
@@ -647,7 +659,6 @@ if (Array.isArray(inject)) {
 有的同学可能会问:`normalized` 函数的目的不就将 `inject` 选项规范化为对象结构吗?那既然已经是对象了还规范什么呢?那是因为我们期望得到的对象是这样的:
 
 ```js
-// 这里为简写,这应该写在Vue的选项中
 inject: {
   'data1': { from: 'data1' },
   'data2': { from: 'data2' }
@@ -688,7 +699,7 @@ for (const key in inject) {
 }
 ```
 
-使用 `for in` 循环遍历 `inject` 选项,依然使用 `inject` 对象的 `key` 作为 `normalized` 的 `key`,只不过要判断一下值(即 `val`)是否为纯对象,如果是纯对象则使用 `extend` 进行混合,则直接使用 `val` 作为 `from` 字段的值,代码总体还是很简单的。
+使用 `for in` 循环遍历 `inject` 选项,依然使用 `inject` 对象的 `key` 作为 `normalized` 的 `key`,只不过要判断一下值(即 `val`)是否为纯对象,如果是纯对象则使用 `extend` 进行混合,则直接使用 `val` 作为 `from` 字段的值,代码总体还是很简单的。
 
 最后一个判断分支同样是在当你传递的 `inject` 选项既不是数组又不是纯对象的时候,在非生产环境下给你一个警告:
 
@@ -706,6 +717,8 @@ if (Array.isArray(inject)) {
 }
 ```
 
+#### 规范化 directives(normalizeDirectives)
+
 最后一个规范化函数是 `normalizeDirectives`,源码如下:
 
 ```js