普通视图

发现新文章,点击刷新页面。
今天 — 2025年7月7日技术

6个你必须掌握的「React Hooks」实用技巧✨

作者 Sun_light
2025年7月7日 11:27

在前端开发的世界里,React 一直以其组件化、声明式的特性深受开发者喜爱。而自从 React 16.8 推出 Hooks(钩子函数)以来,函数组件的能力得到了极大提升,开发体验也变得更加丝滑。今天,我们就一起来深入了解 React Hooks 的核心用法,结合具体例子,轻松掌握它们的精髓,让你的代码更优雅、更高效!


一、什么是 Hooks?为什么要用 Hooks?

Hooks 是 React 官方为函数组件提供的一组“钩子”API,让你无需编写 class 组件,也能拥有状态管理、生命周期等强大功能。它们让代码更简洁、逻辑更清晰,极大提升了开发效率。

简单来说:

  • 以前:只有 class 组件才能用 state、生命周期
  • 现在:函数组件 + Hooks = 一切皆有可能!

二、常用 Hooks 的详解

1. useState —— 让你的变量“活”起来

作用:为函数组件引入响应式状态变量。

用法

import React, { useState } from 'react';

function Counter() {
  // count 是当前状态,setCount 是修改状态的方法
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>当前计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>点我+1</button>
    </div>
  );
}

小结

  • useState 返回一个数组,包含当前状态和修改方法。
  • 每次调用 set 方法,组件会自动重新渲染。

小贴士

  • 可以用来管理表单输入、开关状态等各种场景。

2. useEffect —— 副作用管理小能手

作用:处理副作用,比如数据请求、订阅、手动操作 DOM 等。

用法

import React, { useState, useEffect } from 'react';

function DataLoader() {
  const [data, setData] = useState([]);

  useEffect(() => {
    // 模拟请求数据
    fetch('http://localhost:3000/data')
      .then(res => res.json())
      .then(json => setData(json));
  }, []); // 依赖数组为空,表示只在组件挂载时执行一次

  return (
    <ul>
      {data.map(item => <li key={item.id}>{item.name}</li>)}
    </ul>
  );
}

小结

  • 依赖数组为空时,只在组件首次加载时执行。
  • 依赖数组有值时,只有依赖变化才会执行。
  • 可以返回一个清理函数,用于组件卸载时执行。

小贴士

  • 用于数据请求、事件监听、定时器等场景。

3. useLayoutEffect —— 同步副作用的利器

作用:与 useEffect 类似,但它会在 DOM 更新后、浏览器绘制前同步执行。

用法

import React, { useLayoutEffect, useRef } from 'react';

function LayoutDemo() {
  const divRef = useRef();

  useLayoutEffect(() => {
    divRef.current.style.color = 'red';
  }, []);

  return <div ref={divRef}>我是红色的文字</div>;
}

小结

  • 适合需要同步读取布局、强制同步修改 DOM 的场景。
  • 一般情况下,优先用 useEffect,只有特殊需求才用 useLayoutEffect

4. useReducer —— 复杂状态管理的好帮手

作用:当状态逻辑复杂或多个状态相互关联时,推荐用 useReducer

用法

import React, { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      return state;
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, { count: 0 });

  return (
    <div>
      <p>计数:{state.count}</p>
      <button onClick={() => dispatch({ type: 'increment' })}>加1</button>
      <button onClick={() => dispatch({ type: 'decrement' })}>减1</button>
    </div>
  );
}

小结

  • useReducer 接收 reducer 函数和初始 state。
  • 通过 dispatch 派发 action,reducer 返回新的 state。
  • 适合表单、复杂交互等场景。

5. useRef —— 获取 DOM 或保存变量

作用:获取 DOM 元素引用,或保存一个在组件生命周期内不会变化的变量。

用法

import React, { useRef } from 'react';

function InputFocus() {
  const inputRef = useRef();

  const focusInput = () => {
    inputRef.current.focus();
  };

  return (
    <div>
      <input ref={inputRef} placeholder="点按钮聚焦我" />
      <button onClick={focusInput}>聚焦输入框</button>
    </div>
  );
}

小结

  • useRef 返回一个可变的 ref 对象,.current 属性指向 DOM。
  • 也可用于保存定时器 id、上一次的 props/state 等。

6. useContext —— 跨组件传值不再烦恼

作用:实现跨组件(祖孙、兄弟)间的数据共享,避免层层 props 传递。

用法

import React, { createContext, useContext } from 'react';

const ThemeContext = createContext('light');

function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button style={{ background: theme === 'dark' ? '#333' : '#eee' }}>主题按钮</button>;
}

function App() {
  return (
    <ThemeContext.Provider value="dark">
      <ThemedButton />
    </ThemeContext.Provider>
  );
}

小结

  • 先用 createContext 创建上下文,再用 Provider 提供数据。
  • 子组件用 useContext 获取数据,随时随地都能用!

三、实战小案例:数据列表的增删查

结合上面 Hooks,假如我们要做一个数据列表页面,支持加载、搜索、删除功能:

import React, { useState, useEffect } from 'react';

function DataList() {
  const [data, setData] = useState([]);
  const [keyword, setKeyword] = useState('');

  // 加载数据
  useEffect(() => {
    fetch(`http://localhost:3000/data?name=${keyword}`)
      .then(res => res.json())
      .then(json => setData(json));
  }, [keyword]);

  // 删除数据
  const handleDelete = id => {
    fetch(`http://localhost:3000/data/${id}`, { method: 'DELETE' })
      .then(() => setData(data.filter(item => item.id !== id)));
  };

  return (
    <div>
      <input
        value={keyword}
        onChange={e => setKeyword(e.target.value)}
        placeholder="搜索..."
      />
      <ul>
        {data.map(item => (
          <li key={item.id}>
            {item.name}
            <button onClick={() => handleDelete(item.id)}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

是不是很简单?只用 useStateuseEffect 就能搞定大部分需求啦!😄


四、总结

React Hooks 让函数组件变得更强大、更灵活。只要掌握了 useStateuseEffectuseLayoutEffectuseReduceruseRefuseContext 这 6 个常用钩子,你就能轻松应对绝大多数开发场景。建议大家多动手实践,遇到问题多查文档,慢慢你也会成为 Hooks 大师!💪

深度解析JavaScript中的call方法实现:从原理到手写实现的完整指南

2025年7月7日 11:23

前言:那些年我们踩过的this指向坑

大家在写JavaScript的时候,是不是经常被this指向搞得头疼?明明写的是同一个函数,在不同地方调用结果却完全不一样。今天我们就来彻底搞懂call方法的原理,并且手写一个完整的call实现。

相信看完这篇文章,你不仅能理解call的工作机制,还能在面试中自信地手写出来!

一、call方法到底是个什么东西?

1.1 call的本质:手动指定函数内部的this

var name = "Trump"
function gretting(...args){
    return `hello, I am ${this.name}.`;
}
const lj = {
    name: "王子"
}
console.log(gretting.call(lj)); // "hello, I am 王子."

核心理解: call方法让我们能够"借用"别人的方法,就像是给函数临时换了个身份证一样。

1.2 call、apply、bind的爱恨情仇

这三兄弟经常被放在一起比较,但它们各有特色:

方法 执行时机 参数传递方式 使用场景
call 立即执行 一个个传递 参数确定且不多时
apply 立即执行 数组形式传递 参数不确定或很多时
bind 延迟执行 一个个传递 需要保存函数供后续使用
// call: 参数一个个传
console.log(gretting.call(lj, 18, '抚州'));

// apply: 参数用数组包装
console.log(gretting.apply(lj, [18, '抚州']));

// bind: 返回新函数,延迟执行
const fn = gretting.bind(lj, 18, '抚州');
setTimeout(() => {
    console.log(fn()); // 1秒后执行
}, 1000);

二、深入call的工作原理

2.1 call是怎么改变this指向的?

很多人觉得call很神奇,其实原理很简单。我们先看一个例子:

var obj = {
    name: 'Tom',
    sayHello: function() {
        return `Hello, I am ${this.name}`;
    }
};

console.log(obj.sayHello()); // "Hello, I am Tom"

当我们调用obj.sayHello()时,this自然指向obj。call的原理就是:临时把函数挂载到目标对象上,调用完后再删除

2.2 严格模式下的特殊情况

"use strict"
function gretting() {
    return `hello, I am ${this.name}.`;
}

// 严格模式下,传入null或undefined会报错
console.log(gretting.call(null)); // TypeError
console.log(gretting.call(undefined)); // TypeError

重要提醒: 在严格模式下,如果传入null或undefined,this不会被转换为window对象,而是保持原值,这可能导致错误。

三、手写call方法:从零到一的实现过程

3.1 基础框架搭建

既然call是所有函数都能使用的方法,那它一定在Function.prototype上:

Function.prototype.myCall = function(context, ...args) {
    // 实现逻辑
};

3.2 参数处理:边界情况的优雅处理

Function.prototype.myCall = function(context, ...args) {
    // 处理context为null或undefined的情况
    if (context === null || context === undefined) {
        context = window; // 非严格模式下指向window
    }
    
    // 确保调用者是函数
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall was called on non-function');
    }
};

设计思考: 这里的边界处理体现了健壮性编程的重要性。在实际开发中,用户可能传入各种奇怪的参数,我们需要优雅地处理这些情况。

3.3 核心实现:Symbol的巧妙运用

Function.prototype.myCall = function(context, ...args) {
    if (context === null || context === undefined) {
        context = window;
    }
    
    if (typeof this !== 'function') {
        throw new TypeError('Function.prototype.myCall was called on non-function');
    }
    
    // 使用Symbol确保属性名唯一,避免覆盖原有属性
    const fnKey = Symbol('fn');
    
    // 将函数挂载到context上
    context[fnKey] = this;
    
    // 调用函数并收集结果
    const result = context[fnKey](...args);
    
    // 清理:删除临时添加的属性
    delete context[fnKey];
    
    // 返回执行结果
    return result;
};

技术亮点解析:

  1. Symbol的使用:ES6的Symbol类型确保了属性名的唯一性,避免了覆盖context原有属性的风险
  2. 动态属性添加:利用JavaScript对象的动态性,临时给context添加方法
  3. 及时清理:执行完毕后立即删除临时属性,避免污染原对象

3.4 完整测试:验证我们的实现

// 测试用例
function gretting(...args) {
    console.log(args); // 查看参数
    return `hello, I am ${this.name}.`;
}

var obj = {
    name: 'Tom',
    fn: function() {} // 已有属性,测试是否会被覆盖
};

// 测试我们的实现
console.log(gretting.myCall(obj, 1, 2, 3));
// 输出:[1, 2, 3]
// 输出:"hello, I am Tom."

// 验证原有属性没有被破坏
console.log(typeof obj.fn); // "function"

四、深度思考:为什么要这样设计?

4.1 JavaScript动态性的体现

我们的实现充分利用了JavaScript的动态特性:

  • 动态属性添加context[fnKey] = this
  • 动态方法调用context[fnKey](...args)
  • 动态属性删除delete context[fnKey]

这种设计让JavaScript具有了极大的灵活性,但也要求开发者更加小心地处理边界情况。

4.2 函数式编程思想的体现

// 函数作为一等公民
const boundFunction = gretting.bind(obj);

// 高阶函数的应用
function createBoundFunction(fn, context) {
    return function(...args) {
        return fn.myCall(context, ...args);
    };
}

设计哲学: call方法体现了JavaScript中"函数是一等公民"的设计理念,函数可以被传递、绑定、组合,这为函数式编程提供了基础。

五、实际应用场景:call在实战中的妙用

5.1 数组方法的借用

// 类数组对象借用数组方法
function example() {
    // arguments是类数组对象,没有数组方法
    const argsArray = Array.prototype.slice.call(arguments);
    console.log(argsArray); // 真正的数组
}

example(1, 2, 3); // [1, 2, 3]

5.2 继承中的应用

function Parent(name) {
    this.name = name;
}

function Child(name, age) {
    // 调用父类构造函数
    Parent.call(this, name);
    this.age = age;
}

const child = new Child('小明', 18);
console.log(child.name); // "小明"

5.3 函数柯里化的实现

function curry(fn) {
    return function curried(...args) {
        if (args.length >= fn.length) {
            return fn.call(this, ...args);
        } else {
            return function(...nextArgs) {
                return curried.call(this, ...args, ...nextArgs);
            };
        }
    };
}

六、性能考虑与最佳实践

6.1 性能对比

// 直接调用(最快)
obj.method();

// call调用(稍慢)
method.call(obj);

// apply调用(最慢,因为要处理数组参数)
method.apply(obj, args);

6.2 最佳实践建议

  1. 优先使用直接调用:如果可以直接调用,就不要使用call
  2. 参数少时用call:参数确定且不多时,call比apply性能更好
  3. 避免频繁使用:在性能敏感的场景中,避免在循环中频繁使用call
  4. 合理使用bind:如果需要多次调用同一个绑定函数,使用bind预先绑定

七、常见陷阱与调试技巧

7.1 箭头函数的特殊性

const arrowFunc = () => {
    console.log(this); // 箭头函数的this无法被call改变
};

const obj = { name: 'test' };
arrowFunc.call(obj); // this仍然是定义时的上下文

重要提醒: 箭头函数的this是词法绑定的,无法通过call、apply、bind改变。

7.2 调试技巧

Function.prototype.debugCall = function(context, ...args) {
    console.log('调用函数:', this.name || 'anonymous');
    console.log('绑定对象:', context);
    console.log('传入参数:', args);
    
    return this.myCall(context, ...args);
};

八、总结与展望

通过这篇文章,我们不仅理解了call方法的工作原理,还亲手实现了一个完整的call方法。这个过程让我们深入理解了:

  1. JavaScript的动态特性:对象属性的动态添加和删除
  2. 函数式编程思想:函数作为一等公民的体现
  3. 边界处理的重要性:健壮代码的必要条件
  4. Symbol的实际应用:解决属性名冲突的优雅方案

技术成长感悟: 手写call方法不仅仅是一个面试题,更是理解JavaScript核心机制的重要途径。当你能够从原理层面理解这些基础方法时,你对JavaScript的理解就上升到了一个新的层次。

这不仅仅是一个技术实现,更是一次深入JavaScript内核的探索之旅。希望这篇文章能够帮助你在前端开发的道路上走得更远、更稳!


相关代码示例都可以在项目中找到,建议大家动手实践,加深理解。记住,最好的学习方式就是动手写代码!

Vue 3 中的组件通信与组件思想详解

2025年7月7日 11:23

在现代前端开发中,Vue 作为一款渐进式 JavaScript 框架,凭借其简洁、高效和易上手的特性,深受广大开发者喜爱。尤其是 Vue 3 的推出,带来了 Composition API、性能优化以及更好的 TypeScript 支持,使得构建大型应用变得更加得心应手。

本文将围绕 Vue 3 中的组件通信方式组件化开发的思想 进行详细讲解,并结合生活中的实际案例,帮助你更深入地理解这些概念。


一、组件是什么?为什么需要组件?

1.1 组件的定义

在 Vue 中,组件是可复用的 UI 单元。它可以是一个按钮、一个输入框、一个导航栏,甚至是一个完整的页面结构。每个组件都拥有自己的模板(template)、逻辑(script)和样式(style)。

类比生活:你可以把组件想象成乐高积木。每一块积木都有固定的形状和功能,但通过不同的组合,可以拼出各种复杂的结构。组件也是一样,通过合理的拆分与组合,可以构建出复杂而灵活的用户界面。

1.2 组件化开发的优势

  • 可维护性高:组件独立性强,修改一处不影响全局。
  • 可复用性强:一个组件可以在多个地方重复使用。
  • 开发效率高:多人协作时,不同人可以负责不同的组件模块。
  • 结构清晰:代码结构层次分明,便于理解和调试。

二、Vue 3 中的组件通信方式详解

组件之间并不是完全孤立的,它们往往需要进行数据交互。Vue 提供了多种组件通信方式,适用于不同的场景。

我们将从最基础的父子组件通信讲起,逐步过渡到跨层级通信和全局状态管理。


2.1 父子组件通信:props + emits

这是最常见也是最基础的一种通信方式。

示例场景:

假设我们正在做一个“购物车”系统。父组件是 CartView,子组件是 CartItem,我们需要把商品信息传递给子组件,并且当用户点击删除按钮时,子组件要通知父组件删除该商品。

<!-- CartItem.vue -->
<template>
  <div class="cart-item">
    <span>{{ product.name }}</span>
    <button @click="removeProduct">删除</button>
  </div>
</template>

<script setup>
const props = defineProps({
  product: {
    type: Object,
    required: true
  }
})

const emit = defineEmits(['remove'])

function removeProduct() {
  emit('remove', product.id)
}
</script>
<!-- CartView.vue -->
<template>
  <CartItem v-for="item in cartItems" :key="item.id" :product="item" @remove="handleRemove" />
</template>

<script setup>
import { ref } from 'vue'
import CartItem from './CartItem.vue'

const cartItems = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' }
])

function handleRemove(productId) {
  cartItems.value = cartItems.value.filter(item => item.id !== productId)
}
</script>

生活类比:

这就像父母告诉孩子要做某件事(比如“去拿快递”),孩子做完后会告诉父母:“我拿回来了”。


2.2 子传父:emits 的高级用法

除了基本的事件触发,还可以传递参数。例如上面例子中,子组件在 emit 时传入了 product.id,父组件就可以根据这个 ID 做进一步处理。


2.3 非父子组件通信:provide / inject

有时候我们需要跨越多个层级传递数据,比如主题色、用户信息等全局变量。这时我们可以使用 provideinject

示例场景:

我们有一个网站的主题配置(如深色/浅色模式),希望在整个应用中都能访问到这个配置。

<!-- App.vue -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
</script>
<!-- SomeChildComponent.vue -->
<script setup>
import { inject } from 'vue'
const theme = inject('theme')
</script>

注意:虽然 provide/inject 可以跨级传递数据,但它更像是“祖先传值”,不是响应式的绑定。如果希望实现响应式,建议使用 reactive 或者配合 watch 使用。

生活类比:

这就好比家里的 WiFi 密码写在一张纸上,家里每个人都可以看到并连接。不需要一个个通知,大家都知道怎么连。


2.4 全局状态管理:Pinia

对于大型项目来说,组件之间可能有复杂的通信需求,这时候就需要引入状态管理工具,比如 Vue 官方推荐的 Pinia

示例场景:

我们来模拟一个登录系统,用户登录之后,在多个组件中都需要显示用户名。

步骤一:创建 store
// stores/userStore.js
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', {
  state: () => ({
    username: null
  }),
  actions: {
    login(name) {
      this.username = name
    },
    logout() {
      this.username = null
    }
  }
})
步骤二:在组件中使用
<!-- Login.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()

