大家在使用Vue框架进行开发时,经常会听到虚拟DOM这个名词,但是其本质、工作原理知之甚少。今天我们就来剖析VNode,知道它是如何创建、工作的。
一、VNode概念
我们知道使用Vue开发项目过程中,往往都会拆分成各个组件,这样既隔离了独立的业务逻辑、代码块,又能在项目中有效地复用组件。但是大家有没有想过“组件的本质是什么?”,“组件在调用render函数后生成的是什么?”
在Jquery时代,项目往往搭配template模板引擎进行开发,将数据传入模板后,会返回一段HTML代码块后插入到页面中。而如今的Vue,组件的产出就是我们耳熟能详的“虚拟DOM‘。
一个组件最核心的东西是render函数,剩余的其他内容,如data、
compouted、
props
等都是为render
函数提供数据来源服务的。
为何组件要从直接产出html
变成产出虚拟DOM呢?其原因是虚拟DOM带来了分层设计,它对渲染过程的抽象,使得框架可以渲染到web
(浏览器) 以外的平台,以及能够实现SSR
等。
二、创建VNode
2.1 VNode数据结构
在src/core/vdom/vnode.js中我们看到VNode是基于面向对象进行设计的。
一个html
标签有它的名字、属性、事件、样式、子节点等诸多信息,这些内容都需要在VNode
中体现出来。
我们使用tag
属性来存储标签的名字,用data
属性来存储该标签的附加信息,比如style
、class
、事件等,通常我们把一个VNode
对象的data
属性称为VNodeData
。为了描述子节点,我们需要给VNode
对象添加children
属性,若有多个子节点,则可以把children
属性设计为一个数组。
如何设计VNode十分重要,整个框架的底层核心就是基于VNode数据结构进行开发的,甚至还涉及到数据更新渲染的性能问题。
2.2 _render函数
在Vue中是使用render函数创建VNode的,一个组件在挂载到$el后都会调用Vue构造函数上的_render私有方法,而_render方法返回值正是vnode。
src/core/instance/render.js中定义了_render方法:
关键看红框标注出的那行代码, VNode都是通过$createElement创建的。
createElement('h1', this.blogTitle)
src/core/vdom/create-element.js中的_createElement方法:
通过检测tag标签的值来判断创建VNode的类型,tag的值是html标签字符串,同时也可以是函数式组件或class类组件。红框标注的就是创建一个代表普通div元素的VNode,我们看到是基于VNode构造函数进行实例化的。
三、VNode渲染成DOM
知道了如何创建VNode,但是VNode是如何渲染成真实的DOM节点的呢?VNode渲染器的工作流有两种:mount、patch。下面进行详细讲解。
3.1 mount挂载
在组件初始化阶段,只有新的VNode,没有旧的VNode,直接将新的VNode挂载成全新的DOM,这个过程就叫做mount。
在组件生命周期中,如果想操作DOM节点都是在mounted之后才能获取到节点信息。因此使用Vue.$mount方法挂载组件时,就会将VNode挂载成真实的DOM。
下面我们就来看下mountComponent这个至关重要的方法:
updateComponent方法内部会调用_update方法,之前我们讲过_render方法的返回值是vnode,而_update方法正是接受新的vnode来渲染真实DOM的。
new Watch()是Vue内部实现了一套观察者模式Observer,会监听组件state状态的变动。这里是基于vm进行初始化,提供数据更新方法updateComponent、before回调函数。
在渲染完DOM后,会执行callHook(vm, ‘mounted’)方法给与开发者钩子函数去执行副作用操作。
_update方法接受最新的VNode进行数据更新,内部会判断是否存在preVnode,初次挂载是不会有preNode,因此会进入if判断中将VNode挂载到$el上面。
3.2 patch打补丁
渲染器patch工作流就很好理解了。如果旧的VNode
存在,则会使用新的VNode
与旧的VNode
进行对比,试图以最小的资源开销完成DOM
的更新,这个过程就叫patch。
下面我们就来模拟一下组件初始化完成后,用户修改界面上的数据是如何触发视图更新的。
比如有这么一个简单input输入框、div文本,双向绑定message=1,然后我们修改message=12,此时视图还没有完全更新。我们看看Vue内部是如何工作的。
首先会进入state数据代理proxy方法中,在Vue构造函数初始化一章中我们进行过讲解。
这里会进入set方法,保证用户输入的值能被更新到组件内部的state状态中。然后就会通过初始化时绑定的Watch方法,监听到了数据变动,进行数据更新。与mount不同之处在于,在进行更新操作前会优先执行before回调即”beforeUpdate”钩子函数。
之后就会调用_update方法通过比较preNode、vnode进行数据更新渲染页面。通过断点调试我们会发现,在执行完__patch__操作后页面上的视图才会更新完毕。
我在讲解VNode如何mount、patch的过程中,会夹杂的讲解组件内部生命周期、data相关的东西,目的就是加深大家的印象,知道我们日常开发使用生命周期钩子在底层究竟是如何工作的。__patch__函数的底层工作原理–diff算法、双端比较,有兴趣的同学可以查找、翻阅相关资料。
其实生命周期、data、props等等这些概念,是在基础原理VNode上进行再次设计的产物,state数据是为render函数在生成VNode过程中提供数据来源服务的,生命周期是在VNode渲染成真实DOM工作流中暴露出去的钩子函数。