基於babel實現react核心功能(初始化,fiber,hook)
阿新 • • 發佈:2020-09-12
為什麼我會基於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};