脚本宝典收集整理的这篇文章主要介绍了教你如何从 html 实现一个 react,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
React是一个简单的javascript UI库,用于构建高效、快速的用户界面。它是一个轻量级库,因此很受欢迎。它遵循组件设计模式、声明式编程范式和函数式编程概念,以使前端应用程序更高效。它使用虚拟DOM来有效地操作DOM。它遵循从高阶组件到低阶组件的单向数据流。
👉 我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。官网地址
react 的理念是在于对大型项目的快速响应
,对于新版的 react 16.8 而言更是带来的全新的理念fiber
去解决网页快速响应时所伴随的问题,即 CPU 的瓶颈,传统网页浏览受制于浏览器刷新率、js 执行时间过长等因素会造成页面掉帧,甚至卡顿
react 由于自身的底层设计从而规避这一问题的发生,所以 react16.8 的面世对于前端领域只办三件事:快速响应、快速响应、还是 Tmd 快速响应 !,这篇文章将会从一个 html 出发,跟随 react 的 fiber 理念,仿一个非常基础的 react
html
我们需要一个 html 去撑起来整个页面,支撑 react 运行,页面中添加<div></div>
,之后添加一个 script 标签,因为需要使用import
进行模块化构建,所以需要为 script 添加 type 为module
的属性
<!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> <div id="root"></div> <script type="module" src="./index.js" ></script> </body> </html>
推荐安装一个 Live Server
插件,有助于我们对代码进行调试,接下来的操作也会用到
JavaScript
我们会仿写一个如下的 react,实现一个基础的操作,在 <input/>
绑定事件,将输入的值插入在 <h2/>
标签内:
... function App() { return ( <div> <input onInput={updateValue} value={value} /> <h2>Hello {value}</h2> <hr /> </div> ); } ...
在 react 进行 babel 编译的时候,会将 JSX
语法转化为 React.createElement()
的形式,如上被 retuen 的代码就会被转换成
... React.createElement( "div", null, React.createElement("input", { onInput: updateValue, value: value, }), React.createElement("h2", null, "Hello ", value), React.createElement("hr", null) ); ...
从转换后的代码我们可以看出 React.createElement 支持多个参数:
我们可以按照 React.createElement
的形式仿写一个可以实现同样功能的 createElement
将 jsx 通过一种简单的数据结构展示出来即 虚拟DOM
这样在更新时,新旧节点的对比也可以转化为虚拟 DOM 的对比
{ type:'节点标签', props:{ props:'节点上的属性,包括事件、类...', children:'节点的子节点' } }
这里我们可以写一个函数实现下列需求
TEXT_ELEMENT
的节点类型表示/** * 创建虚拟 DOM 结构 * @param {type} 标签名 * @param {props} 属性对象 * @param {children} 子节点 * @return {element} 虚拟 DOM */ const createElement = (type, props, ...children) => ({ type, props: { ...props, children: children.map(child => typeof child === "object" ? child : { type: "TEXT_ELEMENT", props: { nodeValue: child, children: [], }, } ), }, });
实现 createElement
之后我们可以拿到虚拟 DOM,但是还需要 render
将代码渲染到页面,此时我们需要对 index.js
进行处理,添加输入事件,将 createElement
和 render
通过 import 进行引入,render 时传入被编译后的虚拟 DOM 和页面的根元素 root
, 最后再进行executeRender
调用,页面被渲染,在页面更新的时候再次调用executeRender
进行更新渲染
import {createElement,render} from "./mini/index.js"; const updateValue = e => executeRender(e.target.value); const executeRender = (value = "World") => { const element = createElement( "div", null, createElement("input", { onInput: updateValue, value: value, }), createElement("h2", null, "Hello ", value), createElement("hr", null) ); render(element, document.getElementById("root")); }; executeRender();
before 版本
render
函数帮助我们将 element 添加至真实节点中,首先它接受两个参数:
根组件,其实是一个 JSX 组件,也就是一个 createElement 返回的虚拟 DOM
父节点,也就是我们要将这个虚拟 DOM 渲染的位置
在 react 16.8 之前,渲染的方法是通过一下几步进行的
拿到虚拟 dom 进行如上三步的递归调用,渲染出页面 类似于如下流程
/** * 将虚拟 DOM 添加至真实 DOM * @param {element} 虚拟 DOM * @param {container} 真实 DOM */ const render = (element, container) => { let dom; /* 处理节点(包括文本节点) */ if (typeof element !== "object") { dom = document.createTextNode(element); } else { dom = document.createElement(element.type); } /* 处理属性(包括事件属性) */ if (element.props) { Object.keys(element.props) .filter((key) => key != "children") .forEach((item) => { dom[item] = element.props[item]; }); Object.keys(element.props) .filter((key) => key.startsWith("on")) .forEach((name) => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); } if ( element.props && element.props.children && element.props.children.length ) { /* 循环添加到dom */ element.props.children.forEach((child) => render(child, dom)); } container.appendChild(dom); };
after 版本(fiber)
当我们写完如上的代码,会发现这个递归调用是有问题的
如上这部分工作被 React 官方称为 renderer,renderer 是第三方可以自己实现的一个模块,还有个核心模块叫做 reconsiler,reconsiler 的一大功能就是 diff 算法,他会计算出应该更新哪些页面节点,然后将需要更新的节点虚拟 DOM 传递给 renderer,renderer 负责将这些节点渲染到页面上,但是但是他却是同步的,一旦开始渲染,就会将所有节点及其子节点全部渲染完成这个进程才会结束。
React 的官方演讲中有个例子,可以很明显的看到这种同步计算造成的卡顿:
当 dom tree 很大的情况下,JS 线程的运行时间可能会比较长,在这段时间浏览器是不会响应其他事件的,因为 JS 线程和 GUI 线程是互斥的,JS 运行时页面就不会响应,这个时间太长了,用户就可能看到卡顿,
此时我们可以分为两步解决这个问题
solution I 引入一个新的 Api
requestIdleCallback 接收一个回调,这个回调会在浏览器空闲时调用,每次调用会传入一个 IdleDeadline,可以拿到当前还空余多久, options 可以传入参数最多等多久,等到了时间浏览器还不空就强制执行了。
window.requestIdleCallback 将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件
但是这个 API 还在实验中,兼容性不好,所以 React 官方自己实现了一套。本文会继续使用 requestIdleCallback 来进行任务调度
// 下一个工作单元 let nextUnitOfWork = null /** * workLoop 工作循环函数 * @param {deadline} 截止时间 */ function workLoop(deadline) { // 是否应该停止工作循环函数 let shouldYield = false // 如果存在下一个工作单元,且没有优先级更高的其他工作时,循环执行 while (nextUnitOfWork && !shouldYield) { nextUnitOfWork = performUnitOfWork( nextUnitOfWork ) // 如果截止时间快到了,停止工作循环函数 shouldYield = deadline.timeRemaining() < 1 } // 通知浏览器,空闲时间应该执行 workLoop requestIdleCallback(workLoop) } // 通知浏览器,空闲时间应该执行 workLoop requestIdleCallback(workLoop) // 执行单元事件,并返回下一个单元事件 function performUnitOfWork(nextUnitOfWork) { // TODO }
solution II 创建 fiber 的数据结构
Fiber 之前的数据结构是一棵树,父节点的 children 指向了子节点,但是只有这一个指针是不能实现中断继续的。比如我现在有一个父节点 A,A 有三个子节点 B,C,D,当我遍历到 C 的时候中断了,重新开始的时候,其实我是不知道 C 下面该执行哪个的,因为只知道 C,并没有指针指向他的父节点,也没有指针指向他的兄弟。
Fiber 就是改造了这样一个结构,加上了指向父节点和兄弟节点的指针:
每个 fiber 都有一个链接指向它的第一个子节点、下一个兄弟节点和它的父节点。这种数据结构可以让我们更方便的查找下一个工作单元,假定 A
是挂在 root 上的节点 fiber 的渲染顺序也如下步骤
我们通过这个数据结构实现一个 fiber
//创建最初的根fiber wipRoot = { dom: container, props: { children: [element] }, }; performUnitOfWork(wipRoot);
随后调用performUnitOfWork
自上而下构造整个 fiber 树
/** * performUnitOfWork用来执行任务 * @param {fiber} 我们的当前fiber任务 * @return {fiber} 下一个任务fiber任务 */ const performUnitOfWork = fiber => { if (!fiber.dom) fiber.dom = createDom(fiber); // 创建一个DOM挂载上去 const elements = fiber.props.children; //当前元素下的所有同级节点 // 如果有父节点,将当前节点挂载到父节点上 if (fiber.return) { fiber.return.dom.appendChild(fiber.dom); } let prevSibling = null; /* 之后代码中我们将把此处的逻辑进行抽离 */ if (elements && elements.length) { elements.forEach((element, index) => { const newFiber = { type: element.type, props: element.props, return: fiber, dom: null, }; // 父级的child指向第一个子元素 if (index === 0) { fiber.child = newFiber; } else { // 每个子元素拥有指向下一个子元素的指针 prevSibling.sibling = newFiber; } prevSibling = fiber; }); } // 先找子元素,没有子元素了就找兄弟元素 // 兄弟元素也没有了就返回父元素 // 最后到根节点结束 // 这个遍历的顺序是从上到下,从左到右 if (fiber.child) { return fiber.child; } else { let nextFiber = fiber; while (nextFiber) { if (nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } } }
currentRoot
reconcile 其实就是虚拟 DOM 树的 diff 操作,将更新前的 fiber tree 和更新后的 fiber tree 进行比较,得到比较结果后,仅对有变化的 fiber 对应的 dom 节点进行更新。
新增 currentRoot 变量,保存根节点更新前的 fiber tree,为 fiber 新增 alternate 属性,保存 fiber 更新前的 fiber tree
let currentRoot = null function render (element, container) { wipRoot = { // 省略 alternate: currentRoot } } function commitRoot () { commitWork(wipRoot.child) /* 更改fiber树的指向,将缓存中的fiber树替换到页面中的fiber tree */ currentRoot = wipRoot wipRoot = null }
reconcileChildren
在对比 fiber tree 时
仅更新 props,设置 effectTag 为 UPDATE
;创建一个新的 dom 节点,设置 effectTag 为 PLACEMENT
;删除旧 fiber,设置 effectTag 为 DELETION
/** * 协调子节点 * @param {fiber} fiber * @param {elements} fiber 的 子节点 */ function reconcileChildren(wipFiber, elements) { let index = 0;// 用于统计子节点的索引值 let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //更新时才会产生 let prevSibling;// 上一个兄弟节点 while (index < elements.length || oldFiber) { /** * 遍历子节点 * oldFiber判断是更新触发还是首次触发,更新触发时为元素下所有节点 */ let newFiber; const element = elements[index]; const sameType = oldFiber && element && element.type == oldFiber.type; // fiber 类型是否相同点 /** * 更新时 * 同标签不同属性,更新属性 */ if (sameType) { newFiber = { type: oldFiber.type, props: element.props, //只更新属性 dom: oldFiber.dom, parent: wipFiber, alternate: oldFiber, effectTag: "UPDATE", }; } /** * 不同标签,即替换了标签 or 创建新标签 */ 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; // 父级的child指向第一个子元素 if (index === 0) { // fiber的第一个子节点是它的子节点 wipFiber.child = newFiber; } else { // fiber 的其他子节点,是它第一个子节点的兄弟节点 prevSibling.sibling = newFiber; } // 把新建的 newFiber 赋值给 prevSibling,这样就方便为 newFiber 添加兄弟节点了 prevSibling = newFiber; // 索引值 + 1 index++; } }
在 commit 时,根据 fiber 节点上effectTag
的属性执行不同的渲染操作
after 版本(commit)
在 commitWork 中对 fiber 的 effectTag 进行判断,处理真正的 DOM 操作。
/** * @param {fiber} fiber 结构的虚拟dom */ function commitWork(fiber) { if (!fiber) return; const domParent = fiber.parent.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") { domParent.removeChild(fiber.dom); } // 递归操作子元素和兄弟元素 commitWork(fiber.child); commitWork(fiber.sibling); }
此时我们着重来看updateDom
发生了什么,我们拿到 dom 上被改变的新旧属性,进行操作
/* isEvent :拿到事件属性 isProperty :拿到非节点、非事件属性 isNew :拿到前后改变的属性 */ const isEvent = key => key.startsWith("on"); const isProperty = key => key !== "children" && !isEvent(key); const isNew = (prev, next) => key => prev[key] !== next[key]; /** * 更新dom属性 * @param {dom} fiber dom * @param {prevProps} fiber dom上旧的属性 * @param {nextProps} fiber dom上新的属性 */ function updateDom(dom, prevProps, nextProps) { /** * 便利旧属性 * 1、拿到on开头的事件属性 * 2、拿到被删除的事件 * 3、已删除的事件取消监听 */ Object.keys(prevProps) .filter(isEvent) .filter(key => !(key in nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.removeEventListener(eventType, prevProps[name]); }); /** * 便利旧属性 * 1、拿到非事件属性和非子节点的属性 * 2、拿到被删除的属性 * 3、删除属性 */ Object.keys(prevProps) .filter(isProperty) .filter(key => !(key in nextProps)) .forEach(key => delete dom[key]); /** * 便利新属性 * 1、拿到非事件属性和非子节点的属性 * 2、拿到前后改变的属性 * 3、添加属性 */ Object.keys(nextProps) .filter(isProperty) .filter(isNew(prevProps, nextProps)) .forEach(name => { dom[name] = nextProps[name]; }); /** * 便利新属性 * 1、拿到on开头的事件属性 * 2、拿到前后改变的事件属性 * 3、为新增的事件属性添加监听 */ Object.keys(nextProps) .filter(isEvent) .filter(isNew(prevProps, nextProps)) .forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, nextProps[name]); }); }
完成了一系列对 dom 的操作,我们将新改变的 dom 渲染到页面,当 input 事件执行时,页面又会进行渲染,但此时会进入更新 fiber 树的逻辑,
alternate 指向之前的 fiber 节点进行复用,更快的执行 Update 操作,如图:
大功告成!
完整代码可以看我github。
结论与总结 💢
结论
至此,谢谢各位在百忙之中点开这篇文章,希望对你们能有所帮助,如有问题欢迎各位大佬指正。工作原因这篇文章大概断断续续写了有一个月,工作上在忙一个基于 腾讯云TRTC
+websocket
的小程序电话功能,有时间也会写成文章分享一下,当然 react 的实现文章也会继续
👋:跳转 github 欢迎给个 star,谢谢大家了
参考文献
🍑:妙味课堂大圣老师 手写 react 的 fiber 和 hooks 架构
到此这篇关于教你如何从 html 实现一个 react的文章就介绍到这了,更多相关 html 实现react内容请搜索脚本宝典以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本宝典!
以上是脚本宝典为你收集整理的教你如何从 html 实现一个 react全部内容,希望文章能够帮你解决教你如何从 html 实现一个 react所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。