vue3.0源码解析,patch&diff过程

什么是patch

在浏览器中,每次操作dom,都会引起一次重绘重排过程,如果短时间进行多次操作,对性能损耗很大,容易引起卡顿。
在vue中,使用虚拟dom(Virtual dom),来对真实dom的一种抽象化处理的树结构,模拟真实dom,提升性能。
而在更新dom节点时,通过对虚拟dom的对比diff(如果是更新操作)来进行对虚拟dom结构的增删改的一系列操作流程,就是patch过程。

前置了解

在解读源码之前,需要知道一下vnode中的几个属性以及“最长递增子序列”算法,以便对patch流程阅读更加清晰。

最长递增子序列算法
在对比数组类型的子节点时,当新节点不是旧节点的简单push新增或pop删除等操作,有移动删除新增等操作,会使用此算法算出最小操作节点组合,用最少的执行步骤完成对比。
详情请到LeetCode查看,里面有大量解题思路,最常见的解法是动态规划
而vue里面用了维基百科里的贪心+二分查找解法去做,相比动态规划,时间复杂度能更小。
动态规划:O(n^2);
贪心+二分查找:O(nlogn);
leetcode 300. 最长递增子序列

PatchFlags.FRAGMENT:
什么情况下会为此值,STABLE_FRAGMENT、KEYED_FRAGMENT、UNKEYED_FRAGMENT必定有一个fragment包裹。
1、一个组件有多个根节点,会为其创建一个包裹:STABLE_FRAGMENT
2、v-if,有多个子节点: STABLE_FRAGMENT
3、v-for语句: KEYED_FRAGMENT、UNKEYED_FRAGMENT

// 源码v-for指令创建节点时fragmentFlag变量就是判断是否使用哪个。
const fragmentFlag = isStableFragment ? PatchFlags.STABLE_FRAGMENT : keyProp ? PatchFlags.KEYED_FRAGMENT : PatchFlags.UNKEYED_FRAGMENT;

// 其余其他属性忽略,有兴趣请自行查看
interface VNode {
   
  // 节点的key属性,被当作节点的标志,用以优化
  key: string | number | symbol | null,
  // 一个枚举值,是一个标识,描述该组件的类型,值是位运算左移的结果
  shapeFlag: number,
  // 一个枚举值,也是一个标识,描述组件的特性,帮助实现vue3中patch对比的一个特性:靶向更新
  // 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
  // 值小于0,即代表所对应的element在patch阶段,不需要进行diff
  // 重点:patchFlag可以代表多个状态组合
  patchFlag: number,
  // 存储子组件的变量
  // 纯文本 数组 slot对象
  children: string | VNodeArrayChildren | RawSlots | null
}

export const enum PatchFlags {
   
  // 动态的文本节点
  TEXT = 1,

  // 动态的class节点
  CLASS = 1 << 1,

  // 动态的style节点
  STYLE = 1 << 2,

  // 动态属性节点
  PROPS = 1 << 3,

  // 有动态key的节点,每当key改变,需要进行完整的diff
  FULL_PROPS = 1 << 4,

  // 绑定了监听事件
  HYDRATE_EVENTS = 1 << 5,

  // 不会变换子节点顺序的fragment
  STABLE_FRAGMENT = 1 << 6,
  
  // 有带key的fragment
  KEYED_FRAGMENT = 1 << 7,

  // 没有带key的fragment
  UNKEYED_FRAGMENT = 1 << 8,

  // 一个子节点只会进行非props比较
  NEED_PATCH = 1 << 9,

  // 动态slot 比如带v-for的slot插槽
  DYNAMIC_SLOTS = 1 << 10,

  // 以下类型不会被diff
  // 开发阶段注释文本,不需要diff
  DEV_ROOT_FRAGMENT = 1 << 11,

  // 静态节点,不需要diff
  HOISTED = -1,
  
  // 用来表示一个节点不需要优化模式optimized,patch时进行全比对
  BAIL = -2
}

export const enum ShapeFlags {
   
  // 普通html节点
  ELEMENT = 1, 

  // 函数组件
  FUNCTIONAL_COMPONENT = 1 << 1,

  // 普通有状态组件
  STATEFUL_COMPONENT = 1 << 2,

  // 子组件是纯文本
  TEXT_CHILDREN = 1 << 3,

  // 子组件是数组列表
  ARRAY_CHILDREN = 1 << 4,

  // 子组件有slot插槽
  SLOTS_CHILDREN = 1 << 5,

  // vue3 Teleport组件,具体请查看官方文档
  TELEPORT = 1 << 6,

  // vue3 Supspense组件,具体请查看官方文档
  SUSPENSE = 1 << 7,

  // 准备被KeepAlive的组件
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,

  // 已经被KeepAlive的组件
  COMPONENT_KEPT_ALIVE = 1 << 9,

  // 函数组件或普通有状态组件
  COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}


