普通视图

发现新文章,点击刷新页面。
今天 — 2025年2月21日首页

页面渲染规则之BFC

作者 不是鱼
2025年2月21日 00:35

前言

我们的css中有许多页面渲染规则,它是浏览器用来解释和显示网页样式的一系列原则,决定了元素如何在页面上呈现。比如说:选择器优先级层叠机制继承盒模型流布局视觉格式化模型媒体查询css动画和转换性能优化等等,今天我们重点讲讲属于视觉格式化模型中的BFC,是面试中高频的考查部分。

盒模型

我们先来唠唠盒模型。在css中我们有两种盒模型,分别是:标准盒模型和IE盒模型,我们在实际开发中可以选择适合的盒模型进行使用,在现代浏览器中,默认使用的盒模型是标准盒模型。我们来看看它们两者的区别:

    <style>
        *{
            margin: 0;
            padding: 0;
        }
        /* 盒模型 + 块级  */
        .box,.box2,.box3{
            width: 100px;
            height: 100px;
            padding: 10px;
            border: 1px solid red;;
            background-color: green;
        }
        .box2{
            /* border 以内盒子的大小(包括border)IE 怪异盒模型*/
            box-sizing: border-box;
        }
        .box3{
            /* 默认值,标准盒模型 wh 设置的就是 content 的大小*/
            box-sizing: content-box;
        }
    </style>
</head>
<body>
    <div class="box"></div>
    <div class="box2"></div>
    <div class="box3"></div>
</body>

在这里我们加入了三个盒子,其中box1box3是标准盒模型,box2是IE盒模型,我们给它们都设置了width:100px;height:100px;来看看它们在页面中的效果:

image.png

为什么大小会不一样呢,打开浏览器的开发者工具检查一下,点击我们的box1box3,可以看到:

image.png

我们设置的宽高只是内容的宽高,也就是content,并不包括padding,border,那box2呢:

image.png

我们可以发现它的content + padding +border加起来才是我们设置的100px。

接下来我们来看另一个例子:

<style>
        *{
            margin: 0;
            padding: 0;
        }
        div{
            width: 100px;
            height: 100px;
            border: 1px solid red;
            padding: 10px 20px;
        }
        span{
            width: 100px;
            height: 100px;
            background-color: green;
        }
    </style>
</head>
<body>
    <div>123</div>
    <span>11</span><span>222</span>
</body>

image.png

页面呈现的是这个效果,诶为什么我们给span设置了宽高它还只是这么一点呢,这是因为行内元素不能被设置宽高,它们是根据文本流来排列的,只会占据必要的空间。 如果想要改变,我们可以改变它的display,行内元素默认值为inline,我们将它设置为display:block

image.png

就作为块级元素显示了,并且可以设置宽高,同样我们可以将div也作为行内元素显示:display:inline

image.png

还有display:nline-block它可以让元素像行内元素一样显示,并且可以设置高度:

image.png

BFC

BFC(Block Formatting Context,块级格式化上下文),是web页面中盒模型布局的一种CSS渲染模式,它是一个独立的渲染区域,在内部的html元素按照一定的规矩进行布局,内部的元素不会受到外部元素的影响。接下来我们慢慢学习它的特性:

  1. 垂直排列:内部的Box(块级元素)会在垂直方向上一个接一个地放置,即每个块级元素独占一行。
  2. 外边距折叠:属于同一个BFC的两个相邻Box的margin会发生叠加,这种现象称为“margin塌陷”。
style>
        * {
            margin: 0;
            padding: 0;
        }
        .box {
            width: 200px;
            height: 200px;
            background-color: lightblue;
            border: 1px solid black;
        }
        .box1{
            margin-bottom: 50px;
        }
        .box2{
            margin-top: 30px;
        }
    </style>
</head>
<body>
    <div class="box box1">Box1</div>
     <div class="box box2">Box2</div>
</body>

image.png

可以看到我们为box1设置了margin:50px,为box2设置了margin:30px,按道理来说它们的间距应该是80px,但是并没有,就如同规则所说一样,margin会进行重叠,最终的间距值取决于margin大的那一方。我们可以通过为box2增添一个父容器并设置其属性为overflow:hidden来创建一个新的BFC。这样就可以解决高度塌陷的问题。

    <div style="overflow: auto;">
        <div class="box box2">Box2</div>
    </div>

image.png

  1. 不与浮动元素重叠:BFC的区域不会与float box重叠。这意味着如果一个元素形成了一个新的BFC,那么它将不会被浮动元素覆盖。
  <style>
  .container {
    border: 2px solid black;
    padding: 10px;
  }

  .float-box {
      float: left;
      width: 150px;
      height: 100px;
      background-color: lightblue;
      margin: 10px;
  }

  .bfc-box {
      width: 300px;
      height: 100px;
      background-color: lightgreen;
      /* overflow: auto; 创建 BFC */
  }
  </style>
</head>
<body>
  <div class="container">
    <div class="float-box">这是一个左浮动的盒子</div>
    <div class="bfc-box">这是一个创建了 BFC 的盒子</div>
</div>
</body>

image.png

当我们为container增加一个overflow:auto创建BFC后:

image.png

  1. 包含浮动:计算BFC的高度时,浮动元素也参与计算。这可以用来解决父元素因为浮动子元素而高度坍塌的问题。
  <style>
  /* 而用 BFC 清除浮动的原理就是:计算 BFC 的高度时,浮动元素也参与计算。只要触发父元素的 BFC 即可。 */
  .parent {
    background-color: red;
    /* overflow: hidden; */
  }
  .child {
    float: left;
    height: 200px;
    width: 200px;
    background-color: green;
  }
  </style>
</head>
<body>
  <div class="parent">
    <div class="child"></div>
  </div>
</body>

image.png

可以看到此时,parent元素高度为0,当我们为他加上overflow:hidden创建一个BFC后:

image.png 5. 隔离性:BFC就是页面上的一个独立容器,容器里面的子元素不会影响外面的元素,也不会受到外部元素的影响。

<style>
  .container {
    border: 2px solid black;
    padding: 10px;
}

.float-box {
    float: left;
    width: 150px;
    height: 100px;
    background-color: lightblue;
    margin: 10px;
}

.bfc-box {
    width: 300px;
    height: auto;
    background-color: lightgreen;
    /* overflow: auto; 创建 BFC  切换 */
    padding: 10px;
    border: 1px solid #ccc;
}
  </style>
</head>
<body>
  <div class="container">
    <div class="float-box">这是一个左浮动的盒子</div>
    <div class="bfc-box">
        <p>这是创建了 BFC 的盒子内部的内容。</p>
        <p>注意,这个内容不会被外部的浮动元素影响。</p>
    </div>
  </div>

image.png

在我们未创建BFC时,bfc-box中的元素受到浮动元素的影响导致文字环绕着浮动元素,但是为它创建一个BFC后:

image.png 7. 左对齐:每个元素的margin box的左边,与包含块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此。

    <style>
        *{
            margin: 0;
            padding: 0;
        }
        .container{
            border: 2px solid black;
            padding: 10px;
            /* 父元素的不影响子元素的布局 */
            /* overflow: hidden; */
        }
        .float-left{
            /* 启动了一个新的FFC */
            float: left; 
            width: 150px;
            height: 100px;
            background-color: lightblue;
            margin: 10px;
        }
       .margin-box{
            width: 200px;
            height: 100px;
            background-color: lightgreen;
            margin: 20px 0 20px 50px;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="float-left">这是一个左浮动的元素</div>
        <div class="margin-box">这是有左侧外边距的盒子这是有左侧外边距的盒子这是有左侧外边距的盒子</div>
    </div>

image.png

形成BFC的条件

1. 根元素(HTML)

  • 条件:HTML 文档的根元素(<html>)总是会形成一个新的 BFC。
  • 解释:根元素是页面的最外层容器,它自然会创建一个独立的渲染区域。

2. 浮动元素(float 不为 none

  • 条件:当一个元素的 float 属性设置为 left 或 right 时,它会形成一个新的 BFC。
  • 解释:浮动元素从普通流中部分脱离出来,为了确保浮动元素不会影响其他非浮动元素的布局,浏览器会为浮动元素创建一个新的 BFC。

3. 绝对定位或固定定位元素(position 为 absolute 或 fixed

  • 条件:当一个元素的 position 属性设置为 absolute 或 fixed 时,它会形成一个新的 BFC。
  • 解释:绝对定位和固定定位的元素从普通流中完全脱离出来,它们不再受父元素的影响,因此需要创建一个新的 BFC 来管理它们的布局。

4. display 为某些特定值

  • 条件:当一个元素的 display 属性设置为以下值时,它会形成一个新的 BFC:

    • inline-block
    • table-cell
    • table-caption
    • flex
    • inline-flex
    • grid
    • inline-grid
    • flow-root(CSS 新增的值,专门用于创建 BFC)
  • 解释:这些 display 值会改变元素的默认布局行为,创建一个新的 BFC 来管理其内部内容的布局。

5. overflow 不为 visible

  • 条件:当一个元素的 overflow 属性设置为 hiddenauto 或 scroll 时,它会形成一个新的 BFC。
  • 解释:通过设置 overflow,浏览器会创建一个新的 BFC 来确保容器能够正确包裹其内部的浮动元素或溢出的内容,防止高度坍塌等问题。

6. contain 属性

  • 条件:当一个元素的 contain 属性设置为 layoutcontent 或 paint 时,它会形成一个新的 BFC。
  • 解释contain 属性用于优化布局、样式和绘制,帮助浏览器更高效地处理复杂的页面结构。contain: layout 会创建一个新的 BFC,确保该元素的布局不会受到外部元素的影响。

7. 多列布局(column-count 或 column-width

  • 条件:当一个元素使用了多列布局(column-count 或 column-width)时,它会形成一个新的 BFC。
  • 解释:多列布局会将内容分成多个列,为了确保每一列的内容不会相互干扰,浏览器会为多列容器创建一个新的 BFC。

8. writing-mode 为 vertical-rl

  • 条件:当一个元素的 writing-mode 属性设置为 vertical-rl 时,它会形成一个新的 BFC。
  • 解释writing-mode 用于控制文本的书写方向,vertical-rl 表示从右到左的垂直书写模式。为了适应这种特殊的书写方式,浏览器会为该元素创建一个新的 BFC。

9. will-change 属性

  • 条件:当一个元素的 will-change 属性指定了可能会发生变化的属性时,浏览器可能会为该元素创建一个新的 BFC,以优化性能。
  • 解释will-change 提示浏览器提前为某些属性的变化做好准备,虽然它不一定总是创建 BFC,但在某些情况下确实会触发 BFC 的创建。

10. transform 或 filter 属性

  • 条件:当一个元素应用了 transform 或 filter 属性时,浏览器可能会为该元素创建一个新的 BFC,以确保其渲染效果不会影响其他元素。
  • 解释transform 和 filter 会创建一个新的堆叠上下文(stacking context),并且在某些情况下也会触发 BFC 的创建。

总结

形成 BFC 的条件多种多样,主要包括浮动、绝对定位、特定的 display 值、overflow 设置等。理解这些条件可以帮助开发者更好地控制页面布局,避免常见的布局问题,如浮动元素导致的高度坍塌、外边距折叠等。通过合理使用 BFC,可以创建更加稳定和可控的布局结构。

常见应用场景

  1. 清除浮动:通过设置 overflow: hidden;display: flow-root;,可以创建一个新的 BFC,从而确保父容器能够正确包裹浮动的子元素,防止高度坍塌。
  2. 防止外边距折叠:通过创建 BFC,可以阻止相邻块级元素之间的垂直外边距发生折叠,确保布局的稳定性。
  3. 隔离浮动的影响:通过创建 BFC,可以确保某个容器内的浮动元素不会影响外部的非浮动元素,也不会被外部的浮动元素影响。
  4. 创建独立的布局区域:通过创建 BFC,可以将某些部分的布局与其他部分隔离开来,使得布局更加模块化和易于管理。

浏览器内存管理与优化实战指南:从垃圾回收到内存泄漏排查

作者 鱼樱前端
2025年2月21日 00:26

元宵节到最近更新的频次比较少是因为在准备内容投放掘金和公众号,现在步入正轨里面来了!!!js基础到进阶的内容划分出来一大部分了,正所谓基础不牢地动山摇~希望以下内容对你们有收获!!!欢迎持续收藏关注对标知识点,**本人掘金和公众号(鱼樱AI实验室)**会持续更新有关前端的所有知识链条。

 
# 浏览器内存管理与优化实战指南:从垃圾回收到内存泄漏排查

作为前端开发者,你是否遇到过页面卡顿、内存暴涨甚至崩溃的问题?本文结合 **V8引擎原理****Chrome DevTools实战技巧**,深入剖析浏览器内存管理机制,助你写出高性能、零泄漏的优质代码!

---

一、垃圾回收算法深度解析

1. 新生代内存回收:Scavenge算法

核心原理:将新生代堆内存划分为两个等大的From空间To空间,通过复制存活对象实现垃圾回收。

运作流程

  1. 新创建的对象存入From空间
  2. 当From空间占满时触发GC
  3. 将存活对象复制到To空间
  4. 清空From空间并交换两者角色

优化特性

  • 对象晋升:经历两次GC仍存活的对象会被移到老生代
  • 空间换时间:牺牲50%内存空间换取O(存活对象)的时间复杂度
 
// 示例:触发新生代GC
function createLargeObjects() {
  let temp = [];
  for(let i=0; i<100000; i++){
    temp.push(new Array(100)); // 快速填充新生代空间
  }
}
createLargeObjects();

2. 老生代内存回收:标记-清除 vs 标记-整理

算法 标记-清除 标记-整理
内存分布 产生内存碎片 内存连续
执行速度 较快 较慢(需移动对象)
适用场景 常规回收 内存碎片率过高时

复合策略

  1. 优先使用标记-清除进行常规回收
  2. 当内存碎片超过阈值时,执行标记-整理
  3. 采用增量标记(Incremental Marking)避免长时间阻塞主线程

3. 增量标记与并发标记

增量标记

  • 将标记过程分解为多个小任务
  • 穿插在主线程任务之间执行
  • 典型应用:V8引擎的三色标记法

并发标记

  • 由后台线程执行标记操作
  • 完全不阻塞主线程
  • Chrome 64+ 默认启用并发标记

二、内存泄漏检测实战技巧

1. 堆快照支配树分析

操作步骤

  1. 打开Chrome DevTools → Memory → Heap snapshot
  2. 拍摄页面初始状态的堆快照(Snapshot 1)
  3. 执行可疑操作后拍摄第二个快照(Snapshot 2)
  4. 对比快照,筛选All objectsObjects allocated between Snapshot 1 and Snapshot 2

关键指标

  • Retained Size:对象及其依赖对象的总内存
  • Shallow Size:对象自身占用的内存

image.png

2. 保留路径追踪方法

典型泄漏场景分析

 
// 常见闭包泄漏示例
function createLeak() {
  const hugeData = new Array(1000000);
  return function() {
    console.log('Leaked data:', hugeData[0]);
  };
}
const leakedFn = createLeak();

支配树特征

  • 闭包中未使用的变量仍被保留
  • Detached DOM树仍被JavaScript引用

3. 弱引用使用场景

WeakMap/WeakSet特性

  • 不阻止垃圾回收
  • 键必须是对象引用
  • 不可遍历

适用场景

 
// 使用WeakMap实现私有属性
const privateData = new WeakMap();

class User {
  constructor(name) {
    privateData.set(this, { name });
  }
  
  getName() {
    return privateData.get(this).name;
  }
}

// 当User实例被回收时,关联数据自动清除

不适用场景

  • 需要遍历键值对的场景
  • 缓存需要强引用的重要数据

三、内存优化最佳实践

  1. 及时解绑事件监听
 
// 错误示例
element.addEventListener('click', onClick);

// 正确做法
function addListener() {
  element.addEventListener('click', onClick);
  return () => element.removeEventListener('click', onClick);
}
const removeListener = addListener();
// 不再需要时执行 removeListener()
  1. 避免不可控的全局缓存
 
// 危险操作
window.cache = { bigData: /*...*/ };

// 安全替代方案
const cache = new WeakMap();
function storeData(obj) {
  cache.set(obj, { timestamp: Date.now() });
}
  1. 合理使用requestIdleCallback
 
function processBigData() {
  const taskQueue = [];
  
  function doWork(deadline) {
    while (deadline.timeRemaining() > 0 && taskQueue.length > 0) {
      process(taskQueue.shift());
    }
    if (taskQueue.length > 0) {
      requestIdleCallback(doWork);
    }
  }
  
  requestIdleCallback(doWork);
}

四、高级内存分析工具

  1. Performance Monitor(实时内存监控)

    • 监控JS堆大小
    • 追踪DOM节点数量
    • 观察事件监听器数量
  2. Allocation instrumentation on timeline

    • 按时间线记录内存分配
    • 定位内存分配热点
  3. 内存压力测试

     
    // 手动触发GC(仅DevTools打开时有效)
    window.gc();
    
    // 强制进行完整GC
    performance.memory.measureUserAgentSpecificMemory();
    

总结:内存优化四原则

  1. 及时释放:不再使用的引用立即置null
  2. 大小控制:避免超大对象长期驻留内存
  3. 分层缓存:采用强引用+弱引用混合策略
  4. 定期检测:使用DevTools进行压力测试

转发本文到技术群,与更多开发者探讨内存优化之道! 🚀


扩展阅读

每日一题-用地毯覆盖后的最少白色砖块🔴

2025年2月21日 00:00

给你一个下标从 0 开始的 二进制 字符串 floor ,它表示地板上砖块的颜色。

  • floor[i] = '0' 表示地板上第 i 块砖块的颜色是 黑色 。
  • floor[i] = '1' 表示地板上第 i 块砖块的颜色是 白色 。

同时给你 numCarpets 和 carpetLen 。你有 numCarpets 条 黑色 的地毯,每一条 黑色 的地毯长度都为 carpetLen 块砖块。请你使用这些地毯去覆盖砖块,使得未被覆盖的剩余 白色 砖块的数目 最小 。地毯相互之间可以覆盖。

请你返回没被覆盖的白色砖块的 最少 数目。

 

示例 1:

输入:floor = "10110101", numCarpets = 2, carpetLen = 2
输出:2
解释:
上图展示了剩余 2 块白色砖块的方案。
没有其他方案可以使未被覆盖的白色砖块少于 2 块。

示例 2:

输入:floor = "11111", numCarpets = 2, carpetLen = 3
输出:0
解释:
上图展示了所有白色砖块都被覆盖的一种方案。
注意,地毯相互之间可以覆盖。

 

提示:

  • 1 <= carpetLen <= floor.length <= 1000
  • floor[i] 要么是 '0' ,要么是 '1' 。
  • 1 <= numCarpets <= 1000

iOS开发基础-通过C++快速掌握Objective-C语言(进阶)

2025年2月20日 18:02

本文通过与C++语言对比,介绍Objective-C语言进阶特性。适合具备C++语言基础的程序员快速掌握Object-C语言。

一、内存管理机制

对象生命周期

Objective-C ARC

// 强引用:对象存活期间始终持有
NSObject *obj = [[NSObject alloc] init]; 

// 弱引用:不会阻止对象释放(对象释放后自动置nil)
__weak NSObject *weakRef = obj;

// 自动释放池:延迟释放临时对象
@autoreleasepool {
    NSString *temp = [NSString stringWithFormat:@"%d", 42];
    // temp会在池释放时收到release消息
}
  • 核心原理:编译器自动在合适位置插入retain/release调用
  • 循环引用解法:使用__weak打破相互强引用(类似C++的weak_ptr)

C++智能指针

// 共享所有权(引用计数)
std::shared_ptr<MyClass> p1 = std::make_shared<MyClass>();

// 观察而不持有
std::weak_ptr<MyClass> p2 = p1; 
  • RAII机制:对象析构时自动释放资源
  • 独有所有权unique_ptr禁止复制,只能移动所有权

二、核心特性对比

动态消息转发

触发场景:当对象收到未实现的方法调用时

[obj undefinedMethod]; // 该方法未在类中声明或实现

Objective-C提供三级处理机制(对比C++直接编译报错),避免程序崩溃。


三级处理流程

  1. 动态方法解析
    程序员介入方式
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(undefinedMethod)) {
        // 动态添加C函数作为实现
        class_addMethod(self, sel, (IMP)dynamicIMP, "v@:"); 
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

底层原理
Runtime系统首次发现方法未实现时,调用此方法注入新实现(类似C++的运行时函数注册)。


  1. 快速消息转发
    程序员介入方式
- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_delegate respondsToSelector:aSelector]) {
        return _delegate; // 转交其他对象处理
    }
    return [super forwardingTargetForSelector:aSelector];
}

应用场景

  • 实现简单代理模式
  • 模拟多重继承(类似C++中通过组合对象实现多继承效果)

  1. 完整消息转发
    程序员介入方式
// 步骤1:生成方法签名  
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}

// 步骤2:处理完整调用  
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    if ([_backupObj respondsToSelector:anInvocation.selector]) {
        [anInvocation invokeWithTarget:_backupObj]; // 完整转发
    } else {
        [super forwardInvocation:anInvocation]; // 触发崩溃
    }
}

能力扩展

  • 修改方法参数/返回值
  • 记录调用日志(类似C++的AOP切面编程)
  • 实现方法调用重试机制

对比C++异常捕获

特性 Objective-C C++
动态性支持 三级消息转发机制 虚函数表静态绑定
错误处理 运行时动态修正 编译期类型检查
典型应用 热修复/方法交换 模板元编程/CRTP

实际应用场景示例

崩溃防护

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSLog(@"拦截未实现方法:%@", NSStringFromSelector(invocation.selector));
    // 可在此处上报异常,不触发崩溃
}

动态代理实现

- (id)forwardingTargetForSelector:(SEL)aSelector {
    if ([_networkDelegate respondsToSelector:aSelector]) {
        return _networkDelegate; // 将网络方法转交专门对象
    }
    return [super forwardingTargetForSelector:aSelector];
}

错误处理

Objective-C NSError模式

NSError *error;
BOOL success = [data writeToFile:path options:0 error:&error];

if (!success) {
    NSLog(@"操作失败:%@", error.localizedDescription);
    // 处理错误,而不是崩溃
}
  • 特点:通过指针传递错误对象,与返回值配合使用

C++异常处理

try {
    FileWriter writer("data.bin");
    writer.write(data);
} catch (const FileException& e) {
    std::cerr << "文件错误:" << e.what() << std::endl;
}
  • 特点:通过抛出异常中断正常流程

三、扩展机制

Objective-C分类(Category)

  • 作用:为现有类添加新方法,无需子类化;
  • 限制:不能添加实例变量(可通过关联对象解决) 语法结构
// 声明(ClassName+Feature.h)
@interface NSString (URLEncoding)  // (URLEncoding)为分类名称
- (NSString *)urlEncodedString;    // 新增方法声明
@end

// 实现(ClassName+Feature.m)
@implementation NSString (URLEncoding)
- (NSString *)urlEncodedString {
    return [self stringByAddingPercentEncodingWithAllowedCharacters:
            [NSCharacterSet URLQueryAllowedCharacterSet]];
}
@end

// 使用
NSString *query = @"name=张三";
NSString *encoded = [query urlEncodedString];
  • @interface NSString (URLEncoding):这表示你将为 NSString 类创建一个名为 URLEncoding 的分类。
  • 在分类中,你声明了一个新的方法 - (NSString *)urlEncodedString,这个方法用于对 NSString 对象进行 URL 编码。

C++近似实现

namespace StringExtensions {
    std::string urlEncode(const std::string &s) {
        // 实现编码逻辑
        return encodedStr;
    }
}

// 使用
auto encoded = StringExtensions::urlEncode("name=张三");

四、特殊语法结构

Block语法

基本用法

// 声明:返回值(^块名称)(参数类型)
void (^simpleBlock)(void) = ^{
    NSLog(@"这是一个无参Block");
};

// 调用
simpleBlock();

// 带参数的Block
int (^addBlock)(int, int) = ^(int a, int b) {
    return a + b;
};
NSLog(@"结果:%d", addBlock(3, 5));

// 避免循环引用
__weak typeof(self) weakSelf = self;
self.completionHandler = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
};

对比C++ Lambda

auto lambda = []() { std::cout << "Lambda" << std::endl; };
auto addLambda = [](int a, int b) { return a + b; };

五、混合编程

C语言交互

直接调用C函数

// 定义C函数
void c_print(const char *message) {
    printf("%s\n", message);
}

// Objective-C调用
- (void)logMessage:(NSString *)msg {
    c_print([msg UTF8String]); // NSString转C字符串
}

内存管理注意点

// Core Foundation对象桥接
CFArrayRef cfArray = CFArrayCreate(...);
NSArray *nsArray = (__bridge_transfer NSArray *)cfArray; // 转移所有权

iOS开发基础-通过C++快速掌握Objective-C语言(基础)

2025年2月20日 18:00

语言范式对比

类型系统

  • Objective-C:动态类型(id类型可指向任何对象)
id unknownObj = GetSomeObject(); // 运行时确定具体类型
  • C++:严格静态类型
auto obj = GetSomeObject(); // 编译时类型推导(C++11+)

消息传递机制

  • Objective-C:采用Smalltalk风格动态消息机制,所有方法调用均转换为objc_msgSend运行时函数
[obj doSomething:param]; // 编译后等价于objc_msgSend(obj, @selector(doSomething:), param)
  • C++:静态方法调用,编译时确定函数地址
obj->doSomething(param); // 编译时直接绑定虚函数表地址

基本语法差异

main函数

C语言:

#include <stdio.h>

int main() {
    printf("Hello, World!\n");
    return 0;
}

Objective-C:

#import <Foundation/Foundation.h>

int main() {
    @autoreleasepool {
        NSLog(@"Hello, World!");
    }
    return 0;
}

区别:

  • 在Objective-C中,我们使用@autoreleasepool来管理内存,类似于C语言中的手动内存管理。
  • NSLog是Objective-C的打印函数,类似C语言中的printf

数据类型

C语言:

int a = 5;
float b = 3.14;

Objective-C:

NSInteger a = 5; // 等同于C语言的int
CGFloat b = 3.14; // 适用于浮动类型

区别:

  • Objective-C使用NSIntegerCGFloat来替代C语言中的基本数据类型,以便更好地支持不同架构。

条件和循环

C语言:

#include <stdio.h>

int main() {
    int i = 0;
    while (i < 5) {
        printf("i: %d\n", i);
        i++;
    }
    return 0;
}

Objective-C:

#import <Foundation/Foundation.h>

int main() {
    @autoreleasepool {
        int i = 0;
        while (i < 5) {
            NSLog(@"i: %d", i);
            i++;
        }
    }
    return 0;
}

区别:

  • 条件语句和循环结构在C和Objective-C中是相同的,唯一的区别是输出方式,C使用printf,Objective-C使用NSLog

类与对象系统

类定义对比

// Objective-C(两段式定义)
@interface Vehicle : NSObject
@property (nonatomic) float speed;
- (void)startEngine;
+ (void)registerVehicleType;
@end

