Skip to content

vue3 源码分析

响应式 ref、reactive

首先还是从最开始学的ref的源码看起,他的路径在packages/reactivity/src/ref.ts,这里看源码分析就直接将源码执行的步骤给他粘贴出来了哈。首先我们看一下ref是怎么创建的

1.1、创建Ref

ts
// 第一步,我们还是直接到ref关键字,可以看到这个,这个就是我们使用的ref()用来创建响应式对象的关键。他会去调用createRef,并且第二个指定了false
export function ref(value?: unknown) {
    return createRef(value, false);
}

// 第二步,顺着上面往下执行,会调用createRef,这个时候我们知道第二个参数false是指什么了,也就是shallow,这个时候可能会想到shallowRef?
// shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。那默认创建的这个ref指定了false,那要是shallowRef调用createRef创建Ref是不是就是指定的true呢?这个我们后面再看
// 在这里先判断isRef,这个很好理解,就是看入参的是不是一个ref了,
function createRef(rawValue: unknown, shallow: boolean) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    return new RefImpl(rawValue, shallow);
}

// 第三步,直接看RefImpl,这个就是将一个变量给包装成Ref(响应式对象)
// 这里看构造函数,先都会判断一下shallow是真还是假,响应式对象入的是false,他数据包装会变成toRaw和toReactive
// 在这里我们知道reactive是用来包对象类型的,这里ref创建本质上也是对调reactive的方法,同时我们也知道了为什么使用ref包的对象要加一个.value取取值赋值
// 看一下shallowRef的构造,果然就是return createRef(value, true),这样也解释了shallowRef为什么处理的是基本数据类型
// 看一下isRef方法,return !!(r && r.__v_isRef === true) r就是RefImpl实例对象,用来判断的也就是这个r.__v_isRef === true 是否为ref
class RefImpl<T> {
    private _value: T;
    private _rawValue: T;

    public dep?: Dep = undefined;
    public readonly;
    __v_isRef = true;

    constructor(
        value: T,
        public readonly __v_isShallow: boolean
    ) {
        this._rawValue = __v_isShallow ? value : toRaw(value);
        this._value = __v_isShallow ? value : toReactive(value);
    }

    get value() {
        trackRefValue(this);
        return this._value;
    }

    set value(newVal) {
        const useDirectValue = this.__v_isShallow || isShallow(newVal) || isReadonly(newVal);
        newVal = useDirectValue ? newVal : toRaw(newVal);
        if (hasChanged(newVal, this._rawValue)) {
            this._rawValue = newVal;
            this._value = useDirectValue ? newVal : toReactive(newVal);
            triggerRefValue(this, DirtyLevels.Dirty, newVal);
        }
    }
}

1.2、依赖收集

我们先看一个他的get value是这么拿到值的,也就是trackRefValue方法。首先他在处理的时候先通过toRaw转成原始对象,从这里往下的源码就做了一些删减,有一些对数据进行异常判断处理的这里就都不展示了,主要看执行逻辑

js
export function trackRefValue(ref: RefBase<any>) {
    // true && undefined
    if (shouldTrack && activeEffect) {
        // 先转成原始对象
        ref = toRaw(ref)
        trackEffect(
            activeEffect,
            // ref.dep 不存在就调用createDep赋值给ref.dep  本质上是一个Map
            (ref.dep ??= createDep(
                () => (ref.dep = undefined),
                ref instanceof ComputedRefImpl ? ref : undefined,
            )),
            void 0,
        )
    }
}

重要的还是这个trackEffect方法。简单来说就是将里面的所有属性都给收集到一个map当中,通过这个map来做统一的依赖控制。后面取值也会从Map当中取值。

