您现在的位置是:网站首页> 编程资料编程资料
Vue3 如何通过虚拟DOM更新页面详解_vue.js_
                    
                
                2023-05-24
                202人已围观
            
简介 Vue3 如何通过虚拟DOM更新页面详解_vue.js_
引言
上一讲我们主要介绍了 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:
hello world
上面的 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() { // 处理属性和 class if (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 执行我们写的 ,使用 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 } = instance if (next) { next.el = vnode.el updateComponentPreRender(instance, next, optimized) } else { next = vnode } const nextTree = renderComponentRoot(instance) 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 ) } } // 注册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 = instance const prevProps = instance.vnode.props instance.vnode = nextVNode instance.next = null updateProps(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 } = n2 switch (type) { case Text: processText(n1, n2, container) break // 还有注释,fragment之类的可以处理,这里忽略 default: // 通过shapeFlag判断类型 if (shapeFlag & ShapeFlags.ELEMENT) { processElement(n1, n2, container, anchor) } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { processComponent(n1, n2, container) } } } function processComponent(n1, n2, container) { // 老规矩,没有n1就是mount if (!n1) { // 初始化 component mountComponent(n2, container) } else { updateComponent(n1, n2, container) } } 由于更新之后不是首次渲染了,patch 函数内部会执行 updateComponent,看下面的 updateComponent 函数内部,shouldUpdateComponent 会判断组件是否需要更新,实际执行的是 instance.update:
const instance = (n2.component = n1.component)! if (shouldUpdateComponent(n1, n2, optimized)) { // 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. instance.update() } else { // no update needed. just copy over properties n2.component = n1.component n2.el = n1.el instance.vnode = n2 } 组件的子元素是由 HTML 标签和组件构成,组件内部的递归处理最终也是对 HTML 标签的处理,所以,最后组件的更新都会进入到 processElement 内部的 patchElement 函数中
patchElement 函数
在函数 patchElement 中我们主要就做两件事,更新节点自己的属性和更新子元素
节点自身属性的更新
先看自身属性的更新,这里就能体现出 Vue 3 中性能优化的思想,通过 patchFlag 可以做到按需更新:
如果标记了 FULL_PROPS,就直接调用 patchProps;如果标记了 CLASS,说明节点只有 class 属性是动态的,其他的 style 等属性都不需要进行判断和 DOM 操作
这样就极大的优化了属性操作的性能
内部执行 hostPatchProp 进行实际的 DOM 操作,你还记得上一讲中 hostPatchProp 是从 nodeOps 中定义的吗,其他动态属性 STYLE、TEXT 等等也都是一样的逻辑;Vue 3 的虚拟 DOM 真正做到了按需更新,这也是相比于 React 的一个优势
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, isSVG: boolean, slotScopeIds: string[] | null, optimized: boolean ) => { const el = (n2.el = n1.el!) let { patchFlag, dynamicChildren, dirs } = n2 patchFlag |= n1.patchFlag & PatchFlags.FULL_PROPS const oldProps = n1.props || EMPTY_OBJ const newProps = n2.props || EMPTY_OBJ // full diff patchChildren( n1, n2, el, null, parentComponent, parentSuspense, areChildrenSVG, slotScopeIds, false ) if (patchFlag > 0) { if (patchFlag & PatchFlags.FULL_PROPS) { patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG ) } else { // class是动态的 if (patchFlag & PatchFlags.CLASS) { if (oldProps.class !== newProps.class) { hostPatchProp(el, 'class', null, newProps.class, isSVG) } } // style样式是动态的 if (patchFlag & PatchFlags.STYLE) { hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG) } // 属性需要diff if (patchFlag & PatchFlags.PROPS) { // const propsToUpdate = n2.dynamicProps! for (let i = 0; i < propsToUpdate.length; i++) { const key = propsToUpdate[i] const prev = oldProps[key] const next = newProps[key] // #1471 force patch value if (next !== prev || key === 'value') { hostPatchProp( el, key, prev, next, isSVG, n1.children as VNode[], parentComponent, parentSuspense, unmountChildren ) } } } } //文本是动态的 if (patchFlag & PatchFlags.TEXT) { if (n1.children !== n2.children) { hostSetElementText(el, n2.children as string) } } } } 子元素的更新
而子元素的更新是 patchChildren 函数负责的,这个函数也是虚拟 DOM 中难度最高的一个函数,搞懂它还需要我们下一讲中介绍的算法知识,今天我们就先理解它主要的实现思路
首先我们把子元素分成了文本、数组和空三个状态,新老子元素分别是这三种状态的一个,构成了不同的执行逻辑;这样 patchChildren 内部大致有五种情况需要处理:
- 如果新的子元素是空, 老的子元素不为空,直接卸载 unmount 即可
 - 如果新的子元素不为空,老的子元素是空,直接创建加载即可
 - 如果新的子元素是文本,老的子元素如果是数组就需要全部 unmount,是文本的话就需要执行 hostSetElementText
 - 如果新的子元素是数组,比如是使用 v-for 渲染出来的列表,老的子元素如果是空或者文本,直接 unmout 后,渲染新的数组即可
 
最复杂的情况就是新的子元素和老的子元素都是数组
最朴实无华的思路就是把老的子元素全部 unmount,新的子元素全部 mount,这样虽然可以实现功能,但是没法复用已经存在的 DOM 元素,比如我们只是在数组中间新增了一个数据,全部 DOM 都销毁就有点太可惜了
所以,我们需要判断出可以复用的 DOM 元素,如果一个虚拟 DOM 没有改动或者属性变了,不需要完全销毁重建,而是更新一下属性,最大化减少 DOM 的操作,这个任务就会交给 patchKeyedChildren 函数去完成
patchKeyedChildren 函数,做的事情就是尽可能高效地把老的子元素更新成新的子元素,如何高效复用老的子元素中的 DOM 元素是 patchKeyedChildren 函数的难点:
const patchChildren: PatchChildrenFn = ( n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized = false ) => { const c1 = n1 && n1.children const prevShapeFlag = n1 ? n1.shapeFlag : 0 const c2 = n2.children const { patchFlag, shapeFlag } = n2 // fast path if (patchFlag > 0) { if (patchFlag & PatchFlags.KEYED_FRAGMENT) { // this could be either full
                
                
                
            相关内容
- JavaScript懒加载与预加载原理与实现详解_javascript技巧_
 - vue实现弹窗拖拽效果_vue.js_
 - useEffect理解React、Vue设计理念的不同_vue.js_
 - js如何删除对象/数组中null、undefined、空对象及空数组实例代码_javascript技巧_
 - 解决vue vite启动项目报错ERROR: Unexpected “\x88“ in JSON 的问题_vue.js_
 - Vue中el-menu-item实现路由跳转的完整步骤_vue.js_
 - Vue extend学习示例讲解_vue.js_
 - TS 类型兼容教程示例详解_JavaScript_
 - Vue实现简单弹窗效果_vue.js_
 - Vue中.env、.env.development及.env.production文件说明_vue.js_
 
点击排行
本栏推荐
                                
                                                        
                                
                                                        
                                
                                                        
    