首次渲染
因为是首次渲染,所以不存在先前老的vnode,因此无需进行比较。vue直接调用 createElm
方法创建DOM元素。具体的创建步骤如下:
- 首先为vnode创建DOM元素。
- 如果vnode有子节点,逐个为其子节点创建DOM元素,并将子DOM元素插入到vnode的DOM元素上。
- 调用
setAttribute
为vnode的DOM元素添加属性。
- 将vnode的DOM元素插入到其父元素上。
createElm
方法的主要代码如下:
function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) { if (isDef(vnode.elm) && isDef(ownerArray)) { vnode = ownerArray[index] = cloneVNode(vnode) } const data = vnode.data const children = vnode.children const tag = vnode.tag // 普通元素 if (isDef(tag)) { // 1. 为vnode创建对应的DOM节点 vnode.elm = nodeOps.createElement(tag, vnode) // 设置css的作用域 setScope(vnode) // 2. 为vnode的子节点创建对应的DOM节点,并插入到vnode节点对应的DOM节点上 createChildren(vnode, children, insertedVnodeQueue) // 3. 更新DOM元素的属性,如class、style、event等 if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } // 4. 将vnode的DOM节点插入到父元素上(根节点的父元素是body) insert(parentElm, vnode.elm, refElm) } else if (isTrue(vnode.isComment)) { // 如果vnode是注释,那么创建注释DOM,并插入到父元素上 vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { // 如果是vnode是文本,那么创建文本DOM,并插入到父元素下 vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } // ...省略其他代码 }
我们都知道,data数据对象中保存着与DOM元素有关的属性,如id,class,style,event,ref等。vue有专门的模块负责给DOM元素添加、更新或删除这些属性。因此,我们在代码中可以看到,如果vnode节点存在data数据对象时,vue会调用 invokeCreateHooks
分别使用相应的处理模块来处理data中的属性。
重新渲染
如果不是首次渲染,而是由数据变化所触发的重新渲染,那么vue会最大限度地复用已创建的DOM元素。而复用的前提就是通过比较新老vnode,找出需要更新的内容,然后最小限度地进行替换。这也是vue设计vnode的核心用途。
function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 如果新老vnode没有发生变化,无需更新。 if (oldVnode === vnode) { return } const elm = vnode.elm = oldVnode.elm let i const data = vnode.data const oldCh = oldVnode.children const ch = vnode.children // 1. 更新DOM元素的属性 if (isDef(data) && isPatchable(vnode)) { // 重新执行vue实例的所有的指令和模块 for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) } // 2. 更新子元素 // 如果vnode不是文本节点,则更新子元素。 if (isUndef(vnode.text)) { // 如果新vnode和老vnode中都有子节点 if (isDef(oldCh) && isDef(ch)) { // 如果新老vnode的子vnode不同,则更新子元素 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else if (isDef(ch)) { // 如果新vnode有子节点,而老vnode无子节点,那么需要将dom元素的textContent设置为空 if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') // 为新vnode的子vnode创建DOM元素 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { // 如果老vnode有子节点,而新vnode无子节点。那么需要将老vnode中的子vnode移除 removeVnodes(oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { // 如果新老vnode均无子节点,新vnode无文本内容但老vnode有文本内容,需要将dom元素的textContent设置为空 nodeOps.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { //新老vnode都是文本节点,且两个vnode的text不相同,则更新DOM的textContent。 nodeOps.setTextContent(elm, vnode.text) } // ...省略其他代码 }
从源码中可以看到,当新老vnode完全相等的情况下,vue不会对该节点重新渲染,直接跳过了。
如果新vnode发生了变化,那么vue会遵循以下步骤更新DOM元素:
- 更新DOM元素的属性。这个在首次渲染那部分提到了一些。vue内实现了若干个属性处理模块,专门用于DOM元素属性的创建和更新。这些模块中基本都实现了
create
、update
这两个处理函数。create
负责DOM元素属性的创建,update
负责DOM元素属性的更新。cbs.update[i](oldVnode, vnode)
的意思就是逐个调用这些模块上的update
方法,以更新发生改变的DOM元素属性。
- 更新DOM元素的子元素。关于DOM子元素的更新分为几种情况
1.如果新老vnode的子节点都是文本节点且文本内容不同,处理方式更新DOM元素的textContent属性值。
2. 如果新老vnode的子节点都是非文本节点,需要调用 updateChildren
递归地去更新子节点。
3. 如果新vnode的子节点是非文本节点,而老vnode的子节点是文本节点,需要清除DOM元素的文本,并创建子vnode的DOM元素插入到其父节点的DOM元素上。
4.如果新vnode的子节点不存在,但老vnode的子节点存在,那么调用 removeVnode
删除老vnode的子节点对应的DOM元素。
5.如果老vnode的子节点是文本节点,而新vnode的子节点不存在,则清空老DOM元素的文本。
小结
大量的DOM操作会极损耗浏览器性能。vue在每次数据发生变化后,都会重新生成vnode节点。通过比较新老vnode节点,找出需要进行操作的最小DOM元素子集。根据变化点,进行DOM元素属性、DOM子节点的更新。这种设计方式大大减少了DOM操作的次数。