Vue 学习笔记

2019-11-22  本文已影响0人  Whyn

[TOC]

简介

Vue 是一个用于构建 Web 用户界面的渐进式 JavaScript 框架。其核心库只关注视图层(view layer),同时具备良好的第三方支持库生态用以应付构建复杂大型单页应用(SPA:Single-Page Application)。

MVVM 模型

Vue 虽然没有完全遵循 MVVM(Model-View-ViewModel)模型,当其设计也受到了 MVVM 的启发,以数据驱动界面,如下图所示:

Vue MVVM

Vue 中,充当 ViewModel 的是一个 Vue 实例(new Vue({})),该 Vue实例 作用于某一个 HTML 元素上,全权代理该元素节点的所有操作。

Vue实例 内部通过 DOM Listeners 可以观测到页面上 DOM 元素的变化,从而将该种变化同步更改到 Model 中的对应数据。

同时通过 Data Bindings,当 Model 中的数据改变时,则会对相应视图上的显示进行更改,从而实现了 View 和 Model 的数据双向绑定。

:传统的 Web 编程模型是 结构驱动,即要对一个 DOM 节点进行操作,第一步就是要获取该 DOM 节点对象,然后再修改数据更新到节点上。
Vue 的中心思想是 数据驱动,要更改界面,其实就是要更改数据。
简而言之,在 Vue 中,不应当考虑操作 DOM,而是专注于 操作数据

安装

Vue 的安装有多种方法,这里主要介绍两种方法:

  1. 通过<script>标签直接引入:
<!-- 对于制作原型或学习,你可以这样使用最新版本 -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<!-- 或者-->
<!-- 对于生产环境,我们推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏:-->
<script src="https://cdn.jsdelivr.net/npm/vue@2.6.0"></script>

:通过<script>标签引入,Vue会被注册为一个全局变量

  1. 直接使用官方提供的快速搭建复杂单页面应用 (SPA) 的脚手架 vue-cli
npm install -g @vue/cli
vue create <project-name>

:使用 vue-cli 前需确保系统已安装 nodejs

以上两步操作完成,我们便创建完成一个 Vue 项目。

在项目的 package.json 中,可以看到 vue-cli 提供了两个脚本命令让我们运行与打包项目:

组件化

Vue 的两大特性为 数据驱动组件化

通常一个大的页面可以划分为许多个小区块,这些小区块有些结构是相似的,我们可以将这些相似的区块抽象出一个统一的结构,方便复用,这种抽象结构的方法即称为组件化。

在实际项目开发中,一个大的页面通常都是由许多个小的组件构造而成的,如下图所示:

组件化

Vue 提供了两种组件定义的方式:

<body>
    <div id="app">
        <my-component />
    </div>

    <script>
        const myComponent = Vue.component('my-component',{
            data(){
                return {
                    'message': 'Hello Global Component'
                }
            },
            template:`
            <h1>{{message}}</h1>
            `
        });
        const vm = new Vue({
            el: "#app",
        });
    </script>
</body>
<body>
    <div id="app">
        <component-a></component-a>
        <component-b />
    </div>
    <script>
        const componentA = {
            template: `
            <h1>Component A</h1>
            `
        };
        const componentB = {
            template: `
            <h1>Component B</h1>
            `
        };

        const vm = new Vue({
            el: '#app',
            components: {  // 按需引入需要的组件
                componentA,
                componentB
            }
        });
    </script>
</body>

最佳实践:在 Vue 中,组件通常都定义到一个单独的.vue文件中,其他组件需要时,导入相应组件的.vue文件即可。

// MyComponent.vue
<template>
  <h1>{{message}}</h1>
</template>

<script>
export default {
  name: "MyCompoent",
  data() {
    return {
      message: "Hello MyComponent!"
    };
  }
};
</script>

<style scoped>
h1 {
  background: red;
}
</style>

可以直接使用以下命令直接运行.vue文件,查看组件展示效果:

vue serve MyComponent.vue --open

也可以在其他组件内导入该组件,进行使用:

<template>
  <div id="app">
    <h1>Parent Component</h1>
    <!-- 使用组件 -->
    <my-component />
  </div>
</template>

<script>
// 导入组件
import MyComponent from "./MyComponent.vue";

export default {
  name: "app",
  components: {
    MyComponent // 引入组件
  }
};
</script>

:在 Vue 中,组件实质是带有一个名字的 Vue实例,其性质与 Vue实例 基本一致(遵循 Vue实例 的生命周期等内容),特点是多了个组件复用功能。

