Vue3源码
compiler-core
vue3 编译的核心,字符串模板转AST,最后渲染成真实的代码块
reactivity
isReadOnly isReactive ref isRef
Q:为什么reactiveMap使用weakMap定义?
A:
weakMap接受对象类型作为key
无引用会被回收
runtime-dom
Vue3实现了VNode组成的VDOM,天然的支持了跨平台的能力,runtime-dom为跨平台提供了渲染器的能力
runtime-test
runtime-dom的延展,是对外提供runtime-dom的环境的测试,是为了方便测试runtime-core
实现一个响应式
实现一个effect来实现这个形式的响应式功能,下文中都是基于《Vue.js设计与实现》来实现的。
版本1
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effect = function (fn) {
activeEffect = fn
fn()
}
// 追踪
function track (target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let dep = depsMap.get(key)
if (!dep) {
// dep本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (dep = new Set()))
}
if(!dep.has(activeEffect)){
// 将激活的effectFn存进桶中
dep.add(activeEffect)
}
}
// trigger执行依赖
function trigger (target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) {
return
}
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 挨个执行即可
effects && effects.forEach((fn) => fn())
}
// 统一对外暴露响应式函数
function reactive (state) {
return new Proxy(state, {
get (target, key) {
const value = target[ key ]
track(target, key)
return value
},
set (target, key, newValue) {
target[ key ] = newValue
trigger(target, key)
return true
}
})
}
const data = reactive({name:'foo'})
effect(()=>{
console.log(`我变化了`+data.name);
})
data.name = 'foo2'
版本2
上述有一个什么问题呢,执行下面代码会多执行一次:
const state = reactive({
ok: true,
text: 'hello world',
});
effect(() => {
console.log('渲染执行')
console.log(state.ok ? state.text : 'not')
})
setTimeout(() => {
state.ok = false // 此时页面变成了not
setTimeout(() => {
state.text = 'other' // 页面依然是not,但是副作用函数却还会执行一次
}, 1000)
}, 1000)
为了解决这个问题,可以清空依赖重新收集一次,而调用effect
本身就会重新收集依赖
所以需要修改effect
和track
以及trigger
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effect = function (fn) {
const effectFn = function () {
cleanup(effectFn)
activeEffect = effectFn;
fn()
}
effectFn.deps = [];
// 在这里执行一次_effect,这样可以触发proxy的get执行dep收集
effectFn()
}
function cleanup(effectFn) {
const { deps } = effectFn
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn)
}
deps.length = 0
}
// 追踪
function track(target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let dep = depsMap.get(key)
if (!dep) {
// dep本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
// trigger执行依赖
function trigger(target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 再使用这个会无限循环,因为:
// 1.cleanup清空了effects,同时fn执行
// 2.fn是副作用函数effect,他的执行会触发get添加了新的fn
// 3.所以effects总是循环不完
// effects && effects.forEach((fn) => fn())
// 解决cleanup无限执行就很简单了,直接取出来执行就好,
// 源码里面是定义了一个add方法循环effects取出来的
const effectsToRun = new Set(effects)
// 挨个执行即可
effectsToRun.forEach((fn) => fn());
}
// 统一对外暴露响应式函数
function reactive(state) {
return new Proxy(state, {
get(target, key) {
const value = target[key]
track(target, key)
return value
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
为什么这里text
修改不触发副作用函数呢?因为state.ok = false
,这个时候先触发set,副作用函数走的同时在此触发get,而这个时候三元运算读不到text
,所以他的依赖就消失了
版本3
此时还不支持effect嵌套,首先要知道vue中组件执行:
const Foo = {
render () {
return // ....
}
}
effect(() => {
Foo.render()
})
当组件发生嵌套时,就会存在effect嵌套:
const Bar = {
render () {
return // ....
}
}
const Foo = {
render () {
return <Bar /> // ...
}
}
effect(() => {
Foo.render()
effect(() => {
Bar.render()
})
})
在版本2代码中执行以下代码:
const state = reactive({
foo: true,
bar: true
})
effect(function effectFn1() {
console.log('effectFn1')
effect(function effectFn2() {
console.log('effectFn2')
console.log('Bar', state.bar)
})
console.log('Foo', state.foo)
})
state.foo = false;
state.bar = false;
会发现输出是:
// effectFn1
// effectFn2
// Bar true
// Foo true
// effectFn2
// Bar true
// effectFn2
// Bar false
问题来了,修改foo为什么也是输出bar呢?
很明显是因为activeEffect的问题导致的,当effectFn1
执行的时候会后执行effectFn2
;
那么我等effectFn2
执行完再指向effectFn1
不就好了,这就是一个栈顶指针!!!
所以我们对effcet
做一下修改:
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effectStack = []
const effect = function (fn) {
const effectFn = function () {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = [];
// 在这里执行一次_effect,这样可以触发proxy的get执行dep收集
effectFn()
}
function cleanup(effectFn) {
const { deps } = effectFn
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn)
}
deps.length = 0
}
// 追踪
function track(target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let dep = depsMap.get(key)
if (!dep) {
// deps本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
// trigger执行依赖
function trigger(target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 再使用这个会无限循环,因为cleanup清空了effects,同时fn执行又添加了新的fn
// effects && effects.forEach((fn) => fn())
// 解决cleanup无限执行就很简单了,直接取出来执行就好,'
// 源码里面是定义了一个add方法循环effects取出来的
const effectsToRun = new Set(effects)
// 挨个执行即可
effectsToRun.forEach((fn) => fn());
}
// 统一对外暴露响应式函数
function reactive(state) {
return new Proxy(state, {
get(target, key) {
const value = target[key]
track(target, key)
return value
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
const state = reactive({
foo: true,
bar: true
})
effect(function effectFn1() {
console.log('effectFn1')
effect(function effectFn2() {
console.log('effectFn2')
console.log('Bar', state.bar)
})
console.log('Foo', state.foo)
})
setTimeout(() => {
state.foo = false
}, 1000)
最后的输出:
// effectFn1
// effectFn2
// Bar true
// Foo true
// effectFn1
// effectFn2
// Bar true
// Foo false
实际上我觉得这里的输出还是存在其他问题的,为什么要输出bar,
未完待续研究吧。
版本4
上述代码在遇到这个的时候会无线递归:
const data = reactive({ foo: 1 })
effect(() => obj.foo++)
这是为什么呢?我们来简单分析一下:
obj.foo++
等价于obj.foo = obj.foo + 1
- 先触发effect,那么就会
effectStack.push()
然后走到fn()
,执行内部的方法 - 从右往左执行,先读取值触发
track
,然后设置值触发trigger
,这个时候注意trigger
里面还是会触发fn()
- 所以3会接着走3无限反复,同时呢因为
effectStack.pop()
永远执行不到进而导致爆栈 - 因此解决问题的关键在于解决关键的问题,我只要阻断调用不就好咯?
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effectStack = []
const effect = function (fn) {
const effectFn = function () {
// if (activeEffect == effectFn) return // 解决方案1,直接放开这行
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = [];
// 在这里执行一次_effect,这样可以触发proxy的get执行dep收集
effectFn()
}
function cleanup(effectFn) {
const { deps } = effectFn
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn)
}
deps.length = 0
}
// 追踪
function track(target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let dep = depsMap.get(key)
if (!dep) {
// deps本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
// trigger执行依赖
function trigger(target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
// 解决方案2,放开这行,并且把下面两行注释
// const effectsToRun = new Set()
// effects && effects.forEach(effectFn => {
// // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
// if (effectFn !== activeEffect) { // 新增
// effectsToRun.add(effectFn)
// }
// })
// effectsToRun.forEach(effectFn => effectFn())
// 放开上面注释这里
const effectsToRun = new Set(effects)
effectsToRun.forEach((fn) => fn());
}
// 统一对外暴露响应式函数
function reactive(state) {
return new Proxy(state, {
get(target, key) {
const value = target[key]
track(target, key)
return value
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
const state = reactive({
foo: 1,
bar: true
})
effect(function effectFn1() {
state.foo = 5;
console.log(state.foo)
})
state.foo = 3
state.foo = 6
先说结论,官方使用的第二种方案。
那么第一种方案的问题在哪呢?
- 首先上来一定会走一次
effect
,然后呢会出现递归,递归先走的trigger
,后走fn()
,所以从trigger
打断可以让函数栈-1, - 从架构上来说,易于维护:集中管理副作用函数的执行逻辑,使得代码更加清晰和易于维护
- 跟2差不多,在
trigger
里面集中处理函数,让effect
专心执行
版本5
最后可以聊一聊调度性,即自主控制副作用函数执行的时机。可以让你主动选择什么时候,执行某个副作用函数多少次。
const bucket = new WeakMap()
// 重新定义bucket数据类型为WeakMap
let activeEffect
const effectStack = []
const effect = function (fn, options = {}) {
const effectFn = function () {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
// 将options参数挂在effectFn上,便于effectFn执行时可以读取到scheduler
effectFn.options = options
// 在这里执行一次_effect,这样可以触发proxy的get执行dep收集
effectFn()
}
function cleanup(effectFn) {
const { deps } = effectFn
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effectFn)
}
deps.length = 0
}
// 追踪
function track(target, key) {
// activeEffect无值意味着没有执行effect函数,无法收集依赖,直接return掉
if (!activeEffect) {
return
}
// 每个target在bucket中都是一个Map类型: key => effects
let depsMap = bucket.get(target)
// 第一次拦截,depsMap不存在,先创建联系
if (!depsMap) {
bucket.set(target, (depsMap = new Map()))
}
// 根据当前读取的key,尝试读取key的effects函数
let dep = depsMap.get(key)
if (!dep) {
// deps本质是个Set结构,即一个key可以存在多个effect函数,被多个effect所依赖
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
// 收集当前激活的 effect 作为依赖
dep.add(activeEffect)
// 当前激活的 effect 收集 dep 集合作为依赖
activeEffect.deps.push(dep)
}
}
// trigger执行依赖
function trigger(target, key) {
// 读取depsMap 其结构是 key => effects
const depsMap = bucket.get(target)
if (!depsMap) return
// 真正读取依赖当前属性值key的effects
const effects = depsMap.get(key)
const effectsToRun = new Set()
effects && effects.forEach(effectFn => {
// 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
if (effectFn !== activeEffect) { // 新增
effectsToRun.add(effectFn)
}
})
const run = (effectFn) => {
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
}
// 挨个执行即可
effectsToRun.forEach(run)
}
// 统一对外暴露响应式函数
function reactive(state) {
return new Proxy(state, {
get(target, key) {
const value = target[key]
track(target, key)
return value
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
const jobQueue = new Set()
const p = Promise.resolve()
let isFlushing = false
const flushJob = () => {
if (isFlushing) return
isFlushing = true
// 微任务
p.then(() => {
jobQueue.forEach((job) => job())
}).finally(() => {
// 结束后充值设置为false
isFlushing = false
})
}
const state = reactive({
num: 1
})
effect(() => {
console.log('num', state.num)
}, {
scheduler(fn) {
// 每次数据发生变化都往队列中添加副作用函数
jobQueue.add(fn)
// 并尝试刷新job,但是一个微任务只会在事件循环中执行一次,
// 所以哪怕num变化了100次,最后也只会执行一次副作用函数
flushJob()
}
})
let count = 100
while (count--) {
state.num++
}
输出为:
// num 1
// num 101
继续,computed
他有什么炫酷的特效么:
- 依赖追踪
- 缓存结果
- 懒计算(如果不被引用,就不会执行,坑过我很多次了,一度以为热更新失效了)
懒计算
const state = reactive({
a: 1,
b: 2,
c: 3
})
// 有没有很像计算属性的感觉
const sum = effect(() => {
console.log('执行计算') // 立刻被打印
const value = state.a + state.b
return value
})
console.log(sum) // undefined
诶嘿没有值,那么要解决咋搞捏,很明显搞个主动触发函数,并且返回一个返回值就行了。
const effect = function (fn, options = {}) {
const effectFn = () => {
// ... 省略
// 新增res存储fn执行的结果
const res = fn()
// ... 省略
// 新增返回结果
return res
}
// ... 省略
// 新增,只有lazy不为true时才会立即执行
if (!options.lazy) {
effectFn()
}
// 新增,返回副作用函数让用户执行
return effectFn
}
const state = reactive({
a: 1,
b: 2,
c: 3,
});
// 有没有很像计算属性的感觉
const sum = effect(() => {
console.log("执行计算"); // 调用sum函数后被打印
const value = state.a + state.b;
return value;
}, {
lazy: true
});
// 不执行sum函数,effect注册的回调将不会执行
console.log(sum()); // 3
依赖追踪
首先封装一个function:
function computed (getter) {
const effectFn = effect(getter, {
lazy: true,
})
const obj = {
get value () {
return effectFn()
}
}
return obj
}
测试一下
const state = reactive({
a: 1,
b: 2,
c: 3
})
const sum = computed(() => {
console.log('执行计算')
return state.a + state.b
})
console.log(sum.value)
console.log(sum.value)
// 执行计算
// 3
// 执行计算
// 3
缓存
特性:
- 只有当其依赖的东西发生变化了才需要重新计算
- 否则就返回上一次执行的结果
缓存上次的结果很简单,搞个值存一下就好了,熟悉的dirty
来了;
但是怎么知道依赖变化了重新计算?
function computed (getter) {
const effectFn = effect(getter, {
lazy: true,
})
let value
let dirty = true
const obj = {
get value () {
// 只有数据发生变化了才去重新计算
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
试一试?
const state = reactive({
a: 1,
b: 2,
c: 3
})
const sum = computed(() => {
console.log('执行计算')
return state.a + state.b
})
console.log(sum.value) // 3
state.a = 4
console.log(sum.value) // 3 答案是错误的
当然就算这个也只会执行一次,dirty什么时候变回去呢?
答案是引入强大的调度器。
function computed (getter) {
const effectFn = effect(getter, {
lazy: true,
// 数据发生变化后,不执行注册的回调,而是执行scheduler
scheduler () {
// 数据发生了变化后,则重新设置为dirty,那么下次就会重新计算
dirty = true
}
})
let value
let dirty = true
const obj = {
get value () {
// 只有数据发生变化了才去重新计算
if (dirty) {
value = effectFn()
dirty = false
}
return value
}
}
return obj
}
再试一下:
const state = reactive({
a: 1,
b: 2,
c: 3
})
const sum = computed(() => {
console.log('执行计算')
return state.a + state.b
})
console.log(sum.value) // 3
state.a = 4
console.log(sum.value) // 6 好了
watch
watch可以直接监听一个对象,也可以监听对象的一个属性,这就不演示了,直接上Faker和四个菜:
const state = reactive({
name: '111'
})
effect(() => {
// 原本state发生变化之后,应该执行这里
console.log("raw:" + state.name)
}, {
// 但是指定scheduler之后,会执行这里
scheduler() {
console.log("scheduler:" +state.name)
}
})
state.name = '222'
结果:
// raw:111
// scheduler:222
支持单属性+回调
const watch = (source, cb) => {
effect(()=>source.name, {
scheduler () {
cb()
},
})
}
// 测试一波
const state = reactive({
name: '111',
})
watch(state, () => {
console.log('state.name发生了变化', state.name)
})
state.name = '222'
通用性
但是这样没有通用性,那么需要用一个方法协助访问一个对象内部所有的属性:
function traverse(value, seen = new Set()) {
// 如果要读取的数据是原始值,或者已经被读取过了,那么什么都不做
if (typeof value !== 'object' || value === null || seen.has(value)) return
// 将数据添加到 seen 中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
// 暂时不考虑数组等其他结构
// 假设 value 就是一个对象,使用 for...in 读取对象的每一个值,并递归地调用 traverse 进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
function watch(source, cb) {
effect(() => traverse(source), {
// 但是指定scheduler之后,会执行这里
scheduler() {
cb()
}
})
}
支持对象+回调
const watch = (source, cb) => {
let getter
// 处理传回调的方式
if (typeof source === "function") {
getter = source
} else {
// 封装成读取source对象的函数,触发任意一个属性的getter,进而搜集依赖
getter = () => traverse(source)
}
const effectFn = effect(() => getter(), {
scheduler() {
cb()
}
})
}
const state = reactive({
name: "111",
age: 100,
obj2: {
name: "222",
age: 10,
},
})
watch(state, () => {
console.log("state发生变化了");
});
console.log("第一层");
state.name = 1
console.log("第二层");
state.obj2.name = 2
结果发现 state.obj2.name = 2
这里并没有触发watch
实际上,上文中的 bucket
是这样的:

问题在哪呢,打断点会发现,诶?关键问题在于reactive
函数中,他把对象也当属性塞进去了,那咋搞呢?递归!!!
function reactive(state) {
return new Proxy(state, {
get(target, key) {
const value = target[key]
// 这里要注意无论key是对象还是string等都要劫持
// 否则你的属性后面跟着对象,你把这个属性改成5,监测不到
track(target, key)
if (value !== null && typeof value === 'object') {
return reactive(value)
}
return value
},
set(target, key, newValue) {
target[key] = newValue
trigger(target, key)
return true
}
})
}
再试一次上面测试用例就好咯,个中具体原因自己打断点研究吧。
新值和旧值
别忘了这个还没实现,那么上代码:
const watch = (source, cb) => {
let getter, oldValue, newValue
// 处理传回调的方式
if (typeof source === "function") {
getter = source
} else {
// 封装成读取source对象的函数,触发任意一个属性的getter,进而搜集依赖
getter = () => bfs(source)
}
const effectFn = effect(getter, {
scheduler() {
newValue = effectFn()
cb(newValue, oldValue)
oldValue = newValue
}
})
// 第一次执行获取值
oldValue = effectFn()
}
const state = reactive({
name: "111",
age: 100,
obj2: {
name: "222",
age: 10,
},
})
watch(() => state.obj2.name, (newVal,oVal) => {
console.log("发生变化了" + oVal + "||" + newVal); // 发生变化了222||1
});
state.obj2.name = 1
立即调用
const watch = (source, cb, options = {}) => {
let getter, oldValue, newValue
// 处理传回调的方式
if (typeof source === "function") {
getter = source
} else {
getter = () => bfs(source)
}
const job = () => {
// 变化后获取新值
newValue = effectFn()
cb(newValue, oldValue)
// 执行回调后将新值设置为旧值
oldValue = newValue
}
const effectFn = effect(getter, {
lazy: true,
scheduler: job
})
// 如果指定了立即执行,便执行第一次
if (options.immediate) {
job()
} else {
oldValue = effectFn()
}
}
测试:
watch(() => state.obj2.name, (newVal,oVal) => {
console.log("发生变化了" + oVal + "||" + newVal);
}, { immediate: true });
state.obj2.name = 1
// 发生变化了222||undefined
// 发生变化了1||222
未完待续,比如deep
,或者source
传递一个数组都没实现,下次一定
后续
继续研究vue3源码,点击 跳转