function handleLogin() {
  userStore.login('Tom')
}
</script>
<!-- Header.vue -->
<script setup>
import { useUserStore } from '@/stores/userStore'
const userStore = useUserStore()
</script>

<template>
  <div v-if="userStore.username">欢迎,{{ userStore.username }}</div>
  <div v-else>请登录</div>
</template>

生活类比:

这就像你家有个记事本,谁想记点什么都可以写上去,别人也能看到。Pinia 就像这个记事本,记录着整个家庭(应用)的状态信息。


2.5 自定义事件总线:mitt

如果你不想使用 Pinia,又需要非父子组件之间的通信,可以使用事件总线库,比如 mitt

安装 mitt:

npm install mitt

创建事件中心:

// eventBus.js
import mitt from 'mitt'
export default mitt()

发送事件:

import eventBus from '@/eventBus'

eventBus.emit('update-cart', newCartData)

接收事件:

import eventBus from '@/eventBus'

eventBus.on('update-cart', (data) => {
  console.log('接收到新的购物车数据:', data)
})

生活类比:

这就像小区广播站,谁想发消息就往广播里喊一声,其他人都能听到。适合轻量级的通信需求。


三、组件设计的最佳实践

3.1 单向数据流原则

  • 数据从父组件流向子组件,避免反向修改 props。
  • 如果子组件需要修改父组件的数据,应该通过 emit 事件通知父组件进行更新。

这就像老师布置作业给学生,学生不能擅自更改题目内容,而是应该反馈给老师说:“我觉得这个题太难了,能不能改一下?”老师再决定是否调整。


