likes
comments
collection
share

深入浅出Vue的ref

作者站长头像
站长
· 阅读数 19

上篇文章我们分析了reactive方法的过程,并完成了手写一个reactive方法,但此时的reactive方法是有局限性的;有以下两个问题: 1.reactive方法能接受简单类型的值吗?

<script>
    const { reactive, effect } = Vue

    const obj = reactive('张三')

    console.log(obj)
</script>

深入浅出Vue的ref

结果控制台报错了,reactive最终要生成一个proxy的实例,new Proxy的target必须是一个对象,它是一个被代理对象;如果target变成了一个非对象的值,此时的new Proxy是没办法代理的,所以会报错;

2.如果我们对reactive的obj进行解构,它还具备响应性吗?

<script>
    const { reactive, effect } = Vue

    const obj = reactive({
      name: '张三'
    })

    let { name } = obj

    effect(() => {
      document.querySelector('#app').innerText = name
    })

    setTimeout(() => {
      name = '李四'
      console.log(name)
    }, 1000)
  </script>

我们等待了一秒,控制台里打印出李四,但页面上的张三也没变成李四;也就是说reactive在解构之后会失去响应性;这是为什么呢?obj在解构之前有响应性因为它是一个proxylet { name } = obj解构之后,name不是一个proxy了,它变成了一个字符串,也就失去了响应性;

深入浅出Vue的ref

上述两个🌰中,我们可以知道现在的reactive方法是有局限性的,它不能在非对象的数据上使用;如果想要非对象的数据也具有响应性,该怎么办呢?这时候我们就可以通过ref来实现了;本篇文章就是探究ref是如何解决这个问题的。

ref复杂数据类型的响应性

首先有一个🌰:

<script>
    const { ref, effect } = Vue

    const obj = ref({
      name: '张三'
    })
    console.log(obj)
    effect(() => {
      document.querySelector('#app').innerText = obj.value.name
    })
    setTimeout(() => {
      obj.value.name = '李四'
    }, 2000)
  </script>
export function ref(value?: unknown) {
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

rawValue为{name: "张三"}对象;isRef是false跳过; 判断isRef的方法,就是判断r.__v_isRef的属性;

export function isRef(r: any): r is Ref {
  return !!(r && r.__v_isRef === true)
}

类似于上篇文章中的ReactiveEffect实例;

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) {
    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)
    }
  }
}
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

第8行的constructor中传入的value是{name: "张三"}对象,__v_isShallow是判断它是否是浅层的;this._rawValue toRaw得到,还是{name: "张三"}对象;this._value``是toReactive来转化成reactive;这有一个重点:如果ref中传入的是对象,会走reactive的逻辑;如果不是对象,则会走ref逻辑; 第5行的dep,上篇文章中也有dep,是一个set存放ReactiveEffect实例; 第6行的__v_isRef为true;isRef方法就能判断为true了;

<script>
    const { ref, effect } = Vue

    const obj = ref({
      name: '张三'
    })
    console.log(obj)
    effect(() => {
      document.querySelector('#app').innerText = obj.value.name
    })
    setTimeout(() => {
      obj.value.name = '李四'
    }, 2000)
  </script>

这段代码中的第7行会打印出下图的内容:

深入浅出Vue的ref

现在我们来看一下RefImpl的set和get方法做了什么吧;来写一个简单的🌰;

<script>
  class RefImpl {
    // 实例的getter行为
    get value() {
      console.log('get value')
      return 'get Value'
    }
    // 实例的setter行为
    set value(val) {
      console.log('set value')
    }
  }
  const ref = new RefImpl()
  console.log(ref)
</script>

深入浅出Vue的ref

如果我们在终端输入ref.value = 123会触发RefImpl的set方法,再输入ref.value会触发getter行为,这和Vue中RefImpl是类似的,让我们继续来看Vue的RefImpl;

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) {
    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)
    }
  }
}

第13行的get方法中,调用了trackRefValue完成依赖收集的工作,返回了this._value,这时的this._value是一个proxy的实例; 接着是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()))
    }
  }
}

第2行的activeEffect肯定是存在的,如下图:

深入浅出Vue的ref

第5行触发了trackEffects,ref.dep = createDep()生成了一个dep;

trackEffects方法有点眼熟啊,这是我们上篇文章分析过的;

export function trackEffects(
  dep: Dep,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // 省略
  if (shouldTrack) {
    dep.add(activeEffect!)
    activeEffect!.deps.push(dep)
  }
}

第7行,activeEffect加入dep中;activeEffectdep就建立起了关联;这里和reactive的逻辑相同;

总结: 1.对于ref函数,会返回RefImpl类型的实例; 2.在该实例中,会根据传入的数据类型分别进行处理: a.复杂数据类型:转化为reactive返回的proxy实例; b.简单数据类型:不作处理; 3.无论我们执行obj.value.name还是obj.value.name = XXX本质上都触发了get方法; 4.之所以会进行响应性是因为obj.value是一个reactive函数生产的proxy;

ref简单数据类型的响应性

<script>
    const { ref, effect } = Vue

    const obj = ref('张三')
    console.log(obj)
    effect(() => {
      document.querySelector('#app').innerText = obj.value
    })
    setTimeout(() => {
      obj.value = '李四'
    }, 2000)
  </script>

当我们ref里传入一个字符串时,发现它是有响应性的,页面中张三变成了李四

深入浅出Vue的ref

让我们来探究一下过程,首先进入ref方法:

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

然后进入class RefImpl中,这与上面内容是相同的;不同就是在constructor中,this._value不再是toReactive返回的而直接是value;这就是简单数据类型和复杂数据类型不同的处理逻辑;

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) {
    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)
    }
  }
}

和上文相同的逻辑在这里先省略了; 当执行到setTimeout时会触发第18行的set方法;上文中复杂数据结构出发的是reactive中的setter行为,而这里的简单数据类型触发的是RefImpl中的setter,这里是它们最大的不同; 第20行hasChanged(newVal, this._rawValue)是用来判断张三李四是否相同; 第21行 this._rawValue = newVal修改原始值,改成newVal; 第22行先判断this.__v_isShallow,这里的this._value会直接是newVal; 然后调用triggerRefValue方法:

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)
    }
  }
}

第3行中的ref.dep中是有值的; 第5行triggerEffects方法是用来循环触发依赖;到这时依赖被成功触发,页面中张三就变成了李四

深入浅出Vue的ref

总结: ref的简单数据类型是用proxy或者Object.defineProperty来实现的响应性吗?并不是,我们可以看到是通过上文的class RefImpl中的setget方法;

深入浅出Vue的ref

咱们下篇文章见!

转载自:https://juejin.cn/post/7304268647110901772
评论
请登录