Vue之render方法使用

Vue 推薦在絕大多數情況下使用模板來創建你的 HTML。然而在一些場景中,你真的需要 JavaScript 的完全編程的能力。這時你可以用渲染函數,它比模板更接近編譯器。
瞭解render函數的用法,可以先查看官方文檔 渲染函數 & JSX

1、首先引用下官方的示例:

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

<template>
  <div>
    <h1 v-if="level === 1">
      <slot></slot>
    </h1>
    <h2 v-else-if="level === 2">
      <slot></slot>
    </h2>
    <h3 v-else-if="level === 3">
      <slot></slot>
    </h3>
    <h4 v-else-if="level === 4">
      <slot></slot>
    </h4>
    <h5 v-else-if="level === 5">
      <slot></slot>
    </h5>
    <h6 v-else-if="level === 6">
      <slot></slot>
    </h6>
  </div>
</template>

<script>
export default {
  name: "App",
  props: ['level']
};
</script>

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

<script>
export default {
  name: "App",
  render: function (createElement) {
    return createElement(
      'h' + this.level,   // 標籤名稱
      this.$slots.default // 子節點數組
    )
  },
  props: {
    level: {
      type: Number,
      required: true
    }
  }
};
</script>

這樣看起來代碼就精簡多了,但是需要非常熟悉 Vue 的實例屬性。在這個例子中,你需要知道,向組件中傳遞不帶 v-slot 指令的子節點時,這些子節點被存儲在組件實例中的 $slots.default 中,否則具名插槽可通過 $slots.插槽名 稱來指定。

可以看到,render函數接收一個參數createElement,然後Vue 通過建立一個虛擬 DOMVNode)來追蹤自己要如何改變真實 DOM

createElement 函數中使用模板中的那些功能,它接受的參數如下:

// @returns {VNode}
createElement(
  // {String | Object | Function}
  // 一個 HTML 標籤名、組件選項對象,或者
  // resolve 了上述任何一種的一個 async 函數。必填項。
  "div",
  
  // {Object}
  // 一個與模板中屬性對應的數據對象。可選。
  {
    // 與 `v-bind:class` 的 API 相同,
    // 接受一個字符串、對象或字符串和對象組成的數組
    class: {
      foo: true,
      bar: false,
    },
    // 與 `v-bind:style` 的 API 相同,
    // 接受一個字符串、對象,或對象組成的數組
    style: {
      color: "red",
      fontSize: "14px",
    },
    // 普通的 HTML attribute
    attrs: {
      id: "foo",
    },
    // 組件 prop
    props: {
      myProp: "bar",
    },
    // DOM 屬性
    domProps: {
      innerHTML: "baz",
    },
    // 事件監聽器在 `on` 屬性內,
    // 但不再支持如 `v-on:keyup.enter` 這樣的修飾器。
    // 需要在處理函數中手動檢查 keyCode。
    on: {
      click: this.clickHandler,
    },
    // 僅用於組件,用於監聽原生事件,而不是組件內部使用
    // `vm.$emit` 觸發的事件。
    nativeOn: {
      click: this.nativeClickHandler,
    },
    // 自定義指令。注意,你無法對 `binding` 中的 `oldValue`
    // 賦值,因爲 Vue 已經自動爲你進行了同步。
    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",
    // 其它特殊頂層屬性
    key: "myKey",
    ref: "myRef",
    // 如果你在渲染函數中給多個元素都應用了相同的 ref 名,
    // 那麼 `$refs.myRef` 會變成一個數組。
    refInFor: true,
  },
  
  // {String | Array}
  // 子級虛擬節點 (VNodes),由 `createElement()` 構建而成,
  // 也可以使用字符串來生成“文本虛擬節點”。可選。
  [
    "先寫一些文字",
    createElement("h1", "一則頭條"),
    createElement(MyComponent, {
      props: {
        someProp: "foobar",
      },
    }),
  ]
);
2、父子template組件通過render方法實現:

首先初始單文本組件如下,來模擬一個簡單的TODO頁面:

// 父組件:Todo.vue
<template>
    <div class="todo">
        <input type="text" v-model="content" placeholder="接下來的計劃..." />
        <button @click="commit">提交</button>
        <todo-list :todoList="todoList">待辦事項:</todo-list>
    </div>
</template>

<script>
import TodoList from "./TodoList.vue";

export default {
    name: "Todo",
    data() {
        return {
            content: "",
            todoList: []
        };
    },
    methods: {
        commit() {
            let id = this.todoList.length + 1;
            this.todoList.push({ id: id, title: this.content });
            this.content = '';
        },
    },
    components: {
        TodoList
    },
};
</script>

<style lang="less" scoped>
.todo {
    width: 500px;
    margin: 50px auto;

    input {
        padding: 5px 10px;
    }

    button {
        margin-left: 20px;
        padding: 5px 10px;
    }
}
</style>

// 子組件:TodoList.vue
<template>
    <div class="todo-list">
        <slot></slot>
        <ul>
            <li v-for="item in todoList" :key="item.id">{{ item.title }}</li>
        </ul>
    </div>
</template>

<script>
export default {
    name: "TodoList",
    props: ["todoList"]
};
</script>

<style lang="less" scoped>
.todo-list {
    margin-top: 20px;
    padding-left: 0;

    li {
        margin: 5px 0;
        list-style: none;
    }
}
</style>

通過以上是現實瞭如下頁面:
在這裏插入圖片描述

接下來嘗試使用render函數實現以上功能頁面:

  • 發現如果templaterender函數同時存在時,Vue還是會優先使用template中的內容。

父組件Todo.vue:

<!--<template>
    <div class="todo">
        <input type="text" v-model="content" placeholder="接下來的計劃..." />
        <button @click="commit">提交</button>
        <todo-list :todoList="todoList">{{ listTitle }}</todo-list>
    </div>
</template>-->

<script>
import TodoList from "./TodoList.vue";

export default {
    name: "Todo",
    data() {
        return {
            content: "",
            todoList: [],
            listTitle: "待辦事項:",
        };
    },
    methods: {
        commit() {
            let id = this.todoList.length + 1;
            this.todoList.push({ id: id, title: this.content });
            this.content = "";
        },
    },
    render(createElement) {
        var self = this; // 定義self保存this,使之始終指向vue示例
        return createElement(
            "div",
            {
                // 接受一個字符串、對象或字符串和對象組成的數組
                class: {
                    todo: true, // 通過對象定義class是否啓用
                }
            },
            [
                createElement("input", {
                    // DOM 屬性
                    domProps: {
                        type: "text",
                        placeholder: "接下來的計劃...",
                        value: self.content, // 將content屬性值賦值給輸入框
                    },
                    // 普通的 HTML attribute
                    // attrs: {
                    //     type: "text",
                    //     placeholder: "接下來的計劃...",
                    //     value: self.content, // 
                    // },
                    on: {
                        change: function(event) {
                            self.content = event.target.value; // 輸入框值賦給content屬性
                        },
                    },
                }),
                createElement(
                    "button",
                    {
                        // 給按鈕添加click事件,觸發commit方法
                        on: {
                            click: this.commit,
                        }
                    },
                    "提交"
                ),
                createElement(
                    // 子組件選項對象
                    "todo-list",
                    {
                        // props傳值給子組件
                        props: {
                            todoList: this.todoList,
                        }
                    },
                    this.listTitle
                ),
            ]
        );
    },
    components: {
        TodoList,
    },
};
</script>

<style lang="less" scoped>
.todo {
    width: 500px;
    margin: 50px auto;

    input {
        padding: 5px 10px;
    }

    button {
        margin-left: 20px;
        padding: 5px 10px;
    }
}
</style>

子組件TodoList.vue:

<!--<template>
    <div class="todo-list">
        <slot></slot>
        <ul>
            <li v-for="item in todoList" :key="item.id">{{ item.id }}. {{ item.title }}</li>
        </ul>
    </div>
</template>-->

<script>
export default {
    name: "TodoList",
    props: ["todoList"],
    render(createElement) {
        return createElement(
            "div",
            {
                class: {
                    'todo-list': true,
                }
            },
            [
                this.$slots.default,
                createElement(
                    "ul",
                    this.todoList.map(function(item) {
                        return createElement(
                            "li",
                            {
                                key: item.id,
                            },
                            `${item.id}. ${item.title}`
                        );
                    })
                ),
            ]
        );
    },
};
</script>

<style lang="less" scoped>
.todo-list {
    margin-top: 20px;
    padding-left: 0;

    li {
        margin: 5px 0;
        list-style: none;
    }
}
</style>
3、小結
  • createElement
    createElement,是 Vue 虛擬 DOM 的概念,創建出來的並不是 html節點,而是 VNode 的一個類,類似 DOM 結構的一個結構,並存在內存中,它會和真正的 DOM 進行對比,若發現需要更新的 DOM,纔會去轉換這部分 DOM 內容,並填到真正的 DOM 中,從而提高性能;

  • nativeOn 與 on 的區別
    對於nativeOn,官方的解釋是:僅對於組件,用於監聽原生事件,而不是組件內部使用 vm.$emit 觸發的事件。
    解釋比較抽象,個人理解:
    父組件要在子組件上使用click事件,就像使用正常的html標籤那樣使用click,我們知道在Vue中,普通html標籤中這樣寫click事件是沒問題:

     <h @click="doSomething()"></h>
    

    但假如我們有一個組件叫comA,直接使用click是不行的(除非子組件裏面做了處理),加上了.native 就可以生效:

     <comA @click="doSomething()"></comA> // 無效
     <comA @click.native="doSomething()"></comA> // 有效
    

    所以,僅用於組件這句話意思應該是:
    createElement()裏面使用nativeOn創建的不可以是原生html元素而是組件,比如:

    createElement("p", { nativeOn: { click: function() {} } })
    

    這個時候nativeOn就沒有意義,而下面寫法就會有意義:

    createElement("組件名稱", { nativeOn: { click: function() {} } })
    

    在該組件根節點上發生了點擊事件會觸發nativeOn裏面的click事件。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章