Vue3.0响应式系统原理

Vue3

# 原理介绍

Vue.js3.0底层采用proxy对象实现属性监听,初始化的时候不需要遍历所有的属性通过defineProperty转化成getter和setter,如果有多层属性嵌套,在访问属性过程中处理下一级属性。所以其性能比Vue2.x的好。

Vue.js3.0的响应式系统:

  • 默认可以监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和length属性的修改
  • 还可以作为单独模块使用

下面我们要实现这些核心方法:

  • reactive/ref/toRefs/computed
  • effect
  • track
  • triiger

# proxy回顾

# 问题一

set 和 deleteProperty 中需要返回布尔类型的值

在严格模式下,如果返回 false 的话会出现 Type Error 的异常

PS: 使用esmodule模块默认开启了严格模式,

const target = {
  foo: 'xxx',
  bar: 'yyy'
}
// 创建一个proxy对象
const proxy = new Proxy(target, {
  // 访问
  // receiver 当前的proxy对象或者继承proxy的对象
  get (target, key, receiver) {
    // reflect 反射,获取对象成员
    return Reflect.get(target, key, receiver)
  },
  // 赋值
  set (target, key, value, receiver) {
    Reflect.set(target, key, value, receiver)
  },
  // 删除
  deleteProperty (target, key) {
    // 没有写return,就返回了undefined,会报类型错误
    Reflect.deleteProperty(target, key)
  }
})

proxy.foo = 'zzz'
// delete proxy.foo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

这个时候设置值和删除值就会报类型错误

error

不报错误需要这么改

