React基础源码解析

前言:

前端魔术师卡颂的react学习视频(1 搭建项目架构_哔哩哔哩_bilibili)中提到了Rodrigo Pombo的一篇react源码教程:Build your own React

本文档分组旨在翻译和记录这篇文章的学习心得,作为react源码学习入门。

原文档目录

Step I: The createElement Function

Step II: The render Function

Step III: Concurrent Mode

Step IV: Fibers

Step V: Render and Commit Phases

Step VI: Reconciliation

Step VII: Function Components

Step VIII: Hooks

Review

下面是react应用创建最基础的代码;

首先定义了一个react元素,然后获取一个dom节点作为容器,最后将元素render到容器中;

const element = <h1 title="foo">Hello</h1>
const container = document.getElementById("root")
ReactDOM.render(element, container)

JSX代码通过babel这样的构建工具,转成JS代码;

// 替换前(JSX)
const element = <h1 title="foo">Hello</h1>
// 替换后(利用babel)
const element = React.createElement(
  "h1",  // tagname
  { title: "foo" }, // props
  "Hello" // children
)

我们再次将React.createElement函数的调用替换成输出的结果;

// 替换前
const element = React.createElement(
  "h1",  // tagname
  { title: "foo" }, // props
  "Hello" // children
)
// 替换后(上面代码的输出结果)
// type是tagename,props是元素所有的属性键值对,children通常是一个包含更多元素的数组
const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

接下来,我们需要替换ReactDom.render代码

const element = {
  type: "h1",
  props: {
    title: "foo",
    children: "Hello",
  },
}

const container = document.getElementById("root")

const node = document.createElement(element.type)
node["title"] = element.props.title

const text = document.createTextNode("")
text["nodeValue"] = element.props.children

node.appendChild(text)
container.appendChild(node)

这样,我们就完全使用了js语法,实现了和使用react一样的应用程序;

Step1:creatElement

从现在开始,我们重新开始构建我们自己的 react;

这一小节,我们先实现自己的 creatElement 功能;

tips:此处使用了es6的语法知识;扩展运算符 和 剩余运算符

ES6中扩展运算符(spread)和剩余运算符(rest)详解_es 扩展运算符 英文-CSDN博客

// 我们使用展开语法和rest参数语法传递prop和children;
// 使用rest语法,可以保证children属性始终是数组;
function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children,
    },
  }
}
const element = React.createElement(
  "div",
  { id: "foo" },
  React.createElement("a", null, "bar"),
  React.createElement("b")
)
const container = document.getElementById("root")
ReactDOM.render(element, container)

children数组还可以包含string,number这样的基础文本类型,所以我们将不是对象的内容包装在一个特殊的类型元素 - TEXT-ELEMENT中;

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // children,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

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

上面的例子中,我们仍然使用的是react的creatElement;接下来我们需要定义自己的库Didact;

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      // children,
      children: children.map(child =>
        typeof child === "object"
          ? child
          : createTextElement(child)
      ),
    },
  }
}

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

const Didact = {
  createElement,
}

// 直接使用createElement创建element
const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)

const container = document.getElementById("root")
ReactDOM.render(element, container)

我们还需要增加一行注释,当babel转义JSX的时候,使用我们定义的函数;

/** @jsx Didact.createElement */
// 使用jsx,需要配合babel
const element = (
  <div id="foo">
    <a>bar</a>
    <b />
  </div>
)

const container = document.getElementById("root")
ReactDOM.render(element, container)

要点总结:

我们在使用react的时候,使用的JSX语法,babel会帮我们转译成调用react.creatElement方法;

const element = Didact.createElement(
  "div",
  { id: "foo" },
  Didact.createElement("a", null, "bar"),
  Didact.createElement("b")
)

creatElement 方法最后会返回一个如下的数据结构:

要点是:为基础的文本类型创建一个特殊的type(TEXT_ELEMENT),使用扩展运算符传递props,使用剩余运算符保证children始终是数组形式

{
    type, 
    props: {
      ...props,
      children,
    },
}

Step2:render

上一节,我们为Didact实现了creatElement函数;

const container = document.getElementById("root")
ReactDOM.render(element, container)

但是最后还是使用的ReactDOM.render;

本节,我们将实现render函数;

const Didact = {
  createElement,   // 上节实现
  render, // 本节实现
}

首先,我们需要使用element类型创建 DOM节点, 如果存在子节点,需要递归处理;

function render (elemtn, container) {
  
  const dom = document.creatElement(element.type)
  // 递归处理children
  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

如果是TEXT_ELEMENT元素,需要创建一个text节点;

function render (element, container) {
const dom =
  element.type == "TEXT_ELEMENT"
    ? document.createTextNode("")
    : document.createElement(element.type);
  
  // 递归处理children
  element.props.children.forEach(child =>
    render(child, dom)
  )
  container.appendChild(dom)
}

为节点添加props

function render(element, container) {
  
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);
  
  // 排除掉children属性
  const isProperty = key => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name];
    });
  
  element.props.children.forEach(child => render(child, dom));
  container.appendChild(dom);
}

