来自 Web前端 2020-04-29 17:43 的文章
当前位置: 网上澳门金莎娱乐 > Web前端 > 正文

一个案例搞懂 Vue.js 的作用域插槽

时间: 2019-09-09阅读: 139标签: 插槽

图片 1

作用域插槽是 Vue.js 中一个很有用的特性,可以显著提高组件的通用性和可复用性。问题在于,它实在不太好理解。尝试搞清楚父子作用域之间错综复杂的关系,其痛苦程度不亚于求解一个棘手的数学方程。

Vue

当你无法理解一个东西的时候,最好的办法就是在解决问题的过程中体会它的应用。本文将向你展示如何使用作用域插槽构建一个可复用的列表组件。

(一)安装Vue的方法

1、直接下载并用 <script> 标签引入

2、CDN引入:https://cdn.jsdelivr.net/npm/vue

最基础的组件

(二)模板语法

我们即将构建的组件叫做my-list,用来展示一系列的项目。它的特别之处就在于,你可以在每次使用组件的时候自定义列表项目的渲染方式。

插值

1、文本

Message: {{ msg }}

通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上所有的数据绑定:

这个将不会改变: {{ msg }}

2、原始HTML

<div v-html="rawHtml"></div>

3、特性
Mustache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令

<div v-bind:id="dynamicId"></div>

4、使用JavaScript表达式

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div v-bind:id="'list-' + id"></div>

每个绑定都只能包含单个表达式

我们先从最简单的单个列表开始:一个包含几何图形名字和边数的数组。

指令

指令 (Directives) 是带有 v- 前缀的特殊属性。指令属性的值预期是单个 JavaScript 表达式 (v-for 是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。

<p v-if="seen">现在你看到我了</p>

1、参数

<a v-bind:href="url"></a>

在这里 href 是参数,告知 v-bind 指令将该元素的 href 属性与表达式 url 的值绑定。

另一个例子是 v-on 指令,它用于监听 DOM 事件:

<a v-on:click="doSomething">

2、修饰符
修饰符 (Modifiers) 是以半角句号 . 指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault():

<form v-on:submit.prevent="onSubmit"></form>

app.js

缩写

1、v-bind缩写

<!-- 完整语法 -->
<a v-bind:href="url"></a>
<!-- 缩写 -->
<a :href="url"></a>

2、v-on缩写

<!-- 完整语法 -->
<a v-on:click="doSomething"></a>
<!-- 缩写 -->
<a @click="doSomething"></a>
Vue.component('my-list', { template: '#my-list', data() { return { title: 'Shapes', shapes: [ { name: 'Square', sides: 4 }, { name: 'Hexagon', sides: 6 }, { name: 'Triangle', sides: 3 } ] }; }});new Vue({ el: '#app'});

(三)计算属性和观察者

index.html

计算属性

1、基础例子

<div id="example">
  <p>Original message: "{{ message }}"</p>
  <p>Computed reversed message: "{{ reversedMessage }}"</p>
</div>

var vm = new Vue({
  el: '#example',
  data: {
    message: 'Hello'
  },
  computed: {
    // a computed getter
    reversedMessage: function () {
      // `this` points to the vm instance
      return this.message.split('').reverse().join('')
    }
  }
})

这里我们声明了一个计算属性 reversedMessage。我们提供的函数将用作属性 vm.reversedMessage 的 getter 函数:

console.log(vm.reversedMessage) // => 'olleH'
vm.message = 'Goodbye'
console.log(vm.reversedMessage) // => 'eybdooG'

2、计算属性的缓存vs方法
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:

<p>Reversed message: "{{ reversedMessage() }}"</p>

// in component
methods: {
  reversedMessage: function () {
    return this.message.split('').reverse().join('')
  }
}

对于最终的结果,两种方式确实是相同的。然而,不同的是计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要 message 还没有发生改变,多次访问 reversedMessage 计算属性会立即返回之前的计算结果,而不必再次执行函数。

这也同样意味着下面的计算属性将不再更新,因为 Date.now() 不是响应式依赖:

computed: {
  now: function () {
    return Date.now()
  }
}

3、计算属性vs被观察的属性
通常更好的想法是使用计算属性而不是命令式的 watch 回调。细想一下这个例子:

<div id="demo">{{ fullName }}</div>

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar',
    fullName: 'Foo Bar'
  },
  watch: {
    firstName: function (val) {
      this.fullName = val + ' ' + this.lastName
    },
    lastName: function (val) {
      this.fullName = this.firstName + ' ' + val
    }
  }
})

