欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 手游 > Vue3响应系统的作用与实现

Vue3响应系统的作用与实现

2025/4/19 20:38:18 来源:https://blog.csdn.net/qq_25308331/article/details/140336648  浏览:    关键词:Vue3响应系统的作用与实现

 副作用函数的执行会直接或间接影响其他函数的执行。一个副作用函数中读取了某个对象的属性,当该属性的值发生改变后,副作用函数自动重新执行,这个对象就是响应式数据。

1 响应式系统的实现

拦截对象的读取和设置操作。当读取某个属性值时,把副作用函数存储到一个“桶”里,而设置该属性值时,则将这个副作用函数从“桶”中取出病执行。

/*** 响应式系统基本原理:Proxy 拦截设置及读取操作,读取属性时将副作用* 函数存于桶,设置属性时将副作用函数从桶中取出并执行*/
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}let bucket = new Set()let proxyObj = new Proxy(obj,{get(target, p, receiver) {bucket.add(fun)return target[p]},set(target, p, newValue, receiver) {target[p] = newValuebucket.forEach(fn => fn())}
})function fun() {console.log(proxyObj.name)
}fun() // 触发执行,空字符串
proxyObj.name = "hello" // hello
proxyObj.name = "js" // js

1.1 桶的结构

存储副作用函数的“桶”,应该为不同的对象、及其属性存储对应的副函数集。存储的容器为WeakMap。

/*** 用WeakMap 作为副作用函数的容器,改进响应式系统,支持不同的* 响应式对象及其属性都能响应式执行*/
let obj = {name: '',tag: false,count: 0,num1: 0,num2: 0}let bucketMap = new WeakMap()
let activeFun // 用于指示当前需要注册的副作用函数let proxyObj = new Proxy(obj,{get(target, p, receiver) {track(target,p)return target[p]},set(target, p, newValue, receiver) {target[p] = newValuetrigger(target,p)}
})function track(target,p) { // 跟踪函数if (activeFun) {let map = bucketMap[target]if (!map) map = bucketMap[target] = new Map()let set = map[p]if (!set) set = map[p] = new Set()set.add(activeFun)}
}function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]set && set.forEach(fn => fn())}
}function effect(fn) { // 用于注册副作用函数let tempFun = () => {activeFun = fnfn()activeFun = null}tempFun()
}effect(() => {console.log(proxyObj.name,proxyObj.tag)
})
effect(() => {console.log("name2",proxyObj.name)
})
console.log("------------------------------------")
proxyObj.name = "hello"
console.log("------------")
proxyObj.tag = false
console.log("------------")
proxyObj.name = "js";
console.log("------------")

1.2 分支切换

分支切换是指,函数内部存在一个三元表达式,根据某个字段的值会执行不同的代码分支。当该字段的值发生变化时,代码执行的分支会跟着变化。

例如:console.log(proxyObj.tag ? proxyObj.name : "false");

按照上面的代码,当name或tag的值被设置时,都会触发副作用函数。但是,在副作用函数中,当tag为false时,name的值是不会被显示的,这意味着,当tag为false时,无论name被设置多少次,都不希望执行这个副作用函数。

解决方案:当该副作用函数被触发时,删除属性与该函数的关系。在副作用函数执行时再重新创建关系。

function track(target,p) { // 跟踪函数if (activeFun) {let map = bucketMap[target]if (!map) map = bucketMap[target] = new Map()let set = map[p]if (!set) set = map[p] = new Set()set.add(activeFun)activeFun.funSetList.push(set) }
}function effect(fn) { // 用于注册副作用函数let tempFun = () => {cleanup(tempFun)activeFun = tempFunfn()activeFun = null}tempFun.funSetList = []tempFun()
}function cleanup(fn) {fn.funSetList.forEach(set => {set.delete(fn)})fn.funSetList = []
}

1.3 嵌套的effect

组件在渲染时,会执行effect函数来注册副作用函数,而父组件在渲染时,不仅会执行其本身的effect函数,还会自行其子组件的effect,这是就发生了嵌套的effect的调用。即如下:

effect(() => {effect(() => {console.log(proxyObj.count)})console.log(proxyObj.tag);
})

当修改tga 属性时,父组件的副作用函数并不会执行。

解决方案:创建一个注册的副作用函数指示栈。副作用函数执行前,将函数压入到栈中,执行完后则弹出该函数。

let activeFunStack = []
let registerFunSet = new Set() // 防止函数多次被注册function effect(fn) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]}tempFun.funSetList = []tempFun()}
}effect(() => {effect(sonFun)console.log(proxyObj.tag);
})function sonFun() {console.log(proxyObj.count)
}console.log("------------------------------------")
proxyObj.tag = false
proxyObj.count = 1

1.4 避免无限递归

在一个副作用函数设置及读取同一个属性时,上面代码中,会发生无限递归对情况。这是因为,当设置属性值时,会触发副作用函数执行,而副作用函数中又会设置该属性值…

解决方案:在触发时,不执行当前正在被注册的副作用函数。

function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]if (set) {let tempSet = new Set(set)tempSet.forEach(fn => {if (activeFun !== fn) fn()})}}
}effect(() => {console.log(proxyObj.count++);
})
console.log("------------------------------------")
proxyObj.count++;

1.5 调度执行

可调度,是指当动作触发副作用函数重复执行时,有能力决定副作用函数执行的时机、次数以及方式。

1.5.1 微任务

宏任务

通常是由宿主环境(浏览器)提供的。包括但不限于:script(整体代码)、setTimeout、setInterval、I/O、UI渲染。

