Vue3源码分析之 Ref 与 ReactiveEffect
Vue3中的响应式实现原理
完整 js版本简易源码 在最底部
ref 与 reactive 是Vue3中的两个定义响应式对象的API,其中reactive是通过 Proxy 来实现的,它返回对象的响应式副本,而Ref则是返回一个可变的ref对象,只有一个 .value属性指向他内部的值,本文则重点来分析一下 Ref 的实现原理
ref:接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个
.value
property,指向该内部值。
Ref依赖收集
首先我们需要了解 ReactiveEffect 类,创建这个类需要传入一个 副作用函数 和 scheduler,在这个类中有 active、deps 两个重要的属性:
active:是否为激活状态,默认为:true
deps:所有依赖这个 effect 的响应式对象
ReactiveEffect 简易 js 版本源代码:
// 记录当前活跃的对象
let activeEffect
// 标记是否追踪
let shouldTrack = false class ReactiveEffect{
active = true // 是否为激活状态
deps = [] // 所有依赖这个 effect 的响应式对象
onStop = null // function
constructor(fn, scheduler) {
this.fn = fn // 回调函数,如: computed(/* fn */() => { return testRef.value ++ })
// function类型,不为空那么在 TriggerRefValue 函数中会执行 effect.scheduler,否则会执行 effect.run
this.scheduler = scheduler
} run() {
// 如果这个 effect 不需要被响应式对象收集
if(!this.active) {
return this.fn()
} // 源码这里用了两个工具函数:pauseTracking 和 enableTracking 来改变 shouldTrack的状态
shouldTrack = true
activeEffect = this // 在设置完 activeEffect 后执行,在方法中能够对当前活跃的 activeEffect 进行依赖收集
const result = this.fn() shouldTrack = false
// 执行完副作用函数后要清空当前活跃的 effect
activeEffect = undefined return result
} // 暂停追踪
stop() {
if (this.active) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
cleanupEffect(this)
// 执行onStop回调函数
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
了解了 ReactiveEffect 类,再来分析 Ref
ref 简易JS版本源代码
class RefImpl{
// Set格式,存储 effect
dep
// 存储原始值
_rawValue
// 标记这是一个 Ref 对象,isRef API就可以直接通过判断这个内部属性即可
__v_isRef = true // _shallow:这个值是为了实现 shallowRef API 而存在
// 目前这里没有使用到
constructor(value, _shallow) {
this._value = value
// 存储原始值,用来与新值做对比
this._rawValue = _shallow ? value : toRaw(value)
// _value 内部值
// toReactive => isObject(value) ? reactive(value) : value
// 对 value 进行包装
this._value = _shallow ? value : toReactive(value)
} get value() {
// 在获取这个 Ref 内部值时 进行依赖收集
// trackRefValue() 依赖收集
trackRefValue(this)
return this._value
}
}
trackRefValue方法实现
/**
* activeEffect 当前活跃的 effect 对象
* shouldTrack 是否允许追踪
* const isTracking = () => activeEffect && shouldTrack
*/ function trackRefValue(ref) {
// 若没有活跃的 effect 对象或 不需要进行追踪
if(!isTracking()) {
return
}
// 如果这个 Ref 对象还没有对 dep 进行初始化(Ref 中 dep 属性默认为 undefined)
if(!ref.dep) {
ref.dep = new Set()
} trackEffects(ref.dep)
} function trackEffects(dep) {
// 若改 effect 对象已经收集,跳过
if(!dep.has(activeEffect)) {
// 将 effect 对象添加到 Ref 对象的 dep 中
dep.add(activeEffect)
// 将这个Ref的dep存放到这个 effect 的 deps 中
// 目的是为了在停止追踪时,从 响应式对象将 effect 移除掉
activeEffect.deps.push(dep)
}
}
写个 testComputed 函数来回顾 和 测试一下上面的流程
const testRef = new RefImpl(1) const testComputed = (fn) => {
// 创建一个 effect 对象
const testEffect = new ReactiveEffect(fn)
// 默认执行 effect.run() 函数(设置 activeEffect=this -> 执行 fn(依赖收集) -> 清空 activeEffect=undefined)
return testEffect.run()
// 此时,依赖情况:
// testRef.dep = [ testEffect:effect ]
// testEffect.deps = [ testRef.dep ]
} const fn = () => { console.log('textComputed', testRef.value) }
testComputed(fn)
Ref 触发更新
在 RefImpl 类中增加 set value 方法
class RefImpl{
// ......
// 增加 set value () 方法
set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
// 对比 新老值 是否相等,这里不能简单的使用 == 或 ===
// Object.is() 方法判断两个值是否为同一个值。
if (!Object.is(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
// 在内部值被改变的时候会触发依赖更新
triggerRefValue(this) // ,newValue) 在dev环境下使用了 newValue,这里忽略
}
}
// ......
}
triggerRefValue 方法实现
function triggerRefValue(ref) {
triggerEffects(ref.dep);
} function triggerEffects(dep) {
// 执行 Ref.dep 中收集的所有 effect
for (const effect of dep) {
// 这里做个判断,执行的 effect 不是 当前活跃的 effect
if(effect !== activeEffect) {
if(effect.scheduler) {
// effect.scheduler 文章前面有讲过
/** Vue3中模板更新,就是通过创建了一个 scheduler,然后推入 微任务队列 中去执行的
const effect = new ReactiveEffect(componentUpdateFn,() => queueJob(instance.update))
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
*/
effect.scheduler()
} else {
effect.run()
}
}
}
}
修改 testComputed 函数进行测试
const testRef = new RefImpl(1) const testComputedWatch = (fn) => {
// 创建一个 effect 对象
const testEffect = new ReactiveEffect(fn)
// 默认执行 effect.run() 函数(设置 activeEffect=this -> 执行 fn(依赖收集) -> 清空 activeEffect=undefined)
return testEffect.run()
// 此时,依赖情况:
// testRef.dep = [ testEffect:effect ]
// testEffect.deps = [ testRef.dep ]
} const fn = () => { console.log('textComputed', testRef.value) }
testComputed(fn) testRef.value ++ // -> 'textComputed', 2
testRef.value ++ // -> 'textComputed', 3源代码:
effect.js
// 记录当前活跃的对象
let activeEffect
// 标记是否追踪
let shouldTrack = false
// 存储已经收集的依赖
// const targetMap = new WeakMap() const isTracking = () => activeEffect && shouldTrack function trackEffects(dep) {
if(!dep.has(activeEffect)) {
dep.add(activeEffect)
activeEffect.deps.push(dep)
}
} function triggerEffects(dep) {
for (const effect of dep) {
// 这里做个判断,执行的 effect 不是 当前活跃的 effect
if(effect !== activeEffect) {
if(effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
} class ReactiveEffect{
active = true
deps = []
onStop = null constructor(fn, scheduler) {
this.fn = fn
this.scheduler = scheduler
} run() {
if(!this.active) {
return this.fn()
} // 源码这里用了两个工具函数:pauseTracking 和 enableTracking 来改变 shouldTrack的状态
shouldTrack = true
activeEffect = this // 在设置完 activeEffect 后执行,在方法中能够对当前活跃的 activeEffect 进行依赖收集
const result = this.fn() shouldTrack = false
// 执行完副作用函数后要清空当前活跃的 effect
activeEffect = undefined return result
} // 暂停追踪
stop() {
if (this.active) {
// 找到所有依赖这个 effect 的响应式对象
// 从这些响应式对象里面把 effect 给删除掉
cleanupEffect(this)
// 执行onStop回调函数
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
module.exports = {
ReactiveEffect,
isTracking,
trackEffects,
triggerEffects
}
ref.js
// const { toReactive, toRaw } = require('./reactive.js')
// start reactive.js 模块
const isObject = (val) => val !== null && typeof val === 'object' const toReactive = val =>
isObject(val) ? reactive(val) : val function toRaw(observed) {
// __v_raw 是一个标志位,表示这个是一个 reactive
const raw = observed && observed['__v_raw']
return raw ? toRaw(raw) : observed
}
// end reactive.js const { ReactiveEffect, isTracking, trackEffects, triggerEffects } = require('./effect.js') function trackRefValue(ref) {
if(!isTracking()) {
return
} if(!ref.dep) {
ref.dep = new Set()
} trackEffects(ref.dep)
} function triggerRefValue(ref) {
triggerEffects(ref.dep);
} class RefImpl{
dep
_rawValue
__v_isRef = true
constructor(value, _shallow) {
this._value = value
// 存储原始值,用来与新值做对比
this._rawValue = _shallow ? value : toRaw(value)
this._value = _shallow ? value : toReactive(value)
} get value() {
// 收集依赖
trackRefValue(this)
return this._value
} set value(newVal) {
newVal = this._shallow ? newVal : toRaw(newVal)
// Object.is() 方法判断两个值是否为同一个值。
if (!Object.is(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this._shallow ? newVal : toReactive(newVal)
triggerRefValue(this) // ,newValue) 在dev环境下使用了 newValue,这里忽略
}
}
} function isRef(r) {
return Boolean(r && r.__v_isRef === true)
}
function createRef(rawValue, _shallow) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, _shallow)
} function ref(val) {
return createRef(val, false)
} module.exports = {
isRef,
ref
}
测试代码
/** ** 测试代码 ***/
const testRef = ref(1) const textComputed = () => {
const testEffect = new ReactiveEffect(() => {
console.log('textComputed', testRef.value)
}) testEffect.run() console.log('testEffect.deps', testEffect.deps)
}
const textComputed1 = () => {
const fn = () => { console.log('textComputed', testRef.value) }
const testEffect = new ReactiveEffect(fn) testEffect.run()
} textComputed1()
textComputed()
console.log('testRef', testRef)
// testRef.value ++
Vue3源码分析之 Ref 与 ReactiveEffect的更多相关文章
- Vue3源码分析之Diff算法
Diff 算法源码(结合源码写的简易版本) 备注:文章后面有详细解析,先简单浏览一遍整体代码,更容易阅读 // Vue3 中的 diff 算法 // 模拟节点 const { oldVirtualDo ...
- Vue3源码分析之微任务队列
参考资料:https://zh.javascript.info/microtask-queue#wei-ren-wu-dui-lie-microtaskqueue 简化版 Vue3 中的 微任务队列实 ...
- Vue3中的响应式对象Reactive源码分析
Vue3中的响应式对象Reactive源码分析 ReactiveEffect.js 中的 trackEffects函数 及 ReactiveEffect类 在Ref随笔中已经介绍,在本文中不做赘述 本 ...
- Vue.js 源码分析(十) 基础篇 ref属性详解
ref 被用来给元素或子组件注册引用信息.引用信息将会注册在父组件的 $refs 对象上.如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素:如果用在子组件上,引用就指向组件实例,例如: ...
- 【JUC】JDK1.8源码分析之SynchronousQueue(九)
一.前言 本篇是在分析Executors源码时,发现JUC集合框架中的一个重要类没有分析,SynchronousQueue,该类在线程池中的作用是非常明显的,所以很有必要单独拿出来分析一番,这对于之后 ...
- .net源码分析 - ConcurrentDictionary<TKey, TValue>
List源码分析 Dictionary源码分析 ConcurrentDictionary源码分析 继上篇Dictionary源码分析,上篇讲过的在这里不会再重复 ConcurrentDictionar ...
- docker 源码分析 四(基于1.8.2版本),Docker镜像的获取和存储
前段时间一直忙些其他事情,docker源码分析的事情耽搁了,今天接着写,上一章了解了docker client 和 docker daemon(会启动一个http server)是C/S的结构,cli ...
- 【Cocos2d-x 3.x】内存管理机制与源码分析
侯捷先生说过这么一句话 : 源码之前,了无秘密. 要了解Cocos2d-x的内存管理机制,就得阅读源码. 接触Cocos2d-x时, Cocos2d-x的最新版本已经到了3.2的时代,在学习Coco ...
- spring源码分析(二)Aop
创建日期:2016.08.19 修改日期:2016.08.20-2016.08.21 交流QQ:992591601 参考资料:<spring源码深度解析>.<spring技术内幕&g ...
随机推荐
- 【LeetCode】763. Partition Labels 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 解题方法 日期 题目地址:https://leetcode.com/pr ...
- 【LeetCode】756. Pyramid Transition Matrix 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 回溯法 日期 题目地址:https://leetco ...
- 【LeetCode】712. Minimum ASCII Delete Sum for Two Strings 解题报告(Python & C++)
作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...
- hdu-1593 find a way to escape(贪心,数学)
思路:两个人都要选取最优的策略. 先求外层那个人的角速度,因为他的角速度是确定的,再求内层人的当角速度和外层人一样时的对应的圆的半径r1.外层圆的半径为d; 那么如果r1>=外围圆的半径,那么肯 ...
- Boost的反射库PFR
目录 目录 简介 使用方法 限制 总结 简介 Boost.PFR是一个Boost 1.75版本出的C++14的基础反射库,其使用非常简单,非常便捷,但是适用性也比较差,有很多的地方无法使用,适合比较简 ...
- CS5216 设计于DP转HDMI转换器|DP转HDMI 1080P中继器 电平转化器开关设计方案与线路图
CS5216是一款Displayport to hdmi 1080p音视频信号转换芯片,主要用于设计与开发DP转HDMI 转换器.中继器.电平转换器等产品当中.它支持交流和直流耦合TMDS信号高达1. ...
- 【算法】01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈)
[算法]01-数据结构概述(注意区分jvm堆与堆/jvm栈与栈) 欢迎关注b站账号/公众号[六边形战士夏宁],一个要把各项指标拉满的男人.该文章已在github目录收录. 屏幕前的大帅比和大漂亮如果有 ...
- Java中关于super关键字的作用
在子类继承父类中,一般有一行super的语句,需要记住三种使用方法: 1.super.变量/对象名: 使用这种方法可以直接访问父类中的变量或对象,进行修改赋值等操作 2.super.方法名(): 直接 ...
- springboot的build.gradle增加阿里仓库地址以及eclipse增加lombok
该随笔仅限自己记录,请谨慎参考!! 为什么把这2块内容放一个标题里? 发现lombok和eclipse结合的一些问题 关于lombok如何与eclipse结合,网上应该有很多教程,我这块已经做过了,但 ...
- MongoDB 变更流(Change Stream)介绍
1. 什么是Change Stream Change Stream 是MongoDB用于实现变更追踪的解决方案,类似于关系数据库的触发器,但原理不完全相同: | | Change Stream | 触 ...