前言
在掘金上看染陌同学《剖析 Vue.js 内部运行机制》的掘金小册时,发现自己一个极大问题,基础知识掌握的不够牢靠,导致中间有时候出现一些错误,无法理解,所以在这里写下这个笔记,加深自己的印象。
Object.defineProperty
在记录vue的响应式系统前,一定要对Object.defineProperty的用法掌握,这是实现vue数据双向绑定的基础,但是vue的作者宣布将会在下个版本使用Proxy代替Object.defineProperty,这不重要,这里依然来说Object.defineProperty。这个对象的扩展方法是干什么的?简单的说就是用来劫持对象属性的,已达到对象在改变数据之前可以对对象进行一系列操作,这就是js中数据劫持的一个基本原理。
Object.defineProperty有三个参数,分别为obj, prop和descriptor
obj:要在其上定义属性的对象。
prop:要定义或修改的属性的名称。
descriptor:将被定义或修改的属性描述符。
而整个对象的操作都在descriptor里进行,他接受一个对象参数,对象参数支持六个属性,这六个属性在这里我们只需要使用四个,如下
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {},
set: function () {}
})
enumerable: 当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
configurable: 当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
get: 当读取属性时调用
set: 当属性值改变时调用
此处使用MDN的例子说明Object.defineProperty的使用
function Archiver() {
var temperature = null;
var archive = [];
Object.defineProperty(this, 'temperature', {
get: function() {
console.log('get!');
return temperature;
},
set: function(value) {
temperature = value;
archive.push({ val: temperature });
}
});
this.getArchive = function() { return archive; };
}
var arc = new Archiver();
arc.temperature; // 'get!'
arc.temperature = 11;
arc.temperature = 13;
arc.getArchive(); // [{ val: 11 }, { val: 13 }]
更多详细介绍请查看MDN
vue的响应式系统简析
vue的响应式系统在vue整个框架里有什么作用?或者更详细的说,vue的数据双向绑定是怎么实现的?这里就可以说Object.defineProperty是其关键所在,我先撸代码,然后再详细说
function cb(val) {
console.log("视图更新了!!!")
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
return val
},
set: function (newVal) {
if (val === newVal) return
val = newVal
cb(val)
}
})
}
function observer(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
class Vue {
constructor(options) {
this.data = options.data
observer(this.data)
}
}
let o = new Vue({
data: {
text: "hello world!!!"
}
})
o.data.text = "hello Tom" // 视图更新了!!!
从上面代码我们可以看出,当我们改变text的值时,就会触发cb函数,而整个过程中我们是通过Object.defineProperty的set实现的,当属性值改变时,就会触发set,并把新值当做参数,这里就完成了简单的响应系统,这里并没有对传入的参数做判断,并且并不支持数组,但是vue里是实现了对数组的支持的
依赖收集
为什么要依赖收集?依赖收集发挥着怎样的作用?
在使用vue时,我们经常会遇到data里有多个属性,然而有的属性并没有在template里展示,但是我们即将修改这个属性的值,那么就会造成视图更新,这是没有必要的,如下
new Vue({
template: `
<div>{{text1}}</div>
`,
data: {
text1: '123',
text2: '456'
},
mounted() {
this.text2 = '789'
}
})
这里我们改变了text2的值,但是我们并没有在template里展示这个值,只是vue的内部使用,所以并不需要通知视图,进行更新,所以这里我们就需要进行依赖收集,避免不必要的视图更新。
订阅发布模式/观察者模式
在说依赖收集之前,我先说在程序设计中经常使用的两个设计模式订阅发布模式和观察者模式,这是我们实现依赖收集的设计模式,了解到这些模式,会更容易理解如何进行的依赖收集。
先写个例子
class EventBus {
constructor() {
this._event = new Map()
}
addListener(type, fn) {
const handler = this._event.get(type)
if (!handler) {
this._event.set(type, fn)
}
}
emit(type, ...args) {
const handler = this._event.get(type)
if (handler && typeof handler === 'function') {
if (args.length > 0) {
handler.apply(this, args)
} else {
handler.call(this)
}
}
}
}
var emitter = new EventBus()
emitter.addListener('put', function(name) {
console.log("my name is " + name)
})
emitter.addListener('put', function(name) {
console.log("your name is " + name)
})
emitter.emit('put', 'Lucy') //my name is Lucy
这是一个模拟事件池的代码,先给emitter添加一个事件,用emit触发事件,但是在这里,我们给同一个事件绑定了多个函数,当emit时,希望可以通知绑定到这个事件的所有函数,然而这里只是通知了第一个,当我们希望这种一对多的依赖关系时,就可以用发布订阅模式去描述。可以用微信公众号来形象的说明这个模式,公众号就是发布者,而用户就是订阅者,当文章更新时,就会通知每一个订阅者用户,这样一个发布者维护多个订阅者,就是发布订阅模式。那么什么是观察者模式?其实在很多文章中,这两个模式很难有什么区别,而在百度时,他们也是会成对出现的,这里我就不详细讨论他们的区别了,暂且当做一个模式来看。
那么现在我来给这个EventBus进行升级
class EventBus {
constructor() {
this._event = new Map()
}
addListener(type, fn) {
const handler = this._event.get(type)
if (!handler) {
this._event.set(type, fn)
} else if(handler && typeof handler === 'function') {
this._event.set(type, [handler, fn])
} else {
this._event.set(type, handler.push(fn))
}
}
emit(type, ...args) {
const handler = this._event.get(type)
if (handler && Array.isArray(handler)) {
handler.forEach(fn => {
if (args.length > 0) {
fn.apply(this, args)
} else {
fn.call(this)
}
})
} else {
if (args.length > 0) {
handler.apply(this, args)
} else {
handler.call(this)
}
}
}
}
var emitter = new EventBus()
emitter.addListener('put', function(name) {
console.log("my name is " + name)
})
emitter.addListener('put', function(name) {
console.log("your name is " + name)
})
emitter.emit('put', 'Lucy')
// my name is Lucy
// your name is Lucy
在这里实际上就是监听了所有绑定在listener上的函数,也就是订阅者绑定在发布者上,当emit触发时,就通知所有的订阅者,发布更新了
依赖收集
现在我们对vue的响应式系统进行升级,先撸为敬
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
// 通知所有的订阅者sub更新
notify(val) {
this.subs.forEach(sub => {
sub.update(val)
})
}
}
// 管理订阅者的watcher
class Watcher {
constructor() {
Dep.target = this
}
update(val) {
console.log("视图更新了!!!!")
}
}
Dep.target = null
function defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function () {
dep.addSub(Dep.target)
return val
},
set: function (newVal) {
if (val === newVal) return
val = newVal
dep.notify(val)
}
})
}
function observer(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
class Vue {
constructor(options) {
this.data = options.data
observer(this.data)
new Watcher()
/**
* 这里的console.log是模拟使用this.data的属性
* 以触发defineProperty的get,这样就会对当属性
* 改变时,视图需要更新的属性进行了收集,而未在
* template里使用的进行剔除
*/
console.log(this.data.text)
}
}
var vue = new Vue({
data: {
text: '123',
text1: '456'
}
})
vue.data.text = '456' // 视图更新了!!!!
vue.data.text1 = '789'
改造好的vue响应式系统,基本具有了数据变化,视图更新的流程,并能进行依赖收集。
有错误之处,望请指正。