Vue实例

Vue 实例充当 ViewModel 角色,负责 View 和 Model 之间的数据绑定:

new Vue(Options)

当创建一个 Vue 实例时,你可以传入一个选项对象Options,该Options的选项列表有如下可选:

下面列举一些Options常用选项

<body>
    <div id="app">
        <h1>{{message}}</h1>
    </div>
    <script>
        const vm = new Vue({
            el: "#app",
            data: {
                message: "Hello Vue!"
            }
        });
    </script>
</body>

:Vue 将会递归将data的属性转换为getter/setter,从而让data的属性能够响应数据变化。比如在控制台输入vm.message = 'Hi Vue!!!,可以观察到页面数据发生了更改。

组件 中的data属性必须是Fcuntion类型,其返回一个Object,原因是组件复用时,保证每个新组件都有独一的一份数据拷贝。

// MyComponent.vue
<template>
    <h1>{{message}}</h1>
</template>

<script>
export default {
    name: 'MyComponent',
    data(){       // 函数类型
        return {  // 返回数据对象
            'message': 'Hello Vue!'
        }
    }
}
</script>
Vue.component('my-component', {
  props: {
    // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证)
    propA: Number,
    // 多个可能的类型
    propB: [String, Number],
    // 必填的字符串
    propC: {
      type: String,
      required: true
    },
    // 带有默认值的数字
    propD: {
      type: Number,
      default: 100
    },
    // 带有默认值的对象
    propE: {
      type: Object,
      // 对象或数组默认值必须从一个工厂函数获取
      default: function () {
        return { message: 'hello' }
      }
    },
    // 自定义验证函数
    propF: {
      validator: function (value) {
        // 这个值必须匹配下列字符串中的一个
        return ['success', 'warning', 'danger'].indexOf(value) !== -1
      }
    }
  }
})
var Comp = Vue.extend({
  props: ['msg'],
  template: '<div>{{ msg }}</div>'
})

var vm = new Comp({
  propsData: {
    msg: 'hello'
  }
})

propsData属性只能用于new创建的实例中。

var vm = new Vue({
  data: { a: 1 },
  methods: {
    plus: function () {
      this.a++
    }
  }
})
vm.plus()
vm.a // 2

methods中的this自动绑定到当前 Vue实例。

<template>
  <div>
    <h1>获取数据: {{computedData}}</h1>
    <h1>设置数据:{{setData = 3}}</h1>
    <h1>获取数据: {{setData}}</h1>
  </div>
</template>

<script>
export default {
  name: "Computed",
  data() {
    return {
      count: 1
    };
  },
  computed: {
    // 只读取 data
    computedData() {
      return this.count + 10;
    },
    // 读取和设置 data
    setData: {
      get() {
        return this.count + 1;
      },
      set(value) {
        this.count = value;
      }
    }
  }
};
</script>

computed类型为Object,其具有如下特点:

  1. computed内部定义的属性为访问器属性,即具备gettersetter,且其内部this自动绑定到当前 Vue实例。
  2. computed会自动 缓存 计算结果,只有当依赖的响应式属性变化时,computed才会重新进行计算。
    缓存computedmethods的最大区别之处,methods每次调用一定会运行函数,而computed则不一定。
  1. string:字符串表示回调函数名,当数据改变时,回调该函数:
const vm = new Vue({
    el: '#app',
    data: {
        a: 1
    },
    methods: {
        aChanged(value, oldValue) {
            console.log(`a changed: new:${value} --> old:${oldValue}`);
        }
    },
    watch: {
        a: 'aChanged'
    }
});
  1. Function:当数据改变时,直接回调该函数:
const vm = new Vue({
    el: '#app',
    data: {
        a: 1
    },
    watch: {
        a(value, oldValue) {
            console.log(`a changed: new:${value} --> old:${oldValue}`);
        }
    }
});
  1. Object:对监控的属性为对象时,Vue 默认只能监控到对象重新被赋值的变化,而如果需要监听对象内部属性的变化,则可使用该选项,其中:
    handler代表回调函数。
    deep用来控制监听对象属性的层级,deep=true时只要对象内部 property 改变(不管嵌套有多深),都会监听到。
    immediate用来设置是否立即产生回调。当immediate=true时,回调函数会立即被调用,传递的是属性当前的值。
