Reactivity 模块

安装响应式模块

shell 复制代码
pnpm install @vue/reactivity -w
html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="app"></div>
</body>
<script type="module">
  import {
    reactive,
    effect
  } from '/node_modules/@vue/reactivity/dist/reactivity.esm-browser.js';
  const state = reactive({ name: 'erxiao', age: 30 });
  effect(() => {
    // 副作用函数 默认执行一次,响应式数据变化后再次执行
    app.innerHTML = state.name + '今年' + state.age + '岁了';
  });
  setTimeout(() => {
    state.age++;
  }, 1000);
</script>

</html>

reactive方法会将对象变成 proxy 对象, effect中使用reactive对象时会进行依赖收集,稍后属性变化时会重新执行effect函数~。

1.编写 reactive 函数

typescript 复制代码
import { isObject } from "@vue/shared";

/**
 * 创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function reactive(target: object) {
  return createReactiveObject(target)
}

/**
 * 统一创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function createReactiveObject(target: object) {
  
}

由此可知这些方法接受的参数必须是一个对象类型。否则没有任何效果

typescript 复制代码
const reactiveMap = new WeakMap(); // 缓存列表
const mutableHandlers: ProxyHandler<object> = {
	get(target, key, receiver) {
		// 等会谁来取值就做依赖收集
		const res = Reflect.get(target, key, receiver);
		return res;
	},
	set(target, key, value, receiver) {
		// 等会赋值的时候可以重新触发effect执行
		const result = Reflect.set(target, key, value, receiver);
		return result;
	}
};
function createReactiveObject(target: object) {
	if (!isObject(target)) {
		return target;
	}
	const exisitingProxy = reactiveMap.get(target); // 如果已经代理过则直接返回代理后的对象
	if (exisitingProxy) {
		return exisitingProxy;
	}
	const proxy = new Proxy(target, mutableHandlers); // 对对象进行代理
	reactiveMap.set(target, proxy);
	return proxy;
}

这里必须要使用 Reflect 进行操作,保证 this 指向永远指向代理对象

typescript 复制代码
import { isObject } from "@vue/shared";

/**
 * 统一创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function createReactiveObject(target: object) {
  // 统一判断是否是对象
  if (!isObject(target)) return target
  // 如果已经代理过,则直接返回代理对象
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) return existingProxy
  // 代理
  const proxy = new Proxy(target, mutableHandlers)
  // 缓存代理对象
  reactiveMap.set(target, proxy)
  return proxy
}

/**
 * 代理对象的处理器
 */
const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver)
    return res
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    return res
  }
}

将对象使用 proxy 进行代理,如果对象已经被代理过,再次重复代理则返回上次代理结果。 那么,如果将一个代理对象传入呢?

typescript 复制代码
import { isObject } from "@vue/shared";

const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive'
}

/**
 * 统一创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function createReactiveObject(target: object) {
  if (target[ReactiveFlags.IS_REACTIVE]) {
    // 在创建响应式对象时先进行取值,看是否已经是响应式对象
    return target
  }
  // 统一判断是否是对象
  if (!isObject(target)) return target
  // 如果已经代理过,则直接返回代理对象
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) return existingProxy
  // 代理
  const proxy = new Proxy(target, mutableHandlers)
  // 缓存代理对象
  reactiveMap.set(target, proxy)
  return proxy
}

/**
 * 代理对象的处理器
 */
const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true
    }
    const res = Reflect.get(target, key, receiver)
    return res
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    return res
  }
}

这样我们防止重复代理就做好了~~~

这里我们为了代码方便维护,我们将mutableHandlers抽离出去到baseHandlers.ts中。

typescript 复制代码
import { isObject } from "@vue/shared";
import { mutableHandlers, ReactiveFlags } from "./baseHandlers";

/**
 * 创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function reactive(target: object) {
  return createReactiveObject(target)
}


const reactiveMap = new WeakMap()


/**
 * 统一创建响应式对象
 * @param target 目标对象
 * @returns 代理对象
 */
export function createReactiveObject(target: object) {
  if (target[ReactiveFlags.IS_REACTIVE]) {
    // 在创建响应式对象时先进行取值,看是否已经是响应式对象
    return target
  }
  // 统一判断是否是对象
  if (!isObject(target)) return target
  // 如果已经代理过,则直接返回代理对象
  const existingProxy = reactiveMap.get(target)
  if (existingProxy) return existingProxy
  // 代理
  const proxy = new Proxy(target, mutableHandlers)
  // 缓存代理对象
  reactiveMap.set(target, proxy)
  return proxy
}
typescript 复制代码
const enum ReactiveFlags {
  IS_REACTIVE = '__v_isReactive'
}
/**
 * 代理对象的处理器
 */
