学懂react底层原理,这篇文档就够了

52 阅读25分钟
  1. 回顾react

我们不妨回顾一下在react中我们是如何书写的

import ReactDOM from 'react-dom/client';
const root = ReactDOM.createRoot(document.getElementById('root'));
import App from './App';
root.render(<App />);

让我们来一行一行解读:

  1. 首先第一行代码导入了浏览器端负责渲染的模块,实际上关于react和ReactDom分别实现在两个模块我个人理解为react负责核心逻辑的部分并不关注渲染方式,而react-dom更侧重于渲染,这种设计可以让react即可以运行在web端(react-dom/client),也可以运行在服务端(react-dom/server),同时也可以运行在移动端(react Native),这样的设计大大提高了react的灵活性。
  2. 第二行字面意思就是创建一个根节点。
  3. 第三行引入app函数。
  4. 第四行调用了root中的渲染函数,但是这里需要注意的是是jsx的一种写法,浏览器不认识,实际上会由react-scripts转化为下面代码:
root.render(React.createElement(App));

总结:我们实际上最核心的就是实现一个render函数和createElement函数。

function createElement(node){
   //下面讲
}
function render(root,node){
   //root表示真实dom,node表示虚拟dom
}
  1. createElement函数

在下文变量命名中,我们统一将root和dom视为真实dom,node视为虚拟dom,同时为了大家能够更加直观的理解本文的内容,我们拿一个具体的案例去实现

我们暂时(因为暂时不涉及function(函数组件),后面我们做补充)将目标设置为渲染成这样的真实dom

return (
  <div id="div1">
    <p id="p1">我是p1</p>
    <p id="p2">我是p2</p>
  </div>
);

那我们的jsx会把以上代码转化为以下内容,并且虚拟dom为node

React.createElement(
  "div",
  { id: "div1" },
  React.createElement("p", { id: "p1" }, "我是p1"),
  React.createElement("p", { id: "p2" }, "我是p2")
);

const node = {
    type:"div",
    props:{
        id:"div1",
        children:[
            {
                type:"p",
                props:{
                    id:"p1",
                    children:[
                        {
                            type:"TEXT_ELEMENT",
                            props:{
                                nodeValue:"我是p1",
                                children:[]
                            }
                        }
                    ]
                }
            },
            {
                type:"p",
                props:{
                    id:"p2",
                    children:[
                        {
                            type:"TEXT_ELEMENT",
                            props:{
                                nodeValue:"我是p1",
                                children:[]
                            }
                        }
                    ]
                }
            }
        ]
    }
}

接下来我们来看看不同node的结构是怎么样的:

  1. 普通虚拟dom
const node = {
    type:"div",//标签类型
    props:{//子属性
        children:[a,b,c],//子元素 a,b,c 
        id:"title"//id属性
    }
}
  1. 子元素是文本的虚拟dom
{
    type:"TEXT_ELEMENT",
    props:{
        nodeValue:"我是p1",
        children:[]
     }
}

可以看出createElement接受3个参数,第一个是标签类型,第二个是属性,第三个和以后是子节点,那么我们的createElement应该长这样。

function createElement(type,props,...children){
    return {
        type:type,
        props:{
            ...props,
            children:children.map(child=>{
                return typeof child ==='object'?child:createTextElement(child)
            })
        }
    }
}
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}
  1. Render函数

render函数更简单,我们只需创建虚拟dom,再用创建的虚拟dom创建真实dom,最后添加到container中

function render(container) {
  const root = document.getElementById(container);
  const nodeElement = createElement(
    "div",
    { id: "div1" },
    createElement("p", { id: "p1" }, "我是p1"),
    createElement("p", { id: "p2" }, "我是p2")
  );//创建虚拟dom
  const dom = createDom(nodeElement);//创建真实dom
  container.appendChild(dom)//添加到真实dom中
}
const isProps = (key) => key !== "children";
function createDom(node){
    const dom = node.type==="TEXT_ELEMENT"?document.createTextNode(""):document.createElement(node.type);
    Object.keys(node.props)
    .filter(isProps)
    .forEach((key) => {
        dom[key] = node.props[key]
    }); //为dom添加属性
    node.props.children.forEach(child=>{
        dom.appendChild(createDom(child))
    })//递归为子元素创建dom
    return dom
}
render(document.getElementById("root"));

我们如果不出意外的话就可以看到这样的效果了

  1. 分时函数与fiber(分片)

当然,我们按照以上内容确实可以实现虚拟dom到真实dom的转变,但是如果dom节点很多,层次又很深的话,那么这样必然会阻塞浏览器的渲染,那么现在我们就需要用到浏览器为我们提供的api requestIdleCallback(请求空闲回调),具体使用请点击链接,我们需要做以下步骤

  1. 不能像之前一样一次性把所有内容全部创建好,我们需要对当前虚拟dom进行分片,切片后我们一般叫fiber
  2. 那fiber和我们刚才的虚拟dom有什么区别?答:在原来的基础上多了一些属性,如下
  • sliding:兄弟元素fiber;
  • parent:父元素fiber;
  • child:子元素fiber;
  • dom:对应的真实dom引用;
  • hooks: 后面写hook的时候我会提到;

浏览器空闲时间处理逻辑如下:

function workLoop(idelDeadline){
   let isRemain = true
   while(isRemain&&nextWork){
        nextWork = performWork(nextWork)//执行当前工作单元
        isRemain = idelDeadline.timeRemain()>1//还有剩余时间吗?
   }
   requestIdleCallback(workLoop)//下次有空闲继续执行
}
requestIdleCallback(workLoop)
function performWork(fiber){
    //执行当前fiber并返回下一个工作单元
    //1.为当前fiber创建dom;
    //2.将当前dom添加到父亲fiber的dom;
    //3.为子元素创建新的fiber;
    //4.返回下一个需要执行的fiber;
}

框架就这样搭建好了,接下来我们要完善细节,先说一下思路,performWork处理当前工作单元,那执行什么呢?

  • 为当前fiber创建dom;

在此之前,我们需要修改我们的createDom函数,传入的不再是node,而是fiber,并且我们不需要递归为子元素创建,我们只需要为当前元素创造

