mini-react 之旅

199 阅读8分钟

这是我个人第一次发表文章,没错我也是崔学社的一员,主要还是说说感受之类的,大家就当水文看看便好。

任务拆分,提一个case,解决一个case

下面我举几个例子吧: (下面的代码需要结合上下文才能看懂,我这里就简单列一下未列全,感兴趣可以点击文末链接查看)

1.如何在函数式组件中实现一个简单的useEffect

  • 支持单个useEffect
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 /**
 * @param {function():function?} callback
 * @param {any[]} deps
 * 
*/
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  

  TreeToLinkList(work, true);
};

const commitRoot = () => {
  deletions.forEach((work) => {
    commitDeletion(work);
  });
  commitWork(root.child);
  //处理副作用
  commitEffectHooks();
  deletions = [];
};
 
const commitEffectHooks = () => {
  /**@param {WorkUnit} work*/
  const run = (work) => {
    if (!work) return;
    //1.首次初始化的时候
    if (!work.alternate) {
       work.effectHook.callback()
    } else {
      //2.更新的时候

      let oldEffectHook = work.alternate.effectHook;
      let newEffectHook=work.effectHook
          if (newEffctHook.deps.length > 0) {
   

          let needUpdate = oldEffectHook.deps.some(
            (dep, depIndex) => dep !== newEffectHook.deps[depIndex]
          );

          needUpdate && .callback();
        }
    }
    run(work.child);
    run(work.sibling);
  };

  run(root);
};

/**
 * @param {function():function?} callback
 * @param {any[]} deps
 *
 */
const useEffect = (callback, deps) => {
  let currentWork = need_update_work;
  let effecthook = {
    callback,
    deps,
  };

  currentWork.effectHook = effect_hook;
};
  • 支持多个useEffect
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  effect_hooks = [];

  TreeToLinkList(work, true);
};

const commitRoot = () => {
  deletions.forEach((work) => {
    commitDeletion(work);
  });
  commitWork(root.child);
  //处理副作用
  commitEffectHooks();
  deletions = [];
};
 
 /**
 * @param {function():function?} callback
 * @param {any[]} deps
 * 
*/
const commitEffectHooks = () => {
  /**@param {WorkUnit} work*/
  const run = (work) => {
    if (!work) return;
    //1.首次初始化的时候
    if (!work.alternate) {
      work.effectHooks?.forEach((newhook, index) => {
        newhook.callback();
      });
    } else {
      //2.更新的时候

      let oldEffectHooks = work.alternate.effectHooks;
      work.effectHooks?.forEach((newhook, index) => {
        if (newhook.deps.length > 0) {
          let oldEffect = oldEffectHooks[index];

          let needUpdate = oldEffect.deps.some(
            (dep, depIndex) => dep !== newhook.deps[depIndex]
          );

          needUpdate && newhook?.callback();
        }
      });
    }
    run(work.child);
    run(work.sibling);
  };

  run(root);
};

let effect_hooks = [];
/**
 * @param {function():function?} callback
 * @param {any[]} deps
 *
 */
const useEffect = (callback, deps) => {
  let currentWork = need_update_work;
  let effecthook = {
    callback,
    deps,
   
  };
  effect_hooks.push(effecthook);
  currentWork.effectHooks = effect_hooks;
};

  • 实现cleanUp
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  effect_hooks = [];

  TreeToLinkList(work, true);
};

