vue中响应式数据写法

【VUE2】

在Vue2中,所有的数据都通过一个Data进行统一的return返回,并且在data中对某个组件要用的数据进行统一的管理,常见的使用形式是这样的:

<template>
  <div>
    <el-form :model="form">
      <el-form-item label="话术标题" prop="title">
        <el-input v-model="form.title" placeholder="请输入话术标题"></el-input>
      </el-form-item>
      <el-form-item label="话术内容" prop="content">
        <el-input
          v-model="form.content"
          placeholder="请输入话术内容"
        ></el-input>
      </el-form-item>
    </el-form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      form: {
        name: "",
        title: "",
        content: "",
      },
    };
  },
}
</script>

可以看出来这里定义的内容都在一个数组中进行,或者是一个函数,将要使用的数据返回出来,这里无论怎么进行操作处理,最终进行数据代理的时候得到的都是一个对象,Vue2中直接通过defineProperty进行处理,并绑定对应的监听事件进行响应式的处理。

【VUE3】

而Vue3中,数据的定义可以是单独的,Vue可以让我们随时需要随时定义,我们需要的一个数据可能不是对象。在使用ref的时候,它最终给我们返回的东西是什么?

<template>
  <div class="box">
    <h1>ref demo</h1>
    <div>
      name: {{ name }}
    </div>
  </div>
</template>

<script setup>
import {ref} from 'vue';
const name = ref('hahaha');
console.log('name', name);
console.log('name.value', name.value);

</script>

<style scoped>
.box {
  width: 500px;
  height: 800px;
  padding: 20px;
  background-color: #FADBD8;
}
</style>

我们在这里定义了一个name是string类型的。


我们可以看到,如果直接打印name出来,是没有得到这个hahaha的,只有我们log .value之后才能得到这个我们对应的内容。第一个是我们直接打印出来的内容,它里面是个对象,我们可以从上面这个对象得知,ref 函数实际执行的是 createRef 方法,而该方法实际是返回了一个 RefImpl 构造函数的实例对象其实这个对象是通过 RefImpl 这个类去实例化出来的对象, ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源。该对象只包含一个名为 valueproperty

ref 究竟是如何实现的呢?

我们就一起来看 ref 的实现。

export function ref(value?: unknown) {
  return createRef(value, false)
}

ref Api 的函数签名中,可以看到 ref 函数,实际执行的是 createRef 方法,接收一个任意类型的值作为它的 value 参数,并返回一个 Ref 类型的值。

export interface Ref<T = any> {
  value: T
  [RefSymbol]: true
  _shallow?: boolean
  dep?: Dep
}

Ref 接口定义了 ref 函数返回的类型签名,value 属性保存着 ref 的原始值,RefSymbol 是内部定义的唯一符号用于类型区分,还有一个标识是否为 shallowRef 的_shallow 标识标志这个这个 ref 是否为一个浅层 ref,是布尔类型的属性。

函数体内直接返回了 createRef 函数的返回值。

function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  const ref: any = {}
  def(ref, RefFlag, true)
  def(ref, ReactiveFlags.IS_SHALLOW, shallow)
  def(
    ref,
    'dep',
    defineReactive(ref, 'value', rawValue, null, shallow, isServerRendering())
  )
  return ref
}

createRef 的实现也很简单,通过 createRef 创建 ref,入参为 rawValue shallowrawValue 记录的创建 ref 的原始值,而 shallow 则是表明是否为 shallowRef 的浅层响应式 api。

函数的逻辑为先使用 isRef 判断,如果传入的 rawValue 本身就是一个 ref 的话,如果是的话则直接返回这个 ref 对象。否则返回一个新创建的 RefImpl 类的实例对象。

在 createRef 中,要判断这个 val 是否已经代理过,那么如何才能知道是否已经代理过呢?

那就要来看RefImpl类了,我们可以在RefImpl类中定义一个属性,如果被代理,就让他为true。

class RefImpl<T> {
    private _value: T
  
    public readonly __v_isRef = true
  
    constructor(private _rawValue: T, public readonly _shallow: boolean) {
      // 如果是 shallow 浅层响应,则直接将 _value 置为 _rawValue,否则通过 convert 处理 _rawValue
      this._value = _shallow ? _rawValue : convert(_rawValue)
    }
  
    get value() {
      // 读取 value 前,先通过 track 收集 value 依赖
      track(toRaw(this), TrackOpTypes.GET, 'value')
      return this._value
    }
  
    set value(newVal) {
      // 如果需要更新
      if (hasChanged(toRaw(newVal), this._rawValue)) {
        // 更新 _rawValue 与 _value
        this._rawValue = newVal
        this._value = this._shallow ? newVal : convert(newVal)
        // 通过 trigger 派发 value 更新
        trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
      }
    }
  }

在 RefImpl 类中,有一个私有变量 _value 用来存储 ref 的最新的值;公共的只读变量 __v_isRef 是用来标识该对象是一个 ref 响应式对象的标记与在讲述 reactive api 时的 ReactiveFlag 相同。

而在 RefImpl 的构造函数中,constructor 接受一个私有的 _rawValue 变量,存放 ref 的旧值;公共的 _shallow 变量是区分是否为浅层响应的。在构造函数内部,先判断 _shallow 是否为 true,如果是 shallowRef ,则直接将原始值赋值给 _value,为 false 的情况下,将 rawValue 进行通过 convert 进行转换 reactive 再赋值。

