首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >为什么你3年React还是不懂JavaScript?|从框架的舒适区逃离

为什么你3年React还是不懂JavaScript?|从框架的舒适区逃离

作者头像
前端达人
发布2026-05-11 20:09:16
发布2026-05-11 20:09:16
990
举报
文章被收录于专栏:前端达人前端达人

我见过太多这样的故事。

一个朋友,用了4年React。某个下午,他跟我说了个bug,我听完差点没绷住——

代码语言:javascript
复制
// 他的代码
const [loading, setLoading] = useState(false);

const handleClick = () => {
  setLoading(true);
  console.log(loading); // 他期望是true,结果是false
  
  fetch('/api/data')
    .then(res => res.json())
    .then(data => {
      setLoading(false);
    });
};

他说:"为什么setState之后立马log,值还是旧的?是不是React有bug?"

我问他:"你有没有想过,也许不是React的问题,而是你对JavaScript本身的理解有gap?"

他沉默了。

然后说出了最扎心的话:"我这4年,其实就是在学React怎么用,从来没想过为什么会这样。"

还有个更夸张的。

一个3年经验的开发者,在生产环境发现一个bug——页面加载时会闪烁两次。他折腾了三天,最后是我随口问了句:"你是不是在useEffect里直接改state超过一次?"

他翻出代码一看:

代码语言:javascript
复制
// useEffect里的代码
useEffect(() => {
  const data = fetchUserData();
  setUser(data);  // 这里setUser
  setLoading(false); // 这里又setLoading
  
  // 结果触发了三次render:初始 → setUser → setLoading
}, []);

他说:"我从来不知道一个effect里调用多个setState会这样。"

我说:"这就是问题所在啊——你不知道你的代码执行顺序。"

或者这个例子——我见过一个人,用了5年React和Redux,有天被问:"说说闭包在Hooks里的作用。"

他支支吾吾地说:"那个……就是……能记住状态?"

我接着问:"那为什么你的useCallback依赖数组里不加某个变量,代码就会bug?"

他又卡住了。最后承认:"我其实就是靠着ESLint提示来填依赖数组,从来没想过为什么。"


听了太多这样的故事后,我开始问自己:为什么框架开发者普遍存在这个问题?

答案很残酷——因为框架做得太好了。好到你可以不懂JavaScript也能写代码。

【坦白】我也是这样的人

我坦白说,我曾经就是这种人。

用React 5年,却在某次技术分享时被问"你理解事件循环吗?"时彻底卡壳。

我能用async/await,但说不清为什么Promise的.then会先于setTimeout执行。

我知道useCallback的依赖数组很重要,但只是因为ESLint警告,而不是真的懂为什么。

最尴尬的是,有个前端新手问我:"为什么setState之后console.log拿到的还是旧值?"

我当时的回答是:"哦,React的setState是异步的……"

但当他继续问"为什么异步"时,我就结巴了。因为我从未真正思考过这个问题。

那一刻我意识到——我可能根本不懂JavaScript,只是会用React。

【现象】框架隐藏的东西

框架最厉害的地方,也最危险的地方,就是它把JavaScript的复杂性完全隐藏了。

你看不到状态怎么被记住。你看不到UI怎么被更新。你看不到为什么有些代码会按这样的顺序执行。

你只看到代码能工作。

坑1:setState的"诡异"行为

这可能是最困扰新手的:

代码语言:javascript
复制
const [count, setCount] = useState(0);

const handleClick = () => {
  setCount(1);
  console.log(count); // 期望1,实际0
  
  setCount(2);
  console.log(count); // 期望2,实际还是0
};

有人会说"React的setState是异步的"。但这个解释不完全。真正的原因是——闭包

当handleClick被创建时,它的闭包里捕获了count这个变量。无论调用多少次setCount,这个闭包里的count还是初始值。

但即使理解了这个,新手还是会困惑:"那什么时候count才会更新?"

答案是:"下一次render时"。