const commitRoot = () => {
  deletions.forEach((work) => {
    commitDeletion(work);
  });
  commitWork(root.child);
  //处理副作用
  commitEffectHooks();
  deletions = [];
};
 
 /**
 * @param {function():function?} callback
 * @param {any[]} deps
 * 
*/
const commitEffectHooks = () => {
   /**@param {WorkUnit} work*/
  const run = (work) => {
    if (!work) return;
    //1.首次初始化的时候
    if (!work.alternate) {
      work.effectHooks?.forEach((newhook, index) => {
        newhook.cleanUp = newhook.callback();
      });
    } else {
      //2.更新的时候

      let oldEffectHooks = work.alternate.effectHooks;
      work.effectHooks?.forEach((newhook, index) => {
        if (newhook.deps.length > 0) {
          let oldEffect = oldEffectHooks[index];

          let needUpdate = oldEffect.deps.some(
            (dep, depIndex) => dep !== newhook.deps[depIndex]
          );

          needUpdate && (newhook.cleanUp = newhook?.callback());
        }
      });
    }
    run(work.child);
    run(work.sibling);
  };
  /**@param {WorkUnit} work*/
  const runCleanUp = (work) => {
    if (!work) return;
    work.alternate?.effectHooks?.forEach((effecthook) => {
      if (effecthook.deps.length > 0) {
        effecthook?.cleanUp?.();
      }
    });
    runCleanUp(work.child);
    runCleanUp(work.sibling);
  };
  runCleanUp(root);
  run(root);
};

let effect_hooks = [];
/**
 * @param {function():function?} callback
 * @param {any[]} deps
 *
 */
const useEffect = (callback, deps) => {
  let currentWork = need_update_work;
  let effecthook = {
    callback,
    deps,
   cleanUp:undefined
  };
  effect_hooks.push(effecthook);
  currentWork.effectHooks = effect_hooks;
};

2.如何在函数式组件中实现一个简单的useState

  • 实现useState
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  effect_hooks = [];

  TreeToLinkList(work, true);
};

let state_hooks = null;
let state_hook_index = null;

/**
 * @template T
 * @param {T} initalState An input element

 */
const useState = (initalState) => {
  /**@type {WorkUnit}*/
  let currentWork = need_update_work;
  let oldState = currentWork?.alternate?.stateHooks[state_hook_index];

  let hookState = {
    state: oldState ? oldState.state : initalState,
  };
  state_hooks.push(hookState);
  currentWork.stateHooks = state_hooks;
  state_hook_index++;
  /**@param {function(T) | T} action */
  const setState = (action) => {
   hookState.state=action(hookState.state)
    NextWorkUnit = {
      ...currentWork,
      alternate: currentWork,
    };
    root = NextWorkUnit;
  };
  return [hookState.state, setState];
};
  • 批量更新
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  effect_hooks = [];

  TreeToLinkList(work, true);
};

let state_hooks = null;
let state_hook_index = null;

/**
 * @template T
 * @param {T} initalState An input element

 */
const useState = (initalState) => {
  /**@type {WorkUnit}*/
  let currentWork = need_update_work;
  let oldState = currentWork?.alternate?.stateHooks[state_hook_index];

  let hookState = {
    state: oldState ? oldState.state : initalState,
    queue: oldState ? oldState.queue : [],
  };
  hookState.queue.forEach((action) => {
    hookState.state = action(hookState.state);
  });
  hookState.queue = [];

  state_hooks.push(hookState);
  currentWork.stateHooks = state_hooks;
  state_hook_index++;
  /**@param {function(T) | T} action */
  const setState = (action) => {
    const isActionFunc = typeof action === 'function';

    hookState.queue.push(isActionFunc ? action : () => action);
    NextWorkUnit = {
      ...currentWork,
      alternate: currentWork,
    };
    root = NextWorkUnit;
  };
  return [hookState.state, setState];
};
  • 减少不必要的更新
/**
@typedef   {{callback:(function():function?),deps:any[],cleanUp:function()}} 
EffectHook*/

/**
 *
 * @typedef {{type?:string,props:{[key:string]:any,children:WorkUnit[]},child?:WorkUnit,
 * dom?:HTMLElement,sibling?:WorkUnit,parent?:WorkUnit,effectTag?:'placement'|'update',
 * alternate?:WorkUnit,effectHooks?:EffectHook[]}} WorkUnit
 *
 * */
 
 
 /**@param {WorkUnit} work*/
const updateFunctionalComponent = (work) => {
  need_update_work = work;
  state_hook_index = 0;
  state_hooks = [];
  effect_hooks = [];

  TreeToLinkList(work, true);
};

