Skip to content

前端术语解读 - 编程思想类

发表于
更新于
字数总计
阅读量

前端名词解读,本篇是设计思想类。

OOP 面向对象编程

OOP是面向对象编程(Object-Oriented Programming)的缩写。它是一种编程范式,通过“对象”来建模和解决问题,这些对象是类的实例。在前端开发中,OOP主要应用于JavaScript语言。但是需要注意JavaScript在技术上是一门多范式语言,不仅仅支持面向对象,也支持命令式以及函数式编程风格。

在前端开发里使用OOP可以带来以下好处:

  1. 代码组织:通过将相关属性和方法封装到类中,可以使代码更加有组织性和可维护性;

  2. 代码重用:继承机制允许一个类从另一个类那里继承属性和方法,减少了重复代码;

  3. 易于维护:因为代码被分割成独立的对象,所以更容易进行调试和更新;

  4. 抽象:可以通过定义接口或者父类隐藏复杂的实现细节,只暴露必要的部分给使用者。

应用

在现代JavaScript(ES6及以上版本)中,引入了class关键字来正式支持面向对象编程的概念,使得创建对象和实现继承变得更加直观和简单。例如:

js
class Animal {
  constructor(name) {
    this.name = name;
  }
  
  speak() {
    console.log(`${this.name} makes a noise.`);
  }
}

class Dog extends Animal {
  constructor(name, breed) {
    super(name);
    this.breed = breed;
  }

  speak() {
    console.log(`${this.name} barks.`);
  }
}

let d = new Dog('Mitzie', 'Dachshund');
d.speak(); // 输出: Mitzie barks.

这个例子展示了如何定义一个基类Animal和它的子类Dog,并通过extends关键字实现继承,以及super关键字调用父类的构造函数。

MVC与MVVM

在前端开发中,MVC 和 MVVM 是常见的设计模式,它们用于分离关注点,使得代码更加模块化、易于维护和扩展。

MVC (Model-View-Controller)

MVC 模式将应用程序分为三个核心组件:

  • Model(模型):负责处理应用的数据逻辑,直接管理数据、逻辑和规则。

  • View(视图):负责展示数据给用户,也就是UI部分。

  • Controller(控制器):作为Model和View之间的桥梁,接收输入并转换它为命令发给Model或View。

MVVM (Model-View-ViewModel)

MVVM 是一种专门为简化用户界面的开发而设计的模式,它包括:

  • Model(模型):与MVC中的定义相同,处理业务逻辑和数据。

  • View(视图):用户界面,负责显示数据。

  • ViewModel(视图模型):连接Model和View的部分,负责暴露数据对象以及在Model或者View发生变化时进行同步。

应用

假设有一个简单的待办事项应用。当你添加一个新的待办事项时,输入会发送到Controller,Controller会通知Model更新数据,然后Model通知View刷新显示内容。

MVC

模型负责处理应用的数据逻辑和规则。在我们的待办事项应用中,TodoItem可以作为一个模型,它包含了待办事项的详细信息以及相关的业务逻辑。

js
class TodoItem {
    constructor(id, description) {
        this.id = id;
        this.description = description;
        this.completed = false;
    }

    toggleCompleted() {
        this.completed = !this.completed;
    }
}

视图负责呈现数据给用户,并且响应用户的交互。在这个例子中,视图将展示待办事项列表,并提供添加新项目、完成项目的按钮等UI元素。

html
<div id="todo-app">
    <ul id="todo-list"></ul>
    <input type="text" id="new-todo" placeholder="New todo">
    <button id="add-todo">Add</button>
</div>

JavaScript部分可能如下:

js
function renderTodoList(todoItems) {
    const todoListElement = document.getElementById('todo-list');
    todoListElement.innerHTML = '';
    todoItems.forEach(item => {
        const li = document.createElement('li');
        li.textContent = `${item.description} (${item.completed ? 'Completed' : 'Pending'})`;
        todoListElement.appendChild(li);
    });
}

控制器是模型与视图之间的桥梁,它接收用户输入并更新模型或视图。例如,当用户点击“Add”按钮时,控制器会获取输入框中的文本,创建一个新的TodoItem实例,并更新视图以显示新的待办事项。

js
document.getElementById('add-todo').addEventListener('click', () => {
    const newTodoText = document.getElementById('new-todo').value;
    if (newTodoText.trim()) {
        const newItem = new TodoItem(Date.now(), newTodoText);
        // 假设有一个方法可以添加新的待办事项到模型中
        addTodoItem(newItem);
        renderTodoList(getAllTodoItems());
        document.getElementById('new-todo').value = '';
    }
});

通过这个简单的例子,我们可以看到MVC模式是如何工作的:

  • Model (TodoItem):封装了待办事项的数据和行为。

  • View (HTML + 渲染逻辑):展示了待办事项列表,并提供了用户交互的界面。

  • Controller (事件监听器):处理用户输入(如点击按钮),并根据输入更新模型或视图。

