/*
響應式設計主要是基于js的Proxy對象代理操作,當副作用函數發生資料的修改時
能夠通過代理來攔截對象的資料,進而對資料進行動态修改。
*響應式原理:當觸發資料讀取操作時,執行副作用函數并存儲到桶中
當設定資料操作時,再将副作用函數從桶中取出并執行
*/
//用一個全局變量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
//Symbol唯一,可以作為對象屬性辨別符使用
const ITERATE_KEY=Symbol()
//必須是一個對象
const data={
foo:1,bar:2,
get tep(){
return this.foo
}
}
//對原始資料的代理
const obj=new Proxy(data,{
//攔截讀取操作
get(target,key,receiver){
track(target,key)
//傳回屬性值
return Reflect.get(target,key,receiver)
},
/*
//攔擊in操作符讀取屬性值
ha(target,key){
track(target,key)
return Reflect.has(target,key)
}
*/
/*
*攔截for in 循環讀取屬性值
ownKeys(target){
//将副作用函數和ITERATE_KEY關聯
track(target,ITERATE_KEY)
return Reflect.ownKeys(target)
}
*/
//攔截設定操作
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.tep
})
//重建立立副作用函數
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()
}
})
}