22_快速diff算法 当这段代码运行完毕之后,j 的值为 1,此时我们还需要处理相同的后置节点,思路也是一样的。在这个例子中,p-1 在新旧两组子节点中,就属于相同的前置节点,p-2、p-3 就属于相同的后置节点。此时就可以看到,预处理之后,新旧两组子节点都存在未处理的节点,这种情况就需要判断一个节点是否需要移动,如果不需要移动,则判断是需要被添加还是被移除的节点。这里为什么说是相对索引呢,source 是从 0 开始的,而未处理的节点就未必是从 0 开始的,所以记录一个相对索引。
21_双端 diff 算法 简单 diff 算法的问题在于,它的移动操作并不是最优的,我们还是使用上一节的例子来看,如图:在这个例子中,我们使用简单 diff 算法来更新需要进行两次 DOM 移动,将 p-1 移动到 p-3 后面,将 p-2移动到 p-1 后面。但是其实只需要一次移动 DOM 即可,就是将 p-3 移动到 p-1 的前面即可。这一点想要实现,是简单 diff 算法无法做到的,而使用双端 diff 算法就可以。顾名思义,双端 diff 算法是一种同事对新旧两组子节点的两个端点进行比较的算法,因此我们需要四个索引值,分别
20_简单的diff算法 取新的子节点的第二个节点 p-1,它的 key 为 1,尝试在旧的子节点中找一个可复用的节点,找到了,并且找到的复用节点在旧的子节点中索引为 0,此时变量 lastIndex 的值为 2,取新的子节点的第二个节点 p-2,它的 key 为 2,尝试在旧的子节点中找一个可复用的节点,找到了,并且找到的复用节点在旧的子节点中索引为 1,此时变量 lastIndex 的值为 2,在前文,我们针对新旧vnode 的 children 都是数组时,采用的方案是,先卸载旧节点的子元素,在重新挂在新节点的子元素。
19_文本、注释、Fragment节点 类似的组合还有 select 和 options 这两个标签,而 vue3 中支持多个根节点模板,所以不存在这个问题,而 vue3 中是如何描述这个多根节点模板呢?Fragment 是 vue3 新增的一个 vnode 类型,在实现这个类型之前,我们可以先聊一下为什么需要这个类型。而 Fragment 本身并不会渲染任何内容,因此只会渲染 Fragment 的子节点。而 Items 组件负责渲染一组。
17_事件的处理 是创建浏览器上下文的时间,在这里可以简单理解为一个只会增加的时间,页面刷新就会重置重新计算,所以首次渲染之后,点击 p 元素,触发点击事件,将 bol 的值改为 true,此时会执行 effect 然后绑定事件,并记录下绑定的时间,重点来了,此时的 e.timeStamp 触发的时间是 p 事件触发的时间,而非这个冒泡到 div 的事件触发时间,所以这个 timeStamp 一定比 div 的事件绑定时间要早,而比这个早就会被。这样就解决了我们的问题。解决初次绑定,那么如果是更新事件应该如何处理呢?
16_区分vnode的类型 在前文中,我们进行了一个简单的打补丁的操作,但是这个操作是有一些前提的,比如新旧 vnode 描述的内容相同,比如首次渲染的 vnode 的 type 是一个 div,二次渲染时,这个 vnode 的 type 改为了一个 p,而这种类型都不同的 vnode,其实就没有打补丁的意义,毕竟元素标签都不同了。
15_卸载操作 正确的卸载方式应该是,根据 vnode 对象获取对应与其相关联的真实 DOM 元素,然后使用原生 DOM 操作方式将该元素移除。在之前我们就提到,首次渲染之后,后续如果再调用 render 函数时,传递的 vnode 为 null 则表示是卸载。
14_挂载子节点和元素的属性 通过这个现象可以发现,用户对文本框的修改并不会影响 el.getAttribute(‘value’) 的返回值,这个现象蕴含着 HTML Attributes 所代表的意义,实际上,HTML Attributes 的作用是设置与之对应的 DOM Properties 的初始值,一旦值改变,那么 DOM Properties 始终存储着当前值,而通过 getAttribute 函数得到的值仍然是初始值。而如果用户修改了文本框的值,那么 el.value 的值就是当前文本框的值。
13_渲染器的设计 观察这个对象,我们使用 type 来表示一个 vnode 的类型,不同类型的 type 属性值可以描述多种类型的 vnode。当 type 为属性值为字符串时,表示一个普通的 html 标签,并使用 type 属性的属性值作为标签的名称。// 创建一个渲染器// 调用 render 函数渲染该 vnode// 在这里编写逻辑if(vnode){} else {// 卸载return {render// 如果 n1 不存在,则执行挂载,使用 mountElement 函数完成挂载。
12_实现 ref 这个 ref 内部的 value 的值需要根据 isShallow 来进行判断,因为我们还会实现一个 shallowRef 方法,来作为浅层次的 ref,浅层 ref 的内部值将会原样存储和暴露,并且不会被深层递归地转为响应式。这里出于严谨考虑,并没有采用上一节的,直接使用一个字符串,而是换成了 symbol,当然,目前这里只有根据标识符判断,而没有添加,稍后会进行添加。在上一节中,我们初步实现了一个 ref,可以使用,但是实现结构不够好,本节我们仿照 Vue3 源码重新实现一下。
11_原始值的响应式方案-ref 可以看到,我们利用 proxy 的 get 拦截器作为插入代码逻辑的地方,在这里我们通过判断一个值是否是 ref 数据,如果是在自动的在这里读取 refVal.value。有了自动脱 ref 的能力之后,就可以降低用户在使用时的心智负担,无需关心那个属性是 ref 那个属性是普通数据或者是 reactive。因此,我们需要脱 ref 能力,所谓自动脱 ref,就是指的属性访问行为,即 ref 数据无需通过 xx.value 来访问。然而,这样书写就会失去响应式,当我们修改数据时,不会触发渲染,这是为什么?
10_实现readonly 我们虽然在创建 readonly 函数时,给 createReactiveObject 的第二个参数是 false,表示是深响应的,但从结果可以看到,没有被拦截,而且依然被修改了。在某些时候,我们希望定义一些数据是只读的,不允许被修改,从而实现对数据的保护,即为 readonly。
09_实现reactive之代理 Set 和 Map 而 sProxy.delete 是一个方法,sProxy.delete 是访问状态,并没有真的执行,只有当 sProxy.delete(1) 才是真正执行了,但是这个执行是外部的用户操作的,因此就算我们一样设置 Reflect.get(target, key, target),也不行,因为 JavaScript 中 this 的指向通常需要函数调用那一刻才能确定,所以最后还是变成 sProxy 来调用。至此,我们还要一项缺陷没有补充完成,那就是与值的响应式联系,与 for…in 不同,for…
07_理解 Proxy 和 Reflect 这里的 this 表示其实是 obj 这个原始对象,而非 objProxy 这个代理对象,因为我们在 get 拦截函数中,是直接使用 targt[key] 这样来返回值的,这就等于是 obj.xx 进行访问属性,自然 this 就表示是 obj 这个原始对象,而原始对象自然不会进行依赖的收集,所以只触发了一次。,所以直接使用这种括号的方式是无法指定这个 this 了,但是我们可以通过调用这个 Reflect.get 方法,来完成这个指定 this 的操作。是用于获取当前执行上下文中的。
05_计算属性 computed 和 lazy 可以看到修改值之后,并没有在重新打印 effect 的副作用函数,那么体现实际 Vue 中,即计算属性改变了值,但是模板没有重新渲染,而导致出现这个问题的原因也简单,fullName.value 触发 get value,get value 中第一次会调用 computed 内部的 effectFn 函数来返回 getter 的值,而 getter 也是被一个 effect(即 effect(getter))包裹的;这一文中有讲到,**外部 effect 不会被内部 effect 中的响应式数据收集。
03_处理响应式系统的边界情况 objProxy.a++ 可以划分为 objProxy.a = objProxy.a + 1,这里 objProxy.a 进行了获取,然后又执行了 objProxy.a 的 赋值,也就说进行了读的操作之后,就又进行了写的操作,写的操作又会导致执行副作用函数,而上一次的副作用函数还没执行完成,就立马进行了下一次的执行,而这个下一次的执行又会重新执行这个步骤,这样无限的递归调用自己,就会导致栈溢出。要解决这一点其实也简单,只要每次重新执行依赖之前,就把之前旧的依赖清除掉即可。