MVVM

继续以待办事项应用为例,在MVVM架构下,你可能会使用像Vue.js这样的框架。ViewModel通过双向绑定机制自动同步Model和View之间的数据。当用户在View中更改待办事项的状态时,这些更改会自动反映在Model中,反之亦然。

js
// 简化的Vue示例代码
new Vue({
  el: '#app',
  data: {
    todos: [
      { text: '学习JavaScript', done: false },
      { text: '学习Vue', done: false }
    ]
  },
  methods: {
    addTodo: function() {
      this.todos.push({ text: this.newTodoText, done: false });
      this.newTodoText = '';
    }
  }
});

在这个简化的Vue.js示例中,data属性充当了Model的角色,模板(HTML部分)是View,而Vue实例本身扮演着ViewModel的角色,负责管理数据和逻辑,并确保它们能正确地映射到View上。这样,开发者可以专注于数据和业务逻辑,而不必手动操作DOM来更新界面。

区别

  1. 数据绑定机制
    • MVC:通常依赖于手动的数据同步。控制器负责从模型获取数据,并更新视图,反之亦然。这通常需要编写额外的代码来保持视图和模型之间的一致性。
    • MVVM:通过双向数据绑定自动同步视图(View)和视图模型(ViewModel)。这意味着当视图中的数据发生变化时,这些变化会自动反映在视图模型中,反之亦然,无需开发者手动进行数据同步。
  2. 角色职责
    • MVC:控制器(Controller)作为中介者,接收用户输入并调用模型(Model)或视图(View)的方法来处理业务逻辑或更新界面。
    • MVVM:视图模型(ViewModel)不仅充当了MVC中控制器的角色,还包含了视图的状态和行为逻辑。ViewModel是专门为视图准备的数据模型,它不直接引用视图,而是通过数据绑定与视图交互。
  3. 测试友好性
    • MVC:由于控制器直接处理用户输入并控制应用流程,可能需要模拟HTTP请求等环境来进行单元测试,增加了测试复杂度。
    • MVVM:由于ViewModel不依赖于具体的UI技术,更容易进行单元测试。你可以单独对ViewModel进行测试,而不需要考虑视图的具体实现。
  4. 使用场景
    • MVC:适用于传统的Web应用程序,尤其是那些后端渲染页面较多的应用。
    • MVVM:特别适合现代单页应用(SPA),其中前端负责大部分的用户交互逻辑。Vue.js、Angular等框架都采用了MVVM模式。

Duck Typing 鸭子类型

鸭子类型(Duck Typing)是动态类型语言中的一种概念,主要体现在对对象的行为的关注而非其具体的类型。这一概念源于詹姆斯·惠特科姆·赖利的一句谚语:“如果它走起来像鸭子,叫起来像鸭子,那么它就是鸭子。”在编程领域,这意味着只要一个对象实现了所需的方法和属性,即使该对象不属于某个特定的类或接口,也能被视为某种类型并正常使用。

由于JavaScript 是一门动态类型语言,天然支持这种编程风格,所以在前端开发中广泛的使用鸭子类型(Duck Typing)。

特点

  1. 核心思想

    • 行为优先:鸭子类型关注对象能否完成某项任务,而不关心对象的具体类型。

    • 动态绑定:由于动态类型的特点,对象的类型在运行时才确定,因此可以通过方法的存在与否来进行类型验证。

  2. 优点

    • 灵活性高:减少了代码间的耦合,提高了代码的重用性和适应性。
    • 简化开发流程:开发者不必担心严格的类型约束,可以根据需求快速实现新功能。
  3. 缺点

    • 缺乏明确性:可能导致代码难以理解和调试,因为缺少显式的类型信息。
    • 潜在的风险:如果没有正确处理缺失的方法,可能会导致运行时错误。

应用

通用事件处理器

js
// 假设我们有两个对象,一个是按钮,另一个是链接
const button = {
    click: () => console.log('Button clicked!')
};

const link = {
    click: () => console.log('Link clicked!')
};

// 定义一个通用的点击处理器函数
function handleClick(element) {
    if (typeof element.click === 'function') {
        element.click();
    } else {
        console.error('Element does not have a click method.');
    }
}

handleClick(button); // 输出: Button clicked!
handleClick(link);   // 输出: Link clicked!

// 另外一个不符合条件的对象
const div = {};
handleClick(div);      // 输出: Element does not have a click method.

在这个例子中,handleClick 函数并不关心 element 的具体类型,只需要知道它有没有 click 方法即可。这种方式使得代码更具灵活性和可扩展性。

插件化架构

在一些框架中,如 React 插件或 Vue 组件库,鸭子类型可以帮助实现高度模块化的代码结构。

js
import React from 'react';

