Vue3——基础内容部分(小满版本)(一)https://developer.aliyun.com/article/1470371
第五章 — 虚拟DOM和VueKeyDiff算法(代号:巴别塔)
前置了解
- Diff算法是面试的高频问答点,在很多的语言中也会去使用
TypeScript
转JavaScript
的转换过程也会进行AST转换- 插件babel,ES6语法转ES5的时候也会经过抽象语法树的一个转换
- js走V8引擎转字节码的时候,也会经过AST
介绍虚拟DOM
- 就是通过JS来生成一个AST节点树
查看虚拟DOM上面属性的方法,将下方代码块复制在控制台
let div = document.createElement('div') let str = '' for (const key in div) { str += key + '' } console.log(str)
- 这上面的属性是非常多的,所以直接操作DOM非常浪费性能 (这就是为什么我们不直接去操作这个DOM,而是使用js去描述DOM对象的原因)
- 解决方案就是 我们可以用
JS
的计算性能来换取操作DOM
所消耗的性能,既然我们逃不掉操作DOM
这道坎,但是我们可以尽可能少的操作DOM
操作JS是非常快的
Vue3 源码地址 https://github.com/vuejs/core
Diff算法
- 先来看看图片的描述
没有key的diff算法
一共3步
- 无key,patch的时候会替换
- 新增
- 删除
- 情况1:
- 当替换结束之后,发现有多于的出来就会进行新增的操作,然后diff算法结束
- 情况2:
- 当替换结束之后,发现bew Vnode有变少就会对old Vnode进行删除的操作,然后diff算法结束
- 在上图中,相同的其实不必替换,需要替换的只有一个DDD,相同的我们可以进行复用,想要进行这个复用的操作的话,我们就要进行一个标记,也就
key
,有key才可以复用,那就得到有key的diff算法去啦
const patchUnkeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { c1 = c1 || EMPTY_ARR c2 = c2 || EMPTY_ARR const oldLength = c1.length const newLength = c2.length const commonLength = Math.min(oldLength, newLength) let i ////首先通过for循环去patch重新渲染我们的元素 for (i = 0; i < commonLength; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) patch( c1[i], nextChild, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } //判断 if (oldLength > newLength) { // remove old unmountChildren( c1, parentComponent, parentSuspense, true, false, commonLength ) } else { // mount new 新增 mountChildren( c2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, commonLength ) } }
有key的diff算法
有key的diff算法一共是分为5步,也是新旧虚拟DOM进行一个对比
第一步:前序算法
第二步:后序算法
// can be all-keyed or mixed const patchKeyedChildren = ( c1: VNode[], c2: VNodeArrayChildren, container: RendererElement, parentAnchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { let i = 0 const l2 = c2.length let e1 = c1.length - 1 // prev ending index let e2 = l2 - 1 // next ending index //前序算法,只对比前面的 // 1. sync from start // (a b) c // (a b) d e while (i <= e1 && i <= e2) { const n1 = c1[i] const n2 = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (isSameVNodeType(n1, n2)) { patch( n1, n2, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else { //当旧VNode与新VNode对比着对比着发现不一样的时候,就跳出循环 break } i++//前序算法从前面开始往后推,是i++,如果是尾序算法的话就从后面开始往前进,是i-- //其实都差不多 }
进入isSameVNode Type(n1,n2)内进行一探究竟
下方的type是什么?
- 例如我们现在渲染div这个元素,那type就等于这个div
下方的key是什么?
- 是我们在for循环中绑定的key,唯一值
export function isSameVNodeType(n1: VNode, n2: VNode): boolean { if ( __DEV__ && n2.shapeFlag & ShapeFlags.COMPONENT && hmrDirtyComponents.has(n2.type as ConcreteComponent) ) { // HMR only: if the component has been hot-updated, force a reload. return false } //判断type 和 key是不是一样的,如果是一样的,他才会进行一个复用 return n1.type === n2.type && n1.key === n2.key }
<template> <div> <!-- key是唯一值,通常后端返回,类似item.id的形式 --> <div :key="item" v-for="(item) in Arr">{{ item }}</div> </div> </template> <script setup lang="ts"> const Arr: Array<string> = ['A', 'B', 'C', 'D'] Arr.splice(2,0,'DDD') </script> <style> </style>
第三步:处理新节点
如果有的话,没有就跳过
// 3. common sequence + mount // (a b) // (a b) c // i = 2, e1 = 1, e2 = 2 // (a b) // c (a b) // i = 0, e1 = -1, e2 = 0 if (i > e1) { if (i <= e2) { const nextPos = e2 + 1 const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor while (i <= e2) { patch( null,//patch的参数为null的话就是新增 (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) i++ } } }
第四步:卸载
跟第三步一样,如果有的话,没有就跳过
// 4. common sequence + unmount // (a b) c // (a b) // i = 2, e1 = 2, e2 = 1 // a (b c) // (b c) // i = 0, e1 = 0, e2 = -1 else if (i > e2) { while (i <= e1) { unmount(c1[i], parentComponent, parentSuspense, true) i++ } }
第五步:特殊情况乱序
也是最难的一步,有可能是做了位移,或者新增、删除、位移、更新同时发生,主要为不可控的一些情况
源码将其分为3小节去处理
- 构建新节点映射关系案例
key 1 2 3 4 5 索引 0 1 2 3 4 sort key 5 4 3 2 1 索引 0 1 2 3 4 5=>0 4=>1 3=>2 2=>3 1=>4(构建成map关系)
// 5. unknown sequence // [i ... e1 + 1]: a b [c d e] f g // [i ... e2 + 1]: a b [e d c h] f g // i = 2, e1 = 4, e2 = 5 else {//构建新旧节点的映射关系 const s1 = i // prev starting index const s2 = i // next starting index // 5.1 build key:index map for newChildren const keyToNewIndexMap: Map<string | number | symbol, number> = new Map() for (i = s2; i <= e2; i++) { const nextChild = (c2[i] = optimized ? cloneIfMounted(c2[i] as VNode) : normalizeVNode(c2[i])) if (nextChild.key != null) { if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) { warn( `Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.` ) } keyToNewIndexMap.set(nextChild.key, i) } } // 5.2 loop through old children left to be patched and try to patch // matching nodes & remove nodes that are no longer present let j let patched = 0 const toBePatched = e2 - s2 + 1 let moved = false // used to track whether any node has moved let maxNewIndexSoFar = 0 // works as Map<newIndex, oldIndex> // Note that oldIndex is offset by +1 // and oldIndex = 0 is a special value indicating the new node has // no corresponding old node. // used for determining longest stable subsequence //记录新节点在旧节点中的位置数组 //[5,4,3,2,1] const newIndexToOldIndexMap = new Array(toBePatched) for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0 for (i = s1; i <= e1; i++) { const prevChild = c1[i] if (patched >= toBePatched) { // all new children have been patched so this can only be a removal //多余的旧节点会进行删除 unmount(prevChild, parentComponent, parentSuspense, true) continue } let newIndex if (prevChild.key != null) { newIndex = keyToNewIndexMap.get(prevChild.key) } else { // key-less node, try to locate a key-less node of the same type for (j = s2; j <= e2; j++) { if ( newIndexToOldIndexMap[j - s2] === 0 && isSameVNodeType(prevChild, c2[j] as VNode) ) { newIndex = j break } } } //如果新节点不包含旧节点里面了,也会进行删除操作 if (newIndex === undefined) { unmount(prevChild, parentComponent, parentSuspense, true) } else { newIndexToOldIndexMap[newIndex - s2] = i + 1 if (newIndex >= maxNewIndexSoFar) { maxNewIndexSoFar = newIndex } else { //节点出现交叉,说明是移动要去求最长递增子序列 moved = true } patch( prevChild, c2[newIndex] as VNode, container, null, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) patched++ } } // 5.3 move and mount // generate longest stable subsequence only when nodes have moved(仅当节点发生移动时生成最长稳定子序列) //求最长递增子序列升序 const increasingNewIndexSequence = moved ? getSequence(newIndexToOldIndexMap) : EMPTY_ARR j = increasingNewIndexSequence.length - 1 // looping backwards so that we can use last patched node as anchor(反向循环,以便我们可以使用最后打补丁的节点作为锚点) for (i = toBePatched - 1; i >= 0; i--) { const nextIndex = s2 + i const nextChild = c2[nextIndex] as VNode const anchor = nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor if (newIndexToOldIndexMap[i] === 0) { // mount new patch( null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) } else if (moved) { // move if: // There is no stable subsequence (e.g. a reverse) // OR current node is not among the stable sequence(或者当前节点不在稳定序列中) //如果当前遍历的这个节点不在子序列,说明要进行移动 if (j < 0 || i !== increasingNewIndexSequence[j]) { move(nextChild, container, anchor, MoveType.REORDER) } else { j--//如果节点在子序列中直接跳过 } } } } }
getSequence函数内部
这是第五步中处理的第三小节,最长递增子序列其中处理的算法,由贪心算法和二分查找组成
- 由小满提供注释
function getSequence (arr) { const p = arr.slice() const result = [0] let i, j, u, v, c const len = arr.length for (i = 0; i < len; i++) { const arrI = arr[i] // 排除等于 0 的情况 if (arrI !== 0) { j = result[result.length - 1] // 与最后一项进行比较 if (arr[j] < arrI) { // 存储在 result 更新前的最后一个索引的值 p[i] = j result.push(i) continue } u = 0 v = result.length - 1 // 二分搜索,查找比 arrI 小的节点,更新 result 的值 while (u < v) { // 取整得到当前位置 c = ((u + v) / 2) | 0 if (arr[result[c]] < arrI) { u = c + 1 } else { v = c } } if (arrI < arr[result[u]]) { if (u > 0) { // 正确的结果 p[i] = result[u - 1] } // 有可能替换会导致结果不正确,需要一个新数组 p 记录正确的结果 result[u] = i } } } u = result.length v = result[u - 1] // 回溯数组 p,找到最终的索引 while (u-- > 0) { result[u] = v v = p[v] } return result }
diff算法源码
// fast path if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // this could be either fully-keyed or mixed (some keyed some not) // presence of patchFlag means children are guaranteed to be arrays //有key的diff算法 patchKeyedChildren( c1 as VNode[], c2 as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) { // unkeyed(没有key的diff算法) patchUnkeyedChildren( //c1、c2都是由虚拟DOM生成的 c1 as VNode[],//c1是旧的Vnode c2 as VNodeArrayChildren,//c2是指新的Vnode container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized ) return } }
乱序的对比,为了防止浪费性能,前面对比一下,后面对比一下,能够有效的分辨出是刚开始就不一样了还是最后不一样。最后面往前移,直到遇到不一样了之后在开始进行下一步的排序复用
第六章 — Ref全家桶 & 源码解析(重写部分)
小工具介绍 => 根据输出提前自定义好的单词可以输出自定义对应的内容(乍一看好像有点像剪切板)
在vue.json中进行设置
{ "Print to console":{ "prefix":"自定义单词部分", "body":[ "输出自定义内容" ] } }
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value
property,指向该内部值。
不同的声明方法
- 推荐类型比较复杂的时候方便自定义
- 或者什么都不要,让编辑器自己推导
这里要响应改变值的话,要记得加.value,因为Proxy是在.value中的
<template> <div>{{Man}}</div> <button @click="change()">修改</button> </template> <script setup lang="ts"> import {ref,isRef} from 'vue' const Man = ref({name:"小余"}) const change = () =>{ Man.value.name = 'yupi' console.log(Man) console.log(isRef(Man)) } </script>
isRef
作用:判断一个东西是不是一个ref对象,结果返回布尔值
console.log(isRef(Man))
shallowRef
作用:shallowRef是浅层次的响应,而ref是深层次的响应
shallowRef只响应到.value,如果这个.value后面在跟值进行修改,就只能在控制台中看见值改变了,而没办法响应到页面中
也就是创建一个跟踪自身 .value
变化的 ref,但不会使其值也变成响应式的
注意:ref不能跟shallowRef一起使用,不然shallowRef会被Ref影响,从而造成视图的更新(失去浅层次响应的作用)
<template> <div>Ref:{{XiaoYu}}</div> <div>shallowRef:{{XiaoYu2}}</div> <button @click="change()">修改</button> </template> <script setup lang="ts"> import {ref,isRef,shallowRef} from 'vue' const XiaoYu = ref({name:"小余"}) const XiaoYu2 = shallowRef({name:"小余2号"}) const yupi = {name:"鱼皮"} const change = () =>{ // XiaoYu.value.name = '小余抓住了一只yupi' 这里必须注销掉,不如ref会影响到shallowRef,导致视图的更新(也就是会造成下方的小余今天摸鱼了也一同刷新) XiaoYu2.value.name = '小余今天摸鱼了' console.log(XiaoYu) console.log("这是小余",isRef(XiaoYu)) console.log("这是鱼皮",isRef(yupi)) } </script>
triggerRef
- 因为在ref底层更新的逻辑的时候,会调用triggerRef
- 而triggerRef会强制更新我们这个收集的依赖
- 强制更新页面 DOM
customRef
- 让我们自己去自定义一个ref
- customRef 是个工厂函数要求我们返回一个对象 并且实现 get 和 set 适合去做防抖之类的
- customRef也是一个浅层的响应
- 里面返回的是set跟get两个方法,回调里面接收的是两个函数,一个是track,一个是trigger
- track:收集依赖,收集完return回去
- 触发依赖交给set,接收一个newVal
- track 作用:通知 Vue 追踪 value 的变化 (相当于提前和 get 商量一下,让他认为这个 value 有用的!)
- trigger 作用:通知 Vue 重新解析模板。
<template> <div>{{obj}}</div>//不用设置ref了,因为我们自己手动实现了一个 <hr> <div> {{ name }} </div> <hr> <button @click="change()">修改</button> </template> <script setup lang='ts'> import { ref, reactive, onMounted, shallowRef, customRef } from 'vue' //这下面是customRef的演示,自己定义ref,接近源码的表现 const change = ()=>{ function MyRef<T>(value:T){//泛型 let timer:any return customRef((track, trigger)=>{//track:收集依赖的 trigger:触发依赖 return{ get(){ track() return value//收集完依赖就返回 回去了 }, set(newVal){//这里会收到新的值,调用接口,调用太多次的话,服务器的压力就会很大,这个时候我们就可以自己设置一个防抖节流 clearTimeout(timer)//清除定时器,不让他同时存在多个定时器(同一时间只能有一个定时器) timer = setTimeout(()=>{ console.log("吃午饭了") value = newVal//将新值赋给value timer = null//清空一下时间 trigger() },500)//时间间隔设置为0.5秒,防抖节流 } } }) } const obj = MyRef<string>('customRef小余') const change = ()=>{ obj.value = "customRef被修改了,到饭点准备干饭了" } </script> <style scoped> </style>
ref小技巧
- 打开这个能够让我们观察value方便一点,控制台点一下就行,少点一下。就是格式化了,e f系列跟reactive系列都可以使用
- ref是可以获取DOM元素的,在html部分中ref="xxx",这个xxx要与在JavaScript里面声明的变量名一样,比如const xxx = ref<泛型>(),然后console控制台输出一下,ref.value.innerText就能够获取到了
源码部分
在packages里面这个reactivity下面这个ref.js中,代码在六七十行左右
在视频15分钟开始解析源码
收获:通过观看源码获知了ref跟shallowRef的区别在于.value后会不会进行一个判断,如果后面跟着数组对象之类的,ref会进行判断return create(value,true)
创建一个Ref去继续接收,从而实现响应。shallowRef就不是true而是false,所以当shallowRef的value后面跟着东西的话,不会给他套上Ref的,也就是不会进行创建Ref,那value后面自然就不会是响应的了
第七章 — Reactive全家桶 & 源码讲解
ref支持所有类型,Reactive支持引用类型,Array,Object,Map,Set
ref取值是需要.value的,reactive取值的话不需要.value
- reactive proxy不能直接赋值,不然是会破坏响应式对象的
- 解决方法(解构:从数组和对象中提取值,对变量进行赋值,这被称为解构)
- 数组可以使用push加解构(...res),把数组里的内容拿出来
- 或者添加一个对象 把数组作为一个属性去解决
<script> let obj = reactive({name:'小余'}) const read = readonly(obj) //readonly就是把被他包在里面的变成只读的,但会受到reactive的影响 </script>
- shallowReactive
- 一样跟shallowRef是浅层次的修改
- 但一样跟shallowRef一样,shallowReactive也会受到Reactive的影响
toRefs()适合做一些解构的操作,当我们想要解构reactive的时候先考虑toRefs
toref只能修改响应式对象的值 非常响应式视图毫无变化
解构目的 =>将原本一些没法响应式使其响应
第八章 — toRaw,toRefs,toRaw &源码解析
- toRef 和传入的数据形成引用关系,修改 toRef 会影响之的数据,但是不会更新 UI
- ref 是单纯的复制,影响不影响之前复制的数据,取决于复制的数据类型,但是会更新 UI
- ref 数据会引起监听行为,而 toRef 不会
- 与经过reactive包装的内容相比,我们可以看到,toRaw把reactive套上的Proxy外壳给脱掉了
源码解析
toRef部分
- 通过一个变量将对象内的值取出,然后返回一个3元表达式
- 这个三元表达式内容为:经过isRef的判断是不是Ref类型的,是则直接返回,不是则
(new ObjectRefImpl(Object,key,defaultValue)) 没错,变成了一个RefImpl
- RefImpl是做了收集依赖
trackRefValue(this)
的,然后触发依赖的更新triggerRefValue(this,newVal)
- toRef创建出来的RefImpl并没有收集依赖,也没有触发依赖更新
- 这就是toRef对非响应式对象是不会改变视图的,因为没有收集依赖也没有触发依赖的更新这两个操作
- 响应式对象可以改变视图的因为用
reactive
,reactive
里面调用Proxy,这个里面就已经触发了收集依赖跟触发依赖这两个操作。所以我们在这个RefImpl中没有看见这个<收集依赖、触发更新>两个操作是正常的,不然已经通过reactive触发过两个操作了,这里再次调用就连续调用两次了,会出现bug。并且没办法区分出这个原始对象 - 因为你这里调用的话,已经响应式的变成触发2次,原始对象也触发一次了,那就不好区分了
toRefs部分
- 刚开始就先判断一个是不是对象,是的话将其里面的值放到数组中初始化一下(ps:真的很严谨)
- 然后每个属性去做一下toRef,然后把这个内容做一个返回。也判断了一下是不是Proxy对象
toRaw部分
- 根据一个 Vue 创建的代理返回其原始对象。
- 这是一个可以用于临时读取而不引起代理访问 / 跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
- 在这里对raw进行的是一个跳转操作,
ReactiveFlags.RAW
,ReactiveFlags是定义在上方的,对RAW做一个赋值'_v_raw'
操作,这个操作就是将原始对象给取出来了
Vue3——基础内容部分(小满版本)(三)https://developer.aliyun.com/article/1470375