前端&移动端面经
虚拟列表实现原理
虚拟列表(Virtual List)是一种在数据量较大时,优化显示性能和用户体验的技术。其核心思想是在渲染和显示过程中只加载和显示当前视窗内的部分数据,而不是一次性加载和渲染全部数据。这样可以显著减少内存消耗和提高渲染速度。虚拟列表通常用于长列表或表格组件中,特别是在Web应用和移动应用中。
- 实现原理
- 可视区域(Viewport):虚拟列表只渲染用户当前可见区域内的列表项。用户滚动时,更新可视区域内的列表项。
- 缓冲区(Buffer):为了防止滚动时的卡顿,虚拟列表通常会在可视区域上下增加一定数量的缓冲区列表项。这些缓冲区项可以在用户快速滚动时提前加载,保证平滑滚动。
- 位置计算(Position Calculation):通过计算每个列表项在整个列表中的位置,只渲染可视区域和缓冲区内的项。使用绝对定位或CSS变换属性将这些项放置在正确的位置。
- 事件监听(Event Listener):监听滚动事件,根据滚动位置更新可视区域和缓冲区内的列表项。
- 具体实现步骤
- 初始设置:确定列表项的高度(或宽度),以及可视区域的高度(或宽度)。如果列表项高度不一致,需要预估一个平均值或进行动态调整。
- 渲染可视区域内的项:根据当前滚动位置和可视区域大小,计算需要渲染的列表项的起始和结束索引。
- 更新DOM:只将计算得到的列表项添加到DOM中,并更新其位置。未在可视区域和缓冲区内的项应从DOM中移除。
- 滚动事件处理:监听滚动事件,当滚动位置发生变化时,重新计算需要渲染的列表项,并更新DOM。
JavaScript✅
JS数据类型
原始类型:
undefined
,null
,boolean
,number
,bigint
,string
,symbol
引用类型:
object
,array
,function
,date
,regexp
原始类型:这些类型直接包含值,比较时是值的比较。
Undefined
: 一个变量声明了,但没有赋值时,它的值就是undefined
。1
2let x;
console.log(x); // undefinedNull
: 表示一个空的或不存在的对象。与undefined
不同,null
是一个赋值对象,表示变量没有值。1
2let y = null;
console.log(y); // nullBoolean
: 只有两个值:true
和false
。1
2let isActive = true;
console.log(isActive); // trueNumber
: 包括整数和浮点数。1
2
3
4let num = 42;
let price = 9.99;
console.log(num); // 42
console.log(price); // 9.99BigInt
: 可以表示大于Number
类型范围的整数。1
2let bigIntNum = 1234567890123456789012345678901234567890n;
console.log(bigIntNum); // 1234567890123456789012345678901234567890nString
: 用于表示文本数据。1
2let name = "Alice";
console.log(name); // "Alice"Symbol
: 是一种唯一且不可变的数据类型,通常用于对象属性的唯一标识符。1
2let sym = Symbol('description');
console.log(sym); // Symbol(description)
引用类型:引用类型的值是对象,比较时是引用的比较。
Object
: 用于存储集合数据或更复杂的实体。1
2
3
4
5let person = {
name: "Bob",
age: 30
};
console.log(person); // { name: 'Bob', age: 30 }Array
: 一种特殊类型的对象,用于存储有序集合。1
2let numbers = [1, 2, 3, 4, 5];
console.log(numbers); // [1, 2, 3, 4, 5]Function
: 一种可调用的对象。1
2
3
4function greet() {
console.log("Hello, World!");
}
greet(); // Hello, World!Date
: 用于表示日期和时间。1
2let now = new Date();
console.log(now); // 当前日期和时间RegExp
: 正则表达式,用于模式匹配字符串。1
2let pattern = /ab+c/;
console.log(pattern); // /ab+c/
JavaScript中的数据类型可以通过typeof
操作符来检查:
1 | console.log(typeof 42); // "number" |
堆和栈区别
在JavaScript中,堆和栈是两种不同的内存区域,它们各自负责不同类型数据的存储和管理。
- 栈(Stack)
- 存储内容:栈主要存储基本类型数据,如
undefined
,string
,boolean
,number
等,以及函数调用的上下文和局部变量。 - 内存分配与回收:栈内存是由系统自动管理的,当一个函数被调用时,其参数和局部变量会被压入栈中;当函数返回时,这些数据会被弹出并释放。
- 访问方式:栈中的基本类型数据是按值访问的,这意味着你直接访问的是实际的值。
- 优点:内存分配和释放快速,管理简单,不需要额外的垃圾回收。
- 缺点:缺乏灵活性,数据的大小和生命周期需要在编译时确定。
- 存储内容:栈主要存储基本类型数据,如
- 堆(Heap)
- 存储内容:堆主要用于存储复杂类型或引用类型数据,如对象、数组和函数。这些数据的大小在创建时未知且可变。
- 内存分配与回收:堆内存的分配是动态的,由程序员(或更准确地说,由JavaScript引擎的垃圾回收器)控制。当一个对象不再被引用时,垃圾回收器会自动回收这块内存。
- 访问方式:堆中的引用类型数据是按引用访问的,即在栈中存储的是指向堆中实际对象的引用。
- 优点:提供了更大的灵活性,可以在运行时动态地分配和调整内存。
- 缺点:内存分配和访问相对缓慢,由于垃圾回收机制的介入,可能会导致不可预测的性能影响。
- 总结
- 栈内存适合于那些生命周期短、大小固定的变量,如函数内的局部变量。
- 堆内存适合于那些大小未知或可变的数据结构,如对象和数组。
在JavaScript中,函数调用和基本类型的变量通常使用栈内存,而对象、数组和函数体则存储在堆内存中。垃圾回收机制负责监控堆内存中的对象,并在适当时候释放不再使用的内存,以防止内存泄漏。
JS是单线程还是多线程,什么情况下会新开一个线程
JavaScript 在设计上是单线程的,这意味着在任何给定的时间点,JavaScript 引擎只能执行一个任务。这种设计的主要原因在于JavaScript最初是为了在浏览器环境中操作DOM(文档对象模型)而设计的,而DOM必须保持一致性,避免多个线程同时修改DOM导致的潜在冲突和复杂性。
现代的JavaScript环境提供了几种机制来模拟多线程行为或并行处理能力:
- Web Workers:
Web Workers允许JavaScript代码在后台线程中运行,从而不会阻塞UI线程。Web Workers提供了一种将计算密集型任务放到单独的线程中运行的方式,而不会影响到网页的响应速度。Web Workers和主线程之间通过消息传递进行通信。 - Service Workers:
Service Workers是运行在浏览器后台的特殊类型的Worker,它们可以拦截网络请求,缓存资源,甚至在没有网络连接的情况下提供离线服务。Service Workers完全独立于主线程运行,可以处理推送通知和背景同步等功能。 - Shared Workers:
Shared Workers类似于Web Workers,但它们可以被多个窗口、标签页或框架共享,允许这些上下文共享状态和数据。 - Worker Threads in Node.js:
在Node.js环境中,虽然V8引擎本身是单线程的,但Node.js利用事件循环和异步I/O来处理并发。从Node.js v10开始,引入了Worker Threads模块,允许创建在独立线程中运行的JavaScript代码片段,可以用于CPU密集型任务。
web worker有哪些限制,与主线程怎么通信
- 限制
- DOM 访问限制:
- Web Workers 不能直接访问或操作 DOM。这意味着它们不能修改页面的内容或样式,也不能监听或触发 UI 事件。
- 脚本限制:
- Web Workers 不能调用
alert()
、confirm()
或prompt()
函数,因为它们通常用于与用户交互,而 Web Workers 应用于后台任务。 - 也不能使用
window
、document
或location
等全局对象,因为它们与 UI 相关。
- Web Workers 不能调用
- 同源策略:
- Web Workers 只能加载与创建它们的脚本同源的资源。这意味着如果 Web Worker 的脚本来自于不同的源,则会引发安全错误。
- 资源限制:
- Web Workers 的使用可能会受到资源限制,例如每个页面可以创建的 Web Workers 数量,以及每个 Worker 可以使用的内存量。
- 网络请求:
- 尽管 Web Workers 可以使用
XMLHttpRequest
或fetch
API 发起网络请求,但它们不能接收onload
或onerror
等 UI 相关的事件回调。
- 尽管 Web Workers 可以使用
- DOM 访问限制:
- 与主线程的通信:主要依赖于
postMessage
方法和message
事件处理器- 主线程向 Worker 发送消息:
- 主线程可以通过调用 Worker 对象上的
postMessage()
方法来向 Worker 发送消息。这个方法接受一个参数,可以是任意可序列化的 JavaScript 数据类型(如字符串、数字、数组、对象等)。
- 主线程可以通过调用 Worker 对象上的
- Worker 向主线程发送消息:
- Worker 线程同样可以使用
self.postMessage()
方法(self
指的是 Worker 线程的全局作用域)向主线程发送消息。
- Worker 线程同样可以使用
- 监听消息:
- 在主线程和 Worker 中,都可以通过添加事件监听器来处理接收到的消息。在主线程中,这个事件监听器添加到 Worker 对象上;在 Worker 中,事件监听器添加到
self
上。 - 事件处理器通常包含一个
event
参数,其中event.data
包含了发送方通过postMessage
发送的数据。
- 在主线程和 Worker 中,都可以通过添加事件监听器来处理接收到的消息。在主线程中,这个事件监听器添加到 Worker 对象上;在 Worker 中,事件监听器添加到
- 主线程向 Worker 发送消息:
事件循环Event Loop
- 任务队列:在 JS中存在不同类型的任务队列,其中包括:
- 同步任务队列(Synchronous Task Queue):包含同步执行的任务,例如代码块、函数调用等。在执行完一个同步任务后,才会去处理下一个任务。
- 异步任务队列(Asynchronous Task Queue):包含异步执行的任务,例如事件回调、定时器回调等。这些任务不会立即执行,而是在满足一定条件时被推入执行队列。
- 异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,
Promise.then
,MutationObserver
,宏任务的话就是setImmediate setTimeout setInterval
- 异步队列又分为宏任务队列和微任务队列,因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是,
- Event Loop 是一个持续运行的循环,负责处理任务队列中的任务。它的基本工作流程如下:
- 从同步任务队列中取出一个任务,执行该任务。
- 如果在执行同步任务的过程中产生了异步任务(例如定时器、事件回调等),则将这些异步任务添加到异步任务队列中,等待执行。
- 当同步任务队列为空时,Event Loop 会检查异步任务队列。
- 如果异步任务队列不为空,则按照一定的优先级顺序(通常是 FIFO)从队列中取出一个任务,执行该任务。
- 重复步骤 1 至步骤 4。
- 举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17console.log('1. 同步任务开始');
setTimeout(function() {
console.log('2. 异步任务A');
}, 0);
Promise.resolve().then(function() {
console.log('3. 异步任务B');
});
console.log('4. 同步任务结束');
// 假设没有其他操作,输出顺序应该是:
// 1. 同步任务开始
// 4. 同步任务结束
// 3. 异步任务B
// 2. 异步任务A- 首先,
console.log('1. 同步任务开始');
执行并打印 “1. 同步任务开始”。 setTimeout
函数被调用,但是其回调函数不会立即执行,而是会被添加到微任务队列中(实际上,setTimeout
是宏任务,但在现代浏览器中,它的延迟可以非常短,这里我们假设它为0)。- 接下来,
Promise.resolve().then()
被调用,这是一个微任务,它会在当前宏任务结束时立即执行。 console.log('4. 同步任务结束');
执行并打印 “4. 同步任务结束”。- 当前宏任务执行完毕,所有的微任务(这里是
Promise
的回调)将被执行。因此,console.log('3. 异步任务B');
将会被执行并打印 “3. 异步任务B”。 - 最后,事件循环检查宏任务队列,发现有
setTimeout
的回调,所以它将被取出并执行,打印 “2. 异步任务A”。
- 首先,
需要注意的是,微任务(如Promise)在每个宏任务执行完后会立即执行,而宏任务(如setTimeout)则会等待所有微任务执行完后才执行下一个。这就是为什么在上述示例中,尽管setTimeout
的延迟为0,但Promise
的回调却先于setTimeout
的回调执行的原因。
Promise函数
Promise 提供了一种更优雅的方式来处理异步操作,使得代码更易读、更易维护。通过链式调用、Promise.all 和 Promise.race 等方法,可以更灵活地组织和处理异步操作的结果。
- 原理:Promise 是 JavaScript 中处理异步操作的一种机制,其内部原理涉及到状态、回调函数队列等概念。
- Promise 内部有三种状态:
- Pending(进行中):初始状态,表示异步操作尚未完成。
- Fulfilled(已成功):表示异步操作成功完成。
- Rejected(已失败):表示异步操作失败。
- 状态一旦发生变化,就不会再变化。例如,当 Promise 对象从 Pending 状态变为 Fulfilled 状态时,它就不能再变为 Rejected 状态,反之亦然。
- 回调函数队列:Promise 内部有两个队列,分别用于存储成功态和失败态时的回调函数。
- 成功态回调函数队列:存储 then 方法中的成功回调函数。
- 失败态回调函数队列:存储 then 方法中的失败回调函数。
- then 方法:Promise 提供了 then 方法用于注册对异步操作结果的处理逻辑。then 方法接受两个参数:成功回调函数和失败回调函数,它们分别在异步操作成功和失败时被调用。
- 当 Promise 的状态为 Fulfilled 时,会调用成功态回调函数队列中的回调函数;当 Promise 的状态为 Rejected 时,会调用失败态回调函数队列中的回调函数。
- 异步操作的触发和状态改变:异步操作完成后,调用 resolve 方法将 Promise 的状态从 Pending 变为 Fulfilled,并将结果传递给成功态回调函数队列中的回调函数;调用 reject 方法将 Promise 的状态从 Pending 变为 Rejected,并将错误信息传递给失败态回调函数队列中的回调函数。
Promise.all
Promise.all
是一个静态方法,用于将多个 Promise 实例包装成一个新的 Promise 实例。当使用Promise.all
时,通常会传入一个包含多个Promise
对象的数组。Promise.all
会返回一个新的Promise
,这个新的Promise
会在所有输入的Promise
都成功(resolved)后解析,或者如果任何一个Promise
失败(rejected),则立即失败。
1 | const fetch = require('node-fetch'); // 如果在Node.js环境中使用fetch API,需要引入node-fetch |
例子中创建了两个函数fetchGoogle
和fetchBing
,它们各自返回一个Promise。然后我们使用Promise.all
来等待这两个请求同时完成。当所有的请求都成功完成时,Promise.all
返回的Promise将被解析,并将所有解析值组成的数组传递给.then
方法中的回调函数。 如果任何请求失败,Promise.all
返回的Promise将被拒绝,并跳过.then
方法直接调用.catch
方法中的错误处理器。
ES6新特性有哪些
ES6(ECMAScript 2015)是 JavaScript 的一个重要版本,引入了许多新特性和语法糖,使得 JavaScript 更加现代化、功能强大和易用。以下是 ES6 中一些常见的新特性:
- let 和 const:
let
和const
用于声明变量,相比var
具有块级作用域。1
2let x = 10;
const PI = 3.14; - 箭头函数:箭头函数是一种更简洁的函数声明方式,省略了
function
关键字和return
关键字。1
const add = (a, b) => a + b;
- 解构赋值:解构赋值允许从数组或对象中提取数据,并赋值给变量。
1
2const [x, y] = [1, 2];
const { name, age } = { name: 'Alice', age: 25 }; - 默认参数值:函数参数可以设置默认值。
1
2
3function greet(name = 'World') {
console.log(`Hello, ${name}!`);
} - 模板字符串:模板字符串允许使用反引号(`)定义多行字符串,并在字符串中插入变量。
1
2const name = 'Alice';
console.log(`Hello, ${name}!`); - 扩展运算符:扩展运算符(
...
)可以将数组展开成逗号分隔的参数序列,或将对象展开成键值对。1
2
3
4const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5, 6];
const obj1 = { name: 'Alice' };
const obj2 = { ...obj1, age: 25 }; - 类和模块:ES6 引入了类和模块的概念,使得 JavaScript 更像一种传统的面向对象语言。
1
2
3
4
5
6
7
8
9class Person {
constructor(name) {
this.name = name;
}
greet() {
console.log(`Hello, ${this.name}!`);
}
}
export default Person; - Promise:Promise 是一种用于处理异步操作的对象,使得异步编程更加优雅和易读。
1
2
3
4
5
6
7
8
9
10const fetchData = () => {
return new Promise((resolve, reject) => {
// 异步操作
if (success) {
resolve(data);
} else {
reject(error);
}
});
}; - Map 和 Set:ES6 引入了新的数据结构 Map 和 Set,用于存储键值对和唯一值集合。
1
2
3
4
5const myMap = new Map();
myMap.set('key', 'value');
const mySet = new Set();
mySet.add(1); - Symbol:Symbol 是一种新的原始数据类型,表示唯一标识符。
1
const mySymbol = Symbol('description');
变量作用域和作用域链
- 作用域(Scope)是指变量和函数在代码中可访问的范围。在 JavaScript 中,作用域分为以下几种类型:
- 全局作用域(Global Scope):
- 在整个程序中都可以访问的变量。
- 一般在函数外部定义。
- 局部作用域(Local Scope):
- 变量仅在其定义的代码块或函数内部可见。
- 函数参数和在函数内声明的变量都是局部变量。
- 块级作用域(Block Scope):
- 在某些语言中(如JavaScript ES6+),在特定的代码块(如if语句、for循环)中定义的变量只在该块内可见。
- 这有助于避免命名冲突和提升代码可读性。
- 函数作用域(Function Scope):
- 在函数内部定义的所有变量都具有函数作用域,在函数调用时创建,调用结束时销毁。
- 闭包作用域(Closure Scope):
- 当一个函数被另一个函数内部定义时,它可以访问其外部函数的变量,即使外部函数已经返回。这种现象称为闭包。
- 全局作用域(Global Scope):
- 作用链(Scope Chain)是解释器或编译器在查找变量时遵循的一系列作用域。当在一个函数中引用一个变量时,解释器首先在当前函数的作用域中查找,如果没有找到,则会向上一级作用域查找,直到全局作用域。如果仍然没有找到,那么就认为变量未定义。这个查找的过程形成了作用链。
原型和原型链
- 原型关系:
- 每个 class都有显式原型 prototype
- 每个实例都有隐式原型 _ proto_
- 实例的_ proto_指向对应 class 的 prototype
- 原型(Prototype)
- 在 JavaScript 中,每个对象都有一个关联的原型对象。原型对象是一个普通的对象,它包含共享的属性和方法。当你创建一个对象时,JavaScript 引擎会自动为该对象关联一个原型对象。
- 原型链(Prototype Chain)
- 原型链是 JavaScript 中对象之间的一种链接方式,它是由对象的原型组成的链式结构。
- 函数的原型链对象constructor默认指向函数本身,原型对象除了有原型属性外,为了实现继承,还有一个原型链指针__proto__,该指针是指向上一层的原型对象,而上一层的原型对象的结构依然类似。因此可以利用__proto__一直指向Object的原型对象上,而Object原型对象用Object.prototype.__ proto__ = null表示原型链顶端。如此形成了js的原型链继承。
- 当访问对象的属性或方法时,JavaScript 引擎会首先在对象本身查找,如果没有找到,则会继续在对象的原型上查找,直到找到相应的属性或方法或者到达原型链的顶端(即
Object.prototype
)。 - 特点:
JavaScript
对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。
闭包
闭包是指有权访问另一个函数作用域中的变量的函数。
- 闭包的特性:
- 内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。
1.1. 闭包是密闭的容器,,类似于set、map容器,存储数据的
1.2. 闭包是一个对象,存放数据的格式为 key-value 形式 - 函数嵌套函数
- 本质是将函数内部和外部连接起来。优点是可以读取函数内部的变量,让这些变量的值始终保存在内存中,不会在函数被调用之后自动清除
- 内部函数可以访问定义他们外部函数的参数和变量。(作用域链的向上查找,把外围的作用域中的变量值存储在内存中而不是在函数调用完毕后销毁)设计私有的方法和变量,避免全局变量的污染。
- 闭包形成的条件:
- 函数的嵌套
- 内部函数引用外部函数的局部变量,延长外部函数的变量生命周期
- 闭包的用途:
- 模仿块级作用域
- 保护外部函数的变量 能够访问函数定义时所在的词法作用域(阻止其被回收)
- 封装私有化变量
- 创建模块
- 闭包应用场景
闭包的两个场景,闭包的两大作用:保存/保护
。 在开发中, 其实我们随处可见闭包的身影, 大部分前端JavaScript 代码都是“事件驱动”的,即一个事件绑定的回调方法; 发送ajax请求成功|失败的回调;setTimeout的延时回调;或者一个函数内部返回另一个匿名函数,这些都是闭包的应用。 - 闭包的优点:延长局部变量的生命周期
- 闭包缺点:会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏
- 闭包的作用:
- 保护:划分一个独立的代码执行区域,在这个区域中有自己私有变量存储的空间,保护自己的私有变量不受外界干扰(操作自己的私有变量和外界没有关系);
- 保存:如果当前上下文不被释放【只要上下文中的某个东西被外部占用即可】,则存储的这些私有变量也不会被释放,可以供其下级上下文中调取使用,相当于把一些值保存起来了;
this指针的5种情况
- 作为普通函数执行时,
this
指向window
。 - 当函数作为对象的方法被调用时,
this
就会指向该对象
。 - 构造器调用,
this
指向返回的这个对象
。 - 箭头函数 箭头函数的
this
绑定看的是this所在函数定义在哪个对象下
,就绑定哪个对象。如果有嵌套的情况,则this绑定到最近的一层对象上。 - 基于Function.prototype上的
apply 、 call 和 bind
调用模式,这三个方法都可以显示的指定调用函数的 this 指向。apply
接收参数的是数组,call
接受参数列表,bind
方法通过传入一个对象,返回一个this
绑定了传入对象的新函数。这个函数的this
指向除了使用new
时会被改变,其他情况下都不会改变。若为空默认是指向全局对象window。
new运算符实现机制
- 首先创建了一个新的
空对象
设置原型
,将对象的原型设置为函数的prototype
对象。- 让函数的
this
指向这个对象,执行构造函数的代码(为这个新对象添加属性) - 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
浏览器✅
浏览器的同源策略
浏览器采用同源策略(Same-Origin Policy, SOP)来防止跨域访问,SOP限制了一个源(由协议、域名和端口号组成)上的网页脚本与另一个源上的网页进行交互的能力。同源策略要求以下三个部分完全相同:
- 协议(如HTTP与HTTPS)
- 域名(如www.example.com与api.example.com)
- 端口(如80与8080)
跨域是什么及是为了防止什么
防止恶意网站从不同的源获取或者操作敏感数据。
跨域是指一种网页或应用程序通过浏览器访问不同域名、协议或端口上的资源或服务的行为。由于安全原因,浏览器通常会限制这种跨域访问,这是为了防止某些类型的安全威胁,如跨站脚本攻击(Cross-Site Scripting, XSS)和跨站请求伪造(Cross-Site Request Forgery, CSRF)。
跨域是为了防止如下安全威胁:
- 跨站脚本攻击(XSS):
- 恶意脚本被注入到可信网站中,攻击者利用这些脚本来窃取用户信息、劫持用户会话等。
- 通过限制跨域访问,可以防止恶意网站从可信网站中窃取敏感数据。
- 跨站请求伪造(CSRF):
- 攻击者诱导用户在登录状态下访问攻击者构造的恶意网站,从而以用户的身份执行未授权的操作。
- 跨域访问限制可以减少这种攻击的风险,因为恶意网站不能直接向受保护的资源发出请求。
解决跨域问题的方法
虽然跨域访问受到限制,但在某些情况下需要进行跨域请求,比如在前后端分离的应用中。以下是一些常见的解决跨域问题的方法:
- CORS (Cross-Origin Resource Sharing):最常见且推荐的解决方式,它允许服务器通过响应头中的特定字段来指定哪些源可以访问其资源。
- 服务器在响应头中设置特定的CORS头信息,允许指定的跨域请求。
Access-Control-Allow-Origin
:指定哪些源可以访问资源,可以是一个具体的域名,或通配符*表示允许所有源。Access-Control-Allow-Origin: *
或Access-Control-Allow-Origin: http://example.com
Access-Control-Allow-Methods
: 允许的HTTP方法列表,如GET
,POST
,PUT
等。Access-Control-Allow-Headers
: 允许的请求头列表,这对于预检请求(preflight requests)非常重要。Access-Control-Max-Age
: 预检请求的有效期,单位是秒,在此期间,对于同样的请求,浏览器不会再次发送预检请求。Access-Control-Allow-Credentials
: 如果设置为true
,则表示服务器允许包含身份验证信息(如cookies和HTTP认证)的跨域请求。Access-Control-Expose-Headers
: 指定客户端可以从响应中访问的额外头部信息。- 通常可以在服务端通过框架或中间件来配置这些头部。例如,在Node.js的Express框架中,可以使用
cors
中间件来轻松配置CORS。 - 前端通常不需要直接处理CORS配置,因为它主要是在服务器端完成的。但需要确保API调用正确地设置
credentials
属性,以便在需要cookies
或其他认证信息时正确处理请求。
- JSONP (JSON with Padding):一种古老的技巧,利用
<script>
标签没有同源策略限制的特点,但只适用于GET请求。- 通过动态生成
<script>
标签进行跨域请求,因为<script>
标签不受同源策略限制。 - 主要用于GET请求,但现代应用中使用较少。
- 通过动态生成
- 服务器代理 (Proxy):在服务器端设置代理,让所有跨域请求先通过同一源的服务器转发,这样浏览器就不会视为跨域请求。
- 在同源服务器上设置一个代理,通过代理服务器转发请求到不同的域。
- 客户端只与代理服务器通信,代理服务器与目标服务器通信。
- WebSocket
- WebSocket协议允许跨域通信,适用于需要实时双向通信的应用。
浏览器缓存-强制缓存/协商缓存
浏览器缓存是一种用于存储和重复利用资源的机制,它可以减少网络请求次数,提高网页加载速度。浏览器的缓存机制主要分为两种类型:强制缓存和协商缓存。
强制缓存
- 强缓存是指客户端在接收到响应后,响应中的缓存控制指示允许,客户端可以无条件地使用缓存的副本,而不必再次询问服务器。这通常意味着在给定的时间内,资源不会改变,所以不需要再次验证资源的有效性。常见的响应头有
Cache-Control
和Expires
。 Cache-Control
字段中的常见值包括:max-age=<seconds>
:指示响应可以被缓存的最大时间(以秒为单位),在此期间可以被重复使用而无需向服务器验证。no-store
:指示响应不能被存储在任何缓存中。no-cache
:这可能有些令人困惑,因为名字暗示不允许缓存,但实际上它可以与其他指令结合使用,如no-cache=Set-Cookie
,意味着可以缓存响应,但每次使用前需要先验证。must-revalidate
:指示缓存必须在过期后向服务器验证,即使在网络中断的情况下也不可使用过期的缓存。proxy-revalidate
:类似于must-revalidate
,但只适用于代理服务器,终端用户浏览器可以使用过期的缓存直到收到新的响应。public
:指示响应可以被任何缓存存储,包括共享缓存。private
:指示响应只能被单个用户的缓存存储,不能在代理服务器上共享。
Expires
:指定缓存过期时间,是一个绝对的时间点,直到过期前都可以直接使用缓存。
- 强缓存是指客户端在接收到响应后,响应中的缓存控制指示允许,客户端可以无条件地使用缓存的副本,而不必再次询问服务器。这通常意味着在给定的时间内,资源不会改变,所以不需要再次验证资源的有效性。常见的响应头有
协商缓存
协商缓存是指客户端在使用缓存副本之前,需要先向服务器确认资源是否已经改变。可以通过比较客户端缓存的版本和服务器上的版本完成,通常使用ETag
或Last-Modified
和If-Modified-Since
这样的头部。在协商缓存中,服务器会在响应头中包含:
ETag
:一个代表资源版本的唯一标识符。Last-Modified
:资源最后修改的日期和时间。
客户端在后续请求中可以通过以下头部进行验证:
If-None-Match
:与服务器返回的 ETag 对比,以确定资源是否相同。If-Modified-Since
:与服务器的Last-Modified
时间对比,以判断资源是否已更新。
当服务器收到这些请求头时,如果资源没有变化,它会返回一个304
状态码,表示“未修改”,这样客户端就可以继续使用缓存中的副本,从而避免了完整资源的下载。
介绍一下304过程
在HTTP协议中,状态码“304 Not Modified”是在客户端缓存和服务器之间进行缓存验证时使用的一种响应状态。这个过程涉及到了强制缓存和协商缓存的概念,以及HTTP头部如If-Modified-Since
和ETag
的使用。
- 当客户端首次请求一个资源时,服务器会返回该资源,并可能包含以下头部之一:
Last-Modified
- 表示资源最后一次修改的时间。ETag
- 一个代表资源版本的唯一标识符。- 假设服务器返回了资源和
Last-Modified
头部,例如:1
2
3HTTP/1.1 200 OK
Last-Modified: Wed, 01 Jul 2020 01:23:45 GMT
...
- 当客户端需要再次获取同一资源时,它会检查是否有缓存版本,并在请求中包含
If-Modified-Since
头部。- 其值为上次请求时服务器返回的
Last-Modified
时间:1
2
3GET /resource HTTP/1.1
Host: example.com
If-Modified-Since: Wed, 01 Jul 2020 01:23:45 GMT
- 其值为上次请求时服务器返回的
- 服务器接收到请求后,会检查资源是否自上次修改以来有变化:
- 如果资源没有变化,服务器将返回一个304状态码,表示“Not Modified”。这意味着客户端可以继续使用其缓存的副本,无需接收新的数据。
- 如果资源有变化,服务器将返回一个新的200状态码,包含更新后的资源和新的
Last-Modified
或ETag
头部。
304过程对于提高Web性能至关重要,因为它减少了不必要的网络传输。如果资源没有变化,服务器不需要发送完整的资源内容,客户端可以立即使用缓存版本,这显著加快了页面加载速度,同时也减轻了服务器的负载。
总之,304过程是HTTP缓存机制的核心部分,它优化了客户端与服务器之间的通信,提高了用户体验,同时降低了网络带宽消耗和服务器资源的使用。
重排和重绘
“重排”(Reflow)和“重绘”(Repaint)是浏览器渲染网页时发生的两个重要过程,它们对于页面性能有着直接的影响。
重排发生在浏览器需要重新计算元素的几何属性时,比如位置、大小或形状。以下情况可能会触发重排:
- 添加或删除可见的DOM元素。
- 元素的尺寸发生变化(如通过JavaScript修改CSS样式)。
- 触发布局依赖的属性变化,如
width
、height
、padding
、margin
、border
等。 - 文档流中的内容变化,如文本内容的更改。
- 影响:重排会导致浏览器重新计算页面布局,这是一个比较耗时的操作,因为它涉及到复杂的计算,并且可能涉及整个文档流中多个元素的重新定位。
重绘则是在元素的外观发生变化,但不涉及尺寸或位置改变时发生的过程。例如:
- 改变元素的颜色或背景。
- 应用或移除透明度变化。
- 更改元素的
z-index
值(只要不影响布局)。 - 影响:重绘比重排要快一些,因为它不需要重新计算布局,仅需要更新像素颜色。然而,频繁的重绘也会对性能造成影响。
性能优化,为了减少重排和重绘对性能的影响,开发者可以采取以下策略:
- 减少布局依赖的样式更改,尽量使用不影响布局的属性。
- 使用
requestAnimationFrame
来批量处理动画和DOM操作,减少浏览器的重排/重绘次数。 - 避免使用
window.getComputedStyle
或element.offsetHeight
等会触发重排的方法在循环中。 - 尽量减少DOM树的深度和复杂性。
- 利用CSS层叠上下文和独立容器,比如
position: fixed
或transform
,可以让某些元素的改变只影响自身而不影响其他元素。 - 使用硬件加速的CSS属性,让浏览器尽可能利用GPU进行渲染。
React✅
受控组件和非受控组件
React中受控组件和非受控组件主要用于描述如何处理表单元素(如<input>
, <textarea>
, <select>
等)中的用户输入。两者的主要区别在于状态管理和数据流的方向。
- 受控组件(Controlled Components)
- 定义:在受控组件中,表单的值(
value
属性)是由React组件的状态(state)或属性(props)控制的。这意味着React组件负责维护表单元素的状态,并通过事件处理器(如onChange
)更新状态,进而更新显示的值。 - 数据流:数据流是单向的,从React组件的state或props流向表单元素。用户交互会触发事件处理器,导致state更新,然后state的变化会导致组件重新渲染,更新表单的值。
- 定义:在受控组件中,表单的值(
- 非受控组件(Uncontrolled Components)
- 定义:非受控组件的值不由React组件直接控制,而是由DOM元素自身管理。React不会通过
value
属性来设置表单元素的值,而是通过defaultValue
属性设置初始值,之后的值变化由DOM自行处理。 - 数据读取:由于React不直接控制表单元素的值,它通常使用
ref
来访问DOM节点,以便读取和设置值。
- 定义:非受控组件的值不由React组件直接控制,而是由DOM元素自身管理。React不会通过
- 选择使用哪种组件
- 受控组件通常用于需要实时响应用户输入并执行某些逻辑(如验证)的情况,或是需要将表单值作为应用程序状态的一部分的情况。
- 非受控组件可能在性能敏感或复杂的表单中更有效率,因为它们减少了状态更新的次数,特别是在大型应用中,这可能会带来性能优势。
- 某些情况下可能需要混合使用两种类型,或使用“受控与非受控”的组合组件,这种组件可以在没有提供
value
属性时作为非受控组件工作,在提供了value
时作为受控组件工作。这在一些库组件中比较常见,比如Ant Design的一些组件就支持这种模式。
useLayoutEffect和useEffect区别
useEffect
和 useLayoutEffect
都是 React Hooks 中用于处理副作用(例如数据获取、订阅或者手动修改 DOM)的钩子函数。它们的主要区别在于执行时机和对渲染的影响。
- 执行时机:
useEffect
: 这个 Hook 是在 DOM 更新后异步执行的,即它不会阻塞浏览器的渲染。这意味着在执行useEffect
的回调函数时,屏幕上的元素已经完成渲染,因此任何在useEffect
中的操作都不会影响当前屏幕上的渲染输出。这使得useEffect
更适合处理那些不需要立即反映到用户界面上的副作用,如网络请求、事件监听器的设置或清除等。useLayoutEffect
: 相比之下,useLayoutEffect
是同步执行的,它的回调函数会在所有 DOM 变更完成后但在浏览器绘制更新前执行。这意味着useLayoutEffect
中的代码可以改变布局,并且这种变化会立即反映到用户界面上,不会造成视觉上的闪烁或抖动。这使得useLayoutEffect
更适合处理那些需要立即反映到布局中的副作用,如测量 DOM 节点的尺寸或位置,或直接修改 DOM。
- 性能影响:
- 因为
useEffect
是异步执行的,所以它通常对性能的影响较小,特别是在涉及到复杂的计算或大量的 DOM 操作时。这是因为浏览器可以优先完成渲染,然后再处理副作用,从而保持流畅的用户体验。 useLayoutEffect
的同步执行特性意味着如果它包含的逻辑过于复杂,可能会阻塞浏览器的渲染线程,导致页面卡顿。因此,在性能敏感的场景下,应当谨慎使用useLayoutEffect
。
- 因为
- 兼容性:
useEffect
在所有现代浏览器中都有良好的兼容性。useLayoutEffect
在某些旧版浏览器中可能不被支持,尤其是那些不支持异步渲染的浏览器。
推荐首先尝试使用 useEffect
,因为它的异步执行方式更加符合现代 Web 应用程序的性能要求。只有在特定场景下,比如需要在渲染之前进行精确的 DOM 测量或修改,才应该使用 useLayoutEffect
。
hooks为什么不能在if中使用
React Hooks(如 useState
, useEffect
等)设计时有一些关键的规则需要遵守,以确保组件的正确渲染和生命周期的一致性。其中一条规则是,Hooks 必须在函数组件或自定义 Hook 的顶层调用,而不能在循环、条件或嵌套函数中调用。
不建议在 if 条件语句中调用 Hooks 的原因如下:
- 可预测性:
React 需要知道何时调用 Hooks,以便能够追踪状态更新和副作用。如果在 if 语句中调用,React 将无法确定每次渲染时是否应该调用这些 Hooks,这会导致状态和副作用的混乱。 - 一致性和确定性:
Hooks 被设计成在每次组件渲染时按照相同的顺序调用,这保证了状态和副作用的一致性。如果将 Hooks 放入条件逻辑中,那么每次渲染调用的 Hooks 可能不同,这违反了 Hooks 的这一核心原则。 - 避免内存泄漏:
如果你在条件语句中使用了useEffect
,并且该条件不再满足,你可能忘记清理之前的副作用,从而导致内存泄漏。
为了遵循这些规则,应该在函数组件的主体内始终调用你的 Hooks,而不是在 if 语句中。如果需要根据条件执行某些操作,可以考虑使用 useState
或 useEffect
的依赖数组来控制这些操作的发生,或者在组件内部创建逻辑分支来处理不同的情况,但要确保所有的 Hooks 调用都在组件的顶层。
memorizedState
在React中,memorizedState
这个术语通常与React的Hooks机制相关联,尤其是useState
和useReducer
等Hooks。当使用这些Hooks时,React内部会维护一个叫做“memorized state”的值,这是Hook当前状态的最新版本。
memorizedState
并不是直接暴露给开发者的一个属性或API,而是React为了实现状态管理和更新逻辑的一部分。当组件重新渲染时,React会检查新的props或state是否与前一次渲染的不同。如果memorizedState
发生变化,React将执行必要的更新逻辑,否则可能直接使用之前计算的输出,以优化性能。
对于React.memo
来说,它是一个高阶组件(HOC),用于包装函数组件,提供类似于shouldComponentUpdate
生命周期方法的功能,即它会比较传递给组件的新旧props,如果它们是浅相等的(即引用相等或值相等,取决于props的数据类型),那么React.memo
会告诉React不要重新渲染组件,而是使用上一次渲染的结果。
需要注意的是,React.memo
只比较props,并不会比较组件内部的状态(state)。如果需要根据组件内部状态的变化来决定是否重新渲染,将需要使用React的Hooks,如useState
或useReducer
,以及可能的useCallback
或useMemo
来确保某些函数或对象的引用保持不变,从而避免不必要的重新渲染。
setState是同步还是异步
React
中的 setState
方法的行为在不同情况下有所不同,它通常被视为异步的,但这种异步行为不是传统意义上的异步(如使用 Promise
或 async/await
),而是基于 React 的批处理更新机制。
- 在大多数情况下:当在组件的生命周期方法(如
componentDidMount
,componentDidUpdate
)或 React 合成事件处理器中调用setState
时,React 会将状态更新放入一个队列中,并在当前工作循环结束或在下一次重绘之前的一段时间内进行批处理。这意味着状态更新可能不会立即反映在组件的状态上,而是等待 React 完成当前的工作或等到下一次渲染周期。 - 特殊情况:当在
setTimeout
或原生 DOM 事件(非 React 合成事件)中调用setState
时,React 的批处理机制可能不会生效,导致setState
表现得更像同步行为。这是因为这些环境下的调用通常不在 React 的控制流中,所以 React 可能会立即执行状态更新。 - 如何确保访问最新状态:如果需要确保在
setState
后访问到最新的状态,你可以传递一个回调函数作为setState
的第二个参数。这个回调函数将在状态更新并且组件完成重新渲染后被调用。1
2
3
4this.setState({ someState: newValue }, () => {
// 这里可以安全地访问最新的状态
console.log(this.state.someState);
});
Vue✅
npm✅
npm包有哪些license,哪些允许商用
- MIT License
- Apache License 2.0
- BSD License (2-clause or 3-clause)
- GPL (General Public License)
- LGPL (Lesser General Public License)
- MPL (Mozilla Public License)
- ISC License
- Unlicense
- Creative Commons (CC0)
其中,允许商用的许可证包括:
- MIT License: 允许几乎任何用途,包括商用。
- Apache License 2.0: 允许商用,要求附带版权声明和免责条款。
- BSD License: 允许商用,要求附带版权声明和免责条款。
- ISC License: 类似于BSD许可证,允许商用。
- MPL (Mozilla Public License): 允许商用,但对分发源代码有一定的要求。
- Unlicense: 放弃版权,允许任何用途,包括商用。
- Creative Commons (CC0): 类似于公有领域,允许任何用途,包括商用。
需要注意的是,GPL和LGPL虽然也是开源许可证,但对商业用途有严格的规定。GPL要求衍生作品也必须遵循GPL许可证(即所谓的“传染性”),这对于商业软件可能不太适用。LGPL则允许在商业软件中使用,但如果修改了LGPL库,修改后的部分仍需要开源。
CSS✅
HTML✅
script标签如何加载js文件
在HTML中,可以通过<script>
标签来加载JavaScript文件。
- 内联脚本:直接在
<script>
标签中编写JavaScript代码。1
2
3<script>
alert("Hello, World!");
</script> - 外部脚本:通过
src
属性指定要加载的JavaScript文件的URL。1
<script src="path/to/my/script.js"></script>
- 异步加载:
<script>
标签的async
属性用于指定脚本的异步加载。当指定了async
属性时,脚本将会在加载时不阻塞 HTML 解析,并在加载完成后立即执行。多个异步脚本的执行顺序是不确定的。1
<script src="path/to/my/script.js" async></script>
- 延迟加载:
<script>
标签的defer
属性用于指定脚本的延迟加载。当指定了defer
属性时,脚本将会在 HTML 解析完成后再执行,但在DOMContentLoaded
事件之前执行。多个延迟脚本的执行顺序是按照它们在文档中出现的顺序执行的。1
<script src="path/to/my/script.js" defer></script>
- 动态加载脚本:通过 JavaScript 动态创建
<script>
标签,然后将其插入到文档中。动态加载脚本可以在任何时候进行,例如在页面加载后、用户操作后或其他事件触发时。这种方式可以控制脚本的加载时机。1
2
3var script = document.createElement('script');
script.src = 'script.js';
document.body.appendChild(script);注意,JavaScript 脚本默认是同步加载的,即阻塞 HTML 解析并立即执行。async、defer、动态加载脚本方法可以调整脚本的加载方式,使其异步或延迟加载,提高页面加载性能或控制脚本执行时机。
Flutter✅
Flutter原理
Flutter 是一个由 Google 开发的开源框架,用于构建高性能、高保真度的跨平台移动应用。其核心原理如下:
- Dart 编程语言:
- Flutter 使用 Dart 语言作为开发应用的主要语言。Dart 是一种现代化的、面向对象的语言,具有即时编译特性,可以直接编译为本地代码。
- Skia 渲染引擎:
- Flutter 使用 Skia 作为其渲染引擎,Skia 是一个跨平台的2D图形库,由 C++ 编写,能够提供高性能的绘图能力。
- Widget 树:
- Flutter 的核心概念是 Widget。在 Flutter 中,一切都是 Widget,包括布局、样式、动画等。Flutter 使用基于组合的方式构建用户界面,每个 Widget 都是不可变的。
- Widget 树描述了应用界面的结构,从根部的 Widget 开始,逐级构建出整个应用的 UI。
- Hot Reload:
- Flutter 提供了热重载(Hot Reload)功能,这是一个非常强大的开发工具,可以在保持应用状态的同时快速更新代码和界面,大大加快开发迭代速度。
- 自绘:
- Flutter 不依赖平台的原生控件,而是通过 Skia 直接绘制界面。这种方式使得 Flutter 的 UI 高度定制化,并且能够实现完全一致的跨平台体验。
- 平台通道:
- Flutter 提供了平台通道(Platform Channels),用于在 Dart 代码和原生代码之间进行通信。通过平台通道,Flutter 可以调用平台特定的 API 和功能。
- 性能优化:
- Flutter 通过使用 Skia 渲染引擎和自带的动画库来实现流畅的动画效果和高性能的绘制。它还利用 Dart 的优化能力,如快速的 JIT(即时编译)和 AOT(预编译)技术来提升运行时性能。
通过这些原理和机制,Flutter 实现了高效、美观且跨平台的移动应用开发,为开发者提供了灵活且强大的工具集来构建现代应用。
- Flutter 通过使用 Skia 渲染引擎和自带的动画库来实现流畅的动画效果和高性能的绘制。它还利用 Dart 的优化能力,如快速的 JIT(即时编译)和 AOT(预编译)技术来提升运行时性能。
React-Native✅
ReactNative原理
React Native是一种用于构建跨平台移动应用的框架,其核心原理包括以下几个方面:
- JavaScriptCore:
- React Native使用JavaScriptCore引擎来运行JavaScript代码。对于iOS,React Native使用系统自带的JavaScriptCore;对于Android,它会嵌入一个独立的JavaScriptCore实例。
- Bridge:
- React Native的核心是一个称为“Bridge”的机制。这个桥接机制在JavaScript和原生代码之间建立了通信渠道。JavaScript线程和原生线程通过JSON消息进行通信。
- 当JavaScript代码需要调用原生模块时,它会通过Bridge发送消息到原生线程。原生代码执行相应的操作后,可能会将结果返回给JavaScript线程。
- Shadow Tree和布局引擎:
- React Native使用一个虚拟的Shadow Tree来描述UI结构。这个Shadow Tree类似于React中的虚拟DOM,但它并不直接渲染UI,而是用于计算布局。
- 布局计算完成后,React Native会将布局信息传递给原生线程,由原生视图系统来实际渲染UI。
- UI组件:
- React Native提供了一系列跨平台的UI组件,如
View
,Text
,Image
等。这些组件在JavaScript中定义,但会映射到原生的视图组件。 - 通过这种方式,React Native可以利用原生平台的高性能和丰富功能,同时保留React的声明式编程风格。
- React Native提供了一系列跨平台的UI组件,如
- 原生模块和第三方库:
- 开发者可以编写自己的原生模块并通过Bridge与JavaScript代码进行交互。这样可以扩展React Native的功能,使用平台特定的API。
- 还有很多第三方库提供了常用功能,如相机、地理位置、推送通知等,它们也通过类似的方式与React Native集成。
- 性能优化:
- React Native通过批量更新和异步渲染来提升性能。例如,批量将多次状态更新合并为一次操作,减少与原生线程的通信次数。
- 使用虚拟DOM和高效的diff算法来最小化UI更新的开销。
通过这些核心机制,React Native能够实现一次编码,跨平台运行的目标,同时保留接近原生应用的性能和体验。
ReactNative和Flutter的区别
https://blog.csdn.net/BTTBHT/article/details/131046656
Flutter和ReactNative是目前最流行的跨平台移动应用开发框架。它们分别由谷歌和Facebook开发。
- 性能:Flutter在性能方面具有明显的优势,可以提供更接近原生的用户体验。
- Flutter使用Dart语言编写,可以直接编译成本地代码,避免性能损耗和内存泄漏的风险,提高应用的稳定性和流畅度。Flutter还使用了自己的渲染引擎Skia,可以直接绘制像素到屏幕上,实现高质量的UI效果。
- ReactNative使用JavaScript语言编写,需要通过JavaScript桥接层与本地代码进行通信。这样会增加性能开销和延迟,降低应用的响应速度和运行效率。ReactNative使用了本地的渲染引擎,可以利用本地的UI组件,但也会受到本地平台的限制和差异。
- 代码书写:
- Flutter使用Dart语言编写,这是一种静态类型、面向对象、支持多范式的语言。Dart语言相对于JavaScript来说更加严格和规范,可以避免一些常见的错误和异常,提高代码的可读性和可维护性。Dart语言还支持热重载和热重启功能,可以实时查看代码修改后的效果,提高开发效率。
- ReactNative使用JavaScript语言编写,这是一种动态类型、基于原型、支持多范式的语言。JavaScript语言相对于Dart来说更加灵活和简洁,可以实现更多的功能和逻辑,提高代码的表达力和创造力。JavaScript语言也支持热重载和热重启功能,可以实时查看代码修改后的效果,提高开发效率。
- 学习难度:ReactNative在学习方面具有一定的优势,可以降低开发者的入门难度
- Flutter使用Dart语言编写,这是一种相对较新的语言,目前还没有太多的使用者和教程。开发者需要花费一定的时间和精力来学习Dart语言的语法和特性,以及Flutter框架的原理和组件。
- ReactNative使用JavaScript语言编写,这是一种相对较老的语言,目前有着广泛的使用者和教程。开发者如果已经熟悉JavaScript语言和React框架,可以很快地上手ReactNative框架。
- 运行速度:Flutter在运行速度方面具有明显的优势,可以提供更快速和流畅的用户体验。
- Flutter使用Dart语言编写,可以直接编译成本地代码,无需通过JavaScript桥接层。这样可以提高应用的启动速度和运行速度
- ReactNative使用JavaScript语言编写,需要通过JavaScript桥接层与本地代码进行通信。这样会降低应用的启动速度和运行速度,增加卡顿和闪退的可能性。
移动端✅
对移动端开发的认识?
移动端开发是指开发用于移动设备(如智能手机、平板电脑)的应用程序或网站。随着移动设备的普及和使用量的增加,移动端开发已成为软件开发领域中的重要方向之一。以下是关于移动端开发的一些认识:
- 平台多样性:
- 移动设备的操作系统多样性是移动端开发的一个显著特点。主要的移动操作系统包括 Android、iOS 和可能还有 Windows Phone(已停止开发支持)。因此,移动端开发需要考虑多种操作系统和平台之间的兼容性。
- 开发工具:
- 为了开发移动应用程序,开发人员可以使用各种工具和技术栈。比较常见的移动端开发工具包括 Android Studio(用于 Android 开发)、Xcode(用于 iOS 开发)、React Native、Flutter、Ionic 等跨平台框架,以及 Web 技术(如 HTML、CSS、JavaScript)开发的移动网页应用。
- 用户体验:
- 移动端开发需要特别关注用户体验(UX),因为移动设备的屏幕较小,用户交互方式也有所不同。因此,设计和开发移动应用需要考虑到触摸操作、屏幕尺寸适配、响应式设计等方面,以提供良好的用户体验。
- 性能优化:
- 移动端应用的性能优化是非常重要的,因为移动设备的资源相对有限。开发人员需要注意减少应用的内存占用、优化加载速度、减少功耗等方面,以确保应用在移动设备上的流畅运行。
- 发布和更新:
- 发布移动应用需要遵循各个应用商店的规定和流程。对于 Android 应用,通常使用 Google Play Store 进行发布;而 iOS 应用则需要通过苹果的 App Store 发布。此外,定期更新应用以修复 bug、增加新功能也是移动端开发的常规工作之一。
总的来说,移动端开发涉及多种技术和方面,包括平台选择、开发工具、用户体验、性能优化、发布和更新等。随着移动技术的不断发展和创新,移动端开发也在不断演进和壮大,为用户提供更便捷、高效、丰富的移动应用体验。
- 发布移动应用需要遵循各个应用商店的规定和流程。对于 Android 应用,通常使用 Google Play Store 进行发布;而 iOS 应用则需要通过苹果的 App Store 发布。此外,定期更新应用以修复 bug、增加新功能也是移动端开发的常规工作之一。
场景题✅
有一张1GB的图片需要加载到手机屏幕上,但是手机内存不够怎么办?
- 懒加载(lazy loading):在内存不足的情况下,可以分步加载图片以避免一次性占用大量内存。懒加载的基本思路是按需加载图片的部分数据,直到整个图片被完全加载。
- 实现:
- 分块加载:将图片分割成多个小块,每次只加载一部分数据,这样可以有效降低每次加载时的内存占用。
- 使用缩略图:在用户需要查看图片之前,先加载较小的缩略图,等用户需要查看详细内容时,再加载高清图像。
- 图片压缩:在加载之前,将图片压缩成较小的尺寸和质量,然后在需要时加载完整的图片。
- 文件流处理:使用文件流处理方式,一边读取一边显示,不需要一次性加载整个图片。
- 图片压缩:通过压缩图片的分辨率、质量等方式来减小图片的体积,从而减少内存占用。
- 缓存管理:实现一个智能缓存系统,只缓存当前屏幕上需要显示的图片数据,释放掉那些暂时不需要显示的图片数据。这样可以有效管理内存占用。
- 调整图片分辨率:加载合适分辨率的图片,既能保证显示效果,又能减少内存使用。
- 使用外部存储:将大图片存储在外部存储设备(如SD卡)中,而不是手机的内存中。通过读取外部存储中的图片数据来显示。
- 内存优化:对应用程序进行内存优化,释放不必要的内存占用,以便为大图片的加载腾出空间。这包括清理缓存、减少后台运行的进程等。