Vue笔记系列(二)Vue.js渐进

方旭
Vue笔记系列
1、Vue.js入门
3、Vue.js进阶

API

以下会随用随记一些API,可能会不定期更新。

  • Vue.component( id, [definition] ) .

注册或获取全局组件。注册还会自动使用给定的id设置组件的名称。

// 注册组件,传入一个扩展过的构造器
Vue.component('my-component', Vue.extend({ /* ... */ }))
// 注册组件,传入一个选项对象(自动调用 Vue.extend)
Vue.component('my-component', { /* ... */ })
// 获取注册的组件(始终返回构造器)
var MyComponent = Vue.component('my-component')
  • Vue.extend(options) .

使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。注意:data 选项是特例,在 Vue.extend() 中它必须是函数。

<!-- HTML -->
<div id="mount-point"></div>
// 创建构造器
var Profile = Vue.extend({
      template: '<p v-text="name"></p>',
          data: function () {
            return {
                  name: '第一个构造器!'
            }
      }
})
// 创建 Profile 实例,并挂载到一个元素上(会替换#mount-pointer)。挂载的组件会把被挂载的元素替换掉。
new Profile().$mount('#mount-pointer');

结果如下:

<p>第一个构造器!</p>

如果挂载元素不想被替换掉,可以用以下方法:

var component = new Profile().$mount()
document.getElementById('mount-pointer').appendChild(component.$el)
  • Vue.set( object, key, value ) 设置对象的属性。

Vue 不能检测到对象属性的添加或删除。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化过程,所以属性必须在 data 对象上存在才能让 Vue 转换它,这样才能让它是响应的。Vue 不允许在已经创建的实例上动态添加新的根级响应式属性(所以,set方法的object参数也不能是 Vue 实例,或者 Vue 实例的根数据对象)。可以使用 Vue.set(object, key, value) 方法将响应属性添加到嵌套的对象
之前说过的v-for指令,当你利用索引直接设置一个项时,例如上文的example1.words[0] = {text: 'A'},如果想让视图更新,其中一种方法就是用set。

Vue.set(example1.items, 0, {text: 'A'})
  • Vue.nextTick( [callback, context] )  涉及到Vue的异步更新队列

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

<div id="example">{{message}}</div>
 
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

为毛第一次DOM里面的内容没有变,拿不到改变的内容,经过nextTick方法后才才能拿到改变的内容。
这是因为,当你设置 vm.someData = 'new value' ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。


全局配置

全局配置——Vue.config 是一个对象,包含 Vue 的全局配置。有以下属性:

  • silent    Vue.config.silent = true; 取消 Vue 所有的日志与警告,false时开启。
  • devtools  Vue.config.devtools = true; 配置是否允许 vue-devtools 检查代码。开发版本默认为 true,生产版本默认为 false。vue-devtools指的是一个浏览器插件,在谷歌应用里面有。

vue.devtools.png

安装之后,是在google 开发者工具的这里找到。
2.png


组件

一、使用组件

  • 注册

1、全局注册
注册一个全局组件,可以使用 Vue.component(tagName, options)
注意:对于自定义标签名,Vue.js 不强制要求遵循 W3C规则 (小写,并且包含一个短杠),但是建议这样写。
组件在注册之后,便可以在父实例的模块中以自定义元素的形式使用。谨记要确保在初始化根实例之前注册了组件。并且, el 和 data 选项必须是函数

<!-- HTML  -->
<div id="example">
  <my-component></my-component>
</div>
// 注册,这就是所谓的语法糖,因为下面的方法有点麻烦。
Vue.component('my-component', {
      template: '<div>我的第一个组件!</div>'
})
// 创建父实例
new Vue({
      el: '#example'
})

渲染为:

<div id="example">
      <div>我的第一个组件!</div>
</div>

2、构造器用作组件
可以使用 Vue.extend({...}) 创建一个组件构造器,extend 方法创建基础Vue构造器的子类,参数是一个对象,包含组件选项,这里要注意的特例是 el 和 data 选项,在 Vue.extend() 中,它们必须是函数注册组件的component方法也一样。这是因为,如果使用一个数据对象(是一个引用),那么所有的组件实例都共享这一个对象,这样就会牵一发而动全身。
有了这个构造器,我们既可以用全局注册的方式用 Vue.component(tag, constructor) 注册,也可以利用该构造器构建一个实例,然后用 Vue.$mount() 将该组件实例添加到DOM树上。

