在了解Vue2响应式原理之前需要清楚响应式是用来做什么的,它的作用就是当一个函数所依赖的数据发生改变的时候,我们需要这个函数重新执行一次。
const data = {
name: "张三"
}
const updateData = () => {
console.log("data.name的值为:",data.name)
}
updateData()
data.name = "李四" // 修改data.name之后,需要updateData函数再次执行
我们想要的结果是当data.name被修改之后,函数updateData再次执行,也就是会打印两次:
// data.name的值为:张三
// data.name的值为:李四
要实现响应式我们要做的有两件事:
- 是知道data中的数据被哪些函数所依赖;
- 就是数据修改之后去调用依赖它的函数。
为了实现第一件事我们需要在函数调用的时候将这个函数保存起来,函数里面读取数据的时候将函数保存到当前的数据中。
当第一件事完成之后第二件事就简单了,在修改数据的时候将保存的函数执行一次就行了。
const data = {
name: "张三"
}
const updateData = () => {
console.log("data.name的值为:",data.name)
}
updateData()
data.name = "李四"
/*
当执行到updateData()时,将函数updateData保存下来
target = updateData
updateData执行后会执行打印,在打印中会读取data.name
设置一个用于保存依赖于data.name的函数的数组nameArr,将当前的target添加到数组中
此时nameArr的值为[updateData]
假设再有一个updateDataName2函数也依赖于data.name,同样会按照上述步骤将其push到nameArr中
执行到data.name = "李四"也就是修改数据的时候
去找到依赖修改数据的函数数组也就是nameArr
然后遍历数组得到里面的每一项函数进行执行
*/
大致的思路就是上面这样,接下来我们具体来实现。
上面提到我们需要在读取数据和修改数据的时候去做一些我们想做的事情,那么就可以通过Object.defineProperty()去重写对象的get和set方法。
export function defineReactive(obj,key,val){
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get(){
/*
1.在这里需要保存当前执行的函数
*/
return val
},
set(newVal){
if(val === newVal) return
val = newVal
/*
2.在这里需要将依赖于当前数据的函数全部执行
*/
}
})
}
为了方便调用,我们可以将保存函数和调用函数的操作封装成一个Dep类:
export default class Dep {
constructor(){
// 用来存储方法的数组
this.subs = []
}
// 将方法存储到数组中
addSub(sub){
this.subs.push(sub)
}
// 遍历数组,并执行方法
notify(){
this.subs.forEach(fn => {
fn()
})
}
}
所以在get和set中:
export function defineReactive(obj,key,val){
let dep = new Dep()
Object.defineProperty(obj,key,{
enumerable: true,
configurable: true,
get(){
/*
1.在这里需要保存当前执行的函数
*/
dep.addSub(fn)
return val
},
set(newVal){
if(val === newVal) return
val = newVal
/*
2.在这里需要将依赖于当前数据的函数全部执行
*/
dep.notify()
}
})
}
因为我们需要给对象的每个属性都重新设置get和set,所以可以封装一个Obsrver类,用来遍历对象的每个属性进行设置:
class Observer {
constructor(data) {
this.traverse(data)
}
traverse(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.addSub(fn)
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
}
})
}
}
那么现在的问题时在get中调用dep.addSub传的函数应该怎么得到,也就是在函数执行的时候应该把函数保存到哪儿,才能方便在get中拿到函数传参。首先想到的可能是挂载到window上,这里我们挂载到window上试一下:
const data = {
name: "张三"
}
window.getter = () => {
console.log("data.name的值为:",data.name)
}
window.getter()
这样在get中调用dep.addSub的时候可以直接传window.getter,整体的代码就是这样:
class Observer {
constructor(data) {
this.traverse(data)
}
traverse(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
dep.addSub(window.getter)
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
}
})
}
}
class Dep {
constructor(){
this.subs = []
}
addSub(sub){
this.subs.push(sub)
}
notify(){
this.subs.forEach(fn => {
fn()
})
}
}
const data = {
name: "张三"
}
new Observer(data)
window.getter = () => {
console.log("data.name的值为:", data.name)
}
window.getter()
data.name = "李四"
这样当data.name修改的时候,依赖它的函数会再次执行:
但是,如果直接把函数挂载到window上的话每次调用都需要通过window.来调用,并且每有一个函数就都要覆盖一次,就会很麻烦,所以我们需要封装一个Watcher类来处理:
class Watcher {
constructor(Fn) {
this.getter = Fn
this.get()
}
get() {
this.getter()
}
update() {
this.getter()
}
}
在new Watcher的时候将函数保存在实例的getter上,并直接执行,watcher就是这个函数的观察者,dep的subs中就可以换成保存这些watcher,在数据修改后调用notify的时候触发每个watcher的update:
class Dep {
constructor(){
this.subs = []
}
addSub(watcher){
this.subs.push(watcher)
}
notify(){
this.subs.forEach(watcher => {
watcher.update()
})
}
}
现在又会有个问题,这个watcher怎么在Observer里面的defineReactive中拿到,也就是这个watcher需要保存在哪里?我们发现在Observer中是使用到了Dep类的,所以可以在把watcher存到Dep中:
class Watcher {
constructor(Fn) {
this.getter = Fn
this.get()
}
get() {
Dep.target = this
this.getter()
// 可以在每次执行完函数之后重置target,保证target是唯一的并且是当前执行的函数
Dep.target = null
}
update() {
this.getter()
}
}
那么在 get中:
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
}
})
}
所以整体代码为:
class Observer {
constructor(data) {
this.traverse(data)
}
traverse(data) {
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key])
})
}
defineReactive(obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target)
return val
},
set(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
}
})
}
}
class Dep {
constructor() {
this.subs = []
}
addSub(watcher) {
this.subs.push(watcher)
}
notify() {
this.subs.forEach(watcher => {
watcher.update()
})
}
}
class Watcher {
constructor(Fn) {
this.getter = Fn
this.get()
}
get() {
Dep.target = this
this.getter()
Dep.target = null
}
update() {
this.getter()
}
}
const data = {
name: "张三",
text: "666"
}
new Observer(data)
const updateData = () => {
console.log("data.name:", data.name)
}
const updatetext = () => {
console.log("data.text:", data.text)
}
new Watcher(updateData)
new Watcher(updatetext)
data.name = "李四"
data.text = "6668"
执行结果为:
目前这个是最简单的响应式,还存在着许多问题,比如说Dep中会收集到重复的Watcher、对象实现深层次的响应、数组的响应式等等,这些后续会写到。
(这些只是自己看响应式的一些理解,如有问题,欢迎指正。)