Vue

vue3相关

Posted by Qz on April 13, 2020

“Yeah It’s on. ”

前序

自己写的vue3基本使用

vue api 查询 https://vuejs.org/api/

Reflect

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

首先我们要了解一下,为什么会新添加这么一个全局对象?如果你看过Reflect的一些函数,你就会发现,这个对象上的方法基本上都可以从Object上面找到,找不到的那些,也是可以通过对对象命令式的操作去实现的;那么为什么还要新添加一个呢?

  1. Reflect上面的一些方法并不是专门为对象设计的,比如Reflect.apply方法,它的参数是一个函数,如果使用Object.apply(func)会让人感觉很奇怪。

  2. 用一个单一的全局对象去存储这些方法,能够保持其它的JavaScript代码的整洁、干净。不然的话,这些方法可能是全局的,或者要通过原型来调用。

  3. 将一些命令式的操作如delete,in等使用函数来替代,这样做的目的是为了让代码更加好维护,更容易向下兼容;也避免出现更多的保留字。

Reflect.apply
Reflect.construct
Reflect.defineProperty
Reflect.deleteProperty
Reflect.enumerate // 废弃的
Reflect.get
Reflect.getOwnPropertyDescriptor
Reflect.getPrototypeOf
Reflect.has
Reflect.isExtensible
Reflect.ownKeys
Reflect.preventExtensions
Reflect.set
Reflect.setPrototypeOf

Proxy基础

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler

Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

正文

Vue3 的响应式和以前有什么区别?源码级别详细

而 Vue3 所使用的 Proxy,则是这样拦截的:

new Proxy(data, {
  get(key) { },
  set(key, value) { },
})

可以看到,根本不需要关心具体的 key,它去拦截的是 「修改 data 上的任意 key」 和 「读取 data 上的任意 key」。

所以,不管是已有的 key 还是新增的 key,都逃不过它的魔爪。

但是 Proxy 更加强大的地方还在于 Proxy 除了 get 和 set,还可以拦截更多的操作符。


先最小化的讲解一下响应式的原理,其实就是在 Proxy 第二个参数 handler 也就是陷阱操作符中,拦截各种取值、赋值操作,依托 tracktrigger 两个函数进行依赖收集和派发更新。

track 用来在读取时收集依赖。

trigger 用来在更新时触发依赖。

track

function track(target: object, type: TrackOpTypes, key: unknown) {
  const depsMap = targetMap.get(target);
  // 收集依赖时 通过 key 建立一个 set
  let dep = new Set()
  targetMap.set(ITERATE_KEY, dep)
  // 这个 effect 可以先理解为更新函数 存放在 dep 里
  dep.add(effect)    
}

target 是原对象。

type 是本次收集的类型,也就是收集依赖的时候用来标识是什么类型的操作,比如上文依赖中的类型就是 get,这个后续会详细讲解。

key` 是指本次访问的是数据中的哪个 key,比如上文例子中收集依赖的 key 就是 `count

首先全局会存在一个 targetMap,它用来建立 数据 -> 依赖 的映射,它是一个 WeakMap 数据结构。

targetMap 通过数据 target,可以获取到 depsMap,它用来存放这个数据对应的所有响应式依赖。

depsMap 的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数。

是不是有点绕?我们用一个具体的例子来举例吧。

const target = { count: 1}
const data = reactive(target)

const effection = effect(() => {
  console.log(data.count)
})

对于这个例子的依赖关系,

  1. 全局的 targetMap 是:
targetMap: {
  { count: 1 }: dep    
}

  1. dep 则是
dep: {
  count: Set { effection }
}

这样一层层的下去,就可以通过 target 找到 count 对应的更新函数 effection 了。

trigger

这里是最小化的实现,仅仅为了便于理解原理,实际上要复杂很多,

其实 type 的作用很关键,先记住,后面会详细讲。

export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
) {
  // 简化来说 就是通过 key 找到所有更新函数 依次执行
  const dep = targetMap.get(target)
  dep.get(key).forEach(effect => effect())
}

应用程序初始化

// 在 Vue.js 3.0 中,初始化一个应用的方式如下
import { createApp } from 'vue'
import App from './app'
const app = createApp(App)
app.mount('#app')

const createApp = ((...args) => {
  // 创建 app 对象
  const app = ensureRenderer().createApp(...args)
  const { mount } = app
  // 重写 mount 方法
  app.mount = (containerOrSelector) => {
    // ...
  }
  return app
})

diff

https://mp.weixin.qq.com/s/jW3-icYQaJVITCYi730iIw

// packages/runtime-core/src/renderer.ts
function getSequence(arr: number[]): number[] {
  const p = arr.slice() // 拷贝一个数组 p
  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) { 
        p[i] = j // 最后一项与 p 对应的索引进行对应
        result.push(i)
        continue
      }
      // arrI 比 arr[j] 小,使用二分查找找到后替换它
      // 定义二分查找区间
      u = 0
      v = result.length - 1
      // 开启二分查找
      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]  // 正确的结果
        }
        result[u] = i // 有可能替换会导致结果不正确,需要一个新数组 p 记录正确的结果
      }
    }
  }
  u = result.length
  v = result[u - 1]
  // 倒叙回溯 用 p 覆盖 result 进而找到最终正确的索引
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

贪心 + 二分查找法

getSequence 的作用就是找到那些不需要移动的元素,在遍历的过程中,我们可以直接跳过不进行其他操作。

Vue3 通过拷贝一个数组,用来存储正确的结果,然后通过回溯赋值的方式解决了贪心 + 二分查找替换方式可能造成的值不正确的问题。

@vue/reactivity

响应式的功能被封装在 @vue/reactivity 模块中,该模块为我们提供了一个 reactive 函数来创建响应式对象。

// packages/reactivity/src/reactive.ts
export function reactive(target: object) {
  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
    return target
  }
  return createReactiveObject(
    target,
    false,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

reactive 函数内部,会继续调用 createReactiveObject 函数来创建响应式对象,该函数也是被定义在 reactive.ts 文件中,该函数的的具体实现如下:

// packages/reactivity/src/reactive.ts
function createReactiveObject(
  target: Target,
  isReadonly: boolean,
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 省略部分代码  
  const proxyMap = isReadonly ? readonlyMap : reactiveMap
  const existingProxy = proxyMap.get(target)
  if (existingProxy) {
    return existingProxy
  }
  const proxy = new Proxy(
    target,
    targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
  )
  proxyMap.set(target, proxy)
  return proxy
}

createReactiveObject 函数内部,我们终于见到了期待已久的 Proxy 对象。当 target 对象不是集合类型的对象,比如 Map、Set、WeakMap 和 WeakSet 时,在创建 Proxy 对象时,使用的是 baseHandlers,该 handler 对象定义了以下 5 种捕获器:

export const mutableHandlers: ProxyHandler<object> = {
  get,
  set,
  deleteProperty,
  has,
  ownKeys
}

其中 getset 捕获器是分别用于收集 effect 函数和触发 effect 函数的执行。

Vue 3 的 $emit 与 Vue 2 的 $emit

https://juejin.cn/post/6938176673292484615?utm_source=gold_browser_extension

在 Vue 2 中 $emit 方法是 Vue.prototype 对象上的属性,而 Vue 3 上的 $emit 是组件实例上的一个属性,instance.emit = emit.bind(null, instance)

watch vs watchEffect

https://www.thisdot.co/blog/vue-3-composition-api-watch-and-watcheffect

ref vs reactive

ref vs reactive

  • reactive() only takes objects, NOT JS primitives (String, Boolean, Number, BigInt, Symbol, null, undefined)
  • ref() is calling reactive() behind the scenes
  • Since reactive() works for objects and ref() calls reactive(), objects work for both
  • BUT, ref() has a .value property for reassigning, reactive() does not have this and therefore CANNOT be reassigned

优化

Block Tree 和 PatchFlags

https://zhuanlan.zhihu.com/p/150732926

vue3.0它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。

Block 配合 PatchFlags 做到靶向更新

v-for 的元素作为 Block

不仅 v-if 会让 DOM 结构不稳定,v-for 也会,但是 v-for 的情况稍微复杂一些。思考如下模板:

<div>
    <p v-for="item in list"></p>
    <i></i>
    <i></i>
</div>

假设 list 值由 [1 ,2] 变为 [1],按照之前的思路,最外层的 <div> 标签作为一个 Block,那么它更新前后对应的 Block Tree 应该是:

// 前
const prevBlock = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: 1, 1 /* TEXT */ },
        { tag: 'p', children: 2, 1 /* TEXT */ },
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

