Part3-指令与传值

一、Vue指令(了解)

1、深入响应式原理

Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。Vue里面是怎么做到的的呢?其实就是使用了Object.defineProperty 把Vue内的属性全部转成 getter/setterObject.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

Object.defineProperty 实现了对象劫持这个功能

https://github.com/vuejs/vue/blob/1.0/src/observer/index.js

https://github.com/vuejs/vue/blob/2.6/src/core/observer/index.js

语法:

Object.defineProperty(obj, prop, desc)

  1. obj 需要定义属性的当前对象

  2. prop 当前需要定义的属性名

  3. desc 属性描述符

数据属性:

通过Object.defineProperty()为对象定义属性,有两种形式,分别为数据描述符,存取描述符,下面分别描述两者的区别:

  1. value 表示它的默认值

  2. writable 如果为true标识可以被修改,如果为false标识不能被修改(默认值为false)

  3. configurable 描述属性是否配置,以及可否删除,可以认为是总开关 默认值 false(不可删除)

  4. enumerable 描述属性是否出现在for in 或者 Object.keys()的遍历中 默认值false(不能遍历)

let obj = {};
Object.defineProperty(obj, 'name', {
    value: '张三'
})
obj.name = '李四'
console.log(obj.name) // 张三
let obj = {};
Object.defineProperty(obj, 'name', {
    value: '张三',
    writable: true
})
obj.name = '李四'
console.log(obj.name)
let obj = {};
  Object.defineProperty(obj, 'name', {
    value: '张三',
    writable: true,
    configurable: true,
    enumerable: true
  })
  obj.name = '李四'
  // delete obj.name
  console.log(obj.name) // 李四
  console.log(Object.keys(obj)) // ['name']

存取属性:

let obj = {};
let temp = null;
Object.defineProperty(obj, 'name', {
    get() {
        return temp
    },
    set(val) {
        temp = val
    }
})
obj.name = '李四'
console.log(obj.name)

面试题回答:

vue的双向数据绑定原理是什么?

vue数据双向绑定是通过数据劫持结合“发布者-订阅者模式”的方式来实现的。 vue是通过Object.defineProperty()来实现数据劫持,其中会有getter()和setter方法;当读取属性值时,就会触发getter()方法,在view中如果数据发生了变化,就会通过Object.defineProperty()对属性设置一个setter函数,当数据改变了就会来触发这个函数;

参考:https://segmentfault.com/a/1190000014274840

参考:https://zhuanlan.zhihu.com/p/51357583

2、自定义指令

除了一些内置的制定(v-model和v-show...),Vue也允许注册自定义指令。

// 注册一个全局自定义指令 v-focus
Vue.directive('demo', {
	inserted: function (el, binding, vnode) {
		// 
		console.log(el, binding, vnode);
	}
})
// 组件中注册局部指令
new Vue({
	el: '#app',
	data: {},
	directives: {
		demo: {
			inserted: function (el, binding, vnode) {
				cosnole.log(el, binding, vnode);
			}
		}
	}
})
// 在模板中使用自定义指令
<div v-demo>
    
</div>

钩子函数:

  • bind : 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted :被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。

  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新 (详细的钩子函数参数见下)。

  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。

  • unbind:只调用一次,指令与元素解绑时调用。

接下来我们来看一下钩子函数的参数 (即 elbindingvnodeoldVnode)。

钩子函数的参数:

  • el:指令所绑定的元素,可以用来直接操作 DOM 。

  • binding:一个对象,包含以下属性:

    • name:指令名,不包括 v- 前缀。

    • value:指令的绑定值,例如:v-my-directive="1 + 1" 中,绑定值为 2

    • oldValue:指令绑定的前一个值,仅在 updatecomponentUpdated 钩子中可用。无论值是否改变都可用。

    • expression:字符串形式的指令表达式。例如 v-my-directive="1 + 1" 中,表达式为 "1 + 1"

    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }

  • vnode:Vue 编译生成的虚拟节点。

  • oldVnode:上一个虚拟节点,仅在 updatecomponentUpdated 钩子中可用。