<!-- HTML  -->
<div id="example">
  <my-component></my-component>
</div>
// 创建构造器
var Profile = Vue.extend({
      template: '<p v-text="name"></p>',
          data: function () {
            return {
                  name: '第一个构造器组件!'
            }
      }
})
// 注册
Vue.component('my-component',Profile)
// 创建父实例
new Vue({
      el: '#example'
})

渲染为:

<div id="example">
      <p>第一个构造器组件!</p>
</div>

3、局部注册
通过使用组件实例选项注册,可以使组件仅在另一个实例/组件的作用域中可用。即在注册的对象参数中添加 components 成员,components成员的标签就只在该组件内使用,不在全局DOM树中使用局部注册的组件。

//实例作用域
var Child = {
  template: '<div>一个局部组件!</div>'
}
new Vue({
  // ...
  components: {
    // <my-component> 将只在父模板可用
    'my-component': Child
  }
})
 
//组件作用域
<div id="comp-ex">
      <contact></contact>
</div>
<script>
  var Person = Vue.extend({   // constructor
    template: "<div><span>name:</span> {{name}}  <span>Age: </span> {{age}}</div>",
    data: function() {
      return {
        name: ' li ming',
        age: 22
      }
    }
  });
  var Contact = Vue.extend({
    template: "<div><span>Tel: </span> {{tel}}, <span>E-mail: </span> {{email}}<person></person></div>",
    data: function() {
      return {
        tel: '152-0101-1010',
        email: 'admin#163.com'
      }
    },
    components: {
      'person': Person        // 局部注册 person 标签
    }
  })
  Vue.component('contact', Contact)
  var you = new Vue({        // init component, render template
    el: '#comp-ex'
  })
</script>

子组件只能在父组件的template中使用。注意下面两种子组件的使用方式是错误的:

  1. 以子标签的形式在父组件中使用
<div id="app">
    <parent-component>
        <child-component></child-component>
    </parent-component>
</div>

因为当子组件注册到父组件时,Vue.js会编译好父组件的模板,模板的内容已经决定了父组件将要渲染的HTML。
<parent-component>…</parent-component>相当于运行时,它的一些子标签只会被当作普通的HTML来执行,<child-component></child-component>不是标准的HTML标签,会被浏览器直接忽视掉。

  1. 在父组件标签外使用子组件
<div id="app">
    <parent-component>
    </parent-component>
    <child-component>
    </child-component>
</div>

运行这段代码,浏览器会提示以下错误:

在父组件标签外使用子组件报错

4、is特性
一些 HTML 元素,如<ul> <ol> <table> <select>,限制什么元素可以放在它里面。自定义元素不在白名单上,将被放在元素的外面,因而渲染不正确。这时应当使用 is 特性,指示它是一个自定义元素。

<table>
  <my-row>...</my-row>
</table>
<!-- 自定义组件 <my-row> 被认为是无效的内容,因此在渲染的时候会导致错误。需要使用特殊的 is 属性 -->
<table>
  <tr is="my-row"></tr>
</table>

二、组件通信

良好的流程: Vue.js 中,父子组件的关系可以总结为 props down, events up 。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。
父子组件通信图示

1、Prop显式声明

  • (1)、使用prop传递数据

组件实例的作用域是孤立的。这意味着不能并且不应该在子组件的模板内直接引用父组件的数据。可以使用 props 把数据传给子组件。prop 是父组件用来传递数据的一个自定义属性。子组件需要显式地用 props选项声明 “prop”:

读到这里,我们掌握的组件的构造选项对象的属性包括了:

  • template,要渲染的内容
  • data,数据,必须是一个函数,函数返回一个对象
  • props,从父组件传递数据到子组件。
<div id="comp-ex">
<!--  注意在HTML中属性要写kebab-case(短横线隔开) -->
  <child my-message='qiqihaobenben'></child>
  <child my-message='qiqihaobenben'></child>
