Vue3响应式解读

309 阅读11分钟

Vue响应式的本质

vue响应式的本质:函数和数据进行关联。(如果函数中用到了响应式数据,那么当响应式数据变化时,函数会重新执行,template就是个render函数)

Vue3原始值的响应式原理

vue3是基于proxy来实现数据劫持的,然而proxy它只能用于对象,不能用于基础类型数据。因此vue3对基础类型的数据进行了一层包裹。通过使用ref函数,生成RefImpl对象,内部再通过toReactive函数实现响应式。

ref函数

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

// packages/reactivity/src/ref.ts

function createRef(rawValue: unknown, shallow: boolean) {
  // 如果已经是 ref 对象,直接返回
  if (isRef(rawValue)) {
    return rawValue
  }
  // 创建一个 ref 对象实例
  return new RefImpl(rawValue, shallow)
}

ref接收一个原始值作为参数,内部通过createRef方法,生成一个 RefImpl实例对象

rawValue:即传入的原始值

shallow:是一个boolean值,表示创建的响应式变量是深层次还是浅层次。

**里面_rawValue 即为原始值, _value即响应式变量值 **

RefImpl

// packages/reactivity/src/ref.ts

// ref 的实现类  ref函数返回的其实是RefImpl类的实例对象
class RefImpl<T> {
  private _value: T
  private _rawValue: T  //传入的原始值

  public dep?: Dep = undefined  //调度中心的dep是一个map,键名为监听的对象,键值是个子map。子map中,键名是对应的key,键值是一个set,用来存放watch对应的update方法。而这里的dep只是收集副作用函数
    
  // ref实例下都有一个 __v_isRef 的只读属性,标识它是一个ref
  public readonly __v_isRef = true

  constructor(value: T, public readonly __v_isShallow: boolean) {
    // 如果是 浅层响应,则直接将 _rawValue 置为 value,否则通过toRaw()获取其原始值,因为它可能传入的是响应式变量
    this._rawValue = __v_isShallow ? value : toRaw(value)
    // 如果是 浅层响应,则直接将 _value 置为 value,否则将 value 转换成深响应
    this._value = __v_isShallow ? value : toReactive(value)
  }

  // 拦截 读取 操作
  get value() {
    // 通过 trackEffects 收集 value 依赖 将副作用函数推入dep中
    trackRefValue(this)
    // 返回 该ref 对应的 value 属性值,实现自动脱 ref 能力 
    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, 通过 triggerEffects 派发 value 更新
      triggerRefValue(this, newVal)
    }
  }
}

过程:

  • 定义两个私有变量,_value用来存储原始值变成响应式变量后的数据_rawvalue用来存放原始值
  • 在constructor中,判断其是否是深层次响应式,如果是的话,就调用toReative函数,将其转化为响应式变量
  • 在get中收集副作用函数。
  • 在set中判断新值和旧值是否发生改变,如果改变,就调用trigger通知调度中心,触发对应订阅者的update方法

总结:RefImpl类本质上还是通过toReactive函数,将其转化为响应式变量

响应式丢失问题

在刚接触vue3的时候,我们对reactive创建的响应式变量解构,发现再次调用时,不会触发响应式。这是因为{...obj}得到的是一个 新的对象,而且这个新的对象 是 普通对象,(不是通过reactive创建的),自然没有响应式。为了方便我们解构使用,vue封装了toRef

// obj 是响应式数据
const obj = reactive({ foo: 1, bar: 2 })

// 将响应式数据展开到一个新的对象 newObj 中
const newObj = {
  ...obj
}

effect(() => {
  // 在副作用函数内通过新的对象 newObj 读取 foo 属性值
  console.log(newObj.foo)
})

// 很显然,此时修改 obj.foo 并不会触发响应
obj.foo = 100

toRef

//用来为源响应式对象上的某个 property 新创建一个 ref
// 第一个参数 obj 是一个响应式数据,第二个参数是 obj 对象的一个健
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue?: T[K]
): ToRef<T[K]> {
  const val = object[key]
  // 返回 ref 对象
  return isRef(val)
    ? val
    : (new ObjectRefImpl(object, key, defaultValue) as any)
}

toRef的原理:

  • 第一个参数是响应式对象,第二个参数是响应式对象中的键名。首先通过isRef判断该键名对应的value是否是响应式对象,如果是直接返回,如果不是,就通过ObjectRefImpl类创建一个响应式变量返回

image-20231225152552852 image-20231225152623386

返回的响应式ObjectRefImpl实例,它封装了get valueset value方法,当调用的时候自动触发get方法,它会返回响应式变量_object对应的属性。当修改实例,实际上也是修改_ _object的值。当读取响应式变量,它会读取obj中对应的key的value,修改也是修改obj中的值

image-20240103111639506

props通过toRef解构,父组件修改了props中的值,子组件也能响应式变更。这是因为toRef返回的是ObjectRefImpl实例,当调用时触发get value(),它返回的就是props对象中同名属性(即调用了props中的数据),修改时也是修改props中的数据。因此只要props是响应式变量,解构出来的属性也能响应式更新。