@implementation Vehicle
- (void)startEngine { /*...*/ }
+ (void)registerVehicleType { /*...*/ }
@end
// C++(单段式定义)
class Vehicle : public BaseObject {
public:
    float speed;
    void startEngine();
    static void registerVehicleType();
};

核心差异

  • Objective-C使用@interface声明公开接口,@implementation实现细节
  • C++在类声明中直接实现方法(或通过.cpp文件分离)

方法类型对比

  • 实例方法
- (void)instanceMethod; // 必须通过对象调用 [obj instanceMethod]

类似C++普通成员函数:obj->memberFunction()

  • 类方法
+ (void)classMethod; // 通过类调用 [Vehicle classMethod]

类似C++静态成员函数:Vehicle::staticFunction()

关键区别

  • Objective-C类方法可被子类继承并覆盖(self指向调用类)
  • C++静态方法无多态特性

方法调用与函数调用

C语言: 函数调用直接使用函数名。

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5);
    printf("Result: %d\n", result);
    return 0;
}

Objective-C: 在Objective-C中,方法调用使用[]语法。

#import <Foundation/Foundation.h>

@interface Calculator : NSObject
- (int)add:(int)a to:(int)b;
@end

@implementation Calculator
- (int)add:(int)a to:(int)b {
    return a + b;
}
@end

int main() {
    @autoreleasepool {
        Calculator *calc = [[Calculator alloc] init];
        int result = [calc add:3 to:5];
        NSLog(@"Result: %d", result);
    }
    return 0;
}

区别:

  • C语言使用函数调用,直接传入参数。
  • Objective-C使用方法调用,语法形式为[对象 方法名: 参数],通过-表示实例方法,+表示类方法。

多标签方法命名

在Objective-C中,方法定义允许为每个参数提供标签。这些标签可以帮助我们在调用方法时明确每个参数的作用。多标签的用法包括:

  1. 方法名中的标签:每个参数都有一个标签,这个标签在方法调用时会提供额外的信息,使代码更具可读性。
  2. 标签的命名约定:标签通常简洁且描述性强,代表参数的意义。

示例1:

#import <Foundation/Foundation.h>

@interface Calculator : NSObject
- (int)add:(int)a to:(int)b;
@end

@implementation Calculator
- (int)add:(int)a to:(int)b {
    return a + b;
}
@end

int main() {
    @autoreleasepool {
        Calculator *calc = [[Calculator alloc] init];
        int result = [calc add:3 to:5];
        NSLog(@"Result: %d", result);
    }
    return 0;
}
  • - 表示这是一个实例方法(而不是类方法,类方法用 +)。
  • (int)add: 表示方法的第一个参数 a,它的类型是 int,方法名的一部分是 add:。这个 add: 并不是表示操作符,而是方法的部分名字。
  • to: 是第二个参数的标签(parameter label),它的作用是描述这个参数的意义。这里 to: 表示操作是“从第一个数(a)加到第二个数(b)”,是方法名称的一部分。
  • (int)b 是第二个参数 b,它的类型是 int

示例2:

- (void)setRect:(CGFloat)x 
              y:(CGFloat)y 
          width:(CGFloat)width 
         height:(CGFloat)height;

// 调用方式
[view setRect:10 y:20 width:100 height:200];

近似C++实现:

void setRect(float x, float y, float width, float height);
view.setRect(10, 20, 100, 200);

设计差异

  • Objective-C强制参数标签提升可读性
  • C++依赖参数顺序和类型

告别回调地狱!Swift 并发编程的新时代

作者 Aaron0927
2025年2月20日 11:32

在 iOS 开发中,异步编程一直是一个绕不开的话题。从最初的 Block 回调,到 Combine 框架,再到现在的 async/await,Apple 一步步改进异步编程的方式,使代码更易读、更易维护。今天,我们就来聊聊 async/await 的优势,并深入探讨它如何帮助我们解决并发问题。

1. 回调地狱:异步编程的噩梦

在 iOS 早期,我们使用回调(Block)来处理异步任务,比如网络请求:

func fetchData(completion: @escaping (Result<String, Error>) -> Void) {
    DispatchQueue.global().async {
        // 模拟网络请求
        sleep(2)
        let success = Bool.random()
        if success {
            completion(.success("数据加载成功"))
        } else {
            completion(.failure(NSError(domain: "网络错误", code: -1)))
        }
    }
}

虽然这样可以工作,但当任务依赖增多时,就会形成回调地狱

fetchData { result in
    switch result {
    case .success(let data):
        processData(data) { processedData in
            saveToDatabase(processedData) { success in
                if success {
                    print("数据处理完成")
                }
            }
        }
    case .failure(let error):
        print("发生错误: \(error)")
    }
}

层层嵌套的回调不仅使代码可读性变差,还增加了错误处理的复杂度。Apple 需要一种更优雅的方式。

2. Combine 的改进,但仍然复杂

Combine 通过声明式 API 改进了回调问题,使代码更加流畅:

fetchDataPublisher()
    .map { processData($0) }
    .flatMap { saveToDatabase($0) }
    .sink(receiveCompletion: { completion in
        if case .failure(let error) = completion {
            print("发生错误: \(error)")
        }
    }, receiveValue: { success in
        print("数据处理完成: \(success)")
    })
    .store(in: &cancellables)

但 Combine 仍然需要繁琐的操作符,且新手入门成本较高。

3. async/await:更优雅的异步解决方案

Swift 5.5 引入了 async/await,使异步代码看起来像同步代码:

func fetchData() async throws -> String {
    try await Task.sleep(nanoseconds: 2 * 1_000_000_000)
    if Bool.random() {
        return "数据加载成功"
    } else {
        throw NSError(domain: "网络错误", code: -1)
    }
}

func process() async {
    do {
        let data = try await fetchData()
        print("数据处理完成: \(data)")
    } catch {
        print("发生错误: \(error)")
    }
}

代码逻辑清晰、可读性高,没有回调地狱。

4. 并发问题:为什么需要 actor?

在引入 async/await 之前,我们使用 GCD 和 OperationQueue 进行并发处理。但并发访问共享资源时,可能会发生数据竞争,导致难以复现的 bug。例如:

class Counter {
    var count = 0
    
    func increment() {
        count += 1
    }
}

let counter = Counter()
DispatchQueue.concurrentPerform(iterations: 1000) { _ in
    counter.increment()
}
print(counter.count) // 可能不是 1000,出现数据竞争

原因在于值类型存储在栈上,线程独享;引用类型存储在堆上,多个线程共享,如果多个线程同时修改 count,结果可能是不确定的。

Swift 通过actor 解决了这个问题。

5. actor:解决并发数据竞争

actor 是 Swift 引入的一种并发安全的引用类型,确保同一时间只有一个任务可以访问它的内部状态。

actor SafeCounter {
    private var count = 0
    
    func increment() {
        count += 1
    }
    
    func getCount() -> Int {
        return count
    }
}

let counter = SafeCounter()
Task {
    await counter.increment()
    print(await counter.getCount())
}

actor 内部,Swift 自动确保并发访问安全,使代码更易维护。

6. SwiftUI 中的 Task 和 .task 修饰符

在 SwiftUI 中,我们可以使用 .task 修饰符启动异步任务:

struct ContentView: View {
    @State private var data = "加载中..."
    
    var body: some View {
        Text(data)
            .task {
                do {
                    data = try await fetchData()
                } catch {
                    data = "发生错误"
                }
            }
    }
}

这样,视图在加载时会自动调用 fetchData(),避免使用 onAppear {} 处理异步操作。

7. TaskGroup 处理多个异步任务

如果我们有多个并发任务需要执行,比如并行加载多张图片,可以使用 TaskGroup

func fetchMultipleData() async -> [String] {
    await withTaskGroup(of: String.self) { group in
        for i in 1...3 {
            group.addTask {
                return await fetchData() + "\(i)"
            }
        }
        var results = [String]()
        for await result in group {
            results.append(result)
        }
        return results
    }
}

TaskGroup 允许多个任务并行执行,并在所有任务完成后收集结果。

8. 总结

  • 回调容易导致回调地狱,可读性差

  • Combine 解决了部分问题,但学习成本高

  • async/await 让代码更简洁,解决异步回调问题

  • actor 解决并发数据竞争,保证线程安全

  • SwiftUI .task 让异步任务管理更方便

  • TaskGroup 适用于多个异步任务的并发处理

未来的 Swift 可能会继续演进并发模型,但目前 async/await 和 actor 已经是最佳实践,赶快用起来吧!

iOS已有项目集成React-Native混合开发及热更新实现

2025年2月19日 19:23

在现在App项目开发中,不可避免的会接触各种跨平台开发技术,其中React-Native是最早且最广泛使用的跨平台框架之一,这里分享一下如何在已有iOS项目中混合开发React-Native模块,并且实现模块热更新功能。

1、由于项目历史原因本地环境比较旧,没有使用React-Native的最新版本

实际使用版本:

node v12.16.1、react-native 0.62.3、xcode 15.2、pod 1.12.0

2、创建React-Native模块,在项目根目录执行以下命令

npx react-native init RNModule --version 0.62.3

截屏2025-02-19 13.35.43.png

等待执行完成,目录结构应该是这样

截屏2025-02-19 13.51.04.png

3、podfile文件里需要配置reac-native相关库,这个可以参考react-native官方配置不同的版本配置不一样,这里都可以查询到。

截屏2025-02-19 13.38.23.png

4、在项目根目录执行以下命令

pod install

截屏2025-02-19 13.44.41.png

5、如果是新创建的项目需要删除SceneDelegate,还是使用默认的AppDelegate

截屏2025-02-19 17.41.16.png

6、在ViewController实现如下代码

截屏2025-02-19 17.41.25.png

7、修改React-Native模块

截屏2025-02-19 17.11.39.png

8、运行react-native代码,运行原生App

npx react-native start

截屏2025-02-19 17.51.47.png

9、最终效果,到这一步iOS原生和React-Native混合开发就调通了,可以进入正常的业务开发中

RPReplay_Final1739956322.gif

10、React Native bundle 打离线包,这是实现热更新的关键

mkdir -p /Users/yunxiao/Desktop/outputios && react-native bundle --entry-file ./index.js --platform ios --dev false --bundle-output /Users/yunxiao/Desktop/outputios/main.jsbundle --assets-dest /Users/yunxiao/Desktop/outputios
截屏2025-02-19 18.15.09.png截屏2025-02-19 18.19.12.png

11、bundle包导入项目查看是否可用,也可以作为备用文件,防止热更文件出问题时该模块无法使用

截屏2025-02-19 18.25.49.png

直接访问本地bundle包路径

RPReplay_Final1739960650.gif

12、模块热更新实现就很简单了,只要把该文件上传到云端之后,每次打开App通过接口检查是否有更新,没有更新直接忽略,有更新就启动更新包异步下载,下载完成后,下一次打开该模块时会校验本地目录是否完整且版本号是否和云端返回最新的版本号一致,校验完成后直接加载本地文件路径[NSURL fileURLWithPath:bundlePath],热更新流程完成。

教你一步步思考 DP:从记忆化搜索到递推到空间优化!(Python/Java/C++/Go)

作者 endlesscheng
2022年3月20日 09:23

一、寻找子问题

在示例 1 中,我们要解决的问题(原问题)是:

  • 有 $2$ 条地毯和 $8$ 块砖,计算没被覆盖的白色砖块的最少数目。

考虑从右往左覆盖砖。讨论最后一块砖是否覆盖(选或不选):

  • 如果不覆盖(不选)最后一块砖,那么需要解决的子问题为:有 $2$ 条地毯和 $8-1=7$ 块砖,计算没被覆盖的白色砖块的最少数目。
  • 如果覆盖(选)最后一块砖,那么末尾连续的 $\textit{carpetLen}=2$ 块砖都会被覆盖,需要解决的子问题为:有 $1$ 条地毯和 $8-\textit{carpetLen}=6$ 块砖,计算没被覆盖的白色砖块的最少数目。

由于选或不选都会把原问题变成一个和原问题相似的、规模更小的子问题,所以可以用递归解决。

注:从右往左思考,主要是为了方便把递归翻译成递推。从左往右思考也是可以的。

二、状态定义与状态转移方程

根据上面的讨论,我们需要在递归过程中跟踪以下信息:

  • $i$:还剩下 $i$ 条地毯。
  • $j$:剩余砖块为 $\textit{floor}[0]$ 到 $\textit{floor}[j]$,即 $j+1$ 块砖。

因此,定义状态为 $\textit{dfs}(i,j)$,表示用 $i$ 条地毯覆盖下标在 $[0,j]$ 中的砖,没被覆盖的白色砖块的最少数目。

接下来,思考如何从一个状态转移到另一个状态。

考虑 $\textit{floor}[j]$ 是否覆盖(选或不选):

  • 不覆盖(不选):接下来要解决的问题是,用 $i$ 条地毯覆盖下标在 $[0,j-1]$ 中的砖,没被覆盖的白色砖块的最少数目,再加上 $\texttt{int}(\textit{floor}[j])$(刚好白色是 $1$),得 $\textit{dfs}(i,j) = \textit{dfs}(i,j-1) + \texttt{int}(\textit{floor}[j])$。
  • 覆盖(选):如果 $i>0$,接下来要解决的问题是,用 $i-1$ 条地毯覆盖下标在 $[0,j-\textit{carpetLen}]$ 中的砖,没被覆盖的白色砖块的最少数目,即 $\textit{dfs}(i,j) = \textit{dfs}(i-1,j-\textit{carpetLen})$。

这两种情况取最小值,就得到了 $\textit{dfs}(i,j)$,即

$$
\textit{dfs}(i,j) =
\begin{cases}
\min(\textit{dfs}(i,j-1) + \texttt{int}(\textit{floor}[j]), \textit{dfs}(i-1,j-\textit{carpetLen})), & i>0 \
\textit{dfs}(i,j-1) + \texttt{int}(\textit{floor}[j]), & i=0 \
\end{cases}
$$

递归边界:如果 $j<\textit{carpetLen}\cdot i$,那么 $\textit{dfs}(i,j)=0$,因为剩余砖块可以全部覆盖。

递归入口:$\textit{dfs}(\textit{numCarpets},m-1)$,其中 $m$ 是 $\textit{floor}$ 的长度。这是原问题,也是答案。

三、递归搜索 + 保存递归返回值 = 记忆化搜索

考虑到整个递归过程中有大量重复递归调用(递归入参相同)。由于递归函数没有副作用,同样的入参无论计算多少次,算出来的结果都是一样的,因此可以用记忆化搜索来优化:

  • 如果一个状态(递归入参)是第一次遇到,那么可以在返回前,把状态及其结果记到一个 $\textit{memo}$ 数组中。
  • 如果一个状态不是第一次遇到($\textit{memo}$ 中保存的结果不等于 $\textit{memo}$ 的初始值),那么可以直接返回 $\textit{memo}$ 中保存的结果。

注意:$\textit{memo}$ 数组的初始值一定不能等于要记忆化的值!例如初始值设置为 $0$,并且要记忆化的 $\textit{dfs}(i,j)$ 也等于 $0$,那就没法判断 $0$ 到底表示第一次遇到这个状态,还是表示之前遇到过了,从而导致记忆化失效。一般把初始值设置为 $-1$。

Python 用户可以无视上面这段,直接用 @cache 装饰器。

具体请看视频讲解 动态规划入门:从记忆化搜索到递推,其中包含把记忆化搜索 1:1 翻译成递推的技巧。

class Solution:
    def minimumWhiteTiles(self, floor: str, numCarpets: int, carpetLen: int) -> int:
        floor = list(map(int, floor))  # 避免在 dfs 中频繁调用 int()
        @cache  # 缓存装饰器,避免重复计算 dfs 的结果(一行代码实现记忆化)
        def dfs(i: int, j: int) -> int:
            if j < carpetLen * i:  # 剩余砖块可以全部覆盖
                return 0
            if i == 0:
                return dfs(i, j - 1) + floor[j]
            return min(dfs(i, j - 1) + floor[j], dfs(i - 1, j - carpetLen))
        return dfs(numCarpets, len(floor) - 1)
class Solution {
    public int minimumWhiteTiles(String floor, int numCarpets, int carpetLen) {
        int m = floor.length();
        int[][] memo = new int[numCarpets + 1][m];
        for (int[] row : memo) {
            Arrays.fill(row, -1); // -1 表示没有计算过
        }
        return dfs(numCarpets, m - 1, floor.toCharArray(), memo, carpetLen);
    }

    private int dfs(int i, int j, char[] floor, int[][] memo, int carpetLen) {
        if (j < carpetLen * i) { // 剩余砖块可以全部覆盖
            return 0;
        }
        if (memo[i][j] != -1) { // 之前计算过
            return memo[i][j];
        }
        int res = dfs(i, j - 1, floor, memo, carpetLen) + (floor[j] - '0');
        if (i > 0) {
            res = Math.min(res, dfs(i - 1, j - carpetLen, floor, memo, carpetLen));
        }
        return memo[i][j] = res; // 记忆化
    }
}
class Solution {
public:
    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
        int m = floor.size();
        vector memo(numCarpets + 1, vector<int>(m, -1)); // -1 表示没有计算过
        auto dfs = [&](this auto&& dfs, int i, int j) -> int {
            if (j < carpetLen * i) { // 剩余砖块可以全部覆盖
                return 0;
            }
            int& res = memo[i][j]; // 注意这里是引用
            if (res != -1) { // 之前计算过
                return res;
            }
            if (i == 0) {
                return res = dfs(i, j - 1) + (floor[j] - '0');
            }
            return res = min(dfs(i, j - 1) + (floor[j] - '0'), dfs(i - 1, j - carpetLen));
        };
        return dfs(numCarpets, m - 1);
    }
};
func minimumWhiteTiles(floor string, numCarpets, carpetLen int) int {
    m := len(floor)
    memo := make([][]int, numCarpets+1)
    for i := range memo {
        memo[i] = make([]int, m)
        for j := range memo[i] {
            memo[i][j] = -1 // -1 表示没有计算过
        }
    }
    var dfs func(int, int) int
    dfs = func(i, j int) (res int) {
        if j < carpetLen*i { // 剩余砖块可以全部覆盖
            return
        }
        p := &memo[i][j]
        if *p != -1 { // 之前计算过
            return *p
        }
        defer func() { *p = res }() // 记忆化
        res = dfs(i, j-1) + int(floor[j]-'0')
        if i > 0 {
            res = min(res, dfs(i-1, j-carpetLen))
        }
        return
    }
    return dfs(numCarpets, m-1)
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\textit{numCarpets}\cdot m)$,其中 $m$ 是 $\textit{floor}$ 的长度。由于每个状态只会计算一次,动态规划的时间复杂度 $=$ 状态个数 $\times$ 单个状态的计算时间。本题状态个数等于 $\mathcal{O}(\textit{numCarpets}\cdot m)$,单个状态的计算时间为 $\mathcal{O}(1)$,所以总的时间复杂度为 $\mathcal{O}(\textit{numCarpets}\cdot m)$。
  • 空间复杂度:$\mathcal{O}(\textit{numCarpets}\cdot m)$。保存多少状态,就需要多少空间。

四、1:1 翻译成递推

我们可以去掉递归中的「递」,只保留「归」的部分,即自底向上计算。

具体来说,$f[i][j]$ 的定义和 $\textit{dfs}(i,j)$ 的定义是完全一样的,都表示用 $i$ 条地毯覆盖下标在 $[0,j]$ 中的砖,没被覆盖的白色砖块的最少数目。

相应的递推式(状态转移方程)也和 $\textit{dfs}$ 一样:

$$
f[i][j] =
\begin{cases}
\min(f[i][j-1] + \texttt{int}(\textit{floor}[j]), f[i-1][j-\textit{carpetLen})], & i>0 \
f[i][j-1] + \texttt{int}(\textit{floor}[j]), & i=0 \
\end{cases}
$$

初始值 $f[i][j]=0$,翻译自递归边界 $\textit{dfs}(i,j)=0$。

答案为 $f[\textit{numCarpets}][m-1]$,翻译自递归入口 $\textit{dfs}(\textit{numCarpets},m-1)$。

class Solution:
    def minimumWhiteTiles(self, floor: str, numCarpets: int, carpetLen: int) -> int:
        floor = list(map(int, floor))
        m = len(floor)
        f = [[0] * m for _ in range(numCarpets + 1)]
        f[0] = list(accumulate(floor))  # 单独计算 i=0 的情况,本质是 floor 的前缀和
        for i in range(1, numCarpets + 1):
            for j in range(carpetLen * i, m):
                f[i][j] = min(f[i][j - 1] + floor[j], f[i - 1][j - carpetLen])
        return f[-1][-1]
class Solution {
    public int minimumWhiteTiles(String floor, int numCarpets, int carpetLen) {
        char[] s = floor.toCharArray();
        int m = s.length;
        int[][] f = new int[numCarpets + 1][m];
        // 单独计算 i=0 的情况,本质是 s 的前缀和
        f[0][0] = s[0] - '0';
        for (int j = 1; j < m; j++) {
            f[0][j] = f[0][j - 1] + (s[j] - '0');
        }
        for (int i = 1; i <= numCarpets; i++) {
            for (int j = carpetLen * i; j < m; j++) {
                f[i][j] = Math.min(f[i][j - 1] + (s[j] - '0'), f[i - 1][j - carpetLen]);
            }
        }
        return f[numCarpets][m - 1];
    }
}
class Solution {
public:
    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
        int m = floor.size();
        vector f(numCarpets + 1, vector<int>(m));
        // 单独计算 i=0 的情况,本质是 floor 的前缀和
        f[0][0] = floor[0] - '0';
        for (int j = 1; j < m; j++) {
            f[0][j] = f[0][j - 1] + (floor[j] - '0');
        }
        for (int i = 1; i <= numCarpets; i++) {
            for (int j = carpetLen * i; j < m; j++) {
                f[i][j] = min(f[i][j - 1] + (floor[j] - '0'), f[i - 1][j - carpetLen]);
            }
        }
        return f[numCarpets][m - 1];
    }
};
func minimumWhiteTiles(floor string, numCarpets, carpetLen int) int {
    m := len(floor)
    f := make([][]int, numCarpets+1)
    for i := range f {
        f[i] = make([]int, m)
    }
    // 单独计算 i=0 的情况,本质是 floor 的前缀和
    f[0][0] = int(floor[0] - '0')
    for j := 1; j < m; j++ {
        f[0][j] = f[0][j-1] + int(floor[j]-'0')
    }
    for i := 1; i <= numCarpets; i++ {
        for j := carpetLen * i; j < m; j++ {
            f[i][j] = min(f[i][j-1]+int(floor[j]-'0'), f[i-1][j-carpetLen])
        }
    }
    return f[numCarpets][m-1]
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\textit{numCarpets}\cdot m)$,其中 $m$ 是 $\textit{floor}$ 的长度。
  • 空间复杂度:$\mathcal{O}(\textit{numCarpets}\cdot m)$。

五、空间优化

由于计算 $f[i]$ 只需要知道 $f[i-1]$ 中的数据,我们可以改用两个长为 $m$ 的数组滚动计算。

此外,可以在计算 DP 之前,特判 $\textit{numCarpets}\cdot \textit{carpetLen}\ge m$ 的情况,此时所有砖块都能被覆盖,直接返回 $0$。

class Solution:
    def minimumWhiteTiles(self, floor: str, numCarpets: int, carpetLen: int) -> int:
        m = len(floor)
        if numCarpets * carpetLen >= m:
            return 0

        floor = list(map(int, floor))
        f = list(accumulate(floor))
        for i in range(1, numCarpets + 1):
            nf = [0] * m
            for j in range(carpetLen * i, m):
                nf[j] = min(nf[j - 1] + floor[j], f[j - carpetLen])
            f = nf
        return f[-1]
class Solution:
    def minimumWhiteTiles(self, floor: str, numCarpets: int, carpetLen: int) -> int:
        m = len(floor)
        if numCarpets * carpetLen >= m:
            return 0

        floor = list(map(int, floor))
        f = list(accumulate(floor))
        for i in range(1, numCarpets + 1):
            nf = [0] * m
            for j in range(carpetLen * i, m):
                x = nf[j - 1] + floor[j]
                y = f[j - carpetLen]
                nf[j] = x if x < y else y
            f = nf
        return f[-1]
class Solution {
    public int minimumWhiteTiles(String floor, int numCarpets, int carpetLen) {
        int m = floor.length();
        if (numCarpets * carpetLen >= m) {
            return 0;
        }

        char[] s = floor.toCharArray();
        int[] f = new int[m];
        f[0] = s[0] - '0';
        for (int j = 1; j < m; j++) {
            f[j] = f[j - 1] + (s[j] - '0');
        }
        for (int i = 1; i <= numCarpets; i++) {
            int[] nf = new int[m];
            for (int j = carpetLen * i; j < m; j++) {
                nf[j] = Math.min(nf[j - 1] + (s[j] - '0'), f[j - carpetLen]);
            }
            f = nf;
        }
        return f[m - 1];
    }
}
class Solution {
public:
    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
        int m = floor.size();
        if (numCarpets * carpetLen >= m) {
            return 0;
        }

        vector<int> f(m);
        f[0] = floor[0] - '0';
        for (int j = 1; j < m; j++) {
            f[j] = f[j - 1] + (floor[j] - '0');
        }
        for (int i = 1; i <= numCarpets; i++) {
            vector<int> nf(m);
            for (int j = carpetLen * i; j < m; j++) {
                nf[j] = min(nf[j - 1] + (floor[j] - '0'), f[j - carpetLen]);
            }
            f = move(nf);
        }
        return f[m - 1];
    }
};
func minimumWhiteTiles(floor string, numCarpets, carpetLen int) int {
    m := len(floor)
    if numCarpets*carpetLen >= m {
        return 0
    }

    f := make([]int, m)
    f[0] = int(floor[0] - '0')
    for j := 1; j < m; j++ {
        f[j] = f[j-1] + int(floor[j]-'0')
    }
    for i := 1; i <= numCarpets; i++ {
        nf := make([]int, m)
        for j := carpetLen * i; j < m; j++ {
            nf[j] = min(nf[j-1]+int(floor[j]-'0'), f[j-carpetLen])
        }
        f = nf
    }
    return f[m-1]
}

复杂度分析

  • 时间复杂度:$\mathcal{O}(\textit{numCarpets}\cdot m)$,其中 $m$ 是 $\textit{floor}$ 的长度。
  • 空间复杂度:$\mathcal{O}(m)$。

更多相似题目,见 动态规划题单 中的「§6.3 约束划分个数」。

分类题单

