响应式实现
- 实现响应式原理,需要知道数据被那些函数依赖
- 数据改变的时候去调用依赖他的函数
vue2
重写get和set方法,在调用数据的时候,将依赖进行收集,将依赖的函数执行
import Dep from "./dep";
export class Observer {
constructor(value) {
this.walk(value);
}
/**
* 遍历对象所有的属性,调用 defineReactive
* 拦截对象属性的 get 和 set 方法
*/
walk(obj) {
const keys = Object.keys(obj);
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]);
}
}
}
/**
* Define a reactive property on an Object.
*/
export function defineReactive(obj, key, val) {
const property = Object.getOwnPropertyDescriptor(obj, key);
// 读取用户可能自己定义了的 get、set
const getter = property && property.get;
const setter = property && property.set;
// val 没有传进来话进行手动赋值
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
/*********************************************/
const dep = new Dep(); // 持有一个 Dep 对象,用来保存所有依赖于该变量的 Watcher
/*********************************************/
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
/*********************************************/
// 1.这里需要去保存当前在执行的函数
if (Dep.target) {
dep.depend();
}
/*********************************************/
return value;
},
set: function reactiveSetter(newVal) {
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
/*********************************************/
// 2.将依赖当前数据依赖的函数执行
dep.notify();
/*********************************************/
},
});
}
将第一步和第二步的东西封装为dep
export default class Dep {
static target; //当前在执行的函数
subs; // 依赖的函数
constructor() {
this.subs = []; // 保存所有需要执行的函数
}
addSub(sub) {
this.subs.push(sub);
}
depend() {
// 触发 get 的时候走到这里
if (Dep.target) {
// 委托给 Dep.target 去调用 addSub
Dep.target.addDep(this);
}
}
notify() {
for (let i = 0, l = this.subs.length; i < l; i++) {
this.subs[i].update();
}
}
}
Dep.target = null; // 静态变量,全局唯一
保存当前正在执行的函数
import Dep from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher
let value;
try {
// 调用当前传进来的函数,触发对象属性的 get
value = this.getter.call();
} catch (e) {
throw e;
}
return value;
}
/**
* Add a dependency to this directive.
*/
addDep(dep) {
// 触发 get 后会走到这里,收集当前依赖
// 当前正在执行的函数的 Watcher 保存到 dep 中的 subs 中
dep.addSub(this);
}
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
// 修改对象属性值的时候触发 set,走到这里
update() {
this.run();
}
/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
run() {
this.get();
}
}
每个属性有一个subs数组,watcher会保存当前正在执行的函数,当读取属性的时候触发get,将当前watcher保存到subs数组中。
这里需要考虑两件事情,1.Dep中不要重复收集watcher, 2.重置,如果该属性对dep中的watcher没有影响,watcher中的updateComponent不会获得属性,需要将watcher将dep中删除
- 去重:
- 方案1: Dep中的subs数组换为set
- 方案2: Dep对象中引入id,Watcher对象中记录所有的Dep的id,下次重新收集的时候,如果dep中的id已经存在,就不再收集该watcher了
/*************改动***************************/ let uid = 0; /****************************************/ export default class Dep { static target; //当前在执行的函数 subs; // 依赖的函数 id; // Dep 对象标识 constructor() { /**************改动**************************/ this.id = uid++; /****************************************/ this.subs = []; // 保存所有需要执行的函数 } addSub(sub) { this.subs.push(sub); } depend() { if (Dep.target) { // 委托给 Dep.target 去调用 addSub Dep.target.addDep(this); } } notify() { for (let i = 0, l = this.subs.length; i < l; i++) { this.subs[i].update(); } } } Dep.target = null; // 静态变量,全局唯一
import Dep from "./dep"; export default class Watcher { constructor(Fn) { this.getter = Fn; /*************改动***************************/ this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id /****************************************/ this.get(); } /** * Evaluate the getter, and re-collect dependencies. */ get() { Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher let value; try { value = this.getter.call(); } catch (e) { throw e; } finally { this.cleanupDeps(); } return value; } /** * Add a dependency to this directive. */ addDep(dep) { /*************改动***************************/ const id = dep.id; if (!this.depIds.has(id)) { dep.addSub(this); } /****************************************/ } /** * Subscriber interface. * Will be called when a dependency changes. */ update() { this.run(); } /** * Scheduler job interface. * Will be called by the scheduler. */ run() { this.get(); } }
- 重置
- 全量式移除,保证watcher所影响的所有dep对象,当重新收集watcher之前,把watcher从记录中所有dep对象移除
- 增量式移除,重新收集依赖的时候,用一个新的变量记录所有dep对象,之后在和旧的dep对象列表对比,如果新的没有,旧的中有,就将当前watcher从该dep对象中移除
在watcher类中,引入this.deps来保存所有旧的dep对象,引入this.newDeps来保存所有新的Dep对象import { remove } from "./util"; /* export function remove(arr, item) { if (arr.length) { const index = arr.indexOf(item); if (index > -1) { return arr.splice(index, 1); } } } */ let uid = 0; export default class Dep { static target; //当前在执行的函数 subs; // 依赖的函数 id; // Dep 对象标识 constructor() { this.id = uid++; this.subs = []; // 保存所有需要执行的函数 } addSub(sub) { this.subs.push(sub); } /*************新增************************/ removeSub(sub) { remove(this.subs, sub); } /****************************************/ depend() { if (Dep.target) { // 委托给 Dep.target 去调用 addSub Dep.target.addDep(this); } } notify() { for (let i = 0, l = this.subs.length; i < l; i++) { this.subs[i].update(); } } } Dep.target = null; // 静态变量,全局唯一
import Dep from "./dep"; export default class Watcher { constructor(Fn) { this.getter = Fn; this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id /*************新增************************/ this.deps = []; this.newDeps = []; // 记录新一次的依赖 this.newDepIds = new Set(); /****************************************/ this.get(); } /** * Evaluate the getter, and re-collect dependencies. */ get() { Dep.target = this; // 保存包装了当前正在执行的函数的 Watcher let value; try { value = this.getter.call(); } catch (e) { throw e; } finally { /*************新增************************/ this.cleanupDeps(); /****************************************/ } return value; } /** * Add a dependency to this directive. */ addDep(dep) { const id = dep.id; /*************新增************************/ // 新的依赖已经存在的话,同样不需要继续保存 if (!this.newDepIds.has(id)) { this.newDepIds.add(id); this.newDeps.push(dep); if (!this.depIds.has(id)) { dep.addSub(this); } } /****************************************/ } /** * Clean up for dependency collection. */ /*************新增************************/ cleanupDeps() { let i = this.deps.length; // 比对新旧列表,找到旧列表里有,但新列表里没有,来移除相应 Watcher while (i--) { const dep = this.deps[i]; if (!this.newDepIds.has(dep.id)) { dep.removeSub(this); } } // 新的列表赋值给旧的,新的列表清空 let tmp = this.depIds; this.depIds = this.newDepIds; this.newDepIds = tmp; this.newDepIds.clear(); tmp = this.deps; this.deps = this.newDeps; this.newDeps = tmp; this.newDeps.length = 0; } /****************************************/ /** * Subscriber interface. * Will be called when a dependency changes. */ update() { this.run(); } /** * Scheduler job interface. * Will be called by the scheduler. */ run() { this.get(); } }
当vue开发中存在组件嵌套组件的情况
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
inner: "内部",
};
observe(data);
const updateMyComponent = () => {
console.log("子组件收到:", data.inner);
};
const updateParentComponent = () => {
new Watcher(updateMyComponent);
console.log("父组件收到:", data.text);
};
new Watcher(updateParentComponent);
data.text = "hello, liang";
new watcher首先保存当前函数,然后执行当前函数将全局的Dep.target赋值为watcher对象。但是当出现组件嵌套的时候,会出现bug。
这里使用栈来保存watcher。执行函数将当前watcher入栈,执行函数完毕之后将watcher弹出栈,Dep.target始终指向栈顶watcher,代表当前执行的函数
const targetStack = [];
export function pushTarget(target) {
targetStack.push(target);
Dep.target = target;
}
export function popTarget() {
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1]; // 赋值为栈顶元素
import { pushTarget, popTarget } from "./dep";
export default class Watcher {
constructor(Fn) {
this.getter = Fn;
this.depIds = new Set(); // 拥有 has 函数可以判断是否存在某个 id
this.deps = [];
this.newDeps = []; // 记录新一次的依赖
this.newDepIds = new Set();
this.get();
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
/************修改的地方*******************************/
pushTarget(this); // 保存包装了当前正在执行的函数的 Watcher
/*******************************************/
let value;
try {
value = this.getter.call();
} catch (e) {
throw e;
} finally {
/************修改的地方*******************************/
popTarget();
/*******************************************/
this.cleanupDeps();
}
return value;
}
...
}
import { observe } from "./reactive";
import Watcher from "./watcher";
const data = {
text: "hello, world",
};
observe(data);
let show = true;
const updateComponent = () => {
if (show) {
console.log(data.text);
show = false;
}
};
new Watcher(updateComponent);
new Watcher(() => console.log("依赖", data.text));
data.text = "123";
会出现报错,报错为cannot read properties of undefined.
这个watcher不依赖当前的splice方法,直接将dep中的subs数组元素进行删除。但是现在正在遍历sub数组
notify() {
for(let i = 0, l = this.subs.length; i < 1; i++) {
this.subs[i].update
}
}
将notify进行修改,就可以改正
notify() {
const subs = this.subs.slice()
for(let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
vue3
vue3响应式原理
ref: 接受一个内部值,返回一个响应式的,可以更改的ref对象,该对象只有一个指向其内部值的属性.value。主要是用来定义简单类型的数据比如字符串,数字,布尔值等单一的对象
const count = ref(0)
console.log(count.value) // 0
reactive(): 创建一个响应式对象,用于包装JavaScript对象和数组等复杂类型的数据。
const obj = reactive({ count: 0})
obj.count++
ref解包
const count = ref(1)
const obj = reactive({count})
console.log(obj.count === count.value) // true
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
const count = ref(1)
const obj = reactive({})
obj.count = count
重写get和set方法,在调用数据的时候,将依赖的函数执行,这里使用的是proxy代理函数
reactive
// 源对象和代理对象的映射表。原对象和代理对象关联起来
// 对象是响应式对象
export const reactiveMap = new WeakMap();
// 对象是只读对象
export const readonlyMap = new WeakMap();
// 对象是浅代理对象
export const shallowReadonlyMap = new WeakMap();
export function reactive(target) {
return createReactiveObject(target, reactiveMap, mutableHandlers);
}
function createReactiveObject(target, proxyMap, baseHandlers) {
// 核心就是 proxy
// 目的是可以侦听到用户 get 或者 set 的动作
// 如果命中的话就直接返回就好了
// 使用缓存做的优化点
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
const proxy = new Proxy(target, baseHandlers);
// 把创建好的 proxy 给存起来,
proxyMap.set(target, proxy);
return proxy;
}
这里稍微等下,下章继续,先看ref吧。
ref
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
export function ref(value) {
return createRef(value);
}
function createRef(value) {
const refImpl = new RefImpl(value);
return refImpl;
}
export class RefImpl {
...
constructor(value) {
this._rawValue = value;
this._value = convert(value);
}
get value() {
...
return this._value;
}
set value(newValue) {
// 当新的值不等于老的值的话,
// 那么才需要触发依赖
if (hasChanged(newValue, this._rawValue)) {
// 更新值
this._value = convert(newValue);
this._rawValue = newValue;
...
}
}
}
依赖收集和依赖的函数执行,这里需要封装函数
依赖收集trackRefValue
设置一个属性dep,存储依赖。
这里上面就是用的收集依赖的方案一,使用set方案收集依赖。这样就可以防止重复收集。
get value() {
// 收集依赖
trackRefValue(this); // 依赖收集
return this._value;
}
export class RefImpl {
public dep // 存储依赖
public __v_isRef = true // 表示为ref对象
constructor(value) {
this.dep = createDep()
}
}
// 这里的set对应的是vue2中的dep中的sub,但是vue2使用id来实现的,但是vue3使用的是es6中的set
export function createDep(effects?) {
const dep = new Set(effects);
return dep;
}
export function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
// ref = toRaw(ref)
trackEffect(
activeEffect,
ref.dep
)
}
}
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
) {
// ...
dep.set(effect, effect._trackId)
}
依赖执行triggerRefValue
set value(newValue) {
// 当新的值不等于老的值的话,那么才需要触发依赖
if (hasChanged(newValue, this._rawValue)) {
// 更新值
this._value = convert(newValue);
this._rawValue = newValue;
// 触发依赖
triggerRefValue(this, newVal, oldValue);
}
}
export function triggerRefValue(ref) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
triggerEffects(
dep,
dirtyLevel,
__DEV__
? {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal,
oldValue: oldVal,
}
: void 0,
)
}
triggerEffects(ref.dep);
}
export function triggerEffects(dep) {
// 执行收集到的所有的 effect 的 run 方法
for (const effect of dep) {
effect.trigger()
}
}
重置: 增量式移除,重新收集依赖的时候,用一个新的变量记录所有的dep,然后在旧的dep对象进行对比,如果新的没有,旧的中有,将当前effect移除,这一块也解决了上述vue2中最后一个bug:
// 检查当前副作用是否已经被这个依赖跟踪过
// 这里使用 effect._trackId 作为副作用的唯一标识符
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
) {
// 如果没有被跟踪过
if (dep.get(effect) !== effect._trackId) {
// 将给副作用与依赖关联起来
dep.set(effect, effect._trackId)
// 获取旧的依赖
const oldDep = effect.deps[effect._depsLength]
// 旧的依赖不是当前依赖
if (oldDep !== dep) {
if (oldDep) {
// 执行旧的依赖
cleanupDepEffect(oldDep, effect)
}
// 将新的dep添加到副作用的依赖列表中
effect.deps[effect._depsLength++] = dep
} else {
// 如果旧的依赖是我们要添加的dep,只需要简单的添加_depsLength
effect._depsLength++
}
}
}
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
const trackId = dep.get(effect)
if (trackId !== undefined && effect._trackId !== trackId) {
dep.delete(effect)
if (dep.size === 0) {
dep.cleanup()
}
}
}
shallowRef: ref的浅层作用形式,shallowRef不会对对象进行深度的响应式处理,也就是shallowRef包含的对象内部的属性发生变化的时候,shallowRef本身不会触发重新渲染或响应式更新。
effect
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions,
): ReactiveEffectRunner {
const _effect = new ReactiveEffect(fn, NOOP, () => {
if (_effect.dirty) {
_effect.run()
}
})
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
export class ReactiveEffect<T = any> {
active = true // 当前effect实例是否在工作,会调用stop就会停止工作
deps: Dep[] = []
constructor(
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
scope?: EffectScope,
) {
recordEffectScope(this, scope)
}
public get dirty() {
...
}
public set dirty(v) {
...
}
run() {
this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) {
return this.fn() // 执行依赖收集
}
let lastShouldTrack = shouldTrack
let lastEffect = activeEffect
try {
shouldTrack = true
activeEffect = this
this._runnings++
preCleanupEffect(this)
return this.fn()
} finally {
postCleanupEffect(this)
this._runnings--
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
}
stop() {
...
}
}
这个run作用就是用来执行副作用函数fn,并且在执行过程中进行依赖收集
当vue开发中存在组件嵌套组件的情况
在上面的vue2中,使用的是栈。但是在vue3中的实现为如下
this._dirtyLevel = DirtyLevels.NotDirty
if (!this.active) {
return this.fn()
}
let lastShouldTrack = shouldTrack
let lastEffect = activeEffect
try {
shouldTrack = true
activeEffect = this
this._runnings++
preCleanupEffect(this)
return this.fn()
} finally {
postCleanupEffect(this)
this._runnings--
activeEffect = lastEffect
shouldTrack = lastShouldTrack
}
function preCleanupEffect(effect: ReactiveEffect) {
effect._trackId++
effect._depsLength = 0
}
function postCleanupEffect(effect: ReactiveEffect) {
if (effect.deps.length > effect._depsLength) {
for (let i = effect._depsLength; i < effect.deps.length; i++) {
cleanupDepEffect(effect.deps[i], effect)
}
effect.deps.length = effect._depsLength
}
}
function cleanupDepEffect(dep: Dep, effect: ReactiveEffect) {
const trackId = dep.get(effect)
if (trackId !== undefined && effect._trackId !== trackId) {
dep.delete(effect)
if (dep.size === 0) {
dep.cleanup()
}
}
}
如果当前effect实例是不工作状态,就仅仅执行一下fn,则不需要收集依赖。由于在一个effect.run的过程中可能会触发另外的effect.run,暂存上一次的activeEffect, shouldTrack, 目的是为了把本次执行完成之后的activeEffect,shouldTrack恢复回去。