ts
export function trackEffect(
    effect: ReactiveEffect,
    dep: Dep,
    debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
    // 默认值 eff._trackId = 0
    if (dep.get(effect) !== effect._trackId) {
        dep.set(effect, effect._trackId);
        // 默认值 effect._depsLength = 0
        const oldDep = effect.deps[effect._depsLength];
        if (oldDep !== dep) {
            if (oldDep) {
                cleanupDepEffect(oldDep, effect);
            }
            // effect.deps 本质上还是一个Map对象,在这里将所有的依赖收集起来
            effect.deps[effect._depsLength++] = dep;
        } else {
            effect._depsLength++;
        }
    }
}

1.3、依赖触发

这里从set重新设置值的时候开始执行,核心方法在triggerEffects。进来线通过遍历所有的依赖,找到我们需要修改的依赖的值,然后重新赋值,执行effect.trigger() ,到这里完成了依赖的触发。同时可以看一下这个effect是个什么,在这里面会接收一个匿名函数fn,并且在这里他是回去走run方法的。他的本质还是ReactiveEffect类的实例,他的run方法就是实例的run。最后回去执行那个匿名函数fn,也就是更改视图的方法 document.querySelector('#xxx').innerHTML = xxx 。到这里就完成了依赖值的更改以及视图的实时响应

ts
export function triggerEffects(
    dep: Dep,
    dirtyLevel: DirtyLevels,
    debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
    // pauseScheduleStack++
    pauseScheduling();
    // 遍历所有收集的依赖
    for (const effect of dep.keys()) {
        // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
        let tracking: boolean | undefined;
        // 依赖的_dirtyLevel < 4 && dep.get(effect) === effect._trackId)
        if (
            effect._dirtyLevel < dirtyLevel &&
            (tracking ??= dep.get(effect) === effect._trackId)
        ) {
            // (effect._dirtyLevel)默认为4 === 0 则执行 effect._shouldSchedule
            effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty;
            effect._dirtyLevel = dirtyLevel; // 重新赋值为4
        }

        effect.trigger();

        effect._shouldSchedule = false;
        if (effect.scheduler) {
            queueEffectSchedulers.push(effect.scheduler);
        }
    }
    resetScheduling();
}

export function effect<T = any>(
    fn: () => T,
    options?: ReactiveEffectOptions
): ReactiveEffectRunner {
    if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
        fn = (fn as ReactiveEffectRunner).effect.fn;
    }

    const _effect = new ReactiveEffect(fn, NOOP, () => {
        if (_effect.dirty) {
            _effect.run();
        }
    });
    if (options) {
        extend(_effect, options);
        if (options.scope) recordEffectScope(_effect, options.scope);
    }
    if (!options || !options.lazy) {
        _effect.run();
    }
    const runner = _effect.run.bind(_effect) as ReactiveEffectRunner;
    runner.effect = _effect;
    return runner;
}

1.4、响应式扩展

1.4.1、isRef、shallowRef、toRaw、markRaw

这个的源码在1.1创建Ref的里面大概说了,就跳过了。补充一下toRaw和markRaw

  • isRef:是否为一个Ref

  • shallowRef:只处理基本数据类型的响应式, 不进行对象的响应式处理。

  • toRaw:将响应式对象转换成原始对象

  • markRaw:标记一个对象,使其永远不会再成为响应式对象

ts
// 通过observed尝试获取其原始对象,如果可以找到再递归对象的键的值再进行转换,把代理对象下的每一个值都转成原始对象
export function toRaw<T>(observed: T): T {
    // const raw = observed && (observed as Target)['__v_raw']
    const raw = observed && (observed as Target)[ReactiveFlags.RAW];
    return raw ? toRaw(raw) : observed;
}

// 在markRaw实现是给对象添加了__v_skip属性,从这保证不会触发依赖收集和触发依赖
export function markRaw<T extends object>(value: T): Raw<T> {
    if (Object.isExtensible(value)) {
        def(value, ReactiveFlags.SKIP, true);
    }
    return value;
}

export const def = (
    obj: object,
    key: string | symbol,
    value: any,
    writable = false
) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        writable,
        value
    });
};

1.4.2、triggerRef

