Vue3.0 的响应式系统底层使用了 Proxy 对象实现,在初始化的时候不需要遍历所有的属性,再把属性通过 defineProperty 转化成 getter和setter,另外如果有多层属性嵌套的话,只有访问某个属性的时候才会递归处理下级的属性,所以Vue3.0中的响应式系统的性能要比Vue2 好。
Vue3的响应式默认可以监听动态添加的属性,还可以监听属性的删除操作以及数组的索引和length属性的修改操作,另外,Vue3响应式系统还可以作为一个模块单独使用,接下来我们分别来实现Vue3.0中几个核心函数:
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)
},
deleteProperty (target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key)
}
})
proxy.foo = 'zzz'
delete proxy.foo
这里通过 Proxy 代理target 对象,在创建 Proxy 对象时,传入了第二个参数,它是一个对象,叫处理器或者监听器,get、set、deleteProperty分别可以监听对属性的访问、赋值、删除操作。在获取和设置值的时候使用了 Reflect,Reflect 是ES6新增的成员
reactive函数接收一个参数,在 reactive 中首先需要判断这个参数是否是对象,如果不是的话则返回,reactive 只能把对象转换成响应式对象,这与 Ref 不同,然后创建 Proxy 拦截器对象 hander,里面包含 get/set/deleteProperty 这些拦截的方法,最后创建并返回Proxy 对象;
在 reactivity中创建index.js,以及index.html
// 判断是否是对象
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)
}
ok, reactive 整体的结构就是这样的,现在我们来一步一步实现 get、set、deleteProperty方法
在get 方法中首先要去收集依赖,返回target 对应的值,通过 Reflect.get 来获取,如果当前的key 对应的值也是对象,那么我们还需要将它转换成响应式对象,如果对象中有嵌套属性的话,会在get中递归收集下一级属性的依赖,通过判断result 是否是对象,如果是的话,需要调用reactive 来处理,定义convert来处理。
set 方法首先需要获取key的值,通过Reflect.get 来获取key的值,用来判断当前传入的值和新值跟oldValue 是否相等,相同不作处理,不同这则需要调用 Reflect.set 修改值并去触发更新。
deleteProperty 首先判断在当前的 target 中是否有自己的key 属性,通过 hasOwnProperty来判断, 如果有key 属性,并且把key从target成功删除后,再来触发更新,返回是否删除成功。
// 判断是否是对象
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) {
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) {
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)
}
// index.html
// 测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3.0响应式原理title>
head>
<body>
<script type="module">
import {
reactive } from './index.js'
const obj = reactive({
name: '大白菜',
age: 18
})
obj.name = '大白菜321'
delete obj.age
console.log(obj)
script>
body>
html>
effect 函数
let activeEffect = null;
export function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象的属性,在这个过程中去收集依赖
}
effect 接收一个函数作为参数,在 effect 函数中首先要执行 callback,在callback中会访问响应式对象的属性,定义
activeEffect 将 callback 保存,当依赖收集完毕之后,需要将 activeEffect 设置为 null
track 函数
track函数接收两个参数,目标对象target,需要跟踪的属性 key,内部需要将target 保存在 targetMap 中,通过weackMap 创建一个 targetMap,在内部首先需要判断 activeEffect,如果值为null 直接返回,说明当前没有要收集依赖,否则需要去targetMap 中根据当前的target找depsMap,接这判断是否存在 depsMap,不存在则为当前的 target创建一个对应的 depsMap,去存储 键 和 dep 对象,也就是执行的 effect 函数。最后在 reactive 代理对象中的 get 调用这个函数 track,
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)
}
reactive 函数
// reactive 函数
const handler = {
get (target, key, receiver) {
// 收集依赖
track(target, key)
...
}
}
在trigger 函数中,需要根据 target 去 targetMap 中找到 depsMap,在 depsMap 存储的是 属性以及对应的 dep 集合,dep 集合中存储的是 这个key 对应的 effect 函数,遍历每一个effect 函数。在 reactive 函数中的 set deleteProperty 调用 trigger 函数触发更新
// 在代理对象中调用这个函数, reactive get
// ```js
// track(target, key)
// ```
// 根据target Map
// 在 reactive set 中触发更新 调用trigger, deleteProperty 调用
export function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
在 reactive 函数中调用trigger函数
const handler = {
...
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
let result = true
if (oldValue !== value) {
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
}
}
测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
import {
reactive, effect, computed } from './index.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>
// 15000
// 12000
// 4000
ref 接收一个参数,可以是原始值和对象,如果传入的是对象,并且是 ref 创建的对象,则直接返回,如果是普通对象,在ref 内部会调用reactive 创建响应式对象,否则创建一个只有 value 属性的对象,最后返回。
export function ref (raw) {
// 判断 raw 是否是 ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw) // 判断raw 是否是一个对象,如果是调用reactive 转换成响应式对象,否则返回本身
const r = {
__v_isRef: true,
get value () {
track (r, 'value') // 调用track 收集依赖
return value
},
set value (newValue) {
// 判断新值和旧值是否相等,不相等存储在raw 中
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
}
}
return r
}
测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
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)
script>
body>
html>
// 15000
// 12000
// 4000
reactive 和 ref 的区别
如果一个对象成员非常多的时候,使用ref 并不方便,因为总要带着value 属性,如果一个函数内部只有一个响应式数据,这个时候使用ref 会比较方便,因为可以解构返回。
toRefs 接收一个 reactive 返回的响应式对象,也就是 proxy 对象,如果传入的参数不是 reactive 创建的响应式对象,则直接返回,然后把传入对象的所有属性转换成类似与 ref 返回的对象,把转换后的属性挂载到一个新的对象上返回。
export function toRefs (proxy) {
const ret = proxy instanceof Array ? new Array(proxy.length) : {
}
// 遍历所有 proxy 属性
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 lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
import {
reactive, effect, toRefs } from './index.js'
function useProduct () {
const product = reactive({
name: '大白菜',
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>
// 15000
// 12000
// 4000
computed 接收一个有返回值的函数参数,这个函数返回值就是计算属性的值,需要监听这个函数内部使用的响应式数据的变化,最后把函数执行结果返回,computed 内部会通过effect 监听getter 内部响应式数据变化,因为在 effect 中执行getter 时访问响应式数据属性会收集依赖,当数据变化后,会重新执行effect 函数,把getter 的结果在存储到result 中。
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 './index.js'
const product = reactive({
name: '大白菜',
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>
// 15000
// 12000
// 4000
ok,到这里,我们模拟实现了 Vue3.0 中的响应式原理reactive / ref / toRefs / computed 函数的内部实现,还实现了依赖收集和触发更新的 effect、track、trigger函数。
感谢您花宝贵的时间阅读这篇内容,如果你觉得这篇内容对你有帮助的话,就给本文点个赞吧,
(感谢掘友的鼓励与支持)
Vue3.0 的响应式系统底层使用了 Proxy 对象实现,在初始化的时候不需要遍历所有的属性,再把属性通过 defineProperty 转化成 getter和setter,另外如果有多层属性嵌套的话,只有访问某个属性的时候才会递归处理下级的属性,所以Vue3.0中的响应式系统的性能要比Vue2 好。
Vue3的响应式默认可以监听动态添加的属性,还可以监听属性的删除操作以及数组的索引和length属性的修改操作,另外,Vue3响应式系统还可以作为一个模块单独使用,接下来我们分别来实现Vue3.0中几个核心函数:
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)
},
deleteProperty (target, key) {
// delete target[key]
return Reflect.deleteProperty(target, key)
}
})
proxy.foo = 'zzz'
delete proxy.foo
这里通过 Proxy 代理target 对象,在创建 Proxy 对象时,传入了第二个参数,它是一个对象,叫处理器或者监听器,get、set、deleteProperty分别可以监听对属性的访问、赋值、删除操作。在获取和设置值的时候使用了 Reflect,Reflect 是ES6新增的成员
reactive函数接收一个参数,在 reactive 中首先需要判断这个参数是否是对象,如果不是的话则返回,reactive 只能把对象转换成响应式对象,这与 Ref 不同,然后创建 Proxy 拦截器对象 hander,里面包含 get/set/deleteProperty 这些拦截的方法,最后创建并返回Proxy 对象;
在 reactivity中创建index.js,以及index.html
// 判断是否是对象
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)
}
ok, reactive 整体的结构就是这样的,现在我们来一步一步实现 get、set、deleteProperty方法
在get 方法中首先要去收集依赖,返回target 对应的值,通过 Reflect.get 来获取,如果当前的key 对应的值也是对象,那么我们还需要将它转换成响应式对象,如果对象中有嵌套属性的话,会在get中递归收集下一级属性的依赖,通过判断result 是否是对象,如果是的话,需要调用reactive 来处理,定义convert来处理。
set 方法首先需要获取key的值,通过Reflect.get 来获取key的值,用来判断当前传入的值和新值跟oldValue 是否相等,相同不作处理,不同这则需要调用 Reflect.set 修改值并去触发更新。
deleteProperty 首先判断在当前的 target 中是否有自己的key 属性,通过 hasOwnProperty来判断, 如果有key 属性,并且把key从target成功删除后,再来触发更新,返回是否删除成功。
// 判断是否是对象
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) {
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) {
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)
}
// index.html
// 测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue3.0响应式原理title>
head>
<body>
<script type="module">
import {
reactive } from './index.js'
const obj = reactive({
name: '大白菜',
age: 18
})
obj.name = '大白菜321'
delete obj.age
console.log(obj)
script>
body>
html>
effect 函数
let activeEffect = null;
export function effect (callback) {
activeEffect = callback
callback() // 访问响应式对象的属性,在这个过程中去收集依赖
}
effect 接收一个函数作为参数,在 effect 函数中首先要执行 callback,在callback中会访问响应式对象的属性,定义
activeEffect 将 callback 保存,当依赖收集完毕之后,需要将 activeEffect 设置为 null
track 函数
track函数接收两个参数,目标对象target,需要跟踪的属性 key,内部需要将target 保存在 targetMap 中,通过weackMap 创建一个 targetMap,在内部首先需要判断 activeEffect,如果值为null 直接返回,说明当前没有要收集依赖,否则需要去targetMap 中根据当前的target找depsMap,接这判断是否存在 depsMap,不存在则为当前的 target创建一个对应的 depsMap,去存储 键 和 dep 对象,也就是执行的 effect 函数。最后在 reactive 代理对象中的 get 调用这个函数 track,
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)
}
reactive 函数
// reactive 函数
const handler = {
get (target, key, receiver) {
// 收集依赖
track(target, key)
...
}
}
在trigger 函数中,需要根据 target 去 targetMap 中找到 depsMap,在 depsMap 存储的是 属性以及对应的 dep 集合,dep 集合中存储的是 这个key 对应的 effect 函数,遍历每一个effect 函数。在 reactive 函数中的 set deleteProperty 调用 trigger 函数触发更新
// 在代理对象中调用这个函数, reactive get
// ```js
// track(target, key)
// ```
// 根据target Map
// 在 reactive set 中触发更新 调用trigger, deleteProperty 调用
export function trigger (target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
在 reactive 函数中调用trigger函数
const handler = {
...
set (target, key, value, receiver) {
const oldValue = Reflect.get(target, key, receiver);
let result = true
if (oldValue !== value) {
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
}
}
测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
import {
reactive, effect, computed } from './index.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>
// 15000
// 12000
// 4000
ref 接收一个参数,可以是原始值和对象,如果传入的是对象,并且是 ref 创建的对象,则直接返回,如果是普通对象,在ref 内部会调用reactive 创建响应式对象,否则创建一个只有 value 属性的对象,最后返回。
export function ref (raw) {
// 判断 raw 是否是 ref 创建的对象,如果是的话直接返回
if (isObject(raw) && raw.__v_isRef) {
return
}
let value = convert(raw) // 判断raw 是否是一个对象,如果是调用reactive 转换成响应式对象,否则返回本身
const r = {
__v_isRef: true,
get value () {
track (r, 'value') // 调用track 收集依赖
return value
},
set value (newValue) {
// 判断新值和旧值是否相等,不相等存储在raw 中
if (newValue !== value) {
raw = newValue
value = convert(raw)
trigger(r, 'value')
}
}
}
return r
}
测试一下
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
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)
script>
body>
html>
// 15000
// 12000
// 4000
reactive 和 ref 的区别
如果一个对象成员非常多的时候,使用ref 并不方便,因为总要带着value 属性,如果一个函数内部只有一个响应式数据,这个时候使用ref 会比较方便,因为可以解构返回。
toRefs 接收一个 reactive 返回的响应式对象,也就是 proxy 对象,如果传入的参数不是 reactive 创建的响应式对象,则直接返回,然后把传入对象的所有属性转换成类似与 ref 返回的对象,把转换后的属性挂载到一个新的对象上返回。
export function toRefs (proxy) {
const ret = proxy instanceof Array ? new Array(proxy.length) : {
}
// 遍历所有 proxy 属性
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 lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Documenttitle>
head>
<body>
<script type="module">
import {
reactive, effect, toRefs } from './index.js'
function useProduct () {
const product = reactive({
name: '大白菜',
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>
// 15000
// 12000
// 4000
computed 接收一个有返回值的函数参数,这个函数返回值就是计算属性的值,需要监听这个函数内部使用的响应式数据的变化,最后把函数执行结果返回,computed 内部会通过effect 监听getter 内部响应式数据变化,因为在 effect 中执行getter 时访问响应式数据属性会收集依赖,当数据变化后,会重新执行effect 函数,把getter 的结果在存储到result 中。
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 './index.js'
const product = reactive({
name: '大白菜',
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>
// 15000
// 12000
// 4000
ok,到这里,我们模拟实现了 Vue3.0 中的响应式原理reactive / ref / toRefs / computed 函数的内部实现,还实现了依赖收集和触发更新的 effect、track、trigger函数。
感谢您花宝贵的时间阅读这篇内容,如果你觉得这篇内容对你有帮助的话,就给本文点个赞吧,
(感谢掘友的鼓励与支持)