function createDom(fiber) {
  const dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type); //创建当前dom
  Object.keys(fiber.props)
    .filter(isProps)
    .forEach((key) => {
      dom[key] = fiber.props[key];
    }); //为dom添加属性

  return dom;
}
function performWork(fiber){
    //执行当前fiber并返回下一个工作单元
    //1.创建为当前fiber创建dom;
    if(!fiber.dom){
       fiber.dom = createElement(fiber)
    }
    //2.将当前dom添加到父亲fiber的dom;
    if(fiber.parent.dom){
       fiber.parent.dom.appendChild(fiber.dom)
    }
    //3.为子元素创建新的fiber;
    
    //4.返回下一个需要执行的fiber;
}
  • 将当前dom添加到父亲fiber的dom;
function performWork(fiber){
    //执行当前fiber并返回下一个工作单元
    //1.创建为当前fiber创建dom;
    if(!fiber.dom){
       fiber.dom = createElement(fiber)
    }
    //2.将当前dom添加到父亲fiber的dom;
    if(fiber.parent.dom){
       fiber.parent.dom.appendChild(fiber.dom)
    }
    //3.为子元素创建新的fiber;
    //4.返回下一个需要执行的fiber;
}
  • 为子元素创建新的fiber;
function performWork(fiber) {
  //执行当前fiber并返回下一个工作单元
  //1.创建为当前fiber创建dom;
  if (!fiber.dom) {
    fiber.dom = createElement(fiber);
  }
  //2.将当前dom添加到父亲fiber的dom;
  if (fiber.parent.dom) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  //3.为子元素创建新的fiber;
  const elements = fiber.props.children;
  const preFiber = null; //上一个fiber,大家可以理解为链表,来串联兄弟节点,也就是这里的p1和p2
  for (let i = 0; i < elements.length; i++) {
    const element = elements[i];
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候,也就是第25行
      sliding: null,
      dom: null,
    };
    if (i === 0) {
      fiber.child = newFiber;
    } else if (i > 0) {
      preFiber.sliding = fiber; //串联兄弟节点
    }
    preFiber = fiber;
  }
  //上面的流程中,child,sliding,parent,dom,我们全部正确的给值了。
  //4.返回下一个需要执行的fiber
  if (fiber.child) {
    return fiber.child;//优先子节点
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sliding) {
      return nextFiber.sliding;//如果有兄弟就是兄弟节点
    }
    nextFiber = nextFiber.parent;//回到父元素
  }
  return nextFiber;
}
  • 返回下一个需要执行的fiber;

先来说结论,结论是有子节点就优先子节点,没有的话就返回兄弟节点,如果兄弟节点也没有,那就是父亲节点的兄弟节点(也就是叔叔节点)。

有的同学可能会疑问为什么是这样呢?大家不妨回想一下我们最开始createDom的逻辑,虽然我们是通过递归实现的,但是细心的同学应该能注意到递归的顺序其实和我们现在返回fiber的顺序是一致的。这里我给大家画个图理解,图1是fiber理论上的操作顺序,图二是dom结构。

    <div id="root">
        <div id="div1">
            <p id="p1">我是p1</p>
            <p id="p2">我是p2</p>
        </div>
        <div id="div2">
            <p id="p3">我是p3</p>
            <p id="p4">我是p4</p>
        </div>
    </div>
function performWork(fiber) {
    console.log('fiber',fiber)
  //执行当前fiber并返回下一个工作单元
  //1.创建为当前fiber创建dom;
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  //2.将当前dom添加到父亲fiber的dom;
  if (fiber.parent && fiber.parent.dom) {
    fiber.parent.dom.appendChild(fiber.dom);
  }
  //3.为子元素创建新的fiber;
  const elements = fiber.props.children;

  let preFiber = null; //上一个fiber,大家可以理解为链表,来串联兄弟节点,也就是这里的p1和p2
  for (let i = 0; i < elements.length; i++) {
    const element = elements[i];
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候
      sliding: null,
      dom: null,
    };
    if (i === 0) {
      fiber.child = newFiber;
    } else if (i > 0) {
      preFiber.sliding = newFiber; //串联兄弟节点
    }
    preFiber = newFiber;
  }
  //上面的流程中,child,sliding,parent,dom,我们全部正确的给值了。
  //4.返回下一个需要执行的fiber
  if (fiber.child) {
    return fiber.child;//优先子节点
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sliding) {
      return nextFiber.sliding;//如果有兄弟就是兄弟节点
    }
    nextFiber = nextFiber.parent;//回到父元素
  }
  return nextFiber;
}

最后我们还需要修改render,将下一个待执行单元设定为根节点,与此同时,浏览器就会在空闲时间进行渲染,不出意外的话,我们还是可以得到最终效果。

function render(container,fiber) {
  nextWork = {
    dom:container,
    props:{
        children:[fiber]
    }
  }
}
  1. 更改操作

虽然现在我们实现了最基本的功能,但是现在如果dom结构有更新的话,我们应该怎么办呢?

  • 第一种方案就是我们根据新的fiber,从根节点开始创建新的fiber,创建新的dom
  • 第二种方案我们可以保留上一次dom和fiber,只对变化的属性更新相应的属性

很显然,第二种方案是最优的,那我们该怎么做呢?(思考一会后)😂我们当然可以保存上一次状态的fiber,然后更新的时候对这两个fiber进行对比,所以我们的fiber又多了一个属性 alternate 来保存上一个状态的fiber,那么我们应该什么时候去比较呢,没错就是创建fiber的时候,也就是在 performWork 函数中,在比较中,我们主要判断节点类型,其中有以下3种类型,并且用effectTag来标记fiber

  1. 更新(update),标签类型相同,我们将创建的fiber,标记为update
  2. 替换(placement),标签类型不同,我们将新创建的fiber,标记为placement
  3. 删除(deletion),标签类型不同,我们将的fiber,标记为deletion

接下来我们来做一些修改

  1. render更改

我们知道,对于普通的fiber,我们全部已经有了alternate属性,但是对于根fiber,还没有,所以我们要用一个全局变量(preRootFiber)保存一下上一次的状态,rootFiber保存当前状态

