Vue3响应系统的作用与实现

news2024/10/8 14:29:12

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

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] = newValue
        bucket.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] = newValue
        trigger(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 = fn
        fn()
        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 = tempFun
        fn()
        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 = tempFun
            activeFunStack.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 = tempFun
            activeFunStack.push(activeFun)
            fn()
            activeFunStack.pop()
            activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]
        }
        tempFun.options = options
        tempFun.funSetList = []
        tempFun()
    }
}

const jobQueue = new Set()
const promise = Promise.resolve()
let isFlushing = false

function flushJob() {
    if (!isFlushing) {
        isFlushing = true
        promise.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 = tempFun
            activeFunStack.push(activeFun)
            let res = fn()
            activeFunStack.pop()
            activeFun = activeFunStack.length < 1 ? null : activeFunStack[activeFunStack.length - 1]
            return res
        }
        tempFun.options = options
        tempFun.funSetList = []
        if (!options.lazy) {
            tempFun()
        }
        return tempFun
    }
}

function computed(fn) {
    let value
    let dirty = true
    let tempFun = () => { trigger(obj,"value") }
    let effectFn = effect(fn,{
        lazy: true,
        scheduler() {
            if (!dirty) {
                dirty = true
                jobQueue.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,oldValue
    let getter
    if (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) return
    for (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,oldValue
    let getter
    if (typeof source === "function") {
        getter = source
    } else {
        getter = () => traverse(source)
    }
    let cleanup
    let 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 = false
    cleanFun(() => {
        expire = true
    })
    Promise.resolve().then(() => {
        setTimeout(() => {
            if (!expire) console.log(newValue)
        },newValue * 1000)
    })
})

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1917743.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Python-PLAXIS自动化建模技术与典型岩土工程

原文链接&#xff1a;Python-PLAXIS自动化建模技术与典型岩土工程https://mp.weixin.qq.com/s?__bizMzUzNTczMDMxMg&mid2247608744&idx3&sn41d9fd9ab6e792850000c4990b3f8c65&chksmfa82684fcdf5e15990e4681f032ce9b295a9e2071051218f550a7e63e4ebedee29559d56…

Linux下vim工具应用

1.简介 Vim&#xff08;Vi IMproved&#xff09;是一种高度可配置的文本编辑器&#xff0c;用于有效地创建和更改任何类型的文本。它是从vi发展而来&#xff0c;vi是Unix和类Unix系统上最初的文本编辑器之一。Vim被设计为程序员和文本编辑的爱好者使用&#xff0c;它以其强大的…

广度优先(BFS)

先看一道简单的题&#xff0c;迷宫问题&#xff1a; 洛谷P1746 离开中山路&#xff1a;P1746 离开中山路 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) #include<iostream> #include<cstring> #include<queue> #include <utility> #define N 1002 …

Jenkins设置开机自启(以war包方式启动jenkins)

配置环境&#xff1a;Centos7.6/Jenkins 2.452.2/jdk-21.0.3 jenkins启动命令&#xff1a; nohup java -jar jenkins.war > /dev/null 2>&1 & 1、创建sh脚本文件 # jenkins.war包&#xff0c;放在了/root/software目录 cd /root/software vim jenkins-start.sh …

Matter Open Day:全球生态首秀,中国力量引领智能家居新纪元

▲ 会场全景 7月8日下午&#xff0c;广州南丰朗豪大酒店内人声鼎沸&#xff0c;由连接标准联盟和连接标准联盟‍中国成员组(CMGC)共同主办的首届 Matter Open Day 活动圆满落下帷幕。 此次盛会不仅标志着智能家居领域的一次重要聚首&#xff0c;更见证了连接标准联盟(原Zigbee…

用Python编写一个模拟usb摄像头的程序

昨天有个朋友找我要刷点政府的再就业的视频课&#xff0c;说是就是用浏览器打开固定的网站&#xff0c;然后用身份证号码登录&#xff0c;然后播放里面的视频&#xff0c;不定时的网页会通过usb摄像头拍照做人脸识别&#xff0c;就是这么一个简单的要求&#xff0c;叫我给想想办…

论文AI痕迹过重怎么办?AI降痕工具来帮忙

如何有效利用AI工具提高工作效率&#xff1f;探索这5款顶级AI写作工具 不知道大家有没有发现&#xff0c;随着人工智能技术的快速发展&#xff0c;AI工具正逐渐渗透到我们日常生活的各个方面&#xff0c;极大地提高了我们的工作和学习效率。无论是AI写作、AI绘画、AI思维导图&…

AI算力发展现状与趋势分析

综合算力发展现状与趋势分析 在数字经济的疾速推动下&#xff0c;综合算力作为驱动各类应用和服务的新型生产力&#xff0c;其价值日益凸显。我们深入探讨了综合算力的定义、重要性以及当前发展状况&#xff1b;并从算力形态、运力性能和存储技术等角度&#xff0c;预见了其发展…

C语言实战 | 弹跳的小球

C语言实战——实现弹跳的小球 01、综合案例 编写程序&#xff0c;实现小球(小方块)在屏幕内斜向运动&#xff0c;遇到边界发生反弹&#xff0c;如图2.6所示。 ■ 图2.6弹跳的小球 在例2.2中&#xff0c;通过变量drow、dcol控制方块的运动方向&#xff0c;例如斜向右下运动&am…

从实时监控到风险智能预警:EasyCVR视频AI智能监控技术在工业制造中的应用

随着科技的不断进步和工业制造领域的持续发展&#xff0c;传统的生产管理方式正逐渐转型&#xff0c;迈向更加智能、高效和安全的新阶段。在这个变革过程中&#xff0c;视频智能监控技术凭借其独特的优势&#xff0c;成为工业制造领域的管理新引擎&#xff0c;推动着从“制造”…

真实测评网上较火的两款智能生成PPT产品:秒出PPTAI PPT

测评两款AI生成PPT的工具&#xff1a;秒出PPT和AI PPT。这俩个款是目前竞争比较激烈的且使用起来比较好的产品。一下主要从PPT模板、一键生成及生成效果、Word转PPT来分析一下使用感受。 秒出PPT 秒出PPT是集模板站与编辑站一体的产品&#xff0c;支持微信扫码登录。主页可以直…

Leetcode—236. 二叉树的最近公共祖先【中等】

2024每日刷题&#xff08;142&#xff09; Leetcode—236. 二叉树的最近公共祖先 实现代码 /*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL…

realsense D435l+mid360标定

目录 一、安装realsense环境 二、获取realsense D450L相机内参 三、标定雷达和相机 1.下载livox_camera_calib 2.修改配置参数 3.使用fastlio生成点云 4.标定 一、安装realsense环境 git clone https://github.com/IntelRealSense/librealsense.git cd librealsense //更…

JAVASE进阶day07(泛型,集合,Set,TreeSet,枚举,数据结构)

泛型 1.泛型的基本使用 限制集合存储的数据类型 package com.lu.day07.generics;/*** 定义了一个泛型类* E 泛型通配字母(不固定代替真实数据类型A-Z都可以)* 常见的泛型通配字母:* E:element 元素* T:type 类型* R:return 返回值类型* K:key 键* …

Blender 中导出模型fbx

准备模型&#xff1a;确保你的模型已经完成&#xff0c;并且所有的材质、纹理等都已设置好。 应用所有变换&#xff1a; 选择模型&#xff0c;按下 CtrlA&#xff0c;选择 "All Transforms" 以应用所有的变换&#xff08;位置、旋转和缩放&#xff09;。 导出模型&a…

sql常用语句:

1.联合查询 对表中的数据进行限制&#xff1b; 2.从一个表复制到另一个表 SELECT INTO 将数据复制到一个新表&#xff08;有的 DBMS 可以覆盖已经存在的表&#xff0c;这依赖于 所使用的具体 DBMS&#xff09; SELECT *&#xff08;字段&#xff09; INTO CustCopy FROM Cu…

【Redis】简单了解Redis中常用的命令与数据结构

希望文章能给到你启发和灵感&#xff5e; 如果觉得文章对你有帮助的话&#xff0c;点赞 关注 收藏 支持一下博主吧&#xff5e; 阅读指南 开篇说明一、基础环境说明1.1 硬件环境1.2 软件环境 二、Redis的特点和适用场景三、Redis的数据类型和使用3.1字符串&#xff08;String&…

ThinkPad,联想电脑F1(声音键)一直显示(亮)解决办法

ThinkPad&#xff0c;联想电脑F1&#xff08;声音键&#xff09;一直显示&#xff08;亮&#xff09; 打开“服务” 找到&#xff1a;Lenovo Hotkey Client Loader&#xff0c;选择”启动“

十九.升职加薪系列-JVM优化-解决JVM性能瓶颈的JIT即时编译器

前言 在很多年以前&#xff0c;做C或者C的程序员经常说Java语言的运行速度不如C或C&#xff0c;Java运行速度慢主要是因为它是解释执行的&#xff0c;而C或C是编译执行的&#xff0c;解释执行需要通过JVM虚拟机将字节码实时翻译成机器码&#xff08;边翻译边执行&#xff09;&…

ArcGIS Pro入门制图教程

地理信息系统 (GIS) 是一种使用地图显示和分析数据的方式。在本教程中&#xff0c;您将学习桌面 GIS 应用程序 ArcGIS Pro 的基础知识。 新加坡的一家旅行社希望制作一款宣传册&#xff0c;用于向游客介绍距离市中心热门目的地最近的火车站。该宣传册将与带有文本信息的地图相…