vnode中的dynamicChildren属性

在vnode生成过程中,如果patchFlag>0(含有动态属性、动态class、动态style、动态文案等),则会将含有动态属性的children节点映射到此属性上,进行标记,那么在diff时,就不需要全量对children遍历对比子节点,只需对dynamicChildren进行对比操作,可以减少遍历次数,加快过程。


patch函数

本文会讲出patch流程,但由于patch整体流程太过长,对于不同元素渲染会有特别处理(Fragment、自定义组件、TeleportImpl),特别处理部分不会涉及到,只总结出主要逻辑。
如有兴趣,可下载vue3源码打开packages > runtime-core > src > renderer.ts 阅读。

const patch: PatchFn = (
	// 旧节点
	n1,
	// 新节点
	n2,
	// 节点容器,父节点
	container,
	// 要以哪个节点为标准插入进这个节点的前一个位置,为空则插入到最后
	anchor = null,
	parentComponent = null,
	parentSuspense = null,
	isSVG = false,
	slotScopeIds = null,
	
	// optimized 是否优化vNode
	// __DEV__ 开发环境
	// isHmrUpdating开发热更新模式
	optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
   

	// 等于就是没变化不用对比,跳过此处节点
	if (n1 === n2) {
   
		return
	}

	// 如果tag和key值都不一样,则删除此就节点
	// patching & not same type, unmount old tree
	if (n1 && !isSameVNodeType(n1, n2)) {
   
		anchor = getNextHostNode(n1)
		unmount(/* 忽略参数 */)
		n1 = null
	}

	// 是否启用统一vnode处理
	if (n2.patchFlag === PatchFlags.BAIL) {
   
		optimized = false
		n2.dynamicChildren = null
	}


	const {
    type, ref, shapeFlag } = n2
	switch (type) {
   
		// 文本类型
		case Text:
			processText(/* 忽略参数 */)
			break

		// 注释类型
		case Comment:
			processCommentNode(/* 忽略参数 */)
			break

		// 静态节点,不会变化
		case Static:
			//   没有直接渲染,有则不用渲染,因为不会变化
			if (n1 == null) {
   
				mountStaticNode(/* 忽略参数 */)
			} else if (__DEV__) {
   
				patchStaticNode(/* 忽略参数 */)
			}
			break

		// Fragment 类型
		case Fragment:
			processFragment(/* 忽略参数 */)
			break

		default:
			if (shapeFlag & ShapeFlags.ELEMENT) {
   
				// 普通标签处理
				processElement(/* 忽略参数 */)
			} else if (shapeFlag & ShapeFlags.COMPONENT) {
   
				// 自定义组件的处理
				processComponent(/* 忽略参数 */)
			} else if (shapeFlag & ShapeFlags.TELEPORT) {
   
				// Teleport组件(vue原生组件)的处理
				; (type as typeof TeleportImpl).process(/* 忽略参数 */)
			} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
   
				// Suspense组件(vue原生组件)处理
				; (type as typeof SuspenseImpl).process(/* 忽略参数 */)
			} else if (__DEV__) {
   
				warn('Invalid VNode type:', type, `(${
     typeof type})`)
			}
	}

	// 设置dom的ref
	if (ref != null && parentComponent) {
   
		setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
	}
}

// 对比新旧的tag值以及key值
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
   
  return n1.type === n2.type && n1.key === n2.key
}

流程:

  1. 第一个if判断没什么可说,如果新旧节点相同,则跳过此节点,对下下一个节点。
  2. 第二个if,直接对比新旧的tag值以及key值,如果都不一样,则删除旧节点。
  3. 第三个if,判断是否为BAIL特性,是则不开启优化模式。
  4. 来到switch分支,首先通过节点的type属性对文本类型、注释类型、静态节点和Fragment进行特殊处理,在这里Fragment因为是一个虚拟节点vue3特性,所以实际渲染会将Fragment节点的子节点patch的container为当前的container。
  5. 如果不属于上述类型,则在switch default,通过节点的shapeFlag(描述该组件的类型)进行不同处理。
  6. 先看processElement(源码在下文放出,可滚动至下方查看),processElement函数只判断是否有旧节点,如果旧节点为空则渲染,有旧
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值