vue3发布挺久了,现在看在实践事务中运用问题不大,不过关于一些跑得好好的2.x老项目而言,很难直接升到3.x,在不晋级 3.x的情况下还想运用 composition-api的话,有两种办法,一是运用 @vue/composition-api,二是直接晋级 vue2.7v2.7版本内置了 @vue/composition-api,所以不必手动引入了

本人参与的一个实践事务项目,引入了 vue2.7,但是在运用 ref等呼应式 api的时分发现效果与预期不符合,例如下述基于 vue2.7 的代码

<template>
  <div>
    <p>{{ list[0] }}-{{ data }}</p>
    <button @click="add">Add</button>
  </div>
</template>
<script>
import { ref } from "vue"
export default {
  setup() {
    const list = ref([0])
    const data = ref(0)
    return {
      data,
      list,
      add() {
        list.value[0] = list.value[0] + 1
        data.value = data.value + 1
        conosle.log(list.value, data.value)
      }
    }
  }
}
</script>

当点击 Add按钮的时分,期望 list[0]data 的值自增 1,且页面上展现的值也能一起更新,然而点击之后,控制台打印出来的 list.valuedata.value都没啥问题,但页面上展现的值只有 data 更新了,list[0]却一向固定为初始值,展现出来的值依旧是之前的值,除非我将 list.value指向一个新的地址,页面才能正常更新

add() {
  list.value[0] = list.value[0] + 1
  // 从头指定引证地址
  list.value = list.value.slice()
}

鄙人不才,之前一向以为只需2.x项目引入了 @vue/composition-api,哪怕底层不相同,用法和表现应该是和 vue3.x差不多才对,但是碰到这个问题后我才意识到二者仍是有不同的,由于我的这种写法是比较常见的,作为一个成熟的框架,vue3.x应该不会存在这种问题,然后试了下 vue3.x,相同的写法,的确如我所料,不需要从头指定 list.value的引证地址,页面也能正常更新

现已很长时间没看过源码了,正好借着这个问题简单看下,从源码上搞清楚差异

vue 2.7 的完成

ref 不起作用的原因

源码基于 v2.7.13

问题是由 ref引起的,那么就从它看起

// src/v3/reactivity/ref.ts
export function ref<T extends object>(
  value: T
): [T] extends [Ref] ? T : Ref<UnwrapRef<T>>
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
export function ref(value?: unknown) {
  return createRef(value, false)
}

ref 的类型声明有三个,第一个的意思是假如传入了一个 Ref 类型的参数,则回来这个参数的类型;第二个的意思是传入一个恣意类型的参数,回来一个包装了此恣意类型的 Ref类型,第三个的意思是能够不传入任何参数,这个时分 ts无法自动推导类型,但你能够传入一个类型来告诉 ts 这个值的类型是什么

// src/v3/reactivity/ref.ts
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中首先判别传入值是不是现已是一个 Ref类型的值了,假如是,则不做处理,直接将这个值回来 否则的话,连续调用三次 def 来处理传入的值

// src/core/util/lang.ts
export function def(obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

def调用了 Object.defineProperty 来给目标的特点设置值

第三次调用时,又调用 defineReactiverawValue进行了预处理,在 ref上挂了一个 value特点

// src/core/observer/index.ts
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean
) {
  const dep = new Dep()
  // ...
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  // ...
  let childOb = !shallow && observe(val, false, mock)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // ...
    },
    set: function reactiveSetter(newVal) {
      // ...
    }
  })
  return dep
}

defineReactive 又给 refvalue特点定义了 gettersetter,当写入或读取 ref.value 的时分,就会触发这儿定义的 gettersetter

get: function reactiveGetter() {
  const value = getter ? getter.call(obj) : val
  if (Dep.target) {
    if (__DEV__) {
      // ...
    } else {
      dep.depend()
    }
    // ...
  }
  return isRef(value) && !shallow ? value.value : value
}

get办法里的 Depvue2.x依靠搜集的核心,我从前也写过文章分析这块的完成,在本文这不是要点,就不多说了,get办法的最终有一个对当时获取的数据是否是 ref的判别,假如是,则回来value.value,这便是需要经过 ref.value来获取到咱们设置在 ref里真实数据值的原因

设置值的时分肯定是触发 set 办法

