Vue3 之 响应式 API reactive、 effect源码,详细注释
Vue3之响应式 API reactive、 effect源码,详细注释
简单记录一下 Vue3 和 Vue2 区别
Vue3 源码采用 monorepo 方式进行管理,将模块拆分到 package 目录中
Vue3 采用 ts 开发,增强类型检测, Vue2 采用 flow,对 ts 支持并不好,flow 貌似已经不再维护了
Vue3 劫持数据采用 proxy, Vue2 劫持数据采用 defineProperty。 使用 defineProperty 会进行递归操作,有性能问题和缺陷
proxy 拦截器当数据有很多层时,不需要一上来就全部递归,只有当取到某一个值时,在使用 proxy 进行代理。
一.实现响应式 API:reactive、shallowReactive、readonly、shallowReadonly
实现以下响应式 API:reactive, shallowReactive, readonly, shallowReadonly,核心就是 new proxy
只能实现对象数据的响应式同一个对象,只会被代理一次,支持嵌套属性的响应式,被代理过的对象,不会被再次代理
- reactive:如果是深度监听,在取值的时候,默认会再次进行代理,
- shallowReactive:如果是浅层监听,不会再做深度遍历
- readonly:如果只读属性,就没有 set 方法,不能修改它,而且只读属性不会收集依赖
- shallowReadonly:如果是浅的只读属性,仅第一层不能修改,里层的可以修改
const { reactive, shallowReactive, readonly, shallowReadonly } = VueReactivity;
let obj = { name: "echo", age: { n: 18 } };
const state1 = reactive(obj);
const state2 = shallowReactive(obj);
const state3 = readonly(obj);
const state4 = shallowReadonly(obj);
1. 针对不同的 API 创建不同的响应式对象
// reactive.ts文件
import {
mutableHandlers,
shallowReactiveHandlers,
readonlyHandlers,
shallowReadonlyHandlers,
} from "./baseHandlers"; // 不同的拦截函数
export function reactive(target) {
return createReactiveObject(target, false, mutableHandlers);
}
export function shallowReactive(target) {
return createReactiveObject(target, false, shallowReactiveHandlers);
}
export function readonly(target) {
return createReactiveObject(target, true, readonlyHandlers);
}
export function shallowReadonly(target) {
return createReactiveObject(target, true, shallowReadonlyHandlers);
}
/**
*
* @param target 拦截的目标/对象
* @param isReadonly 是不是仅读属性
* @param baseHandlers 对应的拦截函数
*/
function createReactiveObject(target, isReadonly, baseHandlers) {}
2. 实现 createReactiveObject
Vue3 中采用 proxy 实现数据代理, 核心就是拦截 get 方法和 set 方法,当获取值时收集 effect 函数,当修改值时触发对应的 effect 重新执行
// reactive.ts文件
import { isObject } from "./shared";
const reactiveMap = new WeakMap(); // WeakMap 会自动垃圾回收,不会造成内存泄漏, 存储的key只能是对象
const readonlyMap = new WeakMap();
export function createReactiveObject(target, isReadonly, baseHandlers) {
// 1.如果不是对象直接返回
if (!isObject(target)) {
return target;
}
// 2.获取缓存对象
const proxyMap = isReadonly ? readonlyMap : reactiveMap;
const existProxy = proxyMap.get(target); // proxy代理结果
// 3.代理过直接返回即可
if (existProxy) {
return existProxy;
}
// 4.代理的核心
const proxy = new Proxy(target, baseHandlers); // 使用不同的拦截函数构建proxy
proxyMap.set(target, proxy); // 将代理对象和对应代理结果缓存起来
// 5.返回代理对象
return proxy;
}
3. 实现不同的拦截函数 baseHandlers.ts
实现 new Proxy(target, handler)重写 target 的 get 和 set 方法
// baseHandlers.ts文件
import { extend } from "./shared";
const get = createGetter();
const shallowGet = createGetter(false, true);
const readonlyGet = createGetter(true);
const showllowReadonlyGet = createGetter(true, true);
const set = createSetter();
const shallowSet = createSetter(true);
export const mutableHandlers = {
get,
set,
};
export const shallowReactiveHandlers = {
get: shallowGet,
set: shallowSet,
};
//仅读的属性set时会报异常
let readonlyObj = {
set: (target, key) => {
console.warn(`set on key ${key} falied`);
},
};
export const readonlyHandlers = extend(
{
get: readonlyGet,
},
readonlyObj
);
export const shallowReadonlyHandlers = extend(
{
get: showllowReadonlyGet,
},
readonlyObj
);
// 拦截get功能:执行effect时会取值并且收集effect
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {};
}
// 拦截set功能:当数据更新时通知对应属性的effect重新执行(相当于Vue2中的notify)
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {};
}
二.实现响应式 effect
默认在 Vue 源码中,不包含这个方法的,不能解构出 effect,意思就是说我们在使用 vue 模块时,是内部所有包的整合,但是它并不导出所有包的方法,effect方法是存在于 reactivity 响应式模块中的,但是 reactivity 响应式模块并没有把 effect 方法暴露给最外层的 Vue 模块 所以解构拿不到
effect 方法 相当于 vue2 中的 watcher
effect 方法的作用:该默认会执行一次,一般会放入一些渲染逻辑,执行时会进行取值操作,只要取值就会调用 proxy 的 get 方法 中,此时可以将对应的 effect 函数存起来,
更新数据时,数据变了(effect 中用到的数据变化才会触发) 就会触发 set 方法,重新执行刚刚存储的 effect 函数,重新渲染页面,触发 set 重新渲染的逻辑又相当于 Vue2 中的 notify
// 1. effect中的所有属性 都会收集effect 触发track方法
// 2. 当这个属性值发生变化 会重新执行effect 触发trigger方法
let { effect, reactive } = VueReactivity;
let state = reactive({ name: "echo" });
effect(() => {
console.log("render");
app.innerHTML = state.name;
});
setTimeout(() => {
state.name = "yya"; // 更改name属性需要重新执行
}, 1000);
1. 创建响应式的 effect,并与原函数进行关联
// effect.ts文件
export function effect(fn, options: any = {}) {
// 创建响应式effect,可以做到数据变化重新执行
const effect = createReactiveEffect(fn, options);
// 响应式的effect默认会先执行一次
if (!options.lazy) {
effect();
}
return effect;
}
let uid = 0;
let activeEffect; // 存储当前的effect
const effectStack = []; // 利用栈型结构存储effect,保证依赖关系的顺序
function createReactiveEffect(fn, options) {
const effect = function reactiveEffect() {
// 1.判断 effect是否存在于 effectStack栈中
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect);
// 2.记录当前的effect
activeEffect = effect;
// 3.执行用户传递的fn,函数执行时会取值,会执行get方法
return fn(); // effect方法是有返回值的,用户的返回值就是effect的返回值
} finally {
// 4.执行完effect 弹出栈
effectStack.pop();
// 5.获取栈中最后一个,作为当前活跃effect
activeEffect = effectStack[effectStack.length - 1];
}
}
};
effect.id = uid++; // 标识用于区分effect
effect._isEffect = true; // 标识是响应式effect
effect.raw = fn; // 记录effect对应的原函数->映射关系
effect.options = options; // 在effect上保存用户的属性
return effect;
}
2. 拦截 get 功能 createGetter 函数
createGetter:取值时会在 proxy 对应的 get 方法中,执行依赖收集方法
// baseHandlers.ts文件
import { isObject } from "./shared";
import { TrackOpTypes } from "./shared";
import { track } from "./effect";
import { reactive, readonly } from "./reactive";
// 拦截get功能:执行effect时会取值并且收集effect
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
// 非仅读才需要依赖收集
if (!isReadonly) {
// 收集依赖,数据变化会更新对应的视图
track(target, TrackOpTypes.GET, key); // 会在effect.ts文件中实现
}
// 如果是浅代理,直接返回代理对象结果
if (shallow) {
return res;
}
if (isObject(res)) {
// vue2 是一上来就递归,vue3 是当取值时会进行代理,代理模式是懒代理
return isReadonly ? readonly(res) : reactive(res);
}
return res;
};
}
3. 依赖收集 track 函数
track:依赖收集方法
让对象中的属性 收集当前对应的 effect 函数,维护构建 对象 => 对象属性 => effect之间的关系
最外层是一个 WeakMap,weakMap 的可以是一个对象,value 值是一个 Map, Map 的 key 是属性名,value 是对应的 effect 集合 Set,因为一个属性可能对应多个 effect 函数
{ key => {name:'echo',age:18}, value => (map) => { name => set(effect), age => set(effect) }}
// effect.ts文件
const targetMap = new WeakMap();
export function track(target, type, key) {
// 1. activeEffect 拿到当前正在运行的effect
if (activeEffect === undefined) {
// 此属性不用收集依赖,因为没在effect中使用
return;
}
// 2. 通过对象取值,取到的值应该是一个Map
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map())); // 不存在就构建一个Map
}
// 3. 判断当前 Map 中有没有 key对应的依赖收集
let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set())); // 不存在就构建一个Set,存放去重后的effect方法
}
// 4. 判断当前 Set 中有没有activeEffect
if (!dep.has(activeEffect)) {
dep.add(activeEffect); // 不存在就存放 effect方法
}
}
4. 拦截 set 功能 createSetter 函数
createSetter:当数据更新时通知对应属性的 effect 重新执行(相当于 Vue2 中的 notify)
// baseHandlers.ts文件
import { hasChanged, hasOwn, isArray, isIntegerKey } from "./shared";
import { TriggerOrTypes } from "./shared";
import { trigger } from "./effect";
// 拦截set功能:当数据更新时通知对应属性的effect重新执行
function createSetter(shallow = false) {
return function set(target, key, value, receiver) {
const oldValue = target[key]; // 获取老的值
// hadKey = 既是数组 && 修改的是索引 ? 判断索引是否是当前长度以内的(长度以内就是修改,反之新增) : 其他情况就是对象,判断target有没有key这个属性
let hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key);
const result = Reflect.set(target, key, value, receiver); // target[key] = value
// 对新增属性和修改属性做分类,
if (!hadKey) {
// 新增属性
trigger(target, TriggerOrTypes.ADD, key, value);
} else if (hasChanged(oldValue, value)) {
// 判断新旧两值是否相等
// 修改属性
trigger(target, TriggerOrTypes.SET, key, value, oldValue);
}
return result;
};
}
5. 触发更新 trigger 函数
将需要触发的 effect 找到依次执行
// effect.ts文件
export function trigger(target, type, key?, newValue?, oldValue?) {
// 1.如果这个属性没有 收集过effect,那就直接return
const depsMap = targetMap.get(target);
if (!depsMap) return;
// 2. 将所有要执行的effect 全部存到一个新的集合中,最终一起执行
const effects = new Set();
const add = (effectsToAdd) => {
if (effectsToAdd) {
effectsToAdd.forEach((effect) => effects.add(effect));
}
};
// 3. 看修改的是不是数组的长度
if (key === "length" && isArray(target)) {
// 4. 如果对应的长度 有依赖收集 需要更新
depsMap.forEach((dep, key) => {
// 更改的长度小于收集的索引,那么这个索引也需要触发effect重新执行
if (key === "length" || key > newValue) { //
add(dep);
/**代码实例
effect(() => {
app.innerHTML = state.arr[2] // 收集的索引为 2
})
setTimeout(() => {
state.arr.length = 1 ; // 更改的长度为1,需要重新执行
}, 1000);
}
*/
});
} else { // 5. 其他情况可能是对象
if (key !== undefined) { // 这里肯定是修改,不能是新增,新增的属性没有收集过effect,不需要重新渲染
add(depsMap.get(key));
}
// vue2里无法监控更改索引,无法监控数组的长度变化 -> 需要通过hack的方法特殊处理
// 修改数组中的 某一个索引: 如果添加了一个索引就触发长度的更新
switch (type){
case TriggerOrTypes.ADD:
if (isArray(target) && isIntegerKey(key)) {
add(depsMap.get("length"));
}
}
}
effects.forEach((effect: any) => {
if (effect.options.scheduler) {
effect.options.scheduler();
} else {
effect();
}
});
}
shared 工具方法抽离
export const isObject = (value) => typeof value == "object" && value !== null;
export const extend = Object.assign;
export const isArray = Array.isArray;
export const isIntegerKey = (key) => parseInt(key) + "" === key;
let hasOwnpRroperty = Object.prototype.hasOwnProperty;
// 判断target上是否存在属性key
export const hasOwn = (target, key) => hasOwnpRroperty.call(target, key);
// 判断新值与旧值是否相等
export const hasChanged = (oldValue, value) => oldValue !== value;
// track 操作符
export const enum TrackOpTypes {
GET,
}
// trigger 操作符
export const enum TriggerOrTypes {
ADD,
SET,
}
Vue3 之 响应式 API reactive、 effect源码,详细注释的更多相关文章
- ExcelToHtmlTable转换算法:将Excel转换成Html表格并展示(项目源码+详细注释+项目截图)
功能概述 Excel2HtmlTable的主要功能就是把Excel的内容以表格的方式,展现在页面中.Excel的多个Sheet对应页面的多个Tab选项卡.转换算法的难点在于,如何处理行列合并,将Exc ...
- Bootstrap的Model源码详细注释 (转)
原文: http://my.oschina.net/haogrgr/blog/323079?p=1 /* =============================================== ...
- Vue3中的响应式对象Reactive源码分析
Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...
- Vue3.0工程创建 && setup、ref、reactive函数 && Vue3.0响应式实现原理
1 # 一.创建Vue3.0工程 2 # 1.使用vue-cli创建 3 # 官方文档: https://cli.vuejs.org/zh/guide/creating-a-project.html# ...
- Vue3中的响应式api
一.setup文件的认识 特点1:script 中间的内容就是一个对象 特点2:script 在第一层 定义的方法 或者 变量 => 就是这个对象 属性 => 顶层的绑定回被暴露给模板( ...
- 简单对比vue2.x与vue3.x响应式及新功能
简单对比vue2.x与vue3.x响应式 对响应方式来讲:Vue3.x 将使用Proxy ,取代Vue2.x 版本的 Object.defineProperty. 为何要将Object.defineP ...
- Paip.Php Java 异步编程。推模型与拉模型。响应式(Reactive)”编程FutureData总结... 1
Paip.Php Java 异步编程.推模型与拉模型.响应式(Reactive)"编程FutureData总结... 1.1.1 异步调用的实现以及角色(:调用者 提货单) F ...
- Vue3.0响应式实现
基于Proxy // 弱引用映射表 es6 防止对象不能被回收 let toProxy = new WeakMap(); // 原对象: 代理过得对象 let toRaw = new WeakMap( ...
- Asp.net MVC集成Google Calendar API(附Demo源码)
Asp.net MVC集成Google Calendar API(附Demo源码) Google Calendar是非常方便的日程管理应用,很多人都非常熟悉.Google的应用在国内不稳定,但是在国外 ...
- 阿里云视频直播API签名机制源码
阿里云视频直播API签名机制源码 本文展示:通过代码实现下阿里视频直播签名处理规则 阿里云视频直播签名机制,官方文档链接:https://help.aliyun.com/document_detail ...
随机推荐
- 【LeetCode动态规划#04】不同的二叉搜索树(找规律,有点像智力题)
不同的二叉搜索树 力扣题目链接(opens new window) 给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? 示例: 思路 题意分析 先找一下关系 当n = 1时,如果 ...
- 【实战】SpringBoot+uniapp+uview打造H5+小程序+APP入门学习的聊天小项目
JavaDog Chat v1.0.0 基于SpringBoot+uniapp简单通讯聊天软件 项目介绍 JavaDog Chat 简单通讯聊天软件是基于SpringBoot+MybatisPlus+ ...
- 使用Electron-packager打包已有的web项目,发布客户端
1.先拉electron代码 git clone https://github.com/electron/electron-quick-start 2.将web项目拷贝到electron-quick- ...
- [网络/Linux]CentOS7:OpenSSH升级到7.9p1 | 含: 安装Telnet/OpenSSH【telnet/ssh】
[Q0 OpenSSH/sshd/ssh/scp/sftp,及OpenSSL这些软件组件之间有什么联系吗?] 请跳转咱的另一篇博文,相信阅读完后,你会清楚很多: [网络/SSH]OpenSSH: ss ...
- [Linux]常用命令之【history】#查看历史操作#
1 历史记录: history history命令就是历史记录. 它显示了在终端中所执行过的所有命令的历史. history //显示终端执行过的命令 history 10 //显示最近10条终端执行 ...
- Redis(六)集群
Redis集群 1.1 存在的问题 容量不够Redis如何扩容 并发写操作,Redis如何分摊 当主机或者从机宕机,薪火相传.反客为主等主从模式都会导致ip发生变化,应用程序中的配置需要对应修改主机地 ...
- Kubernetes入门实践(搭建Wordpress网站)
容器只是对单个进程的隔离和封装,实际的应用场景要求许多的应用进程互相协同工作,因此出现了容器编排,Kubernetes将集群中的计算资源定义为节点(Node),其中又划分成控制面和数据面两类,控制面是 ...
- 浅谈php GC(垃圾回收)机制及其与CTF的一点缘分
0x00 侠客日常(一):CTF江湖试剑 众所周知,在php中,当对象被销毁时会自动调用__destruct()方法,同时也要知道,如果程序报错或者抛出异常,则就不会触发该魔术方法. 看题: < ...
- Azure DevOps(二)Azure Pipeline 集成 SonarQube 维护代码质量和安全性
一,引言 对于今天所分析的 SonarQube,首先我们得了解什么是 SonarQube ? SonarQube 又能帮我们做什么?我们是否在项目开发的过程中遇到人为 Review 代码审核规范?带着 ...
- AI 绘画咒语入门 - Stable Diffusion Prompt 语法指南 【成为初级魔导士吧!】
要用好 Stable Diffusion,最最重要的就是掌握 Prompt(提示词).由于提示词对于生成图的影响甚大,所以被称为魔法,用得好惊天动地,用不好魂飞魄散 . 因此本篇整理下提示词的语法(魔 ...