上面代码是命令式的和重复的。将它与计算属性的版本进行比较:

var vm = new Vue({
  el: '#demo',
  data: {
    firstName: 'Foo',
    lastName: 'Bar'
  },
  computed: {
    fullName: function () {
      return this.firstName + ' ' + this.lastName
    }
  }
})

清爽多了...

4、计算属性的setter

// ...
computed: {
  fullName: {
    // getter
    get: function () {
      return this.firstName + ' ' + this.lastName
    },
    // setter
    set: function (newValue) {
      var names = newValue.split(' ')
      this.firstName = names[0]
      this.lastName = names[names.length - 1]
    }
  }
}
// ...

5、观察者

<div id="watch-example">
  <p>
    Ask a yes/no question:
    <input v-model="question">
  </p>
  <p>{{ answer }}</p>
</div>

<!-- Since there is already a rich ecosystem of ajax libraries    -->
<!-- and collections of general-purpose utility methods, Vue core -->
<!-- is able to remain small by not reinventing them. This also   -->
<!-- gives you the freedom to just use what you're familiar with. -->
<script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lodash@4.13.1/lodash.min.js"></script>
<script>
var watchExampleVM = new Vue({
  el: '#watch-example',
  data: {
    question: '',
    answer: 'I cannot give you an answer until you ask a question!'
  },
  watch: {
    // 如果 question 发生改变,这个函数就会运行
    question: function (newQuestion) {
      this.answer = 'Waiting for you to stop typing...'
      this.getAnswer()
    }
  },
  methods: {
    // _.debounce 是一个通过 lodash 限制操作频率的函数。
    // 在这个例子中,我们希望限制访问 yesno.wtf/api 的频率
    // ajax 请求直到用户输入完毕才会发出
    // 学习更多关于 _.debounce function (and its cousin
    // _.throttle),参考:https://lodash.com/docs#debounce
    getAnswer: _.debounce(
      function () {
        if (this.question.indexOf('?') === -1) {
          this.answer = 'Questions usually contain a question mark. ;-)'
          return
        }
        this.answer = 'Thinking...'
        var vm = this
        axios.get('https://yesno.wtf/api')
          .then(function (response) {
            vm.answer = _.capitalize(response.data.answer)
          })
          .catch(function (error) {
            vm.answer = 'Error! Could not reach the API. ' + error
          })
      },
      // 这是我们为用户停止输入等待的毫秒数
      500
    )
  }
})
</script>

在这个示例中,使用 watch 选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这是计算属性无法做到的。

div  my-list/my-list/divscript type="text/x-template"  div  div {{ title }}/div div  div v-for="shape in shapes" div{{ shape.name }} small({{ shape.sides }} sides)/small/div /div /div /div/script

(四)Class与Style绑定

1、绑定HTML Class

<div v-bind:class="{ active: isActive }"></div>

<div class="static"
     v-bind:class="{ active: isActive, 'text-danger': hasError }">
</div>

data: {
  isActive: true,
  hasError: false
}

<div v-bind:class="classObject"></div>

data: {
  classObject: {
    active: true,
    'text-danger': false
  }
}

<div v-bind:class="classObject"></div>

data: {
  isActive: true,
  error: null
},
computed: {
  classObject: function () {
    return {
      active: this.isActive && !this.error,
      'text-danger': this.error && this.error.type === 'fatal'
    }
  }
}

<div v-bind:class="[activeClass, errorClass]"></div>

data: {
  activeClass: 'active',
  errorClass: 'text-danger'
}

<div v-bind:class="[{ active: isActive }, errorClass]"></div>

2、绑定内联样式

<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
  activeColor: 'red',
  fontSize: 30
}

<div v-bind:style="styleObject"></div>

data: {
  styleObject: {
    color: 'red',
    fontSize: '13px'
  }
}

<div v-bind:style="[baseStyles, overridingStyles]"></div>

数组语法可以将多个样式对象应用到同一个元素上。

从 2.3.0 起你可以为 style 绑定中的属性提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:

<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>

在加上一点样式,大概就会是下图这个样子:

(五)条件渲染

<h1 v-if="ok">Yes</h1>
<h1 v-else>No</h1>

