Vue3.0响应式系统原理


Vue3.0响应式系统原理

1.响应式系统原理-介绍

接下来通过模拟Vue3的响应式系统来深入了解它内部的工作原理。先来回顾一下Vue3重写了响应式系统,和Vue2相比,Vue3的响应式系统底层采用proxy对象实现,在初始化的时候不需要遍历所有的属性,把属性通过defineProperty转换成gettersetter。另外如果有多层属性嵌套的话,只有访问某个属性的时候才会递归处理下一级的属性,所以Vue3中响应式系统的性能要比Vue2好。

Vue3的响应式系统默认可以监听动态添加的属性,还可以监听属性的删除操作以及数组的索引和length属性的修改操作。另外Vue3的响应式系统还可以作为一个模块单独使用。

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

接下来自己实现Vue3中显样式系统的核心函数,分别去实现之前使用过的reactive/ref/toRefs/computed的函数。watchwatchEffect是Vue3的runtime.core中实现的。
watch函数的内部其实使用了一个叫做effect的底层函数,我们会模拟实现effect函数以及Vue3中收集依赖和触发更新的函数tracktrigger

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

2.响应式系统原理-Proxy对象回顾

在模拟实现Vue3的响应式原理之前,先来回顾一下Proxy对象。重点来看两个小问题:

  • setdeleteProperty 中需要返回布尔类型的值,在严格模式下,如果返回 false 的话会出现 Type Error 的异常
  • ProxyReflect 中使用的 receiver

Refelct MDN文档

proxy.html

<!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>
  <script>
    'use strict'
    // 问题1: set 和 deleteProperty 中需要返回布尔类型的值
    //        在严格模式下,如果返回 false 的话会出现 Type Error 的异常
    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)
      },
      deleteProperty (target, key) {
        // delete target[key]
        return Reflect.deleteProperty(target, key)
      }
    })

    proxy.foo = 'zzz'
    // delete proxy.foo
    console.log(proxy.foo)
  </script>
</body>
</html>

先来看一下proxy对象的使用,这里先定义了一个对象target,然后通过proxy代理target对象。在创建proxy对象的时候,传入了第二个参数,它是一个对象,可以叫做处理器或者监听器。那这里的getsetdeleteProperty可以分别监听对属性的访问赋值以及删除操作。getset这两个方法,它最后有个参数叫做receiver,在这里代表的是当前的proxy对象或者继承自proxy的对象。

在获取或设置值的时候使用的Reflect,分别调用了Reflect.getReflect.set这两个方法。Reflect是反射的意思,是ES6中新增的成员。Java和C#上里也有反射,是在代码运行期间用来获取或设置对象中的成员。这里是借鉴Java或C#下中的反射,因为Javascript的特殊性,代码在运行期间可以随意的去给对象增加成员或者获取对象中成员的信息,所以在ES6之前,Javascript中并没有反射。

过去Javascript很随意的把一些方法挂载到Object中,比如Object.getPrototypeOf()这个方法,Reflect中也有对应的方法Reflect.getPrototypeOf(),方法的作用是一样的,只是表达语义的问题。如果在Reflect中有对应的Object中的方法,那建议使用Reflect中的方法,所以上面都是使用Reflect来操作对象中的成员。Vue3的源码中也使用的是Reflect

首先来说第一个问题,setdeleteProperty这两个方法中都需要返回一个布尔类型,在严格模式下如果返回false会报type的错误。

set方法中,如果我们给只读属性赋值,那这个时候会设置失败,返回false。当前代码中的setdeleteProperty都没有写return,默认返回的是undefined,转换成布尔类型的话是false。所以在最后去给属性赋值或者删除属性的时候都会报类型错误。注意有一个前提是在严格模式下,非严格模式下是不会报错的。

找到代理对象中的set方法,在set方法中可以直接返回return Reflect.set(target, key, value, receiver),这个方法设置成功之后会返回true,设置失败之后会返回false,那下面deleteProperty也是一样的,return Reflect.deleteProperty(target, key),然后再来打开浏览器。刷新一下浏览器,这时候设置成功之后不会报错。

