Vue3源码【二】—— watch侦听computed计算属性原理及简单实现

1、watch监听器

1.1、使用watch

watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用,然后下面就是使用watch的一个说明。 小声逼逼:我还是习惯用监听,后面的监听也就是侦听。

/*** @param source 监听对象*                监听单个            a*                监听多个            [a,b]*                监听reactive单个值   ()=>{}* @param cb 回调函数 (newVal, oldVal)得到变换前后的值* @param options 配置项*                immediate 是否立即执行*                deep 是否深度监听*                once 是否只执行一次*				  flush 回调执行时机*/
watch(source,(newVal, oldVal)=>{},{})

1.2、创建watch

直接在源码当中找到watch,路径是packages/runtime-core/src/apiWatch.ts,他本身就是一个函数,之后回去执行doWatch,同时我们可以看一下options配置的类型WatchOptions,这个我们先放在着,等下看一下配置项是如何生效的。

export function watch<T = any, Immediate extends Readonly<boolean> = false>(source: T | WatchSource<T>,cb: any,options?: WatchOptions<Immediate>,
): WatchStopHandle {return doWatch(source as any, cb, options)
}//
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {immediate?: Immediatedeep?: booleanonce?: boolean
}

1.3、执行监听

  • 首先对once配置项是否只执行一次进行判断,然后提前先对deep深层监听判断
  • 监听源判断,单值监听、多值监听、回调监听。同时需要去判断ref和reactive。
  • 根据前两步去判断是单层还是深层监听,执行traverse
  • 创建一个job,在job当中去更新新旧值
  • 判断flush,用来确定值变换与dom更新的时机(先后顺序)
  • 判断immediate,是否立即执行一次(执行一次job)
  • 最后通过effect.run()开始依赖收集整体调度
  • doWatch返回了unwatch,这也就是const A =watch(a,()=>{}); A();再调用一个A就可以去除监听的原因
