VUE进阶知识-01

412 阅读4分钟

组件通信常用方式

  • props

  • event

  • vuex

边界情况

  • $parent

  • $root

  • $children

  • $refs

  • provide/inject

⾮prop特性

  • $attrs

  • $listeners

parent/parent/root

兄弟组件之间通信可通过共同祖辈搭桥,parentparent或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访问),那么就会被他们所绑定,通过vbind="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本身是个对象(可以使用von展开),键值对的形式,键是父组件中所有事件监听器的名称,在这儿,父组件Children2上有个click事件,那么在子组件Children2中,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地址