Vue

  1. 知识点
    1. 插值语法 && 指令语法
    2. 数据绑定
    3. el和data的两种写法
      1. Vue 中的使用原则
    4. => 函数不是在new vue里面定义的嘛? 为什么还会指向window?
      1. 1. 区分“定义”和“执行”
      2. 2. 箭头函数的“词法绑定”规则
      3. 3. 为什么普通函数就没问题?
      4. 总结
    5. 箭头函数无法被 call 或 bind 改变 this 指向是因为他是匿名函数?
      1. 1. 核心原因:箭头函数没有“this”
      2. 2. 为什么说和“匿名”无关?
      3. 3. 一个简单的类比
    6. MVVM模型
  2. 测试一下2: {{ $options }} </h1> <hr/> <h1>测试一下3: {{ $emit }} </h1> <hr/> <h1>测试一下4: {{ _c }} </h1> <hr/> </div> <script type="text/javascript"> //view model view与model之间的纽带 //vm view实例对象 const vm = new Vue({ el: '#root', data(){ //model return { name:'武汉科技大学', address: '武汉' } } }); console.log(vm); </script> </body> </html>
    1. 🧱 M - 模型
    2. 🎨 V - 视图
    3. 🔄 VM - 视图模型
    4. 🚀 核心优势:双向绑定
    5. 🔍 补充:你代码中注释的那两点观察
    6. 📌 总结
  3. 数据代理-Object.defineProperty方法
    1. 1. 核心概念:它不只是赋值
    2. 2. 你的代码解析:数据劫持与代理
      1. 属性的“读取”监控 (get)
      2. 属性的“修改”监控 (set)
    3. 3. 为什么遍历不到 age?
    4. 4. 形象的比喻
    5. 5. 它的实战意义(Vue 2 的核心)
  4. 数据代理-何为数据代理?
    1. 🧩 代码拆解:它是如何工作的?
    2. 🎬 生活中的比喻
    3. 💡 为什么要用数据代理?(Vue 的视角)
  5. vue中的数据代理
    1. 🎭 什么是数据代理?
    2. 🌉 基本原理:Object.defineProperty 的魔法
    3. 🤔 为什么要这么做?(好处)
    4. 🔍 验证一下
  6. 事件处理-键盘事件
    1. 🧱 基础用法
    2. 🏷️ 常用按键别名
    3. ⚙️ 系统修饰键 (特殊用法)
    4. ✨ 自定义别名与连用
    5. 💡 关于 tab 键的特别说明
    6. 📌 总结
  7. 事件处理-Command 键
    1. 1. 核心概念:.meta
    2. 2. 具体指代:看你的电脑系统
    3. 3. 为什么会有 “command” 这种写法?
    4. 💡 总结与建议
  8. 事件处理的基本使用
    1. ⌨️ 键盘事件:.meta 修饰符的奥秘
      1. @keydown.meta="showInfo"
      2. @keyup.meta="showInfo"
    2. 🖱️ 鼠标事件:参数与 this
      1. 1. 访问原生事件对象
      2. 2. 正确的 this 指向
    3. 💡 一个潜在的优化点
    4. 📌 总结
  9. 事件修饰
    1. 🛠️ 核心机制:事件修饰符 (Event Modifiers)
      1. 1. 阻断与限制类
      2. 2. 逻辑与性能类
    2. 🌊 事件流:捕获与冒泡
      1. 1. 默认的冒泡模式 (Bubbling)
      2. 2. 捕获模式 (Capturing)
    3. 🚀 性能优化:.passive 的妙用
    4. ⚠️ 关于 this 的再次强调
    5. 📌 总结与建议
  10. 计算属性
    1. 🧮 什么是计算属性?
    2. ⚙️ 工作原理:依赖追踪与缓存
    3. 📝 简写形式
    4. 🎬 代码运行流程图解
    5. 💡 总结
  11. 监视属性
    1. 1. 核心机制:vm.$watch
    2. 2. Vue 的监测奥秘:numbers 对象
      1. ❓ 问题:为什么修改 numbers.c.d 能被监测到?
      2. ⚠️ 注意:deep: true 在这里是“多余”的?
    3. 3. 计算属性 vs. 侦听属性
    4. 4. 代码运行预测
    5. 📌 总结
  12. 计算属性 && 监视属性
    1. 1. computed vs watch:同步与异步的鸿沟
      1. computed 的局限性(纯同步)
      2. watch 的灵活性(可异步)
    2. 2. 关于 this 指向的两个原则
      1. 原则一:Vue 管理的函数,用普通函数
      2. 原则二:非 Vue 管理的函数,用箭头函数
    3. 3. 代码运行流程图解
    4. 总结
  13. 绑定样式
    1. 1. 字符串写法 (v-bind:class / :class)
    2. 2. 数组写法 (:class)
    3. 3. 对象写法 (:class)
    4. 📌 总结与对比
    5. 💡 补充建议
  14. 条件渲染
    1. 1. v-if vs v-show:移除 vs 隐藏
      1. v-if:真正的“条件渲染”
      2. v-show:CSS 的“开关”
    2. 2. 性能与场景选择
      1. 使用 v-if 的场景
      2. 使用 v-show 的场景
    3. 3. v-if 的进阶用法
      1. 1. v-else-if 与 v-else
      2. 2. <template> 标签
    4. 4. 关于“获取元素”的备注
    5. 总结
  15. 列表渲染
  16. 收集表单数据
  17. 过滤器
  18. 内置指令
  19. 自定义指令
  20. 生命周期
  21. 非单文件组件
  22. 单文件组件

知识点

插值语法 && 指令语法

v-bind:XXX 或者 :XXX

案例:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>插值语法</title>
    <script src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <h1>hello, {{ name }}</h1>
        <hr/>
        <a v-bind:href="url_baidu" target="_blank">{{ url_baidu_name }}</a>
        <hr/>
        <p>学校: <a :href="school.url.toString()"> {{ school.name }} </a> </p>
    </div>
    <script>
        new Vue({
            el: '#root',
            data: {
                name: 'Jungle',
                url_baidu: 'https://www.baidu.com',
                url_baidu_name: '点击跳转->百度',
                school: {
                    name: '清华大学',
                    url: 'https://www.tsinghua.edu.cn/'
                }
            }
        })
    </script>
</body>
</html>

image-20260119173741648


数据绑定

Vue中有2种数据绑定的方式:
1.单向绑定(v-bind):数据只能从data流向页面。
2.双向绑定(v-model):数据不仅能从data流向页面,还可以从页面流向data。
备注:
1.双向绑定一般都应用在表单类元素上(如:input、select等)
2.v-model:value 可以简写为 v-model,因为v-model默认收集的就是value值。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue 数据绑定</title>
    <script src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <label>
            单项数据绑定:
            <!--<input type='text' v-bind:value="name"/>-->
            <!--简写-->
            <input type='text' :value="name"/>
        </label>
        <label>
            双向数据绑定:
            <!--<input type='text' v-model:value="name"/>-->
            <input type='text' v-model="name"/>
        </label>
        <br/>
         <!--
         不是什么都可用v-model的 这里v-model不支持h1
         v-model只能应用在表单元素上(输入元素),与用户交互(都有共同的value属性)
         -->
        <h1 v-bind:x="name">
            你好啊
        </h1>
    </div>
    <script type="text/javascript">
        //v-bind可以完成数据绑定(单项绑定)
        //v-model双向数据绑定
        //单项数据绑定 双向数据绑定
        Vue.config.productionTip = false;
        new Vue({
            el: '#root',
            data:{
                name: 'shanghai'
            }
        })
    </script>
</body>
</html>

image-20260120085800149


el和data的两种写法

data与el的2种写法

​ 1.el有2种写法

​ (1).new Vue时候配置el属性。

​ (2).先创建Vue实例,随后再通过vm.$mount(‘#root’)指定el的值。

​ 2.data有2种写法

​ (1).对象式

​ (2).函数式

​ 如何选择:目前哪种写法都可以,以后学习到组件时,data必须使用函数式,否则会报错。

​ 3.一个重要的原则:

​ 由Vue管理的函数(例如data),一定不要写箭头函数,一旦写了箭头函数,this就不再是Vue实例了。

el的第一种写法

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>el和data的两种写法</title>
    <script src="../js/vue.js"></script>
</head>
<body>
   <div id="root">
       <h1>你好,{{ name }}</h1>
   </div>
   <script type="text/javascript">
       Vue.config.productionTip = false;
       const v = new Vue({
           el: '#root', //第一种写法
           data: {
               name: '上海'
           }
       });
   </script>
</body>
</html>

el的第二种写法

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>el和data的两种写法</title>
    <script src="../js/vue.js"></script>
</head>
<body>
   <div id="root">
       <h1>你好,{{ name }}</h1>
   </div>
   <script type="text/javascript">
       Vue.config.productionTip = false;
       const vm = new Vue({
           data: {
               name: '上海'
           }
       });
       // 关联root容器,用mount方法
       setTimeout(() => {
           vm.$mount('#root'); //第二种写法 挂载到页面上
       }, 3000);
   </script>
</body>
</html>

data的第一种写法:对象式

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>el和data的两种写法</title>
    <script src="../js/vue.js"></script>
</head>
<body>
   <div id="root">
       <h1>你好,{{ name }}</h1>
   </div>
   <script type="text/javascript">
       Vue.config.productionTip = false;
       new Vue({
          el: '#root',
          //data的第一种写法:对象式
          data: {
             name: 'shanghai'
          }
       });
   </script>
</body>
</html>

第二种写法: 函数式(返回对象中包含你想渲染的模版数据)

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>el和data的两种写法</title>
    <script src="../js/vue.js"></script>
</head>
<body>
   <div id="root">
       <h1>你好,{{ name }}</h1>
   </div>
   <script type="text/javascript">
       Vue.config.productionTip = false;
       new Vue({
          el: '#root',
          //第二种写法: 函数式(返回对象中包含你想渲染的模版数据)
          //学习组件的时候要是用函数式 这个函数是Vue来调用的
          // data: () => { //箭头函数
          //    console.log(`@@@@`, this); //此时this是window,因为箭头函数没有自己的this,它向外查找找到的window
          //    return ({
          //         name: 'shanghai'
          //     })
          // }
          //尽量不要写成剪头函数,因为会丢失this
          data () { //普通函数
             console.log('@@@', this); //此时this是Vue
             return {
                 name: 'shanghai'
             }
          }
       });
   </script>
</body>
</html>

Vue 中的使用原则

一个简单的判断原则:

  • 由 Vue 管理的函数:例如 data(), methods 中的方法,computed 计算属性,生命周期钩子(如 mounted)等。这些函数需要被 Vue 调用,并且需要访问 Vue 实例 (this),所以必须使用普通函数
  • 不由 Vue 管理的函数:例如 setTimeout 的回调函数,或者一些需要保持 this 指向外部 Vue 实例的场景。为了避免 this 指向丢失,可以使用箭头函数来“继承”外层的 this

总结:在 Vue 2 的选项式 API 中,除了特定的回调场景,绝大多数情况下都应该使用普通函数

=> 函数不是在new vue里面定义的嘛? 为什么还会指向window?

简单直接的回答是:虽然函数是在 new Vue({}) 的配置对象里“定义”的,但决定 this 指向的不是“定义”的位置,而是“执行”的位置。

在箭头函数的特殊规则下,它向外找的“父级作用域”确实是全局的 window

我们来一步步拆解为什么会出现这种情况:

1. 区分“定义”和“执行”

请看下面这段代码,思考 this 指向谁:

const obj = {
    name: '我是obj',
    // 这里只是把函数定义好,存进去了,并没有运行
    sayHello: function() {
        console.log(this.name); 
    }
}

// 这里才是函数真正被执行的时候
// 谁调用的?是 obj.sayHello() 调用的,所以 this 指向 obj。
obj.sayHello(); 

回到你的 Vue 代码:

  • 定义时:你在 new Vue({...}) 的大括号里写了箭头函数。此时,这段代码处于全局作用域(或者模块作用域)中。
  • 执行时:Vue 底层拿到你传入的对象后,会去执行这个 data 函数。

对于普通函数,Vue 可以通过 .call().apply() 强行指定它的 this 是 Vue 实例。
对于箭头函数,它没有自己的 this,它会忽略 Vue 的调用方式,直接去“捕获”它定义时所在环境的 this

2. 箭头函数的“词法绑定”规则

箭头函数的 this 遵循词法作用域。它不关心是谁调用的,只关心它写在哪儿

在你的 HTML 文件中,<script> 标签里的代码属于全局作用域

// 这里的环境就是全局环境
// this 指向 window (非严格模式下)
console.log('脚本顶层的this:', this); 

new Vue({
    data: () => {
        // 箭头函数没有自己的 this
        // 它向外层(即 <script> 标签的顶层)查找
        // 所以这里的 this 就是上面打印出的 window
        console.log('箭头函数里的this:', this); 
        return {}
    }
})

3. 为什么普通函数就没问题?

普通函数(function)的 this动态绑定的,取决于“谁调用了我”。

Vue 源码内部大致是这样处理的:

// 伪代码
const vm = new Vue(options);
// Vue 拿到你传入的 data 函数,并用 call 方法调用它
// 这里的 this 就被强行指向了 vm(Vue 实例)
options.data.call(vm); 

因为普通函数的 this 是由调用者(vm)决定的,所以能正确指向 Vue 实例。而箭头函数无法被 callbind 改变 this 指向,它死死地绑定了定义时的 window

总结

虽然看起来箭头函数是写在 new Vue({...}}) 里面的,但在 JavaScript 引擎看来:

  1. 定义位置:它处于全局 <script> 环境中。
  2. 父级作用域:它的父级就是全局作用域,也就是 window
  3. 结果:箭头函数会把 this 永久地绑定为 window,无论 Vue 怎么调用它,都无法改变。

箭头函数无法被 call 或 bind 改变 this 指向是因为他是匿名函数?

不是的,这两者没有直接关系。“是否是匿名函数”与“this指向能否被改变”无关

箭头函数无法被 callbind 改变 this 指向,根本原因在于它的设计机制:**箭头函数没有自己的 this**。

为了让你彻底理解,我来拆解一下这两者的逻辑:

1. 核心原因:箭头函数没有“this”

普通函数(无论具名还是匿名)在被调用时,JavaScript 引擎会为它创建一个执行上下文,并绑定一个 this。这个 this 可以被 callapplybind 强行修改。

而箭头函数不同,它**根本不去创建自己的 this**。它在定义时,就通过“词法作用域”直接“继承”了外层(父级)作用域的 this

既然它内部没有属于自己的 thiscallbind 就没有东西可以去“修改”。你传入的参数(thisArg)会被它直接忽略。

2. 为什么说和“匿名”无关?

匿名函数只是指“没有名字的函数”。普通函数也可以是匿名的,而匿名的普通函数完全可以被 call 改变 this

反例证明:
下面这段代码是一个匿名的普通函数,你看它是可以被 call 改变 this 的:

// 这是一个匿名函数(没有名字),也是一个普通function
const normalAnon = function() {
    console.log(this.name);
};

// 虽然是匿名的,但是 call 完全生效!
normalAnon.call({name: '我是Call改变的'}); // 输出: 我是Call改变的

结论:
只要是普通函数(哪怕没有名字),它就有 this,就能被改变。
只要是箭头函数(哪怕你给它赋值给一个变量),它就没有 this,就不能被改变。

3. 一个简单的类比

你可以把普通函数想象成“租房客”call 就像是中介,可以强行把租房客塞进不同的房子里(改变 this)。

而箭头函数就像是“跟屁虫”,它没有自己的家,它永远紧紧跟着它爸爸(外层作用域)。无论中介(call)怎么折腾它,它都死死抓着它爸爸不放,根本不住进你给它的新房子里。


MVVM模型

MVVM模型
1. M:模型(Model) :data中的数据
2. V:视图(View) :模板代码
3. VM:视图模型(ViewModel):Vue实例
观察发现:
1.data中所有的属性,最后都出现在了vm身上。
2.vm身上所有的属性 及 Vue原型上所有属性,在Vue模板中都可以直接使用。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>mvvm模型的理解</title>
    <script src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <!--view-->
        <h1>学校名称: {{ name }} </h1> <hr/>
        <h1>学校地址:{{ address }} </h1> <hr/>
        <h1>测试一下1:  {{ 1 + 1 }} </h1> <hr/>
        <!--所有viewModel和vue原型上的属性在视图中的{{}}都可以看到-->
        

测试一下2: {{ $options }} </h1> <hr/> <h1>测试一下3: {{ $emit }} </h1> <hr/> <h1>测试一下4: {{ _c }} </h1> <hr/> </div> <script type="text/javascript"> //view model view与model之间的纽带 //vm view实例对象 const vm = new Vue({ el: '#root', data(){ //model return { name:'武汉科技大学', address: '武汉' } } }); console.log(vm); </script> </body> </html>

运行效果:

image-20260120104546910

MVVM 其实就是一种让网页开发更高效的“分工协作”模式。结合你提供的代码来看,它把页面拆解为三个明确的角色,让数据和界面各司其职,又能自动同步。

我们可以用你代码中的例子来具体拆解这三个部分:

🧱 M - 模型

  • 是什么:对应你代码中的 data 函数返回的对象。

  • 职责:它就是纯粹的数据,不包含任何 HTML 标签或样式。

  • 例子

    data(){
      return {
        name: '武汉科技大学', // 学校名字数据
        address: '武汉'         // 学校地址数据
      }
    }
    

    这就是模型。它只负责存储和管理数据。

🎨 V - 视图

  • 是什么:对应你代码中的 <div id="root"> 里的 HTML 模板。

  • 职责:它是用户看到的界面。它负责决定数据长什么样。

  • 例子

    <h1>学校名称: {{ name }}</h1>
    <h1>学校地址: {{ address }}</h1>
    

    这就是视图。它只负责展示,它通过

    {{ }}
    

    告诉系统:“我要在这里显示

    name
    

    address
    

    的值”。

🔄 VM - 视图模型

  • 是什么:对应你代码中的 new Vue({}) 实例(也就是 vm 对象)。

  • 职责:它是连接 MV 的纽带(桥梁)。它的核心任务是监听数据变化,并自动更新界面;或者监听用户操作(如点击、输入),并自动更新数据。

  • 例子

    const vm = new Vue({
      el: '#root', // 指定控制哪个区域
      data(){ ... } // 提供数据
    });
    

    Vue 实例(VM)内部使用了数据劫持(Object.defineProperty 或 Proxy)和发布订阅模式。它会默默地盯着 data 里的数据(M),一旦数据变了(比如 data 改了),它就立刻通过 el 找到视图(V),把页面上对应的 刷新成新的值。

🚀 核心优势:双向绑定

MVVM 最大的魅力在于“自动同步”

在传统的开发(如原生 JS)中,你需要手动操作 DOM:

  1. 数据变了 -> 2. 找到 DOM 元素 -> 3. 修改 innerHTML。

而在 MVVM 中,这一切都被 VM 搞定了:

  1. 数据(M)变了 -> 2. VM 监听到 -> 3. 自动更新视图(V)。

你只需要关心数据(M),视图(V)会自动跟着变。这就是所谓的数据驱动视图

🔍 补充:你代码中注释的那两点观察

你的代码注释中提到了两个非常关键的细节,这正是 Vue 实现 MVVM 的具体表现:

  • data 中所有的属性,最后都出现在了 vm 身上: Vue 把 data 里的数据代理到了 Vue 实例(vm)上。所以你既可以用 vm.$data.name 访问,也可以直接用 vm.name 访问。
  • vm 身上所有的属性及 Vue 原型上所有属性,在 Vue 模板中都可以直接使用: 这就是为什么你在 {{ }} 里可以直接写 1+1,或者直接调用 Vue 实例上的方法(如 $emit)。因为模板里的代码是在 Vue 实例的上下文中执行的,this 指向的就是 VM。

📌 总结

  • M:就是数据(data)。
  • V:就是页面(HTML)。
  • VM:就是 Vue 实例(new Vue()),它让 M 和 V 保持同步,开发者无需手动操作 DOM。

数据代理-Object.defineProperty方法

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Object.defineProperty</title>
</head>
<body>
   <script type="text/javascript">
       let number = 18;
       let person = {
           name: '张三',
           sex:'男'
       }

       //此时age是不可以枚举的
       //往person对象里添加属性
       //关联
       Object.defineProperty(person,'age', {
           // value: 18,
           // enumerable: true, //此时代表这个属性是可以枚举的
           // writable: true, //代表可以重写该属性(控制属性是否被修改)
           // configurable:true, //控制属性是否可以被删除 默认为false


           //当读取person的age属性时get属性就会被调用,且返回值就是age的值
           //invoke property proxy映射数据代理
           get: function () {
               //测试它的调用情况
               console.log('@@@ GET AGE');
               //此时age的值依赖number的值
               return number
           },
           //当修改person的age属性时set(setter)属性就会被调用,且会收到修改的具体值
           set(v) {
               //测试
               console.log('CHANGE AGE');
               number=v;
           }
       });

       // console.log(Object.keys(person))

       //遍历
       // for(var p  in  person){
       //     console.log('@@', person[p])
       // }



       console.log(person);
   </script>
</body>
</html>

Object.defineProperty() 是 JavaScript 中一个非常底层且强大的方法,你可以把它理解为给对象属性装上了一个“智能监控探头”(或者叫“拦截器”)。

在你的代码中,它不仅仅是在给 person 对象添加一个 age 属性,而是在定义一种关系:当你这个属性时,执行什么逻辑;当你这个属性时,执行什么逻辑。

结合你的代码,我来为你拆解这个概念:

1. 核心概念:它不只是赋值

通常我们给对象加属性是这样做的:

person.age = 18; // 简单粗暴,存进去一个死数据

这种方式存进去的只是一个静态值

Object.defineProperty 是这样做的:

Object.defineProperty(person, 'age', { /* 描述符 */ });

它允许你通过描述符(Descriptor)对象,精确地控制这个属性的行为。它就像是给 age 属性立了一份“合同”,规定了读写时的规则。

2. 你的代码解析:数据劫持与代理

你的代码实现了一个非常经典的数据代理(Data Proxy)效果。我们来看这份“合同”是怎么签的:

属性的“读取”监控 (get)

get: function () {
    console.log('@@@ GET AGE'); // 只要读age,这句话就会打印
    return number; // 实际返回的是外部变量 number 的值
}
  • 含义:你告诉 JavaScript:“每当有人问 person.age 是多少时,请不要直接拿内存里的值,而是先执行这个 get 函数,并把它的返回值当作 age 的值。”
  • 效果person.age 的值不再是固定的,而是动态计算出来的(它依赖于 number 变量)。

属性的“修改”监控 (set)

set(v) {
    console.log('CHANGE AGE'); // 只要改age,这句话就会打印
    number = v; // 把新值 v 存到外部变量 number 里
}
  • 含义:你告诉 JavaScript:“每当有人试图修改 person.age 时,请执行这个 set 函数,并把用户赋的新值传进来。”
  • 效果:修改 person.age 并不会直接改变 person 内部的某个私有字段,而是改变了外部的 number 变量。

3. 为什么遍历不到 age?

你在代码中写了:

console.log(Object.keys(person)) // 结果是 ['name', 'sex']

你会发现 age 不在其中。这是 Object.defineProperty 的另一个特性:默认配置

如果不显式设置,通过 defineProperty 定义的属性默认是不可枚举的(enumerable: false)。

  • 不可枚举意味着:它不会出现在 for...in 循环、Object.keys()JSON.stringify() 的结果中。
  • 潜台词:这个属性是“隐藏”的,虽然你可以通过 person.age 访问它,但程序遍历对象时会假装看不见它。

4. 形象的比喻

你可以把 Object.defineProperty 想象成房产中介

  1. 普通属性:就像你直接把房子钥匙给了别人(直接访问内存地址)。
  2. defineProperty 属性:你把房子钥匙锁在自己手里,告诉中介(get/set):“有人要看房(get),你带他去;有人要改装修(set),你告诉我”。

在你的代码里:

  • number 是真正的房子(数据源)。
  • person.age 是一个挂牌(虚拟属性)。
  • get/set 是中介。
  • 当你问 person.age 时,中介跑去 number 那里拿数据给你。
  • 当你改 person.age 时,中介把新数据存回 number

5. 它的实战意义(Vue 2 的核心)

你在学习 Vue,这个概念至关重要。Vue 2 的响应式原理就是基于 Object.defineProperty 实现的。

  • 依赖收集:当模板渲染读取 person.age 时,get 被触发,Vue 知道“哦,这个视图依赖了 age”。
  • 派发更新:当数据改变(set 被触发)时,Vue 知道“age 变了”,于是通知所有依赖它的视图去重新渲染。

数据代理-何为数据代理?

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>定义数据代理</title>
</head>
<body>
    <!-- 数据代理,通过一个对象代理另一个对象中属性的操作  -->
    <script type="text/javascript">
        let obj = { x:200 }
        let obj2 = { y:200 }
        //通过obj2代理obj,来操作obj的x
        Object.defineProperty(obj2, 'x',{
            get(){
                return obj.x
            },
            set(v){
                obj.x = v;
            }
        });

    </script>
</body>
</html>

数据代理就是:找一个“替身”(代理对象),让你通过这个替身去操作另一个对象(目标对象)的属性。

在你的代码中,obj2 就是那个“替身”(代理),obj 是真正的“本人”(目标)。你表面上是在操作 obj2.x,实际上是在读写 obj.x

🧩 代码拆解:它是如何工作的?

让我们看看你的代码是如何实现这个“替身”机制的:

let obj = { x: 200 }      // 目标对象(真正的数据持有者)
let obj2 = { y: 200 }     // 代理对象(替身)

这里我们有两个独立的对象。现在,我们通过 Object.definePropertyobj2 装上一个“监控器”:

  1. **定义读取行为 (get)**:

    get(){
        return obj.x // 当你问 obj2.x 时,它实际上去问 obj.x
    }
    
    • 效果:当你访问 obj2.x 时,JavaScript 会执行这里的 get 函数,它会去 obj 那里把 x 的值拿回来。
    • 比喻:你问替身“你的年龄是多少?”,替身转身去问本人,然后把本人的答案告诉你。
  2. **定义修改行为 (set)**:

    set(v){
        obj.x = v; // 当你修改 obj2.x 时,它实际上修改的是 obj.x
    }
    
    • 效果:当你给 obj2.x 赋值(比如 obj2.x = 300)时,JavaScript 会执行这里的 set 函数,它会把新值存入 obj.x
    • 比喻:你告诉替身“把你的年龄改成30”,替身转身去把本人的年龄改成了30。

🎬 生活中的比喻

想象一下“明星”“经纪人”的关系:

  • **目标对象 (obj)**:大牌明星(真正的数据源)。
  • **代理对象 (obj2)**:明星的经纪人(代理)。
  • :应用程序。

当你想知道明星的行程(数据)时,你不直接联系明星obj),而是联系经纪人(obj2)。

  • 读取:你问经纪人:“明星明天在哪?” 经纪人(get)去查明星的日程表,然后告诉你。
  • 修改:你告诉经纪人:“把明星明天的行程改到上海。” 经纪人(set)去修改明星的日程表。

你操作的一直是经纪人,但你影响的却是明星。 这就是数据代理。

💡 为什么要用数据代理?(Vue 的视角)

在 Vue 2 中,这个机制被用来实现响应式系统

  1. 数据劫持:Vue 把你的 data 对象(obj)里的所有属性,通过 Object.defineProperty 代理到了 Vue 实例(vm / obj2)上。
  2. 监听变化:因为有了 getset,Vue 就能在你读数据时get)知道“哦,这个组件依赖了这个数据”,在你改数据时set)知道“哦,数据变了,我要去更新页面”。

vue中的数据代理

1.Vue中的数据代理:
通过vm对象来代理data对象中属性的操作(读/写)
2.Vue中数据代理的好处:
更加方便的操作data中的数据
3.基本原理:
通过Object.defineProperty()把data对象中所有属性添加到vm上。
为每一个添加到vm上的属性,都指定一个getter/setter。
在getter/setter内部去操作(读/写)data中对应的属性。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue中的数据代理</title>
    <script src="../js/vue.js"></script>
</head>
<body>
     <div id="root">
         <h2>学校名称:{{ name }}</h2>
         <h2>学校地址: {{ address }}</h2>
     </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        let data;
        //通过vm代理
        const vm = new Vue({
           el:'#root',
            //vm._data === options.data = data
            //
           data(){
               return data = {
                   name:'panyue',
                   address:'shanghai'
               }
           }
        });
    </script>
</body>
</html>

Vue 2 的数据代理机制确实就是这么运作的。

结合你的代码,我们可以把这个过程看作是 Vue 在 vm(Vue 实例)和 data(数据源)之间搭建了一座“隐形桥梁”。下面我来配合代码,一步步拆解你列出的这三个点:

🎭 什么是数据代理?

定义:通过 vm 对象来代理对 data 对象中属性的操作(读/写)。

在你的代码中:

  • 目标对象(真正的数据)data 变量(值为 { name: 'panyue', address: 'shanghai' })。
  • 代理对象(替身)vm(Vue 实例)。

现象

  • 模板中写的是 {{ name }}(看起来像是在找 vmname)。
  • 脚本中写的是 vm.name = 'new'
  • 但实际上,name 属性原本并不属于 vm,它属于 datavm 只是充当了“中间人”。

🌉 基本原理:Object.defineProperty 的魔法

Vue 内部在初始化时,大致做了这么一件事(模拟源码逻辑):

// 1. 拿到你的 data
let data = { name: 'panyue', address: 'shanghai' };

// 2. 遍历 data 的所有属性
Object.keys(data).forEach(key => {
  // 3. 把属性添加到 vm 上,并定义 get/set
  Object.defineProperty(vm, key, {
    get() {
      // 读取 vm.name 时,实际读取 data.name
      return data[key]; 
    },
    set(newValue) {
      // 修改 vm.name 时,实际修改 data.name
      data[key] = newValue; 
    }
  });
});

发生了什么?

  • 当你访问 vm.name 时,触发了 get,它去 data 里取值。
  • 当你修改 vm.name = 'hello' 时,触发了 set,它去 data 里改值。

🤔 为什么要这么做?(好处)

你提到“方便操作数据”,这不仅仅是少打几个字,而是有更深层的设计考量:

  1. 模板中无需前缀
    • 如果没有代理,你在模板里可能得写 {{ data.name }}
    • 有了代理,直接写 {{ name }},代码更简洁,语义更清晰。
  2. 统一上下文
    • 在 Vue 的 methodscomputed 中,this 指向 vm
    • 因为有了代理,你直接用 this.name 就能拿到数据,不需要去查文档或者记混该用 this.$data.name 还是 this.name
  3. 实现响应式的基础(最关键!)
    • 代理层(get/set)是 Vue 的“监控室”。
    • 依赖收集:在 get 里,Vue 知道“哦,这个视图用到了这个数据”。
    • 派发更新:在 set 里,Vue 知道“哦,数据变了,我要去通知视图更新”。

🔍 验证一下

你可以试着在你的代码后面加上这几行,看看代理的“痕迹”:

// 1. 证明 vm 代理了 data
console.log(vm.name); // 'panyue' (直接通过 vm 读取)

// 2. 修改 vm 的属性,实际上修改了 data
vm.name = 'changed';
console.log(data.name); // 'changed' (data 被同步修改了)

// 3. 查看 vm 的属性描述符
// 你会发现 vm.name 是一个访问器属性 (有 get/set),而不是一个数据属性
console.log(Object.getOwnPropertyDescriptor(vm, 'name'));

总结:
Vue 通过 Object.definePropertydata 的属性“映射”到了 vm 上,让你感觉数据就在 vm 上一样。这既方便了开发者,又让 Vue 有机会在底层监听所有的数据变动,从而实现视图的自动更新。


事件处理-键盘事件

1.Vue中常用的按键别名:
回车 => enter
删除 => delete (捕获“删除”和“退格”键)
退出 => esc
空格 => space
换行 => tab (特殊,必须配合keydown去使用)
上 => up
下 => down
左 => left
右 => right

2.Vue未提供别名的按键,可以使用按键原始的key值去绑定,但注意要转为kebab-case(短横线命名)

3.系统修饰键(用法特殊):ctrl、alt、shift、meta
(1).配合keyup使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
(2).配合keydown使用:正常触发事件。

4.也可以使用keyCode去指定具体的按键(不推荐)

5.Vue.config.keyCodes.自定义键名 = 键码,可以去定制按键别名

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue 键盘事件</title>
    <script src="../js/vue.js"></script>
</head>
<body>
  <div id="root">
        <!--vue中键盘事件的绑定 一般用keyUp(keydown)-->
        <h1>欢迎来到{{ name }}</h1>
        <input type="text" @keyup="showKeyInfo" placeholder="按键映射---按下回车提示输入"/>
        <input type="text" @keyup.enter="showInfo" placeholder="按下回车提示输入"/>
        <input type="text" @keyup.delete="showInfo" placeholder="退格或删除提示输入"/>
        <input type="text" @keyup.esc="showInfo" placeholder="按下esc提示输入"/>
        <input type="text" @keydown.tab="showInfo" placeholder="按下tab示输入"/>
        <input type="text" @keyup.ctrl="showInfo" placeholder="[ctrl]按下command提示输入"/>
        <input type="text" @keydown.p="showInfo" placeholder="[ctrl]按下p提示输入"/>
        <!--键盘事件的修饰符也可以连用-->
       <input type="text" @keydown.command.g="showInfo" placeholder="[ctrl]按下command和g提示输入"/>
    </div>
    <script type="text/javascript">
        new Vue({
            el:'#root',
            data:{
                name: 'shanghai'
            },
            methods:{
                showKeyInfo:function (e){
                    console.log("按键名: ", e.key, "按键码: ",  e.keyCode);
                    console.log(e.target.value);
                },
                showInfo:function (e){
                    // if(e.keyCode === 13) {
                    //     console.log(e.target.value);
                    // }
                    console.log(e.target.value);
                    console.log(e.key); //按下的名字
                }
            }
        })
    </script>
</body>
</html>

这段代码完美地演示了 Vue 2 中如何优雅地处理键盘事件。Vue 对原生的 keydownkeyup 事件进行了增强,提供了按键别名修饰符,让我们不再需要死记硬背 keyCode

结合你的代码和注释,我来为你梳理一下 Vue 键盘事件的核心要点:

🧱 基础用法

绑定方式:@keyup@keydown

  • **@keyup**:释放按键时触发。
  • **@keydown**:按下按键时触发(不放会一直触发)。

🏷️ 常用按键别名

Vue 为常用的按键提供了别名,直接通过 . 调用,无需记忆 keyCode。

别名 对应按键 备注
.enter 回车键 常用于表单提交
.delete 删除/退格键 捕获 Delete 和 Backspace
.esc 退出键 常用于关闭弹窗
.space 空格键
.tab 制表键 特殊:一般配合 keydown 使用,因为无法监听“释放 tab”
.up / .down / .left / .right 方向键

⚙️ 系统修饰键 (特殊用法)

修饰键包括 .ctrl.alt.shift.meta (Mac 上的 command 键)。

它们的触发逻辑取决于你配合的是 keydown 还是 keyup

  • 配合 @keydown:按下修饰键时,事件正常触发
  • 配合 @keyup:必须先按住修饰键,再按其他键,最后释放其他键,事件才会触发。

代码示例解析:

<!-- 按下 command 键时,事件就会触发 -->
<input type="text" @keydown.meta="showInfo" />

<!-- 必须先按住 command,再按其他键,最后松开其他键才会触发 -->
<input type="text" @keyup.meta="showInfo" />

✨ 自定义别名与连用

  1. 连用修饰符
    你可以将多个修饰符连在一起使用,表示“组合键”。

    <!-- 按下 command + g 才触发 -->
    <input type="text" @keydown.command.g="showInfo" />
    
  2. 自定义按键别名
    如果 Vue 没有提供你需要的别名(比如 F1 键),你可以通过全局配置添加:

    // 定义一个叫 'f1' 的别名,对应 keyCode 112
    Vue.config.keyCodes.f1 = 112;
    

    然后就可以这样使用:

    <input type="text" @keyup.f1="showInfo" />
    

💡 关于 tab 键的特别说明

在你的代码中,tab 键必须配合 keydown 使用:

<input type="text" @keydown.tab="showInfo" />

原因tab 键的主要功能是切换焦点。如果你使用 @keyup.tab,当用户按下 tab 并释放时,焦点已经转移到下一个元素了,此时 keyup 事件往往无法被正确监听到。

📌 总结

Vue 的键盘事件修饰符极大地简化了开发体验:

  • 别名:告别记忆 keyCode
  • 修饰符:轻松实现组合键判断。
  • 自定义:灵活扩展。

注意:在 Vue 3 中,.keyCode 的使用已被移除,推荐直接使用按键名称(kebab-case 格式,如 .page-down)。


事件处理-Command 键

在 Vue 的键盘事件修饰符中,command 实际上是 .meta 修饰符的别名(在 Mac 系统下)。

为了让你彻底理解,我们需要把它拆解为 “概念名称”“具体按键” 两个层面来看:

1. 核心概念:.meta

在 Vue 的官方文档和源码中,这个修饰符正式的名字叫 **.meta
它的设计初衷是为了匹配操作系统键盘上的
“元键” (Meta Key)**。

2. 具体指代:看你的电脑系统

这个 .meta 键在不同操作系统对应的物理按键是不同的:

系统 对应的物理按键 符号 备注
Mac OS Command 键 (Cmd) 在 Mac 下,.meta 就是 command
Windows Windows 徽标键 (Win) 在 Windows 下,.meta 就是开始键

3. 为什么会有 “command” 这种写法?

虽然 Vue 官方定义的修饰符是 .meta,但在实际开发中(特别是 Mac 用户居多的开发环境),大家习惯称之为 command。

在你的代码中,Vue 为了方便开发者,允许你直接使用按键的名称作为修饰符。

  • 因为 Mac 键盘上那个键印的是 “command”,
  • 所以 Vue 支持你直接写 .command,它会自动识别这等同于 .meta

💡 总结与建议

在你的代码中:

<!-- 这两种写法在 Mac 上是等价的 -->
<input @keydown.meta="doSomething"> 
<input @keydown.command="doSomething"> 
  • **.meta**:是标准的、跨平台的写法(推荐,因为它更通用)。
  • **.command**:是语义化的写法,特指 Mac 上的 command 键(可读性更好,但仅限于你知道目标用户是 Mac 的情况)。

事件处理的基本使用

事件的基本使用:
1.使用v-on:xxx 或 @xxx 绑定事件,其中xxx是事件名;
2.事件的回调需要配置在methods对象中,最终会在vm上;
3.methods中配置的函数,不要用箭头函数!否则this就不是vm了;
4.methods中配置的函数,都是被Vue所管理的函数,this的指向是vm 或 组件实例对象;
5.@click=”demo” 和 @click=”demo($event)” 效果一致,但后者可以传参;

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue 键盘事件</title>
    <script src="../js/vue.js"></script>
</head>
<body>
  <div id="root">
        <!--vue中键盘事件的绑定 一般用keyUp(keydown)-->
        <h1>欢迎来到 {{ name }} </h1>
        <!-- 按下 command 键时,事件就会触发 -->
        <input type="text" @keydown.meta="showInfo" placeholder="按下command键提示输入"/>
        <!-- 必须先按住 command,再按其他键,最后松开其他键才会触发 -->
        <input type="text" @keyup.meta="showInfo" placeholder="松开command键提示输入"/>
        <hr/>
        <!--指令语法 v开头 例如v-on:click点击事件-->
        <button v-on:click="ClickInfo"> 点击提示输入1 </button>
        <!--简写形式 @click-->
        <button @click="showClickInfo($event, 'hello vue')"> 点击提示输入2 </button>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        const vm= new Vue({
            el:'#root',
            data:{
                name: '江西-南昌'
            },
            methods:{
                showInfo:function (e){
                    console.log(e.target.value);
                    console.log(e.key); //按下的名字
                }, 
                ClickInfo(e) {
                    console.log('点击提示输入1');
                    console.log(e.target);
                    console.log(this === vm ? 'this 指向 vm' : 'this 指向 事件触发的元素');
                },
                showClickInfo(e, msg) {
                    console.log('点击提示输入2');
                    console.log(e.target);
                    console.log('接收的参数是:  ' + msg);
                }
            }
        })
    </script>
</body>
</html>

这段代码非常棒,它同时涵盖了 Vue 中键盘事件的特殊机制和鼠标事件的基础用法。通过它,我们可以清晰地对比 Vue 如何处理不同类型的用户交互。

结合你的代码,我来为你详细解读其中的关键点:

⌨️ 键盘事件:.meta 修饰符的奥秘

你代码中的输入框完美展示了 .meta(Command/Win 键)在 keydownkeyup 下的不同表现:

@keydown.meta="showInfo"

  • 触发逻辑:只要按下 Command 键,事件就会立即触发。
  • 场景:适用于需要“长按”执行操作的功能(比如连续加速)。

@keyup.meta="showInfo"

  • 触发逻辑:必须先按住 Command 键,再按另一个键(如 A),然后释放那个另一个键,事件才会触发。
  • 注意:如果你只按 Command 键然后松开,是不会触发的。
  • 场景:这种写法比较少用,通常我们更习惯用 keydown 来监听组合键。

🖱️ 鼠标事件:参数与 this

你定义的两个按钮展示了 Vue 事件处理的两个核心细节:

1. 访问原生事件对象

在原生 JS 中,获取事件对象可能需要处理兼容性问题。但在 Vue 中,Vue 会将原生 DOM 事件作为第一个参数传入回调函数。

  • 代码中:ClickInfo(e)showClickInfo(e, msg) 的第一个参数 e 就是原生事件对象。
  • 用法:你可以通过 e.target 获取触发事件的 DOM 元素。

2. 正确的 this 指向

methods 中定义的方法,Vue 会自动将 this 绑定为 Vue 实例(vm)。

  • 代码中:console.log(this === vm ? ...) 会输出 this 指向 vm
  • 重要性:这意味着你可以在事件处理函数中直接通过 this.name 访问数据,或者调用 this 上的其他方法。

💡 一个潜在的优化点

在你的 showClickInfo 方法中,你传入了字符串 'hello vue'。在 Vue 模板中,如果你不加引号,Vue 会把它当成变量名去 data 里找;加了引号,它才会被当作普通的字符串参数。

你的写法是完全正确的:

@click="showClickInfo($event, 'hello vue')"
<!-- 注意 'hello vue' 的单引号 -->

📌 总结

这段代码展示了 Vue 事件处理的三大支柱:

  • 指令v-on:@ 绑定监听。
  • 修饰符.meta.enter 等让按键监听更语义化。
  • 方法:在 methods 中集中管理逻辑,享受自动绑定的 this 和便捷的参数传递。

事件修饰

Vue中的事件修饰符:

​ 1.prevent:阻止默认事件(常用);

​ 2.stop:阻止事件冒泡(常用);

​ 3.once:事件只触发一次(常用);

​ 4.capture:使用事件的捕获模式;

​ 5.self:只有event.target是当前操作的元素时才触发事件;

​ 6.passive:事件的默认行为立即执行,无需等待事件回调执行完毕;

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue 键盘事件</title>
    <script src="../js/vue.js"></script>
    <style>
        .demo1{
            height: 100px;
            background: deepskyblue;
            padding: 20px;
        }
        .box1{
            height: 100px;
            background: pink;
            padding: 20px;
        }
        .box2{
            height: 50px;
            background: lightgreen;
            padding: 20px;
        }
        .btn3{
            height: 30px;
            background: rgb(185, 230, 173);
            padding: 5px;
            border-radius: 5px;
        }
        .list{
            height: 200px;
            width: 200px;
            background: salmon;
            overflow: auto;
        }
        .list li {
            height: 100px;
        }
    </style>
</head>
<body>
  <div id="root">
        <!--vue中键盘事件的绑定 一般用keyUp(keydown)-->
        <h1>欢迎来到 {{ name }} </h1>
        <br/><br/>
        <a href="https://www.baidu.com" @click.prevent="showInfo"> 阻止默认事件(常用) </a>
        <div class="demo1" @click="showInfo">
            <button @click.stop="showInfo"> 阻止事件冒泡(常用) </button>
        </div>
        <br/><br/>
        <button @click.once="showInfo"> 点击只能触发一次 </button>
        <div class="box1" @click.capture="showMsg('box1')">
            box1 - capture事件的捕获模式, 让事件以捕获的方式来处理(先捕获再冒泡)
            <div class="box2" @click="showMsg('box2')">
                box2 - 点击事件默认是冒泡模式, 点击box2时, 先触发box2的点击事件, 再触发box1的点击事件 
                <br/>
                <button class="btn3" @click="showMsg('btn3')"> btn3 </button>
            </div>
        </div>
        <br/><br/>
        <button v-on:click="ClickInfo"> 点击提示输入1 </button>
        <button @click="showClickInfo($event, 'hello vue')"> 点击提示输入2 </button>
        <br/><br/>
        <div class="demo1" @click.self="showInfo">
            <p>self 只有 event.target 是当前操作的元素时, 才会触发事件 (变相阻止冒泡)</p>
            <button @click="showInfo"> click.self </button>
        </div>
        <br/><br/>
        <p>passive: 事件的默认行为立即执行, 无需等待事件回调执行完毕.</p>
        <p>scroll: 滚动条一滚动就会触发的事件 wheel 鼠标滚轮事件</p>
        <ul class="list" @scroll.passive="demo">
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
            <li>7</li>
            <li>8</li>
            <li>9</li>
            <li>10</li>
        </ul>
        <br/><br/>
    </div>
    <script type="text/javascript">
        Vue.config.productionTip = false;
        const vm= new Vue({
            el:'#root',
            data:{
                name: '江西-南昌'
            },
            methods:{
                showInfo:function (e){
                    // e.preventDefault(); // 阻止默认事件
                    // e.stopPropagation(); // 阻止事件冒泡
                    console.log(e.target);
                }, 
                showMsg(msg) {
                    console.log(msg);
                },
                demo() {
                    console.log('@@@ demo 滚动事件开始');
                    for (let i = 0; i < 1000; i++) {
                        console.log('#');
                    }
                    console.log('demo 滚动事件结束');
                },
                ClickInfo(e) {
                    console.log('点击提示输入1');
                    console.log(e.target);
                    console.log(this === vm ? 'this 指向 vm' : 'this 指向 事件触发的元素');
                },
                showClickInfo(e, msg) {
                    console.log('点击提示输入2');
                    console.log(e.target);
                    console.log('接收的参数是:  ' + msg);
                }
            }
        })
    </script>
</body>
</html>

image-20260121102821210

这段代码是 Vue.js 事件处理机制的集大成者。它不仅涵盖了上一轮讨论的键盘事件,更深入展示了 Vue 强大的事件修饰符系统。

通过这段代码,我们可以看到 Vue 如何通过简单的指令修饰,解决了原生 DOM 事件处理中繁琐的兼容性判断和逻辑控制问题。结合你的代码,我将从修饰符的深度解析事件流的控制以及性能优化三个维度来为你解读。

🛠️ 核心机制:事件修饰符 (Event Modifiers)

Vue 的修饰符是 Vue.js 事件系统中最优雅的设计,它们通过链式调用的方式,让模板代码直接表达意图,而不需要在方法逻辑中混杂 DOM 操作。

1. 阻断与限制类

修饰符 代码示例 核心作用 实际应用场景
.prevent @click.prevent 阻止默认行为 表单提交验证、A 标签拦截
.stop @click.stop 阻止事件冒泡 嵌套元素点击、模态框遮罩
.once @click.once 事件只触发一次 防止重复提交、一次性引导

2. 逻辑与性能类

修饰符 代码示例 核心作用 实际应用场景
.self @click.self 仅目标元素触发 点击模态框外部关闭(点击内容不关闭)
.capture @click.capture 捕获模式触发 需要父元素先于子元素响应的交互
.passive @scroll.passive 立即执行默认行为 移动端滚动优化,解决卡顿

🌊 事件流:捕获与冒泡

代码中关于 .box1.box2.btn3 的嵌套结构,完美演示了 DOM 事件流的两种模式:

1. 默认的冒泡模式 (Bubbling)

.box2.btn3 的代码中:

  • 逻辑:当你点击按钮(.btn3)时,事件会先触发按钮的点击,再传递给父级(.box2),最后传递给祖父级(.box1)。
  • 代码@click(默认写法)。

2. 捕获模式 (Capturing)

.box1 的代码中:

  • 逻辑:加上 .capture 修饰符后,事件流的方向反转。Vue 会先检查父级(.box1)是否有捕获监听,如果有,先执行父级,然后再向下传递给子级(.box2 -> .btn3)。
  • 代码@click.capture
  • 输出顺序:点击按钮时,控制台会先输出 box1,再输出 box2,最后输出 btn3

🚀 性能优化:.passive 的妙用

代码中的 <ul @scroll.passive="demo"> 是一个非常高级的用法,它解决了 Web 开发中一个经典的性能痛点。

  • 问题背景:在移动端,浏览器在触发 scroll 事件前,会等待事件回调执行完,以判断开发者是否会调用 event.preventDefault() 来阻止滚动。如果回调函数里有大量计算(如代码中的 for 循环),页面滚动就会变得极其卡顿。
  • Vue 的解法.passive 修饰符告诉浏览器:“我不会阻止滚动,请你直接滚动,不要等我”。
  • 效果:加上 .passive 后,滚动条会立即响应,无需等待 demo 函数里的循环执行完毕,极大地提升了用户体验。

⚠️ 关于 this 的再次强调

ClickInfo 方法中,你再次验证了 Vue 的 this 绑定机制:

  • 原生 JSthis 指向触发事件的 DOM 元素(e.target)。
  • Vue Methods:Vue 自动将 this 绑定为 Vue 实例(vm)。这意味着你可以在 ClickInfo 中直接通过 this.name 访问数据,或者调用 this 上的其他方法,而不需要手动绑定 this

📌 总结与建议

这段代码是 Vue 事件处理的最佳实践指南。在实际开发中,建议你:

  1. 善用连写:如 @click.prevent.self,既阻止默认行为又防止冒泡,代码极其精简。
  2. **移动端必加 .passive**:在处理 touchstarttouchmovescroll 等高频事件时,如果不需要阻止默认行为,请务必加上 .passive,这是提升流畅度的关键。
  3. 理解事件流.capture.self 虽然不常用,但在处理复杂的嵌套组件交互(如日历组件、拖拽库)时是解决问题的利器。

计算属性

计算属性:

​ 1.定义:要用的属性不存在,要通过已有属性计算得来。

​ 2.原理:底层借助了Objcet.defineproperty方法提供的getter和setter。

​ 3.get函数什么时候执行?

​ (1).初次读取时会执行一次。

​ (2).当依赖的数据发生改变时会被再次调用。

​ 4.优势:与methods实现相比,内部有缓存机制(复用),效率更高,调试方便。

​ 5.备注:

​ 1.计算属性最终会出现在vm上,直接读取使用即可。

​ 2.如果计算属性要被修改,那必须写set函数去响应修改,且set中要引起计算时依赖的数据发生改变。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>计算属性</title>
    <script src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <!--v-model双向绑定-->
        姓:<input type="text" v-model="firstName"/>
        <br/><br/>
        名:<input type="text" v-model="lastName"/>
        <br/><br/>
        全名-computed实现: <span>{{ fullName }}</span>
        <br/><br/>
        全名: <input type="text" v-model="test" placeholder="请输入全名"/>
        <button @click="changeFullName">修改</button>
        <br/><br/>
        <!-- 全名-menthods实现: <span>{{ fullName() }}</span> -->
    </div>
    <script type="text/javascript">
        const vm = new Vue({
           el:'#root',
           data:{
               firstName:'张',
               lastName: '三',
               test: '测--试'
           }, 
           computed: {
               fullName: {
                  get() {
                    console.log('fullNameByComputed get()被调用了');
                    return this.firstName + '--' + this.lastName;
                  },
                  set(val) {
                    console.log('fullNameByComputed set()被调用了');
                    const [firstName, lastName] = val.split('--');
                    this.firstName = firstName;
                    this.lastName = lastName;
                    console.log('fullNameByComputed set()被调用了');
                  }
               }
           },
            //简写形式
            //前提:计算属性只考虑读取不考虑修改 set丢了
            //简写计算书写为一个函数(这个函数当成getter使用), 注意不要写箭头函数
            //执行完getter之后,vm直接保存返回的数据为fullName属性的属性值,此时vm.fullName不是函数而是一个确切的计算值
        //    computed: {
        //        fullName: function() {
        //            console.log('fullName()被调用了');
        //            return this.firstName + '--' + this.lastName;
        //        }
        //    },
           methods: {
                changeFullName() {
                    this.fullName = this.test;
                },
                // fullName() {
                //     console.log('fullName()被调用了');
                //     return `${this.firstName.slice(0, 4)} -- ${this.lastName}`;
                // }
           }
        });
    </script>
</body>
</html>

这段代码非常清晰地展示了 Vue 中计算属性的核心概念。通过“姓”和“名”两个输入框动态拼接“全名”,完美体现了 computed 相比于普通方法的优势。

结合你的代码,我来为你深入拆解 computed 的工作原理和它为什么是 Vue 的“性能利器”:

🧮 什么是计算属性?

计算属性本质上是一个“会自动更新的值”
它看起来像一个属性(在模板中使用时不需要加括号 {{ fullName }}),但背后其实是一个函数,用来根据其他数据计算出新数据。

⚙️ 工作原理:依赖追踪与缓存

这是 computedmethods 最大的区别。

  • 你的代码逻辑
    • fullName 依赖于 this.firstNamethis.lastName
    • 当页面初次渲染时,Vue 会执行 fullName 函数,得到结果“张–三”。
    • Vue 会悄悄记住:“fullName 是由 firstNamelastName 算出来的”。
  • 缓存机制(关键点)
    • 假设你在页面上多次使用 {{ fullName }}(比如用了 3 次),Vue 只会执行 1 次 fullName 函数,然后把结果复用 3 次。
    • 只有当 firstNamelastName 的值发生变化时,Vue 才会“作废”旧缓存,并在下一次读取 fullName 时重新执行函数计算新值。
  • **对比 methods**:
    • 如果你用 methods 实现,每次调用 {{ fullName() }},函数都会无条件执行,不管数据变没变。如果计算逻辑很复杂,这会浪费大量性能。

📝 简写形式

你的代码使用了 computed简写形式

  • 适用场景:只需要读取计算属性的值,不需要修改它。
  • 写法:直接把 get 函数写成 fullName: function() {}
  • 注意:简写形式的函数体里不能有 this.xxx = yyy 这种赋值操作(除非是修改其他无关数据),因为简写默认只有 get,没有 set

🎬 代码运行流程图解

  1. 初始化:Vue 实例创建,firstName 为“张”,lastName 为“三”。
  2. 渲染:模板遇到 {{ fullName }}
  3. 求值:Vue 发现 fullName 是计算属性,于是执行函数,控制台打印 'fullName()被调用了',返回“张–三”。
  4. 缓存:Vue 把“张–三”存起来。
  5. 后续渲染:如果页面其他地方还有 {{ fullName }},Vue 直接拿缓存的值,不再打印 'fullName()被调用了'
  6. 更新:当你在输入框修改“姓”时,firstName 变了。Vue 检测到依赖变化,标记 fullName 为“脏数据”。
  7. 重新求值:当下次渲染需要 fullName 时,Vue 再次执行函数,打印日志,得到新值。

💡 总结

computed 是 Vue 的“智能缓存器”。它利用 Getter 来派生数据,并利用 依赖追踪 来保证只有在必要时才重新计算。这让你的代码既声明式(像属性一样用),又高性能(自动缓存)。


监视属性

监视属性watch:

​ 1.当被监视的属性变化时, 回调函数自动调用, 进行相关操作

​ 2.监视的属性必须存在,才能进行监视!!

​ 3.监视的两种写法:

​ (1).new Vue时传入watch配置

​ (2).通过vm.$watch监视

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>天气案例</title>
    <script src="../js/vue.js"></script>
</head>
<body>
<div id="root">
    <h1>今天天气很 {{ info }}</h1>
    <button @click="changeWeather">
        切换天气
    </button>
    <h2>numbers.a: {{ numbers.a }}</h2>
    <button @click="numbers.a++">
        a++
    </button>
    <h2>numbers.b: {{ numbers.b }}</h2>
    <button @click="numbers.b++">
        b++
    </button>
    <p> 测试vue自身监测数据属性 </p>
    <h3>numbers.c.d: {{ numbers.c.d }}</h3>
    <button @click="numbers.c.d++">
        d++
    </button>
    <h3>numbers.c.e: {{ numbers.c.e }}</h3>
    <button @click="numbers.c.e++">
        e++
    </button>
</div>
<script type="text/javascript">
    Vue.config.productionTip = false;
    const vm = new Vue({
        el:'#root',
        data:{
            isHot: true,
            numbers: {
                a: 100,
                b: 200,
                c: {
                    d: 300,
                    e: 400
                }
            }
        },
        //计算属性
        computed: {
            info(){
                return this.isHot ? '炎热' : '凉爽';
            }
        },
        //改变模版数据的方法
        methods:{
            changeWeather(){
                this.isHot = !this.isHot;
            }
        },
        //监视属性
        // watch: {
        //     // isHot: {
        //     //     // immediate: true, // 初始化时立即调用handler
        //     //     // handler 接收两个参数: 一个是新值, 一个是旧值
        //     //     // 当 isHot 变化时, 调用 handler
        //     //     handler(newVal, oldVal){
        //     //         console.log('ishot 被修改了');
        //     //         console.log('newVal: ', newVal);
        //     //         if (newVal) {
        //     //             console.log('天气炎热');
        //     //         } else {
        //     //             console.log('天气凉爽');
        //     //         }
        //     //         console.log('oldVal: ', oldVal);
        //     //         if (oldVal) {
        //     //             console.log('之前天气炎热');
        //     //         } else {
        //     //             console.log('之前天气凉爽');
        //     //         }
        //     //     }
        //     // },
        //     isHot(newVal, oldVal) { //简写的前提是watch属性不需要immediate和deep属性的时候
        //         console.log('ishot 被修改了');
        //         console.log('newVal: ', newVal);
        //         if (newVal) {
        //             console.log('天气炎热');
        //         } else {
        //             console.log('天气凉爽');
        //         }
        //         console.log('oldVal: ', oldVal);
        //         if (oldVal) {
        //             console.log('之前天气炎热');
        //         } else {
        //             console.log('之前天气凉爽');
        //         }
        //     },
        //     numbers: {
        //         deep: true,
        //         handler() {
        //             console.log('numbers 发生改变');
        //         }
        //     }
        // }
    });
    // 完整写法
    vm.$watch('isHot', {
        deep: true,
        immediate: true,
        handler(newVal, oldVal) {
            console.log('ishot 被修改了');
            console.log('newVal: ', newVal);
            if (newVal) {
                console.log('天气炎热');
            } else {
                console.log('天气凉爽');
            }
            console.log('oldVal: ', oldVal);
            if (oldVal) {
                console.log('之前天气炎热');
            } else {
                console.log('之前天气凉爽');
            }
        }
    })
</script>
</body>
</html>

这段代码是一个非常经典的 Vue.js 侦听属性(Watch) 教学案例。它清晰地展示了 Vue 如何自动追踪数据变化,并演示了浅层侦听深层侦听的区别。

结合你的代码(目前 watch 选项被注释掉了,但逻辑在 $watch 中),我来为你深度解析 Vue 的“监测机制”:

1. 核心机制:vm.$watch

虽然你在 new Vue 里注释掉了 watch,但你在实例创建后使用了 vm.$watch。这两者的效果是完全一样的。

  • 侦听目标'isHot'
  • 配置项
    • immediate: true立即执行。通常 Watcher 是在数据变化后才执行,加上这个参数,页面加载完毕初始化时就会立刻执行一次 handler
    • deep: true深度侦听。对于对象或数组,如果内部属性变化,也能监测到(后面会重点讲这个)。
  • **回调函数 (handler)**:当被侦听的数据变化时,执行这里的逻辑。

2. Vue 的监测奥秘:numbers 对象

页面上有一堆按钮在修改 numbers 对象里的值(abc.dc.e)。这里隐藏了一个 Vue 2 的重要特性:

❓ 问题:为什么修改 numbers.c.d 能被监测到?

如果你在 watch 里写 numbers: { handler() {} },你会发现:

  • 修改 numbers.a能触发侦听。
  • 修改 numbers.c.d能触发侦听。

原因:Vue 2 的响应式系统是基于 Object.defineProperty递归遍历。当你把 numbers 对象交给 Vue 管理时,Vue 会立刻遍历它所有的嵌套属性(a, b, c, d, e),把它们都转换成 getter/setter。所以,无论你改到哪一层,Vue 都知道“数据变了”。

⚠️ 注意:deep: true 在这里是“多余”的?

在你的 $watch 代码中,你给 isHot 用了 deep: true,这没问题,因为 isHot 是布尔值,不需要深度遍历。
但是,如果你去侦听 numbers 对象:

  • **如果不加 deep**:修改 numbers.a 会触发,修改 numbers.c.d 也会触发(因为 Vue 默认就追踪到了)。
  • **加了 deep**:Vue 会强制遍历对象的每一层。对于复杂对象,这会消耗性能。

结论:对于 Vue 实例中 data 定义的深层对象,通常不需要手动加 deep: true,Vue 已经帮你做好响应式了。

3. 计算属性 vs. 侦听属性

你的代码里同时有 computedwatch,正好做个对比:

  • **computed (计算属性)**:
    • 用途派生数据。比如 fullName 是由 firstNamelastName 算出来的。
    • 特点:有缓存,只有依赖的数据变了才会重新计算。
  • **watch (侦听属性)**:
    • 用途异步操作复杂逻辑。比如监听搜索关键词变化,然后去发 Ajax 请求;或者监听数据变化后做一个复杂的动画。
    • 特点:没有缓存,只要数据变(或初始化),就会执行回调。

4. 代码运行预测

当你运行这段代码时,控制台会发生以下事情:

  1. 页面加载:因为 immediate: truehandler 会立刻执行一次。
    • newValtrue(天气炎热)。
    • oldValundefined(因为是初始化,没有旧值)。
  2. 点击“切换天气”isHot 变为 false
    • handler 再次执行。
    • newValfalse(天气凉爽)。
    • oldValtrue(之前天气炎热)。
  3. **点击 a++ / d++**:控制台会打印 numbers 发生改变(如果你取消注释 watch 里的 numbers 配置)。

📌 总结

这段代码展示了 Vue 的响应式核心:

  • 自动追踪:Vue 在初始化时就把数据劫持了,不管你怎么改 numbers 的深层属性,它都能感知。
  • 侦听器:适合做“数据变化后的副作用处理”(比如发请求、改 DOM、打印日志)。
  • 深度侦听:虽然好用,但有性能开销,Vue 默认的对象监测已经很强大了。

计算属性 && 监视属性

computed和watch之间的区别:

​ 1.computed能完成的功能,watch都可以完成。

​ 2.watch能完成的功能,computed不一定能完成,例如:watch可以进行异步操作。

​ 两个重要的小原则:

​ 1.所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm 或 组件实例对象。

​ 2.所有不被Vue所管理的函数(定时器的回调函数、ajax的回调函数等、Promise的回调函数),最好写成箭头函数,

​ 这样this的指向才是vm 或 组件实例对象。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>计算属性</title>
    <script src="../js/vue.js"></script>
</head>
<body>
    <div id="root">
        <!--v-model双向绑定-->
        姓:<input type="text" v-model="firstName"/>
        <br/><br/>
        名:<input type="text" v-model="lastName"/>
        <br/><br/>
        全名: <span>{{ fullName }}</span>
        <br/><br/>
        test: <input type="text" v-model="test" placeholder="请输入全名"/>
        <button @click="changeFullName">修改</button>
        <br/><br/>
        <!-- 全名-menthods实现: <span>{{ fullName() }}</span> -->
    </div>
    <script type="text/javascript">
        const vm = new Vue({
           el:'#root',
           data:{
               firstName:'张',
               lastName: '三',
               fullName: 'zhang--san',
               test: '测--试'
           }, 
           watch: {
                firstName(newF) {
                    setTimeout(() => {
                        this.fullName = newF + '--' + this.lastName;
                    }, 1000);
                },            
                lastName(newL) {
                    setTimeout(() => {
                        this.fullName = this.firstName + '--' + newL;
                    }, 1000);
                }
           },
        //    computed: {
        //        fullName: {
        //           get() {
        //             console.log('fullNameByComputed get()被调用了');
        //             return this.firstName + '--' + this.lastName;
        //           },
        //           set(val) {
        //             console.log('fullNameByComputed set()被调用了');
        //             const [firstName, lastName] = val.split('--');
        //             this.firstName = firstName;
        //             this.lastName = lastName;
        //             console.log('fullNameByComputed set()被调用了');
        //           }
        //        }
        //    },
            //简写形式
            //前提:计算属性只考虑读取不考虑修改 set丢了
            //简写计算书写为一个函数(这个函数当成getter使用), 注意不要写箭头函数
            //执行完getter之后,vm直接保存返回的数据为fullName属性的属性值,此时vm.fullName不是函数而是一个确切的计算值
        //    computed: {
        //        fullName: function() {
        //            console.log('fullName()被调用了');
        //            return this.firstName + '--' + this.lastName;
        //        }
        //    },
           methods: {
                changeFullName() {
                    this.firstName = this.test.split('--')[0];
                    this.lastName = this.test.split('--')[1];
                },
                // fullName() {
                //     console.log('fullName()被调用了');
                //     return `${this.firstName.slice(0, 4)} -- ${this.lastName}`;
                // }
           }
        });
    </script>
</body>
</html>

computedwatch 的本质区别和使用场景。

深入剖析一下为什么说 watch 能做异步,computed 不能”,以及 “箭头函数的 this 指向” 这两个原则。

1. computed vs watch:同步与异步的鸿沟

你的代码完美地证明了这一点。

computed 的局限性(纯同步)

  • 机制computed 是一个纯函数。给定相同的输入(firstNamelastName),它必须立刻返回相同的输出。
  • 限制:它不能包含“等待”逻辑。你不能在 computed 里写 setTimeout,因为计算属性必须立即返回一个值,否则页面渲染就会卡住。
  • 适用:简单的拼接、数学运算、过滤数组等。

watch 的灵活性(可异步)

  • 机制watch 是一个监听器。它不负责返回值,它负责“执行任务”。
  • 优势:在你的代码中,watch 里用了 setTimeout。这意味着:
    • Vue 不需要立刻得到结果。
    • Vue 只是“看着”数据变化,然后在后台开启一个定时器。
    • 1秒后,定时器回调函数执行,再去修改数据。
  • 适用:搜索建议(输入后等用户停顿再发请求)、表单验证、路由监听等。

2. 关于 this 指向的两个原则

你的代码也是这两个原则的最佳实践范例

原则一:Vue 管理的函数,用普通函数

  • 例子watch 里的 firstName(newF) { ... }
  • 分析firstName 是 Vue 帮你调用的。Vue 内部会把 this 绑定为 Vue 实例(vm)。如果你写成箭头函数 firstName: (newF) => { ... },虽然 this 也能指向 vm(因为外层是全局作用域),但在严格模式下可能会出问题,且不符合 Vue 的设计规范。使用普通函数是最稳妥、最符合直觉的写法。

原则二:非 Vue 管理的函数,用箭头函数

  • 例子setTimeout(() => { ... }, 1000)
  • 分析
    • setTimeout 是浏览器的 API,不是 Vue 调用的。
    • 如果你写成普通函数 setTimeout(function() { this.fullName = ... }, 1000),这里的 this 会指向 window(非严格模式)或 undefined(严格模式),导致报错。
    • **箭头函数没有自己的 this**,它会向外层作用域(Vue 的 watch 函数)查找。在 watch 函数里,this 指向 vm,所以箭头函数里的 this 也指向 vm

3. 代码运行流程图解

  1. 初始化firstName 是“张”,lastName 是“三”,fullName 是“zhang–san”。
  2. 输入:你在“姓”的输入框输入“李”。
  3. 触发data.firstName 变为“李”,触发 watch.firstName 回调。
  4. 异步watch 启动一个 1 秒的倒计时。
  5. 更新:1 秒后,倒计时结束,执行回调,将 fullName 修改为“李–三”。
  6. 渲染:页面上的“全名”从旧值变为“李–三”。

总结

你的理解完全正确:

  • computed:当你需要一个实时的、基于数据计算出来的值,且这个计算是同步的。
  • watch:当你需要在数据变化时执行一段代码,且这段代码可能包含异步操作(如 setTimeoutaxios 请求)或复杂的业务逻辑

这段代码就是 watch 处理异步场景(模拟网络请求延迟)的经典例子。


绑定样式

:class=”XXX”

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue绑定样式</title>
    <style>
        .basic{
            width: 400px;
            height: 100px;
            border: 1px solid black;
        }
        .happy{
            border: 4px solid red;;
            background-color: rgba(255, 255, 0, 0.644);
            background: linear-gradient(30deg,yellow,pink,orange,yellow);
        }
        .sad{
            border: 4px dashed rgb(2, 197, 2);
            background-color: gray;
        }
        .normal{
            background-color: skyblue;
        }
        .atguigu1{
            background-color: yellowgreen;
        }
        .atguigu2{
            font-size: 30px;
            text-shadow:2px 2px 10px red;
        }
        .atguigu3{
            border-radius: 20px;
        }
    </style>
    <script src="../js/vue.js"></script>