// 定义一个通用的渲染函数
function renderComponent(componentInstance) {
    if (componentInstance.render && typeof componentInstance.render === 'function') {
        return <div>{componentInstance.render()}</div>;
    } else {
        throw new Error('Invalid component instance');
    }
}

// 实现第一个组件
class MyFirstComponent extends React.Component {
    render() {
        return <p>This is the first component.</p>;
    }
}

// 实现第二个组件
class MySecondComponent extends React.Component {
    render() {
        return <p>This is the second component.</p>;
    }
}

// 使用渲染函数
function App() {
    const components = [
        new MyFirstComponent(),
        new MySecondComponent()
    ];

    return (
        <div>
            {components.map((comp, index) => (
                <div key={index}>{renderComponent(comp)}</div>
            ))}
        </div>
    );
}

export default App;

在这个例子中,renderComponent 函数只关心传入的对象是否有 render 方法,而不限制具体的类名或其他细节。这使得你可以轻松地添加新的组件,只要它们实现了 render 方法即可。

配置选项

在许多 JavaScript 库中,配置选项通常是通过对象字面量传递的,这些对象可以有不同的形状,但只要包含所需的键值对即可。

js
// 定义一个初始化函数
function initialize(options) {
    if (options.start && typeof options.start === 'function') {
        options.start();
    }

    if (options.end && typeof options.end === 'function') {
        options.end();
    }

    if (options.duration !== undefined && !isNaN(options.duration)) {
        setTimeout(() => {
            if (options.complete && typeof options.complete === 'function') {
                options.complete();
            }
        }, options.duration);
    }
}

// 初始化带有 start 和 end 回调的动画
initialize({
    start: () => console.log('Animation started'),
    end: () => console.log('Animation ended'),
    duration: 1000
});

// 初始化只有 complete 回调的动画
initialize({
    complete: () => console.log('Animation completed without explicit start and end')
});

在这个例子中,initialize 函数根据提供的 options 对象的不同属性来执行相应的操作。只要 options 包含所需的方法或属性,就不必遵循固定的类层次结构。

Mutable/Immutable Data 可变/不可变 数据

在编程中,"mutable"(可变的)指的是对象在其创建后可以被改变或修改。相反的,"immutable"(不可变的)指的是对象一旦创建之后就不能再被修改。在前端开发中,特别是在使用JavaScript这样的语言时,理解可变性和不可变性是非常重要的,因为这会影响到代码的行为、性能以及维护性。

在前端开发中,尤其是在React等框架中提倡使用不可变数据结构来管理组件的状态。这样做可以帮助开发者更容易地理解和跟踪状态的变化,减少由于状态意外变更引起的bug,并简化调试过程。此外,利用不可变数据结构还可以优化应用的性能,比如通过浅比较(shallow comparison)快速判断前后状态是否发生变化,从而决定是否需要重新渲染组件。

示例

可变数据(Mutable Data)

当你处理的是可变数据时,你可以直接修改数据而不需要创建新的数据结构。例如,在JavaScript中,数组和对象都是默认可变的:

js
let arr = [1, 2, 3];
arr.push(4); // 直接修改了原数组
console.log(arr); // 输出 [1, 2, 3, 4]

不可变数据(Immutable Data)

与之相对,不可变数据意味着任何“修改”都会产生一个新的数据结构,而原始数据不会被改变。这种方法有助于避免副作用,并且使得追踪状态变化更加容易。在JavaScript中实现不可变数据结构通常需要手动复制原有数据结构并进行修改,或者使用专门的库如Immutable.js。

js
const arr = [1, 2, 3];
const newArr = [...arr, 4]; // 创建一个新数组而不是修改原有的
console.log(arr); // 输出 [1, 2, 3]
console.log(newArr); // 输出 [1, 2, 3, 4]

Class 类

在现代JavaScript(ES6及以后版本)中,class提供了一种定义对象蓝图的清晰且简洁的语法。虽然JavaScript中的类本质上是基于原型的继承的一种语法糖,但它们使得面向对象编程的概念如类、构造函数、继承等更加直观和易于理解。

基本概念

  • 类(Class):定义了如何创建一个对象的类型。它包含了属性(数据成员)和方法(成员函数)。

  • 构造函数(Constructor):特殊的方法用于创建和初始化类的一个实例对象。在类中定义时使用constructor关键字。

  • 继承(Inheritance):允许一个类继承另一个类的特征,这有助于代码重用和建立类之间的关系。

  • 方法(Methods):定义在类内部的函数,描述了该类的对象可以执行的行为。

示例

定义类

js
class Rectangle {
    constructor(width, height) {
        this.width = width;
        this.height = height;
    }

    getArea() {
        return this.width * this.height;
    }

    getPerimeter() {
        return 2 * (this.width + this.height);
    }
}

const myRectangle = new Rectangle(10, 20);
console.log(myRectangle.getArea()); // 输出: 200
console.log(myRectangle.getPerimeter()); // 输出: 60

