最近小组有个关于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 Watcher
,Watcher
暂时不作介绍。
接下来分析一下vm._render()
,此方法在src/core/instance/render.js
中定义,如下:
可以看到,这个方法返回的是虚拟dom,而虚拟dom的创建是调用vm.$createElement
,vm.$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挂载的整个过程主线分析完毕,最后上个图。