Vue3.0响应式系统原理

284 阅读3分钟

内容输出来源:拉勾教育大前端高薪训练营

1.Vue3.0响应式特性

  • Proxy对象实现属性监听
  • 多层属性嵌套,在访问属性过程中处理下一级属性
  • 默认监听动态添加的属性
  • 默认监听属性的删除操作
  • 默认监听数组索引和length属性
  • 可以作为单独的模块使用

2.核心方法

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

3.Proxy对象回顾

Proxy 与 Reflect 是 ES6 为了操作对象引入的 API 。更多关于Proxy和Reflect的内容

  • 在严格模式下,Proxy的set和deleteProperty中需要返回布尔类型的值得返回布尔类型的值,否则会报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'
}
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'
  • 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) // 如果不设置receiver,则this指向的是原本的对象obj,this.bar返回undefined,如果设置了receiver,this指向的是代理对象proxy,this.bar返回'value - bar'
  }
})
console.log(proxy.foo) // value - bar

4.自己实现reactive

  • 接收一个参数,判断这个参数是否是对象
  • 创建拦截器对象handler,设置get/set/deleteProperty
  • 返回Proxy对象
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (target,key) => hasOwnProperty.call(target, key)

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

  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      console.log('get', key)
      track(target, key) // track见下文
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    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, value)
        trigger(target, key) // 见下文trigger
      }
      return result
    },
    deleteProperty(target, key) {
      const hasKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hasKey && result) {
        // 触发更新
        console.log('delete', key)
        trigger(target, key) // 见下文trigger
      }
      return result
    }
  }

  return new Proxy(target, handler)
}

使用

<body>
  <div id="app"></div>
  <script type="module">
    import { reactive } from './reactivity/index.js'

    const obj = reactive({
      name: 'zs',
      age: 18
    })
    obj.name = 'lisi'
    delete obj.age
    console.log(obj)
  </script>
</body>

输出结果:

set name lisi delete age Proxy {name: “lisi”}

5.收集依赖

收集目标对象,目标对象的属性,属性对应的箭头函数

6.effect、track

let activeEffect = null
export function effect(callback) {
  activeEffect = callback
  callback() // 访问响应式对象的属性, 去收集依赖
  activeEffect = null
}

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)
}

7.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()
    })
  }
}

使用:

<body>
  <script type="module">
    import { reactive, effect } from './reactivity/index.js'

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

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

    product.price = 4000
    console.log(total) // 12000

    product.count = 1
    console.log(total) // 4000

  </script>
</body>

8.ref

reactive vs ref

  • ref可以把基本类型的数据,转换成响应式对象(获取数据时使用value属性,模板中使用可以省略value
  • ref返回的对象重新给value赋值成对象以后也是响应式的
  • reactive返回的属性,重新赋值丢失响应式
  • reactive返回的对象不可以解构
export function ref(raw) {
  // 判断raw是否是对象,如果是对象且是ref创建的对象则直接返回
  if (isObject(raw) && raw.__v_isRef) return
  
  let value = convert(raw) // 如果raw是普通对象,convert函数会调用reactive把raw转换成响应式对象
  const r = {
    __v_isRef: true,
    get value () {
      track(r, 'value')
      return value
    },
    set value (newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw) // 保证重新赋值后,返回的还是响应式的对象
        trigger(r, 'value')
      }
    }
  }
  return r
}

使用:

<body>
  <script type="module">
    import { reactive, effect, ref } from './reactivity/index.js'

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

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

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

    count.value = 1
    console.log(total) // 4000

  </script>
</body>

9. toRefs

export function toRefs(proxy) {
  let 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() {
      // 此处不再需要收集依赖因为proxy是reactive对象
      return proxy[key]
    },
    set value (newValue) {
      proxy[key] = newValue
      // 此处不再需要触发更新因为proxy是reactive对象
    }
  }
  return r
}

使用:

<body>
  <script type="module">
    import { reactive, effect, toRefs } from './reactivity/index.js'

    const usePrice = () => {
      const product = reactive({
        price: 5000,
        count: 3
      })
      return toRefs(product) // 如果直接返回product,解构之后的属性并非响应式的,所以调用toRefs将reactive对象的每个属性都转换成ref对象
    }

    const { price,  count } = usePrice()

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

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

    count.value = 1
    console.log(total) // 4000

  </script>
</body>

10.computed

export function computed(getter) {
  let result = ref()

  effect(() => (result.value = getter()))

  return result
}

使用:

<body>
  <script type="module">
    import { reactive, computed } from './reactivity/index.js'

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

    let total = computed(() => {
      return product.price * product.count
    })
    console.log(total.value) // 15000

    product.price = 4000
    console.log(total.value) // 12000

    product.count = 1
    console.log(total.value) // 4000

  </script>
</body>

备注:trigger/track/effect是底层函数,一般不用。使用computed代替effect的使用