let rootFiber = null
let preRootFiber = null
function render(container, elements) {
  rootFiber = {
    dom: container,
    props: {
      children: [elements],
    },
    alternate:preRootFiber//记录上一次的根节点
  };
  nextWork =rootFiber  //更新
}
  1. performWork更改

  • 对于1.为当前fiber创建dom的操作,我们放到对比新老fiber的操作中,如果是placement,则新增dom。
  • 对于2.将当前dom添加到父亲fiber的dom,我们需要也是同样的,只有effectTag是placement的时候,才需要。
  • 对于3.为子元素创建新的fiber,我们需要对比新老fiber,为子元素创建fiber reconcileChildren函数,下面会讲到,最后操作dom(commitWork),我们将这俩功能分别实现在单独的函数中。
  • 第4步保持不变
function performWork(fiber) {
  //执行当前fiber并返回下一个工作单元

  const elements = fiber.props.children;
  //3.比较新老fiber并创建fiber
  reconcileChildren(fiber, elements);
  commitWork(fiber)
  //上面的流程中,child,sliding,parent,dom,我们全部正确的给值了。
  //4.返回下一个需要执行的fiber
  if (fiber.child) {
    return fiber.child; //优先子节点
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sliding) {
      return nextFiber.sliding; //如果有兄弟就是兄弟节点
    }
    nextFiber = nextFiber.parent; //回到父元素
  }
  return nextFiber;
}
  1. reconcileChild函数

本质上还是为子节点创建fiber,只不过多了effectTag(变化类型)属性和alternate(上一个状态的fiber)属性,比较的过程如下图,代码在下面

function reconcileChildren(wipFiber, elements) {
  //当前正在操作的fiber和子元素
  let preFiber = null;//前一个fiber,作用仍然是链接兄弟节点
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //去对应elements的第一个元素
  for (let i = 0; i < elements.length || oldFiber; i++) {
    const element = elements[i];
    let newFiber = null;
    const isSameType = element && oldFiber && element.type === oldFiber.type;//比较类型
    if (isSameType && element) {//类型相同,更新dom即可
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候
        sliding: null,
        dom: oldFiber.dom,
        alternate: oldFiber,//上一个fiber
        effectTag: "update",
      };
    } else if (!isSameType && element) {//类型不同,新增
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候
        sliding: null,
        dom: null,
        alternate: null,
        effectTag: "placement",
      };
    } 
    if (!isSameType && oldFiber) {//类型不同,需要删除老节点
      oldFiber.effectTag = "deletion";
    }
    
    if (i === 0) {
      wipFiber.child = newFiber;//设置子节点
    }
    if (i !== 0 && i < elements.length) {
      preFiber.sliding = newFiber;//串联兄弟节点
    }
    oldFiber = oldFiber && oldFiber.sliding;//老节点往下走
    preFiber = newFiber;
  }
}
  1. commitWork(根据effectTag操作dom)

function commitWork(fiber) {
  if (fiber.effectTag === "update") {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);
  } else if (fiber.effectTag === "placement") {
    fiber.dom = createDom(fiber);//创建后,添加子元素中
    updateDom(fiber.dom, {}, fiber.props);
    fiber.parent.dom.appendChild(fiber.dom);
  } else if (fiber.effectTag === "deletion") {
    fiber.parent.dom.removeChild(fiber.dom);//删除当前dom
  }
}
function updateDom(dom, preProps, nextProps) {
  const isNew = (key) => preProps[key] !== nextProps[key]; //是不是新值
  const isPre = (key) => !(key in preProps); //是不是老值
  const isEvent = (key) => key.startsWith("on");
  Object.keys(nextProps)
    .filter(isProps)
    .filter(isNew)
    .forEach((key) => {
      dom[key] = nextProps[key];
    }); //更新属性
  Object.keys(nextProps)
    .filter(isProps)
    .filter(isNew)
    .filter(isEvent)
    .forEach((event) => {
      const eventName = event.substring(2).toLowerCase();
      dom.addEventListener(eventName, nextProps[event]);
    }); //添加事件
  Object.keys(preProps)
    .filter(isProps)
    .filter(isPre)
    .forEach((key) => {
      dom[key] = "";
    }); //删除属性
  Object.keys(preProps)
    .filter(isProps)
    .filter(isPre)
    .filter(isEvent)
    .forEach((event) => {
      const eventName = event.substring(2).toLowerCase();
      dom.removeEventListener(eventName, nextProps[event]);
    }); //添加事件
  //style样式大家可以自己添加
}

于是我们的performWork变成了这样

function performWork(fiber) {
  //执行当前fiber并返回下一个工作单元
  const elements = fiber.props.children;
  //3.比较新老fiber并创建fiber
  reconcileChildren(fiber, elements);
  commitWork(fiber)
  //上面的流程中,child,sliding,parent,dom,我们全部正确的给值了。
  //4.返回下一个需要执行的fiber
  if (fiber.child) {
    return fiber.child; //优先子节点
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sliding) {
      return nextFiber.sliding; //如果有兄弟就是兄弟节点
    }
    nextFiber = nextFiber.parent; //回到父元素
  }
  return nextFiber;
}

这里请大家思考一个问题,第六行的commitWork能够操作effectTag等于deletion的情况吗?(思考了一会🤔)

答:这里很显然不能处理删除的情况,因为我们是 oldFiber.effectTag = "deletion" ,我们是对旧的fiber更新effectTag,而这里的fiber是刚刚创建的新的fiber,再次提示大家,performWork的作用是处理刚才创建的fiber,并为子节点创建fiber,那我们如何处理?

答:需要用一个全局变量存储需要删除的fiber,然后在比较完全部fiber之后,我们遍历需要删除的oldfiber,删除dom结构,我们需要更改workLoop,在没有可以执行工作单元并且有没删除的oldfiber时,去删除

let deletions = []//全局变量
function workLoop(idelDeadline) {
  let isRemain = true;
  while (isRemain && nextWork) {
    nextWork = performWork(nextWork); //执行当前工作单元,并返回下一个工作单元
    isRemain = idelDeadline.timeRemaining() > 1; //还有剩余时间吗?
  }
  if(!nextWork&&rootFiber){
    commitDeletion()//删除
    preRootFiber = rootFiber;//保存这一次的根节点
    rootFiber = null;//这次为null
  }
  requestIdleCallback(workLoop); //下次继续执行
}
function commitDeletion(){
    deletions.forEach(commitWork)
    deletions = []
}

好了,如果大家代码没写错的话,我们来验证一下,当点击整个大容器后,就只会剩余一个我是p3的标签

const node = createElement(
  "div",
  {
    id: "div1",
    onClick: () => {
      const newNode = createElement(
        "div",
        {},
        createElement("p", { }, "我是p3")
      );
      render(document.getElementById("root"),newNode);
    },
  },
  createElement("p", { id: "p1" }, "我是p1"),
  createElement("p", { id: "p2" }, "我是p2")
);
render(document.getElementById("root"), node);
  1. 支持Function

在react,我们经常会像1.1这样写,当然,这是jsx写法,实际转化过来是1.2这样

//1.1
import App from './App';
root.render(<App />);
//1.2
import App from './App';
root.render(React.createElement(App,{}));

也就是说,createElement的第一个参数type也有可能是一个函数,而且函数组件没有本事没有dom结构,函数组件的返回值也就是他的children节点,总结一下特点

  1. 函数组件的createElement的第一个参数type传入的是一个函数
  2. 函数组件自己没有真实dom,dom来自于返回值是children节点
  3. 函数组件有hooks属性,后面会提到

那我们要怎么做呢?(我们要明白函数组件是没有dom结构的,所以主要影响的就是commitWork(操作dom)函数)

  1. performWork函数

函数组件获取子元素的方法和其他fiber不同,通过调用 [fiber.type(fiber.props)] 来获取组件

function performWork(fiber) {
  //执行当前fiber并返回下一个工作单元
  const isFunction = fiber.type instanceof Function;
  const elements = isFunction
    ? [fiber.type(fiber.props)]
    : fiber.props.children;
  //3.比较新老fiber并创建fiber
  reconcileChildren(fiber, elements);
  commitWork(fiber);
  //。。。。。。
}
  1. commitWork函数

function commitWork(fiber) {
  if (fiber.type instanceof Function &&fiber.effectTag!=="deletion") {
    return;
  }//对于不是函数组件的fiber,是没有update和placement操作的
  if (!fiber.root) {//我们为根节点做了标记,根节点是没有parent的
    var parentFiber = fiber.parent;//用var做变量提升
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    }//因为有了函数组件,我们不能确保每个fiber上全有dom,所以我们要一直往父节点上找真实的dom,placement和deletion全部需要用到父dom
  }

  if (fiber.effectTag === "update") {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props);//更新dom即可
  } else if (fiber.effectTag === "placement") {
    fiber.dom = createDom(fiber); //创建后,添加子元素中
    updateDom(fiber.dom, {}, fiber.props);
    parentFiber.dom.appendChild(fiber.dom);
  } else if (fiber.effectTag === "deletion") {
    if(fiber.dom){//说明不是函数组件
        parentFiber.dom.removeChild(fiber.dom); //删除当前dom
    }else{//说明是函数组件,我们需要继续往下找,但是后续的子fiber的effectTag不一定是deletion,我们需要手动赋值
        fiber.child.effectTag = "deletion"
        commitWork(fiber.child)
    }
  }
}
  1. 引入jsx写法

后续的内容为了方便大家书写,我在这里引入了babal的cdn文件

注意⚠️ /** @jsx createElement */ 这一行注释很关键,会用我们自己写的createElement函数

<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
//大家在html中导入即可
/** @jsx createElement */   
function App() {
   return <ul>
            <li >
              这是li
            </li>
        </ul>;
}//这样babel会自动将jsx转化为我们自己写的createElement函数形式
  1. hooks

在hooks的编写中,我们为fiber新增一个hooks的数组属性,里面保存当前函数组件所有的hooks,并且我们需要一个索引来追踪当前hook,我们声明为hookIndex,因为我们在执行到hooks的组件时,是无法获取到他当前属于的fiber的,所以我们还需要声明一个全局变量,currentFiber来表示我们正在操作的fiber,所以我们需要在performWork中初始化currentFiber,并且在执行函数组件后,将hookIndex重置为0;

let hookIndex = 0
let currentFiber = null
function performWork(fiber) {
  console.log(fiber, "currentFiber");
  //执行当前fiber并返回下一个工作单元
  currentFiber = fiber;//赋值
  const isFunction = fiber.type instanceof Function;
  if (isFunction) {
    fiber.hooks = [];//初始化hooks
  }
  let elements = isFunction
    ? [fiber.type(fiber.props)]//实现函数组件
    : fiber.props.children;
  hookIndex = 0;//重置为0
  //...
 }
  1. useState

官网地址:zh-hans.react.dev/reference/r…

function useState(state) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate; //获取上一个fiber
  const hook = {
    type: "useState",
  };
  if(oldFiber){
    hook.state = oldFiber.hooks[hookIndex].state //说明是后续渲染,用上一次的值
  }else{
    hook.state = state //说明是组件初次渲染,用第一次函数传入的值
  }
  const setState = (callback) => {
    const value =
      callback instanceof Function ? callback(hook.state) : callback; //用户可能传入值,也有可能传入表达式
    hook.state = value; //更新state
    rootFiber = {
      dom: preRootFiber.dom,
      props: preRootFiber.props,
      alternate: preRootFiber,
      root: "root",
    }; //重新渲染
    nextWork = rootFiber;
    deletions = [];
  };
  hook.setState = setState;
  currentFiber.hooks.push(hook); //保存到当前fiber中
  hookIndex++;//索引递增
  return [hook.state, setState];
}
  1. useEffect

官网地址:zh-hans.react.dev/reference/r…

