VUE3学习第四天 ---深入响应式原理(一)

1,112 阅读8分钟

今天我们开始学习响应式原理,模拟实现一个toy响应式系统,深入的去了解vue3的响应式原理!

vue3响应式系统介绍

  • Proxy对象实现属性监听

    vue3响应式系统采用,Proxy对象实现。在初始化的时候不需要遍历所有的属性,再把属性通过Object.defineProperty转化成getter,setter;

  • 另外如果有多层属性嵌套的话,如果访问某个属性的时候,vue3才会递归处理下一层属性。所以vue3中响应式系统的性能要比vue2好

  • 默认监听动态添加属性

  • 默认监听属性的删除操作

  • 默认监听数组索引和length属性

  • 可以作为单独的模块使用

接下来的我们来自己实现vue3中响应式系统的核心函数, reactive/ref/toRefs/computed, effect, track, trigger. watch 和 watchEffect 是vue中runtime.core中实现的。watch函数的内部其实使用了一个叫做effect函数的底层函数.

proxy 对象

我们重点是来看接下来的两个小问题?对了解Vue响应式原理有影响

先准备一段代码

'use strict'; 
// 问题一: set 和 deleteProperty 中需要返回布尔类型的值
// 在严格模式下,如果返回 false 的话会出现 Type Error
const target = {
  foo: 'bar',
  bar: 'baz'
}
// Reflect.getPrototypeOf
// Object.getPrototypeOf
const proxy = new Proxy(target, {
  get (target, key, receiver) {
    // return target[key]
    return Reflect.get(target, key, receiver)
  },
  set (target, key, value, receiver) {
    // target[key] = value
    Reflect.set(target, key, value, receiver)
  },
  deleteProperty (target, key) {
    // delete target[key]
    Reflect.deleteProperty(target, key)
  }
})

Reflect是反射的意思,java,和c#里面也有反射,在代码运行期间用来获取或者设置对象中的成员。这里是借鉴java和c#中的反射,因为javascript语言特殊性,在代码运行期间我们可以随意给对象添加成员或者获取成员对象中的信息,所以在过去的时候javascript并没有反射。过去js会很随意的将方法挂载到全局对象中,比如以前getPrototypeOf就挂载在Object,现在有了反射对象后,反射对象中也有getprototypeOf属性。方法作用都一样,只是表达语义有问题。如果在Reflect中有对应的Object中的方法,则用Reflect中的方法。所以上面代码我们用Reflect的成员来获取对象信息。Vue3的源码中也使用的是Reflect。

问题一: set 和 deleteProperty 中需要返回布尔类型的值,而且在严格模式下,如果返回 false 的话会出现 Type Error。

拿set举例,比如我们给只读属性进行复制,则会赋值失败,并返回false上面代码我们对 set 和 deleteProperty都没有return,则这两个方法会默认返回undefined 则是false,所以在严格模式下,如果赋值了或者删除某个属性,则赋值代码会报TypeError(非严格模式下是不会报错的)

所以将上面代码的set 和 deleteProperty进行修改

···
// 添加return,保证他们返回true
set (target, key, value, receiver) {
    // target[key] = value
    return Reflect.set(target, key, value, receiver)
  },
  deleteProperty (target, key) {
    // delete target[key]
    return Reflect.deleteProperty(target, key)
  }

问题二: Proxy 和 Relect 中使用 receiver

Proxy 中receiver: Proxy 或者继承 Proxy的对象

Reflect 中receiver: 如果target 对象设置了 getter,getter中的this指向就是receiver

看下面代码


const obj = {
    get foo() {
        console.log(this)
        // 如果没有传递receiver的时候 这个this指向的是obj
        // 如果。。。。;这个this指向的就是obj的代理对象
        return this.bar
    }
}

const proxy = new Proxy(obj, {
    get (target, key, receiver) {
        if(key == 'bar') {
            return 'value- bar'
        }
        return Reflect.get(target, key);
        return Reflect.get(target, key, receiver)
    }

})
console.log(proxy.foo)

在vue3的代码中,在获取或者设置值的时候,都会传递receiver这个参数,防止类似的意外发生(this指向原对象)。

明白了两个相关问题,我们认识一下vue3中的响应式核心函数

reactive

分析:

  • 接收一个参数,判断这参数是否是对象,如果不是的话直接返回,(提取函数isObject)reactive只能将对象转化成响应式对象,这是和ref不同的地方
  • 然后来创建拦截器对象handler,里面包含get/set/deleteProperty处理对象的方法。提取(handler对象有三个成员方法,get set deleteProperty)
  • 创建并返回 Proxy对象

代码实现:


const isObject = (val) => val !== null && typeof val === 'object'

export function reactive (target) {
  if(!isObject(target)) return target
  
  const handler = {
      get (target, key, receiver) {
      },
      set (target, key, value, receiver) {
      },
      deleteProperty(target, key) {}
      
  }
  
  return new Proxy(target, handler)
}

根据分析,我们先实现reactive函数的一个基本架构,现有一个整体,接下来在一个一个实现里面的主要细节

  1. get 我们会在get方法中收集依赖,并且返回target对应的值. 这里面有个问题,如果是key的值也是一个对象的话,我们还是要将这个对象调用reactive方法,将其转化成响应式对象,所以封装一个方法convert
