6.2.1 build-your-own-react

365 阅读5分钟

前置知识:

react里使用的jsx语法,浏览器是并不能识别的,所以要经过babel的编译,会将jsx元素转换为虚拟dom的形式,可点击jsx转换js

<div id="counter">
     <span>{this.state.number}</span>
     <button onClick={this.onClick}>+1</button>
</div>

经过babel编译后,会变成下面这个样子:

React.createElement(
"div", 
{ id: "counter"},
React.createElement("span", null, (void 0).state.number), React.createElement("button", {
  onClick: (void 0).onClick
}, "+1"));

类组件 使用

import React from './react';
import ReactDOM from './react-dom';

class ClassCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { number: 0 };
  }
  onClick = () => {
    this.setState(state => ({ number: state.number + 1 }));
  }
  render() {
    return (
      <div id="counter">
          <span>{this.state.number}</span>
          <button onClick={this.onClick}>+1</button>
      </div>
    )
  }
}
ReactDOM.render(ClassCounter, document.getElementById('root'));

函数式组件使用

import React from './react';
import ReactDOM from './react-dom';

const ADD = 'ADD';
function reducer(state, action) {
  switch (action.type) {
    case ADD:
      return { count: state.count + 1 };
    default:
      return state;
  }
}
function FunctionCounter() {
  const [numberState, setNumberState] = React.useState({ number: 0 });
  const [countState, dispatch] = React.useReducer(reducer, { count: 0 });

  return (
    <>
      <div id="counter1">
        <span>{numberState.number}</span>
        <button onClick={() => setNumberState({ number: numberState.number + 1 })}>+ 1</button>
      </div>
      <div id="counter2">
        <span>{countState.count}</span>
        <button onClick={() => dispatch({ type: ADD })}>+1</button>
      </div>
    </>
  )
}
ReactDOM.render(<FunctionCounter />, document.getElementById('root'));

代码结构

  • constants.js
  • react.js (Component、createElement)
  • react-dom.js (render)
  • scheduler.js
  • updateQueue.js
  • utils.js

constants.js

//表示这是一个文本元素
export const ELEMENT_TEXT = Symbol.for('ELEMENT_TEXT');
//React应用需要一个根Fiber
export const TAG_ROOT = Symbol.for('TAG_ROOT');
//原生的节点 span div p  函数组件 类组件
export const TAG_HOST = Symbol.for('TAG_HOST');
//这是文本节点
export const TAG_TEXT = Symbol.for('TAG_TEXT');
//这是类组件
export const TAG_CLASS = Symbol.for('TAG_CLASS');
//函数组件
export const TAG_FUNCTION_COMPONENT = Symbol.for('TAG_FUNCTION_COMPONENT');
//插入节点
export const PLACEMENT = Symbol.for('PLACEMENT');
//更新节点
export const UPDATE = Symbol.for('UPDATE');
//删除节点
export const DELETION = Symbol.for('DELETION');

react.js

function createElement(type, config, ...children) {
  // 删掉不必要的属性
  delete config.__self;
  delete config.__source;//表示这个元素是在哪行哪列哪个文件生成的
  return {
    type,
    props: {
      ...config,//做了一个兼容处理,如果是React元素的话返回自己,如果是文本类型,如果是一个字符串的话,返回元素对象
      children: children.map(child => {
        //如果这个child是一个React.createElement返回的React元素,如果是字符串的话,才会转成文本节点
        return typeof child === 'object' ? child : {
          type: ELEMENT_TEXT,
          props: { text: child, children: [] }
        }
      })
    }
  }
}

class Component {
  constructor(props, context) {
    this.props = props
    this.context = context
    this.state = this.state || {}
    this.refs = {}
    this.updater = null
  }

  setState(partialState) {
    this.updater.enqueueUpdate(
        new Update(partialState)
    )
    scheduleRoot()
  }

  render() {
    throw 'should implement `render()` function'
  }
}
Component.prototype.isReactComponent = true