3.2 组件命名规范

  • 使用 PascalCase(如 UserProfileCard
  • 文件名建议使用 .vue 结尾,如 UserProfileCard.vue

3.3 组件职责单一原则

一个组件只做一件事。比如不要在一个组件里同时处理表单提交和数据展示,应该拆分成两个组件。


3.4 使用 slots 实现组件插槽

插槽允许我们在组件内部插入任意内容,非常灵活。

<!-- Card.vue -->
<template>
  <div class="card">
    <slot></slot>
  </div>
</template>
<!-- 使用 -->
<Card>
  <h2>标题</h2>
  <p>内容区域</p>
</Card>

生活类比:

这就像一个相框,你可以在里面放任何照片。组件提供的是框架,具体内容由使用者决定。


四、总结:组件化思维的本质

组件化不仅仅是技术上的拆分,更是一种思维方式的转变。它要求我们:

  • 把问题拆解成小块
  • 让每个部分专注做好一件事
  • 通过组合的方式解决大问题

这与生活中解决问题的方式非常相似:

想盖一栋房子?先准备好砖头、水泥、钢筋,然后一步步搭建。而不是直接从头开始垒墙,边垒边设计窗户位置。


五、结语

Vue 3 的组件化开发为我们提供了强大的工具和灵活的机制,使我们能够构建出结构清晰、易于维护、高度可复用的应用程序。掌握好组件通信的各种方式,并理解组件背后的设计哲学,是我们成为优秀 Vue 开发者的关键一步。

希望这篇文章能帮助你更好地理解 Vue 3 的组件通信机制与组件化思想。如果你觉得有用,不妨点赞收藏,也可以分享给你的朋友一起学习!

揭秘 CSS 伪元素:不用加标签也能玩转出花的界面技巧 ✨

作者 呆呆的心
2025年7月7日 11:23

作为前端开发者,你是否经常遇到这样的场景:为了一个小小的箭头、一条特殊的下划线,不得不给 HTML 里塞一堆多余的标签?其实,CSS 伪元素早就为我们准备了更优雅的解决方案!今天就带大家全方位解锁伪元素的用法,看完直接省掉 30% 的冗余标签 🚀

一、伪元素到底是个啥?看完秒懂 🧐

简单说,伪元素就是「不用在 HTML 里写标签,却能像真实元素一样出现在页面上」的神奇存在。伪元素

它能像元素标签一样出现在 DOM 树文档流中,但又不是真的标签,完全依赖 CSS 生存。

举个栗子🌰:你想在按钮前加个小图标,不用写 <span class="icon"></span>,直接用 ::before 就能搞定,HTML 依然干干净净!

核心特性总结
✅ 用 ::before(元素内容前)和 ::after(元素内容后)选中位置
✅ 必须写 content 属性(哪怕值为空 '',否则不生效)
✅ 不占用 HTML 结构,却能参与文档流布局
✅ 本质是 CSS 渲染的「虚拟元素」,右键检查元素能看到它的身影

二、实战案例:这 2 个场景直接封神 🔥

场景 1:一行 CSS 画个「向右箭头」,告别图片引入 🎯

css1 里的代码,用伪元素画箭头简直不要太简单!先上效果拆解:

.box{
  position: relative; /* 父元素相对定位,给伪元素做参考 */
  height: 100px;
  background: #ccc;
  padding: 0 10px;
}

.box::before{
  content: ''; /* 必须有,这里为空 */
  position: absolute; /* 脱离文档流,自由定位 */
  width: 10px;
  height: 10px;
  border: 1px solid #000;
  /* 关键:只保留上边框和右边框,其他设为透明 */
  border-left-color: transparent; 
  border-bottom-color: transparent;
  right: 10px; /* 靠右 */
  top: 45px; /* 垂直居中 */
  transform: rotate(45deg); /* 旋转45度,秒变箭头 */
}

👉 原理:利用边框绘制三角形,再通过旋转得到箭头。比起引入图片或 SVG,这种方式体积小、可直接改颜色 / 大小,适配各种场景!

场景 2:hover 时的「动态下划线」,比传统边框高级 10 倍 ✨

看 css2 里的「查看更多」按钮效果,伪元素 + 过渡动画直接拉满质感:

.container .more {
  position: relative; /* 伪元素依赖父元素定位 */
  display: inline-block;
  background: #007bff;
  color: #fff;
  text-decoration: none;
  transition: all 0.3s ease; /* 整体过渡效果 */
}

/* 下划线用伪元素实现 */
.container .more::before {
  content: "start"; /* 内容可自定义,也能为空 */
  position: absolute;
  bottom: 0;
  left: 0;
  width: 100%;
  height: 20px;
  background: #f00;
  transform: scaleY(0); /* 初始隐藏(Y轴缩放为0) */
  transform-origin: top right; /* 动画起点:右上角 */
  transition: transform 0.3s ease; /* 仅缩放动画过渡 */
}

/* hover时显示下划线,动画反向播放 */
.container .more:hover::before {
  transform: scaleY(1); /* 显示下划线(Y轴缩放为1) */
  transform-origin: bottom left; /* 动画终点:左下角 */
}

动画拆解
未 hover 时,下划线被 scaleY(0) 压缩成一条线;hover 时从右上角开始,顺着左下角方向「展开」,整个过程丝滑不生硬 —— 这就是伪元素 + 过渡动画的魔力!

三、伪元素的「潜规则」,踩坑必看 ⚠️

  1. content 不能少
    哪怕你不需要内容,也要写 content: '',否则伪元素根本不会显示(这是反复强调的重点)。
  2. 必须设置定位吗?
    不一定!但如果想让伪元素「跟着父元素走」,建议父元素用 position: relative,伪元素用 absolute,这样定位更灵活(比如箭头案例就靠这招)。
  3. 能被 JS 选中吗?
    不行哦❌!伪元素是 CSS 渲染的产物,不在真实 DOM 中,JS 无法直接操作,所以别想用 document.querySelector 选中它~
  4. 伪元素 vs 真实标签
    伪元素适合简单装饰(箭头、下划线、图标),复杂交互还是得用真实标签。毕竟它本质是「样式层」的东西,过度依赖会让代码可读性下降。

四、总结:为什么要学伪元素?💡

  • 少写标签:一个 ::before 搞定的事,何必加一堆 <i> <span>?HTML 结构直接清爽一半!

  • 样式与结构分离:装饰性的效果全放 CSS 里,HTML 只负责语义,后期改样式根本不用动结构。

  • 提升开发效率:画箭头、加下划线这类高频需求,用伪元素几行代码就搞定,再也不用切图了!

最后放个小彩蛋🥚:伪元素不仅能做「下划线、向右箭头」,其实它还能做更多 —— 比如自定义列表符号、清除浮动、模拟对话框三角标… 快去试试吧!

觉得有用的话,点赞收藏走一波,下次写界面直接翻出来抄作业~👇

Vue组件通信方式详解

作者 莫空0000
2025年7月7日 11:21

一、父子组件通信

1. Props(父→子)

父组件通过props向子组件传递数据,子组件需要显式声明接收的props

// 父组件
<ChildComponent :title="pageTitle" :content="pageContent" />

// 子组件
props: {
  title: String,          // 基础类型检查
  content: {             // 高级配置
    type: String,
    required: true,
    default: 'Default content'
  }
}

2. 自定义事件(子→父)

子组件通过$emit触发事件,父组件通过v-on监听

// 子组件
methods: {
  submitForm() {
    this.$emit('form-submit', { 
      username: this.username,
      password: this.password
    })
  }
}

// 父组件
<ChildComponent @form-submit="handleSubmit" />

methods: {
  handleSubmit(formData) {
    // 处理表单数据
  }
}

3. style和class

父组件传递的样式和类会自动合并到子组件根元素

// 父组件
<ChildComponent 
  class="theme-dark" 
  style="margin: 10px; padding: 20px;"
/>

// 子组件渲染结果
<div class="child-component theme-dark" style="margin: 10px; padding: 20px;">
  <!-- 子组件内容 -->
</div>

4. attribute(非Prop特性)

attribute(非Prop特性)是指父组件传递给子组件,但子组件没有在props中显式声明的属性。这些特性会自动绑定到子组件的根元素上。

// 父组件
<ChildComponent data-id="123" aria-label="Description" />

// 子组件(未在props中声明这些属性)
<div data-id="123" aria-label="Description">
  <!-- 子组件内容 -->
</div>
  1. this.$attrs访问: 在子组件中,可以通过this.$attrs访问所有非Prop特性:

    mounted() {
      console.log(this.$attrs) // 包含所有未在props中声明的属性
    }
    
  2. 使用场景

    • 当需要将多个属性批量传递给子组件时
    • 创建高阶组件(HOC)时传递未知属性
    • 需要手动控制属性绑定位置时
  3. 禁用自动继承: 在子组件选项中设置inheritAttrs: false可以禁用自动绑定到根元素:

    export default {
      inheritAttrs: false,
      // ...
    }
    
  4. 手动绑定示例

    <template>
      <div>
        <input v-bind="$attrs" />
      </div>
    </template>
    
  5. 包含的特性

    • 普通HTML属性(如id, class, data-*等)
    • 自定义属性(非props声明的)
    • 事件监听器(可通过$listeners单独访问)
// 父组件
<ChildComponent 
  data-id="123" 
  aria-label="description"
  custom-attr="value"
/>

// 子组件
export default {
  props: ['customAttr'], // 显式声明customAttr
  mounted() {
    console.log(this.$attrs) 
    // 输出: { "data-id": "123", "aria-label": "description" }
    // customAttr不会出现在$attrs中,因为它已在props中声明
  }
}

5. $listeners

包含父组件中所有v-on事件监听器(不含.native修饰符)

// 父组件
<ChildComponent @focus="handleFocus" @input="handleInput" />

// 子组件
<template>
  <div>
    <input v-on="$listeners" />
  </div>
</template>

6. v-model(双向绑定)

语法糖,相当于value属性+input事件

// 父组件
<ChildComponent v-model="message" />

// 等价于
<ChildComponent :value="message" @input="message = $event" />

// 子组件实现
props: ['value'],
methods: {
  updateValue(newVal) {
    this.$emit('input', newVal)
  }
}

7. .sync修饰符

双向绑定的语法糖(Vue 2.3+)

// 父组件
<ChildComponent :title.sync="doc.title" />

// 子组件更新
this.$emit('update:title', newTitle)

8. parent 和 children

直接访问父/子实例(慎用)

// 子组件访问父组件方法
this.$parent.parentMethod()

// 父组件访问第一个子组件
this.$children[0].childMethod()

9. slots 和 scopedSlots

内容分发相关API

// 具名插槽
<template v-slot:header>
  <h1>标题</h1>
</template>

// 作用域插槽
<template v-slot:item="slotProps">
  <span>{{ slotProps.item.text }}</span>
</template>

11. ref

给子组件添加引用标识后访问

// 父组件
<ChildComponent ref="childComp" />

methods: {
  callChildMethod() {
    this.$refs.childComp.childMethod()
  }
}

二、跨组件通信

1. Provide 和 Inject

祖先组件提供数据,后代组件注入

// 祖先组件
provide() {
  return {
    appContext: this.appData,
    changeContext: this.updateContext
  }
}

// 后代组件
inject: ['appContext', 'changeContext'],
methods: {
  updateData() {
    this.changeContext(newData)
  }
}

2. router(路由参数)

通过路由传递参数

// 传递参数
this.$router.push({
  name: 'user',
  params: { userId: '123' },
  query: { token: 'abc' }
})

// 获取参数
const userId = this.$route.params.userId
const token = this.$route.query.token

3. Vuex(状态管理)

集中式状态管理

// store定义
const store = new Vuex.Store({
  state: { count: 0 },
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

// 组件中使用
computed: {
  count() {
    return this.$store.state.count
  }
},
methods: {
  increment() {
    this.$store.commit('increment')
  },
  incrementAsync() {
    this.$store.dispatch('incrementAsync')
  }
}

4. store模式(简单状态管理)

小型应用替代Vuex的简单方案

// store.js
export const store = {
  debug: true,
  state: {
    user: null
  },
  setUser(newUser) {
    if (this.debug) console.log('setUser triggered with', newUser)
    this.state.user = newUser
  }
}

// 组件中使用
import { store } from './store'

computed: {
  currentUser() {
    return store.state.user
  }
},
methods: {
  updateUser(user) {
    store.setUser(user)
  }
}

5. Event Bus(事件总线)

创建中央事件总线进行组件间通信

// event-bus.js
import Vue from 'vue'
export const EventBus = new Vue()

// 组件A发送事件
import { EventBus } from './event-bus'
EventBus.$emit('notification', { message: 'Hello!' })

// 组件B接收事件
import { EventBus } from './event-bus'

created() {
  EventBus.$on('notification', (payload) => {
    console.log(payload.message) // 'Hello!'
  })
},

// 组件销毁前移除监听
beforeDestroy() {
  EventBus.$off('notification')
}

三、通信方式选择指南

场景 推荐方式 备注
简单父子通信 Props/Events 最基础、最常用的方式
父子双向绑定 v-model/.sync 表单类组件常用
跨层级传递数据 Provide/Inject 适合组件库开发
全局状态管理 Vuex 中大型项目首选
临时全局事件 Event Bus 简单场景使用,注意销毁监听
路由相关数据 路由参数 页面间传递数据
简单状态共享 Store模式 小型项目替代Vuex

四、最佳实践建议

  1. 优先使用Props/Events:保持组件独立性

  2. 避免过度使用parent/children:会破坏组件封装性

  3. Vuex使用原则

    • 多个组件共享的状态才放入Vuex
    • 组件私有状态应保留在组件内部
  4. Event Bus注意事项

    • 事件名建议使用常量
    • 组件销毁前务必移除监听
  5. Provide/Inject适用场景

    • 组件库开发
    • 深层嵌套组件通信
    • 普通业务代码慎用

Dioxus 与数据库协作

作者 susnm
2025年7月7日 11:16

与数据库协作

我们的HotDog应用程序进展顺利!我们实现了一个非常简单的后端,将用户的喜爱狗狗图片保存到本地“dogs.txt”文件中。

在实际应用中,您可能更希望将数据存储在正规的数据库中。现代数据库比文本文件强大得多!

如果您已经对数据库有一定了解,可以直接跳转到我们集成Sqlite与HotDog的章节

选择数据库

在当今应用程序开发时代,可供选择的数据库种类繁多,每种数据库都有其独特的优势、劣势及权衡因素。对于用户数量较少的应用程序,选择一个“更简单”且易于管理的数据库即可。对于用户数量较多的应用程序,你可能需要考虑功能更强大的数据库,并借助额外工具来满足更严格的要求。

以下是一个(不完整!)的数据库列表及其简要概述:

  • PostgreSQL:以强大的插件系统著称的高级数据库。
  • MySQL:全球最受欢迎的开源数据库,适用于各类应用。
  • SQLite:以可靠性和可嵌入性著称的简单文件型数据库引擎。
  • Oracle:以企业级功能著称的高级商业数据库。
  • Redis:以卓越性能著称的简单键值数据库。
  • MongoDB:适用于无法用行和列表示的数据的数据库。
  • SurrealDB:一种新的“全能型”数据库,结合了多种模型。
  • CockroachDB:专为高可用性设计的分布式SQL数据库。

还有更多! 数据库种类繁多,各自擅长不同任务。这些可能包括:

  • 关系型:传统行/列/表结构。
  • 文档型:存储非结构化或松散结构化的数据块。
  • 时间序列型:存储和查询随时间变化的大量数据。
  • 图型:基于数据与其他数据的连接关系进行查询。
  • 键值型:仅存储键值对——一种快速并发哈希表。
  • 内存型:专为低延迟操作设计,通常用作缓存。
  • 嵌入式:随应用程序一起发布的数据库。

对于大多数应用程序 —除非有特殊需求 — 我们推荐主流关系型数据库如 PostgreSQLMySQL

📣 PostgreSQL 目前是一个非常有趣的选择:它可以通过插件扩展以支持时间序列、向量、图、搜索和地理空间数据。

在某些情况下,您可能需要一个仅针对单个应用实例或用户机器的数据库。在这种情况下,您需要使用嵌入式数据库,如 SQLiteRocksDB

为 HotDog 添加数据库

对于 HotDog,我们将使用 SQLite。HotDog 是一个非常简单的应用程序,并且只会有一名用户:您!

要为 HotDog 添加 SQLite 功能,我们将引入 rusqlite 仓库。请注意,rusqlite 仅用于在服务器上编译,因此我们将通过 Cargo.toml 中的“server”功能对其进行功能控制。

[dependencies]
# ....
rusqlite = { version = "0.32.1", optional = true } # <--- add rusqlite

[features]
# ....
server = ["dioxus/server", "dep:rusqlite"] # <---- add dep:rusqlite

要连接到我们的数据库,我们将使用 rusqlite::ConnectionRusqlite 连接不是线程安全的,并且每个线程只能存在一个连接,因此我们需要将其包装在 thread_locals 中。

当连接初始化时,我们将执行一个 SQL 操作来创建包含我们数据的 "dogs" 表。

// The database is only available to server code
#[cfg(feature = "server")]
thread_local! {
    pub static DB: rusqlite::Connection = {
        // Open the database from the persisted "hotdog.db" file
        let conn = rusqlite::Connection::open("hotdog.db").expect("Failed to open database");

        // Create the "dogs" table if it doesn't already exist
        conn.execute_batch(
            "CREATE TABLE IF NOT EXISTS dogs (
                id INTEGER PRIMARY KEY,
                url TEXT NOT NULL
            );",
        ).unwrap();

        // Return the connection
        conn
    };
}

现在,在我们的 save_dog 服务器函数中,我们可以使用 SQL 将值插入数据库:

#[server]
async fn save_dog(image: String) -> Result<(), ServerFnError> {
    DB.with(|f| f.execute("INSERT INTO dogs (url) VALUES (?1)", &[&image]))?;
    Ok(())
}

应用程序启动后,您应该会在仓库目录中看到一个名为“hotdog.db”的文件。让我们保存几张狗狗的照片,然后使用数据库查看器打开该数据库。如果一切顺利,您应该能看到已保存的狗狗照片!

hotdog-db-view-39542875376c1a07.gif

关于数据库和Rust的说明

虽然有许多数据库驱动,但Rust的支持可能有限。Rust仍然是Web开发的新选择。在本节中,我们将提供我们自己的(有偏见的!)关于推荐用于与数据库交互的库的意见。

还需要注意的是,有一些库的抽象层次高于原始SQL。这些被称为对象关系映射器(ORM)。Rust ORM 库将 SQL 语言映射为普通的 Rust 函数。我们通常建议直接使用 SQL,但 ORM 可以让编写某些查询更加轻松。

  • Sqlx:一个简单但功能强大的 Postgres、MySQL 和 SQLite 接口。
  • SeaORM:基于 Sqlx 构建的用于衍生数据库的 ORM。
  • rusqlite:一个直观的 SQLite 接口,没有特殊的 ORM 魔法。
  • rust-postgres:一个与 rusqlite 类似 API 的 Postgres 接口。
  • Turbosql:一个非常简洁的 Sqlite 接口,支持自动派生。

我们未将 Diesel 等库列入此列表,因为 Rust 生态系统似乎已转向支持原生异步的最新项目。

还有许多我们尚未测试的库,但可能值得一试:

  • firebase-rs:Firebase 客户端库
  • postgrest-rs:Supabase 客户端库
  • mongo-rust-driver:官方 MongoDB 客户端库

选择数据库驱动

虽然您可能只考虑少数几种数据库用于应用程序,但数据库提供商众多,各有优缺点。我们未与这些提供商有任何合作关系——这只是我们观察到在 Rust 应用中被使用的驱动列表。

您无需使用数据库驱动。数据库驱动提供付费数据库托管服务。使用这些提供商将产生费用!许多提供商提供免费层级,部分支持“按需缩减”功能,可帮助您在小型应用中节省成本。您随时可以自行托管和管理数据库。

对于流行的关系型数据库:

  • GCP:提供AlloyDB(企业级Postgres)、CloudSQL(MySQL、Postgres)等。
  • AWS:提供RDS、Aurora、DynamoDB等。
  • PlanetScale:可靠的MySQL兼容数据库,支持分片以实现可扩展性。
  • Firebase:谷歌的综合实时数据库,专为快速应用开发设计。
  • Supabase:以出色仪表盘和工具集著称的托管 Postgres 服务。
  • Neon:通过分离计算与存储实现零扩展的应用托管 Postgres 服务。

对于 SQLite:

  • LiteFS:专为与 Fly.io 配合使用的分布式 SQLite 同步引擎。
  • Turso:支持多租户的 SQLite 提供商,为每位用户维护独立隔离的数据库。

“零扩展”关系型解决方案:

  • AWS Aurora
  • LiteFS

我们不推荐特定的数据库提供商。

如果您拥有大量免费云信用额度,可考虑 AWS/GCP/Azure。 如果您希望使用带优秀仪表盘的 Postgres,可考虑 Supabase 或 Neon。 如果您追求简单体验,可考虑 Turso 或 LiteFS。

【Nest指北系列-源码】(七)请求生命周期

作者 plusone
2025年7月7日 11:05

上一篇【Nest指北系列-源码】(六)创建应用实例和初始化应用 讲到 Nest 中路由注册流程,通过 Reflect.getMetadata() 获取控制器中 @Get()@Post() 等装饰器定义的路径请求方法请求处理函数等元数据,然后绑定到 HTTP 服务器的路由系统中。

在 Nest 中,一个请求从进入应用到最终响应,除了执行请求处理函数外,还会依次经过很多处理步骤,完整链路如下:

Middleware -> Guards -> Interceptors(before) -> Pipes -> Controller handler -> Interceptors(after) -> Exception Filters

本文将会分析这条执行链是如何构建的。


先回到 Nest 项目的入口文件:

// main.ts
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

其中,NestFactory.create() 创建 app 实例,然后通过 app.listen() 监听端口。listen 方法中先调用 init 方法初始化,再调用底层 HTTP 服务器的 listen 方法。比如底层服务器为 Express 的话,则进入:

@Injectable()
export class ExpressAdapter extends AbstractHttpAdapter {
  listen() { this.httpServer.listen(...) }
}

之后对于每个请求,会调用其路径对应的 Nest 路由代理

Nest 路由代理

Nest 路由代理是什么呢?让我们来看创建它,并将它绑定到底层 HTTP 框架的源码。

源码路径:packages/core/router/router-explorer.ts

上一篇【Nest指北系列-源码】(六)创建应用实例和初始化应用中提到了 applyPathsToRouterProxy() 方法,其中对于每一个路由定义调用 applyCallbackToRouter() 方法。

applyCallbackToRouter

桥梁函数,负责创建 Nest 路由代理,并将它注册到底层框架中。

精简后的源码如下:

private applyCallbackToRouter<T extends HttpServer>(
  router: T,
  routeDefinition: RouteDefinition,
  instanceWrapper: InstanceWrapper,
  moduleKey: string,
  routePathMetadata: RoutePathMetadata,
  host: string | RegExp | Array<string | RegExp>,
) {
  const {
    path: paths,
    requestMethod,
    targetCallback,
    methodName,
  } = routeDefinition;

  const { instance } = instanceWrapper;
  const routerMethodRef = this.routerMethodFactory
    .get(router, requestMethod)
    .bind(router);

  // ......
  const proxy = this.createCallbackProxy(
    instance,
    targetCallback,
    methodName,
    moduleKey,
    requestMethod,
  );

  // ......
  let routeHandler = this.applyHostFilter(host, proxy);

  paths.forEach(path => {
    // ......

    routePathMetadata.methodPath = path;
    const pathsToRegister = this.routePathFactory.create(
      routePathMetadata,
      requestMethod,
    );
    pathsToRegister.forEach(path => {
      // ......

      const normalizedPath = router.normalizePath
        ? router.normalizePath(path)
        : path;
      routerMethodRef(normalizedPath, routeHandler);

      // ......
    });

    // ......
  });
}

参数

  • router:http 服务器适配器实例,比如 Express 或 Fastify 的包装类。

  • routeDefinition:路由定义。

  • InstanceWrapper:控制器的 InstanceWrapper 实例。

  • moduleKey:模块唯一标识。

  • routePathMetadata: 路由 path 元数据,包含模块路径、控制器路径。

核心流程

1. 通过 routerMethodFactory.get() 获取 http adapter 中的对应处理方法

可以看下 RouterMethodFactory 类的实现:

export const REQUEST_METHOD_MAP = {
  [RequestMethod.GET]: 'get',
  [RequestMethod.POST]: 'post',
  [RequestMethod.PUT]: 'put',
  [RequestMethod.DELETE]: 'delete',
  [RequestMethod.PATCH]: 'patch',
  [RequestMethod.ALL]: 'all',
  // ......
} as const satisfies Record<RequestMethod, keyof HttpServer>;

export class RouterMethodFactory {
  public get(target: HttpServer, requestMethod: RequestMethod): Function {
    const methodName = REQUEST_METHOD_MAP[requestMethod];
    const method = target[methodName];
    if (!method) {
      return target.use;
    }
    return method;
  }
}

通过一个映射表把 Nest 的 RequestMethod 枚举转换为字符串方法名,然后在 http adapter 上获取对应处理方法。

2. 创建 Nest 路由代理

Nest 不直接调用控制器原始方法,而是调用 createCallbackProxy 方法封装一个代理。它的具体实现下面会讲到。

3. 将路由代理注册到底层框架中

  • routePathFactory.create(...) 负责合并模块、控制器和方法路径。

  • normalizePath() 会根据 Express/Fastify 规范化路径。

  • 最后调用 routerMethodRef(path, handler)router.get(path, handler) 之类的方法完成注册。

createCallbackProxy

创建路由代理,其中处理 GuardsInterceptorsPipesException Filters

private createCallbackProxy(
  instance: Controller,
  callback: RouterProxyCallback,
  methodName: string,
  moduleRef: string,
  requestMethod: RequestMethod,
  contextId = STATIC_CONTEXT,
  inquirerId?: string,
) {
  const executionContext = this.executionContextCreator.create(
    instance,
    callback,
    methodName,
    moduleRef,
    requestMethod,
    contextId,
    inquirerId,
  );
  const exceptionFilter = this.exceptionsFilter.create(
    instance,
    callback,
    moduleRef,
    contextId,
    inquirerId,
  );
  return this.routerProxy.createProxy(executionContext, exceptionFilter);
}

参数

  • instance: 控制器实例。

  • callback: 控制器上的 handler 方法。

  • methodName: 路由方法名。

  • moduleRef: 模块唯一标识。

  • requestMethod: 请求方法类型(GET / POST等)。

核心流程

  1. 调用 executionContextCreator.create(...) 创建一个 executionContext 对象,即最终的路由处理函数,内部集成了 handler 函数、守卫、拦截器、管道、响应头、状态码处理。可以理解为,它负责生成一个完整的执行链上下文

  2. 调用 exceptionsFilter.create(...) 创建一个 exceptionFilter 对象,即异常处理函数,内部包含全局默认异常过滤器以及自定义的异常过滤器。用于包装上一步构建的路由处理函数,当它抛出异常时,统一由异常处理函数处理。

  3. 调用 routerProxy.createProxy(...) 将路由处理链和异常处理链包装成最终的路由代理。

executionContext

构建路由处理链。

核心流程

1. 通过 Reflecet.getMetadata() 获取控制器 handler 方法的元数据

比如:

  • http 状态码(@HttpCode)。
  • 参数列表长度(argsLength)。
  • 参数类型数组(用于管道验证)。
  • 参数装饰器 @Body@Param 等的映射。
  • 响应头信息。
const {
  argsLength,
  fnHandleResponse,
  paramtypes,
  getParamsMetadata,
  httpStatusCode,
  responseHeaders,
  hasCustomHeaders,
} = this.getMetadata(
  instance,
  callback,
  methodName,
  moduleKey,
  requestMethod,
  contextType,
);

2. 将参数元数据和参数类型数组合并为 paramOptions 结构,供管道使用

const paramsOptions = this.contextUtils.mergeParamsMetatypes(...);

3. 构建组件链

const pipes = this.pipesContextCreator.create(...);
const guards = this.guardsContextCreator.create(...);
const interceptors = this.interceptorsContextCreator.create(...);
  • 根据控制器 handler 方法上的装饰器(如 @UsePipes() 等)创建执行链。这些 pipesguardsinterceptors 都是实例化后的,每次请求都会执行。

4. 构建逻辑函数

const fnCanActivate = this.createGuardsFn(...);
const fnApplyPipes = this.createPipesFn(...);

将上一步获取的 guardspipes 数组封装成函数,比如会取 guard 上的 canActive 方法。

5. 构建 handler 包裹器

const handler = (args, req, res, next) => async () => {
  fnApplyPipes && (await fnApplyPipes(args, req, res, next));
  return callback.apply(instance, args);
};

对控制器 handler 方法的封装,会先执行管道处理函数,再执行控制器方法

6. 生成最终执行函数

return async (req, res, next) => {
  const args = this.contextUtils.createNullArray(argsLength);

  fnCanActivate && (await fnCanActivate([req, res, next]));  // 守卫判断

  this.responseController.setStatus(res, httpStatusCode);
  if (hasCustomHeaders) {
    this.responseController.setHeaders(res, responseHeaders);
  }

  const result = await this.interceptorsConsumer.intercept(
    interceptors,
    [req, res, next],
    instance,
    callback,
    handler(args, req, res, next),  // 包含管道逻辑的实际 handler
    contextType,
  );

  await (fnHandleResponse as HandlerResponseBasicFn)(result, res, req); // 最终返回
};

其中按顺序:

  • 执行守卫判断。
  • 设置状态码与响应头。
  • 执行拦截器链。
  • 执行包含管道逻辑的实际控制器 handler 方法。
  • 最终将返回值通过响应控制器输出到 res。

可以看出,executionContext.create(...) 返回了最终的路由处理函数,内部集成了守卫、拦截器、管道、响应头、状态码处理,整体执行顺序为:guards -> interceptors -> pipes -> handler

exceptionFilter

构建异常处理器链。

核心流程

1. 创建默认异常处理器

const exceptionHandler = new ExceptionsHandler(this.applicationRef);
  • applicationRef 是对 Express / Fastify Response API 的抽象封装。

  • ExceptionsHandler 是 Nest 默认的异常处理器类,内部提供 next.handle(exception, host) 方法。

2. 读取装饰器元数据构建上下文链

const filters = this.createContext(
  instance,
  callback,
  EXCEPTION_FILTERS_METADATA,
  contextId,
  inquirerId,
);
  • 通过 ContextCreator 类的方法,根据 @UseFilters() 装饰器获取控制器类和方法上注册的异常过滤器,创建上下文链。

3. 如果没有注册自定义过滤器,返回默认异常处理器

if (isEmpty(filters)) {
  return exceptionHandler;
}
  • 如果没有自定义的 @UseFilters(),就返回默认 handler。

4. 注册自定义过滤器链

exceptionHandler.setCustomFilters(filters.reverse());
  • 使用 .reverse() 让后定义的过滤器 先执行(类似中间件执行顺序)。

  • 注册后,异常处理器在处理异常时会按顺序调用这些 filterscatch(exception, host) 方法。

5. 返回最终异常处理器 handler

return exceptionHandler;
  • 返回的 handler 将被用于包装前面构建的路由处理链的执行过程(如果执行失败,会进入 exceptionHandler)。

createProxy

将目标函数(路由 handler 函数)包装成带有异常处理的路由代理函数,用于最终绑定到底层框架中。

public createProxy(
  targetCallback: RouterProxyCallback,
  exceptionsHandler: ExceptionsHandler,
) {
  return async <TRequest, TResponse>(
    req: TRequest,
    res: TResponse,
    next: () => void,
  ) => {
    try {
      await targetCallback(req, res, next);
    } catch (e) {
      const host = new ExecutionContextHost([req, res, next]);
      exceptionsHandler.next(e, host);
      return res;
    }
  };
}

参数

  • targetCallback:路由处理函数(含守卫/拦截器/管道等)。

  • exceptionsHandler:异常处理函数。

核心逻辑

  1. try..catch... 包裹路由处理函数,catch 捕获异常,执行异常处理链。

  2. ExecutionContextHost 是一个包装器,将 [req, res, next] 提供给异常处理器,供其调用 getRequest()getResponse() 等 API。

  3. 调用 exceptionsHandler.next(...) 会遍历注册的异常过滤器,尝试依次调用其 catch(exception, host) 方法。


分析完 applyCallbackToRoutercreateCallbackProxy 方法,可以理解 Nest 中每个路由回调是如下形式:

app.get('/users', async (req, res, next) => {
  try {
    // 守卫 → 拦截器 → 管道 → 控制器方法
  } catch (e) {
    // 自定义异常过滤器
  }
});

整条链路中还差一个中间件,它是在什么阶段处理的呢?我们继续来看。

中间件处理

关于中间件的注册,我们要回到 NestApplication 应用实例的 init 方法。其中调用 registerModules -> this.middlewareModule.register -> resolveMiddleware。使用 expressApp.use(...) 将中间件注册到底层框架。

public async register(
  middlewareContainer: MiddlewareContainer,
  container: NestContainer,
  config: ApplicationConfig,
  injector: Injector,
  httpAdapter: HttpServer,
  graphInspector: GraphInspector,
  options: TAppOptions,
) {
  // ......

  const modules = container.getModules();
  await this.resolveMiddleware(middlewareContainer, modules);
}

public async resolveMiddleware(
  middlewareContainer: MiddlewareContainer,
  modules: Map<string, Module>,
) {
  const moduleEntries = [...modules.entries()];
  const loadMiddlewareConfiguration = async ([moduleName, moduleRef]: [
    string,
    Module,
  ]) => {
    await this.loadConfiguration(middlewareContainer, moduleRef, moduleName);
    await this.resolver.resolveInstances(moduleRef, moduleName);
  };
  await Promise.all(moduleEntries.map(loadMiddlewareConfiguration));
}

于是,每次请求进入之后会先执行中间件。中间件不在 Nest 创建的路由代理中。


总结

最终,Nest 请求的整个生命周期是这样的:

                            Incoming HTTP RequestHttp Adapter (Express/Fastify)
                                     |
                                 Middleware
                                     |
                                handler proxy
                                     |
                      ---------------------------------——
                     |                                  |
 ┌─────────────────────────────────────────┐   ┌──────────────────┐ 
 │ 路由处理链                                |   │ 异常处理链        |
 | GuardInterceptorPipeController |   | Exception Filter |
 └─────────────────────────────────────────┘   └──────────────────┘    

斩获 27k Star,一款开源的网站统计工具

2025年7月7日 11:01

大家好, 我是程序员凌览

无论是企业,还是独立开发者,往往都有自己的网站产品。

想了解网站的访问情况,常常需要使用工具来统计流量、用户信息等,如使用 Google Analytics。

然而,今天我将为大家介绍一款全新的自托管、开源的网站统计工具Umami,它不仅功能强大,而且完全免费开放,能够完美替代 Google Analytics。

开源地址:github.com/umami-softw…

image.png

Umami 是什么

Umami 是一个开源的的网站分析工具,它提供网站流量、用户行为分析和访问统计等功能。

Demo演示网站:eu.umami.is/share/LGazG…

image.png

image.png

image.png

Umami 官方贴心地提供了在线服务 umami.is/,免去了用户私有化部署的成本。

快速上手

安装

如果你不打算进行私有化部署,那么可以直接跳过本小节,这里主要介绍的是基于 Docker 的安装方法。

在开始之前,你需要先安装一个数据库,可以选择 PostgreSQL 或 MySQL。

1、拉取镜像

选择以下其中一个镜像进行拉取:

## PostgreSQL
docker pull docker.umami.is/umami-software/umami:postgresql-latest

## MySQL
docker pull docker.umami.is/umami-software/umami:mysql-latest

2、运行容器

根据你选择的数据库,运行相应的容器:

## PostgreSQL
 
docker run --name umami -d \
    -p 3000:3000 \
    -e DATABASE_URL=postgresql://用户名:密码@localhost:5432/mydb \
    docker.umami.is/umami-software/umami:postgresql-latest


## MySQL

docker run --name umami -d \
    -p 3000:3000 \
    -e DATABASE_URL=mysql://用户名:密码@localhost:3306/mydb \
    docker.umami.is/umami-software/umami:mysql-latest

重要提示:在启动容器时,必须指定数据库的连接地址。

3、安装成功,浏览器访问应用

http://{ip/域名}:3000

使用

1、私有化部署完成后,系统将自动生成一个默认的账号和密码,分别是adminumami

2、使用默认账号登录后,你需要添加要进行统计的网站信息并完成保存操作。

image.png

3、接下来,复制系统提供的Tracking code,并将其粘贴到需要统计的网站的相关位置。

image.png

4、完成上述操作后,系统将能够实时地对网站的访问情况进行统计和分析。

Umami 文档:umami.is/docs

最后

给大家推荐几款超实用的工具:

  • 密码管家 是一款 utools 插件,能帮你轻松管理各种繁杂的账号和密码,再也不用担心忘记密码啦!

  • 微信公众号排版编辑器 是专为微信公众号运营者设计的排版工具,无论是图文排版,还是格式调整,都能轻松搞定,让你的文章看起来更加专业、美观。

对了,我还会不定时分享更多好玩、有趣的 GitHub 开源项目,欢迎持续关注哦!

基于vite适用于 vue和 react 的Three.js低代码与Ai结合编辑器

2025年7月7日 10:48

核心功能链接

  • GitHub 仓库: z2586300277/three-editor
    包含源代码、开发说明及许可证信息(MIT License)。

  • 在线编辑器: Editor Demo
    可直接体验 3D 场景的创建、模型导入、材质调整等交互操作。

  • 案例展示: Example Gallery
    展示已实现的 3D 场景示例,供用户参考或直接复用。

  • 文档指南: Documentation
    提供 API 说明、配置参数及开发教程,适合二次开发。

image.png

技术栈

项目基于 Three.js 实现 3D 渲染,前端框架可能涉及 React/Vue(需查看源码确认),构建工具链可能包含 Webpack 或 Vite。

使用建议

如需本地开发,克隆仓库后安装依赖并运行构建命令。通过文档可快速了解场景节点管理、光照配置等高级功能。

Three-Editor 核心功能解析

Three-Editor 通过可视化界面封装 Three.js 的底层复杂性,提供以下核心能力:

  • 场景构建:拖拽式添加模型、灯光、相机,支持 GLTF/OBJ/FBX 等主流 3D 格式导入。
  • 属性调试:### Three-Editor 核心功能解析
    可视化场景构建
    支持拖拽式添加3D模型、灯光、相机等元素,实时渲染效果。内置材质编辑器、动画时间轴,无需手动编写Three.js代码即可完成基础场景搭建。

低代码交互逻辑配置
通过事件触发器与行为脚本的图形化配置,实现点击、悬停等交互逻辑。支持自定义JavaScript脚本扩展复杂功能,平衡易用性与灵活性。

技术集成方案

与主流框架兼容
提供Vue/React组件封装,可直接嵌入现有项目。示例代码展示Vue集成方式:

混合渲染能力
结合Three.js的细节渲染与Cesium.js的地理坐标系,支持室内外场景无缝衔接。坐标系自动转换模块处理不同场景的空间定位。

性能优化策略

动态加载机制
采用3D Tiles规范实现大规模场景分级加载,LOD(细节层次)控制可配置。测试数据显示,万级面片模型在中等配置设备保持60FPS。

行业解决方案模板

智慧城市套件
预置道路生成器、建筑批量导入工具,支持GIS数据对接。交通流模拟插件可配置车辆行为规则。

工业数字孪生模块
提供PLC数据接口协议,实时绑定设备状态与3D模型动作。异常状态自动触发可视化告警。

开发资源指引

学习路径建议

  1. 基础:官方示例库(含30+场景模板) ### 高度自定义功能
    系统支持深度定制,用户可根据需求自由修改功能模块或添加新功能。通过开放的API接口和插件机制,能够灵活调整界面、交互逻辑及数据处理方式,满足个性化项目需求。

组件化架构设计

采用模块化设计理念,将功能分解为独立组件。用户可像拼装积木一样自由组合,快速构建复杂系统。组件间低耦合,支持热插拔,便于维护和扩展。

可视化编辑体验

提供所见即所得的可视化操作界面,所有修改实时渲染呈现。无需频繁切换预览模式,设计效果与最终输出高度一致,大幅提升工作效率。

多领域案例库

涵盖智慧城市、工业建模、地理信息等领域的丰富案例模板。用户可参考类似场景的实现方案,快速复用成熟设计模式,降低开发门槛。

3Dtiles 大数据支持

内置高性能3Dtiles解析引擎,可流畅加载与渲染大规模三维场景。优化后的数据调度机制确保海量模型数据的稳定运行,避免性能瓶颈。

完备技术文档

配备详细的中英文开发文档,包含API说明、最佳实践和故障排查指南。结合社区论坛和示例代码,帮助开发者高效解决技术问题。
2. 进阶:自定义Shader编写手册
3. 高级:插件开发API文档

  1. 保留three.js 原生态, 只是将three.js 内部案例功能做了一个集成,并不对底层库改变,只撰写你业务通用的逻辑。

  2. 组件化:类似于二维低代码的组件化,three.js 也可以,例如你通过繁琐操作创建了一个三维物体,你将此方法封装起来,下次使用就不用从头写,只需要一些传参就能生成这个物体,一个组件只需要耗费经历写一次,而这个组件代码并不会与其他代码有过多的交集。

  3. 高扩展性 你只是做了项目所需要的业务逻辑封装,内部场景的生命周期流程搭建一定要保留three.js 的相关扩展元素, 如 后期处理系列, 着色器系列等,或者使用者自身去扩展系列。

  4. 创造理念, 一定不是直接就从树苗长成参天大树,而是搭建了一个健康的生长体系,让使用者去搭建成一个家园。

  5. 易用性 低代码的创造一定是减少开发者的学习成本,如果让开发者 用了之后发现学习成本更高了,那这个低代码不开发也罢,封装成一个npm 包 对于这来说是一个最好的选择, 开发者使用的时候只需要知道传什么参数,然后能生成什么结果就好,完全不需要去了解内部逻辑。

  6. 通用性 一定要和以外的斩断联系,例如 vue react, 把思想放到js 上 , 这样你创造的 才会是 轻量,通用, 高效的也会避免一些问题,可以多去看看 node_modules 的每一个依赖都是如何创造的,去获取这种理念。

以下是关于 three-editor 项目的关键信息整理:
three-editor 是一个基于 Three.js 的在线 3D 编辑器,提供可视化编辑和场景搭建功能,支持导出项目文件。项目包含编辑器核心功能、案例展示及详细文档。

🧊 HTML5 王者对象 Blob - 二进制世界的魔法沙漏

作者 WildBlue
2025年7月7日 10:48

💡 程序员破冰

女朋友:"你平时都干啥?"
程序员:"写Blob!"
女朋友:"什么?Blob是什么鬼?"
程序员:"...我需要解释_Blob_是什么吗?"


🌟 技术直男版 vs 白话版 对照表

技术概念 技术直男版 生活白话版 🎯 类比强化
Base64编码 ASCII字符到二进制的转换算法 超市里的糖果厂:把所有东西都包成糖纸 ✨糖果厂:万物皆可糖衣包裹
atob()函数 Base64字符串解码函数 拆快递:把压缩包还原成实物 📦快递站:解冻你的数字冰淇淋
Uint8Array 8位无符号整型数组 数字乐高积木:每块积木代表0-255的数字 🧱数字积木:0~255的彩色方块
Blob对象 二进制大数据容器 魔法沙漏:装着所有数字积木的沙漏 ⏳沙漏:装满数字的神奇玻璃瓶
ObjectURL 浏览器内存中的临时URL 停车场临时车位:只在当前会话有效 🚗停车场:给你一个临时停车位
二进制操作价值 支持文件切割/修改/压缩 乐高改造大师:可以任意拆改积木 🔧工具箱:让数字积木变身新玩具

🧪 实战案例:老板的图片预览需求

image.png

🎬 场景重现

老板怒吼.jpg:"用户上传的图片怎么在线预览?!"
你颤抖着敲下代码,希望用Blob拯救世界! image.png


✅ 四步通关秘籍

image.png

第一步:Base64解码(解冻冰淇淋)

image.png

// 🧊 解冻base64冰淇淋
const base64Data = atob('iVBORw0KGgoAAAANSUhEUgAAASwAAACCCAMAAADQNkiAAAAA1BMVEX///+nxBvIAAAAO0lEQVR4nO3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIBb8Q8AAa0jZQAAAABJRU5ErkJggg==')

🔍 代码显微镜
atob()就像打开快递站的压缩包,把加密的字符串还原成原始的"数字积木"。每块积木都是0-255的数字,准备堆砌成图片城堡!


第二步:构建字节数组(数字小山)

image.png

// 🧱 把积木堆成数字小山
const byteArray = new Uint8Array(base64Data.length)
for (let i = 0; i < base64Data.length; i++) {
    byteArray[i] = base64Data.charCodeAt(i) // 🎯 每块积木都有编号
}

💡 技术直男说
charCodeAt()是数字积木的编号机,把字符转成ASCII码值。
🧱 白话版:就像给每个乐高积木贴上编号,方便后面拼图!


第三步:创建Blob对象(魔法沙漏)

image.png

// 🌈 用魔法沙漏装数字积木
const blob = new Blob([byteArray], { type: 'image/png' })

❗️ 关键警告

  • 必须指定MIME类型(image/png
  • Blob是只读容器,不能直接修改内容
  • 类比快递站:不带标签的匿名快递箱

第四步:生成临时URL(停车场车位)

image.png

// 🚗 在浏览器停车场生成临时车位
const imageUrl = URL.createObjectURL(blob)
document.getElementById('blobImage').src = imageUrl

🔥 程序员段子
这个方法比Ctrl+C/V还可靠!但小心车位到期,记得用revokeObjectURL清场!


🎬 效果演示(程序员小剧场)

  1. 代码运行前:空白的<img>标签

  2. 代码运行后:图片成功显示!

  3. 老板表情包:老板开心.jpg → "这个程序员有前途!"

image.png

image.png


❓ 常见问题解答(FAQ)

image.png

Q1:Blob和File有什么区别?

❗️ 技术直男版
File继承自Blob,自带namesize属性
💡 白话版
Blob是匿名快递箱(📦),File是贴好标签的快递箱(📦+标签)


Q2:为什么用Blob而不是base64?

🔥 程序员梗
Base64是万能翻译器,Blob是原装硬盘!
📝 技术解释
Blob支持二进制操作(切割/修改),而base64只能看不能动


Q3:大文件处理会卡顿吗?

⚠️ 致命警告
超大Blob会导致OOM(内存爆炸)!
🔧 解决方案
FileReader分片处理 → 吃火锅分锅煮!


Q4:如何释放ObjectURL?

🧨 错误示范

// ❌ 会内存泄漏!
const url = URL.createObjectURL(blob)

🛠️ 正确姿势

// ✅ 关掉停车场闸机
URL.revokeObjectURL(imageUrl)

🛠️ 小白自查清单

✅ 是否理解Base64是加密的"糖果包装纸"?
✅ 能否区分Blob和File的快递箱差异?
✅ 能否说出ObjectURL的"临时车位"特性?
✅ 是否掌握"解码→数组→Blob→URL"的四步流程?


🎁 Blob进阶彩蛋

image.png

  1. Blob文件下载
const a = document.createElement('a')
a.href = URL.createObjectURL(blob)
a.download = 'image.png'
a.click()
  1. 文件切片上传
const chunk = blob.slice(0, 1024*1024) // 取前1MB
  1. 视频流处理
const videoUrl = URL.createObjectURL(new Blob([videoChunks]))

🔄 Blob使用三步曲(流程图)

image.png

[Base64字符串]atob()解码 
[Uint8Array字节数组] 
   ↓ new Blob() 
[Blob对象]createObjectURL() 
[临时URL]img.src赋值 
[页面显示]

记住:Blob不是魔法,而是浏览器给你的"数字积木工坊"!
🔥 行动:现在就打开控制台,用Blob打造属于你的二进制世界吧!

"页面白屏了?别慌!前端工程师必备的排查技巧和面试攻略"

2025年7月7日 10:46

作者:程序员成长指北

原文:mp.weixin.qq.com/s/9cxZxJ9eI…

使用场景介绍

哈喽,大家好,我是Fine。页面白屏是前端开发中最严重的问题之一,直接影响用户体验和业务转化。作为前端工程师,快速定位和解决白屏问题是核心技能。一次白屏可能造成用户流失和业务损失,因此需要建立系统的排查方法。

白屏原因与排查方法

1. JavaScript执行错误(最常见)

典型场景:SPA应用中未捕获的异常导致整个应用崩溃。

// 常见错误类型
// 1. 空值引用
const userInfo = null;
console.log(userInfo.name); // TypeError: Cannot read property 'name' of null

// 2. 异步错误未捕获
asyncfunction loadData() {
const response = await fetch('/api/user');
const data = await response.json();
// 如果接口返回格式异常,下面代码会报错
document.getElementById('username').textContent = data.user.name;
}

排查方法

  • 打开Console面板,查看错误信息和堆栈跟踪
  • 错误信息通常直接指向问题代码行
  • 关注TypeError、ReferenceError等常见错误类型

2. 资源加载失败

典型场景:CDN故障、网络不稳定导致关键CSS/JS文件加载失败。

排查方法

  • Network面板查看资源加载状态
  • 重点关注状态码为4xx、5xx的失败请求
  • 检查关键资源:主CSS文件、JavaScript入口文件

预防方案

// 资源加载容错
function loadCriticalResource(primaryUrl, fallbackUrl) {
returnnewPromise((resolve, reject) => {
    const link = document.createElement('link');
    link.rel'stylesheet';
    link.href = primaryUrl;
    
    link.onload = resolve;
    link.onerror = () => {
      // 主资源失败,加载备用资源
      link.href = fallbackUrl;
      link.onerror = reject;
    };
    
    document.head.appendChild(link);
  });
}

3. 接口异常

典型场景:关键数据接口超时或返回异常,页面无法获取必要数据。

排查方法

  • Network面板检查接口请求状态
  • Console面板查看接口错误信息
  • 确认接口返回数据格式是否符合预期

预防方案

// 统一接口错误处理
asyncfunction apiRequest(url, options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);

try {
    const response = await fetch(url, {
      ...options,
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    if (!response.ok) {
      thrownewError(`HTTP ${response.status}${response.statusText}`);
    }
    
    returnawait response.json();
  } catch (error) {
    if (error.name === 'AbortError') {
      thrownewError('请求超时,请稍后重试');
    }
    throw error;
  }
}

4. CSS样式问题

典型场景:样式异常导致内容不可见,造成视觉白屏。

/* 可能导致白屏的CSS */
.content {
  color: white;
  background: white; /* 白色文字配白色背景 */
}

.container {
  position: absolute;
  left: -9999px/* 内容移出可视区域 */
}

排查方法

  • Elements面板检查DOM结构是否正常
  • 查看元素的Computed样式
  • 确认关键元素的display、visibility、opacity等属性

5. 移动端特殊问题

排查工具

  • vConsole:移动端调试面板
  • 真机调试:Chrome DevTools远程调试
  • 抓包工具:Charles、Fiddler分析网络请求
// 移动端vConsole集成
import VConsole from 'vconsole';

if (process.env.NODE_ENV === 'development' || window.location.search.includes('debug=1')) {
  new VConsole();
}

预防与监控

1. 错误边界处理

// React错误边界
class ErrorBoundary extends React.Component {
constructor(props) {
    super(props);
    this.state = { hasErrorfalse };
  }

static getDerivedStateFromError(error) {
    return { hasErrortrue };
  }

  componentDidCatch(error, errorInfo) {
    // 上报错误
    console.error('页面错误:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return<div className="error-fallback">页面出现错误,请刷新重试</div>;
    }
    returnthis.props.children;
  }
}

2. 错误监控系统

// Sentry错误监控
import * as Sentry from"@sentry/browser";

Sentry.init({
dsn"YOUR_DSN_HERE",
environment: process.env.NODE_ENV,
});

// 全局错误捕获
window.addEventListener('error', (event) => {
  Sentry.captureException(event.error);
});

window.addEventListener('unhandledrejection', (event) => {
  Sentry.captureException(event.reason);
});

3. 兼容性检测

// 关键特性检测
function checkCompatibility() {
const requiredFeatures = [
    () =>typeofPromise !== 'undefined',
    () => typeof fetch !== 'undefined',
    () => typeofObject.assign !== 'undefined'
  ];

const unsupported = requiredFeatures.filter(check => !check());

if (unsupported.length > 0) {
    document.body.innerHTML = `
      <div style="text-align: center; padding: 50px;">
        <h2>浏览器版本过低</h2>
        <p>请升级浏览器以获得最佳体验</p>
      </div>
    `;
    returnfalse;
  }

returntrue;
}

面试回答技巧

标准回答模板

第一步:快速分类(30秒) "页面白屏主要有五种原因:JavaScript执行错误、资源加载失败、CSS样式问题、接口异常和浏览器兼容性。其中JavaScript错误最常见,特别是SPA应用中的未捕获异常。"

第二步:排查方法(1分钟) "我的排查步骤是:首先查看Console面板的错误信息,这能快速定位JS异常;然后检查Network面板确认资源加载状态;接着用Elements面板验证DOM和样式;移动端问题会用vConsole或真机调试。生产环境结合Sentry等监控系统分析。"

第三步:预防措施(30秒) "预防方面建立错误边界、资源容错机制、统一接口异常处理、兼容性检测,同时搭建监控告警体系。"

高频追问及回答要点

Q: "如何区分JS错误和资源加载失败?" A:  "JS错误在Console有明确报错信息和堆栈,资源失败在Network显示红色状态码。可以先看Console,无报错再查Network。"

Q: "生产环境白屏如何快速定位?" A:  "依赖监控系统收集错误信息,结合用户反馈确定影响范围,通过错误堆栈和用户环境信息快速定位。必要时可以灰度回滚。"

Q: "移动端白屏有什么特殊性?" A:  "移动端调试困难,需要vConsole或真机调试。还要考虑内存限制、网络环境、浏览器内核差异等因素。"

总结

白屏问题排查需要系统性方法:从Console错误信息入手,结合Network资源状态,通过Elements验证渲染结果。关键是建立完善的错误处理和监控体系,将问题消灭在萌芽状态。

React-Router 全面解析与实战指南

2025年7月7日 10:45

一、React-Router 基础概念

React-Router 是 React 生态系统中最受欢迎的路由库之一,它允许我们在 React 应用中实现页面之间的导航,而无需刷新整个页面。

核心组件

  1. BrowserRouter:使用 HTML5 History API 实现的路由组件
  2. Routes:路由容器,用于包裹多个 Route 组件
  3. Route:定义路由规则的组件
  4. Navigate:用于实现路由重定向
  5. Link:用于创建导航链接
  6. Outlet:用于渲染嵌套路由

react-managem.gif

二、基本路由配置

让我们看一个实际的路由配置示例:

import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import Login from './views/login/Login';
import Layout from './views/layout/index';
import Public from './views/public/index';
import Home from './views/home/index';
import Score from './views/score/index';

function App() {
  return (
    <div>
      <BrowserRouter>
        <Routes>
          <Route path='/' element={<Navigate to='/login' replace />} />
          <Route path='/login' element={<Login />} />
          <Route path='/layout' element={<Layout />}>
            <Route path='' element={<Home />}></Route>
            <Route path='public' element={<Public />}></Route>
            <Route path='score' element={<Score />}></Route>
          </Route>
        </Routes>
      </BrowserRouter>
    </div>
  )
}
export default App;

在这个示例中,我们:

  1. 导入了必要的路由组件
  2. 创建了一个包含所有路由配置的 App 组件
  3. 使用 BrowserRouter 包裹所有路由
  4. 配置了根路径重定向到登录页面
  5. 配置了登录页面路由
  6. 配置了带有子路由的布局页面

三、路由跳转

在登录页面中,我们使用 useNavigate 钩子来实现路由跳转:

import { useNavigate } from 'react-router-dom';

function Login() {
    const navigate = useNavigate();
    const onFinish = values => {
        console.log('Received values of form: ', values);
        // 跳去 layout 页面
        navigate('/layout');
    };
    // 其他代码...
}

四、嵌套路由

嵌套路由是 React-Router 中非常强大的功能,它允许我们在一个页面中嵌套另一个页面。

import { Outlet } from 'react-router-dom';

function MainLayout() {
    // 其他代码...
    return (
        <Layout className='layout-page'>
            {/* 侧边栏代码... */}
            <Layout>
                {/* 头部代码... */}
                <Content
                    style={{
                        margin: '24px 16px',
                        padding: 24,
                        background: colorBgContainer,
                        borderRadius: borderRadiusLG,
                    }}
                >
                    <Outlet />
                </Content>
            </Layout>
        </Layout>
    )
}

五、实际应用场景

1. 登录重定向

在我们的示例中,当用户访问根路径时,会自动重定向到登录页面:

<Route path='/' element={<Navigate to='/login' replace />} />

2. 权限控制

我们可以扩展这个示例,实现权限控制。例如,只有登录后的用户才能访问布局页面。

3. 动态路由

我们可以使用动态路由来处理不确定的路由参数。例如:

<Route path='/user/:id' element={<UserDetail />} />

六、总结

React-Router 是 React 应用中实现路由功能的最佳选择之一,它提供了丰富的 API 和灵活的配置方式,可以满足各种复杂的路由需求。

通过本文的学习,你应该已经掌握了 React-Router 的基本使用方法,包括路由配置、路由跳转、嵌套路由等。

Vue MathJax Beautiful,基于Mathjax的数学公式编辑插件

2025年7月7日 10:42

Vue MathJax Beautiful:专业的Vue 3数学公式编辑器

title.svg

在现代Web开发中,处理数学公式一直是一个具有挑战性的任务。今天,我很高兴为大家介绍一个强大的解决方案 —— Vue MathJax Beautiful,这是一个基于Vue 3和MathJax的专业数学公式编辑器组件库。

🌟 为什么选择Vue MathJax Beautiful?

1. 专业的数学公式编辑能力

  • 基于业界标准的MathJax引擎
  • 支持完整的LaTeX语法
  • 实时预览功能,所见即所得
  • 240+数学符号和38个常用公式模板

2. 现代化的技术栈

  • 基于Vue 3开发
  • 完整的TypeScript支持
  • 响应式设计,完美适配各种设备
  • 深色模式支持

3. 优秀的用户体验

  • 简洁优雅的界面设计
  • 丰富的符号面板
  • 智能的分类系统
  • 多语言支持(中文/英文)

4. 灵活的使用方式

弹窗模式

适合需要临时编辑公式的场景:

<VueMathjaxBeautiful
  v-model="showDialog"
  :existing-latex="formula"
  @insert="handleInsert"
/>
内联模式

适合需要固定编辑区域的场景:

<VueMathjaxBeautiful
  :inline-mode="true"
  :existing-latex="formula"
  @insert="handleInsert"
/>
富文本编辑器

支持数学公式的完整富文本编辑器:

<VueMathjaxEditor
  v-model="content"
  placeholder="开始编写您的内容..."
  :min-height="'400px'"
/>

🚀 特色功能

  1. 智能符号面板

    • 符号分类清晰
    • 常用公式模板
    • 快速预览效果
  2. 实时渲染

    • 即时预览公式效果
    • 快速调整和修改
    • 准确的错误提示
  3. 主题定制

    • 支持亮色/暗色主题
    • 可自定义样式
    • 响应式设计
  4. 多语言支持

    • 中文界面
    • English Interface
    • 易于扩展其他语言

📦 快速开始

安装

# npm
npm install vue-mathjax-beautiful

# yarn
yarn add vue-mathjax-beautiful

# pnpm
pnpm add vue-mathjax-beautiful

基础使用

<template>
  <div>
    <button @click="showFormulaEditor">打开公式编辑器</button>
    
    <VueMathjaxBeautiful
      v-model="showDialog"
      :existing-latex="formula"
      @insert="handleInsert"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import { VueMathjaxBeautiful } from 'vue-mathjax-beautiful'

const showDialog = ref(false)
const formula = ref('E = mc^2')

const showFormulaEditor = () => {
  showDialog.value = true
}

const handleInsert = (latex) => {
  formula.value = latex
}
</script>

🎯 适用场景

  1. 教育领域

    • 在线教育平台
    • 数学教学系统
    • 试题编辑系统
  2. 科研写作

    • 论文编辑器
    • 研究笔记
    • 技术文档
  3. 内容创作

    • 博客平台
    • 知识库系统
    • 教程制作

🔮 未来规划

  1. 更多公式模板
  2. 更强大的编辑功能
  3. 更多主题选项
  4. 性能优化
  5. 更多语言支持

🤝 参与贡献

我们欢迎任何形式的贡献,无论是新功能、bug修复,还是文档改进。您可以通过以下方式参与:

  1. 提交Issue
  2. 提交Pull Request
  3. 完善文档
  4. 分享使用经验

📖 相关资源

🌈 结语

Vue MathJax Beautiful 不仅仅是一个公式编辑器,它是一个完整的数学内容创作解决方案。无论您是在开发教育产品、科研工具,还是需要处理数学内容的应用,Vue MathJax Beautiful 都能满足您的需求。

欢迎试用 Vue MathJax Beautiful,让我们一起把数学公式编辑变得更简单、更优雅!

前端渲染方式

作者 丘耳
2025年7月7日 10:41

前端项目常见的渲染方式主要有以下几种,每种方式有其适用场景和优缺点:


1. 客户端渲染(CSR, Client-Side Rendering)

原理
页面的HTML基本为空或只有一个根节点,所有内容和逻辑都通过JavaScript在浏览器端动态生成。常见于React、Vue、Angular等SPA(单页应用)。

优点

  • 前后端分离,开发体验好
  • 页面切换快,用户体验流畅
  • 适合交互复杂的应用

缺点

  • 首屏加载慢(需要下载JS后再渲染)
  • SEO不友好(爬虫抓取不到内容,需额外处理)

代表框架:React、Vue、Angular


2. 服务端渲染(SSR, Server-Side Rendering)

原理
页面内容在服务器端生成好HTML,直接返回给浏览器,浏览器只需渲染HTML即可看到完整页面。可以配合前端框架实现“同构渲染”。

优点

  • 首屏加载快,用户体验好
  • SEO友好,爬虫能抓取内容

缺点

  • 服务器压力大
  • 开发复杂度提升(需处理同构/水合等问题)

代表框架:Next.js(React)、Nuxt.js(Vue)


3. 静态站点生成(SSG, Static Site Generation)

原理
在构建时(build time)将所有页面预先生成静态HTML文件,用户访问时直接返回静态文件。

优点

  • 性能极佳,访问速度快
  • 服务器压力小,易于部署
  • SEO友好

缺点

  • 不适合频繁变动的数据
  • 构建时间随页面数量增加

代表框架:Next.js、Nuxt.js、Gatsby、Hexo


4. 增量静态生成(ISR, Incremental Static Regeneration)

原理
结合SSG和SSR的优点,部分页面静态生成,部分页面按需在服务端生成并缓存。

优点

  • 兼顾性能和实时性
  • 适合内容更新频率不一的场景

代表框架:Next.js(支持ISR)


5. 混合渲染(Hybrid Rendering)

原理
同一个项目中,不同页面或组件采用不同的渲染方式(如首页用SSR,详情页用SSG,后台用CSR)。

优点

  • 灵活应对不同业务需求
  • 充分利用各种渲染方式的优点

代表框架:Next.js、Nuxt.js


6. 传统多页应用(MPA, Multi-Page Application)

原理
每个页面都是独立的HTML文件,页面跳转会重新加载资源。常见于早期的JSP、PHP、ASP.NET等。

优点

  • 实现简单,SEO友好
  • 适合内容型网站

缺点

  • 用户体验不如SPA
  • 页面切换慢

总结表格

渲染方式 首屏速度 SEO 适用场景 代表框架
CSR 交互复杂 React/Vue/Angular
SSR 首屏重要 Next.js/Nuxt.js
SSG 极快 内容型 Next.js/Gatsby
ISR 内容频繁更新 Next.js
Hybrid 取决于配置 复杂业务 Next.js/Nuxt.js
MPA 一般 传统网站 JSP/PHP/ASP.NET

不聊主业,聊聊你们眼中的副业是什么样的?

作者 阿星呀
2025年7月7日 10:41

刚才的一篇文章,被限制了,可能文中涉及到副业的成绩太过于耀眼,以至于被当下架,但我说的是事实。

所以就有了这篇文章,如果感兴趣,来聊聊你目前的现状以及对副业的了解。

在去年初,我眼中的副业就是摆个摊卖个小吃,或者几个人合伙创业,搞个工作室接单,再或者某音发布视频接广。但对我来说,都不合适。

于是就有了副业的探索,了解后才发现副业的类型有这么,做起来了都能赚钱。比如某书,某夕夕,某鱼等等。

image.png

react中封装组件对三方库或者已有ui组件进行自动调整大小嵌入显示区域

作者 小山不高
2025年7月7日 10:40

原因:有些三方库的组件或者已有的组件ui部分无法调整大小,比如组件是全屏覆盖显示但是在新开发的迭代要求缩小到部分区域显示

解决方法:使用css的属性transform scale,通过绝对定位处理放大或者缩小的处理区域

import React, {
  FC, useEffect, useRef, useState,
} from 'react';
import { useSize } from 'ahooks';
import { isEmpty, min } from 'lodash';
import { css } from '@emotion/css';

interface Props {
  debug?: boolean
  lockMaxWidth?: boolean
  lockMaxHeight?: boolean
}

export const AutoScale: FC<React.PropsWithChildren & Props> = ({ children, debug, lockMaxHeight = true, lockMaxWidth = true }) => {
  const outsideWrapperRef = useRef<HTMLDivElement>(null);
  const insideWrapperRef = useRef<HTMLDivElement>(null);
  const outsideSize = useSize(outsideWrapperRef.current?.parentElement);
  const insideSize = useSize(insideWrapperRef);
  const [scale, setScale] = useState(1);
  const [height, setHeight] = useState(0);
  const [width, setWidth] = useState(0);

  useEffect(() => {
    if (isEmpty(insideSize) || isEmpty(outsideSize)) {
      return;
    }
    if (debug) {
      console.log(outsideSize);
    }

    const scaleWidth = outsideSize.width / insideSize.width;
    const scaleHeight = outsideSize.height / insideSize.height;
    let resultScale;

    switch (true) {
      case lockMaxWidth && !lockMaxHeight:
        resultScale = scaleWidth;
        break;
      case !lockMaxWidth && lockMaxHeight:
        resultScale = scaleHeight;
        break;
      case lockMaxWidth && lockMaxHeight:
        resultScale = min([scaleWidth, scaleHeight]);
        break;
      default:
        resultScale = scaleWidth;
        break;
    }

    setScale(resultScale);
    setHeight(insideSize.height * resultScale);
    setWidth(insideSize.width * resultScale);
  }, [insideSize, outsideSize]);

  return (
    <div
      ref={outsideWrapperRef}
      className={css`
          position: relative;
          width: ${`${width}px` || '100%'};
          height: ${`${height}px` || '100%'};
          overflow: ${width ? 'initial' : 'hidden'};
      `}
    >
      <div
        ref={insideWrapperRef}
        className={css`
          position: absolute;
          top: 0;
          left: 0;
          transform-origin: left top;
          transform: scale(${scale});
      `}
      >
        {children}
      </div>
    </div>
  );
};

Css: flex布局+趣味Demo

2025年7月7日 10:40

布局

flex

flex布局意为“弹性布局”,任何一个容器都可以指定flex布局。行内元素也可以使用。webkit内核浏览器必须加上-webkit前缀,且设置flex布局之后,子元素的floatclearvertical-align属性将失效。

基本概念

使用flex布局的元素称为flex容器。所有的子元素会成为容器的成员目,称为flex item(项目),容器默认存在两条轴:水平的主轴和垂直的交叉轴。项目默认主轴排列。单项目占据的主轴空间较main size,占据的交叉轴空间称为cross size。

如果 flex-directionrow ,一般情况下,主轴的起始线是左边,终止线是右边。交叉轴的起始线是 flex 容器的顶部,终止线是底部。注意如果书写内容时阿拉伯文,那么主轴的起始线是右边,终止线是左边。

属性使用

容器上的属性

  • flex-direction:决定主轴的方向,可选值:row | row-reverse | column | column-reverse

  • flex-wrap:默认情况下项目都排在一条轴线上,如果一条轴线排不下,配置如何换行,可选值:nowrap|warp|wrap-reverse。

    • 其中wrap-reverse项目会在换行时按相反的顺序排列。这意味着,项目首先填充最后一行,然后从右向左排列,而不是从左向右。
  • flex-flow:flex-direction 属性和flex-wrap属性的简写,默认为:flex-flow:row nowrap

  • justify-content:定义了项目在主轴上的对齐方式,可选值:flex-start | flex-end | center | space-between | space-around;

    • flex-start(默认值):左对齐
    • flex-end:右对齐
    • center: 居中
    • space-between:两端对齐,项目之间的间隔都相等。
    • space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
  • align-items:定义项目在交叉轴上如何对齐,可选值flex-start | flex-end | center | baseline | stretch

    • flex-start:交叉轴的起点对齐。
    • flex-end:交叉轴的终点对齐。
    • center:交叉轴的中点对齐。
    • baseline: 项目的第一行文字的基线对齐。
    • stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
  • align-content:定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。可选值:flex-start | flex-end | center | space-between | space-around | stretch

    那这个轴线数怎么确定呢?实际上这主要是由flex-wrap属性决定的,当flex-wrap 设置为 nowrap 时,容器仅存在一根轴线,因为项目不会换行,就不会产生多条轴线。当 flex-wrap 设置为 wrap 时,容器可能会出现多条轴线,这时就需要去设置多条轴线之间的对齐方式。

    • flex-start:与交叉轴的起点对齐。
    • flex-end:与交叉轴的终点对齐。
    • center:与交叉轴的中点对齐。
    • space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
    • space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
    • stretch(默认值):轴线占满整个交叉轴。

项目上属性

  • order:定义项目的排列顺序。数值越小,排列越靠前,默认为0。

  • flex-grow:定义项目的放大比例,默认为0,即如果存在剩余空间,也不放大。

  • flex-shrink:定义了项目的缩小比例,默认为1,即如果空间不足,该项目将缩小。

  • flex-basis:定义了在分配多余空间之前,项目占据的主轴空间(main size),默认值为auto,即项目的本来大小

    • 默认值是 auto

      • 元素设置了宽度,flex-basis 为设置的宽度
      • 元素未设置宽度,flex-basis 为元素内容的尺寸
    • flex-basis 属性优先于 width 属性;

    • 设为 0 ,则子元素的大小不在空间分配计算的考虑之内

  • align-self:允许单个项目有与其他项目不一样的对齐方式,可覆盖align-items属性。默认值为auto,表示继承父元素的align-items属性,如果没有父元素,则等同于stretch

  • flex:是flex-grow, flex-shrinkflex-basis的简写,默认值为0 1 auto。后两个属性可选。该属性有两个快捷值:auto (1 1 auto) 和 none (0 0 auto)

    • 0 1 auto:不放大可缩小,项目占据主轴空间。
    • none:相当于0 0 auto,这表示子元素没有初始大小,它不会增长也不会收缩,不会根据可用空间进行伸缩调整。将保持其初始大小。
    • auto:相当于1 1 auto,这表示子元素可以根据其内容大小自动决定大小,它可以增长也可以收缩。但在尺寸不足时会优先最大化内容尺寸。
    • 0:相当于0 1 0%,这表示子元素不会增长也不会收缩,它将保持其初始大小。
    • 1:相当于1 1 0%,这表示子元素可以增长,占用主轴上的剩余空间

    flex-0 与flex-none对比:flex-0会表现为最小内容宽度,会将高度撑高(当前没有设置高度,如果设置高度文字会超过设置的高度,如下图)flex-none时候会表现为最大内容宽度,字数过多时候会超过容器宽度

    flex:1和flex:auto的区别:虽然都是充分分配容器的尺寸,但是flex:1的尺寸表现更为内敛(优先牺牲自己的尺寸),flex:auto的尺寸表现则更为霸道(优先扩展自己的尺寸)。

flex布局应用

  1. 导航栏菜单

  2. 骰子布局

趣味CSS

Button集合

shareButton

描述:分享按钮,鼠标悬停,按钮将打开,展示出多个svg来进行分享选择。鼠标移开,恢复成share文字展示的按钮。

主要实现:

  • HTML
 <button class="btn-share">
  <span class="btn-overlay">
    <svg t="1580465783605" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9773" width="20" height="20"><path d="M767.99994 585.142857q75.995429 0 129.462857 53.394286t53.394286 129.462857-53.394286 129.462857-129.462857 53.394286-129.462857-53.394286-53.394286-129.462857q0-6.875429 1.170286-19.456l-205.677714-102.838857q-52.589714 49.152-124.562286 49.152-75.995429 0-129.462857-53.394286t-53.394286-129.462857 53.394286-129.462857 129.462857-53.394286q71.972571 0 124.562286 49.152l205.677714-102.838857q-1.170286-12.580571-1.170286-19.456 0-75.995429 53.394286-129.462857t129.462857-53.394286 129.462857 53.394286 53.394286 129.462857-53.394286 129.462857-129.462857 53.394286q-71.972571 0-124.562286-49.152l-205.677714 102.838857q1.170286 12.580571 1.170286 19.456t-1.170286 19.456l205.677714 102.838857q52.589714-49.152 124.562286-49.152z" p-id="9774" fill="currentColor"></path></svg>&nbsp;
    Share
  </span>
  <a href="#"><svg t="1580195676506" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2099" width="28" height="28"><path d="M962.267429 233.179429q-38.253714 56.027429-92.598857 95.451429 0.585143 7.972571 0.585143 23.990857 0 74.313143-21.723429 148.260571t-65.974857 141.970286-105.398857 120.32-147.456 83.456-184.539429 31.158857q-154.843429 0-283.428571-82.870857 19.968 2.267429 44.544 2.267429 128.585143 0 229.156571-78.848-59.977143-1.170286-107.446857-36.864t-65.170286-91.136q18.870857 2.852571 34.889143 2.852571 24.576 0 48.566857-6.290286-64-13.165714-105.984-63.707429t-41.984-117.394286l0-2.267429q38.838857 21.723429 83.456 23.405714-37.741714-25.161143-59.977143-65.682286t-22.308571-87.990857q0-50.322286 25.161143-93.110857 69.12 85.138286 168.301714 136.265143t212.260571 56.832q-4.534857-21.723429-4.534857-42.276571 0-76.580571 53.979429-130.56t130.56-53.979429q80.018286 0 134.875429 58.294857 62.317714-11.995429 117.174857-44.544-21.138286 65.682286-81.115429 101.741714 53.174857-5.705143 106.276571-28.598857z" p-id="2100" fill="currentColor"></path></svg></a>
  <a href="#"><svg t="1580195734305" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2429" width="28" height="28"><path d="M123.52064 667.99143l344.526782 229.708899 0-205.136409-190.802457-127.396658zM88.051421 585.717469l110.283674-73.717469-110.283674-73.717469 0 147.434938zM556.025711 897.627196l344.526782-229.708899-153.724325-102.824168-190.802457 127.396658 0 205.136409zM512 615.994287l155.406371-103.994287-155.406371-103.994287-155.406371 103.994287zM277.171833 458.832738l190.802457-127.396658 0-205.136409-344.526782 229.708899zM825.664905 512l110.283674 73.717469 0-147.434938zM746.828167 458.832738l153.724325-102.824168-344.526782-229.708899 0 205.136409zM1023.926868 356.00857l0 311.98286q0 23.402371-19.453221 36.566205l-467.901157 311.98286q-11.993715 7.459506-24.57249 7.459506t-24.57249-7.459506l-467.901157-311.98286q-19.453221-13.163834-19.453221-36.566205l0-311.98286q0-23.402371 19.453221-36.566205l467.901157-311.98286q11.993715-7.459506 24.57249-7.459506t24.57249 7.459506l467.901157 311.98286q19.453221 13.163834 19.453221 36.566205z" p-id="2430" fill="currentColor"></path></svg></a>
  <a href="#"><svg t="1580195767061" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2759" width="28" height="28"><path d="M950.930286 512q0 143.433143-83.748571 257.974857t-216.283429 158.573714q-15.433143 2.852571-22.601143-4.022857t-7.168-17.115429l0-120.539429q0-55.442286-29.696-81.115429 32.548571-3.437714 58.587429-10.313143t53.686857-22.308571 46.299429-38.034286 30.281143-59.977143 11.702857-86.016q0-69.12-45.129143-117.686857 21.138286-52.004571-4.534857-116.589714-16.018286-5.12-46.299429 6.290286t-52.589714 25.161143l-21.723429 13.677714q-53.174857-14.848-109.714286-14.848t-109.714286 14.848q-9.142857-6.290286-24.283429-15.433143t-47.689143-22.016-49.152-7.68q-25.161143 64.585143-4.022857 116.589714-45.129143 48.566857-45.129143 117.686857 0 48.566857 11.702857 85.723429t29.988571 59.977143 46.006857 38.253714 53.686857 22.308571 58.587429 10.313143q-22.820571 20.553143-28.013714 58.88-11.995429 5.705143-25.746286 8.557714t-32.548571 2.852571-37.449143-12.288-31.744-35.693714q-10.825143-18.285714-27.721143-29.696t-28.306286-13.677714l-11.410286-1.682286q-11.995429 0-16.603429 2.56t-2.852571 6.582857 5.12 7.972571 7.460571 6.875429l4.022857 2.852571q12.580571 5.705143 24.868571 21.723429t17.993143 29.110857l5.705143 13.165714q7.460571 21.723429 25.161143 35.108571t38.253714 17.115429 39.716571 4.022857 31.744-1.974857l13.165714-2.267429q0 21.723429 0.292571 50.834286t0.292571 30.866286q0 10.313143-7.460571 17.115429t-22.820571 4.022857q-132.534857-44.032-216.283429-158.573714t-83.748571-257.974857q0-119.442286 58.88-220.306286t159.744-159.744 220.306286-58.88 220.306286 58.88 159.744 159.744 58.88 220.306286z" p-id="2760" fill="currentColor"></path></svg></a>
  <a href="#"><svg t="1580195779874" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3041" width="28" height="28"><path d="M208.603429 808.009143l132.022857 0 0-396.580571-132.022857 0 0 396.580571zM349.110857 289.133714q-0.585143-29.696-20.553143-49.152t-53.174857-19.456-53.979429 19.456-20.845714 49.152q0 29.110857 20.260571 48.859429t52.882286 19.748571l0.585143 0q33.718857 0 54.272-19.748571t20.553143-48.859429zM683.446857 808.009143l132.022857 0 0-227.401143q0-87.990857-41.691429-133.12t-110.299429-45.129143q-77.677714 0-119.442286 66.852571l1.170286 0 0-57.709714-132.022857 0q1.682286 37.741714 0 396.580571l132.022857 0 0-221.696q0-21.723429 4.022857-32.036571 8.557714-19.968 25.746286-34.011429t42.276571-13.970286q66.267429 0 66.267429 89.746286l0 211.968zM950.857143 237.714286l0 548.571429q0 68.022857-48.274286 116.297143t-116.297143 48.274286l-548.571429 0q-68.022857 0-116.297143-48.274286t-48.274286-116.297143l0-548.571429q0-68.022857 48.274286-116.297143t116.297143-48.274286l548.571429 0q68.022857 0 116.297143 48.274286t48.274286 116.297143z" p-id="3042" fill="currentColor"></path></svg></a>
</button>
  • CSS
#hover之前
# hidden
.btn-overlay {
    position: absolute;
    top: 0;
    left: 0;
    z-index: 1;
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: 100%;
    background: var(--btn-overlay-color);
    border-radius: inherit;
    transition: 2s linear;
  }

  a {
    padding: 14px;
    color: var(--icon-color);
    // 元素不可见
    opacity: 0;  
    // 向左平移自身宽度
    transform: translateX(-100%);
    transition: 0.5s;

    @for $i from 1 through 4 {
      &:nth-child(#{$i}) {
        // 过度延时,从左到右延迟时间逐渐减少
        transition-delay: 1s - 0.2s * ($i - 1);
      }
    }
  }

 # hover之后
&:hover {
    .btn-overlay {
      transform: translateX(-100%);
      // 黑色部分延迟0.25s开始往左移动
      transition-delay: 0.25s; 
    }

    a {
      opacity: 1;
      // 回到原本的位置
      transform: translateX(0);
    }
  }

效果

loadingButton

描述:实现一个加载按钮,点击之后展示loding效果,加载结束显示成功状态。默认状态只有一个Login文字。

主要实现:

  • HTML
 <!-- 按钮 -->
  <button class="login" @click="login" :class="[status==='loading' ? 'loading' :(status === 'success' ? 'success' : 'login') ]">
  <span>login</span>
  <!-- 加载效果 -->
  <div class="loader">
    <div class="line-scale-pulse-out-rapid">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  </div>
  <!-- 对勾 -->
  <svg class="tick" width="30px" height="30px" stroke="white" fill="none">
    <polyline points="2,10 12,18, 28,2"></polyline>
  </svg>
</button>
  • CSS
.line-scale-pulse-out-rapid div {
      display: inline-flex;
      width: 4px;
      height: 35px;
      margin: 2px;
      border-radius: 2px;
      background-color: white;
      // 该动画名为 line-scale-pulse-out-rapid,持续时间为 0.9 秒,
      // 延迟 0.5 秒后开始播放,无限循环,使用自定义的缓动函数来控制动画的速度变化。
      // 重复的脉动或脉冲效果
      animation: line-scale-pulse-out-rapid 0.9s -0.5s infinite cubic-bezier(.11, .49, .38, .78);

      &:nth-child(2),
      &:nth-child(4) {
        animation-delay: -0.25s;
      }

      &:nth-child(1),
      &:nth-child(5) {
        animation-delay: 0s;
      }
    }
    
  .tick {
    position: absolute;
    left: 1.5em;
    bottom: 1.2em;
    opacity: 0;
    stroke: #eee;
    stroke-width: 5px;
    stroke-dasharray: 36px;
    stroke-dashoffset: 36px;
    transition: opacity 0.3s;
  }
  
  // 加载时,按钮的宽度为 4em,字体颜色为透明,不可点击,不可选中。加载条出现
.loading {
    width: 4em;
    color: transparent;
    pointer-events: none;
    user-select: none;

    .loader {
      opacity: 1;
    }
  }
.success {
  // 加载条消失
    .loader {
      opacity: 0;
    }
  // 对勾出现
    .tick {
      opacity: 1;
      animation: show-tick 0.8s 0.4s forwards;
    }
  }

// 元素在垂直方向上缩小到原来的30%高度。这是动画中的压缩状态。
@keyframes line-scale-pulse-out-rapid {

  from,
  90% {
    transform: scaleY(1);
  }

  80% {
    transform: scaleY(0.3);
  }
}
// 路径描边动画
@keyframes show-tick {
  to {
    stroke-dashoffset: 0;
  }
}

效果:

WaveButton

描述:扩散性按钮,像波浪一样

主要实现:

  • HTML
   <ul>
      <li><a href="#"><svg t="1580189699029" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6908" width="20" height="20"><path d="M881.2 158.2v538.4c0 16.1-5.5 30.3-16.4 42.8-10.9 12.5-24.7 22.2-41.3 29.1-16.7 6.9-33.3 12-49.7 15.4-16.5 3.3-31.9 5.1-46.4 5.1s-29.9-1.7-46.4-5.1c-16.5-3.3-33.1-8.5-49.7-15.4s-30.4-16.6-41.3-29.1c-10.9-12.5-16.4-26.8-16.4-42.8s5.5-30.3 16.4-42.8c10.9-12.5 24.7-22.1 41.3-29.1 16.7-6.9 33.3-12 49.7-15.4 16.5-3.3 31.9-5.1 46.4-5.1 33.7 0 64.4 6.3 92.3 18.7V365L450.5 478.9v340.8c0 16-5.5 30.3-16.4 42.8-10.9 12.5-24.7 22.1-41.3 29.1-16.7 6.9-33.3 12-49.7 15.4-16.5 3.3-31.9 5.1-46.4 5.1s-29.9-1.7-46.4-5.1c-16.5-3.3-33.1-8.5-49.7-15.4s-30.4-16.6-41.3-29.1-16.4-26.8-16.4-42.8c0-16.1 5.5-30.3 16.4-42.8 10.9-12.5 24.7-22.1 41.3-29.1 16.7-6.9 33.3-12 49.7-15.4 16.5-3.3 31.9-5.1 46.4-5.1 33.7 0 64.4 6.3 92.3 18.7V281.2c0-9.9 3-19 9.1-27.2s14-13.9 23.6-17.1l399.9-123c3.8-1.3 8.3-1.9 13.4-1.9 12.8 0 23.8 4.4 32.7 13.4 9.1 9.1 13.5 20.1 13.5 32.8z" p-id="6909"></path></svg></a></li>
      <li><a href="#"><svg t="1580189904311" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4874" width="20" height="20"><path d="M897.9 290.2c-18.6-11.7-158.1 53.9-158.1 53.9v-6.9c0-57.3-48-103.7-107.3-103.7H219.3C160 233.6 112 280 112 337.3v348.6c0 57.3 48 103.7 107.3 103.7h413.3c59.3 0 107.3-46.4 107.3-103.7v-8.5s140.3 65.1 158.1 53.9c17.7-11.2 18.6-429.4-0.1-441.1z m-558.2 388V344.1l272.1 167-272.1 167.1z m0 0" p-id="4875"></path></svg></a></li>
    </ul>
  • CSS
@function glow(
  $inner-color: rgb(89, 89, 180),
  $outer-color: rgba(89, 89, 180, 0.5),
  $depth: 5,
  $delta: 15px
) {
  $shadows: ();

  // inner glow
  // 循环用于生成内部发光的阴影值。它从1循环到 ceil((5 / 2)),并将生成的阴影值附加到 $shadows 列表中
  @for $i from 1 through ceil((5 / 2)) {
    $shadows: append($shadows, 0 0 (($delta / 2) * $i) $inner-color, comma);
  }

  // outer glow,循环用于生成外部发光的阴影值
  @for $i from 1 through $depth {
    $shadows: append($shadows, 0 0 0 ($delta * $i) $outer-color, comma);
  }

  @return $shadows;
}

a {
      position: relative;
      display: block;
      padding: 4px;
      width: 100%;
      height: 100%;
      text-decoration: none;
      text-align: center;

      &::before {
        position: absolute;
        content: "";
        top: 50%;
        left:50%;
        transform: translate(-50%,-50%);
        z-index: -1;
        width: 100%;
        height: 100%;
        background: white;
        border-radius: 50%;
        transition: 0.8s;
      }

      &:hover::before {
        box-shadow: glow();
      }
    }

效果:

其他按钮

  • 大致实现
 &-jittery {
    animation: jittery 4s infinite;
  }

  // 创建了一个内阴影效果,该内阴影的主要特点是它在元素的内部并且具有较大的模糊半径
  &-fill {
    transition: 2s;
    &:hover {
      background: transparent;
      box-shadow: inset 0 0 0 36px var(--btn-bg);
    }
  }
  // 外阴影
  &-pulse {
    &:hover {
      box-shadow: 0 0 0 18px transparent;
      animation: pulse 1s;
    }
  }

 

  &-close {
    transition: 0.3s;

    &:hover {
      // 水平偏移的内阴影,使阴影位于元素的右侧,距离元素 54 像素的位置;
      // 水平偏移的内阴影,使阴影位于元素的左侧,距离元素 54 像素的位置
      box-shadow: inset 54px 0 0 0 var(--btn-bg),
        inset -54px 0 0 0 var(--btn-bg);
    }
  }


  &-slide {
    transition: 0.3s;

    &:hover {
      background: transparent;
      box-shadow: inset 100px 0 0 0 var(--btn-bg);
    }
  }


  &-marquee {
    font-weight: 600;
    border: none;
    overflow: hidden;
    transition: 0.3s;

    &:hover {
      transform: scale(1.1);
    }

    span {
      display: block;
      padding: 0 20px;
      animation: move-left 2s linear infinite;  // 左移
      // 选择器将目标应用到元素自身的伪元素,在原始元素的内容之后插入内容。
      &::after {
        position: absolute;
        content: 'hello';
        top: 0;
        left: 100%;
        display: flex;
        justify-content: center;
        align-items: center;
        width: 100%;
        height: 100%;
      }
    }
  }


  &-open-line {
    border: none;
    transition: 0.3s;
    &:hover {
      letter-spacing: 5px;  //在鼠标悬停状态下,文本的字符间距将增加 5 像素
      color: var(--btn-bg);
    }
  }
}

@keyframes jittery {
  5%,
  50% {
    transform: scale(1);
  }

  10% {
    transform: scale(0.9);
  }

  15% {
    transform: scale(1.15);
  }

  20% {
    transform: scale(1.15) rotate(-5deg);
  }

  25% {
    transform: scale(1.15) rotate(5deg);
  }

  30% {
    transform: scale(1.15) rotate(-3deg);
  }

  35% {
    transform: scale(1.15) rotate(2deg);
  }

  40% {
    transform: scale(1.15) rotate(0);
  }
}


@keyframes pulse {
  from {
    box-shadow: 0 0 0 0 var(--btn-bg);
  }
}

@keyframes move-left {
  to {
    transform: translateX(-100%);
  }
}

Card

**描述:**图片卡片,悬停鼠标卡片打开,图片像上,卡片内容向下

主要实现:

  • HTML
  <div class="card" v-for="(item,index) in data" :key="index">
      <img :src="item.url" alt="Sora" class="card-img-top">
      <div class="card-body">
              <h5 class="card-title">{{ item.name }}</h5>
              <p class="card-text">{{ item.info }}</p>
              <a href="#" class="btn">More</a>
      </div>
    </div>
  </div>
  • CSS
.card {
    margin: 1em;

    .card-img-top {
      position: relative;
      z-index: 2;
      width: 302px;
      height: 222px;
      // 下移一半
      transform: translateY(52%);
      transition: 0.5s;
    }

    .card-body {
      z-index: 1;
      box-sizing: border-box;
      padding: 1.25em;
      height: 220px;
      background: white;
      box-shadow: 0 2.8px 2.2px rgba(0, 0, 0, 0.056),
        0 6.7px 5.3px rgba(0, 0, 0, 0.081), 0 12.5px 10px rgba(0, 0, 0, 0.1),
        0 22.3px 17.9px rgba(0, 0, 0, 0.119),
        0 41.8px 33.4px rgba(0, 0, 0, 0.144), 0 100px 80px rgba(0, 0, 0, 0.2);
      // 上移一半
      transform: translateY(-50%);
      transition: 0.5s;
    }

    &:hover {
      // 回原位置
      .card-img-top {
        transform: translateY(0);
      }

      .card-body {
        transform: translateY(0);
      }
    }
  }

效果:

多行字体渐变

**描述:**多行文字渐变效果

主要实现:

  • HTML
<div class="outer">
  <div class="bg-color">
  <div>纯CSS</div>
  <div>实现</div>
  <div>多行</div>
  <div>渐变</div>
  <div>文本</div>
</div>
</div>
  • CSS
.bg-color {
 // 渐变的方向是从 210 度角开始,顺时针方向旋转。
  // 在渐变的起始位置(0%)使用hsl(165, 58%, 55%)。然后,从 0%20% 的范围内
  // 将颜色渐变为该颜色。接下来的 20%21% 范围内,将颜色渐变为透明
  font-size: 36px;
  font-weight: 800;
  line-height: 36px;
  display:block;
  background-clip: text;  // 控制背景图片或颜色绘制区域的 CSS 属性
  color:transparent;
  background-image: linear-gradient(210deg,  
  hsl(165, 58%, 55%) 0 20%, transparent 20% 21%,
  hsl(214, 79%, 65%) 0 40%, transparent 40% 41%,
  hsl(43, 100%, 66%) 0 60%, transparent 60% 61%,
  hsl(354, 81%, 63%)  0 80%, transparent 80% 81%,
  hsl(196, 68%, 54%) 0 100%, transparent 100% 101%);
}

.outer {
  display: flex;
  width:500px;
  height: 500px;
  justify-content: center;
  align-items: center;
  margin: 0;
  background: black;
}

效果:

JS用法:Map,Set和异步函数

2025年7月7日 10:37

Set & Map

Set

Es6提供的新数据结构Set,类似于数组,但是成员值均唯一,不重复。

set本身就是一个构造函数,用来生成Set数据结构。其中Set函数可以接受一个数组,也可以接受具有iterable的其他数据结构作为参数来实现初始化。

const set = new Set([1, 2, 3, 4, 4]);   // 1,2,3,4

set的属性和方法

Set的属性包含size,用于获取当前set的长度;

set的方法包含操作方法和遍历方法两大类

其中,操作方法主要如下:

  • add(value):添加某个值,返回 Set 结构本身。

    set在加入新的内容的时候,不会发生类型转换,它内部判断两个值是否相等类似于严格等于(===),单对于NAN却认为它等于自身。

set2.add(NaN).add(NaN)  // size:1
  • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • has(value):返回一个布尔值,表示该值是否为Set的成员。
  • clear():清除所有成员,没有返回值。

set的遍历方法如下:

  • keys():返回键名的遍历器

  • values():返回键值的遍历器

  • entries():返回键值对的遍历器

  • forEach():使用回调函数遍历每个成员

    由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。在Set结构中,它默认的遍历器生成方法就是上述的values()方法,这意味这可以直接省略values方法,使用for...of即可

  • 代码演示

let set = new Set(['red', 'green', 'blue']);

for (let item of set.keys()) {
  console.log(item);
}

console.log('--------使用values()----------')

for (let item of set.values()) {
  console.log(item);
}
console.log('------------------')

for (let item of set.entries()) {
  console.log(item);
}
console.log('--------直接使用of----------')
console.log(Set.prototype[Symbol.iterator] === Set.prototype.values)
for (let x of set) {
  console.log(x);
}

set的应用

  • 由于扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构,这里就出现了我们常用的数组去重方法:[...new Set(array)] 同时也可以用于字符串去重:
[...new Set('ababbc')].join('')
  • 使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

    • 代码演示
let a = new Set([1, 2, 3]);
let b = new Set([4, 3, 2]);

// 并集
let union = new Set([...a, ...b]);
console.log(union)

// 交集
let intersect = new Set([...a].filter(x => b.has(x)));
console.log(intersect)


// (a 相对于 b 的)差集
let difference = new Set([...a].filter(x => !b.has(x)));
console.log(difference)
  • 如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

    • 代码演示
// 方法一
let set1 = new Set([1, 2, 3]);
set1 = new Set([...set1].map(val => val * 2));
console.log(set1);

// 方法二
let set2 = new Set([1, 2, 3]);
set2 = new Set(Array.from(set2, val => val * 2));
console.log(set2)

WeakSet

weakSet和set类似,主要的区别时:

首先,WeakSet 的成员只能是对象和** Symbol** 值,而不能是其他类型的值;

其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存

这是因为垃圾回收机制根据对象的可达性(reachability)来判断回收,如果对象还能被访问到,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。**因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。**由于这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。

另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历

基本使用

WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

const a = [[1,2],[2,3]]
const ws = new WeakSet(a);
ws.add(1) // 报错
const b = [1,2]
const ws1 = new WeakSet(b);  // error
console.log(ws1)

WeakSet 结构有以下三个方法。

  • add(value):向 WeakSet 实例添加一个新成员,返回 WeakSet 结构本身。
  • delete(value) :清除 WeakSet 实例的指定成员,清除成功返回true,如果在 WeakSet 中找不到该成员或该成员不是对象,返回false
  • has(value):返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
weakSet的应用
  • 可以使用 WeakSet 跟踪一组对象,而不影响这些对象的垃圾回收。这在调试和监控工具中尤为有用。

    • 代码演示
const trackedObjects = new WeakSet();

class MyClass {
  constructor() {
    trackedObjects.add(this);
  }

  isTracked() {
    return trackedObjects.has(this);
  }
}

const obj1 = new MyClass();
console.log(obj1.isTracked()); // true

const obj2 = new MyClass();
trackedObjects.delete(obj2);
console.log(obj2.isTracked()); // false
  • 可以使用 WeakSet 实现类的私有属性。这样,私有属性不会暴露给外部,只能通过类的方法来访问。

    • 代码演示
const foos = new WeakSet()
class Foo {
  constructor() {
    foos.add(this)
  }
  method () {
    if (!foos.has(this)) {
      throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!');
    }
  }
}
let f = new Foo()
f.method() // 通过
Foo.prototype.method() // 报错

Map

js的对象(Object) 本质上时键值对的集合,但是传统上只能使用字符串当作键值,这在使用上产生了很多的限制。

  • 代码演示
<body>
  <div id="box">123</div>
  <script>
    const data = {};
    const element = document.getElementById('box');
    console.log(element); 
    data[element] = 'metadata';
    console.log("data",data)
    console.log(data['[object HTMLDivElement]']) // "metadata"
  </script>
</body>

由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。为了解决这个问题,ES6提供了Map数据结构,与对象的主要区别在于“键”的范围不在局限于字符串,各种类型的值都可以作为键。通俗点说,对象提供了了“字符串-值”的对应,Map提供了“值-值”的对应。还是上面的例子,我们使用Map来实现,效果如下:

map也可以接受数组作为参数,数组的成员是一个个表示键值对的数组,如果对同一个键多次赋值,后面的值将覆盖前面的值,这里我们要注意,只有对同一个对象的引用,才算是同一个键值。如果读取一个未知的键,则返回undefined

  • 代码演示
const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);
console.log('-----------------------')
const map1 = new Map();
map1.set(1,'aaa').set(1,'bbb');
console.log(map1.get(1));  // "bbb" 会覆盖前面的值
console.log(map1.get(2));  // undefined
console.log('-----------------------')
const map2 = new Map();
const k1 = ['a'];
map2.set(['a'], 555);
map2.set(k1, 666);
console.log(map2.get(['a']) ) // undefined
console.log(map2.get(k1) ) // 666
console.log(map2.size)
console.log('-----------------------')
const map3 = new Map();
map3.set(NaN, 123);
console.log(map3.get(NaN)) // 123
map3.set(NaN, 456);
console.log(map3.get(NaN)) // 456

由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然**NaN**不严格相等于自身,但 Map 将其视为同一个键

map的属性和方法

属性:size,返回map结构的成员总数

操作方法

  • set(key,value): set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。可以采用链式写法。
  • get(key):get方法读取key对应的键值,如果找不到key,返回undefined
  • has(key): has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中.
  • delete(key):delete方法删除某个键,返回true。如果删除失败,返回false
  • clear(): clear方法清除所有成员,没有返回值。

遍历方法

  • Map.prototype.keys():返回键名的遍历器。
  • Map.prototype.values():返回键值的遍历器。
  • Map.prototype.entries():返回所有成员的遍历器。
  • Map.prototype.forEach():遍历 Map 的所有成员。

需要特别注意的是,Map 的遍历顺序就是插入顺序。

与set类似,直接使用for...of,等同于使用map.entries(),表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

map[Symbol.iterator] === map.entries   // true

map的应用

  • 实现图结构

    • 代码演示
    class Graph {
      constructor() {
        this.adjacencyList = new Map();
      }
    
      addVertex(vertex) {
        this.adjacencyList.set(vertex, []);
      }
    
      addEdge(vertex1, vertex2) {
        this.adjacencyList.get(vertex1).push(vertex2);
        this.adjacencyList.get(vertex2).push(vertex1);
      }
    }
    
    const graph = new Graph();
    graph.addVertex("A");
    graph.addVertex("B");
    graph.addVertex("C");
    
    graph.addEdge("A", "B");
    graph.addEdge("A", "C");
    
    console.log(graph.adjacencyList);
    
  • 记录元数据

    • 代码演示
    const metadata = new Map();
    
    const user1 = { id: 1, name: "John" };
    const user2 = { id: 2, name: "Jane" };
    
    metadata.set(user1, { role: "admin" });
    metadata.set(user2, { role: "user" });
    
    console.log(metadata.get(user1)); // { role: 'admin' }
    console.log(metadata.get(user2)); // { role: 'user' }
    

WeakMap

WeakMap结构与Map结构类似,也是用于生成键值对的集合。区别有两点。首先,WeakMap只接受对象(null除外)和 Symbol 值作为键名,不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

一般情况下,我们想在一个对象上存放一些数据,会形成对这个对象的引用,一旦不再需要对象就必须手动删除引用,否则垃圾回收机制不会使用占用的内存,造成内存泄漏。WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。也就是说,其他位置对该对象的引用一旦消除,该对象占用的内存就会被垃圾回收机制释放。WeakMap 保存的这个键值对,也会自动消失。

注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

下面我们可以看一下垃圾回收在weakmap上的效果:

  • 代码演示

    首先我们通过手动执行垃圾回收机制,并查看当前内存使用情况。当前使用大概在6M的样子

    $ node --expose-gc    // 允许手动执行垃圾回收机制
    > global.gc()
    > process.memoryUsage()
    

然后我们创建一个weakmap和一个数组类型的key。这时的数组被引用了两次,一次时变量key的引用,一次时weakmap的引用,但是weakmap时弱引用。

> let wm = new WeakMap()
> let key = new Array(5 * 1024 * 1024)
> key
> wm.set(key,1)

这个是时候我们再看一下内存的使用情况,会发现内存被占用了很多

> global.gc()
> process.memoryUsage()

此时我们清空变量key对数组的引用,单不需要手动清除weakmap的实例对键名的引用,并手动执行垃圾回收机制,查看内存的使用情况,会发现内存占用回到了原本的6M,可以看出weakmap的键名没有阻止gc对内存的回收。

> key = null
> global.gc()
> process.memoryUsage()
基本使用

WeakMap 与 Map 在 API 上的区别主要是两个。

一是没有遍历操作(即没有keys()values()entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。

二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()set()has()delete()

weakmap的应用

场景一:WeakMap 应用的典型场合就是 DOM 节点作为键名

 let myWeakmap = new WeakMap();

    myWeakmap.set(
      document.getElementById('logo'),
      {timesClicked: 0})
    ;

    document.getElementById('logo').addEventListener('click', function() {
      let logoData = myWeakmap.get(document.getElementById('logo'));
      logoData.timesClicked++;
      console.log(myWeakmap.get(document.getElementById('logo')))
    }, false);

document.getElementById('logo')是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是这个节点对象。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。

场景二:部署私有属性

// 创建一个 WeakMap 用于存储私有属性
const privateData = new WeakMap();

class MyClass {
  constructor(name) {
    // 将私有属性存储在 WeakMap 中
    privateData.set(this, {name});
  }

  // 公共方法可以访问私有属性
  getName() {
    return privateData.get(this).name;
  }

  // 修改私有属性的方法
  setName(newName) {
    privateData.get(this).name = newName;
  }
}

const person = new MyClass("Lily");
console.log(person.getName()); 
person.setName("Tom");
console.log(person.getName()); 

// 私有属性无法从实例直接访问
console.log(person.name); 

Promise & Generator & Async

Promise

Promise是异步编程的一种解决方案,简单点说,promise是一个容器里面保存着未来才结束的事件的结果。其主要特点有两个:

(1)promise有三种状态,pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。

(2)一旦状态发生了变化就不会在改变了。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

基本使用

Promise对象是一个构造函数,用来生成Promise实例。构造函数接受一个函数作为参数,函数的两个参数分别为resolvereject

注意,调用resolve或reject并不会中断promise的操作,一般来说最好在他们前面加上return语句。

Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数是rejected状态的回调函数,它们都是可选的。then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。**finally本质上是then**方法的特例

  • 链式调用example
function firstTask() {
  return new Promise((resolve) => {
    console.log("First task");
    resolve();
  });
}

function secondTask() {
  return new Promise((resolve) => {
    console.log("Second task");
    resolve();
  });
}

function thirdTask() {
  return new Promise((resolve) => {
    console.log("Third task");
    resolve();
  });
}

firstTask()
  .then(() => {
    return secondTask();
  })
  .then(() => {
    return thirdTask();
  })
  .catch((error) => {
    console.error("An error occurred:", error);
  });

其他API:

  • Promise.all(): 用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    • const p = Promise.all([p1, p2, p3])

      p的状态由p1p2p3决定,只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

      只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

      注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

    • example

    const p1 = new Promise((resolve, reject) => {
      resolve('1');
    })
    .then(result => result)
    .catch(e => e);
    
    const p2 = new Promise((resolve, reject) => {
      resolve('2');
    })
    .then(result => result)
    .catch(e => e);
    
    const p3 = new Promise((resolve, reject) => {
      throw new Error('报错了');
      // resolve('3');
    })
    .then(result => result)
    // .catch(e => e);
    
    Promise.all([p1, p2, p3])
    .then(result => console.log('res',result))
    .catch(e => console.log('error',e));
    
  • Promise.race():同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

    • const p = Promise.race([p1, p2, p3]);

      只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

  • Promise.allSettled():有时候,我们希望等到一组异步操作都结束了,不管每一个操作是成功还是失败,再进行下一步操作**。****Promise.all()方法只适合所有异步操作都成功的情况,如果有一个操作失败,就无法满足要求。为了解决这个问题,ES2020 引入了Promise.allSettled()****方法,**用来确定一组异步操作是否都结束了(不管成功或失败)。

    • example
    const promise1 = Promise.resolve("Promise 1");
    const promise2 = Promise.reject(new Error("Promise 2 rejected"));
    const promise3 = new Promise((resolve) => setTimeout(() => resolve("Promise 3"), 1000));
    
    // 使用 Promise.allSettled()
    Promise.allSettled([promise1, promise2, promise3])
      .then((results) => {
        results.forEach((result, index) => {
          if (result.status === "fulfilled") {
            console.log(`Promise ${index + 1} is fulfilled with value: ${result.value}`);
          } else if (result.status === "rejected") {
            console.log(`Promise ${index + 1} is rejected with reason: ${result.reason}`);
          }
        });
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
    
    // Output:
    // Promise 1 is fulfilled with value: Promise 1
    // Promise 2 is rejected with reason: Error: Promise 2 rejected
    // Promise 3 is fulfilled with value: Promise 3
    
    // 使用 Promise.all()
    Promise.all([promise1, promise2, promise3])
      .then((values) => {
        values.forEach((value, index) => {
          console.log(`Promise ${index + 1} is fulfilled with value: ${value}`);
        });
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
    
    // Output:
    // An error occurred: Error: Promise 2 rejected
    

    Promise.all() 会在遇到第一个 rejected Promise 时立即拒绝整个组,而 Promise.allSettled() 会等待所有 Promise 结束,不管是完成还是拒绝。

  • Promise.any():只要参数实例有一个变成**fulfilled状态,包装实例就会变成fulfilled状态如果所有参数实例都变成rejected状态,包装实例就会变成rejected**状态Promise.any()Promise.race()方法很像,只有一点不同,就是Promise.any()不会因为某个 Promise 变成rejected状态而结束,必须等到所有参数 Promise 变成rejected状态才会结束。

  • Promise.resolve():将现有对象转为 Promise 对象

    • 等价关系
    Promise.resolve('foo')
    // 等价于
    new Promise(resolve => resolve('foo'))
    
  • 如果参数是promise对象,Promise.resolve()将原封返回这个实例。

  • 如果参数是一个具有then方法的对象,Promise.resolve()方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then()方法。

  • 如果参数是一个原始值,或者是一个不具有then()方法的对象,则Promise.resolve()方法返回一个新的 Promise 对象,状态为resolved

  • Promise.reject()

    • Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected
    • 等价关系
    const p = Promise.reject('出错了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出错了'))
    

应用

  • 加载图片:将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

    • example
    const { Image } = require("canvas");
    const fs = require("fs");
    
    function loadImage(path) {
      return new Promise((resolve, reject) => {
        const image = new Image();
        image.onload = () => resolve(image);
        image.onerror = (error) => reject(error);
        image.src = path;
      });
    }
    
    const imagePath = "./O1CN01jJUykX1zwNMUeekbT_!!1990086778.jpg";
    
    loadImage(imagePath)
      .then((image) => {
        console.log("Image loaded:", image);
      })
      .catch((error) => {
        console.error("An error occurred:", error);
      });
    
  • ajax封装

Generator

Generator是ES6提供的一种异步编程的解决方案,语法上可以把他理解成一个状态机,封装了内部多个状态。由于执行generator函数会返回迭代器对象,我们还可以把generator函数理解成遍历器对象的生成函数。

在形式上,generator函数是一个普通函数,主要有两个特征,一是,function关键字和函数名之间有一个*;二是函数内部使用yield表达式定义不同状态。

在调用上,主要特点是调用之后,函数并不执行,返回的也不是函数运行的结果,而是一个执行内部状态的指针对象(遍历器对象),内次调用next方法,会让内部指针从上一次停下来的地方开始执行,知道遇到下一个yield语句或者return语句。可以理解成yield是暂停执行的标识,next是恢复执行的方法。

其中,yield表达式只有在next方法调用,内部执行指向该语句时才会执行,类似于“惰性求值”的语法功能。即下面例子中的123+456不会立即求值。

function* gen() {
  yield  123 + 456;
}

每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

前面我们提到过,Generator 函数就是遍历器生成函数,那么我们可以把generator赋值给对象的Symbol.iterator属性,从而使得对象具备Iterator接口,从而可以被...运算符遍历。

基本使用

Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制

数据交换

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

第一个next方法的value属性,返回表达式x + 2的值3。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量**y**接收。

错误处理:使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出错了');
// 出错了

异步任务封装

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log('res',result.bio);
}

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

基于Thunk函数的自动执行

Thunk 函数是自动执行 Generator 函数的一种方法,可以用于 Generator 函数的自动流程管理。下面我们以文件读取为例来了解一下使用。

首先什么是Thunk函数,编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。举例说明:

function f(m) {
  return m * 2;
}
f(x + 5);

// 等同于
var thunk = function () {
  return x + 5;
};

function f(thunk) {
  return thunk() * 2;
}

js语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。举例说明

// 正常版本的readFile(多参数版本)
fs.readFile(fileName, callback);

// Thunk版本的readFile(单参数版本)
var Thunk = function (fileName) {
  return function (callback) {
    return fs.readFile(fileName, callback);
  };
};

var readFileThunk = Thunk(fileName);
readFileThunk(callback);

Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。

  • example

    内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

function run(gen){
  var g = gen();
  // 层层添加回调函数
  function next(data){
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function(data){
      next(data);
    });
  }
  next();
}

run(gen);

co模块

co 也可以自动执行 Generator 函数,前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

两种方法可以做到这一点。

(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。

  • 使用

    • example
    const fs = require('fs');
    const readFile = function (fileName) {
      return new Promise(function (resolve, reject) {
        fs.readFile(fileName, function(error, data) {
          if (error) return reject(error);
          resolve(data);
        });
      });
    };
    var gen = function* () {
      var f1 = yield readFile('../Async/file1.txt');
      var f2 = yield readFile('../Async/file2.txt');
      console.log(f1.toString());
      console.log(f2.toString());
    };
    
    var co = require('co');
    co(gen).then(function () {
      console.log('Generator 函数执行完成');
    });
    

Async

async 函数是什么?一句话,它就是 Generator 函数的语法糖。我们可以使用generator和async分别实现两个文件读取的异步操作。一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await

但async对Generator的主要改进在以下几方面:

  • Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。不同于Genertor需要co模块或next方法才能执行
  • async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。语义上比*和yeild更明确。
  • yeild语句后面只能时Thunk函数或Promise对象,但是await后面可以是Promise对象或原始类型的值。
  • async返回的是Promise对象,比generator返回Iterator对象方便,可以直接使用then指定下一个操作。

基本用法

async

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。其中return语句返回的值会成为then方法回调函数的参数。

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有**async函数内部的异步操作执行完,才会执行then**方法指定的回调函数。

  • example
async function getTitle(url) {
  let response = await fetch(url);
  let html = await response.text();
  return html.match(/<title>([\s\S]+)</title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)

上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

await命令

await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象,这里我们可以看一个休眠的例子。

  • sleep example
class Sleep {
  constructor(timeout) {
    this.timeout = timeout;
  }
  // then (resolve, reject) {
  //   const startTime = Date.now();
  //   setTimeout(
  //     () => resolve(Date.now() - startTime),
  //     this.timeout
  //   );
  // }
}

(async () => {
  const sleepTime = await new Sleep(1000);
  console.log(sleepTime);
})();

如果没有then方法直接返回一个sleep对象,定义了then方法会将它看作一个promise处理。

使用注意点
  • 防止中断处理

    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject而任何一个**await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。但是很多情况下,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个****await放在try...catch结构里面或者单独给第一个await加上一个catch,这样不管这个异步操作是否成功,第二个await**都会执行

    • example
// reject
async function f1() {
  await Promise.reject('出错了');
  return await Promise.resolve('hello world'); 
}
f1().then(v => console.log(v)).catch(e => console.log(e))
//error
async function f4() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了');
  });
  return await Promise.resolve('hello world');
}
f4().then(v => console.log(v)).catch(e => console.log(e))

优化处理

// reject
async function f2() {
  try {
    await Promise.reject('出错了');
  } catch(e) {
    console.log(e)
  }
  return await Promise.resolve('hello world'); 
}
f2().then(v => console.log(v)).catch(e => console.log(e))
// error
async function f5() {
  try {
    await new Promise(function (resolve, reject) {
      throw new Error('出错了');
    });
  } catch(e) {
    // console.log(e)
  }
  return await Promise.resolve('hello world');
}
f5().then(v => console.log(v)).catch(e => console.log(e))
  • 多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。通常我们可以使用promise.all

  • async函数可以保留运行堆栈,当使用 async 函数时,它会在执行异步操作时暂停函数的执行,并在操作完成后恢复执行。这种暂停和恢复不会导致堆栈信息丢失,因为 async 函数会确保在恢复执行时保留堆栈信息。

    • example
    async function fetchData() {
      try {
        const response = await fetch("https://api.example.com/data");
        const data = await response.json();
        return data;
      } catch (error) {
        console.error("Error:", error);
        // Error stack contains the correct context of the error
        console.error("Error stack:", error.stack);
      }
    }
    
    fetchData();
    

async 函数可以保留运行堆栈,使得在处理异步操作时,可以轻松获取错误发生的上下文,进行调试。这使得错误处理更加简洁和直观,提高了代码的可读性和可维护性。

基本原理

async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  });
}