function doWatch(source: WatchSource | WatchSource[] | WatchEffect | object,cb: WatchCallback | null,{immediate,deep,flush,once,onTrack,onTrigger,}: WatchOptions = EMPTY_OBJ,// EMPTY_OBJ是一个Object.freeze({})冻结的空对象,也就是说当没有传options过来时,这个配置都会从这个{}解构得到
): WatchStopHandle {// 当指定了once只执行一次,会执行一次cb(callback)然后unwatch结束监听if (cb && once) {const _cb = cbcb = (...args) => {_cb(...args)unwatch()}}const instance = currentInstanceconst reactiveGetter = (source: object) =>deep === true? source // 遍历将发生在下面的包装getter中: // 对于deep:false,仅遍历根级属性traverse(source, deep === false ? 1 : undefined)let getter: () => anylet forceTrigger = falselet isMultiSource = falseif (isRef(source)) {// ref对象的get直接访问value属性getter = () => source.value// 判断是否是浅层refforceTrigger = isShallow(source)} else if (isReactive(source)) {// 对于reactive对象来说,是通过deep去控制是否需要深层监听的getter = () => reactiveGetter(source)forceTrigger = true} else if (isArray(source)) {// 数组isMultiSource = true// 看是否有深层监听forceTrigger = source.some(s => isReactive(s) || isShallow(s))getter = () =>source.map(s => {if (isRef(s)) {return s.value} else if (isReactive(s)) {return reactiveGetter(s)} else if (isFunction(s)) {return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)} else {// 不能监听}})} else if (isFunction(source)) {if (cb) {// getter with cbgetter = () =>callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)} else {// no cb -> simple effectgetter = () => {if (cleanup) {cleanup()}return callWithAsyncErrorHandling(source,instance,ErrorCodes.WATCH_CALLBACK,[onCleanup],)}}} else {getter = NOOP}// 有回调并且是单层监听if (__COMPAT__ && cb && !deep) {const baseGetter = gettergetter = () => {const val = baseGetter()if (isArray(val) &&checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) {traverse(val)}return val}}// 深层监听if (cb && deep) {const baseGetter = gettergetter = () => traverse(baseGetter())}let cleanup: (() => void) | undefinedlet onCleanup: OnCleanup = (fn: () => void) => {cleanup = effect.onStop = () => {callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)cleanup = effect.onStop = undefined}}// 在SSR中,不需要设置实际效果,它应该是noop// 除非它很急切或同步刷新let ssrCleanup: (() => void)[] | undefinedif (__SSR__ && isInSSRComponentSetup) {// 我们也不会调用 invalide 回调(没有设置+runner)onCleanup = NOOPif (!cb) {getter()} else if (immediate) {callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [getter(),isMultiSource ? [] : undefined,onCleanup,])}if (flush === 'sync') {const ctx = useSSRContext()!ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = [])} else {return NOOP}}// isMultiSource 用来标记是否是多数据监听let oldValue: any = isMultiSource? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE): INITIAL_WATCHER_VALUE// 开始调度const job: SchedulerJob = () => {// 需要保证依赖收集是开启的if (!effect.active || !effect.dirty) {return}if (cb) {const newValue = effect.run()// 有真则真// 深层监听 || 对象监听 || (多数据监听 遍历通过Object.is(value, oldValue)去比较值是否改变)|| (是数组类型 && ?)if (deep ||forceTrigger ||(isMultiSource? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])): hasChanged(newValue, oldValue)) ||(__COMPAT__ &&isArray(newValue) &&isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))) {// 再次运行cb之前的清理if (cleanup) {cleanup()}callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [newValue,// 第一次更改时将undefined作为旧值传递,到这里oldVal才会有值oldValue === INITIAL_WATCHER_VALUE? undefined: isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE? []: oldValue,onCleanup,])oldValue = newValue}} else {// watchEffecteffect.run()}}// 将job标记为观察程序回调,以便调度程序知道,它被允许自触发 先设置为falsejob.allowRecurse = !!cb // !!undefined falselet scheduler: EffectScheduler/*** flush: 'pre' | 'post' | 'sync'* pre  在侦听器的回调函数运行之前立即运行更新函数* post ------------------之后-------------* sync 同步* */if (flush === 'sync') {scheduler = job as any // the scheduler function gets called directly} else if (flush === 'post') {scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)} else {// default: 'pre'job.pre = true// 把当前示例的id作为job任务idif (instance) job.id = instance.uid// 开始调度scheduler = () => queueJob(job)}// 和响应式那块是一样的,收集依赖const effect = new ReactiveEffect(getter, NOOP, scheduler)const scope = getCurrentScope()const unwatch = () => {// 停止依赖收集,并且把这个effect剔除出去effect.stop()if (scope) {remove(scope.effects, effect)}}// initial runif (cb) {// immediate 是否先调度一次if (immediate) {job()} else {oldValue = effect.run()}} else if (flush === 'post') {queuePostRenderEffect(effect.run.bind(effect),instance && instance.suspense,)} else {effect.run()}if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch)return unwatch
}

1.4、traverse对象值收集

在上面有调用traverse(baseGetter()),把getter传给了这个函数,简单看一下这个函数,其实就是把所有getter能拿到的值全部给加到seen(set集合当中)之后要是有watch的变更也直接从这里面去掉即可

export function traverse(value: unknown,depth = Infinity,seen?: Set<unknown>,
) {if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) {return value}seen = seen || new Set()if (seen.has(value)) {return value}seen.add(value)depth--// 看是不是ref还包了ref// 后面判断递归都是同理,把所有值都给加到seen当中if (isRef(value)) {traverse(value.value, depth, seen)} else if (isArray(value)) {for (let i = 0; i < value.length; i++) {traverse(value[i], depth, seen)}} else if (isSet(value) || isMap(value)) {value.forEach((v: any) => {traverse(v, depth, seen)})} else if (isPlainObject(value)) {for (const key in value) {traverse(value[key], depth, seen)}for (const key of Object.getOwnPropertySymbols(value)) {if (Object.prototype.propertyIsEnumerable.call(value, key)) {traverse(value[key as any], depth, seen)}}}return value
}