至此,第一节和第二节,我们获得了一个可以将jsx渲染为DOM的库:

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map(child =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

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

function render(element, container) {
  // 创建dom
  const dom =
    element.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(element.type);
  const isProperty = key => key !== "children";
  Object.keys(element.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = element.props[name];
    });
  element.props.children.forEach(child => render(child, dom));
  // 挂载dom
  container.appendChild(dom);
}

const Didact = {
  createElement,
  render
};

/** @jsx Didact.createElement */
const element = (
  <div style="background: salmon">
    <h1>Hello World</h1>
    <h2 style="text-align:right">from Didact</h2>
  </div>
);
const container = document.getElementById("root");
Didact.render(element, container);

 Step3:concurrent并发模式

第三节中,我们利用递归调用render函数来创建DOM节点,但是如果我们的元素树很大,会阻塞浏览器线程;

因此,我们将工作拆分为小单元,在我们完成每个单元任务之后,如果有其他事情需要做,会让浏览器终止渲染;

我们可以使用requestIdleCallback制作循环,与settimeout类似,区别在于,前者是浏览器空闲的时候,才会执行调用;

关于requestIdleCallback和settimeout,可以看这篇文档

详解 requestIdleCallback - 掘金

React对于DOM的渲染已经不使用requestIdleCallback,现在使用Scheduler;但是原理上是类似的;

关于Scheduler原理和实现,可以看这篇文档

第八章 Concurrent Mode - Scheduler的原理与实现 - 《React 技术揭秘》 - 书栈网 · BookStack

React 的 Scheduler 的简单说明

React 为了解决 15 版本存在的问题:组件的更新是递归执行,所以更新一旦开始,中途就无法中断。当层级很深时,递归更新时间超过了16ms,用户交互就会卡顿。

React 引入了 Fiber 的架构,同时配合 Schedduler 的任务调度器,在 Concurrent(并发) 模式下可以将 React 的组件更新任务变成可中断、恢复的执行,就减少了组件更新所造成的页面卡顿。

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    
    // 要开始使用循环,我们需要设置第一个工作单元,
    // 然后编写一个 performUnitOfWork 函数,
    // 该函数不仅执行工作,还返回下一个工作单元。
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // requestIdleCallback 还给了我们一个截止日期参数。
    // 我们可以使用它来检查浏览器需要再次控制之前我们还有多少时间。
    shouldYield = deadline.timeRemaining() < 1
    
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

要点总结:

并发模式是对render递归的优化;本节简单实现了一个利用requestIdleCallback制作的workLoop,利用浏览器渲染的空闲时间来执行我们的渲染任务;

首先我们需要设置第一个工作单元,然后剩下的交给performUnitOfWork(执行工作单元)处理;

其中performUnitOfWork函数至关重要。他不仅要执行工作单元,还要返回下一个工作单元。

Step4:fibers

上一节中,我们实现了一个利用 requestIdelCallback 制作的 workLoop,其中有个关键的函数 performUnitOfWork;

那么如何组织工作单元,我们需要一个新的数据结构:fiber;

我们为每个element提供一个fiber,每个fiber都是一个工作单元;

首先,我们创建 root fiber,并将其设置为 nextUnitOfWork,剩下的工作将交给performUnitOfWork处理;

每个fiber需要做三件事情:

  1. 将element添加到DOM;
  2. 为element的children创建fiber;
  3. 选择下一个工作单元;

fiber数据结构的目的是为了更容易得查找下一个工作单元;每个fiber与其children,下一个sibling(兄弟姐妹),parent都有关联;

fibler查找下一个工作单元遵循以下原则:

  1. 如果fiber有child,下一个工作单元就是第一个child;
  2. 如果没有child,下一个工作单元是sibling,
  3. 既没有child,也没有sibling,则去找parent的sibling,也就是uncle;
  4. 如果parent没有sibling,则继续向上寻找parent的sibling,直到root;
  5. 如果到达了root,表示我们完成了所有的render工作;

接下来,我们使用代码实现以上思想:

首先,我们将创建dom独立提取成为一个函数,将render中的代码删除;

function creatDom (fiber) {
  // 创建dom
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // 创建props
  const isProperty = key => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name];
    });
  return dom
}

function render(element, container) {
    // TODO set next unit of work
}


let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    
    // 要开始使用循环,我们需要设置第一个工作单元,
    // 然后编写一个 performUnitOfWork 函数,
    // 该函数不仅执行工作,还返回下一个工作单元。
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    
    // requestIdleCallback 还给了我们一个截止日期参数。
    // 我们可以使用它来检查浏览器需要再次控制之前我们还有多少时间。
    shouldYield = deadline.timeRemaining() < 1
    
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(nextUnitOfWork) {
  // TODO
}

我们在render函数中设置nextUnitOfwork为root根fiber,

当浏览器准备就绪,将调用workLoop,从root节点开始工作;

function render(element, container) {
  nextUnitOfWork = {
    dom: container, // document.getElementByid('root')
    props: {
      children: [element],
    }
  }
}

let nextUnitOfWork = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
    
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(fiber) {
  // TODO add dom node
  // TODO create new fibers
  // TODO return next unit of work
  
}

接下来,我们将聚焦在performUnitOfWork函数中

// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(fiber) {
  
  // 1 add dom node
  if (!fiber.dom) {
    fiber.dom = creatDom(fiber)
  }
  if (fiber.parent) {
    fiber.parent.dom.appendChildren(fiber.dom)
  }
  
  // 2 create new fibers
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null
  // 遍历所有children,将fible的下一级的第一个元素设置为child
  // 并依次将child中的子fible的sibling设置为下一个元素
  while (index < elements.length) {
    const element = elements[index]
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
    if (index === 0) {
      fiber.child = newFiber
    } else {
      // 上一个fiber的sibling设置为当前fible
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    index++
  }
  // 3 return next unit of work
  // 如果存在child,直接return
  if(fiber.child){
     return fible.child
  }
  
  let nextFiber = fiber
  while (nextFiber) {
    // 如果存在同级fible,返回同级
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // 即不存在child,又不存在sibling,则回到parent节点,返回parent的sibling
    nextFiber = nextFiber.parent
  } 

这样,我们就得到了一个可以创建fible,并且可以组织工作单元的performUnitOfWork函数;

这样,我们的并发模式完整代码就是如下:

function creatDom (fiber) {
  // 创建dom
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);
  // 创建props
  const isProperty = key => key !== "children";
  Object.keys(fiber.props)
    .filter(isProperty)
    .forEach(name => {
      dom[name] = fiber.props[name];
    });
  return dom
}

function render(element, container) {
  nextUnitOfWork = {
    dom: container, // document.getElementByid('root')
    props: {
      children: [element],
    }
  }
}

let nextUnitOfWork = null

// workLoop主要控制的是performUnitOfWork的执行
function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
    
  }
  requestIdleCallback(workLoop)
}

requestIdleCallback(workLoop)

// 该函数不仅执行工作,还返回下一个工作单元。
function performUnitOfWork(fiber) {
  
  // 1 add dom node
  if (!fiber.dom) {
    fiber.dom = creatDom(fiber)
  }
  if (fiber.parent) {
    fiber.parent.dom.appendChildren(fiber.dom)
  }
  
  // 2 create new fibers
  const elements = fiber.props.children
  let index = 0
  let prevSibling = null
  // 遍历所有children,将fible的下一级的第一个元素设置为child
  // 并依次将child中的子fible的sibling设置为下一个元素
  while (index < elements.length) {
    const element = elements[index]
    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }
    if (index === 0) {
      fiber.child = newFiber
    } else {
      // 上一个fiber的sibling设置为当前fible
      prevSibling.sibling = newFiber
    }
    prevSibling = newFiber
    index++
  }
  // 3 return next unit of work
  // 如果存在child,直接return
  if(fiber.child){
     return fible.child
  }
  
  let nextFiber = fiber
  while (nextFiber) {
    // 如果存在同级fible,返回同级
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    // 即不存在child,又不存在sibling,则回到parent节点,返回parent的sibling
    nextFiber = nextFiber.parent
  } 

Step5:Render 和 Commit

上一步,我们已经实现了一个performUnitOfWork函数用来组织我们的工作单元;

但是,每次处理element的时候,我们都会向DOM添加一个新的节点;在这个过程中,浏览器可能会终止我们的workLoop,这样,UI渲染将会是不完整的。

所以我们需要删除导致DOM变化部分的代码;

// 删除
if (fiber.parent) {
    fiber.parent.dom.appendChild(fiber.dom)
  }

我们还需要新增一个变量,wipRoot,来跟踪fiber树;

一旦我们完成了所有的工作,我们将整个fible树提交给DOM;所以我们还需要一个commitRoot函数,在这个函数中,我们递归的将节点附加到DOM中;

function commitRoot() {
  commitWork(wipRoot.child)
  wipRoot = null
}

// 递归处理DOM的挂载
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
  }
  nextUnitOfWork = wipRoot
}

let nextUnitOfWork = null
let wipRoot = null

function workLoop(deadline) {
  let shouldYield = false
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(
      nextUnitOfWork
    )
    shouldYield = deadline.timeRemaining() < 1
  }

  // 如果不存在下一个节点(所有fible创建并执行完毕),提交fible树
  if (!nextUnitOfWork && wipRoot) {
    commitRoot()
  }

  requestIdleCallback(workLoop)
}

要点总结:

到此为止,我们已经实现了内容的添加;

将jsx语法通过creatElement变成react的element(包含type,props的对象),

再利用render创建wipRoot,并指定为nextUnitOfWork,

再通过requestIdleCallback执行workLoop,

workLoop负责在浏览器空闲的时候执行performUnitOfWork函数,

performUnitOfWork函数主要负责创建dom,为所有child创建fible并建立关联,根据fible机制返回下一个fible;

一旦所有的fible都执行完毕,则触发commitRoot函数,进行dom的渲染;

Step6:Reconciliation 调和

前面几节,我们已经实现了内容的添加。但是还没有更新和删除节点;

这一节我们要做的就是在我们render函数上收到的 element 与 我们提交给DOM的最后一个fible进行比较

所以在完成commit后,我们需要保存一个 commit给DOM的最后一个fible树的reference引用,称其为currentRoot;

