# 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
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 -> 测试代码的性能的地址 image-20221102153923046 虚拟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 ![image-20221104160539229](https://typora-1309613071.cos.ap-shanghai.myqcloud.com/typora/image-20221104160539229.png) 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 }) ``` image-20221104185353221 如果两个副作用域函数同时读取了同一个对象的同一个属性名 ```js // 2. 如果两个副作用域函数同时读取了一个属性名 effect(function effectFn2() { document.body.innerHTML = obj.text }) effect(function effectFn3() { document.body.innerHTML = obj.text }) ``` image-20221104185554635 如果一个副作用域函数读取了同一个对象的不同属性名 ```js effect(function effectFn2() { document.body.innerHTML = obj.text document.querySelector('div').innerHTML = obj.text2 }) ``` image-20221104185847052 如果不同的副作用域函数读取了不同对象的不同属性名 ```js effect(function effectFn4() { document.body.innerHTML = obj1.text1 }) effect(function effectFn5() { document.querySelector('div').innerHTML = obj2.text2 }) ``` image-20221104190750108 ## 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' 发现副作用函数不会再被触发了 image-20221112152228701 **如何解决,无限循环的问题?** 如果碰到无限循环,栈溢出的问题,下面会出现无限循环的问题 ```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.