实现类似v-show的自定义指令

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app">
    <p v-demo="status">12</p>
    <button @click="status = !status">取反</button>
  </div>
</body>
<script>
const vm = new Vue({
  el: '#app',
  data: {
    status: true
  },
  directives: {
    demo: {
      inserted(el, binding, vnode) {
        console.log(el, binding)
        if (binding.value) {
          el.style.display = 'block'
        } else {
          el.style.display = 'none'
        }
      },
      update(el, binding, vnode, oldVnode){
        console.log(el, binding)
        if (binding.value) {
          el.style.display = 'block'
        } else {
          el.style.display = 'none'
        }
      }
    }
  }
})
</script>
</html>

二、Vue组件化开发(掌握)

1、什么是组件化?

面对复杂问题的处理方式,把问题拆解成很多个能处理的小问题,再将其放在整体中,会发现大的问题也会迎刃而解。

而组件化的思想也类似:

  1. 如果我们实现一个页面结构和逻辑非常复杂的页面时,如果全部一起实现会变得非常复杂,而且也不利于后续的维护和迭代功能。

  2. 但如果我们这时候把页面分成一个个小的功能块,每个功能块能完成属于自己这部分独立的功能,那么整个页面之后的维护和迭代也会变得非常容易。

2、Vue组件化思想

组件化是Vue重要的思想

  1. 它提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用。

  2. 任何的应用都会被抽象成一颗组件树。

组件化思想的应用开发:

  1. 有了组件化的思想,我们在之后的开发中就要充分的利用它。

  2. 尽可能的将页面拆分成一个个小的、可复用的组件。

  3. 这样让我们的代码更加方便组织和管理,并且扩展性也更强。

3、全局组件

通过Vue.component('组件名称', {}),通过这个方法注册的都是全局组件,也就是他们再注册之后可以用在任何新创建的Vue 实例挂载的区域内。

<body>
  <div id="app">
    <my-con></my-con>
    <div>
      <my-con></my-con>
    </div>
  </div>
  <my-con></my-con>
</body>
<script>
  Vue.component('my-con', {
    template: '<section><h3>组件标题</h3><p>组件内容</p></section>'
  })
  const vm = new Vue({
    el: '#app'
  })
</script>

上面案例中,在<div id="app">...</div> 外的组件 my-con 没有替换成组件真正的页面结构,是因为 new Vue() 挂载在 id=app 的节点上,不在这个节点上标签,不会受到Vue的影响。

4、局部组件

通过 Vue.component 方式注册的组件,称之为全局组件。任何地方都可以使用。全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。

注册局部组件

<body>
  <div id="app">
    <my-con></my-con>
    <div>
      <my-con></my-con>
    </div>
  </div>
  <div id="app1">
    <my-con1></my-con1>
  </div>
</body>
<template id="template1">
  <section>
    <h3>组件标题</h3>
    <p>组件内容</p>
  </section>
</template>
<template id="template2">
  <section>
    <h3>组件标题B</h3>
    <p>组件内容B</p>
  </section>
</template>
<script>
  var componentA = {
    template: '#template1'
  }

  var componentB = {
    template: '#template2'
  }
  const vm = new Vue({
    el: '#app',
    components: {
      'my-con': componentA
    }
  })
  const vm1 = new Vue({
    el: '#app1',
    components: {
      'my-con1': componentB
    }
  })
</script>

父组件和子组件

组件和组件之间存在层级关系,而其中一种最重要的关系就是父子组件关系。

5、组件可以访问Vue实例数据吗?

那组件如果要使用data定义自己属性保存数据要怎么做呢?

  1. 组件对象也有一个data的属性(也有methods等属性,下面我们有用到)

  2. 只是这个data属性必须是一个函数,而且函数返回一个对象 ,对象保存着数据

<body>
  <div id="app">
    <my-con></my-con>
    <div>
      <my-con></my-con>
    </div>
  </div>
  <div id="app1">
    <my-con1></my-con1>
  </div>
</body>
<template id="template1">
  <section>
    <h3>{{title}}</h3>
    <p>组件内容</p>
  </section>
</template>
<template id="template2">
  <section>
    <h3>{{title}}B</h3>
    <p>组件内容B</p>
    <aa></aa>
  </section>
</template>
<script>
  var componentA = {
    template: '#template1',
    data() {
      return {
        title: 'zujianbiaoti'
      }
    }
  }

  var componentB = {
    template: '#template2',
    data() {
      return {
        title: 'zj'
      }
    },
    components: {
      'aa': {
        template: '<div>aa</div>'
      }
    }
  }
  const vm = new Vue({
    el: '#app',
    data: {title: '组件标题'},
    components: {
      'my-con': componentA
    }
  })
  const vm1 = new Vue({
    el: '#app1',
    components: {
      'my-con1': componentB
    }
  })
</script>

为什么data在组件中必须是一个函数呢?

原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。

6、父子组件间的通讯

父级向子级传递

在组件中,使用选项props来声明需要从父级接收到的数据。

props的值有两种方式:

  1. 字符串数组,数组中的字符串就是传递时的名称。

  2. 对象,对象可以设置传递时的类型(String,Number,Boolean,Array, Object,Date,Function,Symbol),也可以设置默认值等。

<body>
  <div id="app1">
    <my-con1></my-con1>
  </div>
</body>

<template id="template2">
  <section>
    <h3>{{title}}B</h3>
    <p>组件内容B</p>
    <!-- my-con1组件内的aa组件 --> 
    <aa v-bind:parent-txt="childtxt"></aa>
  </section>
</template>
<script>
  var componentB = {
    template: '#template2',
    data() {
      return {
        title: 'zj',
        childtxt: 'child text'
      }
    },
    components: {
      'aa': {
        template: '<div>{{parentTxt}}</div>',
        props: ['parentTxt']
      }
    }
  }
  
  const vm1 = new Vue({
    el: '#app1',
    components: {
      'my-con1': componentB
    }
  })
</script>

子级向父级传递

父组件向子组件传递数据,通过自定义事件

<body>
  <div id="app1">
    <my-con1></my-con1>
  </div>
</body>

<template id="template2">
  <section>
    <h3>{{title}}B</h3>
    <p>组件内容B</p>
    <aa v-bind:parent-txt="childtxt" v-on:changetitle="changeTitle"></aa>
  </section>
</template>
<script>
  var componentB = {
    template: '#template2',
    data() {
      return {
        title: 'zj',
        childtxt: 'child text'
      }
    },
    components: {
      'aa': {
        template: '<div v-on:click="change">{{parentTxt}}</div>',
        props: ['parentTxt'],
        methods: {
          change() {
            this.$emit('changetitle', {
              a: 1
            })
          }
        }
      }
    },
    methods: {
      changeTitle(obj) {
        console.log(obj)
        this.title = obj.a
      }
    }
  }

  const vm1 = new Vue({
    el: '#app1',
    components: {
      'my-con1': componentB
    }
  })
</script>

案例分析:

  1. 现在的需求是点击子组件aa 然后把父组件my-con1上的标题给改变;

  2. 首先,在父组件的具体页面结构找到子组件aa ,在子组件aa 上使用v-on:changetitle="changeTitle" , changetitle是子组件的自定义事件名称,changeTitle是父组件my-con1里的methods属性定义的方法;

  3. 其次,在子组件aa里为div绑定点击事件 v-on:click="change", 在子组件aa里的methods定义change方法,change方法里使用this.$emit('changetitle'),使用$emit方法来触发绑定在子组件上的自定义事件,$emit第一个参数就是上一步定义的自定义事件changetitle,第二个参数就是传递到父组件的参数,可以不传。

弹窗例子:

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    * {
      margin: 0;
      padding: 0;
    }
    html,body, #app1{
      width: 100%;
      height: 100%;
    }
    
    .wrapper {
      display: flex;
      justify-content: center;
      align-items: center;
      width: 100%;
      height: 100%;
      background-color: rgba(0,0,0, 0.3);
    }
    .content {
      width: 220px;
      height: 160px;
      background-color: #fff;
    }
    .title {
      height: 40px;
      line-height: 40px;
      text-align: center;
    }
    .msg {
      display: flex;
      align-items: center;
      justify-content: center;
      box-sizing: border-box;
      padding: 5px 10px;
      border-top: 1px solid #eee;
      border-bottom: 1px solid #eee;
      height: 80px;
      text-align: center;
      color: gray;
      font-size: 14px;
    }
    .bottom {
      display: flex;
      height: 40px;
      line-height: 40px;
      text-align: center;
    }
    .bottom div {
      flex: 1;
      color: green;
    }
    .bottom div:nth-of-type(1) {
      border-right: 1px solid #eee;
      color: red;
    }
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app1">
    <my-con v-show="showCon" :txt-obj="textObj" @btn-handle="handleBtnClick"></my-con>
  </div>
</body>

<template id="template1">
    <div class="wrapper">
        <div class="content">
          <p class="title">{{txtObj.title}}</p>
          <div class="msg">{{txtObj.msg}}</div>
          <div class="bottom">
            <div @click="cancel">{{txtObj.cancelTxt}}</div>
            <div @click="submit">{{txtObj.submitTxt}}</div>
          </div>
        </div>
      </div>
</template>
<script>
  var componentA = {
    template: '#template1',
    props: {
      txtObj: {
        type: Object,
        default: {}
      }
    },
    methods: {
      cancel() {
        // 自定义事件名称建议全小写
        this.$emit('btn-handle', 'cancel')
      },
      submit() {
        // 自定义事件名称建议全小写
        this.$emit('btn-handle', 'submit')
      },
    }
  }
  const vm1 = new Vue({
    el: '#app1',
    data: {
      textObj: {
        title: 'bug提示',
        msg: '亲,你还有53633个bug,是否要处理?',
        cancelTxt: '忽略,下班',
        submitTxt: '加班处理'
      },
      showCon: true
    },
    components: {
      myCon: componentA
    },
    methods: {
      handleBtnClick(type) {
        console.log(type)
        if (type === 'cancel') {
          this.showCon = false
        }
      }
    }
  })
</script>

</html>

非父子组件通讯

实际工作中如果遇到跨组件或者非父组件间的传递数据,那该怎么办?第一个可以使用中央事件总线,也就是一个中介来完成。第二个就是使用 Vuex 提供的功能,这里先暂时不讨论这种方案,后续专门学习Vuex。

**案例:**点击按钮1,改变按钮2的背景颜色

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>

<body>
  <div id="app1">
    <my-con></my-con>
    <my-con1></my-con1>
  </div>
</body>

<template id="template1">
  <button @click="click1">按钮1</button>
</template>
<template id="template2">
  <button @click="click2" :style="{backgroundColor: fontColor}">按钮2</button>
</template>
<script>
  const bus = new Vue();
  const componentA = {
    template: '#template1',
    methods: {
      click1() {
        // 点击按钮1
        bus.$emit('xxx', this.getRandomColor());
      },
      getRandomColor() {
        return `rgb(${this.getRandomNum()},${this.getRandomNum()},${this.getRandomNum()})`
      },
      getRandomNum() {
        return Math.floor(Math.random() * 256)
      }
    }
  }
  const componentB = {
    template: '#template2',
    data() {
      return {
        fontColor: ''
      }
    },
    methods: {
      click2() {
        // 点击按钮2
      }
    },
    mounted() {
      bus.$on('xxx', (color) => {
        console.log(color)
        this.fontColor = color
      })
    }
  }
  const vm1 = new Vue({
    el: '#app1',
    components: {
      myCon: componentA,
      myCon1: componentB
    }
  })
</script>

</html>

7、编译作用域(掌握)

父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。

也就是说父组件上的数据,只能在父组件上修改,不能传递到子组件里修改。

三、插槽(slot)

1. 为什么使用slot

slot翻译为插槽,组件的插槽:

  1. 组件的插槽也是为了让我们封装的组件更加具有扩展性。

  2. 让使用者可以决定组件内容的一些内容到底展示什么。

京东头部导航栏例子:

2. 如何在组件中使用slot呢?

如何去封装这类的组件呢?

  1. 它们也很多区别,但是也有很多共性。

  2. 如果,我们每一个单独去封装一个组件,显然不合适:比如每个页面都返回,这部分内容我们就要重复去封装。

  3. 但是,如果我们封装成一个,好像也不合理:有些左侧是菜单,有些是返回,有些中间是搜索,有些是文字,等等

如何封装合适呢?抽取共性,保留不同

  1. 最好的封装方式就是将共性抽取到组件中,将不同暴露为插槽。

  2. 一旦我们预留了插槽,就可以让使用者根据自己的需求,决定插槽中插入什么内容。

  3. 是搜索框,还是文字,还是菜单。由调用者自己来决定。

3. slot的基本使用(匿名插槽)

了解了为什么用slot,我们再来谈谈如何使用slot?

  1. 在子组件中,使用特殊的元素<slot>就可以为子组件开启一个插槽。

  2. 该插槽插入什么内容取决于父组件如何使用。

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app1">
    <my-con></my-con>
    <my-con>
      <h2>我是h2标签的内容</h2>
      <p>我是p标签的内容</p>
    </my-con>
  </div>
</body>

<template id="template1">
    <div>
      <slot>我是插槽中的默认内容!!</slot>
    </div>
</template>
<script>
  var componentA = {
    template: '#template1',
  }
  const vm1 = new Vue({
    el: '#app1',
    data: {
      
    },
    components: {
      myCon: componentA
    }
  })
</script>

</html>

4. 具名插槽

当子组件的功能复杂时,子组件的插槽可能并非是一个。

  1. 比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。

  2. 那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢?

  3. 这个时候,我们就需要给插槽起一个名字

如何使用具名插槽呢?

  1. 非常简单,只要给slot元素一个name属性即可

  2. <slot name='myslot'></slot>

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
  <style>
    
  </style>
  <script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
  <div id="app1">
    <my-con>
      <div slot="left">左侧</div>
      <div slot="right">右侧</div>
      <div slot="center">中间</div>
    </my-con>
  </div>
</body>

<template id="template1">
    <div>
      <slot name="left">我是左侧插槽中的默认内容!!</slot>
      <slot name="center">我是中间侧插槽中的默认内容!!</slot>
      <slot name="right">我是右侧插槽中的默认内容!!</slot>
    </div>
</template>
<script>
  var componentA = {
    template: '#template1',
  }
  const vm1 = new Vue({
    el: '#app1',
    data: {
      
    },
    components: {
      myCon: componentA
    }
  })
</script>

</html>

5、课堂练习

京东导航栏

6. 作用域插槽

默认情况下,父组件使用子组件,插槽数据默认是拿父组件的数据,而不是子组件拿数据。

作用域插槽在父组件使用我们的子组件时, 插槽的数据从子组件中拿到数据,而不是从父组件拿到。

7. 作用域插槽的多种写法

// 1、基本写法
<one-comp>
  <button slot="btn" slot-scope="scope">按钮{{scope.msg}}</button>
</one-comp>

// 2、基本写法之模板写法
<one-comp>
  <template slot="btn" slot-scope="scope">
    <button>按钮{{scope.msg}}</button>
  </template>
</one-comp>

// 3、指令写法
<one-comp v-slot:btn="scope">
  <button>按钮{{scope.msg}}</button>
</one-comp>

// 4、指令写法之模板写法
<one-comp>
  <template v-slot:btn="scope">
    <button>按钮{{scope.msg}}</button>
  </template>
</one-comp>

四、作业

作业一:

完成百度首页(https://baidu.com):

作业要求:页面刷新输入框自动聚焦

作业二:

点击登录按钮,打开登录弹框:

作业要求:登录弹框必须是组件话实现,而且点击x按钮,可以关闭登录弹框

最后更新于