本系列文章基于
github上Vue代码库的dev分支最新代码,package.json中描述的版本号为2.5.17-beta.0
目录结构
我们先看看vue的git库中的目录结构:
-scripts
-dist
-flow
-packages
-test
-src
--compiler
--core
--platforms
--server
--sfc
--shared
-
scripts构建相关脚本和配置文件 -
dist构建结果 -
flowFlow 需要用到的类型声明(Flow可以为JavaScript做静态类型检查), 不过vue正在往TypeScript上转,估计Vue3里面就没有这个目录了。 -
packages包含vue-server-renderer和vue-template-compiler, 会单独发布成npm里的package。 -
src自然就是Vue的源码了,代码是基于ES6和Flow的。shared目录下有两个文件, 分别为constants和util, 分别是一些常量和工具方法,可以先忽略。src目录中是用来编译但文件(*.vue)组件的逻辑.在vue-template-compiler中会用到.server目录中是用于服务端渲染的代码platforms中是跟平台有关的代码,每个平台分别有compiler,runtime,server三部分, 目前有web和weex两种core包含通用的跟平台无关的代码,里面又分为observer包含响应式系统的代码vdom包含虚拟dom相关的代码instance包含Vue实例的构造函数和原型方法global-api全局apicomponents通用抽象组件,目前只有keep-alive
compiler包含将template编译成render函数的代码
响应式系统
估计vue最深入人心的就是它的响应式系统了吧,那我们就先来看看这一部分。
src/observer中的目录结构如下:
-array.js
-dep.js
-index.js
-scheduler.js
-traverse.js
-watcher.js
index.js中有个Observer类,watcher.s和Dep.js分别提供了Watcher类和Dep类,这三个类就是Vue响应式系统的核心。
Wacher,Dep,Observer的类图如下:

Observer用于表示一个响应式数据对象,比如一个Vue里面的data值,就会为其创建一个Observer对象。
Watcher代表一个监听,会解析表达式收集依赖,以及在表达式中的值发生变化时触发回调。例如,每一个watch属性,都会有一个watcher对象。而wacher对象收集到的依赖就是这个watch监听的属性。 这个依赖我们用Dep对象表示。
举例:
export default {
data(){
return {
a: 1,
b: 2,
c: 3,
}
},
watch:{
a(value, oldValue){
this.c = this.a + this.b;
}
}
}
例如我们定义了如上的组件,那么Vue就会使用data的返回值创建一个Observer, 然后为a创建一个watcher, 这个wacher依赖了a这个属性对应的Dep对象。
Observer和watcher可以直接new出来, 那么Dep是如何收集到的呢?
分为这几步:
- 在
new Observer(data)的时候,在Observer的构造函数里会为data的每个属性调用Object.defineProperty设置他们的getter和setter,并且创建一个Dep对象。
- 在
getter中会调用这个属性对应Dep对象的depend方法,将当前这个Dep对象加到当前的Watcher对象中的依赖列表中。 - 在
setter中会调用这个属性对应Dep对象的notify方法,通知所有订阅了这个Dep的Wacher,Wacher就会重新计算一遍值并调用对应的函数。
- 在
new Wacher()的构造函数里,会先获取一下当前的值,在真正去get之前,会通过修改Dep.target这个静态属性值的方式,先把直接设置成当前的Wacher,然后就会触发之前observer通过Object.defineProperty为各个属性设置的那些getter了, 在getter中将对应属性的Dep对象加到当前的Watcher对象中的依赖列表中。 - 现在只要修改对应属性,就会触发
Dep对象的notify, 然后通知到对应wacher重新获取值,并执行回调。
到这里,Vue响应式系统的原理就基本讲清楚了。不知道大家能看明白不。本着talk is cheap, show you the code的道理,我讲Vue响应系统的源码精简之后,写了最简版的实现如下, 另外源码也可以在我的git库中找到:
class Dep {
constructor() {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
depend(){
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].run()
}
}
}
class Watcher {
constructor(vm, key, cb) {
this.vm = vm
this.deps = []
this.key = key
this.cb = cb
this.value = this.get()
}
addDep(dep){
this.deps.push(dep);
dep.addSub(this);
}
get(){
Dep.target = this;
var value = this.vm[this.key];
Dep.target = null;
return value;
}
run () {
const value = this.get()
if (value !== this.value) {
const oldValue = this.value
this.value = value
this.cb.call(this.vm, value, oldValue);
}
}
}
class Observer {
constructor(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
}
function defineReactive(obj, key) {
const dep = new Dep()
var value = obj[key]
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
if (Dep.target) {
dep.depend()
}
return value
},
set: function reactiveSetter(newVal) {
value = newVal
dep.notify()
}
})
}
//使用
var data = {
a: 1,
b: 2,
c: 3,
}
new Observer(data)
new Watcher(data, 'a', function(value, oldValue){
this.c = value + this.b;
})
data.a = 3
console.log(data.c == 5)
预告:
下一篇中我们再看看响应式系统中的一些细节, 并尝试回答如下几个问题:
-
为什么对数组数据直接使用this.array[0] = 1的方式赋值不会触发响应? 那怎样才能触发呢?为什么?
-
在watch里面直接从this上取对应属性的值,拿到的是新值还是旧值?
-
为什么修改了某个属性的值之后,对应的watch不是立马触发?
欢迎关注我的公众号:
