副作用函数 (
effect执行渲染了页面
) 如果此函数依赖的数据发生了变化 会重新执行
使用方法
const {effect, reactive} = VueReactivity
// effect 代表
// reactive 将数据变成相应式的 proxy
const state= reactive({name: 'lyp',age:'30'})
console.log(state)
effect(()=>{ // 此 effect 会默认执行一次,对响应式数据取值(取值的过程中数据会依赖当前的effecgt)
state.age = Math.random()
document.getElementById('app').innerHTML = state.name + '今年' + state.age +'岁了'
})
setTimeout(() => {
state.age++
}, 1000)
逻辑
- 1、默认执行一次
- 2、如果非激活的状态 只需要执行函数 不是的话 把
activeEffect
暴露到全局 方便别的模块取到 - 3、嵌套执行处理
- 4、
执行的时候 获取数据 进行依赖收集
- 5、每次执行
effect
的时候都要将之前收集的内容清空 - 6、
停止effect的实现
在 作用域effect
(effectScope)中有用到 - 7、更新的时候判断是否有调度函数 实现组件的异步更新
入口方法
export function effect(fn, options:any = {}) {
// 这里fn 可以根据状态变化重新执行, effect可以嵌套写
// 创建响应式的 effect
const _effect = new ReactiveEffect(fn, options.scheduler)
_effect.run() // 默认执行一次
const runner = _effect.run.bind(_effect) // 绑定this指向
runner.effect = runner // 将effect 挂在到runner函数上 才能找到stop
return runner
}
编写effect函数
export let activeEffect = undefined;// 当前正在执行的effect
class ReactiveEffect {
// 这里表示在实例上新增了active属性
public active = true // 这个 effect默认是激活状态
deps = []; // 收集effect中使用到的属性
public parent = null // 用来存储actEffect上一层的effect
// 用户传递的参数 也会到this上 this.fn = fn
constructor(public fn, public scheduler) {
}
run() {
// 如果非激活的状态 只需要执行函数 不需要进行依赖收集
if (!this.active) {
return this.fn();
}
// 这里就要依赖收集 核心就是 当前的effect 和稍后渲染的属性关联在一起
try {
// 存储前一个 effect赋值给 parent 第一次是null(如果是嵌套的effect会滞后一层)
this.parent = activeEffect;
// 更新activeEffect 设置成正在激活 暴露给外部
activeEffect = this;
// 这里需要在每次effect执行之前 将之前收集的内容清空 activeEffect.deps = [Set[],Set[]]
cleanupEffect(this)
return this.fn();
} finally {
// 执行完毕后还原activeEffect 用parent去还原(parent 是上一层的effect)
activeEffect = this.parent;
}
}
stop(){
if(this.active){
cleanupEffect(this) // 清空依赖
this.active = false // 停止
}
}
}
export function effect(fn, options?) {
const _effect = new ReactiveEffect(fn); // 创建响应式effect
_effect.run(); // 让响应式effect默认执行
}
嵌套执行处理 逻辑
老方法 使用栈给最后一个赋值 [e1] -> 进入e2(加入[e1,e2]) -> e2结束删除e2 ([e1])
effect(()=>{ //effect1
state.name
effect(()=>{ //effect2
state.age
})
state.address //effect1
})
// 这个执行流程 类似于树形结构 记录parent
effect(()=>{ // parent = null activeEffect = e1
state.name //name ->e1
effect(()=>{ // parent = e1 activeEffect = e2
state.age
})
state.address // e2结束 activeEffect=this.parent=e1
}) //e1结束 activeEffect=this.parent=null
依赖收集
默认执行effect
时因为取了一次值会调用属性的get
方法并且将当前的activeEffect
暴露到了外边,因此会对属性,进行依赖收集一个
effect
对应多个属性 一个属性对应多个effect
;多对多的关系
实现原理
对象 的 某个属性 被 多个effect 依赖
WeekMap
= {对象target
: Map{对象的属性key
: Set[多个effect
]} }正向记录指:
属性
记录了所有的effect
;反向记录effect
记录被哪些属性
收集过,作用是为了方便清理依赖
// 上一章reactive模块 的get方法
get(target, key, receiver) {
if (key === ReactiveFlags.IS_REACTIVE) {
return true;
}
const res = Reflect.get(target, key, receiver);
track(target, 'get', key); // 依赖收集
return res;
}
一共三层:targetMap
(类型WeekMap)、depsMap
(类型Map)、dep
(类型Set)
// 依赖收集
const targetMap = new WeakMap(); // 记录依赖关系
export function track(target, type, key) {
if (activeEffect) { // 存在activeEffect才继续执行
let depsMap = targetMap.get(target); // {对象:map} 第一次没有
if (!depsMap) { // 没有的话记录依赖
// WeakMap: {target: Map}; depsMap = Map{}
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key); // 在map中查找是否对属性做了依赖收集、 第一次没有
if (!dep) { // 没有的话 记录
depsMap.set(key, (dep = new Set()))// WeakMap{target{ key :Set[]}}; dep = Set[]
}
let shouldTrack = dep.has(activeEffect) // 是否已收集 Set中不存在activeEffect 就存进去、第一次没有记录
if (!shouldTrack) {
dep.add(activeEffect);
// 让effect记住对应的dep(反向记录),这样后续可以用于清理 activeEffect.deps= [Set,Set]
// 记录的是每个属性对应的effect
activeEffect.deps.push(dep);
}
}
}
将属性和对应的
effect
维护成映射关系,后续属性变化可以触发对应的effect函数
重新run
触发更新
// 上一章reactive模块 的set方法
set(target, key, value, receiver) {
// 等会赋值的时候可以重新触发effect执行
let oldValue = target[key]
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, 'set', key, value, oldValue)
}
return result;
}
export function trigger(target, type, key?, newValue?, oldValue?) {
const depsMap = targetMap.get(target); // 获取对应的映射表 没人依赖 就是没人使用不用更新
if (!depsMap) {
return
}
let effects = depsMap.get(key); // 找到了 属性对应的effect 没有的话 就是没人使用不用更新
if(effects){
// 永远在执行之前 先拷贝一份在执行 防止死循环
effects = new Set(effects)
effects.forEach(effect => {
//effect !== activeEffect 是为了 防止死循环 因为activeEffect 是当前的effect 默认已经执行了;
// run执行用户操作
if (effect !== activeEffect){
if(effect.scheduler){
effect.scheduler() // 如果用户传了scheduler 就让scheduler()
}else{
effect.run() // 否则默认刷新视图
}
};
})
triggerEffects(effects)
}
}
分支切换与cleanup
在渲染时我们要避免副作用函数产生的遗留
const state = reactive({ flag: true, name: 'lyp', age: 30 })
effect(() => { // 副作用函数 (effect执行渲染了页面)
console.log('render')
document.body.innerHTML = state.flag ? state.name : state.age
});
setTimeout(() => {
state.flag = false;
setTimeout(() => {
console.log('修改name,原则上不更新')
state.name = 'zf'
}, 1000);
}, 1000)
function cleanupEffect(effect) {
// 清理effect deps是effect对应的多个属性
// deps的某一项 里面装的是该属性(例如name、age)对应的effect
const { deps } = effect;
// 循环每个属性 清理所有属性 对 当前effect 的记录
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
// 清理 当前effect 对 所有属性的记录
effect.deps.length = 0;
}
class ReactiveEffect {
active = true;
deps = []; // 收集effect中使用到的属性
parent = undefined;
constructor(public fn) { }
run() {
try {
this.parent = activeEffect; // 当前的effect就是他的父亲
activeEffect = this; // 设置成正在激活的是当前effect
// 这里需要在每次effect执行之前 将之前收集的内容清空 activeEffect.deps = [Set[],Set[]]
cleanupEffect(this);
return this.fn(); // 先清理在运行
}
}
}
set死循环
触发时会进行清理操作(清理effect),在重新进行收集(收集effect)。在循环过程中会导致死循环。
let effect = () => {};
let s = new Set([effect])
s.forEach(item=>{s.delete(effect); s.add(effect)}); // 这样就导致死循环了
stop 的使用
let runner = effect(() => { // 副作用函数 (effect执行渲染了页面)
console.log('render')
});
runner.effect.stop()
调度执行
调度的使用 实现组件的异步更新
let waiting = false
let runner = effect(() => { // 副作用函数 (effect执行渲染了页面)
console.log('render')
}, {
scheduler(){ // 调度 如何更新自己决定
if(!waiting){
waiting = true
setTimeout(() => {
runner()
waiting = false
})
}
}
});
调度实现
trigger
触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式
export function effect(fn, options:any = {}) {
// 创建响应式effect 接收scheduler
const _effect = new ReactiveEffect(fn,options.scheduler);
}
export function trigger(target, type, key?, newValue?, oldValue?) {
const depsMap = targetMap.get(target);
if (!depsMap) {
return
}
let effects = depsMap.get(key);
if (effects) {
effects = new Set(effects);
for (const effect of effects) {
if (effect !== activeEffect) {
if(effect.scheduler){ // 如果有调度函数则执行调度函数
effect.scheduler()
}else{
effect.run();
}
}
}
}
}