如何科学刷题?

  1. 滑动窗口与双指针(定长/不定长/单序列/双序列/三指针/分组循环)
  2. 二分算法(二分答案/最小化最大值/最大化最小值/第K小)
  3. 单调栈(基础/矩形面积/贡献法/最小字典序)
  4. 网格图(DFS/BFS/综合应用)
  5. 位运算(基础/性质/拆位/试填/恒等式/思维)
  6. 图论算法(DFS/BFS/拓扑排序/最短路/最小生成树/二分图/基环树/欧拉路径)
  7. 【本题相关】动态规划(入门/背包/状态机/划分/区间/状压/数位/数据结构优化/树形/博弈/概率期望)
  8. 常用数据结构(前缀和/差分/栈/队列/堆/字典树/并查集/树状数组/线段树)
  9. 数学算法(数论/组合/概率期望/博弈/计算几何/随机算法)
  10. 贪心与思维(基本贪心策略/反悔/区间/字典序/数学/思维/脑筋急转弯/构造)
  11. 链表、二叉树与回溯(前后指针/快慢指针/DFS/BFS/直径/LCA/一般树)
  12. 字符串(KMP/Z函数/Manacher/字符串哈希/AC自动机/后缀数组/子序列自动机)

我的题解精选(已分类)

欢迎关注 B站@灵茶山艾府

wqs 二分 O(n log n)

作者 zerotrac2
2022年3月20日 01:37

容易发现当 k 增加时,覆盖的 1 的数量的增量是单调不增的,因此覆盖的 1 的数量关于 k 是一个上凸函数,可以使用 wqs 二分。

贴两个链接,感兴趣的可以学一学。

wqs 二分学习笔记

一种基于 wqs 二分的优秀做法

###C++

class Solution {
public:
    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
        int n = floor.size();
        vector<int> pre(n);
        pre[0] = (floor[0] == '1');
        for (int i = 1; i < n; ++i) {
            pre[i] = pre[i - 1] + (floor[i] == '1');
            if (i >= carpetLen) {
                pre[i] -= (floor[i - carpetLen] == '1');
            }
        }

        vector<int> f(n), g(n);
        int l = 0, r = n, ans = 0;
        while (l <= r) {
            f[0] = g[0] = 0;
            int mid = (l + r) / 2;
            for (int i = 0; i < n; ++i) {
                if (i != 0) {
                    f[i] = f[i - 1];
                    g[i] = g[i - 1];
                }
                if (i >= carpetLen) {
                    int use = f[i - carpetLen] + pre[i] - mid;
                    if (use > f[i]) {
                        f[i] = use;
                        g[i] = g[i - carpetLen] + 1;
                    }
                }
                else {
                    int use = pre[i] - mid;
                    if (use > f[i]) {
                        f[i] = use;
                        g[i] = 1;
                    }
                }
            }
            
            if (g[n - 1] <= numCarpets) {
                ans = f[n - 1] + mid * numCarpets;
                r = mid - 1;
            }
            else {
                l = mid + 1;
            }
        }

        int cnt1 = 0;
        for (char ch: floor) {
            cnt1 += (ch == '1');
        }
        return cnt1 - ans;
    }
};

贪心 && DP C++

作者 Cyber-Space
2022年3月20日 00:13

dp[i][j] 表示考虑前面长度为 j 的地板中,用了 i 条地毯,最后剩余最少白色砖块。
状态转移方程

  1. i == 0 时,此时不用地毯, dp[0][j] 表示前 j 个砖块中白色砖块数目。
  2. i != 0 时,dp[i][j] = min(dp[i][j - 1] + count[j], dp[i - 1][j - carpetLen]);
    其中 count[j] 表示第 j 块地板是否为白色砖块,
  • dp[i][j-1] + count[j] 表示前 j - 1 块地板用了i 条地毯,剩余最少的白色砖块,加上第 j 块地板是否为白色砖块。(即第 j 个位置不用地毯)
  • dp[i - 1][j - carpetLen] 表示在下标 j 这里用了地毯,那么 dp[i][j] 就和前 (j - carpetLen) 块地板中用了 i - 1 条地毯剩余的白色砖块数目一致。(即第 j 个位置用了地毯)

时间复杂度: O(nm)
空间复杂度: O(n
m)
其中 n 为 floor 长度, m 为numcarpets

代码如下:

class Solution {
public:
    int minimumWhiteTiles(string floor, int numCarpets, int carpetLen) {
        int len = floor.size();
        vector<vector<int>> dp(numCarpets + 1, vector<int>(len, 0));
        vector<int> count(len); //第j块地板是否为白色
        dp[0][0] = floor[0] == '1' ? 1 : 0;
        for(int i = 1; i < len; ++i) {
            dp[0][i] = dp[0][i - 1];
            if(floor[i] == '1') dp[0][i]++, count[i] = 1;
        }
        for(int i = 1; i <= numCarpets; ++i) {
            for(int j = 0; j < len; ++j) {
                if(j < carpetLen) dp[i][j] = 0; //少于carpetLen块砖块用了地毯最终都会剩余0块白色砖块
                else dp[i][j] = min(dp[i][j - 1] + count[j], dp[i - 1][j - carpetLen]);
            }
        }
        return dp[numCarpets][len - 1];
    }
};
昨天 — 2025年2月20日首页

DeepSeek的列车,有人准备躺上去了

2025年2月20日 22:44

据说,全国的大城市,都在为错过DeepSeek彻夜难眠。

江苏省委机关报《新华日报》旗下“交汇点”在2月7日晚、8日上午连发三篇专稿文章《DeepSeek为什么会出现在杭州?》《为什么南京发展不出“杭州六小龙”?》《杭州有DeepSeek,南京有什么?》

深圳的自媒体集体反思:这里没有诞生DeepSeek,是不是深圳不行了。还有部分科技企业动了心思:接下来要不要搬到杭州去?

下情上达,深圳立马宣布:全面接入入DeepSeek,“AI公务员”上岗。

但,个别自媒体仍然紧追不放,喊出“拿来主义救不了深圳”。

之前,四大一线城市在互联网科技领域长期“三强一瘸”,北京有字节、美团,上海有拼多多,深圳有腾讯、华为,广州是那“一瘸”。

诞生了阿里等互联网公司的杭州,一直奋起直追,但也仅仅是对“第四席”广州形成威胁。

直到DeepSeek出现,所有城市都露出了紧张感。

因为,很多城市和公司都吃过经济转型升级、“移动互联网”代替PC的痛。现在,全互联网行业都把AI当作了另一次“移动互联网”大潮。

因移动互联网来临而面临死亡的公司:国美、苏宁、天涯、人人、九城、易趣……

后来出现的互联网巨头,字节、拼多多、美团、腾讯、滴滴、淘宝,每一个长大的土壤都是移动互联网。

每一次大潮来临,都会诞生一批巨头,也会有一批企业死去。一家公司的死亡是小事,但在整个产业革新中没有上车,错过的可能是一整个时代。AI时代的到来也会一样。

没有人想错过未来。因此,AI必争。

而DeepSeek就是打破平衡,AI路上最快的那趟列车

01 

“造车”很难,但地产行业有一个非常形象的词,用来形容拿到红利:上车。

他们把房价的飞速上涨比作一辆列车,只要你买房上了车,躺着就可以吃到涨价的红利。

在价格红利之外,还有一种普通人很难看到的红利:资本红利。用股市举例,最直接的吃红利方式就是蹭热点概念股,把低估值资产向高估值领域转移。

因此,由“上车”思维延展,在公司运作层面又有了:搭车、挂车、套牌车的逻辑。

如何“搭车”DeepSeek,上周居然智家完美演示了一次。2月10日至14日五个交易日内,居然智家市值从214.9亿元,暴增至347亿元,增加132亿元。原因是公司接入了Deep Seek,拥抱AI。

在股价暴涨期间,2月12日,元宵节,居然智家董事长汪林朋再次讲话说:新一年有几件重要的事情:第一、3月要参加英伟达GTC大会;第二、推出家装行业的自有大模型;第三、成立智能家居研究院;第四、推动家装AI设计样板间落地;第五、打造北京最大的新能源汽车交付中心。

简要明了,AI、新能源全部击中。

地产企业曾经也深谙其道。听说,有几家地产公司正在打听:如何接入DeepSeek?

地产企业上一次对互联网这么兴奋,还要追溯到“互联网+”时期。

在“互联网+”概念火热的那几年,大量上市地产企业也纷纷“+互联网”。鑫苑做云和机器人,SOHO中国做共享办公,花样年、乐生活做社区O2O,当代地产做众筹买房……万科、碧桂园、恒大们,每家都有几个与当时热门产业关联的业务线。

一个“研习知识”的地产公号说,他们做得好是因为:“善用杠杆享尽市场红利”。“杠杆”不仅包括资本杠杆,还有“品牌杠杆”。

所谓的“品牌杠杆”,就是把高度再拔一拔

02

有的地产企业不在明处搭车,而是暗中“挂车”。

2017年9月,佳兆业斥资17.58亿收购A股上市公司明家联合21.25%的股权,成为其控股股东。

佳兆业入主后,“明家联合”改名“佳云科技”,由郭英成之子郭晓群担任董事长。

佳兆业曾计划将旗下互联网、大健康等产业资源注入佳云科技,推动其从互联网营销向“互联网+”综合服务转型,担当郭英成家族在境内的上市、融资平台,但因政策及市场环境变化未果。

佳云科技只是一个样本。2015年前后,开发商通过互联网、大健康、金融、汽车等曲线寻找境内上市、融资平台的案例比比皆是。

如今,翻开碧桂园、万科等企业上市体系外的“影子公司”,还能看到一个庞大、密集、错综复杂的资产网——每一个公司,都有其注册成立的直接目的。

营销天才史玉柱曾经说过:实业家创造社会财富,资本家优化社会财富。

不过,很多企业家自动把自己归到了“资本家”一档,并把社会财富向自己进行“优化”。

当创造财富,把企业业务做强做大太艰难,他们就开始“走捷径”,创造概念,搭车热门概念。

而DeepSeek、机器人,就是眼下最热门的概念。一群企业跟风而上,跟风而“接(入)”。

对此,小栖的一位朋友评价:“都在找机器人要估值:现在很多机器人公司跟风研发,重点展示走路,其实机器人走路不走路无所谓的,重点研发方向应该是手,因为人类正是因为有了灵巧的手,才能制造各种工具,以及解决日常生活的各种问题。但是走路的机器人展示效果很好,毕竟看上去更像人。”

对于这个问题,DeepSeek自己回答: 

全球经济不确定性下,科技赛道成为资本避风港。AI与机器人作为“硬科技”代表,容易获得政策扶持和融资,导致企业通过概念包装获取资源。但这场竞赛的本质,是对产业痛点的敬畏之心与工程化能力的终极考验。

本文来自微信公众号“未来可栖”,作者:小屋见大屋,36氪经授权发布。

初探 Vue 3响应式系统(四):Watch

作者 Hyyy
2025年2月20日 22:27

无论Vue2还是 Vue3 ,watch 无论在使用还是面试中都是当之无愧的高频,它让我们能够监听响应式数据的变化,并在变化时执行相应的回调函数,今天和大家一起学习下watch的内部实现。

1. watch 的基本用法

在开始解析源码之前,我们先回顾一下 watch 的基本用法。

watch 可以监听一个或多个响应式数据,并在它们发生变化时执行回调函数。

import { ref, watch } from 'vue';

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

count.value++; // 输出: 从 0 到 1

2. watch 的核心实现

2.1 watch 函数的定义

watch 函数的定义如下:

function watch(source, cb, options) {
  // 处理 source 和 cb
  // 创建 effect
  // 返回停止监听的函数
}

watch 函数接收三个参数:

  • source:要监听的数据源,可以是一个响应式对象、ref、或者一个 getter 函数。
  • cb:回调函数,当 source 发生变化时执行。
  • options:可选的配置项,比如 immediatedeep 等。

2.2 处理 source


watch 函数首先需要处理 source,将其转换为一个 getter 函数。这是因为 Vue3 的响应式系统是基于 effect 的,而 effect 需要一个 getter 函数来追踪依赖。

function watch(source, cb, options) {
  let getter;
  if (typeof source === 'function') {
    getter = source;
  } else if (isRef(source)) {
    getter = () => source.value;
  } else if (isReactive(source)) {
    getter = () => source;
    options.deep = true; // 默认深度监听
  } else {
    getter = () => {};
  }

  // 其他逻辑
}

在这个代码片段中,我们根据 source 的类型来决定如何生成 getter 函数:

  • 如果 source 是一个函数,直接将其作为 getter
  • 如果 source 是一个 ref,则 getter 返回 ref.value
  • 如果 source 是一个 reactive 对象,则 getter 返回该对象,并默认启用深度监听。

2.3 初始化环境

首先,watch需要为后续的操作做好准备。这包括定义一个用于存储清理函数的变量和创建一个job函数,这个函数将在数据变化时被调用。

接下来,watch会创建一个ReactiveEffect实例。这个实例是Vue3响应式系统的核心,它负责追踪source的依赖,并在依赖变化时执行job函数。

function watch(source, cb, options) {
    // ...上面的getter逻辑

    let cleanup; // 用于存储清理函数

    // 定义一个函数,用于注册清理函数
    const onInvalidate = (fn) => {
      cleanup = fn;
    };

    // 定义 job 函数,它会在依赖变化时执行
    const job = () => {
      if (cleanup) {
        cleanup(); // 如果存在清理函数,则先执行清理函数
      }
      const newValue = effect.run(); // 执行 effect 来获取新的值
      cb(newValue, oldValue, onInvalidate); // 调用回调函数
      oldValue = newValue; // 更新旧值
    };
    // 创建一个 ReactiveEffect 实例,当getter中依赖的数据变化,就会执行job ⭐️
    const effect = new ReactiveEffect(getter, job);

    // 首次执行 effect 来获取初始值 
    let oldValue = effect.run();
}

2.4 处理 immediate 选项