但这需要理解——setState触发renderrender才会重新执行你的组件函数,这时候闭包里才会是新的count。

坑2:useEffect的依赖数组地狱

无数人被这个坑过:

代码语言:javascript
复制
useEffect(() => {
  console.log('userId:', userId);
  
  fetch(`/api/user/${userId}`)
    .then(setUser);
}, []); // 依赖数组为空

// userId改变时,fetch还是用的旧userId

ESLint会警告你"missing dependency"。你加上userId:

代码语言:javascript
复制
useEffect(() => {
  // ...
}, [userId]);

// 现在userId改变时,effect会重新运行,没问题了

但有的人就此停止思考。他们不知道为什么需要加userId。

答案是闭包。effect的回调函数形成的闭包,会捕获该scope里的变量。如果没加依赖项,闭包里的值永远不会更新。

坑3:异步代码的竟态条件

这是个生产bug的温床:

代码语言:javascript
复制
const [results, setResults] = useState([]);

const handleSearch = (query) => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(data => setResults(data));
};

// 用户快速搜索:"a" → "ab" → "abc"
// 三个请求同时发出
// 但返回顺序可能是 response 3 → response 1 → response 2
// 最后显示的是response 2的过时数据,用户看到的搜索结果是错的

理解这个问题需要知道:

  1. Promise和事件循环——为什么这些请求会同时发出
  2. 竟态条件——为什么返回顺序不保证
  3. 异步流程控制——怎样确保只处理最新的请求

但在React里,你可能永远遇不到这个问题——因为你会直接用react-query或SWR这样的库。

库帮你处理了,所以你不需要理解。

【原因】为什么会这样

1. 学习曲线的陷阱

代码语言:javascript
复制
学习难度 ▲
        │
      5 │         ╱─── React高级特性
        │        ╱
      4 │       ╱
        │      ╱  ← 你在这就能写出工作代码
      3 │     ╱
        │    ╱
      2 │   ╱
        │  ╱
      1 │ ─────── JavaScript基础
        │
        └──────────────────────► 时间

你用一周可以学到足够的React知识,写出能工作的应用。

但学JavaScript基础要花三个月。

所以大多数人选择——继续用React,假装不知道下面发生了什么。

反正能工作。反正有ESLint。反正有Stack Overflow。

2. 框架就是为了隐藏复杂性

这是框架的本质。比如状态管理:

在原生JavaScript里,你要自己做:

代码语言:javascript
复制
let count = 0;

const increment = () => {
  count++;
  render(); // 还要手动重新渲染
};

function render() {
  document.querySelector('#count').textContent = count;
}

在React里,一行代码搞定:

代码语言:javascript
复制
const [count, setCount] = useState(0);
// React自动处理:状态更新、render触发、DOM更新

这太棒了。但代价是你永远看不到"状态更新怎么触发DOM更新"这个过程。

3. 工作中没人逼你思考

实际工作中,没人会问你"为什么"。只要代码工作就行。

代码语言:javascript
复制
领导:这个bug怎么修?
你:看看代码……可能改这里试试?
结果:好了
领导:很好,继续

没有人追究为什么会有这个bug。没有人要求你理解根本原因。

久而久之,你就习惯了"碰巧解决问题"这种开发方式。

【转折】我的觉醒时刻

对我来说,转折点是这样的:

某个周三下午,一个同事来问我一个问题。他在一个useEffect里写了这样的代码:

代码语言:javascript
复制
useEffect(() => {
  // 这样对吗?
  const fetchData = async () => {
    const res = await fetch('/api/data');
    const data = await res.json();
    setData(data);
  };
  
  fetchData();
}, []);

他问我:"为什么不能直接在useEffect里写async?"

我那时的回答是:"因为……useEffect的return应该是cleanup函数,不能是Promise……"

但他继续问:"为什么?"

我支支吾吾。

那一刻我意识到——我知道规则,但根本不知道为什么有这条规则

这触发了我的危机感。

那个周末,我决定做一个实验。我要搞清楚——为什么useEffect不能直接async?为什么setState看起来不同步?为什么闭包这么重要?

我的方法很极端——用纯JavaScript重写一个小项目,不用任何框架

【实验】30天没有框架的日子

我选了一个简单但有意思的项目——一个待办应用。有状态管理、异步保存、实时搜索,没有任何框架。

第一周:状态管理的残酷真相

一开始,我想:状态管理有那么难吗?

代码语言:javascript
复制
const state = {
todos: [],
filter: 'all'
};

// 添加todo
const addButton = document.querySelector('#add');
addButton.addEventListener('click', () => {
const input = document.querySelector('#input');
  state.todos.push(input.value);

// 现在需要更新UI
// 怎么更新?
});

问题立即出现——有多个地方会修改状态,每个地方都要记得更新UI。这很容易遗漏。

我花了一天时间写了一个简单的观察者模式:

代码语言:javascript
复制
class StateManager {
constructor(initialState) {
    this.state = initialState;
    this.listeners = [];
  }

  setState(updates) {
    this.state = { ...this.state, ...updates };
    this.listeners.forEach(fn => fn());
  }

  subscribe(fn) {
    this.listeners.push(fn);
  }
}

写完后,我突然get到了——Redux或Zustand就是这个原理!

这一个小小的StateManager,让我理解了为什么状态管理库那么重要。

第二周:闭包终于活了

实现"编辑todo"功能时,我踩到了经典的闭包坑:

代码语言:javascript
复制
state.todos.forEach((todo, index) => {
  const editBtn = document.createElement('button');
  editBtn.textContent = '编辑';
  
  editBtn.addEventListener('click', () => {
    console.log(`编辑第${index}个`); // 问题:这输出的总是最后一个index
  });
  
  li.appendChild(editBtn);
});

我Google了解决方案,学到了用let代替var。但知道解决方案不是真的理解。

我继续挖掘。我问自己:为什么let就行了?

答案是块级作用域。每个let声明都会创建新的作用域。但这还是太抽象。

直到我写了这样的代码:

代码语言:javascript
复制
function makeEditHandler(todoId) {
  return function() {
    console.log(`编辑todo ${todoId}`);
  };
}

state.todos.forEach((todo, i) => {
  const editBtn = document.createElement('button');
  editBtn.addEventListener('click', makeEditHandler(todo.id));
});

我才真正理解——闭包就是函数"记住"了它被创建时的上下文

在这个例子里,makeEditHandler返回的函数"记住"了todoId。即使makeEditHandler执行完了,返回的函数还能访问todoId。

那一刻,我突然想到React Hooks:

代码语言:javascript
复制
function useState(initialValue) {
let state = initialValue;

return [
    state,
    function setState(newValue) {
      state = newValue; // 这个setState会"记住"这个特定的state变量
    }
  ];
}

// 每次调用useState,都会创建新的闭包
const [count, setCount] = useState(0); // 这个setCount的闭包里的state是count
const [name, setName] = useState('');  // 这个setName的闭包里的state是name

哦天哪。这就是为什么Hooks不能改变调用顺序

如果改变了顺序,setCount可能会改变name的state,因为闭包捕获的是"第一个useState的返回值",不管那个useState里存的是什么。

这一个顿悟,让我理解了为什么useCallback的依赖数组那么重要、为什么useEffect的依赖数组那么重要。

第三周:事件循环不再神秘

当我要实现"自动保存"功能时,事件循环的知识变得必需:

代码语言:javascript
复制
input.addEventListener('change', () => {
  const version = ++requestVersion;
  
  fetch('/api/todos', { method: 'POST', body: JSON.stringify(state.todos) })
    .then(res => res.json())
    .then(data => {
      // 问题1:如果有多个fetch,返回顺序可能不同
      // 问题2:数据怎样才能及时更新
      // 问题3:用户快速操作时会发生什么
      setTodos(data);
    });
});

我遇到的第一个问题是——执行顺序不对

