基本概念
react 描述页面是使用jsx来描述的,经过babel编译后会生成 React.createElement 的函数。
上面的 jsx会转换成对应的 createElement 代码。
然后生成createElement函数被调用返回 vdom。
{
"children": [
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "1",
"children": []
}
}
]
}
},
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "2",
"children": []
}
}
]
}
},
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "3",
"children": []
}
}
]
}
}
]
}
这个就是 vdom 生成的流程,也就是React 16 之前的架构,之后最大的区别就是 16 之后 引入了 fiber,又基于 fiber 实现了 hooks, 天天都听着说 fiber,那fiber到底是什么呢?它和vdom又是什么关系呢?
jsx 编译
react的jsx是依赖于babel生成的createElement函数的
打开项目 执行 npm install --save-dev @babel/core @babel/cli @babel/preset-react
分别是下载了babel的核心库,babel命令行工具cli,babel编译jsx的预设
新建.babelrc 文件
{
"presets": [
[
"@babel/preset-react",
{
"pragma": "my.createElement"
}
]
]
}
执行 npx babel index.js -d build 打包代码。
上面就是配置好了 jsx代码编译,pragma是自定义名称,这里就使用自己的createElement
实现my.createElement
我们只要实现createElement函数就可以拿到vdom了
const my = {
createElement: (type, poprs, ...childer) => {
return {
type,
props: {
...poprs ? poprs : {},
children: childer.map(chiler=> {
if (typeof chiler=== 'object') {
return chiler;
} else {
//文本也有自己对应的vdom
return {
type: "TEXT",
props: {
nodeValue: chiler,
children: [],
},
}
}
})
}
};
}
};
渲染出来就是这样
{
"children": [
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "1",
"children": []
}
}
]
}
},
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "2",
"children": []
}
}
]
}
},
{
"type": "li",
"props": {
"children": [
{
"type": "TEXT",
"props": {
"nodeValue": "3",
"children": []
}
}
]
}
}
]
}
先把 vdom 转 fiber,也就是 reconcile 的过程,因为 fiber 是链表,就可以打断,用 schedule 来空闲时调度,最后全部转完之后,再一次性 render,这个过程叫做 commit
schedule实现(调度器,有空闲时继续调度 reconcile)
利用 setTimeout 唤醒 reconcile 协调器
function reconcile(time){
const timeSlice = (res) => {
const start = performance.now();
do{
//它做的事情就是循环处理完所有的 reconcile:
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
//超过 5 毫秒暂停函数
}while (nextFiberReconcileWork && (performance.now() - start) <= time);
if(!nextFiberReconcileWork)res()
if(nextFiberReconcileWork){
//休眠这一轮宏任务,唤醒后继续递归执行 reconcile
setTimeout(()=>{
timeSlice(res);
});
};
}
return new Promise(timeSlice)
};
reconcile(5)
给reconcile 5 毫秒执行时间,超过5毫秒使用 setTimeout 休眠任务,利用nextFiberReconcileWork 全局变量保存当前 fiber 下一轮reconcile 继续使用 nextFiberReconcileWork 协调 fiber
如果全部都转完了,那就 commit
reconcile(5).then(()=>{
if (!nextFiberReconcileWork) {
commitRoot();
}
})
schedule 的代码就是这样的
let nextFiberReconcileWork = null;
let wipRoot = null;
function reconcile(time){
const timeSlice = (res) => {
const start = performance.now();
do{
//它做的事情就是循环处理完所有的 reconcile:
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
//超过 5 毫秒暂停函数
}while (nextFiberReconcileWork && (performance.now() - start) <= time);
if(!nextFiberReconcileWork)res()
if(nextFiberReconcileWork){
//休眠这一轮宏任务,唤醒后继续递归执行 reconcile
setTimeout(()=>{
timeSlice(res);
});
};
}
return new Promise(timeSlice)
};
reconcile(5).then(()=>{
if(!nextFiberReconcileWork){
commitRoot(wipRoot);
};
})
每次执行的 performNextWork 函数就是 reconcile:
function performNextWork(fiber) {
//拼接fiber树
reconcile(fiber);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
reconcile 当前 fiber 节点,然后再按照顺序继续处理 child、sibling,处理完之后回到 return 的 fiber 节点。这样不断的调度 reconcile。这就是 schedule 做的事情:schedule 就是通过给定的时间调度每个 fiber 节点的 reconcile(vdom 转 fiber),全部 reconcile 完了就执行 commit。
reconcile (调度器)
render 函数实现
function render(element, container) {
wipRoot = {
dom: container,
props: {
children: [element],
}
}
nextFiberReconcileWork = wipRoot
}
创建根 fiber 节点,赋值给 wipRoot,也就是 working in progress 的 fiber root 的意思。并且下一个处理的 fiber 节点指向它,那么下次 schedule 就会调度这个 fiber 节点,开始 reconcile。
reconcile 的实现是这样的:
function reconcile(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber)
}
reconcileChildren(fiber, fiber.props.children)
}
fiber.props.children 就是 vdom 的子节点,这里的 reconcileChildren 就是把之前的 vdom 转成 child、sibling、return 这样串联起来的 fiber 链表:
循环处理每一个 vdom 的 elements,如果 index 是 0,那就是 child 串联,否则是 sibling 串联。创建出的节点都要用 return 指向父节点:
function reconcileChildren(fiber, child) {
let i = 0;
let preSibing = null
while (i < child.length) {
const item = child[i]
const newFiber = {
return: fiber,
sibling: null,
child: null,
type: item.type,
props: item.props,
effectTag: "PLACEMENT"
};
if (i === 0) {
fiber.child = newFiber
} else {
preSibing.sibling = newFiber
};
preSibing = newFiber
i++;
}
}
因为我们只实现渲染,暂时不做 diff 和删除修改,所以这里的 effectTag 都是 placement,也就是新增元素。
通过 schdule 空闲调度这样处理每一个 vdom 转 fiber,就能生成整个 fiber 链表。
所以,这就是 reconcile 做的事情: reconcile 负责 vdom 转 fiber,并且还会准备好要用的 dom 节点、确定好是增、删、还是改,通过 schdule 的调度,最终把整个 vdom 树转成了 fiber 链表。当 fiber 转完了,那么 schdule 调度就进入到了这里
reconcile(5).then(()=>{
if(!nextFiberReconcileWork){
commitRoot(wipRoot);
};
})
开始执行 commit:commitcommit 就是对 dom 的增删改,而且比之前 vdom 架构时的渲染还要快,因为 dom 都提前创建了、也知道是增是删还是改了,那剩下的不就很简单了么?我们从根 fiber 开始 commit,并且把 wipRoot 设置为空,因为不再需要调度它了
function commitRoot() {
commitWork(wipRoot.child);
wipRoot = null
}
每个 fiber 节点的渲染就是按照 child、sibling 的顺序以此插入到 dom 中:
function commitWork(fiber) {
let f = fiber;
while(f){
let domParentFiber = f.return;
if(f.dom){
domParentFiber.dom.appendChild(f.dom);
};
if(f.child){
f = f.child;
}else{
const wrapFun = () => {
let s = f;
while(s){
if(s.sibling){
f = s.sibling;
return;
};
s = s.return;
};
f = null;
}
wrapFun()
}
}
}
这里每个 fiber 节点都要往上找它的父节点,因为我们只是新增,那么只需要 appendChild 就行
dom 已经在 reconcile 节点就创建好了,当时我们没细讲,现在来看下 dom 创建逻辑:
function createDom(fiber) {
let dom = ''
if(fiber.type == "TEXT"){
dom = document.createTextNode(fiber.props.nodeValue);
}else if(typeof fiber.type === 'function'){
const isFunC = f => {
let cFiber = f;
if (typeof cFiber.type === 'function') {
return isFunC(cFiber.type(cFiber.props));
} else {
return cFiber;
}
};
const cFiber = isFunC(fiber)
fiber.props = cFiber.props;
dom = createDom(cFiber);
}else{
dom = document.createElement(fiber.type);
}
return dom;
}
就是根据类型创建元素,然后返回dom节点,如果是 fiber.type === ‘function’ 说明是函数组件,只需要拿着 Fiber.type 调用函数,拿到 vdom 继续剩下的流程
组件其实只是换了个方式储存 vdom 函数组件和class组件同理,如果是class组件直接调用render函数就行了。
这样,我们就实现了简易版 React,当然,目前只实现了渲染,页面效果:
完整代码:
const data = {
item1: 'bb',
item2: 'cc'
};
const my = {
createElement: (type, poprs, ...childer) => {
const t = childer.reduce((arr, item)=>{
if(Array.isArray(item)){
arr.push(...item)
}else{
arr.push(item)
}
return arr
}, [])
return {
type,
props: {
...poprs ? poprs : {},
children: t.map(item => {
if (typeof item === 'object') {
return item;
} else {
return {
type: "TEXT",
props: {
nodeValue: item,
children: [],
},
}
}
})
}
};
}
};
const App = () => {
return (
<ui>
<li>1</li>
<li>2</li>
<li>3</li>
</ui>
)
};
console.log(App())
function render(element, container) {
let nextFiberReconcileWork = null;
let wipRoot = null;
function commitWork(fiber) {
let f = fiber;
while(f){
let domParentFiber = f.return;
if(f.dom){
domParentFiber.dom.appendChild(f.dom);
};
if(f.child){
f = f.child;
}else{
const wrapFun = () => {
let s = f;
while(s){
if(s.sibling){
f = s.sibling;
return;
};
s = s.return;
};
f = null;
}
wrapFun()
}
}
}
function commitRoot() {
commitWork(wipRoot.child);
}
function reconcileChildren(fiber, child) {
let i = 0;
let preSibing = null
while (i < child.length) {
const item = child[i]
const newFiber = {
return: fiber,
sibling: null,
child: null,
type: item.type,
props: item.props,
effectTag: "PLACEMENT"
};
if (i === 0) {
fiber.child = newFiber
} else {
preSibing.sibling = newFiber
};
preSibing = newFiber
i++;
}
}
const setAttribute = (dom, key, value) => {
if (key === 'children') {
return;
}
if (key === 'nodeValue') {
dom.textContent = value;
} else {
dom.setAttribute(key, value);
}
};
function createDom(fiber) {
let dom = ''
if(fiber.type == "TEXT"){
dom = document.createTextNode(fiber.props.nodeValue);
}else if(typeof fiber.type === 'function'){
const isFunC = f => {
let cFiber = f;
if (typeof cFiber.type === 'function') {
return isFunC(cFiber.type(cFiber.props));
} else {
return cFiber;
}
};
const cFiber = isFunC(fiber)
fiber.props = cFiber.props;
dom = createDom(cFiber);
}else{
dom = document.createElement(fiber.type);
}
return dom;
}
function reconcile(fiber) {
let _fiber = fiber;
if (!_fiber.dom) {
_fiber.dom = createDom(_fiber)
};
reconcileChildren(_fiber, _fiber.props.children)
}
function performNextWork(fiber) {
reconcile(fiber);
if (fiber.child) {
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
if (nextFiber.sibling) {
return nextFiber.sibling;
}
nextFiber = nextFiber.return;
}
}
wipRoot = {
dom: container,
props: {
children: [element],
},
}
nextFiberReconcileWork = wipRoot;
function timeSlice(time){
const p = (res) => {
const start = performance.now();
do{
nextFiberReconcileWork = performNextWork(
nextFiberReconcileWork
);
}while (nextFiberReconcileWork && (performance.now() - start) <= time);
if(!nextFiberReconcileWork)res()
if(nextFiberReconcileWork){
setTimeout(()=>{
p(res);
});
};
}
return new Promise(p)
};
timeSlice(5).then(()=>{
if(!nextFiberReconcileWork){
commitRoot(wipRoot);
};
})
}
render(<App />, document.querySelector("#root"))