image-20240103142228317

image-20240103141217095

注意:父组件传递给子组件props的变量必须是响应式变量,只有响应式变量变化,props才会同步更新。而且子组件内部不能直接修改props,需要通过emit()子传父,通知父组件修改。

toRefs

toRefs其实就是遍历响应式对象里的key,判断其键值是否是响应式变量,如果不是,调用toRef,创建响应式变量。最后放在一个对象中返回。

因此,toRef本质上是将响应式对象的第一层属性值转化为ref(通过ObjectRefImpl类),如果我们想要使用属性值,那么需要通过 .value来进行访问

const obj = reactive({ foo: 1, bar: 2 })
const { foo, bar } = { ...toRefs(obj) }
console.log('foo', foo)
console.log('bar', bar)

image-20231225152250714

模板中自动脱ref(省略.value)

在模板编写中,我们会遇到一个很奇怪的事情,就是ref创建的响应式变量,我们无需.value来获取其值。

setup函数是有返回值的,返回的数据会交给proxyRefs来处理,它会再次创建一个proxy对象,在get中直接返回.value。

            let obj = {
                a: 1,
                b: 2,
            }
            let _obj1 = new Proxy(obj, {
                get(target, key) {
                    console.log('第一次get调用')
                    //track
                    return target[key]
                },
                set(target, key, value) {
                    console.log('第一次set调用')

                    target[key] = {
                        value: value,
                    }
                    //trigger
                },
            })
            console.log(_obj1.a)
            let _obj2 = new Proxy(_obj1, {
                get(target, key) {
                    console.log('第二次get调用')
                    return target[key].value
                },
                set(target, key, value) {
                    console.log('第二次set调用')
                    target[key] = value
                },
            })
            _obj2.a = 1111
            console.log(_obj2.a)

Vue非原始值的响应式原理

vue3非原始值的响应式是基于proxy实现的,proxy除了可以代理对象、数组,还可以代理map、set、weakmap、weakset等集合类型。

vue3非原始值响应式通过调用reactive(),基于proxy返回一个代理对象,在get中进行依赖收集,在set中进行通知调度中心,分发依赖,从而实现视图更新

代理Object

属性读取的拦截

一个普通对象的属性读取有:①直接读取obj.key ②for in 循环读取 ③key in obj

对于这些属性的读取,vue都会进行拦截操作,以便数据发生变更时,视图能跟着变化。即 track()

过程:

  • 如果被拦截的key 是一个symbol或者原型链上的属性,那么不进行依赖收集直接返回属性的读取结果(即不做响应式处理)
  • 实现get()方法,在get中调用 track()方法收集依赖
  • 实现set()方法,在set中调用 trigger()方法触发对应副作用,实现视图更新

简单实现reactive:

   let bucket = new WeakMap()
            let proxyMap = new WeakMap()
            const data = {
                text: 'hello vue3',
            }

            function reactive(target) {
                const existingProxy = proxyMap.get(target)
                if (existingProxy) {
                    // 已被代理过,直接返回缓存的代理对象
                    // 避免重复被代理
                    return existingProxy
                }

                const proxy = new Proxy(target, baseHandlers)

                proxyMap.set(target, proxy)
                return proxy
            }

            const baseHandlers = {
                get(target, key, receiver) {
                    const res = Reflect.get(target, key, receiver)
                    track(target, key)
                    return res
                },
                set(target, key, value, receiver) {
                    const res = Reflect.set(target, key, value, receiver)
                    trigger(target, key)
                    return res
                },
            }
            const _data = reactive(data)
            //把effect转为用来注册副作用函数的函数
            let activeEffect = null
            function effect(fn) {
                activeEffect = fn
                fn()
            }
            effect(() => (document.body.innerText = _data.text))

            //将activeEffect添入set中
            function track(target, key) {
                if (!activeEffect) return //depsMap   target:map     map中  key:set
                let depsMap = bucket.get(target)
                if (!depsMap) {
                    bucket.set(target, (depsMap = new Map()))
                }
                let deps = depsMap.get(key)
                if (!deps) {
                    depsMap.set(key, (deps = new Set()))
                }
                deps.add(activeEffect)
            }
            //取出set并循环执行
            function trigger(target, key) {
                const depsMap = bucket.get(target)
                if (!depsMap) return
                const effects = depsMap.get(key)
                effects && effects.forEach((effect) => effect())
            }
            setTimeout(() => {
                _data.text = 'hello react'
            }, 1000)

嵌套属性处理

因为proxy只能实现浅层对象的响应式,如果对象是深层次的,如:

let data={
	son:{
		a:1
	}
}
data.son.a=123  //这样是无法触发对应的set

因此需要递归对象,为其子对象进行代理。

computed源码解读

vue源码中通过effect函数注册副作用,在其options中如果添加了lazy:true,则不立即执行副作用函数。而是将副作用函数返回

