07.vue-charp-07 组件详解(二)

其它

$nextTick

Vue一个重要的概念:异步更新队列。

Vue在观察到数据变化时并不是直接更新DOM,而是开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。在缓冲时会去除重复数据,从而避免不必要的计算和DOM操作。然后,在下一个事件循环tick中,Vue刷新队列并执行实际(已去重的)工作。所以如果你用一个for循环来动态改变数据100次,其实它只会应用最后一次改变,如果没有这种机制,DOM就要重绘100次,这固然是一个很大的开销。
Vue会根据当前浏览器环境优先使用原生的Promise.then和MutationObserver,如果都不支持,就会采用setTimeout 代替。

$nextTick就是用来知道什么时候DOM更新完成的:

<body>
    <div id="app">
        <div id="div" v-if="showDiv">这是一段文本</div>
        <button @click="getText">获取 div内容</button>
    </div>
    <script src="../lib/vue.2.6.11.js"></script>
    <script>
        var app = new Vue({
            el: '#app',
            data: {
                showDiv: false
            },
            methods: {
                getText: function () {
                    this.showDiv = true;
                    //Vue在观察到数据变化时并不是直接更新DOM
                    //let text1 = document.getElementById('div').innerHTML;
                    //console.log(text1);
                    this.$nextTick(function () {
                        let text2 = document.getElementById('div').innerHTML;
                        console.log(text2);
                    });
                }
            }
        });
    </script>
</body>

X-Templates

果你没有使用webpack、gulp等工具,试想一下你的组件template的内容很冗长、复杂,如果都在JavaScript里拼接字符串,效率是很低的,因为不能像写HTML 那样舒服。Vue提供了另一种定义模板的方式,在<script> 标签里使用text/x-template类型,并且指定一个id,将这个id赋给template。


<body>
    <div id="app">
        <my-component></my-component>
        <script type="text/x-template" id="my-component">
            <div>这是组件的内容</div>
            <div>appMsg={{appMsg}}</div>
            <div>cptMsg={{cptMsg}}</div>
        </script>
    </div>
    <script src="../lib/vue.2.6.11.js"></script>
    <script>
        Vue.component('my-component', {
            template: '#my-component',
            data: function () {
                return {
                    cptMsg: "子组件的内容"
                }
            }
        });
        var app = new Vue({
            el: '#app',
            data: {
                appMsg: "父组件的内容"
            }
        });
    </script>
</body>

​ 但是,这种方式,x-template中无法识别父组件和子组件中的变量

手动挂载实例

我们现在所创建的实例都是通过new Vue()的形式创建出来的。在一些非常特殊的情况下,我们需要动态地去创建Vue实例,Vue提供了Vue.extend和$mount 两个方法来手动挂载一个实例。
Vue.extend是基础Vue构造器,创建一个“子类”,参数是一个包含组件选项的对象。
如果Vue实例在实例化时没有收到el选项,它就处于“未挂载”状态,没有关联的DOM元素。可以使用$mount()手动地挂载一个未挂载的实例。这个方法返回实例自身,因而可以链式调用其他实例方法。

动挂载实例(组件)是一种比较极端的高级用法,在业务中几乎用不到,只在开发一些复杂的独立组件时可能会使用,所以只做了解就好。

示例:数组输入框

数字输入框只能输入数字,而且有两个快捷按钮,可以直接减 1或加1。除此之外,还可以设置初始值、最大值、最小值,在数值改变时,触发一个自定义事件来通知父组件。

  • index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Vue 示例:数字输入框</title>
</head>

<body>
    <div id="app">
        <input-number v-model="value" :max="10" :min="0" @on-change="onValueChange"></input-number>
    </div>

    <script src="../../lib/vue.2.6.11.js"></script>
    <script src="input-number.js"></script>
    <script src="index.js"></script>
</body>

</html>
  • index.js

var app = new Vue({
    el: '#app',
    data: {
        value: 0
    },
    methods: {
        onValueChange: function () {
            console.log('onValueChange->', this.value);
        }
    }
});
  • input-number.js
Vue.component('input-number', {
    template: '\
        <div class="input-number"> \
            <input \
                type="text" \
                :value="currentValue" \
                @change="handleChange"> \
            <button \
                @click="handleDown" \
                :disabled="currentValue <= min">-</button> \
            <button \
                @click="handleUp" \
                :disabled="currentValue >= max">+</button> \
        </div>',
    props: {
        max: {
            type: Number,
            default: Infinity
        },
        min: {
            type: Number,
            default: -Infinity
        },
        value: {
            type: Number,
            default: 0
        }
    },
    data: function () {
        return {
            currentValue: this.value
        }
    },
    methods: {
        handleDown: function () {
            if (this.currentValue <= this.min) return;
            this.currentValue -= 1;
        },
        handleUp: function () {
            if (this.currentValue >= this.max) return;
            this.currentValue += 1;
        },
        updateValue: function (val) {
            if (val > this.max) val = this.max;
            if (val < this.min) val = this.min;
            this.currentValue = val;
        },
        handleChange: function (event) {
            var val = event.target.value.trim();

            var max = this.max;
            var min = this.min;

            if (isValueNumber(val)) {
                val = Number(val);
                this.currentValue = val;

                if (val > max) {
                    this.currentValue = max;
                } else if (val < min) {
                    this.currentValue = min;
                }
            } else {
                event.target.value = this.currentValue;
            }
        }
    },
    watch: {
        currentValue: function (val) {
            this.$emit('input', val);
            this.$emit('on-change', val);
        },
        value: function (val) {
            this.updateValue(val);
        }
    },
    mounted: function () {
        this.updateValue(this.value);
    }
});

