邂逅 Vue

认识 Vue

什么是 Vue

  • Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。
    • 全程是Vue.js或者Vuejs;
    • 什么是渐进式框架呢?表示我们可以在项目中一点点来引入和使用Vue,而不一定需要全部使用Vue来开发整个 项目;

Vue3带来的变化

  • 源码通过monorepo的形式来管理源代码:
    • Mono:单个
    • Repo:repository仓库
    • 主要是将许多项目的代码存储在同一个 repository 中;
    • 这样做的目的是多个包本身相互独立,可以有自己的功能逻辑、单元测试等,同时又在同一个仓库下方便管理;
    • 而且模块划分的更加清晰,可维护性、可扩展性更强;
  • 源码使用TypeScript来进行重写:
    • 在Vue2.x的时候,Vue使用 Flow 来进行类型检测;
    • 在Vue3.x的时候,Vue的源码全部使用 TypeScript 来进行重构,并且 Vue 本身对 TypeScript 支持也更好了;
性能方面
  • 使用Proxy进行数据劫持
    • 在 Vue2.x 的时候,Vue2 是使用 Object.defineProperty 来劫持数据的 getter 和 setter 方法的;
    • 这种方式一致存在一个缺陷就是当给对象添加或者删除属性时,是无法劫持和监听的;
    • 所以在 Vue2.x 的时候,不得不提供一些特殊的API,比如 $set$delete ,事实上都是一些 hack 方法,也增加了 开发者学习新的API的成本;
    • 而在 Vue3.x 开始,Vue 使用 Proxy 来实现数据的劫持
  • 删除了一些不必要的API:
    • 移除了实例上的 $on , $off$once
    • 移除了一些特性:如filter、内联模板等;
  • 包括编译方面的优化:
    • 生成Block Tree、Slot编译优化、diff算法优化;
新的API
  • 由Options API 到 Composition API:
    • 在 Vue2.x 的时候,我们会通过 Options API 来描述组件对象;
    • Options API 包括data、props、methods、computed、生命周期等等这些选项;
    • 存在比较大的问题是多个逻辑可能是在不同的地方:
      • 比如created中会使用某一个method来修改data的数据,代码的内聚性非常差;
    • Composition API可以将 相关联的代码 放到同一处 进行处理,而不需要在多个Options之间寻找;
  • Hooks函数增加代码的复用性:
    • 在Vue2.x的时候,我们通常通过mixins在多个组件之间共享逻辑;但是有一个很大的缺陷就是 mixins也是由一大堆的Options组成的,并且多个mixins会存在命名冲突的问题;
    • 在Vue3.x中,我们可以通过Hook函数,来将一部分独立的逻辑抽取出去,并且它们还可以做到是响应式的;

如何使用 Vue

  1. 方式一:在页面中通过CDN的方式来引入;
  2. 方式二:下载Vue的JavaScript文件,并且自己手动引入;
  3. 方式三:通过npm包管理工具安装使用它;
  4. 方式四:直接通过Vue CLI创建项目,并且使用它;

CDN 引入

<script src="https://unpkg.com/vue@next"></script>

下载和引入

  • 下载Vue的源码,可以直接打开CDN的链接:
    • 打开链接,复制其中所有的代码;
    • 创建一个新的文件,比如vue.js,将代码复制到其中;
<script src="../js/vue.js"></script>

声明式编程和命令式编程

  • 原生开发和Vue开发的模式和特点,我们会发现是完全不同的,这里其实涉及到两种不同的编程范式命令式编程和声明式编程
  • 命令式编程关注的是“ how to do”,声明式编程关注的是" what to do",由框架(机器)完成"how"的过程

MVVM模型

  • MVC和MVVM都是一种软件的体系结构
    • MVC是 Model-View-Controller的简称,是在前期被使用非常框架的架构模式,比如iS、前端
    • MVVM是 Model-View- ViewMode的简称,是目前非常流行的架构模式
  • 通常情况下,我们也经常称vue是一个MVVM的框架
    • vue官方其实有说明,vue虽然并没有完全遵守MVVM的模型,但是整个设计是受到它的启发的

template属性

  • 在使用 createApp的时候,我们传入了一个对象,接下来我们详细解析一下之前传入的属性分别代表什么含义。
    • template属性:表示的是Vue需要帮助我们渲染的模板信息
    • 目前我们看到它里面有很多的HTML标签,这些标签会替换掉我们挂载到的元素(比如id为app的dⅳv)的innerHTML
    • 模板中有一些奇怪的语法,比如{},比如@ )click,这些都是模板特有的语法
  • 但是这个模板的写法有点过于别扭了,并且IDE很有可能没有任何提示,阻碍我们编程的效率
  • vue提供了两种方式:
  • 方式一:使用 script标签,并且标记它的类型为 X-template;
<body>
   <div id="app">hhhh</div>
 
   <script type="x-template" id="why">
     <div>
       <h2>{{message}}</h2>
       <h2>{{counter}}</h2>
       <button @click='increment'>+1</button>
       <button @click='decrement'>-1</button>
     </div>
   </script>
 
   <script src="../js//Vue.js"></script>
   <script>
     Vue.createApp({
       template: '#why',
       data: function(){
         return{
           message:"Hello World",
           counter: 100
         }
       },
       methods: {
         increment(){
           this.counter++
         },
         decrement(){
           this.counter--
         }
       }
     }).mount("#app")
   </script>
 </body>
  • 方式二:使用任意标签(通常使用 template标签,因为不会被浏览器渲染),设置id;v template元素是一种用于保存客户端内容的机制,该内容再加载页面时不会被呈现,但随后可以在运行时使用 JavaScript 实例化
 <body>
   <div id="app"></div>
   <template id="why">
     <div>
       <h2>{{message}}</h2>
       <h2>{{counter}}</h2>
       <button @click='increment'>+1</button>
       <button @click='decrement'>-1</button>
     </div>
   </template>
 
   <script src="../js//Vue.js"></script>
   <script>
     Vue.createApp({
       template: '#why',
       data: function(){
         return{
           message:"Hello World",
           counter: 100
         }
       },
       methods: {
         increment(){
           this.counter++
         },
         decrement(){
           this.counter--
         }
       }
     }).mount("#app")
   </script>
 </body>

data属性

  • data属性是传入一个函数,并且该函数需要返回一个对象

    • 在Vue2x的时候,也可以传入一个对象(虽然官方推荐是一个函数);
    • 在Vue3x的时候,必须传入一个函数,否则就会直接在浏览器中报错
  • data中返回的对象会被vue的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理

    • 所以我们在 template中通过{ counter} 访问 counter,可以从对象中获取到数据
    • 所以我们修改 counter的值时, template中的{ counter)也会发生改变;

methods属性(重点)

  • methods属性是一个对象,通常我们会在这个对象中定义很多的方法

    • 这些方法可以被绑定到 template模板中;
    • 在该方法中,我们可以使用this关键字来直接访问到data中返回的对象的属性;
  • 问题:官方文档有这个描述,即不能使用箭头函数

  • 为什么不能使用箭头函数(VUE3.0)?

  • 我们在methods中要使用data返回对象中的数据:

    • 那么这个this是必须有值的,并且应该可以通过this获取到data返回对象中的数据。
  • 那么我们这个this能不能是window呢?

    • 不可以是window,因为window中我们无法获取到data返回对象中的数据;
    • 但是如果我们使用箭头函数,那么这个this就会是window了;
  • 为什么是window呢?

    • 这里涉及到箭头函数使用this的查找规则,它会在自己的上层作用于中来查找this;
    • 最终刚好找到的是script作用于中的this,所以就是window;
  • this到底是如何查找和绑定的呢?

  • 不使用箭头函数的情况下,this到底指向什么

    • 事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过 bind 绑定了 this

具体看 https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA

// window 隐式绑定
foo();
const obj = {
  bar: foo
};
obj.bar();

/*
  Window{}
  {bar: f()}
*/

const foo2 = () => {
  console.log(this);
}
const obj2 = {
  bar: foo2
};

obj2.bar();
/*
  Window{}
  Window{}
*/

编写DOM元素的模板方式

  1. 方式一:template模板的方式:使用 <template> 标签编写模板。这种方式是之前经常使用的方式。
<template>
  <div>
    <h1>{{ message }}</h1>
    <p>{{ description }}</p>
  </div>
</template>
  1. 方式二:render函数的方式:使用h函数来编写渲染的内容。在这种方式中,h函数可以直接返回一个虚拟节点(Vnode节点)。
export default {
  render(h) {
    return h('div', [
      h('h1', this.message),
      h('p', this.description)
    ])
  }
}
  1. 方式三:通过.vue文件中的template来编写模板:在.vue文件中使用<template>标签编写模板。这种方式需要通过特定的代码来对模板进行解析:
  • 对于方式三,即.vue文件中的template,可以通过vue-loader对其进行编译和处理。
  • 对于方式一,即template模板,我们必须通过源码中的一部分代码来进行编译。
<template>
  <div>
    <h1>{{ message }}</h1>
    <p>{{ description }}</p>
  </div>
</template>

因此,Vue在让我们选择版本时,提供了"运行时+编译器"和"仅运行时"两种选项:

  • 运行时+编译器:这个版本包含了Vue的完整功能,包括对template模板的编译代码。在使用这个版本时,我们可以直接在代码中使用template模板,并且Vue会在运行时将其编译成渲染函数。这使得我们可以在开发过程中更方便地编写和调试模板。然而,由于包含了编译代码,这个版本的文件体积相对较大。
  • 仅运行时:这个版本不包含对template模板的编译代码。它更小巧,文件体积更小。在使用这个版本时,我们需要使用render函数来手动编写渲染逻辑,而不是直接使用template模板。这意味着我们需要更多的代码来实现相同的功能,但也带来了更好的性能和更小的文件大小。
  • 选择哪个版本取决于项目的需求和优化目标。如果我们需要更完整的功能,并且对文件大小没有太大的担忧,可以选择运行时+编译器版本。如果我们对文件大小敏感,并且愿意手动编写渲染逻辑,可以选择仅运行时版本。

VSCode对SFC文件的支持

Vue 的单文件组件 (即 *.vue 文件,英文 Single-File Component,简称 SFC) 是一种特殊的文件格式,使我们能够将一个 Vue 组件的模板、逻辑与样式封装在单个文件中。下面是一个单文件组件的示例:

为什么要使用 SFC​ 使用 SFC 必须使用构建工具,但作为回报带来了以下优点:

  • 使用熟悉的 HTML、CSS 和 JavaScript 语法编写模块化的组件

  • 让本来就强相关的关注点自然内聚

  • 预编译模板,避免运行时的编译开销

  • 组件作用域的 CSS

  • 在使用组合式 API 时语法更简单

  • 通过交叉分析模板和逻辑代码能进行更多编译时优化

  • 更好的 IDE 支持,提供自动补全和对模板中表达式的类型检查

  • 开箱即用的模块热更新 (HMR) 支持

  • SFC 是 Vue 框架提供的一个功能,并且在下列场景中都是官方推荐的项目组织方式:

  • 单页面应用 (SPA)

  • 静态站点生成 (SSG)

  • 任何值得引入构建步骤以获得更好的开发体验 (DX) 的项目