// 后
const nextBlock = {
    tag: 'div',
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ },
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

prevBlcok 中有四个动态节点,nextBlock 中有三个动态节点。这时候要如何进行 Diff?有的同学可能会说拿 dynamicChildren 进行传统 Diff,这是不对的,因为传统 Diff 的一个前置条件是同层级节点间的 Diff,但是 dynamicChildren 内的节点未必是同层级的,这一点我们之前就提到过。

实际上我们只需要让 v-for 的元素也作为一个 Block 就可以了。这样无论 v-for 怎么变化,它始终都是一个 Block,这保证了结构稳定,无论 v-for 怎么变化,这颗 Block Tree 看上去都是:

const block = {
    tag: 'div',
    dynamicChildren: [
        // 这是一个 Block 哦,它有 dynamicChildren
        { tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }
        { tag: 'i', children: ctx.foo, 1 /* TEXT */ },
        { tag: 'i', children: ctx.bar, 1 /* TEXT */ },
    ]
}

不稳定的 Fragment

刚刚我们使用一个 Fragment 并让它充当 Block 的角色解决了 v-for 元素所在层级的结构稳定,但我们来看一下这个 Fragment 本身:

{ tag: Fragment, dynamicChildren: [/*.. v-for 的节点 ..*/] }

对于如下这样的模板:

<p v-for="item in list"></p>

在 list 由 [1, 2] 变成 [1] 的前后,Fragment 这个 Block 看上去应该是:

// 前
const prevBlock = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ },
        { tag: 'p', children: item, 2 /* TEXT */ }
    ]
}

// 后
const prevBlock = {
    tag: Fragment,
    dynamicChildren: [
        { tag: 'p', children: item, 1 /* TEXT */ }
    ]
}

我们发现,Fragment 这个 Block 仍然面临结构不稳定的情况,所谓结构不稳定从结果上看指的是更新前后一个 blockdynamicChildren 中收集的动态节点数量或顺序的不一致。这种不一致会导致我们没有办法直接进行靶向 Diff,怎么办呢?其实对于这种情况是没有办法的,我们只能抛弃 dynamicChildrenDiff,并回退到传统 Diff:即 Diff Fragmentchildren 而非 dynamicChildren

但需要注意的是 Fragment 的子节点(children)仍然可以是 Block

const block = {
    tag: Fragment,
    children: [
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ },
        { tag: 'p', children: item, dynamicChildren: [/*...*/], 1 /* TEXT */ }
    ]
}

这样,对于 <p> 标签及其子代节点的 Diff 将恢复 Block TreeDiff 模式。

静态提升

Vue3Compiler 如果开启了 hoistStatic 选项则会提升静态节点,或静态的属性,这可以减少创建 VNode 的消耗,如下模板所示:

<div>
    <p>text</p>
</div>

在没有被提升的情况下其渲染函数相当于:

function render() {
    return (openBlock(), createBlock('div', null, [
        createVNode('p', null, 'text')
    ]))
}

很明显,p 标签是静态的,它不会改变。但是如上渲染函数的问题也很明显,如果组件内存在动态的内容,当渲染函数重新执行时,即使 p 标签是静态的,那么它对应的 VNode 也会重新创建。当开启静态提升后,其渲染函数如下:

const hoist1 = createVNode('p', null, 'text')

function render() {
    return (openBlock(), createBlock('div', null, [
        hoist1
    ]))
}

使用自定义指令的元素