我们也需要将这种备份属性给到每一个fiber,这种备份属性是指向旧fiber的link,即我们在上一个commit阶段提交给DOM的fible;

function commitRoot() {
  commitWork(wipRoot.child)
  // 备份当前提交给DOM的fible
  currentRoot = wipRoot
  wipRoot = null
}

// 递归处理DOM的挂载
function commitWork(fiber) {
  if (!fiber) {
    return
  }
  const domParent = fiber.parent.dom
  domParent.appendChild(fiber.dom)
  commitWork(fiber.child)
  commitWork(fiber.sibling)
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element],
    },
    alternate: currentRoot,
  }
  nextUnitOfWork = wipRoot
}

let nextUnitOfWork = null
let wipRoot = null
// 增加一个currentRoot
let currentRoot = null

我们将 preformUnitOfWork 中创建 fible 的代码提取出来,放到 reconcileChildren 函数中;

在这个函数中,我们将协调旧fiber与新elements;

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
  }
}

function reconcileChildren (fiber,elements){
  let index = 0
  let prevSibling = null

  while (index < elements.length) {
    const element = elements[index]

    const newFiber = {
      type: element.type,
      props: element.props,
      parent: fiber,
      dom: null,
    }

    if (index === 0) {
      fiber.child = newFiber
    } else {
      prevSibling.sibling = newFiber
    }

    prevSibling = newFiber
    index++
  }
}

接下来,我们聚焦 reconcileChildren 函数

在这个函数中,我们需要同时迭代旧fiber和新elements;这里我们只考虑oldFiber和element这两项最重要的东西;

element是我们想要渲染给DOM的内容,oldFiber是上次渲染的内容;

我们需要比较两者,看看是否需要对DOM进行更改;

每次执行reconcileChildren函数的时候,都是performUnitOfWork再次执行的时候,此时函数内的oldFiber是wipFiber的child,每执行一次while,element切换为下一个child,oldFiber切换为child的sibling,再循环一次,oldFiber切换为sibling的sibling,以此类推,完成同一层级元素的比较;

const sameType =
      oldFiber &&
      element &&
      element.type == oldFiber.type

比较处理的方式如下:

  1. 如果oldFiber的type和新element的type相同,保留DOM节点,只更新prop
    1. 创建一个新的fiber,保留旧fiber中的DOM和element中的prop
    2. 添加一个新属性effectTag,并设置为 “UPDATE”
if (sameType) {
      newFiber = {
        type: oldFiber.type,
        props: element.props,
        dom: oldFiber.dom,
        parent: wipFiber,
        alternate: oldFiber,
        effectTag: "UPDATE",
      }
    }
  1. 如果type不同,并且有新的element,则创建一个新的DOM节点;
    1. 将dom属性置空
    2. alternate关联fiber置空
    3. effectTag设置为“PLACEMENT”
newFiber = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        effectTag: "PLACEMENT",
      }
  1. type不同,并且有oldFiber,则删除旧节点;
if (oldFiber && !sameType) {
      oldFiber.effectTag = "DELETION"
      deletions.push(oldFiber)
    }

// 在commitRoot函数中,遍历删除
deletions.forEach(commitWork)

完整的 reconcileChildren 代码:

function reconcileChildren(wipFiber, elements) {
  // 该函数只比较wipFiber关联fiber的child(以及child的sibling)和 elements
  // 每次执行这个函数,wipFiber都会按照fiber遍历的规则,切换为下一个执行单元;
  // 也就是说此时的 wipFiber 就是上一次执行这个函数的时候,创建的fiber;
  // wipFiber.alternate 也是在上一次执行该函数的时候进行的关联
  let index = 0
  // oldFiber一开始是wipFiber关联fiber的第一个子fiber
  // 后面在while中通过sibling进行平移切换
  
  let oldFiber =
    wipFiber.alternate && wipFiber.alternate.child
  let prevSibling = null

  // 通过while,将当前wipFiber的所有子fiber创建完毕
  // 并且完成了和oldFiber的比较,并打上effecttag标签
  while (
    index < elements.length ||
    oldFiber != null
  ) {
    
    const element = elements[index]
    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 = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        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++
  }
}

下面,我们更改commitWork 函数,增加对effectTag的处理;

  1. ‘PLACEMENT’标签,将DOM挂在到parent节点
if (
    fiber.effectTag === "PLACEMENT" &&
    fiber.dom != null
  ) {
    domParent.appendChild(fiber.dom)
  }
  1. ‘DELETION’标签,删除DOM节点
else if (fiber.effectTag === "DELETION") {
    domParent.removeChild(fiber.dom)
  }
  1. ‘UPDATE’标签,更新DOM的props
else if (
    fiber.effectTag === "UPDATE" &&
    fiber.dom != null
  ) {
    updateDom(
      fiber.dom,
      fiber.alternate.props,
      fiber.props
    )
  } 

更新DOM操作我们单独作为一个处理函数:

// 先定义几个过滤器函数
// 是否是属性(排除掉children)
const isProperty = key => key !== "children"
// 是否是新的属性值
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
// 是否是过时的属性(已经不需要的属性)
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
  // 删除旧属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })

  // 设置/更新新的属性值
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
}