const convert = target => isObject(target) ? reactive(target) : target;
get (target, key, receiver) {
    // 1. 收集依赖
    
    console.log('get', key)
    // 如果key的值也是对象,则需要把这个值也需要转化成响应式对象
    const result = Reflect.get(target, key, receiver)
    // 2 需要返回target对象中key的值
    return convert(result)
}

  1. set

vue3会在set中调用触发值钩子,完成dom更新,注意小问题,set方法一定要返回布尔值

set (target, key, value, receiver) {
   const oldValue = Reflect.get(target, key, receiver)
   let result = true
   if(oldValue !== value) {
       result = Reflect.set(target, key, value, receiver)
       // 触发更新
       console.log('set', key, vlaue)
   }
   // set方法中一定要返回布尔值
   return result
},
  1. deleteProperty 该方法一样,会改变对象的值,所以也要触发更新,并且也需要返回一个布尔值
// 我们封装两个辅助方法
const hasOwnProperty = Object.prototype.hasOwnproperty
// 我们这里需要判断target对象有这个key值
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
deleteProperty(target, key) {
    const hasKey = hasOwn(target, key)
    const result = Reflect.deleteProperty(target, key)
    // 触发更新
    return result
}

收集依赖

看下这段代码:vue3例子

import {reactive, effect} from 'vue3'

const product = reactive({
    name: 'iPhone',
    price: 5000,
    count: 3
})

let total = 0

effect(() => {
    total = product.price * product.count
})

console.log(total)

product.price = 4000
console.log(total)

product.count = 1
console.log(total)

关注effect函数,在页面首次加载的时候首次会执行effect函数,这时候effect里面的函数,这个函数里面访问了product,这个product是reactive代理的响应式对象,当被price和count属性时,则会调用get方法,get 方法中就会去收集依赖,收集依赖的过程其实就是将对应的属性和effect里面的回调函数进行存储,而属性又和对象相关,首相会存储target目标对象,然后再存储target的属性,然后把effect里面的对应的回调函数存储起来。在触发对应属性更新的时候回调用这个函数。

接下来给price赋值的化,会调用代理对象的set方法,在set方法里面触发更新(其实就是找到依赖收集过程中,存储对应的effect的回调方法,找到这个函数后悔立即执行)

这是一个收集依赖和触发更新的简单过程,希望有所理解!!!

加入一张图熟悉熟悉。。。、 image.png 在依赖收集的过程中会创建三个集合,

  • targetMap,new WeakMap
    • 用来记录target对象和一个字典(depsMap)
    • key: tagert目标对象弱引用,当目标对象失去的时候可以销毁
    • value:depsMap 字典
  • depsMap,new Map()
    • key :目标对象的属性名称
    • value: dep
  • dep new Set() value值为effect函数,一个属性可能对应对个effect函数,

将来触发更新的时候我们目标对象属性找到对应的effect函数,然后执行,等一下我们要实现的收集依赖的track函数它内部首先要根据当前的targetMap来找到depsMap,如果没有找到的话要给当前对象创建一个depsMap并添加到targetMap中,如果找到了再根据当前属性的depsMap中找到当前对呀的dep,dep里面存储的是effect函数,如果没有找到对应的effect,则创建dep并保存在depsMap中。如果找到当前对象属性的dep集合,就把当前的effect放入集合中。这就是收集依赖的思路

实现收集依赖的功能,effect && track

  1. effect

接收一个函数为参数,方法里面执行该函数。这里还需要将callback记录起来,方便track访问。

let activeEffect = null
export function effect (callback) {
    activeEffect = callback;
    callback()
    activeEffect = null
}
  1. track

这个方法要把target存储到targetMap中,这个targetMap在trigger更新时也要使用所以定义到模块全局中去

    let targetMap = new WeakMap();
    
    export function track (target, key) {
        if(!activeEffect) return // 没有需要收集的依赖
        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(activeEffect)
    }

这个方法 就可以放大reactive中的get中使用

get(target, key, receiver) {
        track(target, key)// 1. 收集依赖
        // 这里调用
        console.log("get", key);
        const result = Reflect.get(target, key, receiver);
        return convert(result);
      },

这样就完成了我们的依赖收集

实现触发更新 --- trigger

这个过程后刚好跟trigger的过程刚好相反

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

然后在set 和 deleteProperty中调用trigger

set(target, key, value, receiver) {
        const oldValue = Reflect.get(target, key, receiver);
        let result = true;
        if (oldValue !== value) {
          result = Reflect.set(target, key, value, receiver);
          // 触发更新
          trigger()
        }
        return result;
      },
      deleteProperty(target, key) {
        const hasKey = hasOwn(target, key);
        const result = Reflect.deleteProperty(target, key);
        // 触发更新
        trigger() //
        return result;
      },

总结: 今天就学到这里,实现了reactive对象响应式原理的实现。 reactive函数,判断是否是对象,不是对象直接返回,是对象执行函数,有一个handler类,有三个方法 set get deleteProperty

  • effect 作为响应式回调函数的调用

  • track 进行依赖收集

  • trigger 触发更新

在设置值的set和deleteProperty里面触发更新,在get里面进行依赖收集,每次取值都有effect产生并收集,完成对象的响应式原理的实现。

下次预告实现 ref 和 toRefs,computed

源代码: https://github.com/zelixag/vue3-learn/tree/master/reactive