所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    const gen = genF();
  **  function step(nextF) {**
      let next;
      try {
        next = nextF();
      } catch(e) {
        return reject(e);
      }
      **if(next.done) {
        return resolve(next.value);
      }**
      Promise.resolve(next.value).then(function(v) {
       ** step(function() { return gen.next(v); });**
      }, function(e) {
        step(function() { return gen.throw(e); });
      });
    }
   ** step(function() { return gen.next(undefined); });**
  });
}

总结:

三种方案都是为解决传统的回调函数而提出的,所以它们相对于回调函数的优势不言而喻。而async/await又是Generator函数的语法糖。

  • Promise的内部错误使用try catch捕获不到,只能只用then的第二个回调或catch来捕获,而async/await的错误可以用try catch捕获
  • Promise一旦新建就会立即执行,不会阻塞后面的代码,而async函数中await后面是Promise对象会阻塞后面的代码。
  • async函数会隐式地返回一个promise,该promisereosolve值就是函数return的值。
  • 使用async函数可以让代码更加简洁,不需要像Promise一样需要调用then方法来获取返回值,不需要写匿名函数处理Promise的resolve值,也不需要定义多余的data变量,还避免了嵌套代码。

vue学习路线(10.监视属性-watch)

2025年7月7日 10:37

一、用案例引出监视属性 watch