const vm = new Vue({
    el: '#app',
    data: {
        a: {
            aa: {
                aaa: 3
            }
        }
    },
    watch: {
        a: {
            handler(value, oldValue) {
                console.log(`a changed: new:${value} --> old:${oldValue}`);
            },
            deep: true // 被监听对象的 property 改变时被调用,无论嵌套的有多深
        }
    }
});

:大多数情况下,观察和响应数据变更使用计算属性(computed)便足够了,但是当在数据变化时需要执行异步或开销较大的操作时,则此时使用侦听属性(watch)会更加适合。

new Vue({
    el: '#app'
});

:如果在实例化时存在这个选项,实例将立即进入编译过程,否则,需要显式调用vm.$mount()手动开启编译。

<div id="app"></div>

const vm = new Vue({
    el: '#app',
    template: '<h1>template</h1>' // <div> 会被 <h1> 完全覆盖
})
<div id="app"></div>

new Vue({
    render(createElement) {
        return createElement('div', {
            class: 'rendered'
        },
            [
                createElement('h1', {
                    domProps: {
                        innerHTML: 'div>h1 rendered by vue'
                    }
                })
            ]
        );
    }}).$mount('#app');

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML,只有在一些特殊场景下,比如模板冗长且具备重复元素,则此时使用渲染函数render通过编写 JavaScript 代码来渲染出页面会更加方便简洁。

生命周期

每个 Vue实例 在挂载到页面时,都会经历一系列的初始化过程,例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。在创建 Vue实例 的这整个过程中,Vue 为我们预留出了一些 Hook 点,方便我们在 Vue实例 创建过程的某个生命周期中进行一些操作。如下图所示:

vue lifecycle

:图片来源于网上,侵删。

这些预留的生命周期钩子函数总共有如下几个:

mounted: function () {
  this.$nextTick(function () {
    // Code that will run only after the
    // entire view has been rendered
  })
}
<template>
    <keep-alive>
        <my-component />
    </keep-alive>
</template>

指令

指令 (Directives) 是带有v-前缀的特殊属性。

Vue 提供了以下内置的指令:

<span v-text="msg"></span>
<!-- 和下面的一样 -->
<span>{{msg}}</span>
<template>
  <div id="app">
    <div v-html="html"></div>
  </div>
</template>

<script>
export default {
  name: "app",
  data() {
    return {
      html: '<a href="https://www.baidu.com" >baidu</a>'
    };
  },
};
</script>

v-html的内容只会按普通 HTML 插入,不会作为 Vue 模板进行编译。
:在单文件组件里,scoped 的样式不会应用在 v-html 内部,因为那部分 HTML 没有被 Vue 的模板编译器处理。
:在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用v-html,永不用在用户提交的内容上。

<h1 v-show="ok">Hello!</h1>

v-show不支持<template>元素,也不支持v-else

<h1 v-if="awesome">Vue is awesome!</h1>

:当和v-if一起使用时,v-for的优先级比v-if更高。
v-show = false时只是把元素设置为:display:none,元素还留着 DOM 树上。
v-if = false时,元素会被整个移除,其上绑定的数据/组件都会被销毁。

<div v-if="Math.random() > 0.5">
  Now you see me
</div>
<div v-else>
  Now you don't
</div>
<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>
<div v-for="item in items">
  {{ item.text }}
</div>
<!-- 另外也可以为数组索引指定别名 (或者用于对象的键):-->
<div v-for="(item, index) in items"></div>
<div v-for="(val, key) in object"></div>
<div v-for="(val, name, index) in object"></div>

v-for渲染元素时,默认使用“就地更新”策略,即当列表数据改变时,Vue 不会移动当前 DOM 元素来重新匹配数据项,而是根据索引位置重新渲染数据。比如:

现在我们有数据项:

data() {
    return {
        datas: [
            { id: 1, name: "one" },
            { id: 2, name: "two" },
            { id: 3, name: "three" }
        ]
    }

将这些数据项渲染到页面上:

<template>
  <div>
    <ul>
      <li v-for="(item,index) in datas">
        <input type="checkbox" />
        {{item.name}}
      </li>
    </ul>
</template>

我们使用v-for将每条数据项渲染到一个<li>上,此时显示效果如下:

如果此时我们勾选第一个<li>checkbox,即one勾选上,然后再往数据列表前面添加一个数据:this.datas.unshift({ id: 4, name: "four" }),则可以看到显示效果如下:

unshift

可以看到,我们想要的是one被勾选了,但是效果是数据列表首位被勾选。出现这种现象的原因就是v-for默认采用的“就地更新”策略:它会复用已渲染完成的 DOM 元素,然后只对变化的数据进行修改,比如这里复用了第一条<li><checkbox>one</li>,添加数据项,对第一条<li>来说,他的数据改变了,但是<checkbox>不包含在数据项里,因此只会修改数据,将one修改为four,而checkbox仍保持勾选状态。

因此,“就地更新”策略是高效的,但是 只适用于不依赖子组件状态或临时 DOM 状态(例如:表单输入值) 的列表渲染输出

而要解决上述问题,只需为v-for提供一个key属性(key必须是唯一的),这样 Vue 就可以识别出数据项对应的渲染条目,从而重用和重新排序现有元素:

<template>
  <div>
    <ul>
      <li v-for="(item,index) in datas" :key="item.id">
        <input type="checkbox" />
        {{item.name}}
      </li>
    </ul>
</template>

由于新添加的数据id=4,当前已存在的<li>没有与之对应的标识key,因此 Vue 会重新渲染一个新的<li>,并将其与id=4对应起来,结果如下图所示:

v-for key

:“就地更新”策略其实就是使用索引作为节点标识,即:key=index

<!-- 方法处理器 -->
<button v-on:click="doThis"></button>

<!-- 动态事件 (2.6.0+) -->
<button v-on:[event]="doThis"></button>

<!-- 内联语句 -->
<button v-on:click="doThat('hello', $event)"></button>

<!-- 缩写 -->
<button @click="doThis"></button>

<!-- 动态事件缩写 (2.6.0+) -->
<button @[event]="doThis"></button>

<!-- 停止冒泡 -->
<button @click.stop="doThis"></button>

<!-- 阻止默认行为 -->
<button @click.prevent="doThis"></button>

<!-- 阻止默认行为,没有表达式 -->
<form @submit.prevent></form>

<!--  串联修饰符 -->
<button @click.stop.prevent="doThis"></button>

<!-- 键修饰符,键别名 -->
<input @keyup.enter="onEnter">

<!-- 键修饰符,键代码 -->
<input @keyup.13="onEnter">

<!-- 点击回调只会触发一次 -->
<button v-on:click.once="doThis"></button>

<!-- 对象语法 (2.4.0+) -->
<button v-on="{ mousedown: doThis, mouseup: doThat }"></button>

v-on用在普通元素上时,只能监听 原生 DOM 事件。用在自定义组件上时,也可以监听子组件触发的自定义事件

<my-component @my-event="handleThis"></my-component>

<!-- 内联语句 -->
<my-component @my-event="handleThis(123, $event)"></my-component>

<!-- 组件中的原生事件 -->
<my-component @click.native="onClick"></my-component>
<!-- 绑定一个属性 -->
<img :src="imageSrc">

<!-- 动态特性名 (2.6.0+) -->
<button :[key]="value"></button>

<!-- class 绑定 -->
<div :class="{ red: isRed }"></div>
<div :class="[classA, classB]"></div>
<div :class="[classA, { classB: isB, classC: isC }]">

<!-- style 绑定 -->
<div :style="{ fontSize: size + 'px' }"></div>
<div :style="[styleObjectA, styleObjectB]"></div>

<!-- 绑定一个有属性的对象 -->
<div v-bind="{ id: someProp, 'other-attr': otherProp }"></div>

<!-- 通过 prop 修饰符绑定 DOM 属性 -->
<div v-bind:text-content.prop="text"></div>

<!-- prop 绑定。“prop”必须在 my-component 中声明。-->
<my-component :prop="someThing"></my-component>

<!-- 通过 $props 将父组件的 props 一起传给子组件 -->
<child-component v-bind="$props"></child-component>

<!-- 支持绑定驼峰命名属性 -->
<svg :view-box.camel="viewBox"></svg>
<input type="text" v-model="message" />

<script>
export default {
  name: 'VModel',
  data() {
    return {
      message: ''
    };
  }
};
</script>
// 子组件:预留插槽
<template>
  <div>
    <h1>Son Component</h1>
    <!-- 预留插槽 -->
    <slot></slot>
  </div>
</template>

// 父组件:传入插槽内容
<template>
  <div>
    <h1>Parent Component</h1>
    <son-component>
        <h2>slot: content from Parent Component</h2>
    </son-component>
  </div>
</template>

后备内容:可以通过为<slot>内部提供默认内容,只有当父组件显示传入内容时,才会覆盖默认内容:

<slot>
    <h1>Default Content</h1>
</slot>

具名插槽:我们可以给插槽进行命名(使用name属性),这样父组件就可指定名字(使用v-slot指令)对特定的插槽进行覆盖:

// 子组件模板
<template>
  <div>
    <h1>Son Component</h1>
    <!-- 预留命名插槽 -->
    <slot name="header"></slot>
    <main>
      <!-- name="default" -->
      <slot></slot>
    </main>
    <slot name="footer"></slot>
  </div>
</template>

// 父组件
<template>
  <div>
    <h1>Parent Component</h1>
    <son-component>
      <template v-slot:header>
        <h2>替换 header 插槽</h2>
      </template>
      <h3>替换默认插槽</h3>
      <template #footer>
        <h2>替换 footer 插槽</h2>
      </template>
    </son-component>
  </div>
</template>

v-slot只能添加在一个<template>或 组件 上。
:默认插槽其实也是一个具名插槽,其名称为:default

插槽 prop:使用 插槽 prop 可以传递子组件的数据给到父组件,使父组件可以在覆盖插槽的内容上使用子组件的数据:

// 子组件
<slot :msg="message"></slot>

<script>
export default {
  name: 'SonComponent',
  data() {
    return {
      message: 'Hello from Son Component!'
    };
  }
};
</script>

// 父组件:slotProps 接收子组件的 插槽props
<son-component #default="slotProps">
    {{slotProps.msg}}
</son-component>
<span v-pre>{{ this will not be compiled }}</span>
[v-cloak] {
  display: none;
}

<div v-cloak>
  {{ message }}
</div>
<span v-once>This will never change: {{msg}}</span>

Vue 提供了两种自定义指令的方式:

  1. 全局指令:使用Vue.directive
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {...})
  1. 局部指令:组件中定义一个directives属性:
// 注册一个局部自定义指令 `v-focus`
directives: {
  focus: {...}
}

示例:使用自定义指令v-customtext模拟v-text

<template>
  <h1 v-customtext="msg"></h1>
</template>

<script>
export default {
  name: 'customeDirective',
  data() {
    return {
      msg: 'Hello Custom Directives!'
    };
  },
  directives: {
    customtext: {
      inserted(el, binding, vnode, oldVnode) {
        el.innerText = binding.value;
      }
    }
  }
};
</script>

其他

父传子:子组件通过props属性可接收父组件传递过来的变量:

// ParentComponent.vue
<template>
  <son-component :msg="message" />
</template>

<script>
import SonComponent from './SonComponent';

export default {
  name: 'ParentComponent',
  components: {
    SonComponent
  },
  data() {
    return {
      message: 'data from Parent Component'
    };
  }
};
</script>

// SonComponent.vue
<template>
  <h1>{{msg}}</h1>
</template>

<script>
export default {
  name: 'SonComponent',
  props: {
    msg: {
      type: String,
      required: true
    }
  }
};
</script>

子传父:子组件可以通过$emit发送自定义事件向父组件传值,父组件直接注册接收该事件即可:

// SonComponent.vue
<template>
  <button @click="sendEvent">点击发送事件</button>
</template>

<script>
export default {
  name: 'SonComponent',
  methods: {
    sendEvent() {
      // 发送自定义事件
      this.$emit('eventFromChild', 'data from Son Component!!');
    }
  }
};
</script>

// ParentComponent.vue
<template>
  <div>
    <!-- 接收事件 -->
    <son-component @eventFromChild="recvChildEvent" />
    <h1>{{data}}</h1>
  </div>
</template>

<script>
import SonComponent from './SonComponent';

export default {
  name: 'ParentComponent',
  components: {
    SonComponent
  },
  data() {
    return {
      data: 'hhhh'
    };
  },
  methods: {
    recvChildEvent(data) {
      this.data = data;
    }
  }
};
</script>

父传子孙:父组件/祖先组件通过provide提供变量,子孙组件通过inject来接收该变量:

// ParentComponent
import SonComponent from './SonComponent.vue'
export default {
    name: 'ParentComponent',
    components: {
        SonComponent
    },
    provide: {
        message: 'data from Parent Component'
    }
}
// SonComponent
<template>
    <h1>{{message}}</h1>
</template>>

<script>
export default {
    name: 'SonComponent',
    inject: ['message']
}
</script>>

更多组件间通信方式,请参考:Vue组件间通信6种方式

参考

上一篇 下一篇

猜你喜欢

热点阅读