10、Vue之#app|ref|key实例组件异同|set原理|滚动加载|v-简写、内置组件、组件传参、单向数据流、插槽slot后备、两版组件写法不同、两版源码不同之从数据变化到页面渲染|异步批处理机制|响应式机制|nextTick、两版用法不同之实例化|双向绑定|生命周期、vuex|pinia、vue-router从加载到登录|无后台开发|页面传参、vue-cli、mock(3350行)

一、vue之#app、$refs、key、实例和组件异同、set原理、滚动加载、指令v-简写
 附、响应式与非响应式的区别
  (1)数据更新
    A、响应式:自动触发渲染,如vue.js
    B、非响应式:需手动调用updateView(),如jQuery.js
  (2)多视图同步
    A、响应式:数据一变,所有依赖视图同步更新
    B、非响应式:容易遗漏更新,导致UI不一致 
  (3)复杂状态管理
    A、响应式:内置依赖追踪,无需额外代码
    B、非响应式:需自行实现发布-订阅或事件总线
1、index.html的#app元素与App.vue组件的关系,
  (1)Vue2和Vue3都会保留#app容器元素,
  (2)#app元素将其子节点替换为App.vue的根元素
2、$refs,获取DOM元素或组件的引用
  (1)$refs,加在组件上,用this.$refs.name 获取到的是组件实例,可以使用组件的所有方法
  (2)$refs,加在普通的元素上,用this.$refs.name获取到的是dom元素
3、vue组件的key属性
  (1)key属性的作用,
    A、标识节点,
    B、让Diff算法更高效地识别节点、更新虚拟DOM
  (2)index不能做key,
    A、用index做key时,新增或删除节点的操作,
    B、会使一个节点使用另一节点的index,进而使用它的key,进而使用它的data,进而产生错误
4、实例和组件的异同
  (1)相同点:两者接收相同的配置,例如data、computed、watch、methods以及生命周期钩子等
  (2)不同点:
    A、自定义组件没有el配置项
    B、Vue2.x和Vue3.x版本要求自定义组件的
      a、data,默认值必须是一个工厂函数,用来返回默认对象
      b、props的某项,如果是对象,默认值必须是一个工厂函数,用来返回默认对象
    C、原因是
      a、如果数据是对象,那么组件实例在不同的地方调用,数据指向的是相同的地址,此处数据改变,它处数据也改变
      b、如果数据是函数,那么组件实例在不同的地方调用,数据指向数据此次的执行结果,是不同的地址,此处数据改变,它处数据不改变
5、vue2为什么能通过Vue.set强制触发视图更新
  (1)vue2通过Object.defineProperty,实现对象的响应式,但无法检测对象新增属性或删除属性
    A、Object.defineProperty,被用来劫持(拦截)对象的属性访问和修改,允许在属性的get和set时自动执行额外逻辑
  (2)vue2通过重写数组的7个方法,实现数组的响应式,但无法检测数组通过索引修改元素或修改长度
    A、7个数组方法,push、pop、shift、unshift、splice、sort、reverse
  (3)Vue.set(或this.$set)的作用及工作原理
    A、给响应式对象添加属性
      a、Vue.set会调用defineReactive方法,为新增属性手动添加getter/setter
      b、立即调用dep.notify()通知所有依赖项
    B、给响应式数组修改长度或通过索引修改元素
      a、用arr.splice(2)替代arr.length=2;用Vue.set(arr,2,5)或arr.splice(2,1,5)替代arr[2]=5
      b、调用splice或Vue.set后,Vue内部会通过dep.notify()通知依赖更新
  (4)Vue.delete(或this.$delete)的作用及工作原理
    A、给响应式对象删除属性,相当于delete obj[key]
    B、调用dep.notify(),通知视图同步更新
  (5)Vue3中,这些问题已通过Proxy解决,无需特殊处理
6、滚动加载(vue-infinite-scroll)
  <div v-infinite-scroll="loadMore" //滚动到底部时触发loadMore,发出请求,追加响应数据this.data.push({name:response.data.list}); 
    infinite-scroll-throttle-delay="500" //滚动事件节流延迟(ms)
    infinite-scroll-disabled="isBusy" //禁用状态(为true时停止加载)
    infinite-scroll-distance="10" //触发距离(距底部10px时加载)
  >
    <div v-for="item in data" :key="item.id">{{item.name}}</div>
  </div>
7、指令v-
  (1)v-if的优先级高于v-show
  (2)v-text,标签绑定文字,可以用{{}}代替
  (3)v-model,在input textarea select中使用
    A、v-model.lazy,只有在input输入框发生blur时才触发change事件,如<input v-model.lazy="msg">
    B、v-model.trim,将用户输入的前后的空格去掉,如<input v-model.trim="msg">
    C、v-model.number,将用户输入的字符串转换成number,如<input v-model.number="msg">
  (4)v-on,标签的事件绑定
    A、全写,<div v-on:click="doSomething">...</div>;
    B、简写,<div @click="doSomething">...</div> 
  (5)自定义指令v-title的定义及使用
    A、定义指令(main.js)
      Vue.directive('title', {
        update: function (el) { //执行时机:1、当指令data-title的值变化时;2、指令所在的组件更新时
          document.title = el.getAttribute("data-title")
        }
      })
    B、使用指令(form.vue)
      <template>
        <div v-title :data-title="basicData.gameName"></div>
      </template>
  (6)v-slot用法示例(可演示)
    //调用组件时,如插槽为空,则使用后备内容
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="UTF-8">
      <title></title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.9/vue.js"></script>
    </head>
    <body>
      <div id="app">
        <this-div :list="list">
          <div>默认插槽</div>
          <template v-slot:head="scope"><!-- 具名插槽,v-slot:head="scope"与#head="scope"等效,v-slot:head与#head等效 -->
            <div v-if="scope.innerItem.id==2">{{scope.innerItem.name}}</div>
          </template>
        </this-div>
      </div>
    </body>
    </html>
    <script type="text/javascript">
      //模板字符串加注释:`<div>${/*注释内容*/''}</div>`
      //模板字符串加变量:`字符串${变量}`
      Vue.component('this-div', {
        props: ['list'],
        template: `<div>
                <slot/><!-- 默认插槽 -->
                <div :key='item.id' v-for='item in list'>
                  <slot name='head' :innerItem='item'>后备内容</slot><!-- 具名插槽 -->
                </div>
              </div>`,
      });
      var vm = new Vue({
        el: '#app',
        data: {
          list: [{
            id: 1,
            name: 'apple'
          }, {
            id: 2,
            name: 'banane'
          }, {
            id: 3,
            name: 'orange'
          }]
        }
      });
    </script>    
  (7)v-bind用法示例(可演示,)
    A、v-bind:简写为:,
    B、:是Vue的语法糖,用于绑定动态的值
    C、vue2.x中,v-bind会覆盖静态属性(无论顺序)
      代码,<div id="red" v-bind="{ id: 'blue' }"></div>
      效果,<div id="blue"></div>
    D、vue3.x 中,后面属性覆盖前面属性
      代码1,<div id="red" v-bind="{ id: 'blue' }"></div>
      效果1,id="blue"
      代码2,<div v-bind="{ id: 'blue' }" id="red"></div>
      效果2,id="red"
    E、示例  
      <!DOCTYPE html>
      <html lang="en">
        <head>
          <meta charset="UTF-8">
          <title>Vue 样式绑定示例</title>
          <style>
            .title {
              font-size: 30px;
              font-weight: bolder;
              margin: 20px 0 10px;
            }
            .red { color: red; }
            .green { color: green; }
            .fontSize { font-weight: bolder; }
            .example-section {
              margin-bottom: 30px;
              padding-left: 20px;
            }
            .example-item {
              margin: 10px 0;
              padding: 5px;
              border-left: 3px solid #eee;
            }
            .example-label {
              display: inline-block;
              min-width: 120px;
              font-weight: bold;
            }
          </style>
          <script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
        </head>
        <body>
          <div id="box">
            <!-- 1. class 绑定示例 -->
            <div class="title">1、Class 绑定</div>
            <div class="example-section">
              <div class="example-item">
                <span class="example-label">(1)三元表达式:</span>
                <span :class="isTrue ? 'red' : 'green'">根据条件显示红色或绿色</span>
              </div>
              <div class="example-item">
                <span class="example-label">(2)数组选择:</span>
                <span :class="['red', 'green'][num1]">A、从1组样式中选择1个;</span>
                <span :class="['one'=='one'?'green':null, 'two'=='two'?'fontSize':null]">B、生成1组样式</span>
              </div>
              <div class="example-item">
                <span class="example-label">(3)对象语法:</span>
                <span :class="{'red': isTrue, 'green': !isTrue}">根据对象属性应用样式</span>
              </div>
            </div>
            <!-- 2. style 绑定示例 -->
            <div class="title">2、Style 绑定</div>
            <div class="example-section">
              <div class="example-item">
                <span class="example-label">(1)三元表达式:</span>
                <span :style="isTrue ? {'color':'green', 'width':'200px'} : {'color':'red', 'width':'300px'}">A、整体三元;</span>
                <span :style="{'color': isTrue? 'green': 'red', 'width': isTrue? '200px': '300px'}">B、分别三元</span>
              </div>
              <div class="example-item">
                <span class="example-label">(2)数组语法:</span>
                <span :style="{'color': ['red','green'][num1]}">从数组中选择颜色</span>
              </div>
              <div class="example-item">
                <span class="example-label">(3)对象语法:</span>
                <span :style="{'color': isTrue ? 'red':'green', 'width': isTrue ? '200px':'300px'}">多属性条件样式</span>
              </div>
            </div>
          </div>
        </body>
      </html>
      <script>
        var vm = new Vue({
          el: '#box',
          data: {
            isTrue: true,
            num1: 1,
            width: '200px',
            height: '20px'
          }
        });
      </script> 

二、内置组件
1、5个内置组件
  (1)component
    渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。
  (2)transition
    A、作为单个元素/组件的过渡效果
    B、只会把过渡效果应用到其包裹的内容上,而不会额外渲染DOM元素,也不会出现在可被检查的组件层级中
    C、name属性:用于自动生成CSS动画类名
      如果transition标签元素没有设置name属性,则对应的动画类名为v-XXX;如果设置了name属性,则对应的动画类名为属性值-XXX
    D、appear属性:一开始就生效显示动画
    E、示例,来源,https://blog.csdn.net/Superman_H/article/details/122851610
      <template>
        <div>
          <button @click="bol = !bol">隐藏/显示</button>
          <!-- transition 标签元素设置了 name、appear 属性 -->
          <transition name="moveCartoon" appear>
            <!-- 动画会在一开始便生效 -->
            <h1 v-show="bol">组件动画效果</h1>
          </transition>
        </div>
      </template>
      <script>
        export default {
          name: 'App',
          data() {
            return { bol: true };
          },
        };
      </script>
      <style>
        /* 类名要对应回 name 的属性值 */
        .moveCartoon-enter-active {
          animation: move 1s;
        }
        .moveCartoon-leave-active {
          animation: move 1s reverse;
        }
        @keyframes move {
          from {
            transform: translateX(-100%);
          }
          to {
            transform: translate(0);
          }
        }
      </style>
  (3)transition-group
    A、作为多个元素/组件的过渡效果。
    B、可以使被包含的组件保留状态,或避免重新渲染。
    C、它渲染一个真实的DOM元素。默认渲染<span>,可以通过tag、attribute配置哪个元素应该被渲染。它每个子节点必须有独立的key,动画才能正常工作。
    D、标签里面的元素需要设置 key 属性,作为当前元素的唯一标识,
    E、其他用法都和 transition 标签一样
  (4)keep-alive
    A、概念,包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
    B、它是一个抽象组件:它自身不会渲染一个DOM元素,也不会出现在组件的父组件链中。
    C、原理,在created函数调用时将需要缓存的VNode节点保存在this.cache中,
  (5)slot
    作为组件模板之中的内容分发插槽。它自身将被替换。
2、动态组件效果的实现方案
  (1)在内置组件<component>里面使用 v-bind: is。没有keep-alive的配合,只能实现切换,不能实现缓存
    <div id="app">
      <component v-bind:is="whichcomp"></component>
      <button v-on:click="choosencomp('a')">a</button>
      <button v-on:click="choosencomp('b')">b</button>
      <button v-on:click="choosencomp('c')">c</button>
    </div>
    var app=new Vue({
      el: '#app',
      components:{
        acomp:{
          template:`<p>这里是组件A</p>`
        },
        bcomp:{
          template:`<p>这里是组件B</p>`
        },
        ccomp:{
          template:`<p>这里是组件C</p>`
        }
      },
      data:{whichcomp:""},
      methods:{
        choosencomp:function(x){
        this.whichcomp=x+"comp"}
      }
    })
  (2)把组件作为子组件放在内置组件<keep-alive>里,后者包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
    A、include:字符串或正则表达式。只有名称匹配的组件会被缓存。
    B、在 2.2.0 及其更高版本中,activated 和 deactivated 将会在 <keep-alive> 树内的所有嵌套组件中触发。
      <keep-alive include="a,b" include='a' :include="/a|b/" :include="['a', 'b']">
      /* 字符串、正则、数组,也可以没有这些 */
        <component :is="view"></component>
      </keep-alive>
      <keep-alive>
        <comp-a v-if="a > 1"></comp-a>
        <comp-b v-else></comp-b>
      </keep-alive>
      <keep-alive>
        <router-view v-if="$route.meta.keepAlive"></router-view>
      </keep-alive>
      <transition>
        <keep-alive>
          <component :is="view"></component>
        </keep-alive>
      </transition>

三、组件通信 //“通信传值传参传数据”
  附、单向数据流,
    A、父组件可以直接向子组件传值,子组件只能通过回调函数或事件机制向父组件传值,
    B、错误用法,逆向数据流,子组件直接修改父组件的props
  附、Vuex,在不同组件之间实现数据共享  
1、在Vue2中,有以下几种组件通信方式
  (1)Props/$emit,父子传参
    A、父组件通过props向子组件传递数据
    B、子组件通过$emit触发自定义事件并向父组件传递数据
  (2)Vuex,在不同组件之间实现数据共享
    A、Vuex是一个专为Vue.js应用程序开发的状态管理模式
    B、通过mutations、actions和getters来修改和获取状态
  (3)Provide/Inject,祖先和后代组件通信
    A、在祖先组件中使用provide提供数据或方法
    B、在后代组件中使用inject来注入这些数据或方法。这种方式在Vue2中需要借助插件来实现响应式
    C、示例
      a、祖先组件(Provider 组件)
        <template>
          <div>
            <child-component></child-component>
          </div>
        </template>
        <script>
          import ChildComponent from './ChildComponent.vue';
          export default {
            components: {
              ChildComponent,
            },
            provide() {
              return {
                message: 'Hello from parent',
              };
            },
          };
        </script>
      b、后代组件(Consumer 组件)
        <template>
          <div>{{ injectedMessage }}</div>
        </template>
        <script>
          export default {
            inject: ['message'],
            data() {
              return {
                injectedMessage: '',
              };
            },
            created() {
              this.injectedMessage = this.message;
            },
          };
        </script>
  (4)Event Bus(任意通信,自定义事件总线)
    A、创建一个新的Vue实例作为事件总线
    B、在组件中通过$emit触发事件,在其他组件中通过$on监听事件来实现通信
    C、示例
      a、创建事件总线
        //eventBus.js
        import Vue from 'vue';
        export const eventBus = new Vue();
      b、发送事件
        <template>
          <div>
            <button @click="sendEvent">Send Event</button>
          </div>
        </template>
        <script>
          import { eventBus } from './eventBus';
          export default {
            methods: {
              sendEvent() {
                eventBus.$emit('customEvent', { message: 'Hello from component A' });
              },
            },
          };
        </script>
      c、接收事件
        <template>
          <div>{{ receivedMessage }}</div>
        </template>
        <script>
          import { eventBus } from './eventBus';
          export default {
            data() {
              return {
                receivedMessage: '',
              };
            },
            created() {
              eventBus.$on('customEvent', (data) => {
                this.receivedMessage = data.message;
              });
            },
          };
        </script>
  (5)$parent/$children(父子组件直接访问)
    A、在子组件中可以通过$parent访问父组件实例
    B、在父组件中可以通过$children访问子组件实例,但这种方式不推荐,因为它使组件之间的关系变得不清晰且难以维护
    C、示例
      a、父组件
        <template>
          <div>
            <h1>Parent Component</h1>
            <child-one></child-one>
            <child-two></child-two>
            <button @click="parentMethod">Parent Method</button>
            <button @click="callChildrenMethods">Call Children Methods</button>
          </div>
        </template>
        <script>
          import ChildOne from './ChildOne.vue';
          import ChildTwo from './ChildTwo.vue';
          export default {
            components: {
              ChildOne,
              ChildTwo
            },
            methods: {
              parentMethod() {
                console.log('Parent method called.');
              },
              callChildrenMethods() {
                //使用 $children 访问子组件实例并调用子组件方法
                this.$children.forEach(child => {
                  if (child.name === 'ChildOne') {
                    child.childOneMethod();
                  } else if (child.name === 'ChildTwo') {
                    child.childTwoMethod();
                  }
                });
              }
            }
          };
        </script>
      b、子组件1
        <template>
          <div>
            <h2>Child One</h2>
            <button @click="childOneMethod">Son Method</button>
          </div>
        </template>
        <script>
          export default {
            name: 'ChildOne',
            methods: {
              childOneMethod() {
                this.$parent.parentMethod();
                console.log('Child One method called.');
              }
            }
          };
        </script>
      c、子组件2
        <template>
          <div>
            <h2>Child Two</h2>
            <button @click="childTwoMethod">Son Method</button>
          </div>
        </template>
        <script>
          export default {
            name: 'ChildTwo',
            methods: {
              childTwoMethod() {
                this.$parent.parentMethod();
                console.log('Child Two method called.');
              }
            }
          };
        </script>
2、在Vue3中,有以下几种组件通信方式
  (1)Vuex(全局状态管理)
    A、Vuex仍然是一种用于集中管理应用状态的方式
    B、通过mutations、actions和getters来修改和获取状态,实现不同组件之间共享数据
  (2)Props/emits(父子组件通信)
    A、父组件通过props向子组件传递数据
    B、子组件通过defineEmits定义并使用emit触发自定义事件向父组件传递数据
  (3)Provide/Inject(祖先和后代组件通信)
    A、在祖先组件中使用provide提供数据或方法
    B、在后代组件中使用inject注入这些数据或方法。在Vue3中可以直接提供响应式数据
    C、示例一
      a、祖先组件(Provider 组件)
        <script setup>
          import { provide } from 'vue';
          const message = 'Hello from provider!';
          provide('sharedMessage', message);
        </script>
      b、后代组件(Consumer 组件)
        <script setup>
          import { inject } from 'vue';
          const sharedMessage = inject('sharedMessage');
          console.log(sharedMessage);
        </script>
    D、示例二,提供的数据是一个响应式对象
      a、祖先组件(Provider 组件)
        <script setup>
          import { reactive, provide } from 'vue';
          const sharedData = reactive({
            count: 0
          });
          provide('sharedData', sharedData);
        </script>
      b、后代组件(Consumer 组件)
        <script setup>
          import { inject } from 'vue';
          const sharedData = inject('sharedData');
          function incrementCount() {
            sharedData.count++;
          }
        </script>
  (4)Event Bus(任意通信,自定义事件总线)
    A、创建一个新的Vue实例作为事件总线
    B、通过$emit和$on进行非父子组件之间的通信
    C、示例
      a、创建事件总线
        //eventBus.js
        import { createApp } from 'vue';
        const eventBus = createApp({});
        export default eventBus;
      b、发送事件
        <script setup>
          import eventBus from './eventBus';
          function sendEvent() {
            eventBus.$emit('customEvent', { message: 'Hello from component A' });
          }
        </script>
      c、接收事件
        <script setup>
          import eventBus from './eventBus';
          eventBus.$on('customEvent', (data) => {
            console.log(data.message);
          });
        </script>
  (5)组合式API(CompositionAPI)中的响应式变量共享
    A、通过创建一个包含响应式变量的模块
    B、在多个组件中引入并使用这些变量来实现通信
    C、示例
      a、创建共享模块(sharedData.js)
        import { reactive } from 'vue';
        const sharedData = reactive({
          count: 0,
          message: 'Initial message',
        });
        const incrementCount = () => {
          sharedData.count++;
        };
        const updateMessage = (newMessage) => {
          sharedData.message = newMessage;
        };
        export { sharedData, incrementCount, updateMessage };
      b、组件A使用共享数据
        <script setup>
          import { sharedData, incrementCount, updateMessage } from './sharedData';
          console.log('Component A - Initial count:', sharedData.count);
          console.log('Component A - Initial message:', sharedData.message);
          incrementCount();
          updateMessage('Updated message from Component A');
        </script>
      c、组件B使用共享数据
        <script setup>
          import { sharedData } from './sharedData';
          console.log('Component B - Updated count:', sharedData.count);
          console.log('Component B - Updated message:', sharedData.message);
        </script>
  (6)Teleport(传送组件内容)
    A、不是严格意义上的组件通信方式
    B、但可以将一个组件的内容传送到指定的DOM节点,在某些场景下可以实现特定的布局和交互效果
2、以下vue2通过属性传参(函数)实现子向父传值(可演示)
  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>Vue子组件给父组件传值</title>
      <script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
    </head>
    <body>
      <div id="el">
        <my-parent></my-parent>
      </div>
      <script>
        Vue.component('my-parent', {
          template: "<div>" + "<div>父组件改变值为:{{parentData}}</div><my-child v-bind:son-method='parentAdd'></my-child></div>",
          data: function() {
            return {
              parentData: 0
            }
          },
          methods: {
            parentAdd: function(key) {
              this.parentData = key;
            }                                
          },
        })
        Vue.component('my-child', {
          template: "<div><button @click='thisMethod(\"222\")'>点击子组件-就向父组件传值</button></div>",
          props: {      
            sonMethod: {
              type: Function
            }
          },
          data: function() {
            return {
              counter: 0,
              sonData: 0
            }
          },
          methods: {
            thisMethod : function (key) {
              this.counter += 1;
              this.sonData = key + this.counter;
              this.sonMethod(this.sonData)
            }                               
          },
        })
        new Vue({
          el: '#el',
          data: {
            total: 0
          }
        })
      </script>
    </body>
  </html>
3、以下vue3通过监听发射(自定义事件)实现子向父传值(可演示,包含element-plus、element-plus-icons-vue)
  <html>
    <head>
      <meta http-equiv="content-type" content="text/html; charset=UTF-8">
      <link href="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.css" rel="stylesheet">
      <script src="https://cdn.jsdelivr.net/npm/vue@3.2.2"></script>
      <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.8.1/index.full.js"></script>
      <script src="https://cdn.bootcdn.net/ajax/libs/element-plus-icons-vue/2.3.1/global.iife.js"></script>
    </head>
    <body>
      <div id="app">
        <div>以下是父组件内容</div>
        <div>案例来源:http://vue3_demo.sweetysoft.com/</div>
        <div>
          <el-input v-model="douBao" :readonly="true" style="width:400px;">
            <template #prefix>
              <el-icon><Calendar /></el-icon>
            </template>
          </el-input>
        </div>
        <el-button @click="changeSon">改变自身,进而改变子组件</el-button>
        <el-divider content-position="left" style="margin: 40px auto;" >分割线</el-divider>
        <div>以下是子组件内容</div>
        <my-component :custom-prop="douBao"  @custom-event="changeSon"></my-component>
      </div>
    </body>
  </html>
  <script>
    //以下是子组件的定义(在.html文件中,只能用vue2语法定义子组件)
    var MyComponent = {
      props: ['customProp'],
      template: 
        `<div>
          <div>{{ message }}</div>
          <div>这是来自父组件的数据:{{ customProp }}(通过属性传参)</div>
          <el-button @click="emitEvent">改变父组件,进而改变自身</el-button>
        </div>`,
      data() {
        return {
          message: '子组件自身数据',
        };
      },
      methods: {
        emitEvent() {
          this.$emit('custom-event');
        }
      }
    };
    //以下是父组件的js 
    var app = Vue.createApp({ //创建一个Vue应用实例(app) 
      setup() {
        var msg1 = '在.html文件中,用vue3.2定义数据并使用';
        var msg2 = '在.html文件中,用vue3.2定义组件的属性、数据、方法';
        var isFlag = true;
        var douBao = Vue.ref(msg1);
        const changeSon = (son) => {
          isFlag = !isFlag;
          douBao.value = isFlag? msg1: msg2;
        };
        return {
          douBao,
          changeSon
        };
      }
    })
    //以下是全局js 
    app.component('my-component', MyComponent); //在这个应用实例上注册全局组件
    app.use(ElementPlus);
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
      app.component(key, component);
    }
    app.mount('#app') //效果同app.mount(document.getElementById('app'))
  </script>

四、两版Vue组件写法不同
1、vue2组件的6种写法//两版Vue组件写法不同-组件定义
  来源:https://www.cnblogs.com/hanguidong/p/9381830.html
  (1)字面量模板
    Vue.component('my-checkbox', {
      template: 
        `<div class="checkbox-wrapper" @click="check()">
          <div :class="{ checkbox: true, checked: checked }"></div>
          <div class="title"></div>
        </div>`,
      data() {
        return { checked: false, title: 'Check me' }
      },
      methods: {
        check() { this.checked = !this.checked; }
      }
    });
  (2)x-template
    <!-- 以下放在父组件的html里 -->
    <script type="text/x-template" id="checkbox-template">
      <div class="checkbox-wrapper" @click="check()">
        <div :class="{ checkbox: true, checked: checked }"></div>
        <div class="title"></div>
      </div>
    </script>
    <!-- 以下放在js文件里 -->
    Vue.component('my-checkbox', {
      template: '#checkbox-template',
      data() {
        return { checked: false, title: 'Check me' }
      },
      methods: {
        check() { this.checked = !this.checked; }
      }
    });
  (3)inline-template
    <!-- 以下放在父组件的html里 -->
    <my-checkbox inline-template>
      <div class="checkbox-wrapper" @click="check()">
        <div :class="{ checkbox: true, checked: checked }"></div>
        <div class="title"></div>
      </div>
    </my-checkbox>
    <!-- 以下放在js文件里 -->
    Vue.component('my-checkbox', {
      data() {
        return { checked: false, title: 'Check me' }
      },
      methods: {
        check() { this.checked = !this.checked; }
      }
    });
  (4)JSX
    Vue.component("my-checkbox", {
      data() {
        return { checked: false, title: "Check me" };
      },
      methods: {
        check() {
          this.checked = !this.checked;
        },
      },
      render() {
        return (
          <div class="checkbox-wrapper" onClick={this.check}>
            <div class={{ checkbox: true, checked: this.checked }}></div>
            <div class="title">{this.title}</div>
          </div>
        );
      },
    });
  (5)render函数
    Vue.component("my-checkbox", {
      data() {
        return { checked: false, title: "Check me" };
      },
      methods: {
        check() {
          this.checked = !this.checked;
        },
      },
      render(createElement) {
        return createElement(
          "div",
          {
            attrs: {
              class: "checkbox-wrapper",
            },
            on: {
              click: this.check,
            },
          },
          [
            createElement("div", {
              class: {
                checkbox: true,
                checked: this.checked,
              },
            }),
            createElement(
              "div",
              {
                attrs: {
                  class: "title",
                },
              },
              [this.title]
            ),
          ]
        );
      },
    });
  (6)单文件
    <template>
      <div class="checkbox-wrapper" @click="check()">
        <div :class="{ checkbox: true, checked: checked }"></div>
        <div class="title">标题</div>
      </div>
    </template>
    <script>
      import comTab from '@/components/ComTab/com-tab'//导入别处组件
      export default {
        name: 'ComTpl',//组件名
        components: {   
          comTab,//此组件依赖的组件
        },
        props: {//用于接收父组件向子组件传递的数据
          tester: {
            type: Object
          }
        },
        data() {//本组件的数据
          return {
            tests: [],
            selectedTest: {}
          };
        },
        methods: {
          handleClick () {
            this.$router.push('/about')
          }
        },
        computed:{//计算属性,所有get,set的this上下文都被绑定到Vue实例
          fullName(){
            return this.firstName+"."+this.lastName;     
          }
        },
        created() {//生命周期之一。在模板渲染成html前调用,即通常初始化某些属性值,然后再渲染成视图
          //ajax请求放在created里,因为此时已经可以访问this和操作DOM了。
          this.classMap = ['a', 'b', 'c', 'd', 'e'];
          //如进行异步数据请求
          this.$http.get('/api/tests').then((response) => {
            response = response.body;
          if (response.errno === 0) {
            this.goods = response.data;
          }
        });
        },
        mounted() { //在模板渲染成html后调用,通常是初始化页面完成后,再对html的dom节点进行一些需要的操作
          this.$nextTick(() => {
            this._initScroll();
            this._initPics();
          });
        },
        filters: {  //过滤器,可用来写,比如格式化日期的代码
          //例
          formatDate(time) {
            let date = new Date(time);
            return formatDate(date, 'yyyy-MM-dd hh:mm');
          }
        },
        watch: {
          //第一种方式:监听整个对象,每个属性值的变化都会执行handler
          //注意:属性值发生变化后,handler执行后获取的 newVal 值和 oldVal 值是一样的
          food: {//每个属性值发生变化就会调用这个函数
            handler(newVal, oldVal) {
                console.log('oldVal:', oldVal)
                console.log('newVal:', newVal)
            },
            immediate: true,//立即处理 进入页面就触发
            deep: true//深度监听 属性的变化
          },
          //第二种方式:监听对象的某个属性,被监听的属性值发生变化就会执行函数    
          'food.name'(newVal, oldVal) {//函数执行后,获取的 newVal 值和 oldVal 值不一样
              console.log('oldVal:', oldVal)   //冰激凌
              console.log('newVal:', newVal)   //棒棒糖
          }
          //其它
          demo(val,oldVal) {
            this.value = this.demo;
          }
        }
      },
    </script>
    <style rel="stylesheet/scss" lang="scss" scoped>
    </style>
2、vue3组件的3种写法//两版Vue组件写法不同-组件定义
  附、响应式API及用法
    A、ref,创建基本类型(或引用类型)响应式数据。访问/修改需通过.value(模板中自动解包)
    B、reactive,创建引用类型(对象/数组)响应式数据。直接修改属性触发更新,不能整体重新赋值
    C、toRef,为响应式对象的单个属性创建引用。保持与原对象的关联,适合提取单个属性
    D、toRefs,为响应式对象的所有属性创建引用。用于解构响应式对象,避免失去响应性
    E、storeToRefs,为Pinia仓库的状态创建引用。保持状态的响应性,适合解构仓库数据
    F、defineComponent,传入的属性可以验证,帮助TypeScript识别组件选项中的类型
      import { defineComponent } from 'vue'
      export default defineComponent({
        props: {
          name: { type: String, required: true },
          age: { type: Number, default: 18 }
        },
        setup(props) {
          console.log(props.name.toUpperCase()) //正确提示字符串方法
          console.log(props.age.toFixed(1)) //正确提示数字方法
        }
      })
  (1)Setup函数
    A、特点
      a、写在export default的setup选项中
      b、接收props和context参数,需显式返回数据供模板使用
      c、可与Vue2选项(如data、methods)共存
    B、适用场景,需要兼容Vue2语法、复杂逻辑拆分
    C、示例
      <template>
        <div class="example">
          <h2>{{ title }}</h2>
          <p>姓名:{{ user.name }}</p>
          <p>全名:{{ fullName }}</p>
          <p>年龄:{{ age }}</p>
          <p>对象属性:{{ obj.name }}</p>
          <button @click="changeName">修改姓名</button>
          <button @click="sendData">发送数据</button>
          <button @click="fn">更新aaa值</button>
          <input @change="handleChange" placeholder="输入内容触发事件" />
          <slot name="footer"></slot>
        </div>
      </template>
      <script>
        import { toRefs, reactive, ref, computed, watch, watchEffect, onMounted } from 'vue'
        export default {
          props: { //声明接收的props
            title: {
              type: String,
              default: '默认标题'
            }
          },
          beforeCreate() { //Vue2生命周期钩子(与setup共存示例)
            console.log('beforeCreate执行了')
          },
          //Setup函数:组合式API入口
          setup(props, context) {
            //1、处理props与上下文
            const { title } = toRefs(props) //将props转为响应式引用
            const titleRef = toRef(props, 'title') //单个props属性的响应式引用
            const { attrs, slots, emit, expose } = context //透传属性、插槽内容、事件发射器、暴露属性/方法给父组件
            //上面props与attrs的区别
            //props:是组件显式声明接收的属性,需要在组件的props选项中定义
            //attrs:包含了所有未在props中声明的属性(除了class和style)
            //2、响应式数据定义
            const user = reactive({ name: '张三' })
            const obj = reactive({ name: 'zs' })
            const age = ref(0)
            const firstName = ref('hello')
            const lastName = ref('world')
            const aaa = ref(1)
            //3、计算属性
            const fullName = computed(() => {
              return firstName.value + '-·-' + lastName.value
            })
            //4、方法定义
            const changeName = () => {
              user.name = '李四'
              emit('name-changed', user.name) //触发姓名变更事件
            }
            const sendData = () => {
              emit('my-event', 1000) //触发自定义事件
            }
            const fn = () => {
              aaa.value = 100 //修改ref值
            }
            const handleChange = (event) => {
              emit("customChange", event.target.value) //触发输入变更事件
            }
            //5、数据监听
            watch(
              age, 
              (newValue, oldValue) => {
                console.log('年龄变化:', newValue, oldValue)
              }
            )
            watchEffect(() => {
              console.log('obj.name变化:', obj.name) //自动追踪obj.name
            })
            //6、生命周期钩子
            onMounted(() => {
              console.log('组件挂载完成(setup内)')
              console.log('DOM元素:', document.querySelector('.example'))
            })
            //7、暴露属性和方法给父组件(通过ref访问)
            expose({
              aaa,
              fn,
              user
            })
            //8、返回需要在模板中使用的数据和方法  
            return {
              title,
              user,
              obj,
              age,
              fullName,
              changeName,
              sendData,
              handleChange,
              fn
            }
          }
        }
      </script>
      <style scoped>
      </style>
  (2)Setup属性
    A、特点
      a、语法糖形式,
      b、无需export default,自动将顶层变量/函数暴露给模板,使用defineProps/defineEmits等API
      c、不兼容Vue2选项式语法
    B、适用场景,新组件开发,追求简洁性和开发效率
    C、示例
      <template>
        <div class="container">
          <!-- 响应式API演示区 -->
          <div class="reactive-demo">
            <h3>响应式API示例</h3>
            <p>基本类型(ref):{{ countRef }}</p>
            <p>对象属性(reactive):{{ user.name }}</p>
            <p>单个属性引用(toRef):{{ ageRef }}</p>
            <p>解构属性(toRefs):{{ name }}</p>
            <p>计算属性:{{ fullInfo }}</p> <!-- 新增计算属性展示 -->
          </div>
          <!-- 功能演示区 -->
          <div class="function-demo">
            <h3>功能综合示例</h3>
            <p>计数(watch监听):{{ countWatch }}</p>
            <button @click="increment">+1(触发事件)</button>
            <button @click="updateUser('王五', 25)">修改用户信息</button> <!-- 新增方法调用 -->
            <p>Props接收值:{{ state }}</p>
          </div>
        </div>
      </template>
      <script setup>
        import { useAttrs, ref, reactive, toRef, toRefs, watch, watchEffect, onMounted, onBeforeUnmount, computed } from 'vue';
        //1、处理props与上下文
        const props = defineProps({
          state: {
            type: String,
            required: true,
            validator: (val) => ['success', 'error', 'loading'].includes(val) //验证合法值
          }
        });
        const attrs = useAttrs() //访问特定的未声明属性
        console.log(attrs.width)
        console.log(attrs.height)
        //上面defineProps与useAttrs的区别
        //defineProps:是组件显式声明接收的属性,需要在组件的props选项中定义。不需要手动导入
        //useAttrs:包含了所有未在props中声明的属性(除了class和style)。需要手动导入
        //2、响应式数据定义
        //基本类型响应式
        const countRef = ref(0);
        countRef.value = 1;
        //对象类型响应式
        const user = reactive({
          name: '张三',
          age: 20
        });
        user.age = 21;
        //响应式属性引用
        const ageRef = toRef(user, 'age'); //提取单个属性的响应式引用
        const { name } = toRefs(user); //解构响应式对象(保持响应性)
        //用于监听演示的计数变量
        const countWatch = ref(0);
        //3、计算属性
        const fullInfo = computed(() => {
          return `${name.value}(${user.age}岁)`;
        });
        //4、方法定义
        //递增方法:修改计数并触发事件
        const emit = defineEmits(['count-changed']); //声明自定义事件
        const increment = () => {
          countWatch.value++;
          emit('count-changed', countWatch.value); //向父组件传递当前值
        };
        //修改用户信息的方法
        const updateUser = (newName, newAge) => {
          user.name = newName;
          user.age = newAge;
        };
        //5、数据监听
        //watch:精确监听countWatch变化
        watch(
          () => countWatch.value,
          (newVal, oldVal) => {
            console.log(`watch监听:计数从${oldVal}变为${newVal}`);
          }
        );
        //watchEffect:自动追踪依赖(此处追踪countWatch)
        watchEffect(() => {
          console.log(`watchEffect自动监听:当前计数为${countWatch.value}`);
        });
        //监听user对象的name属性
        watch(
          () => user.name,
          (newVal, oldVal) => {
            console.log(`用户名从${oldVal}变为${newVal}`);
          }
        );
        //6、生命周期钩子
        onMounted(() => {
          console.log('组件挂载完成:可以执行DOM操作或数据初始化');
          //模拟初始化逻辑
          const timer = setTimeout(() => {
            user.name = '李四'; //延迟修改数据,验证响应式
          }, 1000);
          //清理定时器(避免内存泄漏)
          onBeforeUnmount(() => {
            clearTimeout(timer);
          });
        });
        //组件卸载前的生命周期
        onBeforeUnmount(() => {
          console.log('组件即将卸载:可以清理定时器等资源');
        });
        //7、暴露属性和方法给父组件(通过ref访问)
        defineExpose({
          countRef,
          user,
          increment,
          updateUser,
          fullInfo
        });
      </script>
      <style scoped>
      </style>
  (3)不实用写法
    <!DOCTYPE html>
    <html>
    <head>
      <script src="https://cdn.bootcdn.net/ajax/libs/vue/3.2.39/vue.global.js"></script> 
    </head>
    <body>
      <div id="box">
        <vue3-component :myname="nameMy">插槽</vue3-component>
      </div>    
    </body>
    </html>   
    <script>
      var obj = {
        data(){
          return {
            nameMy:"张三"
          }
        },
        methods:{},
        computed:{}
      }
      var app = Vue.createApp(obj) //创建一个Vue应用实例(app)
      app.component("vue3-component",{ //在这个应用实例上注册全局组件
        props:["myname"],
        template:`
          <div>
            {{myname}}
            <slot></slot>
          </div>
        `
      })
      app.mount("#box") //挂载应用, //效果同app.mount(document.getElementById('box'))
    </script>
    
五、两版Vue源码不同
附、两版VUE说明!!!A
1、两版VUE之方法、计算属性、侦听器
  (1)方法的独特之处
    A、主动触发:需手动调用,不会自动响应数据变化
    B、无缓存:每次调用都会完整执行函数逻辑,适合动态计算
    C、支持参数:可接收外部参数,灵活适配不同场景
    D、自由副作用:适合执行异步操作(如请求)、DOM操作或其他非纯逻辑
  (2)计算属性的独特之处
    A、自动依赖追踪:基于响应式数据自动计算,依赖变化时重新求值
    B、结果缓存:依赖未变化时直接返回缓存值,提升性能
    C、必须返回值:用于派生数据
    D、声明式设计:模板中像普通属性一样使用
    附、vue2和vue3的计算属性在作为依赖方和被依赖方时的区别
      a、vue2中,用相同的逻辑处理。都用getter处理
      b、vue3中,用不同的逻辑处理。作为依赖方,用ReactiveEffect处理;作为被依赖方,由响应式系统统一处理
  (3)侦听器的独特之处
    A、响应数据变化:监听特定数据源,变化时执行回调(如路由、表单字段)
    B、支持深度监听:配置deep:true可监听对象/数组内部变化
    C、副作用中心:适合数据变化后执行异步请求、复杂逻辑或全局状态更新
    D、手动控制:需显式指定监听目标(相比computed的自动依赖)
  (4)方法、计算属性、侦听器、onMounted、updated的区别
    A、方法,主动调用的函数,用于执行特定逻辑(如事件处理、异步操作等)
    B、计算属性,由其他数据推导而来的衍生数据,本质是依赖多个源数据,生成一个目标数据
    C、侦听器,某个数据变化后,执行额外操作,本质是当源数据变化时,触发副作用
    D、onMounted,组件挂载后执行操作,本质是组件DOM就绪后触发初始化逻辑
    E、updated,组件DOM更新后执行操作,本质是DOM更新完成后触发后续处理  
    附、生命周期中,不涉及Dom的可以用before 
2、两版VUE之依赖存储与nextTick执行流程  
  (1)依赖存储机制,两版对比
    A、Vue2的分散式依赖存储,通过为每个响应式属性创建独立的Dep实例来关联Watcher,没有全局统一的依赖存储结构
    B、Vue3的集中式依赖存储,使用全局的targetMap(WeakMap)统一管理所有响应式对象的依赖关系
      a、它的键是响应式对象的内存地址
      b、在不同页面定义同名不同实例的对象,并作为键名添加到实例的键值对,不会冲突
  (2)nextTick的具体执行流程,两版是一致的
    A、用户修改Vue组件中的响应式数据
    B、Vue将组件更新函数加入内部的异步更新队列
    C、若队列未被调度(pending为false),Vue
      a、标记pending为true,
      b、创建微任务并追加到浏览器的微任务队列
    D、用户调用Vue.nextTick,Vue将用户的回调追加到同一队列
    E、当前同步代码执行完毕,JS引擎执行浏览器的微任务队列时,执行到该微任务
      a、先执行DOM更新函数,更新真实DOM
      b、再执行nextTick回调,获取最新DOM状态
      c、执行完成后标记pending为false
    F、浏览器检测到DOM变化,重新渲染页面
    G、示例
      a、Vue2示例
        methods: {
          getText () {
            this.showText = true;
            this.$nextTick(() => {//或Vue.nextTick,加上这层壳,下面内容在DOM更新后执行
              var innerHTML = document.getElementById('divBox').innerHTML;
              console.log(innerHTML);
            })
          }
        },
      b、Vue3示例
        nextTick(() => {//加上这层壳,下面内容在DOM更新后执行
          var innerHTML = document.getElementById('divBox').innerHTML;
          console.log(innerHTML);
        })
  (3)相关术语
    A、模板编译,将<template>转化为render函数
    B、组件挂载,将组件从“内存中的JS对象”渲染为“页面中真实DOM元素”      
3、VUE2从数据变化到页面渲染的过程
  (1)初始化阶段
    A、Vue使用Object.defineProperty对data选项中的每个属性创建getter/setter
    B、递归处理所有嵌套对象属性,为每个属性创建独立的getter/setter
    C、computed属性初始化时创建惰性求值的计算Watcher,watch配置初始化时创建立即执行(除非配置immediate:false)的用户Watcher
    D、每个响应式属性都有一个Dep实例,通过闭包保存所有依赖它的Watcher,同时Watcher通过deps数组记录自己依赖的所有Dep
    E、计算Watcher、用户Watcher、渲染Watcher(后续创建)
      a、都会在首次访问数据时,触发getter、调用dep.depend
      b、通过Dep.target机制建立数据与Watcher的双向关联
  (2)模板编译
    A、模板编译分为三个阶段(解析→优化→生成),最终产出render函数
    B、组件挂载时,创建渲染Watcher(传入updateComponent函数),执行render函数
  (3)数据变化检测
    A、数据修改触发setter→调用dep.notify
    B、Dep遍历subs数组(存储的Watcher集合),依次调用watcher.update
  (4)更新调度与批量处理
    A、Watcher.update将自身推入队列(queueWatcher)
    B、使用has对象实现Watcher.id去重
    C、通过nextTick将flushSchedulerQueue放入异步队列,确保:
      a、同一事件循环内的多次数据变更合并为一次渲染
      b、父组件的更新优先于子组件
  (5)重新渲染与虚拟DOM
    A、执行watcher.run→调用get→执行updateComponent→生成新vnode
    B、diff算法核心逻辑:
      a、同层比较(深度优先)
      b、双端交叉对比(头头、尾尾、头尾、尾头)
      c、基于key的复用优化
    C、产出补丁指令(如INSERT、REMOVE、PROPS等)
  (6)DOM更新
    A、patch方法根据diff结果操作真实DOM
    B、执行插入后的生命周期(updated)
4、VUE3从数据变化到页面渲染的过程
  附、流程:数据变化→Proxy拦截→通知依赖→调度更新→生成新VNode→Diff对比→更新DOM
  (1)初始化阶段
    A、响应式系统基于Proxy,通过reactive创建对象代理,ref处理原始值
    B、Proxy自动处理嵌套对象(惰性代理),动态属性变更可直接触发响应
    C、computed返回ComputedRefImpl实例,watch/watchEffect创建ReactiveEffect
    D、依赖关系存储在全局targetMap
    E、计算属性(computed)、用户侦听器(watch/watchEffect)、渲染副作用(后续创建)
      a、都会在首次访问数据时触发Proxy的get拦截,并通过track建立依赖追踪
      b、通过activeEffect和全局依赖图(targetMap)建立响应式数据与副作用(ReactiveEffect)的双向关联
  (2)模板编译
    A、编译过程新增优化阶段:
      a、静态提升(将静态节点提取到渲染函数外部)
      b、补丁标记(PatchFlags:1=TEXT,2=CLASS,4=STYLE...)
      c、区块树(BlockTree)追踪动态节点
    B、组件挂载时:
      a、创建渲染effect(setupRenderEffect)
      b、执行render触发track→将当前activeEffect存入targetMap
      c、建立响应式数据与effect的关联
  (3)数据变化检测
    A、数据修改触发Proxyset→调用trigger函数
    B、从targetMap中获取对应属性的effects集合
  (4)更新调度与副作用执行
    A、调度系统(scheduler)特性:
      a、使用queueJob管理任务(基于Set自动去重)
      b、优先级:父组件更新优先,用户自定义watch可指定pre/post队列
    B、默认使用微任务队列(Promise.then)批量执行
  (5)虚拟DOM与Diff算法
    A、动态节点对比优化:
      a、静态节点完全跳过diff
      b、带key的列表使用最长递增子序列(LIS)算法
      c、基于patchflag的靶向更新(如仅比对class或style)
    B、BlockTree机制:
      a、动态节点按区块(Block)组织
      b、通过dynamicChildren数组快速定位变化节点
  (6)DOM更新
    A、渲染器(renderer)根据diff结果执行精细化DOM操作
    B、支持选择性挂载Tree-shaking的渲染模块
5、VUE2的异步批处理机制的说明
  (1)原理!!!
    A、利用事件循环机制,通过基础的更新队列处理逻辑,合并同一个事件循环内的所有响应式修改
    B、优先使用Promise.then(微任务)统一触发DOM更新,但有降级策略以兼容旧环境
  (2)响应式数据变化时
    A、触发dep.notify,通知Watcher调用update,Watcher入全局队列(queue),通过has对象(以id为键)去重
    B、同步代码执行完,更新调度机制以nextTick包装队列为微任务,浏览器异步触发
    C、nextTick优先使用微任务(Promise.resolve().then、MutationObserver),不支持则降级为宏任务(setTimeout(fn,0)),确保同步代码后执行
  (3)多次修改响应式变量,Watcher只执行一次。不同类型Watcher的执行顺序及其作用如下
    A、计算属性Watcher:维护计算属性的值,依赖变化时标记为脏值,被访问时重新计算
    B、用户Watcher(如watch):通过watch选项或$watch创建,监测指定数据变化并执行回调
    C、渲染Watcher:负责组件模板渲染,依赖数据变化时触发重新渲染,基于最新数据更新DOM
6、VUE3的异步批处理机制的说明
  (1)原理!!!
    A、利用事件循环机制,通过优化的更新队列处理逻辑,合并同一个事件循环内的所有响应式修改
    B、默认使用Promise.then(微任务)统一触发DOM更新,但提供了更灵活的调度机制,如优先级控制等
  (2)响应式数据变化时
    A、触发Proxy的set拦截器,调用trigger,从依赖图(targetMap)中获取关联的ReactiveEffect,推入全局执行队列queue(基于Set自动去重)
    B、调度器通过Promise.resolve().then(flushJobs)将队列封装为微任务,同步代码执行后由浏览器触发flushJobs按优先级执行副作用函数
    C、默认优先使用微任务(Promise.then),不支持时降级为queueMicrotask或MutationObserver,最终回退到宏任务setTimeout(fn,0)
  (3)多次修改响应式变量,副作用只执行一次。不同类型副作用的执行顺序及其作用如下 
    A、前置阶段
      a、beforeUpdate钩子,组件渲染前触发,可访问更新前的DOM
      b、计算属性,惰性计算,仅标记为脏值
    B、执行阶段
      a、侦听器(默认flush:'pre'),watch和watchEffect执行
      b、组件渲染,执行render函数,触发计算属性求值,生成VNode,更新DOM,期间会重新计算被访问的计算属性
    C、后置阶段
      a、updated钩子,DOM更新后触发,可安全操作更新后的DOM
      b、侦听器(设置flush:'post'),watch和watchEffect执行
7、VUE2响应式机制的实现:依赖收集与触发流程(以计算属性为例)
  (1)说明
    A、初始化配置
      a、响应式数据处理:通过Observer类将数据(对象/数组)转为响应式,对每个属性用Object.defineProperty定义get(读取)和set(修改)拦截
      b、依赖管理器:每个响应式属性对应一个Dep实例(依赖容器),用于存储依赖该属性的Watcher
      c、副作用封装:computed、watch、组件渲染等副作用被封装为Watcher实例,包含更新函数(如update)
    B、依赖收集(get拦截阶段),适用于computed、watch、组件渲染等所有副作用场景
      a、设置全局标记:执行Watcher时,将当前Watcher赋值给Dep.target(全局唯一)!!!A
      b、触发get拦截:副作用函数执行时读取响应式属性,触发其get拦截
      c、建立关联:在get拦截中,
        当前Dep实例会将Dep.target(当前Watcher)添加到自身依赖列表(dep.addSub(watcher))
        同时Watcher也会记录该Dep(watcher.addDep(dep)),形成双向关联
      d、清除全局标记:副作用执行完毕后,Dep.target被重置为上一个Watcher(支持嵌套副作用)
    C、触发更新(set拦截阶段)
      a、修改数据触发set拦截:当响应式属性被修改时,触发其set拦截
      b、通知依赖:set拦截中调用Dep.notify(),遍历依赖列表中的所有Watcher,执行其update方法
      c、批量更新:Watcher进入异步更新队列(nextTick),避免频繁更新,最终执行run方法触发副作用重新执行(如重新渲染组件)
  (2)Vue实例初始化时的计算属性
    A、Vue实例初始化时调用initComputed
      function initState(vm) {
        vm._watchers = [];
        const opts = vm.$options;
        if (opts.computed) {
          initComputed(vm, opts.computed); //触发整个流程
        }
        //...其他初始化逻辑...
      }
    B、初始化计算属性时调用defineComputed
      function initComputed(vm, computed) {
        const watchers = vm._computedWatchers = Object.create(null);
        for (const key in computed) {
          const userDef = computed[key];
          let getter = userDef;
          if (!watchers[key]) {
            watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              {
                lazy: true, //标记为懒执行
                computed: true //标记为计算属性Watcher
              }
            );
          }
          if (!(key in vm)) {
            defineComputed(vm, key, userDef); 
          }
        }
      }
    C、定义计算属性的核心函数
      function defineComputed(target, key, userDef) {
        const shouldCache = !isServerRendering();
        //处理计算属性的getter和setter
        if (typeof userDef === 'function') {
          sharedPropertyDefinition.get = shouldCache
            ? createComputedGetter(key)  
            : userDef;
          sharedPropertyDefinition.set = noop;
        } else {
          sharedPropertyDefinition.get = userDef.get
            ? (shouldCache && userDef.cache !== false)
              ? createComputedGetter(key)  
              : userDef.get
            : noop;
          sharedPropertyDefinition.set = userDef.set || noop;
        }
        //将计算属性定义到目标对象上(通常是vm实例)
        Object.defineProperty(target, key, sharedPropertyDefinition);
      }
    D、创建计算属性的getter函数
      function createComputedGetter(key) {
        return function computedGetter() {
          //获取当前计算属性对应的Watcher实例
          const watcher = this._computedWatchers && this._computedWatchers[key];
          if (watcher) {
            //当计算属性依赖的“响应式数据”发生变化后,页面渲染时,依次调用computedGetter、evaluate、getter。!!!B
            if (watcher.dirty) {
              watcher.evaluate();
            }
            //如果存在活跃的依赖收集目标(通常是渲染Watcher)
            //建立当前计算属性Watcher与目标Watcher的依赖关系
            if (Dep.target) {
              watcher.depend();
            }
            //返回计算结果
            return watcher.value;
          }
        };
      }
  (3)Watcher类
    A、Watcher类定义
      class Watcher { //new watcher
        constructor(vm, expOrFn, cb, options) {
          this.vm = vm;
          this.getter = expOrFn;
          //1、expOrFn在不同场景下的类型
          //(1)watch配置:可以是字符串(数据路径,如'a.b',即this.value=this.a.b,后者触发getter)或函数(返回监测值)
          //(2)computed配置:是计算属性的getter函数
          //(3)组件渲染:是组件的render渲染函数,生成VNode
          this.cb = cb; 
          //2、cb在不同场景下的作用
          //(1)watch配置:是数据变化时的回调函数(接收新值、旧值)
          //(2)computed配置:是noop(空函数,因计算属性无需额外回调,仅需更新自身值)
          //(3)组件渲染:是noop(空函数,渲染的目的是更新DOM,无需额外回调)
          this.options = options;
          this.lazy = options.lazy;
          this.dirty = this.lazy;
          this.id = ++uid;
          this.deps = [];
          this.newDeps = new Set();
          this.value = this.get(); //添加订阅者1/6。!!!C
        }
        evaluate() {
          this.value = this.get();
          this.dirty = false;
        }
        get() {
          pushTarget(this); //getter在执行前,其容器被放在全局
          let value;
          try {
            value = this.getter.call(this.vm, this.vm); //添加订阅者2/6。
          } finally {
            popTarget(); 
          }
          return value;
        }
        update() {
          if (this.lazy) {
            this.dirty = true
            //当计算属性依赖的“响应式数据”发生变化时,通知计算属性的Watcher,将其dirty标志设为true,不立即计算。!!!B
          } else if (this.sync) {
            //同步更新,立即执行。具体场景,极少出现,用户需要数据变化后立即执行回调
            // this.$watch('someData', (newVal, oldVal) => {
            //   console.log('同步执行回调')
            // }, { sync: true })
            this.run()
          } else {
            //异步更新,加入调度队列。具体场景,每个组件的渲染Watcher、用户组件的watch配置
            queueWatcher(this) //通知订阅者3/7。
          }
        }
        run() {
          const value = this.get(); //重新计算值
          if (value !== this.value) { //如果值发生变化,执行回调
            const oldValue = this.value
            this.value = value
            //执行回调(渲染/用户watch等)
            this.cb.call(this.vm, value, oldValue) //通知订阅者7/7。
          }
        }
        depend() {
          this.deps.forEach(dep => dep.depend());
        }
        addDep(dep) {
          const id = dep.id;
          if (!this.newDeps.has(id)) {
            //1、当一个Watcher多次访问一个响应式数据时,上面判断避免出现Dep与Watcher互相重复存储
            this.newDeps.add(id);
            dep.addSub(this); //添加订阅者5/6。
            this.deps.push(dep);
            //2、Dep实例与Watcher实例互相存储,建立双向关联,是上面两行代码的作用
            //(1)Dep存储Watcher,dep.addSub(this)
            //  A、主要目的,派发更新。响应式数据变化时,让Dep通知Watcher,执行回调
            //  B、典型场景,一个Watcher依赖多个响应式数据时,多个Dep实例存储同一个Watcher实例
            //(2)Watcher存储Dep,this.deps.push(dep)
            //  A、主要目的,依赖清理。让Watcher知道自己依赖了哪些Dep,将自身移出Dep
            //  B、典型场景,多个Watcher依赖一个响应式数据时,多个Watcher实例存储同一个Dep实例
          }
        }
        teardown() { //依赖清理1/2。!!!D
          if (this.active) {
            if (!this.vm._isBeingDestroyed) {
              remove(this.vm._watchers, this);
            }
            var i = this.deps.length;
            while (i--) {
              this.deps[i].removeSub(this);
            }
            this.active = false;
          }
        };
      }
    B、Watcher实例的管理与执行
      //以下,定义队列相关变量
      var waiting = false;
      let has = new Set();
      const queue = [];
      const callbacks = [];
      let pending = false;
      //以下,将Watcher去重后加入队列,并在下一个微任务中触发批量更新
      function queueWatcher(watcher) {
        const id = watcher.id;
        if (!has.has(id)) {
          has.add(id);
          queue.push(watcher); 
          if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue) //通知订阅者4/7。
          }
        }
      }
      //以下,按ID排序后,依次执行队列中的Watcher更新,并重置调度状态
      function flushSchedulerQueue() {
        let watcher, id
        //排序:确保父组件Watcher先于子组件执行,用户Watcher先于渲染Watcher
        queue.sort((a, b) => a.id - b.id)
        for (let i = 0; i < queue.length; i++) {
          watcher = queue[i]
          id = watcher.id
          has[id] = null //重置标记
          watcher.run() //通知订阅者5/7。
        }
        resetSchedulerState()
      }
      //以下,将回调函数延迟到下一个微任务队列中异步执行,确保在DOM更新后触发
      function nextTick (cb, ctx) { //nextTick定义
        callbacks.push(() => {
          if (cb) {
            try {
              cb.call(ctx)
            } catch (e) {
              handleError(e, ctx, 'nextTick')
            }
          }
        })
        if (!pending) {
          pending = true
          Promise.resolve().then(flushCallbacks) //通知订阅者6/7。
          //将flushCallbacks推入微任务队列
          //注意:此时callbacks只有一个回调(当前刚添加的)
          //在微任务执行前,可能会被后续nextTick调用追加更多回调
          //浏览器在执行完同步任务后,执行此微任务
        }
      }
      //以下,同步执行当前所有回调函数的副本并清空回调队列
      function flushCallbacks () {
        pending = false
        const copies = callbacks.slice(0)
        callbacks.length = 0
        for (let i = 0; i < copies.length; i++) {
          copies[i]()
        }
      }
      //以下,重置队列相关变量
      function resetSchedulerState() {
        queue.length = 0
        has = {}
        waiting = false
      }
  (4)响应式核心逻辑(依赖收集与触发的底层实现)
    //以下,递归观测对象
    function observe(obj) {
      if (typeof obj !== 'object' || obj === null) {
        return;
      }
      new Observer(obj);
    }
    //以下,定义Observer类
    class Observer {
      constructor(obj) {
        if (Array.isArray(obj)) {
          //处理数组(重写数组方法)
          this.observeArray(obj);
        } else {
          //处理对象(遍历属性添加 getter/setter)
          Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key]);
          });
        }
      }
      observeArray(arr) {
        arr.forEach(item => observe(item));
      }
    }
    //以下,响应式对象创建(基于Object.defineProperty的响应式封装)
    function defineReactive(obj, key, val) {
      observe(val); //递归处理嵌套对象
      const dep = new Dep(); //每个属性对应一个Dep实例(依赖管理器)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get() {
          if (Dep.target) {
            dep.depend(); //添加订阅者3/6。 
          }
          return val;
        },
        set(newVal) {
          if (newVal === val) return;
          val = newVal;
          dep.notify(); //通知订阅者1/7。!!!E
        }
      });
    }
  (5)Dep类
    A、定义Dep类
      class Dep {
        constructor() {
          this.id = ++depId;
          this.subs = [];
        }
        depend() {
          if (Dep.target) { //依赖存储
            Dep.target.addDep(this); //添加订阅者4/6。
          }
        }
        addSub(sub) {
          this.subs.push(sub); //添加订阅者6/6。
        }
        notify() {
          const subs = this.subs.slice(); //(用浅克隆)固定订阅者列表,避免在更新中修改原数组
          subs.forEach(sub => sub.update()); //通知订阅者2/7。
        }
      }
    B、与Dep类相关
      Dep.target = null;
      const targetStack = [];
      function pushTarget(target) {
        targetStack.push(target);
        Dep.target = target;
      }
      function popTarget() {
        targetStack.pop();
        Dep.target = targetStack[targetStack.length - 1];
      }
8、VUE3响应式机制的实现:依赖收集与触发流程(以计算属性为例)
  (1)说明
    A、初始化配置
      a、响应式数据处理:通过reactive或ref创建响应式对象,底层用Proxy代理整个对象,拦截get(读取)、set(修改)等操作
      b、创建副作用:通过effect函数创建副作用,接收一个函数(副作用主体)和配置项(如scheduler调度器、lazy懒执行)
      c、依赖管理:使用targetMap(WeakMap)存储依赖关系,结构为:targetMap->target->depsMap->key->effects(Set)
    B、依赖收集(get拦截阶段),适用于computed、watch、组件渲染等所有副作用场景
      a、设置全局标记:执行副作用前,将当前effect实例赋值给activeEffect(全局变量)!!!A
      b、触发get拦截:副作用函数执行时读取响应式数据,触发Proxy的get拦截器
      c、建立关联:
        从targetMap中获取当前对象(target)的depsMap,若不存在则创建
        从depsMap中获取当前属性(key)的effects集合,若不存在则创建
        将activeEffect添加到effects集合中,同时effect实例会记录自身依赖的deps(便于清理)
      d、清除全局标记:副作用执行完毕后,activeEffect重置为上一个副作用(支持嵌套)
    C、触发更新(set拦截阶段)
      a、修改数据触发set拦截:当响应式数据被修改时,触发Proxy的set拦截器
      b、收集关联副作用:从targetMap中找到当前target和key对应的所有effects
      c、执行副作用:遍历effects集合,
        若副作用配置了scheduler则执行调度器(如watch的回调)
        否则将副作用的run方法加入异步更新队列(queueJob),最终批量执行(通过nextTick实现)
  (2)computed
    A、定义
      class Computed {
        constructor(getterOrOptions) { //构造函数接收getter或配置对象
          if (typeof getterOrOptions === 'function') {//初始化getter和setter
            this.getter = getterOrOptions;
            this.setter = () => console.warn('Write operation failed: computed value is readonly');
          } else {
            this.getter = getterOrOptions.get;
            this.setter = getterOrOptions.set;
          }
          //缓存相关状态
          this.dirty = true; //标记是否需要重新计算
          this.value; //缓存计算结果
          this.obj; //最终返回的响应式对象
          //创建调度器
          const createComputedScheduler = () => { //定义计算属性的调度器创建函数
            return () => { //当依赖变化时,调度器被调用,不立即计算,将其dirty标志设为true,避免重复安排一个微任务来触发更新
              if (!this.dirty) { 
                this.dirty = true; 
                queueJob(() => { //在下一次微任务中触发value的更新通知
                  trigger(this.obj, 'value'); //作为被“依赖方”,通知所有依赖该计算属性的effect(如渲染effect)进行更新。依赖触发2/4
                });   
              }
            };
          };
          //创建响应式副作用实例
          this.effect = new ReactiveEffect( //是Vue响应式系统的核心,用于跟踪依赖和调度更新
            this.getter, //是计算属性的计算逻辑
            createComputedScheduler() //返回的调度器会在依赖变化时被调用
          );
          //定义计算属性的响应式对象(模拟Ref接口)
          this.obj = {
            __v_isRef: true,
            get value() {
              track(this.obj, 'value'); //作为被“依赖方”,计算属性收集依赖于自己的effect,如渲染effect。依赖收集1/4。!!!F
              //当dirty为true时,计算属性依赖的响应式数据发生变化且尚未重新计算
              //当dirty为false时,计算属性已根据最新依赖数据计算并缓存结果
              if (this.dirty) {
                this.value = this.effect.run(); //执行getter。作为“依赖方”,内部响应式数据收集当前(计算属性)的effect,计算新值。依赖收集3/4
                this.dirty = false; //将dirty设为false表示已更新,后续访问直接返回缓存值
              }
              return this.value; //返回计算结果
            },
            set value(newVal) {
              this.setter(newVal);
            }
          };
        }
        //对外暴露计算属性对象
        getInstance() {
          return this.obj;
        }
      }
      //使用方式:const computedObj = new Computed(getter).getInstance();
    B、使用
      <template>
        <div>
          <input v-model="text" placeholder="输入文本">
          <p>反转: {{ reversedText }}</p>
          <p>长度: {{ textLength }} 字符</p>
        </div>
      </template>
      <script setup>
        import { ref, computed } from 'vue'
        const text = ref('')
        const reversedText = computed(() => 
          text.value.split('').reverse().join('')
        )
        const textLength = computed(() => text.value.length)
      </script>
  (3)ReactiveEffect,响应式副作用类
    A、依赖存储
      const targetMap = new WeakMap() //全局依赖存储
      let activeEffect = null //当前活跃的effect
      let shouldTrack = true //是否应该收集依赖
    B、targetMap定义
      const targetMap = new WeakMap()
      targetMap.set(响应式对象, 
        new Map([
          [属性名, new Set([effect1, effect2, ...])] //注意这里是数组形式[键, 值]
        ])
      )
    C、定义
      class ReactiveEffect {
        constructor(fn, scheduler = null) {
          this.fn = fn 
          this.scheduler = scheduler
          this.deps = [] 
          this.active = true 
        }
        run() { 
          if (!this.active) return this.fn()
          try {
            activeEffect = this
            shouldTrack = true
            return this.fn() //执行副作用函数。作为“依赖方”,内部响应式数据收集当前的effect。依赖收集4/4,
          } finally {
            shouldTrack = false
            activeEffect = null
          }
        }
        stop() {
          if (this.active) {
            cleanupEffect(this)
            this.active = false
          }
        }
      }
    D、调度器机制的构成
      a、调度函数scheduler,ReactiveEffect构造函数的第2个参数
      b、任务队列管理模块
        const queue = []
        let isFlushing = false
        let isFlushPending = false
        function queueJob(job) { //起到去重作用。这是dirty标志的防御,正常情况下,不会重复添加任务
          if (!queue.includes(job)) {
            queue.push(job)
            queueFlush()
          }
        }
        function queueFlush() {
          if (!isFlushing && !isFlushPending) {
            isFlushPending = true
            Promise.resolve().then(flushJobs) //!!!G
          }
        }
        function flushJobs() {
          isFlushPending = false
          isFlushing = true
          //排序保证父子组件更新顺序
          queue.sort((a, b) => a.id - b.id)
          try {
            for (let i = 0; i < queue.length; i++) {
              queue[i]()
            }
          } finally {
            queue.length = 0
            isFlushing = false
          }
        }
  (4)响应式核心逻辑(依赖收集与触发的底层实现)
    A、响应式方法ref的定义
      function ref(value) {
        //如果已经是ref直接返回
        if (isRef(value)) {
          return value
        }
        //如果是对象类型,自动调用reactive转换为响应式
        if (typeof value === 'object' && value !== null) {
          value = reactive(value)
        }
        //创建ref对象(变量名从r改为temp)
        const temp = {
          __v_isRef: true,
          get value() {
            //依赖收集,track定义
            track(temp, 'value')
            return value
          },
          set value(newVal) {
            //只有值变化时才触发更新
            if (hasChanged(newVal, value)) {
              value = newVal
              trigger(temp, 'value')
            }
          }
        }
        return temp
      }
    B、响应式方法reactive定义
      const reactiveMap = new WeakMap(); //缓存响应式对象
      function reactive(target) {
        //非对象直接返回
        if (typeof target !== 'object' || target === null) return target;
        //已经是响应式代理则直接返回
        if (target.__v_isReactive) return target;
        //检查缓存
        const cached = reactiveMap.get(target);
        if (cached) return cached;
        //创建代理处理器
        const handler = {
          get(target, key, receiver) {
            //标记响应式对象
            if (key === '__v_isReactive') return true;
            const res = Reflect.get(target, key, receiver);
            //依赖收集(简化参数:target, key)
            track(target, key);
            //递归转换对象为响应式
            if (typeof res === 'object' && res !== null) {
              return reactive(res);
            }
            return res;
          },
          set(target, key, value, receiver) {
            const oldValue = target[key];
            const result = Reflect.set(target, key, value, receiver);
            //只有值变化时才触发更新(简化参数:target, key)
            if (oldValue !== value) {
              trigger(target, key);
            }
            return result;
          }
        };
        //创建代理
        const proxy = new Proxy(target, handler);
        reactiveMap.set(target, proxy);
        return proxy;
      }
    C、依赖收集
      function track(target, key) {
        if (!shouldTrack || !activeEffect) return
        let depsMap = targetMap.get(target)
        if (!depsMap) {
          targetMap.set(target, (depsMap = new Map()))
        }
        let dep = depsMap.get(key)
        if (!dep) {
          depsMap.set(key, (dep = new Set()))
        }
        trackEffects(dep)
      }
      function trackEffects(dep) {
        if (!dep.has(activeEffect)) {
          dep.add(activeEffect)
          activeEffect.deps.push(dep) 
          //2、dep与activeEffect互相存储,建立双向关联,是上面两行代码的作用
          //(1)dep存储activeEffect,dep.add(activeEffect)
          //  A、主要目的,派发更新。响应式数据变化时,让dep通知activeEffect,执行回调
          //  B、典型场景,一个activeEffect依赖多个响应式数据时,多个dep存储同一个activeEffect
          //(2)activeEffect存储dep,activeEffect.deps.push(dep) 
          //  A、主要目的,依赖清理。让activeEffect知道自己依赖了哪些dep,将自身移出dep
          //  B、典型场景,多个activeEffect依赖一个响应式数据时,多个activeEffect实例存储同一个dep
        }
      }
      function cleanupEffect(effect) { //依赖清理2/2。
        for (const dep of effect.deps) {
          dep.delete(effect)
        }
        effect.deps.length = 0
      }
    D、依赖触发
      function trigger(target, key) { //触发依赖本对象的effect
        const depsMap = targetMap.get(target)
        if (!depsMap) return
        const effects = new Set()
        const addEffects = (dep) => { //收集依赖集合中的所有effect
          dep && dep.forEach(effect => {
            effects.add(effect)
          })
        }
        addEffects(depsMap.get(key))
        effects.forEach(effect => {
          if (effect.scheduler) { //有调度器的effect(如computed、watch),执行其自定义调度器 
            effect.scheduler()
          } else { //无调度器的effect(如组件渲染effect),将“副作用函数”加入异步更新队列。!!!G
            queueJob(effect.run.bind(effect))
          }
        })
      }

六、两版Vue用法不同(区别)
  来源,https://blog.51cto.com/knifeedge/5616852
  (1)Vue2.x
    Vue-cli: 3.x、4.x.
    Vue-router: 3.x //渲染位置<router-view>
    Vuex: 3.x
  (2)Vue3.x
    Vue-cli: 4.x 
    Vue-router: 4.x
    Vuex: 4.x
1、实例化不同
  (1)vue2下,用new获取和挂载vue实例
    import Vue from 'vue';
    import router from './router';
    import store from './store';
    new Vue({
      el: '#app',
      router,
      data,
      store,
      render: createElement => createElement(App)//render执行,即createElement执行
    })
    //附、全局方法添加与调用
    //以下添加
      Vue.prototype.$myGlobalMethod = function () {
        console.log(11111);
      };
      new Vue({})
    //以下调用
      export default {
        mounted() {
          this.$myGlobalMethod();
        }
      }
  (2)vue3下,用createApp、mount,获取、挂载vue实例
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    import store from './store'
    import ElementPlus from 'element-plus'
    createApp(App).use(store).use(router).use(ElementPlus).mount('#app')
    //附、全局方法添加与调用
    //以下添加  
      const app = createApp(App)
      app.config.globalProperties.aaa = function(){
        console.log( 22222 );
      }
      app.mount('#app') //效果同app.mount(document.getElementById('app'))
    //以下调用
      <script setup>
        const { proxy } = getCurrentInstance()
        proxy.aaa()
2、双向绑定原理不同
  (1)vue2用ES5的一个Object.defineProperty对数据进行劫持,结合发布订阅模式的方式来实现的
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter() {
        return value;
      },
      set: function reactiveSetter(newVal) {
        dep.notify();
      },
    });
  (2)vue3用ES6的Proxy对数据进行代理
    Proxy 的优势:
     A、可以直接监听对象、数组的变化
     B、有多达13种拦截方法
     C、返回的是一个新对象供操作
    var obj = {};
    var thisObj = new Proxy(obj, {
      get: function (target, key, receiver) {
        console.log(obj === target); //true
        if (key === 4) {
          return 5;
        }
        return 6;
      },
    });
    thisObj.count = 1;
    console.log(thisObj.count);
    console.log("count" in thisObj);    
3、生命周期
  (1)版本对比(不涉及Dom的可以用before)
    A、vue2.2.0之前:
        beforeCreate--、created--、beforeMount--、mounted--、beforeUpdate--、updated--、
        “beforeDestroy--、destroyed--”、activated--、deactivated--、errorCaptured
    B、vue2.2.0之后:
        beforeCreate--、created--、beforeMount--、mounted--、beforeUpdate--、updated--、
        “beforeUnmount--、unmounted--”、activated--、deactivated--、errorCaptured
    C、vue3.0.0-beta.20之前,setup(props, context)作为组件选项,替代beforeCreate和created,与vue2的组件写法并存
        “beforeCreate(setup)created”、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、
        onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured
    D、vue3.0.0-beta.21之后,通过<script setup>简化了该组件选项的写法,与vue2的组件写法二选一
        “onbeforeCreate、oncreated”、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、
        onBeforeUnmount、onUnmounted、onActivated、onDeactivated、onErrorCaptured
  (2)生命周期触发及利用,vue2/vue3
    A、创建
      a、beforeCreate,实例初始化后、watcher生成前调用,data和methods未初始化
      b、created,实例创建后调用,data等配置完成
    B、挂载
      c、beforeMount/onBeforeMount,实例挂载前调用,编译模板,调用render,生成vDom即虚拟DOM
      d、mounted/onMounted,实例挂载后调用,可以访问操作DOM
    C、更新    
      a、beforeUpdate/onBeforeUpdate,数据改变后、虚拟DOM重新渲染前调用
      b、updated/onUpdated,虚拟DOM-重新渲染-后调用,==修改data数据,会造成死循环==
    D、卸载
      a、(beforeDestroy/beforeUnmount)/onBeforeUnmount,实例销毁-前调用
      b、(destroyed/unmounted)/onUnmounted,实例销毁-后调用
    E、请求
      created、beforeMount、mounted,可以在里调用异步请求,进入更新生命周期
    F、getCurrentInstance获取实例原理
      a、在<script setup>中,顶层代码会在组件初始化时同步执行,执行时机与setup()函数内部一致
      b、Vue3的组合式API在
      c、
    G、
  (3)父子,vue2
    A、加载顺序
      a、父beforeCreate -> 父created -> 父beforeMount -> 
      b、子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 
      c、父mounted
    B、更新顺序:父beforeUpdate -> 子beforeUpdate -> 子updated -> 父updated 
    C、卸载顺序:
      a、父(beforeDestroy/beforeUnmount) -> 
      b、子(beforeDestroy/beforeUnmount) -> 子(destroyed/unmounted) -> 
      c、父(destroyed/unmounted)  
  (4)父组件监听子组件的生命周期的方案
    A、方案1
      //Parent.vue
        <Child @mounted="doSomething"/>
      //Child.vue
        mounted() {
          this.$emit("mounted");
        }
    B、方案2
      //Parent.vue
        <Child @hook:mounted="doSomething" ></Child>
        doSomething() {
          console.log('父组件监听到 mounted 钩子函数 ...');
        },
      //Child.vue
        mounted(){
          console.log('子组件触发 mounted 钩子函数 ...');
        },
  (5)全局属性注册与获取
    A、全局属性注册
      app.config.globalProperties.$hasPermit = (permission) => { //这一步将$hasPermit添加到应用的全局配置中
        return checkPermission(permission);
      };
    B、组件实例化时     
      a、在<script setup>中,代码同步执行,时机与setup函数一致      
      b、在setup执行期间,Vue会临时存储当前组件实例(通过内部变量),并通过getCurrentInstance()暴露给用户!!!      
      c、该实例的proxy属性是一个Proxy,代理了对本地状态(如data、props)和全局属性(如$hasPermit)的访问
      d、执行完setup后,Vue会清除临时存储的实例(以避免内存泄漏和错误使用)
    C、全局属性获取   
      a、在组合式API中,通过getCurrentInstance().proxy可访问全局属性,功能上类似Vue2的this,官方不推荐过度依赖它
      b、当访问getCurrentInstance().proxy.$hasPermit时,代理逻辑会先检查“本地属性”,后检查“全局属性”并返回对应方法
      c、在setup同步作用域外或异步回调中,getCurrentInstance()可能返回null或不可靠结果
4、有无瞬移组件teleport不同
  (1)vue2没有teleport
  (2)vue3有teleport,瞬移组件到指定的dom中,相当于react中的ReactDOM.createPortal
    A、代码1
      <body>
        <div id="app"></div>
        <div id="modal"></div>
      </body>
    B、代码2
      <template>
        <teleport to="#modal">
          <div v-if="visible" class="v3-modal">
            <h2 class="v3-modal-title">{{ title }}</h2>
            <div class="v3-modal-content">
              <slot>This is a modal</slot>
            </div>
            <button @click="handleClose">close</button>
          </div>
        </teleport>
      </template>
5、是否支持多根节点不同
  附、template标签的核心作用是包裹组件的HTML结构,本身“不会”被渲染到最终的DOM中,不存在传统DOM意义上的“父子关系”!!!
  (1)vue2不支持多根节点,必须有且仅有一个根节点
    <!-- Layout.vue -->
    <template>
      <div>
        <header>...</header>
        <main>...</main>
        <footer>...</footer>
      </div>
    </template>
  (2)vue3支持多根节点,Fragment 特性
    <!-- Layout.vue -->
    <template>
      <header>...</header>
      <main v-bind="$attrs">...</main>
      <footer>...</footer>
    </template> 

七、vuex,不同组件共享数据(“通信传值传参传数据”)
  附、redux与Vuex的相似之处
    A、redux:处理同步用 dispatch action(对象类型);处理异步用 dispatch action(函数类型)
    B、Vuex:处理同步用 commit(mutations) ;处理异步用 dispatch(action),在action里执行 commit(mutations)
1、vuex3下(对应vue2下),
  (1)定义,
    A、模块定义//store/app.js
      import Cookies from 'js-cookie'
      import { makeRequest } from '@/prototype/httpRequest.js'
      const app = {
        namespaced: true, //为了解决不同模块命名冲突的问题
        state: {
          count: 0,
        },
        mutations: {
          increment(state) {
            state.count++;
          },
          setCount(state, payload) {
            state.count = payload;
          },
        },
        actions: {
          actionOut: ({commit},payload) { //也可以在此处解构payload
            const { num } = payload; //解构payload中的num
            httpRequest().then(res=>{
              commit('setCount', num);
            })
          }
        },
        getters: {
          //getters,是提取出来的computed属性,无法向它传参,可以让getter返回一个函数以接收参数,示例
          //store.getters.getTodoById(2) //-> { id: 2, text: '...', done: false }
          getTodoById: (state) => (id) => {
            return state.todos.find(todo => todo.id === id)
          }
        },
      }
      export default app 
    B、模块合并//store/index.js 
      //允许我们将 store 分割成 module,每个模块拥有自己的 state、mutation、action、getter、module
      import Vuex from 'vuex';
      import app from 'store/app.js'
      import getters from 'store/getters.js'
      Vue.use(Vuex)
      const store = new Vuex.Store({
        modules: {
          app,
          user,
          permission
        },
        getters
      })
      export default store 
  (2)注入,
    import store from 'store/index.js'
    new Vue({
      el: '#root',
      store,
    })
  (3)使用
    A、在.vue组件中使用
      methods: {
        increment() {
          this.$store.commit('increment');
        },
        incrementBy(amount) {
          this.$store.commit('incrementBy', { amount });
        },
        actionIn() {
          this.$store.dispatch('actionIn');
        },
        incrementByAsync(amount) {
          this.$store.dispatch('actionOut', { amount });
        },
      },
    B、在.js中使用
      import store from '../store'; 
      store.dispatch('actionOut', { amount });
  (4)辅助函数
    import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
    export default {
      computed: {
        ...mapState(['count']),
        ...mapGetters(['doubleCount']),
      },
      methods: {
        ...mapMutations(['increment']),
        ...mapActions(['actionIn']),
      }
    }
  (5)示例
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <title>vuex的使用</title>
      <script src="https://cdn.bootcdn.net/ajax/libs/vuex/3.0.0/vuex.js"></script>
      <script src="https://cdn.bootcss.com/vue/2.5.16/vue.js"></script>
      <style>
        span{
          display:inline-block;
          width:200px;
        }
        div{
          margin: 20px;
        }
      </style>
    </head>
    <body>
      <div id="root">
        <my-child></my-child>
      </div>
    </body>
    </html>
    <script>
      Vue.component('my-child', {
        template: 
          `<div>
            <div><span>dataA:{{dataA}}</span><button @click='clickButton()'>普通函数改变dataA</button></div>
            <div><span>stateA:{{stateA}}</span><button @click='mutationsA(100)'>mutations改变stateA</button></div>
            <div><span>stateB:{{stateB}}</span><button @click='actionsC(2)'>actions改变stateB</button></div>
          </div>`,
        props: {},
        data: function () {
          return {
            dataA: 1,
          }
        },
        computed: {
          ...Vuex.mapState(['stateA','stateB']),
          //...Vuex.mapState({stateD:'stateC'}),
          //...Vuex.mapGetters(['getter0']),
        },
        methods: {
          ...Vuex.mapMutations([
            'mutationsA',
          ]),
          ...Vuex.mapActions([
            'actionsC'
          ]),
          clickButton() {
            this.dataA += 1;
          },
        },
      })
      const store = new Vuex.Store({
        state: {
          stateA: 100,
          stateB: 1,
        },
        getters: {
          getter0: (state, getters) => state.stateC//返回值可以为函数
        },
        mutations: {
          mutationsA: (state, num) => {
            state.stateA += num
          },
          mutationsB: (state, num) => {
            state.stateB = state.stateB*num
          }
        },
        actions: {
          actionsC: ({commit},num) => {
            setTimeout(function () {
              commit('mutationsB', num)
            }, 1000)
          }
        },
        modules: { }
      })
      new Vue({
        el: '#root',
        store,
      })
    </script>  
2、vuex4(对应vue3),
  (1)定义
    import { createStore } from 'vuex'
    import app from 'store/app.js'
    import getters from 'store/getters.js'
    export default createStore({
      modules: {
        app,
        user,
        permission
      },
      getters
    })
  (2)注入实例//main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import store from './store';
    const app = createApp(App);
    app.use(store);
    app.mount('#app'); //效果同app.mount(document.getElementById('app'))
  (3)获取和使用  
    <template>
      <div>
        <h2>{{ store.state.count }}</h2>
        <button @click="addCount">点击</button>
      </div>
    </template>
    import { computed, useStore } from "vuex";
    setup() {
      var store = useStore();
      var count = computed(() => store.state.count)
      var addCount = function() {
        console.log(store.state.count);
        store.commit("addCount");
        store.dispatch("addCount");
        store.commit('increment');
        store.dispatch('actionIn')
      };
      return { addCount };
    }
3、vuex的替代--pinia
  来源,https://pinia.web3doc.top/introduction.html
  附、简介
    A、2019年11月,pinia/piːnjʌ/出现
    B、同步和异步都用actions,没有mutations,没有modules嵌套结构,没有命名空间
    C、优势,热模块替换、支持服务端渲染
  (1)主要方法
    A、createPinia,执行结果配置到vue实例里
    B、defineStore,定义状态管理
      a、参数
        第1个参数,是一个唯一的字符串标识符,用于区分不同的store
        第2个参数,是一个配置对象,用于定义store的状态、actions和getters等
      b、返回值,可以接受store为参数
    C、storeToRefs,为pinia实例的属性创建引用
  (2)定义
    A、总状态,store/index.js,以下是该文件里的全部代码
      const store = createPinia()
      export default store
    B、分状态,store/storeA.js
      import { defineStore } from 'pinia'
      const useStoreA = defineStore('storeA', { //第一个参数用于唯一标识这个store
        state: () => ({
          count: 0,
        }),
        actions: {
          increment() {//不接收store参数
            this.count++;
          },
          someAction(store) {//接收store参数
            if (store.count > 5) {
              this.value++;
            }
          },
          getInfo() {
            return getUserInfo().then( 
              (res) => {
                SET_USER_INFO(this,data.loginUser)
                SET_MENU(this,data.menuTreeList)
                return Promise.reject('验证失败,请重新登陆!')
                return Promise.resolve(res)
                return getAAAInfo(res)
              },
              (error) => {
                return Promise.reject(error)
              }
            )
          },
        },
      });
  (3)注入
    A、vue2,
      import store from './store'
      new Vue({
        el: '#app',
        store,
      })
    B、vue3, 
      import { createApp } from 'vue'
      import App from './App.vue'
      import store from './store'
      const app = createApp(App)
      app.use(store)
  (4)获取和使用
    <template>
      <div>
        Count: {{ count }}
      </div>
    </template>
    <script setup>
      import store from './store'
      import useUserStore from '@/store/modules/user'
      let userStore = useUserStore(store);//如果store已经在main.js里注入到项目,那么此处可以不传参(传入参数)store
      userStore.getInfo().then(res =>{ })//使用方法
      const { count } = storeToRefs(userStore);//使用属性
      console.log( userStore.count );//使用属性
    </script>

八、vue-router(含权限,渲染位置<router-view>)
  来源,https://ezdoc.cn/docs/vue-router/advanced/navigation-failures
  附1、url传参对应router中的query配置
    window.open('#/create?domain=government&id=' + row.id)
    http://test.cctv.com/#/create?domain=government&id=666
1、从加载到登录
  (1)vue单文件页面,从首次加载到跳往登录页
    A、开发工具启动并打开浏览器
      a、运行npm run dev,开发工具(如Vite)启动服务器,并打开浏览器访问http://localhost:5173
      b、浏览器请求/,服务器返回index.html
    B、加载入口文件(index.html)
      a、浏览器解析index.html,加载其中的JS/CSS(如main.js)
    C、执行入口文件(main.js)
      a、初始化Vuex/Pinia(store)和Vue Router(router)
      b、如果项目中配置了权限控制(如@/permission.js),会在此处注册全局路由守卫(如router.beforeEach)
      c、创建Vue实例(new Vue()或createApp(App)),并挂载到<div id="app"></div>,注入状态管理和路由
    D、守卫触发
      a、Vue Router根据当前浏览器URL(如/)匹配对应的路由规则
      b、如果注册了全局路由守卫(如router.beforeEach),会触发守卫逻辑
      c、检查用户登录状态(如从Vuex/Pinia或localStorage读取token)
      d、如果未登录,调用next('/login')强制跳转到登录页
    E、跳转登录页
      a、URL更新为/login,Vue Router根据路由配置匹配Login.vue组件
      b、渲染Login.vue,用户填写表单并提交登录请求
      c、登录成功后,通常会在路由守卫中再次跳转到原目标页面(如/dashboard)
  (2)执行入口文件 (main.js)
    A、示例一
      import store from '@/store' //初始化 Vuex
      import router from './router' //初始化路由配置(但尚未注册守卫)
      import '@/permission' //★ 核心:执行权限文件,注册全局路由守卫
      new Vue({
        el: '#app',
        data: {},
        router, //注入路由实例
        store, //注入状态管理
        render: h => h(App) //渲染根组件 App.vue
      })  
    B、示例二
      import store from '@/store' //初始化 Vuex
      import router from './router' //初始化路由配置(但尚未注册守卫)
      import '@/permission' //核心:执行权限文件,注册全局路由守卫
      new Vue({
        router, //注入路由实例
        store, //注入状态管理
        render: h => h(App) //渲染根组件App.vue
      }).$mount('#app') //挂载到 DOM。 //效果同app.mount(document.getElementById('app'))