let state_hooks = null;
let state_hook_index = null;

/**
 * @template T
 * @param {T} initalState An input element

 */
const useState = (initalState) => {
  /**@type {WorkUnit}*/
  let currentWork = need_update_work;
  let oldState = currentWork?.alternate?.stateHooks[state_hook_index];

  let hookState = {
    state: oldState ? oldState.state : initalState,
    queue: oldState ? oldState.queue : [],
  };
  hookState.queue.forEach((action) => {
    hookState.state = action(hookState.state);
  });
  hookState.queue = [];

  state_hooks.push(hookState);
  currentWork.stateHooks = state_hooks;
  state_hook_index++;
  /**@param {function(T) | T} action */
  const setState = (action) => {
    
    const isActionFunc = typeof action === 'function';
    
    const estimateValue = isActionFunc ? action(hookState.state) : action;
    if (estimateValue === hookState.state) return;
    
    hookState.queue.push(isActionFunc ? action : () => action);
    NextWorkUnit = {
      ...currentWork,
      alternate: currentWork,
    };
    root = NextWorkUnit;
  };
  return [hookState.state, setState];
};

可能这就是任务拆分的好处吧,可以不断在上一个case的情况下完善代码,以离目标越来越近,其实配上测试会更好一些,刚好配合一个case一个case地去推动任务的实现。

学会requestIdleCallback的基本使用

requestIdleCallback 是浏览器提供的一个 API,用于在浏览器的空闲时段执行任务,以避免阻塞主线程。在 React 中,requestIdleCallback 可以被用来执行一些不紧急的任务,以提高性能和用户体验。

在 React 中,通常使用 requestIdleCallback 来执行一些较为耗时的任务,如:

  1. 渲染大型列表或表格: 当需要渲染大量数据时,可以将渲染任务分解为多个小任务,并在空闲时段逐步完成,以避免一次性阻塞主线程。
  2. 懒加载组件: 如果你有一些组件在初始加载时并不是必需的,可以使用 requestIdleCallback 来懒加载它们,以减少初始加载时间。
  3. 后台数据同步: 在空闲时段可以执行一些后台数据同步或缓存更新的任务,以提高应用的响应速度。
/**
 * @param {IdleDeadline} deadline
 *
 */
const WorkLoop = (deadline) => {
  let shouldField = false;
  while (!shouldField && NextWorkUnit) {
    /*需要执行task,也就是渲染工作*/
    NextWorkUnit = PerformWorkOfUnit(NextWorkUnit);
    if (NextWorkUnit?.type === root?.sibling?.type) NextWorkUnit = undefined;
    shouldField = deadline.timeRemaining > 1;
  }
  if (!NextWorkUnit && root) {
    //能到这里说明树全部转化成链表,可以在这里一并提交dom的渲染
    commitRoot();
    currentRoot = root;
    root = null;
  }
  requestIdleCallback(WorkLoop);
};
requestIdleCallback(WorkLoop);

我个人认为这段代码是mini-react最核心的一部分,本质就是不断地循环,看看有没有work(实际上react的fiber节点,我个人认为就是加强版的vdom,除了比较重要的元素类型属性等信息,还包含了child,parent,sibling等节点间的联系信息)的存在,如果有且剩余时间也就是deadline.timeRemaining 时间还充足的情况下的话,进行了work间关系的建立,即child,parent,sibling等节点的关系的确立,本来渲染也应该发生在这阶段的,但是可能有些渲染单元任务过长,可能会一段时间后再渲染成功,让用户感觉看到感觉卡顿,所以转换成了先确立关系,把关系树转化成关系链表,然后再一并渲染的策略。

树转链表

treetolist.png

  if (work.child) return work.child;
  //如果两个组件嵌套过深且互为兄弟组件可能出现找不到的现象,所以要不断地往上找

  // if (work.sibling) return work.sibling;
  let nextwork = work;
  while (nextwork) {
    if (nextwork.sibling) return nextwork.sibling;
    nextwork = nextwork.parent;
  }




