手摸手教你简单快速实现vue3 reactivity ref computed

254 阅读3分钟

先谈谈vue3的响应式实现

vue3通过es6的proxy来实现数据的get和set的劫持。get时将订阅函数加入依赖,set时执行依赖列表。

与Object.defineproperty不同的是,proxy监听的是整个对象,因此可以监听到新增的对象属性。

reactivity的实现

看下面的代码 我们如何实现reactive呢和effect呢?

let product = reactive({ price: 5, quantity: 2 })
let total = 0

effect(() => {
  total = product.price * product.quantity
})

console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)

reactive的功能是数据的get和set的接触,需要使用es6的proxy实现。 effect用来进行依赖收集。

const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
let activeEffect = null // The active effect running

function track(target, key) {
	if (activeEffect) {
		// <------ Check to see if we have an activeEffect
		// We need to make sure this effect is being tracked.
		let depsMap = targetMap.get(target) // Get the current depsMap for this target
		if (!depsMap) {
			// There is no map.
			targetMap.set(target, (depsMap = new Map())) // Create one
		}
		let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
		if (!dep) {
			// There is no dependencies (effects)
			depsMap.set(key, (dep = new Set())) // Create a new Set
		}
		dep.add(activeEffect) // Add effect to dependency map
	}
}

function trigger(target, key) {
	const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
	if (!depsMap) {
		return
	}
	let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
	if (dep) {
		dep.forEach((effect) => {
			// run them all
			effect()
		})
	}
}

function reactive(target) {
	const handler = {
		get(target, key, receiver) {
			let result = Reflect.get(target, key, receiver)
			track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
			return result
		},
		set(target, key, value, receiver) {
			let oldValue = target[key]
			let result = Reflect.set(target, key, value, receiver)
			if (result && oldValue != value) {
				trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
			}
			return result
		},
	}
	return new Proxy(target, handler)
}

//对effect函数里的变量都来收集依赖
function effect(eff) {
	activeEffect = eff
	activeEffect()
	activeEffect = null
}

let product = reactive({ price: 5, quantity: 2 })
let total = 0

effect(() => {
	total = product.price * product.quantity
})

console.log('before updated quantity total = ' + total)
product.quantity = 3
console.log('after updated quantity total = ' + total)
console.log('Updated quantity to = ' + product.quantity)

在effect中执行eff, eff中会进行数据的get操作,从而在reactive中的get进行依赖收集。

当更改数据时,执行reactive中的set更新数据。

ref的实现

把上面例子中的total尝试改为用ref包裹的变量。

let total = 0

那么ref怎么实现呢? 方法一:

function ref(intialValue) {
  return reactive({ value: initialValue })

这种方法使用reactive来实现,也可以直接实现。

function ref(raw) {
  const r = {
    get value() {
      track(r, 'value')
      return raw
    },
    set value(newVal) {
      raw = newVal
      trigger(r, 'value')
    },
  }
  return r
}

完整demo如下。



const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
let activeEffect = null // The active effect running

function track(target, key) {
	if (activeEffect) {
		// <------ Check to see if we have an activeEffect
		// We need to make sure this effect is being tracked.
		let depsMap = targetMap.get(target) // Get the current depsMap for this target
		if (!depsMap) {
			// There is no map.
			targetMap.set(target, (depsMap = new Map())) // Create one
		}
		let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
		if (!dep) {
			// There is no dependencies (effects)
			depsMap.set(key, (dep = new Set())) // Create a new Set
		}
		dep.add(activeEffect) // Add effect to dependency map
	}
}

function trigger(target, key) {
	const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
	if (!depsMap) {
		return
	}
	let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
	if (dep) {
		dep.forEach((effect) => {
			// run them all
			effect()
		})
	}
}

function reactive(target) {
	const handler = {
		get(target, key, receiver) {
			let result = Reflect.get(target, key, receiver)
			track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
			return result
		},
		set(target, key, value, receiver) {
			let oldValue = target[key]
			let result = Reflect.set(target, key, value, receiver)
			if (result && oldValue != value) {
				trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
			}
			return result
		},
	}
	return new Proxy(target, handler)
}

//对effect函数里的变量都来收集依赖
function effect(eff) {
	activeEffect = eff
	activeEffect()
	activeEffect = null
}


function ref(raw) {
	const r = {
		get value() {
			track(r, 'value')
			return raw
		},
		set value(newVal) {
			raw = newVal
			trigger(r, 'value')
		},
	}
	return r
}

let product = reactive({ price: 5, quantity: 2 })
let total = ref()

effect(() => {
	total.value = product.price * product.quantity
})

console.log('before updated quantity total = ' + total.value)
product.quantity = 3
console.log('after updated quantity total = ' + total.value)
console.log('Updated quantity to = ' + product.quantity)

computed的实现

computed的实现,依赖于参数的执行结果。

computed的用法:

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

computed的实现依赖于ref:

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

完整demo:

const targetMap = new WeakMap() // targetMap stores the effects that each object should re-run when it's updated
let activeEffect = null // The active effect running

function track(target, key) {
	if (activeEffect) {
		// <------ Check to see if we have an activeEffect
		// We need to make sure this effect is being tracked.
		let depsMap = targetMap.get(target) // Get the current depsMap for this target
		if (!depsMap) {
			// There is no map.
			targetMap.set(target, (depsMap = new Map())) // Create one
		}
		let dep = depsMap.get(key) // Get the current dependencies (effects) that need to be run when this is set
		if (!dep) {
			// There is no dependencies (effects)
			depsMap.set(key, (dep = new Set())) // Create a new Set
		}
		dep.add(activeEffect) // Add effect to dependency map
	}
}

function trigger(target, key) {
	const depsMap = targetMap.get(target) // Does this object have any properties that have dependencies (effects)
	if (!depsMap) {
		return
	}
	let dep = depsMap.get(key) // If there are dependencies (effects) associated with this
	if (dep) {
		dep.forEach((effect) => {
			// run them all
			effect()
		})
	}
}

function reactive(target) {
	const handler = {
		get(target, key, receiver) {
			let result = Reflect.get(target, key, receiver)
			track(target, key) // If this reactive property (target) is GET inside then track the effect to rerun on SET
			return result
		},
		set(target, key, value, receiver) {
			let oldValue = target[key]
			let result = Reflect.set(target, key, value, receiver)
			if (result && oldValue != value) {
				trigger(target, key) // If this reactive property (target) has effects to rerun on SET, trigger them.
			}
			return result
		},
	}
	return new Proxy(target, handler)
}

//对effect函数里的变量都来收集依赖
function effect(eff) {
	activeEffect = eff
	activeEffect()
	activeEffect = null
}

function ref(raw) {
	const r = {
		get value() {
			track(r, 'value')
			return raw
		},
		set value(newVal) {
			raw = newVal
			trigger(r, 'value')
		},
	}
	return r
}

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

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

	return result
}


let product = reactive({ price: 5, quantity: 2 })
let total = computed(() => {
	return product.price * product.quantity
})

console.log('before updated quantity total = ' + total.value)
product.quantity = 3
console.log('after updated quantity total = ' + total.value)
console.log('Updated quantity to = ' + product.quantity)

参考资料

  1. github.com/Code-Pop/vu…
  2. www.bilibili.com/video/BV1SZ…
  3. v3.cn.vuejs.org/api/basic-r…
  4. developer.mozilla.org/zh-CN/docs/…