Vue响应式系统:从原理到核心API全解析

响应式原理

响应式机制的主要功能就是,可以把普通的JavaScript对象封装成为响应式对象,拦截数据的读取和设置操作,实现依赖数据的自动化更新。

Q: 如何才能让JavaScript对象变成响应式对象?

首先需要认识响应式数据和副作用函数,副作用函数指的是产生副作用的函数。

通过一个demo函数来理解一下:

let val = 1;
function effect() {val = 2;// 修改全局变量,产生副作用
}
effect()console.log(val)// 2

当console打印时,会触发字段的读取操作,副作用函数effect执行时,会触发设置操作。当我们能拦截一个对象的读取和设置操作时,那事情就变得简单了。

在Vue2时,通过Object.defineProperty函数实现,Vue3采用Proxy来实现。

根据如上思路,采用Proxy实现方式如下:

// 存储副作用函数的桶
const bucket = new Set();const data = { text: "summer" }
// 对原始数据的代理
const obj = new Proxy(data, {// 拦截读取操作get(target, key) {//将副作用函数effect添加到存储副作用函数的桶中bucket.add(effect)return target[key]},// 拦截设置操作set(target, key, newVal) {target[key] = newVal;// 把副作用函数从桶里取出并执行bucket.forEach(fn => fn())return true}
})

副作用函数可以是任意名字,上面的代码是帮助我们理解响应式数据的基本实现和工作原理。

从上面的代码片段中可以看出,一个响应系统的工作流程如下:

当读取操作发生时,将副作用函数收集到“桶”中;当设置操作发生时,从“桶”中取出副作用函数并执行。

核心API和响应式工具函数

reactive:

reactive是通过ES6中的Proxy特性实现的属性拦截,所以在reactive函数中我们直接返回new Proxy即可:

export function reactive(target) {if (typeof target!=='object') {console.warn(`reactive  ${target} 必须是一个对象`);return target}return new Proxy(target, mutableHandlers);
}
mutableHandlers

mutableHandlers要做的事就是配置Proxy的拦截函数,这里我们只拦截get和set操作,进入到baseHandlers.ts中。

使用createGetter和createSetter来创建get和set函数,mutableHandlers就是配置了get和set的对象返回。

get直接返回读取的数据,这里的Reflect.get和target[key]实现的结果是一致的;并且返回值是对象的话,还会嵌套执行reactive,并且调用track函数收集依赖。set调用trigger函数,执行track收集的依赖。