返回顺序即代表执行顺序

双树交替更新

two tree.png

/**
 * @param {WorkUnit} work
 * @param { boolean} isFunctionalComponent
 */
const TreeToLinkList = (work, isFunctionalComponent) => {
  // console.log(work);
  const children = isFunctionalComponent
    ? [work.type(work.props)]
    : work.props.children;
  let prevChild = null;
  let oldWork = work.alternate?.child;
  children.forEach((child, index) => {
    //每个child是一个vdom
    //先根据vdom创建一个newWork,work相当vdom对象的扩充版
    //如果传入新的vdom的标签是相同就定为是更新操作,否则就是添加操作
    const isSameType = oldWork && oldWork.type === child.type;
    /**@type {WorkUnit | null}*/
    let newWork = null;
    if (isSameType) {
      newWork = {
        type: child.type,
        props: child.props,
        parent: work,
        sibling: null,
        child: null,
        effectTag: 'update',
        dom: oldWork.dom,
        alternate: oldWork,
      };
    } else {
      // console.log(child);
      if (child) {
        newWork = {
          type: child.type,
          props: child.props,
          parent: work,
          sibling: null,
          dom: null,
          child: null, //这里的属性名我感觉叫做child指针更好些,因为链表指针一个指针只有一个指向
          effectTag: 'placement',
        };
      }

      if (oldWork) {
        // console.log('delete :', oldWork);
        deletions.push(oldWork);
      }
    }
    if (oldWork) oldWork = oldWork.sibling;
    if (index === 0) {
      work.child = newWork;
    } else {
      prevChild.sibling = newWork;
    }
    if (newWork) {
      prevChild = newWork;
    }
  });
  // console.log(oldWork);
  while (oldWork) {
    deletions.push(oldWork);
    oldWork = oldWork.sibling;
  }
};

这个alternate属性很关键,指向了的要更新的老的节点,后面还在useEffect中用来判断是首次渲染还是需要更新渲染的时候派上了用场。因为初始的时候是没有这个属性,是在更新的时候在浅拷贝的头部节点上加了alternate然后再放入生成关系链表的函数中处理,处理了的时候生成新的work的时候也加了alternate属性指向了老的节点,并打上effectTag标记,方编判断后续的更新和添加处理。 如果标签相同就判断是相同的节点执行更新,更新props又分成三种 :

const updateProps = (dom, nextwork, prevwork) => {
  let oldProps = prevwork?.props;
  let newProps = nextwork.props;
  //1.旧的work的props中有的属性,新的work中没有 需要删除
  if (oldProps) {
    Object.keys(oldProps).forEach((prop) => {
      if (prop !== 'children') {
        if (!(prop in newProps)) {
          dom.removeAttribute(prop);
        }
      }
    });
  }

  // 2.旧的work中的props中有的属性,新的work中也有 需要更新
  //3.旧的work中props没有的属性,新的work中有  需要添加

  Object.keys(newProps).forEach((prop) => {
    // console.log(prop);
    if (prop !== 'children') {
      if (dom) {
        if (prop.startsWith('on')) {
          const evnetType = prop.slice(2).toLocaleLowerCase();
          if (oldProps) {
            dom.removeEventListener(evnetType, oldProps[prop]);
          }

          dom.addEventListener(evnetType, newProps[prop]);
        } else {
          dom[prop] = newProps[prop];
        }
      }
    }
  });
};

标签不一致的话就是删除旧的添加新的

这里也分为两种,能对应到老节点,以及对应不上的:

注意红圈圈起来的就是要删除的

11.png

22.png

结语

这次跟着崔大坚持打卡七天还是有很多收获的,群内学习氛围也相当好,同学们学习热情高涨,群里提出疑问总是有很多小伙伴热心解答,大家都是互帮互助的,下一次有这种类似活动还会继续参加,下面贴出我的源码链接,有需要的小伙伴请自提

stackblitz.com/edit/vitejs…