vue3基础知识

202 阅读9分钟

一、jsx 结构

1. 结构代码示例

  • setup() 返回一个渲染函数
  • 渲染函数返回 JSX 结构
  • JSX 最终被编译为 h() 函数调用 index.jsx
export default {
  setup() {
    // 返回一个函数,该函数返回JSX元素
    return () => <div>123</div>;
  }
};

2. setup 说明

setup() 关键特性解析:

1. 渲染函数模式

  • setup() 返回的是一个函数 而不是对象
  • 这个返回的函数就是组件的 渲染函数
  • 当组件需要渲染时,Vue 会自动调用这个函数

2. JSX 语法

  • 使用 JSX 语法 (<div>123</div>) 描述 UI
  • 相当于 Vue 的  h()  函数 :h('div', {}, '123')
  • 需要 Babel 插件(如 @vue/babel-plugin-jsx)支持

3. 与常规 setup 的区别

常规 setup本例 setup
返回数据对象给模板使用直接返回渲染函数
需要单独的 <template> 部分渲染逻辑全在 JS 中
适合简单组件适合需要灵活控制渲染的场景

4. 实际等效代码

import { h } from 'vue';

export default {
 setup() {
   // 使用 h 函数实现相同效果
   return () => h('div', {}, '123');
 }
}

3. h 函数

在 Vue 中,h 函数是创建虚拟 DOM 节点(VNode)的核心函数。它的名字来源于"hyperscript",意为"生成 HTML 结构的脚本"。让我们深入理解它的作用和用法:

核心概念

1. 虚拟 DOM (Virtual DOM) :

  • JavaScript 对象表示的真实 DOM 的轻量级副本
  • Vue 通过比较新旧 VNode 来高效更新真实 DOM
  • 避免直接操作真实 DOM,提高性能

2. h 函数的作用:

  • 创建描述 DOM 节点的 JavaScript 对象(VNode)
  • 接收参数定义节点类型、属性和子元素
  • JSX 语法最终会被编译为 h 函数调用

函数签名

function h(
  type: string | Component,  // HTML 标签名或 Vue 组件
  props?: object,            // 属性/事件对象
  children?: string | Array<VNode> // 文本内容或子 VNode
): VNode

使用示例

场景JSX 写法h 函数等价写法
文本元素<div>Hello</div>h('div', 'Hello')
带属性的元素<div class="box"></div>h('div', { class: 'box' })
嵌套元素<div><span>Hi</span></div>h('div', [h('span', 'Hi')])
组件使用<MyComponent prop1="value" />h(MyComponent, { prop1: 'value' })

在 Vue 3 中的特殊作用

// JSX
<button onClick={handler}>Click</button>

// 编译后
h('button', { onClick: handler }, 'Click')

为什么需要 h 函数?

  • 跨平台能力:相同的 VNode 可在不同环境渲染(Web、Canvas、Native)
  • 渲染优化:通过 diff 算法最小化 DOM 操作
  • 灵活组合:可用纯 JavaScript 表达任意 UI 结构
  • 类型安全:在 TypeScript 中提供更好的类型支持

二、常用指令

1. v-model

import { reactive } from "vue"

export default {
  setup() {
    // 创建响应式状态对象, 
    const state = reactive({
      msg: 'hello',
    })
    
    // 返回渲染函数(JSX)
    return () =>
      <div>
        {/* `state.msg` 是响应式属性,值变化会自动更新 UI */}
        <el-input v-model={state.msg} />
      </div>
  }
}

双向数据绑定原理

<el-input
  modelValue={state.msg}
  onUpdate:modelValue={(value) => state.msg = value}
/>

相当于:

  1. 将 state.msg 作为初始值传递给输入框
  2. 当输入框值变化时,通过事件更新 state.msg
  3. state.msg 变化触发重新渲染

2. v-if

使用逻辑与 (&&) 运算符

import {  reactive } from "vue"
export default {

  setup () {
    const state = reactive({
      condition: true,
    })
    return () => <>
    {/* 使用逻辑与 (&&) 运算符 */}
     {state.condition && <div>条件为真时显示</div>}
    </>
  }
}

使用三元表达式

import { reactive } from "vue"
export default {

  setup () {
    const state = reactive({
      condition: false,
    })
    return () => <>
      {state.condition
        ? <div>条件为真时显示</div>
        : <div>条件为假时显示</div>
      }
    </>
  }
}

使用函数

import { reactive } from "vue";