</div>
<script>
  Vue.component('child', {
      // 就像 data 一样,prop 可以用在模板内
      template: '<span>{{message}}</span>',
      //声明 props
      // HTML特性不区分大小写,名字形式为 camelCase 的prop用作特性时,写在HTML中要用短横线隔开,否则不起作用,如上。
      props: ['myMessage'],  
      // 同样也可以在 vm 实例中像 “this.message” 这样使用
      data: function (){
          return {
              message: this.myMessage
          }
      }
  })
  var me = new Vue({
    el: '#comp-ex'
  })
<script>

输出结果为:

<div id="comp-ex">
     <span>qiqihaobenben qiqihaobenben</span>
</div>
  • (2)、命名风格

HTML特性不区分大小写,但是名字形式为 camelCase 的prop用作特性时,需要转为 kebab-case(短横线隔开)。在html中的属性使用短横线隔开,而在js的template中的标识使用的驼峰命名,可以参考上面的例子。

上面的例子使用节点属性方式向子组件传递数据,如果父组件的数据变化,子组件并不会随之变化,这就是其动态性,如果要绑定动态的值,应该使用 v-bind 指令,这样每当父组件的数据变化时,也会传递给子组件

<div>
  <input v-model="parentMsg">
  <br>
  <child v-bind:my-message="parentMsg"></child>
</div>

动态和非动态这两种传递方式在传递数字时是不同的,如下:

<!-- 传递了一个字符串"1" -->
<comp some-prop="1"></comp>
<!-- 传递实际的数字1 -->
<comp v-bind:some-prop="1"></comp>
虽然html渲染的时候并不太区分字符串和数字,但是注意有这种区别。
  • (4)、单项数据流

prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是不会反过来。这是为了防止子组件无意修改了父组件的状态——这会让应用的数据流难以理解。
注意:在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它会影响父组件的状态。

  • (5)、prop验证

本来不想说的,可是看文档时怎么用怎么不对,后来想通了,所以就拿出来说一下。
看官方文档可以知道,type 可以是下面原生构造器:

  • String
  • Number
  • Boolean
  • Function
  • Object
  • Array

其中的除了String,其他的类型都需要动态绑定才能验证正确,否则得到的就是一水的String类型。

//js属性验证
props: {
    myAge: {
            type: [Number,Boolean,Object,Array,Function]
            }
      }
<!--以下都会报错,Expected XXX, got String.-->
<my-component my-age="12"></my-component>
<my-component my-age="true"></my-component>
<my-component my-age="{}"></my-component>
<my-component my-age="[]"></my-component>
<my-component my-age="consoleOne"></my-component>
<!--正确的做法是用v-bind来绑定属性-->
<my-component v-bind:my-age="12"></my-component>
...
...

另外,default和required这两个验证规则,需要组件的属性完全不存在时才会生效

//设置默认值
props: {
        myName: {
            default: 2
        }
    }
//设置必传项
props: {
        myName: {
            required: true
        }
    }
<!--以下,不管是必传项或者默认值都不会有效果-->
<my-component my-name="" ></my-component>
<!--只有这个属性在组件上真的没有,才会触发验证效果-->
<my-component ></my-component>

以上,当 prop 验证失败了,如果使用的是开发版本会抛出一条警告。

2、自定义事件

  • 首先:子组件可以用 this.$parent 访问它的父组件,根实例的后代可以用 this.$root 访问它,父组件有一个数组 this.$children 包含它所有的子元素。

尽管可以访问父链上任意的实例,不过子组件应当避免直接依赖父组件的数据。另外子组件中修改父组件的状态是非常糟糕的,因为:

  • 这让父组件与子组件紧密地耦合
  • 这样的话,只看父组件很难理解父组件的状态,因为它可能被任意子组件修改!理想情况下,只有组件自己能修改它的状态

父组件是使用 props 传递数据给子组件,如果子组件要把数据传递回去,那就是自定义事件!

  • (1)使用v-on绑定自定义事件

每个 Vue 实例都实现了事件接口(Events interface),即:
父组件——使用 $on(eventName) 监听事件
子组件——使用 $emit(eventName) 触发事件

下面的列子跟官网的几乎一样,但是区别在于,是通过传参来改变父组件的状态的赋值。
<div id="counter-event-example">
  <p>{{ total }}</p>
  <button-counter v-on:increment="incrementTotal"></button-counter>
  <button-counter v-on:increment="incrementTotal"></button-counter>
