09.vue-charp-09 Render函数

时间:2020-07-11
本文章向大家介绍09.vue-charp-09 Render函数,主要包括09.vue-charp-09 Render函数使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

什么是虚拟DOM

本章我们就来探索Vue.js用于实现Virtual Dom的Render函数用法,在介绍Render函数前,我们先来看看什么是Virtual Dom。

Virtual Dom并不是真正意义上的DOM,而是一个轻量级的JavaScript对象,在状态发生变化时,Virtual Dom会进行Diff 运算,来更新只需要被替换的DOM,而不是全部重绘。
与DOM操作相比,Virtual Dom是基于JavaScript计算的,所以开销会小很多。

vNode对象通过一些特定的选项描述了真实的DOM结构。
在Vue.js 2中,Virtual Dom就是通过一种VNode类表达的,每个DOM元素或组件都对应一个VNode对象,

VNode 主要可以分为如下几类,

  • TextVNode 文本节点。
  • ElementVNode 普通元素节点。
  • ComponentVNode 组件节点。
  • EmptyVNode 没有内容的注释节点。
  • CloneVNode 克隆节点,可以是以上任意类型的节点,唯一的区别在于isCloned属性为true。

初步使用

自定义锚点组件

<div id="app">
        <div>
            <anchor :level="2" title="title-01">
                slot-标题-1
            </anchor>
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br /> <br />
            <p id="title-01">标题-1</p>
        </div>
    </div>
    <script src="../lib/vue.2.6.11.js"></script>
    <script>
        Vue.component('anchor', {
            props: {
                level: {
                    type: Number,
                    required: true
                },
                title: {
                    type: String,
                    default: ''
                }
            },
            render: function (createElement) {
                return createElement(
                    'h' + this.level,
                    [
                        createElement(
                            'a',
                            {
                                domProps: {
                                    href: '#' + this.title
                                }
                            },
                            this.$slots.default
                        )
                    ]
                )
            }
        });

        var app = new Vue({
            el: '#app'
        })
    </script>
</body>

Render函数通过createElement参数来创建Virtual Dom,结构精简了很多。在第7章组件中介绍slot时,有提到过访问slot的用法,使用场景就是在Render函数。

createElement用法

基本参数

​ createElement构成了Vue Virtual Dom的模板,它有3个参数:

createElement(
    // {String | Object | Function}
    // 一个HTML标签,组件选项,或一个函数
    //必须Return上述其中一个
    'div',
    // {Object}
    // 一个对应属性的数据对象,可选
    // 您可以在template中使用
    {
        // 稍后详细介绍
    },
    // {String | Array}
    // 子节点(VNodes),可选
    [
        createElement('h1', 'hello world'),
        createElement(MyComponent, {
            props: {
                someProp: 'foo'
            }
        }),
        'bar'
    ]
)
  • 第一个参数必选,可以是一个HTML标签,也可以是一个组件或函数;

  • 第二个是可选参数,数据对象,在template中使用。

  • 第三个是子节点,也是可选参数,用法一致。

对于第二个参数“数据对象”,具体的选项如下:

{
    //和v-bind:class一样的API
    'class': {
        foo: true,
        bar: false
    },
    //和v-bind:style一样的API
    style: {
        color: 'red',
        fontSize: '14px'
    },
    // 正常的HTML特性
    attrs: {
        id: 'foo'
id: 'foo'
},
//组件props
props: {
    myProp: 'bar'
},
// DOM属性
domProps: {
    innerHTML: 'baz'
},
// 自定义事件监听器"on"
//不支持如v-on:keyup.enter的修饰器
// 需要手动匹配 keyCode
on: {
    click: this.clickHandler
},
// 仅对于组件,用于监听原生事件
// 而不是组件使用vm.$emit触发的自定义事件
nativeOn: {
    click: this.nativeClickHandler
},
// 自定义指令
directives: [
     {
        name: 'my-custom-directive',
        value: '2'
        expression: '1 + 1',
        arg: 'foo',
        modifiers: {
               bar: true
            }
      }
    ],
    // 作用域slot
    // { name: props => VNode | Array<VNode> }
    scopedSlots: {
        default: props => h('span', props.text)
    },
    // 如果子组件有定义slot的名称
    slot: 'name-of-slot'
    // 其他特殊顶层属性
    key: 'myKey',
    ref: 'myRef'
}

