Vue.js 2.0 Render 函數(shù)

2022-01-28 15:32 更新

Vue.js 2.0 Render 函數(shù)基礎(chǔ)

Vue 推薦使用在絕大多數(shù)情況下使用 template 來創(chuàng)建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力,這就是 render 函數(shù),它比 template 更接近編譯器。

讓我們先深入一個使用 render 函數(shù)的簡單例子,假設(shè)你想生成一個帶錨鏈接的標(biāo)題:

<h1>
  <a name="hello-world" href="#hello-world">
    Hello world!
  </a>
</h1>

在 HTML 層, 我們決定這樣定義組件接口:

<anchored-heading :level="1">Hello world!</anchored-heading>

當(dāng)我們開始寫一個通過 level prop 動態(tài)生成heading 標(biāo)簽的組件,你可很快能想到這樣實現(xiàn):

<script type="text/x-template" id="anchored-heading-template">
  <div>
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-if="level === 6">
      <slot></slot>
    </h6>
  </div>
</script>
Vue.component('anchored-heading', {
  template: '#anchored-heading-template',
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

template 在這種場景中就表現(xiàn)的有些冗余了。雖然我們重復(fù)使用 <slot></slot> 來接收每一個級別的標(biāo)題標(biāo)簽,在標(biāo)題標(biāo)簽中添加相同的錨點元素。但是些都會被包裹在一個無用的 div 中,因為組件必須有根節(jié)點。

雖然模板在大多數(shù)組件中都非常好用,但是在這里它就不是很簡潔的了。那么,我們來嘗試使用 render 函數(shù)重寫上面的例子:

Vue.component('anchored-heading', {
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // tag name 標(biāo)簽名稱
      this.$slots.default // 子組件中的陣列
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

簡單清晰很多!簡單來說,這樣代碼精簡很多,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道當(dāng)你不使用 slot 屬性向組件中傳遞內(nèi)容時,比如 anchored-heading 中的 Hello world!, 這些子元素被存儲在組件實例中的 $slots.default中。如果你還不了解,在深入 render 函數(shù)之前推薦閱讀 instance 屬性 API。

createElement 參數(shù)

第二件你需要熟悉的是如何在 createElement 函數(shù)中生成模板。這里是 createElement接受的參數(shù):

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個 HTML 標(biāo)簽,組件設(shè)置,或一個函數(shù)
  // 必須 Return 上述其中一個
  'div',
  // {Object}
  // 一個對應(yīng)屬性的數(shù)據(jù)對象
  // 您可以在 template 中使用.可選項.
  {
    // (下一章,將詳細說明相關(guān)細節(jié))
  },
  // {String | Array}
  // 子節(jié)點(VNodes). 可選項.
  [
    createElement('h1', 'hello world'),
    createElement(MyComponent, {
      props: {
        someProp: 'foo'
      }
    }),
    'bar'
  ]
)

完整數(shù)據(jù)對象

有一件事要注意:在 templates 中,v-bind:class 和 v-bind:style ,會有特別的處理,他們在 VNode 數(shù)據(jù)對象中,為最高級配置。

{
  // 和`v-bind:class`一樣的 API
  'class': {
    foo: true,
    bar: false
  },
  // 和`v-bind:style`一樣的 API
  style: {
    color: 'red',
    fontSize: '14px'
  },
  // 正常的 HTML 特性
  attrs: {
    id: 'foo'
  },
  // 組件 props
  props: {
    myProp: 'bar'
  },
  // DOM 屬性
  domProps: {
    innerHTML: 'baz'
  },
  // 事件監(jiān)聽器基于 "on"
  // 所以不再支持如 v-on:keyup.enter 修飾器
  // 需要手動匹配 keyCode。
  on: {
    click: this.clickHandler
  },
  // 僅對于組件,用于監(jiān)聽原生事件,而不是組件使用 vm.$emit 觸發(fā)的事件。
  nativeOn: {
    click: this.nativeClickHandler
  },
  // 自定義指令. 注意事項:不能對綁定的舊值設(shè)值
  // Vue 會為您持續(xù)追踨
  directives: [
    {
      name: 'my-custom-directive',
      value: '2'
      expression: '1 + 1',
      arg: 'foo',
      modifiers: {
        bar: true
      }
    }
  ],
  // 如果子組件有定義 slot 的名稱
  slot: 'name-of-slot'
  // 其他特殊頂層屬性
  key: 'myKey',
  ref: 'myRef'
}

完整示例

有了這方面的知識,我們現(xiàn)在可以完成我們最開始想實現(xiàn)的組件:

var getChildrenTextContent = function (children) {
  return children.map(function (node) {
    return node.children
      ? getChildrenTextContent(node.children)
      : node.text
  }).join('')
}
Vue.component('anchored-heading', {
  render: function (createElement) {
    // create kebabCase id
    var headingId = getChildrenTextContent(this.$slots.default)
      .toLowerCase()
      .replace(/\W+/g, '-')
      .replace(/(^\-|\-$)/g, '')
    return createElement(
      'h' + this.level,
      [
        createElement('a', {
          attrs: {
            name: headingId,
            href: '#' + headingId
          }
        }, this.$slots.default)
      ]
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
})

約束

VNodes 必須唯一

所有組件樹中的 VNodes 必須唯一。這意味著,下面的 render function 是無效的:

render: function (createElement) {
  var myParagraphVNode = createElement('p', 'hi')
  return createElement('div', [
    // Yikes - duplicate VNodes!
    myParagraphVNode, myParagraphVNode
  ])
}

如果你真的需要重復(fù)很多次的元素/組件,你可以使用工廠函數(shù)來實現(xiàn)。例如,下面這個例子 render 函數(shù)完美有效地渲染了 20 個重復(fù)的段落:

render: function (createElement) {
  return createElement('div',
    Array.apply(null, { length: 20 }).map(function () {
      return createElement('p', 'hi')
    })
  )
}

使用 JavaScript 代替模板功能

無論什么都可以使用原生的 JavaScript 來實現(xiàn),Vue 的 render 函數(shù)不會提供專用的 API。比如, template 中的 v-if 和 v-for:

<ul v-if="items.length">
  <li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>

這些都會在 render 函數(shù)中被 JavaScript 的 if/else 和 map 重寫:

render: function (createElement) {
  if (this.items.length) {
    return createElement('ul', this.items.map(function (item) {
      return createElement('li', item.name)
    }))
  } else {
    return createElement('p', 'No items found.')
  }
}

JSX

如果你寫了很多 render 函數(shù),可能會覺得痛苦:

createElement(
  'anchored-heading', {
    props: {
      level: 1
    }
  }, [
    createElement('span', 'Hello'),
    ' world!'
  ]
)

特別是模板如此簡單的情況下:

<anchored-heading :level="1">
  <span>Hello</span> world!
</anchored-heading>

這就是會有一個 Babel plugin 插件,用于在 Vue 中使用 JSX 語法的原因,它可以讓我們回到于更接近模板的語法上。

import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
  el: '#demo',
  render (h) {
    return (
      <AnchoredHeading level={1}>
        <span>Hello</span> world!
      </AnchoredHeading>
    )
  }
})

將 h 作為 createElement 的別名是 Vue 生態(tài)系統(tǒng)中的一個通用慣例,實際上也是 JSX 所要求的,如果在作用域中 h 失去作用, 在應(yīng)用中會觸發(fā)報錯。

更多關(guān)于 JSX 映射到 JavaScript,閱讀 使用文檔。

函數(shù)化組件

之前創(chuàng)建的錨點標(biāo)題組件是比較簡單,沒有管理或者監(jiān)聽任何傳遞給他的狀態(tài),也沒有生命周期方法。它只是一個接收參數(shù)的函數(shù)。在這個例子中,我們標(biāo)記組件為 functional, 這意味它是無狀態(tài)(沒有 data),無實例(沒有 this 上下文)。一個函數(shù)化組件就像這樣:

Vue.component('my-component', {
  functional: true,
  // 為了彌補缺少的實例
  // 提供第二個參數(shù)作為上下文
  render: function (createElement, context) {
    // ...
  },
  // Props 可選
  props: {
    // ...
  }
})

組件需要的一切都是通過上下文傳遞,包括:

  • props: 提供props 的對象
  • children: VNode 子節(jié)點的數(shù)組
  • slots: slots 對象
  • data: 傳遞給組件的 data 對象
  • parent: 對父組件的引用

在添加 functional: true 之后,錨點標(biāo)題組件的 render 函數(shù)之間簡單更新增加 context參數(shù),this.$slots.default 更新為 context.children,之后this.level 更新為 context.props.level。

函數(shù)化組件只是一個函數(shù),所以渲染開銷也低很多。但同樣它也有完整的組件封裝,你需要知道這些, 比如:

  • 程序化地在多個組件中選擇一個
  • 在將 children, props, data 傳遞給子組件之前操作它們。

下面是一個依賴傳入 props 的值的 smart-list 組件例子,它能代表更多具體的組件:

var EmptyList = { /* ... */ }
var TableList = { /* ... */ }
var OrderedList = { /* ... */ }
var UnorderedList = { /* ... */ }
Vue.component('smart-list', {
  functional: true,
  render: function (createElement, context) {
    function appropriateListComponent () {
      var items = context.props.items
      if (items.length === 0)           return EmptyList
      if (typeof items[0] === 'object') return TableList
      if (context.props.isOrdered)      return OrderedList
      return UnorderedList
    }
    return createElement(
      appropriateListComponent(),
      context.data,
      context.children
    )
  },
  props: {
    items: {
      type: Array,
      required: true
    },
    isOrdered: Boolean
  }
})

slots() 和 children 對比

你可能想知道為什么同時需要 slots() 和 children。slots().default 不是和 children 類似的嗎?在一些場景中,是這樣,但是如果是函數(shù)式組件和下面這樣的 children 呢?

<my-functional-component>
  <p slot="foo">
    first
  </p>
  <p>second</p>
</my-functional-component>

對于這個組件,children 會給你兩個段落標(biāo)簽,而 slots().default 只會傳遞第二個匿名段落標(biāo)簽,slots().foo 會傳遞第一個具名段落標(biāo)簽。同時擁有 children 和 slots() ,因此你可以選擇讓組件通過 slot() 系統(tǒng)分發(fā)或者簡單的通過 children 接收,讓其他組件去處理。

模板編譯

你可能有興趣知道,Vue 的模板實際是編譯成了 render 函數(shù)。這是一個實現(xiàn)細節(jié),通常不需要關(guān)心,但如果你想看看模板的功能是怎樣被編譯的,你會發(fā)現(xiàn)會非常有趣。下面是一個使用 Vue.compile 來實時編譯模板字符串的簡單 demo:

render:

function anonymous(
) {
  with(this){return _h('div',[_m(0),(message)?_h('p',[_s(message)]):_h('p',["No message."])])}
}
staticRenderFns:
_m(0): function anonymous(
) {
  with(this){return _h('h1',["I'm a template!"])}
}
以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號