ref reactive是怎么实现的

前言

这是vue3系列源码的第五章,使用的vue3版本是3.2.45

推荐

createApp都发生了什么

mount都发生了什么

页面到底是从什么时候开始渲染的

setup中的内容到底是什么时候执行的

背景

在上一篇文章中,我们看了setup中的代码是什么时候执行的,我们顺手提到了ref函数的调用。

这一节,我们就详细看一下我们用ref, reactive函数定义响应式变量的时候,都发生了啥。

前置

我们先看一下我们定义的App.vue组件。

js 复制代码
<template>
  <div>{{ aa }}</div>
  <div>{{ bb.name }}</div>
</template>
<script setup>
import { ref, reactive } from 'vue'

const aa = ref('小识')
const bb = reactive({ name: '谭记' })
</script>

我们这里只定义了两个变量,一个ref变量,一个reactive变量。

基于上篇文章的基础,我们这里直接到setupStatefulComponent函数中的callWithErrorHandling(setup, instance...)中去,直接进入setup的执行。

ref

首先我们看到的是ref函数的执行。

ref函数是定义在reactivity模块中,

js 复制代码
function ref(value) {
    return createRef(value, false);
}

调用了cerateRef函数。

js 复制代码
function createRef(rawValue, shallow) {
    if (isRef(rawValue)) {
        return rawValue;
    }
    return new RefImpl(rawValue, shallow);
}

这里会先判断一下传进来的变量是不是已经是响应式的,isRef函数

js 复制代码
function isRef(r) {
    return !!(r && r.__v_isRef === true);
}

这里面判断是不是响应式的对象,其实就是根据__v_isRef属性来判断的。

这里并不是一个响应式的对象,所以最终返回了一个对象new RefImpl

RefImpl

看一下这个构造函数

js 复制代码
class RefImpl {
    constructor(value, __v_isShallow) {
        this.__v_isShallow = __v_isShallow;
        this.dep = undefined;
        this.__v_isRef = true;
        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, newVal);
        }
    }
}

先看一下传入的参数:

  • value, '小识'
  • __v_isShallow,false

这个构造函数主要做了这几件事:

  • 设置 __v_isShallow: false
  • 设置 __v_isRef: true, 那么下次就能判断出这个对象已经是一个响应式对象了
  • 设置 _rawValue: 小识
  • 设置 _value:小识
  • 设置 value属性的get set

由此可见,ref最终返回的是一个RefImpl对象。

那么ref的设置就到此结束,关于setget我们后面再看。

reactive

js 复制代码
function reactive(target) {
    // if trying to observe a readonly proxy, return the readonly version.
    if (isReadonly(target)) {
        return target;
    }
    return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}

这里先判断了一下isReadonly。

js 复制代码
function isReadonly(value) {
    return !!(value && value["__v_isReadonly" /* ReactiveFlags.IS_READONLY */]);
}

也是通过一个属性来判断的,这里用的是__v_isReadonly

reactive的核心是createReactiveObject函数

createReactiveObject

js 复制代码
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    if (!isObject(target)) {
        if ((process.env.NODE_ENV !== 'production')) {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (target["__v_raw" /* ReactiveFlags.RAW */] &&
        !(isReadonly && target["__v_isReactive" /* ReactiveFlags.IS_REACTIVE */])) {
        return target;
    }
    // target already has corresponding Proxy
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // only specific value types can be observed.
    const targetType = getTargetType(target);
    if (targetType === 0 /* TargetType.INVALID */) {
        return target;
    }
    const proxy = new Proxy(target, targetType === 2 /* TargetType.COLLECTION */ ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
}

先看一下参数:

  • target,'{name: '谭记'}

这个函数主要做了这几件事情:

  • 这里一进来会做一个是否是对象的判断。reactive只接受传入对象,而不能像ref那样,传入一个数字,字符串等基本数据类型
  • 通过getTargetType函数获取了数据类型,这里是Object,结果是1
  • 通过proxy创建了一个代理对象,这里我们详细看一下proxy的baseHandlers
  • 最后在proxyMap里存一下当前的代理对象

由此可见,reactive最终返回了一个proxy代理对象。

那么我们我们定义的ref,reactive变量在template中使用的时候,又会发生什么。

页面到底是从什么时候开始渲染的这篇文章里面,我们提到渲染的时候会走到renderComponentRoot这个函数,在这个函数里面执行了

js 复制代码
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )

我们在这里就详细的看一下执行render函数的过程。

render

render函数的执行,其实是一个字符串匹配的过程。

当读到aa变量的时候,会直接读到我们定义的ref变量。

这里我们在上一篇文章中提到过,setup执行的结果会通过proxyRefs函数进行一次proxy代理,这是代理的具体option:

js 复制代码
const shallowUnwrapHandlers = {
    get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
    set: (target, key, value, receiver) => {
        const oldValue = target[key];
        if (isRef(oldValue) && !isRef(value)) {
            oldValue.value = value;
            return true;
        }
        else {
            return Reflect.set(target, key, value, receiver);
        }
    }
};

unref就是直接返回ref的value属性值

js 复制代码
function unref(ref) {
    return isRef(ref) ? ref.value : ref;
}

因此,这里会读到aa.value

上面我们提到ref对象最终返回的是一个RefImpl构造函数得到的对象,这里读aa.value的时候触发了value的get。

js 复制代码
get value() {
        trackRefValue(this);
        return this._value;
    }

看一下trackRefValue函数

trackRefValue

js 复制代码
function trackRefValue(ref) {
    if (shouldTrack && activeEffect) {
        ref = toRaw(ref);
        if ((process.env.NODE_ENV !== 'production')) {
            trackEffects(ref.dep || (ref.dep = createDep()), {
                target: ref,
                type: "get" /* TrackOpTypes.GET */,
                key: 'value'
            });
        }
        else {
            trackEffects(ref.dep || (ref.dep = createDep()));
        }
    }
}

参数就是aa的那个ref对象。

这个函数的核心是:

  • trackEffects

传入trackEffects函数的参数里有一个dep对象。

js 复制代码
const createDep = (effects) => {
    const dep = new Set(effects);
    dep.w = 0;
    dep.n = 0;
    return dep;
};
  • dep对象中存储的是依赖
  • w 属性通常用于表示当前依赖的状态
  • n 属性通常用于表示该依赖的计数
js 复制代码
function trackEffects(dep, debuggerEventExtraInfo) {
    let shouldTrack = false;
    if (effectTrackDepth <= maxMarkerBits) {
        if (!newTracked(dep)) {
            dep.n |= trackOpBit; // set newly tracked
            shouldTrack = !wasTracked(dep);
        }
    }
    else {
        // Full cleanup mode.
        shouldTrack = !dep.has(activeEffect);
    }
    if (shouldTrack) {
        dep.add(activeEffect);
        activeEffect.deps.push(dep);
        if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
            activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
        }
    }
}