const React = {
    createElement,
    Component
}

export default React;

react-dom.js

import { TAG_ROOT } from './constants';
import { scheduleRoot } from './scheduler';
/**
 * render是要把一个元素渲染到一个容器内部
 */
function render(element, container) {//container=root DOM节点
    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;

scheduler.js

import { TAG_ROOT, ELEMENT_TEXT, TAG_TEXT, TAG_HOST, PLACEMENT, DELETION, UPDATE, TAG_CLASS, TAG_FUNCTION_COMPONENT } from "./constants";
import { setProps } from './utils';
import { UpdateQueue, Update } from "./UpdateQueue";
/**
 * 从根节点开始渲染和调度 两个阶段 
 * 
 * diff阶段 对比新旧的虚拟DOM,进行增量 更新或创建. render阶段
 * 这个阶段可以比较花时间,可以我们对任务进行拆分,拆分的维度虚拟DOM。此阶段可以暂停
 * render阶段成果是effect list知道哪些节点更新哪些节点删除了,哪些节点增加了
 * render阶段有两个任务1.根据虚拟DOM生成fiber树 2.收集effectlist
 * commit阶段,进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
 */
let nextUnitOfWork = null;//下一个工作单元
let workInProgressRoot = null;//正在渲染的根ROOT fiber
let currentRoot = null;//渲染成功之后当前根ROOTFiber
let deletions = [];//删除的节点我们并不放在effect list里,所以需要单独记录并执行
let workInProgressFiber = null;//正在工作中的fiber
let hookIndex = 0;//hooks索引 
export function scheduleRoot(rootFiber) {//{tag:TAG_ROOT,stateNode:container,props: { children: [element] }}
    if (currentRoot && currentRoot.alternate) {//第二次之后的更新
        workInProgressRoot = currentRoot.alternate;//第一次渲染出来的那个fiber tree
        workInProgressRoot.alternate = currentRoot;//让这个树的替身指向的当前的currentRoot
        if (rootFiber) workInProgressRoot.props = rootFiber.props;//让它的props更新成新的props
    } else if (currentRoot) {//说明至少已经渲染过一次了 第一次更新
        if (rootFiber) {
            rootFiber.alternate = currentRoot;
            workInProgressRoot = rootFiber;
        } else {
            workInProgressRoot = {
                ...currentRoot,
                alternate: currentRoot
            }
        }
    } else {//如果说是第一次渲染
        workInProgressRoot = rootFiber;
    }
    workInProgressRoot.firstEffect = workInProgressRoot.lastEffect = workInProgressRoot.nextEffect = null;
    nextUnitOfWork = workInProgressRoot;
}

//react告诉 浏览器,我现在有任务请你在闲的时候,
//有一个优先级的概念。expirationTime 
requestIdleCallback(workLoop, { timeout: 500 });

//循环执行工作 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 performUnitOfWork(currentFiber) { //完成根据虚拟DOM创建Fiber的过程
    beginWork(currentFiber);
    if (currentFiber.child) {
        return currentFiber.child;
    }
    // console.log('构建fiber====>', workInProgressRoot) // 从rootFiber开始的fiber链表
    while (currentFiber) {
        completeUnitOfWork(currentFiber);//没有儿子让自己完成,或者当儿子全部完成当时候让自己完成
        if (currentFiber.sibling) {//看有没有弟弟
            return currentFiber.sibling;//有弟弟返回弟弟
        }
        currentFiber = currentFiber.return;//找父亲然后让父亲完成
    }
    // console.log('收集effectlist====>', workInProgressRoot) // undefined rootFiber的父节点是undefined
}

// —commit阶段
/**
 * beginWork开始收下线的钱
 * completeUnitOfWork把下线的钱收完了
 * 1.创建真实DOM元素
 * 2.创建子fiber  
 */
