前言
在Vue中,实现响应式原理大概会有以下几步:
- 数据劫持(设置、getter和setter,Vue3使用Proxy,Vue2使用Object.defineProperty)
- 依赖收集(Dep)
- 数据更新通知相关订阅者(watcher)执行对应的回调(例如更新虚拟DOM,diff算法比较新旧虚拟DOM,更新真实DOM...)
此文将手写实现Vue2/Vue3响应式原理。但仅到响应式更新这一层,并不会涉及到DOM层面;此外,会先实现Proxy版本,再此基础上稍加变化,实现Vue2的defineProperty版本...
Proxy代理 和 Reflect反射
ES6之后,新增了Proxy类,通过这个类,我们可以代理一整个对象,并通过捕捉器监听到对象的一些操作
都已经有了defineProperty,为什么还要使用Proxy?
const obj = {
age: 1,
}
const proxyObj = new Proxy(obj, {
get(target, key) {
console.log('get执行')
return target[key]
},
set(target, key, newValue) {
console.log('set执行')
target[key] = newValue
},
})
console.log(proxyObj.age)
proxyObj.age = 100
但此时在get/set中,返回/设置值都是直接通过target[key]实现的,我们既然使用了代理,那么我们便不希望还是在原对象身上进行操作,此时可以使用ES6提供的Reflect反射
上述代码就可以变成:
const obj = {
age: 1,
}
const proxyObj = new Proxy(obj, {
get(target, key) {
return Reflect.get(target, key)
},
set(target, key, newValue) {
Reflect.set(target, key, newValue)
},
})
console.log(proxyObj.age)
proxyObj.age = 100
Reflect 主要提供了很多操作 JS 对象的方法,有点像 Object 中操作对象的方法;比如Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()......那么既然 Object 上的方法都已经能实现相关操作了,为什么还需要这个东西?
这是因为在早期的 ECMA 规范中没有考虑到这种对对象本身的操作如何设计会更加规范,所以将这些 API 放到了Object 上面。导致 Object 既是构造函数,又是所有的对象的超类(基类),同时自身也有许多方法,造成 Object 太臃肿,产生了很多弊端。所以在 ES6 新增了 Reflect 对象, 让我们这些操作都集中到了 Reflect 对象上。同时,其方法与 Proxy 的大多数方法对应,在使用 Proxy 的时候使用 Reflect 对象,达到不操作原对象的效果
注意,此处既然使用了Proxy对这个对象进行了代理,那么后续的操作也要在这个代理的对象身上进行
响应式函数的封装:
在前面中,我们通过代理对象,当对象属性被访问/被修改的时候就会触发get/set,从而进行对应逻辑:比如getter的时候就返回对应的值,并进行依赖收集;setter的时候通知对应的watcher执行回调。那么,问题来了,如何知道和收集属性的watcher们?
响应式数据在哪里被使用到?在模板watcher、监听器watcher、计算属性watcher中会被使用,此处我们可以定义一个watchFn来模拟这种操作
let activeFn = null
function watchFn(fn){
activeFn = fn
fn()
activeFn = null
}
watchFn(() => {
console.log(proxyObj.age + '执行了')
})
在watchFn中我们将回调函数存放在activeFn中,是为了后续方便存放到对应的Dep中(后续讲)。而还在watchFn中执行一遍传入的回调,就是为了触发对应属性的get
比如在这个例子中,调用watchFn传入的回调中使用了proxyObj身上的count属性,在watchFn中我们执行了一次fn(),此时proxyObj身上的count的get就会被触发,后续我们就能在get中将其收集到Dep中
Dep类进行依赖收集:
在前面我们知道,依赖需要在被访问(get)的时候进行收集,需要在被修改(set)的时候进行通知。问题是,一个.vue文件中,存在不止一个对象,而对象中又存在不止一个属性,那么怎么精准的确定这个Dep就是收集和obj1的key1相关的watcher,那个Dep就是收集和obj1的key2相关的watcher......
此处需要使用weakMap和Map数据结构:
class Depend{
constructor(){
// 使用 Set 是为了使用去重,因为相同属性不需要重复监听
this.watchFns = new Set()
}
addWatch(){
activeFn && this.watchFns.add(activeFn)
}
notify(){
this.watchFns.forEach(fn => fn())
}
}
const weakMap = new WeakMap()
function getDep(target, key){
let targetMap = weakMap.get(target)
if(!targetMap){
targetMap = new Map()
weakMap.set(target, targetMap)
}
let depend = targetMap.get(key)
if(!depend){
depend = new Depend()
targetMap.set(key, depend)
}
return depend
}
形成如下的一种结构:
Dep进行依赖收集和执行的时机:
结论:在执行过程中,我们访问了某个响应式数据,触发了get,此时就应该进行依赖的收集;我们修改了某个响应式数据的值,触发了set,此时就应该通知订阅者执行对应的回调
const obj = {
count: 1,
}
const proxyObj = new Proxy(obj, {
get(target, key) {
const depend = new Depend()
// 进行依赖的收集(收集watcher)
depend.addWatch()
return Reflect.get(target, key)
},
set(target, key, newValue) {
Reflect.set(target, key, newValue)
const depend = new Depend()
// 通知watcher执行对应的回调
depend.notify()
},
})
console.log(proxyObj.count)
proxyObj.count = 100
完整代码:
let activeFn = null
function watchFn(fn) {
activeFn = fn
fn()
activeFn = null
}
class Depend {
constructor() {
this.watchFns = new Set()
}
addWatch() {
activeFn && this.watchFns.add(activeFn)
}
notify() {
for (const fn of this.watchFns) {
fn()
}
}
}
/**
* weakMap:
* {
* target:Map
* {
* key:Depend
* key:Depend
* }
* }
* */
let weakMap = new WeakMap()
function getDep(target, key) {
let targetMap = weakMap.get(target)
if (!targetMap) {
targetMap = new Map()
weakMap.set(target, targetMap)
}
let depend = targetMap.get(key)
if (!depend) {
depend = new Depend()
targetMap.set(key, depend)
}
return depend
}
const obj = {
name: 'zs',
age: 19,
}
const proxyObj = new Proxy(obj, {
get(target, key) {
const depend = getDep(target, key)
depend.addWatch()
return Reflect.get(target, key)
},
set(target, key, newValue) {
Reflect.set(target, key, newValue)
const depend = getDep(target, key)
depend.notify()
},
})
watchFn(() => {
console.log(proxyObj.age + '执行了')
})
console.log('-----------------')
proxyObj.age = 20
更新age,则通知对应的watcher执行回调
更新name,但并没有name的订阅者,所以不会执行回调
reactive的实现:
其实上述代码就是vue3 reactive的核心代码,我们只需要将其简单的封装即可实现:
let activeFn = null
function watchFn(fn) {
activeFn = fn
fn()
activeFn = null
}
class Depend {
constructor() {
this.watchFns = new Set()
}
addWatch() {
activeFn && this.watchFns.add(activeFn)
}
notify() {
for (const fn of this.watchFns) {
fn()
}
}
}
/**
* weakMap:
* {
* target:Map
* {
* key:Depend
* key:Depend
* }
* }
* */
let weakMap = new WeakMap()
function getDep(target, key) {
let targetMap = weakMap.get(target)
if (!targetMap) {
targetMap = new Map()
weakMap.set(target, targetMap)
}
let depend = targetMap.get(key)
if (!depend) {
depend = new Depend()
targetMap.set(key, depend)
}
return depend
}
function reactive(obj){
const proxyObj = new Proxy(obj, {
get(target, key) {
const depend = getDep(target, key)
depend.addWatch()
return Reflect.get(target, key)
},
set(target, key, newValue) {
Reflect.set(target, key, newValue)
const depend = getDep(target, key)
depend.notify()
},
})
return proxyObj
}
const obj = reactive({
name: 'zs',
age: 19,
})
如果是Vue2,就是将Proxy/Reflect换成Object.defineProperty
function reactive(obj) {
Object.keys(obj).forEach((key) => {
let val = obj[key]
Object.defineProperty(obj, key, {
get() {
const depend = getDep(obj, key)
depend.addWatch()
return val
},
set(newValue) {
val = newValue
const depend = getDep(obj, key)
depend.notify()
},
})
})
return obj
}
存在的问题:
Proxy本身只能代理对象,但无法监听到深层次对象的变化,也就是说,对于一个对象嵌套对象的响应式数据,上述方案无法监听到嵌套对象的属性的变化:
const obj = {
name: 'zs',
age: 19,
self: {
msg: 'this is a message',
},
}
const proxyObj = new Proxy(obj, {...})
watchFn(() => {
console.log(proxyObj.self.msg + '执行了')
})
console.log('---以下是触发更新---')
proxyObj.self.msg = 'new msg'
可以看到,但深层次对象属性发生变化时,就算我们已经收集了watcher,但是也通知不到他执行,本质就是因为Proxy只能代理一层。解决方法:在get中去递归响应式,这样的好处是真正访问到内部对象的时候才会变成响应式,而不是无脑递归,很大程度上提升了性能
const proxyObj = new Proxy(obj, {
get(target, key) {
const value = Reflect.get(target, key)
const depend = getDepend(target, key)
depend.addDepend()
// 递归处理响应式,如果需要的话
if (typeof value === 'object' && value !== null) {
return reactive(value)
}
return value
},
set(target, key, newValue) {
Reflect.set(target, key, newValue)
const depend = getDepend(target, key)
depend.notify()
},
})
ref的实现:
在Vue3中,ref可以用于声明基本数据类型和引用型数据,而reactive只能声明引用型数据。但是你是否想过Vue3的响应式是基于Proxy实现的,但是Proxy只能代理对象(引用型数据),那么ref声明的基本数据类型的响应式是怎么做到的?你是否又想过,为什么ref声明的就必须得通过.value访问(在<script>中)
一切的一切,就是因为:ref本质是基于reactive实现的,ref声明的属性会被包装到一个新对象的value属性中,然后再通过reactive对这个新对象进行处理实现响应式
function ref(value){
const obj = {
value: value
}
return reactive(obj)
}
const count = ref(0)
watchFn(() => {
console.log('count:', count.value)
})
console.log('---触发更新---')
count.value = 2
这里其实顺带引出来另外一个问题:当使用ref去声明引用型数据(对象)后,想要使用watch时,就得开启deep深度监听,因为就类似变成了:
const o = ref({
key1: val1,
key2: val2
})
const newObj = {
value: {
key1: val1,
key2: val2
}
}
ref声明的这个对象被放到另一个对象的value属性上,此时就是对象嵌套对象,所以就得深度监听
结语
在整个的实现方案中,我们需要手动调用watchFn响应式函数进行watcher的回调的收集,但是在Vue开发过程中我们似乎并未使用过watcher这么一个函数。正如“前言”所说,此文只到响应式更新这一层面。在Vue中,其实是在render函数中使用的watchFn(),他会在模板编译的知道模板中使用了哪些响应式数据,并在后续响应式数据更新的时候通知watcher,从而实现视图的更新。随后又牵扯出新的一系列问题:
- 每次数据更新,都直接更新DOM会造成新能的损耗(尤其是在DOM结构复杂的时候),所以引出了虚拟DOM;
- 虚拟DOM又需要
h()函数来创建(虚拟DOM就是一个JS对象,包含tag/type、props、children三个属性) - 第一次渲染的时候,所拿到的虚拟DOM就会被作为真实DOM渲染到页面上,所以又需要一个虚拟DOM->真实DOM的
mountElement() - 在此后的更新中,还需要通过
diff算法比较新旧虚拟DOM的差别,从而更新真实DOM...
欲知后事如何,有空其他文章再讲!!!
若文章内容有误,欢迎指出交流指正