<!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>
  <script>
    'use strict'
    // 问题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)
      }
    })
    console.log(proxy.foo)
  </script>
</body>
</html>

第二个问题和receiver相关,来看一下receiver,在proxyhandler对象的getset中会接收一个receiver,它是当前创建的proxy对象或者继承自当前proxy的子对象。Reflect中调用setget的时候也传入了一个receiver对象。

解释一下receiver对象,如果target对象中设置了getter,那么getter中的this指向的就是这个receiver。那么通过代码来解释一下,我们这里有一个对象,它有一个foo的属性,只有get。在get中打印了this,并且返回了this.bar半,当前对象中并没有定义bar属性。

再来看这个代理对象,这里监听了get,也就是下边在访问proxy.foo的时候,首先会去执行obj这个对象中的foo,它对应get,然后打印this,再来访问this.bar,当Reflect.get没有设置receiver的时候,此处的this就是obj对象。

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)
  }
})
console.log(proxy.foo)

来给Reflect.get添加receiver。此处的receiverproxy对象中的get方法中的receiver,它是当前的proxy对象。访问target对象中的get的时候,会让这里的this指向receiver对象,也就是代理对象。那此处的this就是代理对象,当访问this.bar的时候,会执行代理对象的get方法。

3.响应式系统原理-reactive

reactive

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

接下来我们来实现响应式原理中的第一个函数。目录结构如下:

index.js中编写,reactive函数接收一个参数target,它里面首先要判断target参数是否是对象,如果不是的话,直接返回,因为reactive只能把对象转换成响应式对象,这是和ref不同的地方。

然后来创建proxy中的拦截器对象,也就是handle的对象,它里面包含getsetdeleteProperty这些拦截的方法,最后创建并返回proxy对象,要传入拦截器对象。

index.js中,首先来创建reactive这个函数的形式,模块中导出一个reactive函数,它接受一个target的参数,首先要判断这个target是否是对象,如果不是对象的话直接返回,否则的话把target转换成代理对象。

export function reactive(target) {
    return new Proxy(target, handler)
}

要判断一个变量是否是对象,这件事情其他地方还要使用,所以在模块的最上面先来定一个辅助的函数isObject,用来判断一个变量是否是对象。

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

export function reactive(target) {
    return new Proxy(target, handler)
}

接下来再来定义一个handle的对象,这是proxy构造函数的第二个参数,叫做处理器或者拦截器对象handle的对象中包含getsetdeleteProperty。先把这个对象的形式写出来,一会再分别去实现它里边的方法。

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

下面分别来实现handler中的这三个方法。先来实现get方法,在get方法里首先要去收集依赖,至于如何收集依赖,下一小节再来实现。返回targetkey的值,这里直接通过Reflect.get来获取。直接return Reflect.get(target, key, receiver),这里还有一个问题,如果当前这个key属性对应的值也是对象,那么还需要把它再转换成响应式对象,这是之前说过的,如果对象中有嵌套属性的话,会在get中递归收集下一级属性的依赖。

这里先接受Reflect的返回值const result = Reflect.get(target, key, receiver),接下来需要先去判断一下result是否是对象,如果是对象需要再调用reactive来处理,这件事情待会ref函数内部也要使用,所以再来封装一个辅助的函数covert

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
const convert = target => isObject(target) ? reactive(target) : target

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

  const handler = {
    get(target, key, receiver) {
      // 收集依赖
      console.log('get', key)
      const result = Reflect.get(target, key, receiver)
      return convert(result)
    },
    set(target, key, value, receiver) {
      
    },
    deleteProperty(target, key) {
      
    }
  }
  return new Proxy(target, handler)
}

