/*
*原理:当触发数据读取操作时,执行副作用函数并存储到桶中
当设置数据操作时,再将副作用函数从桶中取出并执行
*/
//用一个全局变量activeEffect存储被注册过的副作用函数
let activeEffect
//const buket=new Set()
//weakMap为弱引用,不影响垃圾回收机制工作,当用户代码对一个
//对象没有引用关系时,垃圾会收器会回收该对象,避免引起栈堆的溢出
const bucket=new WeakMap()
const effectStack=[]
//定义一个宏任务队列
const jobQueue=new Set()
//定义一个Promose,将一个任务添加到微任务队列
const p=Promise.resolve()
//是否正在刷新队列
let isFlushing=false
//必须是一个对象
const data={foo:1,bar:2}
//对原始数据的代理
const obj=new Proxy(data,{
//拦截读取操作
get(target,key){
track(target,key)
//返回属性值
return target[key]
},
//拦截设置操作
set(target,key,newvalue){
//设置属性值
target[key]=newvalue
trigger(target,key)
return true
}
})
//options对象动态调度副作用函数的执行时机
function effect(fn,options={}){
const effectFn=()=>{
//例如effet(function effectFn(){document.body.inntext=obj.ok?obj.text:'not'})
//清除工作
cleanup(effectFn)
//存储被注册过的副作用函数
activeEffect=effectFn
//嵌套的副作用函数
//在调用副作用函数前将其压入栈中,首先压入的内层副作用函数
effectStack.push(effectFn)
let res=fn()
//调用完之后,将其弹出栈,弹出内层的副作用函数
effectStack.pop()
activeEffect=effectStack[effectStack.length-1]
//返回fn的结果
return res
}
//存储与该副作用相关的依赖集合
effectFn.deps=[]
//将options挂在到副作用函数
effectFn.options=options
if(!options.lazy) effectFn()
return effectFn
}
function cleanup(effectFn){
//遍历副作用函数的deps数组
for(let i=0;i<effectFn.length;++i){
const deps=effectFn.deps[i]
//从依赖集合删除
deps.delete(effectFn)
}
effectFn.deps.length=0
}
//微任务队列
//何时执行?在options的调度函数中执行,例如
/*
effect(()=>{console.log(obj.foo)},
scheduler(fn){
//执行调度时,将其添加到微任务队列
jobQueue.add(fn)
//刷新队列
flushJob()
}
)
obj.foo++
obj.foo++
*最终输出
1
3
*微任务队列最终执行的只有一次,而此时obj.foo的值已经是3.
*/
function flushJob(){
//如果正在刷新任务队列,什么都不做,否则isFlushing=true
if(isFlushing) return
isFlushing=true
//将任务添加到微任务队列
p.then(()=>{
jobQueue.forEach(job=>job())
}).finally(()=>{isFlushing=false})
}
/*
*计算属性与懒执行
*/
function computed(getter){
let value
//是否需要重新计算值,true代表需要计算
let dirty=true
//只有调用value的时候才会执行
const effectFn=effect(getter,{
//不执行
lazy:true,
//当值发生变化时,在跳读器中重新设置diarty。
scheduler(){
if(!dirty){
dirty=true
//当计算属性依赖的响应数据发生变化时,手动调用函数触发响应
trigger(obj, 'value')
}
}
})
const obj={
get value(){
if(dirty){
//执行副作用函数
value=effectFn()
//设置为false,下次访问时,直接使用原来的值
dirty=false
}
//当读取value时,手动调用track函数进行追踪
track(obj, 'value')
//返回值为fn的值
return value
}
}
return obj
}
/*
*wach的实现原理
*当数据发生变化时,执行回调
*/
function watch(source,cb,options={}){
let getter
//如果source是函数,则执行函数,否则调用traverse函数递归地读取属性
if(typeof source==='function'){
getter=source
}else{
getter=()=>traverse(source)
}
//旧值与新值
let oldValue,newValue
let cleanup
function onInvalidate(fn){
cleanup=fn
}
//对scheduler函数的封装
const job=()=>{
newValue=effectFn()
if(cleanup){
cleanup()
}
//返回旧值,新值,已经回调给用户使用
cb(newValue,oldValue,onInvalidate)
//已经触发了回调函数,所以这里重新赋值
oldValue=newValue
}
//出发操作,建立回调
const effectFn=effect(
//调用函数递归地读取数据
()=>getter()
,{
lazy:true,
//调度函数
scheduler:()=>{
//创建微任务队列,再DOM加载完成后再执行
if(options.flush==='post'){
const p=Promise.resolve()
p.then(job)
}else{
job()
}
}
})
if(options.immediate){
job()
}else{
//调用副作用函数,拿到旧值
oldValue=effectFn()
}
}
function traverse(value,seen=new Set()){
//如果数据是原始值或者已经被读取过了,则什么都不做
if(typeof value!=='object' || value===null || seen.has(value)) return
seen.add(value)
//堆对象内部地属性,递归地读取数据
for(const k in value){
traverse(value[k],seen)
}
return value
}
watch(()=>obj.foo,(newValue,oldValue)=>alert(oldValue+':'+newValue))
setTimeout(()=>obj.foo++,1000)
const sum=computed(()=>{
document.getElementById('test').innerHTML=obj.foo+obj.bar
})
//重新建立副作用函数
effect(function effectFn(){
sum.value
})
function track(target,key){
//console.dir(target)
if(!activeEffect) return target[key]
let depsMap=bucket.get(target)
if(!depsMap){
bucket.set(target,(depsMap=new Map()))
}
let deps=depsMap.get(key)
if(!deps){
depsMap.set(key,(deps=new Set()))
}
deps.add(activeEffect)
activeEffect.deps.push(deps)
}
function trigger(target,key){
const depsMap=bucket.get(target)
if(!depsMap) return
const effects=depsMap.get(key)
const effectsToRun=new Set(effects)
//避免自增导致无限循环
//ECMA规范:再调用foreach遍历set集合时,如果一个值已经被访问过
//但这个值被删除并重新添加到集合,如果遍历没有结束,那么这个值
//又会重新被访问,解决办法是建立一个新的Set来遍历
effects && effects.forEach(f=>{
if(f!=effectsToRun){
effectsToRun.add(f)
}
})
effectsToRun.forEach(fn=>{
//如果副作用函数存在调度函数,那么执行调度函数,否则执行原函数
if(fn.options.scheduler){
fn.options.scheduler(fn)
}else{
fn()
}
})
}
javaScript基于原始值的响应系统实现方案
于 2023-01-24 16:45:26 首次发布