function useEffect(callback, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;

  const hook = {
    type: "useEffect", //类型
    dependencies: dependencies, //每次拿最新的
    destruction:
      oldFiber && oldFiber.hooks[hookIndex].destruction
        ? oldFiber.hooks[hookIndex].destruction
        : null, //销毁函数
  };
  if (dependencies === void 0) {
    //安全的返回undefined,防止用户声明undefined
    hook.destruction = callback(); //如果不传任何依赖性,每次组件渲染全会更新
  }
  if (dependencies.length === 0) {
    if (!oldFiber) {
      hook.destruction = callback(); //当没有依赖项时,只有初始化的时候会执行一次,并且获取销毁回调
    }
  } else {
    if (oldFiber) {
      const isChange = oldFiber.hooks[hookIndex].dependencies.some(
        (dependency, index) => !Object.is(dependency, dependencies[index])
      ); //查看依赖项是否变化
      if (isChange) {
        hook.destruction = callback(); //变化就执行
      }
    }
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
}

我们记录了当组件销毁时需要执行的函数,但是我们还没有去执行,现在我们需要到commitWork中去执行(28行)

function commitWork(fiber) {
  if (fiber.type instanceof Function && fiber.effectTag !== "deletion") {
    return;
  } //对于不是函数组件的fiber,是没有update和placement操作的
  if (!fiber.root) {
    //我们为根节点做了标记,根节点是没有parent的
    var parentFiber = fiber.parent;
    while (!parentFiber.dom) {
      parentFiber = parentFiber.parent;
    } //因为有了函数组件,我们不能确保每个fiber上全有dom,所以我们要一直往父节点上找真实的dom,placement和deletion全部需要用到父dom
  }

  if (fiber.effectTag === "update") {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props); //更新dom即可
  } else if (fiber.effectTag === "placement") {
    fiber.dom = createDom(fiber); //创建后,添加子元素中
    updateDom(fiber.dom, {}, fiber.props);
    parentFiber.dom.appendChild(fiber.dom);
  } else if (fiber.effectTag === "deletion") {
    if (fiber.dom) {
      //说明不是函数组件
      parentFiber.dom.removeChild(fiber.dom); //删除当前dom
    } else {
      //说明是函数组件,我们需要继续往下找,但是后续的子fiber的effectTag不一定是deletion,我们需要手动赋值
      fiber.child.effectTag = "deletion";
      commitWork(fiber.child);
      //如果是函数组件中包含useEffect hooks 那么我们需要执行销毁的回调
      fiber.hooks
        .filter((item) => item.type === "useEffect")
        .forEach((item) => {
          if (item.destruction) {
            item.destruction(); //执行销毁回调
          }
        });
    }
  }
}
  1. useMemo

官网地址:zh-hans.react.dev/reference/r…

function useMemo(fn, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useMemo",
    dependencies: dependencies,
  };
  if (oldFiber) {//后续渲染
    let isChange = oldFiber.hooks[hookIndex].dependencies.some(
      (dependency, index) => {
        return !Object.is(dependency, dependencies[index]);
      }
    );
    if (isChange) {//对比依赖性是否有变化
      hook.result = fn();
    } else {
      hook.result = oldFiber.hooks[hookIndex].result;//没有变化就用原来的,避免再次计算
    }
  } else {//初次渲染,我们之间调用返回结果
    hook.result = fn();
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook.result;
}
  1. useCallback

官网地址:zh-hans.react.dev/reference/r…

function useCallback(fn, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useCallback",
    dependencies: dependencies,
  };
  if (oldFiber) {//后续渲染
    let isChange = oldFiber.hooks[hookIndex].dependencies.some(
      (dependency, index) => {
        return !Object.is(dependency, dependencies[index]);
      }
    );
    if (isChange) {
      hook.fn = hook.fn.bind(null); //依赖有变化,就返回一个新的函数
    } else {
      hook.fn = oldFiber.hooks[hookIndex].fn; //没有变化就返回旧的函数
    }
  } else {
    hook.fn = fn; //初次,返回fn
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook.fn;
}
  1. useRef

官网地址:zh-hans.react.dev/reference/r…

function useRef(initialValue) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useRef",
  };
  if (oldFiber) {
    const oldHook = oldFiber.hooks[hookIndex];
    currentFiber.hooks.push(hook);
    hookIndex++;
    return oldHook;
  } else {
    hook.current = initialValue;
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook;
}

我们知道,ref还可以引用dom,我们还需要在updateDom中对于有ref属性的dom,为其添加引用;

function updateDom(dom, preProps, nextProps) {
  const isNew = (key) => preProps[key] !== nextProps[key]; //是不是新值
  const isPre = (key) => !(key in preProps); //是不是老值
  const isEvent = (key) => key.startsWith("on");
  const isRef = (key) => key === "ref";

  //省略。。。
  Object.keys(nextProps)
    .filter(isRef)
    .filter(isNew)
    .forEach((key) => {
      nextProps[key].current = dom;
    }); //为ref添加dom
  //省略。。。
  Object.keys(preProps)
    .filter(isRef)
    .filter(isPre)
    .forEach((key) => {
      preProps[key].current = null;
    }); //为ref删除dom
  //style样式大家可以自己添加
}
  1. 细节问题

  1. 对于placement的节点,我们真的应该使用在commitWork中真的应该使用appendChild吗?

答:不能,假如遇到以下函数组件

      function App2() {
        let [show, setShow] = useState(true);
        const handleClick = ()=>{
          setShow(!show)
        }
        return (
          <div>
            <p>我是p1</p>
            {show ? <p>我是p2</p> : <div>我是p3</div>}
            <button onClick={handleClick}>切换</button>
          </div>
        );
      }
      render(document.getElementById("root"), <App2 />); 

当点击切换前后对比,很显然,div3的位置是错误的,appendChild会添加到父亲元素的最后,所以这里我们应该改用insertBefore,mdn文档点这里

function commitWork(fiber){
//....
else if (fiber.effectTag === "placement") {
    fiber.dom = createDom(fiber); //创建后,添加子元素中
    updateDom(fiber.dom, {}, fiber.props);
    if (fiber.sliding && fiber.sliding.dom) {//后续渲染
      parentFiber.dom.insertBefore(fiber.dom, fiber.sliding.dom);
    } else {//首次渲染
      parentFiber.dom.appendChild(fiber.dom);
    }
  }
  //.....
}
  1. 我们知道,react支持map创建子元素,如果我们不做任何修改可以吗?

答:不行,假如遇到以下组件

 function App3() {
        return (
          <div>
            {new Array(100).fill(0).map(item=>{
              return <p>循环创建的p标签</p>
            })}
          </div>
        );
      }

这样的话,div的children属性将会是一个长度只有 1 的数组

显然,我们希望长度是 100,所以我们要对这个数组进行扁平化处理。

function flatObjectArr(arr) {
  //对象数组扁平化
  return arr.reduce((acc, item) => {
    if (item instanceof Array) {
      acc.push(...flatObjectArr(item));
    } else {
      acc.push(item);
    }
    return acc;
  }, []);
}
function performWork(fiber){
    //省略
    let elements = isFunction
    ? [fiber.type(fiber.props)] 
    : fiber.props.children;
  elements = flatObjectArr(elements); //扁平化对象数组,有可能遇到map的情况
    //省略
}
  1. 源码

基本上对较难理解的每一行代码全部加了注释。

在线运行点这里:codesandbox.io/p/sandbox/b…

  1. react.js

const isProps = (key) => key !== "children" && key !== "style";
let deletions = []; //待删除的fiber
let rootFiber = null; //当前根fiber
let preRootFiber = null; //上一次的根fiber
let currentFiber = null; //正在执行的fiber
let hookIndex = 0; //正在执行的fiber的hook的索引
let nextWork = null; //下一个待执行的fiber
function createElement(type, props, ...children) {
  return {
    type: type,
    props: {
      ...props,
      children: children.map((child) => {
        return typeof child === "object" ? child : createTextElement(child);
      }),
    },
  };
}
function createDom(fiber) {
  const dom =
    fiber.type === "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type); //创建当前dom
  Object.keys(fiber.props)
    .filter(isProps)
    .forEach((key) => {
      dom[key] = fiber.props[key];
    }); //为dom添加属性

  return dom;
}
function createTextElement(text) {
  return {
    type: "TEXT_ELEMENT",
    props: {
      nodeValue: text,
      children: [],
    },
  };
}

function render(container, elements) {
  rootFiber = {
    dom: container,
    props: {
      children: [elements],
    },
    alternate: preRootFiber,
    root: "root",
  };
  nextWork = rootFiber;
}

function workLoop(idelDeadline) {
  let isRemain = true;
  while (isRemain && nextWork) {
    nextWork = performWork(nextWork); //执行当前工作单元,并返回下一个工作单元
    isRemain = idelDeadline.timeRemaining() > 1; //还有剩余时间吗?
  }

  if (!nextWork && rootFiber) {
    console.log(rootFiber);
    preRootFiber = rootFiber;
    rootFiber = null;
    commitDeletion();
  }
  requestIdleCallback(workLoop); //下次继续执行
}
requestIdleCallback(workLoop);
function reconcileChildren(wipFiber, elements) {
  //当前正在操作的fiber和子元素
  let preFiber = null; //前一个fiber,作用仍然是链接兄弟节点
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //去对应elements的第一个元素
  for (let i = 0; i < elements.length || oldFiber; i++) {
    const element = elements[i];
    let newFiber = null;
    const isSameType = element && oldFiber && element.type === oldFiber.type; //比较类型
    if (isSameType && element) {
      //类型相同,更新dom即可
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候
        sliding: null,
        dom: oldFiber.dom,
        alternate: oldFiber,
        effectTag: "update",
      };
    } else if (!isSameType && element) {
      //类型不同,新增
      newFiber = {
        type: element.type,
        props: element.props,
        parent: wipFiber,
        child: null, //这里我们没给值,是要在子节点中给,也就是i===0的时候
        sliding: null,
        dom: null,
        alternate: null,
        effectTag: "placement",
      };
    }
    if (!isSameType && oldFiber) {
      //类型不同,需要删除老节点
      oldFiber.effectTag = "deletion";
      deletions.push(oldFiber);
    }

    if (i === 0) {
      wipFiber.child = newFiber; //设置子节点
    }
    if (i !== 0 && i < elements.length) {
      preFiber.sliding = newFiber; //串联兄弟节点
    }
    oldFiber = oldFiber && oldFiber.sliding; //老节点往下走
    preFiber = newFiber;
  }
}
function commitDeletion() {
  deletions.forEach(commitWork);
  deletions = [];
}

