-
Notifications
You must be signed in to change notification settings - Fork 472
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Vue 源码解析:深入响应式原理 #7
Comments
very good |
这是基于vue1.0分析的? |
感觉compiler那一块可以讲的简单一点。 |
这篇关于vue1+的响应式原理源码分析。现在还有参考意义吗? |
基于1+的 |
1.x 的分析,2.x 的分析参考 https://github.com/ustbhuangyi/vue-analysis |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Vue.js 最显著的功能就是响应式系统,它是一个典型的 MVVM 框架,模型(Model)只是普通的 JavaScript 对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖 Vue.js 响应式系统的细节,来看一看 Vue.js 是如何把模型和视图建立起关联关系的。
如何追踪变化
我们先来看一个简单的例子。代码示例如下:
运行后,我们可以从页面中看到,count 后面的 times 每隔 1s 递增 1,视图一直在更新。在代码中仅仅是通过 setInterval 方法每隔 1s 来修改 vm.times 的值,并没有任何 DOM 操作。那么 Vue.js 是如何实现这个过程的呢?我们可以通过一张图来看一下,如下图所示:
图中的模型(Model)就是 data 方法返回的{times:1},视图(View)是最终在浏览器中显示的DOM。模型通过Observer、Dep、Watcher、Directive等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js在这里主要做了三件事:
接下来我们就结合 Vue.js 的源码来详细介绍这三个过程。
Observer
首先来看一下 Vue.js 是如何给 data 对象添加 Observer 的。我们知道,Vue 实例创建的过程会有一个生命周期,其中有一个过程就是调用 vm.initData 方法处理 data 选项。initData 方法的源码定义如下:
在 initData 中我们要特别注意 proxy 方法,它的功能就是遍历 data 的 key,把 data 上的属性代理到 vm 实例上。_proxy 方法的源码定义如下:
proxy 方法主要通过 Object.defineProperty 的 getter 和 setter 方法实现了代理。在前面的例子中,我们调用 vm.times 就相当于访问了 vm.data.times。
在 _initData 方法的最后,我们调用了 observe(data, this) 方法来对 data 做监听。observe 方法的源码定义如下:
observe 方法首先判断 value 是否已经添加了 ob 属性,它是一个 Observer 对象的实例。如果是就直接用,否则在 value 满足一些条件(数组或对象、可扩展、非 vue 组件等)的情况下创建一个 Observer 对象。接下来我们看一下 Observer 这个类,它的源码定义如下:
Observer 类的构造函数主要做了这么几件事:首先创建了一个 Dep 对象实例(关于 Dep 对象我们稍后作介绍);然后把自身 this 添加到 value 的 ob 属性上;最后对 value 的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实 observeArray 方法就是对数组进行遍历,递归调用 observe 方法,最终都会调用 walk 方法观察单个元素。接下来我们看一下 walk 方法,它的源码定义如下:
walk 方法是对 obj 的 key 进行遍历,依次调用 convert 方法,对 obj 的每一个属性进行转换,让它们拥有 getter、setter 方法。只有当 obj 是一个对象时,这个方法才能被调用。接下来我们看一下 convert 方法,它的源码定义如下:
convert 方法很简单,它调用了 defineReactive 方法。这里 this.value 就是要观察的 data 对象,key 是 data 对象的某个属性,val 则是这个属性的值。defineReactive 的功能是把要观察的 data 对象的每个属性都赋予 getter 和 setter 方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下 defineReactive 方法,它的源码定义如下:
defineReactive 方法最核心的部分就是通过调用 Object.defineProperty 给 data 的每个属性添加 getter 和setter 方法。当 data 的某个属性被访问时,则会调用 getter 方法,判断当 Dep.target 不为空时调用 dep.depend 和 childObj.dep.depend 方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变 data 的属性时,则会调用 setter 方法,这时调用 dep.notify 方法进行通知。这里我们提到了 dep,它是 Dep 对象的实例。接下来我们看一下 Dep 这个类,它的源码定义如下:
Dep 类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了 id 和 subs。其中 subs 用来存储所有订阅它的 Watcher,Watcher 的实现稍后我们会介绍。Dep.target 表示当前正在计算的 Watcher,它是全局唯一的,因为在同一时间只能有一个 Watcher 被计算。
前面提到了在 getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify 方法,接下来依次介绍这两个方法。depend 方法的源码定义如下:
depend 方法很简单,它通过 Dep.target.addDep(this) 方法把当前 Dep 的实例添加到当前正在计算的Watcher 的依赖中。接下来我们看一下 notify 方法,它的源码定义如下:
notify 方法也很简单,它遍历了所有的订阅 Watcher,调用它们的 update 方法。
至此,vm 实例中给 data 对象添加 Observer 的过程就结束了。接下来我们看一下 Vue.js 是如何进行指令解析的。
Directive
Vue 指令类型很多,限于篇幅,我们不会把所有指令的解析过程都介绍一遍,这里结合前面的例子只介绍 v-text 指令的解析过程,其他指令的解析过程也大同小异。
前面我们提到了 Vue 实例创建的生命周期,在给 data 添加 Observer 之后,有一个过程是调用 vm.compile 方法对模板进行编译。compile 方法的源码定义如下:
我们可以通过下图来看一下这个方法编译的主要流程:
这个过程通过 el = transclude(el, option) 方法把 template 编译成一段 document fragment,拿到 el 对象。而指令解析部分就是通过 compile(el, options) 方法实现的。接下来我们看一下 compile 方法的实现,它的源码定义如下:
compile 方法主要通过 compileNode(el, options) 方法完成节点的解析,如果节点拥有子节点,则调用 compileNodeList(el.childNodes, options) 方法完成子节点的解析。compileNodeList 方法其实就是遍历子节点,递归调用 compileNode 方法。因为 DOM 元素本身就是树结构,这种递归方法也就是常见的树的深度遍历方法,这样就可以完成整个 DOM 树节点的解析。接下来我们看一下 compileNode 方法的实现,它的源码定义如下:
compileNode 方法对节点的 nodeType 做判断,如果是一个非 script 普通的元素(div、p等);则调用 compileElement(node, options) 方法解析;如果是一个非空的文本节点,则调用 compileTextNode(node, options) 方法解析。我们在前面的例子中解析的是非空文本节点 count: {{times}},这实际上是 v-text 指令,它的解析是通过 compileTextNode 方法实现的。接下来我们看一下 compileTextNode 方法,它的源码定义如下:
compileTextNode 方法首先调用了 parseText 方法对 node.wholeText 做解析。主要通过正则表达式解析 count: {{times}} 部分,我们看一下解析结果,如下图所示:
解析后的 tokens 是一个数组,数组的每个元素则是一个 Object。如果是 count: 这样的普通文本,则返回的对象只有 value 字段;如果是 {{times}} 这样的插值,则返回的对象包含 html、onTime、tag、value 等字段。
接下来创建 document fragment,遍历 tokens 创建 DOM 节点插入到这个 fragment 中。在遍历过程中,如果 token 无 tag 字段,则调用 document.createTextNode(token.value) 方法创建 DOM 节点;否则调用processTextToken(token, options) 方法创建 DOM 节点和扩展 token 对象。我们看一下调用后的结果,如下图所示:
可以看到,token 字段多了一个 descriptor 属性。这个属性包含了几个字段,其中 def 表示指令相关操作的对象,expression 为解析后的表达式,filters 为过滤器,name 为指令的名称。
在compileTextNode 方法的最后,调用 makeTextNodeLinkFn(tokens, frag, options) 并返回该方法执行的结果。接下来我们看一下 makeTextNodeLinkFn 方法,它的源码定义如下:
makeTextNodeLinkFn 这个方法什么也没做,它仅仅是返回了一个新的方法 textNodeLinkFn。往前回溯,这个方法最终作为 compileNode 的返回值,被添加到 compile 方法生成的 childLinkFn 中。
我们回到 compile 方法,在 compile 方法的最后有这样一段代码:
compile 方法返回了 compositeLinkFn,它在 Vue.prototype._compile 方法执行时,是通过 compile(el, options)(this, el) 调用的。compositeLinkFn 方法执行了 linkAndCapture 方法,它的功能是通过调用 compile 过程中生成的 link 方法创建指令对象,再对指令对象做一些绑定操作。linkAndCapture 方法的源码定义如下:
linkAndCapture 方法首先调用了 linker 方法,它会遍历 compile 过程中生成的所有 linkFn 并调用,本例中会调用到之前定义的 textNodeLinkFn。这个方法会遍历 tokens,判断如果 token 的 tag 属性值为 true 且 oneTime 属性值为 false,则调用 vm.bindDir(token.descriptor, node, host, scope) 方法创建指令对象。 vm._bindDir 方法的源码定义如下:
Vue.prototype._bindDir 方法就是根据 descriptor 实例化不同的 Directive 对象,并添加到 vm 实例 directives 数组中的。到这一步,Vue.js 从解析模板到生成 Directive 对象的步骤就完成了。接下来回到 linkAndCapture 方法,它对创建好的 directives 进行排序,然后遍历 directives 调用 dirs[i]._bind 方法对单个directive做一些绑定操作。dirs[i]._bind方法的源码定义如下:
Directive.prototype._bind 方法的主要功能就是做一些指令的初始化操作,如混合 def 属性。def 是通过 this.descriptor.def 获得的,this.descriptor 是对指令进行相关描述的对象,而 this.descriptor.def 则是包含指令相关操作的对象。比如对于 v-text 指令,我们可以看一下它的相关操作,源码定义如下:
v-text 的 def 包含了 bind 和 update 方法,Directive 在初始化时通过 extend(this, def) 方法可以对实例扩展这两个方法。Directive 在初始化时还定义了 this.update 方法,并创建了 Watcher,把 this.update 方法作为 Watcher 的回调函数。这里把 Directive 和 Watcher 做了关联,当 Watcher 观察到指令表达式值变化时,会调用 Directive 实例的 _update 方法,最终调用 v-text 的 update 方法更新 DOM 节点。
至此,vm 实例中编译模板、解析指令、绑定 Watcher 的过程就结束了。接下来我们看一下 Watcher 的实现,了解 Directive 和 Observer 之间是如何通过 Watcher 关联的。
Watcher
我们先来看一下 Watcher 类的实现,它的源码定义如下:
Directive 实例在初始化 Watche r时,会传入指令的 expression。Watcher 构造函数会通过 parseExpression(expOrFn, this.twoWay) 方法对 expression 做进一步的解析。在前面的例子中, expression 是times,passExpression 方法的功能是把 expression 转换成一个对象,如下图所示:
可以看到 res 有两个属性,其中 exp 为表达式字符串;get 是通过 new Function 生成的匿名方法,可以把它打印出来,如下图所示:
可以看到 res.get 方法很简单,它接受传入一个 scope 变量,返回 scope.times。对于传入的 scope 值,稍后我们会进行介绍。在 Watcher 构造函数的最后调用了 this.get 方法,它的源码定义如下:
Watcher.prototype.get 方法的功能就是对当前 Watcher 进行求值,收集依赖关系。它首先执行 this.beforeGet 方法,源码定义如下:
Watcher.prototype.beforeGet 很简单,设置 Dep.target 为当前 Watcher 实例,为接下来的依赖收集做准备。我们回到 get 方法,接下来执行 this.getter.call(scope, scope) 方法,这里的 scope 是 this.vm,也就是当前 Vue 实例。这个方法实际上相当于获取 vm.times,这样就触发了对象的 getter。在第一小节我们给 data 添加 Observer 时,通过 Object.defineProperty 给 data 对象的每一个属性添加 getter 和 setter。回顾一下代码:
当获取 vm.times 时,会执行到 get 方法体内。由于我们在之前已经设置了 Dep.target 为当前 Watcher 实例,所以接下来就调用 dep.depend() 方法完成依赖收集。它实际上是执行了 Dep.target.addDep(this),相当于执行了 Watcher 实例的 addDep 方法,把 Dep 实例添加到 Watcher 实例的依赖中。addDep 方法的源码定义如下:
Watcher.prototype.addDep 方法就是把 dep 添加到 Watcher 实例的依赖中,同时又通过 dep.addSub(this) 把 Watcher 实例添加到 dep 的订阅者中。addSub 方法的源码定义如下:
至此,指令完成了依赖收集,并且通过 Watcher 完成了对数据变化的订阅。
接下来我们看一下,当 data 发生变化时,视图是如何自动更新的。在前面的例子中,我们通过 setInterval 每隔 1s 执行一次 vm.times++,数据改变会触发对象的 setter,执行 set 方法体的代码。回顾一下代码:
这里会调用 dep.notify() 方法,它会遍历所有的订阅者,也就是 Watcher 实例。然后调用 Watcher 实例的 update 方法,源码定义如下:
Watcher.prototype.update 方法在满足某些条件下会直接调用 this.run 方法。在多数情况下会调用 pushWatcher(this) 方法把 Watcher 实例推入队列中,延迟 this.run 调用的时机。pushWatcher 方法的源码定义如下:
pushWatcher 方法把 Watcher 推入队列中,通过 nextTick 方法在下一个事件循环周期处理 Watcher 队列,这是 Vue.j s的一种性能优化手段。因为如果同时观察的数据多次变化,比如同步执行 3 次 vm.time++,同步调用 watcher.run 就会触发 3 次 DOM 操作。而推入队列中等待下一个事件循环周期再操作队列里的 Watcher,因为是同一个 Watcher,它只会调用一次 watcher.run,从而只触发一次 DOM 操作。接下来我们看一下 flushBatcherQueue 方法,它的源码定义如下:
flushBatcherQueue 方法通过调用 runBatcherQueue 来 run Watcher。这里我们看到 Watcher 队列分为内部 queue 和 userQueue,其中 userQueue 是通过 $watch() 方法注册的 Watcher。我们优先 run 内部queue 来保证指令和 DOM 节点优先更新,这样当用户自定义的 Watcher 的回调函数触发时 DOM 已更新完毕。接下来我们看一下 runBatcherQueue 方法,它的源码定义如下:
runBatcherQueued 的功能就是遍历 queue 中 Watcher 的 run 方法。接下来我们看一下 Watcher 的 run 方法,它的源码定义如下:
Watcher.prototype.run 方法再次对 Watcher 求值,重新收集依赖。接下来判断求值结果和之前 value 的关系。如果不变则什么也不做,如果变了则调用 this.cb.call(this.vm, value, oldValue) 方法。这个方法是 Directive 实例创建 Watcher 时传入的,它对应相关指令的 update 方法来真实更新 DOM。这样就完成了数据更新到对应视图的变化过程。 Watcher 巧妙地把 Observer 和 Directive 关联起来,实现了数据一旦更新,视图就会自动变化的效果。尽管 Vue.js 利用 Object.defineProperty 这个核心技术实现了数据和视图的绑定,但仍然会存在一些数据变化检测不到的问题,接下来我们看一下这部分内容。
The text was updated successfully, but these errors were encountered: