响应式原理
Vue 实现了 view 和 model 的双向绑定, 如下:
而响应式指的就是 model (数据)有变化时,能反馈到 view 上, 当然这个反馈是由 view 实例来帮我们完成的, 那 view 怎么知道 model(数据)有变化呢?
JavaScript Proxy
答案之一是JavaScript Proxy, 其行为表现与一般对象相似。不同之处在于 Vue 能够跟踪并执行对响应式对象 property 的访问与更改操作
//.js文件
const monster1 = { eyeCount: 4 };
const handler1 = {
set(obj, prop, value) {
if ((prop === 'eyeCount') && ((value % 2) !== 0)) {
console.log('Monsters must have an even number of eyes');
return true
} else {
return Reflect.set(...arguments);
}
}
};
const proxy1 = new Proxy(monster1, handler1); //接收一个target 和一个 ProxyHandler
proxy1.eyeCount = 1
console.log(proxy1.eyeCount);
proxy1.eyeCount = 2;
console.log(proxy1.eyeCount);
当我们修改数据时, Vue 实例就会感知到, 使用 JS 操作dom(vdom), 完成试图的更新。
那 Vue 必须提供一个构造函数用于初始化 原始对象,这样 Vue 才能跟踪数据变化, Vue 提供一个reactive函数 就是用来干这个的
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
trigger(target, key)
target[key] = value
}
})
}
下面我们修改About例子, 并做验证
<template>
<div class="about">
<h1>{{ person.name }}</h1>
<input v-model="person.name" type="text" />
</div>
</template>
<script>
// 以库的形式来使用vue实例提供的API
import { reactive } from "vue";
export default {
// `setup` 是一个专门用于组合式 API 的特殊钩子
setup() {
// 使用reactive 构造Proxy对象, 这样vue才能跟踪对象变化
const person = reactive({ name: "lfd" });
// 暴露 person 到模板
return {
person,
};
},
};
</script>
getter/setters
Proxy仅对 对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效
为了解决 reactive() 带来的限制, Vue 也提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref
ref 利用的是 JavaScript 的 getter/setters 的方式劫持属性访问
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
trigger(refObject, 'value')
value = newValue
}
}
return refObject
}
向上面我们如果不使用对象, 而是使用一个字符串,就需要使用ref来声明一个响应式变量
<template>
<div class="about">
<!-- 这里为啥没有使用 name.value来访问喃?
当 ref 在模板中作为顶层 property 被访问时,它们会被自动“解包”,所以不需要使用 .value -->
<h2>{{ name }}</h2>
<input v-model="name" type="text" /> // v-model随后讲解
</div>
</template>
<script>
import { ref } from "vue";
export default {
setup() {
// 使用ref来为基础类型 构造响应式变量
const name = ref("lfd");
// 通过value来设置 基础类型的值(Setter方式)
name.value = "ljy";
return {
name
}
}
}
</script>
setup 语法
在 setup() 函数中手动暴露状态和方法可能非常繁琐
<script>
// 以库的形式来使用vue实例提供的API
import { reactive } from "vue";
export default {
setup() {
// 暴露 数据 到模板
return {}
}
}
</script>
幸运的是, 你可以通过使用构建工具来简化该操作。当使用单文件组件(SFC)时,我们可以使用 <script setup> 来简化大量样板代码
<script setup>
import { ref } from "vue";
const name = ref("lfd");
name.value = "ljy";
</script>
大多数 Vue 开发者在开发应用时都会基于: 单文件组件 + <script setup> 的语法的方式, 这也是我们后面常见的写法
DOM异步更新
注意这是一个很重要的概念, 当你响应式数据发送变化时, DOM 也会自动更新, 但是这个更新并不是同步的,而是异步的: Vue 会将你的变更放到一个缓冲队列, 等待更新周期到达时, 一次性完成 DOM (视图)的更新。
比如下面这个例子:
<script setup>
import { onMounted, ref } from "vue";
let name = ref("lwy");
// 只有等模版挂载好后,我门才能获取到对应的HTML元素
onMounted(() => {
name = "ljy";
console.log(document.getElementById("name").innerText); //在template里的h1标签上加上id="name"
});
</script>
由于修改 name 后, 并没有立即更新 DOM , 所以获取到的 name 依然是初始值, 那如果想要获取到当前值怎么办?
Vue 在 DOM 更新时 为我们提供了一个钩子: nextTick()
该钩子就是当 Vue 实例 到达更新周期后, 更新完 DOM 后,留给我们操作的口子, 因此我们改造下:
<script setup>
import { nextTick, onMounted, ref } from "vue";
let name = ref("lwy");
onMounted(() => {
name = "ljy";
// 等待vue下次更新到来后, 执行下面的操作
nextTick(() => {
console.log(document.getElementById("name").innerText);
});
});
</script>
一定要理解 Vue 的异步更新机制, 因为你写的代码 并不是按照你的预期同步执行的, 这是引起很多魔幻 bug 的根源
深层响应性
通过上面我们知道 reactive 初始化的 Proxy 对象是响应式的, 那如果我这个对象里面再嵌套对象, 那嵌套的对象还是不是响应式的呢?
<template>
<div class="about">
<h1 id="name">{{ person }}</h1>
<input v-model="person.name" type="text" />
<input v-model="skill" @keyup.enter="addSkill(skill)" type="text" />//回车后调用addSkill
</div>
</template>
<script setup>
import { reactive, ref } from "vue";
let skill = ref("");
// 使用ref来为基础类型 构造响应式变量
let person = reactive({
name: "ljy",
profile: { city: "北京" },
skills: ["Golang", "Vue"],
});
let addSkill = (s) => {
person.skills.push(s);
person.profile.skill_count = person.skills.length;
};
</script>
我们可以看到,当修改了嵌套的数组 skills 时, persom 对象的 count 和 skills 都动态更新到视图上了, 因此可以看出在 Vue 中,状态都是默认深层响应式的, 这也是大多数场景下我们期望的
当你一个对象很大,嵌套很复杂的时候,这种深层的响应模式 可能会引发一些性能问题, 这个时候我们可以使用 Vue 提供的 shallowReactive 创建一个浅层响应式的数据
// 使用shallowReactive 构造浅层响应式数据, 当数据有变化时,不会理解反馈到界面上
let person = shallowReactive({
name: "ljy",
profile: { city: "北京" },
skills: ["Golang", "Vue"],
});
ref vs reactive
ref 不仅可以用于构造基础类型, 同时也支持用于构造复合类型, 比如对象和数组, 简而言之 reactive 能实现的 ref 也能实现:
<script setup>
import { ref } from "vue";
let skill = ref("");
// 使用ref来为基础类型 构造响应式变量
let person = ref({
name: "张三",
profile: { city: "北京" },
skills: ["Golang", "Vue"],
});
let addSkile = (s) => {
person.value.skills.push(s);
person.value.profile.skill_count = person.value.skills.length;
};
</script>
那 ref 对象是如何兼容 reactive 的呢? 答案很简单,ref 函数会判断传递过来的的值是 复合类型还是简单类型, 如果是复合类型, 比如对象与数组 就会通过 reactive 将其转化为一个深层响应式的 Proxy对象
由于 ref 可以控制到基础类型的力度, 而复合对象可以认为是基础对象的上层封装, 所以很大部分场景下 我们都可以直接使用 ref 代替 reactive
而且由于 ref 控制力度细的问题, 我们可以基于它来构造一个响应式对象,比如:
<template>
<div class="about">
<h2 id="name">{{ person }}</h2>
<input v-model="person.name.value" type="text" />
<input v-model="skill" @keyup.enter="addSkile(skill)" type="text" />
</div>
</template>
<script setup>
import { ref } from "vue";
let skill = ref("");
// 使用ref来构造一个对象
let person = {
name: ref("ljy"),
profile: ref({ city: "北京" }),
skills: ref(["Golang", "Vue"]),
};
// 等价于一个reactive初始化出来的proxy对象
// let person = ref({
// name: "ljy",
// profile: { city: "北京" },
// skills: ["Golang", "Vue"],
// });
let addSkile = (s) => {
person.skills.value.push(s);
person.profile.skill_count = person.skills.value.length;
};
</script>
这样构造出来的对象还是另一个好处, 它在解构赋值时, 解构后的变量依然是响应式的, 可以思考下为啥?
<script setup>
import { ref } from "vue";
let skill = ref("");
// 使用ref来构造一个对象
let person = {
name: ref("张三"),
profile: ref({ city: "北京" }),
skills: ref(["Golang", "Vue"]),
};
// 解构赋值
let { name, profile, skills } = person;
</script>
由于 Proxy 是一个对象,它的响应式是与该对象绑定, 如果对象一旦被解开了, 而对象的属性本身又不具备响应式,响应式就中断了, 而使用 ref 就不会
侦听器
一个简单的需求: 我们一个页面有多个参数, 用户可能把 URL 复制给别人, 我们需要不同的 URL 看到页面内容不同, 不然用户每次到这个页面都是第一个页面
这个就需要我们监听 URL 参数的变化, 然后视图做调整, vue-router 会有个全局属性: $route, 我们可以监听它的变化
由于没引入 vue-router ,那我们如何监听 URL 的变化 window 提供一个事件回调:
window.onhashchange = function () {
console.log('URL发生变化了', window.location.hash);
this.urlHash = window.location.hash
};
在快速上手目录项上我们的 URL 后面的hash为 heading-1
当我们点到安装时, URL 会自动更新为
heading-2
vue 提供的属性 watch 语法如下:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
watch(question, newQuestion => { // question为监视对象, newQuestion为箭头函数参数
if (newQuestion.indexOf('?') > -1) { // 如果输入框内有 ? 的话answer的值改为hello
answer.value = 'hello'
}else{
answer.value = 'Questions usually contain a question mark. ;-)'
}
})
</script>
<template>
<p>
Ask a yes/no question:
<input v-model="question" />
</p>
<p>{{ answer }}</p>
</template>
更多watch用法请参考: 侦听器
模板语法
通过 template 标签定义的部分都是 Vue 的模板, 模版会被 vue-template-compiler 编译后渲染
<template>
...
</template>
访问变量
文本值
在 Vue 的模板中, 我们直接使用 {{ ref_name }} 的方式访问到 JS 部分定义变量(包含响应式和非响应式)
<script setup>
import { ref } from "vue";
const count = ref(0);
</script>
<template>
<button @click="count++">You clicked me {{ count }} times.</button>
</template>
表达式
除了能在模版系统中直接访问到这些变量, 可以在模板系统中 直接使用 JS 表达式, 这对象处理简单逻辑很有用
<template>
<div>{{ name.split('').reverse().join('') }}</div>
</template>
<script setup>
import { ref } from "vue";
const name = ref('');
</script>
计算属性
如果 model 的数据并不是你要直接渲染的,需要处理再展示, 简单的方法是使用表达式,比如
<h2>{{ name.split('').reverse().join('') }}</h2>
这种把数据处理逻辑嵌入的视图中,并不合适, 不易于维护, 我们可以把改成一个方法
<h2>{{ reverseData(name.value) }}</h2>
<script setup>
import { ref } from "vue";
const name = ref('');
</script>
但是使用函数 每次访问该属性都需要调用该函数计算, 如果数据没有变化( Vue 是响应式的它知道数据有没有变化),我们能不能把函数计算出的属性缓存起来,直接使用喃?
Vue把这个概念定义为计算属性,使用 computed 钩子定义:
// 一个计算属性 ref
const reverseName = computed({
// getter
get() {
return name.vaule.split('').reverse().join('')
},
// setter
set(newValue) {
name.value = newValue.split(' ').reverse().join('')
}
})
我们修改为计算属性:
<h2>{{ reverseName }}</h2>
<script setup>
import { ref } from "vue";
const name = ref('');
const reverseName = computed({
get() {
return name.vaule.split('').reverse().join('')
},
set(newValue) {
name.value = newValue.split(' ').reverse().join('')
}
})
</script>
如果我们只有get 没有set方法 也可以简写为:
const reverseName = computed(() => {name.vaule.split('').reverse().join('')})
响应式绑定
模版的变量只能作用于文本值部分, 并不能直接作用于 HTML 元素的属性, 比如下面属性:
- id
- class
- style
变量不能作用在 HTML attribute 上, 比如下面的语法就是错误的
<template>
<!-- html属性id 无法直接访问到变量 -->
<div id={{ name }}>
<!-- 文本值变量 语法ok -->
{{ name }}
</div>
</template>
元素属性
针对 HTML 元素的属性 Vue 专门提供一个 v-bind 指令, 这个指令就是模版引擎里面的一个函数, 他专门帮你完成 HTML 属性变量替换, 语法如下:
v-bind:name="name"(该双引号name为变量) ==> name="name.value"
那我们修改下
<template>
<!-- html属性id 无法直接访问到变量 -->
<div v-bind:id="name">
<!-- 文本值变量 语法ok -->
{{ name }}
</div>
</template>
v-binding 有个缩写: : 等价于 v-bind:
<template>
<!-- html属性id 无法直接访问到变量 -->
<div :id="name">
<!-- 文本值变量 语法ok -->
{{ name }}
</div>
</template>
因此我们可以直接使用 :attr 来为 HTML 的属性绑定变量
元素事件
如果我要给 buttom 这个元素绑定一个事件应该如何写
参考: HTML 事件
原生的写法:
<button onclick="copyText()">复制文本</button>
对于 Vue 的模板系统来说, copyText 这个函数如何渲染, 他不是一个文本,而是一个函数
Vue针对事件专门定义了一个指令: v-on , 语法如下:
v-on:eventName="eventHandler"
eventName: 事件的名称
eventHandler: 处理这个事件的函数
比如 下面我们为button绑定一个点击事件:点击过后不能再次点击了
<template>
<button :disabled="isButtomDisabled" v-on:click="clickButtom" >Button</button>
</template>
<script setup>
import { ref } from "vue";
const isButtomDisabled = ref(false);
const clickButtom() => {isButtomDisabled.value = !isButtomDisabled.value}
</script>
当然v-on这个指令也可以缩写成
@
<template>
<button :disabled="isButtomDisabled" @click="clickButtom" >Button</button>
</template>
Class 与 Style 绑定
骚包的指令
vue遇到不好解决的问题,就定义一个指令, 官方内置了一些指令:
- v-model: 双向绑定的数据
- v-bind: html元素属性绑定
- v-on: html元素事件绑定
- v-if: if 渲染
- v-show: 控制是否显示
- v-for: for 循环
上面的例子 只是指令的简单用法, 指令的完整语法如下:
v-directive:argument.modifier.modifier...
v-directive: 表示指令名称, 如v-on
argument: 表示指令的参数, 比如click
modifier: 修饰符,用于指出一个指令应该以特殊方式绑定
比如当用户按下回车时, 表示用户输入完成, 触发搜索
v-directive: 需要使用绑定事件的指令: v-on
argument: 监听键盘事件: keyup, 按键弹起时
modifier: 监听Enter建弹起时
因此完整写发: v-on:keyup.enter
<template>
<input v-model="name" type="text" @keyup.enter="pressEnter">
</template>
<script>
export default {
name: 'lfd',
data() {
return {
name: 'ljy',
}
},
methods: {
pressEnter() {
alert("点击了回车键")
}
},
}
</script>
最后需要注意事件的指令的函数是可以接受参数的
<template>
<input v-model="name" type="text" @keyup.enter="pressEnter(name)">
</template>
函数是直接读到 model 数据的, 因此别用 {{ }} , 如果要传字符串 使用 ''
修饰符可以玩出花, 具体的请看官方文档
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button v-on:click.ctrl="onClick">A</button>
<!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button v-on:click.ctrl.exact="onCtrlClick">A</button>
<!-- 没有任何系统修饰符被按下的时候才触发 -->
<button v-on:click.exact="onClick">A</button>
自定义指令
除了核心功能默认内置的指令 (v-model 和 v-show),Vue 也允许注册自定义指令, 别问,问就是你需要
比如用户进入页面让输入框自动聚焦, 方便快速输入, 比如登陆页面, 快速聚焦到 username 输入框
如果是 HTML 元素聚焦, 我们找到元素, 调用 focus 就可以了, 如下:
let inputE = document.getElementsByTagName('input')
inputE[0].focus()
添加到mounted中进行测试:
mounted() {
let inputE = document.getElementsByTagName('input')
inputE[0].focus()
}
如何将这个功能做成一个 Vue 的指令喃? 比如 v-focus
我们先注册一个局部指令, 在本组件中使用
<template>
<input v-focus v-model="name" />
</template>
<script>
export default {
name: "lfd",
data() {
return {
name: "ljy",
};
},
directives: {
focus: {
mounted: (el) => {
el.focus();
},
},
},
};
</script>
这里我们注册的指令名字叫 focus , 所有的指令在模版要加一个v前缀, 因此我们的指令就是 v-focus
我们现在一进入页面键盘就直接能在input框输入了
怎么好用的功能,怎么可能局部使用,当然要全局注册, 找到main.js 配置自定义指令
// 注册一个全局自定义指令 `v-focus`
app.directive("focus", {
mounted: (el) => {
el.focus();
},
});
删除局部指令进行测试
条件渲染
有2个指令用于在模版中控制条件渲染:
v-if: 控制元素是否创建, 创建开销较大v-show: 控制元素是否显示, 对象无效销毁,开销较小
v-if 完整语法:
<h1 v-if="" >
<h1 v-else-if="" >
<h1 v-else="" >
v-show完整语法:
<h1 v-show="" >
比如更加用户输入, 判断当前分数的等级
<input v-model="name" >
<div v-if="name >= 90">
A
</div>
<div v-else-if="name >= 80">
B
</div>
<div v-else-if="name >= 60">
C
</div>
<div v-else-if="name >= 0">
D
</div>
<div v-else>
请输入正确的分数
</div>
这些HTML元素都需要动态创建, 我们换成 v-show 看看
<input v-model="name" >
<div v-show="name >= 90">
A
</div>
<div v-show="name >= 80 && name < 90">
B
</div>
<div v-show="name >= 60 && name < 80">
C
</div>
<div v-show="name >= 0 && name < 60">
D
</div>
我们可在元素中看到只是简单地基于 CSS 进行切换
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好
列表渲染
v-for元素的列表渲染, 语法如下:
<t v-for="(item, index) in items" :key="item.message">
{{ item.message }}
</t>
<!-- items: [
{ message: 'Foo' },
{ message: 'Bar' }
] -->
如果你不使用index, 也可以省略, 比如:
<template>
<ul>
<li v-for="item in items" :key="item.message">
{{ item.message }}
</li>
</ul>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
items: [
{ message: 'Foo' },
{ message: 'Bar' }
]
}
},
}
</script>
v-for 除了可以遍历列表,可以遍历对象, 比如我们套2层循环, 先遍历列表,再遍历对象
<ul>
<li v-for="(item, index) in items" :key="item.message">
{{ item.message }} - {{ index}}
<br>
<span v-for="(value, key) in item" :key="key"> {{ value }} {{ key }} <br></span>
</li>
</ul>
<script>
export default {
name: 'HelloWorld',
data() {
return {
items: [
{ message: 'Foo', level: 'info' },
{ message: 'Bar', level: 'error'}
]
}
}
}
</script>
我们也可以在console界面里进行数据修改测试
$vm._data.items.push({message: "num4", level: "pannic"})
$vm._data.items.pop()
注意事项:
- 不推荐在同一元素上使用
v-if和v-for, 请另外单独再起一个元素进行条件判断
比如
<li v-for="todo in todos" v-if="!todo.isComplete">
{{ todo }}
</li>
请改写成下面方式:
<ul v-if="todos.length">
<li v-for="todo in todos">
{{ todo }}
</li>
</ul>
<p v-else>No todos left!</p>