案例:点击‘切换天气’按钮,页面红色文字发生变化(如果是凉爽改为炎热,如果是炎热改为凉爽),同时控制台打印变化日志。

image.png

1、普通写法
<h2>今天天气很
    <span style="color: red">{{info}}</span>
</h2>
<button @click="changeWeather">切换天气</button>
data() {
    return {
        isHot: false,
    };
},
computed: {
    info() {
        return this.isHot ? "炎热" : "凉爽";
    },
},
methods: {
    changeWeather() {
        this.isHot = !this.isHot;
        console.log("天气变化了,现在是:" + this.info + ",原来是" + (this.isHot ? "凉爽" : "炎热"));
    }
},

2、new Vue时传入watch配置

当属性变化时,回调函数自动调用,在函数内部进行计算。

<h2>今天天气很
    <span style="color: red">{{info}}</span>
</h2>
<button @click="isHot = !isHot">切换天气</button>
data() {
    return {
        isHot: false,
    };
},
computed: {
    info() {
        return this.isHot ? "炎热" : "凉爽";
    },
},
watch: {
     info: {
         handler(newValue, oldValue) {
             console.log("天气变化了,现在是:" + newValue + ",原来是" + oldValue);
         },
      },
},
3、通过vm.$watch监视

两种watch用法,handler内部写法一致