在这个例子中,我们定义了一个名为Rectangle的类,它有两个属性:widthheight,以及两个方法:getAreagetPerimeter。通过new Rectangle(10, 20)创建了一个Rectangle类的实例,并调用了它的方法来获取面积和周长。

继承

我们可以从一个已有的类派生出新的类,新类将自动获得父类的所有属性和方法,并可以添加或覆盖这些特性。

js
class Square extends Rectangle {
    constructor(sideLength) {
        super(sideLength, sideLength); // 调用父类的构造函数
    }

    // 可以添加或覆盖方法
    isSquare() {
        return true; // 对于正方形总是返回true
    }
}

const mySquare = new Square(15);
console.log(mySquare.getArea()); // 输出: 225
console.log(mySquare.isSquare()); // 输出: true

这里,Square类继承自Rectangle类,因此它可以访问Rectangle的所有方法。此外,Square还定义了自己的方法isSquare,表明任何Square实例都是正方形。

通过这种方式,class关键字使得JavaScript中的面向对象编程更加直观和高效,支持了诸如封装、继承等重要概念,同时也简化了代码结构。

FP 函数式编程

函数式编程(Functional Programming)是一种编程范式,它将计算视为数学函数的求值,避免了改变状态和可变数据。函数式编程鼓励使用纯函数、高阶函数以及不可变数据结构等概念,使得代码更加简洁、易于理解和测试。

在 JavaScript 这样的语言中,由于其天生支持闭包和高阶函数等特性,函数式编程变得非常流行。利用函数式编程的思想,可以帮助开发者写出更加模块化、更具有复用性的代码,同时也能够提高代码的可靠性和开发效率。流行的前端框架和库,如 React 或者 lodash 等,都或多或少地采用了函数式编程的概念。

示例

假设我们有一个数组,里面包含了一些用户的对象,每个对象都有一个 age 属性表示用户的年龄。我们的目标是过滤出所有成年人(年龄大于等于18岁),然后创建一个新的数组,其中只包含这些成年人的名字。

非函数式编程

js
let users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 17 },
  { name: "Carol", age: 30 }
];

let adultNames = [];
for (let i = 0; i < users.length; i++) {
  if (users[i].age >= 18) {
    adultNames.push(users[i].name);
  }
}
console.log(adultNames); // 输出 ["Alice", "Carol"]

函数式编程

使用函数式编程的思想,我们可以利用 JavaScript 的 filtermap 方法来更简洁地完成同样的任务。

js
const users = [
  { name: "Alice", age: 25 },
  { name: "Bob", age: 17 },
  { name: "Carol", age: 30 }
];

const adultNames = users
  .filter(user => user.age >= 18) // 过滤出年龄大于等于18的用户
  .map(user => user.name); // 将过滤后的用户对象转换为名字

console.log(adultNames); // 输出 ["Alice", "Carol"]

Hooks 钩子

在前端开发中,“Hooks” 概念最先由 React 提出。Hooks允许函数组件拥有状态和生命周期方法等类组件的特性,而无需编写类。它们使得代码更加简洁、易于理解和测试。

从更广泛的角度来看,“Hook”可以被理解为一种设计模式或机制,它允许函数或模块之间以一种更加动态和灵活的方式进行交互。具体来说,Hook通常指的是一个函数,该函数可以在不修改原始代码的情况下扩展或改变某些行为。这种模式不仅限于React框架,实际上,在其他编程环境和语言中也有类似的概念。

在React中,Hooks 存在以下基本概念

  • State Hook:使用useState来为函数组件添加状态。
  • Effect Hook:使用useEffect来执行副作用操作,如数据获取、订阅或手动DOM更新等。
  • 自定义Hooks:封装可复用的逻辑,便于在多个组件间共享功能。

示例

在React中的 Hooks

State Hook:下面的例子展示了如何使用 useState 来管理一个计数器的状态:

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

function Counter() {
    // 使用useState初始化状态,count是当前状态,setCount是更新状态的函数
    const [count, setCount] = useState(0);

    return (
        <div>
            <p>You clicked {count} times</p>
            <button onClick={() => setCount(count + 1)}>
                Click me
            </button>
        </div>
    );
}

Effect HookuseEffect 可以用来处理组件的副作用,比如加载数据或设置订阅等。下面的例子展示了一个组件在挂载时获取数据并在卸载时清理副作用的操作:

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

function FriendStatus(props) {
    const [isOnline, setIsOnline] = useState(null);

    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }

        // 模拟订阅好友状态
        ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
        // 清理副作用
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
        };
    }, [props.friend.id]); // 只有当friend.id改变时才重新执行effect

    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

在这个例子中,useEffect用于模拟监听朋友在线状态的变化,并且在组件卸载时取消了这个监听,避免了潜在的内存泄漏问题。

自定义Hooks:自定义Hooks让我们可以从不同的组件中提取组件逻辑到可重用的函数中。例如,我们可以创建一个自定义Hook来简化上述检查好友状态的功能:

js
function useFriendStatus(friendID) {
    const [isOnline, setIsOnline] = useState(null);

    useEffect(() => {
        function handleStatusChange(status) {
            setIsOnline(status.isOnline);
        }

        ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange);
        return () => {
            ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange);
        };
    }, [friendID]);

    return isOnline;
}

然后,在其他组件中可以这样使用这个自定义Hook:

js
function FriendStatusWithCustomHook(props) {
    const isOnline = useFriendStatus(props.friend.id);

    if (isOnline === null) {
        return 'Loading...';
    }
    return isOnline ? 'Online' : 'Offline';
}

通过这种方式,Hooks不仅简化了React组件的编写方式,还提高了代码的复用性和可读性。

非React环境中的Hooks

脱离React来讨论Hooks,我们可以从以下几个方面来理解:

插件系统或扩展点: 在许多应用程序或框架中,开发者可以通过定义特定的“钩子”(hooks)或者回调函数来扩展默认行为。例如,在Web开发中,Express.js中间件机制就可以看作是一种Hook实现方式。你可以通过添加中间件来处理请求之前或之后的行为。

js
const express = require('express');
const app = express();

// 这里的中间件就是一个hook的例子
app.use((req, res, next) => {
    console.log('Request received');
    next(); // 调用下一个中间件或路由处理器
});

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(3000, () => console.log('Server started'));

事件监听器: 在JavaScript中,事件驱动编程也是利用了类似的Hook概念。比如,DOM事件监听器允许你在特定事件发生时执行自定义逻辑。

js
document.getElementById('myButton').addEventListener('click', function(event) {
    console.log('Button clicked');
});

生命周期回调: 在Node.js中,某些模块提供了生命周期钩子,允许你在特定时刻插入自己的逻辑。例如,Mocha测试框架允许你定义beforeEachafterEach等钩子来设置测试环境或清理资源。

js
describe('Array', function() {
  beforeEach(function() {
    this.arr = [1, 2, 3];
  });

  it('should contain 3 elements', function() {
    assert.equal(this.arr.length, 3);
  });
});

自定义Hook: 在非React环境中,我们也可以创建自己的Hook来复用逻辑。比如,我们可以创建一个通用的日志记录Hook来统一管理日志输出。

js
function useLogger(prefix) {
    return function log(message) {
        console.log(`[${prefix}] ${message}`);
    };
}

const logger = useLogger('INFO');
logger('This is a log message');

总结

总的来说,无论是React中的Hooks还是其他环境下的类似概念,它们的核心思想都是提供一种机制,让开发者能够在不修改原有代码的基础上,通过预定义的“入口点”或“钩子”来注入额外的功能或逻辑。这种方式有助于提高代码的模块化程度、可维护性和复用性。因此,虽然React将“Hooks”这一概念推广开来,但实际上,这种模式在软件开发中有更广泛的应用。

Options API 选项式API

选项式API(Options API)是Vue.js早期版本中用于组织组件逻辑的主要方式,特别是在Vue 2中得到了广泛应用,并在Vue 3中继续得到支持。它通过将组件的不同特性(如数据、计算属性、方法、生命周期钩子等)分门别类地定义在一个对象的各个选项中来构建Vue组件

核心

选项式API的核心在于它 提供了一种结构化的方式来定义组件的不同方面,每个方面都对应于组件对象的一个属性。这种方式使得代码看起来非常直观和有条理,尤其适合初学者理解和使用。

  • data:定义组件的状态(即响应式数据)。可以是一个函数(在Vue 2中对于单文件组件必须是函数,在Vue 3中对于所有组件都是函数),该函数返回一个包含状态的对象。
  • methods:定义组件内使用的函数,这些函数可以改变组件的状态或触发其他操作。
  • computed:定义计算属性,这些属性基于其他数据进行计算,且只有当依赖的数据发生变化时才会重新计算。
  • watch:监听特定数据的变化,并在数据变化时执行相应的回调函数。
  • 生命周期钩子:例如created, mounted, updated, beforeDestroy等,允许你在组件生命周期的不同阶段执行自定义逻辑。

示例

vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <p>Double Count: {{ doubleCount }}</p>
  </div>
</template>

<script>
export default {
  // 定义组件的状态
  data() {
    return {
      count: 0,
    };
  },
  // 计算属性
  computed: {
    doubleCount() {
      return this.count * 2;
    },
  },
  // 方法
  methods: {
    increment() {
      this.count++;
    },
  },
  // 生命周期钩子
  mounted() {
    console.log('Component has been mounted.');
  },
};
</script>

在这个例子中:

  • data:初始化了一个名为count的状态变量。
  • computed:定义了一个计算属性doubleCount,它基于count值进行计算。
  • methods:定义了一个名为increment的方法,用于增加count的值。
  • mounted:生命周期钩子,当组件挂载到DOM后会自动调用,这里用来打印一条消息到控制台。

