Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

react源码分析 #26

Open
lkdghzh opened this issue Feb 24, 2021 · 12 comments
Open

react源码分析 #26

lkdghzh opened this issue Feb 24, 2021 · 12 comments

Comments

@lkdghzh
Copy link
Owner

lkdghzh commented Feb 24, 2021

setState 同步&异步

setstate 更新变量a,立刻打印 a,不发生变化;
通常为异步,大部分场景需要在合成事件&生命周期更新state。进入react的schedule的调度流程
不受react管控场景为同步。没进入react的schedule的调度流程

  • 18之前 使用render
    • 异步 (多次setState,触发一次render,渲染一次页面)
      • react合成事件onClick
      • 生命周期函数
      • 需要在setstate 回调取最新值
    • 同步 (多次setState,触发多次render,渲染多次页面)
      • promise.then
      • 定时器
      • 原生事件 onclick
  • 18引入这个模式rootapi createroot方式

任何情况都会自动执行批处理,所有场景都为异步
再用render,会有waring
提供flushsync 包裹setstate 触发更新

flushSync(() => {
   this.setState({
     text: 'xxxx'
   })
 });
 console.log(text) // xxxx

1.获取优先级
2.创建更新
3.schedule

原因:自动批处理
参考

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 24, 2021

Effect 依赖项怎么发生变化

  • 基本类型
    • 值变化,立即触发
  • 引用类型
    • 数组
    • 对象

比较地址发生变化,不关心内容
arr=[a,b]
arr=[...arr,c] //新建了数组

重点

避免不必要渲染,针对数组、对象,使用useMemo得到memorize。再放进useEffect依赖项内;
props nextprops 进行Object.is 等同===引用比较

@lkdghzh
Copy link
Owner Author

lkdghzh commented Jan 8, 2025

Fiber

16提出的,

  • 数据结构
    • 节点结构
      • tag
      • type
      • stateNode
      • return child silbing
    • 更新相关
      • memoizedState FC hook第一个hook
      • pendingProps
      • memoizedProps
      • updatequeue
    • 副作用
      • deletions
      • flag
      • subflag
    • 调度优先级
      • lane
      • childlans
  • 数据结构
    • 链表结构管理组件树
  • 工作单元:
    渲染任务拆分小块:避免长时间阻塞主线程
    • 时间分片+可中断。
      workloop内while循环完成,大循环, 代码不多, 但是非常精髓,
      • 时间切片原理:
        在每一次具体执行task之前都要进行超时检测 shouldyield, 不超时执行下个fiber创建。否则递归注册下一帧 requestIdleCallback
      • 可中断渲染原理:shouldyield
    • 任务优先级
    • 渲染和更新
function performWork(deadlineInfo) {
  deadline = performance.now() + (deadlineInfo.timeRemaining() || 50); // 模拟每个时间片为 50ms

  while (taskQueue.length > 0 && !shouldYield()) {
    const task = taskQueue.shift();
    task.callback();
  }

  if (taskQueue.length > 0) {
    requestIdleCallback(performWork); // 继续下一帧任务
  }
}

react 18较16有哪些 最新功能点

  • 并发:在页面渲染与更新时,提供createroot 并发渲染模式,作为以后默认。
    本质利用idleCallback时机,做到并发,workloop 生成fiber,performUnitWork执行work创建
    每个work切片5ms左右(关键渲染帧之间空闲时间约5ms),在workloop根据优先级,中断执行高优先级任务
    usedefferval usetransaction 降低任务优先级。 state fn执行的cb 对应的update优先级
    生成fibertree beginwork commit
  • scheduler:基于调度器lane模型通过位运算可方便多优先级划为一个批次。
  • 渲染层面:合成事件实现时间对象,自己递归实现捕获与冒泡,委托挂载从document 调整app 入口

@lkdghzh
Copy link
Owner Author

lkdghzh commented Jan 13, 2025

Hooks HOC

对比

用来解决逻辑复用的问题
HOC:嵌套会导致“HOC 地狱”,调试不方便
Hooks:代码简洁,依赖管理由 Hook 内部控制

为什么顶层

hooks本质是用数组管理状态的

  • 两个数组:state数组 set函数数组,一个索引
  • 假如有两个hook
    • state1 setstate1
    • state2 setstate2,
  • 初始化时state数组[state1,state2],set函数数组[setstate1 ,setstate2],setstate1 通过一个二级闭包保存对应state索引。
  • 在首次 二次渲染重新执行。在setstate时更新对应索引state
  • 如果放置一些逻辑下,生成的索引不对,对应的 state取的也不对
    https://juejin.cn/post/6844904037825921031

常用

  • useImpertiveHandle 暴露组件api 通常useRef forwardRef配合传入
  • useDeferedVal useTransition 降低优先级 对应state变量更新 setState 函数执行优先级
  • redux/toolkit useSelector useDispatch等
  • 自定义 useWatch usePaginator useScroll local等

HOOK

编写useHover() 题目描述 编写useHover(),并且符合以下调用:​ ​ const [ref, hovering] = useHover();

// 使用函数
function useHover() {
  const [hovering, setHovering] = usestate();
  const ref = useRef();
  useEffect(() => {
    dom.addevt("mousemove", () => {setHovering(true)});
    dom.addevt("mouseend", () => {setHovering(false)});
    return () => {
      dom.rvmevt("mousemove", () => {});
      dom.rvmevt("mouseend", () => {});
    };
  }, [ref.current]);
  return [ref, hovering];
}

//调用
 const [ref, hovering] = useHover();
 <div ref={hoverRef} >  {isHovering ? '鼠标悬停中' : '鼠标未悬停'}</div>

HOC 增强

编写基于yup的高阶组件 题目描述 
已知yup是一个流行的schema的校验库,其基本用法如下:​ ​ 
const schema = yup.object({name: yup.string().required(),age: yup.numer()});const validDataOrError = schema.validateSync({ name: 'Koa' }); 
验证通过返回校验修正后的数据,验证失败则返回包含错误原因的Error对象。​​​ ​ 
请根据上述信息,编写高阶组件withYupValidator(WrappedComponent, schema),对传入的组件进行增强,实现可以对props进行校验。如果组件未校验通过,显示错误信息(<div>error message here</div>

// 方案一:外层函数返回函数组件(使用hooks,`增加函数组件组件`嵌套原始组件):()=>{return (props)=>{ return <com {...props}>}}
withYupValidator(WrappedComponent,schema){

  // 1. 高阶返回一个函数(props)
  return (props)=>{ 
          
        useEffect(()=>{        
          const validateProps = async () => {
              try {
                await schema.validate(props);
                setError(null);
              } catch (err) {
                setError(err.message);
              }
            };
      validateProps(); },[props])
         
        if(err){return <div>error message here</div>}
        // 2.返回 组件 
        return <WrappedComponent {...props} />
    }
}

方案二:外层函数返回Cls组件(使用Lifecyclce,`增加Cls组件组件`嵌套原始组件)
const withYupValidator = (WrappedComponent, schema) => {
  return class extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        error: null,
      };
    }

    componentDidMount() {
      this.validateProps(this.props);
    }

    componentDidUpdate(prevProps) {
      if (prevProps !== this.props) {
        this.validateProps(this.props);
      }
    }

    validateProps(props) {
      try {
        schema.validateSync(props);
        this.setState({ error: null });
      } catch (err) {
        this.setState({ error: err.message });
      }
    }

    render() {
      const { error } = this.state;
      if (error) {
        return <div>{error}</div>;
      }
      return <WrappedComponent {...this.props} />;
    }
  };
};

// 调用
const schema = yup.object({name: yup.string().required(),age: yup.numer()});const MyComponent = ({ name, age }) => (
  <div>
    <p>Name: {name}</p>
    <p>Age: {age}</p>
  </div>
);

const EnhancedComponent = withYupValidator(MyComponent, schema);

@lkdghzh
Copy link
Owner Author

lkdghzh commented Jan 13, 2025

双缓冲

current wip 内存替换的>快速更新节点

  • 通过alternate相互指向
  • 下次渲染
    使用上次wip 作为current渲染
    将上次current作为wip

协调diff时本身时jsx 和current对比,得到wip

@lkdghzh
Copy link
Owner Author

lkdghzh commented Jan 13, 2025

Scheduler的优先级lane模型

更好批次管理 位运算 进行赛道归类

scheduler 优先级 expiretime-> lane模型(更好批次管理)

scheduler 5种优先级

export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

对应超时时间

// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;

react 有4种优先级

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 19, 2025

react vs vue

  1. 数据绑定
    • react
      • 单向数据流动:组件通信通过单向数据流props和回调;
      • 数据视图更新:如 usestate setstate触发更新
    • vue mvvm
      • 数据流动:组件通信可通过props emit
      • 数据视图更新:2基于defineProperty 3基于proxy,数据与模版实现实现双向绑定,mvvm ,直接变量赋值和改变dom,双向影响
  • 2.性能在实现上区别
    • react:18 引入fiber支持架构,支持concurent,时间切片、可中断,diff
    • vue:模版编译生成render,diff
  • 3.生态
    • react:关注视图层(UI展示) ,路由状态管理有三方生态提供 redux react-redux react-router-dom
    • vue:关注视图层(UI展示) 外,自己提供vuex pinia vue-router
  • 4.应用
    • react:大型复杂应用;学习难度较高
    • vue :中小型;上手快;

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 19, 2025

diff algorithm

diff 目的找到可以复用的节点(vdom或者fiber),节点直接复用或者移动

react diff

比较对象 新jsx和旧current对比,wipnode复用
old:current
生成:wipnode
new:jsx

单节点

指的新节点为一个
都没设置key 则认为key相同

  • 比较key
    • key不同 不能复用,遍历 current 旧兄弟找能复用的 ,
      例如
      current key a b c
      jsx key c
      c a key不同遍历兄弟fiber a b c元素是否能复用)
    • key相同 比较type
      • type不同:不能复用 current 旧 上 标记删除(兄弟fiber元素一并标记删除)
        current type a b c
        jsx type m
        m a key type不同, a b c都标记删除
      • type相同:可以复用 wipnode复用用(旧 兄弟fiber元素一并标记删除) +增加新的节点

多节点

指的新节点为多个
都没设置key 则认为key相同
更新操作多于 “新增删除移动”,因此两轮遍历
第一轮:尝试逐个复用节点
第二轮:处理第一轮没处理的节点

一轮

结束代表,遍历完新节点列表,或者中间几点被中断循环

  • 比较key
    • key不同 1.结束循环,不能复用
    • key相同 比较type
      • type不同:2.继续遍历,不能复用。标记旧的为删除,生成新fiber,不结束循环,继续遍历到下个节点
      • type相同:3.继续遍历,可以复用

二轮

第一轮遍历,被提前终止,进入二轮遍历,代表:fiber list: 新的没遍历完或者旧的没有遍历完

  • 旧剩余:删除:旧剩余current(子节点添加到deletions里)
    current:a b c
    jsx a b
    c 删除
  • 新剩余:新增
    current:a b
    jsx a b c
    c 新增
  • 新旧都有剩余:旧的剩余放到 map, 移动复用 ,找不到 新增,map剩余的 删除。
    key 不同demo
    current:a b c d
    jsx a c b e
    一:a 复用
    二:
    1:b c key不一样,把{b c d}放到map
    2:遍历jsx,c ,从map中找到 c,复用,map 的c 移动到 wip fiber进行复用
    b ,从map中找到 b,复用 ,map 的b移动到 wip fiber进行复用
    e ,从map中找不到e,新增
    map剩余d,删除

vue2diff

children
[源码链接]https://github.com/vuejs/vue/blob/2.6/src/core/vdom/patch.js#L404

diff children伪代码

时间复杂度
旧 前 新后
5种 else
相同:首首 尾尾 首尾 尾首
不同:
在新老比对时,每次对比,都要经过这5种判断是那种,,

// 参数父节点dom
diffchildren(parentEle,old,new){
    // 首 新老start index ++ end index-- 
    while(old start<end  && new start < end){
         if else
         1 2 // 首首 //尾尾 issame  直接 patch(旧、新)   索引start + ,end -
         3 首尾 issame  移动位置 (旧前 移动到 旧后 ele) ,用silbing表示后
             a b c d
             d c b a
           过程 首首a d  、尾尾d a不同   ,首尾a a比较,相同,看第3种情况
           看新a在尾,针对新 旧a 新a,目的让a显示页面上列表兄弟最后方,
           这里却用老的vdom去找parent ele,  parentinsertbefor(old.start.ele,old.end.sibling.ele) 旧前 移动到 旧后
           索引start + ,end -
         4 尾首相同,  移动位置 (旧后 移动到 旧前) 
            a b c d
            d c b e
          过程 首首a d  、尾尾d a不同   首尾a e不同,尾首d d相同 看第4种情况
          看新d首, 针对新 旧d 新d,目的让d显示页面上最前方,
          这里却用老的vdom去找parent ele,  parentinsertbefor(old.end.ele,old.start.ele) 旧后 移动到 旧前
          索引start + ,end -
         5 首首 尾尾 交叉 都不同  创建map{} 存旧节点,判断newstart 是否有 有移动,没有旧新增
           例子 新增
             a b c d e
              m b e a d
            1.a m   ,ed ,ad em都不同
             2第五种创建 map ={a:1,b:2,c:3,d:4,e:5}
             判断新节点m 在map里面有same的节点
             new start   m 没有的话,新增 在旧start新增一个 parentinsertbefor(new一个,old.start.ele)
             有的话,新增 在旧start新增一个 parentinsertbefor(这个c,old.start.ele)
               如:
            例子 移动
             a b c d e
              c b e a d
            1.a c   ,ed ,ad ec都不同
             2第五种创建 map ={a:1,b:2,c:3,d:4,e:5}
             判断新节点c 在map里面有same的节点
             new start   c 有的话,可以`移动复用`:c 置为undfined,parentinsertbefor(这个c,old.start.ele)

             新start++  
    }
    
    // 新老个数不配,要么新增,要么删除
    // 跳出循环外,需要创建 删除动作   旧新列表对比,如果 多一个或者少一个  需要创建 删除动作
    要么 old start >end   新增
     for(old start end){
         before=new end+1
         parent.insertbefie(createnode ,before)
     }
    要么 new start >end   删除
    for(old start end){
         parent.remove(node)
     }
}

