vue3源码阅读笔记(三)---虚拟dom+diff算法

1,260 阅读4分钟

这篇文章应该是vue3源码阅读系列的最后一篇了,今天学习的是关于vue3的虚拟dom以及它的diff算法。虚拟dom也是vue里比较重要的一个部分,其实三大框架里,react和vue采用的是虚拟dom,angular采用的还是真实的dom。虚拟dom也是由react先提出的。今天就是主要来学习一下vue3里虚拟dom的源码以及它的diff算法

往期:
VUE3源码阅读笔记----初始化流程
VUE3源码阅读笔记(二)---响应式

什么是虚拟dom&&为什么?

我们知道,在浏览器上操作dom是比较消耗性能的,虚拟dom就是用js去生成和控制dom,这样能提高我们的页面性能以及让我们做dom操作时更加的方便。他的本质呢就是一个js对象,来描述我们的dom结构,去生成真实的dom。
除了性能,还有兼容性的问题,在操作dom的时候难免会有兼容性问题,但是如果使用虚拟dom的时候,我可以在js中先处理这些兼容性问题,再进行操作。
当然,虚拟dom还可以跨平台,因为dom是由js维护的,可以由各个平台去做适配。比如小程序。

源码阅读

虚拟dom部分

首先,关于虚拟dom的源码从哪看起呢?当然是从mount看起,在我们挂载vue的时候,肯定会把我们的模版进行渲染,这里就用到了虚拟dom

// 初始化vnode
const vnode = createVNode(
  rootComponent as ConcreteComponent,
  rootProps
)

可以看到mount里第一句就是创建vnode,这个vnode就是虚拟dom。接着我们去看这个vnode是怎么生成的。我们找到createVNode这个函数。

const vnode: VNode = {
    __v_isVNode: true,
    [ReactiveFlags.SKIP]: true,
    type,
    props,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
    scopeId: currentScopeId,
    children: null,
    component: null,
    suspense: null,
    ssContent: null,
    ssFallback: null,
    dirs: null,
    transition: null,
    el: null,
    anchor: null,
    target: null,
    targetAnchor: null,
    staticCount: 0,
    shapeFlag,
    patchFlag,
    dynamicProps,
    dynamicChildren: null,
    appContext: null
  }

我们可以看到这个函数的返回值就是个vnode对象,这个就是虚拟dom,里面包含里一些dom结构的信息。而mount就是先通过这个函数,根据我们不同的配置去生成这么一个vnode对象。之后呢这个vnode会通过patch这个函数来处理。这个之前在看初始化流程的相关代码时已经看过,当时看到这个patch方法就没看下去了,就知道它是个处理vnode的函数。我们看看patch这个方法干了啥。从源码里发现,patch会对你传进来的vnode进行判断,在初始化的时候会走一次renderComponentRoot这个方法。

...
// 执行组件的render函数,此处的result就是vnode
result = normalizeVNode(
  render!.call(
    proxyToUse,
    proxyToUse!,
    renderCache,
    props,
    setupState,
    data,
    ctx
  )
)
...
return result

这里源码比较长,简单来说就是这个方法会执行根组件实例的render函数,获取整个app对应的vdom。

const subTree = (instance.subTree = renderComponentRoot(instance))
patch(
  null,
  subTree,
  container,
  anchor,
  instance,
  parentSuspense,
  isSVG
)

后来我们发现,生成了vdom之后,又通过patch去遍历,这样把children也去生成了。之后的流程就比较简单,通过createElement去生成真实dom就行。

diff

当数据变化时,会生成一个新的vnode,所以它会再执行一下patch函数,把新旧两个vnode都放进去

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
 )

还是根据流程走下去,我们找到了patchElemnt这个函数,这里就看最简单的文本变化

// text
// This flag is matched when the element has only dynamic text children.
if (patchFlag & PatchFlags.TEXT) {
  if (n1.children !== n2.children) {
    hostSetElementText(el, n2.children as string)
  }
}

当文本发生变化时,通过调用hostSetElementText去生成新的dom。在整个程序非常庞大时,其实内部也是一样的,当数据变化时,先判断出变化了什么,然后通过不同的函数去生成dom。

Diff算法只是为了虚拟DOM比较替换效率更高,通过Diff算法得到diff算法结果数据表(需要进行哪些操作记录表)。原本要操作的DOM在vue这边还是要操作的,只不过用到了js的DOM fragment来操作dom(统一计算出所有变化后统一更新一次DOM)进行浏览器DOM一次性更新。其实DOM fragment我们不用平时发开也能用,但是这样程序员写业务代码就用把DOM操作放到fragment里,这就是框架的价值,程序员才能专注于写业务代码。