示例:

<body>
    <div id="app">
        <div>
            <ele-1></ele-1>
            <ele-2></ele-2>
        </div>
    </div>
    <script src="../lib/vue.2.6.11.js"></script>
    <script>
        Vue.component('ele-1', {
            template: '\
                <div id="element-1" :class="{show: show}"  @click="handleClick">文本内容</div>',
            data: function () {
                return {
                    show: true
                }
            },
            methods: {
                handleClick: function () {
                    console.log('clicked!');
                }
            }
        });

        Vue.component('ele-2', {
            data: function () {
                return {
                    show: true
                }
            },
            render: function (createElement) {
                return createElement('div'
                    , {
                        // 动态绑定class,同:class
                        class: {
                            'show': this.show
                        },
                        // 普通html 特性
                        attrs: {
                            id: 'element-2'
                        },
                        //给div绑定click事件
                        on: {
                            click: this.handleClick
                        }
                    }
                    , "文本内容")
            },
            methods: {
                handleClick: function () {
                    console.log('clicked!');
                }
            }
        });

        var app = new Vue({
            el: '#app'
        })
    </script>
</body>

示例中,使用templaterender()两种方式实现相同的效果

约束

有的组件树中,如果VNode是组件或含有组件的slot,那么VNode必须唯一。

对于重复渲染多个组件(或元素)的方法有很多

通过一个循环和工厂函数就可以渲染5个重复的子组件Child。对于含有组件的slot,复用就要稍微复杂一点了,需要将slot的每个子节点都克隆一份。

使用JavaScript代替模板功能

在Render函数中,不再需要Vue内置的指令,比如v-if、v-for,当然,也没办法使用它们。无论要实现什么功能,都可以用原生JavaScript

函数化组件

Vue.js提供了一个functional的布尔值选项,设置为true可以使组件无状态和无实例,也就是没有data和this上下文。这样用render函数返回虚拟节点可以更容易渲染,因为函数化组件只是一个函数,渲染开销要小很多。
使用函数化组件时,Render函数提供了第二个参数context来提供临时上下文。组件需要的data、props、slots、children、parent都是通过这个上下文来传递的,比如this.level要改写为context.props.level,this.$slots.default改写为context.children。

函数化组件在业务中并不是很常用,而且也有其他类似的方法来实现,比如上例也可以用组件的is特性来动态挂载。总结起来,函数化组件主要适用于以下两个场景:

  • 序化地在多个组件中选择一个。
  • 在将children, props, data 传递给子组件之前操作它们。
