Vue3——基础内容部分(小满版本)(二)

简介: Vue3——基础内容部分(小满版本)

Vue3——基础内容部分(小满版本)(一)https://developer.aliyun.com/article/1470371

第五章 — 虚拟DOM和VueKeyDiff算法(代号:巴别塔)

前置了解

  • Diff算法是面试的高频问答点,在很多的语言中也会去使用
  • TypeScriptJavaScript的转换过程也会进行AST转换
  • 插件babel,ES6语法转ES5的时候也会经过抽象语法树的一个转换
  • js走V8引擎转字节码的时候,也会经过AST

介绍虚拟DOM

Vue Template Explorer

  • 就是通过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算法

  • 先来看看图片的描述

image.png

image.png

没有key的diff算法

一共3步


  1. 无key,patch的时候会替换
  2. 新增
  3. 删除


  • 情况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,指向该内部值。

不同的声明方法

image.png

  • 推荐类型比较复杂的时候方便自定义

image.png

  • 或者什么都不要,让编辑器自己推导

image.png

这里要响应改变值的话,要记得加.value,因为Proxy是在.value中的

image.png

<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小技巧

  1. image.png
  2. 打开这个能够让我们观察value方便一点,控制台点一下就行,少点一下。就是格式化了,e f系列跟reactive系列都可以使用
  3. 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 不会
  1. 与经过reactive包装的内容相比,我们可以看到,toRaw把reactive套上的Proxy外壳给脱掉了

源码解析

toRef部分

  • 通过一个变量将对象内的值取出,然后返回一个3元表达式
  • 这个三元表达式内容为:经过isRef的判断是不是Ref类型的,是则直接返回,不是则
(new ObjectRefImpl(Object,key,defaultValue))
没错,变成了一个RefImpl


  • RefImpl是做了收集依赖trackRefValue(this)的,然后触发依赖的更新triggerRefValue(this,newVal)
  • toRef创建出来的RefImpl并没有收集依赖,也没有触发依赖更新
  • 这就是toRef对非响应式对象是不会改变视图的,因为没有收集依赖也没有触发依赖的更新这两个操作
  • 响应式对象可以改变视图的因为用reactivereactive里面调用Proxy,这个里面就已经触发了收集依赖跟触发依赖这两个操作。所以我们在这个RefImpl中没有看见这个<收集依赖、触发更新>两个操作是正常的,不然已经通过reactive触发过两个操作了,这里再次调用就连续调用两次了,会出现bug。并且没办法区分出这个原始对象
  • 因为你这里调用的话,已经响应式的变成触发2次,原始对象也触发一次了,那就不好区分了

toRefs部分

  • 刚开始就先判断一个是不是对象,是的话将其里面的值放到数组中初始化一下(ps:真的很严谨)
  • 然后每个属性去做一下toRef,然后把这个内容做一个返回。也判断了一下是不是Proxy对象

toRaw部分

  • 根据一个 Vue 创建的代理返回其原始对象。
  • 这是一个可以用于临时读取而不引起代理访问 / 跟踪开销,或是写入而不触发更改的特殊方法。不建议保存对原始对象的持久引用,请谨慎使用。
  1. 在这里对raw进行的是一个跳转操作,ReactiveFlags.RAW,ReactiveFlags是定义在上方的,对RAW做一个赋值'_v_raw'操作,这个操作就是将原始对象给取出来了


Vue3——基础内容部分(小满版本)(三)https://developer.aliyun.com/article/1470375

目录
相关文章
|
缓存 JavaScript 前端开发
Vue3——基础内容部分(小满版本)(四)
Vue3——基础内容部分(小满版本)
35 0
|
30天前
|
JavaScript JSON 资源调度
Vue3——基础内容部分(小满版本)(一)
Vue3——基础内容部分(小满版本)
28 0
|
缓存 JavaScript 开发者
Vue3——基础内容部分(小满版本)(三)
Vue3——基础内容部分(小满版本)
43 0
|
30天前
|
算法 JavaScript 前端开发
Vue3——Router4教程(小满版本)(一)
Vue3——Router4教程(小满版本)
47 0
|
30天前
|
缓存 JavaScript 前端开发
Vue3——Router4教程(小满版本)(二)
Vue3——Router4教程(小满版本)
57 0
|
3月前
|
JavaScript
Vue工具和生态系统:请解释Vue中的mixin是什么?有哪些注意事项?
Vue工具和生态系统:请解释Vue中的mixin是什么?有哪些注意事项?
22 0
|
4月前
【Vue2.0学习】—el与data的两种写法(三十六)
【Vue2.0学习】—el与data的两种写法(三十六)
|
5月前
|
JavaScript 前端开发 容器
01Vue基础之模榜语法
01Vue基础之模榜语法
28 0
|
Web App开发 缓存 JavaScript