Composition API 组合式API

Vue 3引入的组合式API(Composition API)是一种新的组织和复用逻辑的方式,它为开发者提供了更大的灵活性来构建Vue组件。与选项式API(Options API)不同,组合式API允许你基于功能而非类型来组织代码。组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件

核心

  1. setup函数
    • 在Vue 3中,setup函数是使用组合式API的入口点。它在组件创建之前执行,并返回一个对象,该对象中的属性将暴露给模板和其他选项。
  2. 响应式数据
    • Vue 3提供了一系列用于创建响应式数据的方法,如refreactive。这些方法使得你可以轻松地创建能够自动追踪更新的数据源。
  3. 生命周期钩子
    • 组合式API提供了与Vue 2相似的生命周期钩子,但它们以函数的形式存在,例如onMounted, onUpdated, onUnmounted等。
  4. 计算属性和监听器
    • 使用computed可以创建计算属性,它会根据其依赖项自动更新。使用watch可以监听特定数据的变化,并在变化时执行副作用操作。
  5. 自定义逻辑复用
    • 通过将相关逻辑封装到可复用的函数中,组合式API使得逻辑复用变得非常简单。这些函数通常被称为“hooks”,尽管Vue 3本身并没有直接使用这个术语。

示例

简单计数器

vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script>
import { ref, computed } from 'vue';

export default {
  setup() {
    // 创建响应式状态
    const count = ref(0);

    // 定义修改状态的方法
    const increment = () => { count.value++; };
    const decrement = () => { count.value--; };

    // 可选:创建计算属性
    const doubleCount = computed(() => count.value * 2);

    return {
      count,
      increment,
      decrement,
      doubleCount // 可以在模板中使用
    };
  }
};
</script>

在这个例子中,我们使用了ref来创建了一个响应式的count变量,并定义了两个方法incrementdecrement来修改它的值。所有这些都被返回并暴露给了模板。

复用逻辑:为了更好地理解逻辑复用的能力,考虑一个更复杂的场景,比如我们需要在多个组件中实现相同的计数器逻辑。我们可以将这部分逻辑提取到一个单独的函数中:

js
// useCounter.js
import { ref } from 'vue';

function useCounter(initialValue) {
  const count = ref(initialValue);

  const increment = () => { count.value++; };
  const decrement = () => { count.value--; };

  return { count, increment, decrement };
}

export default useCounter;

然后,在任何需要的地方使用这个自定义hook:

vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">Increment</button>
    <button @click="decrement">Decrement</button>
  </div>
</template>

<script>
import { defineComponent } from 'vue';
import useCounter from './useCounter';

export default defineComponent({
  setup() {
    const { count, increment, decrement } = useCounter(0);

    return {
      count,
      increment,
      decrement
    };
  }
});
</script>

这种方式不仅提高了代码的可读性和维护性,还促进了逻辑的复用,减少了重复代码。组合式API的强大之处在于它可以让你自由组合不同的逻辑块,适应各种复杂的应用需求。

Class类 / Hooks / FP / 选项式API / 组合式API 的关系和区别

概括

我们再对以上五个概念进行简单总结:

Class类

  • 定义:在JavaScript中,class提供了一种创建对象的模板,包括构造函数、方法和属性等。
  • 用途:主要用于面向对象编程(OOP),允许通过继承等方式实现代码复用。
  • 示例框架:React早期版本使用类组件来定义组件,并且Vue 2也支持使用类来创建组件。

Hooks

  • 定义:在React中,Hooks是一种特殊函数,允许你在不编写类的情况下使用状态和其他React特性。Vue中虽然没有直接称为“Hooks”的概念,但Vue 3中的自定义组合函数与之类似。
  • 用途:简化状态管理和生命周期管理,使得逻辑复用更加方便。
  • 示例框架:主要应用于React,但Vue 3的组合式API也支持类似的逻辑组织方式。

函数式编程(FP)

  • 定义:一种编程范式,强调使用纯函数、避免共享状态、可变数据和副作用。
  • 用途:提高代码的清晰度、可测试性和可维护性。React和Vue 3都鼓励使用函数式编程的原则。
  • 示例框架:React通过Hooks促进了函数式编程风格;Vue 3则通过组合式API支持了更多的函数式编程实践。

选项式API(Options API)

  • 定义:Vue.js的一种编程模式,通过将组件的不同部分(如data, methods, computed等)作为选项添加到一个对象中来定义组件。
  • 用途:提供了一种直观的方式来组织组件逻辑,特别适合初学者理解和使用。
  • 示例框架:主要应用于Vue 2及Vue 3中,用于构建Vue组件。

