响应式原理文章总结
vue源码阅读计划:
1.熟悉vue原理,面试能回答深层源码问题!
2.熟悉代码原理,写业务代码更能理解,找bug能力更上一层楼
3.Vue二次开发
4.在面试官同事朋友装逼~
响应式原理
响应式原理问题
问题概况
- 代码1.1是响应式原理代码,为啥这样写?
答:暂略 - vue3为什么要用Proxy?vue2用Object.defineProperty有啥缺点? 那么Proxy也有啥缺点?
答:暂略
//代码1.1
const data = {
text: '123'
}
let activeEffect = void 0;
let effectMap = new Set()
const obj = new Proxy(data, {
get (target, key) {
//收集依赖
effectMap.add(activeEffect)
return target[key]
},
set (target, key, newval) {
target[key] = newval
//通知依赖变化
effectMap.forEach(fn => fn())
}
})
function effect (fn) {
activeEffect = fn
fn();
}
effect(() => {
console.log(obj.text)
document.getElementById('app').textContent = obj.text
})
setTimeout(() => {
obj.text = 'Hello World'
}, 1000)
问题答案
-
代码1.1是响应式原理代码,为啥这样写?
答:get收集依赖,set通知依赖,get是读取数据,set是设置数据,要通知到对应数据的副作用函数执行 -
vue3为什么要用Proxy?vue2用Object.defineProperty有啥缺点? 那么Proxy也有啥缺点? 答:Proxy能代理对象的任何操作,然后能监听到数组下标等操作,而Object.defineProperty不能,虽然vue2对数组原型方法进行扩展来通知数组触发副作用函数,但是还是不能通过修改下标来触发副作用函数,Proxy则有一个this指向错误的缺点,可以使用Reflect来解决this这个问题
响应式代码数据结构设计
问题概况
- WeakMap和Map区别是什么?
答:暂略 - 代码1.2中设计WeakMap => Map => Set数据结构,为什么这样设计? 答:暂略
//代码1.2
const data = {
text: '123'
}
let activeEffect = void 0;
let bucket = new WeakMap();
const obj = new Proxy(data, {
get (target, key) {
//收集依赖
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
effectMap.add(activeEffect)
return target[key]
},
set (target, key, newval) {
target[key] = newval
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
effectMap.forEach(fn => fn())
}
})
function effect (fn) {
activeEffect = fn
fn();
}
effect(() => {
console.log(obj.text)
document.getElementById('app').textContent = obj.text
})
setTimeout(() => {
obj.text = 'Hello World'
}, 1000)
问题答案
-
WeakMap和Map区别是什么?
答:WeakMap是弱引用,而Map是强引用,- WeakMap只能用对象作为键值,Map可以用对象或基本数据类型作为键值
- WeakMap对对象进行引用计数,如没引用,会对其进行垃圾回收,防止内存泄漏
- Map对对应值会强引用,只能手动清除,如不手动清除,则一直保存其内存,即使没引用,也不会进行垃圾回收,会导致内存泄漏
-
代码1.2中设计WeakMap => Map => Set数据结构,为什么这样设计? 答:WeakMap是代表data一定是个对象,vue代码的data数据结构设计是 { key: value },key有可能是基础数据类型,所以是Map,value则是副作用数据函数,防止重复添加,所以是Set,
组件的data() { return () => {} }, 该函数返回一个对象,则作为WeakMap,所以data的return () => {}会进行垃圾回收
分支切换和cleanup
预期行为 如果当下面伪代码这种情况,预期行为应该是?
const obj = {ok: false, text: '123'}
effect(() => {
body.innerHTML = obj.ok ? obj.text : 'default'
})
obj.text = '456'; //不应该触发副作用函数执行,因为没效果~
解决思路 重新清空cleanup副作用函数依赖,并重新增加副作用函数依赖~~
- effectFn中清空依赖关系 - 删除依赖关系
- Proxy的get增加依赖关系 - 重新获取依赖关系
- proxy的set遍历副作用函数 - 遍历中会获取依赖关系,导致爆栈~
问题概况
- 代码1.3中为啥要单独增加deps来管理副作用函数依赖呢? 答:暂略
- 代码1.3中Proxy的set为啥要new Set一个数据结构来处理副作用函数呢? 答:暂略
//代码1.3
const data = {
text: '123',
ok: true
}
let activeEffect = void 0;
let bucket = new WeakMap();
const obj = new Proxy(data, {
get (target, key) {
//收集依赖
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
effectMap.add(activeEffect) //添加副作用函数 - new Set的代码就是用来处理这个
activeEffect.deps.push(effectMap) //重新添加作用函数依赖
return target[key]
},
set (target, key, newval) {
target[key] = newval
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
const effectsToRun = new Set(effectMap) //重新获取一个依赖副作用数组
effectsToRun.forEach(fn => fn()) //执行,防止执行的时候----获取副作用添加
}
})
function cleanup (effectFn) {
//清空依赖关系
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn) //删除当前副作用函数依赖
}
effectFn.deps.length = 0;
}
function effect (fn) {
const effectFn = () => {
cleanup(effectFn) //清除副作用依赖关系
activeEffect = effectFn
fn() //副作用函数执行, 追踪或触发函数,追踪就增加当前副作用函数依赖
}
effectFn.deps = [] //初始化当前副作用函数的依赖关系
effectFn();
}
effect(() => {
console.log(obj)
document.getElementById('app').textContent = obj.ok ? obj.text : 'default' //触发track依赖
})
setTimeout(() => {
obj.text = 'Hello World'
// obj.ok = true
}, 1000)
问题答案
- 代码1.3中为啥要单独增加deps来管理副作用函数依赖呢? 答:因为要单独管理副作用函数依赖,实现分支切换(依赖关系重新收集)
- 代码1.3中Proxy的set为啥要new Set一个数据结构来处理副作用函数呢? 答:因为副作用函数里面可能增加依赖,到Deps里面,导致死循环,因为是引用关系~
嵌套effect和effect栈
预期行为 如果当下面伪代码这种情况,预期行为应该是?
const obj = {foo: true, bar: true}
effect(() => {
effect(() => {
console.log(obj.bar)
})
console.log(obj.foo)
})
effect.foo = false; //但是执行obj.bar的副作用函数了
解决思路 通过栈来管理effect的嵌套关系,子函数嵌套就会进栈,等子函数执行完毕就出栈,直到栈顶为空~~类似javascript的函数栈
//代码1.4
const data = {
foo: "foo component render 1",
bar: "bar component render 1"
}
let activeEffect = void 0;
let bucket = new WeakMap();
const obj = new Proxy(data, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newval) {
target[key] = newval
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
const effectsToRun = new Set(effectMap)
effectsToRun.forEach(fn => fn())
}
})
const effectStack = [] //副作用函数栈
function effect (fn) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn) //进栈
fn()
effectStack.pop() //出栈
activeEffect = effectStack[effectStack.length - 1] //重新获取上一个activeEffect
}
effectFn.deps = []
effectFn();
}
var fooComponent, barComponent;
effect(() => {
console.log(obj.foo) //先执行
effect(() => {
barComponent = obj.bar
console.log(obj.bar)
})
fooComponent = obj.foo //后执行不符合预期,被子组件的副作用函数覆盖了
})
setTimeout(() => {
obj.foo = 'foo component render 2' //不符合预期,输出obj.bar了
// obj.bar = 'bar component render 2'
}, 500)
防止爆栈
预期行为 如果当下面伪代码这种情况,预期行为应该是? effect(() => { obj.n++; 或 obj.n = obj.n + 1; //死循环,先读取后执行 })
解决思路
- 防止执行等于activeEffect变量,因为在里面的时候不需要trigger,而外面可以执行trigger(set),但是不会track(get)
- 有效防止n++,死循环
问题概况
- i++在副作用函数(effect回调函数)会爆栈,为什么?
//代码1.5
const data = {
n: 1
}
//省略代码
function trigger (target, key) {
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
const effectsToRun = new Set()
effectMap && effectMap.forEach(effectFn => {
if (effectFn !== activeEffect) { //保护机制,如果副作用函数里面set值,那么不触发当前activeEffect函数,防止死循环,副作用函数外面读取不到
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(fn => fn())
}
effect(() => {
console.log(obj.n)
obj.n = 5;
})
obj.n++; //先get后set
// obj.n = obj.n + 1;
问题答案
- i++在副作用函数(effect回调函数)会爆栈,为什么? i++在副作用函数里面(effect回调函数),先读取后设置,读取的时候没有activeEffect不执行,但是设置的时候会执行对应的副作用函数,副作用函数里面死循环 i++,如此类推,所以只需要排除fn不是activeEffect即可。如果解决了在副作用函数(effect回调函数)里面设置值会怎样?是不会触发自身的effect重新渲染的。 所以副作用函数先设置再读取。读取再设置就是获取到oldVal旧值了
调度器
调度器可以做?
- 调度器需要改变数据的时候触发,可合并执行队列,多次改变数据只执行一次
- 调度器可以异步执行副作用函数
- 调度器可以当数据改变的生命周期(update)
//代码1.6调度器
const data = {
n: 1
}
function trigger (target, key) {
//...省略代码
const effectsToRun = new Set()
effectsToRun.forEach(fn => {
if (fn.options.scheduler) {
//核心代码
fn.options.scheduler(fn)
} else {
fn()
}
})
}
const jobQueue = new Set();
const p = Promise.resolve();
const isFlushing = false;
function flushJob () {
if (isFlushing) return //如果正在执行,那么退出
isFlushing = true
p.then(() => {
jobQueue.forEach(fn => fn())
}).finally(() => {
isFlushing = false
})
}
effect(() => {
console.log(obj.n)
}, {
scheduler: (fn) => {
console.log('数据改变前')
// setTimeout(fn) //把任务放到setTimeout中执行
jobQueue.add(fn)
flushJob() //如果处于刷新队列,那么不执行
} //调度器
})
obj.n++;
计算属性和lazy
问题概况
- 计算属性可以根据layz和调度器来实现,追踪对应数据
//代码1.7
const data = {
n: 1
}
//...省略代码
function computed (gettter) {
//缓存值
let result, dirty = true;
const effectFn = effect(gettter, {
lazy: true,
scheduler () {
if (!dirty) {
dirty = true
trigger(obj, 'value') //触发依赖值
}
}
})
const obj = {
get value () {
if (dirty) {
result = effectFn()
dirty = false
track(obj, 'value') //追踪依赖值
}
return result
}
}
return obj
}
//计算属性和lazy
const sum = computed(() => {
console.log('sum')
return obj.n + obj.n
})
const forSum = computed(() => {
console.log('forSum')
return sum.value
})
console.log(sum.value)
console.log(forSum.value)
obj.n = obj.n + 1;
console.log(sum.value)
console.log(forSum.value)
watch
watch可以做?
- 深度监听数据变化,做出改变后,执行副作用函数
- 数据竞态,只获取最后的finalData数据,丢弃过期数据
function watch (source, cb, options = {}) {
let getter;
if (typeof source === 'function') {
getter = source()
} else {
getter = () => traverse(source);
}
let cleanup
const onInvalidate = (fn) => {
cleanup = fn
}
const job = () => {
newval = effectFn();
if (cleanup) {
cleanup()
}
cb(newval, oldval, onInvalidate)
oldval = newval
}
let oldval, newval;
const effectFn = effect(() => getter(), {
scheduler () {
if (options.flush === 'post') {
const p = Promise.resolve()
p.then(job)
} else {
job()
}
},
lazy: true
})
if (options.immediate) {
job()
} else {
oldval = effectFn()
}
}
//递归读取全部值,并且不读取相同的值,防止循环引用
function traverse (value, seen = new Set()) {
if (seen.has(value) || typeof value !== 'object' || value === null) return;
seen.add(value)
for (let key in value) {
traverse(value[key], seen)
}
return value
}
//watch实现一个竞态请求代码
let finalData;
watch(obj, async (newval, oldval, onInvalidate) => {
console.log(newval, oldval, 'update...')
let expired = false
onInvalidate(() => {
expired = true
})
const res = await fetch('/api/xxx')
if (!expired) {
finalData = res;
}
},{
// immediate: true,
flush: 'pre', //pre | post | sync
})
obj.n++
相关对应代码函数
Proxy劫持对象
proxy代理对象操作
let activeEffect = void 0;
let bucket = new WeakMap();
const obj = new Proxy(data, {
get (target, key) {
track(target, key)
return target[key]
},
set (target, key, newval) {
target[key] = newval
trigger(target, key)
}
})
track
追踪数据信息~,添加对应副作用函数
function track (target, key) {
if (!activeEffect) return;
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
effectMap.add(activeEffect)
activeEffect.deps.push(effectMap)
}
trigger
触发副作用函数
function trigger (target, key) {
let targetMap = bucket.get(target)
if (!targetMap) {
bucket.set(target, (targetMap = new Map()))
}
let effectMap = targetMap.get(key)
if (!effectMap) {
targetMap.set(key, (effectMap = new Set()))
}
const effectsToRun = new Set()
effectMap && effectMap.forEach(effectFn => {
if (effectFn !== activeEffect) { //保护机制,如果副作用函数里面i++值,那么不触发当前activeEffect函数,防止死循环
effectsToRun.add(effectFn)
}
})
effectsToRun.forEach(fn => {
if (fn.options.scheduler) {
//调度器
fn.options.scheduler(fn)
} else {
fn()
}
})
}
jobQueue调度函数之防止多次执行队列
const jobQueue = new Set();
const p = Promise.resolve();
const isFlushing = false;
function flushJob () {
if (isFlushing) return //如果正在执行,那么退出
isFlushing = true
p.then(() => {
jobQueue.forEach(fn => fn())
}).finally(() => {
isFlushing = false
})
}
cleanup和effect
/**
目的:防止不必要的渲染
作用:每次执行effect清空依赖关系,防止obj.foo为false,obj.bar改变都渲染一次~,没必要
案例:effect(() => {
document.body.innerHTML = obj.foo ? obj.bar : 'default'
})
*/
function cleanup (effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
const deps = effectFn.deps[i];
deps.delete(effectFn)
}
effectFn.deps.length = 0;
}
/**
目的:实现一个effect嵌套的副作用函数栈
作用:可以实现组件的层级渲染
案例:effect(() => {
effect(() => {
temp1 = obj.foo
})
temp1 = obj.bar
})
*/
const effectStack = [] //副作用函数栈
function effect (fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn) //进栈
const res = fn()
effectStack.pop() //出栈
activeEffect = effectStack[effectStack.length - 1] //重新获取上一个activeEffect
return res
}
effectFn.deps = []
effectFn.options = options
if (!options.lazy) {
effectFn();
}
return effectFn
}
结语
- 1.响应式数据结构 该数据结构能合理存储对应的effect
- 2.封装响应式数据
- 3.分支切换和cleanup清除 切换分支需要清除依赖,并且重新增加依赖
- 4.嵌套effect 实现一个effects栈来实现组件的树级渲染,祖先级渲染必然导致子孙级渲染
- 5.避免无限递归 避免清空依赖Deps,重新收集的时候,收集activeEffect,不等于当前activeEffect就可以执行,同样这个要配合effect栈来使用,每次执行完,清空栈顶
- 6.调度执行,调度器实现多次改变数据,只会更新一次副作用函数,刷新队列,还可以将副作用函数放到不同的时机执行
- 7.计算属性computed和lazy, 通过lazy实现计算属性,并用一个对象的value实现依赖追踪函数,并缓存数据
- 8.watch的利用调度函数监听数据变化,以及立即执行、数据变化后的函数执行时机、过期副作用函数