基于babel实现react核心功能(初始化,fiber,hook)
为什么我会基于babel来实现react,因为jsx浏览器是无法识别的,所以我通过babel编译jsx为js,在手撕源码实现就ok了,废话不多说上才艺,我哩giao。
前方高能,请做好准备
项目结构
首先把相关的插件都装好
package.json
{
"name": "source",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "cross-env NODE_ENV=development webpack-dev-server --sourcemap --inline --progress --config build/webpack.dev.config.js",
"build": "cross-env NODE_ENV=production webpack --progress --config build/webpack.prod.config.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {},
"devDependencies": {
"@babel/core": "^7.11.6",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/preset-env": "^7.11.5",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"cross-env": "^7.0.2",
"css-loader": "^4.3.0",
"html-webpack-plugin": "^4.4.1",
"less-loader": "^7.0.1",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
安装html-webpack-plugin是为了让webpack找到html文件并输出到浏览器,babel-loader加上babel插件将jsx和一些语法进行转换和polyfill
// webpack.dev.config.js
const {resolve, posix:{join}} = require('path');
const HTMLPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development',
entry: {
app: "./src/index"
},
// 出口
output: {
path : resolve(__dirname,"../dist"),
filename: join("static", "js/[name].[hash].js") ,
chunkFilename: join("static", "js/[name].[chunkhash].js"),
publicPath: "/" // 打包后的资源的访问路径前缀
},
module: { // 所有第三方模块的匹配规则, webpack默认只能处理.js后缀名的文件,需要安装第三方loader
rules: [
{
test: /\.m?js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env'],
plugins: ["@babel/plugin-proposal-class-properties"]
}
},
exclude: /(node_modules|bower_components)/, // 千万别忘记添加exclude选项,不然运行可能会报错
},
{
test: /\.less$/,
use: [
// {
// loader:MiniCssExtractPlugin.loader,
// options:{
// hmr: utils.isDev(), // 开发环境热更新 ,然而不起作用
// reloadAll:true,
// }
// },
{
loader: 'style-loader',
},
{
loader: 'css-loader',
},
{
loader: 'postcss-loader'
},
{
loader: 'less-loader', // 编译 Less -> CSS
},
],
}
]
},
plugins: [
new HTMLPlugin(
{
filename: resolve(__dirname, './../dist/index.html'), // html模板的生成路径
template: './public/index.html',//html模板
inject: true, // true:默认值,script标签位于html文件的 body 底部
}
)
],
resolve: {
extensions: ['.js', 'json'],
alias: {
'@': join(__dirname, '..', 'src')
}
},
devtool: "#eval-source-map",
devServer: {
historyApiFallback: true, // 当找不到路径的时候,默认加载index.html文件
hot: true,
contentBase: false, // 告诉服务器从哪里提供内容。只有在你想要提供静态文件时才需要
compress: true, // 一切服务都启用gzip 压缩:
port: "3005", // 指定段靠谱
publicPath: "/", // 访问资源加前缀
}
}
.babelrc
{ "presets":["@babel/react","@babel/env"]}
webpack的入口文件
./src/index.js
import Component from './react/component';
import React from './react';
import ReactDom from './react/react-dom';
class ClassComponent extends Component {
render () {
return (<div><button>666</button></div>)
}
}
const jsx = (<div>
<ClassComponent />
</div>)
ReactDom.render(jsx, document.getElementById('root'))
接下来是实现react源码的代码,我也不爱废话,上代码
jsx会根据每一个节点,转译成React.createElement,比如上面我写的标签其实最后会变成下面的样子
// 下面的null的位置是属性,我上面一个属性都没有所以是null
React.createElement('div', null, React.createElement(ClassComponent, null))
看到这里你会想知道这个React.createElement是怎么实现的呀,感觉有点意思了,那就继续看吧
./src/react/const.js
export const TEXT = 'TEXT';
export const PLACEMENT = "PLACEMENT";
export const UPDATE = "UPDATE";
export const DELETION = "DELETION";
./src/react/component.js
export default class {
static isReactComponent = {}
constructor (props) {
this.props = props;
}
}
./src/react/index.js
``` javascript
import {TEXT} from './const';
function createElement (type, config, ...children) {
const props = {
...(config || {}),
// 这里判断是否为文本,不是文本就是虚拟节点
children: children.map(child => typeof child === 'object' ? child:{
type: TEXT,
props: {
children: [],
nodeValue: child
}
})
}
return {type, props}
}
export default {createElement}
上面的代码执行完应该就会生成虚拟dom树了
接下来我们来实现fiber架构的diff,把虚拟节点变真实节点,也就是把js所描述的对象变成真正的dom插入到页面中
虚拟dom转换成真实dom靠的就是一手新旧节点的比较,而react把它优化的非常nice,通过调用requestIdleCallback让页面更加丝滑,,具体我不想讲了,不懂看看别的博客
./src/react/react-dom.js
import {TEXT, PLACEMENT, UPDATE, DELETION} from "./const";
// 下一个单元任务
let nextUnitOfWork = null;
// work in progress fiber root
let wipRoot = null;
// 现在的根节点
let currentRoot = null;
let deletions = null;
function render(createVnode, container) {
let vnode;
if (createVnode.isReactComponent) {
vnode = new createVnode().render();
} else if (typeof createVnode === 'function') {
vnode = createVnode();
} else {
vnode = createVnode;
}
wipRoot = {
node: container,
props: {
children: [vnode]
},
base: currentRoot
};
nextUnitOfWork = wipRoot;
deletions = [];
}
// vnode->node
// 生成成node节点
function createNode(vnode) {
const {type, props} = vnode;
let node = null;
if (type === TEXT) {
node = document.createTextNode("");
} else if (typeof type === "string") {
node = document.createElement(type);
}
updateNode(node, {}, props);
return node;
}
function reconcileChildren(workInProgressFiber, children) {
// 构建fiber结构
// 更新 删除 新增
let prevSibling = null;
let oldFiber = workInProgressFiber.base && workInProgressFiber.base.child;
for (let i = 0; i < children.length; i++) {
let child = children[i];
let newFiber = null;
const sameType = child && oldFiber && child.type === oldFiber.type;
if (sameType) {
// 类型相同 复用
newFiber = {
type: oldFiber.type,
props: child.props,
node: oldFiber.node,
base: oldFiber,
return: workInProgressFiber,
effectTag: UPDATE
};
}
if (!sameType && child) {
// 类型不同 child存在 新增插入
newFiber = {
type: child.type,
props: child.props,
node: null,
base: null,
return: workInProgressFiber,
effectTag: PLACEMENT
};
}
if (!sameType && oldFiber) {
// 删除
oldFiber.effectTag = DELETION;
deletions.push(oldFiber);
}
if (oldFiber) {
oldFiber = oldFiber.sibling;
}
// 形成链表结构
if (i === 0) {
workInProgressFiber.child = newFiber;
} else {
// i>0
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
}
}
function updateNode(node, preVal, nextVal) {
Object.keys(preVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
// 简单处理 on开头当做事件
let eventName = k.slice(2).toLowerCase();
node.removeEventListener(eventName, preVal[k]);
} else {
if (!(k in nextVal)) {
node[k] = "";
}
}
});
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => {
if (k.slice(0, 2) === "on") {
// 简单处理 on开头当做事件
let eventName = k.slice(2).toLowerCase();
node.addEventListener(eventName, nextVal[k]);
} else {
node[k] = nextVal[k];
}
});
}
function updateFunctionComponent(fiber) {
wipFiber = fiber;
wipFiber.hooks = [];
hookIndex = 0;
const {type, props} = fiber;
const children = [type(props)];
reconcileChildren(fiber, children);
}
function updateClassComponent(fiber) {
wipFiber = fiber;
wipFiber.hooks = [];
hookIndex = 0;
const {type, props} = fiber;
const children = [new type(props).render()];
reconcileChildren(fiber, children);
}
function performUnitOfWork(fiber) {
// 1. 执行当前任务
// 执行当前任务
const {type} = fiber;
if (typeof type === "function") {
type.isReactComponent
? updateClassComponent(fiber)
: updateFunctionComponent(fiber);
} else {
// 原生标签
updateHostComponent(fiber);
}
// 2、 返回下一个任务
// 原则就是:先找子元素
if (fiber.child) {
return fiber.child;
}
// 如果没有子元素 寻找兄弟元素
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
function updateHostComponent(fiber) {
if (!fiber.node) {
fiber.node = createNode(fiber);
}
// todo reconcileChildren
const {children} = fiber.props;
reconcileChildren(fiber, children);
}
function workLoop(deadline) {
// 有下一个任务,并且当前帧还没有结束
while (nextUnitOfWork && deadline.timeRemaining() > 1) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
requestIdleCallback(workLoop);
// ! commit阶段
function commitRoot() {
deletions.forEach(commitWorker);
commitWorker(wipRoot.child);
currentRoot = wipRoot;
wipRoot = null;
}
function commitWorker(fiber) {
if (!fiber) {
return;
}
// 向上查找
let parentNodeFiber = fiber.return;
while (!parentNodeFiber.node) {
parentNodeFiber = parentNodeFiber.return;
}
const parentNode = parentNodeFiber.node;
if (fiber.effectTag === PLACEMENT && fiber.node !== null) {
parentNode.appendChild(fiber.node);
} else if (fiber.effectTag === UPDATE && fiber.node !== null) {
updateNode(fiber.node, fiber.base.props, fiber.props);
} else if (fiber.effectTag === DELETION && fiber.node !== null) {
commitDeletions(fiber, parentNode);
}
commitWorker(fiber.child);
commitWorker(fiber.sibling);
}
function commitDeletions(fiber, parentNode) {
if (fiber.node) {
parentNode.removeChild(fiber.node);
} else {
commitDeletions(fiber.child, parentNode);
}
}
// !hook 实现
// 当前正在工作的fiber
let wipFiber = null;
let hookIndex = null;
export function useState(init) {
const oldHook = wipFiber.base && 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 = {
node: currentRoot.node,
props: currentRoot.props,
base: currentRoot
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
export default {render};