<body>
    <div id="app">
        <smart-item :data="data"></smart-item>
        <button @click="change('img')">切换为图片组件</button>
        <button @click="change('video')">切换为视频组件</button>
        <button @click="change('text')">切换为文本组件</button>
    </div>
    <script src="../lib/vue.2.6.11.js"></script>
    <script>
        // 图片组件选项
        var ImgItem = {
            props: ['data'],
            render: function (createElement) {
                return createElement('div', [
                    createElement('p', '图片组件'),
                    createElement('img', {
                        attrs: {
                            src: this.data.url
                        }
                    })
                ]);
            }
        };
        // 视频组件选项
        var VideoItem = {
            props: ['data'],
            render: function (createElement) {
                return createElement('div', [
                    createElement('p', '视频组件'),
                    createElement('video', {
                        attrs: {
                            src: this.data.url,
                            controls: 'controls',
                            autoplay: 'autoplay'
                        }
                    })
                ]);
            }
        };
        // 纯文本组件选项
        var TextItem = {
            props: ['data'],
            render: function (createElement) {
                return createElement('div', [
                    createElement('p', '纯文本组件'),
                    createElement('p', this.data.text)
                ]);
            }
        };

        Vue.component('smart-item', {
            //函数化组件
            functional: true,
            render: function (createElement, context) {
                // 根据传入的数据,智能判断显示哪种组件
                function getComponent() {
                    var data = context.props.data;
                    // 判断 prop: data的type 字段是属于哪种类型的组件
                    if (data.type === 'img') return ImgItem;
                    if (data.type === 'video') return VideoItem;
                    return TextItem;
                }
                return createElement(
                    getComponent(),
                    {
                        props: {
                            //把smart-item的prop: data传给上面智能选择的组件
                            data: context.props.data
                        }
                    },
                    context.children
                )
            },
            props: {
                data: {
                    type: Object,
                    required: true
                }
            }
        })

        var app = new Vue({
            el: '#app',
            data: {
                data: {}
            },
            methods: {
                // 切换不同类型组件的数据
                change: function (type) {
                    if (type === 'img') {
                        this.data = {
                            type: 'img',
                            url: 'https://raw.githubusercontent.com/iview/iview/master/assets/logo.png'
                        }
                    } else if (type === 'video') {
                        this.data = {
                            type: 'video',
                            url: 'http://vjs.zencdn.net/v/oceans.mp4'
                        }
                    } else if (type === 'text') {
                        this.data = {
                            type: 'text',
                            content: '这是一段纯文本'
                        }
                    }
                }
            },
            created: function () {
                // 初始化时,默认设置图片组件的数据
                this.change('img');
            }
        })

    </script>
</body>

JXS

使用Render函数最不友好的地方就是在模板比较简单时,写起来也很复杂,而且难以阅读出DOM结构,尤其当子节点嵌套较多时,嵌套的createElement就像盖楼一样一层层延伸下去。

为了让Render函数更好地书写和阅读,Vue.js提供了插件babel-plugin-transform-vue-jsx来支持JSX语法。

JSX是一种看起来像HTML,但实际是JavaScript的语法扩展,它用更接近DOM结构的形式来描述一个组件的UI和状态信息,最早在React.js里大量应用。

示例:使用Render函数开发可排序的表格组件

html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>可排序的表格组件</title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
    <div id="app" v-cloak>
        <button @click="handleAddData">添加数据</button>
        <v-table :data="data" :columns="columns"></v-table>
    </div>
    <script src="../../lib/vue.2.6.11.js"></script>
    <script src="table.js"></script>
    <script src="index.js"></script>
</body>

</html>

index.js

var app = new Vue({
    el: '#app',
    data: {
        columns: [
            {
                title: '姓名',
                key: 'name'
            },
            {
                title: '年龄',
                key: 'age',
                sortable: true
            },
            {
                title: '出生日期',
                key: 'birthday',
                sortable: true
            },
            {
                title: '地址',
                key: 'address'
            }
        ],
        data: [
            {
                name: '王小明',
                age: 18,
                birthday: '1999-02-21',
                address: '北京市朝阳区芍药居'
            },
            {
                name: '张小刚',
                age: 25, age: 25,
                birthday: '1992-01-23',
                address: '北京市海淀区西二旗'
            },
            {
                name: '李小红',
                age: 30,
                birthday: '1987-11-10',
                address: '上海市浦东新区世纪大道'
            },
            {
                name: '周小伟',
                age: 26,
                birthday: '1991-10-10',
                address: '深圳市南山区深南大道'
            }
        ]
    },
    methods: {
        handleAddData: function () {
            this.data.push({
                name: '刘小天',
                age: 19,
                birthday: '1998-05-30',
                address: '北京市东城区东直门'
            });
        }
    }
});