https://cn.vuejs.org/guide/scaling-up/sfc.html

  1. 插件一:Vetur,从Vue2开发就一直在使用的VSCode支持Vue的插件;
  2. 插件二:Volar,官方推荐的插件(后续会基于Volar开发官方的VSCode插件);

如何阅读Vue源码

  • 需要的环境 npm,yarn
  • 操作步骤
    1. 安装yarn npm install yarn -g
    2. 在项目中配置yarn yarn install
    3. 在package.json中的dev后加上–sourcemap
    4. 打包项目 yarn dev (在vue/dist文件夹下有两个文件,vue.global.js和vue.global.js.map)
    5. 在vue/examplex新建自己的文件夹以及测试demo
    6. 在demo中打下断点—debugger
    7. 在浏览器中打开调试面板,选择其中的source面板,查看执行对应的源码

Vue3 基本指令

VSCode 代码片段

  • 我们在前面练习Vue的过程中,有些代码片段是需要经常写的,我们再VSCode中我们可以生成一个代码片段,方便我们快速生成。
  • VSCode中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。
  • 具体的步骤如下:
  • 直接 Tab trigger 即可自动填充

image-20231016233051010

模板语法

  • React的开发模式[了解]
    • React使用的jsx,所以对应的代码都是编写的类似于js的一种语法
    • 之后通过Babe将js编译成 React. create Element函数调用
function () {
  return <div></div>
}
  • vue也支持 jsx 的开发模式:
    • 但是大多数情况下,使用基于HTML的模板语法
    • 在模板中,允许开发者以声明式的方式将 DOM 和底层组件实例的数据绑定在-起;口在底层的实现中,vue将模板编译成虚拟DOM渲染函数
<template>
  <div @click v-bind v-once> {{}} </div>
</template>

Mustache 语法 双大括号语法

  • 如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。
    • 并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中;
    • 当data中的数据发生改变时,对应的内容也会发生更新。
    • 当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。
  • mustache的使用
    1. 基本使用 2. 表达式 3. 函数 4. 三元运算符
