かかってこいよ NakamuraEmi
  1. 1 かかってこいよ NakamuraEmi
  2. 2 Last Surprise Lyn
  3. 3 Libertus Chen-U
  4. 4 One Last You Jen Bird
  5. 5 Time Bomb Veela
  6. 6 Flower Of Life 发热巫女
  7. 7 Life Will Change Lyn
  8. 8 Hypocrite Nush
  9. 9 The Night We Stood Lyn
  10. 10 Quiet Storm Lyn
  11. 11 Warcry mpi
2018-01-19 21:30:35

Vue原理解析——模板编译与渲染

之前已经提到,Vue初始化时会在$mount方法前完成一些对象特性的注册,在$mount后触发组件DOM的渲染,也就是从模板到DOM的过程。这篇文章就来分析这个阶段。


render函数的获取

$mount后的第一步就是获取template,然后进入compileToFunctions函数。该函数的作用就是将我们定义的template编译成render函数。源码如下:

const key = options && options.delimiters
            ? String(options.delimiters) + template
            : template
if (cache[key]) {
    return cache[key]
}
const res = {}
const compiled = compile(template, options) // compile 后面会详细讲
res.render = makeFunction(compiled.render) //通过 new Function 的方式生成 render 函数并缓存
const l = compiled.staticRenderFns.length
res.staticRenderFns = new Array(l)
for (let i = 0; i < l; i++) {
    res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i])
}
......
}
return (cache[key] = res)

首先读缓存,没有缓存则调用compile方法拿到render function的字符串形式。compile函数源码如下:

export function compile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options) //1. parse
  optimize(ast, options)  //2.optimize
  const code = generate(ast, options) //3.generate
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

可以看到这个函数由三个步骤组成:

  1. parse
  2. optimize
  3. generate

parse部分使用HTML Parser将模板字符串解析为AST(抽象语法树)。

optimize部分主要用来标记静态节点,为后面patch部分的对比新旧VNode树形结构做优化,被标记为static的节点在后面的diff算法中会被忽略。

generate部分就是根据第一步得到的AST结构拼接生成的render函数的字符串了。

最后通过new Function得到真正的render函数,render函数被用来生成VNode树。


模板数据的观察者生成

在得到render函数之后就需要将其中的数据响应化了。$mount方法的最后调用了_mount函数:

// 触发 beforeMount 生命周期钩子
callHook(vm, 'beforeMount')
// 重点:新建一个 Watcher 并赋值给 vm._watcher
vm._watcher = new Watcher(vm, function updateComponent () {
  //_update方法里会去调用 __patch__ 方法
  //vm._render()会根据数据生成一个新的 vdom, vm.update() 则会对比新的 vdom 和当前 vdom,并把差异的部分渲染到真正的 DOM 树上。
  vm._update(vm._render(), hydrating)
}, noop)
hydrating = false
// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
  vm._isMounted = true
  callHook(vm, 'mounted')
}
return vm

这里出现了之前谈到过的观察者,其本质是一样的,当模板数据变化时触发_update函数去执行render函数输出一个新的VNode树,然后通过patch方法比较新旧VNode树。


Patch

该方法就是一个diff算法,它只会在DOM的同层级进行比较。首先看patch的第一部分:

if (sameVnode(oldVnode, vnode)) {
    patchVnode(oldVnode, vnode)
} else {
        const oEl = oldVnode.el
        let parentEle = api.parentNode(oEl)
        createEle(vnode)
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl))
            api.removeChild(parentEle, oldVnode.el)
            oldVnode = null
        }
    }

先通过sameVNode函数分析两个VNode是否值得比较,该函数认为只有当VNode的key和选择器相同时才会去比较它们。如果值得比较则进入patchVNode函数中,不值得比较时会创建新节点直接将老节点替换掉。

当节点值得比较时,则调用patchVnode函数:

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

节点的变化有5种情况:

  1. 引用一致,则认为没有变化。
  2. 两个节点为文本节点,如果不一致则修改。
  3. 只有新的节点有子节点,则会在老的dom节点上添加子节点。
  4. 只有老的节点有子节点,则直接删除老节点。
  5. 两个节点都有子节点而且不一样会调用updateChildren方法,这是diff算法的核心。源码如下:
updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
            if (oldStartVnode == null) {   //对于vnode.key的比较,会把oldVnode = null
                oldStartVnode = oldCh[++oldStartIdx] 
            }else if (oldEndVnode == null) {
                oldEndVnode = oldCh[--oldEndIdx]
            }else if (newStartVnode == null) {
                newStartVnode = newCh[++newStartIdx]
            }else if (newEndVnode == null) {
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newStartVnode)) {
                patchVnode(oldStartVnode, newStartVnode)
                oldStartVnode = oldCh[++oldStartIdx]
                newStartVnode = newCh[++newStartIdx]
            }else if (sameVnode(oldEndVnode, newEndVnode)) {
                patchVnode(oldEndVnode, newEndVnode)
                oldEndVnode = oldCh[--oldEndIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldStartVnode, newEndVnode)) {
                patchVnode(oldStartVnode, newEndVnode)
                api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
                oldStartVnode = oldCh[++oldStartIdx]
                newEndVnode = newCh[--newEndIdx]
            }else if (sameVnode(oldEndVnode, newStartVnode)) {
                patchVnode(oldEndVnode, newStartVnode)
                api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
                oldEndVnode = oldCh[--oldEndIdx]
                newStartVnode = newCh[++newStartIdx]
            }else {
               // 使用key时的比较
                if (oldKeyToIdx === undefined) {
                    oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
                }
                idxInOld = oldKeyToIdx[newStartVnode.key]
                if (!idxInOld) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    newStartVnode = newCh[++newStartIdx]
                }
                else {
                    elmToMove = oldCh[idxInOld]
                    if (elmToMove.sel !== newStartVnode.sel) {
                        api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                    }else {
                        patchVnode(elmToMove, newStartVnode)
                        oldCh[idxInOld] = null
                        api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                    }
                    newStartVnode = newCh[++newStartIdx]
                }
            }
        }
        if (oldStartIdx > oldEndIdx) {
            before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
            addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
        }else if (newStartIdx > newEndIdx) {
            removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
        }
}

该函数的思路为遍历新旧子节点数组,维护四个变量oldStartIdx,oldEndIdx,newStartIdx,newEndIdx,比较步骤如下:

  1. 先对比新旧Start,若值得比较则递归调用patchVnode
  2. 再对比新旧End,若值得比较则递归调用patchVnode
  3. 对比旧Start和新End,若值得比较则说明旧Start向右移动了,那么先patchVnode再用insertBefore调整位置。
  4. 对比旧End和新Start,若值得比较则说明旧End向左移动了,那么先patchVnode再用insertBefore调整位置。
  5. 如果以上都不满足,则利用vnode的key值生成一个旧节点数组的索引表,如果新节点的key不存在于这个表中则说明是新节点,则添加,如果存在则patchVnode。
  6. 当旧Start大于旧End或新Start大于新End时停止遍历。

小结

Vue模板编译与渲染最重要的函数就是compile和patch了,前者负责将模板转化为AST并优化,然后转化为可生成VNode树的render函数,后者在当利用观察者监听数据变化生成新的VNode时,与旧 VNode进行diff更新DOM树。

-- EOF --

添加在分类「 前端开发 」下,并被添加 「Vue」 标签。