export function effect<T = any>(
  fn: () => T,
  options?: ReactiveEffectOptions
): ReactiveEffectRunner {
  
  // 省略部分代码
  
  // 只有非 lazy 的时,才执行副作用函数
  if (!options || !options.lazy) {
    _effect.run()
  }
  const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
  runner.effect = _effect
  // 将副作用函数作为返回值返回
  return runner
}

computed 本质上就是一个懒加载的副作用函数,在effect函数中通过lazy进行控制,实现懒执行。


computed函数调用签名

// packages/reactivity/src/computed.ts

// 只读的
export function computed<T>(
  getter: ComputedGetter<T>,
  debugOptions?: DebuggerOptions
): ComputedRef<T>
// 可写的 
export function computed<T>(
  options: WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
)
  • 第一种情况,computed接收一个getter作为参数,返回一个不可修改的响应式 ref对象

    const count = ref(1)
    // computed 接受一个 getter 函数
    const plusOne = computed(() => count.value + 1)
    
    console.log(plusOne.value) // 2
    
    plusOne.value++ // 错误
    
    
  • 第二种情况,接收一个具有 setget的options对象,返回一个可修改的响应式ref对象

    const count = ref(1)
    const plusOne = computed({
      // computed 函数接受一个具有 get 和 set 函数的 options 对象
      get: () => count.value + 1,
      set: val => {
        count.value = val - 1
      }
    })
    
    plusOne.value = 1
    console.log(count.value) // 0
    
    

实现原理

  • 在第一种情况下,它会判断传入的第一个参数是否是函数,如果是函数,赋值给定义好的getter,同时,setter赋值为不进行任何操作 NOOP

     // 判断 getterOrOptions 参数 是否是一个函数
      const onlyGetter = isFunction(getterOrOptions)
      if (onlyGetter) {
        // getterOrOptions 是一个函数,则将函数赋值给取值函数getter 
        getter = getterOrOptions
        setter = __DEV__
          ? () => {
            	//dev环境下 setter为提示,prod环境下不进行操作
              console.warn('Write operation failed: computed value is readonly')
            }
          : NOOP
      } else {
        // getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter
        getter = getterOrOptions.get
        setter = getterOrOptions.set
      }
    
    
  • 第二种情况下,传入options对象,就将传入的get和set函数分别赋值给setter和getter

        // getterOrOptions 是一个 options 选项对象,分别取 get/set 赋值给取值函数getter和赋值函数setter
        getter = getterOrOptions.get
        setter = getterOrOptions.set
    
  • 处理完setter和getter后,会通过 ComputedRefImpl类,创建出一个ref对象并返回

     // 实例化一个 computed 实例
      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类的实现

因为computed最后会返回一个 ComputedRefImpl类的实例( 即 ref响应式变量 ),它有 get value() 和 set value()两个方法 ,如果外界读取了value属性,就会触发 get value(),在该方法中手动调用trackRefValue,添加依赖。ComputedRefImpl内部用 _value来缓存上一次的值, _dirty标识是否需要重新计算值,如果为true,则需要重新计算,如果false。直接返回 _value即实现懒加载。

当计算属性依赖的响应式数据变更时,手动触发 triggerRefValue,触发响应式。

而只有手动修改.value的时候,才会触发对应的set value(),修改深层次属性是没有反应的

// packages/reactivity/src/computed.ts

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  // value 用来缓存上一次计算的值
  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  // dirty标志,用来表示是否需要重新计算值,为 true 则意味着具有副作用,需要重新计算
  public _dirty = true
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      // getter的时候,不派发通知
      if (!this._dirty) {
        this._dirty = true
        // 当计算属性依赖响应式数据变化时,手动调用 triggerRefValue 函数 触发响应式
        triggerRefValue(this)
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    // 获取原始对象
    const self = toRaw(this)
    // 当读取 value 时,手动调用 trackRefValue 函数进行追踪  track需要传入一个对象,然后会将activeEffect推入到对应的set中等待触发
    trackRefValue(self)
    // 只有副作用才计算值,并将得到的值缓存到value中
    if (self._dirty || !self._cacheable) {
      // 将dirty设置为 false, 下一次访问直接使用缓存的 value 中的值
      self._dirty = false
      self._value = self.effect.run()!
    }
    // 返回最新的值
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

注意:

computed传入对象写法,它会返回一个computedRef实例,如果是以函数形式写的,当它依赖的响应式数据变更时,它会手动触发trigger。 如果是以对象形式写的,只有修改**computedRef.value才会触发set方法**。

const model=computed({
	set(newValue){},
	get(){}
})
//当model.value=xxx 时,才会触发对应的set

//如果是model.value.name=xx,是不会触发对应的set的,这是因为computedRef内部通过get value()和set value()追踪触发依赖,并不是深层次响应式

如果想要实现深层次响应式,需要用一些奇技淫巧

//get返回一个proxy,它修改时触发proxy的set方法,然后修改原来的值。set除非修改model.value才会触发

const model = computed({
  get() {
    return new Proxy(form, {
      set(obj, key, newValue) {
        obj[key] = newValue
        return true
      },
    })
  },
  set(newValue) {
    form.name = newValue //修改props中的数据
    console.log('newValue', newValue)
  },