下面再来写set方法,在set方法里边,首先要去获取。key属性的值,需要定一个变量oldValue来获取,调用Reflect.get来获取它的值。获取这个oldValue的目的是等会要去判断一下当前传入的newValueoldValue是否相等,如果相等的话,不需要做任何的处理,如果它们的值不同的话,这个时候要调用Reflect.set方法重新去修改这个属性的值,并且还要去触发更新。但是之前说过,set方法中需要返回一个布尔类型的值,标示赋值是否成功。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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)
      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  // set方法需要返回布尔值类型的数据,这里默认为true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)  // 复制成功会返回true,失败会返回false
        // 触发更新
        console.log('set', key, value)
      }
      return result
    },
    deleteProperty(target, key) {
      
    }
  }
  return new Proxy(target, handler)
}

最后再来写deleteProperty这个方法,在deleteProperty中,首先要判断当前target中是否有需要删除的key属性,如果target中有key属性,并且把key成功删除之后,再来触发更新。

最后要返回删除是否成功,现在模块的最上面再来写一个辅助的函数hasOwn,以及抽离出hasOwnProperty,它的作用是用来判断某个对象本身是否具有指定的属性。

这里首先要调用hasOwn来判断一下,并且来接收一下它的返回结果。然后再来调用Reflect.deleteProperty(target, key)来删除target中的key属性,并且它会返回一个布尔类型。如果当前target中有key属性,并且删除成功,这个时候要去触发更新。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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)
      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)
      }
      return result
    },
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // 触发更新
        console.log('delete', key)
      }
      return result
    }
  }
  return new Proxy(target, handler)
}

下面在网页中来测试.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
<script type="module">
  import {reactive} from './my-reactivity/index.js'
  const obj = reactive({
    name: 'zs',
    age: 18
  })

  obj.name = 'lisi'
  obj.age = 20
  delete obj.name
  console.log(obj)
</script>
</body>
</html>

可以看到分别触发了getsetdeleteProperty,这里reactive方法就模拟完了。

4.响应式系统原理-收集依赖

接下来来实现响应式系统中收集依赖的过程。先来演示一下Vue3中的reactivity模块,也就是响应式系统的模块,通过演示响应式系统模块的使用来总结实现依赖收集的思路。

<!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>
<script type="module">
  import { reactive, effect } from './node_modules/vue/dist/vue.esm-browser.js'

  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)

</script>
</body>
</html>

这里已经安装好了reactivity模块,并且实现了一个简单的案例,来看一下代码。首先导入了reactivity模块中的reactiveeffect这两个函数,reactive的作用是用来创建显式数据的,下面来看effect函数的作用。

首先,创建了一个响应式的对象product,它是用来描述商品的,商品的名称是iPhone,商品的单价是5000,库存是三个。
接下来又定义了一个变量total,这是product总价。定了effect这个函数,它接受一个函数作为参数。effect的用法和watchEffect的用法一样,watchEffect内部就是调用effect来实现的。

effect中的函数首先会执行一次,当这个函数中引用响应式数据的时候,比如这里使用了product.price还有product.count,如果响应式数据发生变化,它会再次执行。当利用effect之后,会计算出商品的总价格,打印total,那此时的总价格应该是15000,接下来改变了商品的单价,product.price = 4000。当响应式数据变化之后,effect中的函数会再次执行,最后又把count的值改成了1。当显式数据变化之后,effect中的函数会再次执行。

在首次加载的时候,首先会执行effect中的函数,在effect函数内部首先会调用参数箭头函数,在这个箭头函数中又访问了productproductreactive返回的响应式对象,也就是代理对象。当我们访问product.price的时候,会执行price属性的get方法,在get方法中要收集依赖收集依赖的过程其实就是存储这个属性和回调函数,而属性又跟对象相关,所以在代理对象的get方法中,首先会存储target目标对象,然后是target对象的属性,这里是price。然后再把对应的箭头函数存储起来,这里是有对应关系的,目标对象、对应的属性、对应的这个箭头函数。在触发更新的时候,再根据对象的属性找到这个函数。

price的依赖收集完毕之后会继续访问product.count,它会执行count属性对应的get方法,在get方法中需要存储目标对象以及对应的count属性及当前这个箭头函数。