<h2>今天天气很
    <span style="color: red">{{info}}</span>
</h2>
<button @click="isHot = !isHot">切换天气</button>
      const vm = new Vue({
        el: "#app",
        data() {
          return {
            isHot: false,
          };
        },
        computed: {
          info() {
            return this.isHot ? "炎热" : "凉爽";
          },
        },
      });

      vm.$watch("info", {
        handler(newValue, oldValue) {
          console.log("天气变化了,现在是:" + newValue + ",原来是" + oldValue);
        },
      });

二、watch 的特点

    1. 当被监视的属性发生变化时,回调函数自动调用,进行相关操作;
    1. 监视的属性必须存在,才能进行监视!!
    1. 监视的两种写法:

    1)new Vue时传入watch配置

    2)通过vm.$watch监视

三、watch 属性的选项

  • immediate: true:表示在监听开始时立即执行回调函数,而不是等到数据变化时才执行。
  • deep: true:表示深度监听,适用于监听对象或数组的变化。
 watch: {
          info: {
            immediate:true,//默认false。改为true表示立即执行回调函数。
            deep:true,// 默认false。深度监听,适用于监听对象或数组
            handler(newValue, oldValue) {
              console.log("天气变化了,现在是:" + newValue + ",原来是" + oldValue);
            },
          },
        },

