【React 16】模拟实现3

React | 2020-06-10 01:35:34 570次 2次

通过之前两篇文章的铺垫和分析,大致了解了 react16 中要做的一个方向,下面通过具体细节实现。


一、JSX

JSXJavaScript XML,是 React 提供的语法糖。 通过它可以很方便的在js代码中书写html片段。React 中用 JSX 来创建虚拟DOM。然后会通过 babel 进行编译为 js 代码。

jsx代码片段:

let element = (
  <div id="A1" style={style}>
    <p>
      <span>111</span>
      <span>222</span>
    </p>
    <div id="B1" style={style}>
      B1
      <div id="C1" style={style}>C1</div>
      <div id="C2" style={style}>C2</div>
    </div>
    <div id="B2" style={style}>B2</div>
  </div>
)

babel编译后:

"use strict";

React.createElement("div", {
  id: "A1",
  style: style
}, React.createElement("p", null, React.createElement("span", null, "111"), 
 React.createElement("span", null, "222")), React.createElement("div", {
  id: "B1",
  style: style
}, "B1", React.createElement("div", {
  id: "C1",
  style: style
}, "C1"), React.createElement("div", {
  id: "C2",
  style: style
}, "C2")), React.createElement("div", {
  id: "B2",
  style: style
}, "B2"));


二、React.createElement

通过上面分方式了解到,通过 babel 编译后,会产生一个 React.createElement 方法,接收三个参数,分别为节点类型、一些属性(对象)、子节点。节点类型有可能是一个组件也可能是普通的 html 标签,但是怎么去正确渲染这个节点呢,所以在 react 中一般都是组件首字母大写,这样看到首字母大写的就知道是一个组件,需要再遍历渲染,其他的就是普通的节点来处理。

先看下一段常规的 react 代码:

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

