一、前言
本文基于开源项目:
进行简单的梳理,希望能够让读者一文搞懂React 机制。
1.React中文文档
本文需要对React有所了解,如果暂时没看过React,可以点击下文,学习React基础再来阅读。
传送门: https://react.docschina.org/
二、简单的demo
我们先来看个简单的demo
上面是一个简单的例子,很容易猜到页面会删除“前端早茶”文字,以及一个按钮“点我关注”。
接下来,我们删除 import React from 'react' 删除这行代码,然后仍要得到同样的效果,怎么实现呢?
毫无疑问,我们需要实现自己的React,项目目录结构如下:
+-- src // 开发目录 | +-- gdlz // 存放自己实现react(文件名:广东靓仔) | | +--index.js // 自己react的逻辑 | +--index.js // 主页面
主页面index.js代码如下:
/* * @Author: 前端早茶 * @Date: 2021-03-05 21:59:06 * @LastEditTime: 2021-03-06 07:28:44 * @Description: my react * @FilePath: /reactDemo/src/index.js */ import React from './gdlz' const ReactDOM = React class Child extends React.Component{ constructor (props) { super(props) this.state = { count: 1 } } handleClick = () => { this.setState({ count: this.state.count + 1 }) } render () { return <div> <h2 onClick={this.handleClick}>{this.state.count}</h2> </div> } } ChildComponent = React.useComponent(Child) function App (props){ const [count, setCount] = React.useState(1) return <div id="container" className="front-end"> <h1>{props.title}, {count}</h1> <button onClick={() => setCount(count+1)}>关注</button> <hr/> <ChildComponent></ChildComponent> </div> } let element = <App title="前端早茶" /> ReactDOM.render(element, document.getElementById('root'))
从上面的代码中,我们可以看到,主页面index里面有:
React.Component
React.useComponent
React.useState
ReactDOM.render
因此我们需要实现:
Component,
useComponent
useState,
render,
需要注意,createElement 是我们的基础,具体原因是因为React使用的是JSX,需要转为React.createElement的。
我截取了React官网的一个demo,大家会更直观
自己的React里面最后要导出内容如下:
三、createElement
/** * @description: createElement 函数 * @param {*} type 类型 * @param {*} props 参数 * @param {array} children 子元素 * @return {*} 对象 */ function createElement(type, props, ...children) { return { type, props: { ...props, children: children.map(child => typeof child === "object" ? child : createTextElement(child) ), }, } /** * @description: createTextElement 函数 * @param {*} text 文本,用于直接渲染文字 * @return {*} 对象 */ function createTextElement(text) { return { type: "TEXT", props: { nodeValue: text, children: [], }, } }
四、Component、useComponent
/** * @description: Component 组件函数 * @param {*} * @return {*} */ class Component { constructor(props){ this.props = props } } /** * @description: useComponent * @param {*} Component 传入的class组件 * @return {*} */ function useComponent(Component){ return function(props){ const component = new Component(props) let [state, setState] = useState(component.state) component.props = props component.state = state component.setState = setState console.log(component) return component.render() } }
五、useState
// 下一个单元任务 let nextUnitOfWork = null // 工作中的fiber let wipRoot = null let currentRoot = null let deletions = null let wipFiber = null let hookIndex = null /** * @description: useState 函数 * @param {*} init component.state * @return {*} */ function useState(init){ const oldHook = wipFiber.base && wipFiber.base.hooks && wipFiber.base.hooks[hookIndex] const hook = { state: oldHook ? oldHook.state : init, queue: [], } const actions = oldHook ? oldHook.queue : [] actions.forEach(action => { hook.state = action }) const setState = action => { hook.queue.push(action) wipRoot = { dom: currentRoot.dom, props: currentRoot.props, base: currentRoot, } nextUnitOfWork = wipRoot deletions = [] } wipFiber.hooks.push(hook) hookIndex++ return [hook.state, setState] }
六、render
// 设置全局 下一个任务单元 let nextUnitOfWork = null /** * @description: render函数 * @param {*} vdom 虚拟dom * @param {*} container 容器 * @return {*} */ function render(vdom, container){ // 工作中的fiber wipRoot = { dom: container, props: { children: [vdom], }, base: currentRoot, } deletions = [] nextUnitOfWork = wipRoot }
从这个render可以看到,我们里面有使用到下一个任务单元,很明显会涉及到协调调度children的逻辑。由于篇幅关系这里简单描述一下:
任务调度reconcileChildren
假如当前有任务,而且当前帧还没结束,我们获取下一个任务单元,并且记录起来。当没有下一个任务了,咋们提交root。在这当中我们会使用到window.requestIdleCallback()这个函数。其作用是在浏览器的空闲时段内调用的函数排队,函数一般会按先进先调用的顺序执行。关于这个函数的具体介绍,我们可以在MDN进行阅读。
这里稍微提一下,React17如果子节点是数组,会调用reconcileChildrenArray。
七、总结
我们实现了一个自己的React,很明显我们有很多细节没有考虑进去。不过经过梳理我们现在对React有了一个清晰的认识,阅读源码的时候我们分块进行理解,化整为零~