vue3响应式--proxy

vue3.0快发布了,也带来了很多新的特性,如新的监测设计,PWA,TS支持等,本节一起了解下新的监测原理。

旧的响应式原理

vue2利用Object.defineProperty来劫持data数据的getter和setter操作。这使得data在被访问或赋值时,动态更新绑定的template模块。

对象:会递归得去循环vue得每一个属性,(这也是浪费性能的地方)会给每个属性增加getter和setter,当属性发生变化的时候会更新视图。

数组:重写了数组的方法,当调用数组方法时会触发更新,也会对数组中的每一项进行监控。

缺点:对象只监控自带的属性,新增的属性不监控,也就不生效。若是后续需要这个自带属性,就要再初始化的时候给它一个undefined值,后续再改这个值

数组的索引发生变化或者数组的长度发生变化不会触发实体更新。可以监控引用数组中引用类型值,若是一个普通值并不会监控,例如:[1, 2, {a: 3}] ,只能监控a

Proxy消除了之前 Vue2.x 中基于 Object.defineProperty 的实现所存在的这些限制:无法监听 属性的添加和删除、数组索引和长度的变更,并可以支持 Map、Set、WeakMap 和 WeakSet!

Proxy及使用

MDN 上是这么描述的——Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~

一个例子:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 
let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        return target[key]
    },
    set (target,key,value) {
        console.log('触发更新')
        target[key] = value
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
proxy.arr   //收集依赖
proxy.name = '123'  //触发更新

定义了一个对象obj,通过代理后的对象(上面的proxy)来操作原对象。当取值的时候会走get方法,返回对应的值,当设置值的时候会走set方法,触发更新。

但这是老的写法,新的写法是使用Reflect。

Reflect是内置对象,为操作对象而提供的新API,将Object对象的属于语言内部的方法放到Reflect对象上,即从Reflect对象上拿Object对象内部方法。 如果出错将返回false

简单改写上面这个例子:

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
proxy.arr  //收集依赖
proxy.name.name //收集依赖(只有一个)
proxy.name = '123' //触发更新

效果依旧和上面一样。

但是有一个问题,这个对象是多层对象,它并不会取到里面的那个name的值。

这是因为之前Object.defineProperty方法是一开始就会对这个多层对象进行递归处理,所以可以拿到,而Proxy不会。它是懒代理。如果对这个对象里面的值进行代理就取不到值。就像上面我们只对name进行了代理,但并没有对name.name进行代理,所以他就取不到这个值,需要代理之后才能取到。

改进如下:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        if(typeof target[key] === 'object' && target[key] !== null){
            //递归代理,只有取到对应值的时候才会代理
            return new Proxy(target[key],handler)
        }
        
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
proxy.arr  //收集依赖
proxy.name.name //收集依赖(2个)

接下来看看数组的代理过程:

let obj = {
    name:{name:'hhh'},
    arr: ['吃','喝','玩']
}
//proxy兼容性差 可以代理13种方法 get set
//defineProperty 只对特定 的属性进行拦截 

let handler = {
    get (target,key) { //target就是obj key就是要取obj里面的哪个属性
        console.log('收集依赖')
        if(typeof target[key] === 'object' && target[key] !== null){
            //递归代理,只有取到对应值的时候才会代理
            return new Proxy(target[key],handler)
        }
        
        // return target[key]
        //Reflect 反射 这个方法里面包含了很多api
        return Reflect.get(target,key)
    },
    set (target,key,value) {
        console.log('触发更新')
        // target[key] = value //这种写法设置时如果不成功也不会报错 比如这个对象默认不可配置
        return Reflect.set(target,key,value)
    }
}

let proxy = new Proxy(obj,handler)
//通过代理后的对象取值和设置值
// proxy.name.name = '123' //设置值,取一次,设置一次
proxy.arr.push(456)
//输出
收集依赖 (proxy.arr)
收集依赖 (proxy.arr.push)
收集依赖 (proxy.arr.length)
触发更新 写入新值
触发更新 长度改变
proxy.arr[0]=456
//输出
收集依赖 (proxy.arr)
触发更新 写入新值

这里面它会走两次触发更新的操作,因为第一次需要修改数组的长度,第二次再把元素放进数组里。所以我们需要判断一下它是新增操作还是修改操作

判断新旧属性:

set (target,key,value) {
        let oldValue = target[key]
        console.log(key, oldValue, value)
        if(!oldValue){
            console.log('新增属性')
        }else if(oldValue !== value){
            console.log('修改属性')
        }
        return Reflect.set(target,key,value)
    }

首先拿到它的旧值,如果这个值不存在就是新增,如果存在但不相等就是修改操作

vue3的响应式设计

Vue 3.0 的想法是引入灵感来自于 React Hook 的 Function-based API,作为主要的组件声明方式。

意思就是所有组件的初始状态、computed、watch、methods 都要在一个叫做 setup 的方法中定义,抛弃(暂时会继续兼容)原有的基于对象的组件声明方式。

组件装载
vue3.0用effect副作用钩子来代替vue2.0watcher。我们都知道在vue2.0中,有渲染watcher专门负责数据变化后的从新渲染视图。vue3.0改用effect来代替watcher达到同样的效果。

先简单介绍一下mountComponent流程,后面的文章会详细介绍mount阶段的

// 初始化组件
  const mountComponent: MountComponentFn = (
    ...
  ) => {
    /* 第一步: 创建component 实例   */
    const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
      ...
    ))

    /* 第二步 : TODO:初始化 初始化组件,建立proxy , 根据字符窜模版得到 */
    setupComponent(instance)
    /* 第三步:建立一个渲染effect,执行effect */
    setupRenderEffect(
      instance,     // 组件实例
      initialVNode, //vnode  
      container,    // 容器元素
      ...
    )   
  }

