從源碼解惑,爲何v-if和v-for不該該一塊兒用?

問題描述

在開發中,咱們可能會寫出以下代碼javascript

<!-- html模版 -->
<div id="app">
  <ul>
    <li v-for="item in list" v-if="item.age<30">
      <span>{{item.name}}</span>
      <span>{{item.age}}</span>
    </li>
  </ul>
</div>
複製代碼
// 列表數據
list: [
  {
    name: 'jack',
    age: 23
  },
  {
    name: 'john',
    age: 33
  },
  {
    name: 'petty',
    age: 20
  },
]
複製代碼

這個操做看起來很簡單,就是過濾要展現的列表,可是官方是不推薦這麼寫的,官方連接。 官方給出了兩點緣由:html

  1. 當 Vue 處理指令時,v-for 比 v-if 具備更高的優先級
  2. 哪怕咱們只渲染出一小部分用戶的元素,也得在每次重渲染的時候遍歷整個列表,不論活躍用戶是否發生了變化。

懶得看原文的能夠看下面的截圖:vue

pic1
pic2

問題分析

問題1:當 Vue 處理指令時,v-for 比 v-if 具備更高的優先級

經過上文的描述,大概是懂了,嗯。。。可是看完仍是不知因此然。
好比官網說,v-for比v-if優先級更高,爲何呢?你說優先就優先?🤔
咱們能夠作個簡單的小實驗,就是打印一下render函數,看一下vue對這兩個指令是如何解析的。java

// 打印出來的render函數
(function anonymous() {
    with (this) {
        return _c('div', {
            attrs: {
                "id": "app"
            }
        }, [_c('ul', _l((list), function(item) {
            return (item.age < 30) ? _c('li', [_c('span', [_v(_s(item.name))]), _v(" "), _c('span', [_v(_s(item.age))])]) : _e()
        }), 0)])
    }
})
複製代碼

直接看這個代碼可能不知道各個函數名是什麼意思,咱們打開源碼,能夠看到在renderMixin的時候會把vue的原型傳入下面的方法node

// renderMixin方法執行時註冊渲染快捷方法,所有掛載在vue原型上
// install runtime convenience helpers
installRenderHelpers(Vue.prototype)

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

// _c方法在render.js中定義,表示createElement
// bind the createElement fn to this instance
// so that we get proper render context inside it.
// args order: tag, data, children, normalizationType, alwaysNormalize
// internal version is used by render functions compiled from templates
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
複製代碼

經過上述的函數映射關係,咱們能夠知道,vue經過_l(renderList)函數遍歷list,在函數內部再經過三目語句處理v-if指令,若是條件爲true,則建立li及子節點,不然執行_e(createEmptyVNode)建立一個空節點,其實是一個沒有文本的註釋節點app

// createEmptyVNode建立一個默認爲空文本的註釋節點
export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}
複製代碼

咱們在控制檯中能夠看到這個空的註釋節點,經過對比list數據,能夠知道,這個註釋節點就是那個age>30itemdom

pic3

經過這個小實驗咱們已經可以知道v-for確實比v-if的優先級更高了,可是你可能想問了,爲何你是這樣的render函數?😂ide

那麼咱們再進一步的去探索生成render函數的函數
咱們最終在compiler模版編譯器找到了答案函數

export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
  // 咱們的render函數就是在這裏生成的,裏面的code經過下面的genElement方法生成
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.parent) {
    el.pre = el.pre || el.parent.pre
  }

  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
    // 這裏就是問題的核心,先處理了v-for
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
    // 而後再處理v-if
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      let data
      if (!el.plain || (el.pre && state.maybeComponent(el))) {
        data = genData(el, state)
      }

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
複製代碼

講到這裏,官方說的第一個問題就分析完了,那麼說的第二點又是什麼意思呢?oop

問題2: 哪怕咱們只渲染出一小部分用戶的元素,也得在每次重渲染的時候遍歷整個列表,不論活躍用戶是否發生了變化。

這裏爲何說每次從新渲染的時候都要遍歷整個列表?實際上是這樣的,render函數執行之後會生成vnode,就是虛擬dom。每當數據發生變化時,會觸發watcher執行update方法,就會從新執行render方法生成新的vnode,因此就須要從新遍歷一遍數據

// 每一個組件初始化掛載時(mountComponent)會定義一個渲染watcher
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */
)
  // 每當組件數據變化時,就會執行這個方法
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }
複製代碼

還有最後一句話不知道你注意到了沒有 不論活躍用戶是否發生了變化。你可能會問了,難道個人list數據沒有發生變化也要從新遍歷?
是的,在vue2中,爲了優化性能,將watcher的粒度放大,變爲一個組件一個watcher(用戶自定義的watcher除外),這樣,數據變化就只能通知到組件這一級別,至於組件裏面到底哪一個數據發生了變化,應該更新哪一個節點,須要依靠新老數據生成的vnode虛擬節點進行diff對比才能知道。

結論

講到這裏,你們應該已經清楚了那篇文檔的良苦用心了吧😂。咱們在實際的開發中,應該儘可能避免這種寫法。若是要過濾數據,可使用計算屬性進行過濾,而後再丟給vue進行渲染,儘量的提升性能。

相關文章
相關標籤/搜索