天天看点

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响应式设计原理解析

继续阅读