Vue3 从ref 函数入手透彻理解响应式原理

您所在的位置:网站首页 REF函数看不到图片 Vue3 从ref 函数入手透彻理解响应式原理

Vue3 从ref 函数入手透彻理解响应式原理

2024-01-07 21:00| 来源: 网络整理| 查看: 265

前言

vue3从发布开始已经有一年有余,近来开始撸源码,真是惭愧至极,啥也别说了,洗心革面 开干!直接上源码枯燥乏味

这里仅仅是我自己的理解响应式原理之后的简版代码

目标

我们今天的目标

1、通过从ref 入手,彻底的了解响应式的原理 2、理解effect 的副作用函数是怎么响应式执行的 ref 函数的原理

首先我们来看看ref官方文档是怎么解释ref 函数的

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property.value。

通俗的将其实就是当前的ref 函数返回的值就是一个对象,这个对象包含get 和set ,转换成es5 就是Object.defineProperty 监听的一个值

废话少说,看代码

// 判断是不是对象 const isObject = (val) => val !== null && typeof val === 'object'; // ref的函数 function ref(val) { // 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象 // 我们在对象情况下就不在使用value 访问 return isObject(val) ? reactive(val) : new refObj(val); } //创建响应式对象 class refObj { constructor(val) { this._value = val; } get value() { // 在第一次执行之后触发get来收集依赖 track(this, 'value'); return this._value; } set value(newVal) { console.log(newVal); this._value = newVal; trigger(this, 'value'); } };

看了上述代码我们发现,其实当前的这个神奇的响应式的值,就是一个对象 ,当你改变这个值的时候,就会触发当前这个对象的get 和set 从而达到响应式的能力

接下来发现是个对象就好办了,我们就能在get 和set 的方法中去做一些事情,比如建立副作用和当前这个值的关系,也就是依赖收集

但是此时又会有个问题,如果在ref 中传入一个对象,new 当前这个对象的时候,就不好使了,因为里面的值就监听不到了

于是vue3中大名鼎鼎的Proxy登场了

Proxy

具体的使用方法,咱就不介绍,vue3出来这么长时间了, 相信大家都明白他的特性,直接上代码

// 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况 // 其实对象的响应式处理也就是重复执行reactive function reactive(target) { return new Proxy(target, { get(target, key, receiver) { // Reflect用于执行对象默认操作,更规范、函数式 // Proxy和Object的方法Reflect都有对应 const res = Reflect.get(target, key, receiver); track(target, key); return res; }, set(target, key, value, receiver) { const res = Reflect.set(target, key, value, receiver); trigger(target, key); return res; }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key); trigger(target, key); return res; } }); }

上述代码中,将对象类型的也变成了响应式对象,接下来就是重点的地方了,要在这两个响应式的对象的get 和set 中去做依赖收集,和派发对应的副作用更新

既然需要副作用,那么怎么也要先收集一下吧,于是effect相当于桥梁函数

effect实现

总的来说这个effect 做了什么事情呢?他其实就是对当前的副作用函数进行包装,然后执行,触发副作用函数中的get,在get中在收集当前副作用,代码如下

// 保存临时依赖函数用于包装 const effectStack = []; // 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况 // 我们意在理解原理,只需要包装fn 即可 function effect(fn) { // 包装当前依赖函数 const effect = function reactiveEffect() { // 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处 if (!effectStack.includes(effect)) { try { // 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系 effectStack.push(fn); // 执行当前函数,开始依赖收集了 return fn(); } finally { // 执行成功了出栈 effectStack.pop(); } }; }; effect(); }

他的原理比较巧妙,利用一个栈,将当前正在执行的副作用函数临时存储,在get中取出,存入依赖对象中。

那么如此一来,顺其自然的就需要有一个函数去收集依赖(这个依赖有可能是一个render 函数,也有可能是一个副作用,我们的例子中,由于没有涉及视图渲染相关,都是副作用),于是定义一个track 函数去收集依赖

track实现

废话少说上代码

// 依赖关系的map对象只能接受对象 let targetMap = new WeakMap(); // 在收集的依赖中建立关系 function track(target, key) { // 取出最后一个数据内容 const effect = effectStack[effectStack.length - 1]; // 如果当前变量有依赖 if (effect) { //判断当前的map中是否有target let depsMap = targetMap.get(target); // 如果没有 if (!depsMap) { // new map存储当前weakmap depsMap = new Map(); targetMap.set(target, depsMap); } // 获取key对应的响应函数集 let deps = depsMap.get(key); if (!deps) { // 建立当前key 和依赖的关系,因为一个key 会有多个依赖 // 为了防止重复依赖,使用set deps = new Set(); depsMap.set(key, deps); } // 存入当前依赖 if (!deps.has(effect)) { deps.add(effect); } } }

track就有讲究了,他其实是建立了当前的响应式对象的每一个key 和 依赖对应关系,从而当key 发生变换的时候通知所有的依赖更新,怕大家不太理解,贴心的画了张图供大家理解

