vue2仓库地址:https://github.com/vuejs/vue
参考链接:
1、《剖析 Vue.js 内部运行机制》
2、《Vue.js 源码解析》

声明:此文章为阅读笔记,包含自己理解和注释,并对原内容进行了删减,学习请移步《剖析 Vue.js 内部运行机制》购买,支持正版。

一、vue运行机制概览

1、全局概览图

image.png

2、初始化及挂载

image.png
在 new Vue() 之后。 Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期、事件、 props、 methods、 data、 computed 与 watch 等。其中最重要的是通过 Object.defineProperty 设置 setter 与 getter 函数,用来实现「响应式」以及「依赖收集」,后面会详细讲到,这里只要有一个印象即可。
初始化之后调用 $mount 会挂载组件,如果是运行时编译,即不存在 render function 但是存在 template 的情况,需要进行「编译」步骤。

3、编译

compile编译可以分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
image.png
parse —[pɑːrs] 作语法分析
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。

optimize —[ˈɑːptɪmaɪz] 优化
optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

generate — [ˈdʒenəreɪt] 生成
generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。
在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

4、响应式

接下来也就是 Vue.js 响应式核心部分。
image.png
这里的 getter 跟 setter 已经在之前介绍过了,在 init 的时候通过 Object.defineProperty 进行了绑定,它使得当被设置的对象被读取的时候会执行 getter 函数,而在当被赋值的时候会执行 setter 函数。

当 render function 被渲染的时候,因为会读取所需对象的值,所以会触发 getter 函数进行「依赖收集」,「依赖收集」的目的是将观察者 Watcher 对象存放到当前闭包中的订阅者 Dep 的 subs 中。形成如下所示的这样一个关系。
image.png
在修改对象的值的时候,会触发对应的 setter, setter 通知之前「依赖收集」得到的 Dep 中的每一个 Watcher,告诉它们自己的值改变了,需要重新渲染视图。这时候这些 Watcher 就会开始调用 update 来更新视图,当然这中间还有一个 patch 的过程以及使用队列来异步更新的策略,这个我们后面再讲。

5、Virtual DOM

我们知道,render function 会被转化成 VNode 节点。Virtual DOM 其实就是一棵以 JavaScript 对象( VNode 节点)作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM 的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。由于 Virtual DOM 是以 JavaScript 对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
比如说下面这样一个例子:

  1. {
  2. tag: 'div', /*说明这是一个div标签*/
  3. children: [ /*存放该标签的子节点*/
  4. {
  5. tag: 'a', /*说明这是一个a标签*/
  6. text: 'click me' /*标签的内容*/
  7. }
  8. ]
  9. }

渲染后可以得到

  1. <div>
  2. <a>click me</a>
  3. </div>

这只是一个简单的例子,实际上的节点有更多的属性来标志节点,比如 isStatic (代表是否为静态节点)、 isComment (代表是否为注释节点)等。

6、更新视图

image.png
前面我们说到,在修改一个对象值的时候,会通过 setter -> Watcher -> update 的流程来修改对应的视图,那么最终是如何更新视图的呢?
当数据变化后,执行 render function 就可以得到一个新的 VNode 节点,我们如果想要得到新的视图,最简单粗暴的方法就是直接解析这个新的 VNode 节点,然后用 innerHTML 直接全部渲染到真实 DOM 中。但是其实我们只对其中的一小块内容进行了修改,这样做似乎有些「浪费」。
那么我们为什么不能只修改那些「改变了的地方」呢?这个时候就要介绍我们的「patch」了。我们会将新的 VNode 与旧的 VNode 一起传入 patch 进行比较,经过 diff 算法得出它们的「差异」。最后我们只需要将这些「差异」的对应 DOM 进行修改即可。

