熟悉React的朋友都知道,React支持jsx语法,咱们能够直接将HTML代码写到JS中间,而后渲染到页面上,咱们写的HTML若是有更新的话,React还有虚拟DOM的对比,只更新变化的部分,而不从新渲染整个页面,大大提升渲染效率。到了16.x,React更是使用了一个被称为Fiber
的架构,提高了用户体验,同时还引入了hooks
等特性。那隐藏在React背后的原理是怎样的呢,Fiber
和hooks
又是怎么实现的呢?本文会从jsx
入手,手写一个简易版的React,从而深刻理解React的原理。javascript
本文主要实现了这些功能:java
简易版Fiber架构简易版DIFF算法react
简易版函数组件git
简易版Hook:
useState
github娱乐版
Class
组件算法
本文代码地址:https://github.com/dennis-jiang/Front-End-Knowledges/tree/master/Examples/React/fiber-and-hooks数组
本文程序跑起来效果以下:浏览器
之前咱们写React要支持JSX还须要一个库叫JSXTransformer.js
,后来JSX的转换工做都集成到了babel里面了,babel还提供了在线预览的功能,能够看到转换后的效果,好比下面这段简单的代码:babel
const App = ( <div> <h1 id="title">Title</h1> <a href="xxx">Jump</a> <section> <p> Article </p> </section> </div> );
通过babel转换后就变成了这样:数据结构
上面的截图能够看出咱们写的HTML被转换成了React.createElement
,咱们将上面代码稍微格式化来看下:
var App = React.createElement( 'div', null, React.createElement( 'h1', { id: 'title', }, 'Title', ), React.createElement( 'a', { href: 'xxx', }, 'Jump', ), React.createElement( 'section', null, React.createElement('p', null, 'Article'), ), );
从转换后的代码咱们能够看出React.createElement
支持多个参数:
- type,也就是节点类型
- config, 这是节点上的属性,好比
id
和href
- children, 从第三个参数开始就所有是children也就是子元素了,子元素能够有多个,类型能够是简单的文本,也能够仍是
React.createElement
,若是是React.createElement
,其实就是子节点了,子节点下面还能够有子节点。这样就用React.createElement
的嵌套关系实现了HTML节点的树形结构。
让咱们来完整看下这个简单的React页面代码:
渲染在页面上是这样:
这里面用到了React的地方其实就两个,一个是JSX,也就是React.createElement
,另外一个就是ReactDOM.render
,因此咱们手写的第一个目标就有了,就是createElement
和render
这两个方法。
对于<h1 id="title">Title</h1>
这样一个简单的节点,原生DOM也会附加一大堆属性和方法在上面,因此咱们在createElement
的时候最好能将它转换为一种比较简单的数据结构,只包含咱们须要的元素,好比这样:
{ type: 'h1', props: { id: 'title', children: 'Title' } }
有了这个数据结构后,咱们对于DOM的操做其实能够转化为对这个数据结构的操做,新老DOM的对比其实也能够转化为这个数据结构的对比,这样咱们就不须要每次操做都去渲染页面,而是等到须要渲染的时候才将这个数据结构渲染到页面上。这其实就是虚拟DOM!而咱们createElement
就是负责来构建这个虚拟DOM的方法,下面咱们来实现下:
function createElement(type, props, ...children) { // 核心逻辑不复杂,将参数都塞到一个对象上返回就行 // children也要放到props里面去,这样咱们在组件里面就能经过this.props.children拿到子元素 return { type, props: { ...props, children } } }
上述代码是React的createElement
简化版,对源码感兴趣的朋友能够看这里:https://github.com/facebook/react/blob/60016c448bb7d19fc989acd05dda5aca2e124381/packages/react/src/ReactElement.js#L348
上述代码咱们用createElement
将JSX代码转换成了虚拟DOM,那真正将它渲染到页面的函数是render
,因此咱们还须要实现下这个方法,经过咱们通常的用法ReactDOM.render( <App />,document.getElementById('root'));
能够知道他接收两个参数:
- 根组件,实际上是一个JSX组件,也就是一个
createElement
返回的虚拟DOM- 父节点,也就是咱们要将这个虚拟DOM渲染的位置
有了这两个参数,咱们来实现下render
方法:
function render(vDom, container) { let dom; // 检查当前节点是文本仍是对象 if(typeof vDom !== 'object') { dom = document.createTextNode(vDom) } else { dom = document.createElement(vDom.type); } // 将vDom上除了children外的属性都挂载到真正的DOM上去 if(vDom.props) { Object.keys(vDom.props) .filter(key => key != 'children') .forEach(item => { dom[item] = vDom.props[item]; }) } // 若是还有子元素,递归调用 if(vDom.props && vDom.props.children && vDom.props.children.length) { vDom.props.children.forEach(child => render(child, dom)); } container.appendChild(dom); }
上述代码是简化版的render
方法,对源码感兴趣的朋友能够看这里:https://github.com/facebook/react/blob/3e94bce765d355d74f6a60feb4addb6d196e3482/packages/react-dom/src/client/ReactDOMLegacy.js#L287
如今咱们能够用本身写的createElement
和render
来替换原生的方法了:
能够获得同样的渲染结果:
上面咱们简单的实现了虚拟DOM渲染到页面上的代码,这部分工做被React官方称为renderer,renderer是第三方能够本身实现的一个模块,还有个核心模块叫作reconsiler,reconsiler的一大功能就是你们熟知的diff,他会计算出应该更新哪些页面节点,而后将须要更新的节点虚拟DOM传递给renderer,renderer负责将这些节点渲染到页面上。可是这个流程有个问题,虽然React的diff算法是通过优化的,可是他倒是同步的,renderer负责操做DOM的appendChild
等API也是同步的,也就是说若是有大量节点须要更新,JS线程的运行时间可能会比较长,在这段时间浏览器是不会响应其余事件的,由于JS线程和GUI线程是互斥的,JS运行时页面就不会响应,这个时间太长了,用户就可能看到卡顿,特别是动画的卡顿会很明显。在React的官方演讲中有个例子,能够很明显的看到这种同步计算形成的卡顿:
而Fiber就是用来解决这个问题的,Fiber能够将长时间的同步任务拆分红多个小任务,从而让浏览器可以抽身去响应其余事件,等他空了再回来继续计算,这样整个计算流程就显得平滑不少。下面是使用Fiber后的效果:
上面咱们本身实现的render
方法直接递归遍历了整个vDom树,若是咱们在中途某一步停下来,下次再调用时其实并不知道上次在哪里停下来的,不知道从哪里开始,因此vDom的树形结构并不知足中途暂停,下次继续的需求,须要改造数据结构。另外一个须要解决的问题是,拆分下来的小任务何时执行?咱们的目的是让用户有更流畅的体验,因此咱们最好不要阻塞高优先级的任务,好比用户输入,动画之类,等他们执行完了咱们再计算。那我怎么知道如今有没有高优先级任务,浏览器是否是空闲呢?总结下来,Fiber要想达到目的,须要解决两个问题:
- 新的任务调度,有高优先级任务的时候将浏览器让出来,等浏览器空了再继续执行
- 新的数据结构,能够随时中断,下次进来能够接着执行
requestIdleCallback
是一个实验中的新API,这个API调用方式以下:
// 开启调用 var handle = window.requestIdleCallback(callback[, options]) // 结束调用 Window.cancelIdleCallback(handle)
requestIdleCallback
接收一个回调,这个回调会在浏览器空闲时调用,每次调用会传入一个IdleDeadline
,能够拿到当前还空余多久,options
能够传入参数最多等多久,等到了时间浏览器还不空就强制执行了。使用这个API能够解决任务调度的问题,让浏览器在空闲时才计算diff并渲染。更多关于requestIdleCallback的使用能够查看MDN的文档。可是这个API还在实验中,兼容性很差,因此React官方本身实现了一套。本文会继续使用requestIdleCallback
来进行任务调度,咱们进行任务调度的思想是将任务拆分红多个小任务,requestIdleCallback
里面不断的把小任务拿出来执行,当全部任务都执行完或者超时了就结束本次执行,同时要注册下次执行,代码架子就是这样:
function workLoop(deadline) { while(nextUnitOfWork && deadline.timeRemaining() > 1) { // 这个while循环会在任务执行完或者时间到了的时候结束 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 若是任务还没完,可是时间到了,咱们须要继续注册requestIdleCallback requestIdleCallback(workLoop); } // performUnitOfWork用来执行任务,参数是咱们的当前fiber任务,返回值是下一个任务 function performUnitOfWork(fiber) { } requestIdleCallback(workLoop);
上面咱们的performUnitOfWork
并无实现,可是从上面的结构能够看出来,他接收的参数是一个小任务,同时经过这个小任务还能够找到他的下一个小任务,Fiber构建的就是这样一个数据结构。Fiber以前的数据结构是一棵树,父节点的children
指向了子节点,可是只有这一个指针是不能实现中断继续的。好比我如今有一个父节点A,A有三个子节点B,C,D,当我遍历到C的时候中断了,从新开始的时候,其实我是不知道C下面该执行哪一个的,由于只知道C,并无指针指向他的父节点,也没有指针指向他的兄弟。Fiber就是改造了这样一个结构,加上了指向父节点和兄弟节点的指针:
上面的图片仍是来自于官方的演讲,能够看到和以前父节点指向全部子节点不一样,这里有三个指针:
- child: 父节点指向第一个子元素的指针。
- sibling:从第一个子元素日后,指向下一个兄弟元素。
- return:全部子元素都有的指向父元素的指针。
有了这几个指针后,咱们能够在任意一个元素中断遍历并恢复,好比在上图List
处中断了,恢复的时候能够经过child
找到他的子元素,也能够经过return
找到他的父元素,若是他还有兄弟节点也能够用sibling
找到。Fiber这个结构外形看着仍是棵树,可是没有了指向全部子元素的指针,父节点只指向第一个子节点,而后子节点有指向其余子节点的指针,这实际上是个链表。
如今咱们能够本身来实现一下Fiber了,咱们须要将以前的vDom结构转换为Fiber的数据结构,同时须要可以经过其中任意一个节点返回下一个节点,其实就是遍历这个链表。遍历的时候从根节点出发,先找子元素,若是子元素存在,直接返回,若是没有子元素了就找兄弟元素,找完全部的兄弟元素后再返回父元素,而后再找这个父元素的兄弟元素。整个遍历过程实际上是个深度优先遍历,从上到下,而后最后一行开始从左到右遍历。好比下图从div1
开始遍历的话,遍历的顺序就应该是div1 -> div2 -> h1 -> a -> div2 -> p -> div1
。能够看到这个序列中,当咱们return
父节点时,这些父节点会被第二次遍历,因此咱们写代码时,return
的父节点不会做为下一个任务返回,只有sibling
和child
才会做为下一个任务返回。
// performUnitOfWork用来执行任务,参数是咱们的当前fiber任务,返回值是下一个任务 function performUnitOfWork(fiber) { // 根节点的dom就是container,若是没有这个属性,说明当前fiber不是根节点 if(!fiber.dom) { fiber.dom = createDom(fiber); // 建立一个DOM挂载上去 } // 若是有父节点,将当前节点挂载到父节点上 if(fiber.return) { fiber.return.dom.appendChild(fiber.dom); } // 将咱们前面的vDom结构转换为fiber结构 const elements = fiber.children; let prevSibling = null; if(elements && elements.length) { for(let i = 0; i < elements.length; i++) { const element = elements[i]; const newFiber = { type: element.type, props: element.props, return: fiber, dom: null } // 父级的child指向第一个子元素 if(i === 0) { fiber.child = newFiber; } else { // 每一个子元素拥有指向下一个子元素的指针 prevSibling.sibling = newFiber; } prevSibling = newFiber; } } // 这个函数的返回值是下一个任务,这实际上是一个深度优先遍历 // 先找子元素,没有子元素了就找兄弟元素 // 兄弟元素也没有了就返回父元素 // 而后再找这个父元素的兄弟元素 // 最后到根节点结束 // 这个遍历的顺序其实就是从上到下,从左到右 if(fiber.child) { return fiber.child; } let nextFiber = fiber; while(nextFiber) { if(nextFiber.sibling) { return nextFiber.sibling; } nextFiber = nextFiber.return; } }
React源码中的performUnitOfWork
看这里,固然比咱们这个复杂不少。
上面咱们的performUnitOfWork
一边构建Fiber结构一边操做DOMappendChild
,这样若是某次更新好几个节点,操做了第一个节点以后就中断了,那咱们可能只看到第一个节点渲染到了页面,后续几个节点等浏览器空了才陆续渲染。为了不这种状况,咱们应该将DOM操做都搜集起来,最后统一执行,这就是commit
。为了可以记录位置,咱们还须要一个全局变量workInProgressRoot
来记录根节点,而后在workLoop
检测若是任务执行完了,就commit
:
function workLoop(deadline) { while(nextUnitOfWork && deadline.timeRemaining() > 1) { // 这个while循环会在任务执行完或者时间到了的时候结束 nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // 任务作完后统一渲染 if(!nextUnitOfWork && workInProgressRoot) { commitRoot(); } // 若是任务还没完,可是时间到了,咱们须要继续注册requestIdleCallback requestIdleCallback(workLoop); }
由于咱们是在Fiber树彻底构建后再执行的commit
,并且有一个变量workInProgressRoot
指向了Fiber的根节点,因此咱们能够直接把workInProgressRoot
拿过来递归渲染就好了:
// 统一操做DOM function commitRoot() { commitRootImpl(workInProgressRoot.child); // 开启递归 workInProgressRoot = null; // 操做完后将workInProgressRoot重置 } function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; parentDom.appendChild(fiber.dom); // 递归操做子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); }
reconcile其实就是虚拟DOM树的diff操做,须要删除不须要的节点,更新修改过的节点,添加新的节点。为了在中断后能回到工做位置,咱们还须要一个变量currentRoot
,而后在fiber
节点里面添加一个属性alternate
,这个属性指向上一次运行的根节点,也就是currentRoot
。currentRoot
会在第一次render
后的commit
阶段赋值,也就是每次计算完后都会把当次状态记录在alternate
上,后面更新了就能够把alternate
拿出来跟新的状态作diff。而后performUnitOfWork
里面须要添加调和子元素的代码,能够新增一个函数reconcileChildren
。这个函数里面不能简单的建立新节点了,而是要将老节点跟新节点拿来对比,对比逻辑以下:
注意删除老节点的操做是直接将oldFiber
加上一个删除标记就行,同时用一个全局变量deletions
记录全部须要删除的节点:
// 对比oldFiber和当前element const sameType = oldFiber && element && oldFiber.type === element.type; //检测类型是否是同样 // 先比较元素类型 if(sameType) { // 若是类型同样,复用节点,更新props newFiber = { type: oldFiber.type, props: element.props, dom: oldFiber.dom, return: workInProgressFiber, alternate: oldFiber, // 记录下上次状态 effectTag: 'UPDATE' // 添加一个操做标记 } } else if(!sameType && element) { // 若是类型不同,有新的节点,建立新节点替换老节点 newFiber = { type: element.type, props: element.props, dom: null, // 构建fiber时没有dom,下次perform这个节点是才建立dom return: workInProgressFiber, alternate: null, // 新增的没有老状态 effectTag: 'REPLACEMENT' // 添加一个操做标记 } } else if(!sameType && oldFiber) { // 若是类型不同,没有新节点,有老节点,删除老节点 oldFiber.effectTag = 'DELETION'; // 添加删除标记 deletions.push(oldFiber); // 一个数组收集全部须要删除的节点 }
而后就是在commit
阶段处理真正的DOM操做,具体的操做是根据咱们的effectTag
来判断的:
function commitRootImpl(fiber) { if(!fiber) { return; } const parentDom = fiber.return.dom; if(fiber.effectTag === 'REPLACEMENT' && fiber.dom) { parentDom.appendChild(fiber.dom); } else if(fiber.effectTag === 'DELETION') { parentDom.removeChild(fiber.dom); } else if(fiber.effectTag === 'UPDATE' && fiber.dom) { // 更新DOM属性 updateDom(fiber.dom, fiber.alternate.props, fiber.props); } // 递归操做子元素和兄弟元素 commitRootImpl(fiber.child); commitRootImpl(fiber.sibling); }
替换和删除的DOM操做都比较简单,更新属性的会稍微麻烦点,须要再写一个辅助函数updateDom
来实现:
// 更新DOM的操做 function updateDom(dom, prevProps, nextProps) { // 1. 过滤children属性 // 2. 老的存在,新的没了,取消 // 3. 新的存在,老的没有,新增 Object.keys(prevProps) .filter(name => name !== 'children') .filter(name => !(name in nextProps)) .forEach(name => { if(name.indexOf('on') === 0) { dom.removeEventListener(name.substr(2).toLowerCase(), prevProps[name], false); } else { dom[name] = ''; } }); Object.keys(nextProps) .filter(name => name !== 'children') .forEach(name => { if(name.indexOf('on') === 0) { dom.addEventListener(name.substr(2).toLowerCase(), nextProps[name], false); } else { dom[name] = nextProps[name]; } }); }
updateDom
的代码写的比较简单,事件只处理了简单的on
开头的,兼容性也有问题,prevProps
和nextProps
可能会遍历到相同的属性,有重复赋值,可是整体原理仍是没错的。要想把这个处理写全,代码量仍是很多的。
函数组件是React里面很常见的一种组件,咱们前面的React架构其实已经写好了,咱们这里来支持下函数组件。咱们以前的fiber
节点上的type
都是DOM节点的类型,好比h1
什么的,可是函数组件的节点type
其实就是一个函数了,咱们须要对这种节点进行单独处理。
首先须要在更新的时候检测当前节点是否是函数组件,若是是,children
的处理逻辑会稍微不同:
// performUnitOfWork里面 // 检测函数组件 function performUnitOfWork(fiber) { const isFunctionComponent = fiber.type instanceof Function; if(isFunctionComponent) { updateFunctionComponent(fiber); } else { updateHostComponent(fiber); } // ...下面省略n行代码... } function updateFunctionComponent(fiber) { // 函数组件的type就是个函数,直接拿来执行能够得到DOM元素 const children = [fiber.type(fiber.props)]; reconcileChildren(fiber, children); } // updateHostComponent就是以前的操做,只是单独抽取了一个方法 function updateHostComponent(fiber) { if(!fiber.dom) { fiber.dom = createDom(fiber); // 建立一个DOM挂载上去 } // 将咱们前面的vDom结构转换为fiber结构 const elements = fiber.props.children; // 调和子元素 reconcileChildren(fiber, elements); }
而后在咱们提交DOM操做的时候由于函数组件没有DOM元素,因此须要注意两点:
咱们来修改下commitRootImpl
:
function commitRootImpl() { // const parentDom = fiber.return.dom; // 向上查找真正的DOM let parentFiber = fiber.return; while(!parentFiber.dom) { parentFiber = parentFiber.return; } const parentDom = parentFiber.dom; // ...这里省略n行代码... if{fiber.effectTag === 'DELETION'} { commitDeletion(fiber, parentDom); } } function commitDeletion(fiber, domParent) { if(fiber.dom) { // dom存在,是普通节点 domParent.removeChild(fiber.dom); } else { // dom不存在,是函数组件,向下递归查找真实DOM commitDeletion(fiber.child, domParent); } }
如今咱们能够传入函数组件了:
import React from './myReact'; const ReactDOM = React; function App(props) { return ( <div> <h1 id="title">{props.title}</h1> <a href="xxx">Jump</a> <section> <p> Article </p> </section> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') );
useState
是React Hooks里面的一个API,至关于以前Class Component
里面的state
,用来管理组件内部状态,如今咱们已经有一个简化版的React
了,咱们也能够尝试下来实现这个API。
咱们仍是从用法入手来实现最简单的功能,咱们通常使用useState
是这样的:
function App(props) { const [count, setCount] = React.useState(1); const onClickHandler = () => { setCount(count + 1); } return ( <div> <h1>Count: {count}</h1> <button onClick={onClickHandler}>Count+1</button> </div> ); } ReactDOM.render( <App title="Fiber Demo"/>, document.getElementById('root') );
上述代码能够看出,咱们的useState
接收一个初始值,返回一个数组,里面有这个state
的当前值和改变state
的方法,须要注意的是App
做为一个函数组件,每次render
的时候都会运行,也就是说里面的局部变量每次render
的时候都会重置,那咱们的state
就不能做为一个局部变量,而是应该做为一个所有变量存储:
let state = null; function useState(init) { state = state === null ? init : state; // 修改state的方法 const setState = value => { state = value; // 只要修改了state,咱们就须要从新处理节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [state, setState] }
这样其实咱们就可使用了:
上面的代码只有一个state
变量,若是咱们有多个useState
怎么办呢?为了能支持多个useState
,咱们的state
就不能是一个简单的值了,咱们能够考虑把他改为一个数组,多个useState
按照调用顺序放进这个数组里面,访问的时候经过下标来访问:
let state = []; let hookIndex = 0; function useState(init) { const currentIndex = hookIndex; state[currentIndex] = state[currentIndex] === undefined ? init : state[currentIndex]; // 修改state的方法 const setState = value => { state[currentIndex] = value; // 只要修改了state,咱们就须要从新处理这个节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } hookIndex++; return [state[currentIndex], setState] }
来看看多个useState
的效果:
上面的代码虽然咱们支持了多个useState
,可是仍然只有一套全局变量,若是有多个函数组件,每一个组件都来操做这个全局变量,那相互之间不就是污染了数据了吗?因此咱们数据还不能都存在全局变量上面,而是应该存在每一个fiber
节点上,处理这个节点的时候再将状态放到全局变量用来通信:
// 申明两个全局变量,用来处理useState // wipFiber是当前的函数组件fiber节点 // hookIndex是当前函数组件内部useState状态计数 let wipFiber = null; let hookIndex = null;
由于useState
只在函数组件里面能够用,因此咱们以前的updateFunctionComponent
里面须要初始化处理useState
变量:
function updateFunctionComponent(fiber) { // 支持useState,初始化变量 wipFiber = fiber; hookIndex = 0; wipFiber.hooks = []; // hooks用来存储具体的state序列 // ......下面代码省略...... }
由于hooks
队列放到fiber
节点上去了,因此咱们在useState
取以前的值时须要从fiber.alternate
上取,完整代码以下:
function useState(init) { // 取出上次的Hook const oldHook = wipFiber.alternate && wipFiber.alternate.hooks && wipFiber.alternate.hooks[hookIndex]; // hook数据结构 const hook = { state: oldHook ? oldHook.state : init // state是每一个具体的值 } // 将全部useState调用按照顺序存到fiber节点上 wipFiber.hooks.push(hook); hookIndex++; // 修改state的方法 const setState = value => { hook.state = value; // 只要修改了state,咱们就须要从新处理这个节点 workInProgressRoot = { dom: currentRoot.dom, props: currentRoot.props, alternate: currentRoot } // 修改nextUnitOfWork指向workInProgressRoot,这样下次requestIdleCallback就会处理这个节点了 nextUnitOfWork = workInProgressRoot; deletions = []; } return [hook.state, setState] }
上面代码能够看出咱们在将useState
和存储的state
进行匹配的时候是用的useState
的调用顺序匹配state
的下标,若是这个下标匹配不上了,state
就错了,因此React
里面不能出现这样的代码:
if (something) { const [state, setState] = useState(1); }
上述代码不能保证每次something
都知足,可能致使useState
此次render
执行了,下次又没执行,这样新老节点的下标就匹配不上了,对于这种代码,React
会直接报错:
这个功能纯粹是娱乐性功能,经过前面实现的Hooks来模拟实现Class组件,这个并非React
官方的实现方式哈~咱们能够写一个方法将Class组件转化为前面的函数组件:
function transfer(Component) { return function(props) { const component = new Component(props); let [state, setState] = useState(component.state); component.props = props; component.state = state; component.setState = setState; return component.render(); } }
而后就能够写Class了,这个Class长得很像咱们在React里面写的Class,有state
,setState
和render
:
import React from './myReact'; class Count4 { constructor(props) { this.props = props; this.state = { count: 1 } } onClickHandler = () => { this.setState({ count: this.state.count + 1 }) } render() { return ( <div> <h3>Class component Count: {this.state.count}</h3> <button onClick={this.onClickHandler}>Count+1</button> </div> ); } } // export的时候用transfer包装下 export default React.transfer(Count4);
而后使用的时候直接:
<div> <Count4></Count4> </div>
固然你也能够在React
里面建一个空的class Component
,让Count4
继承他,这样就更像了。
好了,到这里咱们代码就写完了,完整代码能够看我GitHub。
React.createElement
。React.createElement
返回的其实就是虚拟DOM结构。ReactDOM.render
方法是将虚拟DOM渲染到页面的。父 -> 第一个子
,子 -> 兄
,子 -> 父
这几个指针,有了这几个指针,能够从任意一个Fiber节点找到其余节点。父 -> 子 -> 兄 -> 父
,也就是从上往下,从左往右。commit
)必须是同步的。由于异步的commit
可能让用户看到节点一个一个接连出现,体验很差。type
是个函数,直接将type
拿来运行就能够获得虚拟DOM。useState
是在Fiber节点上添加了一个数组,数组里面的每一个值对应了一个useState
,useState
调用顺序必须和这个数组下标匹配,否则会报错。妙味课堂大圣老师:手写react的fiber和hooks架构
这多是最通俗的 React Fiber(时间分片) 打开方式
文章的最后,感谢你花费宝贵的时间阅读本文,若是本文给了你一点点帮助或者启发,请不要吝啬你的赞和GitHub小星星,你的支持是做者持续创做的动力。
做者博文GitHub项目地址: https://github.com/dennis-jiang/Front-End-Knowledges