• VUE进阶知识


    组件通信常用方式

    • props

    • event

    • vuex

    边界情况

    • $parent

    • $root

    • $children

    • $refs

    • provide/inject

    ⾮prop特性

    • $attrs

    • $listeners

    $parent/$root

    兄弟组件之间通信可通过共同祖辈搭桥,$parent或$root。

    			// 兄弟组件1
                sayBai() {
                    // this.$parent.$emit('handle', '我是老大');
                    this.$root.$emit('handle', '我是老大');
                }
                
            // 兄弟组件2
             // this.$parent.$on('handle', message => {
                this.$root.$on('handle', message => {
                    console.log(message);
                })
    

    但是这方式,存在耦合过高问题,因为一旦组件层级发生了改变,那么就会有问题,特别是$parent。

    例如当我们自己封装组件时(自己封装表单),有嵌套关系,很多人会直接梭哈使用$parent,但是如果后面重构层级关系变了,那么很多逻辑都会改,导致很大麻烦。那么官方是怎么做的呢?

    element源码地址

    我们来查看下element中怎么做的呢:

    // 广播: 从上到下派发事件
    function broadcast(componentName, eventName, params) {
    	// componentName: 组件的componentName名
    	// eventName:事件名
    	// params: 参数,需要是一个数组
    	
    	// 遍历所有的子组件:树形的向下遍历,只要名字相同,就都派发事件
      this.$children.forEach(child => {
        var name = child.$options.componentName;
    	// 如果子组件的componentName和传入的componentName名字相同,就派发事件
    	// 需要注意: 组件需要写componentName(和我们在组件中写的name相似)
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params));
        } else {
          broadcast.apply(child, [componentName, eventName].concat([params]));
        }
      });
    }
    export default {
      methods: {
      	// 从下到上派发事件(类似于冒泡)
        dispatch(componentName, eventName, params) {
          var parent = this.$parent || this.$root;
          var name = parent.$options.componentName;
    		
    	  // 向上查找,直到找到componentName和传入的componentName名字相同的组件
          while (parent && (!name || name !== componentName)) {
            parent = parent.$parent;
    
            if (parent) {
              name = parent.$options.componentName;
            }
          }
          // 如果找到,就派发事件
          if (parent) {
            parent.$emit.apply(parent, [eventName].concat(params));
          }
        },
        broadcast(componentName, eventName, params) {
          broadcast.call(this, componentName, eventName, params);
        }
      }
    };
    

    $children

    ⽗组件可以通过$children访问⼦组件实现⽗⼦通信。

    	// 在子组件2中,存在一个状态msg
    	
    	// 父组件中
     	changeChildren1Msg() {
           this.$children[1].msg='变';
        }
    

    注意:

    • 组件中,$children只能访问到自定义组件。
    • $children访问到的是一个数组,并且不能保证⼦元素顺序,例如:如果异步加载组件,那么顺序就会发生变化。

    $attrs

    当父组件传递数据到子组件时,如果没有在props中声明(声明了的就不能通过$attrs访问),那么就会被他们所绑定,通过 v-bind="$attrs" 传⼊到子组件,这在我们创建⾼级别的组件时⾮常有⽤。

     	// 父组件中
        <Children2 name='pyy' />
        
        // 子组件2中
        <span>子组件2: {{msg}} - {{$attrs.name}}</span>
    

    $listeners

    例如当我们封装组件时,在组件中有个回调函数,但是这个回调函数设置是在父组件中设置的,在这个组件中只是负责触发它,不负责相应的实现逻辑,这个时候,我们就可以使用到$listeners。

    	// 在父组件中
    	<Children2 name='pyy' @click="onClickHandle" />
    
    	onClickHandle(){
          console.log('父组件中: onClickHandle');
          this.$children[2].msg='变变变';
        }
    
    	// 在子组件中
    	<span v-on="$listeners">子组件2: {{msg}} - {{$attrs.name}}</span>
    

    v-on="$listeners"解析: $listeners本身是个对象(可以使用v-on展开),键值对的形式,键是父组件中所有事件监听器的名称,在这儿,父组件Children2上有个click事件,那么在子组件Children2中,$listeners中有个键就叫click,值就是父组件中设置的回调函数,也就是说,在子组件Children2中,这个span标签上有一个click事件=父组件中设置的回调函数。这样在子组件中不需要关心这个回调函数的处理,只需要绑定并触发它,在封装组件库时比较常用。

    $refs

    这是我们常用的获取⼦节点引⽤。

    父组件中:

     <Children2 ref="CR2" />
     
      changeChildren1Msg() {
           this.$children[1].msg='变'; // 只会找到第二个子组件修改它状态
           this.$refs.CR2.msg = '变2';
        },
    

    provide/inject

    provide/inject能够实现祖先和后代之间传值,当我们不使用vuex时,vue提供给了我们这种原生接口的方式来实现隔代传值。

    例如:

     // 在app.vue中:
     provide(){ // 提供的意思
        // 隔代传参,用法类似于data
        return {
          foo: 'foo',
        }
      },
    
    // 在需要的后代组件中
    inject: ['foo'], // 注入,注入需要的属性
    

    注意:

    • 如果传递的是基本数据类型,那么这种方式,不是响应式的。只有是引用数据类型-对象,并且这个对象是响应式的,那么传递下去的时候,才会是响应式的。

    • 如果在子组件data中,已经声明了相同的属性,那么子组件中的属性才会生效(就近原则)。

    • 如果在子组件data中,已经声明了相同的属性,那么怎么使用provide提供的属性呢?这个时候我们需要改造inject

      inject: {
      	 foo1: 'foo' // 使用别名foo1
      }
      

    代码

    // app.vue中
    <template>
      <div id="app">
        <Father />
      </div>
    </template>
    
    <script>
    import Father from './components/Father.vue'
    
    export default {
      provide(){ // 提供的意思
        // 隔代传参,用法类似于data
        return {
          foo: 'foo',
        }
      },
      name: 'App',
      components: {
        Father
      }
    }
    </script>
    
    // Father.vue中
    <template>
      <div>
        <span>父组件</span>
        <Children1 />
        <button @click="changeChildren1Msg">我是父组件按钮</button>
        <Children2 name='pyy' @click="onClickHandle" />
        <Children2 ref="CR2" />
      </div>
    </template>
    
    <script>
    import Children1 from "./Children1";
    import Children2 from "./Children2";
    export default {
      methods: {
        changeChildren1Msg() {
           this.$children[1].msg='变'; // 只会找到第二个子组件修改它状态
           this.$refs.CR2.msg = '变2';
        },
        onClickHandle(){
          console.log('父组件中: onClickHandle');
          this.$children[2].msg='变变变';
        }
      },
      components: {
        Children1,
        Children2
      }
    };
    </script>
    
    // Children1.vue中
    <template>
        <div>
            <button @click="sayBai">子组件1</button>
        </div>
    </template>
    
    <script>
        export default {
            methods: {
                sayBai() {
                    // this.$parent.$emit('handle', '我是老大');
                    this.$root.$emit('handle', '我是老大');
                }
            },
             mounted () {
                // this.$parent.$on('handle', message => {
                this.$root.$on('handle', message => {
                    console.log(message);
                })
             },
        }
    </script>
    
    // Children2.vue中
    <template>
        <div>
            <!-- $listeners/$attrs -->
            <span v-on="$listeners">子组件2: {{msg}} - {{$attrs.name}}</span>
            <!-- provide/inject -->
            <span>------{{foo}} </span>
        </div>
    </template>
    
    <script>
        export default {
            // inject: ['foo'], // 注入,注入需要的属性
            inject: {
                foo: 'foo'
            },
            data() {
                return {
                    msg: 'msg',
                }
            },
            // 监听事件
             mounted () {
                // this.$parent.$on('handle', message => {
                this.$root.$on('handle', message => {
                    console.log(message);
                })
             },
        }
    </script>
    

    过滤器filters

    作用: 过滤处理数据的格式,可被用于一些常见的文本格式化。

    使用场景:过滤器可以用在两个地方:双花括号插值和v-bind表达式,注意过滤器要被添加在表达式的尾部,由“管道”符号|表示。

    语法:

    		<!-- 在双花括号中 -->
    		<div>{{ msg | 函数名 }}</div>
    		
    		<!-- 在 `v-bind` 中 --> 
    		<div v-bind:id="msg | 函数名"></div>
    
    
     	// 过滤器
        filters: {
            函数名(msg) {
                return 过滤结果
            }
        }
    

    例如:

    <div id="app">
          <ul>
            <li v-for='item of goodList'>
            	<!-- 不使用过滤器时这么写,但是货币符号是固定的 -->
              <!-- {{item.name}} -  ${{item.price}} -->
              {{item.name}} -  {{item.price | symbol}}
            </li>
          </ul>
        </div>
    
        <script src="vue.js"></script>
        <script>
        
        new Vue({
          el: '#app',
          data() {
            return {
              goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
            }
          },
          filters: {
            symbol: function(value) {
              return '$' + value;
            }
          }
        });
    

    把上面的例子稍微改造一下,符号可以动态传递而不是写死的。

    		<div id="app">
          <ul>
            <li v-for='item of goodList'>
              <!-- 和方法一样调用,传递参数 -->
              {{item.name}} - {{item.price | symbol('¥')}}
            </li>
          </ul>
        </div>
    
        <script src="vue.js"></script>
        <script>
        
        new Vue({
          el: '#app',
          data() {
            return {
              goodList: [{name: '花生', price: 10},{name: '瓜子', price: 40},{name: '啤酒', price: 90}],
            }
          },
          filters: {
            symbol: function(value, sym = '$') { // 第一个参数,理解为上面的item.price  第二个参数,就是('¥')中传递过来的符号,  为了容错处理 默认值给个$
              return sym + value;
            }
          }
        });
        </script>
    

    自定义指令

    除了核心功能默认内置的指令 ,Vue 允许注册自定义指令。有的情况下,仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。

    官方地址

    例如官方输入框自动获取焦点例子:

    		<div id="app">
          <input type="text" v-focus>
        </div>
    
        <script src="vue.js"></script>
        <script>
          // 注册一个全局自定义指令 `v-focus`
          Vue.directive('focus', {
            // 当被绑定的元素插入到 DOM 中时……
            // binding很重要,详细信息参看官网
            inserted: function (el, binding) {
              // 聚焦元素
              el.focus()
            }
          });
    
          new Vue({
            el: '#app',
          });
        </script>
    

    然后我们再来自定义做一个,根据当前登陆用户级别,来做权限设置。

    		<div id="app">
          <input type="text" v-focus>
    
          <!-- 特别需要注意: 指令里,""中是表达式,如果需要传递字符串,则需要加上字符串 -->
          <button v-permission="'superAdmin'">删除</button>
    
        </div>
    
        <script src="vue.js"></script>
        <script>
          // 假设当前登陆用户是会员
          const user = 'member';
    
          // 注册一个全局自定义指令 `v-focus`
          Vue.directive('focus', {
            // 当被绑定的元素插入到 DOM 中时……
            // binding很重要,详细信息参看官网
            inserted: function (el, binding) {
              // 聚焦元素
              el.focus()
            }
          });
          
          // 第一个参数: 指令名,注意使用时要加上v-
          // 第二个参数: 配置项
          Vue.directive('permission', {
            inserted: function (el, binding) {
              console.log(binding);
               // 若指定用户角色和当前用户角色不匹配则删除当前指令绑定的元素
              if (user !== binding.value) {
                el.parentElement.removeChild(el)
              }
            }
          });
    
          new Vue({
            el: '#app',
          });
        </script>
    

    渲染函数

    官方地址

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

    基础:

    render: function (createElement) { 
    	// createElement函数返回结果是VNode(虚拟DOM) 
    	return createElement( // 接收三个参数
        tagname, // 标签名称 
        data, // 传递数据 
        children // 子节点数组
    	) 
    }
    

    基于官网的例子:

    		<div id="app">
          <!-- 用render实现一个组件 : 实现标题 -->
          <!-- level是指需要生成h1-6哪一个标签 -->
          <my-head :level='1' :title='title'>{{title}}</my-head>
          <my-head :level='3' :title='title'>我是另一个我</my-head>
    
          <!-- <h2 :title='title'>
            {{title}}
          </h2> -->
        </div>
    
        <script src="vue.js"></script>
        <script>
          Vue.component('my-head',{
            props: ['level', 'title'],        
            // render函数接收一个 createElement参数,我们一般简写为h    h === createElement
            // 因为Vdom底层的算法是snabbdom算法,这个算法里面生成虚拟dom的方法名就叫h
            render(h){ 
              // 注意这儿一定要有return, return出createElement返回的Vnode。
             return h(
                'h'+this.level, // 参数1:标签名字
                {attr:  { title: this.title }},// 参数2
                this.$slots.default, // 参数3: 子节点数组(虚拟节点)   标签之间的内容,需要使用默认插槽来获取
              )
            }
          });
    
          new Vue({
              el: '#app', 
              data() { 
                return {
                  title: 'hello, vue!'
                }
              },
          });
        </script>
    

    然后我们再来进阶来试一试:

    当用户使用组件时,

    <my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
    

    我们希望渲染成:

    			<!-- 阿里矢量图使用方式 -->
    			<h1 :title='title'>
              <svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
            {{title}}
          </h1>
    

    最终代码为:

    		<div id="app">
          <my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
    
          <!-- <h3 :title='title'>
              <svg class="icon"><use xlink:href="#icon-iconfinder_Food_C_"></use></svg>
            {{title}}
          </h3> -->
        </div>
    
        <script src="./iconfont.js"></script>
        <script src="vue.js"></script>
        <script>
          Vue.component('my-head',{
            props: ['level', 'title', 'icon'],        
            render(h){ 
              let children = [];           
              // 思路: 第一步,把用户传入的icon,生成<svg class="icon"><use xlink:href="#icon-icon名"></use></svg>添加到children数组中
              // 第二步: 把默认插槽内容this.$slots.default放到children数组中
              // 第三步:h函数参数3就替换为children数组
              // 第一步: 生成svg,添加图标   同样是调用h函数生成
              const svgVnode = h(
                'svg',
                { class: 'icon' }, // 添加固定类名为icon  详见官网createElement参数2
                [h('use',{attrs: {"xlink:href": `#icon-iconfinder_${this.icon}_C_`}})] // 参数3: 子节点数组(虚拟节点),svg还有个子级use,所以再调用h方法生成use,需要注意的是需要是数组,所以将返回的vnode放到一个数组中
              );
              children = [svgVnode, ...this.$slots.default];
    
             return h(
                'h'+this.level, // 参数1:标签名字
                {attrs:  { title: this.title }},// 参数2
                children, // 参数3: 子节点数组(虚拟节点)   标签之间的内容,需要使用默认插槽来获取
              )
            }
          });
    
          new Vue({
              el: '#app', 
              data() { 
                return {
                  title: 'hello, vue!'
                }
              },
          });
        </script>
    

    模板语法是如何实现的

    在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

    之前的例子中原本代码如下:

     <!-- 宿主容器(根节点) -->
      <div id="app">
        <ul>
          <!-- class绑定 --> 
          <li v-for="item in goodList" 
            :class="{active: (selected === item)}" 
            @click="selected = item">{{item}}</li> 
            <!-- style绑定 --> 
            <!-- <li v-for="item in goodList" 
                  :style="{backgroundColor: (selected === item)?'#ddd':'transparent'}" 								@click="selectedCourse = item">{{item}}</li> --> 
        </ul>
      </div>
      
      
      <script src="vueJs所在路径"></script>
        <script>
         const vm = new Vue({
              el: '#app', 
              data() {
                return {
                  goodList: ['花生','瓜子','啤酒'],
                  selected: ''
                }
              },
          });
        </script>
    

    然后我们去输出vue替我们生成的渲染函数 :

    执行代码: console.log(vm.$options.render)

    我们看到输出信息:

    (function anonymous(
    ) {
    with(this){return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
    })
    

    然后我们基于这一个点,改写为渲染函数版本。

     <!-- 宿主容器(根节点) -->
      <div id="app"></div>
      
      
      // 创建vue实例
      new Vue({
          el: '#app',
          data() {
            return {
              goodList: ['花生','瓜子','啤酒'],
              selected: ''
            }
          },
          methods: {},
          render() {
            with(this){
              return _c('div',{attrs:{"id":"app"}},[_c('ul',_l((goodList),function(item){return _c('li',{class:{active: (selected === item)},on:{"click":function($event){selected = item}}},[_v(_s(item))])}),0)])}
          }
        })
    

    我们可以看到,结果是一样的。

    结论:Vue通过它的编译器将模板编译成渲染函数,在数据发生变化的时候再次执行渲染函数,通过对比两次执行结果得出要做的dom操作,模板中的神奇魔法得以实现。

    函数式组件

    官方地址

    没有管理任何状态,也没有监听任何传递给它的状态,也没有生命周期方法。实际上,它只是一个接受一些 prop 的函数。在这样的场景下,我们可以将组件标记为 functional,这意味它无状态 (没有响应式数据),也没有实例 (没有 this 上下文)。

    修改上一个例子为函数式组件:

    		<div id="app">
          <my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
        </div>
    
        <script src="./iconfont.js"></script>
        <script src="vue.js"></script>
        <script>
          Vue.component('my-head',{
            functional: true,  // 1. functional设置为true,标示是函数式组件
            props: ['level', 'title', 'icon'],   
            // 在函数式组件中,没有this
            // 所以render函数,提供第二个参数作为上下文             
            render(h, context){ 
              // 之前从this上拿取'level', 'title', 'icon',就要变化了
              // 2. 从context.props上去拿取
              const { level, title, icon } = context.props;
              let children = [];           
              const svgVnode = h(
                'svg',
                { class: 'icon' },
                [h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})] 
              );
              // 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
              children = [svgVnode, ...context.children];
    
             return h(
                'h'+level, 
                {attrs:  { title: title }},
                children,
              )
            }
          });
    
          new Vue({
              el: '#app', 
              data() { 
                return {
                  title: 'hello, vue!'
                }
              },
          });
        </script>
    

    混入

    官方地址

    混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。

    // 定义一个混入对象 
    let myMixin = { 
    	created: function () { 
    		this.hello() 
    	},
    	methods: { 
    		hello: function () { 
    			console.log('hello from mixin!') 
    		} 
    	} 
    }
    // 定义一个使用混入对象的组件 
    Vue.component('mycomponent', { mixins: [myMixin] })
    

    插件

    官方地址

    Vue.js 的插件应该暴露一个 install 方法。这个方法的第一个参数是 Vue 构造器,第二个参数是一

    个可选的选项对象.

    const MyPlugin = { 
    	install (Vue, options) { 
    		Vue.component('my-head', {...}) 
    	} 
    }
    if (typeof window !== 'undefined' && window.Vue) { 
    	window.Vue.use(MyPlugin) 
    }
    

    例如把上面的标题组件,封装成插件。

    首先新建个js文件,存放插件代码:

    const MyPlugin = { 
      // 插件需要install方法
    	install (Vue, options) { 
    		Vue.component('my-head',{
          functional: true,  // 1. functional设置为true,标示是函数式组件
          props: ['level', 'title', 'icon'],   
          // 在函数式组件中,没有this
          // 所以render函数,提供第二个参数作为上下文             
          render(h, context){ 
            // 之前从this上拿取'level', 'title', 'icon',就要变化了
            // 2. 从context.props上去拿取
            const { level, title, icon } = context.props;
            let children = [];           
            const svgVnode = h(
              'svg',
              { class: 'icon' },
              [h('use',{attrs: {"xlink:href": `#icon-iconfinder_${icon}_C_`}})] 
            );
            // 3. 子元素获取: 增加context参数,并将this.$slots.default更新为context.children,然后将this.level更新为context.props.level。
            children = [svgVnode, ...context.children];
    
           return h(
              'h'+level, 
              {attrs:  { title: title }},
              children,
            )
          }
        });
    	} 
    }
    // 判断当前环境  并且判断是否已经存在Vue
    if (typeof window !== 'undefined' && window.Vue) { 
    	window.Vue.use(MyPlugin);
    }
    

    然后在页面上,直接使用插件即可。

     		<div id="app">
          <my-head :level='1' :title='title' icon='Food'>{{title}}</my-head>
        </div>
    
        <script src="./iconfont.js"></script>
        <script src="vue.js"></script>
        <script src="./plugins/head.js"></script>
        <script>   
          new Vue({
              el: '#app', 
              data() { 
                return {
                  title: 'hello, vue!'
                }
              },
          });
        </script>
    

    代码github地址

  • 相关阅读:
    史上最全的网银转账测试分析与设计
    【面试题】你是测试工程师,如何保证软件的质量?
    小白成长建议--小白如何提问
    [感悟]性能测试测什么
    通过一个简单的数据库操作类了解PHP链式操作的实现
    PHP魔术方法小结.md
    谈PHP中信息加密技术
    PHP输入流php://input [转]
    【PHPsocket编程专题(实战篇③)】构建基于socket的HTTP请求类
    从一次面试经历谈PHP的普通传值与引用传值以及unset
  • 原文地址:https://www.cnblogs.com/zz-zrr/p/14468678.html
Copyright © 2020-2023  润新知