Browse Source

根据dev提交更新

HcySunYang 7 years ago
parent
commit
174bc72523
5 changed files with 180 additions and 33 deletions
  1. 1 1
      README.md
  2. 4 4
      note/Vue构造函数.md
  3. 1 1
      note/了解Vue这个项目.md
  4. 3 3
      note/前言.md
  5. 171 24
      note/附录/core-util.md

+ 1 - 1
README.md

@@ -4,7 +4,7 @@
 
 ### 如何阅读
 
-系列文章与 Vue `dev` 分支的源码保持同步,你只需要检出Vue `dev` 分支的代码即可对照阅读。注意:*每天都会查看是否有更新,尽量保证文章的跟新速度与源码持平,偶尔会有延迟,敬请谅解。*
+系列文章与 Vue `dev` 分支的源码保持同步,你只需要检出Vue `dev` 分支的代码即可对照阅读。注意:*每天都会查看是否有更新,尽量保证文章的跟新速度与源码持平,偶尔会有延迟,敬请谅解。*
 
 ### 目录
 

+ 4 - 4
note/Vue构造函数.md

@@ -1,6 +1,6 @@
 ## Vue 构造函数
 
-我们知道,我们在使用 `Vue` 的时候,要使用 `new` 操作符进行调用,这说明 `Vue` 应该是一个构造函数,所以我们做的第一件事就是:把 `Vue` 构造函数搞清楚。
+我们知道,我们在使用 `Vue` 的时候,要使用 `new` 操作符进行调用,这说明 `Vue` 应该是一个构造函数,所以我们做的第一件事就是:把 `Vue` 构造函数搞清楚。
 
 #### Vue 构造函数的原型
 
@@ -104,7 +104,7 @@ renderMixin(Vue)
 export default Vue
 ```
 
-可以看到,这个文件才是 `Vue` 构造函数真正的“出生地”,上面的代码是 `./instance/index.js` 文件中全部的代码,还是比较简短易看的,首先分别从 `./init.js`、`./state.js`、`./render.js`、`./events.js`、`./lifecycle.js` 这五个文件中导五个方法,分别是:`initMixin`、`stateMixin`、`renderMixin`、`eventsMixin` 以及 `lifecycleMixin`,然后定义了 `Vue` 构造函数,其中使用了安全模式来提醒你要使用 `new` 操作符来调用 `Vue`,接着将 `Vue` 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 `Vue`。
+可以看到,这个文件才是 `Vue` 构造函数真正的“出生地”,上面的代码是 `./instance/index.js` 文件中全部的代码,还是比较简短易看的,首先分别从 `./init.js`、`./state.js`、`./render.js`、`./events.js`、`./lifecycle.js` 这五个文件中导五个方法,分别是:`initMixin`、`stateMixin`、`renderMixin`、`eventsMixin` 以及 `lifecycleMixin`,然后定义了 `Vue` 构造函数,其中使用了安全模式来提醒你要使用 `new` 操作符来调用 `Vue`,接着将 `Vue` 构造函数作为参数,分别传递给了导入进来的这五个方法,最后导出 `Vue`。
 
 那么这五个方法又做了什么呢?先看看 `initMixin` ,打开 `./init.js` 文件,找到 `initMixin` 方法,如下:
 
@@ -226,7 +226,7 @@ Vue.prototype._e = createEmptyVNode
 Vue.prototype._u = resolveScopedSlots
 Vue.prototype._g = bindObjectListeners
 ```
-至此,`instance/index.js` 文件中的代码就运行完毕了(注意:所谓的运行,是指执行 `npm run dev` 命令时构建的运行)。我们大概清楚每个 `*Mixin` 方法的作用其实就是包装 `Vue.prototype`,在其上挂载一些属性和方法,下面我们要做一件很重要的事情,就是将上面的内容集中合并起来,放一个单独的地方,便于以后查看,我将它们整理到了这里:[附录/Vue 构造函数整理-原型](./附录/Vue构造函数整理-原型.md),这样当我们在后面详细讲解的时候,提到某个方法你就可以迅速定位它的位置,便于我们思路的清晰。
+至此,`instance/index.js` 文件中的代码就运行完毕了(注意:所谓的运行,是指执行 `npm run dev` 命令时构建的运行)。我们大概清楚每个 `*Mixin` 方法的作用其实就是包装 `Vue.prototype`,在其上挂载一些属性和方法,下面我们要做一件很重要的事情,就是将上面的内容集中合并起来,放一个单独的地方,便于以后查看,我将它们整理到了这里:[附录/Vue 构造函数整理-原型](./附录/Vue构造函数整理-原型.md),这样当我们在后面详细讲解的时候,提到某个方法你就可以迅速定位它的位置,便于我们思路的清晰。
 
 #### Vue 构造函数的静态属性和方法(全局API)
 