DOM元素还有一类属性比较特殊,就是事件监听,所以,如果属性名称以on开头,我们需要特殊处理;

// 是否是事件
const isEvent = key => key.startsWith("on")
// 是否是属性(过滤掉children和event事件)
const isProperty = key =>
  key !== "children" && !isEvent(key)
// 是否是新的属性值
const isNew = (prev, next) => key =>
  prev[key] !== next[key]
// 是否是过时的属性(已经不需要的属性)
const isGone = (prev, next) => key => !(key in next)

function updateDom(dom, prevProps, nextProps) {
  // 移除/更改event监听
  Object.keys(prevProps)
    .filter(isEvent)
    .filter(
      key =>
        !(key in nextProps) ||
        isNew(prevProps, nextProps)(key)
    )
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.removeEventListener(
        eventType,
        prevProps[name]
      )
    })

  // 新增事件监听
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      const eventType = name
        .toLowerCase()
        .substring(2)
      dom.addEventListener(
        eventType,
        nextProps[name]
      )
    })

  
  // 删除旧属性
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach(name => {
      dom[name] = ""
    })

  // 设置/更新新的属性值
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach(name => {
      dom[name] = nextProps[name]
    })
}

Step7:实现函数组件

下面是一个基础的函数组件

/** @jsx Didact.createElement */
function App(props) {
  return <h1>Hi {props.name}</h1>
}
const element = <App name="foo" />
const container = document.getElementById("root")
Didact.render(element, container)

转换成js

function App(props) {
  return Didact.createElement(
    "h1",
    null,
    "Hi ",
    props.name
  )
}
const element = Didact.createElement(App, {
  name: "foo",
})

我们可以看出,App这个函数组件的 fiber 并没有DOM节点,并且child并非在props中,而是在函数运行结果中;

那么我们针对函数式组件需要做特殊处理,定义两个函数

updateHostComponent函数中继续执行之前的操作(creatDom,reconcileChildren)

updateFunctionComponent函数中通过执行函数组件,得到children,再执行reconcileChildren;

function performUnitOfWork(fiber) {
  const isFunctionComponent =
    fiber.type instanceof Function
  if (isFunctionComponent) {
    updateFunctionComponent(fiber)
  } else {
    updateHostComponent(fiber)
  }
  if (fiber.child) {
    return fiber.child
  }
  let nextFiber = fiber
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling
    }
    nextFiber = nextFiber.parent
  }
}

function updateFunctionComponent(fiber) {
  // TODO
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber)
  }
  reconcileChildren(fiber, fiber.props.children)
}

由于函数式组件的fiber没有dom节点,在commitWork中,子fiber的dom无法直接挂载到parentDom上,所以需要特殊处理:

  1. 沿着fiber向上走,找到具有DOM节点的fiber作为parent节点;
let domParentFiber = fiber.parent
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent
  }
  const domParent = domParentFiber.dom

// 如果需要挂载的话,
domParent.appendChild(fiber.dom)
  1. 删除节点,需要删除parent节点中的child
else if (fiber.effectTag === "DELETION") {
    commitDeletion(fiber, domParent)
  }
​

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom)
  } else {
    commitDeletion(fiber.child, domParent)
  }
}

Step8:实现hooks

我们将函数式组件的案例更换为经典的计数器

/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1)
  return (
    <h1 onClick={() => setState(c => c + 1)}>
      Count: {state}
    </h1>
  )
}
const element = <Counter />
const container = document.getElementById("root")
Didact.render(element, container)

那么如何实现这个useState呢?

实现useState之前,我们思考一下useState该满足哪些条件;

首先,useState需要存储一些私有变量,这个私有变量可以在函数外部改变,并且不会相互污染;也就是说useState是个大闭包;

这个闭包return出去的setState可以操作闭包中的hook对象,这个hook对象需要存储state值,和一个queue队列数组。

如何保证下一次执行useState的时候,还能拿到上一次存储的state,以及最新的queue队列呢?

每次执行useState的时候,将这个hook赋值给wipFiber对象,而wipFiber始终是当前正在处理的fiber;这样,就将hook状态巧妙的存储到了fiber中,达到了持久化的效果;由于fiber的关联属性,在处理新的fiber的过程中,我们也能找到oldFiber中的hook对象;相当于通过将hook赋值给fiber,利用fiber的关联属性,持久化并传递hook;

触发setState的时候,都做了些什么呢?

当触发setState的时候,会直接往hook.queue中添加一个action,相当于操作了当前组件的fiber中的hook对象(同一个引用地址);

之后,setState重新设置nextUnitOfWork之后,woorkLoop满足执行条件,再次触发新fiber的创建流程,也就是rerender的过程;

当再次执行到该组件中的useState的时候(preformUnitOfWork > updateFunctionComponent > children = [fiber.type(fiber.props)] > useState),会从当前fiber的关联oldFiber(oldFiber在当前fiber的parent fiber中就已经建立了联系)中取出hook对象,里面有上一次渲染的state,以及setState触发添加的action,执行这个action,得到新的state,并初始化一个新的hook对象,push给当前fiber的hooks对象(存在多个useState的时候,需要通过全局的hookIndex来找到对应的hook);

