vue3的响应式

本文详细介绍了Vue3中如何使用Proxy实现响应式,包括Proxy的has、get、set和deleteProperty等拦截器的用途。同时,阐述了Vue3响应式建立的两种方式:composition-api和options-api。深入讲解了响应式原理,包括reactive函数的处理逻辑和响应式数据变更时的effect、track及trigger机制。总结了Vue3响应式建立的三个阶段:初始化、get依赖收集和set派发更新。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一 proxy

1. 什么是proxy

proxy是es6的新特性,主要是通过handler对象中的拦截方法拦截目标对象target的某些行为(属性查找,赋值,枚举,函数调用等)

const proxy = new Proxy(target, handler)
/* target: 使用Proxy包装的目标对象(可以是任何类型的对象,如原生数组,函数,另外一个代理) */
/* handler: 通常以函数作为属性的对象,定义了在执行各种操作时代理proxy的行为 */ 

2. 为什么改用proxy

vue2使用object.defineProperty()来实现响应式,会有它的局限性,比如无法监听对属性的添加删除操作,无法监听数组基于下标的修改操作,不支持Map,Set,WeakMap,WeakSet等缺陷,改用proxy来实现响应式可以解决这些问题,并且更好的支持了typescript,但同时也意味着vue3将放弃对低版本浏览器的兼容(兼容版本ie11以上)

3. 几个proxy中hander对象的捕获器

1. handler.has(target, propKey):target是目标对象,propKey为拦截属性名,其作用是拦截判断target对象是否含有属性propKey的操作,对应Reflect.has(target, propKey)

const handler = {
    has(target, propKey){
        /* 操作 */
        return propKey in target
    }
}
const proxy = new Proxy(target, handler)

2. handler.get(target, propKey, receiver):target是目标对象,propKey为拦截属性名,receiver是proxy实例,返回读取的属性,作用是拦截对象属性的读取,对应Reflect.get(target, propertyKey, receiver)

const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : '没有此属性'
    }
}

const obj = new Proxy({}, handler)

obj.propA = 'A'
obj.propB = 'B'

console.log(obj.propA, obj.propB)   /* A B */
console.log(obj,propC)              /* 没有此属性 */

3. handler.set(target, propKey, value, receiver):target是目标对象,propKey为拦截属性名,value是新设置的属性值,receiver:是proxy实例,返回true表示操作成功,否则操作失败,作用是拦截对象的属性赋值操作,对应Reflect.set(obj, prop, value, receiver),当对象的属性writable为false时,该属性不能在拦截器中被修改

const handler = {
  set: function(obj, prop, value) {
    if (prop === 'propA') {
      /* 操作 可在这里抛出异常 */
      if (!Number.isInteger(value)) { 
        /* 如果propA不是整数 */
        throw new TypeError('The propA is not an integer')
      }
      if (value > 100) {  
        /* propA超过100 */
        throw new RangeError('The propA More than 100')
      }
    }
    obj[prop] = value // 表示成功
    return true
  }
}
const obj = new Proxy({}, handler)
obj.propA = 27
console.log(obj.propA)  // 27
obj.propA = 27.7        // 抛出异常: Uncaught TypeError: The propA is not an integer
obj.propA = 200         // 抛出异常: Uncaught TypeError: The propA More than 100

4. handler.deleteProperty(target, propKey):target是目标对象,propKey为拦截属性名,返回true表示成功,否则失败,作用是拦截删除target对象的propKey属性的操作,对应Reflect.delete(obj, prop),当属性是不可配置属性时,不能进行删除

const obj = { propA: 'A', propB:'B' }
const proxy = new Proxy(obj, {
  deleteProperty(target, prop) {
    console.log('删除属性:', target[prop])
    return delete target[prop]
  }
})
delete proxy.propA
console.log(obj) 
/*
    删除属性: propA
    { propB:'B' }
*/

二 vue3建立响应式

vue3建立响应式的方法有两种:第一种运用composition-api中的reactive直接构建响应式,composition-api的出现让我们可以直接用setup()函数来处理之前的大部分逻辑,同时也避免了this的使用,像data,watch,computed,生命周期函数都声明在setup函数中,这样就像react-hooks一样提升代码的复用率,逻辑性更强。第二种就是使用传统的 data(){ return{} } 的形式,vue3并没有放弃对vue2写法的支持,而是对vue2的写法完全兼容

1. composition-api

const { reactive , onMounted } = Vue
setup(){
    const data = reactive({
        propA: 0,
        propB: []
    })
    /* 生命周期mounted */
    onMounted(() => {
       console.log('mounted')
    })
    function addPropA() {
        data.propA ++
    }
    const addPropB = () => {
        data.propB.push('vue3')
    }
    return {
        data,
        addPropA,
        addPropB
    }
}

