React16的createElement以及render方法实现
import React from "react";
import ReactDOM from "react-dom";
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
//对象不做处理,字符串或者数字进行处理
return typeof child === "object" ? child : createTextElement(child);
}),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [], //实际没有,只是为了方便
},
};
}
//定义自己的render方法
function render(element, container) {
// element是最外层的标签,插入html中的root容器中
let dom =
element.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(element.type);
//将节点属性绑定到元素身上
const isProperty = (key) => key !== "children";
Object.keys(element.props)
.filter(isProperty)
.forEach((key) => {
element[key] = element.props[key];
});
// 递归遍历节点信息
element.props.children.forEach((child) => {
render(child, dom);
});
container.appendChild(dom);
}
const Didact = {
createElement,
render,
};
/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
var element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b", null)
);
const container = document.getElementById("root");
Didact.render(element, container);
// ReactDOM.render(element, container);
并发模式
但是创建虚拟DOM本质是一个JavaScript代码的执行过程。如果说创建的DOM树很大,递归很久,那么这段JavaScript程序就会长时间的阻塞主线程。如果浏览器需要执行高优先级程序,就必须等待。因此采用的是并发模式执行
// 拆分工作单元
let nextUnitOfWork = null;
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
/*
deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
*/
shouldYield = deadline.timeRemaining() < 1;
}
requestIdleCallback(workLoop);
}
//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(nextUnitOfWork) {}
fibers
每一个fiber就是一个工作单元。
Didact.render(
<div>
<h1>
<p />
<a />
</h1>
<h2 />
</div>,
container
)
将root节点作为创建fiber的初始入口。并设置为初始工作单元nextUnitOfWork
。performUnitOfWork
函数主要做:将元素添加到DOM,为元素的子元素创建fiber,选择下一个工作单元
child
可以理解为查找的第一个子元素,优先查找该选项parent
当元素的父元素sibling
当前元素的兄弟节点
执行的流程大致如下:
- 从根节点出发,依次寻找第一个子元素作为child
- 如果当前元素没有child项,就查找当前元素的兄弟节点,并重复1。例如p节点和a节点
- 如果当前元素既没有child也没有sibling项,就查找当前元素的parent父元素,如a节点的父节点h1,并重复2
- 如果最终处理完成,回到根节点,则代表render函数执行完成
修改代码如下,当render函数初始化工作单元后,浏览器会在空闲的时候自动去执行workloop函数,在函数内部去执行performUnitOfwork函数
function createDom(fiber) {
// element是最外层的标签,插入html中的root容器中
let dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
//将节点属性绑定到元素身上
const isProperty = (key) => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach((key) => {
dom[key] = fiber.props[key];
});
return dom;
}
//定义自己的render方法
function render(element, container) {
//创建工作单元
nextUnitOfWork = {
dom: container, //root作为根执行单元
props: {
children: [element],
},
};
}
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
//1:创建并添加DOM
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
if (fiber.parent) {
//节点插入到父节点(root节点不需要)
fiber.parent.dom.appendChild(fiber.dom);
}
//2:为当前元素的子元素创建fiber
const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
let index = 0;
let prevSibling = null;
while (index < elements.length) {
const element = elements[index]; //处理每一个子元素
const newFiber = {
dom: null,
props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
type: element.type,
parent: fiber,
};
//将新的fiber添加的fiber树中
if (index === 0) {
//类似记录child,即第一个子元素
fiber.child = newFiber;
} else {
/*
div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
然后再次执行 prevSibling = newFiber往下继续绑定关系
*/
prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
}
//这一步操作可以理解为链表
prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
index++;
}
//3:查找下一个工作单元
if (fiber.child) {
//深度遍历原则,优先找第一个子节点
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
//可能会一直处理到root节点
if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
//如果没有子节点,也没有兄弟节点就返回父节点
nextFiber = nextFiber.parent;
}
}
渲染
如下这段代码,会向DOM中添加一个新节点。浏览器可能会在我们完成整个树的渲染之前中断我们的工作。在这种情况下,用户将看到不完整的 UI。我们不希望那样。所以如下代码需要注释掉,不能采用这种方式。
if (fiber.parent) {
fiber.parent.dom.appendChild(fiber.dom)
}
可以理解如下,创建DOM的操作是在performUnitOfWork
函数中完成,但是该函数的执行是根据渲染主线程是否空闲决定的。因此如果浏览器主线程繁忙,可能不会去创建DOM结构。
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
shouldYield = deadline.timeRemaining() < 1;
}
修改部分代码如下
//在在工作的根work in progress
let wipRoot = null; //跟踪根元素
// 完成所有的工作后,提交根节点
function commitRoot() {
// 递归所有的节点添加到root上
}
function workLoop(deadline) {
....
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
function render(element, container) {
//创建工作单元
wipRoot = {
dom: container, //root作为根执行单元
props: {
children: [element],
},
};
nextUnitOfWork = wipRoot;
}
// 完成所有的工作后,提交根节点
function commitRoot() {
// 递归所有的节点添加到root上
commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
wipRoot = null;
}
function commitWork(fiber) {
// 递归执行,将节点依次添加到父元素
if (!fiber) return;
const domParent = fiber.parent.dom; //父节点
domParent.appendChild(fiber.dom);
commitWork(fiber.child);
commitWork(fiber.sibling);
}
更新或删除
上面的代码是进行初始化渲染操作,涉及到更新或者删除的时候,将在 render 函数上收到的元素与我们提交给 DOM 的最后一个fiber树进行比较。因此,我们需要在完成提交后保存对“我们提交到 DOM 的最后一个fiber树”的引用。我们称之为 currentRoot 。同时为每一个fiber添加一个alternate
属性,记录上一个fiber。
let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用
//创建工作单元
wipRoot = {
dom: container, //root作为根执行单元
props: {
children: [element],
},
alternate: currentRoot,
};
function commitRoot() {
// 递归所有的节点添加到root上
commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
currentRoot = wipRoot;
wipRoot = null;
}
直接上最终代码了
import React from "react";
import ReactDOM from "react-dom";
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
//对象不做处理,字符串或者数字进行处理
return typeof child === "object" ? child : createTextElement(child);
}),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [], //实际没有,只是为了方便
},
};
}
// 拆分工作单元
let nextUnitOfWork = null;
//在在工作的根work in progress
let wipRoot = null; //跟踪根元素
let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用
let deletions = null;
// 完成所有的工作后,提交根节点
function commitRoot() {
deletions.forEach(commitWork);
// 递归所有的节点添加到root上
commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
currentRoot = wipRoot;
wipRoot = null;
}
//处理on开头的事件监听
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key); //包含子元素的不需要添加到标签上
const isNew = (prev, next) => {
//判断属性是否发生变化
return (key) => {
return prev[key] !== next[key];
};
};
/* 当更新 DOM 元素时,可能会有一些属性在更新后被移除,因此需要检查上一个属性对象 prevProps 中的属性是否在下一个属性对象 nextProps 中不存在,若不存在,则说明该属性被移除了,需要相应地在 DOM 上进行处理。 */
const isGone = (prev, next) => {
return (key) => {
return !(key in next);
};
};
// 更新才走
function updateDom(dom, prevProps, nextProps) {
//事件处理程序发生改变就直接删除
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)) //新DOM有该事件,且新旧DOM的事件不一致
.forEach((name) => {
// onClick小写并截取适配l2事件监听,也只有l2可以取消事件监听
const eventType = name.toLowerCase().substring(2);
dom.removeEvenetListener(eventType, prevProps[name]);
});
// 移除旧的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps)) //筛选出旧DOM的属性。不存在新DOM上
.forEach((name) => {
dom[name] = "";
});
//添加或修改属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
//添加新的事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitWork(fiber) {
// 递归执行,将节点依次添加到父元素
if (!fiber) return;
const domParent = fiber.parent.dom; //父节点
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
//
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
//当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
domParent.removeChild(fiber.dom);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
/*
deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
*/
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
//1:创建并添加DOM
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// if (fiber.parent) {
// //节点插入到父节点(root节点不需要)
// fiber.parent.dom.appendChild(fiber.dom);
// }
//2:为当前元素的子元素创建fiber
const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
reconcileChildren(fiber, elements);
//3:查找下一个工作单元
if (fiber.child) {
//深度遍历原则,优先找第一个子节点
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
//可能会一直处理到root节点
if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
//如果没有子节点,也没有兄弟节点就返回父节点
nextFiber = nextFiber.parent;
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let prevSibling = null;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //旧fiber架构的第一个子节点
while (index < elements.length || oldFiber != null) {
const element = elements[index]; //处理每一个子元素
// const newFiber = {
// dom: null,
// props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
// type: element.type,
// parent: wipFiber,
// };
let newFiber = null;
//oldFiber是旧的,element是新的,需要比较判断是否需要更改
const sameType = oldFiber && element && element.type === oldFiber.type;
if (sameType) {
/* 如果旧的和新元素的类型相同,我们可以保留 DOM 节点,只需使用新 prop 更新它 */
newFiber = {
//复用旧节点的信息
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", //添加更新标识
};
}
if (element && !sameType) {
/* 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点 */
newFiber = {
type: element.type,
props: element.props,
dom: null, //创建新节点代表不复用旧的
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
//如果类型不同并且有旧fiber,则需要删除旧节点
//对于需要删除的节点,暂时没有新的光fiber,所以需要将标签添加到旧fiber中
//fiber树提交dom的时候,从正在进行的根目录开始,没有旧的fiber,因此创建数组跟踪要删除的节点
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
//自己的比较完了,比较兄弟节点
oldFiber = oldFiber.sibling;
}
//将新的fiber添加的fiber树中
if (index === 0) {
//类似记录child,即第一个子元素
wipFiber.child = newFiber;
} else {
/*
div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
然后再次执行 prevSibling = newFiber往下继续绑定关系
*/
prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
}
//这一步操作可以理解为链表
prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
index++;
}
}
}
function createDom(fiber) {
// element是最外层的标签,插入html中的root容器中
let dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
//将节点属性绑定到元素身上
const isProperty = (key) => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach((key) => {
dom[key] = fiber.props[key];
});
return dom;
}
//定义自己的render方法
function render(element, container) {
//创建工作单元
wipRoot = {
dom: container, //root作为根执行单元
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
const Didact = {
createElement,
render,
};
/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
var element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b", null)
);
const container = document.getElementById("root");
Didact.render(element, container);
// ReactDOM.render(element, container);
添加函数组件的处理过程
//函数组件
function App(props) {
return <h1>Hi {props.name}</h1>;
}
element = <App name="foo" />;
转换为jsx如下
function App(props) {
return Didact.createElement(
"h1",
null,
"Hi ",
props.name
)
}
const element = Didact.createElement(App, {
name: "foo",
})
函数组件的话,不需要创建DOM结构,数据是来自函数的返回值添加到函数组件的父DOM结构中。
那么在更新的时候就需要判断是函数组件还是普通标签
修改创建fiber的部分
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
//1:创建并添加DOM
const isFunctionComponent = fiber.type instanceof Function; //函数组件
if (isFunctionComponent) {
updateFunctionComponent();
} else {
updateHostComponent(fiber);
}
//3:查找下一个工作单元
if (fiber.child) {
//深度遍历原则,优先找第一个子节点
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
//可能会一直处理到root节点
if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
//如果没有子节点,也没有兄弟节点就返回父节点
nextFiber = nextFiber.parent;
}
function reconcileChildren(wipFiber, elements) {
....
}
//处理函数组件更新
function updateFunctionComponent(fiber) {
const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
reconcileChildren(fiber, children);
}
//处理标签更新
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// if (fiber.parent) {
// //节点插入到父节点(root节点不需要)
// fiber.parent.dom.appendChild(fiber.dom);
// }
//2:为当前元素的子元素创建fiber
const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
reconcileChildren(fiber, elements);
}
}
修改创建dom的部分
function commitWork(fiber) {
// 递归执行,将节点依次添加到父元素
if (!fiber) return;
// const domParent = fiber.parent.dom; //父节点
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
//比如app函数组件内部的h1元素,从h1元素开始需要找到app组件使用的地方的父级
// 沿着fiber树向上查找
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
//当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
/*
如果fiber是一个普通标签div,那么直接通过父元素删除就行
如果fiber是一个app函数组件,那么删除当前函数组件返回的最外层标签 类似这种function App(){return <div>...</div>} }
*/
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
钩子
为函数组件添加状态
function useState(initial) {}
const Didact = {
createElement,
render,
useState,
};
//测试
function Counter() {
const [state, setState] = Didact.useState(1);
return (
<div
onClick={() => {
setState(state + 1);
}}
>
Count: {state}
</div>
);
}
element = <Counter />;
let wipFiber = null; //正在处理的fiber
let hookIndex = null;
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = []; //给当前fiber添加一个数组,用来支持用一个组件中多次调用useState
const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
reconcileChildren(fiber, children);
}
```javascript
function useState(initial) {
/*
当函数组件调用 useState 时,我们检查是否有旧的钩子。我们使用钩子索引检查 alternate fiber。
如果存在旧钩子,则状态从旧钩子复用到新钩子,如果没有,则进行初始化状态
*/
//返回状态值和修改状态的函数
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [], //更新队列,批处理
};
// 下次渲染时候,从旧的钩子队列中获取所有操作并应用到新的钩子状态中,然后返回新的钩子状态
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
hook.queue.push(action);
//设置工作根设置为下一个工作单元,每次设置setState就需要进行页面重新渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
完整代码
import React from "react";
import ReactDOM from "react-dom";
function createElement(type, props, ...children) {
return {
type,
props: {
...props,
children: children.map((child) => {
//对象不做处理,字符串或者数字进行处理
return typeof child === "object" ? child : createTextElement(child);
}),
},
};
}
function createTextElement(text) {
return {
type: "TEXT_ELEMENT",
props: {
nodeValue: text,
children: [], //实际没有,只是为了方便
},
};
}
// 拆分工作单元
let nextUnitOfWork = null;
//在在工作的根work in progress
let wipRoot = null; //跟踪根元素
let currentRoot = null; //提交到 DOM 的最后一个fiber树的引用
let deletions = null;
let wipFiber = null; //正在处理的fiber
let hookIndex = null;
// 完成所有的工作后,提交根节点
function commitRoot() {
deletions.forEach(commitWork);
// 递归所有的节点添加到root上
commitWork(wipRoot.child); //performUnitOfWork执行完毕后则会绑定child属性,如第一个子元素
currentRoot = wipRoot;
wipRoot = null;
}
//处理on开头的事件监听
const isEvent = (key) => key.startsWith("on");
const isProperty = (key) => key !== "children" && !isEvent(key); //包含子元素的不需要添加到标签上
const isNew = (prev, next) => {
//判断属性是否发生变化
return (key) => {
return prev[key] !== next[key];
};
};
/* 当更新 DOM 元素时,可能会有一些属性在更新后被移除,因此需要检查上一个属性对象 prevProps 中的属性是否在下一个属性对象 nextProps 中不存在,若不存在,则说明该属性被移除了,需要相应地在 DOM 上进行处理。 */
const isGone = (prev, next) => {
return (key) => {
return !(key in next);
};
};
// 更新才走
function updateDom(dom, prevProps, nextProps) {
//事件处理程序发生改变就直接删除
Object.keys(prevProps)
.filter(isEvent)
.filter((key) => !(key in nextProps) || isNew(prevProps, nextProps)(key)) //新DOM有该事件,且新旧DOM的事件不一致
.forEach((name) => {
// onClick小写并截取适配l2事件监听,也只有l2可以取消事件监听
const eventType = name.toLowerCase().substring(2);
dom.removeEvenetListener(eventType, prevProps[name]);
});
// 移除旧的属性
Object.keys(prevProps)
.filter(isProperty)
.filter(isGone(prevProps, nextProps)) //筛选出旧DOM的属性。不存在新DOM上
.forEach((name) => {
dom[name] = "";
});
//添加或修改属性
Object.keys(nextProps)
.filter(isProperty)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
dom[name] = nextProps[name];
});
//添加新的事件监听
Object.keys(nextProps)
.filter(isEvent)
.filter(isNew(prevProps, nextProps))
.forEach((name) => {
const eventType = name.toLowerCase().substring(2);
dom.addEventListener(eventType, nextProps[name]);
});
}
function commitWork(fiber) {
// 递归执行,将节点依次添加到父元素
if (!fiber) return;
// const domParent = fiber.parent.dom; //父节点
let domParentFiber = fiber.parent;
while (!domParentFiber.dom) {
//比如app函数组件内部的h1元素,从h1元素开始需要找到app组件使用的地方的父级
// 沿着fiber树向上查找
domParentFiber = domParentFiber.parent;
}
const domParent = domParentFiber.dom;
if (fiber.effectTag === "PLACEMENT" && fiber.dom != null) {
domParent.appendChild(fiber.dom);
} else if (fiber.effectTag === "UPDATE" && fiber.dom != null) {
updateDom(fiber.dom, fiber.alternate.props, fiber.props);
} else if (fiber.effectTag === "DELETION") {
commitDeletion(fiber, domParent);
}
commitWork(fiber.child);
commitWork(fiber.sibling);
}
function commitDeletion(fiber, domParent) {
//当前fiber是需要被删除的,那么就通过查找父元素删除其子节点即可
/*
如果fiber是一个普通标签div,那么直接通过父元素删除就行
如果fiber是一个app函数组件,那么删除当前函数组件返回的最外层标签 类似这种function App(){return <div>...</div>} }
*/
if (fiber.dom) {
domParent.removeChild(fiber.dom);
} else {
commitDeletion(fiber.child, domParent);
}
}
function workLoop(deadline) {
let shouldYield = false;
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
/*
deadline.timeRemaining()是requestIdleCallback提供给回调函数的一个方法。
它返回一个表示当前剩余时间的数字,以毫秒为单位。这个时间表示在当前主线程任务重新被调度之前,
回调函数还有多少可用时间执行任务。因此,deadline.timeRemaining() < 1这个条件检查是否剩余的时间不足以继续执行工作,
如果是,则设置shouldYield为true,以便退出当前循环。这样可以确保在浏览器主线程需要进行其他工作之前,能够及时释放控制权。
*/
shouldYield = deadline.timeRemaining() < 1;
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
requestIdleCallback(workLoop);
}
//浏览器主线程空闲时候自动调用(react内部自己实现了一个)
// requestIdleCallback 还给了我们一个 deadline 参数。我们可以用它来检查我们有多少时间,直到浏览器需要再次控制
requestIdleCallback(workLoop);
//设置第一个工作单元同时返回下一个工作单元
function performUnitOfWork(fiber) {
//1:创建并添加DOM
const isFunctionComponent = fiber.type instanceof Function; //函数组件
if (isFunctionComponent) {
updateFunctionComponent();
} else {
updateHostComponent(fiber);
}
//3:查找下一个工作单元
if (fiber.child) {
//深度遍历原则,优先找第一个子节点
return fiber.child;
}
let nextFiber = fiber;
while (nextFiber) {
//可能会一直处理到root节点
if (nextFiber.sibling) return nextFiber.sibling; //返回兄弟节点
//如果没有子节点,也没有兄弟节点就返回父节点
nextFiber = nextFiber.parent;
}
}
function reconcileChildren(wipFiber, elements) {
let index = 0;
let prevSibling = null;
let oldFiber = wipFiber.alternate && wipFiber.alternate.child; //旧fiber架构的第一个子节点
while (index < elements.length || oldFiber != null) {
const element = elements[index]; //处理每一个子元素
// const newFiber = {
// dom: null,
// props: element.props, //开始可能是虚拟DOM的信息,然后会创建符合工作单元的对象
// type: element.type,
// parent: wipFiber,
// };
let newFiber = null;
//oldFiber是旧的,element是新的,需要比较判断是否需要更改
const sameType = oldFiber && element && element.type === oldFiber.type;
if (sameType) {
/* 如果旧的和新元素的类型相同,我们可以保留 DOM 节点,只需使用新 prop 更新它 */
newFiber = {
//复用旧节点的信息
type: oldFiber.type,
props: element.props,
dom: oldFiber.dom,
parent: wipFiber,
alternate: oldFiber,
effectTag: "UPDATE", //添加更新标识
};
}
if (element && !sameType) {
/* 如果类型不同并且有新元素,则意味着我们需要创建一个新的 DOM 节点 */
newFiber = {
type: element.type,
props: element.props,
dom: null, //创建新节点代表不复用旧的
parent: wipFiber,
alternate: null,
effectTag: "PLACEMENT",
};
}
if (oldFiber && !sameType) {
//如果类型不同并且有旧fiber,则需要删除旧节点
//对于需要删除的节点,暂时没有新的光fiber,所以需要将标签添加到旧fiber中
//fiber树提交dom的时候,从正在进行的根目录开始,没有旧的fiber,因此创建数组跟踪要删除的节点
oldFiber.effectTag = "DELETION";
deletions.push(oldFiber);
}
if (oldFiber) {
//自己的比较完了,比较兄弟节点
oldFiber = oldFiber.sibling;
}
//将新的fiber添加的fiber树中
if (index === 0) {
//类似记录child,即第一个子元素
wipFiber.child = newFiber;
} else {
/*
div(h1,h2)从h1开始,初始走到这里不执行,只记录 prevSibling = newFiber,即上一个兄弟节点为h1
然后第二次循环,进入该分支,prevSibling是h1,newFiber是h2,这样子就个h1绑定了兄弟节点h2,
然后再次执行 prevSibling = newFiber往下继续绑定关系
*/
prevSibling.sibling = newFiber; //记录当前节点的兄弟节点,idnex不为0的时候,记录第一个元素的兄弟节点
}
//这一步操作可以理解为链表
prevSibling = newFiber; //准备记录节点的兄弟节点,一开始index=0,记录第一个元素
index++;
}
}
//处理函数组件更新
function updateFunctionComponent(fiber) {
wipFiber = fiber;
hookIndex = 0;
wipFiber.hooks = []; //给当前fiber添加一个数组,用来支持用一个组件中多次调用useState
const children = [fiber.type(fiber.props)]; //执行函数组件并将参数传递
reconcileChildren(fiber, children);
}
//处理标签更新
function updateHostComponent(fiber) {
if (!fiber.dom) {
fiber.dom = createDom(fiber);
}
// if (fiber.parent) {
// //节点插入到父节点(root节点不需要)
// fiber.parent.dom.appendChild(fiber.dom);
// }
//2:为当前元素的子元素创建fiber
const elements = fiber.props.children; //root节点的子元素都是虚拟DOM
reconcileChildren(fiber, elements);
}
function useState(initial) {
/*
当函数组件调用 useState 时,我们检查是否有旧的钩子。我们使用钩子索引检查 alternate fiber。
如果存在旧钩子,则状态从旧钩子复用到新钩子,如果没有,则进行初始化状态
*/
//返回状态值和修改状态的函数
const oldHook =
wipFiber.alternate &&
wipFiber.alternate.hooks &&
wipFiber.alternate.hooks[hookIndex];
const hook = {
state: oldHook ? oldHook.state : initial,
queue: [], //更新队列,批处理
};
// 下次渲染时候,从旧的钩子队列中获取所有操作并应用到新的钩子状态中,然后返回新的钩子状态
const actions = oldHook ? oldHook.queue : [];
actions.forEach((action) => {
hook.state = action(hook.state);
});
const setState = (action) => {
hook.queue.push(action);
//设置工作根设置为下一个工作单元,每次设置setState就需要进行页面重新渲染
wipRoot = {
dom: currentRoot.dom,
props: currentRoot.props,
alternate: currentRoot,
};
nextUnitOfWork = wipRoot;
deletions = [];
};
wipFiber.hooks.push(hook);
hookIndex++;
return [hook.state, setState];
}
function createDom(fiber) {
// element是最外层的标签,插入html中的root容器中
let dom =
fiber.type === "TEXT_ELEMENT"
? document.createTextNode("")
: document.createElement(fiber.type);
//将节点属性绑定到元素身上
const isProperty = (key) => key !== "children";
Object.keys(fiber.props)
.filter(isProperty)
.forEach((key) => {
dom[key] = fiber.props[key];
});
return dom;
}
//定义自己的render方法
function render(element, container) {
//创建工作单元
wipRoot = {
dom: container, //root作为根执行单元
props: {
children: [element],
},
alternate: currentRoot,
};
deletions = [];
nextUnitOfWork = wipRoot;
}
const Didact = {
createElement,
render,
useState,
};
/** @jsx Didact.createElement */
//预处理指令,告诉编辑器使用我指定的函数创建JSX元素
//普通标签
var element = (
<div id="foo">
<a>bar</a>
<b />
</div>
);
element = Didact.createElement(
"div",
{ id: "foo" },
Didact.createElement("a", null, "bar"),
Didact.createElement("b", null)
);
//函数组件
function App(props) {
return <h1>Hi {props.name}</h1>;
}
element = <App name="foo" />;
//测试
function Counter() {
const [state, setState] = Didact.useState(1);
return (
<div
onClick={() => {
setState(state + 1);
}}
>
Count: {state}
</div>
);
}
element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);
// ReactDOM.render(element, container);