前端响应式系统的心路历程
本文最后更新于:2025年6月26日 晚上
前端响应式系统的心路历程
原生JS到底在写什么?
首先探究一下响应式系统的直接动机。
我们在写JS代码的时候,主要就是做三件事:
// 1. 获取数据(通过网络)
data = getData();
// 2. 对数据进行处理(业务逻辑)
handleData();
// 3. 渲染数据到视图(DOM操作)
showData();
其中获取数据只需一次,数据处理和渲染需要多次,即数据每执行一种handleData()
就需要执行showData()
。那就需要我们在多个数据处理逻辑中反复调用同一套渲染逻辑。
既然数据处理和渲染步骤这么繁琐,那我们能否在操作数据(事件控制)的同时自动渲染视图?这就引出了框架响应式的初衷。
数据依赖
再分析一下数据处理和渲染的工作量
实际的代码编写量和我们的期望代码量:
1 getData 1 getData
N handleData -> N handleData
N+1 showData 1 showData
那么如何消除N个的showData
调用?由于N最初和handleData
有关,所以需要决定有多少个handleData
,于是问题变成了得到数据被更改的次数。
事实上,handleData
就是对目标数据的操作,每次操作就会调用一次渲染函数,更深刻来说就是该渲染函数依赖于该数据。所以我们可以在源数据对象上做文章(使用Object.defineProperty
的set
):
var data = { name: 'rok' }
var internalName = data.name
Object.defineProperty(data, 'name', {
set: function(val) {
internalName = val;
// 执行:自动调用showData()来渲染视图
}
})
// 当然vue3选择了Proxy来处理
这样,只要每次data有数据操作,就会在set中自动去重新渲染视图,没有多次编写调用showData
代码的必要
自行设计的响应式系统
实际上,问题还没有完全解决,如果每次有一个数据都需要写一套Object.defineProperty
依然十分繁琐。假如可以将Object.defineProperty
这一套单独整理出来变成框架,就更加方便了
上述代码无法变成通用模版的原因是有一个不确定的函数,即showData
,这里并不知道要调用哪个渲染函数。不过分析可知,showData
也是依赖于目标数据的,并且其会触发数据的get
。于是上面的代码可以演变成下面的通用模式:
function observe(obj) {
for (const key in obj) {
let internalValue = obj[key];
Object.defineProperty(obj, key, {
get: function() {
// 依赖收集,记录:是哪个showData依赖于该数据
return internalValue;
},
set: function(val) {
internalValue = val;
// 派发更新,执行:自动调用showData来渲染视图
}
})
// 当然vue3选择了Proxy来处理
}
}
实际上把注释位置替换成真实代码即:
function observe(obj) {
for (const key in obj) {
let internalValue = obj[key];
let funcs = new Set()
Object.defineProperty(obj, key, {
get: function() {
funcs.add(funcName); // 记录依赖函数(暂时无法得知函数名)
return internalValue;
},
set: function(val) {
internalValue = val;
for (let i = 0; i < funcs.length; i++) {
funcs[i](); // 执行渲染
}
}
})
}
}
现在就差得到渲染函数的函数名了,实际上要解决很简单,就是在第一次调用渲染时给这个函数设置一个标记即可:
function autorun(fn) {
window.__func = fn;
fn();
window.__func = null;
}
function observe(obj) {
...
get: function() {
funcs.add(window.__func)
return internalValue;
},
...
}
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!