2、VueRouter实例
  (1)生成实例//router.js
    //注意,在80端口打开页面,有时成功有时失败,成功是因为80端口在其它项目中,执行过localStorage.setItem('token', 'xxx'),
    import VueRouter from 'vue-router';
    Vue.use(VueRouter);
    var constantRoutes = [
      { 
        path: '/', 
        name: 'HomePage' 
        component: HomePage, 
      },
      { 
        path: '/login',
        name: 'LoginPage',  
        component: LoginPage, 
      },
      {
        name: "首页",
        path: "/home",
        component: "main",
        url: null,
        children: [
          {
            name: "总编室总览",
            //路径匹配,
            path: "annotation",//根路径匹配/home,子路径匹配/annotation,属于嵌套路由
            path: "/home/annotation",//根路径匹配/home/annotation,不属于嵌套路由
            //路由配置既有path又有redirect,当访问path指定的路径时,自动使用redirect,渲染祖级到redirect的各层级组件
            component: "home/index",
            url: null
          }
        ]
      }
    ];
    const router = new VueRouter({
      mode: 'abstract',
      //A、hash模式,最常用
      //   a、依赖于浏览器提供的 hashchange 事件。当 URL 的 hash 值发生变化时,浏览器会触发这个事件
      //   b、Vue-router在初始化的时候会监听这个事件,然后根据当前的hash值去匹配对应的路由规则,并通过这种方式来改变视图
      // B、history模式,刷新浏览器时会出错,解决这个问题需要后台配合
      //   a、通过操作HTML5 History API来实现的,允许我们去创建一个更像原生应用的体验,没有hash(#)在URL中
      //   b、Vue-Router通过history.pushState来改变浏览器的地址栏而不重新加载页面
      //   c、当用户点击浏览器的前进后退按钮时,或者使用浏览器的地址栏进行导航时,可以监听popstate事件来处理路由的变化
      //     window.onpopstate = function(event) { //这是异步函数
      //       console.log( event.state, history.state, event.state == history.state, event, ); // true
      //     };
      // C、abstract模式,抽象。为了在服务端渲染(SSR)而设计的。
      //   a、返回一个用于服务端的虚拟的 router-view 组件
      //   b、服务端可以获取到对应路由的 html 内容,然后将其嵌入到最终的 HTML 中,从而实现服务端渲染
      routes: constantRoutes,
      scrollBehavior: function (to,from,savedPosition){//使用前端路由,切换到新页面时,滚到顶部还是保持上次的位置
        // “to”“from”和都是路由地址
        // “savedPosition”可以为空,如果没有的话
        return {x: 0, y: 0};//期望滚动到的位置
      }
    });
    export default router//导出实例
  (2)扩充实例,添加权限控制 //permission.js
    附、跳转到登录页的情形,1、未登录,2、登录过期(写在总请求里),3、权限不足(请用高权限账户登录)
    附、请求权限的情形,1、登录后,2、刷新浏览器后
    import router from './router'
    import useUserStore from '@/store/modules/user'
    router.beforeEach(async(to, from, next) => {
      //to,即将要进入的目标 路由对象
      //from,当前导航正要离开的路由
      //next,一定要调用该方法来resolve这个钩子,执行效果依赖next方法的调用参数
      NProgress.start();
      var isLogin = getLoginState();//从localStorage获取。会出现80端口设置过为true、81端口没设置过为false的情况
      var whiteList = ['/login']; 
      var useStore = useUserStore();
      /* 路由发生变化修改页面 title */
      if (to.meta.title) {
        document.title = to.meta.title //给title赋值
      }
      if(isLogin){//已登录
        if(useStore.permissionList.length === 0){
          //路过这里的情形:刷新浏览器,权限列表为空
          useStore.getInfo();//获取权限清单
        }
        if(useStore.permissionList.indexOf(to.path) > -1 || whiteList.indexOf(to.path) > -1){
          //路过这里的情形:首次打开页面,还没有获取权限列表
          next(from);
        }else{
          //路过这里的情形:在浏览器输入路由,不在权限列表,不在白名单(公共页面)
          //提示:没有权限,返回from
          next();
        }
      } else {//未登录
        if (whiteList.indexOf(to.path) > -1) {
          next();
        } else {
          next('/login');//next(`/login?redirect=${to.path}`)
        }
      }
      NProgress.done();
    });
  (3)无后台开发方案
    A、模拟数据层 //src/mock/index.js
      export default [
        {
          url: '/sjpt-platform-server/api/user/doLoginByPwd',
          method: 'post',
          timeout: 800,
          response: ({ body }) => {
            if (!body.username || !body.password) {
              return { code: 400, message: '用户名或密码不能为空' }
            }
            const isLoginSuccess = body.username === 'admin' && body.password === '123456';
            return isLoginSuccess ? {
              code: 200,
              data: {
                token: 'mock_token_123456',
                menuList: [1, 2, 3],
                producer: { brief: '人民日报' }
              },
              message: '登录成功'
            } : {
              code: 401,
              message: '用户名或密码错误'
            }
          }
        },
        {
          url: '/sjpt-platform-server/api/user/getSessionID',
          method: 'get',
          response: () => ({
            code: 200,
            data: '5f3b07ae779644bea97f594a4be37a53',
            message: '成功'
          })
        },
        //补充用户信息接口(供getInfo调用)
        {
          url: '/sjpt-platform-server/api/user/info',
          method: 'get',
          response: () => ({
            code: 200,
            data: {
              loginUser: { name: '管理员', role: 'admin' },
              menuTreeList: [/* 菜单路由结构 */]
            },
            message: '成功'
          })
        }
      ]
    B、Vite 配置 //vite.config.js
      export default defineConfig(({ command, mode }) => {
        return {
          plugins: [
            createVitePlugins(),
            viteMockServe({
              mockPath: "./src/mock/",
              localEnabled: true,  //开发环境开启
              prodEnabled: false,  //生产环境关闭
            }),
          ],
        }
      })
    C、状态管理 //src/vuex/index.js
      actions: {
        login(data) {
          console.log(data);
          SET_TOKEN(this, { "tokenName": "user_token", "tokenValue": data.token });
          setToken({ "tokenName": "user_token", "tokenValue": data.token });
          return Promise.resolve();
        },
        getInfo() {
          return getUserInfo()
            .then(res => {
              const { data } = res;
              SET_USER_INFO(this, data.loginUser);
              SET_MENU(this, data.menuTreeList);
              return { status: 3 }; //等价于resolve({status:3})
            });
        },
      },
      function SET_MENU(state, menuList) {
        state.allMenus = menuList;
        menuList.forEach(route => router.addRoute(route));
      }
    D、触发方法优化
      //src/login/login.vue 
      quickLogin() {
        useUserStore().getInfo().then(() => {
          router.push({ path: 'sys/role' });
        }).catch(err => {
          console.error('获取信息失败', err);
        });
      }
      //src/permission/permission.js
      router.beforeEach((to, from, next) => {
        useUserStore().getInfo()
          .then(() => {
            next({ path: 'sys/role' });
          })
          .catch(() => {
            next({ path: '/login' }); //失败时跳转登录页
          });
      });
3、vueRouter三大守卫
  (1)全局守卫-3个
    router.beforeEach(function(to, from, next) { 
      //在路由跳转发生前被调用
    });
    router.beforeResolve(function(to, from, next) { 
      //在导航被确认之前,且在所有组件内守卫和异步路由组件被解析之后被调用
    });
    router.afterEach(function(to, from) { 
      //在路由跳转完成后被调用
    });
    export default router
  (2)路由守卫-1个
    const routes = [
      //vue2中的this.$route,就是下面这样的对象
      {
        path: '/users/:id',
        component: UserDetails,
        meta: { title: '系统首页'},
        beforeEnter: (to, from) => {
          //reject the navigation
          return false
        },
      },
    ]
  (3)组件守卫-3个
    const UserDetails = {
      template: `...`, //name: 'YourComponent',
      beforeRouteEnter(to, from) {
        //在渲染该组件的对应路由被验证前调用
        //不能获取组件实例 `this` !
        //因为当守卫执行时,组件实例还没被创建!
      },
      beforeRouteUpdate(to, from) {
        //在当前路由改变,但是该组件被复用时调用
        //举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
        //由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
        //因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
      },
      beforeRouteLeave(to, from) {
        //在导航离开渲染该组件的对应路由时调用
        //与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
      },
    } 
4、页面传参(vue-router3及以前,渲染位置<router-view>)//“通信传值传参传数据”
  (1)隐式传参,name与params搭配,参数不会出现在url上,页面刷新、数据丢失
    A、在html中实现
      <router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
    B、在js中实现
      methods: {
        clickHand() {
          //以下是传参
          this.$router.push({
            name: 'user',
            params: { //参数不会出现在url路径上,
              thisId: 'id',
            }
          })
          //以下是接参
          this.$route.params.thisId
        }
      }
    C、在js中实现,vue-router任何版本,router与vuex搭配,适用于子传父,页面刷新,数据丢失
      //比如兄弟页面跳到这里,这里初始化数据后又修改数据,进入子页面后回退至这里,这里显示修改后的数据
      router.beforeEach((to, from, next) => {
        //to到这里,from兄弟页面时,初始化数据
      })
  (2)显式传参,path与query搭配,参数出现在url上(问号的后面),页面刷新、数据不丢失,
    A、在html中实现
      <router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link> 
    B、在js中实现
      methods: {
        clickHand() {
          //以下是传参
          this.$router.push({
            path: '/user',
            query: { //参数会出现在url路径上,
              thisId: 'id',
            },
          })
          //以下是接参
          this.$route.query.thisId
        }
      }
  (3)动态路由传参,参数出现在url上,页面刷新、数据不丢失,
    const constantRoutes = [
      {
        path: '/profile/set/:id',
        name: 'set',
        component: import('@/view/profile/set.vue')
      }
    ]
    <script>
      var clickImage = function(){
        this.$router.push({
          path: '/profile/set/2'
        })
      }
    </script>
    <script>
      console.log( this.$route.params.id );
    </script>
5、页面传参(vue-router4及以后,渲染位置<router-view>)//“通信传值传参传数据” 
   附、从2022-08-22发布的vue-router4.1.4开始,
     A、弃用name和params组合传参,
     B、启用path和state组合传参
   附、页面刷新
     A、数据丢失:name与params,vuex
     B、数据不丢失:path与query,path与state,localStorage
  (1)隐式传参,参数不会出现在url上
    A、在html中实现
      <router-link :to="{name: 'user', params:{id: '110'}}">点击查看子页面</router-link>
    B、在js中实现,vue-router4.1.4以前,name与params搭配,适用于父传子,页面刷新,数据丢失
      <script setup> 
        import { useRouter, useRoute } from 'vue-router'
        clickHand() {
          //以下是传参
          const userRouter = useRouter()
          userRouter.push({
            name: 'Home',
            params: {
              name: 'dx',
              age: 18
            }
          })
          //以下是接参
          const route = useRoute()
          console.log(route.params)
        }
      </script> 
    C、在js中实现,vue-router任何版本,router与vuex搭配,适用于子传父,页面刷新,数据丢失
      //比如兄弟页面跳到这里,这里初始化数据后又修改数据,进入子页面后回退至这里,这里显示修改后的数据
      router.beforeEach((to, from, next) => {
        //to到这里,from兄弟页面时,初始化数据
      }) 
    D、在js中实现,vue-router4.1.4及以后,path与state搭配,适用于父传子,页面刷新,数据“不会”丢失
      router.push({
        path: './program', 
        query: {
          sid: row.sid,
          name:row.topicName
        },
        state: {//获取数据,history.state
          filterClient : {...filterClient.value},
          filterServer : {...filterServer.value},
          page : {...page.value},
        }
      });
  (2)显式传参,path与query搭配,参数出现在url上(问号的后面),页面刷新、数据不丢失,
    A、在html中实现
      <router-link :to="{path: '/user', query:{id: '110'}}">点击查看子页面</router-link> 
    B、在js中实现
      <script setup> 
        import { useRouter, useRoute  } from 'vue-router' 
        const router = useRouter()
        //以下是传参
        const jumpDetail = (row) => {
          router.push({
            path: '/sys/column-rule/detail',
            query: {
              sid: row.sid,
              name: 'dx',
              age: 18,
            }
          })
        }
        //以下是接参
        const route = useRoute()
        console.log(route.query)
      </script>
  (3)动态路由传参,参数出现在url上,页面刷新、数据不丢失,
    const constantRoutes = [
      {
        path: '/profile/set/:id',
        name: 'set',
        component: import('@/view/profile/set.vue')
      }
    ]
    <script setup>
      import { useRouter } from "vue-router"
      const router = useRouter()
      var clickImage = function(){
        router.push({
          path: '/profile/set/2'
        })
      }
    </script>
    <script setup>
      import { onMounted } from 'vue'
      import { useRoute } from "vue-router"
      const useRoute = useRoute()
      const useRouter = useRouter()
      onMounted(function() {
        console.log(useRoute.params)
      });
    </script>

九、vue-cli
1、vue主要版本发布时间
 来源,https://github.com/vuejs/core/releases
   附、vue语法插件:Vetur
  (1)vue,1.0.0版,2015年10月27日
  (2)vue,2.0.0版,2016年10月01日
  (3)vue,3.0.0版,2020年01月04日,预发布
  (3)vue,3.0.0版,2020年09月18日,正式发布
2、vue3的7个优点/优势
  (1)体积小,按需编译,Tree shaking
  (2)性能快,diff算法
  (3)复合API,如setup,将逻辑相关的代码放在一起,有利于代码维护,Compostion API
  (4)渲染API,如weex、myvue,Custom Renderer API
  (5)多根组件,vue创建一个虚拟的Fragment节点
  (6)更好地支持TS
  (7)实时请求、实时编译,用vite开发构建工具编辑
3、vue-cli主要版本发布时间
  来源,https://github.com/vuejs/vue-cli/blob/dev/CHANGELOG.md
  (1)vue-cli,3.0.0版,2018年08月10日
  (2)vue-cli,4.0.0版,2019年10月16日
  (3)vue-cli,4.5.0版,2020年07月24日,开始默认使用vue3
  (4)vue-cli,5.0.0版,2022年02月17日
  (5)vue-cli,内部高度集成了webpack,
    A、项目根目录没有webpack.config.js文件,
    B、只有vue.config.js文件,@vue/cli-service会自动加载该文件,去修改默认的webpack配置
  (6)cli,命令行接口(Command Line Interface)
4、@vue/cli 是一个npm包,全局安装,提供了终端里的vue命令
  (1)vue create,快速搭建一个新项目
  (2)vue serve,构建新想法的原型
  (3)vue ui,通过一套图形化界面管理我的所有项目
  (4)vue --version,查看版本 
  (5)vue add XXX,用于在已创建的Vue项目中安装和调用Vue CLI插件
  (6)安装命令 npm install -g @vue/cli,
  (7)升级命令 npm update -g @vue/cli
  (8)安装位置 C:\Users\Haier\AppData\Roaming\npm\node_modules\@vue\cli
5、@vue/cli-service 是一个npm包,安装在每个@vue/cli创建的项目中
  (1)构建于 webpack 和 webpack-dev-server 之上
  (2)内部的 vue-cli-service 命令,提供 serve、build 和 inspect 命令
  (3)vue-cli-service serve,开启本地服务器,加载.env.development文件,把里面的键值对添加到process.env中
  (4)vue-cli-service build,打包压缩项目,加载.env.production文件,把里面的键值对添加到process.env中
  (5)vue-cli-service build --mode staging,项目测试,加载.env.staging文件,把里面的键值对添加到process.env中
