溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何理解渲染函數(shù)和JSX

發(fā)布時間:2021-09-24 17:19:41 來源:億速云 閱讀:140 作者:柒染 欄目:開發(fā)技術

這篇文章給大家介紹如何理解渲染函數(shù)和JSX,內(nèi)容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

    一、基礎

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

    讓我們深入一個簡單的例子,這個例子里 render 函數(shù)很實用。假設我們要生成一些帶錨點的標題:

    <h2>
      <a name="hello-world" href="#hello-world" rel="external nofollow" >
        Hello world!
      </a>
    </h2>

    對于上面的 HTML,我們決定這樣定義組件接口:

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

    當開始寫一個只能通過 level prop 動態(tài)生成標題 (heading) 的組件時,你可能很快想到這樣實現(xiàn):

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

    這里用模板并不是最好的選擇:不但代碼冗長,而且在每一個級別的標題中重復書寫了 <slot></slot>,在要插入錨點元素時還要再次重復。

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

    Vue.component('anchored-heading', {
      render: function (createElement) {
        return createElement(
          'h' + this.level,   // 標簽名稱
          this.$slots.default // 子節(jié)點數(shù)組
        )
      },
      props: {
        level: {
          type: Number,
          required: true
        }
      }
    })

    看起來簡單多了!這樣代碼精簡很多,但是需要非常熟悉 Vue 的實例 property。在這個例子中,你需要知道,向組件中傳遞不帶 v-slot 指令的子節(jié)點時,比如 anchored-heading 中的 Hello world!,這些子節(jié)點被存儲在組件實例中的 $slots.default 中。如果你還不了解,在深入渲染函數(shù)之前推薦閱讀實例 property API。

    二、節(jié)點、樹以及虛擬 DOM

    在深入渲染函數(shù)之前,了解一些瀏覽器的工作原理是很重要的。以下面這段 HTML 為例:

    <div>
      <h2>My title</h2>
      Some text content
      <!-- TODO: Add tagline -->
    </div>

    當瀏覽器讀到這些代碼時,它會建立一個“DOM 節(jié)點”樹來保持追蹤所有內(nèi)容,如同你會畫一張家譜樹來追蹤家庭成員的發(fā)展一樣。

    上述 HTML 對應的 DOM 節(jié)點樹如下圖所示:

    如何理解渲染函數(shù)和JSX

    每個元素都是一個節(jié)點。每段文字也是一個節(jié)點。甚至注釋也都是節(jié)點。一個節(jié)點就是頁面的一個部分。就像家譜樹一樣,每個節(jié)點都可以有孩子節(jié)點 (也就是說每個部分可以包含其它的一些部分)。

    高效地更新所有這些節(jié)點會是比較困難的,不過所幸你不必手動完成這個工作。你只需要告訴 Vue 你希望頁面上的 HTML 是什么,這可以是在一個模板里:

    <h2>{{ blogTitle }}</h2>

    或者一個渲染函數(shù)里:

    render: function (createElement) {
      return createElement('h2', this.blogTitle)
    }

    在這兩種情況下,Vue 都會自動保持頁面的更新,即便 blogTitle 發(fā)生了改變。

    1、虛擬 DOM

    Vue 通過建立一個虛擬 DOM 來追蹤自己要如何改變真實 DOM。請仔細看這行代碼:

    return createElement('h2', this.blogTitle)

    createElement 到底會返回什么呢?其實不是一個實際的 DOM 元素。它更準確的名字可能是 createNodeDescription,因為它所包含的信息會告訴 Vue 頁面上需要渲染什么樣的節(jié)點,包括及其子節(jié)點的描述信息。我們把這樣的節(jié)點描述為“虛擬節(jié)點 (virtual node)”,也常簡寫它為“VNode”。“虛擬 DOM”是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。

    三、createElement 參數(shù)

    接下來你需要熟悉的是如何在 createElement 函數(shù)中使用模板中的那些功能。這里是 createElement 接受的參數(shù):

    // @returns {VNode}
    createElement(
      // {String | Object | Function}
      // 一個 HTML 標簽名、組件選項對象,或者
      // resolve 了上述任何一種的一個 async 函數(shù)。必填項。
      'div',
    
      // {Object}
      // 一個與模板中 attribute 對應的數(shù)據(jù)對象??蛇x。
      {
        // (詳情見下一節(jié))
      },
    
      // {String | Array}
      // 子級虛擬節(jié)點 (VNodes),由 `createElement()` 構建而成,
      // 也可以使用字符串來生成“文本虛擬節(jié)點”??蛇x。
      [
        '先寫一些文字',
        createElement('h2', '一則頭條'),
        createElement(MyComponent, {
          props: {
            someProp: 'foobar'
          }
        })
      ]
    )

    1、深入數(shù)據(jù)對象

    有一點要注意:正如 v-bind:class v-bind:style 在模板語法中會被特別對待一樣,它們在 VNode 數(shù)據(jù)對象中也有對應的頂層字段。該對象也允許你綁定普通的 HTML attribute,也允許綁定如 innerHTML 這樣的 DOM property (這會覆蓋 v-html 指令)。

    {
      // 與 `v-bind:class` 的 API 相同,
      // 接受一個字符串、對象或字符串和對象組成的數(shù)組
      'class': {
        foo: true,
        bar: false
      },
      // 與 `v-bind:style` 的 API 相同,
      // 接受一個字符串、對象,或?qū)ο蠼M成的數(shù)組
      style: {
        color: 'red',
        fontSize: '14px'
      },
      // 普通的 HTML attribute
      attrs: {
        id: 'foo'
      },
      // 組件 prop
      props: {
        myProp: 'bar'
      },
      // DOM property
      domProps: {
        innerHTML: 'baz'
      },
      // 事件監(jiān)聽器在 `on` 內(nèi),
      // 但不再支持如 `v-on:keyup.enter` 這樣的修飾器。
      // 需要在處理函數(shù)中手動檢查 keyCode。
      on: {
        click: this.clickHandler
      },
      // 僅用于組件,用于監(jiān)聽原生事件,而不是組件內(nèi)部使用
      // `vm.$emit` 觸發(fā)的事件。
      nativeOn: {
        click: this.nativeClickHandler
      },
      // 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
      // 賦值,因為 Vue 已經(jīng)自動為你進行了同步。
      directives: [
        {
          name: 'my-custom-directive',
          value: '2',
          expression: '1 + 1',
          arg: 'foo',
          modifiers: {
            bar: true
          }
        }
      ],
      // 作用域插槽的格式為
      // { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: props => createElement('span', props.text)
      },
      // 如果組件是其它組件的子組件,需為插槽指定名稱
      slot: 'name-of-slot',
      // 其它特殊頂層 property
      key: 'myKey',
      ref: 'myRef',
      // 如果你在渲染函數(shù)中給多個元素都應用了相同的 ref 名,
      // 那么 `$refs.myRef` 會變成一個數(shù)組。
      refInFor: true
    }

    2、完整示例

    有了這些知識,我們現(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) {
        // 創(chuàng)建 kebab-case 風格的 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
        }
      }
    })

    3、約束

    VNode 必須唯一

    組件樹中的所有 VNode 必須是唯一的。這意味著,下面的渲染函數(shù)是不合法的:

    render: function (createElement) {
      var myParagraphVNode = createElement('p', 'hi')
      return createElement('div', [
        // 錯誤 - 重復的 VNode
        myParagraphVNode, myParagraphVNode
      ])
    }

    如果你真的需要重復很多次的元素/組件,你可以使用工廠函數(shù)來實現(xiàn)。例如,下面這渲染函數(shù)用完全合法的方式渲染了 20 個相同的段落:

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

    四、使用 JavaScript 代替模板功能

    1、v-if 和 v-for

    只要在原生的 JavaScript 中可以輕松完成的操作,Vue 的渲染函數(shù)就不會提供專有的替代方法。比如,在模板中使用的 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>

    這些都可以在渲染函數(shù)中用 JavaScript 的 if/else 和 map 來重寫:

    props: ['items'],
    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.')
      }
    }

    2、v-model

    渲染函數(shù)中沒有與 v-model 的直接對應——你必須自己實現(xiàn)相應的邏輯:

    props: ['value'],
    render: function (createElement) {
      var self = this
      return createElement('input', {
        domProps: {
          value: self.value
        },
        on: {
          input: function (event) {
            self.$emit('input', event.target.value)
          }
        }
      })
    }

    這就是深入底層的代價,但與 v-model 相比,這可以讓你更好地控制交互細節(jié)。

    3、事件 & 按鍵修飾符

    對于 .passive、.capture .once 這些事件修飾符,Vue 提供了相應的前綴可以用于 on:

    修飾符前綴
    .passive&
    .capture!
    .once~
    .capture.once 或
    .once.capture
    ~!

    例如:

    on: {
      '!click': this.doThisInCapturingMode,
      '~keyup': this.doThisOnce,
      '~!mouseover': this.doThisOnceInCapturingMode
    }

    對于所有其它的修飾符,私有前綴都不是必須的,因為你可以在事件處理函數(shù)中使用事件方法:

    修飾符處理函數(shù)中的等價操作
    .stopevent.stopPropagation()
    .preventevent.preventDefault()
    .selfif (event.target !== event.currentTarget) return
    按鍵:
    .enter, .13
    if (event.keyCode !== 13) return (對于別的按鍵修飾符來說,可將 13 改為另一個按鍵碼)
    修飾鍵:
    .ctrl, .alt, .shift, .meta
    if (!event.ctrlKey) return (將 ctrlKey 分別修改為 altKey、shiftKey 或者 metaKey)

    這里是一個使用所有修飾符的例子:

    on: {
      keyup: function (event) {
        // 如果觸發(fā)事件的元素不是事件綁定的元素
        // 則返回
        if (event.target !== event.currentTarget) return
        // 如果按下去的不是 enter 鍵或者
        // 沒有同時按下 shift 鍵
        // 則返回
        if (!event.shiftKey || event.keyCode !== 13) return
        // 阻止 事件冒泡
        event.stopPropagation()
        // 阻止該元素默認的 keyup 事件
        event.preventDefault()
        // ...
      }
    }

    4、插槽

    你可以通過 this.$slots 訪問靜態(tài)插槽的內(nèi)容,每個插槽都是一個 VNode 數(shù)組:

    render: function (createElement) {
      // `<div><slot></slot></div>`
      return createElement('div', this.$slots.default)
    }

    也可以通過 this.$scopedSlots 訪問作用域插槽,每個作用域插槽都是一個返回若干 VNode 的函數(shù):

    props: ['message'],
    render: function (createElement) {
      // `<div><slot :text="message"></slot></div>`
      return createElement('div', [
        this.$scopedSlots.default({
          text: this.message
        })
      ])
    }

    如果要用渲染函數(shù)向子組件中傳遞作用域插槽,可以利用 VNode 數(shù)據(jù)對象中的 scopedSlots 字段:

    render: function (createElement) {
      // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
      return createElement('div', [
        createElement('child', {
          // 在數(shù)據(jù)對象中傳遞 `scopedSlots`
          // 格式為 { name: props => VNode | Array<VNode> }
          scopedSlots: {
            default: function (props) {
              return createElement('span', props.text)
            }
          }
        })
      ])
    }

    五、JSX

    如果你寫了很多 render 函數(shù),可能會覺得下面這樣的代碼寫起來很痛苦:

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

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

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

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

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

    將 h 作為 createElement 的別名是 Vue 生態(tài)系統(tǒng)中的一個通用慣例,實際上也是 JSX 所要求的。從 Vue 的 Babel 插件的 3.4.0 版本開始,我們會在以 ES2015 語法聲明的含有 JSX 的任何方法和 getter 中 (不是函數(shù)或箭頭函數(shù)中) 自動注入 const h = this.$createElement,這樣你就可以去掉 (h) 參數(shù)了。對于更早版本的插件,如果 h 在當前作用域中不可用,應用會拋錯。

    六、函數(shù)式組件

    之前創(chuàng)建的錨點標題組件是比較簡單,沒有管理任何狀態(tài),也沒有監(jiān)聽任何傳遞給它的狀態(tài),也沒有生命周期方法。實際上,它只是一個接受一些 prop 的函數(shù)。在這樣的場景下,我們可以將組件標記為 functional,這意味它無狀態(tài) (沒有響應式數(shù)據(jù)),也沒有實例 (沒有 this 上下文)。一個函數(shù)式組件就像這樣:

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

    注意:在 2.3.0 之前的版本中,如果一個函數(shù)式組件想要接收 prop,則 props 選項是必須的。在 2.3.0 或以上的版本中,你可以省略 props 選項,所有組件上的 attribute 都會被自動隱式解析為 prop。

    當使用函數(shù)式組件時,該引用將會是 HTMLElement,因為他們是無狀態(tài)的也是無實例的。

    在 2.5.0 及以上版本中,如果你使用了單文件組件,那么基于模板的函數(shù)式組件可以這樣聲明:

    <template functional>
    </template>

    組件需要的一切都是通過 context 參數(shù)傳遞,它是一個包括如下字段的對象:

    • props:提供所有 prop 的對象

    • children:VNode 子節(jié)點的數(shù)組

    • slots:一個函數(shù),返回了包含所有插槽的對象

    • scopedSlots:(2.6.0+) 一個暴露傳入的作用域插槽的對象。也以函數(shù)形式暴露普通插槽。

    • data:傳遞給組件的整個數(shù)據(jù)對象,作為 createElement 的第二個參數(shù)傳入組件

    • parent:對父組件的引用

    • listeners:(2.3.0+) 一個包含了所有父組件為當前組件注冊的事件監(jiān)聽器的對象。這是 data.on 的一個別名。

    • injections:(2.3.0+) 如果使用了 inject 選項,則該對象包含了應當被注入的 property。

    在添加 functional: true 之后,需要更新我們的錨點標題組件的渲染函數(shù),為其增加 context 參數(shù),并將 this.$slots.default 更新為 context.children,然后將 this.level 更新為 context.props.level。

    因為函數(shù)式組件只是函數(shù),所以渲染開銷也低很多。

    在作為包裝組件時它們也同樣非常有用。比如,當你需要做這些時:

    • 程序化地在多個組件中選擇一個來代為渲染;

    • 在將 children、props、data 傳遞給子組件之前操作它們。

    下面是一個 smart-list 組件的例子,它能根據(jù)傳入 prop 的值來代為渲染更具體的組件:

    var EmptyList = { /* ... */ }
    var TableList = { /* ... */ }
    var OrderedList = { /* ... */ }
    var UnorderedList = { /* ... */ }
    
    Vue.component('smart-list', {
      functional: true,
      props: {
        items: {
          type: Array,
          required: true
        },
        isOrdered: Boolean
      },
      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
        )
      }
    })

    1、向子元素或子組件傳遞 attribute 和事件

    在普通組件中,沒有被定義為 prop 的 attribute 會自動添加到組件的根元素上,將已有的同名 attribute 進行替換或與其進行智能合并。

    然而函數(shù)式組件要求你顯式定義該行為:

    Vue.component('my-functional-button', {
      functional: true,
      render: function (createElement, context) {
        // 完全透傳任何 attribute、事件監(jiān)聽器、子節(jié)點等。
        return createElement('button', context.data, context.children)
      }
    })

    通過向 createElement 傳入 context.data 作為第二個參數(shù),我們就把 my-functional-button 上面所有的 attribute 和事件監(jiān)聽器都傳遞下去了。事實上這是非常透明的,以至于那些事件甚至并不要求 .native 修飾符。

    如果你使用基于模板的函數(shù)式組件,那么你還需要手動添加 attribute 和監(jiān)聽器。因為我們可以訪問到其獨立的上下文內(nèi)容,所以我們可以使用 data.attrs 傳遞任何 HTML attribute,也可以使用 listeners (即 data.on 的別名) 傳遞任何事件監(jiān)聽器。

    <template functional>
      <button
        class="btn btn-primary"
        v-bind="data.attrs"
        v-on="listeners"
      >
        <slot/>
      </button>
    </template>

    2、slots() 和 children 對比

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

    <my-functional-component>
      <p v-slot:foo>
        first
      </p>
      <p>second</p>
    </my-functional-component>

    對于這個組件,children 會給你兩個段落標簽,而 slots().default 只會傳遞第二個匿名段落標簽,slots().foo 會傳遞第一個具名段落標簽。同時擁有 children 和 slots(),因此你可以選擇讓組件感知某個插槽機制,還是簡單地通過傳遞 children,移交給其它組件去處理。

    七、模板編譯

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

    <div>
            <header>
              <h2>I'm a template!</h2>
            </header>
            <p v-if="message">{{ message }}</p>
            <p v-else>No message.</p>
          </div>

    render:

    function anonymous(
    ) {
      with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
    }

    staticRenderFns:

    _m(0): function anonymous(
    ) {
      with(this){return _c('header',[_c('h2',[_v("I'm a template!")])])}
    }

    關于如何理解渲染函數(shù)和JSX就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

    向AI問一下細節(jié)

    免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

    AI