export default {
  setup() {
    const state = reactive({
      condition: false,
      anotherCondition: true,
    });

    const renderContent = () => {
      if (state.condition) {
        return <div>复杂条件分支1</div>;
      } else if (state.anotherCondition) {
        return <div>复杂条件分支2</div>;
      } else {
        return <div>默认内容</div>;
      }
    };

    return () => (
      <>
        {renderContent()}
      </>
    );
  }
};

3. v-show

import { reactive } from "vue";

export default {
  setup() {
    const state = reactive({
      condition: true,
    });

    return () => (
      <>
        <div v-show={state.condition}>233333354</div>
      </>
    );
  }
};

4. v-for

在 Vue 3 的 JSX 中,没有直接的 v-for 指令,但我们可以使用 JavaScript 的数组方法(如 map())来实现相同的功能。下面我将详细介绍如何在 JSX 中实现 v-for 功能,并提供完整的示例代码。

v-for 在 JSX 中的实现原理

v-for 的本质是遍历数组或对象并渲染多个元素。在 JSX 中,我们使用 map() 方法来实现:

索引值的使用

import { reactive } from "vue";

export default {
  setup () {
   const items = reactive([
      { id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
      { id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
      { id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
      { id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
      { id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
    ]);

    return () => (
      <>
        {items.map((item, index) => (
          <div key={item.id} class="item">
            <span class="index">{index + 1}.</span>
            <span class="name">{item.name}</span>
          </div>
        ))}
      </>
    );
  }
};

嵌套循环

import { reactive } from "vue";

export default {
  setup () {
    const categories = reactive([
      {
        id: 1,
        name: "Fruits",
        products: [
          { id: 1, name: "Apple" },
          { id: 2, name: "Banana" },
          { id: 3, name: "Orange" }
        ]
      },
      {
        id: 2,
        name: "Vegetables",
        products: [
          { id: 4, name: "Carrot" },
          { id: 5, name: "Broccoli" },
          { id: 6, name: "Tomato" }
        ]
      },
      {
        id: 3,
        name: "Dairy",
        products: [
          { id: 7, name: "Milk" },
          { id: 8, name: "Cheese" },
          { id: 9, name: "Yogurt" }
        ]
      }
    ]);

    return () => (
      <>
        {categories.map(category => (
          <div key={category.id}>
            <h3>{category.name}</h3>
            {category.products.map(product => (
              <div key={product.id} class="product">
                {product.name}
              </div>
            ))}
          </div>
        ))}
      </>
    );
  }
};

遍历对象

import { reactive } from "vue";

export default {
  setup () {
    const user = reactive({
      name: "John Doe",
      age: 30,
      email: "johndoe@example.com",
      address: "123 Main St, Anytown, USA",
      phone: "555-555-1234"
    });

    return () => (
      <>
        {Object.entries(user).map(([key, value]) => (
          <div key={key} class="property">
            <span class="key">{key}:</span>
            <span class="value">{value}</span>
          </div>
        ))}
      </>
    );
  }
};

范围迭代(模拟 v-for="n in 10")

export default {
  setup () {

    return () => (
      <>
        {Array.from({ length: 10 }).map((_, index) => (
          <div key={index} class="item">
            项目 {index + 1}
          </div>
        ))}
      </>
    );
  }
};

5. v-for 与 v-if 结合使用

推荐做法:先过滤再渲染

import { reactive } from "vue";

export default {
  setup () {
    const items = reactive([
      { id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
      { id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
      { id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
      { id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
      { id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
    ]);

    return () => (
      <>
        {items
          .filter(item => item.price > 1)
          .map(item => (
            <div key={item.id}>{item.name}</div>
          ))
        }
      </>
    );
  }
};

复杂场景:使用条件渲染

import { reactive } from "vue";

export default {
  setup () {
    const items = reactive([
      { id: 1, name: 'Apple', price: 2.5, category: 'fruit' },
      { id: 2, name: 'Banana', price: 1.2, category: 'fruit' },
      { id: 3, name: 'Carrot', price: 0.8, category: 'vegetable' },
      { id: 4, name: 'Milk', price: 2.0, category: 'dairy' },
      { id: 5, name: 'Bread', price: 1.5, category: 'bakery' },
    ]);

    return () => (
      <>
        {items.map(item => (
          item.price > 1 ? (
            <div key={item.id} class="expensive">
              {item.name} (${item.price})
            </div>
          ) : (
            <div key={item.id} class="cheap">
              {item.name} (${item.price})
            </div>
          )
        ))}
      </>
    );
  }
};

6. v-for 性能优化技巧

1. 使用唯一且稳定的 key

// 好 - 使用唯一ID

<div key={item.id}>...</div>

// 避免 - 使用索引

<div key={index}>...</div>

2. 虚拟滚动优化长列表

import { VirtualList } from 'vue-virtual-scroller';

<VirtualList
 items={largeList.value}
 itemSize={50}
>
 {item => (
  <div key={item.id} class="item">
     {item.name}
   </div>
 )}
</VirtualList>

3. 提取子组件

// ItemComponent.jsx
export default defineComponent({
 props: ['item'],
 setup(props) {
   return () => (
     <div class="item">
       <h4>{props.item.name}</h4>
       <p>${props.item.price}</p>
     </div>
   );
 }
});

// 在父组件中使用
{items.value.map(item => (
 <ItemComponent key={item.id} item={item} />
))}

与模板语法对比

模板语法JSX 实现
<div v-for="item in items" :key="item.id">{items.map(item => <div key={item.id}>)}
<div v-for="(item, index) in items">{items.map((item, index) => <div key={item.id}>)}
<div v-for="(value, key) in object">{Object.entries(object).map(([key, value]) => <div>)}
<div v-for="n in 10">{Array.from({length: 10}).map((_, n) => <div key={n}>)}

三、事件

1. 父子组件事件

注意: 父组件需要 on +子组件 emit 上报的方法

// 父组件:onChangePageTitle 
<Header title={state.msg} onChangePageTitle={onChangePageTitle}/>

// 子组件: changePageTitle上报
 <el-button onClick={() => emit('changePageTitle', '用户页面')}>子组件事件上报</el-button>

四、父子组件

1. 子组件

接收数据

  1. setup (props, { emit, slots }) 接收 props
  2. 书写 props 对象
export default {
 name: 'ChildComponent',

 // 定义组件的 props
 props: {
  // message prop,类型为 String,默认值为 '默认消息'
   message: {
     type: String,
    default: '默认消息'
  },
  // items prop,类型为 Array,默认值为空数组
  items: {
    type: Array,
     default: () => []
   }
 },

    // setup 函数是 Vue 3 Composition API 的入口
    setup (props, { emit, slots }) {
  
    }
};
import { ref } from 'vue';

export default {
  name: 'ChildComponent',

  // 定义组件的 props
  props: {
    // message prop,类型为 String,默认值为 '默认消息'
    message: {
      type: String,
      default: '默认消息'
    },
    // items prop,类型为 Array,默认值为空数组
    items: {
      type: Array,
      default: () => []
    }
  },
  // 声明组件可以发出的事件
  emits: ['childEvent'], // 声明自定义事件

  // setup 函数是 Vue 3 Composition API 的入口
  setup (props, { emit, slots }) {
    // 定义子组件内部状态
    const childMessage = ref('子组件内部状态');
    const inputValue = ref('');

    // 定义一个方法,用于向父组件发送数据
    function sendToParent () {
      emit('childEvent', {
        childMessage: childMessage.value,
        inputValue: inputValue.value,
        timestamp: Date.now()
      })
    }
    
    // 返回渲染函数
    return () =>
      <div class="child-container" style="margin-top: 20px">
        <h3>子组件</h3>
        <p>接收父组件Props: {props.message}</p>

        {/* 渲染传入的 items 列表 */}
        <ul>
          {props.items.map((item, index) => (
            <li key={index}>{item}</li>
          ))}
        </ul>

        {/* 输入框,双向绑定 inputValue */}
        <input
          type="text"
          v-model={inputValue.value}
          placeholder="输入内容回传父组件"
        />
        <button onClick={sendToParent}>发送数据到父组件</button>

        {/* 渲染插槽内容,如果没有提供默认插槽内容则显示 '默认插槽内容' */}

        <div class="slot-area">
          {slots.default ? slots.default({ text: childMessage.value }) : '默认插槽内容'}
        </div>
      </div>
  }
};

2. 父组件下发数据

import {  ref } from 'vue';
import ChildComponent from './child.jsx';

export default {
  name: 'ParentComponent',

  setup () {
    // 父组件状态
    const parentMessage = ref('来自父组件的消息');
    const childData = ref(null);
    const items = ref(['Apple', 'Banana', 'Cherry']);

    // 接收子组件事件的方法
    function handleChildEvent  (data) {
      console.log('收到子组件数据:', data);
      childData.value = data;
    };

    // 更新列表方法
    function updateItems() {
      items.value = [...items.value, 'New Item ' + Date.now()];
    };

    return () =>
      <div class="parent-container">
        <h2>父组件</h2>
        <p>父组件消息: {parentMessage.value}</p>
        <p>来自子组件的数据: {JSON.stringify(childData.value)}</p>

        {/* 基础Props传递 */}
        <ChildComponent
          message={parentMessage.value}
          items={items.value}
          onChildEvent={handleChildEvent}
        />

        <button onClick={updateItems}>更新列表</button>
      </div>

  }
};

五、watch 方法

1. 用 Composition API 的 watch

import { ref, watch } from 'vue';

export default {
  setup() {
    // 使用 ref 创建一个响应式引用,初始值为 0
    const count = ref(0);
    
    // 使用 watch 监听 count 的变化
    watch(count, (newValue, oldValue) => {
      // 当 count 值发生变化时,打印旧值和新值
      console.log(`count changed from ${oldValue} to ${newValue}`);
    });
    
    // 返回渲染函数,生成包含按钮和计数的 JSX 元素
    return () => (
      <div>
        {/* 点击按钮时,count 值增加 */}
        <button onClick={() => count.value++}>Increment</button>
        {/* 显示当前的 count 值 */}
        <p>Count: {count.value}</p>
      </div>
    );
  }
};

2. 监听多个源

import { ref, watch } from 'vue';

export default {
  setup () {
    // 创建一个响应式引用 count,初始值为 0
    const count = ref(0);
    // 创建一个响应式引用 name,初始值为 'Vue'
    const name = ref('Vue');

    // 使用 watch 函数同时监听 count 和 name 的变化
    watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
      // 当 count 或 name 值发生变化时,打印它们的旧值和新值
      console.log(`Count: ${oldCount} -> ${newCount}`);
      console.log(`Name: ${oldName} -> ${newName}`);
    });

    // 返回渲染函数,生成包含按钮、输入框和显示计数及名称的 JSX 元素
    return () =>
      <div>
        {/* 点击按钮时,count 值增加 */}
        <button onClick={() => count.value++}>Increment</button>
        {/* 输入框,双向绑定 name 值 */}
        <input vModel={name.value} />
        {/* 显示当前的 count 和 name 值 */}
        <p>Count: {count.value}, Name: {name.value}</p>
      </div>

  }
};

3. watchEffect 和 watch 的区别

在 Vue 3 的 Composition API 中,watchEffect 和 watch 都是用于响应式地执行副作用代码的函数,但它们有一些关键区别:

主要区别

特性watchwatchEffect
依赖收集方式显式指定要监听的数据源自动收集回调函数中的响应式依赖
初始执行默认不立即执行(可配置)立即执行
参数获取可以访问新旧值无法直接获取旧值
使用场景需要精确控制监听的数据源时依赖关系简单或需要立即执行时

六、computed 方法

1. 用 Composition API 的 computed

import { computed, reactive } from 'vue'

export default {
  setup() {

    const state = reactive({
      count: 0
    })
    
    // 计算属性
    const doubleCount = computed(() => {
      return state.count * 2
    })
    
    return () => (
      <div>
        <p>Count: {state.count}</p>
        // 需要通过 `.value` 访问 computed 值
        <p>Double Count: {doubleCount.value}</p>
        <button onClick={() => state.count++}>Increment</button>
      </div>
    )
  }
}

2. computed 缓存

Computed 属性的缓存机制是 Vue 响应式系统的核心特性之一.

说明

  • 只有点击"修改 count"按钮时,控制台才会打印"计算 doubleCount"
  • 点击"修改 unrelatedValue"按钮不会触发重新计算,因为计算属性不依赖它
import { computed, ref } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const unrelatedValue = ref(100)
    
    // 这个计算属性只依赖于 count
    const doubleCount = computed(() => {
      console.log('计算 doubleCount') // 仅当 count 变化时才会打印
      return count.value * 2
    })
    
    return () => (
      <div>
        <p>Count: {count.value}</p>
        <p>Double Count: {doubleCount.value}</p>
        <button onClick={() => count.value++}>修改 count (会触发重新计算)</button>
        <button onClick={() => unrelatedValue.value++}>修改 unrelatedValue (不会触发重新计算)</button>
      </div>
    )
  }
}

2. 注意事项

  1. 访问 computed 值:在 Composition API 中,需要通过 .value 访问 computed 值
  2. JSX 中的 v-model:在 JSX 中使用 vModel 而不是 v-model
  3. 性能优化:computed 属性会缓存计算结果,只有当依赖的响应式数据变化时才会重新计算

七、hooks

1. hooks 结构

命名规范

  • 始终以 use 开头(如 useDarkMode
  • 文件名使用驼峰命名(useEventListener.js

2. 新建 hooks.js

import { reactive } from 'vue' // 引入Vue的reactive函数,用于创建响应式状态
import { defineFormConfig } from '@common/hooks' // 引入定义表单配置的函数
import { waybillConfirmBindService } from '@/request' // 引入请求服务,用于处理运单绑定
import { ElMessage } from 'element-plus' // 引入Element Plus的消息提示组件

// 定义一个用于绑定运单的函数
export function useBindWaybill(pageListRef) {
  // 创建响应式状态
  const state = reactive({
    isShow: false, // 控制对话框显示的状态
    currentRow: {}, // 当前选中的行数据
  })

  // 定义表单配置
  const formConfig = defineFormConfig({
    configList: [
      { label: '联单编号', prop: 'waybillId', required: true }, // 联单编号字段,必填
    ],
    labelWidth: 80, // 表单标签的宽度
  })

  // 确认按钮的回调函数
  function onConfirm({ form }) {
    // 调用绑定运单的服务
    return waybillConfirmBindService.bind_waybill({id: form.id, waybillId: form.waybillId}).then(() => {
      ElMessage.success('编辑联单编号成功') // 显示成功消息
      pageListRef.value.search() // 刷新页面列表
    })
  }

  // 返回对象,包含组件所需的状态和方法
  return {
    csoStte: state, // 返回响应式状态
    // 打开绑定运单对话框
    openBindWaybill(currentRow) {
      state.currentRow = currentRow // 设置当前选中的行数据
      state.isShow = true // 显示对话框
    },

    // 渲染绑定运单对话框
    renderBindWaybill() {
      return (
        <DialogForm
          v-model={state.isShow} // 绑定对话框显示状态
          title="编辑联单编号" // 对话框标题
          formConfig={formConfig} // 表单配置
          width={560} // 对话框宽度
          form={state.currentRow} // 表单数据
          onConfirm={onConfirm} // 确认按钮的回调函数
        />
      )
    },
  }
}

3. 引入 hooks

import { waybillConfirmService } from '@/request' // 引入运单确认服务
import { defineFormConfig, defineTableConfig } from '@common/hooks' // 引入定义表单和表格配置的函数
import { useBindWaybill } from './hooks' // 引入绑定运单的自定义hook
import { ref } from 'vue' // 引入Vue的ref函数,用于创建响应式引用

export default {
  setup() {
    const pageListRef = ref() // 创建一个响应式引用,用于引用PageList组件
    const { openBindWaybill, renderBindWaybill } = useBindWaybill(pageListRef) // 使用绑定运单的hook,获取打开对话框和渲染对话框的函数

    // 定义表格配置
    const tableConfig = defineTableConfig({
      columns: [ // 列配置
        { label: '合同编号', prop: 'contractNo', width: 160 },
        { label: '产废企业', prop: 'cfqymc', width: 200 },
        { label: '废物代码', prop: 'wasteCode', width: 160 },
        { label: '废物类型', prop: 'wasteItemType' },
        { label: '废物名称', prop: 'wasteName' },
        { label: '联单编号', prop: 'waybillId', width: 160, showOverflowTooltip: true },
        { label: '废物产生单位/单元', prop: 'unit', width: 160 },
        {
          label: '重量(KG)', prop: 'quantity', width: 120, render({ row }) {
            return row.quantity ? row.quantity * 1000 : '-' // 如果有重量数据,显示重量乘以1000,否则显示'-'
          }
        },
        { label: '设备类型', prop: 'emergencyEquipment' },
        { label: '有害成分', prop: 'harmfulComponents' },
        { label: '主要成分', prop: 'mainComponents' },
        { label: '危害性', prop: 'wftx' },
        { label: '物理形态', prop: 'wfxt' },
        { label: '运输人员', prop: 'driver' },
      ],
      handlerWidth: 100, // 操作列的宽度
      handlerSlot({ row, renderBtns, editRow, deleteRow }) { // 操作列的自定义插槽
        return <el-button link type="primary" onClick={() => openBindWaybill(row)}>编辑联单编号</el-button> // 点击按钮打开编辑联单编号对话框
      },
    })

    // 渲染函数
    return () => <PageList
      ref={pageListRef} // 绑定PageList组件的引用
      tableConfig={tableConfig} // 绑定表格配置
      dialogConfig={{ width: 500 }} // 对话框配置
      operateConfig={{ hiddenDelete: true, hiddenAdd: true }} // 隐藏删除和添加按钮
      api={{ ...waybillConfirmService }} // 绑定API服务
    >
      {{
        operate: ({ renderBtns, search, checkedList }) => <>
          {/* checkedList: 多选中的数据 */}
          {renderBtns()} // 渲染操作按钮
        </>,
        default: () => renderBindWaybill() // 渲染绑定运单对话框
      }}
    </PageList>
  }
}