1 响应式实现的原理
提起vue3的实现原理,你可能会说:Proxy的拦截,在get的时候拦截进行双向绑定,在set的时候更新界面实现。然而再深入询问后可能就说不出什么来了,因为也没有具体看过。
下面根据对vue.js设计和实现的学习,结合自己的理解记录下响应式的实现。
1.1 响应式实现讲解
-
创建原始data数据,添加proxy拦截
-
创建effect副作用函数,里面会有dom的更新,数据的get操作
-
effect副作用函数执行时会触发get的拦截,此时进行依赖收集track,建立数据和副作用函数直接的联系
-
等待数据发生改变,此时会触发set拦截,取出相关的副作用函数,清除相关的依赖,执行副作用函数。
-
副作用函数的执行又会触发get进行依赖收集,相当于回到第二步继续向下执行,形成循环。
1.2 响应式实现流程图
2 代码模拟实现
2.1 先上完整代码,及效果
let activeEffect,
effectStack = []
const bucket = new WeakMap() // 收集副作用函数的桶
function track(target, key) {
if(!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, depsMap = new Map())
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
deps.add(activeEffect)
activeEffect.deps.push(deps) // key和effect之间建立双向联系,方便在取出副作用函数的时候清除相关依赖
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key) // effects就是要执行的副作用函数集合,是个Set
const effectToRun = new Set()
effects && effects.forEach(effectFn => { // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
if (activeEffect !== effectFn) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(effectFn => {
effectFn()
})
}
function effect(fn) {
const effectFn = () => {
cleanUp(effectFn) // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
activeEffect = effectFn // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
effectStack.push(effectFn) // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
fn() // 执行一次函数,这里会触发get拦截
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
function cleanUp(effectFn) {
for (let i = 0, len = effectFn.deps.length; i < len; i++) { // 依赖在deps中
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0 // 清除完依赖后将他置空
}
const data = {
foo: 1,
bar: 2
}
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key) // 收集依赖
return target[key]
},
set(target, key, newValue, receiver) {
target[key] = newValue
trigger(target, key) // 取出相关依赖执行
return true
}
})
effect(() => {
console.log('efffect run')
console.log(obj.foo) // 这里会触发依赖收集
})
obj.foo++ // 期望这里执行后会触发上面收集的依赖,打印出对应的值
2.2 分布讲解
2.2.1 创建data并添加proxy拦截
const data = {
foo: 1,
bar: 2
}
const obj = new Proxy(data, {
get(target, key, receiver) {
track(target, key) // 收集依赖
return target[key]
},
set(target, key, newValue, receiver) {
target[key] = newValue
trigger(target, key) // 取出相关依赖执行
return true
}
})
2.2.2 创建effect副作用函数
function effect(fn) {
const effectFn = () => {
cleanUp(effectFn) // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
activeEffect = effectFn // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
effectStack.push(effectFn) // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
fn() // 执行一次函数,这里会触发get拦截
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
effect(() => {
console.log('efffect run')
console.log(obj.foo) // 这里会触发get拦截依赖收集
})
obj.foo++ // 期望这里执行后会触发上面收集的依赖
2.2.3 依赖收集track
建立数据和副作用函数之间的联系
function track(target, key) {
if(!activeEffect) return
let depsMap = bucket.get(target)
if (!depsMap) {
bucket.set(target, depsMap = new Map())
}
let deps = depsMap.get(key)
if (!deps) {
depsMap.set(key, deps = new Set())
}
deps.add(activeEffect)
activeEffect.deps.push(deps) // key和effect之间建立双向联系,方便在取出副作用函数的时候清除相关依赖
}
2.2.4 数据改变取出依赖执行trigger
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key) // effects就是要执行的副作用函数集合,是个Set
const effectToRun = new Set()
effects && effects.forEach(effectFn => { // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
if (activeEffect !== effectFn) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(effectFn => {
effectFn()
})
}
2.2.5 清除依赖函数
function cleanUp(effectFn) {
for (let i = 0, len = effectFn.deps.length; i < len; i++) { // 依赖在deps中
const deps = effectFn.deps[i]
deps.delete(effectFn)
}
effectFn.deps.length = 0 // 清除完依赖后将他置空
}
到这里就可以运行了,可看到先打印出obj.foo的值为1,obj.foo改变之后又触发了effect执行,打印出obj.foo的值为2
2.2.6 副作用函数添加options,实现computed
现在的effect函数写的比较死,不够灵活,副作用函数应该是能够自定义执行方式的,因此先给,effect添加第二个参数options自定义配置。在执行的时候使用自定义配置
function effect(fn, options = {}) {
const effectFn = () => {
cleanUp(effectFn) // 执行副作用函数前要清除相关依赖,依赖在deps中,后面触发get又会收集的
activeEffect = effectFn // 将副作用函数用activeEffect记录下来,后面收集和执行的时候用
effectStack.push(effectFn) // 用effectStack套一层是为了防止effect嵌套导致外层数据丢失
const res = fn() // 执行一次函数,这里会触发get拦截
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
return res // 新增,注意这里要把值返回,不然computed拉拿不到值
}
effectFn.deps = []
effectFn.options = options // 将自定义的配置保存起来
if(options.lazy) { // 新增,如果有lazy就返回函数后面执行,增强可控性,可利用他实现watch
return effectFn
}
effectFn()
}
function trigger(target, key) {
const depsMap = bucket.get(target)
if (!depsMap) return
const effects = depsMap.get(key) // effects就是要执行的副作用函数集合,是个Set
const effectToRun = new Set()
effects && effects.forEach(effectFn => { // 执行前过滤一下,防止get的同时set(例如++操作)导致的死循环
if (activeEffect !== effectFn) {
effectToRun.add(effectFn)
}
})
effectToRun && effectToRun.forEach(effectFn => {
if (effectFn.options.scheduler) { // 有自定义调度器就用自定义的调度器
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
期望watch实现这样效果
const o = computed(() => {
return obj.foo + obj.bar
})
console.log(o.value) // 期望输出3
effect(() => {
console.log(o.value)
})
obj.foo++ // 自动执行副作用函数,期望输出4
下面实现watch。
- watch里面显示利用effect的options的lazy配置,获取到未执行的副作用函数,后面利用他来获取副作用函数的值,也就是计算属性的值;
- 利用options的调度器,在数据获取的时候为其添加响应式,保证watch的返回的值也是响应式的
function computed(getter) {
let value, // 记录值
dirty = true // 是否需要重新计算
const effectFn = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true // 副作用函数重新触发了,证明值改变了,需要重新计算
trigger(obj, 'value') // 计算属性也是响应式的
}
})
const obj = {
get value() {
if (dirty) {
value = effectFn()
dirty = false
}
track(obj, 'value') // 要保证计算属性也是响应式的,因此需要执行track, 第二个参数为避免重复最好为Symbol类型
return value
}
}
value = effectFn() // 初始化值
return obj
}
最终结果,可以看到计算属性生效并且是响应式的
2.2.7 实现watch
期望像下面这样实现watch
watch(()=>{
return obj.foo
}, (oldValue, newValue) => {
console.log('oldValue', oldValue, 'newValue', newValue)
})
obj.foo++
obj.foo++
obj.foo++
obj.foo++ // 期望在obj.foo修改后能触发watch的回调
实现方式和computed差不多,但是会增加回调函数
function watch(source, cb) {
let getter,
oldValue,
newValue // 增加了newValue,用来给回调
if (typeof source === 'function') {
getter = source
} else {
getter = traverse(source)
}
const effectFn = effect(() => {
return getter()
}, {
lazy: true,
scheduler: () => {
newValue = effectFn()
cb(oldValue, newValue)
oldValue = newValue
}
})
oldValue = effectFn()
}
后续计划
1. 副作用函数添加options配置,利用他实现computed。
2. 进一步实现watch