insertBefore方法

insertBefore方法是DOM操作中的一个经典方法,其作用是将一个节点插入到另一个节点之前。这个方法接受两个参数:第一个参数是要插入的新节点,第二个参数是参考节点(即新节点将要插入到这个节点前面)。

patch

patch(oldnode,newvnode)
1.先对比type ,
2.在看新节点children ,

文本节点

3.再看老节点children
有 ,开启diff
没有,新增元素即可

  • type不同,删除老的,创建新的,真实dom
  • type相同
    • newvnode没有children==判断是文本,而且不等,直接oldnode.ele.innetText=xx
    • newvnode有children
      • 旧oldnode的也有children diff key
      • 旧oldnode的没有children => 元素添加

源码
参考

issame

key props想通

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 19, 2025

1

2 similar comments
@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 19, 2025

1

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 19, 2025

1

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 20, 2025

实现一个调度器 并发执行任务数限制 优先级控制

class TaskScheduler {
  constructor({ concurrency }) {
    this.concurrency = concurrency;
    this.runningTasks = 0;
    this.taskQueue = [];
  }

  addTask(task, { priority = 0 } = {}) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ task, priority, resolve, reject });
      this.taskQueue.sort((a, b) => b.priority - a.priority);
      this.runNext();
    });
  }

  runNext() {
    if (this.runningTasks >= this.concurrency || this.taskQueue.length === 0) {
      return;
    }

    const { task, resolve, reject } = this.taskQueue.shift();
    this.runningTasks++;

    task()
      .then(resolve)
      .catch(reject)
      .finally(() => {
        this.runningTasks--;
        this.runNext();
      });
  }
}

// Example usage:
const scheduler = new TaskScheduler({ concurrency: 3 });

function fetchData(url) {
  return new Promise((resolve) => {
    setTimeout(() => resolve(`Data from ${url}`), 1000);
  });
}

scheduler.addTask(() => fetchData('url1'), { priority: 1 }).then(res => console.log('Task1 Success:', res)).catch(console.error);
scheduler.addTask(() => fetchData('url2'), { priority: 2 }).then(res => console.log('Task2 Success:', res)).catch(console.error);
scheduler.addTask(() => fetchData('url3'), { priority: 1 }).then(res => console.log('Task3 Success:', res)).catch(console.error);
scheduler.addTask(() => fetchData('url4'), { priority: 1 }).then(res => console.log('Task4 Success:', res)).catch(console.error);
scheduler.addTask(() => fetchData('url5'), { priority: 3 }).then(res => console.log('Task5 Success:', res)).catch(console.error);
scheduler.addTask(() => fetchData('url6'), { priority: 1 }).then(res => console.log('Task6 Success:', res)).catch(console.error);

@lkdghzh
Copy link
Owner Author

lkdghzh commented Feb 20, 2025


实现简单实现 React Fiber 和 调度器(Scheduler) 的例子,用来帮助理解 React Fiber 的核心原理。完整的 Fiber 实现非常复杂,这里只实现了一个基础版本,支持以下功能:

schduler和协调渲染

实现调度和FIber

  1. Fiber 树和任务调度器的实现

以下是 Fiber 和调度器的核心代码:

// ----------------------- Fiber 数据结构 -----------------------
class FiberNode {
  constructor(type, props, parent) {
    this.type = type; // 节点类型(如 'div' 或组件函数)
    this.props = props; // 节点的属性
    this.parent = parent; // 父节点
    this.child = null; // 第一个子节点
    this.sibling = null; // 下一个兄弟节点
    this.stateNode = null; // 对应的真实 DOM 或组件实例
    this.alternate = null; // 上一次 Fiber 树中的对应节点
  }
}

// ----------------------- 调度器 -----------------------

// 任务队列
const taskQueue = [];
let workInProgress = null; // 当前正在工作的 Fiber 节点

// 优先级常量
const PRIORITY = {
  IMMEDIATE: 1, // 高优先级
  NORMAL: 2,    // 正常优先级
  LOW: 3        // 低优先级
};

// 当前时间片是否结束
function shouldYield() {
  return performance.now() >= deadline; // 模拟时间分片
}

let deadline = 0; // 时间片结束时间

// 调度任务
function scheduleTask(callback, priority = PRIORITY.NORMAL) {
  taskQueue.push({ callback, priority });
  requestIdleCallback(performWork);
}

// 执行任务
function performWork(deadlineInfo) {
  deadline = performance.now() + (deadlineInfo.timeRemaining() || 50); // 模拟每个时间片为 50ms

  while (taskQueue.length > 0 && !shouldYield()) {
    const task = taskQueue.shift();
    task.callback();
  }

  if (taskQueue.length > 0) {
    requestIdleCallback(performWork); // 继续下一帧任务
  }
}

// ----------------------- Fiber 渲染器 -----------------------

// 创建 Fiber 树
function createFiberTree(vnode, parent = null) {
  const fiber = new FiberNode(vnode.type, vnode.props, parent);

  const children = vnode.props.children || [];
  let previousFiber = null;

  children.forEach((child, index) => {
    const childFiber = createFiberTree(child, fiber);

    if (index === 0) {
      fiber.child = childFiber; // 第一个子节点
    } else {
      previousFiber.sibling = childFiber; // 下一个兄弟节点
    }

    previousFiber = childFiber;
  });

  return fiber;
}

// 渲染 Fiber 树到真实 DOM
function render(vnode, container) {
  // 构建 Fiber 树
  const rootFiber = createFiberTree(vnode);

  // 挂载 Fiber 树并插入 DOM
  scheduleTask(() => commitRoot(rootFiber));

  // 保存根 Fiber
  workInProgress = rootFiber;
}

// 遍历 Fiber 树并更新 DOM
function commitRoot(root) {
  let currentFiber = root;

  while (currentFiber) {
    // 创建真实 DOM 并插入
    if (typeof currentFiber.type === "string") {
      currentFiber.stateNode = document.createElement(currentFiber.type);

      // 设置属性
      for (let key in currentFiber.props) {
        if (key === "children") continue;
        currentFiber.stateNode[key] = currentFiber.props[key];
      }

      // 插入 DOM
      if (currentFiber.parent) {
        currentFiber.parent.stateNode.appendChild(currentFiber.stateNode);
      }
    }

    // 深度优先遍历
    if (currentFiber.child) {
      currentFiber = currentFiber.child;
    } else {
      while (currentFiber && !currentFiber.sibling) {
        currentFiber = currentFiber.parent; // 回溯到父节点
      }
      if (currentFiber) {
        currentFiber = currentFiber.sibling;
      }
    }
  }
}

// 更新 Fiber 树 (Diff 算法的简单实现)
function updateFiberTree(root, newVNode) {
  scheduleTask(() => {
    const oldFiber = root.alternate || root; // 上一次的 Fiber
    const newFiber = reconcile(oldFiber, newVNode);
    commitRoot(newFiber);
  });
}

// 简单的 Diff 算法
function reconcile(oldFiber, newVNode) {
  if (!newVNode) {
    return null; // 删除节点
  }

  if (oldFiber && oldFiber.type === newVNode.type) {
    // 更新属性
    oldFiber.props = newVNode.props;
    oldFiber.alternate = oldFiber;
    return oldFiber;
  }

  // 替换节点
  const newFiber = createFiberTree(newVNode, oldFiber.parent);
  return newFiber;
}

// ----------------------- React-like 接口 -----------------------
function createElement(type, props, ...children) {
  return { type, props: { ...props, children: children.flat() } };
}

const React = { createElement };
const ReactDOM = { render };

// ----------------------- 示例 -----------------------
const root = document.getElementById("root");

const App = React.createElement(
  "div",
  { style: "color: blue;" },
  React.createElement("h1", null, "Hello, Fiber!"),
  React.createElement("button", { onClick: () => alert("Clicked!") }, "Click Me")
);

// 渲染到页面
ReactDOM.render(App, root);

@lkdghzh lkdghzh pinned this issue Feb 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant