基本使用
指令、插值
<template>
<div>
<p>文本插值 {{message}}</p>
<p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
<p :id="dynamicId">动态属性 id</p>
<hr/>
<p v-html="rawHtml">
<span>有 xss 风险</span>
<span>【注意】使用 v-html 之后,将会覆盖子元素</span>
</p>
</div>
</template>
<script>
export default {
data() {
return {
message: 'hello vue',
flag: true,
rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
dynamicId: `id-${Date.now()}`
}
}
}
</script>
computed和wacth
- computed
<template>
<div>
<p>num {{num}}</p>
<p>double1 {{double1}}</p>
<input v-model="double2"/>
</div>
</template>
<script>
export default {
data() {
return {
num: 20
}
},
computed: {
double1() {
return this.num * 2
},
//v-model必须有get、set两个
double2: {
get() {
return this.num * 2
},
set(val) {
this.num = val/2
}
}
}
}
</script>
- watch
<template>
<div>
<input v-model="name"/>
<input v-model="info.city"/>
</div>
</template>
<script>
export default {
data() {
return {
name: '双越',
info: {
city: '北京'
}
}
},
watch: {
name(oldVal, val) {
// eslint-disable-next-line
console.log('watch name', oldVal, val) // 值类型,可正常拿到 oldVal 和 val
},
info: {
handler(oldVal, val) {
// eslint-disable-next-line
console.log('watch info', oldVal, val) // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
},
deep: true // 深度监听
}
}
}
</script>
class和style
<template>
<div>
<p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
<p :class="[black, yellow]">使用 class (数组)</p>
<p :style="styleData">使用 style</p>
</div>
</template>
<script>
export default {
data() {
return {
isBlack: true,
isYellow: true,
black: 'black',
yellow: 'yellow',
styleData: {
fontSize: '40px', // 转换为驼峰式
color: 'red',
backgroundColor: '#ccc' // 转换为驼峰式
}
}
}
}
</script>
<style scoped>
.black {
background-color: #999;
}
.yellow {
color: yellow;
}
</style>
条件渲染v-if、v-show
<template>
<div>
<p v-if="type === 'a'">A</p>
<p v-else-if="type === 'b'">B</p>
<p v-else>other</p>
<p v-show="type === 'a'">A by v-show</p>
<p v-show="type === 'b'">B by v-show</p>
</div>
</template>
<script>
export default {
data() {
return {
type: 'a'
}
}
}
</script>
循环列表渲染
v-if和v-for不能一起使用,v-for优先级高,先循环,再判断,如果false,浪费性能
<template>
<div>
<p>遍历数组</p>
<ul>
<li v-for="(item, index) in listArr" :key="item.id">
{{index}} - {{item.id}} - {{item.title}}
</li>
</ul>
<p>遍历对象</p>
<ul >
<li v-for="(val, key, index) in listObj" :key="key">
{{index}} - {{key}} - {{val.title}}
</li>
</ul>
</div>
</template>
<script>
export default {
data() {
return {
flag: false,
listArr: [
{ id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
{ id: 'b', title: '标题2' },
{ id: 'c', title: '标题3' }
],
listObj: {
a: { title: '标题1' },
b: { title: '标题2' },
c: { title: '标题3' },
}
}
}
}
</script>
事件
<template>
<div>
<p>{{num}}</p>
<button @click="increment1">+1</button>
<button @click="increment2(2, $event)">+2</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
},
methods: {
increment1(event) {
// eslint-disable-next-line
console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
// eslint-disable-next-line
console.log(event.target)
// eslint-disable-next-line
console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
this.num++
// 1. event 是原生的
// 2. 事件被挂载到当前元素
// 和 DOM 事件一样
},
increment2(val, event) {
// eslint-disable-next-line
console.log(event.target)
this.num = this.num + val
},
loadHandler() {
// do some thing
}
},
mounted() {
window.addEventListener('load', this.loadHandler)
},
beforeDestroy() {
//【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
// 自己绑定的事件,需要自己销毁!!!
window.removeEventListener('load', this.loadHandler)
}
}
</script>
事件修饰符
//阻止单击事件继续传播
<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>
//只有当 event.target 是当前元素自身时触发处理函数
//即 事件不是从内部函数触发的
<div v-on:click.self="doThat"></div>
按键修饰符
//即使 Alt 或 Shift 被一同按下时也会触发
<button @click.ctrl="onClick">A</button>
//有且只有 Ctrl 被按下的时候才出发
<button @click.ctrl.exact="onCtrlClick">A</button>
//没有任何系统修饰符被按下的时候才触发
<button @click.exact="onClick"></button>
表单
<template>
<div>
<p>输入框: {{name}}</p>
<input type="text" v-model.trim="name"/>
<input type="text" v-model.lazy="name"/>
<input type="text" v-model.number="age"/>
<p>多行文本: {{desc}}</p>
<textarea v-model="desc"></textarea>
<!-- 注意,<textarea>{{desc}}</textarea> 是不允许的!!! -->
<p>复选框 {{checked}}</p>
<input type="checkbox" v-model="checked"/>
<p>多个复选框 {{checkedNames}}</p>
<input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
<label for="jack">Jack</label>
<input type="checkbox" id="john" value="John" v-model="checkedNames">
<label for="john">John</label>
<input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
<label for="mike">Mike</label>
<p>单选 {{gender}}</p>
<input type="radio" id="male" value="male" v-model="gender"/>
<label for="male">男</label>
<input type="radio" id="female" value="female" v-model="gender"/>
<label for="female">女</label>
<p>下拉列表选择 {{selected}}</p>
<select v-model="selected">
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
<p>下拉列表选择(多选) {{selectedList}}</p>
<select v-model="selectedList" multiple>
<option disabled value="">请选择</option>
<option>A</option>
<option>B</option>
<option>C</option>
</select>
</div>
</template>
<script>
export default {
data() {
return {
name: '双越',
age: 18,
desc: '自我介绍',
checked: true,
checkedNames: [],
gender: 'male',
selected: '',
selectedList: []
}
}
}
</script>
Vue组件通讯
index.vue
<template>
<div>
<Input @add="addHandler"/>
<List :list="list" @delete="deleteHandler"/>
</div>
</template>
<script>
import Input from './Input'
import List from './List'
export default {
components: {
Input,
List
},
data() {
return {
list: [
{
id: 'id-1',
title: '标题1'
},
{
id: 'id-2',
title: '标题2'
}
]
}
},
methods: {
addHandler(title) {
this.list.push({
id: `id-${Date.now()}`,
title
})
},
deleteHandler(id) {
this.list = this.list.filter(item => item.id !== id)
}
},
created() {
// eslint-disable-next-line
console.log('index created')
},
mounted() {
// eslint-disable-next-line
console.log('index mounted')
},
beforeUpdate() {
// eslint-disable-next-line
console.log('index before update')
},
updated() {
// eslint-disable-next-line
console.log('index updated')
},
}
</script>
Input.vue
<template>
<div>
<input type="text" v-model="title"/>
<button @click="addTitle">add</button>
</div>
</template>
<script>
import event from './event'
export default {
data() {
return {
title: ''
}
},
methods: {
addTitle() {
// 调用父组件的事件
this.$emit('add', this.title)
// 调用自定义事件
event.$emit('onAddTitle', this.title)
this.title = ''
}
}
}
</script>
List.vue
<template>
<div>
<ul>
<li v-for="item in list" :key="item.id">
{{item.title}}
<button @click="deleteItem(item.id)">删除</button>
</li>
</ul>
</div>
</template>
<script>
import event from './event'
export default {
// props: ['list']
props: {
// prop 类型和默认值
list: {
type: Array,
default() {
return []
}
}
},
data() {
return {
}
},
methods: {
deleteItem(id) {
this.$emit('delete', id)
},
addTitleHandler(title) {
// eslint-disable-next-line
console.log('on add title', title)
}
},
created() {
// eslint-disable-next-line
console.log('list created')
},
mounted() {
// eslint-disable-next-line
console.log('list mounted')
// 绑定自定义事件
event.$on('onAddTitle', this.addTitleHandler)
},
beforeUpdate() {
// eslint-disable-next-line
console.log('list before update')
},
updated() {
// eslint-disable-next-line
console.log('list updated')
},
beforeDestroy() {
// 及时销毁,否则可能造成内存泄露
event.$off('onAddTitle', this.addTitleHandler)
}
}
</script>
event.js//非父子组件通讯-自定义事件
import Vue from 'vue'
export default new Vue()
组件生命周期
index created
list created
list mounted
index mounted
index beforeUpdate
list beforeUpdate
list updated
index updated
Vue高级特性
自定义v-model
- index.vue
<template>
<div>
<!-- 自定义 v-model -->
<p>{{name}}</p>
<CustomVModel v-model="name"/>
</div>
</template>
<script>
import CustomVModel from './CustomVModel'
export default {
components: {
CustomVModel
},
data() {
return {
name: '双越'
}
}
}
</script>
- CustomVModel.vue
<template>
<!-- 例如:vue 颜色选择 -->
<input type="text"
:value="text1"
@input="$emit('change1', $event.target.value)"
>
<!--
1. 上面的 input 使用了 :value 而不是 v-model
2. 上面的 change1 和 model.event1 要对应起来
3. text1 属性对应起来
-->
</template>
<script>
export default {
model: {
prop: 'text1', // 对应 props text1
event: 'change1'
},
props: {
text1: String,
default() {
return ''
}
}
}
</script>
$nextTick
<template>
<div id="app">
<ul ref="ul1">
<li v-for="(item, index) in list" :key="index">
{{item}}
</li>
</ul>
<button @click="addItem">添加一项</button>
</div>
</template>
<script>
export default {
name: 'app',
data() {
return {
list: ['a', 'b', 'c']
}
},
methods: {
addItem() {
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
this.list.push(`${Date.now()}`)
// 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
// 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
this.$nextTick(() => {
// 获取 DOM 元素
const ulElem = this.$refs.ul1
// eslint-disable-next-line
console.log( ulElem.childNodes.length )
})
}
}
}
</script>
slot
- index.vue
<template>
<div>
<!-- slot -->
<SlotDemo :url="website.url">
{{website.title}}
</SlotDemo>
//作用域插槽:让插槽内容能够访问子组件中才有的数据
<ScopedSlotDemo :url="website.url">
<template v-slot="slotProps">
{{slotProps.slotData.title}}
</template>
</ScopedSlotDemo>
</div>
</template>
<script>
import SlotDemo from './SlotDemo'
import ScopedSlotDemo from './ScopedSlotDemo'
export default {
components: {
SlotDemo,
ScopedSlotDemo,
},
data() {
return {
website: {
url: 'http://imooc.com/',
title: 'imooc',
subTitle: '程序员的梦工厂'
}
}
}
}
</script>
- SlotDemo
<template>
<a :href="url">
<slot>
默认内容,即父组件没设置内容时,这里显示
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {}
}
}
</script>
- ScopedSlotDemo
<template>
<a :href="url">
<slot :slotData="website">
{{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
</slot>
</a>
</template>
<script>
export default {
props: ['url'],
data() {
return {
website: {
url: 'http://wangEditor.com/',
title: 'wangEditor',
subTitle: '轻量级富文本编辑器'
}
}
}
}
</script>
- 具名插槽
//NamedSlot 组件
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
<NamedSlot>
<template v-slot:header>
<h1>将插入 header slot 中</h1>
</template>
<p>将插入 main slot 中,即未命名的slot</p>
<template v-slot:footer>
<p将插入 footer slot 中</p>
</template>
</NamedSlot>
动态组件
<template>
<div>
<!-- 动态组件 -->
<component :is="NextTickName"/>
</div>
</template>
<script>
import NextTick from './NextTick'
export default {
components: {
NextTick
},
data() {
return {
NextTickName: "NextTick"
}
}
}
</script>
异步组件
- index.vue
<template>
<div>
<!-- 异步组件 -->
<FormDemo v-if="showFormDemo"/>
<button @click="showFormDemo = true">show form demo</button>
</div>
</template>
<script>
export default {
components: {
FormDemo: () => import('../BaseUse/FormDemo')
},
data() {
return {
showFormDemo: false
}
}
}
</script>
缓存组件 keep-alive
- index.vue
<template>
<div>
<!-- keep-alive -->
<KeepAlive/>
</div>
</template>
<script>
import KeepAlive from './KeepAlive'
export default {
components: {
KeepAlive
},
data() {
return {
}
}
}
</script>
- KeepAlive.vue
<template>
<div>
<button @click="changeState('A')">A</button>
<button @click="changeState('B')">B</button>
<button @click="changeState('C')">C</button>
<!-- tab 切换 -->
//用keep-alive包起来
<keep-alive>
<KeepAliveStageA v-if="state === 'A'"/>
<KeepAliveStageB v-if="state === 'B'"/>
<KeepAliveStageC v-if="state === 'C'"/>
</keep-alive>
</div>
</template>
<script>
import KeepAliveStageA from './KeepAliveStateA'
import KeepAliveStageB from './KeepAliveStateB'
import KeepAliveStageC from './KeepAliveStateC'
export default {
components: {
KeepAliveStageA,
KeepAliveStageB,
KeepAliveStageC
},
data() {
return {
state: 'A'
}
},
methods: {
changeState(state) {
this.state = state
}
}
}
</script>
- KeepAliveStateA.vue
<template>
<p>state A</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('A mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('A destroyed')
}
}
</script>
- KeepAliveStateB.vue
<template>
<p>state B</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('B mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('B destroyed')
}
}
</script>
- KeepAliveStateC.vue
<template>
<p>state C</p>
</template>
<script>
export default {
mounted() {
// eslint-disable-next-line
console.log('C mounted')
},
destroyed() {
// eslint-disable-next-line
console.log('C destroyed')
}
}
</script>
mixin 抽离多个组件的公共逻辑
- index.vue
<template>
<div>
<!-- mixin -->
<MixinDemo/>
</div>
</template>
<script>
import MixinDemo from './MixinDemo'
export default {
components: {
MixinDemo
},
data() {
return {
}
}
}
</script>
- MixinDemo.vue
<template>
<div>
<p>{{name}} {{major}} {{city}}</p>
<button @click="showName">显示姓名</button>
</div>
</template>
<script>
//引入mixin.js
import myMixin from './mixin'
export default {
mixins: [myMixin], // 可以添加多个,会自动合并起来
data() {
return {
name: '双越',
major: 'web 前端'
}
},
methods: {
},
mounted() {
// eslint-disable-next-line
console.log('component mounted', this.name)
}
}
</script>
- mixin.js
export default {
data() {
return {
city: '北京'
}
},
methods: {
showName() {
// eslint-disable-next-line
console.log(this.name)
}
},
mounted() {
// eslint-disable-next-line
console.log('mixin mounted', this.name)
}
}
- 存在的问题
- 变量来源不明确,不利于阅读
- 多mixin可能会造成命名冲突
- mixin和组件可能出现多对多的关系,复杂度较高
vuex使用
vue-router
基础
- app.vue
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<!-- 跳转命名路由 -->
<router-link :to="{name:'About'}">About</router-link>
</div>
<!-- 在同一个页面,显示多个视图,命名视图 -->
<router-view/>
<router-view name="email"/>
<router-view name="tel"/>
</div>
</template>
<style lang="less">
</style>
- Home.vue
<template>
<div class="home">
编程导航
<button @click="handleClick('back')">返回上一页</button>
<button @click="handleClick('push')">跳转到argu</button>
<button @click="handleClick('replace')">跳转到parent</button>
</div>
</template>
<script>
export default {
name: 'Home',
components: {},
methods: {
handleClick (type) {
if (type === 'back') {
// this.$router.go(-1)
this.$router.back(-1)
} else if (type === 'push') {
// 有历史记录
// this.$router.push('/parent')
const name = 'minierpeng'
this.$router.push({
// 第一种:
// name: 'argu',
// query: {
// name: 'lp'
// }
// 第二种:
// name: 'argu',
// params: {
// name: 'erpeng'
// },
// 第三种:
path: `/argu/${name}`
})
} else if (type === 'replace') {
// 没有历史记录
// this.$router.replace('/parent')
this.$router.replace({
name: 'parent'
})
}
}
}
}
</script>
- argu.vue
<template>
<div>
{{$route.params.name}}
</div>
</template>
<script>
export default {
}
</script>
<style>
</style>
- router.js
import Home from '@/views/Home.vue'
export default [
// 别名路由
{
path: '/',
alias: '/home_page',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
// 懒加载,只有当访问这个路由,才会加载路由
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
},
// 动态路由
{
path: '/argu/:name',
name: 'argu',
component: () => import('@/views/argu.vue')
},
// 嵌套路由
{
path: '/parent',
name: 'parent',
component: () => import('@/views/parent.vue'),
children: [
{
path: 'child',
component: () => import('@/views/child.vue')
}
]
},
// 命名视图
{
path: '/named_view',
components: {
default: () => import('@/views/child.vue'),
email: () => import('@/views/email.vue'),
tel: () => import('@/views/tel.vue')
}
},
// 重定向路由
{
path: '/main',
// redirect:'/',
// redirect: {
// name:'Home'
// },
redirect: to => '/'
// redirect:to=>name:'Home',
}
]
进阶
路由传参
- 动态路由页面
router.js { path: '/argu/:name', name: 'argu', component: () => import('@/views/argu.vue'), //第二种, props: true// $route.params作为组件属性 } argu.vue <template> <div> <!-- {{$route.params.name}} 第一种直接取值--> {{name}} </div> </template> <script> export default { props: { name: { type: String, default: 'lp' } } } </script> - 非动态路由传参
- 第一种、对象模式传入
router.js { path: '/about', name: 'About', component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue'), //第一种、对象模式传入 props: { food: 'banana' } } about.vue <template> <div class="about"> <b>{{food}}</b> </div> </template> <script> export default { props: { food: { type: String, default: 'apple' } } } </script> - 第二种、函数模式
router.js { path: '/', name: 'Home', component: Home, props: route => ({ // 路由参数中传入的值:/?food=banana food: route.query.food }) } Home.vue <template> <div class="home"> <b>{{food}}</b> </div> </template> <script> export default { props: { food: { type: String, default: 'apple' } } } </script>
- 第一种、对象模式传入
HTML5 history模式
- 设置
在router的index.js
export default new VueRouter({
mode: 'history',
routes
})
- 配置404页面
在router.js最后配置,因为路由优先级是从上到下。
{
path: '*',
component: () => import('@/views/error_404.vue')
}
导航守卫
-
全局守卫
- 全局前置守卫
const HAS_LOGINED = false router.beforeEach((to, from, next) => { if (to.name !== 'login') { if (HAS_LOGINED) { next() } else { next({ name: 'login' }) } } else { if (HAS_LOGINED) { next({ name: 'Home' }) } else { next() } } })- 全局解析守卫
// 在导航确认之前,在组件内守卫和异步路由被解析之后,被调用 // router.beforeResolve()- 全局后置钩子
router.afterEach((to, from) => { // 取消在router.beforeEach设置的等待login的加载的效果: // logining = false }) -
路由独享守卫
{ path: '/', name: 'Home', component: Home, beforeEnter: (to, from, next) => { if (from.name === 'About') { alert('这是从about页来的') } else { alert('这不是从about页来的') } next() } } -
组件内守卫
- beforeRouteEnter
// 在渲染该组件的对应路由被确认前调用 // 不能通过this获取组件实例 // 因为当守卫执行前,组件实例还没被创建 beforeRouteEnter (to, from, next) { console.log(from.name, to.name) // 可以通过next回调函数获取组件实例 next(vm => console.log(vm)) },- beforeRouteUpdate
beforeRouteUpdate (to, from, next) { // 在当前路由改变,但是该组件被复用时调用 // 可以访问组件实例 `this` console.log(from.name, to.name) next() }- beforeRouteLeave
beforeRouteLeave (to, from, next) { // 导航离开该组件的对应路由时调用 // 可以访问组件实例 `this` const leave = confirm('您确定离开吗') if (leave) { next() } else { next(false) } }, -
完整导航解析流程
1.导航被触发 2.在失活的组件(即将离开的组件) 里调用离开守卫 beforeRouteLeave 3.调用全局的前置守卫 beforeEach 4.如果是重用的组件,则调用 beforeRouteUpdate 5.调用路由独享的守卫 beforeEnter 6.解析异步路由组件 7.在被激活的组件(即将进入的页面) 调用 beforeRouteEnter 8.调用全局解析守卫 beforeResolve 9.导航被确认 10.调用全局的后置钩子 afterEach 11.触发DOM更新 12.用创建好的实例调用beforeRouteEnter守卫里的next回调函数
路由元信息
定义路由的时候可以配置 meta 字段:
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue'),
meta: {
title: '关于'
}
}
可以在路由守卫里访问到
router.beforeEach((to, from, next) => {
console.log(to.meta)
}
路由切换动效
- 页面切换特效
<transition-group name="router">
<router-view key="default"/>
<router-view key="email" name="email"/>
<router-view key="tel" name="tel"/>
</transition-group>
<style lang="less">
// 页面进入时
// 页面即将显示
.router-enter{
opacity: 0;
}
// 页面从没有到有的过程的效果
.router-enter-active{
transition: opacity 1s ease;
}
//页面完全显示时
.router-enter-to{
opacity: 1;
}
//页面离开时
// 页面即将显离开
.router-leave{
opacity: 1;
}
// 页面从有到没有的过程的效果
.router-leave-active{
transition: opacity 1s ease;
}
//页面完全离开时
.router-leave-to{
opacity: 0;
}
</style>
- 为某个页面设置特定特效
<transition-group :name="routerTransition">
<router-view key="default"/>
<router-view key="email" name="email"/>
<router-view key="tel" name="tel"/>
</transition-group>
<script>
export default {
data () {
return {
routerTransition: ''
}
},
watch: {
'$route' (to) {
to.query && to.query.transitionName && (this.routerTransition = to.query.transitionName)
}
}
}
</script>
状态管理
Bus
Vuex
Vue 原理
如何理解MVVM
监听data变化的核心API:Object.defineProperty(Vue响应式原理)
- 简单使用
var obj = {};
var name = 'lp';
Object.defineProperty(obj,"name",{
get:function (){
//当获取值的时候触发的函数
console.log('get')
return name
},
set:function (value){
//当设置值的时候触发的函数,设置的新值通过参数value拿到
console.log('set')
name = value;
}
});
//获取值
console.log( obj.name ); //get lp
//设置值
obj.name = 'erpeng';// set
- Vue响应式原理
// 触发更新视图
function updateView() {
console.log('视图更新')
}
// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function () {
updateView() // 触发视图更新
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
// 重新定义属性,监听起来
function defineReactive(target, key, value) {
// 深度监听
observer(value)
// 核心 API
Object.defineProperty(target, key, {
get() {
return value
},
set(newValue) {
if (newValue !== value) {
// 深度监听
observer(newValue)
// 设置新值
// 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
value = newValue
// 触发更新视图
updateView()
}
}
})
}
// 监听对象属性
function observer(target) {
if (typeof target !== 'object' || target === null) {
// 不是对象或数组
return target
}
// 污染全局的 Array 原型
// Array.prototype.push = function () {
// updateView()
// ...
// }
if (Array.isArray(target)) {
target.__proto__ = arrProto
}
// 重新定义各个属性(for in 也可以遍历数组)
for (let key in target) {
defineReactive(target, key, target[key])
}
}
// 准备数据
const data = {
name: 'zhangsan',
age: 20,
info: {
address: '北京' // 需要深度监听
},
nums: [10, 20, 30]
}
// 监听数据
observer(data)
// 测试
data.name = 'lisi'
data.age = 21
// console.log('age', data.age)
// data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
// delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
// data.info.address = '上海' // 深度监听
// data.nums.push(4) // 监听数组
-
Object.defineProperty缺点
- 深度监听,需要递归到底,一次性计算量大
- 无法监听新增属性/删除属性(解决:使用Vue.set Vue.delete)
- 无法原生监听数组,需要特殊处理