Vue3 入门下

109 阅读5分钟

响应式原理

Vue 实现了 viewmodel 的双向绑定, 如下:

而响应式指的就是 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);

image.png
当我们修改数据时, 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>

image.png

getter/setters

Proxy仅对 对象类型有效(对象、数组和 Map、Set 这样的集合类型),而对 string、number 和 boolean 这样的 原始类型 无效

为了解决 reactive() 带来的限制, Vue 也提供了一个 ref() 方法来允许我们创建可以使用任何值类型的响应式 ref

ref 利用的是 JavaScriptgetter/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>

image.png 由于修改 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 对象的 countskills 都动态更新到视图上了, 因此可以看出在 Vue 中,状态都是默认深层响应式的, 这也是大多数场景下我们期望的

image.png

当你一个对象很大,嵌套很复杂的时候,这种深层的响应模式 可能会引发一些性能问题, 这个时候我们可以使用 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

image.png 当我们点到安装时, URL 会自动更新为 heading-2

image.png

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>

image.png image.png

更多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>

image.png 当然v-on这个指令也可以缩写成 @

<template>
    <button :disabled="isButtomDisabled" @click="clickButtom" >Button</button>
</template>
Class 与 Style 绑定

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-modelv-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框输入了

image.png

怎么好用的功能,怎么可能局部使用,当然要全局注册, 找到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-ifv-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>