一线大厂高级前端编写,前端初中阶面试题,帮助初学者应聘,需要联系微信:javadudu

响应式系统原理与实现

细粒度响应式

细粒度响应式由一些primitive组成。

signal

signal是响应式系统最基本的组件,它由getter,setter,value组成。在一些文章中,也称为Observable,Atom,Subject, Refs。

基本用法如下


// 定义
const [count, setCount] = createSignal(0);

// 读
console.log(count())

// 写
setCount(5)

通过createSignal创建,返回数组,包括两个值getter和setter。通过getter调用获取值,通过setter更改值。这只是一个可以存储任意值的结构,关键在于getter和setter内部逻辑可以是任意代码,因此可以用来传播更新。函数是主要形式,但是也可以通过对象getter或代理。例如

// vue中ref的定义
const count = ref(0)
// 读
console.log(count.value)

// 写
count.value=5;

也可以通过编译加上语法糖

// Svelte 中定义
let count = 0

// 读
console.log(count)

// 写
count = 5;

本质上signal是event emitter,关键区别是如何管理订阅。

Reaction响应

Signal本身并没太大用途,如果没有Reaction的话。Reaction也叫Effect,Autoruns, Watches, computed,观察signal,每次值变化后自动运行。Reaction是一种函数,初始运行后记录下相关的signal,之后每次signal值发生变化,就会重新运行。

const [count,setCount] = createSignal(0);
createEffect(() => { console.log(count())})

setCount(4) //触发上面的effect运行

看上去很神奇,但这是为什么需要getter的原因。每次signal运行,包裹函数检测到会自动订阅signal。重要的是Signal能存储任何类型的数据。signal和Reaction就是细粒度响应式的基石:观察者和被观察者。你可以用这两者创建出大部分行为。但是除这两者之外还有一个核心primitive。

Derivation衍生状态

通常我们需要用不同形式表示数据,在多个reaction里使用同个signal,我们需要提取成一个函数。

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Smith');
const fullName = () => {
	console.log('creating/updating fullname')
	return `${firstName()} ${lastName()}`
}

createEffect(()=> console.log(fullName()))
createEffect(() => console.log('your name is not', fullName()))

setFirstName('Jacob')

// 'creating/updating fullname'
// John Smith
// 'creating/updating fullname'
// your name is not John Smith
// Set new firstName.....

这里定义了fullName函数,这是因为我们需要在createEffect里面调用signal的getter才能被捕捉到。但是有时候定义衍生值的成本很高,我们不想重复工作。因此我们需要第三个primitive,用来缓存计算结果。通常称为memos, Computed, Pure Computed。

const [firstName, setFirstName] = createSignal('John');
const [lastName, setLastName] = createSignal('Smith');
const fullName = createMemo(() => {
	console.log('creating/updating fullname')
	return `${firstName()} ${lastName()}`
})
console.log("3. Create Reactions");
createEffect(()=> console.log(fullName()))
createEffect(() => console.log('your name is not', fullName()))

setFirstName('Jacob')

// 'creating/updating fullname'
// 3. Create Reactions
// John Smith
// your name is not John Smith
// Set new firstName.....

使用memo后,区别是第一提前计算了memo,后面调用时不再重新运行。当更新signal后,重新运行。虽然上面的例子中,名字并不是一个昂贵的操作,但是使用Derivation将值缓存起来,同时Derivation本身也可追踪。通过Derivation可以保证时同步的,在任何时刻,我们可以根据它的依赖判断值是否过期,而使用effect向表示衍生值的signal去写的方式模拟Derivation却不能保证,因为我们无法看到衍生signal的依赖(signal没用依赖)。

有些库延迟计算Derivation,因为它们只在需要时才计算。

console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);

const displayName = createMemo(() => {
  if (!showFullName()) return firstName();
  return `${firstName()} ${lastName()}`
});

createEffect(() => console.log("My name is", displayName()));

console.log("2. Set showFullName: false ");
setShowFullName(false);

console.log("3. Change lastName");
setLastName("Legend");

console.log("4. Set showFullName: true");
setShowFullName(true);

// 输出
// 1. Create

// My name is John Smith

// 2. Set showFullName: false

// My name is John

// 3. Change lastName

// 4. Set showFullName: true

// My name is John Legend

需要注意:第三步,改lastName时没有触发effect,那是因为此刻没有任何node依赖它,当修改showFullName后,重构了依赖图。每次运行时都会重新构建订阅和依赖关系。

console.log("1. Create");
const [firstName, setFirstName] = createSignal("John");
const [lastName, setLastName] = createSignal("Smith");
const [showFullName, setShowFullName] = createSignal(true);

const displayName = createMemo(() => {
  console.log("### executing displayName");
  onCleanup(() =>
    console.log("### releasing displayName dependencies")
  );
  if (!showFullName()) return firstName();
  return `${firstName()} ${lastName()}`
});

createEffect(() => console.log("My name is", displayName()));

console.log("2. Set showFullName: false ");
setShowFullName(false);

console.log("3. Change lastName");
setLastName("Legend");

console.log("4. Set showFullName: true");
setShowFullName(true);

// 1. Create

// ### executing displayName

// My name is John Smith

// 2. Set showFullName: false

// ### releasing displayName dependencies

// ### executing displayName

// My name is John

// 3. Change lastName

// 4. Set showFullName: true

// ### releasing displayName dependencies

// ### executing displayName

// My name is John Legend

加上oncleanup后可以看到每次运行时清理原有依赖图的时刻。

同步

细粒度响应系统每次更新都是同步操作,我们可以使用batch将多个更新操作包裹起来,当所有都更新完后在执行reaction。

console.log("1. Create");
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
const c = createMemo(() => {
  console.log("### read c");
  return b() * 2;
});

createEffect(() => {
  console.log("### run reaction");
  console.log("The sum is", a() + c());
});

console.log("2. Apply changes");
batch(() => {
  setA(2);
  setB(3);
});
//输出
// 1. Create

// ### read c

// ### run reaction

// The sum is 5

// 2. Apply changes

// ### run reaction

// ### read c

// The sum is 8

需要注意,apply changes后的打印,A和b更新后,必须选一个依赖执行,这里选了从a开始,先执行reaction,当执行时读取c发现是stale,运行c。

实现

signal

export function createSignal(value) {
	const read = () => value;
	const write = (nextValue) => value = nextValue;
	return [read, write]
}


const [count, setCount] = createSignal(3);
console.log("Initial Read", count());

setCount(5);
console.log("Updated Read", count());

setCount(count() * 2);
console.log("Updated Read", count());

// 输出
// Initial Read 3

// Updated Read 5

// Updated Read 10

先如上实现createSignal函数,它接受value,然后通过闭包方式返回getter和setter函数。getter获取value的值,setter更改值。这样实现后已经具备了读写能力,可以获取signal的值,也可以通过setter更新。但是还缺乏依赖订阅的能力,signal是event emitter。

增加依赖追踪的能力,在getter中建立依赖关系,setter遍历所有订阅更新函数。

const context = []
function subscribe(running, subscriptions) {
	subscriptions.add(running);
	running.dependencies.add(subscriptions)
}
function createSignal(value) {
  const subscriptions = new Set();
	const read = () => { 
		const running = context[context.length - 1];
		if (running) {
			subscribe(running, subscriptions)
		}
		return value
	};
	const write = (nextValue) => {
		value = nextValue;
		for(const sub of [...subscriptions]) {
			sub.execute()
		}
	};
	return [read, write]
}

通过全局变量context记录运行中的Reaction栈,每个signal有一个订阅列表记录了观察者。为了实现自动依赖追踪,每个reaction或者Derivation在执行时将自己压入context栈中,它会被加到signal的订阅列表中。反过来,我们也将signal加到了依赖列表里,建立了双向关联,这会有助于cleanup的实现。

最后在signal发生更新时,执行所有的订阅。这里复制了订阅列表,可以避免在执行过程中订阅列表发生变化。

Reaction与Derivation

// 清理关联
function cleanup(running) {
	// 每一个dep时siganl的订阅列表集合
	for(const dep of running.dependencies) {
		dep.delete(running) // 从订阅列表里删除自己
	}
	running.dependencies.clear() // 清空当前的reaction的依赖
}

function createEffect(fn) {
	// 表示订阅的对象,
	const running = {
		dependencies: new Set(),
		execute: function() {
			cleanup(this)
			context.push(this);
			try {
				fn()
			} finally {
				context.pop();
			}
		}
	}
	// 初始运行
	running.execute()
}

createEffect的接受一个函数,内部创建一个订阅对象,有两个重要属性dependencies记录依赖,execute执行函数。execute包裹传入的函数,在函数调用前后设置恢复当前的context栈,由于每次调用都会创建依赖关系,所以在调用开始先首先清理旧的关系。到了这里,已经可以做点东西了。

运行下面示例

function cleanup(running) {
  for (const dep of running.dependencies) {
    dep.delete(running);
  }
  running.dependencies.clear();
}

export function createEffect(fn) {
  const execute = () => {
    cleanup(running);
    context.push(running);
    try {
      fn();
    } finally {
      context.pop();
    }
  };

  const running = {
    execute,
    dependencies: new Set()
  };

  execute();
}
// 1. Create Signal

// 2. Create Reaction

// The count is 0

// 3. Set count to 5

// The count is 5

// 4. Set count to 10

// The count is 10

简单实现里,Derivation可以通过reaction和signal这两者结合实现。

function createMemo(fn) {
	const [s, set] = createSignal();
	createEffect(() => set(fn()))
	return s;
}

在上面实现中,我们可以将memo看作是一种特殊的signal,它是可以依赖其他signal的。因为memo是signal所以可以被reaction追踪,同时memo也具有reaction,当依赖的signal变化,memo会更新合成的signal。

参考:
dev.to/ryansolid/b…

indepth.dev/posts/1289/…

原文链接:https://juejin.cn/post/7240636255973638200 作者:Tomarsh

(0)
上一篇 2023年6月5日 上午10:11
下一篇 2023年6月5日 上午10:21

相关推荐

发表评论

登录后才能评论