当我们由浅入深地认知一样新事物的时候,往往需要遵循 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 的实践。 |
|