接下来给product price重新赋值的时候,会执行price属性对应的set方法。在set方法中需要触发更新,那触发更新其实就是找到依赖收集过程中存储的对象的price属性对应的effect函数,找到这个函数之后会立即执行。

这是依赖收集和触发更新的一个简单的过程,通过一张图解释一下。

在依赖收集的过程中,会创建三个集合,分别是targetMap还有depsMap还有dep。其中targetMap的作用是用来记录目标对象和一个字典,也就是中间的这个depsMaptargetMap使用的类型是weakMap弱引用的map,这里的key其实就是target对象,因为是弱引用,当目标对象失去引用之后,可以销毁。targetMap的值是depsMapdepsMap是一个字典,类型是map,字典中的key目标对象中的属性名称,值是一个set集合,set集合中存储的元素不会重复,它里面存储的是effect函数,因为可以多次调用effect,在effect中访问同一个属性,这时候该属性会收集多次依赖对应多个effect函数。

所以通过这种结构,可以存储目标对象,目标对象的属性以及属性对应的effect函数,一个属性可能对应多个函数,那将来触发更新的时候,可以在这个结构中根据目标对象的属性找到effect函数,然后执行。

一会要实现的收集依赖的track函数,它内部首先要根据当前的targetMap这个对象来找到depsMap,如果没有找到的话,要给当前对象创建一个depsMap并添加到targetMap中。如果找到了,再去根据当前使用的属性来depsMap中找到对应的depdep里存储的是effect函数,如果没有找到的话,为当前属性创建对应的dep,并且存储到depsMap中。
如果找到当前属性对应的dep集合,那就把当前的effect函数存储到dep集合中,这就是整个收集依赖的思路。

5.响应式系统原理-effect-track

下面来实现收集依赖的功能,分别来实现effecttrack两个函数。

effect函数接收一个函数作为参数callback,在effect函数中首先要执行1次callback,在callback中会访问现实对象的属性,在这个过程中去收集依赖,在收集依赖的过程中,要把callback存储起来。所以要想办法让之后的track函数能够访问到这里的callback,在这个函数的上面先来定一个变量activeEffect来记录callback。然后在函数中先把callback存储到activeEffect中。当依赖收集完毕之后,还需要将activeEffect设置为null,因为收集依赖的时候如果有嵌套属性的话,是一个递归的过程。

let activeEffect = null

export function effect(callback) {
  //  effect中首先要执行一次callback,访问响应时对象属性,去收集依赖
  activeEffect = callback
  callback()
  activeEffect = null
}

下面实现track函数,track函数接收两个参数,一个是目标对象target,还有一个是要跟踪的属性keytrack内部要把target存储到一个targetMap中。track是收集依赖,trigger是触发更新。trigger函数要去targetMap中找到属性对应的effect函数,然后来执行。

track内部首先要判断activeEffect的值为null的话,直接返回,说明当前没有要收集的依赖,否则要去targetMap中,根据当前的target来找depsMap,因为我们当前的target就是targetMap中的键。还要判断一下是否找到了depsMap,因为target它可能没有收集的依赖。如果没有找到的话,要为当前的target创建一个对应的depsMap去存储键和对应的dep对象->也就是要执行的effect函数,然后再把它添加到targetMap中。

接下来根据属性查找对应的dep对象,let dep = depsMap.get(key),然后再来判断一下dep是否存在,dep法是一个集合,这个集合用来去存储属性对应的那些effect函数,如果没有找到的话,跟之前一样,也要创建一个新的dep集合,并且把它添加到depsMap中。

接下来就可以把effect函数添加到dep集合中,dep.add(activeEffect)。还要在代理对象的get中来调用一下这个函数。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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)
      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)
      }
      return result
    },
    deleteProperty(target, key) {
      const hadKey = hasOwn(target, key)
      const result = Reflect.deleteProperty(target, key)
      if (hadKey && result) {
        // 触发更新
        console.log('delete', key)
      }
      return result
    }
  }

  return new Proxy(target, handler)

}


