react源码初次渲染
(1)帧
页面是一帧一帧绘制出来的,当每秒绘制的帧数(FPS)达到60时,页面是流畅的,小于这个值,用户会感觉到卡顿,所以每一帧分到的时间大概是16.6ms,这个和屏幕刷新率有关
javaScript执行javaScript引擎和页面渲染引擎在同一个渲染线程,GUI渲染和javaScript执行两者是互斥的。
在一帧里任务执行完处于空闲阶段,在空闲阶段去执行react任务
(2)fiber
react任务执行单元,将虚拟dom转成fiber
vue把每个更新任务分割的足够小,watch组件级更新
react任务还是很大,但是分割成多个小任务,可以中断和恢复,不阻塞主进程
实现
(1)页面效果
(2)代码实现
index.js
import React from "./react";
import ReactDOM from "./react-dom";
let style={border:'3px solid red',margin:'5px'}
let element = (
<div id="A1" style={style}>
<div id="B1" style={style}>
<div id="C1" style={style}>C1</div>
<div id="C2" style={style}>C2</div>
</div>
<div id="B2" style={style}>
B2
</div>
</div>
);
ReactDOM.render(element, document.getElementById("root"));
react.js创建元素
import { ELEMENT_TEXT } from "./constant";
/**
* 创建元素
*/
function createElement(type, config, ...children) {
delete config._self;
delete config._source;
return {
type,
props: {
...config,//作兼容处理,如果是react元素就返回自己,如果是文本类型,如果是一个字符串的话,返回元素对象
children: children.map((child) => {
return typeof child === "object"
? child
: {
type: ELEMENT_TEXT,
props: { text: child, children: [] },
};
}),
},
};
}
const React={
createElement,
}
export default React;
react-dom.js 定义render
import {TAG_ROOT} from './constant'
import {scheduleRoot} from './schedule'
/**
* render是要把一个元素渲染到一个容器内
*/
function render(element,container){
let rootFiber={
tag:TAG_ROOT,//每个fiber会有一个tag标识比元素的类型
stateNode:container,//一般情况下如果这个元素是一个原生节点的话,stateNode指向真实DOM元素
// props.children是一个数组,里面放的是React元素 虚拟DOM 后面会根据每个React元素创建对应的fiber
props:{
children:[element]//这个fiber的属性对象children属性,里面放的是要渲染的元素
}
}
scheduleRoot(rootFiber)
}
const ReactDOM={
render
}
export default ReactDOM;
schedule.js 执行任务,当页面一帧的任务执行完且有空闲时间时,会调用requestIdleCallback方法去执行我们的react任务,workLoop有一个deadline参数,可判断是否有剩余时间,如果有剩余时间,则继续执行,如果没有,则中断任务交给浏览器去执行
performUnitOfWork()深度优先遍历树,创建fiber,当一个节点完成后,执行completeUnitOfWork()建立副作用链表,建立顺序和创建fiber一致,当render阶段结束,进入提交阶段commitRoot(),最后将链表清空
import {
ELEMENT_TEXT,
TAG_TEXT,
TAG_HOST,
PLACEMENT,
TAG_ROOT,
} from "./constant";
import { setProps } from "./utils";
/**
* 从根节点开始渲染和调度
* 两个阶段
* diff阶段 对比新旧的虚拟DOM 进行增量 更新或创建,
* render阶段 这个阶段可能比较花时间,需要我们对任务进行拆分,拆分的维度虚拟DOM,此阶段可暂停
* render阶段成果是effect list知道哪些节点更新哪些节点删除了哪些节点增加了
* render阶段有两个任务,根据虚拟DOM生成fiber树,收集effectlist
* commit阶段,进行DOM更新创建阶段,此阶段不可暂停,要一气呵成
*/
let nextUnitOfWork = null;
let workInProgressRoot = null;
export function scheduleRoot(rootFiber) {
//{tag:TAG_ROOT,stateNode:container,props:{children:[element]}}
workInProgressRoot = rootFiber;
nextUnitOfWork = rootFiber;
}
function performUnitOfWork(currentFiber) {
beginWork(currentFiber);
if (currentFiber.child) {
return currentFiber.child;
}
while (currentFiber) {
completeUnitOfWork(currentFiber); //没有儿子让自己完成
if (currentFiber.sibling) {
//看有没有弟弟
return currentFiber.sibling; //有弟弟返回弟弟
}
currentFiber = currentFiber.return;
}
}
/**
*
* 在完成的时候要收集有副作用的fiber,然后组成effect list
* 每一个fiber有两个属性,firstEffect指向第一个有副作用的子fiber lastEffect指向最后一个有副作用的子fiber
* 中间的用nextEffect做成一个单链表 firstEffect=直系儿子.nextEffect二儿子 lastEffect 最后一个儿子
*/
function completeUnitOfWork(currentFiber) {
//第一个完成A1(TEXT)
let returnFiber = currentFiber.return;
if (returnFiber) {
//把自己儿子的effect链挂到父亲身上
if (!returnFiber.firstEffect) {
returnFiber.firstEffect = currentFiber.firstEffect;
}
if (!!currentFiber.lastEffect) {
if (!!returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber.firstEffect;
}
returnFiber.lastEffect = currentFiber.lastEffect;
}
//把自己挂到父亲身上
const effectTag = currentFiber.effectTag;
if (effectTag) {
//如果自己有副作用
if (returnFiber.lastEffect) {
returnFiber.lastEffect.nextEffect = currentFiber;
} else {
returnFiber.firstEffect = currentFiber;
}
returnFiber.lastEffect = currentFiber;
}
}
}
/**
*
* beginWork开始
* completeUnitOfWork该节点完成
* 1 创建真实DOM元素
* 2 创建子fiber
*/
function beginWork(currentFiber) {
if (currentFiber.tag === TAG_ROOT) {
updateHostRoot(currentFiber);
} else if (currentFiber.tag === TAG_TEXT) {
updateHostText(currentFiber);
} else if (currentFiber.tag === TAG_HOST) {
updateHost(currentFiber);
}
}
function updateHost(currentFiber) {
if (!currentFiber.stateNode) {
//如果比fiber没有创建DOM节点
currentFiber.stateNode = createDOM(currentFiber);
}
const newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
function createDOM(currentFiber) {
if (currentFiber.tag === TAG_TEXT) {
return document.createTextNode(currentFiber.props.text);
} else if (currentFiber.tag === TAG_HOST) {
//span div
let stateNode = document.createElement(currentFiber.type);
updateDOM(stateNode, {}, currentFiber.props);
return stateNode;
}
}
function updateDOM(stateNode, oldProps, newProps) {
setProps(stateNode, oldProps, newProps);
}
function updateHostText(currentFiber) {
if (!currentFiber.stateNode) {
currentFiber.stateNode = createDOM(currentFiber);
}
}
function updateHostRoot(currentFiber) {
//先处理自己,如果是一个原生节点,创建真实DOM,2.创建子fiber
//因为拿到的是根节点,所以直接取子节点
let newChildren = currentFiber.props.children;
reconcileChildren(currentFiber, newChildren);
}
function reconcileChildren(currentFiber, newChildren) {
let newChildIndex = 0; //新子节点的索引
let prevSibling; //上一个新的子fiber
while (newChildIndex < newChildren.length) {
let newChild = newChildren[newChildIndex]; //取出虚拟DOM节点
let tag;
if (newChild.type === ELEMENT_TEXT) {
tag = TAG_TEXT; //这是一个文本节点
} else if (typeof newChild.type === "string") {
tag = TAG_HOST; //这是一个原生DOM节点
}
//beginWork创建fiber,在completeUnit
let newFiber = {
tag, //TAG_HOST
type: newChild.type, //div
props: newChild.props,
stateNode: null, //div还没创建DOM元素
return: currentFiber, //父fiber
effectTag: PLACEMENT, //副作用标识 render我们要会收集副作用 增加 删除 更新
nextEffect: null, //effect list也是一个单链表
//effect list顺序和完成顺序是一样的,但是节点只放那些出钱的人的fiber节点,
};
if (newFiber) {
if (newChildIndex === 0) {
//如果索引为0,说明这是第一个儿子
currentFiber.child = newFiber;
} else {
prevSibling.sibling = newFiber;
}
prevSibling = newFiber;
}
newChildIndex++;
}
}
//循环执行工作 nextUnitWork
function workLoop(deadline) {
let shouldYield = false; //是否要让出时间片或者控制权
while (nextUnitOfWork && !shouldYield) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork); //执行完一个任务后
shouldYield = deadline.timeRemaining() < 1; //没有时间的话就要让出控制权
}
if (!nextUnitOfWork && workInProgressRoot) {
console.log("render阶段结束 ");
commitRoot();
}
//不管有没有任务,都请求再次调度,每一帧都要执行一次workLoop
requestIdleCallback(workLoop, { timeout: 500 });
}
function commitRoot() {
let currentFiber = workInProgressRoot.firstEffect;
while (currentFiber) {
commitWork(currentFiber);
currentFiber = currentFiber.nextEffect;
}
workInProgressRoot = null;
}
function commitWork(currentFiber) {
if (!currentFiber) return;
let returnFiber = currentFiber.return;
let returnDOM = returnFiber.stateNode;
if (currentFiber.effectTag === PLACEMENT) {
returnDOM.appendChild(currentFiber.stateNode);
}
currentFiber.effectTag = null;
}
//react告诉浏览器,我现在有任务请你在闲的时候
//有一个优先级的概念 expirationTime
requestIdleCallback(workLoop, { timeout: 500 });
constant.js
export const ELEMENT_TEXT=Symbol.for("ELEMENT_TEXT"); //文本元素
export const TAG_ROOT=Symbol.for("TAG_ROOT"); //根fiber
export const TAG_HOST=Symbol.for("TAG_HOST"); //原生节点,span div 函数组件 ,类组件
export const TAG_TEXT=Symbol.for("TAG_TEXT"); //文本节点
export const PLACEMENT=Symbol.for("PLACEMENT"); //插入节点
export const UPDATE=Symbol.for("UPDATE"); //更新节点
export const DELETION=Symbol.for("DELETION"); //删除节点
utils.js
export function setProps(dom,oldProps,newProps){
// for(let key in oldProps){
// }
for(let key in newProps){
if(key!=='children'){
setProp(dom,key,newProps[key]);
}
}
}
function setProp(dom,key,value){
if(/^on/.test(key)){
dom[key.toLowerCase()]=value;
}else if(key==='style'){
if(value){
for(let styleName in value){
dom.style[styleName]=value[styleName];
}
}else{
dom.setAttribute(key,value)
}
}
}