2. options-api

options-api的形式和vue2没有什么区别

export default {
    data() {
        return {
            propA: 0,
            propB: [] 
        }
    },
    mounted() {
        console.log('mounted')
    },
    methods: {
        addPropA(){
            this.propA ++
        },
        addPropB(){
           this.propB.push('vue3')
        }
    }
}

三 响应式原理

reactive()的作用主要是将目标转化为响应式的proxy实例,如果是嵌套的对象,会继续递归将子对象转为响应式对象,reactive()是向用户暴露的 API,它真正执行的是createReactiveObject()函数

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
      if (!isObject(target)) {
          {
              console.warn(`value cannot be made reactive: ${String(target)}`);
          }
          return target;
      }
      // target is already a Proxy, return it.
      // exception: calling readonly() on a reactive object
      if (target["__v_raw" /* RAW */] &&
          !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
          return target;
      }
      // target already has corresponding Proxy
      const existingProxy = proxyMap.get(target);
      if (existingProxy) {
          return existingProxy;
      }
      // only a whitelist of value types can be observed.
      const targetType = getTargetType(target);
      if (targetType === 0 /* INVALID */) {
          return target;
      }
      const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
      proxyMap.set(target, proxy);
      return proxy;
  }

这个函数的处理逻辑:

1. 如果 target 不是一个对象,返回 target

2. 如果 target 已经是 proxy 实例,返回 target

3. 如果 target 不是一个可观察的对象,返回 target

4. 生成 proxy 实例,在原始对象 target 上添加一个属性,属性只读为__v_isReadonly,否则为__v_isReactive,指向这个 proxy 实例,最后返回这个实例,添加这个属性就是为了在第 2 步做判断,防止对同一对象重复监听

vue3响应式的实现:

1. 通过effect声明依赖响应式数据的函数cb(如视图渲染render函数),并执行cb函数,执行过程中,会触发响应式数据getter

2. 在响应式数据getter中进行track依赖收集,建立数据和cb的映射关系存储于targetMap

3. 当变更响应式数据时触发triggter,然后根据targetMap找到关联的cb执行

手写vue3响应式

/* 建立响应式数据 */
function reactive(obj){}
 
/* 声明响应函数cb */
function effect(cb){}
 
/* 依赖收集:建立数据和cb映射关系 */
function track(target, key){}
 
/* 触发更新:根据映射关系执行cb */
function trigger(target, key){}

reactive

function reactive(obj) {
  if(!isObject(obj)) return obj
  const proxy = new Proxy(obj, {
    get(target, key, receiver) {
      const res = Reflect.get(target, key, receiver)
      // 依赖收集
      track(target, key)
      return reactive(res)
    },
    set(target, key, val, receiver) {
      const res = Reflect.set(target, key, val, receiver)
      // 触发更新
      trigger(target, key)
      return res
    },
    deleteProperty(target, key) {
      const res = Reflect.deleteProperty(target, key)
      // 触发更新
      trigger(target, key)
      return res
    },
  })
  return proxy
}

effect

const effectStack = []
function effect(cb) {
  // 对函数进行封装
  const rxEffect = function() {
    try {
      effectStack.push(rxEffect)
      return cb()
    } finally {
      effectStack.pop()
    }
  }
  // 最初要执行一次,进行最初的依赖收集
  rxEffect()
  return rxEffect
}

track

const targetMap = new WeakMap()
function track(target, key) {
  // 存入映射关系
  const effectFn = effectStack[effectStack.length - 1]
  if(effectFn) {
    let depsMap = targetMap.get(target)
    if(!depsMap){
      depsMap = new Map()
      targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if(!deps){
      deps = new Set()
      depsMap.set(key, deps)
    }
    deps.add(effectFn)
  }
}

trigger

function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if(depsMap) {
    const deps = depsMap.get(key)
    if(deps){
      deps.forEach(effect => effect())
    }
  }

四 总结

vue3的建立响应式大致可以分为三个阶段

1. 初始化阶段:通过组件初始化方法形成对应的proxy对象,然后形成一个负责进行渲染的effect

2. get依赖收集阶段:通过解析template,替换真实的data属性来触发get,调用track方法,通过proxy对象和key形成对应的deps,将负责渲染的effect存入到deps

3. set派发更新阶段:当我们使用obj[key] = value改变对象属性的时候,首先调用triggter方法,通过proxy对象和key找到对应的deps,然后执行依次effect()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值