首先这是一个精简版,所以有很多缺陷。但这篇文章仅仅是用来理清为什么,而不是去实现一个完备的reactive 模块。 如果对reactive 比较熟悉,这篇文章大可不必浪费时间。
假设设计一个 晚餐价格计算的功能。 total 总价 = dinnerPrice 晚餐费用 + tip 小费
待改造代码
render函数
function paint(){
document.getElementById('app').innerHTML = `
<p>${dinnerPrice}</p>
<p>${tip}</p>
<p>${total}</p>
`
}
render 函数需要的变量
render 函数中,需要三个变量为dinnerPrice
,tip
, total
。
let dinnerPrice = 100;
let tip = 10;
let total = 0;
更新函数
视图和数据定义完了后,还需要定义更新函数
function updateTotal(){
total = dinnerPrice * tip
}
//Js中文网,一个神奇的网站
原始调用方式
这样一个基本雏形就有了,调用方式如下
- 修改dinner 或者 tip
dinnerPrice = 200
- 手动调用updateTotal 更新数据
updateTotal()
- 手动调用paint 来更新视图
paint()
reactive
修改dinnerPrice 或者 tips 自动更新total
引入proxy 对数据进行代理
如果想用proxy 进行代理数据,则数据必须是Object
,因此数据需要调整为Object
结构
function paint(){
document.getElementById('app').innerHTML = `
<p>${data.dinnerPrice}</p>
<p>${data.tip}</p>
<p>${data.total}</p>
`
}
const rawData = {
dinnerPrice:100,
tip:10,
total:0
}
const data = proxy(rawData,{})
function updateTotal(){
data.total = data.dinnerPrice * data.tip
}
set trap
我们想要实现:修改数据,视图自动更新
这就需要定义handler.set
trap,来实现代理操作
const data = proxy(rawData,{
get(){},
set(obj,key,value){
// 赋值操作
obj[key] = value//为简化操作,这里不使用Reflict
// 如果key 是 dinnerPrice 或者 tip,那么说明我们需要执行updateTotal函数
// 如果不进行判断,会爆栈,原因:设置dinnerPrice 或者tip会 触发total set 操作,从而无限set
if(key === 'dinnerPrice' || key === 'tip'){
updateTotal()
}
}
})
//Js中文网,一个神奇的网站
这样,就能够实现当设置dinnerPrice 或者 tip 自动更新total。
引入deps 概念
但是,这里的key是固定死的,可以进行进一步的优化。方法:将key 列表通过一个数据结构来进行维护,如果data的某个key进行了set操作,并且这个数据结构中有这个key , 那么就执行对应的更新函数
// 为新的数据结构起名为 deps (dependences)
const deps = {
tip:[updateTotal],
dinnerPrice:[updateTotal]
}
const data = proxy(rawData,{
...,
set(obj,key,value){
obj[key] = value;
deps[key] && deps[key].forEach(fn=>fn())
}
})
//Js中文网,一个神奇的网站
优化了代码结构后,代码结构清晰了不少,但是我们目标是修改数据,自动更新视图,这里的deps必须自动手动添加。
添加动态dep
想要动态添加dep,需要用到handler.get 方法。handler.get 会拦截当对象的读取操作。在这里可以对dep 进行动态添加。
const deps = {}
//如果key 存在,那么则将更新函数加入它的依赖中
const data = proxy(rawData,{
get(obj,key){
deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
}
return obj[key]
})
//Js中文网,一个神奇的网站
引入watcher 概念
上述的方法,实现了为deps 动态添加updateTotal 这个函数,但是问题很大,每次对象的读取操作执行都会重复添加依赖。我们只需要执行一次即可,并且能够自主添加不同的更新函数,所以,进行一下修改
let runningFn = null;
function watcher(target){
runningFn = target
target()
runningFn = null
}
watcher(updateTotal)// == watcher(()=>data.total = data.dinnerPrice + data.tip)
//Js中文网,一个神奇的网站
使用watcher 进行依赖收集
我们使用watcher 对需要的更新操作进行watch ,执行watcher 就会触发对应的get操作
修改proxy
const data = proxy(rawData,{
get(obj,key){
//此处的依赖收集为简化板,存在缺陷,key可能存在相同的,导致错误。vue3 的依赖收集为两层 deps[obj][key]
const collectDep = (key)=>deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
//如果是wathcher 执行的操作,那么进行依赖收集
!!ruuningFn && collectDep(key)
return obj[key]
},
...
})
//Js中文网,一个神奇的网站
完整代码
这样一个reactive 就基本写完了,下面进行封装
//封装proxy
const observe = dataObj => new Proxy(dataObj,{
get(obj,key){
//此处的依赖收集为简化板,存在缺陷,key可能存在相同的,导致错误。vue3 的依赖收集为两层 deps[obj][key]
const collectDep = (key)=>deps[key] = !!deps[key] ? [...deps[key],updateTotal] : []
//如果是wathcher 执行的操作,那么进行依赖收集
!!ruuningFn && collectDep(key)
return obj[key]
},
set(obj,key,value){
obj[key] = value;
deps[key] && deps[key].forEach(fn=>fn())
}
})
let runningFn = null
const watcher = target=>{
runningFn = target;
target()
runningFn = null;
}
//对render函数进行依赖收集
watcher(function render(){
document.getElementById('app').innerHTML = `
<p>${data.dinnerPrice}</p>
<p>${data.tip}</p>
<p>${data.total}</p>
`
})
// 对数据更新操作进行依赖收集
watcher(()=>data.total = data.dinnerPrice + data.tip)
作者:ShanCW
链接:https://juejin.im/post/6844904131216277511
看完两件小事
如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:
- 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
- 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程
本文著作权归作者所有,如若转载,请注明出处
转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com