hookIndex的作用是什么?

由于wipFiber的hooks数组顺序是按照执行先后顺序来的。下一次组件再次执行的时候,通过全局的hookIndex找到oldFiber中对应的oldHook,这也是为什么组件中的hooks使用不能写在条件语句或者循环体中,是为了保证hooks的顺序不会乱;

下面是具体的代码实现:

// 当前正在进行中的fiber
let wipFiber = null;
// 全局变量,在同一个fiber(组件)中用来在hooks找到对应hook
let hookIndex = null;

function updateFunctionComponent(fiber) {
  debugger;
  wipFiber = fiber;
  hookIndex = 0;
  // 每执行一次hooks,往这个数组里保存一份最新的state,hook:{state,queue:[]}
  wipFiber.hooks = [];
  // useState在此时执行
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

function useState(initial) {
  debugger;
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };
  // 执行action,更新state
  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    hook.state = action(hook.state);
  });

  const setState = (action) => {
    debugger;
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

有一个疑问:

为什么不直接在setState中操作hook.state的值呢?非要兜一圈,通过queue队列?

这样下一次执行useState的时候,也能从oldFiber中取到修改后的值。

总结:

到此为止,我们已经实现了几乎完整的react功能,下面是一段示例代码;

Counter2组件中使用了两次useState,用来演示hookIndex的作用

Counter作为Counter2兄弟组件,用来演示hooks,hook在wipFiber中的流转

执行环境:didact-8 (forked) - CodeSandbox

function createElement(type, props, ...children) {
  return {
    type,
    props: {
      ...props,
      children: children.map((child) =>
        typeof child === "object" ? child : createTextElement(child)
      )
    }
  };
}

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

function createDom(fiber) {
  const dom =
    fiber.type == "TEXT_ELEMENT"
      ? document.createTextNode("")
      : document.createElement(fiber.type);

  updateDom(dom, {}, fiber.props);

  return dom;
}

const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key);
const isNew = (prev, next) => (key) => prev[key] !== next[key];
const isGone = (prev, next) => (key) => !(key in next);
function updateDom(dom, prevProps, nextProps) {
  //Remove old or changed event listeners
  Object.keys(prevProps)
    .filter(isEvent)
    .filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.removeEventListener(eventType, prevProps[name]);
    });

  // Remove old properties
  Object.keys(prevProps)
    .filter(isProperty)
    .filter(isGone(prevProps, nextProps))
    .forEach((name) => {
      dom[name] = "";
    });

  // Set new or changed properties
  Object.keys(nextProps)
    .filter(isProperty)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
      dom[name] = nextProps[name];
    });

  // Add event listeners
  Object.keys(nextProps)
    .filter(isEvent)
    .filter(isNew(prevProps, nextProps))
    .forEach((name) => {
      const eventType = name.toLowerCase().substring(2);
      dom.addEventListener(eventType, nextProps[name]);
    });
}

function commitRoot() {
  deletions.forEach(commitWork);
  commitWork(wipRoot.child);
  currentRoot = wipRoot;
  wipRoot = null;
}

function commitWork(fiber) {
  if (!fiber) {
    return;
  }

  let domParentFiber = fiber.parent;
  while (!domParentFiber.dom) {
    domParentFiber = domParentFiber.parent;
  }
  const domParent = domParentFiber.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") {
    commitDeletion(fiber, domParent);
  }

  commitWork(fiber.child);
  commitWork(fiber.sibling);
}

function commitDeletion(fiber, domParent) {
  if (fiber.dom) {
    domParent.removeChild(fiber.dom);
  } else {
    commitDeletion(fiber.child, domParent);
  }
}

function render(element, container) {
  wipRoot = {
    dom: container,
    props: {
      children: [element]
    },
    alternate: currentRoot
  };
  deletions = [];
  nextUnitOfWork = wipRoot;
}

let nextUnitOfWork = null;
let currentRoot = null;
let wipRoot = null;
let deletions = null;

function workLoop(deadline) {
  let shouldYield = false;
  while (nextUnitOfWork && !shouldYield) {
    nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
    shouldYield = deadline.timeRemaining() < 1;
  }

  if (!nextUnitOfWork && wipRoot) {
    commitRoot();
  }

  requestIdleCallback(workLoop);
}

requestIdleCallback(workLoop);

function performUnitOfWork(fiber) {
  debugger;
  const isFunctionComponent = fiber.type instanceof Function;
  if (isFunctionComponent) {
    updateFunctionComponent(fiber);
  } else {
    updateHostComponent(fiber);
  }
  if (fiber.child) {
    return fiber.child;
  }
  let nextFiber = fiber;
  while (nextFiber) {
    if (nextFiber.sibling) {
      return nextFiber.sibling;
    }
    nextFiber = nextFiber.parent;
  }
}

let wipFiber = null;
let hookIndex = null;

function updateFunctionComponent(fiber) {
  debugger;
  wipFiber = fiber;
  hookIndex = 0;
  wipFiber.hooks = [];
  const children = [fiber.type(fiber.props)];
  reconcileChildren(fiber, children);
}

function useState(initial) {
  debugger;
  const oldHook =
    wipFiber.alternate &&
    wipFiber.alternate.hooks &&
    wipFiber.alternate.hooks[hookIndex];
  const hook = {
    state: oldHook ? oldHook.state : initial,
    queue: []
  };

  const actions = oldHook ? oldHook.queue : [];
  actions.forEach((action) => {
    hook.state = action(hook.state);
  });

  const setState = (action) => {
    debugger;
    hook.queue.push(action);
    wipRoot = {
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot
    };
    nextUnitOfWork = wipRoot;
    deletions = [];
  };

  wipFiber.hooks.push(hook);
  hookIndex++;
  return [hook.state, setState];
}

function updateHostComponent(fiber) {
  if (!fiber.dom) {
    fiber.dom = createDom(fiber);
  }
  reconcileChildren(fiber, fiber.props.children);
}

function reconcileChildren(wipFiber, elements) {
  let index = 0;
  let oldFiber = wipFiber.alternate && wipFiber.alternate.child;
  let prevSibling = null;

  while (index < elements.length || oldFiber != null) {
    const element = elements[index];
    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 = {
        type: element.type,
        props: element.props,
        dom: null,
        parent: wipFiber,
        alternate: null,
        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++;
  }
}

const Didact = {
  createElement,
  render,
  useState
};

/** @jsx Didact.createElement */
function Counter2() {
  const [state1, setState1] = Didact.useState(3);
  const [state2, setState2] = Didact.useState(2);
  return (
    <h2
      onClick={() => {
        setState1((c) => c + 3);
        setState2((c) => c + 2);
      }}
      style="user-select: none"
    >
      Count: {state1} + {state2}
    </h2>
  );
}
function Counter() {
  const [state1, setState1] = Didact.useState(1);
  return (
    <h1 onClick={() => setState1((c) => c + 1)} style="user-select: none">
      Count: {state1}
    </h1>
  );
}
function App() {
  return (
    <div>
      <Counter />
      <Counter2 />
    </div>
  );
}

const element = <App />;
const container = document.getElementById("root");
Didact.render(element, container);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/114402.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

闭循环低温恒温器的使用注意事项

与液氮恒温器相比&#xff0c;闭循环低温恒温器显得稍微复杂一些&#xff01;这主要表现在组成部分、体积重量、使用操作、升降温时间等方面。闭循环低温恒温器主要由冷头、氦压缩机、两根氦气连管组成&#xff0c;配套设备还有控温仪、真空泵&#xff0c;可能还有循环水冷机。…

【Proteus仿真】【Arduino单片机】简易电子琴

文章目录 一、功能简介二、软件设计三、实验现象联系作者 一、功能简介 本项目使用Proteus8仿真Arduino单片机控制器&#xff0c;使用无源蜂鸣器、按键等。 主要功能&#xff1a; 系统运行后&#xff0c;按下K1-K7键发出不同音调。 二、软件设计 /* 作者&#xff1a;嗨小易&a…

动作捕捉系统处理单点多点丢点问题

在动作捕捉数据采集过程中&#xff0c;丢点是经常容易遇到的问题。NOKOV度量动作捕捉软件可以方便地解决丢点问题。 一、单点丢点的处理 如下图&#xff0c;已经采集了动捕数据。 查看是否有丢点&#xff0c;在形影软件左上角选择“窗口分割”&#xff0c;在下方分割出一个空…

Python接口自动化测试实战,一篇足矣

接口自动化测试是指通过编写程序来模拟用户的行为&#xff0c;对接口进行自动化测试。Python是一种流行的编程语言&#xff0c;它在接口自动化测试中得到了广泛应用。下面详细介绍Python接口自动化测试实战。 1、接口自动化测试框架 在Python接口自动化测试中&#xff0c;我们…

ROS学习笔记(4):ROS架构和通讯机制

前提 前4篇文章以及帮助大家快速入门ROS了&#xff0c;而从第5篇开始我们会更加注重知识积累。同时我强烈建议配合B站大学的视频一起服用。 1.ROS架构三层次&#xff1a; 1.基于Linux系统的OS层&#xff1b; 2.实现ROS核心通信机制以及众多机器人开发库的中间层&#xff1b…

HarmonyOS开发:基于http开源一个网络请求库

前言 网络封装的目的&#xff0c;在于简洁&#xff0c;使用起来更加的方便&#xff0c;也易于我们进行相关动作的设置&#xff0c;如果&#xff0c;我们不封装&#xff0c;那么每次请求&#xff0c;就会重复大量的代码逻辑&#xff0c;如下代码&#xff0c;是官方给出的案例&am…

CSS3背景样式

在CSS 2.1中&#xff0c;background属性的功能还无法满足设计的需求&#xff0c;为了方便设计师更灵活地设计需要的网页效果&#xff0c;CSS3在原有background基础上新增了一些功能属性&#xff0c;可以在同一个对象内叠加多个背景图像&#xff0c;可以改变背景图像的大小尺寸&…

nodelist 与 HTMLCollection 的区别

原地址 https://cloud.tencent.com/developer/article/2013289 节点与元素 根据 W3C 的 HTML DOM 标准&#xff0c;HTML 文档中的所有内容都是节点&#xff1a; 整个文档是一个文档节点每个 HTML 元素是元素节点HTML 元素内的文本是文本节点每个 HTML 属性是属性节点注释是注…

基于C语言实现扫雷小游戏

扫雷游戏 1. 扫雷游戏分析和设计1.1 扫雷游戏的功能说明1.2 游戏的分析和设计1.2.1 数据结构的分析 2. 扫雷游戏的代码实现3. 扫雷游戏的扩展 1. 扫雷游戏分析和设计 1.1 扫雷游戏的功能说明 使用控制台实现经典的扫雷游戏 游戏可以通过菜单实现继续玩或者退出游戏 扫雷的棋…

【考研数学】概率论与数理统计 —— 第七章 | 参数估计(2,参数估计量的评价、正态总体的区间估计)

文章目录 一、参数估计量的评价标准1.1 无偏性1.2 有效性1.3 一致性 二、一个正态总体参数的双侧区间估计2.1 对参数 μ \mu μ 的双侧区间估计 三、一个正态总体的单侧置信区间四、两个正态总体的双侧置信区间写在最后 一、参数估计量的评价标准 1.1 无偏性 设 X X X 为总…

技能证里的天花板—阿里云云计算架构师ACE认证!

在当今的社会中&#xff0c;想要获得一份好工作、得到丰厚的报酬&#xff0c;唯一的方法就是证明自己优秀&#xff0c;能给公司创造价值&#xff0c;是大多数公司想要看到的。 那么在面试过程中&#xff0c;怎么样才能让面试官一眼就记住呢&#xff1f;那一定是有一份足够优秀…

JVM虚拟机:JVM的垃圾回收清除算法(GC)有哪些

垃圾回收清除算法 引用计数法 标记清除 拷贝算法 标记压缩 引用计数法 有一个引用指向对象,那么引用计数就加1,少一个引用指向,那么引用计数就减1,这种方法了解一下就好,JVM机会不会使用这种方法,因为它在每次对象赋值的时候都要维护引用计数器,且计数器本身也有一定的…

JAVA虚拟机-第2章 Java自动内存管理-异常实践

Java堆溢出 堆的参数设置&#xff1a;将堆的最小值-Xms参数与最大值-Xmx参数设置 public class HeapOOM {static class OOMObject {}public static void main(String[] args) {List<OOMObject> list new ArrayList<OOMObject>();while (true) {list.add(new OO…

idea 配置checkstyle全过程

checkstyle是提高代码质量,检查代码规范的很好用的一款工具&#xff0c;本文简单介绍一下集成的步骤&#xff0c;并提供一份完整的checkstyle的代码规范格式文件&#xff0c;以及常见的格式问题的解决方法。 一&#xff0c;安装 打开idea的文件选项&#xff0c;选择设置&…

Unity3D实现页面的滑动切换功能

效果展示 Unity3D实现页面的滑动切换 效果 文章目录 前言一、先上代码二、创建UI1.创建Scroll View如下图&#xff0c;并挂载该脚本&#xff1a;2.Content下创建几个Itme 总结 前言 好记性不如烂笔头&#xff01; 一、先上代码 /*******************************************…

前端性能分析工具

前段时间在工作中,需要判断模块bundle size缩减对页面的哪些性能产生了影响, 因此需要了解前端的性能指标如何定义的,以及前端有哪些性能分析工具, 于是顺便整理了一篇笔记, 以供前端小白对性能这块知识点做一个入门级的了解. 页面渲染 在了解性能指标和分析工具之前,有必要先…

怎样去除视频中的杂音,保留人声部分?

怎样去除视频中的杂音&#xff0c;保留人声部分&#xff1f;这个简单嘛&#xff01;两种办法可以搞定&#xff1a;一是进行音频降噪&#xff0c;把无用的杂音消除掉&#xff1b;二是提取人声&#xff0c;将要保留的人声片段提取出来。 这就将两种实用的办公都分享出来&#xf…

datagrip出现 java.net.ConnectException: Connection refused: connect.

出现这样的情况要看一下hadoop有没有启动 start-all.sh nohup /export/server/apache-hive-3.1.2-bin/bin/hive --service hiveserver2 & scp -r /export/server/apache-hive-3.1.2-bin/ node3:/export/server/ /export/server/apache-hive-3.1.2-bin/bin/hive show databa…

Apache POI

文章目录 一、Apache POI介绍二、应用场景三、使用步骤1.导入maven坐标2.写入代码讲解3.读取代码讲解 一、Apache POI介绍 Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是&#xff0c;我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文…

pytorch 笔记:GRU

1 介绍 对于输入序列中的每个元素&#xff0c;每一层都计算以下函数&#xff1a; ht​ 是t时刻 的隐藏状态xt​ 是t时刻 的输入ht−1​ 是 t-1时刻 同层的隐藏状态或 0时刻 的初始隐藏状态rt​,zt​,nt​ 分别是重置门、更新门和新门。σ 是 sigmoid 函数∗ 是 Hadamard 乘积。…