代码语言:javascript
复制
console.log('1');
fetch('/api').then(() => console.log('2'));
console.log('3');

// 输出是 1 → 3 → 2,不是 1 → 2 → 3

这时我才真正理解了事件循环。JavaScript的异步不是什么魔法,而是:

  1. 同步代码先执行
  2. 然后检查微任务队列(Promise的.then)
  3. 然后执行宏任务(setTimeout)
  4. 重复
代码语言:javascript
复制
console.log('1');

setTimeout(() => {
console.log('setTimeout');
}, 0);

Promise.resolve().then(() => {
console.log('promise');
});

console.log('2');

// 输出:1 → 2 → promise → setTimeout
// 因为Promise是微任务,setTimeout是宏任务
// 微任务总是在宏任务之前执行

第二个问题是竟态条件:

代码语言:javascript
复制
// 用户快速改三个值,触发三个fetch
// request 1, 2, 3同时发出
// 但response可能这样返回:3先到 → 2后到 → 1最后到
// 结果UI会显示过时的数据

// 解决方案:添加版本号
let latestVersion = 0;

input.addEventListener('change', () => {
const version = ++latestVersion;

  fetch('/api/save', {...})
    .then(res => res.json())
    .then(data => {
      if (version === latestVersion) {
        // 只处理最新的请求
        updateUI(data);
      }
    });
});

这个版本号模式,我见过在React中以不同形式出现——cancelToken、AbortController等。

但如果我没有经历过这个问题,我永远不会理解为什么需要这些机制。

【结果】30天后的变化

30天后,我回到React。代码看起来完全不同。

同样的useEffect代码:

代码语言:javascript
复制
useEffect(() => {
let mounted = true;

const fetchData = async () => {
    const res = await fetch('/api/data');
    const data = await res.json();
    
    if (mounted) {
      setData(data);
    }
  };

  fetchData();

return() => { mounted = false; };
}, []);

现在我知道这段代码在干什么:

  1. useEffect的return是cleanup函数(异步不行,所以要包裹在IIFE里)
  2. mounted标志防止卸载后的setState(这是竟态条件的一种)
  3. 依赖数组为空表示只在挂载时执行

这不是记住的规则,而是我理解的原理

最关键的改变是——我能debug了。

以前遇到bug,我会:

  1. Google
  2. 试试改改参数
  3. 碰巧工作了

现在遇到bug,我会:

  1. 想想是闭包问题?还是事件循环问题?
  2. 针对性地检查代码
  3. 准确定位原因
  4. 修复

【对比】框架和现实

30天的原生JavaScript,和5年的React,让我看清了两者:

代码语言:javascript
复制
           React              原生JavaScript
       ────────────────────────────────────
开发速度    超快                很慢
学习难度    中等                高
代码量      少                  多
性能        好                  取决于你
理解深度    浅                  深

对初学者:React让你快速建立成就感
对老手:原生JavaScript让你真正懂编程

但更重要的是——现在我知道React在做什么了。

useState = 闭包 + 状态存储 + render触发 useEffect = 闭包 + 生命周期 + cleanup useCallback = 闭包 + 依赖比较

框架没有魔法。只有JavaScript。

【现实】你需要知道的

如果你也想改变这个现状,不需要像我那样极端。但你要有意识地学。

闭包

这是最重要的。理解闭包,你就理解了Hooks的核心。

代码语言:javascript
复制
// React Hooks的工作原理简化版
let componentHooks = [];
let hookIndex = 0;

function useState(initialValue) {
const currentIndex = hookIndex++;

if (!componentHooks[currentIndex]) {
    componentHooks[currentIndex] = initialValue;
  }

const setState = (newValue) => {
    componentHooks[currentIndex] = newValue;
    reRender();
  };

return [componentHooks[currentIndex], setState];
}

// 现在你明白了:
// 1. 为什么不能在条件里调用Hooks(会导致hookIndex错误)
// 2. 为什么不能改变调用顺序(顺序就是mapping关键)
// 3. 为什么setCount能记住count(闭包)

