响应式系统
一、概述
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
双向数据绑定,简单点来说分为三个部分:
1、Observer。这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的watcher。
2、Watcher。观察者,当监听的数据值修改时,执行响应的回调函数(Vue里面的更新模板内容)。
3、Dep。连接Observer和Watcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存与该Observer相关的Watcher。
Object.defineProperty,Vue.js就是基于它实现「响应式系统」
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者(dep),在数据变更的时候通知订阅者更新视图。
/*
obj: 目标对象
prop: 需要操作的目标对象的属性名
descriptor: 描述符
return value 传入对象
*/
Object.defineProperty(obj, prop, descriptor)
enumerable,属性是否可枚举,默认 false。
configurable,属性是否可以被修改或者删除,默认 false。
get,获取属性的方法。
set,设置属性的方法
响应系统的实现
function observe(value, cb) {
Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb))
}
function defineReactive (obj, key, val, cb) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
/*....依赖收集等....*/
/*Github:https://github.com/answershuto*/
},
set:newVal=> {
cb();/*订阅者收到消息的回调*/
}
})
}
class Vue {
constructor(options) {
this._data = options.data;
observe(this._data, options.render)
}
}
let app = new Vue({
el: '#app',
data: {
text: 'text',
text2: 'text2'
},
render(){
console.log("render");
}
})
二、依赖收集
为什么要依赖收集
先看下面这段代码
new Vue({
template:
`<div>
<span>text1:</span> {{text1}}
<span>text2:</span> {{text2}}
<div>`,
data: {
text1: 'text1',
text2: 'text2',
text3: 'text3'
}
});
按照之前响应式原理中的方法进行绑定则会出现一个问题——text3在实际模板中并没有被用到,然而当text3的数据被修改的时候(this.text3 = 'test')的时候,同样会触发text3的setter导致重新执行渲染,这显然不正确。
先说说Dep
当对data上的对象进行修改值的时候会触发它的setter,那么取值的时候自然就会触发getter事件,所以我们只要在最开始进行一次render,那么所有被渲染所依赖的data中的数据就会被getter收集到Dep的subs中去。在对data中的数据进行修改的时候setter只会触发Dep的subs的函数。
定义一个依赖收集类Dep。
class Dep () {
constructor () {
this.subs = [];
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
/*Github:https://github.com/answershuto*/
notify () {
// stabilize the subscriber list first
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Watcher
订阅者,当依赖收集的时候会addSub到sub中,在修改data中数据的时候会触发dep对象的notify,通知所有Watcher对象去修改对应视图。
class Watcher () {
constructor (vm, expOrFn, cb, options) {
this.cb = cb;
this.vm = vm;
/*在这里将观察者本身赋值给全局的target,只有被target标记过的才会进行依赖收集*/
Dep.target = this;
/*Github:https://github.com/answershuto*/
/*触发渲染操作进行依赖收集*/
this.cb.call(this.vm);
}
update () {
this.cb.call(this.vm);
}
}
开始依赖收集
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data, options.render);
let watcher = new Watcher(this, );
}
}
function defineReactive (obj, key, val, cb) {
/*在闭包内存储一个Dep对象*/
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: ()=>{
if (Dep.target) {
/*Watcher对象存在全局的Dep.target中*/
dep.addSub(Dep.target);
}
},
set:newVal=> {
/*只有之前addSub中的函数才会触发*/
dep.notify();
}
})
}
Dep.target = null;
将观察者Watcher实例赋值给全局的Dep.target,然后触发render操作只有被Dep.target标记过的才会进行依赖收集。有Dep.target的对象会将Watcher的实例push到subs中,在对象被修改出发setter操作的时候dep会调用subs中的Watcher实例的update方法进行渲染。
三、完整的小例子
第一部分是Observer
。
function Observer(obj, key, value){
var dep = new Dep();
if (Object.prototype.toString.call(value) == '[object Object]') {
Object.keys(value).forEach(function(key){
new Observer(value,key,value[key])
})
};
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function(){
if (Dep.target) {
dep.addSub(Dep.target);
};
return value;
},
set: function(newVal){
value = newVal;
dep.notify();
}
})
}
我们先略过对数组的处理等。递归地为obj
的每个属性添加getter
和setter
。在getter
中,我们把watcher
添加到dep
中。setter
中,触发watcher
执行回调。
第二部分是Watcher
。
function Watcher(fn){
this.update = function(){
Dep.target = this;
fn();
Dep.target = null;
}
this.update();
}
极其简单的几行代码,fn
是数据变化后要执行的回调函数,一般是获取数据渲染模板。默认执行一遍update
方法是为了在渲染模板过程中,调用数据对象的getter
时建立两者之间的关系。因为同一时刻只有一个watcher
处于激活状态,把当前watcher
绑定在Dep.target
(方便在Observer
内获取)。回调结束后,销毁Dep.target
。
最后是Dep
。
function Dep(){
this.subs = [];
this.addSub = function (watcher) {
this.subs.push(watcher);
}
this.notify = function(){
this.subs.forEach(function(watcher){
watcher.update();
});
}
}
内部一个存放watcher
的数组subs
。addSub
用于向数组中添加watcher
(getter
时)。notify
用于触发watcher
的更新(setter
时)。
以上就是我们简易的双向数据绑定demo。我们看看用起来是什么样的。
<div id="test"></div>
<script type="text/javascript">
var obj = {
a: 1,
b: 2,
c: 3
}
Object.keys(obj).forEach(function(key){
new Observer(obj, key, obj[key])
});
new Watcher(function(){
document.querySelector("#test").innerHTML = obj.a;
})
</script>
首先,我们给obj
的每一个属性都添加getter
和setter
。创建一个Watcher
对象,回调函数是使#test
的内容为obj.a
,这里是1
。
打开控制太,我们修改obj.a == 22
。我们发现页面中显示的内容也变成了22
。相关代码见这里。
以上知识我们的开胃小菜,Vue
中对数组做了处理,而且页面的更新是异步执行的,所以会有许许多多的处理,接下来我们慢慢分解。
四、Vue
的实现
我们先看一张图,这是Vue文档中的一张图,解释的就是Vue
的响应式原理。
整体上和我们之前的demo一样,我们之前说过,Vue
的更新是生成render
函数,然后生成虚拟dom
,映射到页面上。左侧的部分其实就是我们watcher
的回调,右下角的data
就是通过我们上面说的Observer
来添加getter
和setter
。watcher
通过dependency
和data
联系在一起,并触发re-render
。
Vue
中的Observer
接着,我们来看源码,打开src/core/observer/index.js
。
首先是我们的Observer
对象。
export class Observer {
value: any;
dep: Dep;
vmCount: number; // 把该对象作为root $data的vm个数
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 添加__ob__来标示value有对应的Observer
def(value, '__ob__', this)
// 对数组的处理
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
// 处理对象
} else {
this.walk(value)
}
}
// 给每个属性添加getter/setters
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
// 观察数组的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
整体上,value
分为对象或数组两种情况。在这之前,我们先来说一下defineReactive
和observe
这两个比较重要的函数。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: Function
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
const getter = property && property.get
const setter = property && property.set
let childOb = observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
}
if (Array.isArray(value)) {
dependArray(value)
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = observe(newVal)
dep.notify()
}
})
}
这个方法,是具体为obj
的key
值添加装饰器的地方。它会为每个值创建一个dep
,如果用户为这个值传入getter
和setter
,则暂时保存。observe
方法稍后介绍。之后通过Object.defineProperty
,重新添加装饰器。在getter
中,dep.depend
其实做了两件事,一是向Dep.target
内部的deps
添加dep
,二是将Dep.target
添加到dep
内部的subs
,也就是建立它们之间的联系。在setter
中,如果新旧值相同,直接返回,不同则调用dep.notify
来更新与之相关的watcher
。customSetter
在开发过程中输出错误用。
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value)) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
该方法用于观察一个对象,返回与对象相关的Observer
对象,如果没有则为value
创建一个对应的Observer
。defineReactive
中调用该方法,其实就是为所有value
为对象的值递归地观察。
我们再回到Observer
,如果传入的是对象,我们就调用walk
,该方法就是遍历对象,对每个值执行defineReactive
。
另一种情况是传入的对象是数组,因为数组本身只引用了一个地址,所以对数组进行push
、splice
、sort
等操作,我们是无法监听的。所以,Vue
中改写value
的__proto__
(如果有),或在value
上重新定义这些方法。augment
在环境支持__proto__
时是protoAugment
,不支持时是copyAugment
。
function protoAugment (target, src: Object) {
target.__proto__ = src
}
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
protoAugment
很简单,其实就是执行了value.__proto__ = arrayMethods
。
copyAugment
中循环把arrayMethods
上的arrayKeys
方法添加到value
上。
arrayMethods
其实是改写了数组方法的新对象。arrayKeys
是arrayMethods
中的方法列表。我们一起来看看arrayMethods
。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator () {
let i = arguments.length
const args = new Array(i)
while (i--) {
args[i] = arguments[i]
}
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
inserted = args
break
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
整体上其实还是调用数组相应的方法来操作value
,只不过操作之后,添加了相关watcher
的更新。这里解释一下为什么push
、unshift
、splice
参数大于2时,要重新调用ob.observeArray
,因为这三种情况都是像数组中添加新的元素,所以需要重新观察每个子元素。
Vue
中的Dep
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep
对象比较简单,内部有一个为一个id
,用于作为Dep
对象的唯一标识,还有一个保存watcher
的数组subs
。相比于我们之前的小demo,这里多了removeSub
和depend
。removeSub
是从数组中移除某一watcher
,depend
是调用了watcher
的addDep
,等会讲解。
Vue
中的Watcher
let uid = 0
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: Object
) {
this.vm = vm
vm._watchers.push(this)
...
this.cb = cb
this.id = ++uid
...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = function () {}
}
}
this.value = this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
...
value = this.getter.call(vm, vm)
...
popTarget()
this.cleanupDeps()
return value
}
...
update () {
...
queueWatcher(this)
}
run () {
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
...
}
这里我们只说最普通的情况,deep
、lazy
等就不多说了,大家有时间自己看一看就行。
创建Watcher
对象时,有两个比较重要的参数,一个是expOrFn
,一个是cb
。
在Watcher
创建时,会调用的this.get
,里面会执行根据expOrFn
解析出来的getter
。在这个getter
中,我们或渲染页面,或获取某个数据的值。总之,会调用相关data
的getter
,来建立数据的双向绑定。
当相关的数据改变时,会调用watcher
的update
方法,进而调用run
方法。我们看到,run
中还会调用this.get
来获取修改之后的value
值。update
中的queueWatcher
方法稍后解释。
我们先来看看Watcher
的两种主要用途:一种是更新模板,另一种就是监听某个值的变化。
** 模板更新 **
我们之前讲Vue
声明周期时讲到挂载元素时,我们是通过创建Watcher
对象,然后调用updateComponent
来更新渲染模板的。
vm._watcher = new Watcher(vm, updateComponent, noop)
这是因为创建Watcher
会调用this.get
,也就是这里的updateComponent
。在render
的过程中,会调用data
的getter
方法,以此来建立数据的双向绑定,当数据改变是,会重新触发updateComponent
。在这里this.get
的返回值是undefined
,所以主要是用于渲染模板。
** 监听数据 **
另一个用途就是我们的computed
、watch
等,即监听数据的变化来执行响应的操作。
此时this.get
返回的是要监听数据的值。初始化过程中,调用this.get
会拿到初始值保存为this.value
,监听的数据改变后,会再次调用this.get
并拿到修改之后的值,将旧值和新值传给cb
并执行响应的回调。
接下来我们谈谈queueWatcher
,从函数名我们大致可以猜出,它是把当前的watcher
添加到一个队列中。我们知道,Vue
中页面的更新是异步的,所以一系列数据的变化,会在之后的某一时刻统一更新。我们来看看该方法的代码。
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i >= 0 && queue[i].id > watcher.id) {
i--
}
queue.splice(Math.max(i, index) + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
我们的watcher
有从小到大的唯一id
,在页面更新时,会按照一定的顺序依次更新,这里做了一个判断,如果watcher
列表正在更新,则把新的watcher
添加到对应的位置,并更新。否则,在下一个nextTick
中执行flushSchedulerQueue
。
function flushSchedulerQueue () {
flushing = true
let watcher, id, vm
queue.sort((a, b) => a.id - b.id)
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
}
const oldQueue = queue.slice()
resetSchedulerState() // 重置队列
index = oldQueue.length
while (index--) {
watcher = oldQueue[index]
vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
该方法首先按照从小到大将watcher
排序,并依次执行它的run
方法。vm._watcher
保存的是渲染模板时创建的watcher
,所以如果队列中有该watcher
,则说明模板有变化,随之调用'updated'钩子函数。
Watcher
对象中还有一些销毁watcher
等方法,这里就不再赘述,感兴趣的大家可以自己看一看。