组合式API(Composition API)

  • 定义:Vue 3引入的一种新的编程模式,允许开发者基于功能而非类型来组织代码,通常通过setup函数实现。
  • 用途:增强了代码的灵活性和复用性,特别是在处理复杂逻辑时表现尤为突出。
  • 示例框架:Vue 3中推荐的方式,也可以用于React中的某些高级场景(尽管React本身更倾向于使用Hooks)。

关系和区别

  • Class vs. 函数式编程:Class是面向对象编程的核心,而函数式编程则追求无状态和不可变的数据流。两者代表了不同的编程哲学。
  • Hooks vs. 组合式API:虽然名称不同,但两者的目的相似,都是为了增强逻辑复用性和代码组织的灵活性。React的Hooks和Vue 3的组合式API都可以看作是函数式编程原则的应用。
  • 选项式API vs. 组合式API:选项式API按类型组织代码,而组合式API则允许你根据功能需求自由组合逻辑。后者提供了更大的灵活性,尤其是在处理复杂的业务逻辑时。

总结

  • Class类函数式编程代表了两种不同的编程范式,前者侧重于对象和类的关系,后者则强调纯函数和不可变数据。
  • Hooks组合式API都是为了应对传统组件化开发中遇到的问题(如逻辑复用困难)而提出的解决方案,它们使代码更加模块化和易于维护。
  • 选项式API组合式API则是Vue框架内部提供的两种不同的编码风格,前者更适合初学者和小型项目,后者则为大型应用提供了更强的灵活性和扩展能力。

简单画张图表达他们的关系:

image-20250227172813931

Mixins 混入

前端开发中,特别是在Vue.js框架里,mixins(混入)是一种用于分发Vue组件可复用功能的灵活方式。Mixin本质上是一个包含部分选项的对象,这些选项可以被混入到其他组件中,从而让多个组件共享相同的逻辑或行为。通过这种方式,你可以避免重复代码,并促进代码的复用性。

原理

当你将一个mixin对象传递给Vue组件时,该mixin中的选项会与组件自身的选项进行“合并”。对于大多数选项来说,如果组件和mixin都定义了相同的选项,它们的内容会被合并而不是覆盖。但是,对于某些特定选项(如生命周期钩子),mixin和组件的钩子都会被调用。

示例

基本使用:假设我们有一个需要在多个组件中使用的通用功能,比如获取当前时间并格式化显示

vue
// 定义一个mixin
const datetimeMixin = {
  data() {
    return {
      currentTime: new Date(),
    };
  },
  methods: {
    formatTime(date) {
      const hours = date.getHours().toString().padStart(2, '0');
      const minutes = date.getMinutes().toString().padStart(2, '0');
      return `${hours}:${minutes}`;
    }
  },
  mounted() {
    setInterval(() => {
      this.currentTime = new Date();
    }, 1000);
  }
};

// 使用mixin的组件
<template>
  <div>
    <p>Current Time: {{ formattedTime }}</p>
  </div>
</template>

<script>
export default {
  mixins: [datetimeMixin],
  computed: {
    formattedTime() {
      return this.formatTime(this.currentTime);
    }
  }
};
</script>

在这个例子中,datetimeMixin提供了一个currentTime的数据属性、一个formatTime的方法以及一个定时更新当前时间的mounted生命周期钩子。任何使用这个mixin的组件都可以直接访问这些属性和方法。

处理冲突:当组件和mixin都定义了相同的选项时,Vue提供了默认的合并策略来处理这种情况。例如,对于methods、components和directives,它们会简单地合并成一个对象;如果有重复的名字,则组件的选项会覆盖mixin的选项。

js
// 定义两个mixin
const mixin1 = {
  methods: {
    sayHello() {
      console.log('Hello from mixin1');
    }
  }
};

const mixin2 = {
  methods: {
    sayHello() {
      console.log('Hello from mixin2');
    },
    sayGoodbye() {
      console.log('Goodbye from mixin2');
    }
  }
};

// 组件
export default {
  mixins: [mixin1, mixin2],
  created() {
    this.sayHello(); // 输出 "Hello from mixin2"
    this.sayGoodbye(); // 输出 "Goodbye from mixin2"
  }
};

在这个例子中,尽管mixin1mixin2都定义了sayHello方法,但最终调用的是来自mixin2的版本,因为后定义的mixin优先级更高。

总结

虽然mixins是提高代码复用性的有力工具,但在使用时也需要注意一些潜在的问题:

  • 命名冲突:如果不同的mixin定义了相同名称的选项或变量,可能会导致意外的行为。
  • 复杂性增加:过度使用mixins可能导致组件变得难以理解和维护,特别是当多个mixin相互依赖或重叠时。
  • 隐式依赖:Mixins可能引入隐式的依赖关系,这使得追踪数据流变得更加困难。

因此,在设计你的应用架构时,应谨慎考虑是否采用mixins以及如何合理地组织它们。随着Vue 3的组合式API的普及,许多原本需要使用mixins实现的功能现在可以通过组合式API以更清晰的方式达成。