事件循环

理解这个,你就能debug异步bug。

代码语言:javascript
复制
console.log('sync 1');

setTimeout(() => console.log('timeout'), 0);
Promise.resolve().then(() => console.log('promise'));

console.log('sync 2');

// 思考为什么这个顺序是 sync 1 → sync 2 → promise → timeout

异步流程控制

竟态条件是常见bug的根源。理解这个,你能写出健壮的代码。

代码语言:javascript
复制
// ❌ 容易出bug
const handleSearch = (query) => {
  fetch(`/api/search?q=${query}`)
    .then(res => res.json())
    .then(setResults); // 如果有多个请求,返回顺序不同就buggy
};

// ✅ 正确
const handleSearch = (() => {
let latestQuery = null;

return(query) => {
    latestQuery = query;
    fetch(`/api/search?q=${query}`)
      .then(res => res.json())
      .then(data => {
        if (latestQuery === query) {
          setResults(data);
        }
      });
  };
})();

【建议】怎么开始

不一定要像我那样花30天。但你可以:

1. 找个小项目用原生JavaScript实现

不要太简单(hello world没用),不要太复杂(会失败)。

一个待办应用就很好。

2. 遇到问题时停下来思考

不要马上Google。问自己:

  • 为什么会这样?
  • JavaScript在这里做什么?
  • 这和事件循环有关吗?
  • 这是闭包导致的吗?

3. 对比两种实现

原生版本100行,React版本30行。

想想React为你隐藏了什么复杂性。

4. 读一点框架源码

不用读全部。但看看useState是怎么实现的,你会get到闭包的威力。

5. 最关键的:改变你的学习态度

从"怎么用"改成"为什么这样"。

从"代码能工作"改成"我知道为什么能工作"。

【最后】

如果你现在有3-5年框架经验,但对基础有些模糊……

不是你的问题。是框架成功的代价。

但你完全可以改变。

可能不需要30天。可能只需要5天、一周、一个月。

关键是有意识地去学JavaScript的基础。

一旦你理解了——

闭包不再是"函数能访问外层变量"这种抽象定义。

事件循环不再是"微任务比宏任务先执行"这种记忆。

异步不再是"奇怪的执行顺序"。

它们都变成——"啊,原来如此"。

那一刻,你写的代码会不一样。你debug的方式会不一样。你对编程的理解会不一样。

你会从"会用框架的工程师",变成"真正懂编程的开发者"。

你的故事

我想听你的故事。

在你的React/Vue生涯中,有没有那么一个时刻,你意识到自己好像不懂某个基础概念?

或者——有没有一个bug,因为不理解基础,折腾了很久才解决?

分享在评论区。

我很好奇,有多少人有过我这样的经历。

有多少人,也在框架的舒适区里,假装不知道下面发生了什么。

也许,一个故事,就能改变一个人的学习路径。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端达人 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 【坦白】我也是这样的人
  • 【现象】框架隐藏的东西
    • 坑1:setState的"诡异"行为
    • 坑2:useEffect的依赖数组地狱
    • 坑3:异步代码的竟态条件
  • 【原因】为什么会这样
    • 1. 学习曲线的陷阱
    • 2. 框架就是为了隐藏复杂性
    • 3. 工作中没人逼你思考
  • 【转折】我的觉醒时刻
  • 【实验】30天没有框架的日子
    • 第一周:状态管理的残酷真相
    • 第二周:闭包终于活了
    • 第三周:事件循环不再神秘
  • 【结果】30天后的变化
  • 【对比】框架和现实
  • 【现实】你需要知道的
    • 闭包
    • 事件循环
    • 异步流程控制
  • 【建议】怎么开始
    • 1. 找个小项目用原生JavaScript实现
    • 2. 遇到问题时停下来思考
    • 3. 对比两种实现
    • 4. 读一点框架源码
    • 5. 最关键的:改变你的学习态度
  • 【最后】
  • 你的故事
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档