1.5、watchEffect

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。官网说明:前往 https://cn.vuejs.org/api/reactivity-core.html#watcheffect

// use,当在watchEffect当中使用了的变量,就会自动追踪哪个属性,当使用了objEffect对象,他里面所有的属性都会被监听到
const objEffect = reactive({a: 1, b: {c: 1, d: {e: 2}}});
watchEffect(() => {console.log(' =====', objEffect.a);
});// 源码
export function watchPostEffect(effect: WatchEffect,options?: DebuggerOptions,
) {return doWatch(effect,null,__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },)
}// 这个去执行doWatch时,也就是数据源是一个函数,它执行的逻辑就是这一块
getter = () => {if (cleanup) {// 通过这个将依赖收集起来cleanup()}return callWithAsyncErrorHandling(source,instance,ErrorCodes.WATCH_CALLBACK,[onCleanup],)
}

1.6、扩展:watchPostEffect & watchSyncEffect

本质上就是指定了flush,flush默认值是pre,在侦听器的回调函数运行之前立即运行更新函数,也就是watchEffect,而这两个的意义还是用来在语义上对前后、同步调用的一个区分

  • watchPostEffect : 把flush指定为post,也就是回调函数运行之后运行更新函数

  • watchSyncEffect : 同步执行

export function watchPostEffect(effect: WatchEffect,options?: DebuggerOptions,
) {return doWatch(effect,null,__DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' },)
}export function watchSyncEffect(effect: WatchEffect,options?: DebuggerOptions,
) {return doWatch(effect,null,__DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' },)
}

2、computed 计算属性

2.1、使用computed

计算属性是当依赖的属性的值发生变化的时候,才会触发他的更改,如果依赖的值,不发生变化的时候,使用的是缓存中的属性值。

使用computed有以下两种方式,一个是传函数直接返回,一个可以通过传递对象给定get/set函数进去控制。

const A = computed(() => {return `A:${a.age}`;
});const B = computed({get: () => {return a.age;},set: (value) => {a.age = value;}
});

2.2、创建computed

先看一下computed是怎么创建的,源码位置:packages/reactivity/src/computed.ts。在这里通过getterOrOptions接收computed传递的参数,也就是可以拿到上面函数和对象两种方式传递的值,直接分别去取对应的get、set方法,通过ComputedRefImpl创建一个实现实例。

export function computed<T>(// getterOrOptions传递值  ComputedGetter就是一个回调函数,WritableComputedOptions是一个对象包了get/set方法getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,debugOptions?: DebuggerOptions,isSSR = false,
) {let getter: ComputedGetter<T>let setter: ComputedSetter<T>// 判断传递过来是那种方式const onlyGetter = isFunction(getterOrOptions)if (onlyGetter) {// 将回调给到getter,并且不设置settergetter = getterOrOptionssetter = __DEV__? () => {warn('Write operation failed: computed value is readonly')}: NOOP} else {// 传递的是对象形式,直接去对象里面拿get/set方法getter = getterOrOptions.getsetter = getterOrOptions.set}const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)if (__DEV__ && debugOptions && !isSSR) {cRef.effect.onTrack = debugOptions.onTrackcRef.effect.onTrigger = debugOptions.onTrigger}return cRef as any
}

2.2、ComputedRefImpl实例

  • 直接从构造开始看起,主要关注get/set两个方法。在构造创建了一个ReactiveEffect,也就是响应式的实现方式。并且指定了依赖触发
  • set值在计算属性当中不关系,主要是使用别的值改变后,怎么获取计算之后的值,也就是这的get
  • 在get当中通过了_cacheable是否缓存先执行一遍依赖触发和依赖收集的过程,而后则通过脏值判断是否需要使用缓存当中的值还是用新值
export class ComputedRefImpl<T> {public dep?: Dep = undefinedprivate _value!: Tpublic readonly effect: ReactiveEffect<T>public readonly __v_isRef = truepublic readonly [ReactiveFlags.IS_READONLY]: boolean = falsepublic _cacheable: boolean_warnRecursive?: boolean// 从构造开始,先不关心isReadonly和isSSR,就看get/setconstructor(private getter: ComputedGetter<T>,private readonly _setter: ComputedSetter<T>,isReadonly: boolean,isSSR: boolean,) {// 创建了ReactiveEffect,也就是前面说到的reactive响应式,并且指定了triggerRefValue(依赖触发)this.effect = new ReactiveEffect(() => getter(this._value),() =>triggerRefValue(this,this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect // 4 === 2? DirtyLevels.MaybeDirty_ComputedSideEffect // 2: DirtyLevels.MaybeDirty, // 3),)this.effect.computed = thisthis.effect.active = this._cacheable = !isSSR // isSSR === false 添加缓存this[ReactiveFlags.IS_READONLY] = isReadonly // isReadonly === false}get value() {// 计算出的ref可能会被其他代理封装,例如readonly() toRaw 转换成原始对象const self = toRaw(this)/*** self._cacheable 变量是否可缓存* self.effect.dirty 表示该变量是否被修改过* */if ((!self._cacheable || self.effect.dirty) &&hasChanged(self._value, (self._value = self.effect.run()!))) {// 值变换之后会触发依赖更新DOMtriggerRefValue(self, DirtyLevels.Dirty)}// 触发之后重新收集trackRefValue(self)// 这里通过 DirtyLevels.MaybeDirty_ComputedSideEffect 脏标记级别用来控制是否需要重新执行依赖触发去更新DOM等操作if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)}return self._value}set value(newValue: T) {this._setter(newValue)}get _dirty() {return this.effect.dirty}set _dirty(v) {this.effect.dirty = v}
}

2.3、简单实现computed计算属性

  • 首先还是要前面实现的MyReactive响应式的依赖收集依赖触发等方法(
    MyReactive响应式简单示例案例,单击前往),这里就不往下面贴了,但是对应依赖收集、触发(effect、trigger)需要进行细微调整,
    • 依赖收集就是在收集的时候添加了一个options,挂在effect上
    • 依赖触发本质上就是在遍历deps的时候去看一下那些值有scheduler调度。这个调度是我们创建computed去给他添加的。
  • 创建computed,这里简单实现就只接收一个函数入参,之后进行依赖收集,在这里就将scheduler调度给挂载到effect副作用函数上。
  • 当trigger响应式触发之后,会去尝试执行一个scheduler调度。执行了调度之后再去更新DOM时会触发effect,在computed当中get值,在这里判断了dirty,也就是新旧值是否相等(是否去更新cacheValue缓存值)。
const effect = (fn: Function, options: Options) => {const _effect = () => {activeEffect = _effect;return fn();};_effect.options = options;_effect();return _effect;
};const trigger = (target: object, key: any) => {const depsMap = targetMap.get(target);if (!depsMap) return;const deps = depsMap.get(key);if (!deps) return;deps.forEach((effect: { (): void; (): void; options: any; }) => {if (effect?.options?.scheduler) {effect?.options.scheduler?.();} else {effect();}});
};const myComputed = (getter: Function) => {let _value = effect(getter, {scheduler: () => {_dirty = true;}});let _dirty = true;let catchValue: any;class MyComputerRefImpl {get value() {if (_dirty) {catchValue = _value();_dirty = false;}return catchValue;}}return new MyComputerRefImpl();
};

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

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

相关文章

KVB:怎么样选择最优交易周期?

摘要 在金融交易中&#xff0c;周期的选择是影响交易成败的重要因素之一。不同的交易周期对应不同的市场环境和交易策略&#xff0c;选择合适的周期可以提高交易的成功率。本文将详细探讨交易中如何选择最优周期&#xff0c;包括短周期、中周期和长周期的特点及适用情况&#…

软考系统规划与管理师伴读脑图第9章

周末发系统规划与管理师的试听视频&#xff0c;占用了发送次数&#xff0c;所以上周的脑图推迟了今天发出。 不知不觉已经发到了第9章&#xff0c;感叹这就是坚持积累下来的力量&#xff0c;其实考试也是一样的道理。

c++深拷贝、浅拷贝

在 C 中&#xff0c;深拷贝和浅拷贝是两个重要的概念&#xff0c;尤其在涉及动态内存分配和指针成员时。这两个概念描述了对象复制时的行为。 浅拷贝 浅拷贝是指复制对象时&#xff0c;仅复制对象的基本数据成员&#xff0c;对于指针成员&#xff0c;只复制指针地址&#xff…

Python面试题-1

1. 什么是Python&#xff1f; Python 是一种高级编程语言&#xff0c;由 Guido van Rossum 在 1989 年创立&#xff0c;旨在强调代码的可读性和简洁性。Python 是一种解释型语言&#xff0c;这意味着开发过程中没有必要先将程序编译成机器语言&#xff0c;而是直接运行源代码。…

12k star 项目 cmake-examples 阅读和点评

12k star 项目 cmake-examples 阅读和点评 Author: ChrisZZ Time: 2024.06.17 文章目录 12k star 项目 cmake-examples 阅读和点评项目概要01-basicA-hello-cmakeB-hello-headersC-static-libraryD-shared-libraryE-installingF-build-typeG-compile-flagsH-third-party-libra…

TensorFlow音频分类修复

原先传wav格式,后来发现前端生成的wav格式不完整 后端改mp3 其实是mp3和wav都可以接收 但问题是wav文件格式错误的话无法获取时长,这样也就是说只能传mp3,除非你对时长无所谓 修复TensorFlow放到生产后报错问题-CSDN博客 依赖 <dependency><groupId>org.ten…

Git--Part3--远程操作 配置 标签管理

theme: nico 远程仓库 Git 是分布式版本控制系统&#xff0c;同⼀个 Git 仓库&#xff0c;可以分布到不同的机器上。怎么分布呢&#xff1f; 最早&#xff0c;肯定只有⼀台机器有⼀个原始版本库&#xff0c;此后&#xff0c;别的机器可以 “克隆” 这个原始版本库&#xff0…

AI导航网

文章目录 1、[AI导航网](https://www.ainav.cn/) 1、AI导航网 https://www.ainav.cn/

苍穹外卖笔记-18-修改密码、bug记录

文章目录 1 修改密码1.1 需求分析和设计1.2 代码实现1.2.1 admin/EmployeeController1.2.2 EmployeeService1.2.3 EmployeeServiceImpl 1.3 功能测试 2 bug记录 1 修改密码 完结的时候发现还有一个接口未实现。这里补充 1.1 需求分析和设计 产品原型&#xff1a; 业务规则&am…

HDFS小文件过多的危害和解决方案

HDFS&#xff08;Hadoop分布式文件系统&#xff09;中小文件过多危害 1.存储空间浪费&#xff1a;HDFS是为大容量文件设计的&#xff0c;存储大量小文件会浪费存储空间&#xff0c;因为每个文件都会占用一定的存储块。 2.元数据压力&#xff1a;HDFS中的NameNode负责管理文件…

CMake从安装到精通

目录 引言 1. CMake的安装 2. CMake的原理 3. CMake入门 3.1 CMakeLists.txt与注释 3.2 版本指定与工程描述 3.3 生成可执行程序 3.4 定义变量与指定输出路径 3.5 指定C标准 3.6 搜索文件 3.7 包含头文件 4. CMake进阶 4.1 生成动静态库 4.2 链接动静态库 4.…

CTFshow之RCE代码命令远程执行第49关详细讲解。可私信!

棺材里伸手&#xff0c;死要钱&#xff01; --古吉拉特邦 莫迪大仙 引言&#xff1a;由于有些题目实在是让人抓挠&#xff0c;我看完题解后难以接受知识机械的执行获取flag&#xff0c;所以我想着尽可能用我的语言去进行解释&#xff01; 由于是验证猜想实验&#xff0c;所以…

还原其他人的软件

在开发商用软件时&#xff0c;软件的源代码是非常重要的资产。如果源代码被其他公司窃取&#xff0c;他们就能轻松地开发出类似的软件。因此&#xff0c;一般只会发布经过编译的机器语言的可执行文件&#xff0c; 对于竞争对手而言&#xff0c;软件的实现方法是无论如何都想得…

代发考生战报:HCIP H12-725安全变题了

代发考生战报&#xff1a;HCIP H12-725安全变题了&#xff0c;幸好当天找客服办理的包过服务&#xff0c;听同考场的考生说&#xff0c;考试全是新题&#xff0c;只有1-2个是题库上的题&#xff0c;自己考的都考挂了&#xff0c;帮我答题的老师很厉害&#xff0c;很赞&#xff…

GenICam标准(二)

系列文章目录 GenICam标准&#xff08;一&#xff09; GenICam标准&#xff08;二&#xff09; GenICam标准&#xff08;三&#xff09; GenICam标准&#xff08;四&#xff09; GenICam标准&#xff08;五&#xff09; GenICam标准&#xff08;六&#xff09; 文章目录 系列文…

RAG与Langchain简介

RAG与Langchain简介 什么是RAGRAG解决的问题RAG工作流程RAG调优策略LangChain简介 什么是RAG 检索增强生成&#xff08;Retrieval-Augmented Generation&#xff09;&#xff0c;主要是通过从外部给大模型补充一些知识&#xff0c;相当于给模型外挂了一个知识库&#xff0c;让…

单片机(STM32)与上位机传输浮点数

目录 单片机(STM32)与上位机传输数据的方法1. 传输整形数据2. 传输浮点数据3. 如何打包与解包 单片机(STM32)与上位机传输数据的方法 在进行单片机程序的开发时&#xff0c;常常需要与其他设备进行通信。一种情况是与其他电路板通信&#xff0c;比如STM32主机与STM32从机通信&…

胡说八道(24.6.9)——离散时间系统及simulink仿真

上回说道拉普拉斯变换的定义、性质以及在电路分析中的应用。今天先来谈谈simulink仿真&#xff0c;可为是让我非常的震惊&#xff0c;今天做了三种模型的应用。第一个是simulink中有限状态机的应用&#xff0c;用来解决一些复杂的逻辑问题&#xff0c;实现状态之间的转换。第一…

Pytorch深度解析:Transformer嵌入层源码逐行解读

前言 本部分博客需要先阅读博客&#xff1a; 《Transformer实现以及Pytorch源码解读&#xff08;一&#xff09;-数据输入篇》 作为知识储备。 Embedding使用方式 如下面的代码中所示&#xff0c;embedding一般是先实例化nn.Embedding(vocab_size, embedding_dim)。实例化的…

赶紧收藏!2024 年最常见 20道设计模式面试题(二)

上一篇地址&#xff1a;赶紧收藏&#xff01;2024 年最常见 20道设计模式面试题&#xff08;一&#xff09;-CSDN博客 三、解释抽象工厂模式&#xff0c;并给出一个实际应用的例子。 抽象工厂模式是一种创建型设计模式&#xff0c;用于创建一系列相关或依赖对象的接口&#x…