本文使用简单直接的方式讲述React设计思想,适合首次接触React的初学者粗略了解大致的运行机制,但和React实际代码实现有一些出入,主要为后续深入了解细节做个铺垫
1️⃣ React诞生背景
前后端混合时代
在网站开发早期,由后端接收浏览器回传的url参数或表单等信息,在服务端计算数据和渲染模板,返回新的html,由浏览器整页刷新后展示
网站后端典型技术有jsp、php、asp.net等
ajax时代
为了解决上面整页刷新在用户体验上的劣势,开发者们发明了ajax技术进行前后端分离开发,可以在浏览器中使用js调用后端接口数据,动态替换网页局部内容,实现页面无刷新局部更新,在用户体验上有了质的飞跃
ajax技术需要开发者为每个需要更新的区域都编写一遍代码,用以响应用户交互、再触发网络请求、处理响应数据、替换网页内容
前端典型技术有jQuery
var count = 0
$('#button').click(function () {
count++;
$.get({url: './api?count='+count,
success: function (response) {
$('#result').html(response.body);
}
});
})
mvvm时代
为了提高开发效率和支持复杂交互应用的开发,前端领域进入响应式技术支撑的数据驱动视图的时代
由前端框架负责数据变化后对应的网页内容同步逻辑,开发重心放在处理用户交互和数据变化后执行的业务操作,不需要在每次数据更新后再手动更新dom
典型框架有React、Vue、Angular
2️⃣ React核心思想
首先走的是数据驱动视图的路线,在实现上本质是在数据变化前后会有两份不同的html,把新旧html做对比,得到变化的部分,更新到页面对应的元素里,实现UX、DX的双重提升
react很多机制都是围绕着这个最基本的思想进行构建的
下面将介绍react各种机制为何而来并且解决了什么问题
3️⃣ jsx与虚拟dom
react引入虚拟dom绝对是引领前端开发潮流的重要技术
前面提到react本质就是对两份html做diff,由于框架需要以运行代码的形式处理html结构做diff,那么这个步骤的耗时对框架性能有决定性作用
在浏览器中得到两份html,最直接最简单的思路就是用js+模板构建一份新的全量dom,但是这样性能损耗较多
所以react团队使用jsx替代传统的字符串式html模板,再利用编译器对jsx进行转换,得到能体现html结构的轻量数据,即虚拟dom,便于在js中进行对比,比操作两份不同的dom树速度较快
// jsx
<div>
hello world
<Counter count={1} />
<button>click</button>
</div>
// 编译后,得到类似json的轻量数据
_jsxs("div", {
children: [
"hello world",
_jsx(Count, {
count: 1
}),
_jsx("button", {
children: "click"
})
]
});
4️⃣ 状态与组件
既然是数据驱动视图,那么响应数据变化是必不可少的一步,要响应首先要能感知到变化
react中采用的感知方式是使用useState函数创建状态变量和一个专门用于更新变量的函数,把修改变量赋值的代码统一调用这个专用函数来实现感知
function CountComponent(){
// count是变量的当前值
// 使用setCount函数更新值,能让框架感知到该变量发生了变动
const [count,setCount] = useState(0);
setCount(1);
return (
<div>
hello world {count}
<button onClick={setCount(prev=>prev++)}>click</button>
</div>
)
}
在组件初始化时,由react框架根据jsx模板生成虚拟dom,再把虚拟dom渲染出真实的html元素,加载到浏览器中展示
只有调用setCount这个由react返回的函数,react才能知道数据发生变化了,然后才会触发再次渲染的动作
5️⃣ 异步运行
响应数据变化后面一系列的动作都是始于setState函数,如果使用同步立即执行重新生成虚拟dom、对比虚拟dom和更新真实dom等诸多代码,会造成页面卡顿
因为浏览器这个软件设计本身的限制,js和ui运行在同一个线程中,会互相抢占运行时间,同时只能运行一种任务,如果js代码运行时间过长,会阻塞页面的展示与交互,直观感受就是网页卡住了,影响用户使用体验
浏览器对js执行和ui更新是交替执行,只要js连续运行时间不长,就有时间刷新ui,降低卡顿的感受
所以在执行setState函数后,不会立即执行后续动作,而是生成一个任务存储在队列中,交给调度器决定在什么时候启动队列,什么时候停止队列
由于不是立即执行后续操作,所以叫做异步运行,该设计是为了缓解js连续执行时间过长导致的网页卡顿问题
在执行中进行计时,如果js连续运行时间过长超过一定阈值,则停止任务执行,由调度器使用MessageChannel回调事件、requestIdleCallback等方式,在ui更新后再继续执行
再加上批处理更新机制,这导致了使用setState后,更新变量这个任务还没执行,立即访问state变量还是旧值,这就是react设计本身带来的额外特性
6️⃣ fiber与协调
状态变化后最简单直接的实现方式是从头到尾生成一边全量虚拟dom,再整个对比,得到变化的部分,并将这些变化更新到浏览器的真实 DOM 中
但这样性能损耗很大,更好的实现是可以识别到是哪个组件里面触发了状态更新,随后只针对该组件重新生成虚拟dom等,可以显著减少计算量提高执行速度
fiber就是为了能避免不必要的计算,将诸多必要信息进行整合的数据结构。把虚拟dom、组件状态、props属性、真实dom等等必要的信息打包到一个数据对象中
这样在协调这个步骤就可以通过某个变化的状态可以找到对应哪个组件,和这个组件对应虚拟dom的范围,只对这个组件范围内的虚拟dom进行重新生成和对比差异,避免了从头至尾的全量计算
7️⃣ useState与useMemo
在每次协调中,涉及到更新的组件都会再次运行一遍生成组件的方法,上面的代码中在方法内使用useState定义了状态变量,那么再生成一次不就把状态重置了吗?
react巧妙的利用了js单线程的机制,对状态进行了缓存,在执行useState时,按顺序访问当前组件对应索引的缓存,如果找到缓存的状态则直接返回该状态,否则创建一个新状态并加到缓存中,这样就实现了组件函数重新运行,但不会丢失之前状态的效果
这也是useState只能用在组件顶层代码并且不能有条件的创建状态的原因,因为组件每次更新都要保证与初始化时执行useState的顺序一致
useMemo也同理,如果useMemo中使用的状态没有发生变化,那么useMemo也会复用之前的缓存,不会再次执行