React | 2020-06-10 01:35:34 570次 2次
通过之前两篇文章的铺垫和分析,大致了解了 react16 中要做的一个方向,下面通过具体细节实现。
一、JSX
JSX 是 JavaScript 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 编译后的代码中包含这个方法,然后直接调用,返回一个对象,包含 type 和 props 属性,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 更新创建阶段,此阶段不能暂停。
代码实现:
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人赞