</div>
       var allTotal = {number: 0}  //这个是统计两个按钮的点击的次数,这样就能直接赋给父组件
        Vue.component('button-counter', {
          template: '<button v-on:click="increment">{{ counter }}</button>',
          data: function () {
            return {
              counter: 0,
              allCounter: allTotal
            }
          },
          methods: {
            increment: function () {
              this.counter += 1;
              this.allCounter.number += 1;  // 准备给父组件的自定义事件方法传参
              this.$emit('increment',this.allCounter.number);
            }
          },
        })
        new Vue({
          el: '#counter-event-example',
          data: {
            total: 0
          },
          methods: {
            incrementTotal: function (value) {
              this.total = value;
            }
          }
        })
  • (2)给组件绑定原生事件

在某个组件的根元素上监听一个原生事件。可以使用 .native 修饰 v-on 。这就是说所有的原生事件如果在组件的根元素上不加.native,vue会自动认为它是自定义事件

  • (3)使用自定义事件的表单输入组件

使用 v-model 来进行数据双向绑定。牢记:这个指令仅仅是一个语法糖。

<input v-model="something">
<!--上下两种方式是等价的-->
<input v-bind:value="something" v-on:input="something = $event.target.value">

所以在组件中使用时,它相当于下面的简写:

<custom-input v-bind:value="something" v-on:input="something = arguments[0]"></custom-input>
<!--改写成语法糖如下-->
<custom-input v-model="something"></custom-input>

尤其注意,这个地方有点绕:如果是自定义的表单组件,并且父组件在加载这个表单组件时使用了v-model指令,那么作为子组件,接收到的prop应该是value而不是something。


  • (4)子组件的索引
<div id="parent">
  <user-profile ref:profile></user-profile>
</div>
<script>
  var parent = new Vue({el: '#parent'});
  var child = parent.$refs.profile;    // 可以得到子组件
</script>
3、内容分发
  • (1)编译作用域

父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。说白了,就是一眼看上去,在谁里面就是谁的

<div id="app">
        <my-component v-show="display"></my-component>
    </div>

    <template id="myComponent">
        <table>
            <tr>
                <th colspan="3">{{msg}}</td>
            </tr>
        </table>
    </template>
    <script>
        var my = Vue.extend({
            template: '#myComponent',
            data : function (){
                return {
                    msg : '这是子组件',
                    display: false
                }
            }
        })
        var vm = new Vue({
            el: '#app',
            data: {
                display: true
            },
            components: {
                'myComponent': my
            }
        })
    </script>

在my-component标签上使用指令v-show="display",这个display数据是来源于Vue实例vm ,还是my-component组件呢?
答案是Vue实例

Vue实例vm设置的display是true,所以展示出来

  • (2)单个Slot

下面的代码在定义my-component组件的模板时,指定了一个<slot></slot>元素。

<div id="app">
    <my-component>
        <h1>这是父组件的内容!</h1>
    </my-component>

    <my-component>
    </my-component>
</div>
<template id="myComponent">
    <div class="content">
        <h2>这是一个子组件!</h2>
        <slot>如果没有分发内容,则显示slot中的内容</slot>
        <p>Hello,Vue.js</p>
    </div>
</template>
  
<script>
    Vue.component('my-component', {
        template: '#myComponent'
    })

    new Vue({
        el: '#app'
    })
</script>

第一个<my-component>标签有一段分发内容 <h1>这是父组件的内容!</h1>,渲染组件时显示了这段内容。
第二个<my-component>标签则没有,渲染组件时则显示了slot标签中的内容。

单个slot

  • (3)具名slot

<slot> 元素可以用一个特殊的属性 name 来配置如何分发内容。多个 slot 可以有不同的名字。具名 slot 将匹配内容片段中有对应 slot 特性的元素。

<div id="app">
    <my-component>
        <div slot="header">
            这是一个头部
        </div>
        <p>neirong</p>
        <p>neirong</p>
        <div slot="footer">
            这是一个底部
        </div>
    </my-component>
</div>
<template id="myComponent">
    <div class="container">
      <header>
        <slot name="header"></slot>
      </header>
      <main>
        <slot></slot>
      </main>
      <footer>
        <slot name="footer"></slot>
      </footer>
    </div>
</template>
 
<script>
    Vue.component('my-component', {
        template: '#myComponent'
    })

    new Vue({
        el: '#app'
    })
</script>

