【Vue】course_1

一、vue简介

Vue是一款用于构建用户界面的 JavaScript 框架。

它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。

无论是简单还是复杂的界面,Vue 都可以胜任。

二、vue3选项式API

绝大多数和vue2相同

1.vue3初探

1.1 MVX

目标:理解MVVM、MVC、MVP

MV系列框架中,M和V分别指Model层和View层,但其功能会因为框架的不同而变化。

Model层是数据模型,用来存储数据;

View层是视图,展示Model层的数据。

虽然在不同的框架中,Model层和View层的内容可能会有所差别,但是其基础功能不变,变的只是 数据的传输方式

1.1.1 MVC(Model-View-Controller)

MVC是模型-视图-控制器,它是MVC、MVP、MVVM这三者中最早产生的框架,其他两个框架是以它为基础发展而来的。

MVC的目的就是将M和V的代码分离,且MVC是单向通信,必须通过Controller来承上启下。

$ npx express express-app --view=ejs
# npx 项目生成器的管理工具
# express node项目的生成器
# epxress-app 项目名称-自己起名
# --view=ejs 前端的模版
# 如果npx 创建不成  cnpm i express-generator -g  
# 如果npx 创建不成  express express-app --view=ejs
# 如果还不行 找一个创建的项目,拷贝过来,安装依赖
$ cd express-app
$ cnpm i
# npm i
$ cnpm run start
#  http://localhost:3000

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mhku4Gqg-1672136291417)(assets/image-20221212141903204.png)]

Model:模型层,数据模型及其业务逻辑,是针对业务模型建立的数据结构,Model与View无关,而与业务有关。

View:视图层,用于与用户实现交互的页面,通常实现数据的输入和输出功能。

Controller:控制器,用于连接Model层和View层,完成Model层和View层的交互。还可以处理页面业务逻辑,它接收并处理来自用户的请求,并将Model返回给用户。

// express-app/mysql/db.js  -- 可替换自己熟悉的写法
// 链接数据库
const mongoose = require('mongoose')
const DB_URL = "mongodb://localhost:27017/ty2206"

mongoose.connect(DB_URL)

mongoose.connection.on('connected', () => {
  console.log('数据库连接成功')
})

mongoose.connection.on('disconnected', () => {
  console.log('数据库连接断开')
})

mongoose.connection.on('error', (err) => {
  console.log('数据库连接失败' + err)
})

module.exports = mongoose
// express-app/mysql/collections/User.js
const mongoose = require('../db')

const Schema = mongoose.Schema
// MVC 中 M
const userSchema = new Schema({
  userId: {
    required: true,
    type: String
  },
  userName: {
    type: String
  },
  password: String
})

// users 数据库中的集合名称
module.exports = mongoose.model(userSchema, 'users')
// express-app/routes/index.js
var express = require('express');
var router = express.Router();
// var User = require('../mysql/collections/User')
/* GET home page. */
// MVC 中的 C
router.get('/', function(req, res, next) {
  // User.find().exec((err, data) => {
  //  if (err) throw err
  //  res.render 渲染哪一个页面
  //  res.render('index', { title: 'Express', data });
  // })
  // 模拟数据库操作
  res.render('index', { title: 'Express', data: [
    { userId: 'user1', userName: '吴大勋' },
    { userId: 'user2', userName: '纪童伟' },
  ] });
});

module.exports = router;

<!-- express-app/views/index.ejs-->
<!DOCTYPE html>
<html>
  <head>
    <title><%= title %></title>
    <link rel='stylesheet' href='/stylesheets/style.css' />
  </head>
  <body>
    <h1><%= title %></h1>
    <p>Welcome to <%= title %></p>
    <h1>用户列表如下</h1>
    <table>
      <tr>
        <th>序号</th>
        <th>id</th>
        <th>姓名</th>
      </tr>
      <% for(var i = 0; i < data.length; i++) { %>
        <tr>
          <td><%= i + 1 %></td>
          <td><%= data[i].userId %></td>
          <td><%= data[i].userName %></td>
        </tr>
      <% } %>
      
    </table>
  </body>
</html>

遇到条件控制语句 使用 <% %>包裹,遇到变量 使用 <%= %> 或者 <%- %>包裹

  • <%= %> 原样输出 - 转义输出。---- innerText
  • <%- %> 解析输出。 ---- innerHTML

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F5bOGgAW-1672136291419)(assets/image-20220908001836530.png)]

上图可以看出各部分之间的通信是单向的,呈三角形状。

具体MVC框架流程图如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mpPpjclM-1672136291420)(assets/image-20220908002149336.png)]

从上图可以看出,Controller层触发View层时,并不会更新View层中的数据,View层的数据是通过监听Model层数据变化自动更新的,与Controller层无关。换言之,Controller存在的目的是确保M和V的同步,一旦M改变,V应该同步更新。

同时,我们可以看到,MVC框架大部分逻辑都集中在Controller层,代码量也集中在Controller层,这带给Controller层很大压力,而已经有独立处理事件能力的View层却没有用到;而且,Controller层与View层之间是一一对应的,断绝了View层复用的可能,因而产生了很多冗余代码。

MVC 房东 -房客 -中介

为了解决上述问题,MVP框架被提出

1.1.2 MVP(Model-View-Presenter)

MVP是模型-视图-表示器,它比MVC框架大概晚出现20年,是从MVC模式演变而来的。它们的基本思想有相同之处:Model层提供数据,View层负责视图显示,Controller/Presenter层负责逻辑的处理。将Controller改名为Presenter的同时改变了通信方向。

Model:模型层,用于数据存储以及业务逻辑。

View:视图层,用于展示与用户实现交互的页面,通常实现数据的输入和输出功能。

Presenter:表示器,用于连接M层、V层,完成Model层与View层的交互,还可以进行业务逻辑的处理。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yynBcpph-1672136291420)(assets/image-20220908002243164.png)]

上图可以看出各部分之间的通信是双向的。

在MVC框架中,View层可以通过访问Model层来更新,但在MVP框架中,View层不能再直接访问Model层,必须通过Presenter层提供的接口,然后Presenter层再去访问Model层。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ri5NAsii-1672136291421)(assets/image-20220908002315234.png)]

从上图可以看出,View层和Model层互不干涉,View层也自由了很多,所以View层可以抽离出来做成组件,在复用性上就比MVC框架好很多。

但是,由于View层和Model层都需要经过Presenter层,导致Presenter层比较复杂,维护起来也会有一定的问题;而且,因为没有绑定数据,所有数据都需要Presenter层进行“手动同步”,代码量较大,虽然比起MVC框架好很多,但还是有比较多冗余部分。

为了让View层和Model层的数据始终保持一致,MVVM框架出现了。

1.1.3 MVVM(Model-View-ViewModel)

MVVM是模型-视图-视图模型。MVVM与MVP框架区别在于:MVVM采用双向绑定:View的变动,自动反映在ViewModel,反之亦然。

Model:数据模型(数据处理业务),指的是后端传递的数据。

View:视图,将Model的数据以某种方式展示出来。

ViewModel:视图模型,数据的双向绑定(当Model中的数据发生改变时View就感知到,当View中的数据发生变化时Model也能感知到),是MVVM模式的核心。ViewModel 层把 Model 层和 View 层的数据同步自动化了,解决了 MVP 框架中数据同步比较麻烦的问题,不仅减轻了 ViewModel 层的压力,同时使得数据处理更加方便——只需告诉 View 层展示的数据是 Model 层中的哪一部分即可。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lK62X8eN-1672136291422)(assets/image-20220908002410800.png)]

上图可以看出各部分之间的通信是双向的,而且我们可以看出,MVVM框架图和MVP框架图很相似,两者都是从View层开始触发用户的操作,之后经过第三层,最后到达Model层。而关键问题就在于这第三层的内容,Presenter层是采用手动写方法来调用或修改View层和Model层;而ViewModel层双向绑定了View层和Model层,因此,随着View层的数据变化,系统会自动修改Model层的数据,反之同理。

具体MVVM框架流程图如图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UZT7sr6m-1672136291423)(assets/image-20220908002444206.png)]

从上图可以看出,View层和Model层之间的数据传递经过了ViewModel层,ViewModel层并没有对其进行“手动绑定”,不仅使速度有了一定的提高,代码量也减少很多,相比于MVC框架和MVP框架,MVVM框架有了长足的进步。

从MVVM第一张图可以看出,MVVM框架有大致两个方向:

1、模型–>视图 ——实现方式:数据绑定

2、视图–>模型 ——实现方式:DOM事件监听

存在两个方向都实现的情况,叫做数据的双向绑定。双向数据绑定可以说是一个模板引擎,它会根据数据的变化实时渲染。如图View层和Model层之间的修改都会同步到对方。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dMZvwCca-1672136291423)(assets/image-20220908002601014.png)]

MVVM模型中数据绑定方法一般有四种:

  • 数据劫持vue2 - Object.defineProperty
  • 原生Proxy vue3
  • 发布-订阅模式
  • 脏值检查

Vue2.js使用的就是数据劫持和发布-订阅模式两种方法。了解Vue.js数据绑定流程前,我们需要了解这三个概念:

  • Observer:数据监听器,用于监听数据变化,如果数据发生改变,不论是在View层还是在Model层,Observer都会知道,然后告诉Watcher。
  • Compiler:指定解析器,用于对数据进行解析,之后绑定指定的事件,在这里主要用于更新视图。
  • Watcher:订阅者。

首先将需要绑定的数据劫持方法找出来,之后用Observer监听这堆数据,如果数据发生变化,Observer就会告诉Watcher,然后Watcher会决定让那个Compiler去做出相应的操作,这样就完成了数据的双向绑定。

vue3.js使用更快的原生 Proxy,消除了之前 Vue2.x 中基于 Object.defineProperty 的实现所存在的很多限制:无法监听 属性的添加和删除、数组索引和长度的变更,并可以支持 Map、Set、WeakMap 和 WeakSet!

带来的特性:

vue3.0实现响应式

Proxy支持监听原生数组

Proxy的获取数据,只会递归到需要获取的层级,不会继续递归

Proxy可以监听数据的手动新增和删除

Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~

1.2 vue特性

目标:理解声明式,对比传统DOM开发

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E3vBHV7q-1672136291424)(assets/image-20220913091144383.png)]

Vue从设计角度来讲,虽然能够涵盖这张图上所有的东西,但是你并不需要一上手就把所有东西全用上,都是可选的。

声明式渲染和组件系统是Vue的核心库所包含内容,而路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,我们可以在核心的基础上任意选用其他的部件(以插件形势使用),不一定要全部整合在一起。

Vue.js的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进DOM的系统。

假设需要输出 “hello ty2206”

准备工作:cnpm

https://npmmirror.com/

$ npm install -g cnpm --registry=https://registry.npmmirror.com

以后就可以使用cnpm 代替 npm

如果遇到类似于以下**psl这种错误

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uF93TXmt-1672136291425)(assets/image-20220914103517117.png)]

只需要找到这个文件删除即可(这个错误只会出现在windows电脑下)

补充:如果使用cnpm出现 randomUUID is not a function,解决方法

$ npm uninstall -g cnpm
$ npm install cnpm@7.1.0 -g

传统开发模式的原生js,jQuery代码如下:

<div id="test"></div>
<!--原生js-->
<script>
  const msg = "hello ty2206"
  const test = document.getElementById('test')
  test.innerHTML = msg 
  // test.innerText = msg  
  // test.textContent=""
</script>
<!--jQuery-->
<script>
	var msg = 'hello ty2206'
  $('#test').html(msg) 
  // $('#test').text(msg)
</script>
$ cnpm i vue jquery  # 临时安装,不会出现package.json文件

拷贝 node_modules/vue/dist/vue.global.js以及 vue.global.prod.js,还有 jquery/dist/jquery.js以及jquery.min.js到lib文件夹

完整代码:01_base/01_before.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>传统的DOM操作</title>
</head>
<body>
  <div id="jsDOM"></div>
  <div id="jqDOM"></div>
</body>
<script src="../lib/jquery.min.js"></script>
<script>
  const str = 'hello ty2206'

  const jsDOM = document.getElementById('jsDOM')
  // jsDOM.innerHTML = str
  // jsDOM.innerText = str
  jsDOM.textContent = str

  // $('#jqDOM').html(str)
  $('#jqDOM').text(str)

</script>
</html>

02_vue3.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3解决DOM操作</title>
</head>
<body>
  {{ str }}
  <div id="app">
    <div>{{ str }}</div>
    <ul>
      <li v-for="item in list">{{ item }}</li>
    </ul>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = Vue

  const app = createApp({
    data () {
      return {
        str: 'hello ty2206',
        list: ['a', 'b', 'c', 'd']
      }
    }
  })

  app.mount('#app')
</script>
</html>

01_base/03_vue2.html

$ cnpm i vue@2 

拷贝 vue下的 vue.js 以及vue.min.js到lib文件夹

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2解决DOM操作</title>
</head>
<body>
  {{ str }}
  <div id="app">
    <div>{{ str }}</div>
    <ul>
      <li v-for="item in list">{{ item }}</li>
    </ul>
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  new Vue({
    data: { // new Vue实例时使用对象,其余时刻使用函数
      str: 'hello ty2206',
      list: ['a', 'b', 'c', 'd', 'e']
    }
  }).$mount('#app')

  
</script>
</html>

1.3 vue3十大新特性

* setup   ----   组合式API
* ref     ----   组合式API
* reactive  ----   组合式API
* 计算属性computed  ----   组合式API 以及 选项式API
* 侦听属性watch     ----   组合式API 以及 选项式API
* watchEffect函数  ----   组合式API
* 生命周期钩子      ----   组合式API 以及 选项式API
* 自定义hook函数   ----   组合式API
* toRef和toRefs   ----   组合式API 以及 选项式API
* 其他新特性
  * shallowReactive 与 shallowRef ----   组合式API 
  * readonly 与 shallowReadonly ----   组合式API 
  * toRaw 与 markRaw ----   组合式API 
  * customRef ----   组合式API
  * provide 与 inject ----   组合式API 以及 选项式API
  * 响应式数据的判断 ----   组合式API 以及 选项式API
* 新的组件  ----- 类似于新增的HTML标签
  * Fragment
  * Teleport
  * Suspense
* 其他变化
  * data选项应始终被声明为一个函数
  * 过渡类名的更改
  * 移除keyCode作为 v-on 的修饰符,同时也不再支持config.keyCodes  --- 事件处理
  * 移除v-on.native修饰符  --- 事件处理
  * 移除过滤器(filter)  ---  单独讲解vue2和vue3差异化

1.4 创建第一个vue3应用

每个 Vue 应用都是通过 createApp 函数创建一个新的 应用实例

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

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})
app.mount('#app')

简单计数器案例:01_base/04_counter.html

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3示例</title>
  </head>
  <body>
    <div id="app">
      {{ msg }}
      <div>
        {{ count }}
        <div>count的double: {{ count * 2 }}</div>
        <!-- 体验点击事件 -->
        <button @click="count++">1</button>
      </div>
    </div>
  </body>
  <script src="../lib/vue.global.js"></script>
  <script>
    // 解构 创建应用实例的 函数
    const { createApp } = Vue // 有的人喜欢写 window.Vue

    // 创建应用实例
    const app = createApp({ // 当前应用实例的选项
      data () { // 书写形式为函数,表示vue实例中需要使用的 数据的变量,必须含有返回值,返回值为对象
        return {
          msg: 'hi ty2206',
          count: 10
        }
      }
    })

    // 应用实例挂载
    app.mount('#app')
  </script>
  </html>

1.我们传入 createApp 的对象实际上是一个组件,每个应用都需要一个“根组件”,其他组件将作为其子组件。

应用实例必须在调用了 .mount() 方法后才会渲染出来。该方法接收一个“容器”参数,可以是一个实际的 DOM 元素或是一个 CSS 选择器字符串:

1.5 API风格

参考链接

目标:选项式API以及组合式API如何选择

01_base/05_composition.html

  <!DOCTYPE html>
  <html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3_组合式API</title>
  </head>
  <body>
    <div id="app">
      {{ msg }}
      <div>
        {{ count }}
        <div>count的double: {{ count * 2 }}</div>
        <!-- 体验点击事件 -->
        <button @click="count++">加1</button>
      </div>
    </div>
  </body>
  <script src="../lib/vue.global.js"></script>
  <script>
    // 解构 创建应用实例的 函数
    // ref 代表 组合式API 创建数据时的一个响应式的标识
    const { createApp, ref } = Vue // 有的人喜欢写 window.Vue

    // 创建应用实例
    const app = createApp({ // 当前应用实例的选项
      setup () { // 组合式API的标志
        const msg = ref('hello ty2206!') // msg 的初始值
        const count = ref(100) // count 的初始值

        // 数据需要在views视图响应,需要将其返回去
        return {
          msg,
          count
        }
      }
    })

    // 应用实例挂载
    app.mount('#app')
  </script>
  </html>

使用组合式API可以

  • 更好的逻辑复用
  • 更灵活的代码组织
  • 更好的类型推导
  • 更小的生产包体积

选项式 API 确实允许你在编写组件代码时“少思考”,这是许多用户喜欢它的原因。然而,在减少费神思考的同时,它也将你锁定在规定的代码组织模式中,没有摆脱的余地,这会导致在更大规模的项目中难以进行重构或提高代码质量。在这方面,组合式 API 提供了更好的长期可维护性。

组合式 API 能够覆盖所有状态逻辑方面的需求

一个项目可以同时使用两种API

选项式API不会被抛弃

2.模板与指令

2.1 模板语法

学习:插值表达式、js表达式、v-cloak

Vue 使用一种基于 HTML 的模板语法,使我们能够声明式地将其组件实例的数据绑定到呈现的 DOM 上。所有的 Vue 模板都是语法层面合法的 HTML,可以被符合规范的浏览器和 HTML 解析器解析。

在底层机制中,Vue 会将模板编译成高度优化的 JavaScript 代码。结合响应式系统,当应用状态变更时,Vue 能够智能地推导出需要重新渲染的组件的最少数量,并应用最少的 DOM 操作。

2.1.1 文本插值

最基本的数据绑定形式是文本插值,它使用的是“Mustache[ˈmʌstæʃ]”语法 (即双大括号):

<span>Message: {{ msg }}</span>

双大括号标签会被替换为相应组件实例中 msg 属性的值。同时每次 msg 属性更改时它也会同步更新。

2.1.2 js表达式

Vue 实际上在所有的数据绑定中都支持完整的 JavaScript 表达式:

{{ number + 1 }}

{{ ok ? 'YES' : 'NO' }}

{{ message.split('').reverse().join('') }}

<div :id="`list-${id}`"></div>

这些表达式都会被作为 JavaScript ,以组件为作用域解析执行。

在 Vue 模板内,JavaScript 表达式可以被使用在如下场景上:

  • 在文本插值中 (双大括号)
  • 在任何 Vue 指令 (以 v- 开头的特殊 attribute) attribute 的值中

绑定在表达式中的方法在组件每次更新时都会被重新调用,因此应该产生任何副作用,比如改变数据或触发异步操作。

2.1.3 v-cloak

用于隐藏尚未完成编译的 DOM 模板。

当使用直接在 DOM 中书写的模板时,可能会出现一种叫做“未编译模板闪现”的情况:用户可能先看到的是还没编译完成的双大括号标签,直到挂载的组件将它们替换为实际渲染的内容。

v-cloak 会保留在所绑定的元素上,直到相关组件实例被挂载后才移除。配合像 [v-cloak] { display: none } 这样的 CSS 规则,它可以在组件编译完毕前隐藏原始模板。

[v-cloak] {
  display: none;
}

<div v-cloak>
  {{ message }}
</div>

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

完整案例:02_template/06_mustache.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>模版语法</title>
  <style>
    [v-cloak] {
      display: none;
    }
  </style>
</head>
<body>
  <!-- v-cloak 配合属性选择器可隐藏未 编译的 DOM模版 -->
  <div id="app" v-cloak>
    <div>{{ msg }}</div>
    <div>{{ msg.split('').reverse().join('') }}</div>
    <div>{{ msg.split('').reverse().join('-') }}</div>
    <div>{{ flag ? '真' : '假' }}</div>
    <div id="user100">1</div>
    <!-- 绑定属性 指令 v-bind   简写形式为: -->
    <!-- 属性值有变量 一定要使用绑定属性 -->
    <div v-bind:id="'user' + id">2</div>
    <div :id="'user' + id">22</div>
    <div :id="`user${id}`">3</div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = window.Vue

  const app = createApp({
    data () {
      return {
        msg: 'hello',
        flag: true,
        id: 100
      }
    }
  })

  app.mount('#app')
</script>
</html>

2.2 文本类指令

学习:v-text、v-html、v-pre

2.2.1 v-html & v-text

双大括号会将数据插值为纯文本,而不是 HTML。若想插入 HTML,你需要使用 v-html 指令

 <div v-html="rawHTML"></div>
 <div v-text="rawHTML"></div>
 <div>{{rawHTML}}</div>

在网站上动态渲染任意 HTML 是非常危险的,因为这非常容易造成 XSS 漏洞。请仅在内容安全可信时再使用 v-html,并且永远不要使用用户提供的 HTML 内容(script也属于HTML内容)。

2.2.2 v-pre

元素内具有 v-pre,所有 Vue 模板语法都会被保留并按原样渲染。最常见的用例就是显示原始双大括号标签及内容。

 <div v-pre>{{ rawHTML }}</div>

完整案例: 02_template/07_text.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>文本类指令</title>
</head>
<body>
  <div id="app">
    <h1>v-html</h1>
    <div>
      <div v-html="msg"></div>
      <div v-html="str"></div>
    </div>

    <h1>v-text</h1>
    <div>
      <div v-text="msg"></div>
      <div v-text="str"></div>
    </div>

    <h1>v-pre</h1>
    <div>{{ msg }}</div>
    <!-- v-pre 跳过当前DOM下的 vue语法编译 -->
    <div v-pre>{{ msg }}</div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = Vue
  // v-html  v-text  v-pre
  const app = createApp({
    data () {
      return {
        msg: 'hello',
        str: '<h1>hi</h1>'
      }
    }
  })

  app.mount('#app')
</script>
</html>

2.3 属性绑定

学习:v-bind 以及简写形式

双大括号不能在 HTML attributes 中使用。想要响应式地绑定一个 attribute,应该使用 v-bind 指令

<div v-bind:id="myId"></div>

如果绑定的值是 null 或者 undefined,那么该 attribute 将会从渲染的元素上移除。

因为 v-bind 非常常用,提供了特定的简写语法:

<div :id="myId"></div>

开头为 : 的 attribute 可能和一般的 HTML attribute 看起来不太一样,但它的确是合法的 attribute 名称字符,并且所有支持 Vue 的浏览器都能正确解析它。

布尔型 attribute 依据 true / false 值来决定 attribute 是否应该存在于该元素上。disabled 就是最常见的例子之一。

<button :disabled="flag">Button</button>

flag真值或一个空字符串 (即 <button disabled="">) 时,元素会包含这个 disabled attribute。而当其为其他假值时 attribute 将被忽略。

如果你有像这样的一个包含多个 attribute 的 JavaScript 对象:

data () {
  return {
	 obj: {
       a: 1,
	   	 b: 2
     }
  }
}
<div v-bind="obj"></div>

完整案例: 02_template/08_attribute.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>绑定属性</title>
</head>
<body>
  <div id="app">
    <div :id="myId"></div>
    <div :id="`user${num}`"></div>
    <!-- num - 字符串 -->
    <div num="100"></div>
    <!-- num - number -->
    <div :num="1000"></div>
    <button disabled="true"></button>
    <button :disabled="true"></button>

    <div arr="[1, 2, 3]"></div>
    <div :arr="[1, 2, 3]"></div>

    <div obj="{a:1, b:2 }"></div>
    <div :obj="{a:1, b:2 }"></div>

    <div class="null"></div>
    <div :class="null"></div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const { createApp } = Vue
  // 如果属性的值是变量或者包含变量,number类型的数据,boolean类型数据,对象,数组,
  // null,undefined,正则,需要使用绑定属性
  const app = createApp({
    data () {
      return {
        myId: 'user100',
        num: 100
      }
    }
  })

  app.mount('#app')
</script>
</html>

如果属性的值是变量或者包含变量,number类型的数据,boolean类型数据,对象,数组,
null,undefined,正则,需要使用绑定属性

2.4 事件绑定

学习:v-on以及简写形式,methods应用

我们可以使用 v-on 指令 (简写为 @) 来监听 DOM 事件,并在事件触发时执行对应的 JavaScript。用法:v-on:click="methodName"@click="handler"

事件处理器的值可以是:

  1. 内联事件处理器:事件被触发时执行的内联 JavaScript 语句 (与 onclick 类似)。
  2. 方法事件处理器:一个指向组件上定义的方法的属性名或是路径。

·内联事件处理器·通常用于简单场景,例如:

<button @click="count++">加 1</button>
<p>Count is: {{ count }}</p>

随着事件处理器的逻辑变得愈发复杂,内联代码方式变得不够灵活。因此 v-on 也可以接受一个方法名或对某个方法的调用。

方法事件处理器

<button @click="greet">问候</button>
 data () {
   return {
      name: 'Vue.js'
   }
 },
 methods: {
   greet(event) {
     // 方法中的 `this` 指向当前活跃的组件实例
     alert(`Hello ${this.name}!`)
     // `event` 是 DOM 原生事件
     if (event) {
       alert(event.target.tagName)
     }
  }
}

除了直接绑定方法名,你还可以在内联事件处理器中调用方法。这允许我们向方法传入自定义参数以代替原生事件:

methods: {
  say(message) {
    alert(message)
  }
}
<button @click="say('hello')">说 hello</button>
<button @click="say('bye')">说 bye</button>

内联事件处理器中访问原生 DOM 事件。你可以向该处理器方法传入一个特殊的 $event 变量,或者使用内联箭头函数:

 <!-- 使用特殊的 $event 变量 -->
 <button @click="warn('表单不能提交.', $event)">提交</button>
 <!-- 使用内联箭头函数 -->
 <button @click="(event) => warn('表单不能提交.', event)">提交</button>
methods: {
  warn(message, event) {
    // 这里可以访问 DOM 原生事件
    if (event) {
      event.preventDefault()
    }
    alert(message)
  }
}

在处理事件时调用 event.preventDefault()event.stopPropagation() 是很常见的。尽管我们可以直接在方法内调用,但如果方法能更专注于数据逻辑而不用去处理 DOM 事件的细节会更好。

为解决这一问题,Vue 为 v-on 提供了事件修饰符

<!-- 单击事件将停止传递 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
 <!-- 事件修饰符 -->
 <!-- 阻止冒泡-原生 -->
 <div class="parent" @click="clickParent1">
 <div class="child" @click="clickChild1"></div>
 </div>
 <!-- 阻止冒泡-事件修饰符 -->
 <div class="parent" @click="clickParent2">
 <div class="child" @click.stop="clickChild2"></div>
 </div>
methods: {
	clickParent1 (event) {
    console.log('parent1')
    },
    clickChild1 (event) {
    event.stopPropagation()
    console.log('child1')
    },
    clickParent2 (event) {
    console.log('parent2')
    },
    clickChild2 (event) {
    console.log('child2')
    },
}
.parent {
    width: 200px;
    height: 200px;
    background: #f66;
    display: flex;
    justify-content: center;
    align-items: center;
    margin-top: 10px;
}
.child {
    width: 100px;
    height: 100px;
    border: 1px solid #ccc;
}

在监听键盘事件时,我们经常需要检查特定的按键。Vue 允许在 v-on@ 监听按键事件时添加按键修饰符

 <!-- 按键修饰符 -->
 <!-- 原生 -->
 <input type="text" @keyup="print1">
 <!-- vue -->
 <input type="text" @keyup.enter="print2">
methods: {
	print1 (event) {
        if (event.keyCode === 13) {
        	console.log('打印1')
        }
    },
    print2 () {
    	console.log('打印2')
    }
}
.enter
.tab
.delete (捕获“Delete”和“Backspace”两个按键)
.esc
.space
.up
.down
.left
.right

你可以使用以下系统按键修饰符来触发鼠标或键盘事件监听器,只有当按键被按下时才会触发

.ctrl
.alt
.shift
.meta
<!-- Alt + Enter -->
<input @keyup.alt.enter="clear" />
<!-- Ctrl + 点击 -->
<div @click.ctrl="doSomething">Do something</div>

完整案例: 02_tempalte/09_event.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>09_绑定事件</title>
  <style>
    .parent {
      width: 200px;
      height: 200px;
      background-color: #f66;
      display: flex;
      justify-content: center;
      align-items: center;
    }
    .child {
      width: 100px;
      height: 100px;
      background-color: #ccc;
    }
  </style>
</head>
<body>
  <div id="app">
    <!-- 内联事件处理器 -->
    <button v-on:click="count++">加1</button> {{ count }}<br />
    <!-- 方法事件处理器 -->
    <button v-on:click="add">加1</button> {{ count }}<br />

    <button @click="say('hello', $event)">say hello</button>
    <button @click="say('bye', $event)">say bye</button>

    <!-- 事件对象 - 阻止事件冒泡,阻止默认事件 -->
    <form @submit="getUserInfo">
      <input type="text" name="userName" placeholder="用户名" />
      <input type="password" name="password" placeholder="密码" />
      <input type="submit" value="提交">
    </form>
    <div class="parent" @click="print('parent', $event)">
      <div class="child" @click="print('child', $event)"></div>
    </div>

    <!-- 事件修饰符 - 阻止事件冒泡,阻止默认事件  -->
    <form @submit.prevent="getUserInfoVue">
      <input type="text" name="userName" placeholder="用户名" />
      <input type="password" name="password" placeholder="密码" />
      <input type="submit" value="提交">
    </form>
    <div class="parent" @click="printVue('parent')">
      <div class="child" @click.stop="printVue('child')"></div>
    </div>

    <!-- 事件对象 -  回车时打印数据 -->
    <input type="text" @keyup="printData" />
    <!-- 按键修饰符 -  回车时打印数据 -->
    <input type="text" @keyup.enter="printDataVue" />
    <!-- vue2中可以 根据 keyCode 作为修饰符, vue3中不支持 -->
    <input type="text" @keyup.13="printDataVueCode" />

    <!-- 使用系统修饰符可以自定义组合按键 -->
    <!-- 用户点击 alt 加 enter时 清空输入框数据 -->
    <!-- v-model 属于表单的输入绑定 -->
    <input type="text" v-model="text" @keyup.alt.enter="clear"/> {{ text }}
    <div @click.ctrl="doSomething">Do something</div>
  </div>
</body>
<script src="lib/vue.global.js"></script>
<script>

  const { createApp } = Vue
  const app = createApp({
    data () {
      return {
        count: 10,
        name: 'Vue.js',
        text: '1'
      }
    },
    methods: { // 所有自定义的vue的事件都应该写在 methods 中
      // 如果使用事件时,没有添加(),那么事件含有默认参数为 event
      add (event) {
        // this其实就是vue的实例
        // this.count++
        // this.count += 1
        this.count = this.count + 1
        console.log(this.name)
        console.log(event) // PointerEvent 与原生js中的事件对象保持一致(react中使用的不是原生js的事件对象)
      },
      // 如果使用事件时添加(),并且还想用事件对象,那么传递事件对象的vue的专属参数 $event
      say (msg, event) {
        console.log(msg, event)
      },
      getUserInfo (event) {
        event.preventDefault()
      },
      getUserInfoVue () {

      },
      print (msg, event) {
        event.stopPropagation()
        console.log(msg)
      },
      printVue (msg) {
        console.log(msg)
      },
      printData (event) {
        if (event.keyCode === 13) {
          console.log('1')
        }
      },
      printDataVue () {
        console.log(2)
      },
      printDataVueCode () {
        console.log(3)
      },
      clear () {
        this.text = ''
      },
      doSomething () {
        console.log('doSomething')
      }
    }
  })

  app.mount('#app')
  
</script>

</html>

2.5条件渲染

学习:v-if、v-else-if、v-else、v-show

v-if 指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回真值时才被渲染。

也可以使用 v-elsev-if 添加一个“else 区块”

一个 v-else 元素必须跟在一个 v-if 或者 v-else-if 元素后面,否则它将不会被识别。

<div v-if="grade >= 90">优</div>
<div v-else-if="grade >= 80">良</div>
<div v-else-if="grade >= 70">中</div>
<div v-else-if="grade >= 60">差</div>
<div v-else>不及格</div>
data () {
    return {
        grade: 66
    }
}

v-elsev-else-if 也可以在 <template> 上使用。

想要切换不止一个元素,在这种情况下我们可以在一个 <template> 元素上使用 v-if,这只是一个不可见的包装器元素,最后渲染的结果并不会包含这个 <template> 元素。---- tempalte是一个空标签

另一个可以用来按条件显示一个元素的指令是 v-show。其用法基本一样:

不同之处在于 v-show 会在 DOM 渲染中保留该元素;v-show 仅切换了该元素上名为 display 的 CSS 属性。

v-show 不支持在 <template> 元素上使用,也不能和 v-else 搭配使用。

<!-- v-show -->
<div v-show="grade >= 90 && grade < 100">优</div>
<div v-show="grade >= 80 && grade < 90">良</div>
<div v-show="grade >= 70 && grade < 80">中</div>
<div v-show="grade >= 60 && grade < 70">差</div>
<div v-show="grade < 60">不及格</div>

完整案例:02_tempalte/10_condition.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>10_条件渲染</title>
</head>
<body>
  <div id="app">
    <button @click="grade++"></button> {{ grade }}<button @click="grade--"></button>
    <!-- v-if v-else-if v-else -->
    <div v-if="grade >=90"></div>
    <div v-else-if="grade >=80"></div>
    <div v-else-if="grade >=70"></div>
    <div v-else-if="grade >=60"></div>
    <div v-else>不及格</div>

    <hr />
    <!-- v-show -->
    <div v-show="grade >= 90"></div>
    <div v-show="grade >= 80 && grade < 90"></div>
    <div v-show="grade >= 70 && grade < 80"></div>
    <div v-show="grade >= 60 && grade < 70"></div>
    <div v-show="grade < 60">不及格</div>

    <!-- 假如不同条件下需要同时控制多个元素的显示和不显示 -->
    <div v-if="flag">1</div>
    <div v-if="flag">2</div>
    <div v-if="flag">3</div>
    <!-- template 属于vue框架中的空标签,审查元素时不会被渲染出来 -->
    <template v-if="flag">
      <div>4</div>
      <div>5</div>
      <div>6</div>
    </template>

  </div>
</body>
<script src="lib/vue.global.js"></script>
<script>

  const { createApp } = Vue
  const app = createApp({
    data () {
      return {
        grade: 61,
        flag: true
      }
    }
  })

  app.mount('#app')
  
</script>

</html>

v-if vs v-show

v-if 是“真实的”按条件渲染,因为它确保了在切换时,条件区块内的事件监听器和子组件都会被销毁与重建。

v-if 也是惰性的:如果在初次渲染时条件值为 false,则不会做任何事。条件区块只有当条件首次变为 true 时才被渲染。

相比之下,v-show 简单许多,元素无论初始条件如何,始终会被渲染,只有 CSS display 属性会被切换。

总的来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要频繁切换,则使用 v-show 较好;如果在运行时绑定条件很少改变,则 v-if 会更合适。

2.6列表渲染

学习:v-for 以及key属性

我们可以使用 v-for 指令基于一个数组(对象、字符串)来渲染一个列表。v-for 指令的值需要使用 item in/of items 形式的特殊语法,其中 items 是源数据的数组,而 item 是迭代项的别名

data() {
  return {
    items: [{ message: 'Foo' }, { message: 'Bar' }]
  }
}

<li v-for="item in items">
  {{ item.message }}
</li>

v-for 块中可以完整地访问父作用域内的属性和变量。v-for 也支持使用可选的第二个参数表示当前项的位置索引。

data() {
  return {
    parentMessage: 'Parent',
    items: [{ message: 'Foo' }, { message: 'Bar' }]
  }
}

<li v-for="(item, index) in items">
  {{ parentMessage }} - {{ index }} - {{ item.message }}
</li>

列表渲染需要添加key值,对于多层嵌套的 v-for,作用域的工作方式和函数的作用域很类似

完整案例: 02/template/11_list.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>列表渲染</title>
</head>
<body>
  <div id="app">
    <button @click="addAfter">后面追加数据</button>
    <button @click="addBefore">前面追加数据</button>
    <ul>
      <li v-for="item in list">{{ item }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="item in list" :key="item">{{ item }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="(item, index) in arr" :key="item.id">{{ index + 1 }} -{{ item.name }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="value in obj" >{{ value }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="(value, key) in obj" >{{key}}:{{ value }}</li>
    </ul>
    <hr/>
    <ul>
      <li v-for="(value, key, index) in obj" >{{ index + 1}} - {{key}}:{{ value }}</li>
    </ul>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>

  const { createApp } = Vue

  const app = createApp({
    data () {
      return {
        list: ['a', 'b', 'c', 'd'],
        arr: [
          { name: '大勋', id: 1 },
          { name: '小吴', id: 2 },
          { name: '纪童伟', id: 3 },
          { name: '大勋', id: 4 }
        ],
        obj: {
          a: 11,
          b: 22,
          c: 33
        }
      }
    },
    methods: {
      addBefore () {
        this.list.unshift('f')
      },
      addAfter () {
        this.list.push('e')
      }
    }
  })

  app.mount('#app')
</script>
</html>

v-if 与 v-for 同时存在于一个元素上,会发生什么?

02_template/12_vue3_if_for.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue3 v-for v-if优先级</title>
</head>
<body>
    <div id="app">
       <!-- 审查元素查看优先级:vue3中,v-if的优先级高于v-for -->
       <ul>
         <li v-if="flag" v-for="item in list" :key="item">{{ item }}</li>
       </ul>
     </div>
  </body>
<script src="../lib/vue.global.js"></script>
<script>
  Vue.createApp({
      data () {
         return {
           list: ['a', 'b', 'c', 'd'],
           flag: false
         }
       }
     }).mount('#app')
  </script>
</html>

02_template/13_vue2_if_for.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue2 v-for v-if优先级</title>
  </head>
<body>
  <div id="app">
      <!-- 审查元素查看优先级:vue2中,v-for的优先级高于v-if -->
       <ul>
         <li v-if="flag" v-for="item in list" :key="item">{{ item }}</li>
       </ul>
     </div>
   </body>
  <script src="../lib/vue.js"></script>
<script>
  new Vue({
    data: {
          list: ['a', 'b', 'c', 'd'],
           flag: false
         }
     }).$mount('#app')
   </script>
  </html>

通过审查元素得知:

vue3中,v-if的优先级高于v-for

vue2中,v-for的优先级高于v-if

2.7 表单输入绑定

学习:v-model

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量

<input
  :value="text"
  @input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

<input v-model="text">

v-model 还可以用于各种不同类型的输入,<textarea><select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

  • 文本类型的 <input><textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"><input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件

v-model 会忽略任何表单元素上初始的 valuecheckedselected attribute。它将始终将当前绑定的 JavaScript 状态视为数据的正确来源。你应该在 JavaScript 中使用data 选项来声明该初始值。

完整案例: 02_template/14_model.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>表单输入绑定</title>
</head>
<body>
  <div id="app">
    <input type="text" :value="userName" @input="getUserName"/> {{ userName }} <br/>
    <!-- lazy   input ===> change -->
    <!-- number   转换为数字,以数字开头-->
    <!-- trim   去除两端空格-->
    <input type="password" v-model.trim="password"/> {{ password }} <br/>

    <input type="radio" name="sex" value="1" v-model="sex"><input type="radio" name="sex" value="0" v-model="sex"> 女 - {{ sex === '1' ? '男' : '女' }}<br/>
  
    <input type="checkbox" name="hobby" v-model="hobby" value="🏀">🏀
    <input type="checkbox" name="hobby" v-model="hobby" value="🏐️">🏐️
    <input type="checkbox" name="hobby" v-model="hobby" value="🎾">🎾
    <input type="checkbox" name="hobby" v-model="hobby" value="⚽️">⚽️ - {{ hobby }}<br/>

    <select v-model="lesson">
      <option disabled value="">请选择</option>
      <option value="1">1阶段</option>
      <option value="2">2阶段</option>
      <option value="3">3阶段</option>
    </select> - {{ lesson === '1' ? '1阶段': (lesson === '2' ? '2阶段' : (lesson === '3' ? '3阶段' : ''))}}<br/>
  
    <textarea v-model="note"></textarea>{{ note }} <br/>

    <input type="checkbox" v-model="flag"> 同意******协议 -- {{ flag }}

    <button @click="getData">获取数据</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  // input (text password) textarea  -----  value  input
  // select ----  value  change
  // radio  checkbox -----  checked  change
  Vue.createApp({
    data () {
      return {
        userName: '',
        password: '',
        sex: '1',
        hobby: [],
        lesson: '',
        note: '',
        flag: false
      }
    },
    methods: {
      getUserName (event) {
        this.userName = event.target.value
      },
      getData () {
        console.log(this.password)
      }
    }
  }).mount('#app')
</script>
</html>

如果 v-model 表达式的初始值不匹配任何一个选择项,<select> 元素会渲染成一个“未选择”的状态。在 iOS 上,这将导致用户无法选择第一项,因为 iOS 在这种情况下不会触发一个 change 事件。因此,我们建议提供一个空值的禁用选项

修饰符

.lazy input — change

.number — 输出的为数字

.trim ---- 去除两端的空格

2.8 类与样式绑定

数据绑定的一个常见需求场景是操纵元素的 CSS class 列表和内联样式。因为 classstyle 都是 attribute,我们可以和其他 attribute 一样使用 v-bind 将它们和动态的字符串绑定。但是,在处理比较复杂的绑定时,通过拼接生成字符串是麻烦且易出错的。因此,Vue 专门为 classstylev-bind 用法提供了特殊的功能增强。除了字符串外,表达式的值也可以是对象或数组。

不管是类class还是样式style,都有对象和数组的写法

完整案例: 02_template/15_style.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>样式绑定</title>
  <style>
    .box {
      width: 200px;
      height: 200px;
     
      float: left;
      margin-left: 5px;
    }
    .border {
      border: 5px solid #000;
    }
    .box1 {
      background-color: #f66;
    }
    .box2 {
      background-color: #ccc;
    }
    .box3 {
      background-color: #00f;
    }
  </style>
</head>
<body>
  <div id="app">
    <input type="color" v-model="color">
    <h1>style</h1>
    <div style="width: 100px; height: 100px; background-color:#f66;">1</div>
    <div :style="`width: 100px; height: 100px; background-color:${color};`">2</div>
    <div :style="{ width: '100px', height: '100px', backgroundColor: color }">3</div>
    <div :style="[{ width: '100px', height: '100px'}, {backgroundColor: color }]">4</div>
    <h1>class</h1>
    <div class="box box1 border"></div>
    <div class="box box2 border" ></div>
    <div class="box box3 border"></div>
    <div class="box" :class="{box1: flag, border: flag}"></div>
    <div class="box" :class="{box2: flag, border: flag}"></div>
    <div class="box" :class="{box3: flag, border: flag}"></div>

    <div class="box" :class="[{box1: flag}, {border: flag}]"></div>
    <div class="box" :class="[{box2: flag}, {border: flag}]"></div>
    <div class="box" :class="[{box3: flag}, {border: flag}]"></div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  Vue.createApp({
    data () {
      return {
        color: '',
        flag: true
      }
    }
  }).mount('#app')
</script>
</html>

3.生命周期

学习:常见的8个生命周期钩子函数

3.1 vue2生命周期

每个 Vue 实例在被创建之前都要经过一系列的初始化过程。例如需要设置数据监听、编译模板、挂载实例到 DOM,在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,目的是给予用户在一些特定的场景下添加他们自己代码的机会。

Vue生命周期的主要阶段:4个before, 4个ed,创建,装载,更新,销毁

  • 挂载(初始化相关属性)

    • beforeCreate ---- 备孕

      注意点:在此时不能获取data中的数据,也就是说 this.msg 得到的是

    • created ---- 怀上了

    • beforeMount ---- 怀胎十月

    • mounted【页面加载完毕的时候就是此时】 ---- 生下来了

      注意点:默认情况下,在组件的生命周期中只会触发一次

  • 更新(元素或组件的变更操作)

    • beforeUpdate

    • updated

      注意点:可以重复触发的

  • 销毁(销毁相关属性)

    • beforeDestroy — game over前
    • destroyed — game over

销毁(手动)使用 this.$destroy()

关于8个生命周期涉及到的方法,可以参考Vue官网API:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SfHw9f4r-1672136291426)(assets/lifecycle.png)]

3.2 vue3生命周期

选项式API中将 beforeDestroy 以及 destroyed 修改为 beforeUnmount 和 unmounted,其余一致

https://cn.vuejs.org/guide/essentials/lifecycle.html#lifecycle-diagram

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Y3mJC6B8-1672136291427)(assets/lifecycle.16e4c08e.png)]