使用triggerRef可以强制更新页面DOM。这是因为我们创建了这个triggerRef,他会去调用triggerEffects ,那也就是1.3的依赖触发,依赖出发后会执行回调去更新页面。同样的因为shallowRef他没有去转成toReactive() ,那么他也就不会去做依赖收集和依赖触发的操作

ts
export function triggerRefValue(
    ref: RefBase<any>,
    dirtyLevel: DirtyLevels = DirtyLevels.Dirty,  // 4
    newVal?: any,
) {
    ref = toRaw(ref)
    const dep = ref.dep
    if (dep) {
        triggerEffects(
            dep,
            dirtyLevel,
            __DEV__
                ? {
                    target: ref,
                    type: TriggerOpTypes.SET,
                    key: 'value',
                    newValue: newVal,
                }
                : void 0,
        )
    }
}

1.4.3、customRef

自定义ref,它是个工厂函数要求我们返回一个对象 并且实现 get 和 set 适合去做防抖之类的。这个的源码实现和Ref的区别就在于这里没有实现get和set的依赖收集和触发,需要手动实现。

ts
export type CustomRefFactory<T> = (
    track: () => void,
    trigger: () => void
) => {
    get: () => T
    set: (value: T) => void
}

class CustomRefImpl<T> {
    public dep?: Dep = undefined;

    private readonly _get: ReturnType<CustomRefFactory<T>>['get'];
    private readonly _set: ReturnType<CustomRefFactory<T>>['set'];

    public readonly __v_isRef = true;

    constructor(factory: CustomRefFactory<T>) {
        const {get, set} = factory(
            () => trackRefValue(this),
            () => triggerRefValue(this)
        );
        this._get = get;
        this._set = set;
    }

    get value() {
        return this._get();
    }

    set value(newVal) {
        this._set(newVal);
    }
}

1.4.4、toRef

创建一个ref对象,其value值指向另一个对象中的某个属性。先看一下它怎么用的

js
const obj = {
    a: 1,
    b: 2
};

// 把obj对象当中的a拿出来重新创建一个ref对象 ==>  const a = ref(1)
const a = toRef(obj, 'a');

setTimeout(() => {
    obj.a = 2;
    obj.b = 3;
    // 在这里修改了obj.a的值,a.value的值也会跟踪发生变化,但是DOM元素不会发生变化
    // 原因在于obj对象不是响应式的,那么a也不会更新视图。反之如果obj是响应式的,那么a.value的值也会更新视图
    console.log(' =====', obj, a);
}, 2000);

源码实现(简化一下):

  • 先找到toRef的实现,这里面有3种情况,第一种是source是ref,第二种是source是函数,第三种是source是对象,并且key存在,那么就返回propertyToRef
  • propertyToRef:判断对象的key的值是不是ref,如果是就返回,不是就返回propertyToRef
  • propertyToRef:把对象key的值做一个依赖收集
ts
export function toRef(source, key, defaultValue): Ref {
    // 如果source是一个Ref直接返回...这个我就删了,
    // 如果是一个函数,就拿函数返回值
    if (isFunction(source)) {
        return new GetterRefImpl(source) as any;
    } else if (isObject(source) && arguments.length > 1) {
        // 主要还是在这,入参是一个对象,并且有key
        return propertyToRef(source, key!, defaultValue);
    } else {
        return ref(source);
    }
}

// 看对象值是不是Ref直接返回,不然再实例化一个ObjectRefImpl
function propertyToRef(source, key, defaultValue) {
    const val = source[key];
    return isRef(val)
        ? val
        : (new ObjectRefImpl(source, key, defaultValue) as any);
}

// 在这里没有对get、set方法做依赖收集和触发,所以toRef包装后的对象的响应式跟_object是否是一个响应式对象相关
class ObjectRefImpl<T extends object, K extends keyof T> {
    public readonly __v_isRef = true;

    // 构造接收传递过来的对象,key,默认值
    // constructor(_object,_key,_defaultValue)

    get value() {
        return val === undefined ? this._defaultValue! : this._object[this._key];
    }

    set value(newVal) {
        this._object[this._key] = newVal;
    }
}

1.4.5、toRefs

toRefs的实现:接收入参的一个object,遍历这个object,然后将每个值都用toRef包一层

ts
export function toRefs<T extends object>(object: T): ToRefs<T> {
    const ret: any = isArray(object) ? new Array(object.length) : {}
    for (const key in object) {
        ret[key] = propertyToRef(object, key)
    }
    return ret
}

1.4.6、Reactive&shallowReactive&readonly

  • 对于reactive来说他的响应式其实就是上面ref针对于对象那一块的响应式实现。
  • 而shallowReactive就是在创建reactive的时候传递的是mutableHandlers还是shallowReactiveHandlers, 就是在创建MutableReactiveHandler实例的时候是否将_isShallow指定为了true,默认值为false
  • readonly在创建reactive时指定了_readonly,当值为true时会直接返回当前对象

1.5、简单实现reactive响应式

这里还是直接在vue的模版当中写了哈,就不用vue当中的ref、reactive什么的,从头实现一遍。

1.5.1、创建myReactive

首先我们直接创建一个myReactive变量指向一个回调函数,直接返回一个代理对象,首先看get方法,获取里面的key对应的值,我们把所有的key都放到track方法当中,也就是依赖收集,这个方法我们下一步再实现,同时我们判断对象的属性值是不是还是一个对象,如果还是我们就再用myReactive包一层,这样也就实现了深层监听。

然后是set方法。这里主要就是实现trigger依赖触发方法

ts
const isObject = (target: any) => target !== null && typeof target === 'object';
const myReactive: any = <T extends object>(target: T) => {
    return new Proxy(target, {
        get(target, key, receiver) {
            const result = Reflect.get(target, key, receiver) as object;
            track(target, key);
            if (isObject(result)) {
                return myReactive(result);
            }
            return result;
        },
        set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver);
            trigger(target, key);
            return result;
        }
    });
};

1.5.2、effect副作用函数

先实现一个effect,也就是一个对象去绑定一个对应的更新DOM的方法,当数据改变之后调用这个传递给effect的匿名函数去更新DOM

ts
let activeEffect: () => void;
const effect = (fn: Function) => {
    const _effect = () => {
        activeEffect = _effect;
        fn();
    };

    _effect();

    console.log(' =====', activeEffect);
};

1.5.3、get方法:依赖收集,

  • 创建一个名为targetMapWeakMap实例。(WeakMap是一种特殊的Map,它的键名所指向的对象,不计入垃圾回收机制)
  • 方法入参:target(目标对象)和key(属性名)。这个函数用于追踪目标对象的属性与副作用函数(effect)之间的关系
  • 首先尝试从targetMap中获取depsMap(依赖映射),如果没有找到,则创建一个新的Map实例并将其与目标对象关联
  • 尝试从depsMap中获取deps(依赖集合),如果没有找到,则创建一个新的Set实例并将其与属性名关联
  • 最后,将当前的副作用函数(activeEffect)添加到deps集合中。这样,当目标对象的属性发生变化时,可以触发与之相关的副作用函数
ts
const targetMap = new WeakMap();
const track = (target: object, key: any) => {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
        depsMap = new Map();
        targetMap.set(target, depsMap);
    }

    let deps = depsMap.get(key);
    if (!deps) {
        deps = new Set();
        depsMap.set(key, deps);
    }

    deps.add(activeEffect);
};

1.5.4、set方法:依赖触发

这里就简单说明一下,在前面get方法已经做好了依赖收集操作,所以当对对象属性重新赋值的时候会触发trigger,会先从targetMap找当前对象,并且发现该对象存在targetMap当中(也就是说这个是一个响应式对象),再会去depsMap寻找key(重新赋值的key)对应副作用函数,然后通过副作用函数更新DOM就完成了响应式。

ts
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: Function) => {
        effect();
    });
};

1.5.5、测试

直接放到vue的模版里面测试,其中myReactive、effect、track、trigger方法从上面拿下来即可,在这里测试只需要定义好effect函数,并且指定匿名函数去修改DOM元素,然后其他的使用myReactive和reactive是一样的。

vue
<!-- 实现reactive -->
<script lang="ts" setup>
  const user = myReactive({
    name: '张三',
    age: 18,
    foo: {
      bar: {
        a: 1
      }
    }
  });


  onMounted(() => {
    effect(() => {
      const demo = document.querySelector('#demo');
      if (demo) {
        demo.innerHTML = user.name + '-----' + user.age + '-----' + user.foo.bar.a;
      }
    });
  });


  const change = () => {
    user.name = '李四';
    setInterval(() => {
      user.foo.bar.a = Math.random() * 100;
      user.age++;
    }, 2000);
  };

</script>

<template>
  <div id="demo"></div>
  <el-button plain type="primary" @click="change">change</el-button>
</template>

侦听watch&计算属性computed

watch监听器

使用watch

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

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

watch源码

创建watch

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

ts
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?: Immediate;
    deep?: boolean;
    once?: boolean;
}

执行监听

  • 首先对once配置项是否只执行一次进行判断,然后提前先对deep深层监听判断
  • 监听源判断,单值监听、多值监听、回调监听。同时需要去判断ref和reactive。
  • 根据前两步去判断是单层还是深层监听,执行traverse
  • 创建一个job,在job当中去更新新旧值
  • 判断flush,用来确定值变换与dom更新的时机(先后顺序)
  • 判断immediate,是否立即执行一次(执行一次job)
  • 最后通过effect.run()开始依赖收集整体调度
  • doWatch返回了unwatch,这也就是const A =watch(a,()=>{}); A();再调用一个A就可以去除监听的原因
ts
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 = cb;
        cb = (...args) => {
            _cb(...args);
            unwatch();
        };
    }

    const instance = currentInstance;
    const reactiveGetter = (source: object) =>
        deep === true
            ? source // 遍历将发生在下面的包装getter中
            : // 对于deep:false,仅遍历根级属性
            traverse(source, deep === false ? 1 : undefined);

    let getter: () => any;
    let forceTrigger = false;
    let isMultiSource = false;

    if (isRef(source)) {
        // ref对象的get直接访问value属性
        getter = () => source.value;
        // 判断是否是浅层ref
        forceTrigger = 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 cb
            getter = () =>
                callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER);
        } else {
            // no cb -> simple effect
            getter = () => {
                if (cleanup) {
                    cleanup();
                }
                return callWithAsyncErrorHandling(
                    source,
                    instance,
                    ErrorCodes.WATCH_CALLBACK,
                    [onCleanup]
                );
            };
        }
    } else {
        getter = NOOP;
    }

    // 有回调并且是单层监听
    if (__COMPAT__ && cb && !deep) {
        const baseGetter = getter;
        getter = () => {
            const val = baseGetter();
            if (
                isArray(val) &&
                checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
            ) {
                traverse(val);
            }
            return val;
        };
    }

    // 深层监听
    if (cb && deep) {
        const baseGetter = getter;
        getter = () => traverse(baseGetter());
    }

    let cleanup: (() => void) | undefined;
    let onCleanup: OnCleanup = (fn: () => void) => {
        cleanup = effect.onStop = () => {
            callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP);
            cleanup = effect.onStop = undefined;
        };
    };

    // 在SSR中,不需要设置实际效果,它应该是noop
    // 除非它很急切或同步刷新
    let ssrCleanup: (() => void)[] | undefined;
    if (__SSR__ && isInSSRComponentSetup) {
        // 我们也不会调用 invalide 回调(没有设置+runner)
        onCleanup = NOOP;
        if (!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 {
            // watchEffect
            effect.run();
        }
    };

    // 将job标记为观察程序回调,以便调度程序知道,它被允许自触发 先设置为false
    job.allowRecurse = !!cb; // !!undefined false

    let 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任务id
        if (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 run
    if (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;
}

traverse对象值收集

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

ts
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;
}

watchEffect

watchEffect解析

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

ts
// 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]
    );
};

