Vue3 响应式原理的深度剖析与实战

232 阅读6分钟

一、什么是响应式

在开始响应式原理与源码解析之前,需要先了解一下什么是响应式?首先明确一个概念:响应式是一个过程,它有两个参与方: 触发方:数据, 响应方:引用数据的函数;当数据发生改变时,引用数据的函数会自动重新执行,例如,视图渲染中使用了数据,数据改变后,视图也会自动更新,这就完成了一个响应的过程。

Vue2.x的响应式

  • 实现原理:
  • 对象类型:通过``Object.defineProperty()``对属性的读取、修改进行拦截(数据劫持)。
  • 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
  • 存在问题:
  • 新增属性、删除属性, 界面不会更新。
  • 直接通过下标修改数组, 界面不会自动更新。

Vue3.0的响应式

  • 实现原理:
  • 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
  • 通过Reflect(反射): 对源对象的属性进行操作。
  • MDN文档中描述的Proxy与Reflect:
new Proxy(data, {
    // 拦截读取属性值
    get (target, prop) {
        return Reflect.get(target, prop)
    },
    // 拦截设置属性值或添加新属性
    set (target, prop, value) {
        return Reflect.set(target, prop, value)
    },
    // 拦截删除属性
    deleteProperty (target, prop) {
        return Reflect.deleteProperty(target, prop)
    }
})

proxy.name = 'tom'   

二. Proxy 与 Reflect

Proxy

Proxy: 代理,顾名思义主要用于为对象创建一个代理,从而实现对对象基本操作的拦截和自定义。可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。基本语法:

let proxy = new Proxy(target, handler);
  • target: 需要拦截的目标对象
  • handler: 也是一个对象,用来定制拦截行为 举个例子:
const obj = {
    name: 'John',
    age: 16
}

const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);

这里objProxyhandler为空,则直接指向被代理对象,并且代理对象与数据源对象并不全等.如果需要更加灵活的拦截对象的操作,就需要在handler中添加对应的属性。例如:

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return target[key]
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        target[key] = value
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return delete target[key]
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

上面的例子,我们在捕获器中定义了set()get()deleteProperty()属性,通过对proxy的操作实现了对obj的操作拦截。这些属性的触发方法有如下参数: * target —— 是目标对象,该对象被作为第一个参数传递给new Proxy

  • key —— 目标属性名称
  • value —— 目标属性的值
  • receiver —— 指向的是当前操作 正确的上下文。如果目标属性是一个 getter 访问器属性,则 receiver 就是本次读取属性所指向的 this 对象。通常,receiver这就是 proxy 对象本身,但是如果我们从 proxy 继承,则receiver指的是从该 proxy 继承的对象

Reflect

Reflect: 反射,就是将代理的内容反射出去。ReflectProxy一样,也是 ES6 为了操作对象而提供的新 API。它提供拦截JavaScript操作的方法,这些方法与Proxy handlers 提供的的方法是一一对应的,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。还是上面的例子

const obj = {
    name: 'John',
    age: 16
}

const handler = {
    get(target, key, receiver) {
        console.log(`获取对象属性${key}值`)
        return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
        console.log(`设置对象属性${key}值`)
        return Reflect.set(target, key, value, receiver)
    },
    deleteProperty(target, key) {
        console.log(`删除对象属性${key}值`)
        return Reflect.deleteProperty(target, key)
    },
}

const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)

上面的例子中 Reflect.get()代替target[key]操作 Reflect.set()代替target[key] = value操作 * Reflect.deleteProperty()代替delete target[key]操作 。Reflect的作用在于提供了一种更加安全和易于管理的方式来执行底层操作,而不是直接操作对象本身。

三、简单实现reactive函数

// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)

 function reactive(target) {
    // 首先先判断是否为对象
    if (!isObject(target)) return target

    const handler = {
        get(target, key, receiver) {
            console.log(`获取对象属性${key}值`)
            // ... 这里还需要收集依赖,先空着

            const result = Reflect.get(target, key, receiver)
            // 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
            if (isObject(result)) {
                return reactive(result)
            }
            return result
        },

        set(target, key, value, receiver) {
            console.log(`设置对象属性${key}值`)

            // 首先先获取旧值
            const oldValue = Reflect.get(target, key, reactive)

            // set 是需要返回 布尔值的
            let result = true
            // 判断新值和旧值是否一样来决定是否更新setter
            if (oldValue !== value) {
                result = Reflect.set(target, key, value, receiver)
                // 更新操作 等下再补
            }
            return result
        },

        deleteProperty(target, key) {
            console.log(`删除对象属性${key}值`)

            // 先判断是否有key
            const hadKey = hasOwn(target, key)
            const result = Reflect.deleteProperty(target, key)

            if (hadKey && result) {
                // 更新操作 等下再补
            }

            return result
        },
    }
    return new Proxy(target, handler)
}
const obj = {
                name: '小浪',
                age: 22,
                test: {
                    test1: {
                        test2: 21,
                    },
                },
            }

