# Vue.js 设计与实现的读书分享
**Repository Path**: wenjun00/Vue_strength
## Basic Information
- **Project Name**: Vue.js 设计与实现的读书分享
- **Description**: 这里记录了《Vue.js设计与实现》的读书分享,希望能够更新完毕呢
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 1
- **Created**: 2023-02-20
- **Last Updated**: 2023-02-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Vue.js设计与实现
# 1. 权衡的艺术
## 1.1 命令式和声明式
**命令式框架**:关注的是过程。代码本身就是描述做事的过程。jQuery就是命令式的框架
1. 获取到div
2. 设置文本的内容
3. 绑定点击的事件
```js
$('app')
.text('hello')
.on('click', () => alert('hello'))
```
```js
const div = document.querySelector('#app')
div.innerHTML = 'hello world'
div.addEventListener('click', () => { alert('ok') })
```
**声明式框架**: 关注结果。
1. hey,vue.js,帮我把div这个元素的内容设置为hello world;帮我绑定一个点击事件,
2. @click这样的指令,内部已经帮我们实现好了
3. 内部一定是**命令式的**,但是暴露给我们开发者的一定是**声明式的**
```js
alert('ok')">hello world
```
## 1.2 性能和可维护性的权衡
**命令式和声明式的性能,谁的更好?**
声明式代码的性能不优于命令式。
**命令式的操作**
```js
div.textContext = 'Hello World'
```
**声明式的操作**
```vue
{{msg}}
```
1. 为了达成修改内容的目标,命令式的操作的性能是最高的
2. 声明式本质上,是对命令式进行了封装
3. 声明式的操作,还有多的一步,就是去比较新旧dom的差异,找到差异这一步,是命令式没有的
- 直接更新内容的性能消耗是A
- **找到差异的性能消耗是B**。那么命令式的性能消耗就是A,声明式的性能消耗就是A + B
`所以声明式的框架的性能消耗反而更差?vue为什么要设计为声明式的框架呢?`
1. 如果声明式的,**找到差异的性能消耗B的性能**消耗能够**降低为0**,那么其实是可以追赶上命令式的。但是无法超越。
1. vue.js选择声明式,因为其代码更好去维护。采用命令式的代码,要手动写代码的增删改查,但是声明式,直接就是各种指令,封装好的方法,让你去使用。具体怎么实现,vue内部帮我们去实现了。
1. `框架设计者如何权衡`: 在**保持可维护性**的同时让性能损失最小化。s
## 1.3 虚拟dom的性能到底如何?
**虚拟dom存在的目的到底是为了什么?**
1.声明式代码的性能 = 找出差异的性能消耗 + 直接修改的性能消耗
2.虚拟dom就是让我们**找出差异的性能最**小化
3.没有极致的写出命令式的代码,特别是项目很大时,或者是精力投入很大,投入产出比不高。而虚拟dom,能够让框架:
- 性能不至于太差,甚至逼近命令式的性能
- 不用写代码太费劲
### 创建页面
**innerHTML创建页面是什么样的呢?**
```js
const html = `
`
div.innerHTML = html
```
1.构造一个js字符串的运算量
2.字符串赋值给dom的innerHTML属性,dom操作的运算量。设计dom的运算远远比纯JS的运算性能差的很多。
下面的图片我们就可以看到。
掘进地址:https://juejin.cn/post/6844904112023142407 -> 测试代码的性能的地址
虚拟dom,**创建dom元素**时的运算量?
1. 创建JS对象,描述真实DOM
```js
{sel: '', children: '', text: ''}
```
2. 递归遍历虚拟DOM树,创建真实的DOM。虚拟dom也是必须要经历创建的阶段的。
3. 创建页面时,无论是纯JS还是DOM运算,虚拟DOM和innerHTML差别不大
| | 虚拟DOM | innerHTML |
| ---------------- | ------------------------- | ----------------- |
| 纯JavaScript运算 | 创建JavaScript对象(Vnode) | 渲染HTML字符串 |
| DOM运算 | 新建所有的DOM元素 | 新建所有的DOM元素 |
### 更新页面
**innerHTML更新页面时:**
1. 重新构建HTML字符串,再重新设置DOM元素的innerHTML属性。哪怕只更新了一个文字,也要重新设置innerHTML => 等价于,销毁所有的旧的DOM元素,再全量创建新的DOM元素
**虚拟DOM更新页面时:**
1. 创建新的JS对象
2. 比较新旧的虚拟DOM,找到变化的元素,再去更新它。`虚拟DOM的优势`,就是不会全部销毁旧的DOM
| | 虚拟DOM | innerHTML |
| -------- | ------------------------------------- | ------------------------------------ |
| 纯JS运算 | 创建新的JS对象,`diff算法` | 重新拼接字符串 |
| DOM运算 | 比较新旧的dom差异,再去更新不同的部分 | 销毁所有的旧的dom,更新所有的新的dom |
| 性能因素 | 与数据变化量有关系 | 与页面大小,模板大小有关系 |
3. innerHTML是模板越大,性能越差。虚拟DOM是和数据变化量有关系的
| | innerHTML | 虚拟dom | 原生的JS |
| -------- | --------- | ------- | -------- |
| 心智负担 | 中等 | 小 | 大 |
| 可维护性 | | 强 | 差 |
| 性能 | 差 | 还不错 | 特别好 |
1. 原生的JS,手动操作DOM操作,增加、删除、修改大量的DOM元素。但是性能是最高的,但是我们要承受很大的心智负担,并且可以维护性就很低
2. 虚拟dom,可以维护性很强,我们不用管很多底层的设计,使用声明式的语法,心智负担很小。性能也相当不错,有比较新旧虚拟dom的过程
3. innerHTML,一部分是通过拼接HTML字符串来实现的,有点“声明式”,但是还是有心智负担。如果页面很大,少量的内容更改,都需要完全替换DOM,性能是最差的。
```js
const str = `
`
```
小结:
1. 原生js操作dom和虚拟dom,和innerHTML拼接字符串操作dom,谁的性能高低不能直接下定论。上面我们分为,心智负担,可维护性,性能进行了权衡。
2. 虚拟dom在“找到差异”的性能上做出了很大的努力,性能还不赖,可维护最强,心智负担小。
3. 原生js操作dom,心智负担最大,性能是最好的,可维护性是很差的
4. innerHTML操作dom时,心智负担也很大,性能很差,可维护性也不好。所以虚拟dom是一个不错的选择
5. 页面的html字符串越大时,innerHTML的性能损耗就会越大。
## 1.4 运行时和编译时
1.有三种类型的框架,纯运行时的、运行时+编译时的、纯编译时的。需要你对目标框架的特征、目标框架的期望来做出选择
**纯运行时的框架**
1.obj是一个对象,里面的children可以是数组也可以是text
2.我们把obj传递给Render函数使用
3.obj.children分为字符串和数组两种情况
4.用户直接传入obj对象,不需要其他的操作,直接运行就可以执行这段代码
```js
let obj = {
tag: 'div',
childern: [
{tag: 'span', children: 'hello world'}
]
}
function Render (obj, root) {
let el = document.createElement(obj.tag)
if (typeof obj.children === 'text') {
let text = document.createTextNode(obj.children)
el.appendChild(text)
} else if (typeof obj.children === 'array') {
// 把children里面的元素放到el里面去
obj.children.forEach(child => Render(obj, el))
}
root.appendChild(el)
}
Render(obj, document.body)
```
**编译+运行时的框架是怎么回事呢?**
1. 如果用户希望把html标签直接转化为obj对象,而不是我们自己手写,那么就需要额外的一个函数去编译html标签,这个时候比如还需要一个Compiler函数,这时就有编译啦
```html
Hello world
-->
let obj = {
tag: 'div',
childern: [
{tag: 'span', children: 'hello world'}
]
}
```
2. 用户分别调用**Compiler**函数和**Render**函数,这个时候就是一个**运行时+编译时**的框架
运行时,用户传入obj对象,无需编译
编译时,把html字符串转化为obj对象,再交给运行时处理。**(代码在运行的时候编译的,这里会产生性能开销)**
在代码构建的时候,就执行Compiler程序,提前编译好HTML为对象,运行时,就直接执行处理obj对象了,这对于性能是很好的。
```js
const obj = Compiler(html)
Render(obj, document.body)
```
**纯编译是怎么回事呢?**
1. 把html字符串 -> 对象 -> 命令式的代码 中间的对象砍掉,直接 html字符串 -> 命令式的代码
2. 用户给的代码必须要经过编译器翻译才能执行,只需要一个Compiler函数就好了
```js
const div = document.createElement('div')
const span = document.createElement('span')
span.innerText = 'hello world'
div.appendChild(span)
document.body.appendChild(div)
```
**哪一个框架是更好的?**
1. 纯运行的,没有编译的过程,无法分析用户提供的内容(不理解)
2. 如果有编译的过程,就可以分析,哪一部分未来可能是发生改变的,哪一部分未来是不会发生改变的,方便我们做出进一步的优化
3. Svelte是纯编译的,但是框架的真实性能可能达不到理论上的高度。且灵活性不高,必须编译才能够运行
4. Vue.js3保持了运行+编译,保持灵活性,尽可能的去优化框架的性能
# 4. 响应式系统的作用和实现
## 4.1 响应式数据和副作用函数
### **副作用函数**:
该函数的执行,会直接/间接的影响到其它函数的执行。
```js
function effect() {
// 这里修改了document.body.innerHTML的值 影响了下面的getBody函数读取 那么effect就是副作用函数
document.body.innerHTML = 'Hello vue3'
}
function getBody() {
console.log(document.body.innerHTML);
}
```
修改了全局的变量 change也是副作用函数
```js
let val = 1
function change() {
val = 2
}
function get() {
console.log(val)
}
```
### 响应式数据
```js
const obj = {
text: 'Hello world'
}
function effect2() {
doument.body.innerHTML = obj.text
}
// 如果obj.text的值发生了改变 我们希望document.body.innerHTML也能够发生改变,那么obj就是一个响应式的数据
```
## 4.2 响应式数据的基本实现
1. 第一次就把effect函数存起来
2. 下一次obj.text重新赋值时,再次执行effect