let style = { border: '3px solid red', margin: '5px' };
let element = (
  <div id="A1" style={style}>
    <p>
      <span>111</span>
      <span>222</span>
    </p>
    <div id="B1" style={style}>
      B1
      <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 文件,暴露出 React 方法,其属性中有一个 createElement 方法:

import { ELEMENT_TEXT } from './constants';
/**
 * 创建元素(虚拟DOM)
 * @param {} type  元素的类型div span p
 * @param {*} config  配置对象 属性 key ref
 * @param  {...any} children  子节点 数组
 */
function createElement(type, config, ...children) {
    return {
        type,
        props: {
            ...config,
            children: children.map(child => {
                //如果这个child是一个React.createElement 则返回React元素,
                //如果是字符串的话会转成文本节点
                return typeof child === 'object' ? child : {
                    type: ELEMENT_TEXT,
                    props: { text: child, children: [] }
                }
            })
        }
    }
}
const React = {
    createElement
}
export default React;

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 PLACEMENT = Symbol.for('PLACEMENT');
//更新节点
export const UPDATE = Symbol.for('UPDATE');
//删除节点
export const DELETION = Symbol.for('DELETION');

其中我们注意到,在最上面的那段代码中虽然引用了 React,但是并没有显式的直接调用,但是如果不引入一定会报错,这是因为通过 babel 编译后的代码中包含这个方法,然后直接调用,返回一个对象,包含 typeprops 属性,props 在整个数据流向中就成了一条主线路,并且是不可逆转的,即单向流数据。

然后创建一个 react-dom.js 文件,暴露出 ReactDom 方法,其属性中有一个 render 方法:

import { TAG_ROOT } from './constants';
import { scheduleRoot } from './schedule';
/**
 * 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;

首先创建了一个 rootFiber 根节点,通过 scheduleRoort 方法来开始遍历创建出 fiber 树,这个方法要做的事情就很多了,React 后续会把这个功能也单独发出一个包可直接调用,我们这里只是简单实现一些基本功能就好了。


三、scheduleRoot

从根节点开始渲染和调度两个阶段,render 阶段进行 diff 对比新旧的虚拟 DOM,进行增量更新或创建(首次渲染创建),这个阶段可以比较花时间,可以我们对任务进行拆分,拆分的维度虚拟 DOM,此阶段可以暂停。

render 阶段有两个任务

    ① 根据虚拟 DOM 生成 fiber 树 

    ② 收集 effectlist 它记录了节点的更新、修改或删除。

commit 阶段,进行 DOM 更新创建阶段,此阶段不能暂停。

微信截图_20200612235554.png

代码实现:

import { TAG_ROOT, ELEMENT_TEXT, TAG_TEXT, TAG_HOST, PLACEMENT } from "./constants";
import { setProps } from './utils';
/**
 * 从根节点开始渲染和调度 两个阶段 
 * 
 * render阶段成果是effect list知道哪些节点更新哪些节点删除了,哪些节点增加了
 * render阶段有两个任务1.根据虚拟DOM生成fiber树 2.收集effectlist
 * commit阶段,进行DOM更新创建阶段,此阶段不能暂停,要一气呵成
 */
let nextUnitOfWork = null;//下一个工作单元
let workInProgressRoot = null;//RootFiber应用的根
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) {
        console.log(currentFiber)
        completeUnitOfWork(currentFiber);//没有儿子让自己完成
        if (currentFiber.sibling) {//看有没有弟弟
            return currentFiber.sibling;//有弟弟返回弟弟
        }
        currentFiber = currentFiber.return;//找父亲然后让父亲完成
    }
}
//在完成的时候要收集有副作用的fiber,然后组成effect list
//每个fiber有两个属性 firstEffect指向第一个有副作用的子fiber lastEffect 指儿 最后一个有副作用子Fiber
//中间的用nextEffect做成一个单链表 firstEffect=大儿子.nextEffect二儿子.nextEffect三儿子 lastEffect
function completeUnitOfWork(currentFiber) {//第一个完成的A1(TEXT)
    let returnFiber = currentFiber.return;//A1
    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) {// 自己有副作用 A1 first last=A1(Text)
            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) {//原生DOM节点
        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);//div
        updateDOM(stateNode, {}, currentFiber.props);
        return stateNode;
    }
}
function updateDOM(stateNode, oldProps, newProps) {
    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);
}
function reconcileChildren(currentFiber, newChildren) {//[A1]
    let newChildIndex = 0;//新子节点的索引
    let prevSibling;//上一个新的子fiber
    //遍历我们的子虚拟DOM元素数组,为每个虚拟DOM元素创建子Fiber
    while (newChildIndex < newChildren.length) {
        let newChild = newChildren[newChildIndex];//取出虚拟DOM节点[A1]{type:'A1'}
        let tag;
        if (newChild.type == ELEMENT_TEXT) {
            tag = TAG_TEXT;//这是一个文本节点
        } else if (typeof newChild.type === 'string') {
            tag = TAG_HOST;//如果是type是字符串,那么这是一个原生DOM节点 "A1" div
        }//beginWork创建fiber 在completeUnitOfWork的时候收集effect
        let 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我们要会收集副作用 增加 删除 更新
            nextEffect: null,//effect list 也是一个单链表
            //effect list顺序和 完成顺序是一样的
        }
        //最小的儿子是没有弟弟的
        if (newFiber) {
            if (newChildIndex == 0) {//如果当前索引为0,说明这是太子
                currentFiber.child = newFiber;
            } else {
                prevSibling.sibling = newFiber;//让太子的sibling弟弟指向二皇子
            }
            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) {
        console.log('commitRoot', currentFiber.type, currentFiber.props.id, currentFiber.props.text);
        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 });

utils:

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)) {//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);
    }
}

这里实现了初次 fiber 树构建,之间会生成 effectList 链表,记录哪些节点发生改变或初次创建,接下来继续实现 diff 算法和双缓冲优化 fiber

2人赞

分享到: