Skip to content

Vue开发文档

    • 一套用于构建用户界面的渐进式 JavaScript框架;
    • 基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型;
    • 渐进式框架:
      • 在项目中一点点来引入和使用Vue,而不一定需要全部使用Vue来开发整个项目;
    • 更好的性能;
    • 更小的包体积; (Tree shaking)
      • 按需打包,只有用到的api才会打包,很大程度地减少了开发中的冗余代码,提升编译速度。
    • 更好的TypeScript集成;
    • 更优秀的API设计:
      • Composition函数式开发,很大程度地提高为了组件、业务逻辑的复用性;高度解耦,提高代码质量、开发效率;减少代码体积。
  • 原生开发和Vue开发的模式和特点,两种不同的编程范式:
    • 原生是命令式编程,命令“机器”如何去做事情 ;
      • 原生JavaScript和jQuery开发;
    • Vue是声明式编程,告诉机器你想要的是什么,让机器来完成过程;
      • Vue、React、Angular、小程序的编程模式;
  • MVVM 是一种设计思想,将数据双向绑定作为核心思想。

    • Model:数据层,存储数据及对数据的处理;
    • View:视图层,主要负责数据的展示;
    • ViewModel:业务逻辑层,是一个同步 View 和 Model 的对象,进行数据与 DOM 的绑定。
  • data属性是一个函数,并且该函数需要返回一个对象;

  • data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理;

    • 用来定义方法;
    • 方法可以被绑定到模板中;
    • 在方法中,可以使用this关键字来直接访问到data中返回的对象的属性;
    • Vue的源码当中就是对methods中的所有函数进行了遍历,并且使用bind函数将函数的this指向Vue实例上,若使用箭头函数,箭头函数会往上层作用域找this,所以this将不会按照期望指向组件实例;
  • 把数据显示到模板(template)中;
    Section titled “把数据显示到模板(template)中;”
    • data返回的对象是有添加到Vue的响应式系统中;
    • 当data中的数据发生改变时,对应的内容也会发生更新;
    • 插值语法可以是data方法返回对象的属性、JavaScript的表达式、三元表达式、调用methods中的函数,computed的属性等;
      • 注:不能定义语句(如使用const、if等)
  • 如果Vue.createApp方法传入的对象中没有template属性,那么会默认使用id为app的元素作为模板;

    • <h2 v-once>why</h2>
    • 用于指定元素或者组件只渲染一次;

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

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

    • <h2 v-text="message"></h2>
    • 用于更新元素的 textContent;

    • 注:

      • 跟插值语法类似,不过插值语法可以插入到原有的文本中,而v-text会把原有的文本给覆盖;
      • 值为表达式;
    • <h2 v-html="content"></h2>
    • 把html字符串作为html来解析;

    • 注:

      • 插值语法v-text中,html字符串只会被当成文本来编译;
      • 也会覆盖元素原有的内容;
      • 值为表达式;
    • <div v-pre>{{ message }}</div>
    • 用于跳过元素和它的子元素的编译;

      • 不会编译元素中的插值语法;
      • 跳过不需要编译的节点,加快编译的速度;
    • 保持在元素上直到关联组件实例编译结束。

    • 和 CSS 规则一起用;

      • 如和 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的插值语法标签直到组件实例准备完毕。

      • [v-cloak] {
        display: none;
        }
        <div id="app">
        <h2 v-cloak>{{message}}</h2>
        </div>
        //避免把未编译的插值语法标签显式出来;
    • 该指令接收一个固定长度的数组作为依赖值进行记忆比对。如果数组中的每个值都和上次渲染的时候相同,则整个该子树的更新会被跳过。

      • <div v-memo="[name, age]">
        <h2>{{ name }}</h2>
        <h2>{{ age }}</h2>
        <h2>{{ height }}</h2>
        </div>
      • 如data返回对象中只有name属性添加到如上的数组中,如果name属性值没变化,即使age有变化也不会再次被渲染;

    • <h2 :style="{ color: fontColor, fontSize: fontSize + 'px' }">哈哈哈哈</h2>
    • 属性名可以用驼峰式或短横线分隔 (用引号括起来)来命名;

    • 对象里的属性为css属性;

    • 若要往css属性插入响应式数据,需要使用对象语法。

      • <h2 :style="color: redcolor" > //报错,错误写法;
    • key的类型默认是字符串,若key要使用响应式数据,则要使用[]

    • 可以将多个样式对象应用到同一个元素上;

      • <h2 :style="[objStyle, { backgroundColor: 'purple' }]">嘿嘿嘿嘿</h2>
        data: function() {
        return {
        objStyle: {
        fontSize: '50px',
        color: "green"
        }
        }
        },
    • 数组元素为字符串写法:

      • <h2 :style="['color: red; fontSize: 50px']">{{message}}</h2>
  • 在某些情况下,属性的名称可能也不是固定的:

    • 可以使用:[属性名]=“值” 的格式来定义;

    • <h2 :[name]="message">Hello World</h2>
  • 将一个对象的所有属性,绑定到元素上作为元素的属性;

    • 使用 v-bind 绑定一个对象;

    • <h2 :="infos">Hello Bind</h2>
      data() {
      return {
      infos: { name: "why", age: 18, height: 1.88, address: "广州市" },
      }
      }
  • 对于任何包含响应式数据的复杂逻辑,都应该使用计算属性;
    Section titled “对于任何包含响应式数据的复杂逻辑,都应该使用计算属性;”
    • <h2>{{ fullname }}</h2>
      const app = Vue.createApp({
      data() {
      return {
      firstName: "kobe",
      lastName: "bryant",
      }
      },
      computed: {
      // 1.计算属性默认对应的是一个函数,相当于对象写法的getter,只能读不能写;
      fullname() {
      return this.firstName + " " + this.lastName
      }
      // 完整写法:(了解)
      //在大多数情况下,只需要一个getter方法即可
      fullname: {
      get: function() {
      return this.firstname + " " + this.lastname
      },
      set: function(value) {
      const names = value.split(" ")
      this.firstname = names[0]
      this.lastname = names[1]
      }
      }
      },
      methods: {
      setFullname() {
      this.fullname = "kobe bryant"
      }
      }
      })
  • 用于在代码逻辑中监听某个数据的变化;
    Section titled “用于在代码逻辑中监听某个数据的变化;”
    • <button @click="changeMessage">修改message</button>
      const app = Vue.createApp({
      // data: option api
      data() {
      return {
      message: "Hello Vue",
      info: { name: "why", age: 18 }
      }
      },
      methods: {
      changeMessage() {
      this.message = "你好啊, 李银河!"
      this.info = { name: "kobe" }
      }
      },
      watch: {
      // 1.默认有两个参数: newValue/oldValue
      message(newValue, oldValue) {
      console.log(newValue, oldValue)
      //你好啊, 李银河! Hello Vue
      },
      info(newValue, oldValue) {
      // 2.如果是对象类型, 那么拿到的是代理对象
      console.log( newValue, oldValue)
      //Proxy {name: 'kobe'} Proxy {name: 'why', age: 18}
      // 3.获取原生对象
      // console.log({ ...newValue })
      console.log(Vue.toRaw(newValue))
      }
      }
      })
    • deep选项:

      • 默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应;
      • 可以使用选项deep进行深层侦听;
    • immediate选项:

      • 一开始的就会立即执行一次,无论数据是否有变化;
    • watch: {
      // 进行深度监听
      info: {
      handler(newValue, oldValue) {
      console.log("侦听到info改变:", newValue, oldValue)
      },
      // 监听器选项:
      // info进行深度监听
      deep: true,
      // 第一次渲染直接执行一次监听器
      immediate: true
      },
      }
    • watch: {
      "info.name"(newValue, oldValue) {
      console.log("name发生改变:", newValue, oldValue)
      }
      }
    • // 生命周期回调函数: 当前的组件被创建时自动执行
      created() {
      this.$watch("message", (newValue, oldValue) => {
      console.log("message数据变化:", newValue, oldValue)
      }, { deep: true, immediate: true })
      }
  1. 支持异步操作,计算属性不支持异步操作
  2. 计算属性有缓存,且不能修改
  3. 一个是监听值的变化,一个是根据收集依赖的变化才变化
  • <div id="app">
    <template v-if="books.length">
    <table>
    <tr>
    <th>序号</th>
    <th>书籍名称</th>
    <th>出版日期</th>
    <th>价格</th>
    <th>购买数量</th>
    <th>操作</th>
    </tr>
    //toggol形式给选中的tr添加背景,其他则清除背景;(:class)
    <tr v-for="(item, index) in books" :key="item.id" @click="changeCss(index)" :class="{active: currentIndex === index}">
    <td>{{ index }}</td>
    <td>{{ item.name }}</td>
    <td>{{ item.date }}</td>
    <td>{{ formatData(item.price) }}</td>
    <td>
    <button :disabled="item.count <= 1" @click="item.count--">-</button>
    {{item.count}}
    <button @click="item.count++">+</button>
    </td>
    <td>
    <button @click.stop="deleteData(index)">移除</button>
    </td>
    </tr>
    </table>
    <h2>总价:{{formatData(total)}}</h2>
    </template>
    <template v-else>
    <div>无数据</div>
    </template>
    </div>
    <script src="../lib/vue.js"></script>
    <script src="./data/data.js"></script>
    <script>
    const app = Vue.createApp({
    data() {
    return {
    books,
    // 切换背景使用的变量
    currentIndex: -1
    }
    },
    computed: {
    total() {
    return this.books.reduce((preValue, item) => {
    return preValue += item.price * item.count
    }, 0)
    }
    },
    methods: {
    formatData(price) {
    return "" + price
    },
    deleteData(index) {
    this.books.splice(index, 1)
    },
    changeCss(index) {
    this.currentIndex = index
    }
    }
    })
    app.mount("#app")
    </script>
  • <textarea cols="30" rows="10" v-model="content"></textarea>
    • v-model即为布尔值。

    • 此时input的value属性并不影响v-model的值。

    • <label for="agree">
      <input id="agree" type="checkbox" v-model="isAgree"> 同意协议
      </label>
      data() {
      return {
      isAgree: true
      }
      }
    • 当是多个复选框时,因为可以选中多个,所以v-model绑定的响应式数据是一个数组。

    • 当选中某一个时,就会将input的value添加到数组中。

    • <div class="hobbies">
      <h2>请选择你的爱好:</h2>
      <label for="sing">
      <input id="sing" type="checkbox" v-model="hobbies" value="sing">
      </label>
      <label for="jump">
      <input id="jump" type="checkbox" v-model="hobbies" value="jump">
      </label>
      <h2>爱好: {{hobbies}}</h2> //[ "sing", "jump" ]
      </div>
      data() {
      return {
      hobbies: []
      }
      }
    • 是否受value的影响,是根据v-model绑定的响应式数据的类型来决定的;
    • 单勾选框的v-model也能使用数组来获取选中的勾选框的value值;
    • 多个相关联的checkbox正常来说需要使用相同的name属性关联起来,但是v-model已经对其进行了处理,因此不需要设置name属性;
  • 当选中某一个时,就会将input的value赋值给v-model绑定的响应式数据。

  • <div class="gender">
    <label for="male">
    <input id="male" type="radio" v-model="gender" value="male">
    </label>
    <label for="female">
    <input id="female" type="radio" v-model="gender" value="female">
    </label>
    //多个input的v-model绑定同一个响应式数据时也就代表以往所需设置的name相同;
    <h2>性别: {{gender}}</h2>
    </div>
    data() {
    return {
    gender: "female" //对应一个字符串
    }
    },
  • select也分单选和多选两种情况:
    Section titled “select也分单选和多选两种情况:”
      • v-model绑定的是一个值;

      • 当选中option中的一个时,会将它对应的value赋值到v-model对应的响应式数据中;

      • <select v-model="fruit">
        <option value="apple">苹果</option>
        <option value="orange">橘子</option>
        <option value="banana">香蕉</option>
        </select>
        <h2>单选: {{fruit}}</h2>
        data() {
        return {
        fruit: "orange"
        }
        },
      • v-model绑定的是一个数组;

      • 当选中多个值时,就会将选中的option的value值添加到数组中;

      • <select multiple size="3" v-model="fruits">
        <option value="apple">苹果</option>
        <option value="orange">橘子</option>
        <option value="banana">香蕉</option>
        </select>
        <h2>多选: {{fruits}}</h2>
        data() {
        return {
        fruits: []
        }
        },
  • 将请求到的数据绑定到data返回的对象中,再通过v-bind来进行值的绑定,这个过程就是值绑定。
    • 默认情况下,v-model在进行双向绑定时,绑定的是input事件,那么会在每次内容输入后就将最新的值和绑定的响应式数据进行同步;

    • 如果在v-model后跟上lazy修饰符,那么会将绑定的事件切换为 change 事件,只有在提交时(比如回车)或失去焦点时才会触发;

    • <input type="text" v-model.lazy="message">
    • 将内容转换成数字类型;

    • <input type="text" v-model.number="counter">
    • 去除首尾的空格;

    • <input type="text" v-model.trim="content">
    • <input type="text" v-model.lazy.trim="content">
    • createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是应用程序的根组件;
    • 组件化提供了一种抽象,使得可以开发出一个个独立可复用的小组件来构造应用;
    • 任何的应用都会被抽象成一颗组件树;
    • 使用全局创建的app对象component方法来注册组件;
    • component方法传入组件名称、组件对象即可注册一个全局组件了,组件本身也可以有自己的代码逻辑;
    • 可以在任何组件的template中使用这个全局组件;
    • 类似于webpack这种打包工具在打包项目时,对于没有使用到的全局组件依然会对其进行打包;
    • <div id="app">
      <!-- <home-nav></home-nav> -->
      <HomeNav></HomeNav> // 在vue脚手架中可以使用,普通运行不了
      <home-nav></home-nav>
      <product-item></product-item>
      <product-item></product-item>
      <product-item></product-item>
      </div>
      <template id="nav">
      <h2>我是应用程序的导航</h2>
      </template>
      <template id="product">
      <div class="product">
      <h2>{{title}}</h2>
      <p>商品描述, 限时折扣, 赶紧抢购</p>
      <p>价格: {{price}}</p>
      <button @click="favarItem">收藏</button>
      </div>
      </template>
      <script src="../lib/vue.js"></script>
      <script>
      // 1.创建app
      const app = Vue.createApp({
      // data: option api
      data() {
      return {
      message: "Hello Vue"
      }
      },
      })
      // 2.注册全局组件
      app.component("product-item", {
      template: "#product",
      data() {
      return {
      title: "我是商品Item",
      price: 9.9
      }
      },
      methods: {
      favarItem() {
      console.log("收藏了当前的item")
      }
      }
      })
      app.component("HomeNav", {
      template: "#nav"
      })
      // 2.挂载app
      app.mount("#app")
      </script>

1.2.2、注册局部组件(开发中使用较多)

Section titled “1.2.2、注册局部组件(开发中使用较多)”
    • 通过components属性选项来进行注册;
    • 该components选项对应的是一个对象,对象中的键值对是组件的名称: 组件对象;
    • <div id="app">
      <product-item></product-item>
      </div>
      <template id="product">
      <div class="product">
      <h2>{{title}}</h2>
      <p>商品描述, 限时折扣, 赶紧抢购</p>
      <p>价格: {{price}}</p>
      <button>收藏</button>
      </div>
      </template>
      <script src="../lib/vue.js"></script>
      <script>
      // 1.1.组件打算在哪里被使用
      const app = Vue.createApp({
      // components: option api
      components: {
      ProductItem: {
      template: "#product",
      data() {
      return {
      title: "我是product的title",
      price: 9.9
      }
      }
      }
      },
      // data: option api
      data() {
      return {
      message: "Hello Vue"
      }
      }
      })
      // 2.挂载app
      app.mount("#app")
      </script>

1.2.3、定义组件名的方式有两种:

Section titled “1.2.3、定义组件名的方式有两种:”
  • 短横线分割符:kebab-case
  • 大驼峰:PascalCase
  • 在使用vue-cli创建的项目的main.js文件里:
    • 使用import {createApp} from "vue"引入的vue是有runtime,是会解析处理.vue文件的,里面已经配置好vue-loader用来解析.vue文件的template元素;
    • 若在main.js文件里在组件对象里使用template属性,main.js不是.vue文件,只有runtime是不会对template属性进行处理,所以需要导入import {createApp} from "vue/dist/vue.esm-bundler" ,其内部具有runtime和compile,compile用于解析处理模板template属性;
    • 通过props来完成组件之间的通信;
    • Props是在父组件上注册的一些自定义的attribute;
    • 父组件给这些attribute赋值,子组件通过attribute的名称获取到对应的值;
    • 字符串元素的数组,数组中的字符串就是attribute的名称;
    • 对象类型,对象类型可以在指定attribute名称的同时,指定它需要传递的类型、是否必须传、默认值等等;(必须和默认值二选一)
    • App.vue(父组件):

      • <show-info name="kobe" :age="30" />
    • ShowInfo.vue(子组件):

      • export default {
        // 作用: 接收父组件传递过来的属性
        // 1.props数组语法
        // 弊端: 1> 不能对类型进行验证 2.没有默认值的
        props: ["name", "age", "height"]
        // 2.props对象语法(必须掌握)
        props: {
        name: {
        type: String,
        default: "我是默认name" // 默认值
        },
        age: {
        type: Number,
        required: true // 必须传递
        }
        // 重要的原则: 对象类型写默认值时, 需要编写default的函数, 函数返回默认值;(数组一样)
        friend: {
        type: Object,
        default: () => ({ name: "james" })
        }
        }
        // 3.补:这种写法也可以
        props: {
        name: String
        }
        }
    • attribute 名是大小写不敏感;
    • 驼峰命名法等价于短横线分隔命名;
    • 当传递给一个组件某个属性,但是该属性并没有定义对应的props或者emits时,就称之为非Prop的Attribute
    • 当组件有单个根节点时,非Prop的Attribute将自动添加到根节点的Attribute中;
    • 如果不希望组件的根元素继承attribute,可以在组件中设置:

      • export default {
        inheritAttrs: false
        }
    • 可以通过$attrs来访问所有的非props的attribute

      • <div class="others" v-bind="$attrs"></div>
    • 多个根节点的attribute如果没有显示的绑定,那么会报警告,所以必须指定其中一个节点:

    姓名: {{ name }}

    年龄: {{ age }}

    身高: {{ height }}

    ```
    • 在子组件中定义好在某些情况下触发的事件名称,同时可传入参数;
    • 在父组件中以v-on的方式传入要监听的事件名称,并且绑定到对应的方法中;
    • 在子组件中发生某个事件的时候,父组件根据事件名称触发对应的事件,同时获取到参数;
    • 子组件:

      • this.$emit("add", 100)
    • 父组件:

      • <add-counter @add="addBtnClick"></add-counter>
        addBtnClick(count) { //count = 100
        this.counter += count
        },
    • 用来定义一个组件可以向其父组件触发的事件。

    • 便于查看组件有哪些自定义事件;

    • export default {
      // 1.emits数组语法
      emits: ["add"]
      // 2.emits对象语法,以对传递的参数进行验证
      emits: {
      add: function(count) {
      if (count <= 10) {
      return true
      }
      return false
      }
      }
      }
    • <template>
      <div class="app">
      <!-- 1.tab-control -->
      <tab-control :titles="['衣服', '鞋子', '裤子']" @tab-item-click="tabItemClick"/>
      <!-- 2.展示内容 -->
      <h1>{{ pageContents[currentIndex] }}</h1>
      </div>
      </template>
      <script>
      import TabControl from './TabControl.vue'
      export default {
      components: {
      TabControl
      },
      data() {
      return {
      pageContents: [ "衣服列表", "鞋子列表", "裤子列表" ],
      currentIndex: 0
      }
      },
      methods: {
      tabItemClick(index) {
      this.currentIndex = index
      }
      }
      }
      </script>
      <style scoped>
      </style>
  • 子组件:(TabControl.vue)

    • <template>
      <div class="tab-control">
      <template v-for="(item, index) in titles" :key="item">
      <div class="tab-control-item"
      :class="{ active: index === currentIndex }"
      @click="itemClick(index)">
      <span>{{ item }}</span>
      </div>
      </template>
      </div>
      </template>
      <script>
      export default {
      props: {
      titles: {
      type: Array,
      default: () => []
      }
      },
      data() {
      return {
      currentIndex: 0
      }
      },
      emits: ["tabItemClick"],
      methods: {
      itemClick(index) {
      this.currentIndex = index
      this.$emit("tabItemClick", index)
      }
      }
      }
      </script>

八、Vue组件化 - 插槽Slot/非父子通信

Section titled “八、Vue组件化 - 插槽Slot/非父子通信”
    • 让组件具备更强的通用性;
    • 自主决定使用导入组件的某一块区域存放的内容和元素;
    • 抽取共性、预留不同;
    • 共同的元素、内容依然在组件内进行封装;
    • 将不同的元素使用slot作为占位,插槽插入什么内容取决于父组件;
    • 父组件:(App.vue)

      • <show-message>
        <button>我是按钮元素</button>
        </show-message>
    • 子组件:(ShowMessage.vue)

      • <template>
        <slot></slot>
        // 插槽的默认内容写法
        <slot>
        <p>我是默认内容, 哈哈哈</p>
        </slot>
        </template>
    • 一个组件中含有多个插槽,默认情况下每个插槽都会获取到插入的所有内容来显示;
  • 给插槽起一个名字,<slot> 元素有一个特殊的attribute:name

  • 一个不带 name 的slot,会带有隐含的名字 default;

  • 注:可以实现多个插槽与插入内容一一对应的效果;

    • 父组件:(App.vue)

      • <template>
        <nav-bar>
        <template v-slot:left>
        <button>{{ leftText }}</button>
        </template>
        <!-- 简写,字符#替换(v-slot:) -->
        <template #left>
        <button>{{ leftText }}</button>
        </template>
        </nav-bar>
        </template>
    • 子组件:(NavBar.vue)

      • <slot name="left">left</slot>
    • <template v-slot:[name]> //简写#[name]
      <button>{{ leftText }}</button>
      </template>
    • 父级模板里的所有内容都是在父级作用域中编译的;
    • 子模板里的所有内容都是在子作用域中编译的;
    • 如:默认情况下,父组件模板里使用插值语法往子组件插槽插入内容,插值语法里的变量对应的是父组件的响应式数据;
    • 在父组件模板里往子组件插槽插入内容时,使插入内容可以使用子组件的数据;
    • 父组件:(App.vue)

      • <tab-control>
        <!-- props表示子组件<slot>元素内的所有Attribute组成的对象;-->
        <template v-slot:default="props"> <!-- #default="props" -->
        <button>{{ props.item }}</button>
        </template>
        <!-- 独占默认插槽的缩写:-->
        <!-- 1.只有一个默认插槽时,可以省去default;可同时存在其他插槽;-->
        <template v-slot="props"> <!-- #="props" -->
        <button>{{ props.item }}</button>
        </template>
        </tab-control>
        <!-- 独占默认插槽的缩写: -->
        <!-- 2.如果只有一个默认插槽,且不可存在其他插槽时, 那么template可以省略,而v-slot写在子组件元素上;-->
        <tab-control v-slot="props">
        <button>{{ props.item }}</button>
        </tab-control>
    • 子组件:(TabControl.vue)

      • <slot :item="item" abc="cba">
        <span>{{ item }}</span>
        </slot>
    • 如果同时有默认插槽和具名插槽,那么按照完整的template来编写;
  • Vue3从实例中移除了 $on、$off 和 $once 方法,需通过第三方的库:

    • 推荐一些库,例如 mitt 或 tiny-emitter;
    • hy-event-store;(自己封装)
      • npm install hy-event-store
    • 创建文件event-bus.js

      • import { HYEventBus } from 'hy-event-store'
        const eventBus = new HYEventBus()
        export default eventBus
    • HomeBanner.vue

      • import eventBus from './utils/event-bus'
        export default {
        methods: {
        bannerBtnClick() {
        eventBus.emit("whyEvent", "why", 18, 1.88)
        }
        }
        }
    • Category.vue

      • import eventBus from './utils/event-bus'
        created() {
        eventBus.on("whyEvent",this.whyEventHandler)
        },
        unmounted() {
        eventBus.off("whyEvent", this.whyEventHandler)
        }
  • Provide/Inject用于非父子组件之间共享数据:
    Section titled “Provide/Inject用于非父子组件之间共享数据:”
    • 无论层级结构有多深,祖先组件都可以作为其所有后代组件的依赖提供者;
    • 祖先组件有一个 provide 选项来提供数据;
    • 后代组件有一个 inject 选项来使用这些数据;
  • 注:仅用于祖先、后代组件共享数据;
    Section titled “注:仅用于祖先、后代组件共享数据;”
    • 祖先组件:(App.vue)

      • <button @click="message = 'hello world'">修改message</button>
        import { computed } from 'vue'
        export default {
        data() {
        message: "hello"
        }
        // provide一般都是写成函数,便于this指向组件实例
        provide() {
        return {
        name: "why",
        age: 18,
        message: computed(() => this.message) // 用于把数据变成响应式;
        }
        }
        }
  • 后代组件:(HomeContent.vue)

    • //手动解包.value
      <h2>{{ name }}-{{ age }}-{{message.value}}</h2>
      export default {
      inject: ["name", "age", "message"] // message元素是ref对象,需要解包
      }
    • 使用响应式的一些API来完成这些功能,比如说computed函数;

    • computed返回的是一个ref对象,需要用.vaule手动解包,取出其中的value来使用;

      • 也可以在入口文件main.js设置自动解包;

      • app.config.unwrapInjectedRef = true
      • 使用了computed方法的属性在后代组件里是不允许更改的;
      • computed方法让祖先组件改掉响应式数据后,使子组件对应的inject的数据也是响应式的;
  • 直接获取到元素对象或者子组件实例:
    Section titled “直接获取到元素对象或者子组件实例:”
    • 不推荐进行DOM操作;
    • 给元素或者组件绑定一个ref的attribute属性;
    • 它是一个对象Object,持有注册过 ref attribute 的所有 DOM 元素和组件实例。
    • App.vue:

      • <h2 ref="title">{{ message }}</h2>
        <banner ref="banner"/>
        // 2.获取元素
        console.log(this.$refs.title)
        // 3.获取组件实例
        console.log(this.$refs.banner)
        // 3.1.在父组件中可以主动的调用子组件的组件实例方法
        this.$refs.banner.bannerClick()
        // 3.2.获取banner组件实例, 获取banner中的根元素
        console.log(this.$refs.banner.$el)
        // 3.3.如果banner template是多个根, 拿到的是第一个node节点
        // 注意: 开发中不推荐一个组件的template中有多个根元素
        // console.log(this.$refs.banner.$el.nextElementSibling)
        // 4.组件实例还有两个属性(了解):
        console.log(this.$parent) // 获取父组件
        console.log(this.$root) // 获取根组件
        //在Vue3中已经移除了$children的属性
  • 动态组件是使用 component 组件,通过一个特殊的属性is来实现;
    Section titled “动态组件是使用 component 组件,通过一个特殊的属性is来实现;”
    • 父组件:(App.vue)

      • // is中的组件需要来自两个地方: 1.全局注册的组件 2.局部注册的组件
        // 动态组件的传值: 将属性和监听事件放到component上,与普通组件使用一样;
        <component name="why" :age="18" @homeClick="homeClick" :is="currentTab"></component>
        components: {
        Home,
        About,
        Category
        },
        data() {
        return {
        currentTab: "home" // 修改currentTab对应组件名可以切换不同的组件;(写法如短横线对应驼峰)
        }
        }
    • 其中一个子组件:(home.vue)

      • export default {
        props: ["name"],
        emits: ["homeClick"],
        methods: {
        homeBtnClick() {
        this.$emit("homeClick", "home") //自定义事件;
        }
        }
        }
  • 在某些情况使组件继续保持状态,而不是销毁掉;
    Section titled “在某些情况使组件继续保持状态,而不是销毁掉;”
    • include:

      • string | RegExp | Array。只有名称匹配的组件会被缓存;
    • exclude:

      • string | RegExp | Array。只有名称不匹配的组件会被缓存;
    • max:

      • number | string。最多可以缓存多少组件实例,一旦达到这个数

      字,那么缓存组件中最近没有被访问的实例会被销毁;

    • 字符串写法写入多个组件时,需要用逗号分割且不能有空格;include="a,b"
    • 上方的a,b对应的是组件Options API的name选项;
    • 数组写法::include="['home']"
    • 对于缓存的组件来说,再次进入时,是不会执行created或者mounted等生命周期函数的;
    • 可以使用 activateddeactivated 这两个生命周期钩子函数来监听;
    • 父组件:(App.vue)

      • <!-- include: 组件的名称来自于组件定义时name选项 -->
        <keep-alive include="home,about">
        <component :is="currentTab"></component>
        </keep-alive>
    • 子组件:

      • // 对于保持keep-alive组件, 监听有没有进行切换
        // keep-alive组件进入活跃状态
        activated() {
        console.log("home activated")
        },
        //失活
        deactivated() {
        console.log("home deactivated")
        }
  • 如果的项目过大了,对于某些组件希望通过异步的方式来进行加载(目的是可以对其进行分包处理),那么Vue中给提供了一个函数:defineAsyncComponent

  • defineAsyncComponent有两种类型的参数:

    • 工厂函数,该工厂函数需要返回一个Promise对象;
    • 接受一个对象类型,对异步函数进行配置;(了解);
    • const AsyncCategory = defineAsyncComponent(() => import("./views/Category.vue"))
      components: {
      Category: AsyncCategory
      },
    • 绑定了modelValue属性;
    • 监听了 @update:modelValue的事件;
    • 父组件:(App.vue)

      • <counter :modelValue="appCounter" @update:modelValue="appCounter = $event"></counter> //$event只指传递的第一个参数
    • 子组件:(Counter.vue)

      • export default {
        //1.获取父组件传递过来的modelValue数据;
        props: {
        modelValue: {
        type: Number,
        default: 0
        }
        },
        //2.再自定义事件,向父组件传递数据;
        emits: ["update:modelValue"],
        methods: {
        changeCounter() {
        this.$emit("update:modelValue", 999)
        }
        }
        }
    • 父组件:(App.vue)

      • <counter v-model="appCounter"></counter>
    • 子组件与上方一样;

    • 注:会直接把子组件传递过来的参数赋值给对应的响应式数据;

  • modelValueundate:modelValue是规定的默认属性和事件,修改方式:
    Section titled “modelValue 和 undate:modelValue是规定的默认属性和事件,修改方式:”
    • 父组件:(App.vue)

      • <counter2 v-model:counter="appCounter" v-model:why="appWhy"></counter2>
    • 子组件:(Counter2.vue)

      • export default {
        props: {
        counter: {
        type: Number,
        default: 0
        }
        },
        emits: ["update:counter"], // 事件名需要与属性名一致
        methods: {
        changeCounter() {
        this.$emit("update:counter", 999)
        }
        }
        }
    • 注:设置不同属性名后,可以绑定多个v-model;

1.1、Options API与Composition API的区别

Section titled “1.1、Options API与Composition API的区别”
    • 在对应的属性中编写对应的功能模块;
    • 实现某一个功能时,对应的代码逻辑会被拆分到各个属性中;
    • 组件变得更大、更复杂,同一个功能的逻辑就会被拆分的很分散;
    • 不便于查找和处理单个逻辑关注点;
    • 能将同一个逻辑关注点相关的代码收集在一起;
  • 返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值;
    Section titled “返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值;”
    • 内部的值是在refvalue属性中被维护的;
  • 可以传入简单数据类型和复杂数据类型;
    Section titled “可以传入简单数据类型和复杂数据类型;”
    • 在模板中引入ref的值时,Vue会自动解包,所以不需要通过 .value的方式来使用;
    • setup函数内部,它依然是一个 ref引用,所以依然需要使用.value的方式来使用;
    • 对象类型解包后是proxy对象;
  • // 使用的时候不需要写.value;使用时候是深层解包
    <h2>当前计数: {{ info.counter }}</h2>
    // 修改的时候需要写.value;修改时候不是深层解包
    <button @click="info.counter.value++">+1</button>
    const counter = ref(0)
    const info = { //普通对象里面套ref
    counter
    }
    • 定义本地的一些简单数据;

    • 定义从网络中获取的数据也是使用ref;

      • // 若使用reactive函数,只会返回一个代理对象,若对对象进行替换则失去响应式;只能把数据逐个push到数组里;
        // const musics = reactive([])
        const musics = ref([])
        // musics的值是被ref对象.value属性所维护,修改ref对象的value值后,值依然被ref对象所包裹,所以依然存在响应式;
        onMounted(() => {
        const serverMusics = ["海阔天空", "小苹果", "野狼"]
        musics.value = serverMusics
        })
    • 默认是深层ref对象;
    • 对于从其他组件接收到的数据只读取,不修改。
    • 防止传递给其他组件的响应式数据被修改;
    • readonly会返回原始对象的只读代理(依然是一个Proxy,proxy的set方法被劫持了,不能对其进行修改);
    • 普通对象;
    • reactive返回的对象;
    • ref的对象;
    • readonly返回的对象都是不允许修改的(无论在父组件还是在子组件);
    • 但是经过readonly处理的原来的对象是允许被修改的;
      • <show-info :roInfo="roInfo" @changeRoInfoName="changeRoInfoName"></show-info>
        const info = reactive({
        name: "why",
        age: 18,
        height: 1.88
        })
        // 使用readOnly包裹info
        const roInfo = readonly(info)
        return {
        roInfo,
        }
      • // 只要在子组件修改接收的数据,代码就会无效(报警告)
        <button @click="roInfo.name = 'james'">ShowInfo按钮</button>
        props: {
        // readonly数据
        roInfo: {
        type: Object,
        default: () => ({})
        }
        },
        // 需要修改数据,必须通过注册自定义事件,让父组件监听事件来修改数据;
        emits: ["changeRoInfoName"],
        setup(props, context) {
        function roInfoBtnClick() {
        context.emit("changeRoInfoName", "james")
        }
        return { roInfoBtnClick }
        }
    • readonly传入响应式数据返回才是响应式数据;
  • toRefs:转换一个 reactive 对象的所有属性为ref;

  • toRef:转换 reactive 对象中的一个属性为ref;

    • setup() {
      const info = reactive({
      name: "why",
      age: 18,
      height: 1.88
      })
      // reactive被解构后会变成普通的值, 失去响应式
      // const { name, age } = info
      // toRefs: 使得解构出来的属性具有响应式;
      const { name, age } = toRefs(info)
      // 在setup中修改解构的值需要解包
      name.value = "coder"
      // toRef:
      const height = toRef(info, "height")
      return {
      name,
      age,
      height
      }
      }
    • 传入一个getter函数,在函数里返回包含响应式数据的复杂逻辑,得到最终的计算结果,computed方法返回一个只读的ref对象;

      • <h2>{{ fullname }}</h2>
        import { reactive, computed, ref } from 'vue'
        const names = reactive({
        firstName: "kobe",
        lastName: "bryant"
        })
        const fullname = computed(() => {
        return names.firstName + " " + names.lastName
        })
    • 传入一个具有 get 和 set 的对象,返回一个可变的(可读写)ref 对象;

      • const fullname = computed({
        set: function(newValue) {
        const tempNames = newValue.split(" ")
        names.firstName = tempNames[0]
        names.lastName = tempNames[1]
        },
        get: function() {
        return names.firstName + " " + names.lastName
        }
        })
    • key:提供的属性名称;

    • value:提供的属性值;

      • import { provide, ref } from 'vue'
        const name = ref("why")
        //不同于Options API,其传递过去接收到的就是响应式数据;
        //reactive不能单独传递对象里的某个属性,响应式会失效;
        //ref可以单独传递解包后的proxy对象,但不能是proxy的属性,对应包裹的是基本数据类型更不能传递,否则失去响应式;
        provide("name", name)
        provide("age", 18)
    • 注入的属性名称;

    • 默认值;

      • import { inject } from 'vue'
        const name = inject("name") //提供的是ref对象,那么获取到的也是ref对象
        const age = inject("age")
        //定义默认值;
        const height = inject("height", 1.88)
    • 为了增加 provide 值和 inject 值之间的响应性,可以给 provide 值使用 refreactive
  • 类似于Option API的watch选项:

    • watch需要侦听特定的数据源,并且执行其回调函数;
    • 默认情况,只有当被侦听的源发生变化时才会执行回调;
    • 不同的是默认情况Options API是浅层监听;
    • import { watch } from 'vue'
      //1.默认深层监听
      const message = ref("Hello World")
      const age = ref(18)
      //监听一个数据源
      watch(message, (newValue, oldValue) => {
      console.log(newValue, oldValue) //Hello World
      })
      //监听多个数据源,数组形式
      watch([message, age], (newValue, oldValue) => {
      console.log(newValue, oldValue)
      })
      //2.监听reactive数据变化后, 获取普通对象,默认是浅层监听;
      watch(() => ({ ...info }), (newValue, oldValue) => {
      console.log(newValue, oldValue)
      }, {
      immediate: true,
      deep: true
      })
  • 注意:

    • 如果监听源是reactive响应式数据,是深层监听;如果监听源是ref对象,是浅层监听;如果监听源是ref.value,是深层监听;
    • 数据源是ref对象,返回的newValue和oldValue会自动解包;
    • 监听不了普通值;
    • watchEffect传入的函数会被立即执行一次,并且在执行的过程中会收集依赖;
    • 只有收集的依赖发生变化时,watchEffect传入的函数才会再次执行;
    • import { watchEffect } from 'vue'
      //返回值是一个函数;不需要停止监听可不用变量接收;
      const stopWatch = watchEffect(() => {
      console.log("-------", counter.value, name.value)
      // 判断counter.value >= 10,停止监听;
      if (counter.value >= 10) {
      stopWatch() //调用函数停止监听;
      }
      })
    • 监听不了普通值;
  • watch懒执行副作用(第一次不会直接执行);
  • watch更具体的说明当哪些状态发生变化时,触发侦听器的执行;
  • watch可以访问侦听状态变化前后的值;
  • watchEffect 不能收集异步方法中的依赖
    • 也就是锚点(#), 本质上是改变window.location的href属性;
    • 可以通过直接赋值location.hash来改变href, 但是页面不发生刷新;
  • hash的优势就是兼容性更好,在老版IE中都可以运行,但是缺陷是有一个#,显得不像一个真实的路径。

  • import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
    // 创建一个路由: 映射关系
    const router = createRouter({
    // 指定采用的模式: hash
    history: createWebHashHistory(),
    // history: createWebHistory(),
    )}
    • hashhistory
      有 # 号没有 # 号
      能够兼容到IE8只能兼容到IE10
      实际的url之前使⽤哈希字符,这部分url不会发送到服务器,不需要在服务器层⾯上进⾏任何处理每访问⼀个⻚⾯都需要服务器进⾏路由匹配⽣成html ⽂件再发送响应给浏览器,消耗服务器⼤量资源
      刷新不会存在 404 问题浏览器直接访问嵌套路由时,会报 404 问题。
      不需要服务器任何配置需要在服务器配置⼀个回调路由
  • name属性:路由记录独一无二的名称;
  • meta属性:自定义的数据;

2.3.1、代码的页面跳转和query参数

Section titled “2.3.1、代码的页面跳转和query参数”
    • //普通写法
      //传入路径
      this.$router.push('/home')
      //对象写法
      //属性为路由名或路径
      this.$router.push({
      // name: "home"
      path: "/home",
      //传入query参数
      query: {
      name: "why",
      age: 18
      }
      })
    • import { useRouter } from 'vue-router'
      const router = useRouter()
      // 普通写法
      router.push("/home")
      // 对象写法
      router.push({
      // name: "home"
      path: "/home",
      //传入query参数
      query: {
      name: "why",
      age: 18
      }
      })
    • <h2>{{ $route.query.name }}</h2>
  • go:
    • router.go(1)
  • back:
    • router.back()
  • forward:
    • router.forward()
    • 创建路由需要映射的组件;
    • 通过createRouter创建路由对象,并且传入history模式和routes;
      • 创建基于hash或者history的模式;
      • 配置路由映射: 组件和路径映射关系的routes数组;
    • 使用app注册路由对象(use方法);
    • 路由使用: 通过<router-link><router-view>
      • import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'
        // 创建一个路由: 映射关系
        const router = createRouter({
        // 指定采用的模式: hash,
        // 函数调用;
        history: createWebHashHistory(),
        // history: createWebHistory(),
        routes: [
        {
        path: "/",
        redirect: "/home" //重定向
        },
        {
        name: "home",
        path: "/home",
        component: () => import("../Views/Home.vue"), //路由懒加载
        //自定义数据
        meta: {
        name: "why",
        age: 18
        },
        children: [
        //嵌套路由重定向
        {
        path: "/home",
        redirect: "/home/recommend"
        },
        {
        path: "recommend", // /home/recommend
        component: () => import("../Views/HomeRecommend.vue")
        },
        {
        path: "ranking", // /home/ranking
        component: () => import("../Views/HomeRanking.vue")
        },
        ]
        },
        {
        name: "about",
        path: "/about",
        component: () => import("../Views/About.vue")
        },
        {
        path: "/user/:id",
        component: () => import("../Views/User.vue")
        },
        {
        // abc/cba/nba
        path: "/:pathMatch(.*)*",
        component: () => import("../Views/NotFound.vue")
        }
        ]
        })
        export default router
      • <div class="nav">
        <router-link to="/home" replace>首页</router-link>
        <!-- <router-link :to="{ path: '/home' }" replace>首页</router-link> -->
        <router-link to="/about" replace active-class="active">关于</router-link>
        <router-link to="/user/123">用户123</router-link>
        </div>
        <!-- 占位组件 -->
        <router-view></router-view>
      • <div class="home-nav">
        <router-link to="/home/recommend">推荐</router-link>
        </div>
        <!-- 占位组件 -->
        <router-view></router-view>
    • state: () => ({
      counter: 100,
      name: "coderwhy",
      level: 100
      }),
    • <h2>{{ $store.state.counter }}</h2>
    • 使用computed来简化写法:

      • <h2>{{ storeCounter }}</h2>
        export default {
        computed: {
        storeCounter() {
        return this.$store.state.counter
        }
        }
        }
    • 多数据写法:(使用mapState的辅助函数)

      • import { mapState } from 'vuex'
        export default {
        computed: {
        //数组写法
        ...mapState(["name", "level"]),
        //对象写法(也可以解决命名冲突)
        ...mapState({
        //会传入一个state对象,固定写法
        //可以重命名
        sName: state => state.name,
        sLevel: state => state.level
        })
        }
        }
    • 使用mapState的写法:

      • import { mapState, useStore } from 'vuex'
        import { toRefs } from 'vue'
        //解构mapState返回的对象,得到一个个函数
        const { name, level } = mapState(["name", "level"])
        //利用导入的useStore函数获取store对象
        const store = useStore()
        //给函数
        const cName = computed(name.bind({ $store: store }))
        const cLevel = computed(level.bind({ $store: store }))
    • 直接解构, 并且包裹成ref:

      • <h2>{{ counter }}</h2>
        import { toRefs } from 'vue'
        import { useStore } from 'vuex'
        const store = useStore()
        //使用toRefs可以让让解构后的数据是ref对象,依然具有响应式
        const { name, level } = toRefs(store.state)
  • 处理包含响应式数据的复杂逻辑,如同computed;
    Section titled “处理包含响应式数据的复杂逻辑,如同computed;”
    • getters: {
      // 1.基本使用
      doubleCounter(state) {
      return state.counter * 2
      },
      // 2.在该getters属性中, 获取其他的getters
      message(state, getters) {
      return `name:${state.name};TotalAge:${getters.doubleCounter}`
      },
      // 3.getters是可以返回一个函数的, 调用这个函数可以传入参数(了解)
      getFriendById(state) {
      return function(id) {
      const friend = state.friends.find(item => item.id === id)
      return friend
      }
      }
      }
      }
    • <h2>{{ $store.getters.doubleCounter }}</h2>
      //对返回的函数进行调用
      <h2>{{ $store.getters.getFriendById(111) }}</h2>
    • 可以使用computed来简化写法;

    • 多数据写法:(使用mapGetters的辅助函数)

      • import { mapGetters } from 'vuex'
        export default {
        computed: {
        ...mapGetters(["doubleCounter"]),
        }
        }
    • 使用mapGetters的写法:

      • import { computed } from 'vue'
        import { mapGetters, useStore } from 'vuex'
        const store = useStore()
        //解构后得到得是函数
        const { message: messageFn } = mapGetters(["message"])
        //给函数绑定this;
        const message = computed(messageFn.bind({ $store: store }))
    • 直接解构, 并且包裹成ref:

      • <h2>{{ message }}</h2>
        import { toRefs } from 'vue';
        import { useStore } from 'vuex'
        const store = useStore()
        const { message } = toRefs(store.getters)
        //针对某一个getters属性使用computed或toRef
        //使用computed函数后返回的也是ref对象;
        const message = computed(() => store.getters.message)
  • 更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;
    Section titled “更改 Vuex 的 store 中的状态的唯一方法是提交 mutation;”
  • 若要使用常量代替函数名,在store文件夹创建mutation_types.js:
    Section titled “若要使用常量代替函数名,在store文件夹创建mutation_types.js:”
    • //定义并导出常量
      export const CHANGE_INFO = "changeInfo"
    • //导入常量文件
      import { CHANGE_INFO } from './mutation_types'
      mutations: {
      increment(state) {
      state.counter++
      },
      changeName(state, payload) {
      state.name = payload
      },
      //使用常量作为函数名
      [CHANGE_INFO](state, newInfo) {
      state.level = newInfo.level
      state.name = newInfo.name
      }
      }
      }
    • 普通使用:

      • import { CHANGE_INFO } from "@/store/mutation_types"
        export default {
        methods: {
        changeName() {
        this.$store.commit("changeName", "王小波")
        //对象写法:
        this.$store.commit({
        type: CHANGE_INFO,
        count: 100
        })
        },
        changeInfo() {
        //指定方法名
        this.$store.commit(CHANGE_INFO, {
        name: "王二",
        level: 200
        })
        }
        }
        }
        }
    • 使用mapMutations的方法:

      • //使用mapMutations方法,若要给mutations的方法传参则直接在元素里传
        <button @click="changeName('王小波')">修改name</button>
        import { mapMutations } from 'vuex'
        import { CHANGE_INFO } from "@/store/mutation_types"
        methods: {
        ...mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
        //对象写法
        ...mapMutations({
        changeinfo: CHANGE_INFO
        })
        }
    • 使用mapMutations方法:

      • <button @click="changeName('王小波')">修改name</button>
        import { mapMutations, useStore } from 'vuex'
        import { CHANGE_INFO } from "@/store/mutation_types"
        const store = useStore()
        const mutations = mapMutations(["changeName", "incrementLevel", CHANGE_INFO])
        const newMutations = {}
        Object.keys(mutations).forEach(key => {
        newMutations[key] = mutations[key].bind({ $store: store })
        })
        const { changeName, incrementLevel, changeInfo } = newMutations
        //传参在元素上传
    • mutation必须是同步函数:
      • 因为devtool工具会记录mutation的日记;
      • 每一条mutation被记录,devtools都需要捕捉到前一状态和后一状态的快照;
      • 但是在mutation中执行异步操作,就无法追踪到数据的变化;
    • //简单使用
      actions: {
      //可以传入参数
      incrementAction(context, payload) {
      context.commit("increment", payload)
      },
      }
    • <button @click="btnClick">发起action</button>
    • 普通使用:

      • export default {
        methods: {
        btnClick() {
        this.$store.dispatch("incrementAction")
        }
        }
        }
    • mapActions的使用:

      • methods: {
        //要传参数在元素上传
        ...mapActions(["incrementAction", "changeNameAction"])
        //对象写法
        ...mapActions({
        addAction: "addAction" //属性为绑定的事件,属性值为actions里对应的属性
        })
        }
    • 默认写法:

      • import { useStore } from 'vuex'
        const store = useStore()
        function increment() {
        store.dispatch("incrementAction")
        }
    • mapActions写法:(使用多个actions方法时)

      • import { useStore, mapActions } from 'vuex'
        const store = useStore()
        const actions = mapActions(["incrementAction", "changeNameAction"])
        const newActions = {}
        Object.keys(actions).forEach(key => {
        newActions[key] = actions[key].bind({ $store: store })
        })
        const { incrementAction, changeNameAction } = newActions

1.7.2、actions的异步操作—发起网络请求:

Section titled “1.7.2、actions的异步操作—发起网络请求:”
    • 3.await/async
      //发送网络请求
      actions: {
      async fetchHomeMultidataAction(context) {
      const res = await fetch("http://123.207.32.32:8000/home/multidata")
      const data = await res.json()
      // 修改state数据
      context.commit("changeBanners", data.data.banner.list)
      context.commit("changeRecommends", data.data.recommend.list)
      return data
      }
      }
    • import { useStore } from 'vuex'
      // 告诉Vuex发起网络请求
      const store = useStore()
      // 若需要在setup中使用数据可以使用then方法,但返回的数据必须是promise)
      store.dispatch("fetchHomeMultidataAction").then(res => {
      console.log("home中的then被回调:", res)
      })

1.8.3、在模块中获取根模块的state

Section titled “1.8.3、在模块中获取根模块的state”
    • getters: {
      bar(state, getters, rootState, rootGetters) {}
      },
      mutations: {
      foo(state, e, rootState, rootGetters) {}
      },
      actions: {
      baz({ state, rootState,rootGetters, commit, dispatch }, e) {}
      }
  • 最初是作为一个实验为Vue重新设计状态管理,使其更契合组合式API;
  • 并且目前同时兼容Vue2、Vue3,可以使用Optons APIComposition API
  • Pinia本质上依然是一个状态管理的库,用于跨组件和页面进行状态共享;

1.3.2、在一个store使用另一个store

Section titled “1.3.2、在一个store使用另一个store”
    • import { defineStore } from 'pinia'
      //导入另一个仓库
      import useUser from './user'
      // 1.获取user信息
      const userStore = useUser()
      export default defineStore("user", {
      state: () => ({}),
      getters: {
      foo(state) {
      // 使用其他store的状态,不需要跟state
      return userStore.name + state.name
      }
      }
      })
  • Store获取到的state不能被解构,会失去响应式:
    Section titled “Store获取到的state不能被解构,会失去响应式:”
    • 从 Store 中提取属性同时保持其响应式,需要使用storeToRefs();

    • //不需要跟state;
      <h2>count: {{ userStore.name }}</h2>
      <h2>{{ name }}</h2>
      import useUser from '@/stores/user'
      import { storeToRefs } from 'pinia'
      const userStore = useUser()
      const { name, age, level } = storeToRefs(userStore)
  • // 跟patch的功能类似, 合并state, 就像使用了Object.assign(), 添加或替换某个状态
    const oldState = userStore.$state
    userStore.$state = {
    name: "curry",
    level: 200
    }
    console.log(oldState === userStore.$state) //true
  • 通过调用store上的 $reset()方法将状态重置到其初始值;

  • function resetState() {
    userStore.$reset()
    }
  • 配置Actions中,获取到的参数只是普通传进来的参数,不是context;

    • actions: {
      incrementNum(num) {
      this.count += num
      }
      }
  • 调用Actions中的方法:

    • //不需要跟dispatch
      import useHome from '@/stores/home'
      const homeStore = useHome()
      homeStore.fetchHomeMultidata().then(res => {
      console.log(res)
      })
    • 在浏览器中发送 XMLHttpRequests 请求;
    • 在 node.js 中发送 http请求;
    • 支持 Promise API;
    • 拦截请求和响应;
    • 转换请求和响应数据;
    • axios(config) :
      • axios(‘/user/12345’) //发起一个 GET 请求 (默认请求方式)
    • axios.request(config)
    • axios.get(url[, config])
    • axios.delete(url[, config])
    • axios.head(url[, config])
    • axios.post(url[, data[, config]])
    • axios.put(url[, data[, config]])
    • axios.patch(url[, data[, config]])
    • axios.all([…]):底层原理是调用Promise.all();
  • 发送request请求:

    • axios.request({
      url: "http://123.207.32.32:8000/home/multidata",
      method: "get"
      }).then(res => {
      console.log("res:", res.data)
      })
  • 发送get请求:

    • //查询字符串形式
      axios.get(`http://123.207.32.32:9001/lyric?id=500665346`).then(res => {
      console.log("res:", res.data.lrc)
      })
      //query参数
      axios.get("http://123.207.32.32:9001/lyric", {
      params: {
      id: 500665346
      }
      }).then(res => {
      console.log("res:", res.data.lrc)
      })
  • 发送post请求:

    • //第二个参数为传入参数的data
      //第三个参数为配置config,也可以写在第二个参数的位置,库会对其进行判断处理;
      //第二个参数为data
      axios.post("http://123.207.32.32:1888/02_param/postjson", {
      name: "coderwhy",
      password: 123456
      }).then(res => {
      console.log("res", res.data)
      })
      //第二个参数为配置,配置里写入data
      axios.post("http://123.207.32.32:1888/02_param/postjson", {
      data: {
      name: "coderwhy",
      password: 123456
      }
      }).then(res => {
      console.log("res", res.data)
      })

1.2.2、 给axios实例配置公共的基础配置

Section titled “1.2.2、 给axios实例配置公共的基础配置”
  • // 1.baseURL;对于一些需要重复使用的基础地址抽取出来
    const baseURL = "http://123.207.32.32:8000"
    // 给axios实例配置公共的基础配置,每次使用实例都会默认进行以下配置;
    axios.defaults.baseURL = baseURL
    axios.defaults.timeout = 10000
    axios.defaults.headers = {}
    // get: /home/multidata
    axios.get("/home/multidata").then(res => {
    console.log("res:", res.data)
    })
  • axios.all([
    axios.get("/home/multidata"),
    //有完整地址,直接使用完整地址
    axios.get("http://123.207.32.32:9001/lyric?id=500665346")
    ]).then(res => {
    console.log("res:", res)
    })
  • axios的也可以设置拦截器:拦截每次请求和响应
    Section titled “axios的也可以设置拦截器:拦截每次请求和响应”
    • axios.interceptors.request.use(成功拦截回调, 失败拦截回调)

      • 作用:

        • 开始loading的动画
        • 对原来的配置进行一些修改
          • header
          • 认证登录: token/cookie
          • 请求参数进行某些转化
      • // 对实例配置拦截器
        axios.interceptors.request.use((config) => {
        console.log("请求成功的拦截")
        return config
        }, (err) => {
        console.log("请求失败的拦截")
        return Promise.reject(err)
        })
    • axios.interceptors.response.use(成功拦截回调, 失败拦截回调)

      • 作用:

        • 结束loading的动画
        • 对数据进行转化, 再返回数据
      • axios.interceptors.response.use((res) => {
        console.log("响应成功的拦截")
        return res.data
        }, (err) => {
        console.log("响应失败的拦截:", err)
        return Promise.reject(err)
        })
  • 注:拦截器需要写在网络前请求的前面;
    Section titled “注:拦截器需要写在网络前请求的前面;”
    • import axios from 'axios'
      class RequestHttp {
      constructor(baseURL, timeout=10000) {
      this.instance = axios.create({
      baseURL,
      timeout
      })
      // 拦截器设置;
      this.instance.interceptors.request.use(config => {
      return config
      }, err => {
      return err
      })
      this.instance.interceptors.response.use(res => {
      return res
      }, err => {
      return err
      })
      }
      request(config) {
      return new Promise((resolve, reject) => {
      this.instance.request(config).then(res => {
      //对结果进行处理
      resolve(res.data)
      }).catch(err => {
      reject(err)
      })
      })
      }
      get(config) {
      return this.request({ ...config, method: "get" })
      }
      post(config) {
      return this.request({ ...config, method: "post" })
      }
      }
      export default new RequestHttp("http://123.207.32.32:9001")
  • 使用的文件:

    • import RequestHttp from './service'
      hyRequest.request({
      url: "/lyric?id=500665346"
      }).then(res => {
      console.log("res:", res)
      })

目录结构

request

​ index.ts

​ config.ts

​ type.ts

config.ts

// 1.方式一: 手动的切换不同的环境(不推荐)
// const BASE_URL = 'http://coderwhy.org/dev'
// const BASE_NAME = 'coderwhy'
// const BASE_URL = 'http://coderwhy.org/prod'
// const BASE_NAME = 'kobe'
// const BASE_URL = 'http://coderwhy.org/test'
// const BASE_NAME = 'james'
// 2.根据process.env.NODE_ENV区分
// 开发环境: development
// 生成环境: production
// 测试环境: test
let BASE_URL = ''
const TIME_OUT = 10000
if (process.env.NODE_ENV === 'development') {
BASE_URL = '/api'
} else if (process.env.NODE_ENV === 'production') {
BASE_URL = 'http://coderwhy.org/prod'
} else {
BASE_URL = 'http://coderwhy.org/test'
}
export { BASE_URL, TIME_OUT }

index.ts

import axios from 'axios'
import type { AxiosInstance } from 'axios'
import type { HYRequestInterceptors, HYRequestConfig } from './type'
import { ElLoading } from 'element-plus'
import { ILoadingInstance } from 'element-plus/lib/el-loading/src/loading.type'
const DEAFULT_LOADING = true
class HYRequest {
instance: AxiosInstance
interceptors?: HYRequestInterceptors
showLoading: boolean
loading?: ILoadingInstance
constructor(config: HYRequestConfig) {
// 创建axios实例
this.instance = axios.create(config)
// 保存基本信息
this.showLoading = config.showLoading ?? DEAFULT_LOADING
this.interceptors = config.interceptors
// 使用拦截器
// 1.从config中取出的拦截器是对应的实例的拦截器
this.instance.interceptors.request.use(
this.interceptors?.requestInterceptor,
this.interceptors?.requestInterceptorCatch
)
this.instance.interceptors.response.use(
this.interceptors?.responseInterceptor,
this.interceptors?.responseInterceptorCatch
)
// 2.添加所有的实例都有的拦截器
this.instance.interceptors.request.use(
(config) => {
if (this.showLoading) {
this.loading = ElLoading.service({
lock: true,
text: '正在请求数据....',
background: 'rgba(0, 0, 0, 0.5)'
})
}
return config
},
(err) => {
return err
}
)
this.instance.interceptors.response.use(
(res) => {
// 将loading移除
this.loading?.close()
const data = res.data
if (data.returnCode === '-1001') {
console.log('请求失败~, 错误信息')
} else {
return data
}
},
(err) => {
// 将loading移除
this.loading?.close()
// 例子: 判断不同的HttpErrorCode显示不同的错误信息
if (err.response.status === 404) {
console.log('404的错误~')
}
return err
}
)
}
request<T = any>(config: HYRequestConfig<T>): Promise<T> {
return new Promise((resolve, reject) => {
// 1.单个请求对请求config的处理
if (config.interceptors?.requestInterceptor) {
config = config.interceptors.requestInterceptor(config)
}
// 2.判断是否需要显示loading
if (config.showLoading === false) {
this.showLoading = config.showLoading
}
this.instance
.request<any, T>(config)
.then((res) => {
// 1.单个请求对数据的处理
if (config.interceptors?.responseInterceptor) {
res = config.interceptors.responseInterceptor(res)
}
// 2.将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEAFULT_LOADING
// 3.将结果resolve返回出去
resolve(res)
})
.catch((err) => {
// 将showLoading设置true, 这样不会影响下一个请求
this.showLoading = DEAFULT_LOADING
reject(err)
return err
})
})
}
get<T = any>(config: HYRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'GET' })
}
post<T = any>(config: HYRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'POST' })
}
delete<T = any>(config: HYRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'DELETE' })
}
patch<T = any>(config: HYRequestConfig<T>): Promise<T> {
return this.request<T>({ ...config, method: 'PATCH' })
}
}
export default HYRequest

