reactive 的实现
源码位置
const toProxy = new WeakMap()
const toRaw = new WeakMap()
function reactive(target) {
let observed = toProxy.get(target)
// 原数据已有相应的响应式数据,直接返回
if (observed !== void 0) {
return observed
}
// 原数据已经是响应式数据了
if (toRaw.get(target)) {
return target
}
observed = new Proxy(target, handler)
// 设置缓存
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
复制代码
toProxy
和 toRaw
主要是缓存 原始数据
和 响应式数据
封装Proxy的handler方法
const isObject = (obj) => obj !== null && typeof obj === 'object'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (val, key) => hasOwnProperty.call(val, key)
const handler = {
get: (target, key, receiver) => {
const res = Reflect.get(target, key, receiver)
// 收集依赖
track(target, key)
// 递归寻找
return isObject(res) ? reactive(res) : res
},
set: (target, key, value, receiver) => {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const res = Reflect.set(target, key, value, receiver)
// 触发更新
if (!hadKey || value !== oldValue) {
trigger(target, key, oldValue, value)
}
return res
},
}
复制代码
- 代码
isObject(res) ? reactive(res) : res
由于 proxy
代理的对象只能代理到第一层,当对多层级的对象操作时,set
并不能感知到,但是 get
会触发,需要我们进行递归实现,利用 Reflect.get()
返回的“多层级对象中内层” ,再对“内层数据”做一次代理。
- 代码
!hadKey || oldValue !== value
当输入
const arr = ['a', 'b']
const r = reactive(arr)
r.push('c')
// 结果
// ['a', 'b'] 2 'c'
// ['a', 'b', 'c'] 'length' 3
复制代码
r.push('c')
会触发 set
执行两次,一次是值本身 'c'
,一次是 length
属性设置。
此时的 length
属性其实是属于数组本身的一个属性
为了避免触发多次 trigger
,通过判断 key
是否为 target
自身属性,以及设置 value
是否跟 target[key]
相等 可以确定 trigger
的类型,并且避免多余的 trigger
这边只做了逻辑或的判断,在Vue3源码中通过 !hadKey
和 value !== oldValue
分别进行 ADD
和 SET
类型的判断进行不同的操作
track
和 trigger
主要的作用于 effect
,下面我们分别实现一下 track
, trigger
和 effect
effect 的实现
源码位置
const effectStack = [] // 存储effect
function effect(fn, options = {}) {
const reactiveEffect = createReactiveEffect(fn, options)
if (!options.lazy) {
reactiveEffect()
}
return reactiveEffect
}
function createReactiveEffect(fn, options) {
const effect = function effect(...args) {
return run(effect, fn, args)
}
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
复制代码
reactive与effect的相结合
function run(effect, fn, args) {
if (!effectStack.includes(effect)) {
try {
// 将effect push到全局数组中
effectStack.push(effect)
return fn(...args)
} finally {
// 清除已经收集过的effect
effectStack.pop()
}
}
}
复制代码
track 收集依赖
源码位置
// 收集依赖最终数据的结构
targetMap = {
target: {
name: [effect], (Set对象)
age: [effect] (Set对象)
}
}
复制代码
const targetMap = new WeakMap() // 缓存effect
function track(target, key) {
const effect = effectStack[effectStack.length - 1]
if (effect === void 0) {
return
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
// 收集依赖时,通过 key 建立一个 Set
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
}
}
复制代码
首先全局会存在一个 targetMap
,它用来建立 数据 -> 依赖
的映射,它是一个 WeakMap
数据结构。
而 targetMap
通过查找 target
,可以获取到 depsMap
,它用来存放这个数据对应的所有响应式依赖。
depsMap
的每一项则是一个 Set
数据结构,而这个 Set
就存放着对应 key
的更新函数
trigger 触发更新
源码位置
const targetMap = new WeakMap()
function trigger(target, key, oldValue, newValue) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
return
}
const effects = new Set()
const computedRunners = new Set()
if (key) {
// 通过 key 找到所有更新函数,依次执行
let deps = depsMap.get(key)
deps.forEach((effect) => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
const run = (effect) => effect()
effects.forEach(run)
computedRunners.forEach(run)
}
复制代码
computed的实现
源码位置
当然在我们实现完 effect
以后,computed
的实现就显得简单许多了
function computed(fn) {
const runner = effect(fn, { computed: true, lazy: true })
return {
effect: runner,
get value() {
return runner()
},
}
}
复制代码
完整代码
<!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>
<div id="app"></div>
<button id="btn">加1</button>
<script>
// utils
const isObject = (obj) => obj !== null && typeof obj === 'object'
const hasOwnProperty = Object.prototype.hasOwnProperty
const hasOwn = (val, key) => hasOwnProperty.call(val, key)
const toProxy = new WeakMap()
const toRaw = new WeakMap()
const handler = {
get: (target, key, receiver) => {
const res = Reflect.get(target, key, receiver)
track(target, key)
return isObject(res) ? reactive(res) : res
},
set: (target, key, value, receiver) => {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const res = Reflect.set(target, key, value, receiver)
if (!hadKey || value !== oldValue) {
trigger(target, key, oldValue, value)
}
return res
},
}
function reactive(target) {
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
if (toRaw.get(target)) {
return target
}
observed = new Proxy(target, handler)
toProxy.set(target, observed)
toRaw.set(observed, target)
return observed
}
const effectStack = []
const targetMap = new WeakMap() // 缓存effect
// 依赖收集
function track(target, key) {
const effect = effectStack[effectStack.length - 1]
if (effect === void 0) {
return
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (dep === void 0) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
}
}
// 触发更新
function trigger(target, key, oldValue, newValue) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
return
}
const effects = new Set()
const computedRunners = new Set()
if (key) {
let deps = depsMap.get(key)
deps.forEach((effect) => {
if (effect.computed) {
computedRunners.add(effect)
} else {
effects.add(effect)
}
})
}
const run = (effect) => effect()
effects.forEach(run)
computedRunners.forEach(run)
}
// 副作用
function effect(fn, options = {}) {
const reactiveEffect = createReactiveEffect(fn, options)
if (!options.lazy) {
reactiveEffect()
}
return reactiveEffect
}
function createReactiveEffect(fn, options) {
const effect = function effect(...args) {
return run(effect, fn, args)
}
effect.deps = []
effect.computed = options.computed
effect.lazy = options.lazy
return effect
}
function run(effect, fn, args) {
if (!effectStack.includes(effect)) {
try {
effectStack.push(effect)
return fn(...args)
} finally {
effectStack.pop()
}
}
}
function computed(fn, options) {
const runner = effect(fn, { ...options, computed: true, lazy: true })
return {
effect: runner,
get value() {
return runner()
},
}
}
</script>
<script>
const app = document.getElementById('app')
const btn = document.getElementById('btn')
const obj = reactive({
name: 'Charles',
age: 18,
location: {
city: 'Guangzhou'
}
})
let double = computed(() => obj.age * 2)
effect(() => {
app.innerHTML = `My name is ${obj.name}, ${obj.age} years old, ${double.value}`
})
btn.addEventListener('click', () => { obj.age += 1 }, false)
</script>
</body>
</html>
复制代码