本系列文章基于
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
构建结果 -
flow
Flow 需要用到的类型声明(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
全局api
components
通用抽象组件,目前只有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不是立马触发?
欢迎关注我的公众号: