Vue之render方法使用

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
了解render函数的用法,可以先查看官方文档 渲染函数 & JSX

1、首先看一个初级的示例:

这里用模板并不是最好的选择:不但代码冗长,而且在每一个级别的标题中重复书写了<slot></slot>,在要插入锚点元素时还要再次重复。

<template>
  <div>
    <div v-if="level === 1">
    	<slot></slot>
    </div>
    <p v-else-if="level === 2">
    	<slot></slot>
    </p>
    <h1 v-else-if="level === 3">
    	<slot></slot>
    </h1>
    <h2 v-else-if="level === 4">
    	<slot></slot>
    </h2>
    <strong v-else-if="level === 5">
	    <slot></slot>
    </stong>
    <textarea v-else>
    	<slot></slot>
    </textarea>
  </div>
</template>
<script>
export default {
  props: {
    level: {
      type: Number,
      required: true
    }
  }
};
</script>

虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render函数重写上面的例子:

<template>
  <div>
    <child :level="level">Hello world!</child>
  </div>
</template>

<script>
 import Vue from 'vue'
 Vue.component('child', {
    render(createElement) {
      const tag = ['div', 'p', 'strong', 'h1', 'h2', 'textarea'][this.level-1]
      return createElement(tag, this.$slots.default)
    },
    props: {
    	level: {
      		type: Number,
      		required: true
    	}
  	}
 })   
 export default {
    name: 'hehe',
    data() {
    	return {
    		level: 3
    	}
    }
 }
</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事件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值