watch

watch选项用来监听某个prop或data的改变,当它们发生变化时,就会触发watch配置的函数

在本例中,我们要监听两个量:value和currentValue。监听value是要知晓从父组件修改了value,监听currentValue是为了当currentValue 改变时,更新value。

watch监听的数据的回调函数有2个参数可用,第一个是新的值,第二个是旧的值,这里没有太复杂的逻辑,就只用了第一个参数。

input事件与v-model

监听currentValue的回调里,this.$emit('input', val)是在使用v-model时改变value的;this.$emit('on-change', val)是触发自定义事件on-change

组件的为什么不用v-model进行双向绑定

这里绑定的currentValue也是单向数据流,并没有用v-model,所以在输入时,currentValue的值并没有实时改变。如果输入的不是数字(比如英文和汉字等),就将输入的内容重置为之前的currentValue。如果输入的是符合要求的数字,就把输入的值赋给currentValue。

示例:Tab组件

  • index.html
<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8">
    <title>Vue 示例:Tab组件</title>
    <link rel="stylesheet" type="text/css" href="style.css">
</head>

<body>
    <div id="app" v-cloak>
        <tabs v-model="activeKey">
            <pane label="标签一" name="1">
                标签一的内容
            </pane>
            <pane label="标签二" name="2">
                标签二的内容
            </pane>
            <pane label="标签三" name="3">
                标签三的内容
            </pane>
        </tabs>
    </div>

    <script src="../../lib/vue.2.6.11.js"></script>
    <script src="pane.js"></script>
    <script src="tab.js"></script>
    <script src="index.js"></script>
</body>

</html>
  • index.js
var app = new Vue({
    el: '#app',
    data: {
        activeKey: '1'
    }
});
  • tabs.js
Vue.component('tabs', {
    template: '\
    <div class="tabs"> \
        <div class="tabs-bar">\
           <div class="tabs-bar"> \
                <div :class="tabCls(item)" v-for="(item, index) in navList"  @click="handleChange(index)"> \
                    {{ item.label }} \
                </div> \
            </div> \
        </div> \
        <div class="tabs-content"> \
            <slot></slot> \
        </div> \
    </div>',
    props: {
        value: {
            type: [String, Number]
        }
    },
    data: function () {
        return {
            // 用于渲染 tabs的标题
            navList: [],
            // 因为不能修改 value,所以复制一份自己维护
            currentValue: this.value,
        }
    },
    methods: {
        getTabs() {
            // 通过遍历子组件,得到所有的pane组件
            return this.$children.filter(function (item) {
                return item.$options.name === 'pane';
            });
        },
        updateNav() {
            this.navList = [];
            // 设置对this的引用,在function 回调里,this指向的并不是Vue实例
            var _this = this;

            this.getTabs().forEach(function (pane, index) {
                _this.navList.push({
                    label: pane.label,
                    name: pane.name || index
                });
                // 如果没有给pane 设置 name,默认设置它的索引
                if (!pane.name) pane.name = index;
                // 设置当前选中的tab的索引,在后面介绍
                if (index === 0) {
                    if (!_this.currentValue) {
                        _this.currentValue = pane.name || index;
                    }
                }
            });

            this.updateStatus();
        },
        updateStatus() {
            var tabs = this.getTabs();
            var _this = this;
            // 显示当前选中的tab对应的pane组件,隐藏没有选中的
            tabs.forEach(function (tab) {
                return tab.show = tab.name === _this.currentValue;
            })
        },
        tabCls: function (item) {
            return [
                'tabs-tab',
                {
                    //给当前选中的tab 加一个class
                    'tabs-tab-active': item.name === this.currentValue
                }
            ]
        },
        // 点击 tab 标题时触发
        handleChange: function (index) {
            var nav = this.navList[index];
            var name = nav.name;
            // 改变当前选中的tab ,并触发下面的watch
            this.currentValue = name;
            // 更新value
            this.$emit('input', name);
            // 触发一个自定义事件,供父级使用
            this.$emit('on-click', name);
        }
    },
    watch: {
        value: function (val) {
            this.currentValue = val;
        },
        currentValue: function () {
            //在当前选中的tab 发生变化时,更新pane的显示状态
            this.updateStatus();
        }
    }

})
  • pane.js
Vue.component('pane', {
    name: 'pane',
    template: '\
        <div class="pane" v-show="show"> \
            <slot></slot> \
        </div>',
    props: {
        name: {
            type: String
        },
        label: {
            type: String,
            default: ''
        }
    },
    data: function () {
        return {
            show: true
        }
    }
    , methods: {
        updateNav() {
            this.$parent.updateNav();
        }
    },
    watch: {
        label() {
            this.updateNav();
        }
    },
    mounted() {
        this.updateNav();
    }
});
  • style.css
[v-cloak] {
    display: none;
}
.tabs{
    font-size: 14px;
    color: #657180;
}
.tabs-bar:after{
    content: '';
    display: block;
    width: 100%;
    height: 1px;
    background: #d7dde4;
    margin-top: -1px;
}
.tabs-tab{
    display: inline-block;
    padding: 4px 16px;
    margin-right: 6px;
    background: #fff;
    border: 1px solid #d7dde4;
    cursor: pointer;
    position: relative;
}
.tabs-tab-active{
    color: #3399ff;
    border-top: 1px solid #3399ff;
    border-bottom: 1px solid #fff;
}
.tabs-tab-active:before{
    content: '';
    display: block;
    height: 1px;
    background: #3399ff;
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
}
.tabs-content{
    padding: 8px 0;
}
posted @ 2020-07-06 21:42  easy5  阅读(212)  评论(0编辑  收藏  举报