可以看出仍然可以有一个匿名 slot ,它是默认 slot ,作为找不到匹配的内容片段的备用插槽。如果没有默认的 slot ,这些找不到匹配的内容片段将被抛弃。

  • (4)作用域插槽

作用域插槽是一种特殊类型的插槽,用作使用一个(能够传递数据到)可重用模板替换已渲染元素。
数据传递,可重用,自然而然的想到循环

  • 基于官网给出一个完整的例子
<div id="app">
    <my-awesome-list :items="items">
      <template slot="item" scope="props">
        <li>{{ props.text }}</li>
      </template>
    </my-awesome-list>
</div>
 
<script>
    Vue.component('my-awesome-list',{
        template: `<ul>
                      <slot name="item"
                        v-for="item in items"
                        :text="item.text">
                   
                      </slot>
                </ul>`,
        props: ['items']
    })
    var demo = new Vue({
        el: '#app',
        data: {
            items: [
                {text: 'aaaaa'},
                {text: 'bbbbb'},
                {text: 'ccccc'},
                {text: 'ddddd'},
                {text: 'eeeee'}
            ]
        }
    })
</script>
三、组件小贴士
  • (1)组件的命名

当注册组件(或者 props)时,可以使用 kebab-case ,camelCase ,或 TitleCase。但是,在 HTML 模版中,请使用 kebab-case 形式:

// 在组件定义中
components: {
  // 使用 kebab-case 形式注册
  'kebab-cased-component': { /* ... */ },
  // register using camelCase
  'camelCasedComponent': { /* ... */ },
  // register using TitleCase
  'TitleCasedComponent': { /* ... */ }
}
 
<!-- 在HTML模版中始终使用 kebab-case -->
<kebab-cased-component></kebab-cased-component>
<camel-cased-component></camel-cased-component>
<title-cased-component></title-cased-component>

注意:当使用字符串模式时,可以不受 HTML 的 case-insensitive 限制。


生命周期

生命周期图示

1、生命周期的各阶段
  • (1)beforeCreate

在实例初始化之后,数据观测(data observer) 和 event/watcher 事件配置之前被调用。

  • (2)created

实例已经创建完成之后被调用。在这一步,实例已完成以下的配置:数据观测(data observer),属性和方法的运算, watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见。

  • (3)beforeMount

在挂载开始之前被调用:相关的 render 函数首次被调用。该钩子在服务器端渲染期间不被调用。

  • (3)mounted

el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。该钩子在服务器端渲染期间不被调用。

  • (4)beforeUpdate

数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。
你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。
该钩子在服务器端渲染期间不被调用。

  • (5)updated

由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。
该钩子在服务器端渲染期间不被调用。

  • (6)activated

keep-alive 组件激活时调用。该钩子在服务器端渲染期间不被调用。

  • (7)deactivated

keep-alive 组件停用时调用。该钩子在服务器端渲染期间不被调用。

  • (8)beforeDestroy

实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。

  • (8)destroyed

Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务器端渲染期间不被调用。

2、实例方法
  • (1) vm.$mount( [elementOrSelector] )

手动地挂载一个未挂载的实例,返回值是实例自身。因而可以链式调用其它实例方法。
如果没有提供 elementOrSelector 参数,模板将被渲染为文档之外的的元素,并且你必须使用原生DOM API把它插入文档中。

var MyComponent = Vue.extend({
  template: '<div>Hello!</div>'
})
// 创建并挂载到 #app (会替换 #app)
new MyComponent().$mount('#app')
// 同上
new MyComponent({ el: '#app' })
// 或者,在文档之外渲染并且随后挂载,这种方式不会替换#app
var component = new MyComponent().$mount()
document.getElementById('app').appendChild(component.$el)
  • (2) vm-destroy()

完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发 beforeDestroy 和 destroyed 的钩子。
注意:在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。

  • (3)vm.$nextTick( [callback] )  涉及到Vue的异步更新队列

回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 Vue.nextTick 一样,不同的是回调的 this 自动绑定到调用它的实例上。
应用上,在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: 'not updated'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = 'updated'
      console.log(this.$el.textContent) // => '没有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})
阅读 1.8k

陈工移山
移山之志,一点一滴。

移山之志,一点一滴。

3.2k 声望
435 粉丝
0 条评论

移山之志,一点一滴。

3.2k 声望
435 粉丝
宣传栏