参考16.x的版本
2个包的作用
import React from './react';
import ReactDOM from './react-dom';
- 每次写的时候 都会默认引入这两个包,
ReactDOM
这个是主要提供了render方法,吧react元素挂载到页面中。React
则是核心的包,大部分react api 都源于他,比如createElement Component createRef createContext 等
jsx
- jsx:把一种js和html混合的语法,jsx是语法糖 会通过babel转义成
React.createElement
语法(16版本之前 React 必须要引入就这个原因) - jsx 语法通过 babel转译成js
babel-loader
@babel/core
@babel/preset-react
(预设,他负责把html标签转成js代码)
<div>123</div>
babel 转义后 ===>
React.createElement("div", null, "123");
createElement
- React.createElement将编译后的jsx语法创建组件,接收三个参数
- 第一个是组件或者标签
- 第二个是标签的配置对象id、style等
- 第三个是及其后面的都是children,可能是一个对象,也可能是一个数组
- 静态属性 isReactComponent 是用来用区别函数组件和类组件,在render 里面用到的
- 伪代码
function createElement(type,config,children){
let propName;
const props = {};
for(propName in config){
props[propName] = config[propName]
}
const childrenLength = arguments.length - 2//看看有几个儿子
if(childrenLength === 1){
props.children = children;// props.children就是一个普通的对象
}else if(childrenLength >1){// 如果说儿子数量大于1的话 props.children就是一个数组
props.children = Array.from(arguments).slice(2);
}
return {type,props}
}
class Component{
static isReactComponent = true
constructor(props){
this.props = props
}
}
export default {
createElement,
Component
}
render
- render函数就是将
React.createElement
创建的虚拟DOM转化成真实DOM挂载到第二个参数上。核心就就是createDOM
函数 createDOM
会根据传进的元素进行分类普通文本 class 原生DOM function
根据不同的类型分别进行处理,最后都会返回处理后的新DOM进行返回,然后挂载到root上。对于react多数组,同一级别的元素react都会进行打平操作
// 打平 最后会打平成这个样子
{[1],[2],[3,[4]]} ==={[1,2,3,4]}
渲染
- 主要解析要渲染的对象:
原生js class fnction
React.createElement
用来创建虚拟DOM, 他主要对元素进行分类,分成原生js class fnction
,最后包装成一个对象返回,如果儿子节点是多层的情况 还会进行递归处理。
import React from './react';
import ReactDOM from './react-dom';
function FunctionCounter(props){
return React.createElement('div', {id: 'counter'}, 'hello2','123');
}
class ClassComponent extends React.Component{
render() {
return React.createElement(FunctionCounter, {id: 'counter'}, 'hello1');
}
}
ReactDOM.render(
elm,
document.getElementById('root')
);
babel 编译后的样子
let onClick1 = (event) => {
console.log('onclick1',event);
}
let onClick = (event) => {
console.log('onclick', event);
event.persist();
setInterval(()=>{
console.log('-----------------------------------')
},1000)
}
// js里面变量名基本上是驼峰
// 写法
let elm = React.createElement('div',
{
id:'sayHello',
onClick,
style:{
with:'100px',
height:'100px',
border:'1px solid red'
}
},
'div',
React.createElement('section', {
onClick:onClick1,
style: {
color: 'red',
with:'50px',
height:'50px',
border:'1px solid green'
}},
'section')
)
合成事件
- 1、因为合成事件可以屏蔽浏览器的差异,不同浏览器绑定事件和触发事件的方法不一样
- 2、合成阔以实现事件对象的复用,重用,减少垃圾回收,提高性能
- 3、因为默认我要实现批量更新 setState, setState 两个SetState 合并成一次更新,这个也是合成事件中实现的
事件流程,所有的事件都是冒泡到 document,进行统一管理
- 比如 click 事件触发的时候,他会从目标源到document进行遍历,获取每一个需要处理的元素绑定了click事件函数,进行触发
- event persist 作用是event事件持久化,默认情况下 合成event 在函数执行完成后, 合成event 里面的数据会被指向null。persist作用是让当前的合成的event继续存在,实现就是在当前函数执行的时候执行 event.persist(),他会改变内部 合成event 指向,等待事件遍历完成,去清除合成event时候,只是处理改变后的,之前的合成event并没有被清除
- 处理批量更新,在处理事件的之前就开始 批量更新模式,再去处理函数里面的setState,等所有的事件函数都执行完成后,在关闭批量更新模式,对页面更新
setState
- 里面有个变量控制 批量更新,默认是开启批量更新。
- 在事件处理中,同步情况下,每次调用
setState
他都会把状态保存起来,然后尝试类组件更新。- 如果当前还是处于批量更新的情况,把自己放到更新队列中。多次调用情况下, 会一直走这里 请数据保存起来
- 如果当前不是批量更新,那么就去更新,去执行
shouldUpdate
更新钩子逻辑,在对组件进行forceUpdate
强制更新
- 类组件 阔以调用
forceUpdate
对组件 进行强制更新。 import { unstable_batchedUpdates } from './react-dom'
强制更新unstable_batchedUpdates
逻辑很简单,强制开始批量更新,执行传递进来的逻辑,在关闭批量更新,调用队列更新,去更新组件
forceUpdate
- 每次setState执行完成组件尝试去更新,首先判断
shouldUpdate
钩子是否支持,如果支持才调用forceUpdate
执行强制更新。另外我们可以通过类组件直接调用forceUpdate
进行强制更新 - 在这里 会走
componentWillMount
&getSnapshotBeforeUpdate
&render
&componentDidUpdate
钩子一次执行, render之后, 拿新老dom比较更新
diff
- 1、新的元素没有、type不一样、文本不一样直接进行替换
- 2、如果都是类和函数组件,他们会再次进入循环体进行dom比较
- 3、核心在两个type相同的native元素进行比较 以下面例子比较
A B C D
和A C B E F
- 下面有2点 深度优先 第一点是 diffQueue 收集,对需要操作的dom进行搜集
- 第二点 patch 对dom进行统一处理
// 1、patch 打印diffQueue 需要操作的dom
// [
// {
// "parentNode": {
// "eventStore": {}
// },
// "type": "MOVE",
// "fromIndex": 1,
// "toIndex": 2
// },
// {
// "parentNode": {
// "eventStore": {}
// },
// "type": "INSERT",
// "toIndex": 3,
// "dom": {}
// },
// {
// "parentNode": {
// "eventStore": {}
// },
// "type": "INSERT",
// "toIndex": 4,
// "dom": {}
// },
// {
// "parentNode": {
// "eventStore": {}
// },
// "type": "REMOVE",
// "fromIndex": 3
// }
// ]
// 2、下面是具体的4个变化的dom
// a、首先匹配A情况 都是一样的直接赋用老元素
// b、查看新元素里面的C,在老元素有对应的 并且赋用老元素的位子lastIndex(2)大于当前挂载的位子mountIndex(1),即同样不用操作
// c、查看B元素,去老的元素找到同样的,但是他的mountIndex(1)小于lastIndex(2),即需要操作,将他移动到lastIndex的后面,
// 看上面 patch 第一个参数 type 即为MOVE,fromIndex 代表最后要落下的位子(用新元素的mountIndex即可), toIndex(代表当前老元素的位子)
// d、查看E,老元素里面没有 那就插入,patch 第二个参数 type 为INSERT, toIndex 表示要插入到那儿(用新元素的mountIndex 表示)
// e、F同E
// f、新元素遍历完成,去遍历老元素 删除新元素内没有用到的,即D 被删除掉 fromIndex代表当前老元素的位子
// MOVE {$$typeof: Symbol(ELEMENT), type: "li", key: "B", ref: undefined, props: {…}, …}
// INSERT {$$typeof: Symbol(ELEMENT), type: "li", key: "E", ref: undefined, props: {…}}
// INSERT {$$typeof: Symbol(ELEMENT), type: "li", key: "F", ref: undefined, props: {…}}
// REMOVE {$$typeof: Symbol(ELEMENT), type: "li", key: "D", ref: undefined, props: {…}, …}
// 3、深度优先 搜集 diffQueue 对dom进行统一处理
// a、 MOVE 的操作是 先删除 在插入,先将 REMOVE 和 MOVE 统一删除
// b、 在将 INSERT 和 MOVE 统一插入节点操作
class ClassComponent extends React.Component{
constructor(props){
super(props);
this.state = {
show: true
}
}
handleClick = () => {
this.setState(state => ({show: !state.show}));
}
render() {
if(this.state.show){
return (
<ul onClick={this.handleClick}>
<li key='A'>A</li>
<li key='B'>B</li>
<li key='C'>C</li>
<li key='D'>D</li>
</ul>
)
}else {
return (
<ul onClick={this.handleClick}>
<li key='A'>A</li>
<li key='C'>C</li>
<li key='B'>B</li>
<li key='E'>E</li>
<li key='F'>F</li>
</ul>
)
}
}
}
life cycle
初始阶段
- 组建实例化 会执行
constructor
=>getDerivedStateFromProps
=>render
=>componentDidMount
更新阶段
- 接受新的数据
getDerivedStateFromProps
=>shouldComponentUpdate
=>render
=> 获取快照getSnapshotBeforeUpdate
这个的返回值会传给componentDidUpdate
第三个参数
销毁阶段
componentWillUnMount
这个是发生在diff的时候 如果比较dom diff 元素不见了 就会卸载组件
老生命周期有2个被删除了~
componentWillMount
&componentWillUpdate
- 使用不当 会造成死循环,他阔以拿到this 修改父组件的数据,新的
getDerivedStateFromProps
方法替代 他是一个函数没法获取到this
context
- context用法很简单
createContext
返回两个对象Provider
组件注册数据,Consumer
接受回调拿数据- 类组件中,获取可以通过
static contextType = ThemeContext
内部在解析组件的时候 会判断是否有这个存在 如果有的话,会把Provider
传递的数据挂在当前实例上context
上
let ThemeContext = React.createContext(null);
// ....父组件
<ThemeContext.Provider value={{}}>
<div>
</div>
</ThemeContext.Provider>
<ThemeContext.Consumer>
{
(value) => (
<div>
{value}
</div>
)
}
</ThemeContext.Consumer>
// 解析类组件
if(oldElement.type.contextType){
componentInstance.context = oldElement.type.contextType.Provider.value;
}
function createContext(defaultValue){
Provider.value = defaultValue;// context会复制一个初始值
function Provider(props){
Provider.value = props.value;// 每次Provider重新更新时候 也会重新赋值
return props.children;
}
function Consumer(props){
return onlyOne(props.children)(Provider.value)
}
return {Provider, Consumer}
}
fiber(17+)
屏幕刷新率
- 大多数设备的屏幕都是 60次/秒,页面是一帧绘制出来,当每秒绘制的帧数(FPS)到达60时,页面是流程的,小于这个值,用户会感觉到卡段
- 每个帧的预算事件是16.66毫秒(1秒/60),1s 60帧,所以每一帧分到的事件是 1000/60 = 16ms,所以我们的代码力求不让义诊的工作量超过16ms
帧
- 每个帧的开头包括样式计算,布局和绘制
- js执行的js引擎和页面渲染引擎在同一个渲染现成,GUI渲染和js执行是互斥
- 如果某个任务执行时间过长 浏览器会推迟渲染
- 图片若显示过小 在新网页中看
rAf(requestAnimationFrame)
- requestAnimationFrame回调函数会在绘制之前执行,上图中显示他执行的时候在layout前面
- 下面的用法 当浏览器绘制前操作dom 让他的width 一直增加
<body>
<div style="background: red;width: 0;height: 20px;"></div>
<button>开始</button>
<script>
const div = document.querySelector('div')
const button = document.querySelector('button')
let start;
function progress(){
div.style.width = div.offsetWidth + 1 + 'px'
div.innerHTML = div.offsetWidth + '%'
if(div.offsetWidth < 100){
let current = Date.now()
start = current
timer = requestAnimationFrame(progress)
}
}
button.onclick = function(){
div.style.width = 0;
start = Date.now();
requestAnimationFrame(progress);
}
</script>
</body>
requestIdleCallback
- requestIdleCallback 作用是 当正常帧任务完成后没超过16秒,说明时间有富余,此时就会执行
requestIdleCallback
里注册的响应 requestIdleCallback(callback,{timeout:1000})
,callback接收2个参数(didTimeout,timeRemaining())- didTimeout,布尔值,表示任务是否超时,结合 timeRemaining 使用
- timeRemaining(), 表示当前帧剩余的时间
- timeout表示如果超过这个时间后,任务还没有执行,则强制执行,不必等待
<body>
<script>
//
function sleep(d){
for(var t = Date.now();Date.now() - t <= d;){}
}
const works = [
()=>{
console.log('第一个任务开始')
sleep(20)
console.log('第一个任务结束')
},
()=>{
console.log('第二个任务开始')
sleep(20)
console.log('第二个任务结束')
},
()=>{
console.log('第三个任务开始')
sleep(20)
console.log('第三个任务结束')
}
]
// timeout 意思是 告诉浏览器 1000ms 即使你没有空闲时间也得帮我执行, 因为我等不及了
requestIdleCallback(workLoop,{timeout:1000})
function workLoop(deadLine){
// 返回值 deadLine.didTimeout 布尔值 表示任务是否超时
// deadLine.timeRemaining() 表示当前帧剩余的时间
console.log('本帧剩余时间', parseInt(deadLine.timeRemaining()));
while((deadLine.timeRemaining() > 1 || deadLine.didTimeout) && works.length>0){
performUnitOfWork()
}
if(works.length>0){
console.log(`只剩下${parseInt(deadLine.timeRemaining())}ms,时间片到了等待下次空闲时间的调度`);
requestIdleCallback(workLoop)
}
}
function performUnitOfWork(){
works.shift()()
}
</script>
</body>
单链表
- 单链表是一种链式存取的数据结构
- 链表中的数据是以节点来表示的,每个节点的构成:元素+指针(指示后继元素的存储位子),元素就是存储数据的存储单元,指针就是连接每个节点的地址
class Update{
constructor(payload,nextUpdate){
this.payload = payload
this.nextUpdate = nextUpdate
}
}
class UpdateQueue{
constructor(){
this.baseState = null
this.firstUpdate = null
this.lastUpdate = null
}
enqueueUpdate(update){
if(this.firstUpdate == null){
this.firstUpdate = this.lastUpdate = update
}else{
this.lastUpdate.nextUpdate = update
this.lastUpdate = update
}
}
forceUpdate(){
let currentState = this.baseState || {}
let currentUpdate = this.firstUpdate
while(currentUpdate){
let nextState = typeof currentUpdate.payload == 'function' ? currentUpdate.payload(currentState) : currentUpdate.payload
currentState = {...currentState,...nextState}
currentUpdate = currentUpdate.nextUpdate
}
this.firstUpdate = this.lastUpdate = null;
this.baseState = currentState
return currentState
}
}
let queue = new UpdateQueue();
queue.enqueueUpdate(new Update({name:'sg'}))
queue.enqueueUpdate(new Update({age:12}))
queue.enqueueUpdate(new Update((data)=>({age:data.age+1})))
queue.enqueueUpdate(new Update((data)=>({age:data.age+2})))
console.log(queue.forceUpdate()) ;
console.log(queue.baseState)
DOM=>fiber
- 为什么需要fiber这种结构?
Fiber 之前的Reconcilation(协调/调和)
- React 会递归对比VirtualDOM树,找出需要变动的节点,然后同步更新他们,这个过程叫
Reconcilation
- 在协调期间,React 会一直占用着浏览器的资源,一则会导致用户触发的事件得不到响应,二会导致卡顿
Fiber
- 我们可以通过某些调度策略合理分配CPU资源,从而提高用户的响应数据
- 通过Fiber,让自己的
Reconcilation
过程变成可被中断。适时让出CPU执行权,可以让浏览器及时地响应用户的交互 - Fiber也是一个执行单元,每次执行完一个执行单元,React就会检查现在还剩多少时间,如果没有时间就将控制权让出去
- fiber是一个数据结构,也是一个对象。
- React目前使用的是链表,每个VirtualDOM 节点内部表示一个Fiber
type Fiber = {
//类型
type: any,
//父节点
return: Fiber,
// 指向第一个子节点
child: Fiber,
// 指向下一个弟弟
sibling: Fiber
}
- 下面是通过babel编译后的DOM, 每一个节点都会转成 特定的数据结构(fiber),所以第一步是将dom全部转换成fiber。fiber有一些必备属性(type props return effectTag nextEffect等)他们记录了每个DOM信息以及DOM直接的关联
// let element = (
// <div id='A1'>
// <div id='B1'>
// <div id='C1'></div>
// <div id='C2'></div>
// </div>
// <div id='B2'></div>
// </div>
// )
// console.log(JSON.stringify(element, null, 2))
let element = {
"type": "div",
"props": {
"id": "A1",
"children": [
{
"type": "div",
"props": {
"id": "B1",
"children": [
{
"type": "div",
"props": {
"id": "C1"
},
},
{
"type": "div",
"props": {
"id": "C2"
},
}
]
},
},
{
"type": "div",
"props": {
"id": "B2"
},
}
]
},
}
- fiber 遍历是从跟节点开始 深度优先。图可以看出关联,child指向元素的子字节,sibling 指向 元素的下一个兄弟节点,return 执行父节点。
beginWork
方法,实现了DOM转fiber,以及child
return
sibling
之间的关联 performUnitOfWork
方法对DOM进行递归遍历,原则是先找最深的第一个元素,接着找他的下一个兄弟元素, 依次寻找,兄弟元素找完成后, 找父元素 在找父元素的兄弟元素 依次循环,遍历所有节点
模拟fiber 执行过程 后面会详细分析
/*
1、从顶点开始遍历
2、如果有儿子,先遍历大儿子
*/
// 在浏览器执行
let A1 = {type:'div',key:'A1'}
let B1 = {type:'div',key:'B1',return:A1};
let B2 = {type:'div',key:'B2',return:A1};
let C1 = {type:'div',key:'C1',return:B1};
let C2 = {type:'div',key:'C2',return:B1};
A1.child = B1;
B1.sibling = B2;
B1.child = C1;
C1.sibling = C2;
function sleep(d){
for(var t = Date.now();Date.now() - t <= d;){}
}
let nextUnitOfWork = null;// 下一个执行单元
function workLoop(deadLine){
while((deadLine.timeRemaining() > 1 || deadLine.didTimeout) && nextUnitOfWork){
nextUnitOfWork = performUnitOfWork(nextUnitOfWork)
}
if(!nextUnitOfWork){
console.log('render阶段执行结束')
}else{
requestIdleCallback(workLoop,{timeout:1000})
}
}
// 开始遍历
function performUnitOfWork(fiber){
beginWork(fiber)//处理fiber
if(fiber.child){// 如果有儿子 返回大儿子
return fiber.child
}
// 如果没有儿子 说明此fiber已经完成了
while(fiber){
completeUnitOfWork(fiber)
if(fiber.sibling){
return fiber.sibling//如果有弟弟就返回 亲弟弟
}
// 在找父亲的弟弟
fiber = fiber.return
}
}
function completeUnitOfWork(fiber){
console.log('结束',fiber.key)
}
function beginWork(fiber){
sleep(20)
console.log('开始',fiber.key)
}
nextUnitOfWork = A1
requestIdleCallback(workLoop,{timeout:1000})
react渲染流程 && fiber 三个阶段
- 调度(scheduleRoot)、调和(Reconcilation)、提交(commitRoot)
- 只有调和是异步的
1、调度(scheduleRoot)
- 下面是个demo模板
- 1、element通过babel编译成jsx语法,传给render方法,render会将element包装成一个fiber结构,进行调度根节点(scheduleRoot)
import React from './react';
import ReactDOM from './react-dom';
let style = { border:'3px solid red',margin:'5px'}
let element = (
<div id='A1' style={style}>
A1
<div id='B1' style={style}>
B1
<div id='C1' style={style}>C1</div>
<div id='C2' style={style}>C2</div>
</div>
<div id='B2' style={style}>B2</div>
</div>
)
ReactDOM.render(
element,
document.getElementById('root')
)
- 2、schedule(调度整体流程)
- 根据下面3条规则能将react串联起来
- a、遍历的规则
- 先儿子,后弟弟,在叔叔,
- b、完成链规则
- 自己所有的子节点完成后完成自己
- c、effect规则
- 自己所有的子节点完成后完成自己
调和(Reconcilation)
- 调和 把虚拟DOM转成Fiber节点的过程,以及每个fiber之间的关联,收集 effectList(需要更新的Fiber)
- 这个阶段是异步 如果没有完成 下一个时间节点 重新收集 effectList
commit阶段
- commit阶段处理 effectList(指调和阶段需要更新的DOM),此处的流程C1开始直接到A1结束,与调和阶段是反的,最后挂到
root
上 - 类比Git分支功能,从旧树中fork出来一份,在新分支进行添加、删除和更新操作,经过测试后进行提交
DOM-DIFF
- 在React17+中DOM-DIFF就是根据老的fiber树和最新的JSX对比生成新的fiber树的过程
React优化原则
- 只对同级节点进行对比,如果DOM节点跨层级移动,则React不会复用
- 不同类型的元素会产出不同的结构 ,会销毁老结构,创建新结构
- 可以通过key标识移动的元素
单节点
- 如果新的子节点只有一个元素的情况,key和type不同,在调和阶段,需要把老节点标记为删除,生产新的fiber节点 标记为插入
- 在调和阶段,需要把老节点标记为删除
多节点
- 如果新的节点有多个节点的话,多节点的时候会经历二轮遍历
- 第一轮遍历主要是处理节点的更新,更新包括属性和类型的更新
一一对比,都可复用,只需更新 <ul> <li key="A">A</li> <li key="B">B</li> <li key="C">C</li> <li key="D">D</li> </ul> /*************/ <ul> <li key="A">A-new</li> <li key="B">B-new</li> <li key="C">C-new</li> <li key="D">D-new</li> </ul> 一一对比,key相同,type不同,删除老的,添新的 <ul> <li key="A">A</li> <li key="B">B</li> <li key="C">C</li> <li key="D">D</li> </ul> /*************/ <ul> <div key="A">A-new</div> <li key="B">B-new</li> <li key="C">C-new</li> <li key="D">D-new</li> </ul> 一一key不同退出第一轮循环 <ul> <li key="A">A</li> <li key="B">B</li> <li key="C">C</li> <li key="D">D</li> </ul> /*************/ <ul> <li key="A">A-new</li> <li key="C">C-new</li> <li key="D">D-new</li> <li key="B">B-new</li> </ul>
- 移动(这里和16版本的diff 是一样)