扩展:watchPostEffect & watchSyncEffect

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

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

  • watchSyncEffect : 同步执行

ts
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'}
    );
}

computed 计算属性

使用computed

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

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

js
const A = computed(() => {
    return `A:${a.age}`;
});

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

computed源码

创建computed

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

ts
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,并且不设置setter
        getter = getterOrOptions;
        setter = __DEV__
            ? () => {
                warn('Write operation failed: computed value is readonly');
            }
            : NOOP;
    } else {
        // 传递的是对象形式,直接去对象里面拿get/set方法
        getter = getterOrOptions.get;
        setter = getterOrOptions.set;
    }

    const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR);

    if (__DEV__ && debugOptions && !isSSR) {
        cRef.effect.onTrack = debugOptions.onTrack;
        cRef.effect.onTrigger = debugOptions.onTrigger;
    }

    return cRef as any;
}

ComputedRefImpl实例

  • 直接从构造开始看起,主要关注get/set两个方法。在构造创建了一个ReactiveEffect,也就是响应式的实现方式。并且指定了依赖触发
  • set值在计算属性当中不关系,主要是使用别的值改变后,怎么获取计算之后的值,也就是这的get
  • 在get当中通过了_cacheable是否缓存先执行一遍依赖触发和依赖收集的过程,而后则通过脏值判断是否需要使用缓存当中的值还是用新值
ts
export class ComputedRefImpl<T> {
    public dep?: Dep = undefined;
    private _value!: T;
    public readonly effect: ReactiveEffect<T>;
    public readonly __v_isRef = true;
    public readonly [ReactiveFlags.IS_READONLY]: boolean = false;
    public _cacheable: boolean;

    _warnRecursive?: boolean;

    // 从构造开始,先不关心isReadonly和isSSR,就看get/set
    constructor(
        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 = this;
        this.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()!))
        ) {
            // 值变换之后会触发依赖更新DOM
            triggerRefValue(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;
    }
}

简单实现computed计算属性

  • 首先还是要前面实现的MyReactive响应式的依赖收集依赖触发等方法,这里就不往下面贴了,但是对应依赖收集、触发(effect、trigger)需要进行细微调整,
    • 依赖收集就是在收集的时候添加了一个options,挂在effect上
    • 依赖触发本质上就是在遍历deps的时候去看一下那些值有scheduler调度。这个调度是我们创建computed去给他添加的。
  • 创建computed,这里简单实现就只接收一个函数入参,之后进行依赖收集,在这里就将scheduler调度给挂载到effect副作用函数上。

当trigger响应式触发之后,会去尝试执行一个scheduler调度。执行了调度之后再去更新DOM时会触发effect,在computed当中get值,在这里判断了dirty,也就是新旧值是否相等(是否去更新cacheValue缓存值)。

js
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();
};

使用createApp

在vue3当中是通过createApp将页面给挂在到index.html文件根元素下。

js
createApp(App)    // APP是一个vue组件
    .use(ElementPlus, { // 注册El-plus组件
        locale: zhCn,
        size: 'small',
        zIndex: 3000
    })
    .use(router)        // 注册Vue-Router
    .use(ContextMenu)    // 一个右键菜单组件
    .use(createPinia()) // 注册pinia
    .mixin(drawMixin)    // 混入
    .provide('M', '1')    // 
    .directive('color', (el, binding) => {		// 添加自定义指令
        el.style.color = binding.value;
    })
    .directive('load', loadingDirective)
    .use(i18nPlugin, {
        greetings: {
            hello: '你好!'
        }
    })
    .mount('#app');		// 挂载

By Modify.