// src/core/observer/index.ts
set: function reactiveSetter(newVal) {
  const value = getter ? getter.call(obj) : val
  if (!hasChanged(value, newVal)) {
    return
  }
  // ...
  if (setter) {
    setter.call(obj, newVal)
  } else if (getter) {
    // #7981: for accessor properties without setter
    return
  } else if (!shallow && isRef(value) && !isRef(newVal)) {
    value.value = newVal
    return
  } else {
    val = newVal
  }
  childOb = !shallow && observe(newVal, false, mock)
  if (__DEV__) {
    // ...
  } else {
    dep.notify()
  }
}

这是派发更新的那一套,最终的 dep.notify() 就用于从头渲染,但直到看到这儿我也没看到哪里有对数组的子项设置值时的处理了,Object.defineProperty 仅仅设置 了ref.value 时的处理,ref.value[0]上并没有 set办法,所以设置 list.value[0] 值的时分,在数据层面的确能够设置成功,但并不会触发页面更新

不运用 composition-api的时分,假如 props或者data是一个数组,改动数组子项的时分,是能够触发页面更新的,这是由于 vue 专门对这种情况做了遍历处理,确保了数组子项也是被 Object.defineProperty处理过的,例如,关于 data来说,在初始化的时分会调用 initDatainitData又会调用 observe,在 observe里就有对数组子项的遍历处理

// src/core/observer/index.ts
export class Observer {
  constructor(public value: any, public shallow = false, public mock = false) {
    // ...
    if (isArray(value)) {
      // ...
      for (let i = 0, l = arrayKeys.length; i < l; i++) {
        const key = arrayKeys[i]
        def(value, key, arrayMethods[key])
      }
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      // ...
    }
  }
}

解决办法

不过,尽管直接设置 ref.value的数组子项无法触发更新,但咱们知道,vue2.x 是劫持了数组的 pushsplice 等办法的,便是用于数组的更新,所以假如你实在是不想给 ref.value 换个地址又想在修正子项的时分触发页面更新,能够调用这些被劫持的办法来到达目的

// 第一种,修正 .value 地址
list.value[0] = list.value[0] + 1
list.value = list.value.slice()
// 第二种,调用 splice
list.value.splice(0, 1, list.value[0] + 1)

实践上,在对一个目标做呼应式处理的时分更主张运用 reactive 而不是 refreactive就能够完成在数组子项修正的时分也触发更新,原理跟propsdata类似,也是调用了 observe办法进行数组遍历

// src/v3/reactivity/reactive.ts
function reactive(target: object) {
  makeReactive(target, false)
  return target
}
function makeReactive(target: any, shallow: boolean) {
  // ...
  if (__DEV__) {
    if (isArray(target)) {
      warn(
        `Avoid using Array as root value for ${
          shallow ? `shallowReactive()` : `reactive()`
        } as it cannot be tracked in watch() or watchEffect(). Use ${
          shallow ? `shallowRef()` : `ref()`
        } instead. This is a Vue-2-only limitation.`
      )
    }
  }
  // ...
  const ob = observe(
    target,
    shallow,
    isServerRendering() /* ssr mock reactivity */
  )
  // ...
}

一开始我没用 reactive的原因是,假如写成 const list = reactive([0])vue会报一个 warn,主张用 ref所以我遵从主张用了 ref,仅仅没想到这个主张不是那么靠谱

reactive warn 的原因

不对劲,用了vue2.7 composition-api的 ref 办法,页面怎样不更新?

看到这个 warn信息我又有点猎奇了,为啥数组作为 reactiveroot valuewatch/watchEffect就会失效呢?仍是看代码

// src/v3/apiWatch.ts
function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  {
    immediate,
    deep,
    flush = 'pre',
    onTrack,
    onTrigger
  }: WatchOptions = emptyObject
): WatchStopHandle {
  // ...
  if (isRef(source)) {
    // ...
  } else if (isReactive(source)) {
    getter = () => {
      ;(source as any).__ob__.dep.depend()
      return source
    }
    deep = true
  }
  // ...
}

watch会调用 doWatch,里面有对传入值类型的判别以调用不同的处理办法,当要 watch的值是 reactive的时分,会调用 depend办法,这是个依靠搜集的办法,所以当修正watch的值的时分,会触发派发更新使得视图更新,但这儿仅仅对 root值进行了依靠搜集,假如传入的值是一个数组,并没有对数组的子项进行依靠搜集,所以改动数组子项的时分,并不会被watch到,也不会触发呼应式更新