</head>
<body>
<div id="root">
    <hr/><!--:class 绑定class样式字符串写法 适用于样式的类名不确定需要动态琢磨的状况-->
    <div class="basic" :class="mood" @click="changeMood">{{ name }}</div>
    <hr/><!--:class 绑定class样式数组写法 适用于要绑定的样式个数不确定,名字也不确定的状况-->
    <div class="basic" :class="classArr">{{ name }}</div>
    <hr/><!--:class 绑定class样式对象写法 适用于要绑定的样式个数确定,名字确定,但动态决定要不要用的状况-->
    <div class="basic" :class="classObj">{{ name }}</div>
    <hr/>
</div>
<script type="text/javascript">
    const vm = new Vue({
       el:'#root',
       data: {
           name:'test',
           mood: 'normal',
           classArr: ['atguigu1','atguigu2','atguigu3'],
           classObj: {
               atguigu1: true,
               atguigu2: true,
               atguigu3: true
           }
       },
       methods:{
         changeMood(){
             //vue绑定事件
             //不要亲手操作dom
             //随机切换心情
             const moods = ['happy','sad','normal'];
             const random = Math.floor(Math.random() * moods.length);
             this.mood = moods[random];
         }
       }
    });
</script>
</body>
</html>

这段代码是 Vue.js 动态样式绑定 的教科书级范例。它清晰地展示了如何通过 :class 指令,用三种不同的数据结构(字符串、数组、对象)来动态控制 DOM 的 CSS 类名。

结合你的注释和代码,我来为你深度拆解这三种写法的适用场景和底层逻辑:

1. 字符串写法 (v-bind:class / :class)

代码:

<div class="basic" :class="mood" @click="changeMood">{{ name }}</div>
data: {
    mood: 'normal'
},
methods: {
    changeMood() {
        const moods = ['happy','sad','normal'];
        const random = Math.floor(Math.random() * moods.length);
        this.mood = moods[random]; // 动态改变字符串值
    }
}
  • 原理:直接将 data 中的一个字符串变量绑定到 class 上。
  • 效果:HTML 渲染为 <div class="basic happy">(假设 mood 为 happy)。
  • 适用场景“状态切换”。比如心情切换、主题切换(白天/黑夜模式)、选项卡高亮。类名是动态的,且互斥。
  • 关键点class 可以和普通的 class 共存(basic 一直存在,mood 动态变化)。

2. 数组写法 (:class)

代码:

<div class="basic" :class="classArr">{{ name }}</div>
data: {
    classArr: ['atguigu1','atguigu2','atguigu3']
}
  • 原理:绑定一个数组,数组的每一项都是一个字符串类名。
  • 效果:HTML 渲染为 <div class="basic atguigu1 atguigu2 atguigu3">
  • 适用场景“组合式样式”。当样式的个数不确定,或者名字需要通过循环/接口动态生成时。
  • 动态性:你可以通过 pushsplice 等数组方法动态修改 classArr,DOM 会自动更新。

3. 对象写法 (:class)

代码:

<div class="basic" :class="classObj">{{ name }}</div>
data: {
    classObj: {
        atguigu1: true,
        atguigu2: true,
        atguigu3: true
    }
}
  • 原理:绑定一个对象。键 (key) 是类名,值 (value) 是布尔值。
  • 效果:只有值为 true 的键,才会被渲染到页面上。
  • 适用场景“开关式样式”。样式的个数和名字是确定的,但需要根据业务逻辑(如是否选中、是否禁用)来决定是否应用。
  • 优势:逻辑非常清晰,一眼就能看出哪个样式开,哪个样式关。

📌 总结与对比

写法 数据类型 核心特点 典型应用场景
字符串 String 值直接对应类名 状态切换(如:active, error, success
数组 Array 多个类名的列表 动态组合(如:从接口获取标签样式列表)
对象 Object 键值对,值为布尔值 条件渲染(如:{ bold: isBold, hidden: isHidden }

💡 补充建议

  1. 三元表达式
    你也可以在数组或对象中使用三元表达式:

    <!-- 如果 isActive 为真,应用 activeClass,否则应用 defaultClass -->
    <div :class="[isActive ? activeClass : defaultClass]"></div>
    
  2. 混合使用
    Vue 允许你同时使用静态 class 和动态 :class,它们会自动合并,不会冲突。

  3. **内联样式绑定 (:style)**:
    虽然这段代码没展示,但 :style 的用法与 :class 类似,通常使用对象写法({ color: activeColor, fontSize: fontSize + 'px' })来动态修改内联样式。

你的代码注释写得非常棒,准确地概括了每种写法的适用场景!


条件渲染

v-if=”XXX” v-else-if=”XXX” v-else=”XXX”

v-show=”XXX”

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>条件渲染</title>
    <script src="../js/vue.js"></script>
</head>
<body>
<!--
                条件渲染:
                            1.v-if
                                        写法:
                                                (1).v-if="表达式"
                                                (2).v-else-if="表达式"
                                                (3).v-else="表达式"
                                        适用于:切换频率较低的场景。
                                        特点:不展示的DOM元素直接被移除。
                                        注意:v-if可以和:v-else-if、v-else一起使用,但要求结构不能被“打断”。

                            2.v-show
                                        写法:v-show="表达式"
                                        适用于:切换频率较高的场景。
                                        特点:不展示的DOM元素未被移除,仅仅是使用样式隐藏掉

                            3.备注:使用v-if的时,元素可能无法获取到,而使用v-show一定可以获取到。
         -->

<div id="root">
    <!--v-show要能转换为一个布尔值 v-show条件渲染-->
<!--    <h2 v-show="a">欢迎来到{{ name }}</h2>-->

<!--    <h2 v-show="1===1">欢迎来到{{ name }}</h2>-->
<!--    <button @click='a = !a'>{{ a ? '隐藏': '显示' }}</button>-->
    <!--使用v-if来进行条件渲染 -->
<!--    <h2 v-if="1">欢迎来到{{ name }}</h2>-->
    <!--v-if v-else-if v-else记住不能打断,要连着写-->
<!--    <h2>当前的n值是{{ n }}</h2>-->
    <button @click="n++">点我n+1</button>
<!--    <div v-if="n === 1">angular</div>-->
<!--    <div v-else-if="n === 2">react</div>-->
<!--&lt;!&ndash;    <div>111</div>&ndash;&gt;-->
<!--    <div v-else-if="n === 3">vue js</div>-->
<!--    <div v-else>not found</div>-->

    <!--v-if与template的配合使用-->
    <template v-if="n === 1">
        <h2>你好</h2>
        <h2>shanghai</h2>
        <h2>shenzhen</h2>
    </template>
</div>
<script type="text/javascript">
    const vm = new Vue({
        el: '#root',
        data:{
            name: 'shanghai',
            n:0
        }
    })
</script>
</body>
</html>

这段代码是 Vue.js 条件渲染 的完整指南。它清晰地展示了 v-ifv-show 这对“双胞胎”的区别,以及 v-if 在复杂逻辑和模板分组中的用法。

结合你的注释和代码,我来为你深度拆解这两个指令的底层逻辑和使用策略:

1. v-if vs v-show:移除 vs 隐藏

这是 Vue 面试和实际开发中最常遇到的选择题。

v-if:真正的“条件渲染”

  • 机制惰性渲染。如果初始条件为假,什么也不做;为真时,才会创建元素并添加到 DOM 中。切换时,会销毁/重建组件。
  • 本质:通过添加/移除 DOM 元素来控制显示。
  • 代码验证:打开浏览器开发者工具,当你使用 v-if 时,条件为假的元素在 HTML 结构中完全消失

v-show:CSS 的“开关”

  • 机制强制渲染。无论条件真假,元素都会被创建并保留在 DOM 中。通过 display: none 样式来控制视觉上的隐藏。
  • 本质:通过CSS 样式控制显示。
  • 代码验证:使用 v-show 时,即使元素看不见了,它依然存在于 HTML 结构中,只是多了一个 style="display: none;"

2. 性能与场景选择

使用 v-if 的场景

  • 切换频率低:因为切换需要销毁和重建 DOM,开销较大。
  • 包含复杂组件:比如模态框、弹窗,内部可能有大量逻辑和子组件,关闭时销毁可以释放内存。
  • 首次渲染:如果初始状态是隐藏,v-if 不会渲染,节省了初始渲染时间。

使用 v-show 的场景

  • 切换频率高:只是切换 CSS,性能极高。
  • 简单元素:不需要频繁销毁重建的开销。

3. v-if 的进阶用法

1. v-else-ifv-else

  • 逻辑:完全等同于 JavaScript 的 if...else if...else
  • 关键规则不能被打断。必须连在一起写。如果你在 v-ifv-else 中间插入了一个普通 HTML 标签,v-else 就会失效,因为它找不到对应的“兄弟” v-if 了。
  • 你的代码测试:你注释掉的那段 n === 1n === 2n === 3 就是典型的多分支判断。

2. <template> 标签

  • 问题:如果你想要同时切换多个元素,是不是要给每个元素都写一遍 v-if
  • 解法:使用 <template>。它是一个不可见的包装器
  • 你的代码分析
    • n === 1 时,三个 <h2> 标签会同时显示。
    • n !== 1 时,这三个标签会被整体移除
    • 注意<template> 只能配合 v-if,不能配合 v-show(因为 v-show 是 CSS 控制,<template> 无法被渲染,也就无法应用 CSS)。

4. 关于“获取元素”的备注

你注释中提到的那句“使用 v-if 时,元素可能无法获取到,而使用 v-show 一定可以获取到”非常关键。

  • **v-show**:元素始终在 DOM 树里。你可以随时通过 document.getElementByIdref 获取到它。
  • **v-if**:元素可能根本不存在。如果你在 v-if="false" 的时候尝试获取该 DOM 元素,会得到 null。这在操作 DOM 或使用第三方库时需要特别注意。

总结

  • v-if“有无”**的问题。适合**懒加载**、节省内存一次性展示**。
  • v-show“显隐”**的问题。适合**频繁切换**、保留状态**。

在你的代码中,<template v-if> 的写法展示了如何优雅地控制一组元素的显示,这是编写复杂模板时非常实用的技巧。


列表渲染

收集表单数据

过滤器

内置指令

自定义指令

生命周期

非单文件组件

单文件组件


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 jungle8884@163.com

×

喜欢就点赞,疼爱就打赏