const get = createGetter();
const set = createSetter();function createGetter(shallow = false) {return function get(target, key, receiver) {const res = Reflect.get(target, key, isRef(target) ? target : receiver);if (!isReadonly) {track(target, "get", key);}if(isObject(res)) {// 值也是对象的话,需要嵌套调用reactivereturn isReadonly ? readonly(res) : reactive(res)}return res;}
}function createSetter() {return function set(target, key, value, receiver) {const result = Reflect.set(target, key, value, isRef(target) ? target : receiver);if (target === toRaw(receiver)) {if (!hadKey) {trigger(target, 'add', key, value)} else if (hasChanged(value, oldValue)) {trigger(target, 'set', key, value, oldValue)}}return result}
}export const mutableHandlers = {get, set
}
track

在track函数中,我们可以使用一个巨大的targetMap去存储依赖关系。map的key是我们要代理的target对象,值还是一个depsMap,存储每一个key依赖的函数,每一个key都可以依赖多个effect。代码如下:

const targetMap = new WeakMap();export function track(target,type, key) {// 没有 activeEffect,直接returnif(!activeEffect) return;let depsMap = targetMap.get(target);if(depsMap) {targetMap.set(target, (depsMap = new Map()))}let dep = depsMap.get(key);if(!dep) {depsMap.set(key, (dep = new Set()))}deps.add(activeEffect)
}
trigger

根据targetMap的实现机制,trigger函数实现的思路就是从targetMap中,根据target和key找到对应的依赖函数集合deps,然后遍历deps执行依赖函数。代码如下:

export function trigger(target,type, key) {const depsMap = targetMap.get(target);if(!depsMap) {// 没找到依赖return;}const deps = depsMap.get(key)if(!deps) return;deps.forEach((effectFn) => {// 判断副作用函数是否存在调度器if(effectFn.scheduler) {effectFn.scheduler()} else {effectFn()}})
}
effect

我们把传递进来的fn函数通过effectFn函数包裹执行,在effectFn函数内部,把函数赋值给全局变量activeEffect;然后执行fn()的时候,就会触发响应式对象的get函数,get函数内部就会把activeEffect存储到依赖中,完成依赖的收集。

export function effect(fn, options =  {}) {// effect嵌套通过队列管理const effectFn = () => {try {activeEffect = effectFn;return fn();} finally {activeEffect = null;}}if(!options.lazy) {// 没有配置lazy,直接执行effectFn()}effectFn.scheduler = options.scheduler;return effectFn;
}

effect传递的函数,可以通过lazy和scheduler来控制函数的执行时机,默认是同步执行。

scheduler

使用数组管理传递的执行任务,最后使用Promise.resolve只执行最后一次,这也是Vue中watchEffect函数的大致原理。

const obj = reactive({ count: 1});
effect(() => {console.log(obj.count)
}, {scheduler: queueJob
});
// 调度器实现
const queue: Function[] = [];
let isFlushing = false;
function queueJob(job: () => void) {if(!isFlushing) {isFlushing = true;Promise.resolve().then(() => {let fn;while(fn = queue.shift()) {fn()}})}
}

ref

ref的执行逻辑比reactive要简单一些,不需要使用Proxy代理语法,直接使用对象语法中的getter和setter配置,监听value属性即可。对象的get value方法,使用track函数去收集依赖,set value方法中使用trigger函数去触发函数的执行。

export function ref(val) {if(isRef(val)) {return val;}
}export function isRef(val) {return !!(val && val.__isRef)
}// 利用面向对象的getter和setter进行track和trigger
class RefImpl {constructor(val, isShallow: boolean) {this._rawValue = isShallow ? value : toRaw(value)this._value = isShallow ? value : toReactive(value)this.__v_isShallow = isShallow}get value() {track(this, 'value')return this._value;}set value(newValue) {const oldValue = this._rawValue;const useDirectValue =this.__v_isShallow ||isShallow(newValue) ||isReadonly(newValue);newValue = useDirectValue ? newValue : toRaw(newValue);if(hasChanged(newValue,oldValue)) {this._rawValue = newValue;this._value = useDirectValue ? newValue : toReactive(newValue);trigger(this, "value")}}
}export const hasChanged = (value: any, oldValue: any): boolean =>!Object.is(value, oldValue)

ref也可以包裹复杂的数据结构,内部会直接调用reactive来实现,这也解决了日常对ref和reactive使用时机的疑惑,现在可以全部都用ref函数,ref内部会帮我们调用reactive。

computed

computed计算属性也是一种特殊的effect函数。在computed函数,我们拦截了computed的value属性,并且定制了effect的lazy和scheduler配置,computed注册的函数就不会直接执行,而是要通过scheduler函数中对_dirty属性决定是否执行。

export function computed(getterOrOptions) {let getter, setter;if(isFunction(getterOrOptions)) {getter = getterOrOptions;setter = () => {console.warn('computed value is readonly')}} else {getter = getterOrOptions.get;setter = getterOrOptions.set;}return new ComputedRefImpl(getter, setter);
}class ComputedRefImpl {constructor(getter, setter) {this._setter = setter;this._val = undefined;this._dirty = true;// computed就是一个特殊的effect,设置lazy和执行时机this.effect = effect(getter, {lazy: true,scheduler:() => {if(!this._dirty) {this._dirty = true;trigger(this, 'value')}}})}get value() {track(this, 'value')if(this._dirty) {this._dirty = false;this._val = this.effect()}return this._val;}set value(val) {this._setter(val)}
}

watch

watch本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数;实现本质上就是利用了effect以及options.scheduler选项。

function watch(source, cb) {let getter;if(typeof source === 'function') {getter = source;} else {getter = () => traverse(source)}let oldValue, newValue;const effectFn = effect(// 调用traverse递归地读取() => getter, {lazy: true,scheduler() {// 执行副作用函数,获取新值newValue = effectFn();// 将旧值和新值作为回调函数的参数cb(newValue, oldValue);// 更新旧值oldValue = newValue;}})// 手动调用副作用函数,获取到旧值oldValue = effectFn();
}function traverse(value, seen = new Set()) {// 如果要读取的数据是原始值,或者已经被读取过,那么就不做处理if(!isObject(value) || value === null || seen.has(value)) return;seen.add(value)if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], seen)}}}return value;
}

总结

在探索Vue响应式系统的旅程中,我们深入剖析了其底层逻辑与关键实现。响应式原理作为基石,通过数据劫持(Vue2依托Object.defineProperty, Vue3借助更强大的Proxy),搭配依赖收集与派发更新机制,构建起数据与试图自动同步的桥梁。

核心API与工具函数则是响应式能力的具体延伸:reactive借助mutableHandlers等完成对象响应式转换,track精准收集依赖、trigger触发更新,effect与scheduler协同管控副作用执行;ref适配基本类型与对象的响应式需求,computed基于依赖缓存实现高效计算,watch灵活监听数据变化。这些内容共同编织出Vue响应式系统的完整生态,理解它们,方能在Vue开发中精准驾驭数据驱动视图的精髓,应对复杂场景时游刃有余,为打造高效、可维护的Vue应用筑牢基础。

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

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

相关文章

水下目标检测:突破与创新

水下目标检测技术背景 水下环境带来独特挑战&#xff1a;光线衰减导致对比度降低&#xff0c;散射引发图像模糊&#xff0c;色偏使颜色失真。动态水流造成目标形变&#xff0c;小目标&#xff08;如1010像素海胆&#xff09;检测困难。声呐与光学数据融合可提升精度&#xff0…

高通SG882G平台(移远):2、使用docker镜像编译

其实之前已经编译过了。今日搜索时发现&#xff0c;只有当时解决问题的汇总&#xff0c;没有操作步骤。于是记录下来。 建议使用Ubuntu20 LTS。 安装docker $ sudo apt update $ sudo apt install docker.io $ sudo docker -v Docker version 27.5.1, build 27.5.1-0ubuntu3…

轻松上手:使用Nginx实现高效负载均衡

接上一篇《轻松上手&#xff1a;Nginx服务器反向代理配置指南》后&#xff0c;我们来探讨一下如何使用Nginx实现高效负载均衡。 在当今高并发、大流量的互联网环境下&#xff0c;单台服务器早已无法满足业务需求。想象一下&#xff1a;一次电商平台的秒杀活动、一个热门应用的…

身份证号码+姓名认证接口-身份证二要素核验

身份证号实名认证服务接口采用身份证号码、姓名二要素核验的方式&#xff0c;能够快速确认用户身份。无论是新用户注册&#xff0c;还是老用户重要操作的身份复核&#xff0c;只需输入姓名及身份证号&#xff0c;瞬间即可得到 “一致” 或 “不一致” 的核验结果。这一过程高效…

自动驾驶基本概念

目录 自动驾驶汽车&#xff08;Autonomous Vehicles &#xff09; 单车智能 车联网 智能网联&#xff08;单车智能车联网&#xff09; 自动驾驶关键技术 环境感知与定位 车辆运动感知 车辆运动感知 路径规划与决策 自动驾驶发展历程 自动驾驶应用场景 自动驾驶路测…

提示词框架(10)--COAST

目前&#xff0c;有很多提示词框架都叫COAST&#xff0c;但是每个的解释都不同&#xff0c;出现很了很多解释和演化版本&#xff0c;不要在意这些小事&#xff0c;我们都是殊途同归--让AI更好的完成任务COAST框架&#xff0c;比较适合需要详细背景和技术支持的任务&#xff0c;…

基于selenium实现大麦网自动抢票脚本教程

闲来无事&#xff0c;打开大麦网发现现在大多数演唱票都需要手机端才能抢票&#xff0c;仅有很少一部分支持pc端用网页去抢票&#xff0c;但正所谓&#xff1a;道高一尺&#xff0c;魔高一丈&#xff0c;解决这个反爬问题&#xff0c;我们可以采用Airtest连接仿真机来模拟手机端…

2048小游戏实现

2048小游戏实现 将创建一个完整的2048小游戏&#xff0c;包含游戏核心逻辑和美观的用户界面。设计思路 4x4网格布局响应式设计&#xff0c;适配不同设备分数显示和最高分记录键盘控制&#xff08;方向键&#xff09;和触摸滑动支持游戏状态提示&#xff08;胜利/失败&#xff0…

Windows VMWare Centos Docker部署Springboot + mybatis + MySql应用

前置文章 Windows VMWare Centos环境下安装Docker并配置MySqlhttps://blog.csdn.net/u013224722/article/details/148928081 Windows VMWare Centos Docker部署Springboot应用https://blog.csdn.net/u013224722/article/details/148958480 Windows VMWare Centos Docker部署…

【科普】Cygwin与wsl与ssh连接ubuntu有什么区别?DIY机器人工房

Cygwin、WSL&#xff08;Windows Subsystem for Linux&#xff09;和通过 SSH 连接 Ubuntu 是三种在 Windows 环境下与类 Unix/Linux 系统交互的工具&#xff0c;但它们的本质、运行环境、功能范围有显著区别。以下从核心定义、关键差异和适用场景三个维度详细说明&#xff1a;…

Web前端数据可视化:ECharts高效数据展示完全指南

Web前端数据可视化&#xff1a;ECharts高效数据展示完全指南 当产品经理拿着一堆密密麻麻的Excel数据走向你时&#xff0c;你知道又到了"化腐朽为神奇"的时刻。数据可视化不仅仅是把数字变成图表那么简单&#xff0c;它是将复杂信息转化为直观洞察的艺术。 在过去两…

# IS-IS 协议 | LSP 传输与链路状态数据库同步机制

略作整理&#xff0c;待校。 SRM 和 SSN 标志的作用 SRM 标志 功能&#xff1a;SRM 标志用于跟踪路由器从一个接口向邻居发送链路状态协议数据单元&#xff08;LSP&#xff09;的状态。作用&#xff1a;确保 LSP 的正确传输和状态跟踪。 SSN 标志 广播网络 功能&#xff1…

Windows DOS CMD 100

1. systeminfo&#xff1a;显示系统详细信息&#xff08;安装日期/补丁/内存等&#xff09; 2. sfc /scannow&#xff1a;扫描并修复系统文件损坏 [管理员] 3. chkdsk /f&#xff1a;检查磁盘错误并修复&#xff08;需重启&#xff09; [管理员] 4. cleanmgr&#xff1a;启动…

HTML初学者第三天

<1>文档类型声明标签——<!DOCTYPE><!DOCTYPE>文档声明&#xff0c;作用是告诉浏览器使用哪种HTML版本来显示网页。<!DOCTYPE html>这句代码的意思是&#xff1a;当前页面采用的是HTML5版本来显示网页。注意&#xff1a;-<!DOCTYPE>声明位于文档…

学车笔记6

“不踩离合利用发动机制动”是指在驾驶过程中&#xff0c;驾驶员抬起油门踏板&#xff0c;但不踩下离合器踏板&#xff0c;利用发动机自身的阻力来减缓车辆速度的一种制动方式。具体介绍如下&#xff1a; #### 原理 - **动力传递反向**&#xff1a;正常情况下&#xff0c;发动…

人体坐姿检测系统项目教程(YOLO11+PyTorch+可视化)

&#x1f4a1;本文主要内容&#xff1a;本项目基于YOLO11深度学习目标检测算法&#xff0c;设计并实现了一个人体坐姿检测系统。系统能够自动识别图像或视频中的多种坐姿类型&#xff08;如&#xff1a;正常坐姿、不良坐姿等&#xff09;&#xff0c;为健康监测、智能教室、办公…

服务网格可观测性深度实践与创新优化

主题&#xff1a;突破服务网格监控瓶颈——基于eBPF的无侵入式全链路可观测性实践 技术领域&#xff1a;云原生/微服务/服务网格&#xff08;Service Mesh&#xff09; 一、问题背景&#xff1a;传统服务网格监控的痛点 在Istio、Linkerd等服务网格架构中&#xff0c;可观测…

微信小程序41~50

1.列表渲染-进阶用法 如果要对默认的变量名和下标进行修改&#xff0c;可以使用wx:for-item和wx:for-index wx:for-item可以指定数组当前元素的变量名 wx:for-index可以指定数组当前下标的变量名将wx:for用在标签上&#xff0c;以渲染一个包含多个节点的结构快 并不是一个组件…

向量数据库-Milvus快速入门

Milvus 概述 向量是神经网络模型的输出数据格式&#xff0c;可以有效地对信息进行编码&#xff0c;在知识库、语义搜索、检索增强生成&#xff08;RAG&#xff09;等人工智能应用中发挥着举足轻重的作用。 Milvus 是一个开源的向量数据库&#xff0c;适合各种规模的人…

uniapp的光标跟随和打字机效果

1、准备好容器文字的显示textRef&#xff0c;以及光标的显示 &#xff0c;使用transform-translate对光标进行移动到文字后面<template><view class"container" ref"contentRef"><u-parse :content"nodeText" ref"textRef&q…