@@ -728,7 +728,7 @@ Vue.prototype.$mount = function (
 
 首先在 `Vue.prototype` 上添加 `__patch__` 方法,如果在浏览器环境运行的话,这个方法的值为 `patch` 函数,否则是一个空函数 `noop`。然后又在 `Vue.prototype` 上添加了 `$mount` 方法,我们暂且不关心 `$mount` 方法的内容和作用。
 
-之后的一段代码是 `vue-devtools` 的全局钩子,它被包裹在 `setTimeout` 中。最后导出了 `Vue`。
+之后的一段代码是 `vue-devtools` 的全局钩子,它被包裹在 `Vue.nextTick` 中(对于 `Vue.nextTick` 我们会单独讲到),最后导出了 `Vue`。
 
 现在我们就看完了 `platforms/web/runtime/index.js` 文件,该文件的作用是对 `Vue` 进行平台化的包装:
 

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

@@ -98,7 +98,7 @@
 "module": "dist/vue.runtime.esm.js",
 ```
 
-`main` 和 `module` 指向的都是运行时版的Vue,不同前者是 `cjs` 模块,后者是 `es` 模块。
+`main` 和 `module` 指向的都是运行时版的Vue,不同的是:前者是 `cjs` 模块,后者是 `es` 模块。
 
 其中 `main` 字段和 `module` 字段分别用于 `browserify 或 webpack 1` 和 `webpack2+ 或 Rollup`,后者可以直接加载 `ES Module` 且会根据 `module` 字段的配置进行加载,关于 `module` 可以参考这里:[https://github.com/rollup/rollup/wiki/pkg.module](https://github.com/rollup/rollup/wiki/pkg.module)。
 

+ 3 - 3
note/前言.md

@@ -4,7 +4,7 @@
 
 #### 你应该了解的
 
-文章将会尽可能详细,且尽可能对基础的知识点进行讲解,但需要太多口舌的东西即使再基础也不会去讲,这里列出我希望在阅读这些文章前你最好了解的东西:
+文章将会尽可能详细,且尽可能对基础的知识点进行讲解,但需要太多口舌的东西即使再基础也不会去讲,这里列出我希望你在阅读该系列文章前最好了解的东西:
 
 * ES6+
 * node & npm & package.json
@@ -14,9 +14,9 @@
 * flow(类型检查)
 	* [flow](https://flow.org/en/)
 
-因为 Vue 的源码采用 ES6,所以你至少应该掌握 ES6 才能看得懂,其次你最好对 `package.json` 中的字段的作用有所了解。由于 Vue 使用 `Rollup` 构建,所以你不了解 `Rollup` 的话,你就看不懂 Vue 的构建配置,最后 Vue 采用 `flow` 做类型系统,最起码就应该知道 `flow` 的简单语法,否则会影响你看源码。
+由于 Vue 的源码采用 ES6,所以你至少应该掌握 ES6 才能看得懂,其次你最好对 `package.json` 中的字段的作用有所了解。由于 Vue 使用 `Rollup` 构建,所以你不了解 `Rollup` 的话,你就看不懂 Vue 的构建配置,最后 Vue 采用 `flow` 做类型系统,最起码就应该知道 `flow` 的简单语法,否则会影响你看源码。
 
 #### 推荐阅读这套文章的方式
 
-既然是阅读源码,没有源码怎么读?所以你要使用你喜欢的方式拿到源码才行,最简单的方式是,clone 一份源码到你的本地。如果你不想这么做,你可以安装一个 `chrome` 的扩展程序,使得你以在线以资源管理器的方式阅读GitHub仓库的代码,我常用的 `chrome` 扩展是:[octotree](https://github.com/buunguyen/octotree),类似的扩展还有很多,你喜欢就好。
+既然是阅读源码,没有源码怎么读?所以你要使用你喜欢的方式拿到源码才行,最简单的方式是,clone 一份源码到你的本地。如果你不想这么做,你可以安装一个 `chrome` 的扩展程序,使得你能够以在线以资源管理器的方式阅读GitHub仓库的代码,我常用的 `chrome` 扩展是:[octotree](https://github.com/buunguyen/octotree),类似的扩展还有很多,你喜欢就好。
 

+ 171 - 24
note/附录/core-util.md

@@ -69,7 +69,15 @@ console.log(classify('aaa-bbb-ccc')) // AaaBbbCcc
 
 #### error.js 文件代码说明
 
-该文件导出一个函数:`handleError`
+该文件只导出一个函数:`handleError`,在看这个函数的实现之前,我们需要回顾一下 `Vue` 的文档,我们知道 `Vue` 提供了一个全局配置 `errorHandler`,用来捕获组件生命周期函数等的内部错误,使用方法如下:
+
+```js
+Vue.config.errorHandler = function (err, vm, info) {
+  // ...
+}
+```
+
+我们通过设置 `Vue.config.errorHandler` 为一个函数,实现对特定错误的捕获。具体使用可以查看官方文档。而接下来要讲的 `handleError` 函数就是用来实现 `Vue.config.errorHandler` 这一配置功能的,我们看看是怎么做的。
 
 ##### handleError
 
@@ -77,19 +85,20 @@ console.log(classify('aaa-bbb-ccc')) // AaaBbbCcc
 
 ```js
 export function handleError (err: Error, vm: any, info: string) {
-  if (config.errorHandler) {
-    config.errorHandler.call(null, err, vm, info)
-  } else {
-    if (process.env.NODE_ENV !== 'production') {
-      warn(`Error in ${info}: "${err.toString()}"`, vm)
-    }
-    /* istanbul ignore else */
-    if (inBrowser && typeof console !== 'undefined') {
-      console.error(err)
-    } else {
-      throw err
+  if (vm) {
+    let cur = vm
+    while ((cur = cur.$parent)) {
+      if (cur.$options.errorCaptured) {
+        try {
+          const propagate = cur.$options.errorCaptured.call(cur, err, vm, info)
+          if (!propagate) return
+        } catch (e) {
+          globalHandleError(e, cur, 'errorCaptured hook')
+        }
+      }
     }
   }
+  globalHandleError(err, vm, info)
 }
 ```
 
@@ -105,42 +114,180 @@ try {
 }
 ```
     * `{any} vm` 这里应该传递 `Vue` 实例
-    * `{String} info` Vue 特定的错误提示信息
+    * `{String} info` `Vue` 特定的错误提示信息
 
 * 源码分析
 
-首先检测是否定义 `config.errorHandler`,其中 `config` 为全局配置,来自于 `core/config.js`。如果发现 `config.errorHandler` 为真,就会执行这句
+首先迎合一下使用场景,在 `Vue` 的源码中 `handleError` 函数的使用一般如下
 
 ```js
-config.errorHandler.call(null, err, vm, info)
+try {
+  handlers[i].call(vm)
+} catch (e) {
+  handleError(e, vm, `${hook} hook`)
+}
 ```
 
-所以你尽管通过 `Vue.config` 修改 `errorHandler` 的定义即可自定义错误处理错误的方式:
+上面是声明周期钩子回调执行时的代码,由于声明周期钩子是开发者自定义的函数,这个函数的执行是很可能存在运行时错误的,所以这里需要 `try catch` 包裹,且在发生错误的时候,在 `catch` 语句块中捕获错误,然后使用 `handleError` 进行错误处理。知道了这些,我们再看看 `handleError` 到底怎么处理的,源码上面已经贴出来了,首先是一个 `if` 判断
 
 ```js
-Vue.config.errorHandler = (err, vm, info) => {
-
+if (vm) {
+  let cur = vm
+  while ((cur = cur.$parent)) {
+    if (cur.$options.errorCaptured) {
+      try {
+        const propagate = cur.$options.errorCaptured.call(cur, err, vm, info)
+        if (!propagate) return
+      } catch (e) {
+        globalHandleError(e, cur, 'errorCaptured hook')
+      }
+    }
+  }
 }
 ```
 
-如果 `config.errorHandler` 为假,那么程序将走 `else` 分支,可以理解为默认的错误处理方式,如果不是生产环境,首先提示一段文字信息:
+那这段代码是干嘛的呢?我们先不管,回头来说。在判断语句后面直接调用了 `globalHandleError` 函数,且将三个参数透传了过去
 
 ```js
-if (process.env.NODE_ENV !== 'production') {
-    warn(`Error in ${info}: "${err.toString()}"`, vm)
+globalHandleError(err, vm, info)
+```
+
+`globalHandleError` 函数就定义在 `handleError` 函数的下面,源码如下:
+
+```js
+function globalHandleError (err, vm, info) {
+  if (config.errorHandler) {
+    try {
+      return config.errorHandler.call(null, err, vm, info)
+    } catch (e) {
+      logError(e, null, 'config.errorHandler')
+    }
+  }
+  logError(err, vm, info)
 }
 ```
 
-然后判断是否是浏览器环境,是的话使用 `console.error` 打印错误信息,否则直接 `throw err`:
+`globalHandleError` 函数首先判断 `config.errorHandler` 是否为真,如果为真则调用 `config.errorHandler` 并将参数透传,这里的 `config.errorHandler` 就是 `Vue` 全局API提供的用于自定义错误处理的配置我们前面讲过。由于这个错误处理函数也是开发者自定义的,所以可能出现运行时错误,这个时候就需要使用 `try catch` 语句块包裹起来,当错误发生时,使用 `logError` 函数打印错误,当然啦,如果你们有配置 `config.errorHandler` 也就是说 `config.errorHandler` 此时为假,那么将使用默认的错误处理函数,也就是 `logError` 进行错误处理。
+
+所以 `globalHandleError` 是用来检测你是否自定义了 `config.errorHandler` 的,如果有则用之,如果没有就是用 `logError`。
+
+那么 `logError` 是什么呢?这个函数定义在 `globalHandleError` 函数的下面,源码如下:
 
 ```js
-if (inBrowser && typeof console !== 'undefined') {
+function logError (err, vm, info) {
+  if (process.env.NODE_ENV !== 'production') {
+    warn(`Error in ${info}: "${err.toString()}"`, vm)
+  }
+  /* istanbul ignore else */
+  if (inBrowser && typeof console !== 'undefined') {
     console.error(err)
-} else {
+  } else {
     throw err
+  }
+}
+```
+
+可以看到,在非生产环境下,先使用 `warn` 函数报一个警告,然后判断是否在浏览器环境且 `console` 是否可用,如果可用则使用 `console.error` 打印错误,没有则直接 `throw err`。
+
+所以 `logError` 才真正打印错误的函数,且实现也比较简单。这其实已经达到了 `handleError` 的目的了,但是大家注意我们此时忽略了一段代码,就是 `handleError` 函数开头的一段代码:
+
+```js
+if (vm) {
+  let cur = vm
+  while ((cur = cur.$parent)) {
+    if (cur.$options.errorCaptured) {
+      try {
+        const propagate = cur.$options.errorCaptured.call(cur, err, vm, info)
+        if (!propagate) return
+      } catch (e) {
+        globalHandleError(e, cur, 'errorCaptured hook')
+      }
+    }
+  }
 }
 ```
 
+那么这个 `if` 判断是干嘛的呢?这其实是在支持新的 `Vue` 选项 `errorCaptured`。在编写该文章的时候 `Vue` 的文档还没有跟新,实际上你可以这样写代码:
+
+```js
+var vm = new Vue({
+  errorCaptured: function (err, vm, info) {
+    console.log(err)
+    console.log(vm)
+    console.log(info)
+  }
+})
+```
+
+`errorCaptured` 选项可以用来捕获子代组件的错误,当子组件有错误被 `handleError` 函数处理时,父组件可以通过该选项捕获错误。这个选项与生命周期钩子并列。
+
+举一个例子,如下代码:
+
+```js
+var ChildComponent = {
+  template: '<div>child component</div>',
+  beforeCreate: function () {
+    JSON.parse("};")
+  }
+}
+
+var vm = new Vue({
+  components: {
+    ChildComponent
+  },
+  errorCaptured: function (err, vm, info) {
+    console.log(err)
+    console.log(vm)
+    console.log(info)
+  }
+})
+```
+
+上面的代码中,首先我们定义了一个子组件 `ChildComponent`,并且在 `ChildComponent` 的 `beforeCreate` 钩子中写了如下代码:
+
+```js
+JSON.parse("};")
+```
+
+这明显会报错嘛,然后我们在父组件中使用了 `errorCaptured` 选项,这样是可以捕获到错误的。
+
+接下来我们就看看 `Vue` 是怎么实现的,原理就在这段代码中:
+
+```js
+if (vm) {
+  let cur = vm
+  while ((cur = cur.$parent)) {
+    if (cur.$options.errorCaptured) {
+      try {
+        const propagate = cur.$options.errorCaptured.call(cur, err, vm, info)
+        if (!propagate) return
+      } catch (e) {
+        globalHandleError(e, cur, 'errorCaptured hook')
+      }
+    }
+  }
+}
+```
+
+首先看这个 `while` 循环:
+
+```js
+while ((cur = cur.$parent))
+```
+
+这是一个链表遍历嘛,逐层寻找父级组件,如果父级组件使用了 `errorCaptured` 选项,则调用之,就怎么简单。当然,调用 `errorCaptured` 的语句是被包裹在 `try catch` 语句块中的。
+
+这里有两点需要注意:
+
+* 第一、既然是逐层寻找父级,那意味着,如果一个子组件报错,那么其使用了 `errorCaptured` 的所有父代组件都可以捕获得到。
+* 第二、注意这句话:
+
+```js
+if (!propagate) return
+```
+
+其中 `propagate` 是 `errorCaptured` 的返回值,也就是说,如果 `errorCaptured` 函数什么都不反回或者返回假,那么直接 `return`,程序不会走 `if` 语句块后面的 `globalHandleError`,如果 `errorCaptured` 函数返回真,那么除了 `errorCaptured` 被调用外,`if` 语句块后面的 `globalHandleError` 也会被调用。
+
 #### lang.js 文件代码说明
 
 ##### emptyObject