前置知识
运行时
- h函数生成vnode。
- render函数渲染vnode,生成真实的dom。
runtime-core 和 runtime-dom分开编写的原因: 不同的宿主环境使用不同的api。runtime-core只涉及运行时核心代码。暴露出统一的接口,让不同宿主环境定制化。
因为vue中在做一些标记计算时,大量使用到位运算,所以我们需要了解一些基础的 位运算。
左移 a << b
: 表示a乘以2的b次方右移 a >> b
: 表示a除以2的b次方,小于1的都为0。
js
1 << 2
4
1 >> 2
0
1 >> 4
0
2 >> 4
0
无符号右移 (左边空出位用 0 填充)
: 正数和a >> b
效果是一样的,但是负数就不一样了。按位与 a & b
: 表示a、b二进制对应位置全部为1,就为1。然后再转为10进制。
js
15 & 9 = 1111 & 1001 = 1001 = 9
按位或 a | b
: 表示a、b二进制对应位置只要有一位为1,就为1。然后再转为10进制。
js
15 | 9 = 1111 | 1001 = 1111 = 15
按位异或 a ^ b
: 表示a、b二进制对应位置二进制都不一样。才为1。然后再转为10进制。
js
15 | 9 = 1111 | 1001 = 0110 = 6
按位非 ~a
:表示将a二进制对应位置反位。(0改成1,1改成0),然后转为10进制。注意位运算符"非"将所有的 32 位取反,而值的最高位 (最左边的一位) 为 1 则表示负数 因为32位最后一位表示符号。 计算秘诀就是值 + 1取反(正数变负数,负数变正数)。
js
// 我以为的
~15 = ~1111 = 0000 = 0
// -(原值 + 1)
~15 = -16
话说看完这些我已经有点晕了。记住>> 、<<、| 、 &
即可。
h
h函数参数处理,调用createVNode函数
类似于适配器模式,主要是处理参数的差异,然后调用createVNode函数创建VNode。注意h函数不是只能传递三个参数,他会将三个参数往后的都作为children。
调用createVnode时,我们需要做了一些优化。
动态节点做标记 patchFlag
- 标记区分不同的类型。
- diff算法时,可以区分静态节点(不做处理),以及不同类型的动态节点。 vue2、3模板编译时进行优化对比
静态节点提升作用域 hoistStatic
- 将静态节点的定义提升到父级作用域缓存起来。
- 多个相邻的静态节点会被合并起来,作为一个静态的html模板
这种优化是一个典型的空间换时间的优化。
事件缓存 cacheHandler
标记节点类型shapeFlag(父节点和子节点)
createVNode,createBaseVNode
基本逻辑,最终输出标准vnode
- 处理type传入的类型,赋值,然后通过children的类型和type的类型
按位或
算出shapeFlag。- element + string children = 9
- element + array chidlren = 17
第一次赋值区分字符串,组件,Text, Fragment, Comment, Suspense, Teleport
等类型。其中Text, Comment, Fragment类型都是Vue内置的类型,都是Symbol类型。使用时需要导入。 第二次赋值区分children的类型值。
- 标准化props(normalizeClass, normalizeStyle),处理class, style增强写法。
- 标准化children(normalizeChidlren),然后赋值给VNode对象。
通过断点调试发现,vnode的生成是先执行children的h函数,再执行外部的h函数。
h函数的执行还是比较简单的,主要就是上面所讲到的问题,搞清楚就没啥问题了。
render
- 判断是挂载还是更新。
- 生成dom元素。
- 生成dom元素内容。
- 处理props,将其挂载到dom对应的属性上。(这里都标准化了在h函数阶段)。
- 将dom元素插入到指定的根元素中。
- 将vnode对象放在当前根元素之中。
看了一下源码,非常的复杂,暴露出一个createApp函数,回调了很多个函数。在源码实现的时候需要注意一下事项
- 在组件挂载后,我们一定要将当前vnode赋值给挂载的根节点
_vnode
属性,他是后续作为oldVNode传递的。
js
const render = (vnode: VNode, container) => {
if (vnode == null) {
// TODO: 卸载
} else {
// 打补丁patch / mount
patch(container._vnode, vnode, container)
}
// 将当前vnode赋值给container._vnode作为旧节点
container._vnode = vnode
}
- 在当前组件挂载后,我们需要将创建出来的dom元素赋值给vnode的
el
这样作为下次更新时,节点挂载的根对象。
js
const mountElement = (vnode, container, anchor) => {
const { type, props, shapeFlag } = vnode
// 创建 element
const el = (vnode.el = hostCreateElement(type))
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 设置 文本子节点
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 设置 Array 子节点
mountChildren(vnode.children, el, anchor)
}
// 处理 props
if (props) {
// 遍历 props 对象
for (const key in props) {
hostPatchProp(el, key, null, props[key])
}
}
// 插入 el 到指定的位置
hostInsert(el, container, anchor)
}
- 我们在dom更新时,需要取出挂载时赋值给
oldVNode.el
的根对象,并赋值给newVNode.el
,让其设置children和props时使用。
js
function patchElement(oldVNode, newVNode, anchor) {
// 将根节点赋值给新VNode 用于比对后挂载,并且处理props
const el = (newVNode.el = oldVNode.el!)
const oldProps = oldVNode.props || EMPTY_OBJ
const newProps = newVNode.props || EMPTY_OBJ
// patch children
patchChildren(oldVNode, newVNode, el, anchor)
// patch props
patchProps(el, newVNode, oldProps, newProps)
}
挂载element + text children
- path
- 根据type和shapeFlag来决定怎么处理挂载, 这里是通过processElement。
- 根据是否有oldVnode,来决定是挂载mountElement 还是更新patchElement。
- 内部去创建元素 hostCreateElement , 创建text hostSetElementText , 再去处理props hostPatchProp , 最后是将元素插入到container中 hostInsert。
js
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text: // 挂载文本节点
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) { // 挂载原生节点
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) { // 挂载组件节点
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
更新element + text children
- patchElement。
- 内部进行children比较。patchChildren ,判断vNode第三个参数是否是数组还是字符串,分情况比较。然后再进行props的比较, patchProps。
js
// children has 3 possibilities: text, array or no children.
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
}
if (c2 !== c1) {
hostSetElementText(container, c2 as string)
}
} else {
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// prev children was array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// two arrays, cannot assume anything, do full diff
patchKeyedChildren(
c1 as VNode[],
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// no new children, just unmount old
unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
}
} else {
// prev children was text OR null
// new children is array OR null
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// mount new if array
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
c2 as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
}
}
不同节点的element更新时
通过isSameVNodeType
比较其类型和key值是否相同。不同就卸载旧节点。然后就将旧的vNode赋值为null。由于旧节点被设置为空,所以就将新节点挂载。
patchProps 非事件props的挂载
-
class。 直接通过
el.className
设置就可。 -
style。通过style.setProperty。并且会处理css兼容。autoPreFix。
jsconst prefixes = ['Webkit', 'Moz', 'ms']
-
value。通过
el.value
设置即可。 -
其他属性。通过
el.setAttribute()
设置。
之所以区别挂载,是因为dom对象的属性和html元素的attr是不一样的概念。
在设置完新vNode的props后,它将再次遍历旧Vnode,删除新Vnode不存在的属性。
patchProps 事件props的挂载
这个和之前的属性不一样,这个会通过vei
对象进行事件缓存。将事件缓存在vei.value
中。对于事件的更新直接修改value就行,而不是通过调用addEventListener,removeEventListener
频繁的创建和移除。
js
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName] // 缓存事件
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = nextValue
} else {
const [name, options] = parseName(rawName) // 处理事件名称
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
无状态组件挂载(element + text children 组件)
- 初始化组件,createComponentInstance。组件实例中vnode属性绑定当前组件的vnode,vnode对象中的component属性绑定组件实例。
- 初始化组件一系列数据。setupComponent
- 有状态组件,执行setupStatefulComponent,如果有setup将被调用。如果没有setup我们将执行finishComponentSetup(给组件实例render赋初值)
- setupRenderEffect,为组件实例effect属性赋值ReactiveEffect对象。这个对象将是以后触发状态更新时更新组件渲染视图的关键。 其中内部生成组件的vnode对象进行挂载。
无状态组件更新 (element + text children 组件)
其实就是卸载原来组件,重新挂载组件。(这是简单的分析来说的,如果同一个type并且key相同就需要diff了)
有状态组件挂载(element + text children 组件)
其他逻辑和无状态组件挂载一样,在初始化组件实例(setupComponent)时,需要处理状态数据,将其转换成响应式数据。
有状态组件更新(element + text children 组件)
其实就是卸载原来组件,重新挂载组件。(这是简单的分析来说的,如果同一个type并且key相同就需要diff了)
生命周期
setupComponent中直接执行
- beforeCreate是在options选项处理之前调用的。
- created 是在一些数据处理options处理完毕后执行的。
通过注册,等待时机执行
- beforeMount,mounted 是在组件更新时触发的。
- beforeUpdate,updated 是在组件更新时触发的。
setup函数挂载与更新
本质上就是在setupComponent(初始化组件实例属性)中判断是否有setup,如果有将执行并将setup返回值作为组件实例render属性的值。
往期文章
- 带你从0开始了解vue3核心(computed, watch)
- 带你从0开始了解vue3核心(响应式)
- 3w+字的后台管理通用功能解决方案送给你
- 入职之前,狂补技术,4w字的前端技术解决方案送给你(vue3 + vite )
专栏文章
最近也在学习nestjs,有一起小伙伴的@我哦。