整个mountComponent的主要分为了三步,我们这里分别介绍一下每个步骤干了什么:

① 第一步: 创建component 实例 。
② 第二步:初始化组件,建立proxy ,根据字符窜模版得到render函数。生命周期钩子函数处理等等
③ 第三步:建立一个渲染effect,执行effect。
从如上方法中我们可以看到,在setupComponent已经构建了响应式对象,但是还没有初始化收集依赖。

setupRenderEffect 构建渲染effect

const setupRenderEffect: SetupRenderEffectFn = (
   ...
  ) => {
    /* 创建一个渲染 effect */
    instance.update = effect(function componentEffect() {
      //...省去的内容后面会讲到
    },{ scheduler: queueJob })
  }

setupRenderEffect的作用
① 创建一个effect,并把它赋值给组件实例的update方法,作为渲染更新视图用。
② componentEffect作为回调函数形式传递给effect作为第一个参数

effect做了些什么

export function effect(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect {
  const effect = createReactiveEffect(fn, options)
  /* 如果不是懒加载 立即执行 effect函数 */
  if (!options.lazy) {
    effect()
  }
  return effect
}

effect作用如下
① 首先调用。createReactiveEffect.
② 如果不是懒加载 立即执行 由createReactiveEffect创建出来的ReactiveEffect函数。

接着看createReactiveEffect(这就是vue2.x中的Watcher)

function createReactiveEffect(
  fn: (...args: any[]) => T, /**回调函数 */
  options: ReactiveEffectOptions
): ReactiveEffect {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    try {
        enableTracking()
        effectStack.push(effect) //往effect数组中里放入当前 effect
        activeEffect = effect //TODO: effect 赋值给当前的 activeEffect
        return fn(...args) //TODO:    fn 为effect传进来 componentEffect
      } finally {
        effectStack.pop() //完成依赖收集后从effect数组删掉这个 effect
        resetTracking() 
        /* 将activeEffect还原到之前的effect */
        activeEffect = effectStack[effectStack.length - 1]
    }
  } as ReactiveEffect
  /* 配置一下初始化参数 */
  effect.id = uid++
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = [] /* TODO:用于收集相关依赖 */
  effect.options = options
  return effect
}

createReactiveEffect的作用主要是配置了一些初始化的参数,然后包装了之前传进来的fn重要的一点是把当前的effect赋值给了activeEffect,这一点非常重要,和收集依赖有着直接的关系.
整个响应式初始化阶段进行总结
① setupComponent创建组件,调用composition-api,处理options(构建响应式)得到Observer对象。
② 创建一个渲染effect,里面包装了真正的渲染方法componentEffect,添加一些effect初始化属性。
③ 然后立即执行effect,然后将当前渲染effect赋值给activeEffect

数据响应
vue3新的响应式书写方式(老的也兼容)

setup() {
     const state = {
         count: 0,
         double: computed(() => state.count * 2)
     }
     function increment() {
         state.count++
     }
     
    onMounted(() => {
        console.log(state.count)
    })
    
    watch(() => {
        document.title = `count ${state.count}`
   })
    return {
        state,
      increment
   }
}

感觉setup这块就有点像 react hooks 理解成一个带有数据的逻辑复用模块,不再以vue组件为单位的代码复用了
和React钩子不同,setup()函数仅被调用一次。
所以新的响应书数据两种声明方式:

  1. Ref
    前提:声明一个类型 Ref
    ref()函数源码:
function ref(raw: unknown) {
   if (isRef(raw)) {
     return raw
   }
   // convert 内容:判断 raw是不是对象,是的话 调用reactive把raw响应化
   raw = convert(raw)
   const r = {
     _isRef: true,
     get value() {
      // track 理解为依赖收集
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      // trigger 理解为触发监听,就是触发页面更新好了
      trigger(r, OperationTypes.SET, '')
    }
  }
  return r as Ref
}

上面的convert函数内容为

const convert = val => isObject(val) ? reactive(val) : val