function beginWork(currentFiber) {
    if (currentFiber.tag === TAG_ROOT) {//根fiber
        updateHostRoot(currentFiber);
    } else if (currentFiber.tag === TAG_TEXT) {//文本fiber
        updateHostText(currentFiber);
    } else if (currentFiber.tag === TAG_HOST) {//原生DOM节点 stateNode dom
        updateHost(currentFiber);
    } else if (currentFiber.tag === TAG_CLASS) {//类组件
        updateClassComponent(currentFiber);
    } else if (currentFiber.tag === TAG_FUNCTION_COMPONENT) {//类组件
        updateFunctionComponent(currentFiber);
    }
}
function updateFunctionComponent(currentFiber) {
    workInProgressFiber = currentFiber;
    hookIndex = 0;
    workInProgressFiber.hooks = [];
    const newChildren = [currentFiber.type(currentFiber.props)];
    reconcileChildren(currentFiber, newChildren);
}
function updateClassComponent(currentFiber) {
    if (!currentFiber.stateNode) {//类组件 stateNode 组件的实例
        // new ClassCounter(); 类组件实例   fiber双向指向 
        currentFiber.stateNode = new currentFiber.type(currentFiber.props); // 类组件的实例
        currentFiber.stateNode.internalFiber = currentFiber;
        currentFiber.stateNode.updater = currentFiber.updateQueue = new UpdateQueue();
    }
    //给组件的实例的state 赋值
    currentFiber.stateNode.state = currentFiber.updateQueue.forceUpdate(currentFiber.stateNode.state);
    let newElement = currentFiber.stateNode.render();
    const newChildren = [newElement];
    console.log(newElement);
    reconcileChildren(currentFiber, newChildren);// 类组件fiber和对应的虚拟dom
}
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);//div
        updateDOM(stateNode, {}, currentFiber.props);
        return stateNode;
    }
}
function updateDOM(stateNode, oldProps, newProps) {
    if (stateNode && stateNode.setAttribute)
        setProps(stateNode, oldProps, newProps);
}
function updateHostText(currentFiber) {
    if (!currentFiber.stateNode) {//如果此fiber没有创建DOM节点
        currentFiber.stateNode = createDOM(currentFiber);
    }
}
function updateHostRoot(currentFiber) {
    //先处理自己 如果是一个原生节点,创建真实DOM 2.创建子fiber 
    let newChildren = currentFiber.props.children;//[element=<div id="A1"]
    reconcileChildren(currentFiber, newChildren);
}
//newChildren是一个虚拟DOM的数组 把虚拟DOM转成Fiber节点
function reconcileChildren(currentFiber, newChildren) {//[A1]
    let newChildIndex = 0;//新子节点的索引
    //如果说currentFiber有alternate并且alternate有child属性
    let oldFiber = currentFiber.alternate && currentFiber.alternate.child;
    if (oldFiber) oldFiber.firstEffect = oldFiber.lastEffect = oldFiber.nextEffect = null;
    let prevNewFiber;//上一个新的子fiber
    //遍历我们的子虚拟DOM元素数组,为每个虚拟DOM元素创建子Fiber
    while (newChildIndex < newChildren.length || oldFiber) {
        let newChild = newChildren[newChildIndex];//取出虚拟DOM节点[A1]{type:'A1'}
        let newFiber;//新的Fiber
        const sameType = oldFiber && newChild && oldFiber.type === newChild.type;
        let tag;
        if (newChild && typeof newChild.type === 'function' && newChild.type.prototype.isReactComponent) {
            tag = TAG_CLASS;//
        } else if (newChild && typeof newChild.type === 'function') {
            tag = TAG_FUNCTION_COMPONENT;//这是一个文本节点
        } else if (newChild && newChild.type == ELEMENT_TEXT) {
            tag = TAG_TEXT;//这是一个文本节点
        } else if (newChild && typeof newChild.type === 'string') {
            tag = TAG_HOST;//如果是type是字符串,那么这是一个原生DOM节点 "A1" div
        }//beginWork创建fiber 在completeUnitOfWork的时候收集effect
        if (sameType) {//说明老fiber和新虚拟DOM类型一样,可以复用老的DOM节点,更新即可
            if (oldFiber.alternate) {//说明至少已经更新一次了
                newFiber = oldFiber.alternate;//如果有上上次的fiber,就拿 过来作为这一次的fiber
                newFiber.props = newChild.props;
                newFiber.alternate = oldFiber;
                newFiber.effectTag = UPDATE;
                newFiber.updateQueue = oldFiber.updateQueue || new UpdateQueue();
                newFiber.nextEffect = null;
            } else {
                newFiber = {
                    tag: oldFiber.tag,//TAG_HOST
                    type: oldFiber.type,//div
                    props: newChild.props,//{id="A1" style={style}} 一定要用新的元素的props
                    stateNode: oldFiber.stateNode,//div还没有创建DOM元素
                    return: currentFiber,//父Fiber returnFiber
                    updateQueue: oldFiber.updateQueue || new UpdateQueue(),
                    alternate: oldFiber,//让新的fiber的alternate指向老的fiber节点
                    effectTag: UPDATE,//副作用标识 render我们要会收集副作用 增加 删除 更新
                    nextEffect: null,//effect list 也是一个单链表
                }
            }
        } else {
            if (newChild) {//看看新的虚拟DOM是不是为null
                newFiber = {
                    tag,//TAG_HOST
                    type: newChild.type,//div
                    props: newChild.props,//{id="A1" style={style}}
                    stateNode: null,//div还没有创建DOM元素
                    return: currentFiber,//父Fiber returnFiber
                    effectTag: PLACEMENT,//副作用标识 render我们要会收集副作用 增加 删除 更新
                    updateQueue: new UpdateQueue(),
                    nextEffect: null,//effect list 也是一个单链表
                    //effect list顺序和 完成顺序是一样的,但是节点只放那些出钱的人的fiber节点,不出钱绕过去
                }
            }
            if (oldFiber) {
                oldFiber.effectTag = DELETION;
                deletions.push(oldFiber);
            }

        }
        if (oldFiber) {
            oldFiber = oldFiber.sibling;//oldFiber指针往后移动一次
        }
        //最小的儿子是没有弟弟的
        if (newFiber) {
            if (newChildIndex == 0) {//如果当前索引为0,说明这是太子
                currentFiber.child = newFiber;
            } else {
                prevNewFiber.sibling = newFiber;//让太子的sibling弟弟指向二皇子
            }
            prevNewFiber = newFiber;
        }
        newChildIndex++;
    }

}