const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true
    }
    const res = Reflect.get(target, key, receiver)
    return res
  },
  set(target, key, value, receiver) {
    const res = Reflect.set(target, key, value, receiver)
    return res
  }
}

export {
  mutableHandlers,
  ReactiveFlags
}

2.编写 effect 函数

typescript 复制代码
/**
 * 创建effect
 * @param fn 回掉函数
 * @param options 配置项
 */
export function effect(fn, options?) {
  // 创建ReactiveEffect实例,传入回调函数和调度器
  const _effect = new ReactiveEffect(fn, () => {
    _effect.run()
  })
  // 立即执行一次副作用函数(首次依赖收集)
  _effect.run()
}


export let activeEffect: ReactiveEffect | undefined = undefined
/**
 * 依赖收集
 */
class ReactiveEffect {
  public active = true

  constructor(public fn, public scheduler) { }

  run() {
    console.log('run')
    // 非激活状态直接执行函数
    if (!this.active) return this.fn()
    // 设置当前活跃的effect
    let lastEffect = activeEffect
    try {
      // 设置当前活跃的effect
      activeEffect = this
      // 执行回调函数
      return this.fn()
    } finally {
      // 执行完恢复上一个活跃的effect
      activeEffect = lastEffect
    }
  }
}

3.依赖收集

默认执行effect时会对属性,进行依赖收集

typescript 复制代码
const mutableHandlers: ProxyHandler<object> = {
  get(target, key, receiver) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true
    }
    // 取值的时候应该让响应式数据与effect函数建立联系
    const res = Reflect.get(target, key, receiver)
    track(target, key);  // 依赖收集
    return res
  },
}

4.track 方法实现

reactiveEffect.ts

typescript 复制代码
const targetMap = new WeakMap(); // 记录依赖关系
export const createDep = (cleanup, key) => {
	const dep = new Map() as any;
	dep.cleanup = cleanup; // 给清理依赖埋下伏笔
	dep.name = key;
	return dep;
};

export function track(target, key) {
  // {name: 'erxiao', age: 30} name
  //console.log('track', target, key)
  // 如果存在活跃的effect,则进行依赖收集
  if (activeEffect) {
    // console.log('track', target, key)
    let depsMap = targetMap.get(target)
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (!dep) {
      depsMap.set(key, dep = createDep(() => depsMap.delete(key), key))
    }
    trackEffect(activeEffect, dep)
  }
}

将属性和对应的 effect 维护成映射关系,后续属性变化可以触发对应的 effect 函数重新run

effect.ts

typescript 复制代码
class ReactiveEffect {
	_depsLength = 0; // 用于记录依赖的个数
	_trackId = 0; // 用于记录收集的次数
	deps = [];
	// ...
}
export function trackEffect(effect, dep) {
	// 属性记住effect
	dep.set(effect, effect._trackId);
	// effect记住依赖
	effect.deps[effect._depsLength++] = dep;
}

5.触发更新

typescript 复制代码
  set(target, key, value, receiver) {
    // 设置值的时候应该让对应的effect重新执行
    let oldValue = target[key]
    const res = Reflect.set(target, key, value, receiver)
    if (oldValue !== value) {
      trigger(target, key, value, oldValue)
    }
    return res
  }

reactiveEffect.ts

typescript 复制代码
export function trigger(target, key, value, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    triggerEffect(dep)
  }
}

effect.ts

typescript 复制代码
export function triggerEffects(dep) {
	for (const effect of dep.keys()) {
		if (effect.scheduler) {
			effect.scheduler();
		}
	}
}

6.依赖清除

在渲染时我们要避免副作用函数产生的遗留

typescript 复制代码
const state = reactive({ flag: true, name: 'erxiao', 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);

export class ReactiveEffect {
	// ...
	run() {
		// ...
		try {
			activeEffect = this;
			preCleanEffect(this); // 每次渲染前重新进行依赖收集
			return this.fn();
		} finally {
			activeEffect = lastEffect;
		}
	}
}

function preCleanEffect(effect) {
	// 执行函数前先进行清理操作
	effect._trackId++;
	effect._depsLength = 0;
}

重新进行依赖收集时,需要做比对

typescript 复制代码
export function trackEffect(effect, dep) {
	// 如果一个属性多次收集,需要排除
	if (dep.get(effect) !== effect._trackId) {
		// 更新id
		dep.set(effect, effect._trackId);
		//  获取上次的依赖,比对是否有变化
		const oldDep = effect.deps[effect._depsLength];
		// {flag, name} 1
		// {flag, age}  2
		// 如果有变化,需要清理以前的,并且塞入新的
		if (oldDep !== dep) {
			if (oldDep) {
				cleanDepEffect(oldDep, effect);
			}
			// 记录effect对应的依赖
			effect.deps[effect._depsLength++] = dep;
		} else {
			effect._depsLength++;
		}
	}
}

删除掉收集器中对应的 effect

typescript 复制代码
function cleanDepEffect(dep, effect) {
	dep.delete(effect);
	if (dep.size === 0) {
		dep.cleanup(); // 清理此属性
	}
}

销毁多于的依赖

typescript 复制代码
try {
	activeEffect = this;
	preCleanEffect(this); // 每次渲染前重新进行依赖收集
	return this.fn();
} finally {
	postCleanEffect(this); // 清理依赖
	activeEffect = lastEffect;
}

function postCleanEffect(effect) {
	// 重新做收集后,看依赖列表有没有增加,有增加就要删除 (map是不能添加重复的)
	if (effect.deps.length > effect._depsLength) {
		// 仅处理多出来的删除即可
		for (let i = effect._depsLength; i < effect.deps.length; i++) {
			cleanDepEffect(effect.deps[i], effect);
		}
		effect.deps.length = effect._depsLength;
	}
}

7.调度执行

trigger触发时,我们可以自己决定副作用函数执行的时机、次数、及执行方式

typescript 复制代码
export function effect(fn, options?) {
	// 创建一个响应式effect 数据变化后可以重新执行

	// 创建一个effect,只要依赖的属性变化了就要执行回调
	const _effect = new ReactiveEffect(fn, () => {
		_effect.run();
	});
	_effect.run();

	if (options) {
		// 合并参数
		Object.assign(_effect, options);
	}
	const runner = _effect.run.bind(_effect);
	runner.effect = _effect;
	return runner; // 返回run方法
}

8.防止递归调用

如果本次在执行effect的时候更新了数据 不会触发effect的更新

typescript 复制代码
effect(() => {
  document.getElementById('app').innerHTML = state.name
  state.name = Math.random()
})

通过 running 属性防止递归调用

typescript 复制代码
class ReactiveEffect {
  public active = true
  // 记录当前effect执行了几次
  _trackId = 0
  deps = [];
  _depsLength = 0; // 用于记录依赖的个数
  _running = 0
  constructor(public fn, public scheduler) { }

  run() {
    // 非激活状态直接执行函数
    if (!this.active) return this.fn()
    // 设置当前活跃的effect
    let lastEffect = activeEffect
    try {
      // 设置当前活跃的effect
      activeEffect = this
      // 在收集依赖前清空上一个活跃的effect的依赖
      preCleanEffect(this)
      this._running++
      // 执行回调函数 收集依赖
      return this.fn()
    } finally {
      this._running--
      // 删除多余的依赖
      postCleanEffect(this)
      // 执行完恢复上一个活跃的effect
      activeEffect = lastEffect
    }
  }
}
typescript 复制代码
export function triggerEffect(dep) {
  for (const effect of dep.keys()) {
    if (effect.scheduler) {
      // 判断是否在执行 如果在执行就不执行
      if (!effect._running)
        effect.scheduler();
    }
  }
}

9.深度代理

typescript 复制代码
effect(() => {
  document.getElementById('app').innerHTML = state.user.city
})

setTimeout(() => {
  state.user.city = 'beijing'
}, 1000);
typescript 复制代码
get(target, key, receiver) {
  if (key === ReactiveFlags.IS_REACTIVE) {
    return true
  }
  // 取值的时候应该让响应式数据与effect函数建立联系
  const res = Reflect.get(target, key, receiver)
  track(target, key);  // 依赖收集
  // 判断取值是否是对象
  if (isObject(res)) return reactive(res)
  return res
},

当取值时返回的值是对象,则返回这个对象的代理对象,从而实现深度代理

相关推荐
豐儀麟阁贵6 分钟前
8.5在方法中抛出异常
java·开发语言·前端·算法
zengyuhan50336 分钟前
Windows BLE 开发指南(Rust windows-rs)
前端·rust
醉方休39 分钟前
Webpack loader 的执行机制
前端·webpack·rust
前端老宋Running1 小时前
一次从“卡顿地狱”到“丝般顺滑”的 React 搜索优化实战
前端·react.js·掘金日报
隔壁的大叔1 小时前
如何自己构建一个Markdown增量渲染器
前端·javascript
用户4445543654261 小时前
Android的自定义View
前端
WILLF1 小时前
HTML iframe 标签
前端·javascript
枫,为落叶1 小时前
Axios使用教程(一)
前端
小章鱼学前端1 小时前
2025 年最新 Fabric.js 实战:一个完整可上线的图片选区标注组件(含全部源码).
前端·vue.js
ohyeah1 小时前
JavaScript 词法作用域、作用域链与闭包:从代码看机制
前端·javascript