vue2和vue3的响应式简单实现
响应式本质是数据与函数的关系, 数据驱动页面, 其实是数据驱动函数. 当数据改变时调用某些函数.
const title = document.getElementById('title')
const data = {
title: 'Hello, world!'
}
title.textContent = data.title
这是一个简单的操作, 实际上vue模板的就会编译成类似的东西, 只不过可能不是用id来索引, 不过最后都是要用textContent或者innerHTML的.
如果每次修改title后, 都要自己写一行title.textContent = data.title
太麻烦了, 需要再封装一下.
我们希望每次set这个title后, 就执行一些操作, 那我们可以拦截set, 这些需要拦截set的操作就是依赖这个响应式变量的函数, deps
; 我们还希望这个响应式变量可以仅在某些时候拦截, 其它情况不拦截, 因此我们可以拦截get, 只有需要响应式的时候才将依赖收集起来.
在这里可以利用一点: js是单线程的, 不可能有两个函数同时对一个变量同时写同时读, 因此我们可以准备一个全局变量ACTIVE_EFFECT
和一个函数effect, 通过effect调用的函数就是需要收集依赖的.
let ACTIVE_EFFECT = null
function effect(fn) {
ACTIVE_EFFECT = fn
fn()
ACTIVE_EFFECT = null
}
接下来就是要想怎么拦截get/set了.
在js中, 提供了一些很有用的东西: Object.defineProperty
和Proxy
, 当然也可以自己写一个类, 大概这样子:
class ReactiveValue {
get value() {}
set value(newValue) {}
}
vue2
vue2为了考虑兼容性, 选择了Object.defineProperty
. 因此, 在vue2中采用了选项式API, 并要求data返回一个Object.
const data = {
title: 'Hello, world!'
}
这里为了方便, 就不写成data(){}了, 直接写成一个对象.
在vue2中, 如果data不是一个函数, 而是一个普通的对象, 就会出现同个组件使用同个data的问题.
先以title为例, 然后改成通用的函数.
let titleValue = data.title // 在拦截get/set前需要保存原值, 如果get返回data.title或者set修改data.title就会无限调用
const deps = new Set()
Object.defineProperty(data, 'title', {
get(){
if (ACTIVE_EFFECT) {
deps.add(ACTIVE_EFFECT)
}
return titleValue
},
set(newValue){
titleValue = newValue
deps.forEach(fn => fn())
},
})
这就是一个简单的响应式变量了, 不过只有title具有响应式. 整个对象的话, 其实就是遍历所有key-value
function makeReactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj // 只对object进行响应式化
}
const depsMap = new Map() // 对于每一个对象, 都应该有一个depsMap. key是obj的key, value是对应的依赖集合
Object.keys(obj).forEach((key) => {
let value = obj[key] // 跟title一样, 要保存原值
obj[key] = makeReactive(value) // 如果是object的话, 还要继续响应式化, 不是的话返回原值
Object.defineProperty(obj, key, {
get() {
if (ACTIVE_EFFECT) {
// 收集依赖
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(ACTIVE_EFFECT)
}
return value
},
set(newValue) {
value = makeReactive(newValue) // 如果是对象, 那么新赋值的对象也要响应式化
const deps = depsMap.get(key)
if (deps) {
deps.forEach(fn => fn())
}
},
})
})
}
这里基本实现了vue2中响应式实现. 实际可能更加复杂. 这里是没有考虑如果传进来的obj已经是响应式变量该怎么办的, 以及watch需要的旧值没有处理.
vue2简单实现代码
可以进一步封装.
function makeReactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj // 只对object进行响应式化
}
const depsMap = new Map() // 对于每一个对象, 都应该有一个depsMap. key是obj的key, value是对应的依赖集合
Object.keys(obj).forEach((key) => {
let value = obj[key] // 跟title一样, 要保存原值
obj[key] = makeReactive(value) // 如果是object的话, 还要继续响应式化, 不是的话返回原值
const track = () => {
if (ACTIVE_EFFECT) {
// 收集依赖
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(ACTIVE_EFFECT)
}
}
const trigger = () => {
const deps = depsMap.get(key)
if (deps) {
deps.forEach(fn => fn())
}
}
Object.defineProperty(obj, key, {
get() {
track()
return value
},
set(newValue) {
value = makeReactive(newValue) // 如果是对象, 那么新赋值的对象也要响应式化
trigger()
},
})
})
}
vue3
vue3的实现有两种, 一种是object的, 另一种是普通数据的. 也就是reactive和ref了. 不过我们在使用ref的时候会发现, ref就算传入一个object也可以正常使用, 其实背后是reactive在发力了.
ref(不含object)
ref的实现比较简单, 毕竟不像对象有多个key-value, 像1, '1'这种就一个value
class RefImpl<T> {
private _value: T
private deps: Set<() => unknown>
constructor(value) {
this._value = value
}
private track() {
if (ACTIVE_EFFECT) {
if (!this.deps) {
this.deps = new Set<() => unknown>()
}
this.deps.add(ACTIVE_EFFECT)
}
}
private trigger() {
if (this.deps) {
this.deps.forEach(fn => fn())
}
}
get value(): T {
this.track()
return this._value
}
set value(newValue: T) {
this._value = newValue
this.trigger()
}
}
function ref<T>(value: T) {
return new RefImpl<T>(value)
}
同样要声明一下, 实际实现比这要复杂, 这里只是简单实现.
现在的ref就可以实现普通值的响应式了.
const count = ref(1)
effect(() => {
console.log('a changed:', count.value)
})
setInterval(() => {
count.value++
}, 1000)
reactive
reactive只允许传入一个对象, 使用Proxy来拦截get/set.
function reactive<T extends object>(obj: T): T {
if (typeof obj !== 'object' || obj === null) {
return obj
}
const depsMap = new Map<string | symbol, Set<()=>unknown>>()
function track(key: string | symbol) {
if (!ACTIVE_EFFECT) {
return
}
let deps = depsMap.get(key)
if (!deps) {
deps = new Set()
depsMap.set(key, deps)
}
deps.add(ACTIVE_EFFECT)
}
function trigger(key: string | symbol) {
const deps = depsMap.get(key)
if (deps) {
deps.forEach(fn => fn())
}
}
return new Proxy(obj, {
get(target, key) {
track(key)
const result = Reflect.get(target, key)
return (typeof result === 'object' && result !== null) ? reactive(result) : result
},
set(target, key, value) {
const result = Reflect.set(target, key, value)
trigger(key)
return result
}
})
}
末尾
面试
vue的响应式实现思路只有一点: 拦截get/set. 可能常见的面试题是这样(不一定全):
- vue2响应式实现和vue3的有什么不同
vue2使用Object.defineProperty, vue3对于基础类型使用简单的getter/setter对, 对于引用类型使用Proxy
.
- vue2响应式实现有什么问题
- 效率低, 需要遍历对象的每一个key-value, 如果value是引用类型还要继续遍历.
- 响应式不充分, 对于原本不存在的key-value无法跟踪(可能有追问要跟踪怎么办, 使用
Vue.set
), 数组的push等方法也无法跟踪.
- vue3响应式实现有什么优点
- 效率高, 只需要执行一次, 如果遇到对象再生成一个Proxy.
- 响应式完全, 原本不存在的key-value也可以跟踪.
- ref和reactive有什么区别
- vue3为什么比vue2效率高
- ...
react?
之前也写过react, react的响应式感觉没有vue的要好, 至少从理解角度来看, vue的要更好理解. 现在的话我虽然可能可以手写react的响应式, 不过估计会比vue要折磨很多.
react要求一个组件每次调用都应该有相同的响应式变量, 大概就是这样:
function TestComponent() {
const [isLoading, setIsLoading] = useState(true)
// ... 一些操作, 比如fetch
if (isLoading) {
return <div>loading...</div>
}
const [data, setData] = useState(...)
return <div>
{/* 展示data */}
</div>
}
这种就不行, 会警告还是报错有点忘了. 因为react依赖响应式变量的顺序, 如果其中一个组件突然少了个响应式变量, 那么后面的顺序全错了. react会执行两次组件, 并确保执行的结果相同.
个人理解, 我也不太清楚. 我从来没有觉得写react开心过, 离开上一家公司后根本不想碰react.