这儿没有主动对数组子项进行呼应式包装,但假如数组的子项本来便是现已被呼应式包装过的,那仍是能够被 watch到的,比方子项现已是 ref类型数据了

setup() {
  const data1 = ref(0)
  const list = reactive([data1])
  watch(list, v => {
    console.log('list 改动', v)
  })
  return {
    list,
    add() {
      list[0].value = list[0].value + 1
    }
  }
}

不过这儿 watch 呼应的直接原因不是由于 list 的子项改动,按照常理,想要watch到数组子项改动,是需要第二个参数的deep: true参数的,这儿是由于 data1的改动而触发的呼应,所以不需要这个参数也能够

vue 3.x 的完成

源码基于 v3.2.41

// packages/reactivity/src/ref.ts
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)
}

初始化一个 RefImpl 实例作为 ref实例

// packages/reactivity/src/ref.ts
class RefImpl<T> {
  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value)
    this._value = __v_isShallow ? value : toReactive(value)
  }
}

分别调用 toRawtoReactive处理传入 value,并将处理后的成果设置到实例的两个内部特点上, this._rawValue 存储传入的值,用于后续的数值对比等,主要看 this._value,这才是咱们想看的值

// packages/reactivity/src/reactive.ts
export const toReactive = <T extends unknown>(value: T): T => isObject(value) ? reactive(value) : value

假如传入的 value 是一个目标,那么调用 reactive办法处理,否则直接回来原值,这儿能够看到,当 value 是一个目标的时分,ref 是借助了 reactive的,先看下假如不是一个目标的情况

RefImpl 上有 value 特点getset 办法,先看 get

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

trackRefValue 用于依靠搜集,这儿不必管,最终是看到 get 是把 this._value 回来了的 再看 set

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的时分,会看下新旧值是否相同,假如相同就不管了,否则会重置 this._value,然后从头依靠搜集一下,这儿关于 this._value的从头设置同样判别下是不是目标

当给 ref传入一个数组的时分,显然会走 toReactive,也便是会调用 reactivereactive又调用了 createReactiveObject

// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  // ...
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers,
    reactiveMap
  )
}
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>,
  proxyMap: WeakMap<Target, any>
) {
  // ..
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject 函数体前面一大段都是对合法性等判别不必看,最终是执行了 new Proxy,回来了一个 proxy 实例,也便是对 value进行了署理

看到 proxy就大约理解了,proxy 是能够监听到恣意层级子级的 setget的,且不需要额定的逻辑,就和监听其他特点相同,那么除非是成心写不处理数组子项的逻辑,否则默许便是契合这套基于 Proxy的呼应式体系的

主要看第二个参数,假如 ref参数是一个正常数组的话,那么第二个参数便是 baseHandlers

// packages/reactivity/src/baseHandlers.ts
export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}
const set = /*#__PURE__*/ createSetter()
function createSetter(shallow = false) {
  return function set(
    target: object,
    key: string | symbol,
    value: unknown,
    receiver: object
  ): boolean {
    // ...
    const hadKey =
      isArray(target) && isIntegerKey(key)
        ? Number(key) < target.length
        : hasOwn(target, key)
    const result = Reflect.set(target, key, value, receiver)
    // don't trigger if target is something up in the prototype chain of original
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, TriggerOpTypes.ADD, key, value)
      } else if (hasChanged(value, oldValue)) {
        trigger(target, TriggerOpTypes.SET, key, value, oldValue)
      }
    }
    return result
  }
}

最终会调用 triggertrigger调用 triggerEffects->triggerEffect,触发 effect.run/effect.scheduler,这个办法就会触发组件的视图更新,这些就都是呼应式的内容了,完全能够单开系列文章细说,本文就不再扩展了

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // ...
  if (effect.scheduler) {
    effect.scheduler()
  } else {
    effect.run()
  }
}

小结

本文是带着问题去看源码,仅仅为了弄清楚为什么 ref 跟我幻想的运行成果不一致,所以其他不直接相关的代码我都是直接跳过,比方呼应式体系,尽管跟ref的确有联络,但由于不是我所关注的,所以到底怎样个呼应式法不需要去深入去看,知道有这么回事就行了,避免错综复杂的逻辑越绕越多,只看与我问题相关的逻辑,直到找出我想要的答案

当然了,假如你有时间精力的话,能把整个源码看完并串起来那最好不过了,但那毕竟是个体系性工程,仍是得徐徐图之,无法快速得到一些针对性的答案