Vue3.0的响应式系统原理的简单分析

661 阅读7分钟

一、介绍

1. Vue3.0响应式特点

  • Proxy对象实现属性监听
  • 多层属性嵌套,只有在访问对应层级属性的时候才会处理对应层级的属性
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和length属性
  • 可以作为单独的模块使用

2. 核心函数

  • reactive/ref/toRefs/computed
  • effect
  • track
  • trigger 注:trigger/track/effct是低层的函数,一般不用,使用computed代替effect的使用

二、Proxy对象回顾

1. 在严格模式下,Proxy的函数必须返回布尔类型的值,否则会报TypeError

Uncaught TypeError: ‘set’ on proxy: trap returned falsish for property ‘foo’

'use strict' // 使用严格模式
// 问题1: set和deleteProperty中需要返回布尔类型的值
// 严格模式下,如果返回false的话,会出现TypeError的异常
const target = {
  foo: 'xxx',
  bar: 'yyy'
}
// 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
    return Reflect.set(target, key, value, receiver) // 这里得写return
  },
  deleteProperty(target, key) {
    // delete target[key]
    return Reflect.deleteProperty(target, key) // 这里得写return
  }
})

proxy.foo = 'zzz'

2. 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'
    }
    return Reflect.get(target, key, receiver) // 执行this.bar的时候,this指向代理对象,也就是获取target.bar
  }
})
console.log(proxy.foo) // value - bar

如果return Reflect.get(target, key, receiver)写成return Reflect.get(target, key)的话,则响应式属性foo里面的this还是指向原本的对象obj,this.bar就是undefined,而传入了receiver之后,响应式属性里的this就指向新的响应式对象proxy,this.bar返回value - bar。

三、reactive

  • 接受一个参数,判断这个参数是否是对象
  • 创建拦截器对象handler,设置get/set/deleteProperty
  • 返回Proxy对象

自己创建一个js文件实现reactive

// 判断target是否是为对象
const isObject = (val) => {
  if (val !== null && typeof val === 'object') {
    return true
  } else {
    return false
  }
}
// 如果result依然是个对象,继续递归调用reactive函数
const convert = (result) => {
  // 如果result依然是个对象
  if (isObject(result)) {
    // 继续递归调用reactive函数
    reactive(result)
  } else {
    return result
  }
}
// reactive()函数
export function reactive(target) {
  // 判断target是否是为对象,不是直接原样返回
  if (!isObject(target)) return target
  // target是对象,调用Proxy的的getter和setter方法,把对象变成响应式对象,返回
  return new Proxy(target, {
    get(target, prop, receiver) {
      // 调用tarck方法去收集依赖
      track(target, prop)
      
      // Reflect.get的返回值: 如果属性key存在,则返回属性key对应的值value;否则返回 undefined
      const result = Reflect.get(target, prop, receiver)
      // 如果result依然是个对象,继续递归调用reactive函数
      return convert(result)
    },
    set(target, prop, value, receiver) {
      const oldValue = Reflect.get(target, prop, receiver)
      let result = true
      if (oldValue !== value) {
        // Reflect.set的返回值: 如果设置属性成功,则返回 true;否则返回false
        // return Reflect.set(target, prop, value, receiver)
        result = Reflect.set(target, prop, value, receiver)
        //调用trigger函数触发更新操作
        trigger(target, prop)
      }
      return result
    },
    deleteProperty(target, propName) {
      //判断对象内是否具有指定的属性
      if (Object.keys(target).indexOf(propName) !== -1) {
        const result = Reflect.deleteProperty(target, propName)
        if (result) {
          //调用trigger函数触发更新操作
          trigger(target, propName)
        }
        return result
      }
    },
  })
}

自己创建一个html文件使用自己创建的reactive

<body>
  <script type="module">
    import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js''
    const obj = reactive({
      name: 'zs',
      age: 18
    })
    obj.name = 'lisi'
    delete obj.age
    console.log(obj)
  </script>
</body>

四、收集依赖 effect、track

自己创建一个js文件实现effect、track

// effect函数 是用户来调用的
let activeEffect = null
export function effect(callback) {
  activeEffect = callback // 用户调用effect函数的时候,传入的回调函数
  // 执行回调函数,通过函数内的数据得知,是要去获取数据的值,即要去执行Proxy中的get方法
  // 然后在get方法中,调用去trak方法去收集依赖
  // trak方法中把activeEffect函数(即activeEffect = callback 用户传入的回调函数)保存起来了
  callback() 
  activeEffect = null // 防止内存溢出,执行完callback后,把activeEffect值清空
}

// track函数 :用来收集依赖
// target: 目标对象  propName:目标对象里的属性名
let targetMap = new WeakMap()
export function track(target, propName) {
  if (!activeEffect) return
  // 从targetMap中找到对应的target对象
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 向targetMap中新增一个target对象,值为Map
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }
  console.log('targetMap:', targetMap)
  // 如果depsMap中有值,根据属性名字,找到dep
  
  let dep = depsMap.get(propName)
  if (!dep) {
    dep = new Set()
    depsMap.set(propName, dep)
  }
  console.log('depsMap:', depsMap)
  // 向dep内添加effect方法的回调函数
  dep.add(activeEffect)
  console.log('dep:', dep)
}

自己创建一个html文件使用自己创建的effect

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

<script type="module"> 
  import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js'
  // 测试effect函数
  const obj = reactive({
    name:'iphone',
    price:5000,
    count:3,
  })
  let total = 0 
  effect(()=>{
    total = obj.price *  obj.count
  })
  console.log(total)
  
  obj.price = 4000
  console.log(total)

  obj.count = 1
  console.log(total)
  