type.ts

import type { AxiosRequestConfig, AxiosResponse } from 'axios'
export interface HYRequestInterceptors<T = AxiosResponse> {
requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
requestInterceptorCatch?: (error: any) => any
responseInterceptor?: (res: T) => T
responseInterceptorCatch?: (error: any) => any
}
export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
interceptors?: HYRequestInterceptors<T>
showLoading?: boolean
}
  • 自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;
    Section titled “自定义局部指令:组件中通过 directives 选项,只能在当前组件中使用;”
    • template:

      • <input type="text" v-focus>
    • option API:

      • export default {
        directives: {
        focus: { //自定义指令名称
        // 生命周期的函数(自定义指令)
        mounted(el) {
        el.focus()
        }
        }
        }
        }
    • compositions API:(setup语法糖写法)

      • // 写法为v + 大驼峰
        const vFocus = {
        // 生命周期的函数(自定义指令)
        mounted(el) { //el为元素对象
        el.focus()
        }
        }
      • 注意:对于setup函数没有写法;

  • 自定义全局指令:app的 directive 方法,可以在任意组件中被使用;
    Section titled “自定义全局指令:app的 directive 方法,可以在任意组件中被使用;”
    • app.directive("focus", {
      // 生命周期的函数(自定义指令)
      mounted(el) {
      el.focus()
      }
      })
    • 创建一个directives文件夹 -> focus.js和index.js ;

    • focus.js:

      • export default function directiveFocus(app) {
        app.directive("focus", {
        mounted(el) {
        el.focus()
        }
        })
        }
    • index.js:

      • import directiveFocus from "./focus"
        ...
        export default function directives(app) {
        directiveFocus(app)
        ... // 可以添加更多的dir...
        }
    • main.js:

      • import directives from "./01_自定义指令/directives/index"
        // 1.可以直接调用,但要传入app
        const app = createApp(APP)
        directives(app)
        // 2.或者使用use(),use()会给传入的函数传入参数app对象并调函数;
        createApp(App).use(directives).mount("#app")
  • <h2 v-why:kobe.abc.cba="message">哈哈哈哈</h2>
    // :kobe为参数的名称;
    // .abc和.cba为修饰符;
    // message为传入的值;
    // 通过bindings来获取对应的内容,bindings为钩子函数的第二个参数;
    const vWhy = {
    mounted(el, bindings) {
    el.textContent = bindings.value //可以进行对应操作
    }
    }

