写在前面
本篇是从零实现vue2系列第五篇,将 YourVue 实例的 render 函数转换成真实 dom 和更新算法。
正文
上篇文章我们把 render 函数挂在了 options 属性上,执行 render() 就可以得到 template 对应的虚拟 dom 树了。
1export default class YourVue{
2 update(){
3 if(this.$options.template){
4 if(this._isMounted){
5 const vnode = this.$options.render()
6 patch(this.vnode, vnode)
7 this.vnode = vnode
8 }else{
9 this.vnode = this.$options.render()
10 let el = this.$options.el
11 this.el = el && query(el)
12 patch(this.vnode, null, this.el)
13 this._isMounted = true
14 }
15 }
16 }
17}
Vue 将虚拟 dom 转换成真实 dom 有两种阶段,一个是 mount,一个是 update。都是通过 patch 函数来操作 dom 的。
1export function patch (oldVnode, vnode, el) {
2 if(isUndef(vnode)){
3 createElm(oldVnode, el)
4 return
5 }
6 if (oldVnode === vnode) {
7 return
8 }
9 if(sameVnode(oldVnode, vnode)){
10 patchVnode(oldVnode, vnode)
11 }else{
12 const parentElm = oldVnode.elm.parentNode;
13 createElm(vnode,parentElm,oldVnode.elm)
14 removeVnodes(parentElm,[oldVnode],0,0)
15 }
16}
如果是 mount 阶段,会执行 createElm,如果是 update 阶段,先判断两个根节点的 vnode 是否相同,如果不同则直接创建新的 dom,如果相同则执行 patchVnode。
先看 mount 阶段的 createElm,就是createElement和setAttribute,updateListeners就是第一篇文章中事件绑定到 dom 的方法。最后将生成的 dom 插入到指定的位置。
1function createElm (vnode, parentElm, afterElm = undefined) {
2 let element
3 if(!vnode.tag && vnode.text){
4 element = document.createTextNode(vnode.text);
5 }else{
6 element = document.createElement(vnode.tag)
7 if(vnode.props.attrs){
8 const attrs = vnode.props.attrs
9 for(let key in attrs){
10 element.setAttribute(key, attrs[key])
11 }
12 }
13 if(vnode.props.on){
14 const on = vnode.props.on
15 const oldOn = {}
16 updateListeners(element, on, oldOn, vnode.context)
17 }
18 for(let child of vnode.children){
19 if(child instanceof VNode){
20 createElm(child, element)
21 }else if(Array.isArray(child)){
22 for (let i = 0; i
23 createElm(child[i], element)
24 }
25 }
26 }
27 }
28 vnode.elm = element;
29 if(isDef(afterElm)){
30 insertBefore(parentElm, element, afterElm)
31 }else if(parentElm){
32 parentElm.appendChild(element)
33 }
34 return element;
35}
update 阶段,就是对比两棵虚拟 dom 树的阶段。Vue 对比两棵虚拟 dom 树时是按层对比的,如果根节点相同,判断 children 是否相同:
如果新树有 child 旧树没有,则新建 child
如果新树没有 child,旧树没有,则删掉 child
如果都有 children,就到了虚拟 dom 中非常有名的 diff 算法。
1function patchVnode(oldVnode, vnode){
2 if (oldVnode === vnode) {
3 return
4 }
5 const ch = vnode.children
6 const oldCh = oldVnode.children
7 const elm = vnode.elm = oldVnode.elm
8 if(isUndef(vnode.text)){
9 if(isDef(ch) && isDef(oldCh)){
10 updateChildren(elm,oldCh,ch)
11 }else if(isDef(ch)){
12 if (isDef(oldVnode.text)) setTextContent(elm, '')
13 addVnodes(oldVnode, ch, 0, ch.length - 1)
14 }else if(isDef(oldCh)){
15 removeVnodes(elm, oldCh, 0, oldCh.length - 1)
16 }
17 }else{
18 setTextContent(elm, vnode.text);
19 }
20}
diff 算法步骤比较多,但是也都不复杂,核心思想就是使用四个指针分别指向新 children 和旧 children 数组的头和尾,尽量找到和新树相同的节点,通过移动进行元素复用,将旧树变换成新树的结构,减少新建 dom 节点的操作。
当两个头指针指向的节点相同时,头指针后移。当两个尾指针节点相同时,尾指针前移。
当头和头,尾和尾都不同时,先比较旧树的头和新树的尾,如果相同,就把旧树的头指针指向的节点移动到尾指针指向节点的后面。旧头指针后移,新尾指针前移。
当前面都不同,旧树的尾和新树的头相同时,把旧树的尾移动到旧树的头前面,旧尾指针前移,新头指针后移。
当头尾指针都不同的时候,vue 还会遍历旧树剩余节点的 key 与新树的头节点的 key 进行比较,也就是 v-for 时必须要写的 key 的值,如果有相同的 key,就将旧树的节点移到旧头前面。
如果都没有,就在旧树的头前面新建新树的头节点,新树头指针后移。
最后当旧树头尾指针相遇,新树头尾指针之间仍有元素节点时,新建这些节点。
当新树头尾指针相遇,旧树头尾指针之间还有元素时,删除这些节点。
这样就通过元素节点的移动和新建,将旧的 dom 结构转换成新的 dom 树结构啦!理解思路后,再看代码就清晰了。
1function updateChildren(parentElm, oldCh, newCh,){
2 let oldStartIdx = 0
3 let newStartIdx = 0
4 let oldEndIdx = oldCh.length - 1
5 let oldStartVnode = oldCh[0]
6 let oldEndVnode = oldCh[oldEndIdx]
7 let newEndIdx = newCh.length - 1
8 let newStartVnode = newCh[0]
9 let newEndVnode = newCh[newEndIdx]
10 let oldKeyToIdx, idxInOld, vnodeToMove, refElm
11
12 while (oldStartIdx
13 if (isUndef(oldStartVnode)) {
14 oldStartVnode = oldCh[++oldStartIdx]
15 } else if (isUndef(oldEndVnode)) {
16 oldEndVnode = oldCh[--oldEndIdx]
17 } else if (sameVnode(oldStartVnode, newStartVnode)) {
18 patchVnode(oldStartVnode, newStartVnode)
19 oldStartVnode = oldCh[++oldStartIdx]
20 newStartVnode = newCh[++newStartIdx]
21 } else if (sameVnode(oldEndVnode, newEndVnode)) {
22 patchVnode(oldEndVnode, newEndVnode)
23 oldEndVnode = oldCh[--oldEndIdx]
24 newEndVnode = newCh[--newEndIdx]
25 } else if (sameVnode(oldStartVnode, newEndVnode)) {
26 patchVnode(oldStartVnode, newEndVnode)
27 insertBefore(parentElm, oldStartVnode.elm, oldEndVnode.elm.nextSibling)
28 oldStartVnode = oldCh[++oldStartIdx]
29 newEndVnode = newCh[--newEndIdx]
30 } else if (sameVnode(oldEndVnode, newStartVnode)) {
31 patchVnode(oldEndVnode, newStartVnode)
32 insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
33 oldEndVnode = oldCh[--oldEndIdx]
34 newStartVnode = newCh[++newStartIdx]
35 } else {
36 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
37 idxInOld = isDef(newStartVnode.key)
38 ? oldKeyToIdx[newStartVnode.key]
39 : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
40 if (isUndef(idxInOld)) {
41 createElm(newStartVnode, parentElm, oldStartVnode.elm)
42 } else {
43 vnodeToMove = oldCh[idxInOld]
44 if (sameVnode(vnodeToMove, newStartVnode)) {
45 patchVnode(vnodeToMove, newStartVnode)
46 oldCh[idxInOld] = undefined
47 insertBefore(parentElm,vnodeToMove.elm, oldStartVnode.elm)
48 } else {
49 createElm(newStartVnode, parentElm, oldStartVnode.elm)
50 }
51 }
52 newStartVnode = newCh[++newStartIdx]
53 }
54 }
55 if (oldStartIdx > oldEndIdx) {
56 refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
57 addVnodes(parentElm, newCh, newStartIdx, newEndIdx, refElm)
58 } else if (newStartIdx > newEndIdx) {
59 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
60 }
61}
虚拟 dom 完成实现。综合本篇和上篇文章的代码:
https://github.com/buppt/YourVue/tree/master/oldSrc/4.vdom
领取专属 10元无门槛券
私享最新 技术干货