微任务

由JS引擎(如V8)提供的。它们在当前宏任务之后,下一个宏任务之前执行。常见的微任务:Promose.then()

微任务通常用于执行需要尽快完成的异步操作。

过多使用微任务可能会导致主线程被阻塞,影响页面的响应。

表 宏任务和微任务两种类型的队列

执行顺序:

  1. 宏任务队列:从宏任务队列中取出一个任务执行。
  2. 执行宏任务:执行宏任务中的所有同步代码。
  3. 微任务队列:在执行完宏任务中的所有同步代码后,会查看并清空微任务队列中的所有任务。
  4. 渲染UI:微任务队列清空后,浏览器会进行UI渲染(如果需要)。
  5. 循环:重复步骤1~4,直到宏任务队列和微任务队列都为空。
function trigger(target,p) { // 触发函数let map = bucketMap[target]if (map) {let set = map[p]if (set) {let tempSet = new Set(set)tempSet.forEach(fn => {if (activeFun !== fn) {if (fn.options.scheduler) {fn.options.scheduler(fn)} else {fn()}}})}}
}function effect(fn,options = {}) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]}tempFun.options = optionstempFun.funSetList = []tempFun()}
}const jobQueue = new Set()
const promise = Promise.resolve()
let isFlushing = falsefunction flushJob() {if (!isFlushing) {isFlushing = truepromise.then(() => {jobQueue.forEach(fn => fn())}).finally(() => {isFlushing = false})}
}effect(() => {console.log(proxyObj.count);
},{scheduler(fn) {jobQueue.add(fn)flushJob()}
})
console.log("------------------------------------")
proxyObj.count++;
proxyObj.count++;

1.6 计算属性computed 与 lazy

计算属性,只有当相关依赖发生改变时,计算属性才会重新求值。否则,就是多次访问计算属性,也会立即返回之前的计算结果,不需要再次执行函数。

function effect(fn,options = {}) { // 用于注册副作用函数if (!registerFunSet.has(fn)) {registerFunSet.add(fn)let tempFun = () => {cleanup(tempFun)activeFun = tempFunactiveFunStack.push(activeFun)let res = fn()activeFunStack.pop()activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]return res}tempFun.options = optionstempFun.funSetList = []if (!options.lazy) {tempFun()}return tempFun}
}function computed(fn) {let valuelet dirty = truelet tempFun = () => { trigger(obj,"value") }let effectFn = effect(fn,{lazy: true,scheduler() {if (!dirty) {dirty = truejobQueue.add(tempFun)flushJob()}}})let obj = {get value() {if (dirty) {value = effectFn()dirty = false}track(obj,"value")return value}}return obj
}let computedRes = computed(() => proxyObj.num1 + proxyObj.num2)effect(()=> {console.log("computedRes1",computedRes.value)
})proxyObj.num1 = 2
proxyObj.num2 = 3

1.7 watch 的实现原理

Vue 的 watch,可以监听对象、对象的某个属性。可以对对象进行深层次监听。当属性值改变时,会触发监听的回调函数。

function watch(source,callBack) {let newValue,oldValuelet getterif (typeof source === "function") {getter = source} else {getter = () => traverse(source)}const job = () => {newValue = effectFun()callBack(newValue,oldValue)if (typeof newValue === "object") {oldValue = {...newValue}} else {oldValue = newValue}}let effectFun = effect(getter,{scheduler() {job()}})let tempRes = effectFun()if (typeof tempRes === "object") {oldValue = {...tempRes}} else {oldValue = tempRes}
}function traverse(value) {if (typeof value != "object" || value === null) returnfor (const k in value) traverse(value[k])return value
}watch(proxyObj,(newValue,oldValue) => {console.log("proxyObj",newValue,oldValue)
})watch(proxyObj.name,(newValue,oldValue) => {console.log("name",newValue,oldValue)
})proxyObj.count = 1
proxyObj.name = "hello"

1.8 过期的副作用

竞态问题指的是两个或多个操作几乎同时发生,并且结果依赖于它们发生的顺序,但顺序又是不确定的。 在单线程JS环境中(浏览器),我们通常不会遇到竞态问题,但是,随着Web API的引入(如异步操作,Promises,async/aswait,Web Workers等),导致JS代码中仍然可以出现竞态问题。

watch(() => proxyObj.count,(newValue,oldValue) => {Promise.resolve().then(() => {setTimeout(() => {console.log(newValue)},newValue * 1000)})
})proxyObj.count = 5
setTimeout(()=> {proxyObj.count = 2
},500)

解决方案:在第二次触发时,将前一次的触发状态设置为过期,只有状态非过期,产生的结果才有效。

function watch(source,callBack) {let newValue,oldValuelet getterif (typeof source === "function") {getter = source} else {getter = () => traverse(source)}let cleanuplet cleanFun = (fn) => {cleanup = fn}const job = () => {if (cleanup) cleanup()newValue = effectFun()callBack(newValue,oldValue,cleanFun)if (typeof newValue === "object") {oldValue = {...newValue}} else {oldValue = newValue}}let effectFun = effect(getter,{scheduler() {job()}})let tempRes = effectFun()if (typeof tempRes === "object") {oldValue = {...tempRes}} else {oldValue = tempRes}
}watch(() => proxyObj.count,(newValue,oldValue,cleanFun) => {let expire = falsecleanFun(() => {expire = true})Promise.resolve().then(() => {setTimeout(() => {if (!expire) console.log(newValue)},newValue * 1000)})
})

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词