1.4、内置组件—异步组件和Suspense

Section titled “1.4、内置组件—异步组件和Suspense”
  • 渲染函数可以使用JavaScript来创建HTML;
    Section titled “渲染函数可以使用JavaScript来创建HTML;”
    • 使用渲染函数生成对应的VNode;
  • template中的HTML 最终也是使用渲染函数生成对应的VNode
    Section titled “template中的HTML 最终也是使用渲染函数生成对应的VNode;”
    • 用于创建 vnode 的一个函数;
    • 也可以是createVNode()函数,相同;
    • tag:标签名、组件、异步组件、或函数式组件;

    • props:与attribute、prop和事件相对应的对象;

    • children:使用h()构建子VNode,或是文本内容、有插槽的对象;

    • // 标签,属性,子元素
      h(tag, props, [
      h(tag, props, "内容")]
      ])
    • Options API:

      • 不能有template元素;

      • import { h } from 'vue'
        import Home from "./Home.vue"
        export default {
        render() { //渲染函数
        return h("div", { className: "app" }, [
        h("h2", null, `当前计数: ${this.counter}`),
        h("button", { onClick: this.increment }, "+1"),
        h("button", { onClick: this.decrement }, "-1"),
        h(Home)
        ])
        }
        }
    • setup函数:

      • 注意:

        • 有无<template><render />都可以;
        • template中只能有<render />,其他元素不会被渲染;
      • <template>
        <render/>
        </template>
        <script>
        import { h, ref } from 'vue'
        import Home from "./Home.vue"
        export default {
        setup() {
        ...
        //返回一个函数,函数又返回一个h函数;
        return () => h("div", { className: "app" }, [
        h("h2", null, `当前计数: ${counter.value}`),
        h("button", { onClick: increment }, "+1"),
        h("button", { onClick: decrement }, "-1"),
        h(Home) //可以是组件,区分大小写;其他写法都是区分大小写
        ])
        }
        }
        </script>
    • setup语法糖:

      • 注意:

        • 必须要有<template><render />
        • template元素里除<render />,还可以有其他元素;
      • <template>
        <render/>
        </template>
        <script setup>
        import { ref, h } from 'vue';
        ...
        const render = () => h("div", { className: "app" }, [
        h("h2", null, `当前计数: ${counter.value}`),
        h("button", { onClick: increment }, "+1"),
        h("button", { onClick: decrement }, "-1"),
        h(Home)
        ])
        </script>
  • 注意:

    • 如果没有props,那么通常可以将children作为第二个参数传入;
    • 如果会产生歧义,可以将null作为第二个参数传入,将children作为第三个参数传入;
  • JSX是一个 JavaScript 的语法扩展,也是可以使能用javaScript来编写HTML;

  • jsx要通过Babel来进行转换,vue需要在Babel中配置对应的插件:

    • webpack:

      • npm install @vue/babel-plugin-jsx -D

      • babel.config.js配置文件:

        • module.exports = {
          plugins: [
          "@vue/babel-plugin-jsx"
          ]
          }
    • vite:

      • npm install @vitejs/plugin-vue-jsx -D

      • vite.config.js:

        • import jsx from '@vitejs/plugin-vue-jsx'
          plugins: [
          jsx()
          ]
    • Options API:

      • 注意:不能有template;

      • import About from "./Home.vue"
        render() {
        return (
        <div class="app">
        <h2>当前计数: { this.counter }</h2>
        <button onClick={ this.increment }>+1</button>
        <button onClick={ this.decrement }>-1</button>
        <About/> //也可以是组件,区分大小写
        </div>
        )
        }
    • setup:

      • 注意:(与vue的render函数一样)

        • 有无<template><jsx />都可以;
        • template中只能有<jsx />,其他元素不会被渲染;
      • <template>
        <jsx/>
        </template>
        setup() {
        ...
        return () => (
        <div class="app">
        <h2>当前计数: { counter.value }</h2>
        <button onClick={ increment }>+1</button>
        <button onClick={ decrement }>-1</button>
        <About/>
        </div>
        )
        }
    • setup语法糖:

      • 注意:(与vue的render函数一样)

        • 必须要有<template><jsx/>
        • template元素里除<jsx/>,还可以有其他元素;
      • <template>
        <jsx/>
        </template>
        const jsx = () => (
        <div class="app">
        <h2>当前计数: { counter.value }</h2>
        <button onClick={ increment }>+1</button>
        <button onClick={ decrement }>-1</button>
        <About/>
        </div>
        )
  • 在下列情形中,可以给任何元素和组件添加进入/离开过渡:
    Section titled “在下列情形中,可以给任何元素和组件添加进入/离开过渡:”
    • 条件渲染; (使用 v-if、v-show)
    • 动态组件;(<component>
    • 组件根节点;
      • 给导入的组件使用包裹transition,添加的class会添加到组件的根节点;
    • 当插入或删除包含在transition组件中的元素时,vue的处理是:
      • 自动监测目标元素是否应用了CSS过渡或者动画,有则在恰当的时机添加/删除 CSS类名;
      • 如果 transition 组件提供了JavaScript钩子函数,这些钩子函数将在恰当的时机被调用;
      • 如果没有找到JavaScript钩子并且也没有检测到CSS过渡/动画,DOM插入、删除操作将会立即执行;
    • transtion主要是针对单个元素或者组件;(transition里只能插入一个元素,多个会报警告)
    • 或是同一时间渲染多个节点中的一个;
    • 如果使用一个没有name的transition:
      • 那么所有的class是以 v- 作为默认前缀;
    • 如果添加了一个name属性:
      • 比如 <transition name="why">
      • 那么所有的class会以 why- 开头;
    • <transition>
      <h2 v-if="isShow">哈哈哈哈</h2>
      </transition>
      .v-enter-from, .v-leave-to {
      opacity: 0;
      }
      .v-enter-to, .v-leave-from {
      opacity: 1;
      }
      .v-enter-active, .v-leave-active {
      transition: all 2s ease;
      }
    • <transition name="why">
      <h2 v-if="isShow">哈哈哈哈</h2>
      </transition>
      .why-enter-active {
      animation: whyAnim 2s ease;
      }
      .why-leave-active {
      animation: whyAnim 2s ease reverse;
      }
      @keyframes whyAnim {
      0% {
      opacity: 0;
      }
      50% {
      opacity: 0.5;
      }
      100% {
      opacity: 1;
      }
      }

1.5、同时设置过渡和动画(一般不设置)

Section titled “1.5、同时设置过渡和动画(一般不设置)”
    • number类型:同时设置进入和离开的过渡时间;

      • <transition :duration="2000">...</transition>
    • object类型:分别设置进入和离开的过渡时间;

      • <transition name="cjf" :duration="{ enter: 1000, leave: 1000}"></transition>
    • 只有设置的时间比transition和animation动画的时间短才有效果,会打断原先动画的执行;
    • 只设置duration的时间是不生效的,要在css里也设置;
  • 两个元素之间切换的时候,进入和离开动画是同时发生的,会出现位置问题:

    • 如果不希望同时执行进入和离开动画,需要设置mode:
      • in-out: 新元素先进行过渡,完成之后当前元素过渡离开;
      • out-in: 当前元素先进行过渡,完成之后新元素过渡进入;
    • <transition mode="out-in"></transition>
    • 动态组件的切换:

      • <transition mode="out-in">
        <component :is="isShow? Home: About"></component>
        </transition>
        //setup语法糖,is要被动态绑定,值不能为字符串;
        import About from "./components/about.vue"
        import Home from "./components/home.vue"
  • 使得首次渲染的时候会执行动画;

  • <transition appear>...</transition>
    // 不需要设置appear动画,会自动执行设置好的动画
  • <transition-group> 有如下的特点:

    • 默认情况下,它不会渲染一个元素的包裹器,但是你可以指定一个元素并以 tag 属性进行渲染;
    • 过渡模式不可用,因为我们不再相互切换特有的元素;
    • 内部元素总是需要提供唯一的 key 属性值;
  • CSS 过渡的类将会应用在内部的元素中,而不是这个组/容器本身;

  • <template>
    <div class="app">
    <button @click="addNumber">添加数字</button>
    <button @click="removeNumber">删除数字</button>
    <button @click="shuffleNumber">打乱数字</button>
    <transition-group tag="div" name="why"> // 可以添tag属性来添加包裹元素
    <template v-for="item in nums" :key="item"> // key不能有任何重复
    <span>{{ item }}</span>
    </template>
    </transition-group>
    </div>
    </template>
    <script setup>
    import { reactive, ref } from 'vue';
    import { shuffle } from "underscore";
    const nums = ref([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
    const addNumber = () => {
    nums.value.splice(randomIndex(), 0, nums.value.length)
    }
    const removeNumber = () => {
    nums.value.splice(randomIndex(), 1)
    }
    const shuffleNumber = () => {
    nums.value = shuffle(nums.value)
    }
    const randomIndex = () => {
    return Math.floor(Math.random() * nums.value.length)
    }
    </script>
    <style scoped>
    span {
    margin-right: 10px;
    display: inline-block;
    }
    .why-enter-from, .why-leave-to {
    opacity: 0;
    transform: translateY(30px);
    }
    .why-enter-to, .why-leave-from {
    opacity: 1;
    transform: translateY(0);
    }
    .why-enter-active, .why-leave-active {
    transition: all 2s ease;
    }
    .why-leave-active {
    position: absolute; // 删除动画和其他元素移动动画是同时进行的,删除的元素会占位,因此其他元素无法移动,需要给删除元素删除时添加绝对定位
    }
    /* 针对其他移动的阶段需要的动画 */
    .why-move {
    transition: all 2s ease;
    }
    </style>
  • // 用来创建了一个Depend对象,用来管理对于对象属性的变化需要监听的响应函数:
    class Depend {
    constructor() {
    // 使用Set集合,使得收集到的相同函数只存留一个
    this.reactiveFns = new Set()
    }
    // 响应式依赖的收集
    depend() {
    if (reactiveFn) { // 该判断是为了防止把赋值给变量的null添加到Set集合;
    this.reactiveFns.add(reactiveFn)
    }
    }
    notify() {
    this.reactiveFns.forEach(fn => {
    fn()
    })
    }
    }
    // 设置一个专门执行响应式函数的一个函数
    let reactiveFn = null
    function watchFn(fn) {
    reactiveFn = fn
    fn()
    reactiveFn = null // 不设置为null的话,若有两个不同的对象,对第一个对象进行修改的过程中会把后一个函数添加到集合里接着遍历,会把下一个函数也执行;
    }
    // 对象的依赖管理,管理不同对象的不同依赖关系;
    // 封装一个函数: 负责通过obj的key获取对应的Depend对象;
    const objMap = new WeakMap()
    function getDepend(obj, key) {
    // 1.根据对象obj, 找到对应的map对象
    let map = objMap.get(obj)
    if (!map) {
    map = new Map()
    objMap.set(obj, map)
    }
    // 2.根据key, 找到对应的depend对象;
    let dep = map.get(key)
    if (!dep) {
    dep = new Depend()
    map.set(key, dep)
    }
    return dep
    }
    // 封装函数,针对所有的对象都可以变成响应式对象;
    // 方案一: Object.defineProperty() -> Vue2
    function reactive(obj) {
    // 遍历所有的key,并且通过属性存储描述符来监听属性的获取和修改;
    Object.keys(obj).forEach(key => {
    let value = obj[key]
    // 监听对象属性的变化以及收集相关的依赖;
    Object.defineProperty(obj, key, {
    set: function(newValue) {
    value = newValue
    const dep = getDepend(obj, key)
    // 调用属性对应的dep对象里的所有函数
    dep.notify()
    },
    get: function() {
    // 找到对应的obj对象的key对应的dep对象
    const dep = getDepend(obj, key)
    // 依赖收集
    dep.depend()
    return value
    }
    })
    })
    return obj
    }
    // ========================= 业务代码 ========================
    // 其他对象跟以下写法相同
    const obj = reactive({
    name: "why",
    age: 18,
    address: "广州市"
    })
    watchFn(function() {
    console.log(obj.name)
    console.log(obj.age)
    })
    // 修改name
    console.log("--------------")
    obj.age = 20
  • // 用来创建了一个Depend对象,用来管理对于对象属性的变化需要监听的响应函数:
    class Depend {
    constructor() {
    // 使用Set集合,使得收集到得相同函数只存留一个
    this.reactiveFns = new Set()
    }
    // 响应式依赖的收集
    depend() {
    if (reactiveFn) {
    this.reactiveFns.add(reactiveFn)
    }
    }
    notify() {
    this.reactiveFns.forEach(fn => {
    fn()
    })
    }
    }
    // 设置一个专门执行响应式函数的一个函数
    let reactiveFn = null
    function watchFn(fn) {
    reactiveFn = fn
    fn()
    reactiveFn = null
    }
    // 对象的依赖管理,管理不同对象的不同依赖关系;
    // 封装一个函数: 负责通过obj的key获取对应的Depend对象;
    const objMap = new WeakMap()
    function getDepend(obj, key) {
    // 1.根据对象obj, 找到对应的map对象
    let map = objMap.get(obj)
    if (!map) {
    map = new Map()
    objMap.set(obj, map)
    }
    // 2.根据key, 找到对应的depend对象;
    let dep = map.get(key)
    if (!dep) {
    dep = new Depend()
    map.set(key, dep)
    }
    return dep
    }
    // 封装函数,针对所有的对象都可以变成响应式对象;
    // 方式二: new Proxy() -> Vue3
    function reactive(obj) {
    // 监听对象属性的变化以及收集相关的依赖;
    return new Proxy(obj, {
    set: function(target, key, newValue, receiver) {
    Reflect.set(target, key, newValue, receiver)
    const dep = getDepend(target, key)
    // 调用属性对应的dep对象里的所有函数;
    dep.notify()
    },
    get: function(target, key, receiver) {
    // 找到对应的obj对象的key对应的dep对象;
    const dep = getDepend(target, key)
    // 依赖收集;
    dep.depend()
    // 返回代理对象对应的属性值;
    return Reflect.get(target, key, receiver)
    }
    })
    }
    // ========================= 业务代码 ========================
    const obj = reactive({
    name: "why",
    age: 18,
    address: "广州市"
    })
    watchFn(function() {
    console.log(obj.name)
    console.log(obj.age)
    console.log(obj.age)
    })
    // 修改name
    console.log("--------------")
    obj.age = 20
****