美文网首页
vue-03-计算属性和监听和组件

vue-03-计算属性和监听和组件

作者: 未来在奋斗 | 来源:发表于2019-12-15 11:12 被阅读0次

    watch 监听(重点)

    <div id="app">
        <input v-model="a" /> + <input v-model="b" /> = <span>{{c}}</span>
    </div>
    <script>    
    var app = new Vue({    
        el: '#app',
        data: { a:1, b:2, c:3 },
        watch: {    
            a(val){ this.c = Number(val) + Number(this.b); },
            b(val){ this.c = Number(this.a) + Number(val); }
        }
    })    
    </script>
    

    computed 计算属性(重点)

    <div id="app">
        <input v-model="a" /> + <input v-model="b" /> = <span>{{c}}</span>
    </div>
    <script>    
    var app = new Vue({    
        el: '#app',
        data: { a:1, b:2 },
        computed: { 
            c:function(){
                return Number(this.a)+Number(this.b);
            }
        }
    })    
    </script>
    

    计算属性缓存

    注意:视图层不建议使用methods函数,该需求应该用计算属性代替。

    <div id="app">
        {{str}}
        <input type="text" v-model="a" />
        +
        <input type="text" v-model="b" />
        =
        {{c}} <br> {{d()}}
    </div>
    
    <script>
    var vm = new Vue({
        el:"#app",
        data(){
            return {
                str : "加法运算",
                a : 1,
                b : 2
            }
        },
        computed:{  // 推荐用计算属性 (因为计算属性有缓存)
            c(){
                console.log('c 执行了');
                return Number(this.a) + Number(this.b);
            }
        },
        methods:{   // 不推荐用方法
            d(){
                console.log('d 执行了');
                return Number(this.a) + Number(this.b);
            },
            e(){
                console.log('e 执行了')
            }
        }
    })
    // 执行 vm.str="abc" 时,这段代码与视图层的d()没有任何关系,但是d()被执行了,
    // 意味着只要视图发生变化,视图层的方法都会被执行
    </script>
    

    setter和getter

    注意:不建议使用setter和getter。

    {{ c }}
    

    在视图层上渲染计算属性c,本身就会走一个c的get。

    var vm = new Vue({
        el:"#app",
        data(){
            return {
                a : 1,
                b : 2
            }
        },
        computed:{
            c : {
                get(){ // c被获取时执行的代码
                    console.log('get');
                    return this.a+this.b;
                },
                set(v){ // c被设置时执行的代码
                    console.log('set:', v)
                }
            }
        }
    })
    
    // 设置c
    vm.c = 6;   // 自动执行 set()
    
    // 获取c
    vm.c;       // 首次自动触发 get() 
    // 如果再次获取 vm.c 时,如果 c 没有变化,因为计算属性缓存问题,get()函数是不会被触发的。
    
    // 没有直接设置c,而是设置依赖的属性a时,c发生变化,重新获取,然后渲染到视图层
    vm.a = 123; // 自动触发c的 get()
    

    计算属性参数

    注意:不建议在计算属性中使用参数,该需求应该用filter过滤代替。

    <div id="app">
        {{a(5)}}
    </div>
    
    <script>
    var vm = new Vue({
        el:"#app",
        data(){
            return {
                x : 2
            }
        },
        computed:{
            a(){
                return function(n){
                    return n + this.x;
                };
            }
        }
    })
    </script>
    

    mixins 指混入

    mixins 指混入,其实就是把对象合并了。

    <div id="app">
        <input v-model="a">+
        <input v-model="b">=
        {{c}}
    </div>
    <script>
        var obj1 = {
            data : {
                a : 1,
                b : 2
            }
        }
    
        var obj2 = {
            computed:{
                c(){
                    return this.a + this.b;
                }
            }
        }
    
        var app = new Vue({
            el : '#app',
            mixins : [obj1, obj2]
        });
    </script>
    

    虚拟DOM和DIFF算法

    虚拟DOM(Virtual dom),也就是我们常说的虚拟节点,它是通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM的节点。

    // 创建虚拟节点
    var temp = document.createDocumentFragment();
    
    for( var i=0; i<100; i++ ){
        var li = document.createElement('li');
        // 将li放入虚拟节点中,这一步操作不会产生重绘回流
        temp.appendChild(li);
        li.innerHTML = i;
    }
    
    // 真实节点的操作
    ul1.appendChild(temp);
    

    为什么要使用虚拟节点?

    频繁的操作DOM,会大量的造成页面的重绘和回流,出于性能优化的考虑,我们应该减少重绘和回流的操作。

    重绘:例如 div1.style.color='red' 这种代码,只能改变颜色,并不会影响其他元素的布局,这种操作被称为重绘。

    回流:例如 div1.style.padding = '20px' 这种代码,会影响到其他元素的布局,这种操作被称为回流。

    回流必将引起重绘,而重绘不一定会引起回流。比如:只有颜色改变的时候就只会发生重绘而不会引起回流。

    对虚拟节点的DOM操作,并不会触发重绘和回流,把处理后的虚拟节点映射到真实DOM上,只触发一次重绘和回流。

    DIFF算法

    DIFF算法是DOM更新的一种算法,指页面被更新时,程序用哪种策略去做更新DOM。

    渲染函数 createElement()

    <div id="app">
        <abc></abc>
    </div>
    <script>
        Vue.component('abc', {
            render: function (createElement, context) {
                // createElement() 的返回值就是一个VNode(虚拟节点)
                return createElement('div', {style:{color:'green'}}, '你好')
            }
        })
    
        var app = new Vue({
            el: '#app'
        })
    </script>
    

    createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,及其子节点。我们把这样的节点描述为“虚拟节点 (Virtual Node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

    createElement() 参数1为标签名,参数2是一个对象,参数3是子节点。

    createElement() 第二个参数的内容:

    {
      // 和`v-bind:class`一样的 API
      // 接收一个字符串、对象或字符串和对象组成的数组
      'class': {
        foo: true,
        bar: false
      },
      // 和`v-bind:style`一样的 API
      // 接收一个字符串、对象或对象组成的数组
      style: {
        color: 'red',
        fontSize: '14px'
      },
      // 普通的 HTML 特性
      attrs: {
        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
      },
      // 自定义指令。注意,你无法对 `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
    }
    

    例如:

    Vue.component('my-component', {
      render: function (h, context) {
        return h("h1", {
            "style":"color:red", 
            domProps:{
                innerHTML : "你好",
                title : "hello"
            }
        }, ["abc"])
      }
    })
    

    component 组件(重点)

    一个项目特别大,所有的功能都写在一起是不合适的,所以把功能抽象出来,形成一个个组件,最后把这些组件组合在一起。

    注意:

    • template 只能有1个子节点
    • data 属性必须用函数形式描述

    全局组件

    Vue.component('my-component-name', {
        template:'<div>abc</div>'
    })
    
    <my-component-name> </my-component-name>
    

    局部组件

    <div id="app"><hi></hi></div>
    <script>
    var app = new Vue({
        el: '#app',
        components: {
            'hi': {"template": "<h1>hi</h1>"}
        }
    })
    </script>
    

    prop 属性

    使用组件时,把数据从组件的外面,传入到组件的内部,组件内使用props接收。

    <btn v-bind:a="4"></btn>
    props:['a']
    

    注意:prop需要小写

    单向数据流

    所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:

    父级 prop 的更新会向下流动到子组件中,但是反过来则不行。

    这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。

    prop 验证

    使用script标签引入vue.js文件这种方法测试props验证是无效的,需要用webpack开发环境测试。

    props: {
        propA: Number, // 基础的类型检查 (`null` 匹配任何类型)     
        propB: [String, Number], // 多个可能的类型    
        propC: {type: String, required: true}, // 必填的字符串 ,属性名称必须存在   
        propD: {type: Number, default: 100}, // 带有默认值的数字  , 值为undefined时,默认值才生效
        // 带有默认值的对象    
        propE: {type: Object, default: function () {
            return { message: 'hello' }
        }},    
        // 自定义验证函数    
        propF: {validator: function (value) {
            return ['success', 'warning', 'danger'].indexOf(value) !== -1
        }}
    }
    

    子页面向父页面传递数据

    子组件

    const component2 = {
        template: `
            <div>component2 <button @click="fn">按钮</button> </div>
        `,
        methods:{
            fn(){
                this.$emit("abc", 2)
            }
        }
    }
    

    父组件

    const component1 = {
        template: `
            <div>
                component1 
                <component2 @abc="fn2"></component2>
            </div>
        `,
        methods:{
            fn2(num){ alert(num) }
        },
        components: { component2 }
    }
    

    组件边界

    访问组件

    • $root 指跟
    • $parent 指父
    • $children 指子
    • $refs 当前页面中查找
    <input ref="x"/>
    <comp1 ref="y" />
    
    this.$refs 能够找到当前页面中的ref定义
    

    依赖注入

    解决的是复杂组件关系中,数据传递的问题。

    限制:provide 和 inject 必须在一条线路上。比如说 爷爷提供了方法,孙子使用方法,这是可以的;如果叔叔提供了方法,侄子使用方法,是不可以的。(必须是前代组件提供方法,后代组件使用方法,前代组件和后代组件之间跨多少层没关系)。

    • provide (提供给后代组件的方法)
    • inject(接收前代组件提供的方法)

    例如abc组件中含有xyz组件,abc组件provide提供一个方法,xyz组件inject可以直接使用这个方法

    Vue.component('abc', {
        template:`
            <div>1<xyz></xyz></div>
        `,
        provide(){ return { abc:this.fn } },
        methods:{ fn(){alert()} }
    })
    
    Vue.component('xyz', {
        template:`
            <div><button @click='abc'>2</button></div>
        `,
        inject:["abc"] 
    })      
    

    组件切换

    vue 提供了 component 标签,该组件可以通过 is 属性来显示不同的组件。

    <div id="app">
        <template>
            <button type="button" @click="fn('aaa')">AAA</button>
            <button type="button" @click="fn('bbb')">BBB</button>
            <component v-bind:is="view"></component>    
        </template>
    </div>
    <script>
    var app = new Vue({
        el: '#app', data: { view:"bbb" },
        methods: { fn(str){this.view=str} },
        components: {
            "aaa": {template:"<div>aaa</div>"},
            "bbb": {template:"<div>bbb</div>"},
            "ccc": {template:"<div>ccc</div>"}
        }
    });
    </script>
    

    keep-alive 缓存

    在动态组件上使用 keep-alive

    组件实例能够被在它们第一次被创建的时候缓存下来

    主要用于保留组件状态或避免重新渲染。

    keep-alive 的组件必须有名字

    <div id="app">
        <template>
            <button type="button" @click="fn('aaa')">AAA</button>
            <button type="button" @click="fn('bbb')">BBB</button>
            <keep-alive>
                <component v-bind:is="view"></component>
            </keep-alive>
        </template>
    </div>
    <script>
    var app = new Vue({
        el: '#app', data: { view:"bbb" },
        methods: { fn(str){this.view=str} },
        components: {
            "aaa": {template:"<div>aaa</div>", created(){console.log('aaa')}},
            "bbb": {template:"<div>bbb</div>", created(){console.log('bbb')}},
            "ccc": {template:"<div>ccc</div>", created(){console.log('ccc')}}
        }
    });
    </script>
    

    点击app.vue中的按钮,切换组件,观察created执行了几次。

    异步组件

    需要用到某个组件的时候,现去加载(按需加载)。

    指使用 ajax、fetch、axios等技术,发起请求,将组件对象获取到当前页面。

    ajax这类技术必须使用 http 或 https 协议访问,所以传统的 file 这种形式是无法使用 ajax 功能的,所以需要开启服务器。

    vscode 中,左侧侧边栏中有扩展按钮,点击扩展按钮,在输入框中输入 Live Server ,查找并安装。

    自定义文件描述组件 abc.txt

    ({
        "template" : `
            <div>
                组件1<br>
                <input v-model="a" />
                <input v-model="b" />
                {{c}}
            </div>
        `,
        data(){
            return {
                a : 1,
                b : 2
            }
        },
        computed:{
            c(){
                return this.a + this.b
            }
        },
        created(){
            console.log('创建了组件1')
        }
    })
    

    点击按钮加载组件

    <div id="app">
        <button type="button" @click="fn">加载组件</button>
        <component v-bind:is="view"></component>    
    </div>
    <script>
    var app = new Vue({
        el: '#app', data: { view:"" },
        methods: {
            fn(){
                axios.get('./abc.txt').then(res=>{
                    var obj = eval(res.data);
                    this.view = obj;
                })
    
                // or
                fetch('./abc.txt').then(res=>res.text()).then(res=>{
                    var obj = eval(res);
                    this.view = obj;
                })            
            }
        }
    });
    </script>
    
    // 如果通过webpack捆绑单文件组件,可以使用 require 引入 
    // this.view = function(resolve){ require(["./abc.vue"], resolve); }
    
    // 如果是 vue.min.js 这种 script 标签引入,可以使用 fetch 获取一段组件数据
    this.view = {template:"<div>这个对象应该由ajax之类的方法异步获取</div>"}
    

    组件的按需加载和缓存

    在异步组件中,直接使用 keep-alive 并不能直接实现缓存效果,因为 keep-alive 需要通过组件名进行缓存读取。

    <keep-alive>
        <component :is="abc"></component>
    </keep-alive>
    

    程序中用一个对象保存加载过来的组件,每次使用的时候,先在该缓存对象中读取组件,如果不存在,再ajax请求组件。

    var app = new Vue({
        el : '#app',
        data:{
            abc : "",
            obj : {}
        },
        created(){
            this.tab('comp1', 0)
        },
        methods:{
            tab( n, ind ){
                // 判断 在 data.obj 中是否存在该组件
                // 比如n='comp1', this.obj['comp1']===undefined
                if( this.obj[n] == undefined ){ 
                    // 组件不存在时,发起请求获取组件,然后将组件保存到obj中                    
                    axios.get('./comps/'+n+'.txt').then(res=>{
                        var obj = eval(res.data);                           
                        this.obj[n] = this.abc = obj;
                    })
                }else{
                    // 组件存在时,直接调用(缓存机制)
                    this.abc = this.obj[n]; 
                }
            }
        }
    });
    

    函数化组件

    函数化组件也被称为无状态组件、无实例组件、影子组件,是一种性能优化方案,具有较小的渲染开销。

    有些组件仅描述了一些html、css相关的内容,他们是不需要js相关的能力的,所以,如果vue给这类组件提供js能力,相当于消耗了不必要消耗的性能,所以造成了资源浪费,所以vue提供functional属性,来决定当前组件是不是函数化组件。

    函数化组件不能使用template,没有生命周期,没有prop,没有data属性,没有this。

    <div id="app">
        <abc xyz="hello"></abc>
    </div>
    <script>
        Vue.component('abc', {
            functional:true,    // 为无状态组件后,不能写template只能写render
            props:['xyz'],
            render: function (createElement, context) {
                // createElement() 的返回值就是一个VNode(虚拟节点)
                return createElement('div', {style:{color:'green'}}, '你好'+this.xyz)
            },
            mounted(){
                // 无状态组件时,不会存在生命周期
                // 组件的生命周期,钩子函数
                console.log('页面渲染完毕了')
            }
        })
    
        var app = new Vue({
            el: '#app'
        })
    </script>
    

    .native 将原生事件绑定到组件

    父页面

    <Hello @click.native="fn"></Hello>
    

    如果没有.native,那么点击hello是无效的;有了.native,则表示在组件根元素上添加这个事件。

    methods:{
      fn(event){
          console.log(event.currentTarget)
      }
    }
    

    hello 子页面

    <div style="background:pink; padding:20px">          
        <input >
    </div>
    

    如果我们想把事件关联到其他元素上(非根元素)怎么办?

    父页面

    <Hello @click="fn"></Hello>
    
    fn(event){
      console.log(event.currentTarget)
    }
    

    子页面

    <div style="background:pink; padding:20px">          
      <input @click="fn" >
    </div>
    
    data() { return {} },
    props: [],
    created(){
        console.log(this.$listeners)  // 获取该组件上的所有事件及绑定函数
        this.fn = this.$listeners.click;
    }
    

    程序化的事件侦听器

    on 创建事件;emit 触发事件;off 移除事件;once 触发一次后自删除

    created(){
        //this.$once("aa", this.aaa)
        this.$on("aa", this.aaa)
    },
    methods:{
        aaa(){
            console.log(this.$parent.a++)
            console.log('aaa()被执行了')
        },
        fn(){
            this.$emit("aa")
        },
        fn2(){
            this.$off("aa", this.aaa)
        }
    }
    

    EventBus中央事件总线

    解决复杂组件关系中,数据维护的问题。

    以下为 webpack 管理的 vue 项目中,EventBus 的写法。

    eventbus.js

    import Vue from 'vue'
    const eventbus = new Vue();
    export default eventbus;
    

    main.js

    import eventbus from './eventbus.js'
    Vue.prototype.$eventbus = eventbus
    

    任意组件A(监听事件)

    mounted(){ this.$eventbus.$on("fnName", function(payload){ }) }
    

    任意组件B(触发事件)

    this.$eventbus.$emit("fnName", {a:2})
    

    相关文章

      网友评论

          本文标题:vue-03-计算属性和监听和组件

          本文链接:https://www.haomeiwen.com/subject/ntirnctx.html