let activeEffect = null

export function effect(callback) {
  //  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)
}

好,那到这里整个依赖收集的过程就完成了,这个过程可以通过之前看到那张图来回顾。

6.响应式系统原理-trigger

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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) {
      // 收集依赖
      track(target, key)
      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)
        // 触发更新
        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
    }
  }

  return new Proxy(target, handler)

}


let activeEffect = null

export function effect(callback) {
  //  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)
}


// 触发更新
export function trigger(target, key) {
  // 根据target,在targetMap中找打key
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

依赖收集完毕之后,再来实现触发更新,对应要实现的函数是trigger,这个过程跟track的过程正好相反。

trigger函数也有两个参数,分别是targetkey,要根据targettargetMap中来找到depsMap。判断是否找到了depsMap,如果没有找到直接return

如果找到了depsMap,再根据key来找对应的dep集合,dep集合里边存储的是这个key所对应的effectt函数。再来判断一下dep集合是否有值,如果dep有值的话,需要去遍历dep集合,然后执行里面的每一个effect函数。

到这里trigger函数就写完了。下面要找到reactive函数,要找到代理对象的set方法和deleteProperty方法,然后在这两个方法中来调用trigger触发更新。随后进行测试。

可以看到正常执行getsetdeleteProperty

7.响应式系统原理-ref

之前已经实现了reactive函数,它可以创建响应式的对象,下面再来实现一个创建响应式对象的函数ref,这个函数之前使用过了,它接收一个参数,可以是原始值,也可以是对象,如果传入的是对象,并且这个对象是ref创建的对象,那直接返回。如果是普通对象的话,它内部会调用reactive来创建响应式对象,否则的话创建一个只有value属性的响应式对象,然后把它返回。

ref函数里边,首先要判断raw是否是使用ref创建的对象,ref创建的对象有什么特点呢?我们还不知道,所以我们一会儿来写,我们先来写上注释,要判断要是否是ref创建的对象,如果是的话,直接返回。

接下来来判断raw是否是对象,如果是对象,就要reactive创建响应式式对象,否则的话返回原始值。这件事情直接写过一个函数convert,这里直接调用就可以了,还要把这个调用的结果存储到一个变量value中。

接下来不管value当前是什么值,都要去创建一个ref的对象,这个对象是一个只有value属性的对象,并且这个value属性具有getset标识,标识是否是ref创建的对象。

下面来实现这个对象,这个对象里边首先要创建一个标识的属性__v_isRef,它的值是true。接下来再来创建value的属性,value属性只有对应的getset

如果给value重新赋制成一个对象,它依然是响应式的,因为当raw是对象的时候,convert里边会利用reactive把它转换成响应式对象,这是跟reactive的一个区别。接下来开始测试。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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) {
      // 收集依赖
      track(target, key)
      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)
        // 触发更新
        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
    }
  }

  return new Proxy(target, handler)

}


let activeEffect = null

export function effect(callback) {
  //  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)
}

