Skip to content

Latest commit

 

History

History
253 lines (213 loc) · 7.58 KB

v-once.md

File metadata and controls

253 lines (213 loc) · 7.58 KB

本篇文章,我们要讲的是v-once的实现,添加了该指令的元素及其子内容,将只会渲染一次。

v-once的处理还是比较复杂的,因为它涉及到和v-ifv-for等指令在一起使用时的特殊情况。Vue需要保证在多个指令混合使用时,依然可以正常运行。

v-oncev-ifv-for类似,它影响的是最终生成的render函数。parser的过程中processOnce函数用来获取v-once标识并设置el.once

function processOnce (el) {
  const once = getAndRemoveAttr(el, 'v-once')
  if (once != null) {
    el.once = true
  }
}

因为v-once的元素在第一次渲染之后,会被当做静态内容来处理,所以它的处理和我们之前讲过的静态内容有很多相似的地方。其中主要一点是标记是否在for循环包裹内。

function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    if (node.static || node.once) {
      node.staticInFor = isInFor
    }
    ...
  }
}

在生成render函数时,对el.once的处理放在第二位:

function genElement (el: ASTElement): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el)
  } else if (el.for && !el.forProcessed) {
    ...
  }
}

我们一起来看genOnce

function genOnce (el: ASTElement): string {
  el.onceProcessed = true
  if (el.if && !el.ifProcessed) {
    return genIf(el)
  } else if (el.staticInFor) {
    let key = ''
    let parent = el.parent
    while (parent) {
      if (parent.for) {
        key = parent.key
        break
      }
      parent = parent.parent
    }
    if (!key) {
      process.env.NODE_ENV !== 'production' && warn(
        `v-once can only be used inside v-for that is keyed. `
      )
      return genElement(el)
    }
    return `_o(${genElement(el)},${onceCount++}${key ? `,${key}` : ``})`
  } else {
    return genStatic(el)
  }
}

1、如果和v-if同时使用,则调用 genIf来进行处理。在v-if的讲解中,我们讲过它的处理流程,最终会再次调用genOnce方法来处理。这也说明,v-if是优于v-once来进行处理的。

2、如果父辈元素中使用了v-forel.staticInFor就会返回true。首先会获取到添加v-for指令元素的ast,然后获取它的key值,如果没有key则抛出异常,并返回genElement(el),这种情况其实就相当于v-once失效。

例:

<div id="app">
  <div v-for="item in list">
    <div v-once>
      <p>{{msg}}{{item}}</p>
    </div>
  </div>
</div>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    data: {
      msg: "message",
      list: [1, 2, 3]
    }
  })
</script>

以上示例可以正常运行,但会抛出v-once can only be used inside v-for that is keyed.的错误。

生成的render函数如下:

"with(this){return _c('div',{attrs:{"id":"app"}},_l((list),function(item){return _c('div',[_c('div',[_c('p',[_v(_s(msg)+_s(item))])])])}))}"

此时我们修改list的值,我们会发现v-once包裹的内容,会跟着改变。从上面的render函数我们也可以看出,v-once没有失效。

如果有key,则返回_o函数。我们给上面有v-for指令的div添加一个key,它的render函数就会变成如下,自增的onceCount是因为一个v-for中,可能会包含多个v-once,它用于给vnode生成唯一的key

"with(this){return _c('div',{attrs:{"id":"app"}},_l((list),function(item){return _c('div',{key:"a"},[_o(_c('div',[_c('p',[_v(_s(msg)+_s(item))])]),0,"a")])}))}"

_o其实就是markOnce方法:

export function markOnce (
  tree: VNode | Array<VNode>,
  index: number,
  key: string
) {
  markStatic(tree, `__once__${index}${key ? `_${key}` : ``}`, true)
  return tree
}

function markStatic (
  tree: VNode | Array<VNode>,
  key: string,
  isOnce: boolean
) {
  if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
      if (tree[i] && typeof tree[i] !== 'string') {
        markStaticNode(tree[i], `${key}_${i}`, isOnce)
      }
    }
  } else {
    markStaticNode(tree, key, isOnce)
  }
}

function markStaticNode (node, key, isOnce) {
  node.isStatic = true
  node.key = key
  node.isOnce = isOnce
}

tree其实就是当前元素的vnodemarkOnce里面的操作,其实就是给vnode添加了key,同时设置vnode.isStatic = truevnode.once = true

那么这种情况,在list改变时,是如何保证不会再次渲染呢?

我们之前讲过,list改变之后,会触发render函数重新执行,然后新旧vnode树进行diff操作,如果两个元素可以复用,就会调用patchVnode方法:

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  if (oldVnode === vnode) {
    return
  }
  if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))) {
    vnode.elm = oldVnode.elm
    vnode.componentInstance = oldVnode.componentInstance
    return
  }
  ...
}

我们的v-once元素,在这里就会直接复用之前的elm,并直接返回。

3、如果以上两种情况都不符合,就当做静态内容来处理。

function genStatic (el: ASTElement): string {
  el.staticProcessed = true
  staticRenderFns.push(`with(this){return ${genElement(el)}}`)
  return `_m(${staticRenderFns.length - 1}${el.staticInFor ? ',true' : ''})`
}

这里我们会把vnode的生成函数,直接添加到staticRenderFns数组中,然后在render函数中,通过_m接收一个索引值来引用。

<div id="app">
  <div v-once>
    <p>{{msg}}</p>
  </div>
</div>
<script type="text/javascript">
  var vm = new Vue({
    el: '#app',
    data: {
      msg: "message"
    }
  })
</script>

上面例子最终生成的render函数如下:

render: "with(this){return _c('div',{attrs:{"id":"app1"}},[_m(0)])}"
staticRenderFns: ["with(this){return _c('div',[_c('p',[_v(_s(msg))])])}"]

_m对应的是renderStatic方法:

export function renderStatic (
  index: number,
  isInFor?: boolean
): VNode | Array<VNode> {
  let tree = this._staticTrees[index]
  if (tree && !isInFor) {
    return Array.isArray(tree)
      ? cloneVNodes(tree)
      : cloneVNode(tree)
  }
  // otherwise, render a fresh tree.
  tree = this._staticTrees[index] =
    this.$options.staticRenderFns[index].call(this._renderProxy)
  markStatic(tree, `__static__${index}`, false)
  return tree
}

当模板第一次渲染时,会把staticRenderFns中对应函数生成的vnode放到_staticTrees中。markStatic方法会给它添加key,同时设置vnode.isStatic = truevnode.once = false

当数据变化再次渲染时,会直接从_staticTrees中获取,然后通过cloneVNode方法来复制一份。

export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isCloned = true
  return cloned
}

cloneVNode其实就是会复制之前的所有值,来重新创建一个vnode,同时给它添加vnode.isCloned = true。同第二种情况下一样,在patchVNode的过程中,直接复用之前的elm,并直接返回。