6、引入插件
  (1)引入echarts:import * as echarts from 'echarts';
  (2)引入axios:import axios from 'axios';在plugins文件夹下进行配置
7、修改插件
  (1)用config.module.rule('svg').exclude.add(resolve('src/icons')).end()对vue-cli-5.0里内置的'svg'模块规则进行修改
  (2)用config.module.rule('icons').test(/\.svg$/).include.add(resolve('src/icons')).end()定义并向vue-cli-5.0里注入名为'icons'的模块规则
8、问题与解决
  (1)问题1
    现象:<router-view/>有下划红线
    问题:无法使用 JSX,除非提供了 "--jsx" 标志
    解决:在jsconfig.json(与package.json同级)里加"jsx": "preserve",
9、项目结构分析
  (1)node_modules:项目依赖文件夹
  (2)public:存放静态资源;webpack打包时,会将该文件夹中的静态资源原封不动地打包到dist文件夹中
    favicon.ion:图标
    index.html:模板,可通过webpack配置修改
  (3)src:源码目录
    assets:存放静态资源,webpack打包时,会把该文件夹中的静态资源当作一个模块,编译、打包到JS文件里面
    components:存放非页面组件。组件中的name属性应与文件名一样,方便后期维护;组件名使用大驼峰
    router:存放路由文件,主文件名为index.js
    store:存放Vuex文件,主文件名为index.js
    views:存放页面组件
    App.vue:根组件,是所有组件的父组件
    main.js:入口文件,是整个程序中最先执行的文件
  (4).gitignore:用于配置不需要被Git管理的文件(夹)
  (5)babel.config.js:babel的配置文件,用于处理ES语法的兼容问题
  (6)jsconfig.json:配置webpack的文件
  (7)package-lock.json:包版本控制文件。指定项目依赖包的版本号,保证其他人在执行npm i时下载的依赖包与原版本一致
  (8)package.json:应用包配置文件
  (9)postcss.config.js:配置(vue移动端自适应)。加载时机,在执行vue-cli里的开发或生产命令执行时,该文件在适当时机会被加载进去并执行--自悟。
  (10)README.md:项目注释
  (11).editorconfig,编辑器配置
    # 告诉EditorConfig插件,这是根文件,不用继续往上查找
    root = true
    # 匹配全部文件
    [*]
    # 设置字符集
    charset = utf-8
    # 缩进风格,可选space、tab
    indent_style = space
    # 缩进的空格数
    indent_size = 2
    # 结尾换行符,可选lf、cr、crlf
    end_of_line = lf
    # 在文件结尾插入新行
    insert_final_newline = true
    # 删除一行中的前后空格
    trim_trailing_whitespace = true
    # 匹配md结尾的文件
    [*.md]
    insert_final_newline = false
    trim_trailing_whitespace = false
  (12).env,环境配置,开发、生产环境均有效
  (13).env.development,开发环境配置
  (14).env.production,生产环境配置
  (15).env.staging,测试环境配置
  (16).eslintignore,忽略eslint检查的文件列表,
  (17).eslintrc.js,eslint检查规则,
  (18).gitignore,忽略git上传文件列表,
   
十、mock拦截请求|模拟数据
1、说明
  (1)原理:通过覆盖和模拟原生XMLHttpRequest来拦截Ajax请求
  (2)拦截:拦截成功或失败,都不发出请求,network里无记录
  (3)参数:Mock.mock(rurl?,rtype?,template|function(options))
    来源,https://www.jianshu.com/p/b5c58ae144d9
    来源,https://blog.csdn.net/qq_51357960/article/details/127285388
    A、Mock.mock(template),根据模板,生成数据
    B、Mock.mock(rurl,template),监听路由,根据模板,生成数据
    C、Mock.mock(rurl,function(options)),监听路由,根据函数返回值,生成数据
    D、Mock.mock(rurl,rtype,template),监听路由,根据请求方式、模板,生成数据
    E、Mock.mock(rurl,rtype,function(options)),监听路由,根据请求方式、函数返回值,生成数据
    F、参数说明
      a、rurl:表示要拦截的接口,可以是绝对地址或者正则表达式
      b、rtype:表示请求类型 get/post 等等
      c、template|function(options):生成数据的模板
2、Mock.mock只有一个对象参数时的用法
  来源,http://mockjs.com/examples.html
  说明,对象参数就是数据模板,包含数据占位符
  <!DOCTYPE html>
  <html lang='en'>
    <head>
      <meta charset='UTF-8'>
      <meta name='viewport' content='width=device-width, initial-scale=1.0'>
      <title>数据展示</title>
      <style>
        span{
          color:red;
          font-size: 16px;
        }
        .fontSize16{
          font-size: 16px;
        }
        .result{
          margin-left: 40px;
          padding: 40px;
          font-size: 12px;
          background: rgb(231, 228, 228);
          max-width: 400px;
          white-space: pre-wrap;
        }
      </style>
      <script src='https://cdn.jsdelivr.net/npm/mockjs@1.1.0/dist/mock.min.js'></script>
    </head>
    <body>
      <pre>
        <div class="fontSize16">以下来源,http://mockjs.com/examples.html</div>
        <div style="display: flex;">
          <div class="fontSize16">
            //以下<span>数据占位符</span>
            //以下基本数据
            '@boolean'
            '@natural(min, max)'
            '@integer(min, max)'
            '@range(start, stop, step)'
            //上面,返回由数字构成的数组, 
            '@float(min,max, dmin, dmax)'
            //上面,dmin(小数最少位数), 
            //上面,dmax(小数最多位数)
            '@string'
            //以下日期时间
            '@date'
            '@time'
            '@datetime'
            '@now'
            //以下网络
            '@domain'
            '@email'
            '@guid'
            '@id'
            '@ip'
            '@url'
          </div>
          <div class="fontSize16">
            //以下<span>数据占位符</span>
            //以下英文数据
            '@paragraph'
            '@sentence'
            '@word'
            '@title'
            '@first'
            '@last'
            '@name'
            //以下中文数据
            '@cparagraph'
            '@csentence'
            '@cword'
            '@ctitle'
            '@cfirst'
            '@clast'
            '@cname'
            '@region'
            '@province'
            '@city'
            '@county'
          </div>
          <div class="fontSize16">
            //以下<span>数据模板</span>
            //以下是data的定义
            var data= Mock.mock({
              'aaa': 200,
              'bbb': /\d{4,7}/, 
              'ccc': '@boolean',
              'ddd|2': [{
                'title': '@cword(2, 4)',
                'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
              }],
              'eee|2': {
                '省级1': '@province',
                '省级2': '浙江省',
                '省级3': {
                  '河北省': '@city',
                  '河南省': {
                    '信阳市': {
                      '固始县': ['A',[1,2],'B',['3','4']],
                      //把数组某项换成'省级3'对象,看效果
                    }
                  }
                }
              },
              'fff|2-5': '★',
            });
            <span>'ddd|2': 对象、数组、字符串,由2项构成</span>
            <span>数据模板-包含-数据占位符;刷新页面,可以更新右图数据</span>
            <span>2种JS方案-展示data,1、直接展示,2、格式化展示</span>
            <span>JSON格式在线验证,https://www.bejson.com/</span>
          </div>
          <div id='div' class="result"></div>
        </div>
      </pre>
    </body>
  </html>
  <script>
    var data= Mock.mock({
      'aaa': 200,
      'bbb': /\d{4,7}/, 
      'ccc': '@boolean',
      'ddd|2': [{
        'title': '@cword(2, 4)',
        'status|1': ['成功', '失败', '其它-@cword(1, 2)'],
      }],
      'eee|2': {
        '省级1': '@province',
        '省级2': '浙江省',
        '省级3': {
          '河北省': '@city',
          '河南省': {
            '信阳市': {
              '固始县': ['A',[1,2],'B',['3','4']],
              //把数组某项换成'省级3'对象,看效果
            }
          }
        }
      },
      'fff|2-5': '★',
    });
    //以下2种展示方式
    //1、以下直接展示
    //document.getElementById('div').innerText = JSON.stringify(data);
    //2、以下格式化展示
    function isArray(value) { return {}.toString.call(value) === "[object Array]"; }
    function isObject(value) { return {}.toString.call(value) === "[object Object]"; }
    function addLine(num) {//添加换行
      var str ='\n';
      for(var i = 0; i < num; i++) str +='\n'
      return str;
    }
    function addSpace(num) {//添加空格
      var str ='\xa0';
      for(var i = 0; i < num; i++) str +='\xa0'
      return str;
    }
    function addStr(data, num, isAddDot) {//添加字符串
      var str = '';
      if (isObject(data)) {
        str += '{' + addLine(1);
        var i = 0;
        var strIn = ',';
        var length = Object.keys(data).length;
        for(var attr in data){
          var value = data[attr];
          i++;
          if(i == length) strIn = '';
          str += addSpace(4*num) + JSON.stringify(attr) + ":" + addStr(value, num+1) + strIn + addLine(1);
        }
        str +=  addSpace(4*(num-1)) + '}';
        if(isAddDot) str += ',';
      } else if (isArray(data)) {
        str += '[';
        for(var i=0;i<data.length;i++){
          var value = data[i];
          var isHasDot = i<data.length-1;
          str += addStr(value, num, isHasDot);
        }
        str += ']';
        if(isAddDot) str += ',';
      } else {
        str += JSON.stringify(data);
        if(isAddDot) str += ',';
      }
      return str;
    }
    document.getElementById('div').innerText = addStr(data, 1);
  </script>                      
3、示例,案例来源,online-class-manage
  (1)添加依赖,package.json
    {
      "name": "vue-admin-template",
      "scripts": {
        "dev": "vue-cli-service serve",
      },
      "dependencies": {
        "axios": "0.18.1",
        "vuex": "3.1.0"
      },
      "devDependencies": {
        "mockjs": "1.0.1-beta3",
      },
    }
  (2)使用依赖,配置模拟,require
    A、定义分散的路由与返回值
      ./mock/table.js
      const Mock = require('mockjs')
      const data = Mock.mock({
        'items|30': [{
          id: '@id', //数据占位符
          title: '@sentence(10, 20)',
          'status|1': ['published', 'draft', 'deleted'],
          author: 'name',
          display_time: '@datetime',
          pageviews: '@integer(300, 5000)'
        }]
      })
      module.exports = [
        {
          url: '/vue-admin-template/table/list',
          type: 'get',
          response: config => {
            const items = data.items
            return {
              code: 20000,
              data: {
                total: items.length,
                items: items
              }
            }
          }
        }
      ]
    B、合并分散的路由与返回值,在生产环境中使用模拟数据
      ./mock/index.js 
      const Mock = require('mockjs') //覆盖和模拟原生XMLHttpRequest
      const { param2Obj } = require('./utils')
      const user = require('./user.js')
      const table = require('./table.js')
      const mock_ = [...user,...table] //合并分散的路由与返回值
      function mockXHR() { //在生产环境中使用模拟数据
        for (const i of mock_) { 
          Mock.mock(new RegExp(i.url), i.type || 'get', i.response) //直接传入i.response,似乎也行
        }
      }
      //以下,导出数据
      module.exports = {
        mock_, 
        mockXHR 
      }
    C、把每个定义的路由与返回值绑定
      ./mock/mock-server.js, 
      const chokidar = require('chokidar')
      const bodyParser = require('body-parser')
      const chalk = require('chalk')
      const path = require('path')
      const Mock = require('mockjs') 
      const { mock_ } = require('./mock/index.js') 
      const mockDir = path.join(process.cwd(), 'mock') //process.cwd(),获取当前工作目录
      function registerRoutes(app) { 
        let mockLastIndex
        const mocksForServer = mock_.map(route => {
          return {
            url: new RegExp(`${process.env.VUE_APP_BASE_API}${route.url}`),
            //console.log(new RegExp("ab+c", "i"));///ab+c/i
            //console.log(new RegExp(/ab+c/, "i"));///ab+c/i
            type: route.type || 'get',
            response: function(req, res) { 
              res.json(Mock.mock(route.response instanceof Function ? route.respond(req, res) : route.response))
            }
          }
        })
        for (const mock of mocksForServer) {
          //----在node里,拦截并重新定义ajax请求,url可以用正则 
          app[mock.type](mock.url, mock.response) 
          mockLastIndex = app._router.stack.length
        }
        const mockRoutesLength = Object.keys(mocksForServer).length
        return {
          mockRoutesLength: mockRoutesLength,
          //自己开始的索引:最后索引 - 路由长度;避免后来多删
          mockStartIndex: mockLastIndex - mockRoutesLength 
        }
      }
      function clearModuleCache() { //项目原名,unregisterRoutes
        Object.keys(require.cache).forEach(i => {
          if (i.includes(mockDir)) {
            delete require.cache[require.resolve(i)]//require.cache:引入的模块将被缓存在这个对象中 
          }
        })
      }
      module.exports = function(app){
        app.use(bodyParser.json()) //解析请求体中json数据
        app.use(bodyParser.urlencoded({ //解析请求体中form表单数据
          extended: true
        }))
        const mockRoutes = registerRoutes(app)
        var mockRoutesLength = mockRoutes.mockRoutesLength
        var mockStartIndex = mockRoutes.mockStartIndex
        chokidar.watch(mockDir, { //初始化时,监听mock目录的变化
          ignored: /mock-server/,
          ignoreInitial: true
        }).on('all', (event, path) => { //更新时,执行下列代码
          if (event === 'change' || event === 'add') {
            try {
              clearModuleCache(); //1/3--这里清除“模块缓存”,下面会再次执行require('./mock/index.js'),更新“模块缓存”
              app._router.stack.splice(mockStartIndex, mockRoutesLength) //2/3--清除旧的“路由栈”,以免与新的重复
              const mockRoutes = registerRoutes(app); //3/3--生成新的“路由栈”,存储匹配关系
              mockRoutesLength = mockRoutes.mockRoutesLength;
              mockStartIndex = mockRoutes.mockStartIndex;
              console.log(chalk.magentaBright(`\n > Mock Server hot reload success! changed  ${path}`))
            } catch (error) {
              console.log(chalk.redBright(error))
            }
          }
        })
      }
  (3)使用模拟,require
    A、开发环境中注入//vue.config.js
      module.exports = {
        devServer: { //开发环境中
          before: require('./mock/mock-server.js') //添加新模拟后,自动更新 
          before: function(app, server) { //添加新模拟后,手动更新 
            //生成新的“路由栈”,存储匹配关系
            app.get('/some/path', function(req, res) { 
              res.json({ custom: 'response' });;
            });
          }
        }
      };
    B、生产环境中注入模拟数据//main.js 
      if (process.env.NODE_ENV === 'production') { //在生产环境中使用模拟数据,上线之前,请移除
        const { mockXHR } = require('./mock/index.js')
        mockXHR()
      }

  

posted @ 2020-07-28 17:49  WEB前端工程师_钱成  阅读(9183)  评论(0)    收藏  举报