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

本文主要是介绍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();
};

这篇关于Vue3源码【二】—— watch侦听computed计算属性原理及简单实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1071145

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

这15个Vue指令,让你的项目开发爽到爆

1. V-Hotkey 仓库地址: github.com/Dafrok/v-ho… Demo: 戳这里 https://dafrok.github.io/v-hotkey 安装: npm install --save v-hotkey 这个指令可以给组件绑定一个或多个快捷键。你想要通过按下 Escape 键后隐藏某个组件,按住 Control 和回车键再显示它吗?小菜一碟: <template

【 html+css 绚丽Loading 】000046 三才归元阵

前言:哈喽,大家好,今天给大家分享html+css 绚丽Loading!并提供具体代码帮助大家深入理解,彻底掌握!创作不易,如果能帮助到大家或者给大家一些灵感和启发,欢迎收藏+关注哦 💕 目录 📚一、效果📚二、信息💡1.简介:💡2.外观描述:💡3.使用方式:💡4.战斗方式:💡5.提升:💡6.传说: 📚三、源代码,上代码,可以直接复制使用🎥效果🗂️目录✍️

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

csu 1446 Problem J Modified LCS (扩展欧几里得算法的简单应用)

这是一道扩展欧几里得算法的简单应用题,这题是在湖南多校训练赛中队友ac的一道题,在比赛之后请教了队友,然后自己把它a掉 这也是自己独自做扩展欧几里得算法的题目 题意:把题意转变下就变成了:求d1*x - d2*y = f2 - f1的解,很明显用exgcd来解 下面介绍一下exgcd的一些知识点:求ax + by = c的解 一、首先求ax + by = gcd(a,b)的解 这个

hdu2289(简单二分)

虽说是简单二分,但是我还是wa死了  题意:已知圆台的体积,求高度 首先要知道圆台体积怎么求:设上下底的半径分别为r1,r2,高为h,V = PI*(r1*r1+r1*r2+r2*r2)*h/3 然后以h进行二分 代码如下: #include<iostream>#include<algorithm>#include<cstring>#include<stack>#includ

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象