前言
本篇还是以看得见的思考来分析整个 update 过程
后续还会有总结的文章
基本的流程是
先用看的见的思考来看源码
然后总结
问题
- scopeId 都有啥用
正文
初始化逻辑搞定之后 接着就是看看更新逻辑了
更新逻辑着可是核心中的核心
这个搞懂之后 在去给 vue 提 pr 那可就轻松多了
好了, 废话不多说了 先看源码
什么时候会触发 update 的逻辑
首先第一个问题 我们需要考虑什么时候会触发 update 的逻辑
先上应用层的代码
可以看这个组件,当我们点击 button 的时候 视图一定会刷新
那么在源码里面的流程是怎么样的呢
还记得我们的 setupRenderEffect 逻辑嘛
还是贴代码在回顾一下吧
重点来了,注意这里的 effect
我们是在 effect 里面调用的 render 函数
而当我们调用 render 函数的话,肯定会触发响应式对象的 get ,着其实是关于 reactivity 的核心逻辑,如何收集依赖和如何触发依赖的。
这里我们暂时知道当我们调用 render 函数之后,会触发依赖收集,收集的就是当前用 effect 包裹的这个 function,后面当我们的响应式数据变动的时候回再次调用这个 function。
比如上面使用层的代码,点击按钮的时候 count 变了。当 count 变了之后就会触发依赖,也就调用了我们的这个 function
着也就是 update 逻辑的入口
这个入口整明白了这样就和初始化的逻辑结合在一起了
接着往下看
现在可以直接挂住 else 里面的逻辑了
我先整理一下代码
// updateComponent
// This is triggered by mutation of component's own state (next: null)
// OR parent calling processComponent (next: VNode)
let { next, bu, u, parent, vnode } = instance
let originNext = next
let vnodeHook: VNodeHook | null | undefined
if (next) {
updateComponentPreRender(instance, next, optimized)
} else {
next = vnode
}
const nextTree = renderComponentRoot(instance)
const prevTree = instance.subTree
instance.subTree = nextTree
next.el = vnode.el
// beforeUpdate hook
if (bu) {
invokeArrayFns(bu)
}
// onVnodeBeforeUpdate
if ((vnodeHook = next.props && next.props.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parent, next, vnode)
}
// reset refs
// only needed if previous patch had refs
if (instance.refs !== EMPTY_OBJ) {
instance.refs = {}
}
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
next.el = nextTree.el
if (originNext === null) {
// self-triggered update. In case of HOC, update parent component
// vnode el. HOC is indicated by parent instance's subTree pointing
// to child component's vnode
updateHOCHostEl(instance, nextTree.el)
}
// updated hook
if (u) {
queuePostRenderEffect(u, parentSuspense)
}
// onVnodeUpdated
if ((vnodeHook = next.props && next.props.onVnodeUpdated)) {
queuePostRenderEffect(() => {
invokeVNodeHook(vnodeHook!, parent, next!, vnode)
}, parentSuspense)
}
}
}, __DEV__ ? createDevEffectOptions(instance) : prodEffectOptions)
这里有个疑惑点是 Instance.next 是个什么鬼,
先去查查注释
好吧,看着注释也没有太看明白,先过,回头再来看
不过猜测一下的话,第一次 update 的时候这个 next 应该是个 null ,
那么我先把涉及到处理 next 的逻辑先去掉
咦 我发现在这里给 next 赋值了。next 等于当前的 vnode 。
调用 renderComponentRoot
然后调用了 renderComponentRoot(instance)
简化逻辑
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
parent,
vnode,
proxy,
withProxy,
props,
slots,
attrs,
emit,
renderCache
} = instance
let result
currentRenderingInstance = instance
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// withProxy is a proxy with a different `has` trap only for
// runtime-compiled render functions using `with` block.
const proxyToUse = withProxy || proxy
result = normalizeVNode(
instance.render!.call(proxyToUse, proxyToUse!, renderCache)
)
fallthroughAttrs = attrs
} else {
// 暂时先都干掉,不需要关心
}
// attr merging
// in dev mode, comments are preserved, and it's possible for a template
// to have comments along side the root element which makes it a fragment
let root = result
// inherit scopeId
const parentScopeId = parent && parent.type.__scopeId
if (parentScopeId) {
root = cloneVNode(root, { [parentScopeId]: '' })
}
// inherit directives
if (vnode.dirs) {
if (__DEV__ && !isElementRoot(root)) {
warn(
`Runtime directive used on component with non-element root node. ` +
`The directives will not function as intended.`
)
}
root.dirs = vnode.dirs
}
// inherit transition data
if (vnode.transition) {
root.transition = vnode.transition
}
// inherit ref
if (Component.inheritRef && vnode.ref != null) {
root.ref = vnode.ref
}
result = root
} catch (err) {
}
currentRenderingInstance = null
return result
}
我们看看到底都做了啥
- 再次调用 render 函数得到新的 vnode ,这里命名为 result
- 继承之前 vnode 的父级 scopeId ? 这里的 scopeId 都有啥用呢?(记录一下)
- 继承 directives
- 继承 transition data
- 继承 ref
稍微分析分析,其实呢 这里就是再次调用 render 函数,然后返回出去,别的杂七杂八的事先不管。
再次回到 setupRenderEffect 继续往下看
着也是个关键逻辑,做数据的更替了
把之前的 vnode 赋值给 prevTree,把现在的 vnode 赋值给 instance.subTree
接着还需要更新一下 el(实际渲染出来的 element)
接着调用一些 hook
- beforeUpdate hook
- onVnodeBeforeUpdate
接着就是重点啦,再次调用 patch
只不过和我们初始化的时候对比,现在的 n1 是有值的了。
我们先把后面的逻辑看完,然后在看是如何在 patch 里面对比两个节点的
因为 patch 完了之后有可能会生成一个新的 el
所以需要把新的 el 赋值给新的 vnode 上
这里还是 hook 的调用
- updated hook
- onVnodeUpdated
好,接着我们就进入重头戏
updateComponent
因为会再次调用 patch ,然后会进行 component 类型的处理,这里当然是调用 updateComponent 啦,所以我们直接看着逻辑
简化逻辑:
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
updateComponentPreRender(instance, n2, optimized)
return
} else {
// normal update
instance.next = n2
// in case the child component is also queued, remove it to avoid
// double updating the same child component in the same flush.
invalidateJob(instance.update)
// instance.update is the reactive effect runner.
instance.update()
}
} else {
// no update needed. just copy over properties
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}
这里有几个比较重要的逻辑函数
- shouldUpdateComponent 判断到底需不需要更新
- 如果需要更新的话调用 updateComponentPreRender 或者 invalidateJob 和 instance.update()
- 不需要更新的话直接把之前的属性拿过来即可
我们这里主要分析的是 happy path ,所以只会执行到 else 里面的逻辑
也就是调用 instance.update()
而调用 update 的话,就会再次执行一遍 setupRenderEffect
我们等等在来看,先看下 shouldUpdateComponent
shouldUpdateComponent
其实这个函数的回答的问题是
什么情况下需要更新组件呢?
先简化一下代码
export function shouldUpdateComponent(
prevVNode: VNode,
nextVNode: VNode,
optimized?: boolean
): boolean {
const { props: prevProps, children: prevChildren } = prevVNode
const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
// force child update for runtime directive or transition on component vnode.
if (nextVNode.dirs || nextVNode.transition) {
return true
}
if (patchFlag > 0) {
if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
// slot content that references values that might have changed,
// e.g. in a v-for
return true
}
if (patchFlag & PatchFlags.FULL_PROPS) {
if (!prevProps) {
return !!nextProps
}
// presence of this flag indicates props are always non-null
return hasPropsChanged(prevProps, nextProps!)
} else if (patchFlag & PatchFlags.PROPS) {
const dynamicProps = nextVNode.dynamicProps!
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
if (nextProps![key] !== prevProps![key]) {
return true
}
}
}
} else if (!optimized) {
// this path is only taken by manually written render functions
// so presence of any children leads to a forced update
if (prevChildren || nextChildren) {
if (!nextChildren || !(nextChildren as any).$stable) {
return true
}
}
if (prevProps === nextProps) {
return false
}
if (!prevProps) {
return !!nextProps
}
if (!nextProps) {
return true
}
return hasPropsChanged(prevProps, nextProps)
}
return false
}
逐个来分析的话
- 如果有 dirs 和 transition 的话,会更新
- 接着是判断了 patchFlag ,这个 flag 也是个很值得一说的点,它是在编译阶段生成的,不同的模板类型会生成不一样的值,这个可以单独写个专题来分析,暂时我们先知道有这个 flag 即可。 大概有这么几种情况都需要更新
- PatchFlags.DYNAMIC_SLOTS
- PatchFlags.FULL_PROPS 这种情况的话还会对比一下之前的 props 和现在的 props 有啥不一样的,发现只要有一个 prop 不一样就会更新
- PatchFlags.PROPS 这种情况是检测动态的 props ,这里主要要关注的逻辑点是 nextVNode.dynamicProps 是什么时候给赋值的
- 接着就是检测如果有 chilren 的话,那么也需要更新。 以上分析的暂时也只是个简单的分析,具体的情况到时候在具体的分析,在回顾一下我们的目标,是了解 update 的流程,暂时先不太关注于细节
好,最后的结论是对比一下,发现需要更新的话 就返回 true,这个还思考了一个问题,就是为什么不需要对比 chilren 呢, 应该是因为 component 就是个虚拟的箱子,假如箱子的表面行为都有变动,那么在继续深入,下面要关注的点就是如何触发 updateElement 的
接下来的逻辑应该是进入了这个分支
我们在重新读一下 update ,这里和第一次进入 update 时有一点不同,就是 vnode.next 是有值的。(我们在第一次执行 update 的时候给 vnode 赋值的,还记得吗)
哈哈,暂时发现一个有趣的点,默认的代码第一次读的时候感觉好难好复杂,但是你多看它几遍的话,也就那么回事,所以以后看到复杂的代码你就多看它几遍哈哈。
因为现在 next 是有值的,所以应该会进入到 updateComponentPreRender 函数内
updateComponentPreRender
简化逻辑
const updateComponentPreRender = (
instance: ComponentInternalInstance,
nextVNode: VNode,
optimized: boolean
) => {
nextVNode.component = instance
const prevProps = instance.vnode.props
instance.vnode = nextVNode
instance.next = null
updateProps(instance, nextVNode.props, prevProps, optimized)
updateSlots(instance, nextVNode.children)
}
就是更新了 props 和 slots ,这里细节咱就先不看了
在继续往下看,我们暂时的问题是怎么更新到 component 内部的 element 的呢
啊哦,并没有发现多余的线索
看来我需要找个例子来 debug 一下。
这里我选择的策略是先从最简单的逻辑看起来
test('测试element 的 id 逻辑', async () => {
const pId = ref(1)
const Parent = {
render: () => {
return createVNode("div",{id:pId.value},null,8,['id'])
}
}
debugger;
const root = nodeOps.createElement('div')
render(h(Parent), root)
pId.value++;
await nextTick()
})
这种情况是当前的这个 div (element) 的 id 是动态的,然后 8 代表的是 PatchFlags.PROPS 在后面的数组["id"] 里面是标记着动态的 prop 是什么,这里当然就是 id 了。 有一点要说明的是,我们这里是直接写死的,如果是利用 template 来写的话,它会自动生成。
好,按照这个思路 我看看它是如何处理 component 类型的。
test('测试 component 类型的 update id逻辑', async () => {
const cId = ref(1)
const Child = {
name:"Child",
props:["id"],
render:()=>{
return h("p")
}
}
const Parent = {
name:"Parent",
render: () => {
return createVNode(Child,{id:cId.value},null,8,["id"])
}
}
debugger
const root = nodeOps.createElement('div')
render(h(Parent), root)
cId.value++
await nextTick()
})
在这种情况下,Parent 里面是嵌套了一个 Child 组件,然后我们是在 parent 的 render 函数内传给 child 的 props ,也就是说当 cId 变化后,它应该是先影响到 Child 。
接着我们来分析一下它整个 runtime 的所有逻辑流程
- cId.value ++ 的时候触发 Parent 的 update 逻辑
- 然后再次调用 Parent 的 render 函数,获取到 subTree
- 接着会触发 patch ,着时候的参数就是新得到的 subTree ,也就是 createVNode(Child)
- 因为这个 vnode 是 Child ,类型是 component 所以会走 processComponent 逻辑
- 因为 n1 是肯定有值的,所以走 updateComponent 逻辑
- 在接下来会触发 shouldUpdateComponent 逻辑,比对两个 vnode ,看看是否需要更新,这里是肯定需要更新的
- 然后又触发了 instance.update(),注意一下这里的 instance 可是 Child
- 好,我们又一次来到了 instance.update 内,着时候会再次调用 Child.render(),也可以说现在拆箱 Child
- 拆箱 Child 得到的 vnode 就是 element p 了。
- 接着用 p 的 vnode 来调用 patch
- 会调用到 patchElement ,继而对比 element
至此,对于上面的问题,从 component 是如何调用更新到内部的 element 的,就有了答案。
当然我们到现在为止只是分析了两个最简单的更新,
- 组件的更新
- element 的更新
接着把整个流程整理一下
- 修改响应式的值,触发 effect 的回调函数(触发依赖)
- 再次调用 render 函数,获取最新的 vnode 值
- 把新的 vnode 和旧的 vnode ,交给 patch
- patch 来基于 vnode 的类型进行处理具体的 update 逻辑
- 如果是 component 类型的话,会做一个 updateComponent() 的处理,检测是否可更新,如果可以更新的话会再次调用 update
- 如果是 element 类型的话,会调用 patchElement 来检测更新
- 接着就是递归的调用当前组件的 render,获取到最新的 subTree(vnode)
- 重复上面的过程
我们稍微隐喻一下,如果是 component 类型的话,我们就需要检测要不要开箱,当需要开箱的话,在处理箱子里面的 element 或者 component ,如果是 component 那么就重复上面的过程。应该是递归的向下查,截止点就是当前的 component 能不能开箱。
好,这个流程整理完了,怎么对比细节,我们先不管,先把整个流程在 mini-vue 里面实现一遍,看看有没有逻辑落下。
至此整个 update 的流程就都已经分析完了。
剩下的就是针对细节来分析了
后面的策略是基于特定的场景来分析对应的 patch 逻辑
不然的话,逻辑太多,容易在细节中迷路
- 这是我们团队的开源项目 element3
- 一个支持 vue3 的前端组件库