// 触发更新
export function trigger(target, key) {
  // 根据target,在targetMap中找打key
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

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

  let value = convert(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
}
<!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>
  <script type="module">
    import { reactive, effect, ref } from './my-reactivity/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)

  </script>
</body>
</html>

可以看到正常运行。

最后再来总结一下refreactive这两个函数的区别。

ref可以把基本类型的数据转换成响应式对象,当获取数据时要使用value属性,模板中使用的时候可以省略。reactive不能把基本数据类型的数据转换成响应式对象。ref返回的对象重新给value属性赋值成对象以后也是响应式的,那刚刚我们代码里边是通过convert来处理的,我们在todo list案例删除已完成代办项的时候使用过。

reactive创建的响应式对象重新赋值以后会丢失响应式,因为重新赋值的对象不再是dep对象。reactive返回的对象中的属性不可以解构,如果想要解构的话,需要使用toRefs来处理返回的这个对象。

如果一个对象中的成员非常多的时候,使用ref并不方便,因为总要带着value属性。如果一个函数内部只有一个响应式数据,这个时候可以使用ref会比较方便,因为可以直接解构返回,之前案例里使用的都是ref。

8.响应式系统原理-toRefs

接下来再来实现toRefs函数,首先要知道这个函数的作用,toRefs函数接受一个reactive返回的响应式对象,也就是一个proxy对象。

如果传入的参数不是reactive创建的显项式对象直接返回,然后再把传入对象的所有属性转换成一个类似于ref返回的对象,把转换后的属性挂载到一个新的对象上返回。

先来定一个toRefs的函数,它接收一个参数proxy,在这个函数里边,做的第一件事情是判断这个函数的参数是否是一个reactive创建的对象,如果不是的话,发送警告,因为上面实现的reactive中创建的对象没有做标识的属性,所以这步跳过。

接下来定义一个ret,要给ret去赋值,要判断一下传过来这个参数,如果是数组的,那么创建一个长度是length数组,否则的话返回一个空对象。const ret = proxy instanceof Array ? new Array(proxy.length) : {}

处理完成之后,接下来来遍历key这个对象的所有属性,如果是数组的话,遍历它的所有索引,把每一个属性都转换成类似于ref返回的对象。在变历的过程中,要把每一个属性都转换成一个ref返回的对象。

这个转换的过程再来封装成一个函数toProxyRef。在这个函数里边,直接来创建一个对象,最终要返回这个对象。遍历的时候这里边要去调用toRefs去把所有属性转换一下,并且把转换好的属性存储到ret[key]对象里面来。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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) {
      // 收集依赖
      track(target, key)
      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)
        // 触发更新
        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
    }
  }

  return new Proxy(target, handler)

}


let activeEffect = null

export function effect(callback) {
  //  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)
}

// 触发更新
export function trigger(target, key) {
  // 根据target,在targetMap中找打key
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

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

  let value = convert(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
}

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) {
  return {
    __v_isRef: true,
    get value() {
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    }
  }
}

最后来测试一下。

<!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>
  <script type="module">
    import { reactive, effect, toRefs } from './my-reactivity/index.js'

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

    const { price, count } = useProduct()


    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>
</body>
</html>

可以看到正常返回显示。

9.响应式系统原理-computed

最后再来简单模拟一下computed的函数的内部实现,computed的需要接受一个有返回值的函数作为参数,这个函数的返回值就是计算属性的值,并且要监听这个函数内部使用的响应式数据的变化,最后把这个函数执行的结果返回。

export function computed(getter) {
  const result = ref()
  effect(() => (result.value = getter))
  return result
}

effect中执行get的时候,访问响应式数据的属性会去收集依赖,当数据变化后会重新执行effect函数,把get的结果再存储到result中。打开页面进行测试。

// 判断是否为对象
const isObject = val => val !== null && typeof val === 'object'
// 判断target是否为对象,如果是对象,则继续调用reactive函数将其转为响应式对象,如果不是对象,直接返回target
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) {
      // 收集依赖
      track(target, key)
      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)
        // 触发更新
        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
    }
  }

  return new Proxy(target, handler)

}


let activeEffect = null

export function effect(callback) {
  //  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)
}

// 触发更新
export function trigger(target, key) {
  // 根据target,在targetMap中找打key
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.forEach(effect => {
      effect()
    })
  }
}

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

  let value = convert(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
}

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) {
  return {
    __v_isRef: true,
    get value() {
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    }
  }
}

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

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

  return result
}
<!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>
<script type="module">
  import { reactive, effect, computed } from './my-reactivity/index.js'

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

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


  console.log(total.value)

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

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

</script>
</body>
</html>

到这里模拟了响应式系统中的reactivereftoRefscomputed的函数的内部实现,还实现了依赖收集和触发更新的tracktrigger以及effect函数,但这三个函数比较底层,一般情况下不会直接去调用好。


文章作者: 5coder
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 5coder !
  目录