react笔记

119 阅读41分钟

react核心拆解

  • react 核心是以 状态声明视图可变内容显示,通过事件控制状态更新,状态更新后驱动视图更新,
    • 数据(状态)
    • 视图(render)
    • 事件
    • 而他们三方又是独立的
  • react 通过 调度器 和 调和器 来进行三方的协调工作
    • 为什么不使用 requestIdleCallback 和或者 scheduler 实现
      • react 自己开发了一个包 scheduler 去实现
    • 调度器
      • 等待浏览器有空闲就执行 调和器
    • 调和器
      • react 的diff实现,关注需要更新的内容,有更新才发生render
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源码阅读过程,各个包理解顺序

  1. react-dom
    1. 处理渲染相关,处理端的事情,浏览器的api 跨端开发,3d(渲染器逻辑)
      1. createRoot,(ReactDom.createRoot),createContainer
      2. render, updateContainer
  2. react 是为了统一为外部开发者提供接口协议
    1. useState
    2. useEffect
  3. react-reconciler 处理状态调和
    1. createFiberRoot
    2. initializeUpdateContainer
    3. createUpdate
    4. enqueueUpdate
  4. scheduler 调度包
    1. 由于web提供的api无法显示优先级调度,所以react自己实现了这个功能
    2. expirationTime 过期时间 => lanes 模型
  5. react-noop-renderer
    1. 实现无状态的
    2. 可以实现 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需要传递一个函数)
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
      • 通常会将网络请求,启动计时器等一开始操作放入该函数
  • 组件进入活动状态,待机状态,等待组件状态或者属性发生变化
  • 更新阶段 - 属性或者状态变化
    • componentWillReceiveProps - 已过期
      • 当属性值改变才会触发
      • 即将接收到新的属性值,当前属性值还没被改变
    • shouldComponentUpdate
      • 状态和属性值发生变化时触发
      • 指示react是否需要重新渲染组件(重新调用render方法),通过返回true或者false来指定
      • 必须要设置返回值,true表示需要更新
    • componentWillUpdate - 已过期
      • 组件即将被重新渲染
    • render
    • componentDidUpdate
      • 已完成重新渲染
  • 销毁阶段 - 从dom树移除
    • componentWillUnmount
      • 组件被销毁时触发,通常在该函数销毁定时器
  • 新版声明周期,将几个will的生命周期钩子设为过期提示,不建议后续使用,提供了几个新的钩子
    • getDerivedStateFromProps
      • 状态和属性更新时触发,返回值可以决定状态变更后的数据
    • getSnapshotBeforeUpdate
      • 真实的dom构建完成,还未实际渲染到页面中
      • 可以在该函数实现一些额外的dom操作
      • 该函数的返回可以作为 componentDidUpdate 的第三个参数

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) 发生错误,并且可以阻止错误继续传播

  1. 书写生命周期函数 getDerivedStateFromError,静态函数,
    1. 在渲染子组件发生错误时,在更新页面之前 运行
    2. 只有子组件发生错误才会运行
    3. 该函数返回一个对象,react会将该对象的属性覆盖调state中同名属性,方便更新视图
    4. 该函数接收的参数是错误对象
  2. 编写声明周期函数 componentDidCatch(error,info),实例方法
    1. 是在渲染子组件发生错误时,在更新页面之后运行
    2. 由于时间比较靠后,如果在该函数中改变状态,会出现组件树销毁又重新构建,推荐使用 getDerivedStateFromError, 在销毁之前拦截
    3. 该函数通常用于记录错误信息

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

  1. 第n次调用useState
  2. 检查该节点的状态数组是否存在下标n
  3. 如果不存在
    1. 使用一个默认值创建一个状态
    2. 将该状态加入到状态数组中,下标为n
  4. 存在的情况
    1. 忽略掉默认值
    2. 直接得到状态值

注意

  • 如果是使用同一个函数组件,内部的状态也不会互相干扰,因为他们的状态是挂载对应的节点上的,函数在使用状态会从节点上取对应的状态表格
  • 最好写在函数起始位置方便阅读
  • 严禁出现在代码块里面(判断,循环)
    • 会破坏写入状态数组的顺序,使得后续状态获取状态不正确
  • 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

  1. react-router : 路由核心库,包含诸多和路由功能相关的核心代码
  2. 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 将当前的 locationhistory 对象传递给子组件。
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 决定是否渲染对应的组件。

  • 匹配 URLRoute 组件会从上下文中获取当前的 location,并与自身的 path 属性进行匹配。
  • 渲染组件:如果匹配成功,则渲染 componentrender 属性指定的组件。
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
    • 细节二: 通过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对路由的处理,主要是通过两种方式:

  1. 约定式: 使用约定好的文件夹和文件来代表页面,umi会根据开发者书写的页面,生成路由配置
  2. 配置式: 直接书写路由配置文件

umi 约定

  • 工程中pages文件夹中存放的是页面,如果工程包含src目录.则src/pages是页面文件夹
  • 页面的文件名以及文件的文件路径,是该页面匹配的路由
  • 如果页面的文件名是index,则可以省略文件名(首页)
  • 如果src/layout目录存在,则该目录中的index.js表示的是全局的通用布局,布局中的child则会添加具体的页面
  • 如果pages文件夹中包含_layout.js则_layout.js所在的目录以及其所有的子目录中的页面公用该布局
  • 404约定,umi约定pages/404.js表示404页面,如果路由无法匹配,则会渲染该页面,该模式在开发模式中无效,只有部署后生效