const proxy = reactive(obj)
console.log(proxy.age)
proxy.test.test1.test2 = 22
console.log(delete proxy.age)

四、收集依赖/触发更新

在 Proxy 第二个参数 handler 中,拦截各种取值、赋值操作,依托 track 和 trigger 两个函数进行依赖收集和派发更新。

track 用来在读取时收集依赖。

trigger 用来在更新时触发依赖。

image.png

track

// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()

function track(target, key) {
    // 如果当前没有effect就不执行追踪
    if (!activeEffect) return
    // 获取当前对象的依赖图
    let depsMap = targetMap.get(target)
    // 不存在就新建
    if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()))
    }
    // 根据key 从 依赖图 里获取到到 effect 集合
    let dep = depsMap.get(key)
    // 不存在就新建
    if (!dep) {
        depsMap.set(key, (dep = new Set()))
    }
    // 如果当前effectc 不存在,才注册到 dep里
    if (!dep.has(activeEffect)) {
        dep.add(activeEffect)
    }
}

target 是原对象;

key 是指本次访问的是数据中的哪个 key,比如下文例子中收集依赖的 key 就是 count

最后添加到hander 里 get 中

get(target, key, receiver) {
    // ...
    // 收集依赖
    track(target, key)

   	// ...
},

effect函数:在Vue3中,副作用函数(Effect Function)通常指的是那些依赖于响应式数据并在数据变化时重新执行的函数。我们使用effect函数来创建副作用函数。effect函数接受一个函数作为参数,并在该函数内部访问响应式状态时收集依赖,从而创建一个响应式的副作用。

依赖地图:

首先全局会存在一个 targetMap,它用来建立 数据 -> 依赖 的映射,它是一个 WeakMap 数据结构。而 targetMap 通过数据 target,可以获取到 depsMap,它用来存放这个数据对应的所有响应式依赖。depsMap 的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数。

例如对于下面这个例子的依赖关系:

image.png

let product=reactive({ price:5,quantity:2})
let total=0
let effect=()=>{
    total =product.price *product.quantity
}
effect()
console.log(total)
product.quantity=3
console.log(total)
  1. 全局的 targetMap 是:
targetMap: {
   { price:5,quantity:2}: dep    
}

2. dep 则是

dep: {
  price: Set { effection },
  quantity: Set { effection }
}

这样一层层的下去,就可以通过 target 找到 count 对应的更新函数 effection 了。

trigger

// trigger 响应式触发
export function trigger(target, key) {
    // 拿到依赖图
    const depsMap = targetMap.get(target)
    if (!depsMap) {
        // 没有被追踪,直接 return
        return
    }
    // 拿到了视图渲染effect 就可以进行排队更新 effect 了
    const dep = depsMap.get(key)

    // 遍历 dep 集合执行里面 effect 副作用方法
    if (dep) {
        dep.forEach(effect => {
            effect()
        })
    }
}

最后添加到hander 的 set 和 deleteProperty 中

set(target, key, value, receiver) {
    // ...
    if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)
        trigger(target, key)
    }
	// ...
},

deleteProperty(target, key) {
    // ...
    if (hadKey && result) {
        // 更新操作
        trigger(target, key)
    }
	// ...
}

五、简化版Vue3响应式实现

const targetMap = new WeakMap();
let total = 0;
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => { 
  let depsMap = targetMap.get(target);
  if(!depsMap){
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if(!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(effect);
}

const trigger = (target, key) => {
  const depsMap = targetMap.get(target);
  if(!depsMap) return;
    let dep = depsMap.get(key);
  if(dep) {
    dep.forEach( effect => effect() );
  }
};

const reactive = (target) => {
  const handler = {
    get(target, key, receiver){
      console.log('正在读取的数据:',key);
      const result = Reflect.get(target, key, receiver);
      track(target, key);  // 自动调用 track 方法收集依赖
      return result;
    },
    set(target, key, value, receiver){
      console.log('正在修改的数据:', key, ',值为:', value);
      const oldValue = target[key];
      const result = Reflect.set(target, key, value, receiver);
      if(oldValue != result){
         trigger(target, key);  // 自动调用 trigger 方法执行依赖
      }
      return result;
    }
  }
  
  return new Proxy(target, handler);
}

let product = reactive({price: 10, quantity: 2}); 
effect();
console.log(total); 
product.price = 20;
console.log(total); 

六、补充:reactive对比ref

  • 从定义数据角度对比:
  • ref用来定义:基本类型数据。
  • reactive用来定义:对象(或数组)类型数据。
  • 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过``reactive``转为代理对象。
  • 从原理角度对比:
  • ref通过`Object.defineProperty()```get````set``来实现响应式(数据劫持)。
  • reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
  • 从使用角度对比:
  • ref定义的数据:操作数据需要``.value``,读取数据时模板中直接读取不需要``.value``
  • reactive定义的数据:操作数据与读取数据:均不需要``.value``