trackEffects函数主要干了这几件事:

  • dep.n和trackOpBit按位或运算,0 | 2 得到 2
  • 计算shouldTrackd的值,得到的值为true

这里的activeEffect对象其实就是new ReactiveEffect得到的对象,前文提到过 --> 页面到底是从什么时候开始渲染的

这里我们来捋一下dep和activeEffect这二者的关系。

  • dep 表示一个响应式数据的依赖集合。每个 dep 对象代表一个数据(可以是一个 ref、reactive 对象等, 这里就代表了aa对象),它维护了一个存储 effect 函数的集合,这些 effect 函数依赖于这个数据。
  • 而对于activeEffect对象,在这里其实就是解析到{{ aa }}插值表达式生成的。

这一段是追踪依赖关系的核心逻辑。

按位或运算:将各个数位的数字进行逻辑或,都是 0 才为 0,否则为 1。

总结一下,这里的trackRefValue其实就是做了依赖追踪。

createBaseVNode

render函数对每一个标签解析之后,其实还会调用createBaseVNode函数创建vnode。

js 复制代码
function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {
  const vnode = {
    __v_isVNode: true,
    __v_skip: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    slotScopeIds: null,
    children,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null,
    ctx: currentRenderingInstance
  } as VNode

  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    currentBlock.push(vnode)
  }
  return vnode
}

这里其实就是根据<div>小识谭记</div>生成vnode。

接下来我们看一下{{bb.name}}的解析,前面其实和aa是一样的,只不过在get的option上有区别。

createGetter

js 复制代码
function createGetter(isReadonly = false, shallow = false) {
    return function get(target, key, receiver) {
        if (!isReadonly) {
            track(target, "get" /* TrackOpTypes.GET */, key);
        }
   
        return res;
    };
}

这里最终是触发了track函数

js 复制代码
function track(target, type, key) {
    if (shouldTrack && activeEffect) {
        let depsMap = targetMap.get(target);
        if (!depsMap) {
            targetMap.set(target, (depsMap = new Map()));
        }
        let dep = depsMap.get(key);
        if (!dep) {
            depsMap.set(key, (dep = createDep()));
        }
        const eventInfo = (process.env.NODE_ENV !== 'production')
            ? { effect: activeEffect, target, type, key }
            : undefined;
        trackEffects(dep, eventInfo);
    }
}

可以看见,最终也是触发了trackEffects函数,和ref定义的变量在依赖收集上并没有本质上的不同。后面也是对这一段生成了vnode。

最后,又生成了一个type为fragment的vnode。

并把这个vonde作为参数传进了normalizeVNode函数中。

js 复制代码
result = normalizeVNode( render!.call(proxyToUse, proxyToUse!, renderCache, props, setupState, data, ctx) )

总结

这篇文章,我们主要看了一下ref, reactive函数是如何定义响应式变量的。定义的方式稍有不同,但是核心都是对get, set的劫持。

这里,我们还顺带看了一下get的流程,主要就是一个依赖收集。

这里我们没有看一下set的过程。

那么在后面的文章里,我们会专门讲一下vue3的响应式原理。

流程图

graph LR ref --> createRef --> RefImpl["new RefImpl"] -- get -->trackRefValue --> trackEffects --> 依赖收集
graph LR reactive --> createReactiveObject --> Proxy["new Proxy"] --get--> track --> trackEffects --> 依赖收集
相关推荐
轻口味37 分钟前
【每日学点鸿蒙知识】AVCodec、SmartPerf工具、web组件加载、监听键盘的显示隐藏、Asset Store Kit
前端·华为·harmonyos
alikami39 分钟前
【若依】用 post 请求传 json 格式的数据下载文件
前端·javascript·json
吃杠碰小鸡1 小时前
lodash常用函数
前端·javascript
emoji1111111 小时前
前端对页面数据进行缓存
开发语言·前端·javascript
泰伦闲鱼1 小时前
nestjs:GET REQUEST 缓存问题
服务器·前端·缓存·node.js·nestjs
m0_748250032 小时前
Web 第一次作业 初探html 使用VSCode工具开发
前端·html
一个处女座的程序猿O(∩_∩)O2 小时前
vue3 如何使用 mounted
前端·javascript·vue.js
m0_748235952 小时前
web复习(三)
前端
迷糊的『迷』2 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
web135085886352 小时前
uniapp小程序使用webview 嵌套 vue 项目
vue.js·小程序·uni-app