Hooks 和 Mixins 的关系和区别

MixinsHooks都是为了提高代码复用性和模块化而设计的机制,但它们在实现方式、使用场景以及设计理念上存在显著的不同。

  1. 设计理念
    • Mixins倾向于通过横向组合来扩展功能,即不同来源的功能片段被组合在一起形成完整的组件。
    • Hooks则侧重于纵向切分,围绕特定功能(如状态管理、数据获取等)组织代码,使每个Hook专注于单一职责。
  2. 代码结构
    • 使用Mixins时,可能会遇到命名冲突和难以追踪的数据流问题,尤其是在大型项目中。
    • Hooks通过显式的输入输出参数传递依赖关系,减少了意外副作用的可能性,同时保持了良好的局部性。
  3. 适用范围
    • Mixins适用于需要跨多个组件共享通用行为的情况,但在复杂场景下可能导致维护成本上升。
    • Hooks更适合现代前端开发的趋势,尤其是对于追求高内聚低耦合的应用来说,Hooks提供了一种更加直观和灵活的方式来组织和重用逻辑。
  4. 框架支持
    • Mixins主要应用于Vue.js中,尽管在其他语言或框架中也有类似的概念。
    • Hooks最初由React引入,随后影响到了Vue 3的设计,Vue 3的组合式API提供了类似于Hooks的功能,用于逻辑复用和组件构建。

总结

总之,虽然Mixins和Hooks都能帮助开发者提高代码复用性,但Hooks因其更清晰的逻辑组织方式和更好的维护性,在现代前端开发中得到了更广泛的认可和支持。特别是在处理复杂的业务逻辑时,Hooks通常能提供更为简洁有效的解决方案。

生命周期 Lifecycle

在前端开发中,尤其是使用框架如 React 或 Vue 时,“生命周期” 指的是组件从创建到销毁的整个过程中的不同阶段。每个阶段都有特定的钩子(Hooks)或方法,允许开发者执行特定的操作。理解这些生命周期概念对于有效地管理资源、优化性能和确保应用的行为符合预期至关重要。

阶段

通常,一个组件的生命周期可以分为三个主要阶段:挂载(Mounting)、更新(Updating)和卸载(Unmounting)。

示例

React中的生命周期

挂载阶段 (Mounting)

  • 当组件首次被插入到 DOM 中时会经历这个阶段。
  • 主要的生命周期方法包括 constructor()(类组件),componentDidMount()(类组件),以及在函数组件中通过 useEffect 钩子模拟。
  • 示例:在这个阶段,你可以发起网络请求获取数据,设置定时器等。
jsx
useEffect(() => {
  console.log('Component did mount');
  return () => console.log('Cleanup on unmount'); // 清理副作用
}, []); // 空数组意味着只在组件挂载和卸载时执行

更新阶段 (Updating)

  • 当组件的状态或属性发生变化时,会触发重新渲染,进入更新阶段。
  • 包括 shouldComponentUpdate()(决定是否需要重新渲染),render()componentDidUpdate()(类组件),以及在函数组件中可以通过依赖项数组控制的 useEffect 来处理。
  • 示例:可以在 componentDidUpdate 中比较前后状态或属性的变化来进行特定操作。
jsx
useEffect(() => {
  console.log('Component updated');
}, [props.someProp]); // 只有当 someProp 改变时才会触发

卸载阶段 (Unmounting)

  • 当组件从 DOM 中移除时,会调用卸载阶段的方法。
  • 主要是 componentWillUnmount()(类组件),或者在函数组件中通过 useEffect 返回的清理函数实现。
  • 示例:清除定时器、取消未完成的网络请求等。
jsx
useEffect(() => {
  const timer = setInterval(() => console.log('Tick'), 1000);
  return () => clearInterval(timer); // 清理定时器
}, []);

Vue 中的生命周期

  1. 挂载阶段
    • beforeCreatecreated:实例初始化后和数据观测、事件等配置完成之前/之后。
    • beforeMountmounted:模板编译/虚拟DOM挂载到真实DOM前/后。
  2. 更新阶段
    • beforeUpdateupdated:当数据变化导致虚拟DOM重新渲染并在更新之前/之后触发。
  3. 卸载阶段
    • beforeDestroydestroyed:实例销毁之前/之后,适合做清理工作。
js
export default {
  mounted() {
    console.log('Component has been mounted');
  },
  updated() {
    console.log('Component has been updated');
  },
  beforeDestroy() {
    console.log('Cleanup before component is destroyed');
  }
}

总结

生命周期的概念帮助开发者更好地控制组件的行为,特别是在资源管理和状态同步方面。无论是 React 还是 Vue,了解并合理利用生命周期钩子都能极大地提升应用的质量和性能。例如,通过在适当的生命周期钩子中加载数据或释放资源,可以避免内存泄漏和其他潜在问题。