//— 组成effect list
function completeUnitOfWork(currentFiber) {
    let returnFiber = currentFiber.return;
    if (returnFiber) {
        ////这一段是把自己儿子的effect 链挂到父亲身上
        if (!returnFiber.firstEffect) {
            // console.log(currentFiber)
            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;
        }
    }
}


// —commit阶段

function commitRoot() {
    deletions.forEach(commitWork);//执行effect list之前先把该删除的元素删除
    let currentFiber = workInProgressRoot.firstEffect;
    while (currentFiber) {
        commitWork(currentFiber);
        currentFiber = currentFiber.nextEffect;
    }
    deletions.length = 0;//提交之后要清空deletion数组
    currentRoot = workInProgressRoot;//把当前渲染成功的根fiber 赋给currentRoot
    workInProgressRoot = null;
}

function commitWork(currentFiber) {
    if (!currentFiber) return;
    let returnFiber = currentFiber.return;
    while (returnFiber.tag !== TAG_HOST &&
        returnFiber.tag !== TAG_ROOT &&
        returnFiber.tag !== TAG_TEXT) {
        returnFiber = returnFiber.return;
    }
    let domReturn = returnFiber.stateNode;
    if (currentFiber.effectTag === PLACEMENT) {//新增加节点
        let nextFiber = currentFiber;
        /*  if (nextFiber.tag === TAG_CLASS) {
                    return;
                } */
        // 如果要挂载的节点不是DOM节点,比如说是类组件Fiber,一直找第一个儿子,直到找到一个真实DOM节点为止
        while (nextFiber.tag !== TAG_HOST && nextFiber.tag !== TAG_TEXT) {
            nextFiber = currentFiber.child;
        }
        domReturn.appendChild(nextFiber.stateNode);
    } else if (currentFiber.effectTag === DELETION) {//删除节点
        return commitDeletion(currentFiber, domReturn);
    } else if (currentFiber.effectTag === UPDATE) {
        if (currentFiber.type === ELEMENT_TEXT) {
            if (currentFiber.alternate.props.text != currentFiber.props.text){
                currentFiber.stateNode.textContent = currentFiber.props.text;
            }
        } else {
            updateDOM(currentFiber.stateNode,
                currentFiber.alternate.props, currentFiber.props);
        }
    }
    currentFiber.effectTag = null;
}
function commitDeletion(currentFiber, domReturn) {
    if (currentFiber.tag == TAG_HOST || currentFiber.tag == TAG_TEXT) {
        domReturn.removeChild(currentFiber.stateNode);
    } else {
        commitDeletion(currentFiber.child, domReturn)
    }
}
/**
    workInProgressFiber = currentFiber;
    hookIndex = 0;
    workInProgressFiber.hooks = [];
 */