四、watch 监视属性简写

  • 当配置项只有handler时,可以简写
  • 不能写成箭头函数
1、new Vue时传入watch配置(简写)
watch: {
    info(newValue, oldValue) {
        console.log("天气变化了,现在是:" + newValue + ",原来是" + oldValue);
    },
},
2、通过vm.$watch监视(简写)
vm.$watch("info", function (newValue, oldValue) {
    console.log("天气变化了,现在是:" + newValue + ",原来是" + oldValue);
});

五、watch 深度监视特点

1、深度监听特点
  • 1.vue中的watch默认不监测对象内部值的改变(一层)。
  • 2.配置deep:true可以监测对象内部值的改变(多层)。
2、深度监听案例

image.png

      <h3>a的值是{{numbers.a}}</h3>
      <button @click="numbers.a++">点我a+1</button>
      <h3>b的值是{{numbers.b}}</h3>
      <button @click="numbers.b++">点我b+1</button>
        data() {
          return {
            numbers: {
              a: 1,
              b: 1,
            },
          };
        },
实现一:只监视a,不监视b(监视多级结构中某个属性的变化)
        watch: {
          // 只监视a,不监视b
           'numbers.a': {
              handler(newValue, oldValue) {
                console.log(newValue,oldValue);
              },
            },
        },
实现二:深度监听number变化(监视多级结构中所有属性的变化)
        watch: {
          //深度监听
           'numbers': {
            deep:true,//开启深度监听
              handler(newValue, oldValue) {
                console.log('numbers变化了');
              },
            },
        },
3、备注
  • 1.vue自身可以监测对象内部值的改变,但vue提供的watch默认不可以(为了效率)。

  • 2.使用watch时根据数据的具体结构,决定是否采用深度监听。

❌
❌