引言
ref与reactive都是vue3中的响应式api,用于创建响应式数据。所谓响应式就是在适当的时间去触发某个函数。
ref与reactive的区别:
- ref 常用于基本数据类型,当接受的是一个对象时,内部会使用reactive机制来处理。reactive 常用于对象和数组
- ref 使用原生js的getter和setter机制, reactive 使用es6上的proxy代理机制
- ref需要.value , reactive不用
reactive 原理
手搓一个reactive:创建一个文件夹reactivity,在里面创建一个reactive.js,在里面抛出一个函数reactive(),然后返回一个响应式对象createReactiveObject()函数。这样就可以在其它地方调用手写的方法了。
js
// reactive.js
export function reactive(target) {
return createReactiveObject(target)
}
function createReactiveObject(target) {
}
createReactiveObject函数里面首先判断target是否为一个对象,不是则直接返回,反之则使用proxy代理这个传进来的值,但是呢,为了更好维护就将proxy里面的set,get等方法放到另外的文件里了,vue3源码将其命名为mutableHandlers。如下;
js
// reactive.js
import { mutableHandles } from './baseHandlers.js'
export function reactive(target) {
return createReactiveObject(target, mutableHandles)
}
function createReactiveObject(target, proxyHandlers) { // 创建响应式对象
if (typeof target !== 'object') {
return target
}
const proxy = new Proxy(target, proxyHandlers)
return proxy
}
但是呢,还考虑到代理过的对象就不需要再代理了,但是代理过的对象与原对象没有什么不同的特征,于是官方又创建了一个WeakMap对象,用来标记是否已经被代理过。
js
// reactive.js
import { mutableHandles } from './baseHandlers.js' // 将proxy里面的set,get等方法封装
export const reactiveMap = new WeakMap() // 对象用来标记是否被代理过
export function reactive(target) {
return createReactiveObject(target, mutableHandles, reactiveMap)
}
function createReactiveObject(target, proxyHandlers, proxyMap) { // 创建响应式对象
if (typeof target !== 'object') {
return target
}
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy // 被代理过直接返回
}
const proxy = new Proxy(target, proxyHandlers)
proxyMap.set(target, proxy) // 第一次代理就往这个对象上挂
return proxy
}
接下来就是写baseHandlers.js里面的代码了。
js
// baseHandlers.js
const get = createGetter()
const set = createSetter()
function createGetter() {
return function (target, key, receiver) {
}
}
function createSetter() {
return function (target, key, value, receiver) {
}
}
export const mutableHandles = {
get,
set
}
为什么要这样写,我也不知道,大佬是这样写的,为了更好维护.然后再在里面return出值就可以了。
js
function createGetter() {
return function (target, key, receiver) {
return Reflect.get(target, key, receiver)
}
}
function createSetter() {
return function (target, key, value, receiver) {
return Reflect.set(target, key, value, receiver)
}
}
但是呢?如果代理的对象的属性是一个对象的话,get值时就需要接着往下响应,子对象递归。所以需要先判断是否为一个对象。
js
// shared/index.js
export function isObject(val) {
return val !== null && typeof val === 'object'
}
js
// baseHandlers.js
import { isObject } from '../shared/index.js'
import { reactive } from './reactive.js'
const get = createGetter()
const set = createSetter()
function createGetter() {
return function (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
if (isObject(res)) {
return reactive(res) // 子对象再代理
}
return res
}
}
function createSetter() {
return function (target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
return res
}
}
export const mutableHandles = {
get,
set
}
然后就是触发响应式了,当reactive里面的值变更后,会带来视图更新,compute计算属性,watch监听属性触发,页面上用到这个值会更新,这些统称为依赖,那么就需要实现当值改变时这些依赖都重新触发。
比如:
js
// App.vue
import { reactive } from './reactivity/reactive.js'
const state = reactive({
count: 0,
like: {
a: 2
}
})
const res = computed(() => { // 依赖于state.count
return state.count + 1
})
const add = () => {
state.count++ // 当state.count改变会导致computed触发
}
然后就是要收集全部依赖,也就是副作用函数,跟这些变量绑在一起的函数。当App.vue加载完就需要知道state.count在哪些地方用到了,那么怎么知道呢?就是当state.count被用到的时候就相当于state.count被读取到的时候,就会触发get函数,所以就可以在get里面进行依赖收集。
代码如下:
js
// baseHandlers.js 完整代码
import { isObject } from '../shared/index.js'
import { reactive } from './reactive.js'
import { track, trigger } from './effect.js'
const get = createGetter()
const set = createSetter()
function createGetter() {
return function (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
// 在值初次被读取时就要进行依赖收集
track(target, 'get', key)
if (isObject(res)) {
return reactive(res)
}
return res
}
}
function createSetter() {
return function (target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
// 触发依赖
trigger(target, 'set', key)
return res
}
}
export const mutableHandles = {
get,
set
}
这样基本就完成了,get里面调用track进行依赖收集,set里面调用trigger触发依赖。将这两个函数放在./effect.js里面。接下来就是./effect.js文件里面的track和trigger了。
如何进行依赖收集,大佬将依赖收集成如下:
js
targetMap = { // 收集到一个map对象里面
obj1: { // 第一个reactive接收的对象
// 对象里面的属性作key
key1: [effect1, effect2, ...], // effect为该属性的依赖也就是用到这个属性的函数
key2: [effect1, effect2, ...],
},
obj2:{ // 第二个reactive接收的对象
key: [effect1, effect2, ...]
}
}
js
// vue3 中
effect(() => {
console.log('effect', state.count);
})
在 Vue 3 中,effect
是一个用于追踪依赖和触发副作用的函数,effect
函数的主要作用是允许你定义一个函数,这个函数会在其依赖的响应式数据发生变化时自动重新执行。
js
// 完整 effect.js
const targetMap = new WeakMap()
let activeEffect = null // 副作用函数
export function effect(fn, options = {}) { // 更新时触发
const effectFn = () => {
try {
activeEffect = fn // 将回调存到 activeEffect
return fn()
} catch (error) {
activeEffect = null
}
}
effectFn()
}
// 依赖收集
export function track(target, type, key) {
// targetMap = {
// state: {
// key: [effect1, effect2, ...]
// }
// }
let depsMap = targetMap.get(target) // 先看下该对象是否已经收集过
if (!depsMap) { // 如果没有收集过
targetMap.set(target, (depsMap = new Map()))
}
let deps = depsMap.get(key) // 对象上的属性是否收集过
if (!deps) {
deps = new Set() // 也相当于 [effect1, effect2, ...]
}
if (!deps.has(activeEffect) && activeEffect) { // activeEffect 就是computed里面的回调函数
deps.add(activeEffect)
}
depsMap.set(key, deps) // 往map里面添加
}
// 触发依赖
export function trigger(target, type, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return // 定义了一个对象,没有使用过,没有依赖这个target的副作用函数
const deps = depsMap.get(key)
if (!deps) return // 没有依赖这个具体属性的副作用函数
deps.forEach(effect => {
effect() // 这里触发
})
}
以上就是 reactive 的简版实现过程了。
js
// 效果
<template>
<div @click="add">
hello world -- {{ state.count }} -- {{ res }}
</div>
</template>
<script setup>
import { reactive } from './reactivity/reactive.js'
import { effect } from './reactivity/effect.js'
const state = reactive({
count: 0,
like: {
a: 2
}
})
effect(() => {
console.log('effect', state.count);
})
const add = () => {
state.count++
}
</script>
当代码通篇被读到的时候,effect函数作为副作用函数被收集起来了,等到修改state.count的时候,就会触发proxy里面的set,set里面触发trigger,trigger就会帮你找出state.count的依赖有哪些,然后遍历执行。
因为官方打造的proxy方法第一个参数不支持原始数据类型,所以reactive不能处理原始类型。
ref 原理
ref接收的如果是一个原始数据类型就走类里面的get和set机制,如果接收的是一个引用类型,则与reactive一样使用proxy函数来实现。
js
// ref.js
export function ref(val) {
return new RefImpl(val)
}
class RefImpl {
constructor(val) {
this._val = convert(val)
}
get value() {
return this._val
}
}
const n = ref(1)
console.log(n.value);
如果RefImpl类里面的value函数前面不加get就需要n.value()来访问。所以我们使用ref后面接的数据的使用要加.value,这个value其实是个函数体,因为有这个语法(往一个函数前面加get,set调用该函数时就不要打括号)存在所以省略了()。
如果是原始类型:
js
set value(newVal) {
if (newVal !== this._val) {
this._val = newVal
}
}
加个这个就结束了,但是如果是个引用类型的话,就需要使用reactive实现响应式的机制了。加个判断就可以了。完整代码如下;
js
// 完整 ref.js
import { reactive } from "./reactive.js";
import { isObject } from "../shared/index.js";
import { track, trigger } from "./effect.js";
export function ref(val) {
return new RefImpl(val)
}
class RefImpl {
constructor(val) {
this._val = convert(val)
}
get value() {
track(this, 'get', 'value')
return this._val
}
set value(newVal) {
if (newVal !== this._val) {
this._val = newVal
trigger(this, 'set', 'value')
}
}
}
function convert(val) {
return isObject(val) ? reactive(val) : val
}
ref 与 reactive 区别
reactive使用proxy代理了对象上的各种操作行为,在读取属性这个过程当中,为属性去添加副作用函数,在属性修改或删除时去触发该属性身上绑定的副作用函数,ref当参数是引用类型时,直接借助reactive机制来完成,当参数是一个原始值的时候,借助原生js中的getter和setter机制,实现为值做副作用函数收集和触发副作用函数的效果。