实际上一个元素如果使用除 v-pre/v-cloak 之外的所有 Vue 原生提供的指令,都不会被提升,使用自定义指令也不会被提升,例如:

<p v-custom></p>

和使用 key 一样,会为这段模板对应的 VNode 打上 NEED_PATCH 标志。顺便讲一下手写渲染函数时如何应用自定义指令,自定义指令是一种运行时指令,与组件的生命周期类似,一个 VNode 对象也有它自己生命周期

手写高性能渲染函数

几个需要记住的小点:

  1. 一个 Block 就是一个特殊的 VNode,可以理解为它只是比普通 VNode 多了一个 dynamicChildren 属性
  2. createBlock() 函数和 createVNode() 函数的调用签名几乎相同,实际上 createBlock() 函数内部就是封装了 createVNode(),这再次证明 Block 就是 VNode
  3. 在调用 createBlock() 创建 Block 前要先调用 openBlock() 函数,通常这两个函数配合逗号运算符一同出现:
render() {
    return (openBlock(), createBlock('div'))
}

Slot hint

我们在“稳定的 Fragment”一节中提到了 slot hint,当我们为组件编写插槽内容时,为了告诉 runtime:“我们已经能够保证插槽内容的结构稳定”,则需要使用 slot hint

render() {
    return (openBlock(), createBlock(Comp, null, {
        default: () => [
            refVal.value
               ? (openBlock(), createBlock('p', ...)) 
               ? (openBlock(), createBlock('div', ...)) 
        ],
        // slot hint
        _: 1
    }))
}

当然如果你不能保证这一点,或者觉得心智负担大,那么就不要写 hint 了。

EffectScope

聊一聊 Effect Scope API

副作用生效的作用域

// effect, computed, watch, watchEffect created inside the scope will be collected

const scope = effectScope()

scope.run(() => {
  const doubled = computed(() => counter.value * 2)

  watch(doubled, () => console.log(doubled.value))

  watchEffect(() => console.log('Count: ', doubled.value))
})

// to dispose all effects in the scope
scope.stop()

补充

vue3 放弃了 Object.defineProperty

Object.defineProperty存在的问题

  1. 不能监听数组索引和长度的变更
  2. 无法监听 属性的添加和删除
  3. 必须遍历对象的每个属性, 必须深层遍历嵌套的对象。Object.defineProperty是对对象属性的操作,所以需要对对象进行深度遍历去对属性进行操作。

为什么我们在使用vue2的时候,一样可以监听数组索引和长度的变更?

我们只是说Object.defineProperty不能,并没说vue2.0不能。并不能说因为vue2.0使Object.defineProperty,vue2.0就不能监听数组内部属性的变化了。而vue2之所以能监听,是vue2.0对数组相关的方法或其他进行了重写。当然vue2.0中还是存在无法监听直接修改数组中某一项值和数组长度,如ar[0]=1, arr.length=12是无法监听的

vue-cli-plugin-commitlint

vue-cli-plugin-commitlint 是以 vue 插件的形式写的,可以执行 vue add commitlint 直接使用,如果不是 vue 的项目也可以根据下面的配置自行配置。

  1. 自动检测 commit 是否规范,不规范不允许提交
  2. 自动提示 commit 填写格式。不怕忘记规范怎么写
  3. 集成 git add && git commit 不需要执行两个命令
  4. 自动生成 changelog

Ref vs Reactive

https://vuejsdevelopers.com/2022/06/01/ref-vs-reactive/

Unlike ref, reactive can only be initialized with an object. Each property of the object can be a different reactive variable, however.

const data = reactive({
  count: 0,
  name: 'Peter Griffin',
  flag: false
})

One advantage of reactive is that it doesn’t use a value property so it may be a little easier to read.

data.name === 'Peter Griffin' // true
data.name.value === 'Peter Griffin' // false

ref advantages:

  • Much easier to pass single variables around your app
  • Avoids destructuring pitfall

reactive advantages:

  • Can be less verbose if declaring lots of reactive variables
  • Consistency between JavaScript and template
  • Similar to Vue 2’s data object

I normally use ref as I find it more flexible.