VUE3.x

vue3搭项目的配套方案: vite+ vue3 +pinia + vue-router4

一. 概述

  • vue3最大的改进: 用proxy重写响应式
  • ES6, 即ES2015

越灵活的东西, 越不可控

扩展性更好, 代码更不容易组织. 追求什么, 就要牺牲点别的, 达到平衡

vue3 的改进

1. TS重构

        TypeScript 带类型的js, 先编译转换为js, 再? 让程序员的代码质量更高, 尤其是大型项目

2. 用JS的新特性Proxy重写响应式

        vue2使用 Object.defineProperty, 这个api 会把data上每个数据都改写, 只能操作存在的属性, 而新增与删除属性时会丢失响应式, 性能缺点

        vue2, 代码组织的非常零散, 被打乱, 分不到不同位置

3. 性能的提升

4. Composition API

        代码复用与模块可维护性

vue2和vue3区别

  1. 实现响应式原理的api不同
  2. watch上, 做了很大的优化和改进. 支持flush:post 控制回调的执行时机, 在DOM更新后调用回调

vue2和vue3的响应式原理区别, 哪个更好

defineProperty, Proxy

  • Proxy 是ES6中新增的一个特性,实现的过程是在目标对象之前设置了一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写
  • Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应
  • 采用Proxy劫持整个对象,相比Vue2.0中的Object.defineProperty,能够动态监听添加的属性,可以监听数组的索引和length属性

选项式与组合式

初学用选项式api(上限高), 而组合式api更自由灵活 但写不好就跟石山一样(下限低)

选项式

this指向proxy代理

this 指向当前组件实例proxy对象

通过对vm代理对象(proxy) 操作,

来操作代理的目标对象(源对象)

  <!-- 1. 引入vue.js -->
  <script src="../node_modules//vue/dist/vue.global.js"></script>
</head>
<body>
  <div id="app">
    <button @click="handleClick">点击了{{count}}下
    </button>
  </div>

  <script>
    const { createApp } = Vue
    // 组件实例
    const vm = createApp({
      data() {
        return {
          count: 0,
        }
      },
      methods: {
        handleClick() {
          // this 指向当前组件实例proxy对象
          console.log(this === vm)  //true
          this.count++ // 对vm代理对象操作
        },
      },
    }).mount('#app')
    console.log(vm) // 1. 代理的目标对象(源对象)
  </script>
</body>

组合式

二. 起步

1 安装

初始化 npm init -y

安装3.2版本 npm i vue

2 使用

常规写法 与 模块化写法

  • app.use: 注册插件, 返回app应用实例(可以一直.use().use().component().mount('#app')
  • app.component: 注册全局组件 返回app应用实例
  • app.mount: 挂载, 返回根组件实例: mount放在最后
  • app.unmount: 卸载 返回空

常规写法

<head>
  <title>Document</title>
  <!-- 1. 引入vue.js -->
  <script src="../node_modules//vue/dist/vue.global.js"></script>
</head>
<body>
  <!-- 2. 准备页面容器 -->
  <div id="app">
    <button @click="count++">
      点击了{{count}}下</button>
  </div>

  <!-- 3. 创建app实例 -->
  <script>
    // 对象解构语法
    const { createApp } = Vue

    // 返回一个app实例
    const app = createApp({
      // optionAPI  写在这里
      data() {
        return {
          count: 0,
        }
      },
    })
    // console.log(app)
    //返回组件实例
    const vm = app.mount('#app') 
    // console.log(vm)
  </script>
</body>

缺点: 在模块外不能直接打印访问vm实例

模块化写法: 

<head>
  <title>Document</title>
</head>
<body>
  <!-- 2. 准备页面容器 -->
  <div id="app">
    <button @click="count++">点击了{{count}}下</button>
  </div>

  <!-- 3. 创建app实例 -->
  <script type="module">
    // <!-- 1. 引入vue.js -->
    import { createApp } from '../node_modules/vue/dist/vue.esm-browser.js'

    const vm = createApp({
      data() {
        return {
          count: 0,
        }
      },
    }).mount('#app')
  </script>
</body>

三. setup语法 (选项式)

  • setup:是一个函数
  • setup中返回的属性, 方法可以直接在模板中使用

reactive处理对象类型

ref处理普通类型

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <script src="../node_modules//vue/dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      <h3>在setup中导出的变量和函数可以直接在模板中使用</h3>
      <p>姓名:{{name}}</p>
      <p>{{sayHi()}}</p>
    </div>

    <script>
      // 增加了一个setup的选项
      // setup:是一个函数
      // 函数的返回值是源对象
      // setup中返回的属性, 方法可以直接在模板中使用
      const { createApp } = Vue

      // 组合式API
      //  - 引用类型 reactive
      //  - 普通类型 ref
      // 使用API: 响应式API(对象), 转为响应式数据
      // 3.0
      const vm = createApp({
        setup() {
          // 定义一个变量
          const name = 'xiaoming'
          // 定义一个方法
          function sayHi() {
            console.log('hello')
            return 'hello'
          }
          // 将props, data, methods, computed, watch整合到一个对象(源对象)导出
          // createApp在内部基于setup导出的源对象创建一个代理对象
          // 再创建这一个对象的代理对象, vm实例通过这个源对象 创建一个代理对象
          return {
            name,
            sayHi,
          }
        },
      }).mount('#app')
      console.log(vm)
    </script>
  </body>
</html>

响应式原理 💡

响应式: 数据改变时视图更新.

reactive+effect 实现响应式

  • reactive API: 将一个普通对象转换为 响应式对象(proxy)
  • effect: 接收一个函数作为参数, vue3.2的版本可以解构出来

步骤:

一. 将数据转换成响应式数据

二. 注册副作用函数(建立属性和该副作用函数之间的联系). 读取属性

三. 自动: 当属性改变时, 跟属性关联的副作用函数自动执行

effect是一个底层的实现, 一般不直接使用, watch computed都是基于它实现的

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Document</title>
    <script src="../node_modules/vue/dist//vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      console.log(Vue)
      const { reactive, effect } = Vue
      // reactive API: 将一个普通对象转换为 响应式对象(proxy)
      // effect: 接收一个函数作为参数, vue3.2的版本可以解构出来
      // 监听数据改变, 类似watcher观察者
      const obj = {
        name: 'xiaoming',
        age: 20,
      }
      // 一. 将数据转换成响应式数据
      const p = reactive(obj) //proxy实例
      console.log(p)

      // 后续操作, 都是操作响应式对象
      // p.name = 'xiaomei'
      // console.log(obj)

      // 二. 注册副作用函数(建立属性和该副作用函数之间的联系)
      effect(() => {
        app.innerHTML = p.name //触发proxy的自定义getter函数, 放到一个副作用桶里
      })
      effect(() => {
        console.log(p.name)
      })

      // 三. 自动: 当属性改变时, 跟属性关联的副作用函数会自动执行
    </script>
  </body>
</html>

在setup中使用reactive

组合式api 不要和 选项式 api混用

reactive 只能对引用类型(对象, 数组) 进行代理

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">
      计数器{{obj.count}}
      <button @click="handleClick">+1</button>
    </div>

    <script>
      const { createApp, reactive } = Vue
      
      createApp({
        setup() {
          // 定义数据
          // 将一个普通对象 转换成 代理对象
          const obj = reactive({
            count: 0,
          })
          // 定义方法
          function handleClick() {
            obj.count++
          }
          
          return { obj, handleClick }
        },
      }).mount('#app')
    </script>
  </body>
</html>

ref

基本数据类型

也可以对对象进行包装, 对对象进行包装的时候不需要.value

ref是对reactive的再次封装

  • ref 通用性更好, 组件实例只能用ref
  • ref 效率高?
  • 使用上js中需要手动 .value

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
		<!--   1. 引入vue.js   -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
		<!--   2. 定义模板   -->
    <div id="app">
			<!--   加法器     -->
      <!-- 在模板中, vue自动解析.value -->
      <input type="text" v-model.number="num1" /> +
      <input type="text" v-model.number="num2" />
      <button @click="handleClick"> = </button>
      <input type="text" v-model.number="result" />
    </div>

    <script>
      const { createApp, ref } = Vue
      // 3. 创建实例并挂载
      const vm = createApp({
        setup() {
          // 使用ref, 将普通数据 转换成 响应式数据
          // 通过ref定义的数据, 在使用时, 需要借助.value
          const num1 = ref(0)
          const num2 = ref(0)
          const result = ref(0)
          // console.log(ref(''))

          function handleClick() {
            result.value = num1.value + num2.value
          }

          return { num1, num2, result, handleClick }
        },
      }).mount('#app')
    </script>
  </body>
</html>

对象

解构了也不会丢失

唯一的缺点就是要用.value

const { createApp, ref } = Vue
createApp({
  setup() {
    const stu = ref({
      name: 'xiaoming',
      age: 20,
    })
    
    console.log(stu.value) //是proxy
    console.log(stu.value.name) //只有第一层用.value
  },
}).mount('#app')

计算属性

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 1. 导入vue -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
    <!-- 2. 创建模板 -->
    <div id="app">
      姓: <input type="text" v-model="data.lastName" />
      名: <input type="text" v-model="data.firstName" /><br />
      全名: {{fullName}}
    </div>

    <!-- 3. 创建实例并挂载 -->
    <script>
      const { createApp, reactive, computed } = Vue
      const vm = createApp({
        setup() {
          // 定义数据
          const data = reactive({
            lastName: '',
            firstName: '',
          })
          // 定义计算属性
          //  computed: {fullName: () => {}}
          const fullName = computed(() => {
            return data.lastName + data.firstName
          })

          return { data, fullName }
        },
      }).mount('#app')
    </script>
  </body>
</html>

watch

1. watch默认深层次监听

2. 回调触发时机: 可以通过flush: post 控制为 在DOM更新之后调用回调

watch(source, callback, {
  flush: 'post'
})

3. 回调默认是懒执行. 通过watchEffect, 立即执行回调(组合式). 选项式不同

watchEffect(
  async () => {
  const response = await fetch(url.value)
  data.value = await response.json()
})

这个例子中,回调会立即执行。在执行期间,它会自动追踪 url.value , 变化时,回调再次执行

普通数据类型可以使用 newValue, oldValue

引用数据类型会有问题

监听整个对象 (常用, 一般用这个)

监听某个属性时需要用函数(不常用)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 1. 导入vue -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
    <!-- 2. 创建视图容器 -->
    <div id="app">
      <h3>计数器: {{count}}</h3>
      <button @click="handleClick">+1</button>
      <hr />
      <h2>watch通过reactive定义的数据</h2>
      <h3>消息: {{data.msg}}</h3>
      <button @click="data.msg += '~'">+~</button>
    </div>

    <!-- 3. 创建实例并挂载 -->
    <script>
      const { createApp, ref, reactive, watch } = Vue
      const vm = createApp({
        setup() {
          /* 计数器模块 */
          const count = ref(0)
          function handleClick() {
            count.value++
          }
          // 定义watch
          watch(count, (newValue, oldValue) => {
            console.log('count的值变化了...')
            console.log(oldValue)
            console.log(newValue)
          })

          /* 消息模块, 默认是深层次监听 */
          const data = reactive({
            msg: 'hello',
            a: {
              b: {
                c: 'ccc',
              },
            },
          })
          // 定义watch
          watch(data, () => {
            console.log('监听整个data')
          })
          // 监听具体对象的某个属性
          watch(
            () => data.a.b.c,
            () => {
              console.log('监听data某个属性:data.a.b.c')
            }
          )
          return { count, data, handleClick }
        },
      }).mount('#app')
    </script>
  </body>
</html>

练习-购物车案例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 1. 导入vue.js -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
    <style>
      table,
      tr,
      th,
      td {
        border: 1px solid #ddd;
        border-collapse: collapse;
        border-spacing: 0;
      }
      .cart {
        min-width: 650px;
      }
      .cart caption {
        font-weight: 700;
      }
      .cart th {
        padding: 5px 20px;
        background-color: #f7f7f7;
        color: #666;
      }
      .cart td {
        padding: 5px 20px;
      }
      .cart .total {
        text-align: right;
      }
    </style>
  </head>
  <body>
    <!-- 2. 创建模板容器 -->
    <div id="app">
      <!-- vue3 购物车案例 -->
      <table class="cart">
        <caption>
          购物车
        </caption>
        <!-- 表头 -->
        <tr>
          <th v-for="item in listItems">{{item}}</th>
        </tr>
        <!-- 列表项, 书 -->
        <tr v-for="(book, index) in books" :key="book.id">
          <td>{{index+1}}</td>
          <td>{{book.name}}</td>
          <td>{{book.publicDate}}</td>
          <td>{{toPrice(book.price)}}</td>
          <td>
            <button @click="book.quantity==1? 0 : book.quantity--">-</button>
            {{book.quantity}}
            <button @click="book.quantity++">+</button>
          </td>
          <td><button @click="books.splice(index,1)">移除</button></td>
        </tr>
        <tr v-show="sumPrice" class="total">
          <td colspan="6">总价格:{{toPrice(sumPrice)}}</td>
        </tr>
        <tr v-show="!sumPrice">
          <td colspan="6">当前购物车中暂无商品</td>
        </tr>
      </table>
    </div>

    <!-- 3. 创建实例对象并挂载 -->
    <script>
      const { createApp, ref, computed } = Vue
      const vm = createApp({
        setup() {
          // 购物车表头项
          const listItems = ref([
            '',
            '书籍名称',
            '出版日期',
            '价格',
            '购买数量',
            '操作',
          ])
          // 购物车中的商品-书
          const books = ref([
            {
              id: 1,
              name: '算法导论',
              publicDate: '2006-9',
              price: 85,
              quantity: 1,
            },
            {
              id: 2,
              name: 'Unix编程艺术',
              publicDate: '2006-2',
              price: 59,
              quantity: 1,
            },
            {
              id: 3,
              name: '编程珠玑',
              publicDate: '2008-10',
              price: 39,
              quantity: 1,
            },
          ])

          // 给计算属性传参方法: 返回一个带参的匿名函数
          const toPrice = computed(() => {
            return (price) => '¥' + price.toFixed(2)
          })

          // 计算属性-计算商品总价
          const sumPrice = computed(() => {
            let sum = 0
            books.value.forEach((book) => {
              sum += book.price * book.quantity
            })
            return sum
          })

          return { listItems, books, toPrice, sumPrice }
        },
      }).mount('#app')
    </script>
  </body>
</html>

生命周期

基础

1. 不执行mount方法, 所有的生命周期不会执行

2. setup阶段和create阶段都是初始化.

        setup是组合式API的入口(setup阶段用来初始化组合式API).

        create阶段是选项式API的入口(create阶段初始化选项式API)

3. setup 是最先执行的生命周期函数, 所以setup中的this是没有初始化的

在vue3中, 最常用的生命周期是 onMounted

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 1. 导入vue -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
    <!-- 2. 创建页面容器 -->
    <div id="app"></div>

    <!-- 3. 创建实例 -->
    <script>
      const app = Vue.createApp({
        setup(props, ctx) {
          // 在setup中, this没有意义, 指向window
          console.log(this)
        },
        beforeCreate() {
          console.log('optionsAPI...beforeCreate')
          console.log(this)
        },
        created() {
          console.log('optionsAPI...created')
          // 在这里, 才可以访问this
          console.log(this)
        },
      }).mount('#app')
    </script>
  </body>
</html>

onMounted

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <!-- 1. 导入vue -->
    <script src="../node_modules/vue/dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app">{{count}}</div>

    <script>
      const { createApp, onMounted, ref } = Vue

      createApp({
        setup(props, ctx) {
          const count = ref(0)

          // 模拟从后端请求数据库
          // 使用组合式API, 允许注册多个(onMounted)函数
          onMounted(() => {
            // 模拟axios发请求
            setTimeout(() => {
              count.value = 100
            }, 1000)
          })
          // 模拟从后端请求数据库
          onMounted(() => {
            console.log(111)
          })

          return { count }
        },
      }).mount('#app')
    </script>
  </body>
</html>

四. 工程化

vue3 两种打包方式

vue-cli (商业项目, 大型项目. )

vue-cli图形化界面 vue ui

没安装的包 可以 npm add 包名

与vue2的区别查看 (待总结)

        main.js

        router

        store

vue create vue3-project

vite (还要进一步完善)

  • npm create vite '项目名'
  • npm i 安装依赖
  • npm run dev 运行

使用组件, 父子传参, 获取组件对象

  • 父传子

                接收: defineProps

  • 子传父

                defineEmits

  • 在setup中获取子组件对象

                defineExpose

                ref="test" ref(null)

                onMounted --- 要导入

                test.value

components/Test.vue

<template>
  <div>
    <h4>我是test子组件的标题</h4>
    从父组件接收的数据: {{ icon }}
    <button @click="handleClick">点击进行子传父</button>
  </div>
</template>

<script setup>
/* 父传子 */
const props = defineProps({
  icon: {
    type: String,
    required: true,
  },
}) 
//props接受的数据是 只读 readOnly
console.log(props.icon)

/* 子传父 */
// defineEmits要接收一下
const emit = defineEmits(['send'])
function handleClick() {
  emit('send', '这是子向父传递的参数')
}

/* 在父组件setup中获取子组件对象 */
const name = 'xiaoming'
defineExpose({
  name,
})
</script>

 App.vue

<template>
  <!-- 在vue3的模板中允许多个根节点 -->
  <h3>我是父组件的标题</h3>
  <div>
    {{ count }}
  <button @click="handleClick">+1</button>
  </div>

  <!-- 父传子 -->
  <!-- 在setup中获取组件对象 -->
  <my-test :icon="user" @send="onSend" ref="test"></my-test>
</template>

<script setup>
import { ref, onMounted } from 'vue'
/* 1. setup中导入的组件可以直接使用 */
import MyTest from './components/Test.vue'
// components: {MyTest: MyTest}

/* 2. setup中定义的响应式数据, 函数可以直接在模板中使用 */
const count = ref(0)
function handleClick() {
  count.value++
}

/* 父传子 */
const user = ref('user')
/* 子传父 */
function onSend(msg) {
  console.log(msg)
}

/* 在父组件setup中获取子组件对象 */
// 定义一个变量, 变量名跟子组件的ref属性值一致(此处ref并非ref API)
// 此时test就是子组件对象
const test = ref(null)
onMounted(() => {
  // 如果要使用子组件对象. 只能在子组件挂载完成后, 在父组件的onMounted方法中使用
  console.log('拿到组件对象', test.value)
  // 如果要访问子组件对象中的属性. 需要在子组件中通过defineExpose暴露
  console.log('拿到组件对象的属性', test.value.name)
})
</script>

五. hooks函数

作用:

  • 解耦
  • 复用

将特定功能封装到一个单独的文件usePoint.js

封装到文件里, 方便复用

抽离功能: 在src下建hooks文件夹

先在.vue里写出来, 再拆到hooks里

在组件中使用:

<template>
  <h3>TheDemo子组件</h3>
  <p>
    x的坐标: {{ point.x }} 
    -- y的坐标: {{ point.y }}	</p>
</template>

<script setup>
import usePoint from '@/hooks/usePoint'

const point = usePoint()
</script>

 src/hooks/usePoint.js

import { reactive, onMounted, onBeforeUnmount } from 'vue'

// Hooks函数通常返回一个 响应式对象
// 按照惯例,组合式函数名以“use”开头
export default function usePoint() {
  // 被组合式函数封装和管理的状态
  const point = reactive({
    x: 0,
    y: 0,
  })
  function onClick(e) {
    point.x = e.pageX
    point.y = e.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期上, 来启动和卸载副作用
  // 挂载完成: 绑定事件
  onMounted(() => {
    window.addEventListener('click', onClick)
  })
  // 卸载之前, 解绑事件
  onBeforeUnmount(() => {
    window.removeEventListener('click', onClick)
  })

  // 通过返回值暴露所管理的状态
  return point
}

六. pinia

  • state保存数据
  • actions修改数据

.vue

<template>
  <h3>TheDemo子组件</h3>
  <button @click="handleClick">+1</button>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
  
// 操作pinia数据
function handleClick() {
  // counter.count++ //直接操作count属性
  counter.increment() //调用方法操作
}
</script>

stores/counter.js

// 1. 导入defineStore
import { defineStore } from 'pinia'

// 2. 导出use函数
export const useCounterStore = defineStore('counter', {
  // 设置state和actions
  state() {
    return {
      count: 0,
    }
  },
  actions: {
    increment() {
      // 直接使用this来访问state中的属性
      this.count++
    },
  },
})
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

假以时日♪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值