分享

React 整体感知

 盐焗太阳饼qiqi 2021-01-31

当我们由浅入深地认知一样新事物的时候,往往需要遵循 Why > What > How 这样一个认知过程。它们是相辅相成、缺一不可的。而了解了具体的 What 和 How 之后,往往能够更加具象地回答理论层面的 Why,因此,在进入 Why 的探索之前,我们先整体感知一下 What 和 How 两个过程。

What

打开一眼便能看到官方给出的回答。

React 是用于构建用户界面的 JavaScript 库。

不知道你有没有想过,构建用户界面的方式有千百种,为什么 React 会突出?站长交易同样,我们可以从 里得到回应。

我们认为, React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。

可见,关键是实现了 快速响应 ,那么制约 快速响应 的因素有哪些呢?React 是如何解决的呢?

How

让我们带着上面的两个问题,在遵循真实的React代码架构的前提下,并舍弃部分优化代码和非必要的功能,将其命名为 HuaMu。

注意:为了和源码有点区分,函数名首字母大写,源码是小写。

CreateElement 函数

在开始之前,我们先简单的了解一下JSX,如果你感兴趣,可以关注下一篇《JSX背后的故事》。

JSX会被工具链Babel编译为React.createElement(),接着React.createElement()返回一个叫作React.Element的JS对象。

这么说有些抽象,通过下面demo看下转换前后的代码:

// JSX 转换前const el = <h1 title="el_title">HuaMu<h1>;

// 转换后的 JS 对象const el = {

  type:"h1",

  props:{

    title:"el_title",

    children:"HuaMu",

  }

}

可见,元素是具有 type 和 props 属性的对象,而 CreateElement 函数的主要任务就是创建该对象。

/**

 * @param {string} type    HTML标签类型

 * @param {object} props   具有JSX属性中的所有键和值

 * @param {string | array} children 元素树

 */function CreateElement(type, props, ...children) {

  return {

    type,

    props:{

      ...props,

      children,

    }

  }

}

说明:我们将剩余参数赋予children,扩展运算符用于构造字面量对象props,对象表达式将按照 key-value 的方式展开,从而保证 props.children 始终是一个数组。接下来,我们一起看下 demo:

CreateElement("h1", {title:"el_title"}, 'hello', 'HuaMu')

// 返回的 JS 对象

{

  "type": "h1",

  "props": {

    "title": "el_title"            // key-value

    "children": ["hello", "HuaMu"] // 数组类型

  }

}

注意:当 ...children 为空或为原始值时,React 不会创建 props.children,但为了简化代码,暂不考虑性能,我们为原始值创建特殊的类型TEXT_EL。

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_EL",

    props: {

      nodeValue: text,

      children: []

    }

  }

}

Render 函数

CreateElement 函数将标签转化为对象输出,接着 React 进行一系列处理,Render 函数将处理好的节点根据标记进行添加、更新或删除内容,最后附加到容器中。下面简单的实现 Render 函数是如何实现添加内容的:

首先创建对应的DOM节点,然后将新节点附加到容器中,并递归每个孩子节点做同样的操作。

将元素的 props 属性分配给节点。

function Render(el,container) {

// 创建节点

const dom = el.type === 'TEXT_EL'? document.createTextNode("") : document.createElement(el.type);

el.props.children.forEach(child => Render(child, dom))

// 为节点分配 props 属性

const isProperty = key => key !== 'children';

const setProperty = name => dom[name] = el.props[name];

Object.keys(el.props).filter(isProperty).forEach(setProperty)

container.appendChild(dom);

}

注意:文本节点使用textNode而不是innerText,是为了保证以相同的方式对待所有的元素 。

到目前为止,我们已经实现了一个简易的用于构建用户界面的 JavaScript 库。现在,让 Babel 使用自定义的 HuaMu 代替 React,将 /** @jsx HuaMu.CreateElement */ 添加到代码中

并发模式

在继续向下探索之前,我们先思考一下上面的代码中,有哪些代码制约 快速响应 了呢?

是的,在Render函数中递归每个孩子节点,即这句代码el.props.children.forEach(child => Render(child, dom))存在问题。一旦开始渲染,便不会停止,直到渲染了整棵元素树,我们知道,GUI渲染线程与JS线程是互斥的,JS脚本执行和浏览器布局、绘制不能同时执行。如果元素树很大,JS脚本执行时间过长,可能会阻塞主线程,导致页面掉帧,造成卡顿,且妨碍浏览器执行高优作业。

那如何解决呢?

通过时间切片的方式,即将任务分解为多个工作单元,每完成一个工作单元,判断是否有高优作业,若有,则让浏览器中断渲染。下面通过requestIdleCallback模拟实现:

简单说明一下:

window.requestIdleCallback(cb[, options]) :浏览器将在主线程空闲时运行回调。函数会接收到一个IdleDeadline的参数,这个参数可以获取当前空闲时间(timeRemaining)以及回调是否在超时前已经执行的状态(didTimeout)。

React 已不再使用requestIdleCallback,目前使用 但在概念上是相同的。

依据上面的分析,代码结构如下:

// 当浏览器准备就绪时,它将调用 WorkLoop

requestIdleCallback(WorkLoop)

let nextUnitOfWork = null;

function PerformUnitOfWork(nextUnitOfWork) {

  // TODO

}

function WorkLoop(deadline) {

  // 当前线程的闲置时间是否可以在结束前执行更多的任务

  let shouldYield = false;

  while(nextUnitOfWork && !shouldYield) {

    nextUnitOfWork = PerformUnitOfWork(nextUnitOfWork) // 赋值下一个工作单元

    shouldYield = deadline.timeRemaining() < 1;  // 如果 idle period 已经结束,则它的值是 0

  }

  requestIdleCallback(WorkLoop)

}

我们在 PerformUnitOfWork 函数里实现当前工作的执行并返回下一个执行的工作单元,可下一个工作单元如何快速查找呢?让我们初步了解 Fibers 吧。

Fibers

为了组织工作单元,即方便查找下一个工作单元,需引入fiber tree的数据结构。即每个元素都有一个fiber,链接到其第一个子节点,下一个兄弟姐妹节点和父节点,且每个fiber都将成为一个工作单元。

// 假设我们要渲染的元素树如下const el = (

  <div>

    <h1>

      <p />

      <a />

    </h1>

    <h2 />

  </div>

)

function UseState(initial) {

const oldHook =

wipFiber.alternate &&

wipFiber.alternate.hooks &&

wipFiber.alternate.hooks[hookIndex]

const hook = {

state: oldHook ? oldHook.state : initial,

}

wipFiber.hooks.push(hook)

hookIndex++

return [hook.state]

}

UseState还需返回一个可更新状态的函数,因此,需要定义一个接收action的setState函数。

action添加到队列中,再将队列添加到fiber。

在下一次渲染时,获取old hook的action队列,并代入new state逐一执行,以保证返回的状态是已更新的。

setState函数中,执行跟Render函数类似的操作,将currentRoot设置为下一个工作单元,以便开始新的渲染。

function UseState(initial) {

...

const hook = {

state: oldHook ? oldHook.state : initial,

queue: [],

}

const actions = oldHook ? oldHook.queue : []

actions.forEach(action => {

hook.state = action(hook.state)

})

const setState = action => {

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]

}

现在,我们已经实现一个包含时间切片、fiber、Hooks 的简易 React。

结语

到目前为止,我们从 What > How 梳理了大概的 React 知识链路,后面的章节我们对文中所提及的知识点进行 Why 的探索,相信会反哺到 What 的理解和 How 的实践。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多