二、响应式系统的基本原理

Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」。尽管我们在使用 Vue.js 进行开发时不会直接修改「响应式系统」,但是理解它的实现有助于避开一些常见的「」,也有助于在遇见一些琢磨不透的问题时可以深入其原理来解决它。

  1. //更新视图方法
  2. function cb (val) {
  3. /* 渲染视图 */
  4. console.log("视图更新啦~");
  5. }
  6. //对象的响应式化
  7. function defineReactive (obj, key, val) {
  8. Object.defineProperty(obj, key, {
  9. enumerable: true, /* 属性可枚举 */
  10. configurable: true, /* 属性可被修改或删除 */
  11. get: function reactiveGetter () {
  12. return val; /* 实际上会依赖收集,下一小节会讲 */
  13. },
  14. set: function reactiveSetter (newVal) {
  15. if (newVal === val) return;
  16. cb(newVal);
  17. }
  18. });
  19. }
  20. //观察者,这个函数传入一个 value(需要「响应式」化的对象)
  21. //通过遍历所有属性的方式对该对象的每一个属性都通过 defineReactive 处理。
  22. function observer (value) {
  23. if (!value || (typeof value !== 'object')) {
  24. return;
  25. }
  26. Object.keys(value).forEach((key) => {
  27. defineReactive(value, key, value[key]);
  28. });
  29. }
  30. //用observer封装 Vue
  31. //在 Vue 的构造函数中,对 options 的 data 进行处理,
  32. // 这里的 data 就是平时我们在写 Vue 项目时组件中的 data 属性(实际上是一个函数,这里当作一个对象来简单处理)。
  33. class Vue{
  34. constructor(options){
  35. this._data = options.data
  36. observer(this._data)
  37. }
  38. }
  39. //调用
  40. let o = new Vue({
  41. data: {
  42. test: "I am test."
  43. }
  44. });
  45. o._data.test = "hello,world."; /* 视图更新啦~ */

三、响应式系统的依赖收集追踪原理

1、订阅者

  1. //订阅者Dep,它的主要作用是用来存放 Watcher 观察者对象。
  2. class Dep {
  3. constructor () {
  4. /* 用来存放Watcher对象的数组 */
  5. this.subs = [];
  6. }
  7. /* 在subs中添加一个Watcher观察者对象 */
  8. addSub (sub) {
  9. this.subs.push(sub);
  10. }
  11. /* 通知所有Watcher对象更新视图 */
  12. notify () {
  13. this.subs.forEach((sub) => {
  14. sub.update();
  15. })
  16. }
  17. }

2、观察者

  1. //观察者 Watcher
  2. class Watcher {
  3. constructor () {
  4. /* 在new一个Watcher对象时将该对象赋值给Dep.target,在get中会用到 */
  5. Dep.target = this;
  6. }
  7. /* 更新视图的方法 */
  8. update () {
  9. console.log("视图更新啦~");
  10. }
  11. }
  12. Dep.target = null;

3、依赖收集
重写对象的响应式化defineReactive 以及 Vue 的构造函数

  1. function defineReactive (obj, key, val) {
  2. /* 一个Dep类对象 */
  3. const dep = new Dep();
  4. Object.defineProperty(obj, key, {
  5. enumerable: true,
  6. configurable: true,
  7. get: function reactiveGetter () {
  8. /* 将Dep.target(即当前的Watcher对象存入dep的subs中) */
  9. dep.addSub(Dep.target);
  10. return val;
  11. },
  12. set: function reactiveSetter (newVal) {
  13. if (newVal === val) return;
  14. /* 在set的时候触发dep的notify来通知所有的Watcher对象更新视图 */
  15. dep.notify();
  16. }
  17. });
  18. }

4、总结
首先在 observer 的过程中会注册 get 方法,该方法用来进行「依赖收集」。在它的闭包中会有一个 Dep 对象,这个对象用来存放 Watcher 对象的实例。其实「依赖收集」的过程就是把 Watcher 实例存放到对应的 Dep 对象中去。get 方法可以让当前的 Watcher 对象(Dep.target)存放到它的 subs 中(addSub)方法,在数据变化时,set 会调用 Dep 对象的 notify 方法通知它内部所有的 Watcher 对象进行视图更新。
这是 Object.defineProperty 的 set/get 方法处理的事情,那么「依赖收集」的前提条件还有两个:

  1. 触发 get 方法;
  2. 新建一个 Watcher 对象。

这个我们在 Vue 的构造类中处理。新建一个 Watcher 对象只需要 new 出来,这时候 Dep.target 已经指向了这个 new 出来的 Watcher 对象来。而触发 get 方法也很简单,实际上只要把 render function 进行渲染,那么其中的依赖的对象都会被「读取」,这里我们通过打印来模拟这个过程,读取 test 来触发 get 进行「依赖收集」。
本章我们介绍了「依赖收集」的过程,配合之前的响应式原理,已经把整个「响应式系统」介绍完毕了。其主要就是 get 进行「依赖收集」。set 通过观察者来更新视图,配合下图仔细捋一捋,相信一定能搞懂它!
image.png