学习设计模式js中的设计原则
设计原则是一系列指导性的原则和准则,用于指导软件和系统的设计过程。这些原则旨在提供一种通用的方法,帮助开发人员在设计和实现过程中做出合理的决策,以创建出高质量、可维护和可扩展的解决方案。
实际上,前端开发是软件开发的一个重要领域,并且在现代Web应用程序中扮演着越来越重要的角色。前端开发人员需要掌握和应用许多软件开发的基础知识和概念,例如设计模式、代码架构、测试和调试等,才能开发出高质量、可维护、可扩展的Web应用程序。
设计原则不仅限于特定的编程语言或技术,可以应用于任何类型的软件开发,包括前端开发。
设计原则的作用
提供指导方针
-
设计原则提供了一些通用的指导方针,帮助开发人员在设计和实现过程中做出决策。这些原则可以帮助他们避免一些常见的设计陷阱,提高设计的质量
促进可维护性和可扩展性
-
设计原则强调代码的可维护性和可扩展性。通过遵循这些原则,可以创建出结构清晰、易于理解和修改的代码,使得软件系统更加易于维护和扩展。
提高代码质量
-
设计原则鼓励采用良好的设计实践,包括模块化、单一职责、高内聚低耦合等原则。这些原则可以提高代码的可读性、可测试性和可靠性,从而提高代码的质量。
降低复杂性
-
设计原则通过提供抽象和结构化的方法来降低系统的复杂性。这可以使开发人员更好地理解和处理复杂的问题,减少错误和不必要的复杂性
促进重用性
-
设计原则鼓励重用已有的代码和组件,减少重复编写和维护相似的代码。这可以提高开发效率,减少代码冗余,并提供一致性和标准化的解决方案
改善用户体验
-
一些设计原则也关注用户体验方面,例如简单性原则和可访问性原则。通过设计简单直观的用户界面和考虑用户的需求和能力,可以提供更好的用户体验
一个类或模块(函数、对象)应该只有一个引起它变化的原因,换句话说,一个类应该只有一个责任(通俗点说比如:一个函数只做一件事)。如果一个对象具有多个职责,职责之间相互耦合,那么如果一个职责的逻辑需要修改,势必会影响到其他职责的代码。
代码示例(违反原则)
// 不符合单一职责原则的示例
function calculateAreaAndPerimeter(rectangle) {
const area = rectangle.width * rectangle.height;
const perimeter = 2 * (rectangle.width + rectangle.height);
console.log(`周长: ${perimeter}`);
}
在上面的示例中,展示了一个不符合单一职责原则的函数calculateAreaAndPerimeter
。该函数负责计算矩形的面积和周长,并直接将结果打印到控制台。这违反了单一职责原则,因为函数承担了多个计算和打印多个不同的责任。
代码重构(符合原则)
// 符合单一职责原则的示例
function calculateArea(rectangle) {
return rectangle.width * rectangle.height;
}
function calculatePerimeter(rectangle) {
return 2 * (rectangle.width + rectangle.height);
}
function printPerimeter(perimeter) {
console.log(`周长: ${perimeter}`);
}
// 使用示例
const rectangle = { width: 10, height: 5 };
const area = calculateArea(rectangle);
const perimeter = calculatePerimeter(rectangle);
printPerimeter(perimeter);
我们将计算面积和周长的功能分别提取到两个独立的函数calculateArea
和calculatePerimeter
中。这样,每个函数都只负责一个具体的计算任务,符合单一职责原则。
为了打印结果,我们也创建了独立的函数printPerimeter。这样,计算和打印的责任得到了清晰的分离。
通过这种重构,我们使得代码更加清晰、可维护,并且每个函数都遵循了单一职责原则
。
软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。当需要增加功能需求的时候,则尽量通过扩展新代码的方式
,而不是修改已有代码。因为修改已有代码,则会给依赖原有代码的模块带来隐患,增加测试成本。
代码示例(违反原则)
// 不符合开放封闭原则的示例
function calculateTotalPrice(cartItems) {
let totalPrice = 0;
for (const item of cartItems) {
if (item.type === 'book') {
totalPrice += item.price * 0.9; // 10% 折扣
} else if (item.type === 'clothing') {
totalPrice += item.price * 1.2; // 20% 涨价
} else if (item.type === 'electronic') {
totalPrice += item.price * 1.5; // 50% 涨价
}
}
return totalPrice;
}
const totalPrice = calculateTotalPrice(cartItems)
在上面的示例中,首先展示了一个不符合开放封闭原则的函数calculateTotalPrice
。该函数根据不同商品的类型进行不同的计算逻辑,这意味着每次添加新的商品类型时,都需要修改该函数
。这违反了开放封闭原则,因为函数不是对扩展开放的,而是对修改开放的。为了符合开放封闭原则,我们对代码进行了重构。
代码重构(符合原则)
// 符合开放封闭原则的重构后的代码
const discountList = {
book: 0.9, // 书籍 10% 折扣
clothing: 1.2, // 服装 20% 涨价
electronic: 1.5, // 电子产品 50% 涨价
}
function calculateTotalPrice(cartItems, discount = discountList) {
let totalPrice = 0;
for (const item of cartItems) {
if (discount[item.type]) totalPrice += item.price * discount[item.type]
}
return totalPrice;
}
const totalPrice = calculateTotalPrice(cartItems)
// 新增鞋子类型,打8折
// discountList.shoe = 0.8
const discountListNew = {
...discountList,
shoe: 0.8
}
calculateTotalPrice(cartItems, discountListNew)
我们将商品的类型、对应商品类型的折扣优惠,提取到一个公共对象discountList
中,商品type类型作为key值
,对应的折扣优惠为value值
,重构了calculateTotalPrice方法中关于totalPrice的计算逻辑,商品的折扣优惠根据商品的type类型作为key从discountList对象中,取出对应的value值(折扣优惠)。这样每次添加新的商品时,无需再次更改calculateTotalPrice方法
,只需要给discountList新增属性即可。或者,传入一个新的 discountListNew 对象
它是关于模块之间相互依赖(引用、使用)关系
的一个约束,高层模块不应该依赖于低层模块的具体实现(实例对象),它应该依赖于低层模块的抽象
。
代码示例(违反原则)
// 不符合依赖倒置原则的示例
class EmailSender {
sendEmail(email, message) {
// 发送电子邮件的具体实现
}
}
class NotificationService {
constructor() {
this.emailSender = new EmailSender();
}
sendNotification(user, message) {
const email = user.getEmail();
this.emailSender.sendEmail(email, message);
}
}
在上面的示例中,首先展示了一个不符合依赖倒置原则的类设计。NotificationService
类内部创建了一个具体的EmailSender
对象,并依赖于该对象来发送通知
。这种设计限制了NotificationService
类的灵活性,使其与具体的EmailSender
实现紧密耦合
在一起。
代码重构(符合原则)
// 符合依赖倒置原则的示例
class EmailSender {
sendEmail(email, message) {
// 发送电子邮件的具体实现
}
}
class NotificationService {
constructor(sender) {
// 接收一个实现了EmailSender的对象
this.sender = sender;
}
sendNotification(user, message) {
const email = user.getEmail();
this.sender.sendEmail(email, message);
}
}
// 使用示例
const emailSender = new EmailSender();
const notificationService = new NotificationService(emailSender);
为了符合依赖倒置原则,我们对代码进行了重构。首先,我们定义了一个EmailSender类,用于发送电子邮件。然后,NotificationService类的构造函数接受一个实现了EmailSender类的对象,而不是直接创建EmailSender对象。
这样,NotificationService类不再依赖于具体的EmailSender实现
,而是依赖于抽象。这符合依赖倒置原则,高层模块(NotificationService)依赖于抽象,而不依赖于具体的实现细节
。
在使用示例中,我们首先创建了一个具体的EmailSender对象。然后,我们通过将EmailSender对象传递给NotificationService的构造函数来创建NotificationService对象。这样,NotificationService类可以与任何实现了EmailSender接口的对象进行协作。
通过这种依赖倒置的设计,我们降低了类之间的耦合性
,提高了代码的灵活性和可维护性。任何实现了EmailSender类的对象都可以被传递给NotificationService类,实现了依赖的反转
。
在传统的依赖关系中,高层模块(或类、函数)依赖于低层模块的具体实现。这意味着高层模块需要直接引用和依赖于低层模块。如:
// 一个类直接依赖另一个类的实例对象
// DataFetcher 紧耦合了 MySQLDatabase 这个数据库类
class DataFetcher {
fetchData(){
const dataBase = new MySQLDatabase()
return dataBase.getData()
}
}
// 一个函数直接依赖一个第三方函数
// calcFunc 方法紧耦合了 lodash.multiply 方法
const calcFunc = function(width, height){
//...一些逻辑
const lodash = require('lodash');
return lodash.multiply(width, height)
}
而在依赖倒置中,我们将这个依赖关系进行反转。高层模块不再直接依赖于低层模块的具体实现,而是依赖于一个抽象或者说”约定”。根据依赖倒置原则重构代码如下:
class DataFetcher {
constructor(database) {
this.database = database;
}
fetchData(){
return this.database.getData()
}
}
const SQLDataBase = new MySQLDatabase()
const SQLDataFetcher = new DataFetcher(SQLDataBase);
SQLDataFetcher.fetchData()
const OracleDataBase = new OracleDatabase()
const OracleDataFetcher = new DataFetcher(OracleDataBase);
OracleDataFetcher.fetchData()
// 将DataFetcher和具体的数据库类解耦,DataFetcher赖于抽象接口 database,
// 而不依赖于具体类或实例对象。这样,无论是使用 MySQLDatabase 还是 OracleDatabase,
// 都可以通过依赖注入的方式传递给 DataFetcher,实现对不同数据库的数据获取操作
const lodash = require('lodash');
const calcFunc = function(width, height, func = lodash.multiply){
//...一些逻辑
return func(width, height)
}
calcFunc(width, height)
calcFunc(width, height, lodash.add)
// 将 calcFunc 方法与lodash.multiply 解耦,不再依赖于具体的函数,而是依赖一个抽象'func'
简单来说依赖倒置原则的两个核心
:
高层模块应依赖于一个抽象或者说“约定”,而不是直接依赖于具体实现。
具体的低层模块应实现了这个抽象或者说'约定',从而能够满足高层模块的需求。
它是关于父类、子类继承关系的一个约束。该原则的定义是:如果S是T的子类型,那么在任何程序中,所有使用T类型的地方都可以用S类型的对象替换而不会导致程序的任何错误
。
换句话说,子类应该能够完全替代父类
并在不破坏程序正确性的前提下扩展或修改其行为。
代码示例:
class Animal {
makeSound() {
console.log("动物在发出声音");
}
}
class Cat extends Animal {
makeSound() {
console.log("猫在喵喵喵");
}
}
class Dog extends Animal {
makeSound() {
console.log("狗在汪汪汪");
}
}
function animalMakeSound(animal) {
animal.makeSound();
}
const animal = new Animal();
const cat = new Cat();
const dog = new Dog();
animalMakeSound(animal); // 输出: "动物在发出声音"
animalMakeSound(cat); // 输出: "猫在喵喵喵"
animalMakeSound(dog); // 输出: "狗在汪汪汪"
在上述代码示例中,我们定义了一个Animal类作为父类,以及Cat和Dog作为子类。这些子类继承了父类的makeSound方法,并根据自身特性进行了具体实现。
在animalMakeSound函数中,我们接收一个Animal类型的参数,然后调用其makeSound方法。根据里式替换原则,我们可以将Cat和Dog实例作为参数传递给animalMakeSound函数,而不会导致逻辑错误或异常。
这个示例展示了符合里式替换原则的代码。子类能够替换父类,并且在使用父类类型的地方,我们可以使用子类类型进行替代,而不会破坏程序的正确性
。
里式替换原则之子类重写父类方法时,应遵循:
子类的方法参数类型应该与父类的方法参数类型相同或更宽松
子类的方法返回类型应该与父类的方法返回类型相同或更严格
子类的方法不应该抛出比父类方法更多或更宽泛的异常
子类的方法不应该改变父类方法的预置条件 (即方法的前置条件)
子类的方法不应该改变父类方法的后置条件(即方法的后置条件)
如果子类重写父类的方法时遵循了上述原则,那么它就符合里氏替换原则。
客户端(接口使用端)不应该强迫依赖于它们不使用的接口
。简而言之,这意味着一个类不应该被迫实现它不需要的接口方法或属性,接口应该被分离成更小的、更具体的部分
,以使得每个类只需要实现与其相关的接口
。接口隔离原则强调将大型、臃肿的接口拆分成更小、更具体的接口,以便每个客户端(接口使用端)只需关注其所需的接口方法,确保接口的精简和高内聚,减少代码耦合。
代码示例(违反原则)
// 不符合接口隔离原则的示例
class Vehicle {
constructor() {
this.speed = 0;
}
start() {
console.log('启动')
}
stop() {
console.log('停止')
}
accelerate() {
console.log('加速')
}
brake() {
console.log('刹车')
}
refuel() {
console.log('加油')
}
}
class Car extends Vehicle {
constructor() {
super();
// 具体汽车的特性和实现
}
}
class Bicycle extends Vehicle {
constructor() {
super();
// 具体自行车的特性和实现
}
}
const bicycle = new Bicycle()
bicycle.refuel() // 自行车不需要加油,它的实例却可以使用 refuel方法
代码重构(符合原则)
// 符合接口隔离原则的示例
class Vehicle {
constructor() {
this.speed = 0;
}
start() {
console.log('启动')
}
stop() {
console.log('停止')
}
accelerate() {
console.log('加速')
}
brake() {
console.log('刹车')
}
}
class Motor {
refuel() {
console.log('加油')
}
}
// mixinClass函数支持实现多重继承
class Car extends mixinClass(Vehicle, Motor) {
constructor() {
super();
// 具体汽车的特性和实现
}
}
class Bicycle extends Vehicle {
constructor() {
super();
// 具体自行车的特性和实现
}
}
// 使用示例
const car = new Car();
car.start();
car.refuel()
const bicycle = new Bicycle()
bicycle.start();
前端开发中,通常提到的接口(API)指的是前端应用程序与后端服务器之间的通信接口
,用于请求和响应数据。这些接口定义了前端应用程序可以向服务器发送哪些请求以及服务器将返回哪些数据。然而,在接口隔离原则中,"接口"的含义有所不同。接口在这里指的是面向对象编程中的接口,它描述了一个类或对象所提供的方法和行为
。它是一种契约或协议,定义了类或对象与其使用者之间的约定
。
在前端开发中,我们可以应用接口隔离原则来设计前端组件、模块或类的接口,以确保它们的设计合理、可维护和可扩展。
也被称为最少知识原则
,它提出:一个对象应该对其他对象保持最小的了解
。换句话说,一个对象应该只与它直接相关的对象进行通信,而不应该了解其他对象的内部细节。迪米特法则的核心思想是减少对象之间的依赖关系(降低耦合度),使得对象的设计更加独立和可复用。对象只应该与其直接的朋友进行通信,而不需要与朋友的朋友进行直接通信
直接的朋友是指与当前对象有紧密关联的对象:
当前对象本身
当前对象的成员变量
当前对象所创建的对象
以参数形式传入到当前对象的方法中的对象
两个核心:
对象之间的通信限制:迪米特法则要求一个对象只与直接的朋友进行通信,不要和陌生的对象直接通信。
限制对象对其他对象的了解程度:迪米特法则要求一个对象对其他对象的了解应该越少越好。一个对象只需知道它需要与之交互的对象的接口,不需要深入了解对象的内部实现细节。
代码示例(违反原则)
class Customer {
constructor(name) {
this.name = name;
this.cart = new ShoppingCart();
}
addToCart(item) {
this.cart.addItem(item);
}
checkout() {
const totalPrice = this.cart.calculateTotalPrice();
// 执行支付逻辑
console.log(`顾客 ${this.name} 已结账。总价: ${totalPrice}`);
}
}
class ShoppingCart {
constructor() {
this.goods = [];
}
addItem(item) {
this.goods.push(item);
}
calculateTotalPrice() {
let totalPrice = 0;
for (const item of this.goods) {
totalPrice += item.price;
}
return totalPrice;
}
}
class Goods {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
// 使用示例
const customer = new Customer("张三");
const goods1 = new Goods("product 1", 10);
const goods2 = new Goods("product 2", 20);
customer.addToCart(goods1);
customer.addToCart(goods2);
customer.checkout();
在上述示例中,Customer 类直接依赖于 ShoppingCart 类的内部实现细节。Customer类知道ShoppingCart类具有addItem和calculateTotalPrice方法,并且直接调用这些方法来添加商品和计算总价格。
这违反了迪米特法则,因为Customer类过度了解ShoppingCart类的内部实现细节
。按照迪米特法则,Customer类应该只与ShoppingCart类的公共接口进行交互,而不需要了解其具体实现。
代码重构(符合原则)
class Customer {
constructor(name) {
this.name = name;
}
// 与传入的参数对象通信,通过它提供的合适的接口(不暴露过多内部实现细节)访问到必要的信息
checkout(cart) {
const totalPrice = cart.getTotalPrice();
// 执行支付逻辑
console.log(`顾客 ${this.name} 已结账。总价: ${totalPrice}`);
}
}
class ShoppingCart {
constructor() {
this.goods = [];
}
addGoods(item) {
this.goods.push(item);
}
calculateTotalPrice() {
let totalPrice = 0;
for (const item of this.goods) {
totalPrice += item.price;
}
return totalPrice;
}
getTotalPrice() {
return this.calculateTotalPrice()
}
}
class Goods {
constructor(name, price) {
this.name = name;
this.price = price;
}
}
// 使用示例
const goods1 = new Goods("Product 1", 10);
const goods2 = new Goods("Product 2", 20);
const cart = new ShoppingCart()
cart.addGoods(goods1);
cart.addGoods(goods2);
const customer = new Customer("张三");
customer.checkout(cart);
一个对象应该尽量减少对其他对象的直接依赖
。一个对象不应该直接访问其他对象的内部属性和方法
,而应该通过尽可能少的接口与其他对象进行通信。
对象之间的通信应该通过最少的层次进行
,即避免出现"链式调用"
的情况。因为这会增加对象之间的耦合性。如果需要多个对象进行协作,应该通过引入中间对象或通过事件/消息机制进行通信
,以减少直接依赖和耦合。
通过封装和抽象来隐藏对象的内部实现细节。对象应该暴露合适的接口
,让其他对象只能访问到必要的信息,从而降低对象之间的耦合性。
当对象需要与其他对象进行通信时,应该使用依赖注入或依赖反转等技术,而不是直接创建和管理对象的实例。
所谓"依赖注入":通过外部传入的方式将依赖对象注入到对象中。在依赖注入中,依赖关系不是由对象自身创建或者获取,而是由外部的调用者或者容器负责创建和传递给对象。具体实现依赖注入的方式有多种,包括构造函数注入(new)、属性注入和接口注入(提供专门的注入接口)等。
以上的几个原则,有些技术文章分享中把它们称为 “SOLID 原则” ,也有的叫“面向对象编程中的六大原则”。不管叫什么吧,我们在理解这些设计原则时,应该从一种更加广义的范围去理解这些设计原则背后的思想
,做开发时尽量遵循这些设计原则的约定。
模块内部的元素应该紧密相关(高内聚
),而模块之间的依赖应该尽量减少(低耦合
)。高内聚和低耦合可以提高代码的可读性、可维护性和可测试性
软件的行为应该符合用户的预期,避免令人困惑或出乎意料的行为。最小意外原则要求开发人员设计和实现软件时尽量遵循常规的、符合用户认知和预期的方式
,以提供一致性和易用性的用户体验。
在设计软件时,应该鼓励和促进代码的重用
。通过抽象、模块化和组件化的方式,可以使得代码更易于复用,并减少重复编写代码的工作量,提高开发效率。
软件设计应该保持简单,避免不必要的复杂性和过度工程化。简单原则强调优先选择简单而有效的解决方案,避免过度设计和过度优化
,以确保代码的可读性和可理解性
我们学习了这些设计原则,并不是要强调开发时一定要遵循什么原则,什么样的代码书写是符合原则的,什么样的代码是错误的。
我们只是讨论和理解这些设计原则背后的思想、关键点是什么。比如我个人的一些理解:
1.对于函数,单一职责是说 “一个函数只做一件事儿”
。但在实际开发中,我们一个函数肯定有“综合性函数”,它做了很多件事,我们不能直接说这个函数是不对的,是不符合原则的,这样就片面了。而是说,理解了设计原则的思想,我们尽量把 “一大坨代码”进行逻辑归类、拆分出不同职责的几个函数,尽可能一个函数承担“一个责任”,这样做是好的。
2.开放封闭原则,是说我们在书写代码的过程中,要考虑它的一个开放性和封闭性
,尽可能实现对扩展开放,对修改封闭。个人体会,就是写函数时尽量 单一职责,尽量 函数内依赖一个“抽象”(依赖函数传入的参数),而不直接依赖一个“具体细节”。如果说在一个综合性函数中我们把一些实现的具体细节写的非常清楚,那么大概率是不符合开放封闭的,因为一旦有需求的变化,这些“具体实现的逻辑代码”就一定会要修改的。正确的做法是,我们把一些具体实现的细节放在另一个单一职责的函数中去,然后把这个函数通过参数,传入进来使用。
3.对于我们前端来说,书写函数,最重要的两个原则一是 单一职责(一个函数只干一件事而)
;二是 依赖倒置(应依赖一个抽象,而不依赖于具体实现)
;只要能大概遵循,那我们的代码也大概率是低耦合的,能轻松实现开放封闭。
存在3个函数A、B、C, 要求A函数执行完成,返回成功态,然后执行B函数,B函数判断A函数是成功态则执行,执行完成返回成功态,然后执行C函数,C函数执行完成,重置events,输出重置语句。
let events = {}
function A(){
// A函数的逻辑...
console.log('A函数执行完成')
events.A = true
B()
}
function B() {
if (!events.A) return
// B函数的逻辑...
console.log('B函数执行完成')
events.B = true
C()
}
function C(){
if (!events.B) return
// C函数的逻辑...
console.log('C函数执行完成')
events = {}
console.log('A、B、C函数都执行完成,events被重置')
}
// 执行A
A()
export events
const status = {
success: 200,
fail: 500,
reset: 100,
}
// type Status = 'success' | 'fail' | 'reset'
function A(){
// A函数的逻辑...
console.log('A函数执行完成')
return status.success
}
function B() {
// B函数的逻辑...
console.log('B函数执行完成')
return status.success
}
function C(){
// C函数的逻辑...
console.log('C函数执行完成')
return status.reset
}
let events = {}
function execute() {
events.A_status = A()
if (events.A_status === status.success) events.B_status = B()
if (events.B_status === status.success) events.C_status = C()
if (events.C_status === status.reset) {
events = {}
console.log('A、B、C函数都执行完成,events被重置')
}
}
// 执行
execute()
export events
需求变更(无需改动原先的代码)
我希望先执行B函数,B函数执行成功再执行A函数, 然后再执行C函数...
function execute_B() {
events.B_status = B()
if (events.B_status === status.success) events.A_status = A()
if (events.A_status === status.success) events.C_status = C()
if (events.C_status === status.reset) {
message = {}
console.log('A、B、C函数都执行完成,events被重置')
}
}
execute_B()
// 我希望可以任意改变函数执行顺序,并且,可以重复执行某个函数,或者任意新增函数执行,只要能保证最后一个函数执行完,重置就可以了
const status = {
success: 200,
fail: 500,
reset: 100,
}
function A(){
// A函数的逻辑...
console.log('A函数执行完成')
}
function B() {
// B函数的逻辑...
console.log('B函数执行完成')
}
function C(){
// C函数的逻辑...
console.log('C函数执行完成')
}
// 新增加D函数
function D() {
// D函数的逻辑...
console.log('D函数执行完成')
return {msg: 'D函数执行完了'}
}
const running = (func, returnStatus, fail) => (func(), returnStatus)
const getReturnStatus = (index, last, reset, success) => index === last ? reset : success
const handleReset = (e, reset) => e.currentStatus === reset ? (e = {}, console.log('所有函数都执行完成,events被重置'), e) : e
// 重新定义新的执行
function handleExecute(...args) {
let events = {}
args.forEach((func, i) => {
if (!events.currentStatus || events.currentStatus === status.success) {
events.currentStatus = running(func, getReturnStatus(i, args.length - 1, status.reset, status.success), status.fail)
}
})
events = handleReset(events, status.reset)
return events
}
let events = handleExecute(A, B, C)
events = handleExecute(C, B, A)
events = handleExecute(C, B, C)
events = handleExecute(C, B, A, D)
一个按钮的文本色为黑色背景色为浅灰色,当光标 mouseover 的时候文本色为蓝色、背景色为绿色、尺寸变为 1.5 倍,当光标 mouseleave 的时候还原文本色、背景色、尺寸,在鼠标按下的时候文本色变为红色、背景色变为紫色、尺寸变为 0.5 倍,抬起后恢复原状。
Document
Document
提问:
还可以优化吗?
假如出现了第2个dom元素需要应用不同的逻辑, 你不得不这样写了:
第二个dom
...
...
const { hoverStyle, clickStyle, defaultStyle } = requestStyleConfig();
var btn = document.getElementById("btn");
btn.addEventListener("mouseover", setElementStyle(hoverStyle));
btn.addEventListener("mouseleave", setElementStyle(defaultStyle));
btn.addEventListener("mousedown", setElementStyle(clickStyle));
btn.addEventListener("mouseup", setElementStyle(defaultStyle));
var second = document.querySelector(".second");
second.addEventListener("mouseover", setElementStyle(clickStyle));
second.addEventListener("mouseleave", setElementStyle(defaultStyle));
second.addEventListener("mousedown", setElementStyle(hoverStyle));
second.addEventListener("mouseup", setElementStyle(defaultStyle));
// 上面dom 绑定这块可以被抽象和解耦吗?
const btnStyle = {
id:'btn',
class: '',
events : {
mouseover: hoverStyle,
mouseleave: defaultStyle,
mousedown: clickStyle,
mouseup: defaultStyle
},
handler: setElementStyle
}
const secondStyle = {
id:'',
class: '.second',
events: {
mouseover: clickStyle,
mouseleave: defaultStyle,
mousedown: hoverStyle,
mouseup: defaultStyle
},
handler: setElementStyle
}
const bindEvents = (dom, events) => {
Object.keys(events).forEach(key => dom.addEventListener(key, events[key]))
}
const createEvents = (eventStyle, creatEventFunc) => {
const events = {}
Object.keys(eventStyle).forEach(key => {
events[key] = creatEventFunc(eventStyle[key])
})
return events
}
const domBindStyle = (domStyle) => {
const id = domStyle.id
const className = domStyle.class
const dom = id ? document.getElementById(id) : document.querySelector(className)
if (!isNodeElement(dom)) throw Error("找不到Dom元素");
bindEvents(dom, createEvents(domStyle.events, domStyle.handler))
}
domBindStyle(btnStyle)
domBindStyle(secondStyle)
正如你所看到的,这里有三个不同的组件,每个组件有自己的单一职责:ProductList
、ProductFilter
和 ProductDetail
。
ProductList 组件负责显示产品列表,并在选择某个产品时发出 product-selected 事件。
ProductFilter 组件负责显示类别列表,并在选择某个类别时发出 category-selected 事件。
ProductDetail 组件负责显示所选产品的详细信息。
将职责分离到不同的组件中,使得代码更容易理解、维护和测试
。每个组件都可以单独进行测试,对一个组件的更改不会影响到其他组件。
为 List.vue
组件,添加排序功能
- {{ item.text }}
扩展1个composable(useSorting.js):
import { ref, watchEffect } from 'vue'
export default function useSorting (items) {
const sortOrder = ref('ascending')
const sortedItems = ref([])
function toggleSortOrder () {
sortOrder.value = sortOrder.value === 'ascending' ? 'descending' : 'ascending'
}
watchEffect(() => {
sortedItems.value = items.value.sort((a, b) => {
if (sortOrder.value === 'ascending') {
return a.text.localeCompare(b.text)
} else {
return b.text.localeCompare(a.text)
}
})
})
return {sortOrder, sortedItems, toggleSortOrder}
}
使用 list 组件并通过 useSorting composable 为其添加排序功能的 App 组件:
Form.vue组件
扩展Form.vue组件并添加两个文本字段:
通过这种方式 UI 的开发变得更加灵活和可扩展。里氏替换原则也是已经集成到 Vue 架构中的东西,我们在使用它时并不关心它实际上叫什么名字。
它要求我们将复杂结构分解成具有更简单行为和单一职责的较小组件.
A组件依赖于B组件的数据,并没有直接去使用B组件数据,而是"约定好"使用抽象props来传递数据。
组件之间通过共同约定好的“接口”、”信号”进行通信,不关心各自内部的实现。
举个“不友好”的例子(实际业务开发场景):
有一个form.vue表单组件,嵌套多个input.vue等子组件,每个子组件都有1个 validate()方法,用来校验用户输入是否正确,返回检验值,现在,form组件点击 提交submit,需要首先执行所有子组件的validate()完成校验通过,才可以提交成功。