前言
在官方文档中有这样的描述:用于构建用户界面的 JavaScript 库。在武侠中,侠客都会有兵器,有的拿剑,有的拿刀。在我们前端中也一样,有的手中Vue行天下,而我们是腰配React仗天涯。我们想让自己的兵器有杀伤力,就得及时多多磨剑,了解自己的武器,你对了解你的武器(React)吗?
我的400行React代码地址:
React是如何构建用户界面的?
- 通过虚拟DOM,下面我们通过几行简短的代码来看怎么通过jsx来构建一个真实DOM
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.bootcdn.net/ajax/libs/babel-standalone/7.0.0-beta.3/babel.js"></script>
<title>Document</title>
</head>
<body>
<div id="root"></div>
<script>
// babel在编译时候会调用React.createElement方法
// 通过createElement定义虚拟DOM
const createElement = (type, props, ...children) => {
return {
type,
props,
children
}
}
window.React = { createElement }
</script>
<script type="text/babel">
// 创建一个虚拟DOM
const element = <ul>
<li>1</li>
<li><b>2</b></li>
<li>3</li>
</ul>
console.log(element); // 虚拟dom
// 虚拟DOM渲染函数
function render(element, root) {
if (typeof element === "string" || typeof element === "number") {
const text = document.createTextNode(element)
root.appendChild(text)
} else if (typeof element === 'object') {
const { type, children } = element;
const dom = document.createElement(type)
children && children.forEach((ele) => {
render(ele, dom)
})
root.appendChild(dom)
}
}
render(element, root)
</script>
</body>
</html>
上面,我们用了一个简单的递归方式构建出了一个界面,但是它有什么缺点吗? 答案:有,我们知道我们一个页面是单线程的方式运行的,如果当jsx比较大,层次比较深时,会影响用户的交互。我们先看一张图
浏览器通常是以帧的方式去绘制页面,如果当我们的js运行时间过长,超过了帧时长,我们会有明显的卡顿感,甚至还无法完成用户交互,所以为了优化这种渲染方式,React采用了Fiber架构。
- 什么是fiber? ReactFiber其实是一种数据结构,我们可以理解一个Fiber节点对应一个React节点。fiber是react中最小的执行单元Unit
我们发现在fiber中有三根指针,return指向父节点,child指向子节点,sibling指向兄弟节点
其中还有2个节点alternate和stateNode两个指针,stateNode指向当前fiber对应的真实dom,alternate指向的另一颗fiber树的对应fiber节点(下面会介绍)。
这样做有什么好处呢?
避免递归,把执行变成一个异步可中断的dom更新。
是如何实现的呢?
let nextUnitWork
let currentTime = new Date().getTime()
// 判断当前是否有时间执行 有则返回true 没有就返回false
function shouldYield() {
if (new Date().getTime() - currentTime > 60) {
currentTime = new Date().getTime()
return false
} else {
return true
}
}
// 执行工作单元
function performUnitOfWork() {
// 做一些操作
// 操作完后将下一个单元交给nextUnitWork
nextUnitWork = nextUnitWork.nextFiber
}
function workLoopConcurrent() {
while (nextUnitWork !== null && !shouldYield()) {
performUnitOfWork(nextUnitWork);
}
// 如果还存在需要执行的单元,返回该函数
if (nextUnitWork) {
return workLoopConcurrent
} else {
null
}
}
// 开始工作 只能工作60ms
function work(loop) {
const fn = loop()
if (typeof fn === 'function') {
setTimeout(() => {
work()
}, 30)
}
}
work(workLoopConcurrent)
我们看下当我们使用异步可中断的更新后,React的调用栈
在React官网中,有这样一句话:我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式,我们可以看到React在快速响应上做出的努力。
既然我们可以渲染出DOM,怎么更新DOM呢?
双缓存
在React中存在两颗Fiber树,分别是currentFiber和workInprogress Fiber两棵树,currentFiber表示当前页面的fiber树,workInprogress表示更新时,新生成的fiber树。他们各自的fiber节点通过alternate链接。
在mount时,我们会构建fiber树,并通过fiber树生成节点,再提交时,将该fiber树变成currentFiber。
在update时,我们会同样新生成一颗workInprogress fiber树,它根据diff算法来区分是否可以复用该fiber,精确到我们只操作变了的dom部分。
将碎片化的知识整理一下React的工作流程
render阶段 创建根fiber 遍历element生成fiber树 并收集EffectTag
commit阶段 根据EffectTag更新dom 执行生命周期 将currnetRoot指向工作树
如何学习React源码
- 1.首先得熟悉使用React库,了解React的基本概念
- 2.了解一些基础的数据结构算法,如链表,最小堆,二进制运算
- 3.避免直接入手源码,可以按照某一个功能点去看源码
- 4.下载React代码,并手动针对特定的功能模块调试
- 5.推荐学习文档:卡松React技术揭秘:react.iamkasong.com/