如果是vue2的生命周期钩子函数

完整案例: 03_lifeCycle/16_lifeCycle_vue2.html 官方解释

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2 生命周期</title>
</head>
<body>
  <div id="app">
    {{ count }}
    <button @click="add">加1</button>
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  // new Vue({
  //   data: {
  //     count: 100
  //   }
  // }).$mount('#app')

  new Vue({
    el: '#app',
    data: {
      count: 100
    },
    methods: {
      add () {
        this.count++
        if (this.count === 110) {
          this.$destroy()
        }
      }
    },
    beforeCreate () { // 备孕 
      // 在实例初始化之后,进行数据侦听和事件/侦听器的配置之前同步调用
      console.log('beforeCreate')
    },
    created () { // 备孕怀上  意外怀孕  ----  请求数据,修改状态 --- 但是不建议 - 不确定性
      // 在实例创建完成后被立即同步调用。
      // 在这一步中,实例已完成对选项的处理,
      // 意味着以下内容已被配置完毕:数据侦听、计算属性、方法、事件/侦听器的回调函数。
      // 然而,挂载阶段还没开始,且 $el property 目前尚不可用。
      console.log('created')
    },
    beforeMount () { // 怀胎十月
      // 在挂载开始之前被调用:相关的 render 函数首次被调用。
      console.log('beforeMount')
    },
    mounted () { // 出生了 --- 数据请求,修改状态,DOM操作,实例化操作,定时器,延时器
      // 实例被挂载后调用,这时 el 被新创建的 vm.$el 替换了。
      // 如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el 也在文档内。
      // 注意 mounted 不会保证所有的子组件也都被挂载完成。
      // 如果你希望等到整个视图都渲染完毕再执行某些操作,
      // 可以在 mounted 内部使用 vm.$nextTick:
      console.log('mounted')
      this.$nextTick(() => { // 保证了整个视图都渲染完毕
        // 业务逻辑
      })
    },
    beforeUpdate () {
      // 在数据发生改变后,DOM 被更新之前被调用。
      console.log('beforeUpdate')
    },
    updated () { // DOM操作,实例化操作
      // 在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用。
      // 不要修改状态,使用计算属性 和watcher代替
      console.log('updated')
    },
    beforeDestroy () { // 一不小心触发条件  ---  取消定时器,延时器,销毁对象等
      // 实例销毁之前调用。在这一步,实例仍然完全可用。
      console.log('beforeDestroy')
    },
    destroyed () { // 没了
      // 实例销毁后调用。
      // 该钩子被调用后,对应 Vue 实例的所有指令都被解绑,
      // 所有的事件监听器被移除,所有的子实例也都被销毁。
      console.log('destroyed')
    }
  })
</script>
</html>

03_lifeCycle/17_lifeCycle_vue3.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3 选项式API生命周期</title>
</head>
<body>
  <div id="app">
    {{ count }}
    <button @click="add">加1</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>

  const app = Vue.createApp({
    data ()  {
      console.log('data')
      return {
        count: 100
      }
    },
    methods: {
      add () {
        this.count++
        if (this.count === 110) {
          app.unmount()
        }
      }
    },
    beforeCreate () { // 备孕 
      // 在组件实例初始化完成之后立即调用
      // 会在实例初始化完成、props 解析之后、data() 和 computed 等选项处理之前立即调用
      console.log('beforeCreate')
    },
    created () { // 备孕怀上  意外怀孕  ----  请求数据,修改状态 --- 但是不建议 - 不确定性
      // 在组件实例处理完所有与状态相关的选项后调用
      // 当这个钩子被调用时,以下内容已经设置完成:响应式数据、计算属性、方法和侦听器。
      // 然而,此时挂载阶段还未开始,因此 $el 属性仍不可用。
      console.log('created')
    },
    beforeMount () { // 怀胎十月
      // 在组件被挂载之前调用。
      // 当这个钩子被调用时,组件已经完成了其响应式状态的设置,
      // 但还没有创建 DOM 节点。它即将首次执行 DOM 渲染过程。
      console.log('beforeMount')
    },
    mounted () { // 出生了 --- 数据请求,修改状态,DOM操作,实例化操作,定时器,延时器
      // 在组件被挂载之后调用。
      // 所有同步子组件都已经被挂载。
      // 其自身的 DOM 树已经创建完成并插入了父容器中。
      console.log('mounted')
      this.$nextTick(() => { // 保证了整个视图都渲染完毕
        // 业务逻辑
      })
    },
    beforeUpdate () {
      // 在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
      console.log('beforeUpdate')
    },
    updated () { // DOM操作,实例化操作
      // 在组件因为一个响应式状态变更而更新其 DOM 树之后调用。
      console.log('updated')
    },
    beforeUnmount () { // 一不小心触发条件  ---  取消定时器,延时器,销毁对象等
      // 在一个组件实例被卸载之前调用。
      console.log('beforeDestroy')
    },
    unmounted () { // 没了
      // 在一个组件实例被卸载之后调用。
      console.log('destroyed')
    }
  })
  app.mount('#app')
</script>
</html>

4.响应式

4.1 响应式基础

学习:状态选项data,$data

选用选项式 API 时,会用 data 选项来声明组件的响应式状态。此选项的值应为返回一个对象的函数。Vue 将在创建新组件实例的时候调用此函数,并将函数返回的对象用响应式系统进行包装。此对象的所有顶层属性都会被代理到组件实例 (即方法和生命周期钩子中的 this) 上。

Vue 在组件实例上暴露的内置 API 使用 $ 作为前缀。它同时也为内部属性保留 _ 前缀。因此,你应该避免在顶层 data 上使用任何以这些字符作前缀的属性。

data 选项函数中返回的对象,会被组件赋为响应式。组件实例将会代理对其数据对象的属性访问。

完整案例04_reactive/18_data.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>data</title>
</head>
<body>
  <div id="app">
    {{ count }}
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () {
      return {
        count: 100
      }
    },
    mounted () {
      console.log(this)
      console.log(this.count) // 100
      this.count = 200
      console.log(this.$data.count) // 200
      this.$data.count = 300
      console.log(this.count) // 300
      console.log(this.$data.count) // 300
      console.log(this._.data.count) // 300
      this._.data.count = 400
      console.log(this.count) // 400
      console.log(this.$data.count) // 400
      console.log(this._.data.count) // 400
      console.log(this.$.data.count) // 400

    }
  })

  app.mount('#app')
</script>
</html>

4.2 计算属性

学习computed

模板中的表达式虽然方便,但也只能用来做简单的操作。如果在模板中写太多逻辑,会让模板变得臃肿,难以维护

推荐使用计算属性来描述依赖响应式状态的复杂逻辑

计算属性是基于它们的响应式依赖进行缓存的,计算属性比较适合对多个变量或者对象进行处理后返回一个结果值,也就是说多个变量中的某一个值发生了变化则我们监控的这个值也就会发生变化。

计算属性定义在Vue对象中,通过关键词 computed 属性对象中定义一个个函数,并返回一个值,使用计算属性时和 data 中的数据使用方式一致。

4.2.1 一般计算属性以及方法对比

完整案例:04_reactive/19_computed.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>计算属性</title>
</head>
<body>
  <div id="app">
    <div>{{ msg }}</div>
    <button @click="msg = 'hi computed'">改变msg</button>
    <div>表达式:
      {{ msg.split('').reverse().join('') }} - 
      {{ msg.split('').reverse().join('') }} -
      {{ msg.split('').reverse().join('') }}
    </div>
    <div>计算属性:{{ reverseMsg }} - {{ reverseMsg }} - {{ reverseMsg }}</div>
    <div>方法:{{ reverseMsgFn() }} - {{ reverseMsgFn() }} - {{ reverseMsgFn() }}</div>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  // 计算属性优于 表达式以及方法
  const app = Vue.createApp({
    data () {
      return {
        msg: 'hello computed'
      }
    },
    methods: {
      reverseMsgFn () {
        console.log('22')
        return this.msg.split('').reverse().join('')
      }
    },
    computed: { // 所有计算属性
      reverseMsg () { // 计算属性写为函数,必须含有返回值,使用和data一致
        console.log('11')
        return this.msg.split('').reverse().join('')
      }
    }
  })
  app.mount('#app')
</script>
</html>

计算属性具有依赖性,只有当依赖的值发生改变,才会重新计算

同等条件下,计算属性优于 方法 以及 js表达式。

4.2.2 可写计算属性

计算属性默认仅能通过计算函数得出结果。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

完整案例:04_reactive/20_computed.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>计算属性</title>
</head>
<body>
  <div id="app">
    {{ reverseMsg }}
    <button @click="updateComputed">修改计算属性</button>

    <input type="text" v-model="firstName"> + 
    <input type="text" v-model="lastName"> = {{ fullName }}

    <button @click="resetName">重置名字</button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () {
      return {
        msg: 'hello',
        firstName: 'wu',
        lastName: 'daxun'
      }
    },
    computed: {
      reverseMsg () {
        return this.msg + '!!!'
      },
      // fullName () { // 默认是只读的
      //   return this.firstName + this.lastName
      // }
      // fullName: {
      //   get () {
      //     return this.firstName + this.lastName
      //   }
      // }
      fullName: {
        get () { // 可读
          return this.firstName + this.lastName
        },
        set (val) { // **************  这里的setter 表示计算属性是可写的
          this.firstName = val.split(' ')[0]
          this.lastName = val.split(' ')[1]
        }
      }
    },
    methods: {
      updateComputed () {
        this.reverseMsg = 'hi!!!!!'
      },
      resetName () {
        this.fullName = "吴 大勋"
      }
    }
  })
  app.mount('#app')
</script>
</html>

4.3 侦听器

学习:watch以及实例方法$watch

使用watch来侦听data中数据的变化,watch中的属性现阶段一定是data 中已经存在的数据。

watch 只能监听data中的数据变化吗?

watch可以监听路由中的数据的变化

**使用场景:**数据变化时执行异步或开销比较大的操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W2a8G4LI-1672136291428)(assets/20.png)]

完整案例:04_reactive/21_watch.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>侦听属性</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="firstName"> + 
    <input type="text" v-model="lastName"> = {{ fullName }} 
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () {
      return {
        firstName: '',
        lastName: '',
        fullName: ''
      }
    },
    watch: {
      firstName (newVal, oldVal) {
        this.fullName = newVal + this.lastName
      },
      lastName (newVal, oldVal) {
        this.fullName = this.firstName + newVal
      }
    }
  })

  app.mount('#app')
</script>
</html>

使用计算属性可以简化

完整案例:04_reactive/22_computed.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>计算属性代替侦听属性</title>
</head>
<body>
  <div id="app">
    <input type="text" v-model="firstName"> + 
    <input type="text" v-model="lastName"> = {{ fullName }} 
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  const app = Vue.createApp({
    data () {
      return {
        firstName: '',
        lastName: ''
      }
    },
    computed: {
      fullName () {
        return this.firstName + this.lastName
      }
    }
  })

  app.mount('#app')
</script>
</html>

如何监听一个对象下的属性的变化?深度侦听

watch 默认是浅层的:被侦听的属性,仅在被赋新值时,才会触发回调函数——而嵌套属性的变化不会触发。如果想侦听所有嵌套的变更,你需要深层侦听器:

如果一开始就需要监听数据,建议直接在options Api中添加 watch选项

如果在达到某一个条件下再开启监听,需要使用 this.$watch()手动添加侦听器

如果不使用深度侦听,如何监听对象下的属性的变化,可以通过 监听 对象.属性的变化(vue2 + vue3),注意this指向

完整案例:04_reactive/23_deep_watch.html

<!DOCTYPE html>
<html lang="en">
<head>
     <meta charset="UTF-8">
     <meta http-equiv="X-UA-Compatible" content="IE=edge">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
     <title>深度侦听属性</title>
   </head>
<body>
  <div id="app">
       <input type="text" v-model="user.firstName"> + 
       <input type="text" v-model="user.lastName"> = <div id="full">{{ user.fullName }} </div>
       <button @click="count++">加1</button>{{ count }}
       <button @click="startWatch">开始监听</button>
       <button @click="stopWatch">停止监听</button>
     </div>
   </body>
<script src="../lib/vue.global.js"></script>
<script>
     const app = Vue.createApp({
       data () {
         return {
           user: {
             firstName: '1',
             lastName: '2',
             fullName: '12'
           },
           count: 10
         }
       },
       methods: { // 手动开启监听器
         startWatch () {
           this.unwatch = this.$watch('count', (newVal) => {
             console.log(newVal)
           })
         },
         stopWatch () {
           this.unwatch()
         }
       },
       watch: {
         // count (newVal) {
         //   console.log(newVal)
         // },
         // 侦听 对象.属性, 变相的完成的 对象下数据的侦听,如果数据层级比较深呢?
         // 'user.firstName' (newVal, oldVal) {
         //   this.user.fullName = newVal + this.user.lastName
         // },
         // 'user.lastName' (newVal, oldVal) {
         //   this.user.fullName = this.user.firstName + newVal
         // }
         user: {
           deep: true, // 开启深度侦听
           immediate: true, // 自动执行一次监听数据,默认值为false
           handler (newVal, oldVal) {
             console.log(newVal)
             this.user.fullName = newVal.firstName + newVal.lastName
             
           },
           // 默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。
           // 这意味着你在侦听器回调中访问的 DOM 将是被 Vue 更新之前的状态。
          //  在侦听器回调中能访问被 Vue 更新之后的DOM,你需要指明 flush: 'post' 选项
          // 了解,一般使用在 组合式API
           flush: 'post' // vue3中新增的
         }
       }
     })
   
     app.mount('#app')
   </script>
   </html>

4.4 深入响应式系统

学习:renderTracked 以及 renderTraggered

https://cn.vuejs.org/guide/extras/reactivity-in-depth.html

Vue 最标志性的功能就是其低侵入性的响应式系统。组件状态都是由响应式的 JavaScript 对象组成的。当更改它们时,视图会随即自动更新。这让状态管理更加简单直观,但理解它是如何工作的也是很重要的,这可以帮助我们避免一些常见的陷阱。

4.4.1 什么是响应性

这个术语在今天的各种编程讨论中经常出现,但人们说它的时候究竟是想表达什么意思呢?本质上,响应性是一种可以使我们声明式地处理变化的编程范式。一个经常被拿来当作典型例子的用例即是 Excel 表格:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bixEjkfd-1672136291430)(assets/image-20220915004948125.png)]

这里单元格 A2 中的值是通过公式 = A0 + A1 来定义的 (你可以在 A2 上点击来查看或编辑该公式),因此最终得到的值为 3,正如所料。但如果你试着更改 A0 或 A1,你会注意到 A2 也随即自动更新了。

而 JavaScript 默认并不是这样的。如果我们用 JavaScript 写类似的逻辑:

let A0 = 1
let A1 = 2
let A2 = A0 + A1

console.log(A2) // 3

A0 = 2
console.log(A2) // 仍然是 3

当我们更改 A0 后,A2 不会自动更新。

那么我们如何在 JavaScript 中做到这一点呢?首先,为了能重新运行计算的代码来更新 A2,我们需要将其包装为一个函数:

let A0 = 1
let A1 = 2
let A2
function update() {
  A2 = A0 + A1
}
update()
console.log(A2) // 3

A0 = 2
update()
console.log(A2) // 4

然后,我们需要定义几个术语:

  • 这个 update() 函数会产生一个副作用,或者就简称为作用 (effect),因为它会更改程序里的状态。
  • A0A1 被视为这个作用的依赖 (dependency),因为它们的值被用来执行这个作用。因此这次作用也可以说是一个它依赖的订阅者 (subscriber)。

我们需要一个魔法函数,能够在 A0A1 (这两个依赖) 变化时调用 update() (产生作用)。

whenDepsChange(update)

这个 whenDepsChange() 函数有如下的任务:

  1. 当一个变量被读取时进行追踪。例如我们执行了表达式 A0 + A1 的计算,则 A0A1 都被读取到了。
  2. 如果一个变量在当前运行的副作用中被读取了,就将该副作用设为此变量的一个订阅者。例如由于 A0A1update() 执行时被访问到了,则 update() 需要在第一次调用之后成为 A0A1 的订阅者。
  3. 探测一个变量的变化。例如当我们给 A0 赋了一个新的值后,应该通知其所有订阅了的副作用重新执行。
4.4.2 Vue 中的响应性是如何工作的

后续单独讲解,刨析vue2的响应式和vue3的响应式的区别以及实现原理

5.组件化

5.1 什么是组件化?理解组件化

组件允许我们将 UI 划分为独立的、可重用的部分,并且可以对每个部分进行单独的思考。在实际应用中,组件常常被组织成层层嵌套的树状结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kaHvim5E-1672136291431)(assets/components.7fbb3771.png)]

这和我们嵌套 HTML 元素的方式类似,Vue 实现了自己的组件模型,使我们可以在每个组件内封装自定义内容与逻辑。

5.2 如何封装一个vue组件

目标:理解一般思路即可

一个 Vue 组件在使用前需要先被“注册”,这样 Vue 才能在渲染模板时找到其对应的实现。

  • 先构建组件的模板
  • 定义组件
  • 注册组件
  • 使用组件

5.3 vue3组件注册和使用

学习:app.component()、components选项、template选项

组件注册有两种方式:全局注册和局部注册。

5.3.1 全局注册组件

我们可以使用 Vue 应用实例app.component() 方法,让组件在当前 Vue 应用中全局可用

import { createApp } from 'vue'

const app = createApp({})

app.component(
  // 注册的名字
  'MyComponent',
  // 组件的实现
  {
    /* ... */
  }
)

app.component() 方法可以被链式调用:

app
  .component('ComponentA', ComponentA)
  .component('ComponentB', ComponentB)
  .component('ComponentC', ComponentC)

全局注册的组件可以在此应用的任意组件的模板中使用

<!-- 这在当前应用的任意组件中都可用 -->
<ComponentA/>
<ComponentB/>
<ComponentC/>

所有的子组件也可以使用全局注册的组件,这意味着这三个组件也都可以在彼此内部使用

完整案例:05_component/24_component_vue3.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3全局注册组件</title>
</head>
<body>
  <div id="app">
    <!-- 使用组件
      在html文件中 只能使用短横线方式调用组件,如果使用模版,则也可以使用 驼峰式
    -->
    <!-- <ComA ></ComA>  这里报警告 -->
    <com-a></com-a>
    <com-b></com-b>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>

  const app = Vue.createApp()

  // 定义组件
  const ComA = {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello coma'
      }
    }
  }

  // 全局注册组件 app.component(组件名称, 定义好的组件的选项)
  // 组件的名称可以采用 驼峰式命名 以及 短横线方式
  app.component('ComA', ComA)
  // app.component('com-a', ComA)

  app.component('ComB', {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello comb'
      }
    }
  })

  app.mount('#app')
</script>
</html>

vue2全局注册组件

完整案例:05_component/25_component_vue2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2全局注册组件</title>
</head>
<body>
  <div id="app">
    <!-- 使用组件
      在html文件中 只能使用短横线方式调用组件,如果使用模版,则也可以使用 驼峰式
    -->
    <!-- <ComA ></ComA>  这里报警告 -->
    <com-a></com-a>
    <com-b></com-b>
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>


  // 定义组件
  const ComA = {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello coma'
      }
    }
  }

  // 组件的名称可以采用 驼峰式命名 以及 短横线方式
  Vue.component('ComA', ComA)
  // app.component('com-a', ComA)

  Vue.component('ComB', {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello comb'
      }
    }
  })

  // Vue.component(组件名称, 组件选项)
  // vue2全局注册组件必须放在 new Vue实例之前,
  // 相当于当你new Vue 时,组件的已经挂载到了 Vue对象的原型上
  const app = new Vue()

  app.$mount('#app')
</script>
</html>
5.3.2 局部注册组件

全局注册虽然很方便,但有以下几个问题:

  1. 全局注册,但并没有被使用的组件无法在生产打包时被自动移除 (也叫“tree-shaking”)。如果你全局注册了一个组件,即使它并没有被实际使用,它仍然会出现在打包后的 JS 文件中。
  2. 全局注册在大型项目中使项目的依赖关系变得不那么明确。在父组件中使用子组件时,不太容易定位子组件的实现。和使用过多的全局变量一样,这可能会影响应用长期的可维护性。

相比之下,局部注册的组件需要在使用它的父组件中显式导入,并且只能在该父组件中使用。它的优点是使组件之间的依赖关系更加明确,并且对 tree-shaking 更加友好。

局部注册需要使用 components 选项

对于每个 components 对象里的属性,它们的 key 名就是注册的组件名,而值就是相应组件的实现。

完整案例:05_component/26_components_vue3.htmlvue3局部注册组件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3局部注册组件</title>
</head>
<body>
  <div id="app">
    <!-- 使用组件
      在html文件中 只能使用短横线方式调用组件,如果使用模版,则也可以使用 驼峰式
    -->
    <!-- <ComA ></ComA>  这里报警告 -->
    <com-a></com-a>
    <com-b></com-b>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<script>
  // 定义组件
  const ComA = {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello coma'
      }
    }
  }

  const app = Vue.createApp({
    components: { // 局部注册组件
      ComA: ComA,
      'com-b': {
        // 组件模版
        template: `
          <div>
            {{ msg }}  
          </div>
        `,
        data () {
          return {
            msg: ' hello comb'
          }
        }
      }
    }
  })



  app.mount('#app')
</script>
</html>

完整案例:05_component/27_components_vue2.htmlvue2局部注册组件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2局部注册组件</title>
</head>
<body>
  <div id="app">
    <!-- 使用组件
      在html文件中 只能使用短横线方式调用组件,如果使用模版,则也可以使用 驼峰式
    -->
    <!-- <ComA ></ComA>  这里报警告 -->
    <com-a></com-a>
    <com-b></com-b>
  </div>
</body>
<script src="../lib/vue.js"></script>
<script>
  // 定义组件
  const ComA = {
    // 组件模版
    template: `
      <div>
        {{ msg }}  
      </div>
    `,
    data () {
      return {
        msg: ' hello coma'
      }
    }
  }

  const app = new Vue({
    components: { // 局部注册组件
      ComA: ComA,
      'com-b': {
        // 组件模版
        template: `
          <div>
            {{ msg }}  
          </div>
        `,
        data () {
          return {
            msg: ' hello comb'
          }
        }
      }
    }
  })

  app.$mount('#app')
</script>
</html>

如何抽离组件的模版呢

image-20221215101445687

局部注册的组件在后代组件中并*不*可用

5.3.3 组件使用注意事项

以上案例使用 PascalCase 作为组件名的注册格式,这是因为:

  1. PascalCase 是合法的 JavaScript 标识符。这使得在 JavaScript 中导入和注册组件都很容易,同时 IDE 也能提供较好的自动补全。
  2. <PascalCase /> 在模板中更明显地表明了这是一个 Vue 组件,而不是原生 HTML 元素。同时也能够将 Vue 组件和自定义元素 (web components) 区分开来

在单文件组件和内联字符串模板中,我们都推荐这样做。但是,PascalCase 的标签名在 DOM 模板(html文件)中是不可用的

为了方便,Vue 支持将模板中使用 kebab-case 的标签解析为使用 PascalCase 注册的组件。这意味着一个以 MyComponent 为名注册的组件,在模板中可以通过 <MyComponent><my-component> 引用。这让我们能够使用同样的 JavaScript 组件注册代码来配合不同来源的模板。

6.组件间通信

组件有 分治 的特点,每个组件之间具有一定的独立性,但是在实际工作中使用组件的时候有互相之间传递数据的需求,此时就得考虑如何进行 组件间传值 的问题了。

image-20221215103812564

完整案例:05_component/28_parent_child_component.html父子组件

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父子组件</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child'
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

6.1 Prop

学习:状态选项props 以及 实例属性 $attrs

一个组件需要显式声明它所接受的 props,这样 Vue 才能知道外部传入的哪些是 props,哪些是透传 attribute

props 需要使用 props 选项来定义:

{
  props: ['foo'],
  created() {
    // props 会暴露到 `this` 上
    console.log(this.foo)
  }
}

除了使用字符串数组来声明 prop 外,还可以使用对象的形式:

{
  props: {
    title: String,
    likes: Number
  }
}

对于以对象形式声明中的每个属性,key 是 prop 的名称,而值则是该 prop 预期类型的构造函数。比如,如果要求一个 prop 的值是 number 类型,则可使用 Number 构造函数作为其声明的值。

如果一个 prop 的名字很长,应使用 camelCase 形式,因为它们是合法的 JavaScript 标识符,可以直接在模板的表达式中使用,也可以避免在作为属性 key 名时必须加上引号。

虽然理论上你也可以在向子组件传递 props 时使用 camelCase 形式 (使用 DOM 模板时例外),但实际上为了和 HTML attribute 对齐,我们通常会将其写为 kebab-case 形式

<my-com :likeNum="100"></my-com> ===> <my-com :like-num="100"></my-com>

对于组件名我们推荐使用 PascalCase,因为这提高了模板的可读性,能帮助我们区分 Vue 组件和原生 HTML 元素。然而对于传递 props 来说,使用 camelCase 并没有太多优势,因此我们推荐更贴近 HTML 的书写风格-短横线。

所有的 props 都遵循着单向绑定原则(单项数据流),props 因父组件的更新而变化,自然地将新的状态向下流往子组件,而不会逆向传递。这避免了子组件意外修改父组件的状态的情况,不然应用的数据流将很容易变得混乱而难以理解。

另外,每次父组件更新后,所有的子组件中的 props 都会被更新到最新值,这意味着你不应该在子组件中去更改一个 prop。

6.1.1 父组件给子组件传值1

完整案例:05_component/29_parent_child_component_value1.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父组件给子组件传值1</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <!-- 
      父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
      对象,数组,null,undefined,正则,需要使用绑定属性
     -->
    <my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <div>{{ msg }}</div>
    <div>{{ flag }}</div>
    <div>{{ num }}</div>
    <div>{{ obj }}</div>
    <div>{{ arr }}</div>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // 在定义子组件的地方,添加一个 props 选项
  // 方式1
  //     props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
  const Child = {
    template: '#child',
    props: ['msg', 'flag', 'num', 'obj', 'arr']
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    data () {
      return {
        msg: 'hello vue'
      }
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

虽然上述案例已经完成了父组件给子组件传值,但是不够严谨

可能A负责父组件的编写,B负责了子组件的编写,容易造成 不知道 自定义的属性名的 数据类型

6.1.2 父组件给子组件传值2

完整案例:05_component/30_parent_child_component_value2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父组件给子组件传值2</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <!-- 
      父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
      对象,数组,null,undefined,正则,需要使用绑定属性
     -->
    <my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <div>{{ msg }}</div>
    <div>{{ flag }}</div>
    <div>{{ num }}</div>
    <div>{{ obj }}</div>
    <div>{{ arr }}</div>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // 在定义子组件的地方,添加一个 props 选项
  // 方式1
  //     props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
  // 方式2
  //     props 书写为一个对象,对象的key值为自定义的属性名,value值为 数据类型
  const Child = {
    template: '#child',
    // props: ['msg', 'flag', 'num', 'obj', 'arr']
    props: {
      msg: String,
      flag: Boolean,
      num: Number,
      obj: Object,
      arr: Array
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    data () {
      return {
        msg: 'hello vue'
      }
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

现在只能知道哪一个属性是哪一种数据类型,但是有时候我们可以不需要设置 自定义的属性(. )

<input /> <===> <input type="text" />

6.1.3 父组件给子组件传值3

完整案例: 05_component/31_parent_child_component_value3.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父组件给子组件传值3</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <!-- 
      父组件在调用子组件的地方,添加自定义的属性,如果属性的值是变量,boolean类型,number类型,
      对象,数组,null,undefined,正则,需要使用绑定属性
     -->
    <my-child :msg="msg" :flag="true" :num="1000" :obj="{a:1}" :arr="[1, 2, 3]"></my-child>
    <my-child :obj="{b:2}" ></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <div>{{ msg }}</div>
    <div>{{ flag }}</div>
    <div>{{ num }}</div>
    <div>{{ obj }}</div>
    <div>{{ arr }}</div>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // 在定义子组件的地方,添加一个 props 选项
  // 方式1
  //     props 书写为一个数组,数组的元素名就是之前定义好的自定义属性名,子组件的模版按照 自定义的属性名 渲染即可
  // 方式2
  //     props 书写为一个对象,对象的key值为自定义的属性名,value值为 数据类型
  // 方式3
  //     props 书写为一个对象
  //        对象的key值为 自定义的属性名,
  //        value值又为一个对象
  //            对象的key值为 type, 表示需要传递数据的 数据类型
  //            对象的key值为 default,表示数据的默认值, 如果数据默认值为 对象和数组,需要通过函数返回
  //            对象的key值为 required, 表示该数据是必须传递的项
  //            对象的key值为 validator, 表示需要自定义验证规则
  //        如果一个属性的值既可以是 String,也可以是number类型,通过 || 分割,或者数组书写
  const Child = {
    template: '#child',
    // props: ['msg', 'flag', 'num', 'obj', 'arr']
    // props: {
    //   msg: String,
    //   flag: Boolean,
    //   num: Number,
    //   obj: Object,
    //   arr: Array
    // }
    props: {
      msg: {
        type: String
      },
      flag: {
        type: Boolean,
        default: true
      },
      num: {
        // type: Number || String,
        // type: [Number, String],
        type: Number,
        default: 100000,
        validator: (value) => {
          return value > 20000
        }
      },
      obj: {
        required: true, 
        type: Object,
        default () {
          return { a: '1111' }
        }
      },
      arr: {
        type: Array,
        default () {
          return [4, 5, 6]
        }
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    data () {
      return {
        msg: 'hello vue'
      }
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>
6.1.4 $attrs

一个包含了组件所有透传 attributes 的对象。

透传 Attributes 是指由父组件传入,且没有被子组件声明为 props 或是组件自定义事件的 attributes 和事件处理函数。

<my-button class="btn"></my-button>

子组件模板只有button,那么my-button的class直接透传给子组件

<button class="btn"></button>

默认情况下,若是单一根节点组件,$attrs 中的所有属性都是直接自动继承自组件的根元素。而多根节点组件则不会如此,同时你也可以通过配置 inheritAttrs 选项来显式地关闭该行为。

6.2 监听事件

学习:状态选项emits、实例方法 $emit

6.2.1 子组件给父组件传值- $emit

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件

$emit() 方法在组件实例上也同样以 this.$emit() 的形式可用

父组件可以通过 v-on (缩写为 @) 来监听事件

同样,组件的事件监听器也支持 .once 修饰符

像组件与 prop 一样,事件的名字也提供了自动的格式转换。注意这里我们触发了一个以 camelCase 形式命名的事件,但在父组件中可以使用 kebab-case 形式来监听。与 prop 大小写格式一样,在模板中我们也推荐使用 kebab-case 形式来编写监听器。

完整案例:05_component/32_child_parent_component_value1.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>子组件给父组件传值1</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <!-- 
      父组件调用子组件的地方,绑定一个自定义的事件
      该事件由父组件定义,默认参数就是子组件传递给父组件的值
     -->
    <my-child v-on:my-event="getData"></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <button @click="sendData">传值2000</button>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // 在子组件的某一个事件内部,通过 this.$emit('自定义事件名', 参数)完成传递
  const Child = {
    template: '#child',
    mounted () {
      this.$emit('my-event', 1000)
    },
    methods: {
      sendData () {
        this.$emit('my-event', 2000)
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    methods: {
      getData (val) {
        console.log(val)
      }
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>
6.2.2 子组件给父组件传值-声明触发的事件

组件要触发的事件可以显式地通过 emits 选项来声明:

{
	emits: ['inFocus', 'submit']
}

这个 emits 选项还支持对象语法,它允许我们对触发事件的参数进行验证:

{
	 emits: {
        submit(payload) {
          // 通过返回值为 `true` 还是为 `false` 来判断
          // 验证是否通过
        }
      }
}

要为事件添加校验,那么事件可以被赋值为一个函数,接受的参数就是抛出事件时传入 this.$emit 的内容,返回一个布尔值来表明事件是否合法。

{
  emits: {
    // 没有校验
    click: null,

    // 校验 submit 事件
    submit: ({ email, password }) => {
      if (email && password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  },
  methods: {
    submitForm(email, password) {
      this.$emit('submit', { email, password })
    }
  }
}

完整案例:05_component/33_child_parent_component_value2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>子组件给父组件传值1</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <!-- 
      父组件调用子组件的地方,绑定一个自定义的事件
      该事件由父组件定义,默认参数就是子组件传递给父组件的值
     -->
    <my-child v-on:my-event="getData"></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    <button @click="sendData">传值2000</button>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // 在子组件的某一个事件内部,通过 this.$emit('自定义事件名', 参数)完成传递
  const Child = {
    template: '#child',
    // 声明触发的事件并且验证
    // emits: ['my-event'], // 声明触发的事件
    emits: { // 声明触发的事件并且验证
      'my-event': (payload) => {
        return payload > 500
      }
    },
    mounted () {
      this.$emit('my-event', 1000)
    },
    methods: {
      sendData () {
        this.$emit('my-event', 2000)
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    methods: {
      getData (val) {
        console.log(val)
      }
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>
6.2.3 自定义表单组件使用v-model

自定义事件可以用于开发支持 v-model 的自定义表单组件

<CustomInput> 组件内部需要做两件事:

  1. 将内部原生 input 元素的 value attribute 绑定到 modelValue prop
  2. 输入新的值时在 input 元素上触发 update:modelValue 事件

完整案例:05_component/34_custom_form_v-model.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3-自定义组件使用 v-model</title>
</head>
<body>
  <div id="app">
    <my-input
      type="text" 
      v-model="userName"
      placeholder="用户名"
    ></my-input> {{ userName }}
    <my-input
      type="password" 
      v-model="password"
      placeholder="密码"
    ></my-input> {{ password }}
  </div>
</body>
<template id="input">
  <div>
    <input 
      :type="type"
      :placeholder="placeholder"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
    />
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  // v-model 用到自定义组件上,就相当于 
  // 父组件在调用子组件时 给子组件传了一个 叫 modelValue 的属性
  // 同时 也相当于 父组件直接给子组件绑定了一个 叫 update:modelValue 的事件
  // 在子组件模版中 将 input 的value的值赋值给 modelValue,
  // 给input绑定一个事件,事件提交 update:modelValue

  // vue2中自定义表单组件,父传给子 value 属性,事件为 input 事件
  const Input = {
    template: '#input',
    emits: ['update:modelValue'],
    props: {
      type: {
        type: String,
        default: 'text'
      },
      placeholder: {
        type: String
      },
      modelValue: {
        type: String
      }
    }
  }
  Vue.createApp({
    data () {
      return {
        userName: '',
        password: ''
      }
    },
    components: {
      MyInput: Input
    }
  }).mount('#app')
</script>
</html>

另一种在组件内实现 v-model 的方式是使用一个可写的,同时具有 getter 和 setter 的计算属性。get 方法需返回 modelValue prop,而 set 方法需触发相应的事件:

const Input = {
  template: `#input`,
  props: ['modelValue'],
  emits: ['update:modelValue'],
  computed: {
    value: {
      get() {
        return this.modelValue
      },
      set(value) {
        this.$emit('update:modelValue', value)
      }
    }
  }
}
6.2.4 多个v-model的绑定

v-model 的参数

仅限于 vue3

完整案例:05_component/35_custom_form_v-model_params.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>一个组件使用多个v-model,仅限于vue3</title>
</head>
<body>
  <div id="app">
    <my-form 
      v-model:username = "username"
      v-model:password = "password"
    ></my-form> {{ username }} -- {{ password }}
  </div>
</body>
<template id="form">
  <form>
    <div>
      <input type="text" placeholder="用户名" :value="username" @input="$emit('update:username', $event.target.value)"/>
    </div>
    <div>
      <input type="password" placeholder="密码" :value="password" @input="$emit('update:password', $event.target.value)"/>
    </div>
    <button>登录</button>
  </form>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Form = {
    template: '#form',
    emits: ['update:username', 'update:password'],
    props: {
      username: String,
      password: String
    }
  }

  Vue.createApp({
    data () {
      return {
        username: '',
        password: ''
      }
    },
    components: {
      MyForm: Form
    }
  }).mount('#app')
</script>
</html>

6.3 透传Attribute

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

6.3.1 attribute继承

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

<!-- <MyButton> 的模板 -->
<button>click me</button>

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">click me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

6.3.2 对 classstyle 的合并

如果一个子组件的根元素已经有了 classstyle attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton> 组件的模板改成这样:

<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>

则最后渲染出的 DOM 结果会变成:

<button class="btn large">click me</button>
6.3.3 v-on 监听器继承

同样的规则也适用于 v-on 事件监听器:

<MyButton @click="onClick" />

click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。

6.3.4 禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。

6.3.5 多根节点的 Attributes 继承

$attrs 被显式绑定

完整案例: 05_component/36_attribute_transmission.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>透传</title>
</head>
<body>
  <div id="app">
    {{ count }}
    <my-button class="large" @click="count++"></my-button>
  </div>
</body>
<script src="../lib/vue.global.js"></script>
<template id="btn">
  <button class="btn">按钮</button>
</template>
<script>
  Vue.createApp({
    data () {
      return {
        count: 10
      }
    },
    components: {
      MyButton: { template: '#btn', inheritAttrs: false }
    }
  }).mount('#app')
</script>
</html>

以下案例仅供参考

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>36_透传attribute</title>
  <style>
    .btn {
      border: 0;
      padding: 10px 20px;
    }
    .btn-success {
      background-color: rgb(29, 198, 29);
      color: #fff;
    }
    .btn-danger {
      background-color: rgb(218, 23, 39);
      color: #fff;
    }
    .btn-primary {
      background-color: rgb(75, 104, 236);
      color: #fff;
    }
  </style>
</head>
<body>
  <div id="app">
    <my-button type="a" class="btn-success" @click="print('success')" @my-event="myalert(1)"></my-button>
    <my-button type="b" class="btn-danger"  @click="print('danger')" @my-event="myalert(2)"></my-button>
    <my-button type="c" class="btn-primary"  @click="print('primary')" @my-event="myalert(3)"></my-button>
  </div>
</body>

<template id="button">
  <!-- Attributes 继承(class 与style合并, v-on事件继承) -->
  <!--  <button class="btn">按钮</button> -->
  <!-- 深层组件继承 -->
  <base-button></base-button>
</template>
<template id="base">
  <button class="btn" v-bind="$attrs" >按钮</button>
  <div >测试</div>
</template>
<script src="lib/vue.global.js"></script>
<script>
  const Base = {
    template: '#base',
    mounted () { // 在js中访问透传的 attributes
      console.log('2', this.$attrs)
    },
    // inheritAttrs: false // 不想要一个组件自动地继承 attribute,你可以在组件选项中设置
  }
  const Button = {
    template: '#button',
    components: {
      BaseButton: Base
    },
    mounted () {
      console.log('1', this.$attrs)
    },
   
  }
  

  const { createApp } = Vue

  const app = createApp({
    components: {
      MyButton: Button
    },
    methods: {
      print (msg) {
        console.log(msg)
      }
    },
    myalert (num) {
      alert(num)
    }
  })
  

  app.mount('#app')
</script>
</html>

6.4 特殊Attribute -ref

学习:实例属性refs

用于注册模板引用

ref 用于注册元素或子组件的引用。

使用选项式 API,引用将被注册在组件的 this.$refs 对象里

放在DOM元素上,获取DOM节点,放到组件上,获取子组件的实例,可以直接使用子组件的属性和方法

完整案例:05_component/37_attribute_ref.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父子组件</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <my-child ref="child"></my-child>
    <div ref="oDiv">DOM操作</div>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child',
    data () {
      return {
        msg: 'child 在此'
      }
    },
    methods: {
      fn () {
        console.log('子组件的方法被调用')
      }
    }
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    },
    mounted () {
      console.log(this.$refs)
      console.log(this.$refs.child.msg)
      this.$refs.child.fn()
      console.log(this.$refs.oDiv.innerHTML)
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

6.5 $parent

当前组件可能存在的父组件实例,如果当前组件是顶层组件,则为 null

完整案例05_component/38_parent.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父子组件</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件</h1>
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件</h3>
    {{ $parent.msg }}
    <button @click="$parent.fn()">点击</button>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child'
  }

  const Parent = {
    template: '#parent',
    data () {
      return {
        msg: '父组件在此'
      }
    },
    methods: {
      fn () {
        console.log('父组件的方法被调用')
      }
    },
    components: {
      'my-child': Child
    }
  }

  Vue.createApp({
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

6.6$root

当前组件树的根组件实例。如果当前实例没有父组件,那么这个值就是它自己。

完整案例:05_component/39_root.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>父子组件</title>
</head>
<body>
  <div id="app">
    <my-parent></my-parent>
  </div>
</body>
<template id="parent">
  <div>
    <h1>父组件 - {{ $root.msg }}</h1>
    <my-child></my-child>
  </div>
</template>
<template id="child">
  <div>
    <h3>子组件 - {{ $root.msg }}</h3>
    <button @click="$root.fn()">按钮</button>
  </div>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Child = {
    template: '#child'
  }

  const Parent = {
    template: '#parent',
    components: {
      'my-child': Child
    }
  }

  Vue.createApp({
    data () {
      return {
        msg: 'root'
      }
    },
    methods: {
      fn () {
        console.log('root 被调用')
      }
    },
    components: {
      'my-parent': Parent
    }
  }).mount('#app')
</script>
</html>

6.7 非父子组件传值

兄弟组件传值 - 中央事件总线传值 ---- vue2

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7DnaI1Ez-1672136291433)(assets/image-20220916160921781.png)]

完整案例:05_component/40_brother_value-vue2.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue2兄弟组件传值</title>
</head>
<body>
  <div id="app">
    <my-content></my-content>
    <my-footer></my-footer>
  </div>

</body>
<template id="content">
  <div>当前页面是:{{ page }}</div>
</template>
<template id="footer">
  <ul>
    <li @click="sendData('首页')">首页</li>
    <li @click="sendData('分类')">分类</li>
    <li @click="sendData('购物车')">购物车</li>
    <li @click="sendData('我的')">我的</li>
  </ul>
</template>
<script src="../lib/vue.js"></script>
<script>
  // new Vue 实例作为中央事件总线 eventBus
  // 通过  eventBus.$emit('自定义事件', 数据) 传值
  // 通过 eventBus.$on('自定义事件', 回调函数)接收值,回调函数参数就是传递过来的值
  const eventBus = new Vue()
  const Content = {
    template: '#content',
    data () {
      return {
        page: '首页'
      }
    },
    mounted () {
      // 在此处接收数据
      eventBus.$on('my-event', (val) => {
        this.page = val
      })
    }

  }

  const Footer = {
    template: '#footer',
    methods: {
      sendData (val) {
        eventBus.$emit('my-event', val)
      }
    }
  }

  new Vue({
    components: {
      MyContent: Content,
      MyFooter: Footer
    }
  }).$mount('#app')
</script>
</html>

vue3中没有明确的兄弟组件传值的方案,可以使用状态提升(找到这两个组件共同的父级组件,然后通过父与子之间的传值实现)

完整案例:05_component/41_brother_value-vue3.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>vue3兄弟组件传值-状态提升</title>
</head>
<body>
  <div id="app">
    <my-content :page="page"></my-content>
    <my-footer @change-page="changePage"></my-footer>
  </div>

</body>
<template id="content">
  <div>当前页面是:{{ page }}</div>
</template>
<template id="footer">
  <ul>
    <li @click="sendData('首页')">首页</li>
    <li @click="sendData('分类')">分类</li>
    <li @click="sendData('购物车')">购物车</li>
    <li @click="sendData('我的')">我的</li>
  </ul>
</template>
<script src="../lib/vue.global.js"></script>
<script>
  const Content = {
    template: '#content',
    props: {
      page: String
    }
  }

  const Footer = {
    template: '#footer',
    methods: {
      sendData (val) {
        this.$emit('change-page', val)
      }
    }
  }

  Vue.createApp({
    data () {
      return {
        page: '首页'
      }
    },
    methods: {
      changePage (val) {
        this.page = val
      }
    },
    components: {
      MyContent: Content,
      MyFooter: Footer
    }
  }).mount('#app')
</script>
</html>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值