可以看得出 ref类型 只会包装最外面一层,内部的对象最终还是调用reactive,生成Proxy对象进行响应式代理。
疑问:可能有人想问,为什么不都用proxy, 内部对象都用proxy,最外层还要搞个 Ref类型,多此一举吗?
理由可能比较简单,那就是proxy代理的都是对象,对于基本数据类型,函数传递或对象结构是,会丢失原始数据的引用。
官方解释:

However, the problem with going reactive-only is that the consumer of a composition function must keep the reference to the returned object at all times in order to retain reactivity. The object cannot be destructured or spread:

  1. Reactive
    源码如下:
    注:target一定是一个对象,不然会报警告
function reactive(target) {
   // 如果target是一个只读响应式数据
   if (readonlyToRaw.has(target)) {
     return target
   }
   // 如果是被用户标记的只读数据,那通过readonly函数去封装
   if (readonlyValues.has(target)) {
     return readonly(target)
   }
  // go ----> step2
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers, // 注意传递
    mutableCollectionHandlers
  )
}

createReactiveObject函数如下:

function createReactiveObject(
   target: unknown,
   toProxy: WeakMap,
   toRaw: WeakMap,
   baseHandlers: ProxyHandler,
   collectionHandlers: ProxyHandler
 ) {
     // 判断target不是对象就 警告 并退出
   if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }
  // 通过原始数据 -> 响应数据的映射,获取响应数据
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // 如果原始数据本身就是个响应数据了,直接返回自身
  if (toRaw.has(target)) {
    return target
  }
  // 如果是不可观察的对象,则直接返回原对象
  if (!canObserve(target)) {
    return target
  }
  // 集合数据与(对象/数组) 两种数据的代理处理方式不同
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers
    : baseHandlers
  // 声明一个代理对象 ----> step3
  observed = new Proxy(target, handlers)
  // 两个weakMap 存target observed
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

通过上面源码创建proxy对象的大致流程是这样的:

①首先判断目标对象有没有被proxy响应式代理过,如果是那么直接返回对象。

②然后通过判断目标对象是否是[ Set, Map, WeakMap, WeakSet ]数据类型来选择是用collectionHandlers , 还是baseHandlers->就是reactive传进来的mutableHandlers作为proxy的hander对象。

③最后通过真正使用new proxy来创建一个observed ,然后通过rawToReactive reactiveToRaw 保存 target和observed键值对。

拦截器
在baseHandles基类处理对象中可以看到对set/get的拦截处理
(我们以对象类型为例,集合类型的handlers稍复杂点)
handlers如下,new Proxy(target, handles)的 handles就是下面这个对象

export const mutableHandlers = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

依赖收集
打开createGetter(false)看实现思路如下:
思路:当我们代理get获取到res时,判断res 是否是对象,如果是那么 继续reactive(res),可以说是一个递归

reactive(target) ->
createReactiveObject(target,handlers) ->
new Proxy(target, handlers) ->
createGetter(readonly) ->
get() -> res ->
isObject(res) ? reactive(res) : res

function createGetter(isReadonly: boolean) {
   // isReadonly 用来区分是否是只读响应式数据
   // receiver即是被创建出来的代理对象
   return function get(target: object, key: string | symbol, receiver: object) {
     // 获取原始数据的响应值
     const res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
    if (isRef(res)) {
      return res.value
    }
    // 收集依赖
    track(target, OperationTypes.GET, key)
    // 这里判断上面获取的res 是否是对象,如果是对象 则调用reactive并且传递的是获取到的res,
    // 则形成了递归
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

在vue2.0的时候。响应式是在初始化的时候就深层次递归处理了

但是与vue2.0不同的是,即便是深度响应式我们也只能在获取上一级get之后才能触发下一级的深度响应式。

这样做好处是:

  • 初始化的时候不用递归去处理对象,造成了不必要的性能开销。
  • 有一些没有用上的state,这里就不需要在深层次响应式处理。

先来看看track源码:

/* target 对象本身 ,key属性值  type 为 'GET' */
export function track(target: object, type: TrackOpTypes, key: unknown) {
  /* 当打印或者获取属性的时候 console.log(this.a) 是没有activeEffect的 当前返回值为0  */
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    /*  target -map-> depsMap  */
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    /* key : dep dep观察者 */
    depsMap.set(key, (dep = new Set()))
  }
   /* 当前activeEffect */
  if (!dep.has(activeEffect)) {
    /* dep添加 activeEffect */
    dep.add(activeEffect)
    /* 每个 activeEffect的deps 存放当前的dep */
    activeEffect.deps.push(dep)
  }
}

里面主要引入了两个概念 targetMap和 depsMap
track作用大致是,首先根据 proxy对象,获取存放deps的depsMap,然后通过访问的属性名key获取对应的dep,然后将当前激活的effect存入当前dep收集依赖。

主要作用
①找到与当前proxy 和 key对应的dep。
②dep与当前activeEffect建立联系,收集依赖。

为了方便理解,targetMap 和 depsMap的关系,下面我们用一个例子来说明:

例子:

{{ state.a }} {{ state.b }}

你可能感兴趣的:(vue3响应式--proxy)