深度剖析 Vue3 如何通过虚拟DOM更新页面

上一讲我们主要介绍了 Vue 项目的首次渲染流程,在 mountComponent 中注册了effect 函数,这样,在组件数据有更新的时候,就会通知到组件的 update 方法进行更新

Vue 中组件更新的方式也是使用了响应式 + 虚拟 DOM 的方式,这个我们在第一讲中有介绍过 Vue 1、Vue 2 和 Vue 3 中更新方式的变化,今天我们就来详细剖析一下 Vue 组件内部如何通过虚拟 DOM 更新页面的代码细节

Vue 虚拟 DOM 执行流程

我们从虚拟 DOM 在 Vue 的执行流程开始讲起。在 Vue 中,我们使用虚拟 DOM 来描述页面的组件,比如下面的 template 虽然格式和 HTML 很像,但是在 Vue 的内部会解析成 JavaScript 函数,这个函数就是用来返回虚拟 DOM:

<div id="app"><p>hello world</p><Rate :value="4"></Rate>
</div> 

上面的 template 会解析成下面的函数,最终返回一个 JavaScript 的对象能够描述这段HTML:

function render(){return h('div',{id:"app"},children:[h('p',{},'hello world'),h(Rate,{value:4}),])
} 

知道虚拟 DOM 是什么之后,那么它是怎么创建的呢?


DOM 的创建

我们简单回忆上一讲介绍的 mount 函数,在代码中,我们使用 createVNode 函数创建项目的虚拟 DOM,可以看到 Vue 内部的虚拟 DOM,也就是 vnode,就是一个对象,通过 type、props、children 等属性描述整个节点

const vnode = createVNode( (rootComponent as ConcreteComponent,rootProps
)
function _createVNode() {// 处理属性和 classif (props) {...}// 标记vnode信息const shapeFlag = isString(type)? ShapeFlags.ELEMENT: __FEATURE_SUSPENSE__ && isSuspense(type)? ShapeFlags.SUSPENSE: isTeleport(type)? ShapeFlags.TELEPORT: isObject(type)? ShapeFlags.STATEFUL_COMPONENT: isFunction(type)? ShapeFlags.FUNCTIONAL_COMPONENT: 0 return createBaseVNode(type,props,children,patchFlag,dynamicProps,shapeFlag,isBlockNode,true)
}

function createBaseVNode(type,props,children,...){const vnode = {type,props,key: props && normalizeKey(props),ref: props && normalizeRef(props),children,shapeFlag,patchFlag,dynamicProps,...
} as VNode
// 标准化子节点
if (needFullChildrenNormalization) {normalizeChildren(vnode, children)
} else if (children) {vnode.shapeFlag |= isString(children)? ShapeFlags.TEXT_CHILDREN: ShapeFlags.ARRAY_CHILDREN}return vnode
}componentUpdateFn 

createVNode 负责创建 Vue 中的虚拟 DOM,而上一讲中我们讲过 mount 函数的核心逻辑就是使用 setupComponent 执行我们写的 <script setup>,使用 setupRenderEffect 监听组件的数据变化;所以我们来到 setupRenderEffect 函数中,去完整地剖析 Vue 中虚拟 DOM 的更新逻辑

我们给组件注册了 update 方法,这个方法使用 effect 包裹后,当组件内的 ref、reactive 包裹的响应式数据变化的时候就会执行 update 方法,触发组件内部的更新机制

看下面的代码,在 setupRenderEffect 内部的 componentUpdateFn 中,updateComponentPreRenderer 更新了属性和 slots,并且调用 renderComponentRoot 函数创建新的子树对象 nextTree,然后内部依然是调用 patch 函数

可以看到,Vue 源码中的实现首次渲染和更新的逻辑都写在一起,我们在递归的时候如果对一个标签实现更新和渲染,就可以用一个函数实现

const componentUpdateFn = ()=>{if (!instance.isMounted) {//首次渲染instance,parentSuspense,isSVG)。。。
}else{let { next, bu, u, parent, vnode } = instanceif (next) {next.el = vnode.elupdateComponentPreRender(instance, next, optimized)} else {next = vnode}const nextTree = renderComponentRoot(instance)patch(prevTree,nextTree,// parent may have changed if it's in a teleporthostParentNode(prevTree.el!)!,// anchor may have changed if it's in a fragmentgetNextHostNode(prevTree),instance,parentSuspense,isSVG)}
}

// 注册effect函数

const effect = new ReactiveEffect(componentUpdateFn,() => queueJob(instance.update),instance.scope // track it in component's effect scope
)
const update = (instance.update = effect.run.bind(effect) as S chedulerJo
update()

const updateComponentPreRender = ( instance: ComponentInternalInstance,nextVNode: VNode,optimized: boolean ) => {

nextVNode.component = instanceconst prevProps = instance.vnode.propsinstance.vnode = nextVNodeinstance.next = nullupdateProps(instance, nextVNode.props, prevProps, optimized)updateSlots(instance, nextVNode.children, optimized)pauseTracking()// props update may have triggered pre-flush watchers.// flush them before the render update.flushPreFlushCbs(undefined, instance.update)resetTracking()
} 

比较关键的就是上面代码中 32-39 行的 effect 函数,负责注册组件,这个函数也是 Vue 组件更新的入口函数


patch 函数

数据更新之后就会执行 patch 函数,下图就是 patch 函数执行的逻辑图:

在 patch 函数中,会针对不同的组件类型执行不同的函数,组件我们会执行 processComponent,HTML 标签我们会执行 processElement:

function path(n1, n2, container){const { type, shapeFlag } = n2switch (typ
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值