Vue源码阅读之VNode虚拟DOM(二

大家在使用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属性来存储该标签的附加信息,比如styleclass、事件等,通常我们把一个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工作流中暴露出去的钩子函数。

Published by

风君子

独自遨游何稽首 揭天掀地慰生平

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注