function commitWork(fiber) {
  if (fiber.type instanceof Function && fiber.effectTag !== "deletion") {
    return;
  } //对于不是函数组件的fiber,是没有update和placement操作的
  if (fiber.root) {
    return; //根节点有dom了,不需要任何操作
  }
  //我们为根节点做了标记,根节点是没有parent的
  var parentFiber = fiber.parent;
  while (!parentFiber.dom) {
    parentFiber = parentFiber.parent;
  } //因为有了函数组件,我们不能确保每个fiber上全有dom,所以我们要一直往父节点上找真实的dom,placement和deletion全部需要用到父dom

  if (fiber.effectTag === "update") {
    updateDom(fiber.dom, fiber.alternate.props, fiber.props); //更新dom即可
  } else if (fiber.effectTag === "placement") {
    fiber.dom = createDom(fiber); //创建后,添加子元素中
    updateDom(fiber.dom, {}, fiber.props);
    if (fiber.sliding && fiber.sliding.dom) {
      parentFiber.dom.insertBefore(fiber.dom, fiber.sliding.dom);
    } else {
      parentFiber.dom.appendChild(fiber.dom);
    }
  } else if (fiber.effectTag === "deletion") {
    if (fiber.dom) {
      //说明不是函数组件
      parentFiber.dom.removeChild(fiber.dom); //删除当前dom
    } else {
      //说明是函数组件,我们需要继续往下找,但是后续的子fiber的effectTag不一定是deletion,我们需要手动赋值
      fiber.child.effectTag = "deletion";
      commitWork(fiber.child);
      //如果是函数组件中包含useEffect hooks 那么我们需要执行销毁的回调
      fiber.hooks
        .filter((item) => item.type === "useEffect")
        .forEach((item) => {
          if (item.destruction) {
            item.destruction(); //执行销毁回调
          }
        });
    }
  }
}
function updateDom(dom, preProps, nextProps) {
    console.log(arguments,'args')
  const isNew = (key) => preProps[key] !== nextProps[key]; //是不是新值
  const isPre = (key) => !(key in preProps); //是不是老值
  const isEvent = (key) => key.startsWith("on");
  const isRef = (key) => key === "ref";

  Object.keys(nextProps)
    .filter(isProps)
    .filter(isNew)
    .forEach((key) => {
      dom[key] = nextProps[key];
    }); //更新属性
  Object.keys(nextProps)
    .filter(isProps)
    .filter(isNew)
    .filter(isEvent)
    .forEach((event) => {
      const eventName = event.substring(2).toLowerCase();
      dom.addEventListener(eventName, nextProps[event]);
    }); //添加事件
  nextProps.style &&
    Object.keys(nextProps.style)
      .filter((key) => {
        if (!preProps.style) {
          return true;
        } else {
          return preProps.style[key] !== nextProps.style[key];
        }
      })
      .forEach((key) => {
        dom.style[key] = nextProps.style[key];
      });
  Object.keys(nextProps)
    .filter(isRef)
    .filter(isNew)
    .forEach((key) => {
      nextProps[key].current = dom;
    }); //为ref添加dom
  Object.keys(preProps)
    .filter(isProps)
    .filter(isPre)
    .forEach((key) => {
      dom[key] = "";
    }); //删除属性
  Object.keys(preProps)
    .filter(isProps)
    .filter(isPre)
    .filter(isEvent)
    .forEach((event) => {
      const eventName = event.substring(2).toLowerCase();
      dom.removeEventListener(eventName, nextProps[event]);
    }); //添加事件
  Object.keys(preProps)
    .filter(isRef)
    .filter(isPre)
    .forEach((key) => {
      preProps[key].current = null;
    }); //为ref删除dom
    preProps.style &&
    Object.keys(preProps.style)
      .filter((key) => {
        if (!nextProps.style) {
          return true;
        } else {
          return !(key in nextProps.style)
        }
      })
      .forEach((key) => {
        dom.style[key] = ''
      });
}
function flatObjectArr(arr) {
  //对象数组扁平化
  return arr.reduce((acc, item) => {
    if (item instanceof Array) {
      acc.push(...flatObjectArr(item));
    } else {
      acc.push(item);
    }
    return acc;
  }, []);
}
function performWork(fiber) {
  //执行当前fiber并返回下一个工作单元
  currentFiber = fiber;
  const isFunction = fiber.type instanceof Function;
  if (isFunction) {
    fiber.hooks = [];
  }
  let elements = isFunction
    ? [fiber.type(fiber.props)] //扁平化对象数组,有可能遇到map的情况
    : fiber.props.children;
  elements = flatObjectArr(elements);
  hookIndex = 0;
  //3.比较新老fiber并创建fiber
  reconcileChildren(fiber, elements);
  commitWork(fiber);
  //上面的流程中,child,sliding,parent,dom,我们全部正确的给值了。
  //4.返回下一个需要执行的fiber
  if (fiber.child) {
    return fiber.child; //优先子节点
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sliding) {
      return nextFiber.sliding; //如果有兄弟就是兄弟节点
    }
    nextFiber = nextFiber.parent; //回到父元素
  }
  return nextFiber;
}
function useEffect(callback, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;

  const hook = {
    type: "useEffect", //类型
    dependencies: dependencies, //每次拿最新的
    destruction:
      oldFiber && oldFiber.hooks[hookIndex].destruction
        ? oldFiber.hooks[hookIndex].destruction
        : null, //销毁函数
  };
  if (dependencies === void 0) {
    //安全的返回undefined,防止用户声明undefined
    hook.destruction = callback(); //如果不传任何依赖性,每次组件渲染全会更新
  } else if (dependencies.length === 0) {
    if (!oldFiber) {
      hook.destruction = callback(); //当没有依赖项时,只有初始化的时候会执行一次,并且获取销毁回调
    }
  } else {
    if (oldFiber) {
      const isChange = oldFiber.hooks[hookIndex].dependencies.some(
        (dependency, index) => !Object.is(dependency, dependencies[index])
      ); //查看依赖项是否变化
      if (isChange) {
        hook.destruction = callback(); //变化就执行
      }
    }
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
}
function useState(state) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate; //获取上一个fiber
  const hook = {
    type: "useState",
  };
  if (oldFiber) {
    hook.state = oldFiber.hooks[hookIndex].state; //说明是后续渲染,用上一次的值
  } else {
    hook.state = state; //说明是组件初次渲染,用第一次函数传入的值
  }
  const setState = (callback) => {
    const value =
      callback instanceof Function ? callback(hook.state) : callback; //用户可能传入值,也有可能传入表达式
    hook.state = value; //更新state
    rootFiber = {
      dom: preRootFiber.dom,
      props: preRootFiber.props,
      alternate: preRootFiber,
      root: "root",
    }; //重新渲染
    nextWork = rootFiber;
    deletions = [];
  };
  hook.setState = setState;
  currentFiber.hooks.push(hook); //保存到当前fiber中
  hookIndex++; //索引递增
  return [hook.state, setState];
}
function useCallback(fn, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useCallback",
    dependencies: dependencies,
  };
  if (oldFiber) {
    let isChange = oldFiber.hooks[hookIndex].dependencies.some(
      (dependency, index) => {
        return !Object.is(dependency, dependencies[index]);
      }
    );
    if (isChange) {
      hook.fn = hook.fn.bind(null); //依赖有变化,就返回一个新的函数
    } else {
      hook.fn = oldFiber.hooks[hookIndex].fn; //没有变化就返回旧的函数
    }
  } else {
    hook.fn = fn; //初次,返回fn
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook.fn;
}
function useMemo(fn, dependencies) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useMemo",
    dependencies: dependencies,
  };
  if (oldFiber) {
    let isChange = oldFiber.hooks[hookIndex].dependencies.some(
      (dependency, index) => {
        return !Object.is(dependency, dependencies[index]);
      }
    );
    if (isChange) {
      hook.result = fn();
    } else {
      hook.result = oldFiber.hooks[hookIndex].result;
    }
  } else {
    hook.result = fn();
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook.result;
}
function useRef(initialValue) {
  let oldFiber =
    currentFiber.alternate &&
    currentFiber.alternate.hooks &&
    currentFiber.alternate;
  const hook = {
    type: "useRef",
  };
  if (oldFiber) {
    const oldHook = oldFiber.hooks[hookIndex];
    currentFiber.hooks.push(hook);
    hookIndex++;
    return oldHook;
  } else {
    hook.current = initialValue;
  }
  currentFiber.hooks.push(hook);
  hookIndex++;
  return hook;
}
  1. index.html

我参考react官网中hooks部分给出的demo和自己写的做了验证,大家可以取消render函数的注释去尝试

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.26.0/babel.min.js"></script>
    <div id="root">
      <!-- <div id="div1">
            <p id="p1">我是p1</p>
            <p id="p2">我是p2</p>
        </div>
        <div id="div2">
            <p id="p3">我是p3</p>
            <p id="p4">我是p4</p>
        </div> -->
    </div>
    <script src="./react.js"></script>
    <script type="text/babel">
      /** @jsx createElement */
      function createTodos() {
        const todos = [];
        for (let i = 0; i < 50; i++) {
          todos.push({
            id: i,
            text: "Todo " + (i + 1),
            completed: Math.random() > 0.5,
          });
        }
        return todos;
      }
      function filterTodos(todos, tab) {
        console.log(
          "[ARTIFICIALLY SLOW] Filtering " +
            todos.length +
            ' todos for "' +
            tab +
            '" tab.'
        );
        let startTime = performance.now();
        while (performance.now() - startTime < 500) {
          // 在 500 毫秒内不执行任何操作以模拟极慢的代码
        }

        return todos.filter((todo) => {
          if (tab === "all") {
            return true;
          } else if (tab === "active") {
            return !todo.completed;
          } else if (tab === "completed") {
            return todo.completed;
          }
        });
      }
      function TodoList({ todos, theme, tab }) {
        const visibleTodos = useMemo(
          () => filterTodos(todos, tab),
          [todos, tab]
        );
        return (
          <div className={theme}>
            <p>
              <b>
                Note: <code>filterTodos</code> is artificially slowed down!
              </b>
            </p>
            <ul>
              {visibleTodos.map((todo) => (
                <li key={todo.id}>
                  {todo.completed ? <s>{todo.text}</s> : todo.text}
                </li>
              ))}
            </ul>
          </div>
        );
      }
      function App() {
        const todos = createTodos();
        const [tab, setTab] = useState("all");
        const [isDark, setIsDark] = useState(false);
        return (
          <div>
            <button onClick={() => setTab("all")}>All</button>
            <button onClick={() => setTab("active")}>Active</button>
            <button onClick={() => setTab("completed")}>Completed</button>
            <br />
            <label>
              <input
                type="checkbox"
                checked={isDark}
                onChange={(e) => setIsDark(e.target.checked)}
              />
              Dark mode
            </label>
            <hr />
            <TodoList
              todos={todos}
              tab={tab}
              theme={isDark ? "dark" : "light"}
            />
          </div>
        );
      }
      //render(document.getElementById("root"), <App />);//测试成功 验证useState和useMemo
      function Counter() {
        //测试ref
        let ref = useRef(0);

        function handleClick() {
          ref.current = ref.current + 1;
          alert("You clicked " + ref.current + " times!");
        }

        return <button onClick={handleClick}>点击!</button>;
      }
      //render(document.getElementById("root"), <Counter />);//测试成功 验证useRef
      function Form() {
        //测试ref操作dom
        const inputRef = useRef(null);

        function handleClick() {
          inputRef.current.focus();
        }

        return (
          <div>
            <input ref={inputRef} />
            <button onClick={handleClick}>聚焦输入框</button>
          </div>
        );
      }
      //render(document.getElementById("root"), <Form />); //测试成功 验证useRef操作dom
      function VideoPlayer() {
        const [isPlaying, setIsPlaying] = useState(false);
        const ref = useRef(null);

        function handleClick() {
          const nextIsPlaying = !isPlaying;
          setIsPlaying(nextIsPlaying);

          if (nextIsPlaying) {
            ref.current.play();
          } else {
            ref.current.pause();
          }
        }

        return (
          <div>
            <button onClick={handleClick}>{isPlaying ? "暂停" : "播放"}</button>
            <video
              width="250"
              ref={ref}
              onPlay={() => setIsPlaying(true)}
              onPause={() => setIsPlaying(false)}
            >
              <source
                src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.mp4"
                type="video/mp4"
              />
            </video>
          </div>
        );
      }
      //render(document.getElementById("root"), <VideoPlayer />); //测试成功 验证useRef操作dom
      function createConnection(serverUrl, roomId) {
        // 真正的实现会实际连接到服务器
        return {
          connect() {
            console.log(
              '✅ Connecting to "' + roomId + '" room at ' + serverUrl + "..."
            );
          },
          disconnect() {
            console.log(
              '❌ Disconnected from "' + roomId + '" room at ' + serverUrl
            );
          },
        };
      }
      function ChatRoom({ roomId }) {
        const [serverUrl, setServerUrl] = useState("https://localhost:1234");

        useEffect(() => {
          const connection = createConnection(serverUrl, roomId);
          connection.connect();
          return () => {
            connection.disconnect();
          };
        }, [roomId, serverUrl]);

        return (
          <div>
            <label>
              Server URL:{" "}
              <input
                value={serverUrl}
                onChange={(e) => setServerUrl(e.target.value)}
              />
            </label>
            <h1>Welcome to the {roomId} room!</h1>
          </div>
        );
      }

      function App1() {
        const [roomId, setRoomId] = useState("general");
        const [show, setShow] = useState(false);
        return (
          <div>
            <label>
              Choose the chat room:{" "}
              <select
                value={roomId}
                onChange={(e) => setRoomId(e.target.value)}
              >
                <option value="general">general</option>
                <option value="travel">travel</option>
                <option value="music">music</option>
              </select>
            </label>
            <button onClick={() => setShow(!show)}>
              {show ? "Close chat" : "Open chat"}
            </button>
            {show && <hr />}
            {show && <ChatRoom roomId={roomId} />}
          </div>
        );
      }
      //render(document.getElementById("root"), <App1 />); //验证成功 useEffect
      function DestroyComponent(){
        useEffect(()=>{
          return ()=>{
            console.log("销毁的时候促发")
          }
        })
        return (
          <p>我是p2</p> 
          )
      }
      function App2() {
        let [show, setShow] = useState(false);
        const handleClick = ()=>{
          setShow(!show)
        }
        return (
          <div>
            <p>我是p1</p>
            {show ? <DestroyComponent/>: <div>我是div3</div>}
            <button onClick={handleClick}>切换</button>
          </div>
        );
      }
      //render(document.getElementById("root"), <App2 />); //验证成功 切换后位置正确 useEffect 销毁后的函数执行
      function App3() {
        return (
          <div>
            {new Array(100).fill(0).map(item=>{
              return <p>循环创建的p标签</p>
            })}
          </div>
        );
      }
      render(document.getElementById("root"), <App3 />); //验证成功 对map情况做适配
    </script>
  </body>
</html>

暂时无法在飞书文档外展示此内容