table.js

Vue.component('vTable', {
    render: function (h) {
        var _this = this;

        //数据行
        var trs = [];
        this.currentData.forEach(function (row) {
            var tds = [];
            _this.currentColumns.forEach(function (cell) {
                tds.push(h('td', row[cell.key]));
            });
            trs.push(h('tr', tds));
        });

        //表头行
        var ths = [];
        this.currentColumns.forEach(function (col, index) {
            if (col.sortable) {
                ths.push(h('th', [
                    h('span', col.title),
                    // 升序
                    h('a', {
                        class: {
                            on: col._sortType === 'asc'
                        },
                        on: {
                            click: function () {
                                _this.handleSortByAsc(index)
                            }
                        }
                    }, '↑'),
                    // 降序
                    h('a', {
                        class: {
                            on: col._sortType === 'desc'
                        },
                        on: {
                            click: function () {
                                _this.handleSortByDesc(index)
                            }
                        }
                    }, '↓')
                ]));
            } else {
                ths.push(h('th', col.title));
            }
        });

        return h('table', [
            h('thead', [
                h('tr', ths)
            ]),
            h('tbody', trs)
        ])
    },
    props: {
        columns: {
            type: Array,
            default: function () {
                return [];
            }
        },
        data: {
            type: Array,
            default: function () {
                return [];
            }
        }
    },
    data: function () {
        return {
            /*
            为了让排序后的columns和data不影响原始数据,
            给v-table组件的data选项添加两个对应的数据,
            组件所有的操作将在这两个数据上完成,不对原始数据做任何处理 
            */
            currentColumns: [],
            currentData: []
        }
    },
    methods: {
        makeColumns: function () {
            this.currentColumns = this.columns.map(function (col, index) {
                //添加一个字段标识当前列排序的状态,后续使用
                col._sortType = 'normal';
                //添加一个字段标识当前列在原始数组中的索引,后续使用
                col._index = index;
                return col;
            });
        },
        makeData: function () {
            this.currentData = this.data.map(function (row, index) {
                //添加一个字段标识当前行在原始数组中的索引,后续使用
                row._index = index;
                return row;
            });
        },
        handleSortByAsc: function (index) {
            var key = this.currentColumns[index].key;
            this.currentColumns.forEach(function (col) {
                col._sortType = 'normal'; //排序前,先将所有列的排序状态都重置为normal,
            });
            this.currentColumns[index]._sortType = 'asc';

            this.currentData.sort(function (a, b) {
                return a[key] > b[key] ? 1 : -1;
            });
        },
        handleSortByDesc: function (index) {
            var key = this.currentColumns[index].key;
            this.currentColumns.forEach(function (col) {
                col._sortType = 'normal'; //排序前,先将所有列的排序状态都重置为normal,
            });
            this.currentColumns[index]._sortType = 'desc';

            this.currentData.sort(function (a, b) {
                return a[key] < b[key] ? 1 : -1;
            });
        }
    },
    mounted() {
        // v-table 初始化时调用
        this.makeColumns();
        this.makeData();
    },
    watch: {
        /**
         * 当渲染完表格后,父级修改了data数据,
         * 比如增加或删除,v-table的currentData也应该更新,
         * 如果某列已经存在排序状态,更新后应该直接处理一次排序。
         */
        data: function () {
            this.makeData();

            //通过遍历currentColumns来找出是否按某一列进行过排序,
            //如果有,就按照当前排序状态对更新后的数据做一次排序操作
            var sortedColumn = this.currentColumns.filter(function (col) {
                return col._sortType !== 'normal';
            });

            if (sortedColumn.length > 0) {
                if (sortedColumn[0]._sortType === 'asc') {
                    this.handleSortByAsc(sortedColumn[0]._index);
                } else {
                    this.handleSortByDesc(sortedColumn[0]._index);
                }
            }
        }
    }
});

style.css

[v-cloak]{
    display: none;
}
table{
    width: 100%;
    margin-bottom: 24px;
    border-collapse: collapse;
    border-spacing: 0;
    empty-cells: show;
    border: 1px solid #e9e9e9;
}

table th{
    background: #f7f7f7;
    color: #5c6b77;
    font-weight: 600;
    white-space: nowrap;
}
table td, table th{
    padding: 8px 16px;
    border: 1px solid #e9e9e9;
    text-align: left;
}
table th a{
    display: inline-block;
    margin: 0 4px;
    cursor: pointer;
}
table th a.on{
    color: #3399ff;
}
table th a:hover{
    color: #3399ff;
}

示例:留言列表

与之前的几个实战案例不同的是,留言列表更偏向于业务,而之前的实战(数字输入框、标签页、表格)都是独立的功能组件

出现问题

按照原书的代码,会出现如下bug:

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "value"

大概意思是:通过props传递给子组件的value,不能在子组件内部修改props中的value值。

需要定义一个本地的 data 属性并将这个 prop 用作其初始值,同步对组件的修改,再通知父组件更新:

解决方案:

Vue.component('vTextarea', {
    props: {
        value: {
            type: String,
            default: ''
        }
    },
    data: function () {
        return {
            currValue: this.value
        }
    },
    watch: {
        currValue: function (val) {
            this.$emit('input', val);
        },
        value: function (val) {
            this.currValue = val;
        }
    },
    render: function (h) {
        var _this = this;
        return h('div', [
            h('span', '留言内容:'),
            h('textarea', {
                ...
                on: {
                    input: function (event) {
                        _this.currValue = event.target.value;
                        _this.$emit('input', event.target.value);
                    }
                }
            })
        ]);
    },
  1. 定义一个变量 data.currValue,并用props.value初始化,

  2. props.value的值是单向地由父组件流向子组件,并且是随着父组件的变量data.message变化而变化

  3. data.currValue与父组件的变量data.message,没有任何关系,两者相互独立

  4. data.currValue与父组件的变量data.message其中任何一个值发生变化,要通知对方,可以在

    子组件中添加监听watch:

    watch: {
        currValue: function (val) {
            this.$emit('input', val);
        },
        value: function (val) {
            this.currValue = val;
        }
    }

5 . $emit('input', val): input 事件与 v-model结合可以自动修改绑定值

index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>留言列表</title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
    <div id="app" v-cloak style="width: 500px;margin: 0 auto;">
        <div class="message">
            <v-input v-model="username"></v-input>
            <v-textarea v-model="message" ref="message"></v-textarea>
            <button @click="handleSend">发布</button>
        </div>
        <list :list="list" @reply="handleReply"></list>
    </div>
    <script src="../../lib/vue.2.6.11.js"></script>
    <script src="input.js"></script>
    <script src="list.js"></script>
    <script src="index.js"></script>
</body>

</html>

index.js

var app = new Vue({
    el: '#app',
    data: {
        username: '',
        message: '',
        list: []
    },
    methods: {
        handleSend: function () {
            this.list.push({
                name: this.username,
                message: this.message
            });
            this.message = '';
        }
    },
    methods: {
        handleSend: function () {
            if (this.username === '') {
                window.alert('请输入昵称');
                return;
            }

            if (this.message === '') {
                window.alert('请输入留言内容');
                return;
            }
            this.list.push({
                name: this.username,
                message: this.message
            });
            this.message = '';
        },
        handleReply: function (index) {
            var name = this.list[index].name;
            this.message = '回复@' + name + ':';
            this.$refs.message.focus();
        }
    }

});

input.js

Vue.component('vInput', {
    props: {
        value: {
            type: [String, Number],
            default: ''
        }
    },
    data: function () {
        return {
            currValue: this.value
        }
    },
    render: function (h) {
        var _this = this;
        return h('div', [
            h('span', '昵称:'),
            h('input', {
                attrs: {
                    type: 'text'
                },
                domProps: {
                    value: this.currValue
                },
                on: {
                    //使用v-model:动态绑定value,并且监听input事件,把输入的内容通过$emit('input')派发给父组件。
                    input: function (event) {
                        _this.currValue = event.target.value;
                        _this.$emit('input', event.target.value);
                    }
                }
            })
        ]);
    }
});

Vue.component('vTextarea', {
    props: {
        value: {
            type: String,
            default: ''
        }
    },
    data: function () {
        return {
            currValue: this.value
        }
    },
    render: function (h) {
        var _this = this;
        return h('div', [
            h('span', '留言内容:'),
            h('textarea', {
                attrs: {
                    placeholder: '请输入留言内容'
                },
                domProps: {
                    value: this.currValue
                },
                ref: 'message',
                on: {
                    input: function (event) {
                        _this.currValue = event.target.value;
                        _this.$emit('input', event.target.value);

                    }
                }
            })
        ]);
    },
    methods: {
        focus: function () {
            this.$refs.message.focus();
        }
    },
    watch: {
        currValue: function (val) {
            this.$emit('input', val);
        },
        value: function (val) {
            this.currValue = val;
        }
    },
});

list.js

Vue.component('list', {
    props: {
        list: {
            type: Array,
            default: function () {
                return [];
            }
        }
    },
    render: function (h) {
        var _this = this;
        var list = [];
        this.list.forEach(function (msg, index) {
            var node = h('div', {
                attrs: {
                    class: 'list-item'
                }
            }, [
                h('span', msg.name + ':'),
                h('div', {
                    attrs: {
                        class: 'list-msg'
                    }
                }, [
                    h('p', msg.message),
                    h('a', {
                        attrs: {
                            class: 'list-reply'
                        },
                        on: {
                            click: function () {
                                _this.handleReply(index);
                            }
                        }
                    }, '回复')
                ])
            ])
            list.push(node); list.push(node);
        });
        if (this.list.length) {
            return h('div', {
                attrs: {
                    class: 'list'
                },
            }, list);
        } else {
            return h('div', {
                attrs: {
                    class: 'list-nothing'
                }
            }, '留言列表为空');
        }
    },
    methods: {
        handleReply: function (index) {
            this.$emit('reply', index);
        }
    }
});

style.css

[v-cloak]{
    display: none;
}

*{
    padding: 0;
    margin: 0;
}
.message{
    width: 450px;
    text-align: right;
}
.message div{
    margin-bottom: 12px;
}
.message span{
    display: inline-block;
    width: 100px;
    vertical-align: top;
}
.message input, .message textarea{
    width: 300px;
    height: 32px;
    padding: 0 6px;
    color: #657180;
    border: 1px solid #d7dde4;
    border-radius: 4px;
    cursor: text;
    outline: none;
}
.message input:focus, .message textarea:focus{
    border: 1px solid #3399ff;
}
.message textarea{
    height: 60px;
    padding: 4px 6px;
}
.message button{
    display: inline-block;
    padding: 6px 15px;
    border: 1px solid #39f;
    border-radius: 4px;
    color: #fff;
    background-color: #39f;
    cursor: pointer;
    outline: none;
}
.list{
    margin-top: 50px;
}
list-item{
    padding: 10px;
    border-bottom: 1px solid #e3e8ee;
    overflow: hidden;
}
.list-item span{
    display: block;
    width: 60px;
    float: left;
    color: #39f;
}
.list-msg{
    display: block;
    margin-left: 60px;
    text-align: justify;
}
.list-msg a{
    color: #9ea7b4;
    cursor: pointer;
    float: right;
}
.list-msg a:hover{
    color: #39f;
}
.list-nothing{
    text-align: center;
    color: #9ea7b4;
    padding: 20px;
}

$flag 上一页 下一页