1、在<template>中配合v-if条件渲染一整组

<template v-if="ok">
  <h1>Title</h1>
  <p>Paragraph 1</p>
  <p>Paragraph 2</p>
</template>

2、v-else

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>

3、2.1.0后的版本新增v-else-if

<div v-if="type === 'A'">
  A
</div>
<div v-else-if="type === 'B'">
  B
</div>
<div v-else-if="type === 'C'">
  C
</div>
<div v-else>
  Not A/B/C
</div>

4、用key管理可复用的元素

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address">
</template>

那么在上面的代码中切换 loginType 将不会清除用户已经输入的内容。因为两个模板使用了相同的元素,<input> 不会被替换掉——仅仅是替换了它的 placeholder。如图:

图片 2

key.gif

如果不想复用只需添加一个具有唯一值的 key 属性即可:

<template v-if="loginType === 'username'">
  <label>Username</label>
  <input placeholder="Enter your username" key="username-input">
</template>
<template v-else>
  <label>Email</label>
  <input placeholder="Enter your email address" key="email-input">
</template>

更通用的my-list

(六)列表渲染

1、用v-for把一个数组对应为一组元素

<ul id="example-1">
  <li v-for="item in items">
    {{ item.message }}
  </li>
</ul>

var example1 = new Vue({
  el: '#example-1',
  data: {
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

图片 3

输出结果

在 v-for 块中,我们拥有对父作用域属性的完全访问权限。v-for 还支持一个可选的第二个参数为当前项的索引。

<ul id="example-2">
  <li v-for="(item, index) in items">
    {{ parentMessage }} - {{ index }} - {{ item.message }}
  </li>
</ul>

var example2 = new Vue({
  el: '#example-2',
  data: {
    parentMessage: 'Parent',
    items: [
      { message: 'Foo' },
      { message: 'Bar' }
    ]
  }
})

你也可以用 of 替代 in 作为分隔符,因为它是最接近 JavaScript 迭代器的语法:

<div v-for="item of items"></div>

2、对象的v-for

<div v-for="(value, key, index) in object">
  {{ index }}. {{ key }}: {{ value }}
</div>

new Vue({
  el: '#v-for-object',
  data: {
    object: {
      firstName: 'John',
      lastName: 'Doe',
      age: 30
    }
  }
})

现在我们想要让my-list更加通用,可以渲染任何类型的列表。这次我们展示的是一堆颜色的名字以及对应的颜色方块。

数组更新监测

1、变异方法 (mutation method),顾名思义,会改变被这些方法调用的原始数组。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()
    Vue 包含一组观察数组的变异方法,所以它们也将会触发视图更新。

对于非变异的数组方法例如:filter(), concat() 和 slice() 。这些不会改变原始数组,但总是返回一个新数组。当使用非变异方法时,可以用新数组替换旧数组:

example1.items = example1.items.filter(function (item) {
  return item.message.match(/Foo/)
})

可以用替换来触发数组监测。

2、由于 JavaScript 的限制,Vue 不能检测以下变动的数组:

  • 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

第一类问题使用下面方案来进行替换
以下两种方式都可以实现和 vm.items[indexOfItem] = newValue 相同的效果,同时也将触发状态更新:

// Vue.set
Vue.set(example1.items, indexOfItem, newValue)

// Array.prototype.splice
example1.items.splice(indexOfItem, 1, newValue)

为了解决第二类问题,你可以使用 splice:

example1.items.splice(newLength)

为此,我们需要将上例列表独有的数据进行抽象化。由于列表中的项目可能有不同的结构,我们将会给my-list一个插槽,让父组件来定义列表的展示方式。

对象更改监测

对于已经创建的实例,Vue 不能动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, key, value) 方法向嵌套对象添加响应式属性。

var vm = new Vue({
  data: {
    userProfile: {
      name: 'Anika'
    }
  }
})

Vue.set(vm.userProfile, 'age', 27)

有时你可能需要为已有对象赋予多个新属性,比如使用 Object.assign()

this.userProfile = Object.assign({}, this.userProfile, {
  age: 27,
  favoriteColor: 'Vue Green'
})

app.js

显示过滤/排序结果

我们想要显示一个数组的过滤或排序副本,而不实际改变或重置原始数据。

<li v-for="n in evenNumbers">{{ n }}</li>

data: {
  numbers: [ 1, 2, 3, 4, 5 ]
},
computed: {
  evenNumbers: function () {
    return this.numbers.filter(function (number) {
      return number % 2 === 0
    })
  }
}
Vue.component('my-list', { template: '#my-list', props: [ 'title' ]});
整数的v-for
<div>
  {{ n }} 
</div>

index.html

v-for on a<template>
<ul>
  <template v-for="item in items">
    <li>{{ item.msg }}</li>
    <li class="divider"></li>
  </template>
</ul>
script type="text/x-template"  div  div {{ title }}/div div  slot/slot /div /div/script

v-for with v-if

当它们处于同一节点,v-for 的优先级比 v-if 更高,这意味着 v-if 将分别重复运行于每个 v-for 循环中。

<li v-for="todo in todos" v-if="!todo.isComplete">
  {{ todo }}
</li>

现在,我们在根实例中创建my-list组件的两个实例,分别展示两个测试用例列表:lists:

(七)事件处理

app.js

监听事件
<div id="example-1">
  <button v-on:click="counter += 1">增加 1</button>
  <p>这个按钮被点击了 {{ counter }} 次。</p>
</div>

var example1 = new Vue({
  el: '#example-1',
  data: {
    counter: 0
  }
})

图片 4

事件.gif

new Vue({ el: '#app', data: { shapes: [ { name: 'Square', sides: 4 }, { name: 'Hexagon', sides: 6 }, { name: 'Triangle', sides: 3 } ], colors: [ { name: 'Yellow', hex: '#F4D03F', }, { name: 'Green', hex: '#229954' }, { name: 'Purple', hex: '#9B59B6' } ] }});

div  my-list :title="Shapes" div v-for="item in shapes" div{{ shape.name }} small({{ shape.sides }} sides)/small/div /div /my-list my-list :title="Colors" div v-for="color in colors" div div :/div {{ color.name }} /div /div /my-list/div
方法时间处理器
<div id="example-2">
  <!-- `greet` 是在下面定义的方法名 -->
  <button v-on:click="greet">Greet</button>
</div>

var example2 = new Vue({
  el: '#example-2',
  data: {
    name: 'Vue.js'
  },
  // 在 `methods` 对象中定义方法
  methods: {
    greet: function (event) {
      // `this` 在方法里指当前 Vue 实例
      alert('Hello ' + this.name + '!')
      // `event` 是原生 DOM 事件
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
})
// 也可以用 JavaScript 直接调用方法
example2.greet() // => 'Hello Vue.js!'

效果如下图:

内联
<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>

new Vue({
  el: '#example-3',
  methods: {
    say: function (message) {
      alert(message)
    }
  }
})

大材小用的组件

事件修饰符

Vue.js 为 v-on 提供了事件修饰符。通过由点 (.) 表示的指令后缀来调用修饰符。

  • stop
  • prevent
  • capture
  • self
  • once
<!-- 阻止单击事件冒泡 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件侦听器时使用事件捕获模式 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当事件在该元素本身 (比如不是子元素) 触发时触发回调 -->
<div v-on:click.self="doThat">...</div>

2.1.4新增

<!-- 点击事件将只会触发一次 -->
<a v-on:click.once="doThis"></a>

我们刚才创建的组件确实符合要求,但那段代码算不上很好。my-list本来应该是一个展示列表的组件,但我们却把渲染列表需要的逻辑部分抽象到了父组件中,这样一来,子组件在这里只不过是用来包裹列表而已,未免显得大材小用了。

键值修饰符
<!-- 同上 -->
<input v-on:keyup.enter="submit">
<!-- 缩写语法 -->
<input @keyup.enter="submit">

全部的按键别名:

  • .enter
  • .tab
  • .delete(捕获“删除”和“退格”键)
  • .esc
  • .space
  • .up
  • .down
  • .left
  • .right

更糟糕的是,在两个组件的声明中存在着大量重复代码(例如,div v-for="item in ...")。如果我们能够在子组件中编写这些代码,那么子组件就不再是“打酱油的角色”了。

修饰键
  • .ctrl
  • .alt
  • .shift
  • .meta
<!-- Alt + C -->
<input @keyup.alt.67="clear">
<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

作用域插槽

鼠标按钮修饰符
  • .left
  • .right
  • .middle

本文由网上澳门金莎娱乐发布于Web前端,转载请注明出处:一个案例搞懂 Vue.js 的作用域插槽

关键词: