天天看點

VUE3.js響應式設計原了解析

作者:純屬3369
/*
響應式設計主要是基于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()
		}
	})
}



           
VUE3.js響應式設計原了解析
VUE3.js響應式設計原了解析

繼續閱讀