export function useReducer(reducer, initialValue) {
    let newHook = workInProgressFiber.alternate && workInProgressFiber.alternate.hooks
        && workInProgressFiber.alternate.hooks[hookIndex];
    if (newHook) {
        newHook.state = newHook.updateQueue.forceUpdate(newHook.state);
    } else {
        newHook = {
            state: initialValue,
            updateQueue: new UpdateQueue()// 空的更新队列
        }
    }
    const dispatch = action => {//{type:'ADD'}
        let payload = reducer ? reducer(newHook.state, action) : action;
        newHook.updateQueue.enqueueUpdate(
            new Update(payload)
        );
        scheduleRoot();
    }
    workInProgressFiber.hooks[hookIndex++] = newHook;
    return [newHook.state, dispatch];
}
export function useState(initialValue) {
    return useReducer(null, initialValue);
}

updateQueue.js

export class Update {
  constructor(payload) {
      this.payload = payload;
  }
}
//数据结构是一个单链表
export class UpdateQueue {
  constructor() {
      this.firstUpdate = null;
      this.lastUpdate = null;
  }
  enqueueUpdate(update) {
      if (this.lastUpdate === null) {
          this.firstUpdate = this.lastUpdate = update;
      } else {
          this.lastUpdate.nextUpdate = update;
          this.lastUpdate = update;
      }
  }
  forceUpdate(state) {
      let currentUpdate = this.firstUpdate;
      while (currentUpdate) {
          let nextState = typeof currentUpdate.payload === 'function' ? currentUpdate.payload(state) : currentUpdate.payload;
          state = { ...state, ...nextState };
          currentUpdate = currentUpdate.nextUpdate;
      }
      this.firstUpdate = this.lastUpdate = null;
      return state;
  }
}

utils.js

export function setProps(dom, oldProps, newProps) {
  for (let key in oldProps) {
      if (key !== 'children') {
          if (newProps.hasOwnProperty(key)) {
              setProp(dom, key, newProps[key]);// 新老都有,则更新
          } else {
              dom.removeAttribute(key);//老props里有此属性,新 props没有,则删除
          }
      }
  }
  for (let key in newProps) {
      if (key !== 'children') {
          if (!oldProps.hasOwnProperty(key)) {//老的没有,新的有,就添加此属性
              setProp(dom, key, newProps[key]);
          }
      }
  }
}
function setProp(dom, key, value) {
  if (/^on/.test(key)) {//onClick
      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);
  }
}

参考: zhuanlan.zhihu.com/p/486935572
juejin.cn/post/694389…
github.com/RubyLouvre/…

欢迎关注我的前端自检清单,我和你一起成长