前言
Vue 3.0 的源码已经公布出来 链接,大家感叹学不动了,但是还是要趁热学起来。当然在了解源码之前,还是要熟悉下新的API说明 首先科普下调试方法,先安装好依赖,然后
- 修改
rollup.config.js
const externals = Object.keys(aliasOptions).filter(p => p !== '@vue/shared')
output.sourcemap = true // 新加这行
return {
input: resolve(`src/index.ts`),
- tsconfig.json 文件设置
"sourceMap": true - 新建自己的测试页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="../packages/vue/dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { reactive, computed, effect, watch, createApp, h } = Vue
const App = {
setup() {
const state = reactive({
count: 0
})
function increment(e) {
state.count++
}
effect(() => {
console.log('count改变', state.count);
})
return (props, slots, attrs, vnode) => (
h('button', {
onClick: increment
}, state.count)
)
}
}
createApp().mount(App, '#app')
</script>
</body>
</html>
yarn run dev就可以愉快的断点调试了
然而,本文并不是要一行一行的分析 Vue 3.0 的源码,而是想要以自己的语言来讲清楚 Vue 3.0 的响应式原理。
Reactive 的实现
let a = reactive({ name: 'hello' })
console.log(b.name) // 'hello'
在 Vue 3.0 中使用 reactive 方法来创建响应式变量,熟悉 2.x 的肯定知道,在 2.x 里面是在 data 里返回一个对象,初始化的时候会遍历这个对象,使用 Object.defineProperty 来重写 get 和 set,对于嵌套的对象,会递归的调用,使值也变成响应式的。但是,使用 Object.defineProperty 存在问题,一个是新增的对象无法直接变为响应式的,需要调用 Vue.set,二个是无法检测数组的变化,vue 劫持了原生数组,来实现对数组的变化检测。但是都不是很好的方法。Vue 3.0 使用了 Proxy 来拦截数据的变化,代码变得简洁了很多。
function reactive(obj) {
return createReactiveObject(obj)
}
function createReactiveObject(obj) {
// 不是对象则直接返回
if (!isObject(obj)) {
return obj
}
const handlers = {
get(obj, key) {
console.log('----------get---------')
console.log(key)
let result = Reflect.get(obj, key)
// 如果值为对象则继续调用 reactive 方法使其变为响应式
return isObject(result) ? reactive(result) : result
},
set(obj, key, value) {
console.log('----------set----------')
console.log(key, value)
let result = Reflect.set(obj, key, value)
return result
},
deleteProperty(obj, key) {
return Reflect.deleteProperty(obj, key)
}
}
let proxy = new Proxy(obj, handlers)
return proxy
}
let a = reactive({ name: 'hello' })
console.log(a.name)
a.name = 'world'
----------get---------
name
hello
----------set----------
name world
这里使用 Proxy 实现了一个简单的 reactive 函数,当访问 a.name 时,会触发get,当使用 a.name = 'world' 时触发 set,里面使用了 ES6 的 Reflect.set 和 Reflect.get 来赋值和取值,这个比单纯的使用对象操作符要好些,它与 Proxy的方法是相对应的,并且有返回。
let obj = { name: 'hello' }
let a = reactive(obj)
let b = reactive(obj)
let c = reactive(a)
这样存在几个问题,首先,多次调用 reactive(obj) 时,还是会重复执行,其次,将代理过的对象继续代理。这时就需要对于已经代理过的对象就不进行处理。
const toProxy = new WeakMap()
const toRaw = new WeakMap()
function createReactiveObject(obj) {
// 不是对象则直接返回
if (!isObject(obj)) {
return obj
}
let observed = toProxy.get(obj)
// 说明当前对象已经被代理过
if (observed) return observed
// 说明当前就是代理对象
if (toRaw.has(obj)) return obj
const handlers = {
get(obj, key) {
console.log('----------get---------')
console.log(key)
let result = Reflect.get(obj, key)
return isObject(result) ? reactive(result) : result
},
set(obj, key, value) {
console.log('----------set----------')
console.log(key, value)
let result = Reflect.set(obj, key, value)
return result
},
deleteProperty(obj, key) {
return Reflect.deleteProperty(obj, key)
}
}
let proxy = new Proxy(obj, handlers)
toProxy.set(obj, proxy)
toRaw.set(proxy, obj)
return proxy
}
这里使用了 WeakMap 数据结构,它类似于对象,但是 key 只能为对象,且其值为弱引用,不计入垃圾回收机制。 toProxy.set(obj, proxy); toRaw.set(proxy, obj)在代理成功后,会将对象及代理后的对象存在两个变量中,在执行代理对象之前,会在这两个 weakmap 里查找,是否已经存在,如果 toProxy 里存在,则说明改对象已经被代理过,直接返回对应的值即可,如果 toRaw 里存在,则说明改对象已经是个被代理后的对象,直接返回该对象。
effect的实现
let obj = { name: 'hello' }
let a = reactive(obj)
effect(() => {
console.log(a.name)
})
a.name = 'world'
// hello
// world
effect 意思是副作用,初始会执行一次,然后当 name值变化时,会执行一次,这就是响应式原理的另一块,依赖搜集和依赖执行。在 vue 2.0 中,已 computed 举例,首先会执行一下 getter 方法,此时会访问响应式变量的值,触发 get,在 get 里搜集依赖,然后当变量改变时,会触发 set,在 set 里执行依赖,从而使值可以随着另一个值的变化而变化。其实在 3.0 里也类似。
const toProxy = new WeakMap()
const toRaw = new WeakMap()
const targetMap = new WeakMap()
function createReactiveObject(obj) {
// 不是对象则直接返回
if (!isObject(obj)) {
return obj
}
let observed = toProxy.get(obj)
// 说明当前对象已经被代理过
if (observed) return observed
// 说明当前就是代理对象
if (toRaw.has(obj)) return obj
const handlers = {
get(obj, key) {
console.log('----------get---------')
console.log(key)
let result = Reflect.get(obj, key)
track(obj, key) // 搜集依赖
return isObject(result) ? reactive(result) : result
},
set(obj, key, value) {
console.log('----------set----------')
console.log(key, value)
let result = Reflect.set(obj, key, value)
trigger(obj, key) // 执行依赖
return result
},
deleteProperty(obj, key) {
return Reflect.deleteProperty(obj, key)
}
}
let proxy = new Proxy(obj, handlers)
toProxy.set(obj, proxy)
toRaw.set(proxy, obj)
return proxy
}
let activeEffectStacks = []
function track(obj, key) {
let effect = activeEffectStacks[activeEffectStacks.length - 1]
if (effect) {
let depsMap = targetMap.get(obj)
// 如果不存在,则为该对象新建一个 map
if (!depsMap) {
targetMap.set(obj, depsMap = new Map())
}
let dep = depsMap.get(key)
// 如果该属性不存在则为该属性新建一个 set
if (!dep) {
depsMap.set(key, dep = new Set())
}
if (!dep.has(effect)) {
dep.add(effect)
}
}
}
function trigger(obj, type, key) {
let depsMap = targetMap.get(obj)
if (!depsMap) return
let deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => {
effect()
})
}
}
function effect(fn, options) {
let effect = createReactiveEffect(fn, options)
effect()
return effect
}
function createReactiveEffect(fn, options) {
let effect = function() {
return run(effect, fn)
}
return effect
}
function run(effect, fn) {
try {
activeEffectStacks.push(effect)
fn()
} finally {
activeEffectStacks.pop()
}
}
effect函数包装了一下fn,因为之后 effect 上还有一些其他的参数需要定义,所以使用createReactiveEffect来包装一下,第66行进行初次执行,这里使用activeEffectStacks数组来保存 effect 自身,执行函数体后,会访问 a.name,此时就触发了依赖搜集(17 行),这里使用了 targetMap 来保存对象以及其属性的依赖,结构类似于
{
obj1: {
a: [fn1, fn2],
b: [fn2, fn4]
},
obj2: {
c: [fn5]
}
}
这里分别使用了 WeakMap、Map、Set,如 39~51 行,当函数执行完毕后会将其从 activeEffectStacks 弹出,这样就可以保证依赖正确。其实这里和 vue 2.0 里类似,Vue 2.0 使用了 Dep.target 来保存这个依赖,搜集完毕后会将其赋为 null,trigger 函数就比较好理解了,触发 set 时从 targetMap 取出对象和属性对应的 set 集合,执行里面的每一个函数。 这里存在着一个问题,对于数组来说,set 会触发两次,一次是新增 index = 3 的值,一次是将 length 设为 4,这样就会导致依赖被执行两次。
let c = reactive([1, 2, 3])
c.push(4)
----------get---------
push
----------get---------
length
----------set----------
3 4
----------set----------
length 4
这里稍作修改,将这两种分为两类,一种是新增(第一个set),一种是修改(第二个set)。如何区分这两者呢?其实只需要判断该对象上是否存在该值即可,修改 set 函数如下
set(obj, key, value) {
console.log('----------set----------')
console.log(key, value)
let oldValue = obj[key]
let hasKey = obj.hasOwnProperty(obj, key)
let result = Reflect.set(obj, key, value)
if (!hasKey) {
trigger(obj, 'add', key)
} else if (oldValue !== value) {
trigger(obj, 'set', key)
}
return result
}
function trigger(obj, type, key) {
let depsMap = targetMap.get(obj)
if (!depsMap) return
let deps = depsMap.get(key)
if (deps) {
deps.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect()
}
})
}
if (type === 'add') {
let deps = depsMap.get('length')
if (deps) {
deps.forEach(effect => effect())
}
}
}
修改 trigger 函数,如果 type 是新增,则上半部分的key肯定早不到对应的 deps,下半部分取 length,执行依赖。
ref的实现
ref 可以将原始类型转换为响应式数据
let r = ref(1)
console.log(r.value)
effect(()=> {
console.log(r.value)
})
r.value = 2
1
1
2
原理也很简单,使用一个对象包装一层,访问对象的 value 值即为真实的值
function ref(raw) {
raw = isObject(raw) ? reactive(raw) : raw
const v = {
_isRef: true, // 标识为 ref 类型
get value() {
track(v, 'get', '')
return raw
},
set value(newValue) {
raw = newValue
trigger(v, 'set', '')
}
}
return v
}
这时存在一个问题, 假如对象为
let r = ref(1)
let c = reactive({ b: r })
此时每次访问 r 还得使用 c.b.value 这时只需要改写 get,在返回值的时候判断如果是 ref 类型,直接返回其 value值
if (result._isRef) {
return result.value
}
computed的实现
在 Vue 2.0 里,computed 简直不要太好用,在 3.0 里 用法稍许不同
// computed 是懒执行,不改变值不会执行
let a = reactive({ foo: 0 })
let c = computed(()=>{
console.log('执行')
return a.foo + 1
})
// 不取不执行,取n次只执行一次
console.log(c.value)
console.log(c.value)
computed是惰性的,也就是说只有在取 c.value 时,函数体才会执行,如果其依赖的 a.foo 的值不变,则不管访问多少次 c.value,computed内部的函数都只会执行一次,computed使用上面的 effect函数来实现
function computed(getter) {
let dirty = true
const runner = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true
}
})
let value
return {
_isRef: true,
get value() {
if (dirty) {
value = runner()
dirty = false
}
return value
}
}
}
修改 effect 方法 和 trigger 方法
function effect(fn, options) {
let effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}
function createReactiveEffect(fn, options) {
let effect = function() {
return run(effect, fn)
}
effect.scheduler = options.scheduler
return effect
}
deps.forEach(effect => {
if (effect.scheduler) {
effect.scheduler()
} else {
effect()
}
})
这里使用了 effect 来包装 getter,使其变成响应式的,和 ref 类似,返回了一个包装对象,当读取 value 的值的时候,才会执行,计算最新的值。由于 computed 是惰性求值,所以在使用 effect 包装的时候,加了个参数 lazy: true 此时在 effect 函数里如果 lazy 为 true 则并不会执行。再一点,如果依赖的值不发生变化,则computed 不会重新求值。源码设计的非常巧妙,使用了一个标识 dirty,初始 dirty 为 true,则第一次读取 computed 值的时候会计算,并将 dirty 设为 false,在使用 effect 包装的时候,会在 effect 上添加 scheduler 方法,第一次取值的时候,会搜集对应变量的依赖,即该 effect,只有当这个变量发生改变的时候,才会触发 trigger 执行依赖,此时如果有scheduler 则会执行 scheduler 将 dirty 设为 true,这样下次读取 computed 的值的时候才会重新执行getter,获取最新的值。否则 dirty 会一直为 false,返回的值仍然为之前计算的值。
以上就是 vue 3.0 的响应式原理解析,把主要原理部分抽取出来了,便于理清原理。实际的源码还有很多细节的地方处理,这部分代码主要在 packages/reactvity 里,各个函数分的很清晰,推荐自己按照上面的方法自己写些demo,调试一下,里面的单元测试也可以对照着看,以便更好的了解用法。
附完整代码