Summarized some common knowledge points and questions in intermediate front-end interviews, covering topics such as the event loop, on-demand loading in Webpack and Vite, HTTP cache control strategies, and the differences between Vue2 and Vue3.
Event Loop
The JS event loop is a mechanism that allows JavaScript to remain responsive while handling user input, network requests, and other asynchronous events.
It is a continuous loop that performs the following steps:
- Check the event queue: The event queue is a queue that stores events to be processed. If there are events in the queue, they are taken out and the corresponding event handlers are executed.
- Execute microtasks: Microtasks are small pieces of code in JavaScript that are executed before the current execution context ends. For example, the code in the then() method of a Promise is a microtask.
- Execute macrotasks: Macrotasks are pieces of code in JavaScript that are executed after the current execution context ends. For example, the code inside the setTimeout() function is a macrotask.
- Render the page: If any changes occur at any stage of the event loop, the browser will re-render the page to reflect these changes.
The event loop in JS is one of the core mechanisms for implementing asynchronous programming. It ensures that JS can handle asynchronous tasks in a single-threaded environment, maintains the execution order of the code, and avoids blocking.
Event Loop Mechanism
The event loop is part of the JS runtime environment and is responsible for managing the execution stack and the task queue. The basic process of the event loop is as follows:
- Execution Stack: During the execution of JavaScript code, an execution stack, also known as the call stack, is formed. When the main thread executes synchronous tasks, it places tasks in the execution stack in order to execute them.
- Task Queue: After an asynchronous task is completed, it is placed in the task queue awaiting execution. The task queue is divided into the macrotask queue and the microtask queue.
- Event Loop: After the main thread finishes executing the current call stack, it will check whether there are tasks in the microtask queue. If there are, it will execute all tasks in the microtask queue one by one. Then, the main thread will take a task from the macrotask queue and place it in the call stack for execution. This process repeats continuously, forming the event loop.
It can actually be understood as:
Check event queue --> Execute microtasks --> Execute macrotasks --> Render the page
Macrotasks and Microtasks
- Macrotasks: Include the overall code, setTimeout, setInterval, I/O, UI rendering, etc. They are placed in the macrotask queue to wait for execution.
- Microtasks: Include Promise callback functions, MutationObserver callback functions, etc. They are placed in the microtask queue to wait for execution.
The execution priority of microtasks is higher than that of macrotasks. To be more precise:
After the tasks in the call stack are executed, before executing the next macrotask, the microtask queue will be checked first. If there are tasks, all tasks in the microtask queue will be executed first. Only after all microtasks are completed will the next macrotask be executed.
Example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>微任务和宏任务的优先级</title>
</head>
<body>
<button onclick="handleClick()">点击我</button>
<script>
function handleClick() {
console.log('点击了按钮');
setTimeout(() => {
console.log('计时器');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
}
</script>
</body>
</html>When the user clicks the button, a click event is triggered. The browser executes the corresponding click event handler, which outputs "Button clicked" to the console.
Then, the event handler calls the setTimeout() function, which executes a callback function after 0 milliseconds.
Next, the event handler calls the Promise.resolve() method, which creates a new Promise object and resolves it to a successful state.
Because microtasks have higher priority than macrotasks, the code in the Promise's then method executes before the callback function in the setTimeout() function, and outputs "Promise" to the console.
Finally, after all microtasks have executed, all macrotasks are executed. Therefore, the callback function in the setTimeout() function runs after all microtasks and outputs "Timer" to the console.
It is important to note that the concepts of microtasks and macrotasks only exist in the browser environment. In the Node.js environment, the execution order of the event loop is different.
On-demand Loading in Webpack and Vite
Both Webpack and Vite support code on-demand loading (Code Splitting) and lazy loading, but their implementation methods differ.
On-demand Loading in Webpack
Webpack mainly uses dynamic imports and code splitting strategies to implement on-demand loading.
Dynamic Imports
By using the 'import()' syntax to dynamically import modules in code, Webpack will automatically package the imported modules into a separate chunk file.
import("path/to/module").then((module) => {
// ...
});Code Splitting
Webpack also provides some configuration options to optimize code splitting, such as automatically separating third-party library files through the optimization.splitChunks option, or manually separating code using import().
Vite's On-Demand Loading
Vite leverages the dynamic import feature of native ES modules, using the browser's native import syntax for on-demand loading in the development environment, while using Rollup to implement code splitting in the production environment.
ES Module Dynamic Import
Vite uses the browser's native import() syntax to achieve dynamic module imports, thereby enabling on-demand loading.
Efficient Caching
In development mode, Vite provides an efficient module caching mechanism, using the browser cache to avoid unnecessary module reloads. For source code, it uses a negotiated cache, while for dependencies, it uses a strong cache strategy, achieving efficient HMR (hot module replacement).
Pre-Built Dependencies
Vite pre-builds third-party dependencies, improving the efficiency of dynamic imports.
Summary
Both Webpack and Vite support on-demand loading, but the implementation methods and focus areas differ. Webpack relies on its bundling mechanism and the import() syntax to achieve code splitting and lazy loading, suitable for large projects that require complex configuration and optimization. Vite, on the other hand, leverages modern browsers' ES module import features, providing faster development experience and efficient on-demand loading, suitable for modern web projects, especially in development environments. In production builds, Vite optimizes via Rollup to achieve efficient code splitting and loading.
HTTP Cache Control Strategy
HTTP cache control strategy refers to the technique where the client and server work together to store resource copies in the client's cache, reducing repeated requests and improving performance. HTTP cache strategies are generally of two types: strong cache and negotiated cache.
Strong Cache
Controlled by the server, forcing the client to cache resources.
Strong cache refers to the server forcing the client to cache resources by setting the Cache-Control and Expires fields in the HTTP response headers.
Cache-Control
Cache-Control is a more fine-grained cache control mechanism, and it can specify the following attributes:
max-age: Specifies the cache duration of the resource, in seconds.s-maxage: Specifies the cache duration of the resource in shared caches, in seconds.public: Specifies that the resource can be cached by all cache proxies.private: Specifies that the resource can only be cached by the client.no-cache: Specifies that the resource cannot be cached.no-store: Specifies that the resource cannot be cached and must be fetched from the server for every request.
Example:
Cache-Control: max-age=3600, publicExpires
Expires The expiration time of the specified resource, in GMT.
Expires: Thu, 01 Dec 2023 16:00:00 GMTNegotiated Caching
Negotiated caching is determined collaboratively by the client and server to decide whether to cache a resource.
The process of negotiated caching is as follows:
- The client sends a request to the server.
- The server checks whether the resource is in the cache.
- If the resource is in the cache, the server checks whether the resource has expired.
- If the resource has not expired, the server returns the resource to the client and sets the
Etagfield in the response header. - If the resource has expired, the server returns the resource to the client and sets the
Last-Modifiedfield in the response header. - After receiving the response, the client checks the
EtagorLast-Modifiedfields. - If the
EtagorLast-Modifiedfields match the resource in the client cache, the client uses the cached resource. - If the
EtagorLast-Modifiedfields do not match the resource in the client cache, the client sends a new request to the server.
Etag
Etag is a unique string used to identify the version of a resource.
Last-Modified
Last-Modified is the last modification time of the resource.
Example:
GET /index.html HTTP/1.1
If-None-Match: "1234567890"Cache Strategy Selection
Forced caching is suitable for the following scenarios:
- Resources are accessed frequently.
- The content of the resources does not change often.
Negotiated caching is suitable for the following scenarios:
- The content of the resources may change frequently.
- Resources that require precise cache control.
Here are some best practices for cache strategies:
- Use
Cache-Controlinstead ofExpiresto control caching. - Set a longer cache time for static resources.
- Set a shorter cache time for dynamic resources.
- Use
EtagorLast-Modifiedfor negotiated caching. - Regularly update resources in the cache.
Differences between Vue2's data and Vue3's Ref
Both Vue2 and Vue3's ref are used to store reactive data, but there are the following differences between them.
Usage
- Vue2: The
dataoption is used to define reactive data in components. - Vue3: The
reffunction is used to create reactive variables.
Scope
- Vue2: Data in the
dataoption is globally visible in the component instance. - Vue3: Variables created by the
reffunction are visible within the current scope.
Reactivity
- Vue2: Data in the
dataoption is reactive by default. - Vue3: Variables created by the
reffunction need to be accessed with.valueto trigger reactive updates.
Type
- Vue2: Data in the
dataoption can be of any type. - Vue3: Variables created by the
reffunction can be of any type, but it is recommended to use basic types or simple objects to avoid performance issues.
Default Value
- Vue2: Data in the
dataoption can have default values set. - Vue3: Variables created by the
reffunction require default values to be explicitly passed in.
Updates
- Vue2: Data in the
dataoption can be directly updated throughthis.data.xxx. - Vue3: Variables created by the
reffunction can be updated through.value.
Destruction
- Vue2: Data in the
dataoption will be destroyed when the component is destroyed. - Vue3: Variables created by the
reffunction need to be manually destroyed; otherwise, it will cause memory leaks.
Differences Between watch and watchEffect in Vue3
Listening Method
watch: Can listen to one or multiple reactive data sources and specify a callback function to execute when the data changes.watchEffect: Listens to all reactive data used inside the callback function and re-executes the callback when the data changes.
Callback Function Parameters
watch: The callback function receives two parameters, the new value and the old value.watchEffect: The callback function does not receive any parameters.
Execution Timing
watch: The callback function is not executed during the component's initial render; it only executes when the data changes.watchEffect: The callback function executes immediately when the component initially renders and executes again when the data changes.
Performance
watch: Executes the callback only when the watched data changes, so the performance cost is lower.watchEffect: Re-executes the callback whenever any dependent data changes, so the performance cost is relatively higher.
Usage Scenarios
watch: Suitable for situations where precise control over the watched scope is needed and when both new and old values are required.watchEffect: Suitable for situations where there is no need to explicitly define the data to watch and only automatic operations based on data changes are needed.
Summary
watchandwatchEffectare both functions in Vue 3 used to listen to changes in reactive data.watchis more suitable for situations that require precise control over the listening scope and access to both new and old values.watchEffectis more suitable for situations where there is no need to explicitly define the data to be watched and actions only need to be executed automatically based on data changes.
Using WebSocket
WebSocket is a protocol for full-duplex communication between the client and the server. It can be used in various real-time communication applications, such as chat, games, and data streams.
The usage of WebSocket is as follows:
- Establish a WebSocket connection on both the client and server sides.
- Send and receive data on both the client and server sides.
- Close the WebSocket connection.
The following is a simple example of using WebSocket:
Client-side code:
const socket = new WebSocket('ws://localhost:8080');
socket.onopen = () => {
console.log('WebSocket connection opened');
};
socket.onmessage = (event) => {
console.log('Received message:', event.data);
};
socket.onclose = () => {
console.log('WebSocket connection closed');
};
socket.send('Hello, world!');Server-side code:
import asyncio
import websockets
async def handler(websocket, path):
while True:
message = await websocket.recv()
await websocket.send(message)
async def main():
async with websockets.serve(handler, 'localhost', 8080):
await asyncio.Future() # run forever
if __name__ == '__main__':
asyncio.run(main())Other ways to achieve real-time communication without using WebSocket are as follows:
- HTTP Polling: The client regularly sends HTTP requests to the server to get updates.
- Long Polling: The client sends an HTTP request to the server and keeps the connection open until the server has updated data.
- SSE (Server-Sent Events): The server sends a continuous stream of events to the client.
- Comet: A technique that combines HTTP polling and long polling.
The choice of method depends on the specific application requirements.
- WebSocket is the best choice for real-time communication because it has low latency and high performance.
- HTTP polling is a simple and easy choice, but it has higher latency.
- Long polling can reduce latency but increases server load.
- SSE and Comet are options between HTTP polling and long polling.
Here are some examples of real-time communication without using WebSocket:
- HTTP Polling:
function poll() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/updates');
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// 更新 UI
}
};
xhr.send();
}
setInterval(poll, 1000);- Long Polling:
function poll() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/updates');
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// 更新 UI
poll();
}
};
xhr.send();
}
poll();- SSE:
<script>
const source = new EventSource('/api/updates');
source.onmessage = (event) => {
const data = JSON.parse(event.data);
// 更新 UI
};
</script>- Comet:
function poll() {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/api/updates');
xhr.onload = () => {
if (xhr.status === 200) {
const data = JSON.parse(xhr.responseText);
// 更新 UI
if (data.more) {
poll();
}
}
};
xhr.send();
}
poll();Advantages of Upgrading a Vue2 Project to Vue3
Performance Improvement
- Faster Virtual DOM: Vue 3 introduces a rewritten virtual DOM algorithm, which significantly improves rendering speed and memory usage compared to Vue 2.
- Tree Shaking Support: Vue 3's design allows unused code to be removed through tree shaking, meaning the final bundled files can be smaller and load faster.
- Fragment, Teleport, Suspense: These new features offer more flexibility and, in some cases, can enhance performance or improve user experience.
Enhanced Composition API
- Better Logic Reuse and Code Organization: The Composition API makes logic reuse easier and helps maintain code organization and clarity in complex components, especially for large projects or applications.
- Reactive System Improvements: Vue 3's reactive system is based on Proxy, which not only performs better but can also detect more types of changes (such as array indices and adding/removing object properties).
Type Support and TypeScript Integration
- Better TypeScript Integration: Vue 3 was designed with TypeScript support from the start, providing improved type inference and integration experience. This allows developers to leverage TypeScript's type safety and increased productivity when working on large projects.
New Component Features
- Composition API: In addition to providing better code organization, it allows more flexible use of Vue’s reactive system.
- Teleport Component: Allows parts of a child component template to be “teleported” to other locations in the DOM, which is very useful for handling modals, tooltips, and similar cases.
- Suspense Component: Offers a new way to manage the loading state of asynchronous components, making the user experience smoother.
Custom Directive API
Vue 3 has improved the custom directive API, making it simpler and more intuitive to use.
Modularization
Vue 3 has a higher degree of modularity, allowing developers to import specific functionalities as needed, which helps reduce the size of the application.
Challenges Brought by the Upgrade
Although Vue 3 brings many improvements, there may also be some challenges during the upgrade process, such as needing to relearn new APIs, migrate existing code, and ensure compatibility with third-party libraries/plugins.
Assessing Whether to Upgrade to Vue 3
Project Requirements
- Performance Requirements: If the project has high performance demands, considering that Vue 3 offers more optimized rendering performance and smaller bundle sizes, upgrading might be a good choice.
- Feature Requirements: Vue 3 introduces several new features, such as the Composition API, Teleport, and Suspense. If these new features are particularly helpful in meeting specific project needs, then upgrading may be appropriate.
Team Skills and Learning Curve
- Adaptability to new technologies: Upgrading to Vue 3 may require team members to learn new concepts and APIs, such as the Composition API. The team needs to assess their own learning abilities and rate of adaptability to new technologies.
- Use of TypeScript: Vue 3 provides better TypeScript support. If the team is already using or planning to use TypeScript, then upgrading to Vue 3 would be a natural choice.
Ecosystem and Third-party Library Support
- Plugin and library compatibility: Ensure that the third-party libraries and plugins your project depends on are compatible with Vue 3. Although many libraries have been updated to support Vue 3, some may not have been updated yet.
- Maturity of tools and ecosystem: Although Vue 3’s ecosystem is growing quickly, it is necessary to evaluate whether the development and debugging tools required are already sufficiently mature and stable.
Project Scale and Complexity
- New or existing projects: For new projects, using Vue 3 directly may be a good choice, as it allows you to take advantage of all the new features and improvements from the start. For existing projects, the complexity and cost of migration need to be evaluated.
- Project scale and longevity: For large or long-term projects, investing in upgrading to Vue 3 may bring long-term benefits, especially considering performance optimization and future maintainability.
Time and Cost
- Upgrade Cost: Assess the time and resource costs during the upgrade process, including learning new APIs, refactoring existing code, testing, and fixing upgrade-related bugs.
- Long-term Maintenance Cost: Considering that Vue 3 will be the main direction for future development, in the long run, upgrading may help reduce maintenance costs and ensure ongoing project updates.
Front-end Design Patterns
First, it's worth stating that this is just a rough introduction to front-end design patterns. To really elaborate, it could take a lot!
Front-end design patterns refer to general solutions used to solve common problems in front-end development. They help developers improve the maintainability, readability, and reusability of code.
Front-end design patterns can be divided into three major categories:
- Creational Patterns: Patterns used for creating objects.
- Structural Patterns: Patterns used for organizing objects.
- Behavioral Patterns: Patterns used for defining the communication between objects.
Creational Patterns
Creational patterns are used for creating objects. They can help developers create objects more flexibly and decouple the object creation process.
Common creational patterns include:
- Singleton Pattern: Ensures a class has only one instance.
- Factory Method Pattern: A factory class that creates an object.
- Abstract Factory Pattern: A factory class that creates multiple types of objects.
- Builder Pattern: Breaks down the creation process of a complex object into multiple steps.
- Prototype Pattern: Creates new objects by cloning existing objects.
Structural Patterns
Structural patterns are used for organizing objects. They help developers combine objects into larger structures and improve code maintainability.
Common structural patterns include:
- Adapter Pattern: Converts an incompatible interface into a compatible one.
- Bridge Pattern: Decouples an object's interface and implementation.
- Composite Pattern: Combines objects into a tree structure.
- Decorator Pattern: Adds functionality to an object without changing the object itself.
- Facade Pattern: Provides a simple facade for the interface of a complex system.
Behavioral Patterns
Behavioral patterns are used to define the communication between objects. They help developers improve code scalability and reusability. Common behavioral patterns include:
- Strategy Pattern: Encapsulates algorithms or behaviors and allows clients to choose different algorithms or behaviors.
- Observer Pattern: Defines a one-to-many dependency so that when one object's state changes, all objects depending on it are notified.
- Publish-Subscribe Pattern: A variant of the observer pattern that allows publishers to release messages, which subscribers can subscribe to.
- Chain of Responsibility Pattern: Connects multiple objects that can handle a request and processes the request one by one.
- Template Method Pattern: Defines the skeleton of an operation and allows subclasses to redefine certain steps without changing the skeleton structure.
- Iterator Pattern: Provides a way to traverse elements in a collection.
Application of Front-End Design Patterns
Front-end design patterns can be applied to various front-end development scenarios, such as:
- Component Development: Design patterns can be used to build maintainable and reusable components.
- Data Management: Design patterns can be used to manage and manipulate data.
- User Interaction: Design patterns can be used to design user interaction interfaces.
- Performance Optimization: Design patterns can be used to optimize front-end performance.
Discussion
Support the author