function convert(val) {
  if (typeof val !== 'object' || val === null) {  // 不是对象
    return val
  } else {
    return reactive(val)
  }
}

convert 函数的内部,其实就是判断传入的参数是否是一个对象,如果是一个对象则通过 reactive api 创建一个代理对象并返回,否则直接返回原参数。

    get value() {
      // 读取 value 前,先通过 track 收集 value 依赖
      track(toRaw(this), TrackOpTypes.GET, 'value')
      return this._value
    }
  
    set value(newVal) {
      // 如果需要更新
      if (hasChanged(toRaw(newVal), this._rawValue)) {
        // 更新 _rawValue 与 _value
        this._rawValue = newVal
        this._value = this._shallow ? newVal : convert(newVal)
        // 通过 trigger 派发 value 更新
        trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
      }
    }

__v_isRef 标识 一个对象是否是 ref,当我们在vue当中去执行一个ref .value读取 value 的时候,它就会触发这里的get value ,在 getter 中会先通过 track 收集该 ref 对象的 value 的依赖,收集完毕后返回该 ref 的值。

<template>
  <div class="box">
    <h1>ref demo</h1>
    <div>
      name: {{ name }}
    </div>
  </div>
</template>

<script setup>
import {ref} from 'vue';
const name = ref('hahaha');
console.log('name', name);
console.log('name.value', name.value);

setTimeout(() => {
  name.value = 'xixixi'
}, 1000);

</script>

当我们触发修改 ref 的值的时候,就会触发这里的set value,会将新旧 value 进行比较,如果值不同需要更新,则先更新新旧 value,之后通过 trigger 派发该 ref 对象的 value 属性的更新,让依赖该 ref 的副作用函数执行更新。通过这样一个机制,其实就实现了一个响应式。

为什么要处理set value 跟 get value 呢?

<script setup>
const obj = {};
cons proxyObj = new Proxy(obj, {
  set() {
    console.log('触发了set');
  },
  get() {
    console.log('触发了get');
  }
})

</script>

其实就是去模拟Proxy一样的机制,在Proxy里面就是监听了getter和setter的行为,但是由于Proxy是不能处理const name = ref('hahaha');像字符串这种原始数据类型,所以class RefImpl这里通过get value 和 set value的形式来模拟Proxy里面的getter和setter,这样子就使得原始数据类型也可以监听对应的getter和setter。

另外我们还需知道,对于基本类型的数据 ref 是不具备数据监听的,当赋值或修改值时主动触发了 get 和 set 方法。之后执行 effect 函数,传入一个匿名函数,接着执行赋值行为触发 get 方法。

get value() {
    // 依赖收集
    trackRefValue(this)
    return this._value
}

get 方法核心 trackRefValue(this) 实际触发了 trackRefValue 方法进行数据的依赖收。

export function trackRefValue(ref: RefBase<any>) {
  if (shouldTrack && activeEffect) {
    ref = toRaw(ref)
    if (__DEV__) {
      trackEffects(ref.dep || (ref.dep = createDep()), {
        target: ref,
        type: TrackOpTypes.GET,
        key: 'value'
      })
    } else {
      trackEffects(ref.dep || (ref.dep = createDep()))
    }
  }
}

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: 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 (__DEV__ && activeEffect!.onTrack) {
      activeEffect!.onTrack({
        effect: activeEffect!,
        ...debuggerEventExtraInfo!
      })
    }
  }
}

这块逻辑同 reactive,给指定属性绑定对应的 fn,目的是 dep 对象与 ReactiveEffect 相关联,完成整个依赖收集的过程

set value(newVal) {
    newVal = this.__v_isShallow ? newVal : toRaw(newVal)
    // 新旧值比较
    if (hasChanged(newVal, this._rawValue)) {
      this._rawValue = newVal
      this._value = this.__v_isShallow ? newVal : toReactive(newVal)
      // 依赖触发 
      triggerRefValue(this, newVal)
    }
}

set 方法中 triggerRefValue(this, newVal) 进行依赖触发。

export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
  ref = toRaw(ref)
  if (ref.dep) {
    if (__DEV__) {
      triggerEffects(ref.dep, {
        target: ref,
        type: TriggerOpTypes.SET,
        key: 'value',
        newValue: newVal
      })
    } else {
      triggerEffects(ref.dep)
    }
  }
}

triggerRefValue 方法实际执行了 triggerEffects

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  for (const effect of effects) {
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

最终执行的是每个 effect.run 方法,即传入的匿名函数,从而触发赋值操作,此时整个依赖触发的过程完成

Ref为什么需要.value

const name = ref('hahaha');
console.log('name.value', name.value);
name.value = 'xixixi'

比如我们name.value的时候,才能够走到get value这个函数来监听它读取值的这个行为。当我们name.value = 'xixixi'赋值或者修改值的时候,才可以走到set value这样一个函数来监听它设置值的行为。我们要拿到ref对象的值,要通过.value访问,但是.value其实是一个函数, 在这里的get就是让我们不用手动调用函数,而是直接访问。

简单总结:
ref 其实就是 reactive 包了一层,读取值要通过 ref.value 进行读取,同时进行 track ,而设置值的时候,也会先判断相对于旧值是否有变化,有变化才进行设置,以及 trigger


CcChan
53 声望5 粉丝