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
},

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

相关推荐
excel4 分钟前
webpack 核心编译器 七 节
前端
一只月月鸟呀11 分钟前
HTML中数字和字母不换行显示
前端·html·css3
天下代码客31 分钟前
【八股】介绍Promise(ES6引入)
前端·ecmascript·es6
lb291744 分钟前
CSS 3D变换,transform:translateZ()
前端·css·3d
啊阿狸不会拉杆1 小时前
第二十二章:Python-NLTK库:自然语言处理
前端·python·自然语言处理
萧寂1731 小时前
html实现手势密码
前端·css·html
excel1 小时前
webpack 核心编译器 八 节
前端
JoyZ1 小时前
AI的时代学习还有意义吗?
前端
好_快1 小时前
Lodash源码阅读-getSymbolsIn
前端·javascript·源码阅读