</script>

五、trigger

自己创建一个js文件实现trigger

// effect函数 是用户来调用的
let activeEffect = null
export function effect(callback) {
  activeEffect = callback // 用户调用effect函数的时候,传入的回调函数
  // 执行回调函数,通过函数内的数据得知,是要去获取数据的值,即要去执行Proxy中的get方法
  // 然后在get方法中,调用去trak方法去收集依赖
  // trak方法中把activeEffect函数(即activeEffect = callback 用户传入的回调函数)保存起来了
  callback() 
  activeEffect = null // 防止内存溢出,执行完callback后,把activeEffect值清空
}

// track函数 :用来收集依赖
// target: 目标对象  propName:目标对象里的属性名
let targetMap = new WeakMap()
export function track(target, propName) {
  if (!activeEffect) return
  // 从targetMap中找到对应的target对象
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    // 向targetMap中新增一个target对象,值为Map
    depsMap = new Map()
    targetMap.set(target, depsMap)

    // targetMap.set(target, (depsMap = new Map()))
  }
  console.log('targetMap:', targetMap)
  // 如果depsMap中有值,根据属性名字,找到effect方法的回调函数
  let dep = depsMap.get(propName)
  if (!dep) {
    dep = new Set()
    depsMap.set(propName, dep)

    // depsMap.set(propName, (dep = new Set()))
  }
  console.log('depsMap:', depsMap)
  // 向dep内添加effect方法的回调函数
  dep.add(activeEffect)
  console.log('dep:', dep)
}

// trigger函数:更新响应式数据
export function trigger(target, propName) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(propName)
  if (dep) {
    dep.forEach((effect) => {
      effect()
    })
  }
}

自己创建一个html文件使用自己创建的trigger

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

<script type="module"> 
  import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js'
  // 测试effect函数和trigger函数
  // trigger不需要导入,响应式数据更改后,自动在reactive函数中的set方法内执行
  const obj = reactive({
    name:'iphone',
    price:5000,
    count:3,
  })
  let total = 0 
  effect(()=>{
    total = obj.price *  obj.count
  })
  console.log(total)
  
  obj.price = 4000 // trigger不需要导入,响应式数据更改后,自动在reactive函数中的set方法内执行
  console.log(total)

  obj.count = 1 // trigger不需要导入,响应式数据更改后,自动在reactive函数中的set方法内执行
  console.log(total)
  
</script>

六、ref

reactive 和 ref 的对比

  • ref可以把基本数据类型数据转换成响应式对象
  • ref返回的对象,重新赋值成对象也是响应式的
  • reactive返回的对象,重新赋值丢失响应式
  • reactive返回的对象不可解构

自己创建一个js文件实现ref

// ref函数
export function ref(raw) {
  // 判断 raw 是否是 ref 创建的对象,如果是的话,直接返回
  if (isObject(raw) && raw.__v_isRef) return

  // 调用convert()函数把传入的数据,变成响应式对象
  let value = convert(raw)
  console.log(value)

  // ref 函数自身,最终要返回一个对象
  const r = {
    __v_isRef: true, // 对象是ref创建的对象的一个标识
    get value() {
      // 调用track函数收集依赖
      track(r, 'value')
      return value
    },
    set value(newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        // 调用trigger函数触发更新
        trigger(r, 'value')
      }
    },
  }
  return r
}

自己创建一个html文件使用自己创建的ref

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

<script type="module"> 
  import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js'
  // 测试ref函数
  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) 
  
</script>

七、toRefs

自己创建一个js文件实现toRefs

// roRefs函数:把reactive函数创建的相应是对象,变成ref类型的响应式对象,同时可以解构
// proxy 是 reactive函数创建后,生成的代理对象
export function toRefs(proxy) {
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

function toProxyRef(proxy, key) {
  const r = {
    __v_isRef: true,
    get value() {
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    },
  }
  return r
}

自己创建一个html文件使用自己创建的toRefs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

<script type="module"> 
  import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js'
  // 测试toRefs函数
  const reactiveFn = ()=>{
    const obj = reactive({
      name:'iphone',
      price:5000,
      count:3,
    })
    return  toRefs(obj)
  }
  const {price,count} = reactiveFn()

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

  price.value = 4000
  console.log(total)

  count.value = 1
  console.log(total) 
  console.log(total,'00000000000000000000000') 
  
</script>

八、computed

自己创建一个js文件实现computed

// computed函数
export function computed(getterCallBack) {
  const result = ref() // 创建一个ref类型的响应式对象

  // 调用 effect函数 要求传入一个函数作为参数
  // getterCallBack 是computed函数调用的时候,用户传递的函数
  // 把getterCallBack函数的返回值,赋值给result
  effect(() => (result.value = getterCallBack()))

  return result
}


自己创建一个html文件使用自己创建的computed

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  
</body>
</html>

<script type="module"> 
  import {reactive,effect,ref,toRefs,computed} from './js/myIndex.js'
  // 测试computed函数
  const reactiveFn = ()=>{
    const obj = reactive({
      name:'iphone',
      price:5000,
      count:3,
    })
    return  toRefs(obj)
  }
  const {price,count} = reactiveFn()

  const total = computed(()=>{
     return price.value *  count.value
  })
  console.log(total.value)

  price.value = 4000
  console.log(total.value)

  count.value = 1
  console.log(total.value) 
  console.log(total,'computed') 
  
</script>