[Vue源码分析] Virtual DOM

版权声明:本文为博主原创文章,转载请注明出处。 https://blog.csdn.net/Fabulous1111/article/details/83281336

最近小组有个关于vue virtual dom的分享会,提前准备一下…

读前须知:

本文章涉及源码版本为Vue 2.5.2,文中涉及到源码部分,解释直接写在源码中(中文部分为本人添加),截图尽量放完整代码,但由于截图的大小限制,部分只能放关键截图,建议结合源码阅读此文章。
附上尤雨溪大佬的github vue仓库地址:https://github.com/vuejs/vue

为什么使用virtual dom

做一件事一般都先问问为什么,那么为什么使用virtual dom?真正的 DOM 元素是非常庞大的,因为浏览器的标准把 DOM 设计的很复杂。如果频繁地操作 DOM ,会产生一定的性能问题。
举个例子:创建一个header标签,并打印dom的描述信息:
在这里插入图片描述
可以看到,输出的信息虽然挺多看不懂,但可以看得出来这是很庞大的一段内容。
在这里插入图片描述
相比之下:Virtual DOM 用一个原生的 JS 对象去描述一个 DOM 节点,所以它比创建一个 DOM 的代价要小很多。

virtual dom 的数据结构

virtual dom的定义在src/core/vdom/vnode.js 中,是一个VNode 类,定义如图:
在这里插入图片描述
virtual dom映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。

virtual dom 的创建

VNode 的 create 是通过createElement 方法创建的,源码位于src/core/vdom/create-elemenet.js中
在这里插入图片描述
这个方式实际调用的是_createElement()方法,此方法同样位于src/core/vdom/create-elemenet.js,为了方便阅读,关键代码解释已经写在源码中,如图
在这里插入图片描述

VNode children的VNode化

VNode化的意思是什么?Virtual DOM 实际上是一个树状结构,每一个 VNode 可能会有若干个子节点,这些子节点应该也是 VNode 的类型,由于上边传入的children是any类型的,因此需要将children转为VNode类型。
这个过程用到两个方法:normalizeChildren(children)simpleNormalizeChildren(children)
这两个方法的定义在src/core/vdom/helpers/normalzie-children.js中:
在这里插入图片描述
当一个 childrean 包含组件的时候,由于 functional component 函数式组件返回的是一个数组而不是一个根节点,所以需要通过Array.prototype.concat方法把整个 children 转化为深度只有一层。

如果children是数组类型,normalizeChildren方法实际上调用的是normalizeArrayChildren方法,该方法在同文件下,源码中已经添加关键代码解释,如下:
在这里插入图片描述
至此,我们知道了Virtual DOM是通过createElement方法创建的,但是createElement方法是什么时候调用的呢?
接下来我们将从主线上分析模板和数据如何渲染成最终的DOM。

new Vue()发生了那些事

我们都知道,vue项目中的入口是src下的main.js,如图:
在这里插入图片描述
new Vue()发生了什么?接下来我们看一下Vue源码,源码位于src/core/instance/index.js
在这里插入图片描述
可以看到,Vue实际上是一个类,源码中判断了this instanceof Vue,用于限制此类只能通过new关键字初始化,这个类看起来很简单,只是调用了一个this._init()方法,这个方法做了什么?this_init()方法在src/core/instance/init.js中定义,主要是做了一堆初始化操作,关键源码已添加中文注释:
在这里插入图片描述
可以看到this_init()方法在做了一堆初始化操作后调用的是vm.$mount()方法,这个方法时怎么挂载实例的?接下来看一下这个方法的源码,此方法的定义位于src/platform/web/entry-runtime-with-compiler.js中,关键代码已经添加中文注释:
在这里插入图片描述
逗了一圈,发现这个方法只是对options做了一些规范化的操作,最后调用的还是最初缓存下来的Vue原型上的$mount方法,那么这个方法是什么时候定义的?查找一下,发现此方法位于:src/platform/web/runtime/index.js
在这里插入图片描述

以下为$mount中使用到的query方法的源码,源码位于src/platform/web/util/index.js中:

在这里插入图片描述
最后$mount方法调用的是mountComponent方法,此方法在src/core/instance/lifecycle.js中定义,关键代码注释已经添加到源码中:
在这里插入图片描述
从上边源码可以看出,mountComponent最核心的方法有三个:vm._render()vm._update()new WatcherWatcher暂时不作介绍。
接下来分析一下vm._render(),此方法在src/core/instance/render.js中定义,如下:
在这里插入图片描述
可以看到,这个方法返回的是虚拟dom,而虚拟dom的创建是调用vm.$createElementvm.$createElement是什么?
在这里插入图片描述
可以看到,它正是文章开头介绍到的createElement方法,createElement方法就是这时候调用的,兜了一大圈,终于解决了之前提到的createElement方法什么时候调用的问题。

vm._update()又是什么?
上边介绍mountComponent时提到过,vm.update()的作用是把创建好的虚拟dom渲染成真实的dom,vm._update()源码位于:src/core/instance/lifecycle.js,如图:
在这里插入图片描述
从源码中可以看到,_update方法在首次渲染和更新时都调用了vm.__patch__方法,只是传入参数不一样,可以想象得到vm.__patch__会有两套逻辑,在这里先分析首次渲染时vm.__patch__做了什么。

vm.__patch__源码位于src/platforms/web/runtime/index.js,如下:
在这里插入图片描述
patch应该就是要将虚拟dom转换为真实dom的函数,但是noop是什么?从上边的导入路径可以得知此方法地址位于src/shared/util.js,如下:
在这里插入图片描述
可见,noop是一个空函数,也就是当前如果不是浏览器环境的话,vm.__patch__将会是一个空函数。可以得知,在非浏览器渲染(服务端渲染)中,不需要把虚拟dom转换为真实dom。
接下来,重点看一下patch方法,源码位于src/platforms/web/runtime/patch.js中:
在这里插入图片描述
这个函数的定义很简单,就一句代码,接受的是一个Function类型的返回值,该值由createPatchFunction返回,createPatchFunction接受一个对象,这个对象包含nodeOps以及modules两个模块。

createPatchFunction源码位于src/core/vdom/patch.js中,这个方法很复杂,但总的来说就是定义了一系列的辅助方法,辅助方法那么多,不可能一开始就全部看一遍,应该先想想应该怎么看。把辅助方法都收缩起来,会发现这个函数最后会返回一个方法——patch,我们可以从这个方法开始阅读父方法的源码,从而跟踪用到了哪些辅助方法,做了些什么。
在这里插入图片描述

接着看一些patch函数的具体内容,解释请看源码中的中文注释:
在这里插入图片描述
接上图:
在这里插入图片描述

做完了上边的一波操作之后,调用createElm方法,这个方法位于同文件中,也就是辅助方法,解释请看源码中的中文注释:
在这里插入图片描述

createChildren方法位于同文件中,是一个遍历虚拟节点的操作:
在这里插入图片描述

最后会调用insert方法将dom插入父节点中。
在这里插入图片描述

insert方法中nodeOps.insertBefore以及nodeOps.appendChild实际调用的是原生的dom操作函数,源码位于:web/runtime/node-ops.js
在这里插入图片描述
在这里插入图片描述

至此,从new Vue()到真实dom挂载的整个过程主线分析完毕,最后上个图。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/Fabulous1111/article/details/83281336
今日推荐