<template id="my-app">
  <!-- 1.mustache的基本使用 -->
  <h2>{{message}} - {{message}}</h2>
  <!-- 2.是一个表达式 -->
  <h2>{{counter * 10}}</h2>
  <h2>{{ message.split(" ").reverse().join(" ") }}</h2>
  <!-- 3.也可以调用函数 -->
  <!-- 可以使用computed(计算属性) -->
  <h2>{{getReverseMessage()}}</h2>
  <!-- 4.三元运算符 -->
  <h2>{{ isShow ? "哈哈哈": "" }}</h2>
  <button @click="toggle">切换</button>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        message: "Hello World",
        counter: 100,
        isShow: true
      }
    },
    methods: {
      getReverseMessage() {
        return this.message.split(" ").reverse().join(" ");
      },
      toggle() {
        this.isShow = !this.isShow;
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

以下为错误写法

<!-- 错误用法 -->
var name = "abc" ;

<h2>{{var name = "abc"}}</h2>
<h2>{{ if(isShow) {  return "哈哈哈" } }}</h2>

不常用指令

v-once指令

  • v-once用于指定元素或者组件只渲染一次

    • 当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过;

    • 该指令可以用于性能优化;

      <h2 v-once>{{counter}}</h2>
      <button @click="increment">+1</button>
      
  • 如果是子节点的化,也只能渲染一次

    <div v-once>
         <h2>{{counter}}</h2>
         <h2>{{message}}</h2>
     </div>
     <button @click="increment">+1</button>
    

完整代码

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

<template id="my-app">
  <h2>{{counter}}</h2>
  <div v-once>
    <h2>{{counter}}</h2>
    <h2>{{message}}</h2>
  </div>
  <button @click="increment">+1</button>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        counter: 100,
        message: "abc"
      }
    },
    methods: {
      increment() {
        this.counter++;
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

v-html

  • 默认情况下,如果我们展示的内容本身是 html 的,那么vue并不会对其进行特殊的解析。
    • 如果我们希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示
<template id="my-app">
  <div>{{msg}}</div>
  <div v-html="msg"></div>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        msg: '<span style="color:red; background: blue;">哈哈哈</span>'
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

image-20231018083558572

v-text

  • 用于更新元素的 textContent
 <h2 v-text="message"></h2>
 <!-- 等价于 -->
 <h2>{{message}}</h2>

v-pre

  • v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签:
    • 跳过不需要编译的节点,加快编译的速度
 <template id="my-app">
   <h2 v-pre>{{message}}</h2>
 </template>
<!-- {{message}} -->

v-cloak

  • 用于隐藏尚未完成编译的 DOM 模板。
    • 无需传入
    • 详细信息
  • 该指令只在没有构建步骤的环境下需要使用。
    • 当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。
    • v-cloak 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像 [v-cloak] { display: none } 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。
[v-cloak] {
  display: none;
}
<div v-cloak>
  {{ message }}
</div>

直到编译完成前,<div> 将不可见。

v-bind

动态的绑定一个或多个 attribute,也可以是组件的 prop。

  • 缩写:: 或者 . (当使用 .prop 修饰符)

  • 期望:any (带参数) | Object (不带参数)

  • 参数:attrOrProp (可选的)

  • 修饰符

    • .camel - 将短横线命名的 attribute 转变为驼峰式命名。
    • .prop - 强制绑定为 DOM property。3.2+
    • .attr - 强制绑定为 DOM attribute。3.2+
  • 用途

    当用于绑定 classstyle attribute,v-bind 支持额外的值类型如数组或对象。详见下方的指南链接。

    在处理绑定时,Vue 默认会利用 in 操作符来检查该元素上是否定义了和绑定的 key 同名的 DOM property。如果存在同名的 property,则 Vue 会将它作为 DOM property 赋值,而不是作为 attribute 设置。这个行为在大多数情况都符合期望的绑定值类型,但是你也可以显式用 .prop.attr 修饰符来强制绑定方式。有时这是必要的,特别是在和 自定义元素打交道时。

    当用于组件 props 绑定时,所绑定的 props 必须在子组件中已被正确声明。

    当不带参数使用时,可以用于绑定一个包含了多个 attribute 名称-绑定值对的对象。

  • 用法

    • 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。

小知识: vue3 是允许template中有多个根元素

<!-- vue2 template模板中只能有一个根元素 -->
<!-- vue3 是允许template中有多个根元素 -->
<template id="my-app">
    <!-- 1.v-bind的基本使用 -->
    <img v-bind:src="imgUrl" alt="">
    <a v-bind:href="link">百度一下</a>

    <!-- 2.v-bind提供一个语法糖 : -->
    <img :src="imgUrl" alt="">
    <img src="imgUrl" alt="">
</template>

基本使用

<template id="my-app">
    <!-- 1.v-bind的基本使用 -->
    <img v-bind:src="imgUrl" alt="">
    <a v-bind:href="link">百度一下</a>

    <!-- 2.v-bind提供一个语法糖 : -->
    <img :src="imgUrl" alt="">
    <!-- 以下报错 -->
    <img src="imgUrl" alt="">
</template>

<script>
    const App = {
        template: '#my-app',
        data() {
            return {
                imgUrl: "https://avatars.githubusercontent.com/u/10335230?s=60&v=4",
                link: "https://www.baidu.com"
            }
        }
    }
    Vue.createApp(App).mount('#app');
</script>

绑定 class

  • 在开发中,有时候我们的元素 class也是动态的,比如
    • 当数据为某个状态时,字体显示红色。
    • 当数据另一个状态时,字体显示黑色
  • 绑定class有两种方式:
    • 对象语法
    • 数组语法
对象语法
  • 对象语法:我们可以传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class
<body>
  <div id="app"></div>

  <template id="my-app">
    <div :class="className">哈哈哈哈</div>
    <!-- 对象语法: {'active': boolean} -->
    <div :class="{'active': isActive}">呵呵呵呵</div>
    <button @click="toggle">切换</button>

    <!-- 也可以有多个键值对 -->
    <div :class="{active: isActive, title: true}">呵呵呵呵</div>

    <!-- 默认的class和动态的class结合 -->
    <div class="abc cba" :class="{active: isActive, title: true}">
      呵呵呵呵
    </div>

    <!-- 将对象放到一个单独的属性中 -->
    <div class="abc cba" :class="classObj">呵呵呵呵</div>

    <!-- 将返回的对象放到一个methods(computed)方法中 -->
    <div class="abc cba" :class="getClassObj()">呵呵呵呵</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: "#my-app",
      data() {
        return { 
          className: "why",
          isActive: true,
          title: "abc",
          classObj: {
            active: true,
            title: true
          },
        };
      },
      methods: {
        toggle() {
          this.isActive = !this.isActive;
          this.classObj.active = !this.classObj.active;
        },
        getClassObj() {
          return this.classObj;
        }
      },
    };

    Vue.createApp(App).mount("#app");
  </script>

  <style>
    .active {
      color: red;
    }
  </style>
</body>
数组语法
  • 绑定class – 数组语法
    • 数组语法:我们可以把一个数组传给 :class,以应用一个 class 列表
<body>
  <div id="app"></div>

  <template id="my-app">
    <div :class="['abc', title]">哈哈哈哈</div>
   <!-- class="abc cba active" -->
    <div :class="['abc', title, isActive ? 'active': '']">哈哈哈哈</div>
 <!-- 可以嵌套对象语法 -->
    <div :class="['abc', title, {active: isActive}]">哈哈哈哈</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          title: "cba",
          isActive: true
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

绑定 style

  • 我们可以利用 v-bind:style 来绑定一些CSS内联样式
    • 这次因为某些样式我们需要根据数据动态来决定
    • 比如某段文字的颜色,大小等等
  • CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名
  • 绑定class有两种方式
    • 对象语法
    • 数组语法

CSS property 名短横线分隔 (kebab-case,记得用引号括起来)

对象语法
<body>
  <div id="app"></div>

  <template id="my-app">
    <!-- :style="{cssPropertyName: cssPropertyValue}" -->
    <div :style="{color: finalColor, 'font-size': '30px'}">哈哈哈哈</div>
    <div :style="{color: finalColor, fontSize: '30px'}">哈哈哈哈</div>
    <div :style="{color: finalColor, fontSize: finalFontSize + 'px'}">哈哈哈哈</div>

    <!-- 绑定一个data中的属性值, 并且是一个对象 -->
    <div :style="finalStyleObj">呵呵呵呵</div>
    <!-- 调用一个方法 -->
    <div :style="getFinalStyleObj()">呵呵呵呵</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          finalColor: 'red',
          finalFontSize: 50,
          finalStyleObj: {
            'font-size': '50px',
            fontWeight: 700,
            backgroundColor: 'red'
          }
        }
      },
      methods: {
        getFinalStyleObj() {
          return {
            'font-size': '50px',
            fontWeight: 700,
            backgroundColor: 'red'
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>
数组语法
<body>
  <div id="app"></div>

  <template id="my-app">
    <div :style="[style1Obj, style2Obj]">哈哈哈</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          style1Obj: {
            color: 'red',
            fontSize: '30px'
          },
          style2Obj: {
            textDecoration: "underline"
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body

动态绑定属性

  • 在某些情况下,我们属性的名称可能也不是固定的
    • 前端我们无论绑定src、href、class、style,属性名称都是固定的
    • 如果属性名称不是固定的,我们可以使用 :[属性名]=“值” 的格式来定义
    • 这种绑定的方式,我们称之为 动态绑定属性;如下:
<body>
  <div id="app"></div>

  <template id="my-app">
    <!-- <div cba="kobe">哈哈哈</div> -->
    <div :[name]="value">哈哈哈</div>
    <img :src="" alt="">
    <a :href=""></a>
    <div :class></div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          name: "cba",
          value: "kobe"
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

属性直接绑定一个对象

  • 如果我们希望将一个对象的所有属性,绑定到元素上的所有属性,应该怎么做呢?
    • 非常简单,我们可以直接使用 v-bind 绑定一个 对象
  • 案例:info对象会被拆解成div的各个属性
<body>
  <div id="app"></div>

  <template id="my-app">
      <!-- <div name="why" age="18" height="1.88">哈哈哈哈</div> -->
    <div v-bind="info">哈哈哈哈</div>
    <div :="info">哈哈哈哈</div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          info: {
            name: "why",
            age: 18,
            height: 1.88
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

v-on

  • v-on绑定事件

    • 前面我们绑定了元素的内容和属性,在前端开发中另外一个非常重要的特性就是交互。
    • 在前端开发中,我们需要经常和用户进行各种各样的交互
      • 这个时候,我们就必须监听用户发生的事件,比如点击、拖拽、键盘事件等等
      • 在Vue中如何监听事件呢?使用v-on指令。
  • v-on的用法

    • 缩写:@

    • 预期:Function | Inline Statement | Object

    • 参数:event p 修饰符:

      • .stop - 调用 event.stopPropagation()。
      • .prevent - 调用 event.preventDefault()。
      • .capture - 添加事件侦听器时使用 capture 模式。
      • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
      • .{keyAlias} - 仅当事件是从特定键触发时才触发回调。
      • .once - 只触发一次回调。
      • .left - 只当点击鼠标左键时触发。
      • .right - 只当点击鼠标右键时触发。
      • .middle - 只当点击鼠标中键时触发。
      • .passive - { passive: true } 模式添加侦听器
    • 用法:绑定事件监听

event 事件可以参考 https://developer.mozilla.org/en-US/docs/Web/Events

基本使用

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

  <template id="my-app">
    <!-- 完整写法: v-on:监听的事件="methods中方法" -->
    <button v-on:click="btn1Click">按钮1</button>
    <div class="area" v-on:mousemove="mouseMove">div</div>
    <!-- 语法糖 -->
    <button @click="btn1Click">按钮1</button>
    <!-- 绑定一个表达式: inline statement -->
    <button @click="counter++">{{counter}}</button>
    <!-- 绑定一个对象 -->
    <div class="area" v-on="{click: btn1Click, mousemove: mouseMove}"></div>
    <div class="area" @="{click: btn1Click, mousemove: mouseMove}"></div>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World",
          counter: 100
        }
      },
      methods: {
        btn1Click() {
          console.log("按钮1发生了点击");
        },
        mouseMove() {
          console.log("鼠标移动");
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

参数传递

  • 当通过methods中定义方法,以供 @click调用时,需要注意参数问题:
  • 情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。
    • 但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
  • 情况二:如果需要同时传入某个参数,同时需要 event 时,可以通过 $event 传入事件。
<body>
  <div id="app"></div>

  <template id="my-app">
    <!-- 默认传入event对象, 可以在方法中获取 -->
    <button @click="btn1Click">按钮1</button>
    <!-- $event可以获取到事件发生时的事件对象 -->
    <button @click="btn2Click($event, 'coderwhy', 18)">按钮2</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World"
        }
      },
      methods: {
        btn1Click(event) {
          console.log(event);
        },
        btn2Click(event, name, age) {
          console.log(name, age, event);
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

image-20231019004604619

v-on 的修饰符

  • v-on支持修饰符,修饰符相当于对事件进行了一些特殊的处理:
    • .stop - 调用 event.stopPropagation()。
    • .prevent - 调用 event.preventDefault()。
    • .capture - 添加事件侦听器时使用 capture 模式。
    • .self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
    • .{keyAlias} - 仅当事件是从特定键触发时才触发回调。
    • .once - 只触发一次回调。
    • .left - 只当点击鼠标左键时触发。
    • .right - 只当点击鼠标右键时触发。
    • .middle - 只当点击鼠标中键时触发。
    • .passive - { passive: true } 模式添加侦听器

stopPropagation 是一个事件修饰符,用于阻止事件冒泡。在 Vue.js 中,当一个元素上的事件被触发时,它会先执行该元素上的事件处理函数,然后再冒泡到该元素的父元素,继续执行父元素的事件处理函数。使用 stopPropagation 可以阻止事件继续冒泡到父元素。在给元素绑定事件时,可以使用 @click.stop 来阻止 click 事件冒泡到父元素。

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

  <template id="my-app">
    <div @click="divClick">
      <button @click="btnClick">按钮</button>
      <button @click.stop="btnClick">按钮</button>
    </div>
    <input type="text" @keyup.enter="enterKeyup">
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          message: "Hello World"
        }
      },
      methods: {
        divClick() {
          console.log("divClick");
        },
        btnClick() {
          console.log('btnClick');
        },
        enterKeyup(event) {
          console.log("keyup", event.target.value);
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

image-20231019005251770

条件渲染

  • 在某些情况下,我们需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了。
    • Vue提供了下面的指令来进行条件判断:
      • v-if
      • v-else
      • v-else-if
      • v-show

基本使用

  • v-if、v-else、v-else-if用于根据条件来渲染某一块的内容:
    • 这些内容只有在条件为true时,才会被渲染出来;
    • 这三个指令与JavaScript的条件语句if、else、else if类似;
 <template id="my-app">
     <input type="text" v-model="score">
     <h2 v-if="score > 90">优秀</h2>
     <h2 v-else-if="score > 60">良好</h2>
     <h2 v-else>不及格</h2>
 </template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        score: 95
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

template 和 v-if 结合使用

  • v-if的渲染原理:
    • v-if是惰性的;
    • 当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉;
    • 当条件为true时,才会真正渲染条件块中的内容;
  • template元素
    • 因为v-if是一个指令,所以必须将其添加到一个元素上:
      • 但是如果我们希望切换的是多个元素呢?
      • 此时我们渲染div,但是我们并不希望div这种元素被渲染;
      • 这个时候,我们可以选择使用template;
    • template元素可以当做不可见的包裹元素,并且在v-if上使用,但是最终template不会被渲染出来:
      • 有点类似于小程序中的block
<body>  
  <div id="app"></div>

  <template id="my-app">
    <template v-if="isShowHa">
      <h2>哈哈哈哈</h2>
      <h2>哈哈哈哈</h2>
      <h2>哈哈哈哈</h2>
    </template>

    <template v-else>
      <h2>呵呵呵呵</h2>
      <h2>呵呵呵呵</h2>
      <h2>呵呵呵呵</h2>
    </template>

    <button @click="isShowHa = !isShowHa">切换</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          isShowHa: true
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

v-show

  • v-show和v-if的用法看起来是一致的,也是根据一个条件决定是否显示元素或者组件
 <template id="my-app">
     <h2 v-show="isShow">哈哈哈哈</h2>
 </template>

v-show和v-if的区别

  • 首先,在用法上的区别:
    • v-show是不支持template;
    • v-show不可以和v-else一起使用;
  • 其次,本质的区别:
    • v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行 切换;
    • v-if当条件为false时,其对应的元素压根不会被渲染到DOM中;
  • 开发中如何进行选择呢?
    • 如果我们的元素需要在显示和隐藏之间频繁的切换,那么使用v-show;
    • 如果不会频繁的发生切换,那么使用v-if;
<template id="my-app">
  <h2 v-if="isShow">哈哈哈哈</h2>
  <h2 v-show="isShow">呵呵呵呵</h2>
</template>

image-20231019222841008

列表渲染

  • 在真实开发中,我们往往会从服务器拿到一组数据,并且需要对其进行渲染。
    • 这个时候我们可以使用v-for来完成;
    • v-for类似于JavaScript的for循环,可以用于遍历一组数据;

基本使用

  • n v-for的基本格式是 “item in 数组”:

    • 数组通常是来自data或者prop,也可以是其他方式;
    • item是我们给每项元素起的一个别名,这个别名可以自定来定义;
  • 我们知道,在遍历一个数组的时候会经常需要拿到数组的索引:

    • 如果我们需要索引,可以使用格式: “(item, index) in 数组”;
    • 注意上面的顺序:数组元素项item是在前面的,索引项index是在后面的;
  • v-for支持的类型

    • v-for也支持遍历对象,并且支持有一二三个参数:
      • 一个参数: “value in object”;
      • 二个参数: “(value, key) in object”;
      • 三个参数: “(value, key, index) in object”;
    • v-for同时也支持数字的遍历:
      • 每一个item都是一个数字;
<body>
  <div id="app"></div>

  <template id="my-app">
    <h2>电影列表</h2>
    <ul>
      <!-- 遍历数组 -->
      <li v-for="(movie, index) in movies">{{index+1}}.{{movie}}</li>
    </ul>
    <h2>个人信息</h2>
    <ul>
      <!-- 遍历对象 -->
      <li v-for="(value, key, index) in info">{{value}}-{{key}}-{{index}}</li>
    </ul>
    <h2>遍历数字</h2>
    <ul>
      <li v-for="(num, index) in 10">{{num}}-{{index}}</li>
    </ul>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          movies: [
            "星际穿越",
            "盗梦空间",
            "大话西游",
            "教父",
            "少年派"
          ],
          info: {
            name: "why",
            age: 18,
            height: 1.88
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

template元素使用

  • 类似于v-if,你可以使用 template 元素来循环渲染一段包含多个元素的内容:
    • 我们使用template来对多个元素进行包裹,而不是使用div来完成;
<body>
  <div id="app"></div>

  <template id="my-app">
    <ul>
      <template v-for="(value, key) in info">
        <li>{{key}}</li>
        <li>{{value}}</li>
        <li class="divider"></li>
      </template>
    </ul>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          info: {
            name: "why",
            age: 18,
            height: 1.88
          }
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

数组更新检测

  • Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
    • push()
    • pop()
    • shift()
    • unshift()
    • splice()
    • sort()
    • reverse()
  • 替换数组的方法
    • 上面的方法会直接修改原来的数组,但是某些方法不会替换原来的数组,而是会生成新的数组,比如 filter()、 concat() 和 slice()。
<body>
  <div id="app"></div>

  <template id="my-app">
    <h2>电影列表</h2>
    <ul>
      <li v-for="(movie, index) in movies">{{index+1}}.{{movie}}</li>
    </ul>
    <input type="text" v-model="newMovie">
    <button @click="addMovie">添加电影</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          newMovie: "",
          movies: [
            "星际穿越",
            "盗梦空间",
            "大话西游",
            "教父",
            "少年派"
          ]
        }
      },
      methods: {
        addMovie() {
          this.movies.push(this.newMovie);
          this.newMovie = "";

          // filter 是过滤的意思, 下文中的代码的意思是: 过滤掉长度小于等于2的电影
          // this.movies = this.movies.filter(item => item.length > 2);
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

v-for 中的 key 是什么作用?

https://cn.vuejs.org/api/built-in-special-attributes.html#key

  • 在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性。

  • 这个key属性有什么作用呢?我们先来看一下官方的解释:

    • key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;

    • 如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;

    • 而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;

  • 官方的解释对于初学者来说并不好理解,比如下面的问题:

    • 什么是新旧nodes,什么是VNode?
    • 没有key的时候,如何尝试修改和复用的?
    • 有key的时候,如何基于key重新排列的?
认识 VNode
  • 我们先来解释一下VNode的概念:
    • 因为目前我们还没有比较完整的学习组件的概念,所以目前我们先理解HTML元素创建出来的VNode;
    • VNode的全称是Virtual Node,也就是虚拟节点;
    • 事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;
    • VNode的本质是一个JavaScript的对象;可以用于描述某一个标签/ 元素 的样子
    • 好处: 多平台的渲染, 跨平台(主要好处)

image-20231020162812819

虚拟 DOM
  • 如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个 VNode Tree
  • 虚拟 DOM 与 真实 DOM 不一定一一对应

image-20231020163154944

插入 F 的案例
  • 我们先来看一个案例:这个案例是当我点击按钮时会在中间插入一个f;

  • 我们可以确定的是,这次更新对于ul和button是不需要进行更新,需要更新的是我们 li 的列表:

    • 在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列 表;

    • 因为对于列表中 a、b、c、d它们都是没有变化的;

    • 在操作真实DOM的时候,我们只需要在中间插入一个 f 的 li 即可;

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

  <template id="my-app">
    <ul>
      <li v-for="item in letters" :key="item">{{item}}</li>
    </ul>
    <button @click="insertF">插入F元素</button>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          letters: ['a', 'b', 'c', 'd']
        }
      },
      methods: {
        insertF() {
          this.letters.splice(2, 0, 'f')
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>
  • 那么Vue中对于列表的更新究竟是如何操作的呢?
    • Vue事实上会对于有key和没有key会调用两个不同的方法;
Vue源码对于key的判断
  • 有key,那么就使用 patchKeyedChildren方法;
  • 没有key,那么久使用 patchUnkeyedChildren方法;

image-20231020164733530

diff算法: diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁。

  • 没有key的操作过程

image-20231022182341619

  • 我们会发现上面的diff算法效率并不高:
    • c和d来说它们事实上并不需要有任何的改动;
    • 但是因为我们的c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;

image-20231022182012027

  • 有key的操作过程-diff算法
    • 第一步的操作是从头开始进行遍历、比较:
      • a和b是一致的会继续进行比较;
      • c和f因为key不一致,所以就会break跳出循环
    • 第二步的操作是从尾部开始进行遍历、比较
    • 第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
    • 第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:
    • 第五步是最特色的情况,中间还有很多未知的或者乱序的节点:

image-20231022182232738

image-20231022182140133

image-20231022182241976

image-20231022182251535

image-20231022182300781

有无key的结论
  • 有无key的结论
    • 所以我们可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:
      • 在没有key的时候我们的效率是非常低效的;
      • 在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;

Vue 的 Options API

Computed

认识计算属性

我们知道,在模板中可以直接通过插值语法显示一些data中的数据。

但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;

  • 比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
  • 在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
  • 在模板中放入太多的逻辑会让模板过重和难以维护;
  • 并且如果多个地方都使用到,那么会有大量重复的代码;

我们有没有什么方法可以将逻辑抽离出去呢?

  • 可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;
  • 但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;
  • 另外一种方式就是使用计算属性computed;

什么是计算属性呢?

  • https://cn.vuejs.org/api/reactivity-core.html#computed
  • 官方并没有给出直接的概念解释;
  • 而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性
  • 计算属性将被混入到组件实例中。所有 getter 和 setter 的 this 上下文自动地绑定为组件实例;

基本使用

计算属性的用法:

  • 选项: computed
  • 类型:{ [key: string]: Function | { get: Function, set: Function } }

我们来看三个案例:

  • 我们有两个变量:firstName 和 lastName,希望它们拼接之后在界面上显示;
  • 我们有一个分数:score
    • 当score大于60的时候,在界面上显示及格;
    • 当score小于60的时候,在界面上显示不及格;
  • 我们有一个变量message,记录一段文字:比如Hello World
    • 某些情况下我们是直接显示这段文字;
    • 某些情况下我们需要对这段文字进行反转;
  • 我们可以有三种实现思路:
    • 思路一:在模板语法中直接使用表达式;
    • 思路二:使用method对逻辑进行抽取;
    • 思路三:使用计算属性computed;
在模板语法中直接使用表达式
  • 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算);
  • 缺点二:当有多次一样的逻辑时,存在重复的代码;
  • 缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
<template id="my-app">
    <!-- 1.实现思路一: -->
    <h2>{{ firstName + lastName }}</h2>
    <h2>{{ score >= 60 ? "及格": "不及格" }}</h2>
    <h2>{{ message.split("").reverse().join(" ") }}</h2>
</template>
使用method对逻辑进行抽取
  • 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用;
  • 缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
<template id="my-app">
  <h2>{{getFullName()}}</h2>
  <h2>{{getResult()}}</h2>
  <h2>{{getReverseMessage()}}</h2>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        firstName: "Kobe",
        lastName: "Bryant",
        score: 80,
        message: "Hello World"
      }
    },
    methods: {
      getFullName() {
        return this.firstName + " " + this.lastName;
      },
      getResult() {
        return this.score >= 60 ? "及格": "不及格";
      },
      getReverseMessage() {
        return this.message.split(" ").reverse().join(" ");
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>
computed 实现
  • 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到;
  • 我们会发现无论是直观上,还是效果上计算属性都是更好的选择;
  • 并且计算属性是有缓存的;
<template id="my-app">
  <h2>{{fullName}}</h2>
  <h2>{{result}}</h2>
  <h2>{{reverseMessage}}</h2>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        firstName: "Kobe",
        lastName: "Bryant",
        score: 80,
        message: "Hello World"
      }
    },
    computed: {
      // 定义了一个计算属性叫fullname
      fullName() {
        return this.firstName + " " + this.lastName;
      },
      result() {
        return this.score >= 60 ? "及格": "不及格";
      },
      reverseMessage() {
        return this.message.split(" ").reverse().join(" ");
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

计算属性 vs methods

在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属性有缓存。

接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:

<template id="my-app">
  <button @click="changeFirstName">修改firstName</button>

  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>
  <h2>{{fullName}}</h2>

  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
  <h2>{{getFullName()}}</h2>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        firstName: "Kobe",
        lastName: "Bryant"
      }
    },
    computed: {
      // 计算属性是有缓存的, 当我们多次使用计算属性时, 计算属性中的运算只会执行一次.
      // 计算属性会随着依赖的数据(firstName)的改变, 而进行重新计算.
      fullName() {
        console.log("computed的fullName中的计算");
        return this.firstName + " " + this.lastName;
      }
    },
    methods: {
      getFullName() {
        console.log("methods的getFullName中的计算");
        return this.firstName + " " + this.lastName;
      },
      changeFirstName() {
        this.firstName = "Coder"
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

打印结果如下:

  • 我们会发现methods在多次使用时,会调用多次;
  • 而计算属性虽然使用了多次,但是计算的过程只调用了一次;
  • 这是因为计算属性会基于它们的依赖关系进行缓存;
  • 在数据不发生变化时,计算属性是不需要重新计算的;
  • 但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;如下:
<body>
  <div id="app"></div>

  <template id="my-app">
    
    <input type="text" v-model="score">
    <!-- 1.使用methods -->
    <h2>{{getResult()}}</h2>
    <h2>{{getResult()}}</h2>
    <h2>{{getResult()}}</h2>

    <!-- 2.使用computed -->
    <h2>{{result}}</h2>
    <h2>{{result}}</h2>
    <h2>{{result}}</h2>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          score: 90
        }
      },
      computed: {
        result() {
          console.log("调用了计算属性result的getter");
          return this.score >= 60 ? "及格": "不及格";
        }
      },
      methods: {
        getResult() {
          console.log("调用了getResult方法");
          return this.score >= 60 ? "及格": "不及格";
        }
      }
    }
    
    Vue.createApp(App).mount('#app');
  </script>
</body>

当 score 变化, console 输出如下:

调用了getResult方法
调用了getResult方法
调用了getResult方法
调用了计算属性result的getter

计算属性的 setter 和 getter

计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。

但是,如果我们确实想设置计算属性的值呢?

  • 这个时候我们也可以给计算属性设置一个setter的方法;
<template id="my-app">
  <button @click="changeFullName">修改fullName</button>
  <h2>{{fullName}}</h2>
</template>

<script src="../js/vue.js"></script>
<script>
  const App = {
    template: '#my-app',
    data() {
      return {
        firstName: "Kobe",
        lastName: "Bryant"
      }
    },
    computed: {

      // fullName 的 getter方法
      fullName() {
        return this.firstName + " " + this.lastName;
      },
      
      // fullName的getter和setter方法
      fullName: {
        get: function() {
          return this.firstName + " " + this.lastName;
        },
        set: function(newValue) {
          console.log(newValue);
          const names = newValue.split(" ");
          this.firstName = names[0];
          this.lastName = names[1];
        }
      }
    },
    methods: {
      changeFullName() {
        this.fullName = "Coder Why";
      }
    }
  }

  Vue.createApp(App).mount('#app');
</script>

以下为内部判断

图片

侦听器 watch

用于声明在数据更改时调用的侦听回调。watch 选项期望接受一个对象,其中键是需要侦听的响应式组件实例属性 (例如,通过 datacomputed 声明的属性)——值是相应的回调函数。该回调函数接受被侦听源的新值和旧值。

  • 什么是侦听器?
    • 开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;
    • 当数据变化时,template会自动进行更新来显示最新的数据;
    • 但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;
  • 用法如下:
    • 选项:watch
    • 类型: {[key: string]: string | Function | Object | Array}

简单案例

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

  <template id="my-app">
    您的问题: <input type="text" v-model="question">
    <button @click="queryAnswer">查找答案</button>

    <p>您的问题是: {{ question }}</p>
    <p>答案是: {{ anwser }}</p>
  </template>

  <script src="../js/vue.js"></script>
  <script>
    const App = {
      template: '#my-app',
      data() {
        return {
          // 侦听question的变化时, 去进行一些逻辑的处理(JavaScript, 网络请求)
          question: "Hello World",
          anwser: ""
        }
      },
      watch: {
        // question侦听的data中的属性的名称
        // newValue变化后的新值
        // oldValue变化前的旧值
        question: function(newValue, oldValue) {
          console.log("新值: ", newValue, "旧值", oldValue);
          this.queryAnswer();
        }
      },
      methods: {
        queryAnswer() {
          // console.log(`你的问题${this.question}的答案是哈哈哈哈哈`);
          this.anwser = `你的问题${this.question}的答案是哈哈哈哈哈`;
        }
      }
    }

    Vue.createApp(App).mount('#app');
  </script>
</body>

配置选项

  • watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:
  • 以下为不使用深度监听, 当 info.name 在方法中被赋值改变时, 页面会改变, 但是watch不会侦听到, 理由如上