1. data是初始的数据
- set存储所有的函数
2. 利用proxy,针对 对象进行劫持
- 先在get的时候把函数添加到set结构里面去,再去成功返回读取到的值
- 在set的时候,先修改值,再把所有的set结构里面的函数都调用一遍
```js
let data = { text: '123' }
let bucket = new Set()
let obj = new Proxy(data, {
get(target, key) {
// 为了后续响应式做准备
bucket.add(effect)
return target[key]
},
set(target, key, newVal) {
// 先修改值
target[key] = newVal
// 修改值后立马调用之前更新页面的操作
bucket.forEach(fn => fn())
return true
}
})
function effect() {
// 这儿写的是obj.text而不是data.text 下面的obj.text = 进行赋值也是一样的
document.body.innerHTML = obj.text
}
effect()
debugger
obj.text = '456'
```
## 4.3 缺陷是什么
1. 理论上,obj没有notExist这个属性,那么notExist属性不会和副作用域函数建立联系
- effect调用 -> obj.text访问 -> 副作用域函数加入bucket桶的数据结构 -> 下次修改时 -> 触发set -》 先修改值 -》再调用副作用域函数 那么字段和函数建立联系
- 都没有字段 -> 建立什么联系
- 但是确实就是有联系 -> 因为我们原来并没有明确联系 -> 无论读取哪一个属性,都会触发set -> 无论设置哪一个属性,都会触发get -> 这个可以直接测试
2. 这一段代码有三个角色 effectFn副作用域函数 obj对象 text属性
```js
effect(function effectFn() {
document.body.innerHTML = obj.text
})
```
如果两个副作用域函数同时读取了同一个对象的同一个属性名
```js
// 2. 如果两个副作用域函数同时读取了一个属性名
effect(function effectFn2() {
document.body.innerHTML = obj.text
})
effect(function effectFn3() {
document.body.innerHTML = obj.text
})
```
如果一个副作用域函数读取了同一个对象的不同属性名
```js
effect(function effectFn2() {
document.body.innerHTML = obj.text
document.querySelector('div').innerHTML = obj.text2
})
```
如果不同的副作用域函数读取了不同对象的不同属性名
```js
effect(function effectFn4() {
document.body.innerHTML = obj1.text1
})
effect(function effectFn5() {
document.querySelector('div').innerHTML = obj2.text2
})
```
## 4.4 设计一个完善的响应式系统
1.为什么target -> 和key是weakMap 而 key和副作用函数的数据结构是 Map呢?
- 可能没有使用对象
- 如果使用了对象,obj[name], 肯定是要是使用对象里面的某个值,不然没有意义,所以我们这里建立强相关
2.我们往WeakMap里面添加的key是对象,就是代理的对象。而key对应的value还是一个map,里面是属性和副作用函数的映射。而map里面的key是属性,对应的值是set数据结构
3.effect里面的功能,不仅仅是执行一段代码,而是存取全局的副作用函数,然后执行它
4.我们在get里面存副作用函数,在set里面调用副作用函数
```js
let activeAffect // 这个是全局变量 存储副作用函数
let obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log('访问了', target[key]);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
console.log('修改了', target[key], '为', newVal);
trigger(target, key)
}
})
function track(target, key) {
if (!activeAffect) {
return target[key]
}
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let depSet = depMap.get(key)
if (!depSet) {
depMap.set(key, (depSet = new Set()))
}
depSet.add(activeAffect)
}
function trigger(target, key) {
let depMap = bucket.get(target)
let effects = depMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
activeAffect = fn
fn()
}
effect(() => {
document.body.innerHTML = obj.msg
})
```
## 4.5 解决缺陷
**最终的效果-解决响应式递归的缺陷**
1. 修改obj.ok的值,没有修改obj.text的值,但是副作用函数仍然被触发了。按理来说不应该被触发
```js
effect(() => {
console.log(1); //1
debugger
// 我们怎么去测试这个效果呢?我们把 obj.ok = false;然后 obj.text = '45' 改他个很多很多次。发现这里还是会被触发很多次。即便这里的document.body.innerHTML的值不需要改变
document.querySelector('body').innerHTML = obj.ok ? obj.text : 'not'
})
```
2. 如何修改呢?
```diff
/*
1. 分支切换如何进行优化
2.
*/
const data = { ok: true, text: '123' }
let activeAffect // 这个是全局变量 存储副作用函数
let bucket = new WeakMap()
let obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log('访问了', target[key]);
return target[key]
},
set(target, key, newVal) {
debugger
target[key] = newVal
console.log('修改了', target[key], '为', newVal);
trigger(target, key)
}
})
function track(target, key) {
if (!activeAffect) {
return target[key]
}
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let depSet = depMap.get(key)
if (!depSet) {
depMap.set(key, (depSet = new Set()))
}
// depSet就是专门用来收集副作用域函数的集合
debugger
depSet.add(activeAffect)
// deps就是与副作用函数存在联系的依赖集合
// 把它添加到activeEffect.deps的数组中
activeAffect.deps.push(depSet)
}
function trigger(target, key) {
debugger
let depMap = bucket.get(target)
let effects = depMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
const effectFn = () => {
// 清除所有effectFn里面的deps的依赖集合
+ cleanUp(effectFn)
+ activeAffect = effectFn
fn()
}
// 用于收集所有和这个副作用函数有关的集合
+ effectFn.deps = []
// 执行副作用函数
effectFn()
}
effect(() => {
console.log(1); //1
debugger
// 我们怎么去测试这个效果呢?我们把 obj.ok = false;然后 obj.text = '45' 改他个很多很多次。发现这里还是会被触发很多次。即便这里的document.body.innerHTML的值不需要改变
document.querySelector('body').innerHTML = obj.ok ? obj.text : 'not'
})
+function cleanUp(effectFn) {
debugger
for (let i = 0; i < effectFn.deps.length; i++) {
// deps是依赖集合
const deps = effectFn.deps[i]
// 将effectFn从依赖集合中删除
deps.delete(effectFn)
}
// 重置deps数组
effectFn.deps.length = 0
}
```
最终的效果就是obj.ok = false; 然后再去修改obj.text='456' 发现副作用函数不会再被触发了
**如何解决,无限循环的问题?**
如果碰到无限循环,栈溢出的问题,下面会出现无限循环的问题
```js
let set = new Set([1])
debugger
set.forEach(item => {
set.delete(1)
set.add(1)
console.log('遍历仍然在继续');
})
```
下面这里呢?我们创建一个set数据结构就能够解决这个问题
```js
let set = new Set()
let newSet = new Set(set)
newSet.forEach(item => {
set.add(1)
set.delete(1)
console.log('遍历还在继续嘛?');
})
```
语言规范中,明确规定,如果一个遍历中,如果一个值被访问过了,并且这个值被删除后又被重新添加到集合里面去了,那么遍历就会一直重复的进行,陷入死循环,解决的方式,就是创建一个新的set结构,并且遍历它
**改造trigger函数**
```diff
function trigger(target, key) {
// debugger
let depMap = bucket.get(target)
if (!depMap) return
let effects = depMap.get(key)
if (!effects) return
+ let newEffect = new Set(effects)
+ newEffect && newEffect.forEach(fn => fn())
}
```
## 4.6 嵌套effect栈和effect栈的解决
出现的情况:
1. **effect是可以发生嵌套的**
```js
// effect是可以发生嵌套的
effect(function effectFn1() {
effect(function effectFn2() {
})
})
```
2. 哪些情况是可能发生嵌套的呢?
**Vue中的渲染函数render就经常出现嵌套的情况**
```js
const bar = {
render() {
return
}
}
```
```js
const foo = {
render() {
return
}
}
```
**最终调用effect**
```js
effect(function () {
Foo.render()
effect(function () {
Bar.render()
})
})
```
**出现的问题**
```diff
let data = {
text: 'xiaobai',
ok: true
}
let activeAffect
let bucket = new WeakMap()
let obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log('读取了', key);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
return true
}
})
function track(target, key) {
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let deps = depMap.get(key)
if (!deps) {
depMap.set(key, (deps = new Set()))
}
deps.add(activeAffect)
// 在get读取变量的时候就要这么去操作
// 新增 把deps副作用集合添加到activeAffect变量中去
activeAffect.deps.push(deps)
}
function trigger(target, key) {
let depMap = bucket.get(target)
if (!depMap) return
let effects = depMap.get(key)
if (!effects) return
// (避免无限循环)创建一个新的set 数据是effects 然后遍历执行它
// 如果直接effects 然后遍历执行
let newSet = new Set(effects)
newSet.forEach(effect => effect())
}
function effect(fn) {
const effectFn = () => {
// 执行前 先清除当前的副作用函数 然后
cleanUp(effectFn)
activeAffect = effectFn
fn()
}
effectFn.deps = []
effectFn()
}
function cleanUp(effectFn) {
for (let i = 0; i < effectFn.deps.length; i++) {
let deps = effectFn.deps[i]
// deps是一个集合 deps.delete是从集合中删除 effectFn函数
// 而for循环 则是从所有的集合中删除effectFn函数
deps.delete(effectFn)
}
effectFn.deps.length = 0
}
// 修改obj.ok的时候 只有effectFn2执行
// 修改obj.text的时候 effectFn1和effectFn2同时执行
// 我们修改obj.text试试看 -> 我们发现 effectFn2执行了,但是我们更加希望effectFn1执行如果不是的话就是错误的 -> 因为内层的副作用函数会覆盖外层的副作用函数 effectFn2覆盖了effectFn1 所以即便我们修改了obj.text 也是执行obj.ok
let temp1, temp2
+effect(function effectFn1() {
+ console.log('effectFn1执行了');
+ effect(function effectFn2() {
console.log('effectFn2执行了');
temp1 = obj.ok
})
temp2 = obj.text
})
```
1. 在effect方法里面,修改obj.text的内容
2. 在里面嵌套了effect修改obj.ok
3. 如果修改obj.text,先执行effectFn1(),然后打印,然后执行effectFn2,打印,读取obj.text的内容。
4. **问题:**我们发现,修改obj.text的内容时,只会打印effectFn2执行了的内容,原因是什么?是因为activeEffect的变量一次只能存储一个副作用函数,而之前的收集,effectFn2覆盖了effectFn1,所以修改obj.text的内容时,effectFn2会覆盖effectFn1
**解决的方法是:**
增加一个栈,每次收集新的副作用函数时,就把它推入栈,然后执行完毕,就把它移出栈。栈顶的指针始终指向最上面。
```diff
function effect(fn) {
const effectFn = () => {
// 执行前 先清除当前的副作用函数 然后
cleanUp(effectFn)
activeAffect = effectFn
+ effectStack.push(effectFn)
fn()
+ effectStack.pop()
+ activeAffect = effectStack[effectStack.length - 1]
}
effectFn.deps = []
effectFn()
}
```
**我们来查看一下代码的执行顺序:**
目的就是 `内层副作用函数执行完毕后 能够弹出栈 然后activeAffect能够指向更新为外层的副作用函数`。这样就不会让activeAffect始终指向覆盖的内层副作用函数了
读取obj.text: effect -> effectFn1 -> 打印1 -> effect -> effectFn2 -> 打印 -> get -> track(activeAffect添加到deps 然后dep给到activeAffect.deps) ->
## 复习思考一些问题
1. 我们讲了副作用函数的概念和响应式的机制
**副作用函数**是指,函数A发生变化,会直接或者间接的影响到其它函数的执行
```js
function effect() {
// 这里修改了document.body.innerHTML的值 影响了下面的getBody函数读取 那么effect就是副作用函数
document.body.innerHTML = 'Hello vue3'
}
function getBody() {
console.log(document.body.innerHTML);
}
```
**响应式的机制**
```js
const obj = {
text: 'Hello world'
}
function effect2() {
doument.body.innerHTML = obj.text
}
// 如果obj.text的值发生了改变 我们希望document.body.innerHTML也能够发生改变,那么obj就是一个响应式的数据
```
2. 初步的响应式,把effect函数提取出来存储到Set桶里面去,get里面添加,set里面拿出来执行
```js
let data = { text: '123' }
let bucket = new Set()
let obj = new Proxy(data, {
get(target, key) {
bucket.add(effect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
function effect() {
// 这儿写的是obj.text而不是data.text 下面的obj.text = 进行赋值也是一样的
document.body.innerHTML = obj.text
}
effect()
debugger
obj.text = '456'
```
3. 修改硬编码,支持匿名函数
- 修改effect为一个工具函数,传入匿名函数为具体的形参,来执行
- 有一个全局变量来存储匿名函数(修改的操作)
```diff
let data = { text: '123' }
let bucket = new Set()
let activeAffect
+function effect(fn) {
// 把副作用函数存储给 activeAffect
activeAffect = fn
// 执行副作用函数
fn()
}
let obj = new Proxy(data, {
get(target, key) {
+ bucket.add(activeAffect)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
bucket.forEach(fn => fn())
return true
}
})
+effect(
() => {
document.body.innerHTML = obj.text
}
)
obj.text = '456'
```
4. 解决问题:无论读取哪个属性都会触发get,无论设置哪个属性都会触发get,字段和副作用函数之间没有明确的关系。即便设置一个不存在的属性,obj.notExist,也会触发set
重新设置了一个数据结构 `WeakMap -> Map -> Set`
```js
{
target1: {
key1: {fn1, fn2},
key2: {fn1, fn2}
},
target2: {
key3: {fn3, fn4},
key4: {fn5, fn6}
}
}
```
```diff
let data = { msg: '123' }
let bucket = new WeakMap()
/*
理解数据结构:
1. bucket -> 里面是 target是键
target是对象
而值是map数据结构
map的键是key,也就是target里面的key
而target的key对应的值就是set数据结构里面存储了所有的affect副作用域函数
{
target1: {
key1: {fn1, fn2},
key2: {fn1, fn2}
},
target2: {
key3: {fn3, fn4},
key4: {fn5, fn6}
}
}
*/
let activeAffect
let obj = new Proxy(data, {
get(target, key) {
// 1. 没有activeAffect 就直接return
// 2. 从weakMap里面根据对象拿出map map就是一个对象 包括所有的属性和副作用域函数的对应关系
// 3. 从map里面根据key,key就是对象的所有属性,拿出所有该属性对应的的副作用域的函数
// 4. 所有的副作用域函数 添加到 depSet里面去
+ track(target, key)
console.log('访问数据了', target[key]);
return target[key]
},
set(target, key, newVal) {
// 1.从bucket里面,根据target这个对象,拿到一个all的副作用域函数的结合。记得判空
// 2.根据key,从集合里面找到,该key对应的所有的副作用域函数
// 3.然后执行所有的副作用域函数 记得判空
console.log('数据更新了', target[key], '改为', newVal);
target[key] = newVal
// 这两句的顺序是不能够相反的
+ trigger(target, key)
// obj1.name = 123
// weakMap(obj1, 123)
// obj.name = 456
// weakMap.get(obj1) =>还是123
}
})
+function track(target, key) {
debugger
if (!activeAffect) {
return target[key]
}
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let depSet = depMap.get(key)
if (!depSet) {
depMap.set(key, (depSet = new Set()))
}
depSet.add(activeAffect)
}
+function trigger(target, key) {
debugger
let depMap = bucket.get(target)
if (!depMap) return
let effects = depMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
debugger
activeAffect = fn
fn()
}
effect(() => {
debugger
document.querySelector('#app').innerHTML = obj.msg
})
```
5. 分支切换的情况 和 分支切换存在的问题
```diff
const data = { ok: true, text: '123' }
let activeAffect // 这个是全局变量 存储副作用函数
let bucket = new WeakMap()
let obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log('访问了', target[key]);
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
console.log('修改了', target[key], '为', newVal);
trigger(target, key)
}
})
function track(target, key) {
if (!activeAffect) {
return target[key]
}
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let depSet = depMap.get(key)
if (!depSet) {
depMap.set(key, (depSet = new Set()))
}
depSet.add(activeAffect)
}
function trigger(target, key) {
let depMap = bucket.get(target)
let effects = depMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
activeAffect = fn
fn()
}
effect(() => {
console.log(1);
// 我们怎么去测试这个效果呢?我们把 obj.ok = false;然后 obj.text = '45' 改他个很多很多次。发现这里还是会被触发很多次。即便这里的document.body.innerHTML的值不需要改变
+ document.querySelector('body').innerHTML = obj.ok ? obj.text : 'not'
})
```
- effect里面修改body的内容时,obj.ok如果是true就读取obj.text的内容,如果是false,就显示not
- 如果obj.ok是false,那么后续不会读取obj.text的内容了,因此后续修改obj.text的内容,副作用函数应该不会被触发,因为我根本就没有使用你,但是还是会被触发。为什么,因为它已经存储在bucket的存储桶里面了。
- **函数的执行顺序**
**obj.ok和obj.text**的读取:effect -> get -> track -> get -> track
obj.ok改为false:set -> trigger -> fn执行 打印1 -> get -> track 已经存在了 不添加了
obj.text改为‘444’: set -> trigger -> fn执行打印1
6. 我们去解决分支切换的问题:
```diff
const data = { ok: true, text: '123' }
let activeAffect // 这个是全局变量 存储副作用函数
let bucket = new WeakMap()
let obj = new Proxy(data, {
get(target, key) {
track(target, key)
console.log('访问了', target[key]);
return target[key]
},
set(target, key, newVal) {
debugger
target[key] = newVal
console.log('修改了', target[key], '为', newVal);
trigger(target, key)
}
})
function track(target, key) {
if (!activeAffect) {
return target[key]
}
let depMap = bucket.get(target)
if (!depMap) {
bucket.set(target, (depMap = new Map()))
}
let depSet = depMap.get(key)
if (!depSet) {
depMap.set(key, (depSet = new Set()))
}
// depSet就是专门用来收集副作用域函数的集合
debugger
depSet.add(activeAffect)
// deps就是与副作用函数存在联系的依赖集合
// 把它添加到activeEffect.deps的数组中
+ activeAffect.deps.push(depSet)
}
function trigger(target, key) {
debugger
let depMap = bucket.get(target)
let effects = depMap.get(key)
effects && effects.forEach(fn => fn())
}
function effect(fn) {
+ const effectFn = () => {
// 清除所有effectFn里面的deps的依赖集合
+ cleanUp(effectFn)
activeAffect = effectFn
fn()
}
// 用于收集所有和这个副作用函数有关的集合
+ effectFn.deps = []
// 执行副作用函数
+ effectFn()
}
effect(() => {
console.log(1); //1
debugger
// 我们怎么去测试这个效果呢?我们把 obj.ok = false;然后 obj.text = '45' 改他个很多很多次。发现这里还是会被触发很多次。即便这里的document.body.innerHTML的值不需要改变
document.querySelector('body').innerHTML = obj.ok ? obj.text : 'not'
})
+function cleanUp(effectFn) {
debugger
for (let i = 0; i < effectFn.deps.length; i++) {
// deps是依赖集合
const deps = effectFn.deps[i]
// 将effectFn从依赖集合中删除
deps.delete(effectFn)
}
// 重置deps数组
effectFn.deps.length = 0
}
```
我们去分析代码的行走的流程:
obj.ok的读取:effect -> effectFn -> cleanUp(看不出它的魔力) -> fn -> 打印1 -> get -> track
obj.text的读取:get -> track
obj.ok = false: set -> trigger -> effects执行时 -> cleanUp -> 打印1 -> get -> track(depSet里面只有关于属性ok的了,因为text的被cleanUp清除干净了,然后再 activeEffect = effect)
死循环 -> get -> track -> set里面的trigger ->cleanUp(`因为上面的depSet又被添加了,此时遍历还是在进行中的,`) 所以我们创建新的set解构,然后遍历它,就没事啦~
**obj.text = '444' -> 此时不会打印1了**
7. 回到了我们的嵌套栈的问题
我们希望effect的一个函数effectFn1能够嵌套另一个effectFn2函数,Fn1读取foo属性,Fn2读取bar属性
我们希望修改bar属性的时候,触发Fn2里面的代码,修改foo的时候,触发Fn1和Fn2里面的代码,但是我们发现,修改Fn1里面的foo属性时,还是只触发Fn2里面的代码
```diff
let temp1, temp2
+effect(function effectFn1() {
+ console.log('effectFn1执行了');
+ effect(function effectFn2() {
console.log('effectFn2执行了');
temp1 = obj.ok
})
temp2 = obj.text
})
```
## 4.7 无限递归
1. 无限递归 不太理解
2. obj.foo访问的时候报错,这是为什么呢?
## 4.8 调度执行
1.