watch 还支持 immediate 选项,当 immediatetrue 时,回调函数会在 watch 创建时立即执行一次。

最后,watch会根据immediate选项决定是否立即执行job函数。同时,它返回一个函数,允许我们手动停止对source的监听。

if (options.immediate) {
  job();
} else {
  oldValue = effect.run();
}

在这个代码片段中,我们根据 immediate 选项来决定是否立即执行 job 函数。

2.5 返回停止监听的函数

最后,watch 函数返回一个停止监听的函数,调用这个函数可以停止对 source 的监听。

return () => {
  effect.stop();
};

这个函数通过调用 effect.stop() 来停止 effect 的运行,从而停止对 source 的监听。

2.6 完整代码

function watch(source, cb, options) {
    let getter;
    if (typeof source === 'function') {
      getter = source;
    } else if (isRef(source)) {
      getter = () => source.value;
    } else if (isReactive(source)) {
      getter = () => source;
      options.deep = true; // 默认深度监听
    } else {
      getter = () => {};
    }
  
    let cleanup; // 用于存储清理函数

    // 定义一个函数,用于注册清理函数
    const onInvalidate = (fn) => {
      cleanup = fn;
    };

    // 定义 job 函数,它会在依赖变化时执行
    const job = () => {
      if (cleanup) {
        cleanup(); // 如果存在清理函数,则先执行清理函数
      }
      const newValue = effect.run(); // 执行 effect 来获取新的值
      cb(newValue, oldValue, onInvalidate); // 调用回调函数
      oldValue = newValue; // 更新旧值
    };
    // 创建一个 ReactiveEffect 实例 
    const effect = new ReactiveEffect(getter, job);

    // 首次执行 effect 来获取初始值 
    let oldValue = effect.run();
    
    // 如果选项中设置了 immediate 为 true,则立即执行 job ⭐️新增
    if (options.immediate) { 
      job();
    } else {
      // 否则,再次执行 effect 来确保 oldValue 是响应式数据的当前值
      oldValue = effect.run();
    }

    // 返回一个函数,调用它可以停止 effect,从而停止对 source 的监听 ⭐️新增
    return () => {
      effect.stop();
    };
}

3. 代码测试

watch部分就自动中就用我们文章最下方的附录代码

3.1 测试普通 ref

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}
const count = ref(0);


watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

count.value++; // 输出: 从 0 到 1

在这个例子中,watch 监听了 count 的变化,并在 count 的值发生变化时打印出新旧值。

3.2 测试 reactive 对象

import { reactive, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const state = reactive({ count: 0 });

watch(() => state.count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
});

state.count++; // 输出: 从 0 到 1

在这个例子中,watch 监听了 state.count 的变化,并在 state.count 的值发生变化时打印出新旧值。

3.3 使用 immediate 选项

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const count = ref(0);

watch(count, (newValue, oldValue) => {
  console.log(`从 ${oldValue}${newValue}`);
}, { immediate: true });

// 输出: 从 undefined 到 0
count.value++; // 输出: 从 0 到 1

在这个例子中,watch 在创建时立即执行了一次回调函数,输出了 count 的初始值。

3.4 使用 onInvalidate 清理副作用

import { ref, watch } from 'vue';

function watch(){
    // 此处为我们的watch代码
    // ...
}

const count = ref(0);

watch(count, (newValue, oldValue, onInvalidate) => {
  let expired = false;
  onInvalidate(() => {
    expired = true;
  });

  setTimeout(() => {
    if (!expired) {
      console.log(`从 ${oldValue}${newValue}`);
    }
  }, 1000);
});

count.value++; // 1秒后输出: 从 0 到 1
count.value++; // 不会输出,因为上一次的副作用被清理了

在这个例子中,我们使用 onInvalidate 来清理上一次的副作用,确保只有最新的回调函数会执行。

3. 总结

对于watch的解析就到这里咯,有什么问题的话,感谢指正!

深入JavaScript引擎与模块加载机制:从V8原理到模块化实战

作者 鱼樱前端
2025年2月20日 22:11

元宵节到最近更新的频次比较少是因为在准备内容投放掘金和公众号,现在步入正轨里面来了!!!js基础到进阶的内容划分出来一大部分了,正所谓基础不牢地动山摇~希望以下内容对你们有收获!!!欢迎持续收藏关注对标知识点,**本人掘金和公众号(鱼樱AI实验室)**会持续更新有关前端的所有知识链条。

# 深入JavaScript引擎与模块加载机制:从V8原理到模块化实战

作为现代前端开发者,理解JavaScript运行环境的底层机制是突破性能瓶颈的关键。本文将结合 **V8引擎官方文档****ECMAScript规范**,通过原理图解和真实案例,彻底解析JavaScript从代码到执行的完整生命周期!

---

一、JavaScript引擎架构深度解析

1. V8引擎核心工作流程

三阶段处理流水线

源码 → 解析器(Parser) → AST 
     → 解释器(Ignition) → 字节码 + 类型反馈 
     → 编译器(TurboFan) → 优化机器码**各组件协作原理**
组件 功能 优化策略
Ignition 生成快速启动的字节码 收集类型反馈数据
TurboFan 生成高度优化的机器码 基于类型反馈的推测优化
Orinoco GC 垃圾回收 分代式回收 + 并行标记

性能对比数据(执行1e7次加法):

阶段 执行时间 内存占用
纯解释执行 420ms 35MB
优化编译后 58ms 82MB

2. 内存堆与调用栈原理

内存结构图解

[内存堆]
  ├─ 新生代(New Space): Scavenge算法
  ├─ 老生代(Old Space): 标记-清除/整理算法
  ├─ 大对象空间(Large Object Space)
  └─ 代码空间(Code Space)

[调用栈]
  ├─ 全局执行上下文
  ├─ 函数A执行上下文(变量对象、作用域链、this)
  └─ 函数B执行上下文(嵌套调用)

闭包内存管理

function createCounter() {
  let count = 0; // 闭包变量存入堆内存
  return () => count++;
}
// 函数执行上下文销毁后,count仍被闭包引用

3. 事件循环与渲染引擎协作

浏览器线程模型

主线程(Main Thread):
  └─ JS引擎(V8)
  └─ 渲染引擎(Blink)
  └─ 事件循环调度

独立线程:
  └─ 定时器线程
  └─ 网络线程
  └─ GPU合成线程

协作时序图

[Task] → [执行JS] → [Microtasks] → [RAF回调] → [Layout] → [Paint] → [下一帧]

关键渲染策略

  • VSync同步:60Hz刷新率下每16.6ms执行一次渲染
  • 增量布局:避免大规模DOM变更导致的布局抖动
  • 图层合并:通过will-change创建独立合成层

二、模块加载机制深度剖析

1. ES6模块的静态解析特性

与CommonJS对比

特性 ES模块 CommonJS
加载方式 静态分析(编译时) 动态加载(运行时)
导出类型 实时绑定(Live Binding) 值拷贝
顶层代码 严格模式(强制) 非严格模式(默认)

模块解析过程

  1. 构造阶段:解析所有import/export语句生成模块记录
  2. 实例化阶段:创建作用域链并绑定导入导出
  3. 求值阶段:执行模块顶层代码

2. 模块映射表与缓存机制

浏览器加载流程

1. 解析入口文件 → 发现import语句
2. 发起模块请求 → 检查缓存(Module Map)
3. 缓存未命中 → 下载并解析新模块
4. 存入缓存 → 建立模块依赖图

缓存策略示例

// 模块缓存表结构
const moduleMap = new Map([
  ['https://example.com/app.js', {
    dependencies: new Set(['./utils.js']),
    module: ModuleRecord
  }]
]);

3. 循环依赖处理策略

ES模块解决方案

// a.js
import { b } from './b.js';
export const a = 'A' + b;

// b.js
import { a } from './a.js';
export const b = 'B' + a;

// 执行结果:a = "Aundefined", b = "Bundefined"

处理流程

  1. 模块a开始解析 → 标记为"fetching"
  2. 发现依赖b → 开始加载b
  3. 模块b解析时发现依赖a → 返回已部分解析的a
  4. 最终完成所有模块的链接

最佳实践

// 解决方案:动态导入
// a.js
export let a;
import('./b.js').then(({ b }) => {
  a = 'A' + b;
});

// b.js
export const b = 'B';

三、引擎优化与模块化最佳实践

1. V8性能优化技巧

  • 隐藏类稳定:保持对象属性顺序一致
  • 类型反馈优化:避免多态函数参数
  • 内存管理:及时释放大数组/对象引用

2. 模块化开发规范

// 分层架构示例
import core from './core/index.js';       // 核心层
import utils from '../libs/utils.js';    // 工具层
import './analytics.js';                 // 副作用模块

// 动态加载优化首屏
button.onclick = async () => {
  const heavyModule = await import('./heavy.js');
  heavyModule.run();
};

3. 调试技巧

// 查看模块依赖图(浏览器控制台)
console.log(performance.getEntriesByType('resource'));

// 强制禁用缓存(开发模式)
import module from './module.js?t=' + Date.now();

// 内存快照分析
window.performance.memory; // 获取堆大小信息

总结:引擎与模块化核心原理

  1. 分层编译:解释器快速启动 + 编译器深度优化
  2. 内存隔离:堆栈分离管理 + 分代垃圾回收
  3. 模块静态化:依赖预解析 + 实时绑定
  4. 循环引用:软链接解决未初始化问题

转发本文,帮助更多开发者突破性能瓶颈! 🚀


扩展阅读

性能工具推荐

  • Chrome Performance面板
  • Webpack Bundle Analyzer
  • Node.js --trace-opt参数

从“小白”到“大神”:JS箭头函数与普通函数的深度对决

作者 天涯学馆
2025年2月20日 22:10

开场:引入话题

在 JavaScript 的奇妙世界里,函数是一等公民,它们赋予了代码强大的灵活性和复用性。而随着 ES6 的到来,箭头函数以其简洁的语法和独特的特性,迅速成为了开发者们的心头好。但它与传统的普通函数相比,究竟有何不同呢?接下来,让我们通过一个简单的例子来一探究竟。

假设我们有一个需求:计算一个数组中所有元素的平方和。先看看使用普通函数如何实现:

const numbers = [1, 2, 3, 4, 5];
function sumOfSquares(arr) {
    let sum = 0;
    for (let i = 0; i < arr.length; i++) {
        sum += arr[i] * arr[i];
    }
    return sum;
}
const result1 = sumOfSquares(numbers);
console.log(result1); 

再看看使用箭头函数的实现方式:

const numbers = [1, 2, 3, 4, 5];
const sumOfSquares = arr => arr.reduce((acc, num) => acc + num * num, 0);
const result2 = sumOfSquares(numbers);
console.log(result2); 

同样的功能,两种不同的实现方式,代码量和风格却大相径庭。是不是已经勾起了你对箭头函数和普通函数区别的好奇呢?别着急,接下来我们就深入剖析它们之间的差异。

一、语法大不同

(一)普通函数语法剖析

在 JavaScript 的历史长河中,普通函数一直是定义和使用函数的传统方式。它使用function关键字来宣告一个函数,拥有一套完整的结构,包括函数名、参数列表以及包裹在花括号内的函数体。以一个简单的加法函数为例:

function add(a, b) {    
return a + b;
}

在这个例子中,add是函数名,它就像是给这个函数取的一个独特的名字,方便我们在代码的其他地方调用它。(a, b)是参数列表,这两个参数就像是函数执行时的 “原材料”,函数会根据传入的参数进行相应的操作。而花括号内的return a + b;则是函数体,这里面包含了具体的执行逻辑,也就是将两个参数相加并返回结果。

(二)箭头函数语法亮点

ES6 引入的箭头函数,犹如一阵清新的风,为 JavaScript 的函数定义带来了全新的体验。它的语法简洁明了,使用=>来定义函数。当箭头函数只有一个参数时,可以省略参数的括号;如果函数体只有一条语句,还能省略花括号和return关键字,直接返回该语句的结果。比如同样是上述加法函数,用箭头函数可以写成:

const add = (a, b) => a + b;

这里直接将函数赋值给add常量,参数(a, b)直接跟在=>前面,而函数体a + b因为只有一条语句,所以省略了花括号和return关键字,代码瞬间变得简洁高效。

(三)代码示例对比

下面通过更多不同参数和函数体情况的代码示例,更直观地感受普通函数和箭头函数的语法差异。

无参数情况

  • 普通函数:
function sayHello() {    
console.log('Hello!');
}
  • 箭头函数:
const sayHello = () => console.log('Hello!');

单参数情况

  • 普通函数:
function square(x) {
return x * x;
}
  • 箭头函数:
const square = x => x * x;

多参数情况

  • 普通函数:
function multiply(a, b, c) {    
return a * b * c;
}
  • 箭头函数:
const multiply = (a, b, c) => a * b * c;

函数体为多条语句情况

  • 普通函数:
function calculate(a, b) {
    let sum = a + b;
    let product = a * b;
    return sum + product;
}
  • 箭头函数:
const calculate = (a, b) => {
    let sum = a + b;
    let product = a * b;
    return sum + product;
};

通过这些示例可以清晰地看到,在不同场景下,箭头函数的语法相较于普通函数更加简洁,能够减少代码的冗余,提高代码的可读性 。尤其是在一些简单的函数定义中,箭头函数的优势更为明显。

二、this 指向的奥秘

(一)普通函数的 this 指向规则

在 JavaScript 的世界里,普通函数的this指向就像是一个 “变色龙”,它会根据函数的调用方式而发生变化。当普通函数在全局作用域中被调用时,它的this指向全局对象。在浏览器环境下,这个全局对象就是window。例如:

function globalFunction() {
console.log(this); 
}
globalFunction(); 

上述代码中,globalFunction在全局作用域中被调用,所以this指向window。

当普通函数作为对象的方法被调用时,this指向调用该方法的对象。比如:

const person = {
    name: 'Alice',
    sayHello: function() {
        console.log(`Hello, I'm ${this.name}`); 
    }
};
person.sayHello(); 

这里sayHello是person对象的方法,当person.sayHello()调用时,this就指向person对象,所以会输出Hello, I'm Alice。

另外,普通函数还可以通过call、apply、bind方法来显式地改变this的指向。call和apply方法会立即调用函数,并且将函数中的this指向传入的第一个参数。不同的是,call方法后续的参数是逐个传入,而apply方法则是将后续参数以数组的形式传入。bind方法则是返回一个新的函数,这个新函数的this被绑定为传入的第一个参数,且不会立即调用。示例如下:

function greet(message) {
    console.log(`${message}, I'm ${this.name}`); 
}
const person1 = { name: 'Bob' };
const person2 = { name: 'Charlie' };
greet.call(person1, 'Hi'); 
greet.apply(person2, ['Hello']); 
const boundGreet = greet.bind(person1, 'Hey');
boundGreet(); 

(二)箭头函数的 this 绑定特性

箭头函数的this指向与普通函数截然不同,它就像是一个 “忠贞不渝的卫士”,始终指向定义时外层作用域的this,并且无法通过call、apply、bind方法来改变它的指向。这是因为箭头函数没有自己的this绑定,它的this是从外层作用域继承而来的。例如:

const outerThis = this;
const arrowFunction = () => {
    console.log(this === outerThis); 
};
arrowFunction(); 

在这段代码中,箭头函数arrowFunction定义时,外层作用域的this是outerThis,所以箭头函数内部的this也指向outerThis,输出结果为true。

(三)场景分析与示例

通过一个具体的场景来深入分析普通函数和箭头函数在this指向不同时所导致的结果差异。假设我们有一个对象,里面包含一个数组和一个方法,方法的作用是遍历数组并打印每个元素以及当前对象的属性。先用普通函数来实现:

const data = {
    numbers: [1, 2, 3],
    printNumbers: function() {
        const self = this;
        this.numbers.forEach(function(number) {
            console.log(`${number} belongs to ${self.name}`); 
        });
    }
};
data.printNumbers(); 

在上述代码中,forEach回调函数是一个普通函数,它的this指向window(在非严格模式下),所以不能直接访问data对象的属性。为了解决这个问题,我们使用了self = this的方式,将this保存到一个变量self中,然后在回调函数中通过self来访问data对象的属性。

再看看使用箭头函数的实现方式:

const data = {
    numbers: [1, 2, 3],
    printNumbers: function() {
        this.numbers.forEach((number) => {
            console.log(`${number} belongs to ${this.name}`); 
        });
    }
};
data.printNumbers(); 

这里forEach回调函数是一个箭头函数,它的this继承自外层的printNumbers方法,也就是data对象,所以可以直接访问data对象的属性,代码更加简洁明了。

三、arguments 参数处理

(一)普通函数的 arguments 对象

在普通函数的世界里,有一个神奇的arguments对象,它就像是一个 “百宝箱”,默默地收集着所有传入函数的参数。无论你在函数定义时是否声明了这些参数,arguments对象都会将它们一一收纳。例如,我们定义一个简单的求和函数:

function sum() {
    let total = 0;
    for (let i = 0; i < arguments.length; i++) {
        total += arguments[i];
    }
    return total;
}
console.log(sum(1, 2, 3)); 

在这个sum函数中,没有预先声明参数,但通过arguments对象,我们可以轻松地访问到传入的所有参数,并对它们进行求和操作。arguments对象是一个类数组对象,它具有length属性,可以获取传入参数的个数,并且可以通过索引来访问每个参数,就像访问数组元素一样 。

(二)箭头函数的参数处理方式

与普通函数不同,箭头函数并没有属于自己的arguments对象。如果在箭头函数中尝试访问arguments,会导致错误。不过,箭头函数有它自己独特的参数处理方式 —— 使用剩余参数...。剩余参数可以将所有传入的参数收集到一个数组中,方便我们进行处理。例如,同样是上述求和功能,用箭头函数可以这样实现:

const sum = (...args) => {
    let total = 0;
    for (let i = 0; i < args.length; i++) {
        total += args[i];
    }
    return total;
};
console.log(sum(1, 2, 3)); 

这里的...args就是剩余参数,它将传入的所有参数收集到args数组中,我们可以像操作普通数组一样对其进行遍历和计算 。

(三)应用场景对比

在实际开发中,当我们遇到需要处理不定参数的场景时,普通函数和箭头函数的不同处理方式就会展现出各自的优势和适用场景。比如,在一个日志记录函数中,我们可能需要记录不同数量的参数信息。使用普通函数可以这样实现:

function logInfo() {
    let message = 'Log Info: ';
    for (let i = 0; i < arguments.length; i++) {
        message += arguments[i] + ' ';
    }
    console.log(message);
}
logInfo('User logged in', 'John', '192.168.1.1'); 

而如果使用箭头函数,则可以利用剩余参数来实现:

const logInfo = (...args) => {
    let message = 'Log Info: ';
    for (let i = 0; i < args.length; i++) {
        message += args[i] + ' ';
    }
    console.log(message);
};
logInfo('User logged in', 'John', '192.168.1.1'); 

从代码实现上看,两者都能完成任务,但在一些复杂的场景中,箭头函数的剩余参数语法可能会使代码更加简洁和直观,尤其是在结合数组的一些方法(如map、reduce等)时,能够更方便地对参数数组进行操作。而普通函数的arguments对象则在一些需要兼容旧代码或者对参数顺序和个数有严格要求的场景中,依然发挥着重要作用 。

四、函数的其他特性差异

(一)构造函数的使用限制

在 JavaScript 的面向对象编程中,普通函数扮演着一个重要的角色 —— 它可以作为构造函数来创建对象实例。当我们使用new关键字来调用普通函数时,它就摇身一变成为了构造函数。构造函数在执行时,会创建一个新的空对象,这个新对象的__proto__属性会指向构造函数的prototype属性,然后构造函数内部的this会指向这个新对象,最后返回这个新对象。例如,我们定义一个Person构造函数:

function Person(name, age) {
    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello, I'm ${this.name}, ${this.age} years old.`);
    };
}
const person1 = new Person('Tom', 25);
person1.sayHello(); 

这里的Person函数就是一个构造函数,通过new Person('Tom', 25)创建了一个person1对象实例,并且可以调用sayHello方法。每个构造函数都有一个prototype属性,这个属性指向一个对象,称为原型对象,原型对象上的属性和方法可以被通过该构造函数创建的所有实例对象共享 。

而箭头函数则不能作为构造函数使用。这是因为箭头函数没有自己的this绑定,它的this是从外层作用域继承而来的,并且箭头函数也没有prototype属性。如果尝试使用new关键字调用箭头函数,会抛出TypeError错误。例如:

const PersonArrow = (name, age) => {
    this.name = name;
    this.age = age;
};
const person2 = new PersonArrow('Jerry', 30); 

上述代码会报错,提示PersonArrow is not a constructor,这表明箭头函数无法像普通函数那样作为构造函数来创建对象实例 。

(二)函数的原型与继承

普通函数在 JavaScript 的原型链和继承机制中起着关键的作用。通过构造函数的prototype属性,我们可以为一类对象定义共享的属性和方法。例如:

function Animal(name) {
    this.name = name;
}
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a sound.`);
};
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log(`${this.name} barks.`);
};
const myDog = new Dog('Buddy', 'Golden Retriever');
myDog.speak(); 
myDog.bark(); 

在这个例子中,Animal是一个构造函数,它的prototype上定义了speak方法。Dog构造函数通过Animal.call(this, name)继承了Animal的属性,并且通过Dog.prototype = Object.create(Animal.prototype)将Dog的原型设置为Animal原型的一个实例,从而实现了继承。这样,myDog对象既可以调用Animal原型上的speak方法,也可以调用Dog原型上的bark方法。

然而,由于箭头函数没有prototype属性,所以它在继承方面存在很大的局限性。它无法像普通函数那样通过原型链来实现继承,也不能作为构造函数为子类提供基础的属性和方法。在需要使用继承的场景中,箭头函数就显得力不从心了。

(三)适用场景总结

在实际的 JavaScript 编程中,选择使用普通函数还是箭头函数,需要根据具体的场景来决定。

在面向对象编程中,当我们需要定义类和创建对象实例时,普通函数作为构造函数是必不可少的。它能够方便地为对象定义属性和方法,并且通过原型链实现继承和多态等面向对象的特性。例如,在开发一个游戏引擎时,可能会定义各种角色类,如Warrior、Mage、Archer等,这些类都可以通过普通函数作为构造函数来实现,并且可以通过继承一个基类Character来共享一些通用的属性和方法 。

在事件处理中,普通函数也有其优势。因为事件处理函数的this通常需要指向触发事件的元素,而普通函数的this动态绑定特性正好满足这一需求。例如,在一个网页中,当点击按钮时需要执行某个操作,我们可以使用普通函数来定义点击事件的处理函数:

<!DOCTYPE html>
<html>

<body>
    <button id="myButton">Click me</button>
    <script>
        const myButton = document.getElementById('myButton');
        myButton.addEventListener('click', function () {
            this.style.backgroundColor = 'red';
        });
    </script>
</body>

</html>

这里的this指向myButton元素,所以可以直接修改按钮的背景颜色。

而箭头函数则更适合用于那些需要简洁语法和固定this指向的场景。比如在数组的方法回调函数中,箭头函数可以使代码更加简洁易读。例如,使用map方法将数组中的每个元素翻倍:

const numbers = [1, 2, 3, 4];
const doubledNumbers = numbers.map(num => num * 2);
console.log(doubledNumbers); 

在这个例子中,箭头函数作为map方法的回调函数,简洁地实现了对数组元素的操作。

另外,在一些需要访问外层作用域this的场景中,箭头函数也能很好地发挥作用。比如在类的方法中定义一个内部函数,并且这个内部函数需要访问类的this时,使用箭头函数就可以避免this指向的问题 。

结尾:总结与展望

通过以上全方位的对比分析,我们清晰地看到了 JavaScript 中箭头函数和普通函数的显著区别。箭头函数以其简洁的语法,让代码书写更加高效,尤其在处理简单逻辑和作为回调函数时,能大幅提升代码的可读性;它那独特的this指向特性,避免了在复杂场景中this指向混乱的问题,为开发者提供了更加稳定的编程体验 。

而普通函数则凭借其灵活的this绑定规则,在面向对象编程、事件处理等领域发挥着不可替代的作用;其arguments对象和可作为构造函数的特性,也为解决各种复杂的编程需求提供了有力的支持。

在未来的 JavaScript 编程旅程中,我们应根据具体的业务场景和需求,精准地选择使用箭头函数或普通函数。让我们充分利用它们各自的优势,编写出更加优雅、高效、易维护的代码,在 JavaScript 的广阔天地中自由翱翔,创造出更多精彩的应用。

前端面试题

2025年2月20日 21:50

以下是一些前端面试题:

一、HTML/CSS部分

  1. 如何实现一个自适应的图片容器,在不同屏幕尺寸下图片按比例缩放且不失真?
  • 答案
  • 方法一:使用max - widthheight: auto属性。
  • 在CSS中为图片设置max - width: 100%; height: auto;,这样图片会根据其容器的大小按比例缩放,并且不会出现失真的情况。
  • 方法二:使用object - fit属性(适用于现代浏览器)。
  • 如果希望图片填充整个容器并且保持比例,可以设置object - fit: cover;;如果希望图片完整显示在容器内并且可能留白,可以设置object - fit: contain;
  1. CSS中display: nonevisibility: hidden有什么区别?
  • 答案
  • display: none
  • 元素完全从页面布局中移除,不占据任何空间,其后的元素会占据该元素原本的空间位置。
  • 例如,当隐藏一个导航菜单时,如果使用display: none,菜单所在的空间将空出,后面的内容会向上移动。
  • visibility: hidden
  • 元素仍然占据页面布局中的空间,只是不可见。
  • 例如,当隐藏一个装饰性元素时,如果使用visibility: hidden,它所在的空间仍然保留,后面的元素不会移动。
  1. 如何使用CSS实现一个简单的动画效果(如淡入淡出)?
  • 答案
  • 可以使用CSS的transition属性或者@keyframes规则结合animation属性来实现淡入淡出效果。
  • 使用transition实现淡入淡出(以一个div元素为例):
  • HTML:
<div class="fade - element"></div>  
  • CSS:
.fade - element {  
opacity: 0;  
transition: opacity 1s ease - in - out;  
}  
.fade - element.show {  
opacity: 1;  
}  
  • 然后通过JavaScript或其他方式给div元素添加或移除show类来触发淡入淡出效果。
  • 使用@keyframesanimation实现淡入淡出:
  • HTML同上。
  • CSS:
@keyframes fade - in - out {  
0% {  
opacity: 0;  
}  
50% {  
opacity: 1;  
}  
100% {  
opacity: 0;  
}  
}  
.fade - element {  
animation: fade - in - out 2s infinite;  
}  

二、JavaScript部分

  1. JavaScript中的this关键字是如何工作的?请举例说明不同情况下this的指向。
  • 答案
  • 在全局作用域中,this指向全局对象(在浏览器中是window对象)。
  • 例如,在全局脚本中直接定义一个函数function foo() { console.log(this); },调用foo()时,this指向window
  • 在函数内部,this的值取决于函数的调用方式。
  • 如果是作为普通函数调用,this指向全局对象(严格模式下为undefined)。
  • 如果是作为对象的方法调用,this指向该对象。
  • 例如:
let obj = {  
name: 'John',  
sayName: function () {  
console.log(this.name);  
}  
};  
obj.sayName(); // this指向obj  
  • 在构造函数中,this指向新创建的对象实例。
  • 使用callapplybind方法可以显式地设置this的值。
  1. 如何实现一个简单的JavaScript模块模式?请举例说明其优点。
  • 答案
  • 模块模式的实现:
  • 可以使用立即执行函数表达式(IIFE)来创建模块。
  • 例如:
let module = (function () {  
let privateVariable = 'This is private';  
function privateFunction() {  
console.log(privateVariable);  
}  
return {  
publicMethod: function () {  
privateFunction();  
}  
};  
})();  
module.publicMethod();  
  • 优点:
  • 数据封装,将变量和函数封装在模块内部,避免了全局命名空间的污染。
  • 可以定义私有变量和函数,只暴露必要的公共接口,提高代码的安全性和可维护性。
  1. 请解释JavaScript中的异步编程概念,并且说明Promise相对于回调函数的优点。
  • 答案

  • 异步编程是指在执行耗时操作(如网络请求、文件读取等)时不阻塞后续代码的执行。

  • Promise相对于回调函数的优点:

  • 避免回调地狱(Callback Hell),当有多个嵌套的回调函数时,代码结构会变得非常复杂难读,而Promise可以通过链式调用的方式使代码结构更加清晰。

  • 更好的错误处理机制,Promise可以使用.catch()方法统一处理错误,而回调函数需要在每个回调中单独处理错误。

三、框架相关(以Vue.js为例)

  1. Vue.js中的计算属性(computed properties)和方法(methods)有什么区别?
  • 答案
  • 计算属性是基于它们的依赖进行缓存的。只有当依赖发生变化时,计算属性才会重新计算。
  • 例如,在一个Vue组件中,如果有一个计算属性fullName依赖于firstNamelastName,当firstNamelastName改变时,fullName才会重新计算。
  • 方法则是在每次组件重新渲染时都会被调用,不管依赖是否改变。
  • 例如,在模板中直接调用一个方法来获取某个值,每次渲染都会执行这个方法。
  1. 如何在Vue.js中实现组件间的数据传递(除了props$emit之外)?
  • 答案
  • 使用Vuex进行状态管理。将共享的数据存储在Vuex的store中,组件通过mapStatemapGetters等辅助函数来获取数据,通过mapActionsmapMutations来修改数据。
  • 使用provideinject(在Vue 3中仍然支持这种方式并且更加灵活),祖先组件可以提供数据,后代组件可以注入这些数据,不需要一层一层地传递。
  1. Vue.js中的v - ifv - show有什么区别?
  • 答案

  • v - if是“真正”的条件渲染,它会根据表达式的值在DOM中添加或移除元素。如果初始条件为假,则元素不会被渲染到DOM中;当条件变为真时,才会被渲染。

  • v - show只是切换元素的display属性,元素始终存在于DOM中,根据表达式的值在display: none和默认的display值之间切换。

四、性能优化

  1. 如何优化JavaScript的执行效率?
  • 答案
  • 避免全局查找,在函数内部使用局部变量来缓存全局变量或对象属性的值。
  • 减少DOM操作,尽量将多次DOM操作合并为一次,例如使用DocumentFragment来进行批量DOM更新。
  • 对于循环操作,优化循环条件和循环体,避免在循环内部进行复杂的计算。
  • 使用事件委托来减少事件处理程序的数量,例如将多个子元素的事件处理委托给它们的父元素。

宁王咬了一口的新蛋糕,也是一块硬骨头

2025年2月20日 20:38

出品丨虎嗅汽车组

作者丨李赓

头图丨视觉中国


大规模换电,再次迎来从PPT变成现实的曙光。


前几天,工商信息数据爆出了一个新闻:宁德时代和滴滴新成立了一家名为“时代小桔(厦门)新能源技术有限公司”的合资公司。


公司注册资本为3.3亿元人民币,其中宁德时代持股69.7%,滴滴持股30.3%。法定代表人由宁德时代换电业务总经理、时代电服科技有限公司CEO杨峻(曾任滴滴出行副总裁、小桔车服总经理)担任。业务范围涵盖了新能源汽车换电设施销售、电动汽车充电基础设施运营、蓄电池租赁以及电池销售等多个关键领域。


从表面上看,这只不过是两家“赛道龙头”的合作走向落地,但站在行业视角看,这很有可能会成为新能源换电赛道再次兴盛的“起点”。


严格来说,宁德时代和滴滴的合作并不是新鲜事。


早在去年1月26日,双方已经宣布计划成立换电合资公司,当时更是有滴滴董事长兼CEO程维与宁德时代董事长、总经理曾毓群共同见证。关于官宣计划到正式工商注册的过程发生了什么,双方目前都未透露更多信息。


无论是按照受益大小,还是参考过往在“换电”上的尝试程度,宁德时代大概率是本次合作的“主推”方。


2024年初,宁德时代就已经与滴滴宣布合作,但彼时公司并未注册


宁德时代“死磕”换电已5年


按照宁德时代2024年中报(只有中报和年报有更为细致的运营信息)中的产品和服务营收分类信息,“动力电池系统”、“储能电池系统”两个以成品销售实现收入的类别占到了整体营收的85%。



近几年新能源汽车的快速渗透带来了显著的装车量增长,同时宁德时代在动力电池行业的市占比也稳定在了40~50%的区间,让其净利润不断上升,但宁德时代的下一块蛋糕在哪?


早在2021年初, “中国电动汽车百人会”就曾给出了长期发展视角下,动力电池资产运营的商业模式预测。彼时已经布局了电池生产(宁德时代)和电池回收以及废旧利用(广东邦普)的宁德时代,目标也非常明确——从“电池厂+拆解厂”的单一角色,逐步覆盖其他角色,最终向动力电池服务商和资产管理公司转型。


我们做个简单的估算:按照换电站需要额外20~30%的动力电池,以及相应换电服务费计算,宁德时代假如能够将1/2自己交付的动力电池都纳入资产管理服务,营收保守估计就能增长20~30%,以及增幅只会比营收更大的利润。


最重要的是,这部分收入还不会受当年汽车销量的影响,甚至成为宁德时代在国内动力电池行业竞争和海外拓展阻力下的稳固支撑。


前景足够明确,宁德时代于是在2021年成立了时代电服。后者成立之后一年,就推出了“EVOGO”品牌和一整套换电解决方案,可惜发展并不顺利(截至目前,全国EVOGO换电站仅有数十座,城市覆盖也相对有限,地图中能够查询到的只有厦门、成都、福州、泉州、赤峰、深圳)


最大挑战是宁德时代初代解决方案自身的问题:初代解决方案中,EVOGO将一整块动力电池拆分成26.5度电的“小块”,根据汽车的车型尺寸和里程需求来分别安装1~3块。


虽然看似灵活,但一代电池只有三元锂一种类型,造成单价过于昂贵;过小的电池块也使得换电的流程更加复杂(3块就要单独3次安装);并且同一款车并没有灵活装配的可能性(不同的电池个数和安装会对车辆的重心点和悬架避震位置产生巨大影响,甚至对安全性构成威胁)。初代解决方案落地的2年多时间里,仅有一汽奔腾、东风富康、爱驰进行了适配,完全没能进入规模化进程。


痛定思痛的宁德时代,最终在2024年选择“回炉重造”。在12月中旬召开“巧克力生态大会”,对换电的业务进行了全面的调整:


  • 电池包更换为完整大电池,最小也有42度电,最大有70度电;

  • 敲定电池的订阅费用,按照不同容量和不同单月行驶里程每月收费369~599元;

  • 公布换电站未来的扩张规划,2025年1000座、中期10000座、远期30000座。


另外一个最重要的,是在这次生态大会上,宁德时代还官宣了与长安、广汽、北汽、五菱、一汽等多家车企的合作,现场直接拿出10款合作车型:长安欧尚520、广汽AION S、红旗E-QM5、上汽荣威D7、北汽C66、五菱缤果、五菱星光、上汽非凡F7、上汽大通Mifa 9 和上汽大通大拿。


图自 电动汽车观察家


有趣的是,这些车型中绝大部分都自带“网约车”的标签。在宁德时代“巧克力换电”官方关于长安(就是长安欧尚520)、宁德时代、时代电服的重庆换电项目签约新闻中,甚至也专门强调了“重庆出租车网约车企业与会,并且观看了换电的全过程”。


在本次与滴滴正式合资之前,宁德时代显然已经计划好了通过网约车这个打开新能源汽车市场的“老切口”,再次打开换电赛道的战略。


滴滴,不会是最后一个


根据有关咨询机构数据,中国网约车市场规模早在2023年就已经突破5000亿元,其中网约车司机数量也已经来到750万人。其中2024年上半年滴滴(不包括花小猪)的市占率就能达到73%。


作为目前网约车行业的唯一“超(其他只是“强”)”存在,滴滴显然有帮助宁德时代快速普及换电网约车及解决方案的能力。更关键的是,站在滴滴的视角看,普及换电对于自身同样存在好处。


根据滴滴2024年三季度财报,在网约车供给过剩、其他聚合平台参与价格战的影响下,其出行订单量同比增长10.6%,达到31.8亿单,但中国GTV(核心平台交易总额)只增长了7.8%,也就是单季度客单价继续下滑。


为了避免客单价下滑趋势导致更多“恶性循环”(诸如司机端服务质量问题等)的发生,良性地从发展中挖掘商业价值,滴滴除了优化资深的运营机制,还需要从综合成本上想办法,突破口就在运营车辆端:


  1. 运营车辆为了降低成本,基本只配备了慢速快充或者慢充,换电可以大大减少闲置耗时;

  2. 因为补能,运营车辆当日运营时长受限,导致不必要的资产损耗(闲置时间车辆也有折旧成本)换电可以实现“一车多司机”,从而大幅降低车辆资产损耗成本;

  3. 充电站与电网相连,只有晚间电价便宜,换电可以晚上多补能(车和备用电池一起补能)白天换电,从而降低电价。


在成本之上,通过与宁德时代合作换电,滴滴更可以通过需要初期投入的海量资金和人力的换电站网络(例如专有使用权)和车辆,与网约车司机群体形成更紧密的联系,并且成为滴滴在如今饱和且竞争激烈的网约车行业中新的“护城河”。


可以确定的是,在滴滴和宁德时代接下来进一步探索和落地网约车换电商业前景的过程中,必然会有越来越多的网约车平台尝试加入合作。全国几百万辆网约车的庞大市场,可以推动换电站在全国范围内的普及。


乐观情况下,宁德时代甚至将顺势拥有借助网约车换电网络,向普通消费者普及换电的“关键窗口”。如何达成这个终极目标,将和“海外拓展”一起成为决定宁德时代未来高度的最关键因素。

下载虎嗅APP,第一时间获取深度独到的商业科技资讯,连接更多创新人群与线下活动

H5端向App端通信(Uniapp必会)

2025年2月20日 21:27

大家好,我是大澈!一个喜欢结交朋友、喜欢编程技术和科技前沿的老程序员👨🏻‍💻,关注我,科技未来或许我能帮到你!

今天这篇文章算是分享一段uniapp的坑吧,H5端向App端通信的一种解决实现。

我感觉很有必要和没有遇到过的朋友们分享一下,下次用到的时候可以直接拿下,避免踩坑。

一、重点描述

先说说前置条件,我的App端和H5端都是用uniapp写的,H5端通过webview在App中加载,底层算是webview套webview了。

注意,如果H5端不是用uniapp写的,则考虑使用deeplink与App端进行通信。

再说说需求场景,App端支付成功后,通过第三方链接跳转到H5端支付结果页,然后在H5端结果页点击完成按钮,回到App端订单列表页,完成流程。

此时,如果H5端不与App端通信,就会陷入webview循化嵌套问题。

二、细节剖析

实现原理如图所示:

图片

1、App端接收消息

通过webview组件加载H5页面,在webview组件的message事件中,接收到H5端发送的消息,并做App端的逻辑处理,完成通信接收。

图片

2、H5端发送消息

在H5端页面中,先引入依赖文件:https://gitcode.net/dcloud/uni-app/-/raw/dev/dist/uni.webview.1.5.6.js,再通过postMessage方法发送消息到App端,完成通信发送。

注意,这里如果直接调用uni.postMessage方法时,可能会报错。所以先引入依赖文件再调用会比较可靠,避免踩坑。

图片

好了,今天要分享的内容就是这么多,最后感谢朋友们给个点赞、分享、推荐,拜拜~

力合科技:国科瑞华拟减持不超4.86%公司股份

2025年2月20日 20:57
36氪获悉,力合科技发布公告,持股4.86%的股东国科瑞华创业投资企业(简称“国科瑞华”)计划3个交易日后的3个月内以集中竞价交易方式减持公司股份不超过1151万股,即减持比例不超过公司总股本的4.86%。

杭钢股份:股东诚通金控拟参与换购证券投资基金份额计划

2025年2月20日 20:50
36氪获悉,杭钢股份发布公告,持股12.2%的股东诚通金控拟于2025年2月21日起15个交易日后的3个月内将持有的本公司部分股份参与换购中证500交易型开放式指数证券投资基金份额。本次换购股份来源为以无偿划转方式取得的公司A股股份,换购价格根据市场价格确定。诚通金控拟参与换购的股份不超过公司股份总数的1%,即不超过3377.19万股。
❌
❌