const proxy = new Proxy(target, {
  // 访问
  get (target, key, receiver) {
    return Reflect.get(target, key, receiver)
  },
  // 赋值
  set (target, key, value, receiver) {
    // 设置成功返回true,设置失败返回false
    return Reflect.set(target, key, value, receiver)
  },
  // 删除
  deleteProperty (target, key) {
    // 也加上return
    return Reflect.deleteProperty(target, key)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

之后就不会报类型错误了。

# 问题二

Proxy 和 Reflect 中使用的 receiver

  • Proxy 中 receiver:Proxy 或者继承 Proxy 的对象
  • Reflect 中 receiver:如果 target 对象中设置了 getter,getter 中的 this 指向 receiver
const obj = {
  get foo() {
    // 当前对象
    console.log(this)
    return this.bar
  }
}

const proxy = new Proxy(obj, {
  get (target, key, receiver) {
    if (key === 'bar') { 
      return 'value - bar' 
    }
    // 这里没有设置receiver
    return Reflect.get(target, key)
  }
})
console.log(proxy.foo)

// undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

如果在proxy的get添加一个receiver

const obj = {
  get foo() {
    // 代理对象proxy
    console.log(this)
    return this.bar
  }
}

const proxy = new Proxy(obj, {
  get (target, key, receiver) {
    if (key === 'bar') { 
      return 'value - bar' 
    }
    // 添加了receiver
    return Reflect.get(target, key, receiver)
  }
})
console.log(proxy.foo)
// value - bar
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

所以这里都会使用receiver,防止意外发生

# reactive函数的实现

  • 接收一个参数,判断这个参数是否是对象,如果不是直接返回,reactive只能把对象转换成响应式对象,和ref不同
  • 创建拦截器对象 handler,设置 get/set/deleteProperty
  • 返回 Proxy 对象
  1. 第一步先搭架子
// 判断是否是对象
const isObject = val => val !== null && typeof val === 'object'

// 返回一个函数,接收一个参数target
export function reactive (target) {
  // 判断是否是对象,如果不是对象就直接返回
  if (!isObject(target)) return target

  // 定义拦截器对象,包括get、set、deleteProperty
  const handler = {
    // 接收三个参数
    get (target, key, receiver) {
      // 收集依赖
      console.log('get', key)
    },
    // 接收四个参数,多一个value
    set (target, key, value, receiver) {
      
    },
    // 删除:接收两个参数
    deleteProperty (target, key) {
      
    }
  }

  // 返回proxy对象,传入target和handler
  return new Proxy(target, handler)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  1. 在get中写收集依赖的函数
// 判断是否是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否是对象,如果是对象就调用reactive进行处理,如果不是就直接返回target
const convert = target => isObject(target) ? reactive(target) : target

const handler = {
    // 接收三个参数
    get (target, key, receiver) {
      // 收集依赖
      console.log('get', key)
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  1. 写set方法
export function reactive (target) {
  if (!isObject(target)) return target

  // 定义拦截器对象,包括get、set、deleteProperty
  const handler = {
    ...
    // 接收四个参数,多一个value
    set (target, key, value, receiver) {
      // 或者旧值的目的要判断新旧值是否相同
      const oldValue = Reflect.get(target, key, receiver)
      // 返回的值默认是true
      let result = true
      // 如果不相同设置新值
      if (oldValue !== value) {
        // 设置的值之后赋给result
        result = Reflect.set(target, key, value, receiver)
        // 触发更新
        console.log('set', key, value)
      }
      return result
    },
    ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
  1. 写deleteProperty方法
// 为了简化书写
const hasOwnProperty = Object.prototype.hasOwnProperty
// 用call改this为target,判断对象中是否有属性
const hasOwn = (target, key) => hasOwnProperty.call(target, key)

export function reactive (target) {
  if (!isObject(target)) return target

  // 定义拦截器对象,包括get、set、deleteProperty
  const handler = {
    ...
    // 删除:接收两个参数
    deleteProperty (target, key) {
      // 判断是否有该属性
      const hadKey = hasOwn(target, key)
      // 删除该属性,返回成功还是失败
      const result = Reflect.deleteProperty(target, key)
      // 有该属性且删除成功
      if (hadKey && result) {
        // 触发更新
        console.log('delete', key)
      }
      return result
    }
  }
  ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  1. 测试一下,创建一个index.html,将模块导入,可以看到get、set和deleteProperty的功能都能正常工作。
import { reactive } from './reactivity/index.js'
const obj = reactive({
  name: 'zs',
  age: 18
})
obj.name = 'lisi'
delete obj.age
console.log(obj)

// set name lisi
// delete age
// Proxy {name: "lisi"}
1
2
3
4
5
6
7
8
9
10
11
12

# 收集依赖

下面分析一下收集依赖的思路

# 举个例子

effect函数,定义的时候首次会执行,每次修改里面的响应式数据,函数就会再次调用。

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)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

effect函数中访问 product.price 和 product.count 里面的get方法收集依赖,记录target,target对应的属性,还有其对应的函数。

在赋值给price的时候,就触发其set方法,更新数据,找到其对应的函数,执行函数。

# 分析思路

下面看一张图: 在收集依赖的时候会创建三个集合:targetMap、depsMap和dep。

其中targetMap的作用:是用来记录目标对象和一个字典(即depsMap),targetMap使用的类型的WeakMap,弱引用的Map,这里的key就是target对象,因为是弱引用,当目标对象失去引用之后可以销毁。

depsMap的作用:这是字典Map类型,字典中的key是目标对象的属性名称,值是一个set集合dep。

dep的作用:set里面存储的元素不会重复,里面存储的就是effect函数,因为我们可以多次调用一个effect,在effect中访问同一个属性,那这个时候该属性会收集多次依赖,对应多个effect函数。

收集依赖数据结构

一会要实现的收集依赖的track函数:
其内部首先要根据targetMap对象来找到depsMap

  • 如果没有找到的话,就给当前对象创建一个depsMap并添加到targetMap中
  • 如果找到了再根据当前的属性来depsMap中找到对应的dep。

然后dep里存储的是effect函数

  • 如果没有找到为当前的属性创建dep并且存储到depsMap中
  • 如果找到dep集合就将effect存储到dep集合中。

# effect-track函数的实现

# 实现effect函数

// 定义一个变量记录callback,默认是null,track函数也可以访问这个变量
let activeEffect = null
// effect,接收一个参数callback
export function effect (callback) {
  activeEffect = callback
  callback() // 先执行一下callback,访问响应式对象属性,去收集依赖
  // 因为收集依赖,有嵌套属性是一个递归的过程
  activeEffect = null
}
1
2
3
4
5
6
7
8
9

# 实现track函数

// track要使用,trigger也要使用,所以定义到外面,targetMap是weakMap
let targetMap = new WeakMap()
// track,两个参数,目标对象target和key
export function track (target, key) {
  // 要保存activeEffect,如果为null,就直接return
  if (!activeEffect) return
  // 定义depsMap
  let depsMap = targetMap.get(target)
  // 如果没有,那么就创建一个新的depsMap并且添加到target中
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 根据属性查找dep对象
  let dep = depsMap.get(key)
  // dep如果不存在就创建一个新的dep并添加到depsMap中
  if (!dep) {
    depsMap.set(key, (dep = new Set()))
  }
  // 最后将activeEffect添加到dep中
  dep.add(activeEffect)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 在reactive中添加依赖

get (target, key, receiver) {
  // 收集依赖
  track(target, key)
  const result = Reflect.get(target, key, receiver)
  return convert(result)
},
1
2
3
4
5
6

# trigger函数的实现

trigger函数和track函数正好相反

// trigger函数,两个参数target和key
export function trigger (target, key) {
  // 要根据target去targetMap中找到depsMap
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  // depsMap中存的是键key和key对应的effect函数集合dep
  const dep = depsMap.get(key)
  // 如果存在就循环调用effect函数
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

然后要去reactive中进行调用

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(target, key)
  }
  return result
},
deleteProperty (target, key) {
  const hadKey = hasOwn(target, key)
  const result = Reflect.deleteProperty(target, key)
  if (hadKey && result) {
    // 触发更新
    trigger(target, key)
  }
  return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

打开浏览器找到之前的例子,运行结果相同。

# ref函数的实现

# 功能

ref接收一个参数:

  • 如果是对象,并且对象是ref创建的对象,直接返回
  • 如果是普通对象就用reactive创建并返回
  • 如果是原始值,就创建一个有value属性的对象返回。
// ref函数,接收一个参数raw
export function ref (raw) {
  // 判断 raw 是否是 ref 创建的对象,如果是的话直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  // 这里直接嗲用convert,判断是不是对象,是就用reactive包装
  let value = convert(raw)
  // 要创建一个ref对象,只有value属性
  const r = {
    // 标识属性
    __v_isRef: true,
    // value属性的getter
    get value () {
      // 收集依赖,r对象的value属性
      track(r, 'value')
      return value
    },
    // value属性的setter
    set value (newValue) {
      // 判断新值和旧值是否相等,如果不相等新值存储到raw中
      if (newValue !== value) {
        raw = newValue
        // 将新值进行reactive包装
        value = convert(raw)
        // 触发更新
        trigger(r, 'value')
      }
    }
  }
  return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

测试的时候,除了要导入reactive,effect还要导入ref函数

import { reactive, effect, ref } from './index.js'

const price = ref(5000)
const count = ref(3)

let total = 0 
effect(() => {
  total = price.value * count.value
})
console.log(total)

price.value = 4000
console.log(total)

count.value = 1
console.log(total)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# reactive vs ref

reactive ref
不能转基本数据类型 可以转成响应式对象
直接访问 需要访问其value属性
返回的对象重新赋值丢失响应式,不可以解构,如果要解构的话需要toRefs函数 返回的对象重新赋值成对象也是响应式的
如果一个对象成员很多的时候,ref并不方便,因为总要带着value属性 如果只有一两个响应式数据,使用ref比较方便,可以解构使用

# toRefs函数的实现

作用:接收一个reactive对象(即proxy对象),如果传入的不是proxy对象就直接返回,然后把传入对象的所有属性转换成类似ref返回的对象。

// toRefs函数,接收一个参数proxy对象
export function toRefs (proxy) {
  // 判断是不是proxy对象,reactive对象没有标识,所以这一步跳过
  // proxy如果是数组就创建一个新的数组,长度是proxy.length,否则就是空对象
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  // 遍历所有属性、或者数组的索引
  for (const key in proxy) {
    // 将每一个属性都转换成ref返回的对象
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

// 每个属性转换成ref返回的对象,接收proxy和key
function toProxyRef (proxy, key) {
  // ref函数内部实现
  const r = {
    // 标识
    __v_isRef: true,
    get value () {
      // 直接返回proxy的key,这里不需要收集依赖
      // 因为代理对象内部已经有收集依赖的了
      return proxy[key]
    },
    set value (newValue) {
      // 这里也不需要触发更新,proxy内部自己触发
      proxy[key] = newValue
    }
  }
  return r
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

这里如果对对象解构的话,里面每一个属性都是ref返回的对象,直接用value属性访问。

import { reactive, effect, toRefs } from './index.js'

function useProduct () {
  const product = reactive({
    name: 'iPhone',
    price: 5000,
    count: 3
  })
  
  // 将reactive返回的对象用toRefs包装一下
  return toRefs(product)
}

// 解构
const { price, count } = useProduct()

let total = 0 
effect(() => {
  // 这里使用value属性访问
  total = price.value * count.value
})
console.log(total)

// 使用value属性赋值
price.value = 4000
console.log(total)

count.value = 1
console.log(total)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# computed函数的实现

computed需要接收一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值,并且我们要监听函数内部使用的响应式数据的变化,最后把执行的结果返回。

// computed函数 接收一个参数getter
export function computed (getter) {
  // 创建一个对象,不传参数默认返回undefined
  const result = ref()

  // 通过effect监听响应式数据的变化,把getter的结果存储到result的value中
  effect(() => (result.value = getter()))

  // 将result返回
  return result
}
1
2
3
4
5
6
7
8
9
10
11

测试的时候将effect用computed替换

import { reactive, computed } from './index.js'

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

let total = computed(() => {
  return product.price * product.count
})

product.price = 4000
// 这里要用value属性访问
console.log(total.value)

product.count = 1
console.log(total.value)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
更新时间: 2022-01-10 09:52