image.png

根据图中结构,我们就能看到,所有依赖的数据结构

接下来我们就需要派发更新,使用trigger函数来处理

trigger实现

代码如下

// 用于触发更新 function trigger(target, key) { // 获取所有依赖内容 const depsMap = targetMap.get(target); // 如果有依赖的话全部拉出来执行 if (depsMap) { // 获取响应函数集合 const deps = depsMap.get(key); if (deps) { // 执行所有响应函数 const run = (effect) => { // 源码中有异步调度任务,我们在这里省略 effect(); }; deps.forEach(run); } } }

从以上代码看就非常简单取出当前修改的key 对应的依赖,全部执行一下也就是所谓的派发更新,到这里基本响应式原理基本都结束了。就是这么简单且有趣!之后附上自己画的响应式流程图,供大家理解,不对之处请指点

image.png

最后

我自己所理解的vue的响应式模块到此全部完毕,当然源码中有这很多兼容处理,高端写法。我们这里只为研究原理,暂不深究如有兴趣请移步 reactivity模块,详细研究。结尾附上可以跑的完整源码,亲自尝试一下吧!

// 保存临时依赖函数用于包装 const effectStack = []; // 依赖关系的map对象只能接受对象 let targetMap = new WeakMap(); // 判断是不是对象 const isObject = (val) => val !== null && typeof val === 'object'; // ref的函数 function ref(val) { // 此处源码中为了保持一致,在对象情况下也做了用value 访问的情况value->proxy对象 // 我们在对象情况下就不在使用value 访问 return isObject(val) ? reactive(val) : new refObj(val); } //创建响应式对象 class refObj { constructor(val) { this._value = val; } get value() { // 在第一次执行之后触发get来收集依赖 track(this, 'value'); return this._value; } set value(newVal) { console.log(newVal); this._value = newVal; trigger(this, 'value'); } }; // 对象的响应式处理 在这里我们为了理解原理原理暂时不考虑对象里嵌套对象的情况 // 其实对象的响应式处理也就是重复执行reactive function reactive(target) { return new Proxy(target, { get(target, key, receiver) { // Reflect用于执行对象默认操作,更规范、函数式 // Proxy和Object的方法Reflect都有对应 const res = Reflect.get(target, key, receiver); track(target, key); return res; }, set(target, key, value, receiver) { const res = Reflect.set(target, key, value, receiver); trigger(target, key); return res; }, deleteProperty(target, key) { const res = Reflect.deleteProperty(target, key); trigger(target, key); return res; } }); } // 到此处,当前的ref 对象就已经实现了对数据改变的监听 const newRef = ref(0); // 但是还是没有响应式的能力,那么他是怎样实现响应式的呢----依赖收集,触发更新= // 用来做依赖收集 // 在源码中为为了方法的通用性,他还传入了很多参数用于兼容不同情况 // 我们意在理解原理,只需要包装fn 即可 function effect(fn) { // 包装当前依赖函数 const effect = function reactiveEffect() { // 模拟源码中也加入错误处理,为了避免你瞎写出现错误的情况,这就是框架的高明之处 if (!effectStack.includes(effect)) { try { // 给当前函数放入临时栈中,为在下面执行中,触发get,在依赖收集中能找到当前变量的依赖项来建立关系 effectStack.push(fn); // 执行当前函数,开始依赖收集了 return fn(); } finally { // 执行成功了出栈 effectStack.pop(); } }; }; effect(); } // 在收集的依赖中建立关系 function track(target, key) { // 取出最后一个数据内容 const effect = effectStack[effectStack.length - 1]; // 如果当前变量有依赖 if (effect) { //判断当前的map中是否有target let depsMap = targetMap.get(target); // 如果没有 if (!depsMap) { // new map存储当前weakmap depsMap = new Map(); targetMap.set(target, depsMap); } // 获取key对应的响应函数集 let deps = depsMap.get(key); if (!deps) { // 建立当前key 和依赖的关系,因为一个key 会有多个依赖 // 为了防止重复依赖,使用set deps = new Set(); depsMap.set(key, deps); } // 存入当前依赖 if (!deps.has(effect)) { deps.add(effect); } } } // 用于触发更新 function trigger(target, key) { // 获取所有依赖内容 const depsMap = targetMap.get(target); // 如果有依赖的话全部拉出来执行 if (depsMap) { // 获取响应函数集合 const deps = depsMap.get(key); if (deps) { // 执行所有响应函数 const run = (effect) => { // 源码中有异步调度任务,我们在这里省略 effect(); }; deps.forEach(run); } } } effect(() => { console.log(11111); // 在自己实现的effect中,由于为了演示原理,没有做兼容,不能来触发set,否则会死循环 // vue源码中触发对effect中的做了兼容处理只会执行一次 newRef.value; }); newRef.value++;


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3