react核心拆解
- react 核心是以 状态声明视图可变内容显示,通过事件控制状态更新,状态更新后驱动视图更新,
- 数据(状态)
- 视图(render)
- 事件
- 而他们三方又是独立的
- react 通过 调度器 和 调和器 来进行三方的协调工作
- 为什么不使用 requestIdleCallback 和或者 scheduler 实现
- react 自己开发了一个包 scheduler 去实现
- 调度器
- 等待浏览器有空闲就执行 调和器
- 调和器
- react 的diff实现,关注需要更新的内容,有更新才发生render
- 为什么不使用 requestIdleCallback 和或者 scheduler 实现
const queue = [];
let index = 0;
//初始化state
const useState = (initialState) => {
queue.push(initialState);
const update = (state) => {
// 为什么在 react 中, hooks 不能写在判断里面
queue.push(state);
index++;
};
return [queue[index], update];
};
const [count, setCount] = useState(0);
// 事件
window.addEventListener(
"click",
() => {
setCount(queue[index] + 1);
},
false
);
// 重新渲染
const render = () => {
console.log(count);
document.body.innerHTML = queue[index];
};
let prevCount = count;
// fiber
const reconcile = () => {
// 尽可能少的更新
// 尽可能大的复用
// 为什么使用key
if (prevCount !== queue[index]) {
render();
prevCount = queue[index];
}
};
// 通过这个api实现的render 会一直rerender
// 我们需要知道什么时候数据发生了变化
// 判断变没变的过程叫diff
// 调度器
const workLoop = () => {
//视图初始化
reconcile();
// 会一直执行
requestIdleCallback(() => {
workLoop();
});
};
render();
workLoop();
hook实现
放在react-reconciler包
//初始化state
const useState = (initialState) => {
queue.push(initialState);
const update = (state) => {
// 为什么在 react 中, hooks 不能写在判断里面
queue.push(state);
index++;
};
return [queue[index], update];
};
react架构实现
react源码阅读过程,各个包理解顺序
- react-dom
- 处理渲染相关,处理端的事情,浏览器的api 跨端开发,3d(渲染器逻辑)
- createRoot,(ReactDom.createRoot),createContainer
- render, updateContainer
- 处理渲染相关,处理端的事情,浏览器的api 跨端开发,3d(渲染器逻辑)
- react 是为了统一为外部开发者提供接口协议
- useState
- useEffect
- react-reconciler 处理状态调和
- createFiberRoot
- initializeUpdateContainer
- createUpdate
- enqueueUpdate
- scheduler 调度包
- 由于web提供的api无法显示优先级调度,所以react自己实现了这个功能
- expirationTime 过期时间 => lanes 模型
- react-noop-renderer
- 实现无状态的
- 可以实现 react 的渲染器
react理解目标
初中级
- 深入理解react执行全过程
- 以react18为例,深入理解react应用从创建到更新到销毁的全过程,并能够理解其核心关键节点
- 掌握scheduler原理
- 掌握从早期expirationTime时间切片机制到lanes的演进,并能理解其设计用意
- 掌握reconciler原理
- 掌握从早期stack reconciler到fiber的演进过程,并理解其重构目的
- 了解hooks原理
- 了解react hooks设计原理,理解代数效应在react 整体设计中的地位
高级
- 深入scheduler,reconciler细节
- 深入理解可优先级中断更新实现,理解更新中断与恢复,理解双缓存构建
- 手写react
- 从0到1,实现一个简版核心react
Jsx 语法
什么是jsx
- jsx是用对象的形式表述页面结构的语法,
- 目前在js不能直接使用需要使用babel进行编译,经过babel编译后会转为 React.createElement
注意事项
-
jsx 只能有一个根节点,如果必须要使用多根节点的话可以使用
React.Fragment,或者使用语法糖<></>-
使用该语法糖的话节点类型会变成
Fragment -
<></> <React.Fragment></React.Fragment>
-
-
jsx语法必须要有结束标签
-
<img />
-
插值语句
-
使用 { } ,内容支持 字符串 , 数字 ,数组(普通类型) , 元素 ,表达式
-
const App = () => <div> { 'aa' } { 1 } { <span> M </span> } {[1,2,3,4]} { 1 + 1}</div> -
react 会把表达式的结果计算后放在视图显示
-
-
true false null undefined 是无法展示的,一般是用于做判断
-
三元表达式
-
const App = () => <div> { a > 1 ? '11' : '222' } </div>
-
-
Api调用
-
const App = () => <div> { num.toFixed(2) }</div>
-
-
普通对象不能作为内容,可以使用react元素对象
-
const App = () => <div> { JSON.stringfly({a:1}) } </div> -
直接使用对象会报错,需要序列表
-
-
在jsx里面写注释,不会显示在视图里面
-
里面使用数组会把数组的内容展开,生成多个子元素
-
const App = () => <div> {[1,2,3,4]} </div> -
数组内容是元素也是一样的道理
-
使用数组内容是元素的情况需要给每个元素增加一个key,react优化逻辑需要使用
-
事件绑定
function test() {}
// 使用onClick 绑定一个函数体,实现绑定事件
const App = () => <div onClick = {test}> 点击</div>
// 接收传入的参数 使用一个函数包装,去调用模板函数进行传参
const App = () => <div onclick = {() => test}> 点击</div>
-
函数事件使用泛型
-
//<T,> 需要使用 , 隔开,默然会被当做元素 const fn = <T,>(params:T) => {} const App = () => <div onclick = {() => fn}> 点击</div>
-
属性绑定
-
绑定自定义属性(也就是 v-bind)
-
const id = '11' const App = () => <div id={id}> 点击</div>
-
-
绑定class, 需要使用className
-
const cls = 'test' const App = () => <div className={test}> 点击</div> // 绑定多个class const App = () => <div className={`${cls} aa bb `}> 点击</div>
-
-
绑定style, 它需要是一个对象
-
const styles = {color:"red"} const App = () => <div style={styles}> 点击</div> // 直接使用对象 const App = () => <div style={{color:"red"}}> 点击</div>
-
-
添加html代码片段(v-html)
-
const html = `<div>html</div>` // 使用该属性后 元素不能写内容,如果有内容则不会进行html模板渲染 const App = () => <div dangerouslySetInnerHTML={{__html:html}}></div>
-
-
如何遍历数组(v-for)
-
const arr = [1,2,3,4,5,6] const App = () => <div> { arr.map(el => { return <div>el</div> }) } </div> -
使用js的api调用,遍历数组返回新的元素
-
Babel
- Jsx -> React.createElement 的转换
- React使用jsx 开发,但是本身React是无法识别jsx语法,需要使用babel转换成js代码
- react组件使用大写做为名称的原因是因为babel在遇到小写名称的时候会把该元素当做html原生标签
const App = () => {
return (<div id="2">
<span>cscs</span>
</div>)
}
// 转换后
const App = () => {
return React.createElement('div', { id: 2 },
React.createElement('span', null, 'cscs')
);
};
createElement源码
- babel会把子元素属性(children)放在props 里面
function createELement(type,config,children) {
return {
// react 组件标识,固定标识
$$typeof:REACT_ELEMENT_TYPE,
// 当前组件类型,html标签,类名,函数名,特殊的标识
type:type,
// 当前类型
key:key,
// 引用
ref:ref,
// 其余属性
props:props
}
}
const element = <div className='test'> i am div</div>
// 拆解后
{
$$typeif:Symbol(react.element),
key:null,
props:{className:"test",children:'i am div'},
ref:null,
type:"div" // 函数式组件是存储的这个function class的话是这个实例
}
事件触发核心源码
/**
* 通过事件代理的方式 事件事务系统
*/
const allEvents = ["click"];
/**
* 收集冒泡阶段经过的所有节点的对应事件
* @param {*} reactName 事件名
* @param {*} fiber 当前触发事件的fiber节点
* @returns
*/
function accumulateListeners(reactName, fiber) {
const listeners = [];
// 当前操作的节点
let currentFiber = fiber;
while (currentFiber) {
// 原生节点
if (currentFiber.tag === "HostComponent" && currentFiber.stateNode) {
// 读取节点上的props 获取click事件
const listener = currentFiber.memoizedProps[reactName];
if (listener) {
listeners.push(listener);
}
}
currentFiber = currentFiber.return;
}
return listeners;
}
// 事件合成
class SyntheticEvent {
constructor(event) {
this.nativeEvent = event;
Object.keys(event).forEach((key) => {
if (key == "preventDefault") {
this[key] = function () {};
} else if (key == "stopPropagation") {
this[key] = function () {};
} else {
this[key] = event[key];
}
});
}
}
// 触发代理事件
function dispatchEvent(event) {
const { type, target } = event;
// 拿到当前触发事件的类型
const reactName = "on" + type[0]?.toLocaleUpperCase() + type.slice(1);
// 收集当前冒泡阶段链条上所有节点的当前类型事件
const listeners = accumulateListeners(reactName, target?.internalFiber);
// 合成事件,重写事件系统的阻止冒泡和捕获
const syntheticEvent = new SyntheticEvent(event);
// 触发冒泡链条上所有绑定的对应的事件
listeners.forEach((listener) => {
listener(syntheticEvent);
});
// 执行
}
// 核心出口
function listenToAllEvents(container) {
allEvents.forEach((eventName) => {
container.addEventListener(eventName, dispatchEvent, false);
});
}
虚拟dom
使用js对象去描述一个dom 结构,虚拟dom 不是直接操作浏览器的真实dom,而是首先在虚拟dom中对ui 进行更新,然后再将变更高效的同步到真实的dom中
优点
- 性能优化: 直接操作dom是比较消耗性能的,尤其是涉及到大量节点更新,虚拟dom通过减少不必要的dom操作,复用一些节点(diff算法),(提升不大)
- 跨平台: 虚拟dom 是一个与平台无关的概念,他可以映射到不同的渲染目标,比如浏览器或者移动端(rn)
简单实现虚拟dom
// jsx -> babel/swc -> React.createElement
const ELEMENT_TYPES = {
TEXT_ELEMENT: "TEXT_ELEMENT",
};
const React = {
/**
* 实现React.createElement
* @param {*} type 节点类型
* @param {*} props 组件参数
* @param {...any} children 剩余子元素
*/
createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
if (typeof child == "object") {
return child;
} else {
return React.createTextElement(child);
}
}),
},
};
},
/**
* 实现文本节点构建 因为文本节点没有子集
* @param {*} text
*/
createTextElement(text) {
return {
type: ELEMENT_TYPES.TEXT_ELEMENT,
props: {
children: [],
nodeValue: text,
},
};
},
};
const text = React.createElement("span", null, "测试");
const root = React.createElement("div", { id: 1 }, text);
fiber架构
fiber之前的问题
- 在react15中它的reconciler过程是根据reactELement不断递归调用实现的,整个过程是同步的,当reactELement节点增多时.reconciler的过程也会变久,此时js执行时间也会变长
- 在浏览器的环境下js执行和浏览器渲染是串行的,当渲染任务和渲染任务因为js的执行时间变成会导致间隔增加,超出 一帧的刷新率(16ms),就会出现卡顿的情况
- 因为js的执行时间是不能控制,所以react的设计是将一个大的任务拆分成多个小的任务,在渲染的空隙去执行小的任务,当小的任务执行完毕之后,大的任务也会完成了
- 通过打断递归调用的过程,判断当前剩余时长是否能够继续执行,如果没有时间了就不再执行了,任何在下一帧继续执行任务
fiber实现的具体目标
- 可中断的渲染
- 允许将大的渲染任务拆分成多个晓得工作单元,使得react 可以在空闲时间执行这些小任务,当浏览器需要处理更高优先级的任务时(用户输入,动画),可以暂停渲染,先处理这些任务,然后再继续执行未完的渲染工作
- 优先级调度
- 在fiber架构下,React可以根据不同任务的优先级决定何时更新那些部分,React会优先更新用户可以感知的部分(动画,用户输入),而优先级的任务(数据加载后的页面更新)可以延后执行
- 双缓存树
- fiber架构中有两棵fiber树
- current fiber tree (当前正在渲染的fiber树)
- work in progress fiber tree (正在处理的fiber树)
- React 使用这两棵树报存更新前后的状态,从而更高效的进行比较和更新
- 任务切片
- 在浏览器的空闲时间内(使用requestidleCallback思想),React可以将渲染任务拆分成多个片段
- 逐步完成fiber树的构建,避免一次性完成所有渲染任务的阻塞
任务切片
浏览器的一帧任务执行时间,浏览器一般为60FPS 也就是1秒刷新60次,就可以求出一帧渲染时间
1000 / 60 => 16.67 毫秒
浏览器一帧需要执行那些任务
- 处理事件的回调
- 处理计时器的回调
- 开始帧
- 执行requestAnimation动画的回调
- 计算机页面布局计算(准备更新页面,合并到主线程
- 绘制
- 如果此时还有空闲时间,执行requestidleCallback(需要注意,react 不是使用该函数,而是自己模拟实现)
fiber节点的具体实现
虚拟dom 是一种概念,react element也是虚拟dom,fiber的架构在reactElement基础上又抽象出一层.就是filber节点
fiber 属性分类
- fiber实现(链表)
- 根节点下只有一个节点,其余节点都是它的兄弟节点
- 通过链表的形式进行串联
- fiber的属性分类
- 从reactELement获得的属性 . key type props 等
- 链表属性
- child -> 当前fiber节点的第一个子节点
- sibling -> 下一个兄弟节点
- return -> 父节点
- 状态属性
- memoizedProps -> 当前节点的props
- memoizedState -> 当前节点的state
- paddingProps => 即将要处理的props
- updateQueue -> 即将要更新的一些属性
- 标志位属性
- flags -> 当前节点的副作用标记
- subTreeFlags -> 当前此节点的所有的子节点的标记
- lanes -> 当前节点的优先级信息
- childLanes -> 当前节点的子节点优先级信息
- 其他属性
- index
- mode
- deletions
- alternate => 当前节点的副本,两棵fiber树对应的节点,相互指向,复用节点使用
手写简单fiber 源码
// jsx -> babel/swc -> React.createElement
const ELEMENT_TYPES = {
TEXT_ELEMENT: "TEXT_ELEMENT",
};
const React = {
/**
* 实现React.createElement
* @param {*} type 节点类型
* @param {*} props 组件参数
* @param {...any} children 剩余子元素
*/
createElement(type, props = {}, ...children) {
return {
type,
props: {
...props,
children: children.map((child) =>
typeof child === "object" ? child : React.createTextElement(child)
),
},
};
},
/**
* 实现文本节点构建 因为文本节点没有子集
* @param {*} text
*/
createTextElement(text) {
return {
type: ELEMENT_TYPES.TEXT_ELEMENT,
props: {
nodeValue: text,
children: [],
},
};
},
};
// const vdom = React.createElement('div', { id: 1 }, React.createElement('span', null, '小满zs'));
// // console.log(root);
// 实现虚拟dom 转fiber 和时间切片
// 下一个工作单元
let nextUnitOfWork = null;
// 旧的fiber树
let currentRoot = null;
// 当前正在工作的fiber树
let wipRoot = null;
// 存储需要删除的fiber节点
let deletions = null;
/**
* 初始化fiber根节点
* @param {*} element
* @param {*} container
*/
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
},
alternate: currentRoot, // 保存旧的fiber结构
};
nextUnitOfWork = wipRoot;
deletions = [];
}
/**
* 创建fiber节点
* @param {*} element
* @param {*} parent
*/
function createFiber(element, parent) {
return {
type: element.type,
props: element.props,
parent,
dom: null,
child: null,
sibling: null,
alternate: null,
effectTag: null,
};
}
/**
* 创建真实dom节点
* @param {*} fiber
* @returns
*/
function createDom(fiber) {
const dom =
fiber.type === ELEMENT_TYPES.TEXT_ELEMENT
? document.createTextNode("")
: document.createElement(fiber.type);
updateDom(dom, {}, fiber.props);
return dom;
}
/**
* 删除旧的属性
添加新的属性
* @param {*} dom
* @param {*} prevProps
* @param {*} nextProps
*/
function updateDom(dom, prevProps, nextProps) {
Object.keys(prevProps)
.filter((name) => name !== "children")
.forEach((name) => {
dom[name] = "";
});
Object.keys(nextProps)
.filter((name) => name !== "children")
.filter((name) => prevProps[name] !== nextProps[name])
.forEach((name) => {
dom[name] = nextProps[name];
});
}
/**
* 实现递归调用切片任务 在每一个切片里面判断是否有剩余时间可以执行任务单元
* @param {*} deadline
*/
function workLoop(deadline) {
// 记录是否存在空闲时间 有空闲时间才执行
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
// 任务执行完成 并且还有待提交的工作根
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
/**
* 从当前单元开始 遍历构建真实dom节点, 先查找子节点 没有子节点 就构建兄弟节点
* 最后实现就是 深度优先 => 广度优先
* @param {*} fiber
* @returns
*/
function performUnitOfWork(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// 获取当前节点的子节点
const elements = fiber.props.children;
// 遍历子节点
reconcileChildren(fiber, elements);
// 如果有子节点 把子节点当做下一个工作单元
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
// 没有子节点 查找兄弟节点
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
// 没有兄弟节点 返回父节点
nextFiber = nextFiber.parent;
}
// 所有的元素查找完毕之后 就结束了
return null;
}
/**
* diff 算法 形成fiber树
* @param {*} wipFiber
* @param {*} elements
*/
function reconcileChildren(wipFiber, elements) {
let index = 0;
// 获取当前节点的旧的fiber结构 进行diff
let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
let prevSibling = null;
// 遍历子集 创建fiber对象
while (index < elements.length || oldFiber != null) {
const element = elements[index];
// diff 过程 复用 新增 删除
let newFiber = null;
const sameType = oldFiber && element && element.type == oldFiber.type;
// 判断节点是一样的
if (sameType) {
// 节点不变 属性更新
newFiber = {
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", // 打个标记 更新
};
}
// 新增的节点
if (element && !sameType) {
newFiber = createFiber(element, wipFiber);
newFiber.effectTag = "PLACEMENT"; // 新增
}
// 删除掉的节点
if (oldFiber && !sameType) {
oldFiber.effectTag = "DELETION"; //删除
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 给当前节点插入后续节点 形成链表
if (index === 0) {
wipFiber.child = newFiber;
} else if (element) {
// 挂载到相邻节点后面
prevSibling.sibling = newFiber;
}
// 存储上一个的兄弟元素
prevSibling = newFiber;
index++;
}
}
function commitRoot() {
deletions.forEach(commitWork);
commitWork(wipRoot.child);
// 修改指向 存储旧的fiber树
currentRoot = wipRoot;
// 重置当前构建的这个fiber树 以便下次对比
wipRoot = null;
}
/*
构建fiber节点
*/
function commitWork(fiber) {
if (!fiber) {
return;
}
const domParent = fiber.parent.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
render(
React.createElement(
"div",
{ id: "root" },
React.createElement("span", null, "cs")
),
document.getElementById("root")
);
setTimeout(() => {
render(
React.createElement(
"div",
{ id: "root" },
React.createElement("p", null, "新元素")
),
document.getElementById("root")
);
}, 2000);
fiber构建总结
- jsx 转换获得对应的dom对象结构
- 开启 render 根节点初始化
- 执行 requestidleCallback 任务调用,分任务构建 fiber 节点
- 创建真实节点存在当前 fiber 节点,遍历当前节点的子节点,依次构建真实节点
- 如果子节点存在子节点,还是继续遍历
- 没有子节点了 查找兄弟节点 ,兄弟节点构建完回到父节点(递归构建)
- 节点对比,判断 type 打上标记,如果是删除需要修改指向
- 所有的任务执行完毕之后,开始渲染到真实dom
- 对打上标记的元素进行处理,更新到真实dom(递归处理)
- 切换fiber树指向
渲染构建过程
-
react构建过程是从根节点开始,将react元素转化成react节点
-
他支持dom节点,空节点,组件节点,数组节点,文本节点,遇到组件后递归构建,如果是函数组件则会执行函数得到结果,
-
如果内部是节点则会继续往下执行,最终递归构建结果,
-
类组件也是一样,会递归构建结构,会先触发construct 然后触发static方法设置默认state,然后触发render方法,递归创建子元素,子元素同样执行,
-
特别需要注意的是dim的执行是在render 完成之后挂载到页面之后才会执行,所以在组件构建时只会将其放入执行队列,
- effect 也是先放入执行队列,然后等待render完成才会触发
-
又因为react是递归构建,所以子组件的dim 先放入执行队列,后面渲染完之后就会先执行子组件的dim,
-
并且react构建采用的是深度优先加后续遍历,如果存在多个子节点的情况,react会构建左子节点后再继续构建右子节点最后才构建根节点,生命周期就是左子左父右子右父最后根
为什么要自己实现调度器
为什么react 不使用原生的 requestidlCallback 实现
- 兼容性不好
- 控制精细度
- react要根据组件的优先级和紧急情况等信息,更精确的安排渲染工作
- 执行时机
- requestidleCallback回调函数的执行间隔是50ms,也就是20fps,1秒内执行20次,间隔比较长
- 差异性
- 每个浏览器实现该api的方式不同,导致执行时机有差异
为什么不使用定时器,在嵌套的情况下,会有最小超时时间
最优方案 - MessageChannel
-
也是宏任务,但是没有最小间隔时间,也没有延迟, 在不支持的情况下降级使用settimeout
-
浏览器设计初衷是为了实现和iframe和worker的多线程通信,有点类似于发布订阅
const ImmediatePriority = 1; // 立即执行的优先级, 级别最高 [点击事件,输入框,]
const UserBlockingPriority = 2; // 用户阻塞级别的优先级, [滚动,拖拽这些]
const NormalPriority = 3; // 正常的优先级 [render 列表 动画 网络请求]
const LowPriority = 4; // 低优先级 [分析统计]
const IdlePriority = 5; // 最低阶的优先级, 可以被闲置的那种 [console.log]
function getCurrentTime() {
return performance.now();
}
class SimpleScheduler {
constructor() {
/*
收集任务队列
{
callback
priorityLevel 优先级
expirationTime 过期时间
}
*/
this.taskQueue = [];
this.isPerformingWork = false; // 是否正在工作 防止多次执行
const channel = new MessageChannel();
this.port = channel.port2; // 发消息的
//绑定接收到消息的回调
channel.port1.onmessage = this.performWorkUntilDeadLine.bind(this);
}
/**
* 根据优先级排序
* @param {*} priorityLevel
* @param {*} callback
*/
scheduleCallback(priorityLevel, callback) {
const curTime = getCurrentTime();
let timeout;
// 根据优先级设置超时时间
// 超时时间越小 优先级越高
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = 250;
break;
case LowPriority:
timeout = 10000;
break;
case IdlePriority:
timeout = 1073741823; // 32位操作系统 v8引擎最大时间
break;
case NormalPriority:
default:
timeout = 5000;
break;
}
const task = {
callback,
priorityLevel,
expirationTime: curTime + timeout,
};
this.push(this.taskQueue, task);
this.schedulePerformWorkUntilDeadLine();
}
//触发消息
schedulePerformWorkUntilDeadLine() {
if (!this.isPerformingWork) {
this.isPerformingWork = true;
this.port.postMessage(null);
}
}
//收到消息执行
performWorkUntilDeadLine() {
this.isPerformingWork = true;
this.workLoop();
this.isPerformingWork = false;
}
workLoop() {
let currentTask = this.peek(this.taskQueue);
while (currentTask) {
const cb = currentTask.callback;
cb && cb();
this.pop(this.taskQueue);
currentTask = this.peek(this.taskQueue);
}
}
push(queue, task) {
queue.push(task);
queue.sort((a, b) => a.expirationTime - b.expirationTime); // 根据超时时间排序 升序
}
peek(queue) {
return queue[0] || null;
}
pop(queue) {
return queue.shift();
}
}
const s = new SimpleScheduler();
s.scheduleCallback(NormalPriority, () => {
console.log(1);
});
s.scheduleCallback(ImmediatePriority, () => {
console.log(3);
});
s.scheduleCallback(UserBlockingPriority, () => {
console.log(2);
});
组件
-
组件可以理解为一个可复用的独立的 ui 单元,它内部可以自己实现一些逻辑和维护需要显示的数据, 使用者只需要根据其使用方式传递数据和使用,即可实现多次显示和减少复杂度
-
react元素都可以看为是组件,组件的可以接受外部传入的数据,该数据不能在接收方直接修改,因为react在创建元素的时候对数据进行了冻结(Object.freeze)
-
而且react遵循一个设计原则
数据仅可以提供方可以修改,并且数据是从顶往下流.下方直接接受数据不能修改数据 -
react组件分为两种书写方法
class和function-
class 组件必须要有render方法
-
class 组件接受的props 会放入构造函数的构造器中
-
class 组件接受props在没有写constructor的情况会被react自动进行props的赋值 ( this.props = props )
-
如果需要自己使用constructor 的话 需要使用 super 的调用
super(props),传给父类进行实例化 -
class MyComp extends React.component { constructor(props){ super(props) } render() { return <div>{this.props.name}</div> } }
-
-
function 组件传入的参数会放入函数的入参
-
function MyComp(props) { return <div>{props.name}</div> }
-
-
React 组件需要的渲染会在实例化或者调用function的时候调用其render方法或者直接调用函数得到ui视图,将其通过babel进一步构建成react元素,最后转换为dom视图
组件状态
-
react class 组件中,可以使用state维护组件内部数据状态,这个状态会影响视图的显示
-
state的更新不能直接进行赋值修改,这样不会触发视图的更新,需要使用父类的方法
this.setState来触发更新 -
setState接受一个对象,该函数会将该数据和原来的state进行合并,然后react 会根据结果自动重新渲染 -
如果将
state当做属性传递给其他组件做为props后,一旦状态发生变更会接收该数据的组件也会重新render -
class MyComp extends React.component { constructor(props){ super(props) this.state = { name:"111" } } /* 也可以直接写在外面,它会自动在 constructor的super之后运行 this.state = { name:"111" } */ onClickHandler = () => { this.setState({ name:"222" }) } render() { return <div onclick={onClickHandler}>{this.state.name}</div> } }
-
属性默认值
-
函数组件,在调用函数的时候进行属性混合
-
react官方计划将该操作在后续移除,推荐使用js的属性默认值
-
function App(props) { return <div>{props.a}</div> } App.defaultProps = { a: 1, }
-
-
class组件,在调用class的构造函数进行属性混合
-
class App extends React.component { static defaultProps = { a: 1, } render() { return <div>{props.a}</div> } }
-
属性类型检查
使用 prop-types 这个库进行静态属性类型检查
pnpm add @types/prop-types prop-types
- 给组件增加一个静态属性
propTypes - 里面可以约束变量的类型,然后在编译阶段发出错误提示,不会阻塞代码执行
- 这个库的实现方式就是提供了一个函数,每个参数都会调用对应的函数
- 属性的验证在混合之后
PropTypes.number // 数字类型
PropTypes.number.isRequired // 数字类型 并且必填
PropTypes.func // 函数类型
PropTypes.array // 不限制类型的数组类型
PropTypes.object // 不限制类型的对象类型
PropTypes.any.isRequired // 不限制类型但是必填
PropTypes.node // 可以被渲染的内容,可以是数字,字符串,react元素
PropTypes.elementType // react元素类型
PropTypes.element // react元素
PropTypes.instanceOf // 必须是指定构造函数的实例 本质上使用 xx instanceOf xx
PropTypes.oneOf // 属性值是数组里面的一个
PropTypes.oneOfType // 属性类型必须在数组中 PropTypes.oneOfType([PropTypes.number])
PropTypes.arrayOf // 必须是指定类型的数组 PropTypes.arrayOf(PropTypes.number)
PropTypes.objectOf // 指定对象里面的value必须是指定类型 同arrayOf
PropTypes.shape // 属性必须是对象,并且满足指定的对象要求,属性可以多
PropTypes.exact // 同shape,但是更加精准,属性要一一匹配
自定义属性 // 没有通过验证抛出错误即可
import PropTypes from "prop-types";
function Test(props) {
return <div>{props.a}</div>;
}
Test.propTypes = {
a: PropTypes.number,
}
elementType案例
// 传一个元素类型
<Test a={Comp} />
// 通过标签名的形式使用
function Test(props) {
const Name = props.a;
return (
<div>
<Name />
</div>
);
}
PropTypes.shape案例
Test.propTypes = {
c: PropTypes.shape({
a: PropTypes.any,
d:PropTypes.shape({
a: PropTypes.any,
})
}),
};
// 使用shape约束arrayOf
PropTypes.arrayOf( PropTypes.shape({
a: PropTypes.any,
}))
自定义验证属性案例
- 不能直接在自定义验证调用 PropTypes 的验证器
Test.propTypes = {
c: function (props, propName) {
const val = props[propName];
if (!val) {
return new Error("属性必填");
}
if (typeof val !== "number") {
return new Error("必须是数字");
}
if (val < 0 || val > 100) {
return new Error("数字必须0-100以内");
}
},
};
HOC 高阶组件
- 使用一个组件或者函数进行包装,调用后会返回一个新的组件
- 可以在高阶组件内完成一些公用的能力,
import React from "react";
//使用泛型 P extends object 来表示传入组件的属性类型。
//React.ComponentType<P> 表示传入的组件可以是类组件或函数组件,并且它的属性类型是 P
function Hoc<P extends object>(Comp: React.ComponentType<P>) {
return class LogWrapper extends React.Component<P> {
constructor(props) {
super(props);
}
componentDidMount(): void {
console.log(`${Comp.name}被渲染`);
}
render(): React.ReactNode {
return (
<div>
<Comp {...this.props} />
</div>
);
}
};
}
function Test(props) {
return <div>{props.a}</div>;
}
function Test1(props) {
return <div>{props.a}</div>;
}
const Hoc1 = Hoc(Test1);
const Hoc2 = Hoc(Test);
function App() {
return (
<>
<Hoc2 a={"111"} />
<Hoc1 a={"111"} />
</>
);
}
export default App;
ref 控制
- ref作用于内置的html组件,得到将是真实的dom对象
- ref作用于类组件,得到的将是组件实例
- ref 不能直接写在函数的属性上
class LogWrapper extends React.Component {
constructor(props) {
super(props);
this.text = React.createRef();
/*
this.text = {
current: null,
};
*/
}
render(): React.ReactNode {
return (
<input
ref={this.text}
onChange={() => {
console.log(this.text); // {current: input}
}}
/>
);
}
}
-
ref 不再推荐使用字符串赋值,字符串赋值的方式将来可能会被移除
-
ref 推荐使用对象或者函数
-
对象格式
-
this.text = { current: null };
-
-
函数格式
-
<input ref={el => this.text = el} /> -
componentDidMount 时会调用该函数,这个时候可以使用 ref
-
如果ref的值发生了变动,旧的函数被新的函数替代,会分别调用新的函数和旧的函数,调用时间在 componentDidUpdate 之前
- 旧的函数被调用时,传递null
- 新的函数被调用时,传递对象
-
如果ref 所在的组件被卸载时会调用一次
-
-
ref转发
- 使用
React.forwardRef,该函数是一个高阶函数,传入组件会返回一个新的组件,会在使用该新组件的时候将ref传入原始组件, - 类组件不能使用该函数传递,可以通过普通属性传递
import React from "react";
// 开启第二个参数,接收ref
function Test(props, ref) {
console.log("🚀 ~ Test ~ ref:", ref);
return <div> div</div>;
}
const NewTest = React.forwardRef(Test);
function App() {
const Aref = React.createRef();
return (
<>
<NewTest ref={Aref} />
</>
);
}
Context
可共享的上下文数据
- 当某个组件创建了上下文后,上下文中的数据会被所有后代组件共享
- 如果某个组件依赖了上下文,会导致组件不再纯粹(不再仅是依赖props)
- 一般情况下,用于第三方组件(通用组件 例如 redux react-router)
旧版
- 只有类组件才能创建上下文
- 给类组件书写静态属性
childContextTypes,使用该属性对上下文中的数据类型进行约束 - 添加实例方法 getChildContext, 该方法返回的对象,即为上下文中的数据,该数据必须满足类型约束,该方法会在每次render之后运行
- 子组件如果需要使用上下文的数据,必须要有一个静态属性,
contextTypes对上下文类型进行约束,会把声明过类型的变量注入 - 上下文的数据不能直接修改,一般由状态生成,如果后代需要修改状态,一般可以暴露一个方法在context中,以便后代组件修改
import PropTypes from "prop-types";
import React from "react";
class App extends React.Component {
state: Readonly<any> = {
a: 123,
};
/**
* 约束上下文数据类型
*/
static childContextTypes = {
a: PropTypes.number,
};
/**
* 得到上下文数据
* @returns
*/
getChildContext() {
return {
a: this.state.a,
};
}
render(): React.ReactNode {
return (
<div>
111
<ChildA />
</div>
);
}
}
class ChildA extends React.Component {
constructor(props, context) {
super(props);
console.log(context);
}
static contextType = {
a: PropTypes.number,
};
render(): React.ReactNode {
return <div>111{this.context.a}</div>;
}
}
新版
- 上下文是一个独立的对象,通过
React.createContext(默认值)创建 - 返回的是一个包含两个属性的对象
- Provider 属性: 生产者,一个组件,该组件会创建一个上下文,该组件有一个value属性,可以通过该属性进行赋值
- 同一个Provider,不要用到多个组件中,如果需要再其他组件中使用该数据,应该考虑将数据提升到更高的层次
- Consumer 属性: 消费者
- 在类组件中直接使用this.context获取上下文件数据
- 在函数组件中需要使用Consumer来获取上下文数据
- Consumer 是一个组件
- 它的子节点,是一个函数(它的props.children需要传递一个函数)
- Provider 属性: 生产者,一个组件,该组件会创建一个上下文,该组件有一个value属性,可以通过该属性进行赋值
import React from "react";
const ctx = React.createContext<any>({});
class Test extends React.Component {
static contextType = ctx;
render(): React.ReactNode {
return (
<div
onClick={() => {
this.context.change();
}}
>
{this.context.a}
</div>
);
}
}
function Test1() {
return (
<div>
<ctx.Consumer>{(value) => <span>{value.a}</span>}</ctx.Consumer>
</div>
);
}
class App extends React.Component {
state: Readonly<any> = {
a: 2,
change: () => {
this.setState({
a: this.state.a + 1,
});
},
};
render(): React.ReactNode {
const Provider = ctx.Provider;
return (
<Provider value={this.state}>
<div>
<Test />
<Test1 />
</div>
</Provider>
);
}
}
context注意事项
- 如果上下文提供者中的value属性发生变化,会导致该上下文提供的所有后代元素全部重新渲染,无论该子元素是否有优化( 无论 shouldComponentUpdate 函数返回什么结果 ),实际上不会运行该函数
PureComponent
纯组件,用于避免不必要的渲染(运行render函数),从而提升效率
优化: 如果一个组件的属性和状态都没有发生变化,该组件时没必要渲染的,这一点可以使用 shouldComponentUpdate 实现
- PureComponent 是一个组件,如果某个组件继承自该组件,则该组件的 shouldComponentUpdate 会进行优化,
- 会对属性和状态进行浅比较,如果相等则不会重新渲染
- 可以自己实现shouldComponentUpdate ,然后让其他组件继承,实现 PureComponent
class Test extends React.PureComponent {
render(): React.ReactNode {
return <div>{this.props.a}</div>;
}
}
React.memo
-
由于函数组件没有生命周期,无法通过 shouldComponentUpdate 控制渲染,
-
React.memo是一个高阶组件,简单理解可以是内部通过class组件的 shouldComponentUpdate 控制传入的组件更新
-
function Memo(FunComp) { return class Memo extends PureComponent { render(): React.ReactNode { return <>{FunComp(this.props)}</>; } }; }
-
-
源码实现
-
function memo(Component, compare) { // 返回一个新的组件 return function MemoizedComponent(props) { // 比较前后 props 是否相等 if (compare) { if (compare(prevProps, props)) { return prevResult; } } else { if (shallowEqual(prevProps, props)) { return prevResult; } } // 如果 props 有变化,重新渲染组件 const result = Component(props); prevProps = props; prevResult = result; return result; }; }
-
复用节点(跳过渲染)
- react中通常会根据props和state进行浅比较判断是否有更新,复用更新的实现在源码里面是通过复用fiber节点来实现
- 复用fiber节点后意味着render函数不会执行,而是直接复用上一次的渲染结果
源码实现
function beginWork(current, workInProgress, renderLanes) {
if (current !== null) {
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (oldProps === newProps && /* 其他条件 */) {
// 跳过渲染,复用上一次的结果
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
}
}
// 继续渲染组件
// ...
}
Render props
- 某个组件需要某个属性,该属性是一个函数,函数的返回值用于渲染
- 函数的参数会传递为需要的参数
- 需要纯组件的属性(尽量避免每次传递的render是一致的)
import React, { useState } from "react";
function Test1(props) {
const [state, SetState] = useState(11111);
return <>{props.children(state)}</>;
}
class App extends React.Component {
render() {
return (
<div>
{
<Test1>
{(value) => {
return <div>{value}</div>;
}}
</Test1>
}
</div>
);
}
}
export default App;
setState原理
-
react批量更新分为 收集 和 触发 两个阶段
- 生成变更任务 => 存储更新到fiber节点 => 调度更新
- 每次的setState都会产生一个更新任务,react会将更新任务存储起来,并不是每一次变更都会触发渲染,通过调度器合并一次变更中优先级相同的变更,合并成一次渲染任务
-
调用setState的时候往当前filber节点上打一个标记,然后一直向上冒泡传递到顶部节点都会打上更新标记,
- 当构建fiber树的时候,就可以根据这个标记,找到真正需要重新生成的fiber节点,
- 没有更新标识的节点直接复用之前的节点,然后更新fiber节点到页面
-
更新任务会被收集成一个队列(updateQueue.pending)(用链表是方便任务变更顺序和结束标记)
-
多次setState会产生多个update(更新任务,react会把update通过环形链表的形式关联起来,每个update都有一个next指向下一个update,这个环形链表存储在fiber节点上
-
使用pending保存的永远是最后一个任务,通过next就能拿到第一个
-
在fiber构建中class实例会有一个属性指向它的filber节点,将两者关联起来
-
filberNode:{ updateQueue:{ lane:null, // 任务优先级 pending:null } }
-
-
调度器会读取更新队列里面的任务拿到优先级最高的渲染任务,和当前存在的渲染任务进行优先级对比,如果一样则进行合并,没有的优先级高于或等于当前的替换渲染任务为当前的,并且存储更新任务,等待执行
数据批量更新实际场景
-
多次触发,直接使用触发的初始值一致都是
-
this.state = {n : 0} this.setState({n:this.state.n + 1}) this.setState({n:this.state.n + 1}) this.setState({n:this.state.n + 1}) // 结果等于1 -
使用函数形式触发,获取上一次的值
-
this.state = {n : 0} this.setState((pre) => {n:pre.n + 1}) this.setState((pre) => {n:pre.n + 1}) this.setState((pre) => {n:pre.n + 1}) //结果等于3 -
在set之后的回调获取state
-
this.setState({n:this.state.n + 1},() => { console.log(this.state.n) }) // 1 -
在setState第二个参数是在state变更完成,页面渲染完成之后才会触发,('render','componentDidUpdate') 之后
-
在react中触发setState,会放入执行队列中,会把当前的值放入执行队列中做为默认值,然后挨个执行,但是state会在函数结束时才会更新,在set一次后继续set则拿到的还是未变的值,但是如果是使用函数的形式,react会把上一次设置后的结果传入到函数中,这样能够拿到最最新的值
class组件生命周期
- 是指组件从诞生到销毁经历的一系列的过程,该过程叫做声明周期,react在组件的声明周期运行期间提供了一系列的钩子函数(类似于自动触发事件),可以让开发者在函数中编写代码,在声明周期钩子函数触发时运行
- 生命周期函数仅存在于类组件,函数式组件每次调用都是重新运行函数,旧的组件会被销毁
- 初始化阶段
- constructor
- 初始化状态和属性,同一个组件只会创建一次,也就是会触发一次
- 不能在该函数中使用setState,因为此时组件的更新队列还没创建,无法往里面增加更新任务
componentWillMount - 已过期- 和构造函数一样,只会运行一次
- 可以使用setState,但是不允许使用,因为在fiber可中断的情况下,该函数可能会被调用多次
- render
- 返回的react元素会被挂载到虚拟dom树,并且显示到页面上
- 每次更新渲染都会重新运行
- 在render里面调用
setState会反复渲染,导致递归渲染,导致页面崩溃
- componentDidMount
- 挂载完成函数,只会执行一次,可以使用setState
- 通常会将网络请求,启动计时器等一开始操作放入该函数
- constructor
- 组件进入活动状态,待机状态,等待组件状态或者属性发生变化
- 更新阶段 - 属性或者状态变化
componentWillReceiveProps - 已过期- 当属性值改变才会触发
- 即将接收到新的属性值,当前属性值还没被改变
- shouldComponentUpdate
- 状态和属性值发生变化时触发
- 指示react是否需要重新渲染组件(重新调用render方法),通过返回true或者false来指定
- 必须要设置返回值,true表示需要更新
componentWillUpdate - 已过期- 组件即将被重新渲染
- render
- componentDidUpdate
- 已完成重新渲染
- 销毁阶段 - 从dom树移除
- componentWillUnmount
- 组件被销毁时触发,通常在该函数销毁定时器
- componentWillUnmount
- 新版声明周期,将几个will的生命周期钩子设为过期提示,不建议后续使用,提供了几个新的钩子
- getDerivedStateFromProps
- 状态和属性更新时触发,返回值可以决定状态变更后的数据
- getSnapshotBeforeUpdate
- 真实的dom构建完成,还未实际渲染到页面中
- 可以在该函数实现一些额外的dom操作
- 该函数的返回可以作为 componentDidUpdate 的第三个参数
- getDerivedStateFromProps
children - 传递元素内容
react 有两种传递元素到子组件显示的方式
-
使用自定义属性作为传递值,
-
<Test html={<div>2222</div>} />; function Test(props) { return <div>{props.html}</div>; }
-
-
使用语法糖,将组件内容作为参数,会被放置到props的children属性中
-
<Test> <div>2222</div> </Test> function Test(props) { return <div>{props.children}</div>; }
-
-
使用React.cloneElment 进行属性透传
-
class Connect extends React.Component { constructor(props) { super(props); this.state = { a: 1, }; } render() { return ( <> {React.cloneElement(this.props.children, { ...this.props, ...this.state, })} </> ); } }
-
插槽 - Portals
- 将一个React元素渲染到指定的DOM容器中
- 真实dom树和虚拟dom树可以有差异
- 它的事件冒泡是根据虚拟dom树冒泡
import ReactDOM from "react-dom";
function ChildB() {
return <div className="child-b"></div>;
}
function ChildA() {
return ReactDOM.createPortal(
<div className="child-a">
<ChildB />
</div>,
document.querySelector(".modal")!
);
}
function App() {
return (
<div className="App">
<ChildA />
</div>
);
}
export default App;
错误边界捕捉
默认情况下,若一个组件在渲染期间(render) 发生错误,会导致整个组件树全部被卸载
错误边界: 是一个组件,该组件会捕获到渲染期间(render) 发生错误,并且可以阻止错误继续传播
- 书写生命周期函数
getDerivedStateFromError,静态函数,- 在渲染子组件发生错误时,在更新页面之前 运行
- 只有子组件发生错误才会运行
- 该函数返回一个对象,react会将该对象的属性覆盖调state中同名属性,方便更新视图
- 该函数接收的参数是错误对象
- 编写声明周期函数
componentDidCatch(error,info),实例方法- 是在渲染子组件发生错误时,在更新页面之后运行
- 由于时间比较靠后,如果在该函数中改变状态,会出现组件树销毁又重新构建,推荐使用 getDerivedStateFromError, 在销毁之前拦截
- 该函数通常用于记录错误信息
class ErrorComp extends React.Component {
state = {
hasError: false,
};
static getDerivedStateFromError() {
return {
hasError: true,
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.log(error, errorInfo);
}
render(): React.ReactNode {
return <div>11</div>;
}
}
React.strictMode
本质是一个组件,该组件不进行ui渲染,他的作用是在渲染内部组件时,发现不合适的代码
- 识别不安全的声明周期
- 关于使用过时字符串 ref API 的警告
- 关于使用废弃的 findDOMNode 方法的警告
- 检测意外的副作用
- 一个函数中做了一些影响函数外部数据的事情
- 异步处理
- 改变参数值
- setState
- 本地存储
- React 要求,副作用代码仅出现在以下声明周期函数中
- componentDidMount
- componentDidUpdate
- componentWillUnmount
- 一个函数中做了一些影响函数外部数据的事情
- 检测过时的 context API
Profiler
在react-devtools 中使用,分析某一个或多次提交涉及到组件的渲染时间
useState
- 第n次调用useState
- 检查该节点的状态数组是否存在下标n
- 如果不存在
- 使用一个默认值创建一个状态
- 将该状态加入到状态数组中,下标为n
- 存在的情况
- 忽略掉默认值
- 直接得到状态值
注意
- 如果是使用同一个函数组件,内部的状态也不会互相干扰,因为他们的状态是挂载对应的节点上的,函数在使用状态会从节点上取对应的状态表格
- 最好写在函数起始位置方便阅读
- 严禁出现在代码块里面(判断,循环)
- 会破坏写入状态数组的顺序,使得后续状态获取状态不正确
- useState返回的函数,始终不变,节省内存
- 如果使用函数改变数据,若之前的数据和当前数据完全一样(object.is)判断,则不会重新渲染,已达到优化效率的目的
- 使用函数改变数据,传入的值不会和原来的数据进行合并,而是直接替换
- 如果要实现强制刷新
- 类组件 => 调用 forceUpdate ,不会触发shuldUpdate
- 调用一个空的useState
- 如果某些状态直接没有必然的联系,应该分化为不同的状态,而不要合并参与一个对象
- 和类组件的状态一样,函数组件中改变状态可能是异步的(在dom事件中),多个状态变化会合并以提高效率,此时不能信任之前的状态,而应该使用回调函数获取之前的状态以改变状态
- setState函数,在事件完成之后统一运行
Effect hook
用于在函数组件中处理副作用
- ajax请求
- 计时器
- 其他异步操作
- 更改真实dom对象
- 本地存储
- 会对其他外部产生影响的操作
细节
- 副作用函数的执行时间点,是在页面发生变化之后(渲染之后),因此他的执行是异步的,不会阻塞浏览器
- 和componentDidMount和componentUpdate的区别是,更新改了真实的dom,但是用户还没看到页面完成更新,
- useEffect是更新了真实dom,并且用户已经看到了ui更新
- 每个函数组件中可以多次使用useEffect,不能放入判断或者循环等代码块中(等同于useState,他也会创建一个状态表格)
- useEffect中的副作用函数,可以有返回值,返回值必须是一个函数,该函数叫清理函数
- 该函数的运行时间点,在每次运行副作用函数之前
- 首次渲染不会运行
- 第二次执行 会先执行清理函数,再执行副作用函数
- 组件被销毁时一定会运行
- useEffect函数可以传递第二个参数
- 第二个参数是一个数组
- 数组中记录该副作用的依赖数据
- 当数组重新渲染后,只有依赖数据与上一次不一样时,才会执行副作用
- 所以当传递了依赖数据之后,如果数据没有发生变化
- 副作用函数仅在第一次渲染后执行
- 清理函数仅在卸载组件后执行
- 副作用函数中,如果使用了函数上下文中的变量,则由于闭包的影响,会导致副作用函数中变量不会实时变化
- 副作用函数在每次注册时,会覆盖之前的副作用函数,因此尽量保持副作用函数稳定,不然控制起来比较麻烦
-
function odd() { } function even() {} useEffect(n % 2 == 0 ? even : odd)
自定义hook
- 将一些常用的,跨越多个组件的hook功能,抽离出去形成一个函数,该函数就是自定义hook
- 因为自定义hook就是一个函数,当组件每次调用的时候也会调用该函数,触发自定义hook内部的hook
- 自定义hook跟官方提供的hook一样,只能写在组件的最顶层
import { useEffect, useState } from "react";
function getData() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 2000);
});
}
function useLogin() {
const [login, setLogin] = useState(false);
useEffect(() => {
async function loginIn() {
const res = await getData();
setLogin(res);
}
loginIn();
}, []);
return [login];
}
function App() {
const [login] = useLogin();
return <div className="App">{login ? "已经登录" : " 没登录"}</div>;
}
export default App;
实现是个定时器hook执行操作
import { useEffect } from "react";
function useTimer(time, callback) {
useEffect(() => {
const timer = setInterval(() => {
callback();
}, time);
return () => {
clearInterval(timer);
};
}, []);
}
function App() {
useTimer(1000, () => {
console.log("timer");
});
return <div className="App">111</div>;
}
export default App;
useContext
通过 React.useContext 直接获取Context里面的内容
import React from "react";
const ctx = React.createContext<any>(null);
function Test() {
const value = React.useContext(ctx);
return <div>{value.a}</div>;
}
function App() {
return (
<div className="App">
<ctx.Provider value={{ a: 11 }}>
<Test />
</ctx.Provider>
</div>
);
}
export default App;
useCallBack
- 用于得到一个固定引用值的函数,通用用它做进行性能优化
- 传入给组件的属性为函数时,函数的地址每次渲染都发生了变化, 会导致子组件跟着重新渲染
- useCallBack接收两个参数,
- 参数一是一个函数,使用useCallback会固定该函数的引用,只要依赖项没有发生变化,则始终返回之前的函数地址
- 参数二是依赖数组
- 该函数返回的是相对固定的函数引用地址
import React from "react";
const Test = React.memo(function (props) {
console.log("更新");
return <div>test</div>;
});
function App() {
const [state, setState] = React.useState(222);
const handle = React.useCallback(() => {
console.log(state);
}, []);
return (
<div className="App">
<Test click={handle} />
<button
onClick={() => {
setState(state + 1);
}}
>
点击
</button>
</div>
);
}
export default App;
useMemo
- 用于保持那些需要经过高开销的计算才能得到的值
- 比如说要根据一个数据渲染庞大的ui,这个时候就可以使用memo缓存结果,在其他的state更新的时候,只要依赖项没有更新的情况,不会影响memo的缓存结果
import React from "react";
function App() {
const [state, setState] = React.useState(
new Array(222).fill("1").map((el, i) => i)
);
const [count, setCount] = React.useState(0);
const list = React.useMemo(() => {
return (
<>
{state.map((el) => (
<div key={el}>{el}</div>
))}
</>
);
}, [state.length]);
return (
<div className="App">
{list}
<button
onClick={() => {
setCount(count + 1);
}}
>
点击
</button>
</div>
);
}
export default App;
useRef
- useRef函数接受一个参数做为默认值
- 返回一个固定的对象
{current:值},在每次状态更新重新执行函数的时候,ref不会更新会保留之前的值 - ref的更新不会触发视图的更新
import { useEffect, useRef, useState } from "react";
function App() {
const timerRef = useRef<any>(null);
const [count, setCount] = useState(0);
useEffect(() => {
console.log("定时器执行", count);
timerRef.current = setTimeout(() => {
setCount(count + 1);
}, 1000);
}, [count]);
return (
<div className="App">
<button
onClick={() => {
clearTimeout(timerRef.current);
timerRef.current = null;
}}
>
点击清除定时器{count}
</button>
</div>
);
}
export default App;
useImperativeHandle
- 用于函数式组件暴露ref属性给外部使用
import { forwardRef, useRef, useImperativeHandle } from "react";
const Test = forwardRef(function (props, ref) {
// 该函数是第一次加载组件调用
useImperativeHandle(
ref,
() => {
// 如果不给依赖项,则每次运行函数组件都会调用该方法
// 如果使用了依赖项,则第一次调用后会进行缓存,后续是依赖项变化才会触发
// 相当于给ref current 赋值为1
console.log("imp");
return {
method() {
console.log("test");
},
};
},
[]
);
return <div ref={ref}>test</div>;
});
function App() {
const testRef = useRef(null);
return (
<div className="App">
<button
onClick={() => {
testRef.current.method();
}}
>
点击
</button>
<Test ref={testRef} />
</div>
);
}
export default App;
useLayoutEffect
- 在浏览器渲染之前执行, class组件 中
componentDidMount , componentDidUpdate触发时机一样 - useEffect 触发在浏览器渲染完成之后
function App() {
const [count, setCount] = useState(0);
useLayoutEffect(() => {
console.log("layoutEffect");
}, [count]);
useEffect(() => {
console.log("effect");
}, [count]);
return (
<div className="App">
<button
onClick={() => {
setCount(count + 1);
}}
>
点击
</button>
</div>
);
}
useDebugValue
用于将自定义hook的关联数据显示到控制栏
function useTest() {
const [test, setTest] = useState(0);
// 暴露一个名称 提示当前hook
useDebugValue("testHook");
return [test];
}
function App() {
const [count, setCount] = useState(0);
const [count1, setCount1] = useState(0);
const [test] = useTest();
return (
<div className="App">
<button>点击</button>
</div>
);
}
路由概念
- 无论是使用vue 还是react, 开发的单页应用程序,可能都是某个站点的一部分(某一个功能块)
- 一个单页应用里,可能会划分多个页面(几乎完全不同的页面效果)
- 如果在单页应用中完成组件的切换.需要实现下面两个功能
- 根据不同的页面地址,展示不同的组件
- 完成无刷新的地址切换
- 如果实现了以上两个功能的插件称之为路由
react-router
- react-router : 路由核心库,包含诸多和路由功能相关的核心代码
- react-router-dom : 利用路由核心库,结合实际的页面,实现跟页面路由密切相关的功能
如果是在页面中实现路由,更多需要安装 react-router-dom
路由模式
-
hash router
- 根据url地址中的哈希值来确定显示的组件
- 原因: hash的变化不会导致页面刷新(不会重新请求这个html文件)
- 这种模式的兼容性最好
-
borswer history router 浏览器历史记录路由
-
html出现后新增了history Api,从此之后浏览器拥有了改变路径不再刷新页面的方式
-
history 表示浏览器的浏览记录
-
history.length 获取页面栈长度,跟窗口绑定
-
history.pushState 向当前历史记录栈中加入一条新的记录
-
参数一 附加数据,可以是任何类型,history对象state属性值
-
参数二 页面标题
-
参数三 新的页面地址
-
history.pushState('ces',null,'/a/b') -
history.replaceState 替换当前路由 ,参数和pushState一样
-
-
根据页面路径来决定渲染那个组件
-
需要后端支持,因为当用户直接访问深层路由或刷新页面时,浏览器会向服务器发送请求,如果后端没有正确配置,会导致 404 错误
-
import { createHashRouter, RouterProvider } from "react-router-dom";
import Docs from "@/router/docs";
import Dashboard from "@/router/dashboard";
import GuideLayout from "@/layouts/guideLayout";
import CompLayout from "@/layouts/compLayout";
import Button from "@/pages/button";
import Empty from "@/pages/empty";
import "./App.less";
const router = createHashRouter([
{
path: "/",
element: <Dashboard />,
},
{
path: "/docs",
element: <Docs />,
children: [
{
path: "guide",
element: <GuideLayout />,
},
{
path: "comp",
element: <CompLayout />,
children: [
{
path: "button",
element: <Button />,
},
{
path: "empty",
element: <Empty />,
},
],
},
],
},
]);
function App() {
return <RouterProvider router={router} />;
}
export default App;
import { Outlet, useNavigate } from "react-router-dom";
import "./compLayout.less";
const GuideLayout = () => {
const navigate = useNavigate();
const changeRoute = (path: string) => {
navigate(path);
};
return (
<div className="guideLayout">
<div className="guideLayoutNav">
<div onClick={() => changeRoute("/docs/comp/empty")}>跳转empty</div>
<div onClick={() => changeRoute("/docs/comp/button")}>跳转button</div>
</div>
<div className="guideLayoutContent">
<Outlet />
</div>
</div>
);
};
export default GuideLayout;
源码实现
Router 组件
它负责监听 URL 的变化,并将当前的 URL 传递给子组件。
- 监听 URL 变化:
Router组件会订阅 History API 的popstate事件,当 URL 变化时,更新组件的状态。 - 传递上下文:
Router通过 React 的 Context API 将当前的location和history对象传递给子组件。
class Router extends React.Component {
constructor(props) {
super(props);
this.state = {
location: props.history.location,
};
// 监听 popstate 事件
this.unlisten = props.history.listen((location) => {
this.setState({ location });
});
}
componentWillUnmount() {
// 取消监听
this.unlisten();
}
render() {
return (
<RouterContext.Provider
value={{
location: this.state.location,
history: this.props.history,
}}
>
{this.props.children}
</RouterContext.Provider>
);
}
}
Route 组件
Route 组件用于定义路由规则,并根据当前的 location 决定是否渲染对应的组件。
- 匹配 URL:
Route组件会从上下文中获取当前的location,并与自身的path属性进行匹配。 - 渲染组件:如果匹配成功,则渲染
component或render属性指定的组件。
function Route({ path, component: Component, render }) {
return (
<RouterContext.Consumer>
{(context) => {
const { location } = context;
const match = matchPath(location.pathname, { path });
if (match) {
if (Component) {
return <Component {...context} match={match} />;
} else if (render) {
return render({ ...context, match });
}
}
return null;
}}
</RouterContext.Consumer>
);
}
Link 组件
用于实现无刷新的页面跳转。
- 阻止默认行为:
Link组件会阻止<a>标签的默认跳转行为。 - 使用 History API:通过
history.push方法更新 URL,并触发Router组件的重新渲染。
function Link({ to, children }) {
return (
<RouterContext.Consumer>
{(context) => {
const { history } = context;
const handleClick = (e) => {
e.preventDefault();
history.push(to);
};
return (
<a href={to} onClick={handleClick}>
{children}
</a>
);
}}
</RouterContext.Consumer>
);
}
History对象
react-router 依赖于 history 库来管理 URL 的变化。history 库封装了浏览器 History API 的细节,并提供了统一的接口。
关键方法
- push(path): 导航到新的 URL。
- replace(path): 替换当前 URL。
- listen(callback): 监听 URL 变化。
import { createBrowserHistory } from 'history';
const history = createBrowserHistory();
history.listen((location) => {
console.log('Location changed:', location);
});
history.push('/new-path');
Redux 核心概念
redux 是facebook提出的数据解决方案,他引入了action的概念
- action 是一个普通对象,用于描述需要要什么
- 是一个平面对象,必须要有一个type属性,可以是任何类型
- reducer 处理器,用于根据action来处理数据,处理后的数据会被仓库重新保存
- store 表示数据仓库,用于存储共享数据,可以根据不同的action变更仓库中的数据
- dispatch 分发一个action
- getState 获得仓库数据
- replaceReducer 替换当前的reducer
- subscribe 注册一个监听器,监听器是一个无参函数,该分发一个action后触发,状态没有变都会触发
// reducer
import {ADD_TODO} from '../../constants/index';
const initialState: any = {
count: 0,
};
export default function todosReducer(state = initialState, action: any) {
switch (action.type) {
case ADD_TODO: {
return {
...state,
count: state.count + action.num,
};
}
default:
return state;
}
}
// action
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
// store
import {legacy_createStore as createStore} from 'redux';
import rootReducer from './reducers/index';
const store = createStore(rootReducer);
export default store;
// 调用store
store.getState() // 得到store当前的数据
store.dispatch({type:ADD_TODO}) // 触发action
手写createStore
function isPlainObject(obj) {
if (typeof obj !== 'object') { return false }
return Object.getPrototypeOf(obj) === Object.prototype
}
function getRandomString(length) {
return Math.random().toString(36).substring(2, length)
}
/**
* 实现createStore的功能
* @param {*} reducer
* @param {*} defaultState
* @param (*) enhanced 增强函数 中间件
*/
function createStore(reducer, defaultState, enhanced) {
//enhanced 表示appleMiddleWare 返回的函数
if (typeof defaultState === 'function') {
enhanced = defaultState
defaultState = undefined
}
if (typeof enhanced === 'function') {
// 进入appleMiddleWare处理逻辑
return enhanced(createStore)(reducer, defaultState)
}
let currentReducer = reducer, currentState = defaultState
const listeners = []
function dispatch(action) {
if (!isPlainObject(action)) {
throw new TypeError('action muse be a plain object')
}
if (!action.type) {
throw new TypeError('action muse has a property of type')
}
currentState = currentReducer(currentState, action)
for (const listener of listeners) {
listener()
}
}
function getState() {
return currentState
}
function subscribe(listener) {
listeners.push(listener)
let isRemove = false
return function () {
if (isRemove) {
return
}
// 将listener移除
const index = listeners.indexOf(listener)
listeners.splice(index, 1)
isRemove = true
}
}
dispatch({
type: `@@redux/init${getRandomString(7)}`
})
return {
dispatch,
getState,
subscribe
}
}
const store = createStore(function todosReducer(state = {}, action) {
switch (action.type) {
case 'add': {
return {
...state,
count: state.count + action.num,
};
}
default:
return state;
}
}, {
count: 0
})
store.dispatch({
type: "add",
num: 1
})
const unLink = store.subscribe(() => {
console.log('store更新')
})
console.log(store.getState())
store.dispatch({
type: "add",
num: 1
})
unLink()
store.dispatch({
type: "add",
num: 1
})
手写bindActionCreators
/**
* 得到一个自动分发的action创建函数
*/
function bindActionCreators(actionCreators, dispatch) {
if (typeof actionCreators == 'function') {
return getAutoDispatchActionCreator(actionCreators, dispatch)
}
else if (typeof actionCreators == 'object') {
const result = {}
for (const key in actionCreators) {
if (Object.prototype.hasOwnProperty.call(actionCreators, key)) {
const actionCreator = actionCreators[key]
if (typeof actionCreator == 'function') {
result[key] = getAutoDispatchActionCreator(actionCreator, dispatch)
}
}
}
return result
}
else {
throw new TypeError('actionCreators must be an object or function which meas action creator')
}
}
function getAutoDispatchActionCreator(actionCreator, dispatch) {
return function (...args) {
const action = actionCreator(...args)
dispatch(action)
}
}
手写combineReducers
function isPlainObject(obj) {
if (typeof obj !== 'object') { return false }
return Object.getPrototypeOf(obj) === Object.prototype
}
function validateReducers(reducers) {
if (typeof reducers !== 'object') {
throw new TypeError('reducers must be an object')
}
if (!isPlainObject(reducers)) {
throw new TypeError('reducers must be an object')
}
// 验证Reducer的返回结果是不是undefined
for (const key in reducers) {
if (Object.prototype.hasOwnProperty.call(reducers, key)) {
const reducer = reducers[key]
// 传递特殊的type值
let state = reducer(undefined, {
type: ActionTypes.INIT()
})
if (state == undefined) {
throw new TypeError('reducers must not return undefined')
}
// 传递特殊的type值
state = reducer(undefined, {
type: ActionTypes.UNKNOWN()
})
if (state == undefined) {
throw new TypeError('reducers must not return undefined')
}
}
}
}
/**
* 组装reducers 返回一个reducer,数据使用一个对象表示,对象的属性名与传递的参数对象保持一致
* 返回的是一个reducer 函数,调用该函数会在内部按顺序调用Reducer拿到所有Reducer的返回值后合并返回
*/
function combineReducers(reducers) {
validateReducers(reducers)
return function (state = {}, action) {
const newState = {} // 要返回的新的状态
for (const key in reducers) {
if (Object.prototype.hasOwnProperty.call(reducers, key)) {
const reducer = reducers[key]
newState[key] = reducer(state[key], action)
}
}
return newState
}
}
redux中间件
- 中间件: 类似于插件,可以在不影响原本功能.并且在不改动原本代码的基础上,对其功能进行增强
- 在redux中,中间件只要是用于增强dispatch函数
- 实现redux中间件的基本原理,是更改仓库中的dispatch函数
基本原理
const oldDispatch = store.dispatch //保留原始的dispatch
// 后续出现多个中间件要操作的还是store.dispatch
// 修改原始的dispatch
store.dispatch = function (action) {
console.log('中间件1' )
console.log('旧数据', store.getState())
console.log('action', action)
oldDispatch(action)
console.log('新数据', store.getState())
}
oldDispatch = store.dispatch
store.dispatch = function (action) {
console.log('中间件2' )
oldDispatch(action)
}
redux中间件书写
-
中间件本身是一个函数,该参数接收一个store参数,表示创建的仓库,该仓库并非一个完整的仓库对象,仅包含getState,dispatch,该函数运行的时间,是在仓库创建之后运行
-
由于创建仓库后需要自动运行设置的中间件函数,因此需要再创建仓库时,告诉仓库有哪些中间件
-
需要使用 appleMiddleware 函数
- 将函数的返回结果做为 createStore 的第二 或者第三个参数
- createStore内部会判断第二个参数是默认值还是中间件函数
-
中间件函数必须返回一个dispatch创建函数
-
返回的函数需要有一个参数dispatch,
-
中间件函数是逆向创建 ,正向执行,每次修改后吧修改后的dispatch传递给下一个,直到中间件执行完毕后才修改store的dispatch
-
从后往前执行 创建好对应的中间件函数,然后从头往后执行创建的函数,顺序就是书写顺序了
-
// 这里拿到的是原始的store function logger2(store) { console.log('logger2') // 创建 中间件函数 // next 代表的时候是上一步修改后的dispatch函数 return function (next) { // 下面返回的函数 是最终要应用的dispatch函数 会替换store里面的dispatch函数 return function dispatch(action) { console.log('旧数据', store.getState()) console.log('action', action) next(action) console.log('新数据', store.getState()) } } } // 简化写法 const logger1 = store => next => action => { console.log('旧数据', store.getState()) console.log('action', action) next(action) console.log('新数据', store.getState()) }
-
-
appleMiddleware 函数 用于记录有些中间件,他会返回一个函数
-
该函数用于记录创建仓库的方法,然后返回一个函数
-
后续返回的函数是创建仓库的函数
-
有点像柯里化的操作, 每次都是固定一个参数,最后执行
-
appleMiddleWare(logger1,logger2)(createStore)(reducer) -
middleWare的本质是一个调用后可以得到dispatch创建函数的函数
-
手写appleMiddleWare
/**
* 注册中间件函数
* @param {...any} middleWares
* @returns {} 创建仓库的函数
*/
unction applyMiddleWare(...middleWares) {
return function (createStore) {
// 下面的函数用于创建仓库
return function (reducer, defaultState) {
const store = createStore(reducer, defaultState)
let dispatch = () => {
throw new Error('不能使用')
}
const simpleStore = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
// 根据中间件数组,得到一个dispatch创建函数的数组
const dispatchProducers = middleWares.map(el => el(simpleStore))
dispatch = compose(...dispatchProducers)(store.dispatch)
return {
...store,
dispatch
}
}
}
}
副作用处理中间件
redux-thunk
- thunk允许action是一个带有副作用的函数,当action 被分发时,会阻止action继续向后提交,会直接调用函数
- thunk需要函数中传递三个参数
- dispatch : 来自于store.dispatch
- getState : 来自于 store.getState
- extra:来自于用于设置的额外参数
- thunk会拦截副作用函数的action,不继续往下执行,但是action内部执行的dispatch当是正常的平面对象时就会走正常的dispatch
- 需要改动action,可接收action是一个函数
// 简单源码实现
function createThunkMiddleWare(extra) {
return store => next => action => {
if (typeof action == 'function') {
return action(store.dispatch, store.getState, extra)
} else {
return next(action)
}
}
}
const thunk = createThunkMiddleWare()
thunk.withExtraArgument = createThunkMiddleWare
export default thunk
redux-promise
- 如果action 是一个promise,则会等待promise完成,将完成的结果作为action触发
- 如果 action 不是一个promise,则判断其payload是否是一个promise,
- 如果是promise,则等待promise完成,然后将得到的结果作为payload 的值触发
- 需要改动action,可接收的action是一个promise对象,或action的payload是一个promise对象
function reduxPromise({ dispatch }) {
return next => action => {
// 不是标准的action
if (!isFSA(action)) {
// 如果action是promise ,则将其resolve的值dispatch,否则调用next
return action.then ? action.then(dispatch) : next(action)
}
return action.payload.then ?
action.payload.then(payload => dispatch({ ...action, payload }))
.catch(err => dispatch({ ...action, payload: error, error: true })) :
next(action)
}
}
redux-saga
-
以上两个中间件,会导致action或action创建函数不再纯净,
- redux-saga将解决这样的问题,它不仅可以保持action,action创建函数,reducer的纯净,而且可以使用模块化的方式解决副作用
-
在最开始的时候启动一个saga任务, saga任务提供了一些功能,这些功能是以指令的形式出现,而且出现在yield的位置,因此可以被saga中间件控制它的执行
- saga任务是一个生成器函数
-
在saga任务中,如果yield了一个普通数据,saga不做任何处理,仅仅将数据传递给yield表达式(把得到的数据放到next的参数中),因此在saga中.yield一个普通数据没什么意义,
-
saga需要再yield后面放上一些合适的saga指令,如果放的是指令(saga effect),会根据指令执行不同的操作,来控制整个任务的流程
-
每个指令本质上就是一个函数,该函数调用后,会返回一个指令对象,saga会接收到该指令对象,进行各种处理
-
一旦saga任务完成(生成器函数完成),则saga中间件一定结束
-
saga不会阻止action的传递,只是控制自己的saga任务
-
const sagaMid = createSagaMiddleWare() // 得到一个saga中间件 function* saga() { const action = yield take('add') // 监听action的type console.log('2222', action) yield delay(2000) } const store = createStore(reducer, appleMiddleWare(sagaMid)) sagaMid.run(saga) // 启动saga任务
-
-
指令集合
-
take [阻塞]
-
用来监听某个action,如果action发生了,则会进行下一步处理,take指令仅监听一次,yield得到是完整的action对象
-
yield take('add') // 对action的做监听,相当于一次watch
-
-
all [阻塞]
-
传入一个生成器数组,saga会等待所有的生成器全部完成后才会进一步处理
-
一般在入口saga做合并使用
-
sagaMid.run(rootSaga) function *rooSaga() { yield UserSaga() yield LoginSaga() }
-
-
takeEvery
-
不断地监听某个action,当某个action到达之后,运行一个函数
-
takeEvery永远不会结束当前的生成器
-
yield take('add') // 对action的做监听,相当于持久watch
-
-
delay [阻塞]
-
阻塞指令的毫秒数,延迟触发
-
yield delay(2000) // 等待2秒
-
-
put
-
相当于dispatch一个action,用于重新触发一个action
-
yield put({ type: 'add' }) -
如果yield的返回值是promise,他会自动等待promise完成,会把完成的结果作为值传递到下一次next
-
如果promise对象出现reject,会使用generator.throw抛出错误,可以使用try捕获
-
const res = yield Promise.resolve(2000) yield put({ type: 'add', payload: { data: res } })
-
-
-
call [看情况阻塞]
-
使用指令的形式主动调用函数,有点类似于js的call方法
-
const res = yield call(test,12,3) // 绑定this const res = yield call([target,test],12,3)
-
-
apply [看情况阻塞]
-
同call一样,调用函数,同js的apply一样
-
const res = yield call(this,test,[12,3])
-
-
select
-
用于得到仓库中数据
-
const res = yield select() // 使用函数筛选数据 const res = yield select(state => state.count)
-
-
cps
- 回调函数的写法转为异步的形式
-
fork
-
开启一个新的任务,该任务不会阻塞,该函数需要传递一个生成器函数,返回了一个对象,类型为Task
-
相当于开了一个新的线程,不阻塞主的saga任务
-
function* saga() { const res = yield fork(test) yield put({ type: 'add', payload: { data: res } }) } // 主saga先执行,fork不阻塞 function* test() { yield delay(2000) }
-
-
cancel
-
用于取消一个或多个任务,使用generator.return实现
-
function* test() { let task; while (true) { yield take('add') if (task) { yield cancel(task) } task = yield fork(function* () { yield delay(2000) yield put({ type: "add" }) }) } }
-
-
takeLastest
- 功能和takeEvery一样,只不过会自动取消之前开启的任务
-
cancelled
- 判断当前任务线是否被取消掉了
-
race [[阻塞]]
-
可以传递多个指令,当其中任何一个指令结束后,会直接结束,与Promise.race类似 ,
-
返回的结果是最先完成的指令结果,并且该函数会自动取消其他任务(开启多个任务,完成后终止其他任务)
-
const res = yield race({ action: call(asyncAction), action1: call(asyncAction) })
-
-
手写redux-saga
- 首先启动一个任务
- 当action触发时,直接将action分发到下一个中间件
- runSaga: 一个函数,用于启动一个任务,一个任务的本质是一个Generator function,runSaga在内部得到该函数的Generator,并且控制生成器的每一步
// 创建发布订阅仓库
class Channel {
listeners = {}
// 创建订阅者
take(prop, func) {
if (this.listeners[prop]) {
this.listeners[prop].push(func)
} else {
this.listeners[prop] = [func]
}
}
// 触发监听
put(prop, ...args) {
if (this.listeners[prop]) {
let funcs = this.listeners[prop]
Reflect.deleteProperty(this.listeners, prop)
funcs.forEach(el => {
el(...args)
});
}
}
}
// task任务
class Task {
constructor(next, cbObj) {
this.next = next
this.cbObj = cbObj
this.cbObj.callback = () => {
this.resolve && this.resolve()
}
}
// 取消当前任务
cancel() {
this.next(null, null, true)
}
toPromise() {
return new Promise((resolve) => {
this.resolve = resolve
})
}
}
// 初始化action的type
const specialName = '@@redux-saga/IO'
// 指令的枚举
const effectTypes = {
CALL: "CALL",
TAKE: "TAKE",
FORK: "FORK",
ALL: "ALL",
DELAY: "DELAY",
PUT: "PUT",
SELECT: "SELECT",
CANCEL: "CANCEL",
TAKEEVERY: "TAKEEVERY",
}
// 创建effcet平面对象
function createEffect(type, payload) {
return {
type,
payload,
}
}
/**
* 提供了call函数,用于产生call effect
* 处理call effect
*/
function call(fn, ...args) {
const context = null, func = fn
if (Array.isArray(fn)) {
context = fn[0]
func = fn[1]
}
return createEffect(effectTypes.CALL, {
context,
fn: func,
args
})
}
// call指令运行
function runCallEffect(env, effect, next) {
const { context, fn, args } = effect
const _res = fn.call(context, ...args)
if (isPromise(_res)) {
_res.then(r => next(r).catch(err => null, err))
} else {
next(_res)
}
}
// 实现delay指令
function delay(duration) {
return call(function () {
return new Promise((resolve) => {
setTimeout(() => {
resolve()
}, duration);
})
})
}
// 实现put指令
function put(action) {
return createEffect(effectTypes.PUT, {
action
})
}
function runPutEffect(env, effect, next) {
const { action } = effect.payload
const result = env.store.dispatch(action)
next(result)
}
// 实现select指令
function select(func) {
return createEffect(effectTypes.SELECT, {
fn: func
})
}
function runSelectEffect(env, effect, next) {
let state = env.store.getState() //得到整个仓库的数据
if (effect.payload.fn) {
state = effect.payload.fn(state)
}
next(state)
}
// 实现take指令
function take(actionType) {
return createEffect(effectTypes.TAKE, {
actionType
})
}
function runTakeEffect(env, effect, next) {
const actionType = effect.payload.actionType
env.channel.take(actionType, (action) => {
//订阅函数
next(action)
})
}
// 实现fork指令
function fork(generatorFunc, ...args) {
return createEffect(effectTypes.FORK, {
fn: generatorFunc,
args
})
}
function runForkEffect(env, effect, next) {
//启动一个新的任务
const task = runSaga(env, effect.payload.fn, ...effect.payload.args)
next(task) // 当前任务不会阻塞
}
// 实现cancel指令
function cancel(task) {
return createEffect(effectTypes.CALL, {
task
})
}
function runCancelEffect(env, effect, next) {
effect.payload.task.cancel()
}
// 实现takeEvery指令
function takeEvery(actionType, func, ...args) {
return fork(function* () {
while (true) {
const action = yield take(actionType)
yield fork(func, ...args.concat(action))
}
})
}
function runTakeEveryEffect(env, effect, next) {
//启动一个新的任务
const task = runSaga(env, effect.payload.fn, ...effect.payload.args)
next(task) // 当前任务不会阻塞
}
// 实现all指令
function all(generators) {
return createEffect(effectTypes.ALL, {
generators: generators || []
})
}
function runAllEffect(env, effect, next) {
const generators = effect.payload.generators
const tasks = generators.map(g => proc(env, g))
//等到所有tasks完成
const proms = tasks.map(t => t.toPromise())
Promise.all(proms).then(v => next())
}
/**
* 该模块要是处理一个effect对象需要做那些事,根据不同的ype值做不同的处理
* @param {*} env 全局的环境对象
* @param {*} effect effect对象
* @param {*} next 下一个处理
*/
function runEffect(env, effect, next) {
switch (effect.type) {
case effectTypes.CALL:
runCallEffect(env, effect, next)
break;
case effectTypes.PUT:
runPutEffect(env, effect, next)
break;
case effectTypes.SELECT:
runSelectEffect(env, effect, next)
break;
case effectTypes.TAKE:
runTakeEffect(env, effect, next)
break;
case effectTypes.FORK:
runForkEffect(env, effect, next)
break;
case effectTypes.CANCEL:
runCancelEffect(env, effect, next)
break;
case effectTypes.TAKEEVERY:
runTakeEveryEffect(env, effect, next)
break;
case effectTypes.ALL:
runAllEffect(env, effect, next)
break;
}
}
// 为创建effect和判断effect提供支持
/**
*
* @param {*} type 有效的类型
* @param {*} payload
*/
function effectHelper(type, payload) {
if (!Object.values(effectTypes).includes(type)) {
throw new TypeError('无效的type')
}
return {
type,
payload,
[specialName]: true
}
}
/**
* 判断对象是不是effect
* @param {*} obj
* @returns
*/
function isEffect(obj) {
if (typeof obj !== 'object') {
return false
}
if (obj?.[specialName]) {
return true
}
return false
}
/**
* 开启一个函数
* @param {*} env 全局的环境数据,被saga执行期共享的数据
* @param {*} generatorFunc 生成器函数
* @param {*} args 生成器函数的参数
*/
function runSaga(env, generatorFunc, ...args) {
const iterator = generatorFunc()
if (isGenerator(iterator)) {
return proc(iterator)
} else {
}
}
// 执行一个iterator
function proc(env, iterator) {
const cbObj = {
callback: null
}
/**
* @param {*} nextValue 正常调用iterator.next时传递的值
* @param {*} err 错误对象
* @param {*} isOver 是否结束
*/
function next(nextValue, err, isOver) {
// 情况1 调用iterator.next(nextValue)
// 情况2 调用iterator.throw(err)
// 情况3 调用iterator.return()
let result; // 记录迭代的结果 {value:xxx,done:false}
if (err) {
result = iterator.throw(err)
} else if (isOver) {
result = iterator.return()
cbObj.callback && cbObj.callback()
} else {
result = iterator.next(nextValue)
}
const { value, done } = result
// 结束了
if (done) {
cbObj.callback && cbObj.callback()
return
}
//判断是不是effect
if (isEffect(value)) {
runEffect(env, value, next)
} else if (ifPromise(value)) {
// 情况1 value是一个promise
value.then(r => next(r)).catch(err => next(null, err))
} else {
// 情况2 其他情况直接下一步
next(value)
}
}
return new Task(next, cbObj)
}
// 核心出口
function createSagaMiddleWare() {
function sagaMiddleWare(store) {
const env = {
store,
channel: new Channel() // 全局唯一的
}
sagaMiddleWare.run = runSaga.bind(null, env)
return function (next) {
return function (action) {
const res = next(action)
// 发布
env.channel.put(action.type, action)
return res
}
}
}
return sagaMiddleWare
}
// 调用中间件
const middle = createSagaMiddleWare()
function* test1() {
while (true) {
yield take('add')
yield delay(1000)
yield put({ type: 'add' })
}
}
function* test() {
yield '222'
let task = yield fork(test1, 123, 33)
console.log('saga运行结束')
}
// 会被redux中间件调用一次,此时run就有值了
middle()
middle.run(test)
redux-actions
createAction
该函数用于帮助你创建一个action creator
import { createAction } from 'redux-actions'
// 使用
const increase = createAction(Symbol('increase'))
// 简单实现
// 接收获取payload的参数
function myCreateAction(type, payloadCreator) {
return function actionCreator(...args) {
if (typeof payloadCreator === 'function') {
const payload = payloadCreator(...args)
return {
type,
payload
}
}
return {
type
}
}
}
createActions
创建多个action creator,以对象格式返回
const actions = createActions({
['INCREASE']: null,
['DECREASE']: null,
['asyncDECREASE']: null,
['asyncINCREASE']: null,
['ADD']: v => v,
})
/*
产物
会变成小驼峰命名
{
increase:fn,
decrease:fn,
asyncDecrease:fn,
asyncIncrease:fn,
add:fn(v),
}
*/
// 简单手写
function myCreateActions(mapToActionCreators) {
const result = {}
for (const prop in mapToActionCreators) {
const payloadCreator = mapToActionCreators[prop];
const actionCreator = (...args) => {
if (typeof payloadCreator === 'function') {
return {
type: prop,
payload: payloadCreator(...args)
}
} else {
return {
type: prop
}
}
}
actionCreator.toString = () => {
return prop
}
const propName = toSmallCamel(prop)
result[propName] = actionCreator
}
return result
}
function toSmallCamel(str) {
return str.split("_").map((s, i) => {
s = s.toLowerCase()
if (i !== 0 && s.length >= 1) {
s = s[0].toUpperCase() + s.substr(1)
}
return s
}).join('')
}
handleAction
简化针对单个action类型的reducer处理,当它匹配到对应的action类型后,会执行对应的函数
import { handleAction } from 'redux-actions'
const reducer = handleAction('INCREASE', (state, action) => {
return state + 1
}, 10)
// 等于以下写法
function reducer(state = 10, { type, payload }) {
switch (type) {
case 'increase':
return state + 1;
default:
return state
}
}
// 源码实现
function handleActions(reducerMap, defaultState) {
return (state = defaultState, action) => {
for (const key in reducerMap) {
if (key.split('|').includes(action.type)) {
const reducer = reducerMap[key];
return reducer(state, action);
}
}
return state;
};
}
handleActions
简化针对多个action类型的做处理
import { handleActions } from 'redux-actions'
const reducer = handleActions({
['INCREASE']: (state) => state + 1,
['DECREASE']: (state) => state - 1,
['ADD']: (state, action) => state + action.payload,
}, 5)
// 源码实现
function handleActions(reducerMap, defaultState) {
return (state = defaultState, action) => {
const { type } = action;
if (key.split('|').includes(action.type)) {
const reducer = reducerMap[key];
if (typeof reducer === 'function') {
return reducer(state, action);
}
}
// 如果没有匹配的 reducer,返回当前状态
return state;
};
}
combineActions
配合createActions和combineActions两个函数,用于处理多个action-type对应同一个reducer处理函数
const actions = createActions({
['INCREASE']: () => 1,
['DECREASE']: () => -1,
['ADD']: v => v,
})
const reducer = handleActions({
[combineActions(actions.ADD, actions.INCREASE, actions.DECREASE)]: (state, { payload }) => state + payload,
}, 5)
/// 源码实现
// 需要在createAction 重写toString方法
function combineActions(...actionTypes) {
if (actionTypes.length === 0) {
throw new Error('combineActions: 至少需要传入一个 action 类型');
}
return actionTypes.join('|');
}
react-redux
用于链接redux和react
-
Provider组件: 没有任何ui界面,该组件的作用是将redux的仓库放到一个上下文中
-
const store = createStore() function App() { return <> <Provider store={store}> <div></div> </Provider> </> } function Test() { return <div></div> }
-
-
connect: 高阶组件,用于链接仓库和组件
-
细节一: 如果对返回的容器组件加上额外的属性,则这些属性会之间传递到展示组件
-
mapStateToProps
- 参数一 整个参数状态
- 参数二 传递的属性值
-
mapDispatchToProps
- 情况1 传递一个函数,
- 参数一 dispatch
- 参数2 传递的属性对象
- 函数返回的对象会作为属性传递到展示组件中
- 情况2 传递一个对象,对象的每个属性是一个action函数,,会自动调用dispatch函数返回的值action
- 情况1 传递一个函数,
-
细节二: 通过connect链接的组件,会自动得到一个属性: dispatch,组件就可以自行触发action,但是不推荐
-
// 基本使用 function Test() { return <div></div> } const Test1 = connect(mapStateToProps, mapDispatchToProps)(Test) -
大概实现connect高阶组件
function App() { return <> <Provider store={store}> <Connect ><Test /></Connect> </Provider> </> } function mapStateToProps(state) { return { number: state.number } } function mapDispatchToProps(dispatch) { return { onIncrease() { dispatch(increase()) }, onDecrease() { dispatch(decrease()) } } } class Connect extends React.Component { constructor(props) { super(props) this.state = mapStateToProps(store.getState()) store.subscribe(() => { this.setState(mapStateToProps(store.getState())) }) } render() { const eventHandlers = mapDispatchToProps(store.dispatch) return <> { React.cloneElement(this.props.children, {...this.state, ...eventHandlers, ...this.props} ) } </> } }
-
手写react-redux
function Provider(props) {
return <ctx.Provider value={props.store}>{props.children}</ctx.Provider>;
}
function connect(mapStateToProps, mapDispatchToProps) {
return function (Comp) {
//控制更新频率
class Temp extends React.PureComponent {
constructor(props, context) {
super(props, context);
this.store = this.context;
if (mapStateToProps) {
// 状态中的数据
this.state = mapStateToProps(this.store.getState(), this.props);
// 监听仓库中的数据变化
this.clean = this.store.subscribe(() => {
this.setState(mapStateToProps(store.getState(), this.props));
});
}
if (mapDispatchToProps) {
this.handlers = this.getEventHandlers();
}
}
getEventHandlers() {
if (typeof mapDispatchToProps === "function") {
return mapDispatchToProps(this.store.dispatch, this.props);
} else if (typeof mapDispatchToProps === "object") {
return bindActionCreators(mapDispatchToProps, this.store.dispatch);
}
}
componentWillUnmount() {
this.clean && this.clean();
}
render() {
const { children, ...props } = this.props;
return <Comp {...this.state} {...this.handlers} {...props} />;
}
}
Temp.displayName = Comp.displayName || Comp.name;
return Temp;
};
}
dva
- dva不仅仅是一个第三方库,更是一个框架,它主要整合了redux的相关内容,让使用者处理数据更加容易,实际上dva依赖了很多 react,react-redux,react-saga,router-router,connect-react-router之类的第三方库
dva - 启动
-
默认导出一个函数,通过调用该函数可以得到一个dva的应用程序对象
-
dva对象,router: 路由方法,传入一个函数,该函数返回一个react元素,将来应用程序启动后,会自动渲染该节点
-
app.router(() => <App/>); app.router(App);
-
-
dva对象.start: 该方法用于启动dva程序,可以理解为启动react程序,该函数传入一个选择器,用于选中页面中某个dom元素,react会将内容渲染到该元素内部
-
// 内部这么实现 ReactDom.render(<App/>,document.getElementById("root")) -
app.start("#root");// dva启动
-
dva - model
该方法用于定义一个模型,该模型可以理解为redux的action,reducer,redux-saga副作用处理的整合,整和成一个对象,将该对象传入model方法即可
-
namespace
- 命名空间,该属性是一个字符串,字符串的值会被当做仓库的属性名保存
-
state
- 该模型的默认状态
-
reducers
-
该属性配置为一个对象,对象中每个方法就是一个reducer,
-
dva约定方法的名字就是action的类型
-
export default { namespace: "counter", state: 0, reducers: { increase(state) { return state + 1 }, decrease(state) { return state - 1 }, add(state, action) { return state + action.payload } } } -
const mapDispatchToProps = (dispatch) => ({ onDecrease: () => { dispatch({ type: "counter/decrease", }); }, onAdd: (value) => { dispatch({ type: "counter/add", payload: value, }); }, onAsyncDecrease() { dispatch({ type: "counter/asyncDecrease", }); }, });
-
-
effects
-
处理副作用,底层是使用redux-saga实现,该属性配置为一个对象,对象中的每个方法都是处理一个副作用,方法的名字就是匹配的action类型
-
函数的参数一 是action 对象
-
函数的参数二 是封装好的saga effect对象
-
effects: { * asyncIncrease(action, { call, put }) { yield put({ type: "increase" }) }, },
-
-
subscriptions
-
订阅或者生命周期,在启动时触发
-
配置为一个对象,该对象中可以写任意数量任意名称的属性,每个属性是一个函数,这些函数会在模型加入到仓库中后立即运行
-
subscriptions: { resizeIncrease({ dispatch }) { // 订阅窗口尺寸变化,每次变化让数字增加 window.onresize = () => { dispatch({ type: "increase" }) } }, resizeDecrease({ history, dispatch }) { history.listen(() => { dispatch({ type: "decrease" }) }) }, }
-
dva - router
在dva中同步路由到仓库
- 在调用dva函数时,配置history对象
- 使用connectedRouter提供路由上下文
import { createBrowserHistory } from "history";
const app = dva({
history: createBrowserHistory(),
});
//
import Counter from "./Counter";
import { BrowserRouter, NavLink, Route, Switch, routerRedux } from "dva/router";
// routerRedux 包含了connected-react-router的东西
function Home() {
return <div>首页</div>;
}
export default ({ history }) => {
return (
<routerRedux.ConnectedRouter history={history}>
<div>
<ul>
<li>
<NavLink to="/">首页</NavLink>
</li>
<li>
<NavLink to="/counter">计数器</NavLink>
</li>
</ul>
<Switch>
<Route path="/counter" component={Counter} />
<Route path="/" component={Home} />
</Switch>
</div>
</routerRedux.ConnectedRouter>
);
};
dva - 配置
-
history : 同步到仓库的history对象
-
initialState: 创建redux仓库时使用的默认状态,一般会在内部配置
-
initialState: { counter: 1, //需要跟模型名统一 },
-
-
onError 当仓库发生错误的时候运行的函数
-
onError(err, dispatch) {},
-
-
onAction 可以配置redux中间件
- 传入中间件对象
- 传入中间件数组
-
onStateChange 当仓库中数据发生变化时触发的函数
-
onStateChange(state) { console.log(state.counter); },
-
-
onReducer 对模型中的reducer进一步封装.
-
每个reducer运行之前都会运行这个统一的函数,返回新的reducer
-
onReducer(reducer) { return function (state, action) { return reducer(state, action); }; },
-
-
onEffect 类似于对模型中的effect进行封装
-
onEffect(oldEffect, sagaEffect, model, actionType) { return function* (action) { console.log("即将执行副作用代码"); yield oldEffect(action); }; },
-
-
extraReducers 配置额外的reducer,是一个对象,每个属性是一个方法,每个方法就是一个需要合并的reducer,方法名就是属性名
-
extraReducers: { abc(state = 123, action) { return state; }, },
-
-
extraEnhancers 他是用于封装createStore函数的,dva会将原来的仓库创建函数作为参数传递,返回一个新的用于创建仓库的函数,传递的函数必须放在数组中,可能会有多个增强函数
-
执行顺序为执行顺序,跟redux一样,合并时逆行执行时正向
-
extraEnhancers: [ function (creatStore) { console.log("即将创建仓库"); return function (...args) { return creatStore(...args); }; }, ],
-
手写dva
//index.js
export { default } from './dva'
export { connect } from 'react-redux'
//router.js
export * from 'react-router-dom'
import * as routerRedux from 'connected-react-router'
export { routerRedux }
// sasga.js
export * from 'redux-saga/effects'
import ReactDom from "react-dom";
import { Provider } from "react-redux";
import { applyMiddleware, createStore, combineReducers } from "redux";
import { composeWithDevTools } from "redux-devtools-extension";
import createSagaMiddleware from "redux-saga";
import * as sagaEffects from "./saga";
import { createHashHistory } from "history";
import { connectRouter, routerMiddleware } from "connected-react-router";
/**
* @param {*} opts 配置
*/
export default (opts = {}) => {
const app = {
model,
start,
router,
use,
_models: [], // 记录已经定义的模型
_router: null, // 记录已经定义的模型
};
let options = getOptions();
return app;
/**
* 使用dva插件
* @param {*} plugin 配置对象
*/
function use(plugin = {}) {
options = {
...options,
...plugin,
};
}
function getOptions() {
const options = {
history: opts.history || createHashHistory(),
initialState: opts.initialState === undefined ? {} : opts.initialState,
onError: opts.onError || (() => {}),
onStateChange: opts?.onStateChange || (() => {}),
onReducer:
opts?.onReducer ||
((reducer) => (state, action) => reducer(state, action)),
onEffect: opts?.onEffect,
extraReducers: opts?.extraReducers || {},
extraEnhancers: opts?.extraEnhancers || [],
};
if (opts.onAction) {
if (Array.isArray(opts.onAction)) {
options.onAction = opts.onAction;
} else {
options.onAction = [opts.onAction];
}
} else {
options.onAction = [];
}
return options;
}
/**
* 根据模型对象定义模型
* @param {*} modelObj
*/
function model(modelObj) {
app._models.push(modelObj);
}
/**
* 传入一个路由函数
* @param {*} routerFnc
*/
function router(routerFnc) {
app._router = routerFnc;
}
function start(selector) {
const store = getStore();
// 运行注册的subscriptions
runSubscriptions(store.dispatch);
render(selector, store);
}
/**
* 将action的type和modal关联
* @param {*} action
* @param {*} model
*/
function getNewAction(action, model) {
let newAction = action;
// 没有加入命名空间 增加当前的命名空间
if (!action.type.includes("/")) {
newAction = {
...action,
type: `${model.namespace}/${action.type}`,
};
}
return newAction;
}
/**
* 运行注册函数
*/
function runSubscriptions(dispatch) {
for (const model of app._models) {
const newDispatch = function (action) {
dispatch(getNewAction(action, model));
};
if (model.subscriptions) {
for (const prop in model.subscriptions) {
var func = model.subscriptions[prop];
func({
dispatch: newDispatch,
history: options.history,
});
}
}
}
}
function getMiddelwares() {
const sagaMid = createSagaMiddleware();
getMiddelwares.runSaga = function (store) {
const arr = [];
for (const model of app._models) {
// 改造put函数 关联模型
const put = function (action) {
return sagaEffects.put(getNewAction(action, model));
};
if (model.effects) {
for (const key in model.effects) {
arr.push({
type: `${model.namespace}/${key}`,
generatorFunc: model.effects[key],
put,
model,
});
}
}
}
sagaMid.run(function* () {
for (const item of arr) {
let func = function* (action) {
try {
yield item.generatorFunc(action, {
...sagaEffects,
put: item.put,
});
} catch (error) {
options.onError(error, store.dispatch);
}
};
if (options?.onEffect) {
let oldEffect = func;
func = options.onEffect(
oldEffect,
sagaEffects,
item.model,
item.type
);
}
yield sagaEffects.takeEvery(item.type, func);
}
});
};
const mids = [
routerMiddleware(options.history),
sagaMid,
...options.onAction,
];
return composeWithDevTools(applyMiddleware(...mids));
}
/**
* 根据一个模型得到一个reducer
* @param {*} model
* @returns
*/
function getReducer(model) {
const actionTypes = []; // 要匹配的action类型
if (model.reducers) {
for (const prop in model.reducers) {
actionTypes.push({
type: `${model.namespace}/${prop}`,
reducer: model.reducers[prop],
});
}
}
const reducerObj = {
name: model.namespace,
reducer(state = model.state, action) {
const temp = actionTypes.find((_p) => _p.type == action.type);
if (temp) {
return temp.reducer(state, action);
} else {
return state;
}
},
};
return reducerObj;
}
/**
* 得到一些额外的reducer,会合并到根reducer中去
*/
function getExtraReducers() {
return {
router: connectRouter(options.history),
["@@dva"](state = 0, action) {
return state;
},
...options.extraReducers,
};
}
/**
* 得到一个仓库对象
* @param {*} store
*/
function getStore() {
let rootReducerObj = {};
for (const model of app._models) {
const obj = getReducer(model);
rootReducerObj[obj.name] = obj.reducer;
}
rootReducerObj = {
...rootReducerObj,
...getExtraReducers(),
};
let rootReducer = combineReducers(rootReducerObj);
// 封装了onStateChange的reducer
let oldReducer = rootReducer;
rootReducer = function (state, action) {
const newState = oldReducer(state, action);
options.onStateChange(newState);
return newState;
};
// 进一步封装onReducer
let oldReducer2 = rootReducer;
rootReducer = options.onReducer(oldReducer2);
const newCreateStore = options?.extraEnhancers.reduce((fn1, fn2) => {
return fn2(fn1);
}, createStore);
// 根据模型得到一个根reducer
const store = newCreateStore(
rootReducer,
options.initialState,
getMiddelwares()
);
getMiddelwares.runSaga(store);
window.store = store;
return store;
}
function render(selector, store) {
const routerConfig = app._router({
history: options.history,
app,
});
const root = <Provider store={store}>{routerConfig}</Provider>;
ReactDom.render(root, document.querySelector(selector));
}
};
// main.jsx
import "./index.css";
import dva from "./dva";
import counterModel from "./models/counter.js";
import studentsModel from "./models/students.js";
import routerConfig from "./routerConfig.jsx";
import { createBrowserHistory } from "history";
const logger = (store) => (next) => (action) => {
console.log("老状态", store.getState());
next(action);
console.log("新状态", store.getState());
};
const app = dva({
history: createBrowserHistory(),
initialState: {
counter: 123,
},
onError(err, dispatch) {
console.log(err);
},
onAction: logger,
onStateChange: () => {
console.log("111");
},
onReducer(reducer) {
return function (state, action) {
console.log("reducer 即将执行");
return reducer(state, action);
};
},
onEffect(oldEffect, sagaEffects, model, actionType) {
return function* (action) {
console.log("副作用即将产生");
yield oldEffect(action);
};
},
extraReducers: {
abc(state = 0, action) {
return state + 1;
},
},
extraEnhancers: [
function (createStore) {
return function (...args) {
console.log("即将创建仓库");
return createStore(...args);
};
},
],
});
//在启动之前定义模型
app.model(counterModel);
app.model(studentsModel);
// 设置根路由 即启动后要运行的函数
app.router(routerConfig);
// createRoot(document.getElementById("root")!).render(<App />);
app.start("#root");
dva - 插件
通过dva对.use(插件),来使用插件,插件本质上就是一个对象,该对象与配置对象相同,dva对象会在启动时,将传递的插件对象混合到配置对象中
- dva-loading
- 配置: namespace 修改在仓库中名称
- 该插件会在仓库加入一个状态,名称为loading,他是一个对象,其中有以下属性
- global
- 全局是否正在处理副作用,只要有任何一个模型在处理副作用,则该属性为true
- modle
- 一个对象,对象中属性名以及属性的值,表示那个对应的模型是否在处理副作用中
- effects
- 一个对象,对象中属性名和属性值表示是那个action触发的副作用
源码实现
const NAMESPACE = 'loading'
const SHOW = '@DVA_LOADING/SHOW'
const HIDE = '@DVA_LOADING/HIDE'
export default function (opts = {}) {
const namespace = opts.namespace || NAMESPACE
const initialState = {
global: false,
models: {},
effects: {}
}
function reducer(state = initialState, action) {
const { namespace, actionType } = action.payload || {}
switch (action.type) {
case SHOW:
return {
global: true,
models: {
...state.models,
[namespace]: true
},
effects: {
...state.effects,
[actionType]: true
}
};
case HIDE:
const models = {
...state.models,
[namespace]: false
}
const effects = {
...state.effects,
[actionType]: false
}
const global = Object.keys(models).some(el => models[el])
return {
global,
models: models,
effects: effects
};
default:
return state
}
}
function onEffect(oldEffect, sagaEffects, model, actionType) {
return function* (action) {
yield sagaEffects.put({
type: SHOW,
payload: {
namespace: model.namespace,
actionType
}
})
yield oldEffect(action)
yield sagaEffects.put({
type: HIDE,
payload: {
namespace: model.namespace,
actionType
}
})
}
}
return {
extraReducers: {
[namespace]: reducer,
},
onEffect
}
}
umi
- 插件化
- 开箱即用
- 约定式路由
全局安装
提供了一个命令行工具 : umi,通过该命令可以对umi工程进行操作
-
umi 还可以使用对应的脚手架
-
pnpm dlx create-umi@latest
-
-
dev: 使用开发模式启动工程
-
umi dev
-
-
build: 打包产物
-
umi build
-
约定式路由
umi对路由的处理,主要是通过两种方式:
- 约定式: 使用约定好的文件夹和文件来代表页面,umi会根据开发者书写的页面,生成路由配置
- 配置式: 直接书写路由配置文件
umi 约定
- 工程中pages文件夹中存放的是页面,如果工程包含src目录.则src/pages是页面文件夹
- 页面的文件名以及文件的文件路径,是该页面匹配的路由
- 如果页面的文件名是index,则可以省略文件名(首页)
- 如果src/layout目录存在,则该目录中的index.js表示的是全局的通用布局,布局中的child则会添加具体的页面
- 如果pages文件夹中包含_layout.js则_layout.js所在的目录以及其所有的子目录中的页面公用该布局
- 404约定,umi约定pages/404.js表示404页面,如果路由无法匹配,则会渲染该页面,该模式在开发模式中无效,只有部署后生效