- 深入掌握虚拟dom
- 掌握render、Component两个基础核心api
- 掌握常见组件的渲染
- 掌握虚拟dom、diff策略
- 掌握fiber原理及实现
知识点
React 本身只是一个 DOM 的抽象层,使用组件构建虚拟 DOM。
虚拟dom
常见问题:react virtual dom是什么?说一下diff算法?
what?
用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对象结构。这个 JavaScript 对象称为virtual dom;
传统dom渲染流程
why?
DOM操作很慢,轻微的操作都可能导致页面重新排版,非常耗性能。相对于DOM对象,js对象处理起来更快,而且更简单。通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行dom操作,从而提高性能。
where?
React中用JSX语法描述视图(View),通过babel-loader转译后它们变为React.createElement(...)形式,该函数将生成vdom来描述真实dom。将来如果状态变化,vdom将作出相应变化,再通过diff算法对比新老vdom区别从而做出最终dom操作。
JSX
1. 什么是JSX
语法糖
React 使用 JSX 来替代常规的 JavaScript。
JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。
2. 为什么需要JSX
开发效率:使用 JSX 编写模板简单快速。
执行效率:JSX编译为 JavaScript 代码后进行了优化,执行更快。
类型安全:在编译过程中就能发现错误。
3. React 16原理
babel-loader
会预编译JSX为React.createElement(...)
4. React 17原理
React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从React 的 package 中引入新的入口函数并调用。另外此次升级不会改变 JSX 语法,旧的 JSX 转换也将继续工作。
5. 与vue的异同:
react中虚拟dom+jsx的设计一开始就有,vue则是演进过程中才出现的
jsx本来就是js扩展,转义过程简单直接的多;vue把template编译为render函数的过程需要复杂的编译器转换字符串->ast->js函数字符串
核心精简后:
const React = {
createElement,
Component
}
核心的api
- React.Component:实现自定义组件
- ReactDOM.render:渲染真实DOM
ReactDOM
- render()
- ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 DOM 差分算法(DOM diffing algorithm)进行高效的更新。如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
节点类型
注意节点类型:
- 文本节点
- HTML标签节点
- 函数组件
- 类组件
- 函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
类组件
React 的组件可以定义为 class 或函数的形式。如需定义 class 组件,需要继承 React.Component
或者 React.PureComponent
:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
类组件源码
实现ReactDom.render, Component
src/index.js
// import ReactDOM from "react-dom";
import ReactDOM from "react-dom";
import Component from "Component";
import "./index.css";
function FunctionComponent(props) {
return (
<div className="border">
<p>{props.name}</p>
</div>
);
}
class ClassComponent extends Component {
render() {
return (
<div className="border">
<p>{this.props.name}</p>
</div>全栈工程师
修改index.js实际引入react,测试
ReactDOM.render
);
}
}
function FragmentComponent(props) {
return (
<>
<h1>111</h1>
<h1>222</h1>
</>
);
}
const jsx = (
<div className="border">
<h1>慢慢慢</h1>
<h1>全栈</h1>
<FunctionComponent name="函数组件" />
<ClassComponent name="类组件" />
<FragmentComponent />
</div>
);
ReactDOM.render(jsx, document.getElementById("root"));
ReactDOM.render
import ReactDOM from "react-dom";
import Component from "Component";
// vnode 虚拟dom节点
// node 真实dom节点
function render(vnode, container) {
console.log("vnode", vnode); //sy-log
// step1 : vnode->node
const node = createNode(vnode);
// 把node更新到container中
container.appendChild(node);
}
function isStringOrNumber(sth) {
return typeof sth === "string" || typeof sth === "number";
}
// 根据vnode生成node节点
function createNode(vnode) {
let node;
const {type} = vnode;
// todo 根据vnode生成node
if (typeof type === "string") {
// 原生标签节点
node = updateHostComponent(vnode);
} else if (isStringOrNumber(vnode)) {
node = updateTextComponent(vnode + "");
} else if (typeof type === "function") {
node = type.prototype.isReactComponent
? updateClassComponent(vnode)
: updateFunctionComponent(vnode);
} else {
node = updateFragmentComponent(vnode);
}
return node;
}
// 更新原生标签的,即根据原生标签的vnode生成node
function updateHostComponent(vnode) {
const {type, props} = vnode;
const node = document.createElement(type);
updateNode(node, props);
reconcileChildren(node, props.children);
return node;
}
// 更新原生标签的属性,如className、href、id、(style、事件)等
function updateNode(node, nextVal) {
Object.keys(nextVal)
.filter(k => k !== "children")
.forEach(k => (node[k] = nextVal[k]));
}
// 函数组件 执行函数
// 返回node
function updateFunctionComponent(vnode) {
const {type, props} = vnode;
const child = type(props);
// child->node
const node = createNode(child);
return node;
}
// 类组件
// 先实例化 再执行render函数
function updateClassComponent(vnode) {
const {type, props} = vnode;
const instance = new type(props);
const child = instance.render();
// child->node
const node = createNode(child);
return node;
}
// 文本节点
function updateTextComponent(vnode) {
const node = document.createTextNode(vnode);
return node;
}
// 实现Fragment
function updateFragmentComponent(vnode) {
}
// 遍历子节点,假的协调
function reconcileChildren(parentNode, children) {
const newChildren = Array.isArray(children) ? children : [children];
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
// child是vnode, child->node, 把node更新到parentNode中
render(child, parentNode);
}
}
export default {render};
Component
class Component {
static isReactComponent = {};
constructor(props) {
this.props = props;
}
}
// function Component(props) {
// this.props = props;
// }
// Component.prototype.isReactComponent = {};
export default Component;
总结:
-
React17中,React会自动替换JSX为js对象
-
JS对象即vdom,它能够完整描述dom结构
-
ReactDOM.render(vdom, container)可以将vdom转换为dom并追加到container中
-
实际上,转换过程需要经过一个diff过程。
reconciliation协调
设计动力
在某一时间节点调用 React 的 render() 方法,会创建一棵由 React 元素组成的树。在下一次 state 或props 更新时,相同的 render() 方法会返回一棵不同的树。React 需要基于这两棵树之间的差别来判断如何有效率的更新 UI 以保证当前 UI 与最新的树保持同步。
这个算法问题有一些通用的解决方案,即生成将一棵树转换成另一棵树的最小操作数。 然而,即使在最 前沿的算法中,该算法的复杂程度为 O(n 3 ),其中 n 是树中元素的数量。
如果在 React 中使用了该算法,那么展示 1000 个元素所需要执行的计算量将在十亿的量级范围。这个 开销实在是太过高昂。于是 React 在以下两个假设的基础之上提出了一套 O(n) 的启发式算法:
-
两个不同类型的元素会产生出不同的树;
-
开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
在实践中,我们发现以上假设在几乎所有实用的场景下都成立。
diffing算法
算法复杂度O(n)
diff 策略
-
同级比较,Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
-
拥有不同类型的两个组件将会生成不同的树形结构。例如:div->p, CompA->CompB
-
开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定;
diff过程
比对两个虚拟dom时会有三种操作:删除、替换和更新
vnode是现在的虚拟dom,newVnode是新虚拟dom。
- 删除:newVnode不存在时
- 替换:vnode和newVnode类型不同或key不同时
- 更新:有相同类型和key但vnode和newVnode不同时
在实践中也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
fiber
为什么需要fiber
对于大型项目,组件树会很大,这个时候递归遍历的成本就会很高,会造成主线程被持续占用,结果就是主线程上的布局、动画等周期性任务就无法立即得到处理,造成视觉上的卡顿,影响用户体验。
2. 任务分解的意义
解决上面的问题
- 增量渲染(把渲染任务拆分成块,匀到多帧)
- 更新时能够暂停,终止,复用渲染任务
- 给不同类型的更新赋予优先级
- 并发方面新的基础能力更流畅
什么是fiber
A Fiber is work on a Component that needs to be done or was done. There can be more than one per component.
fiber是指组件上将要完成或者已经完成的任务,每个组件可以一个或者多个。
实现fiber
window.requestIdleCallback(callback[, options])
window.requestIdleCallback()
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。函数一般会按先
进先调用的顺序执行,然而,如果回调函数指定了执行超时时间 timeout ,则有可能为了在超时前执行
函数而打乱执行顺序。
你可以在空闲回调函数中调用 requestIdleCallback() ,以便在下一次通过事件循环之前调度另一个 回调。
callback
一个在事件循环空闲时即将被调用的函数的引用。函数会接收到一个名为 IdleDeadline 的参数,这
个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。
options 可选
包括可选的配置参数。具有如下属性:
timeout
:如果指定了timeout并具有一个正值,并且尚未通过超时毫秒数调用回调,那么回调会
在下一次空闲时期被强制执行,尽管这样很可能会对性能造成负面影响。
Fiber
是 React 16 中新的协调引擎。它的主要目的是使 Virtual DOM
可以进行增量式渲染。
一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段(Phase):
第一个阶段:Reconciliation Phase
第二阶段:Commit Phase。
// import React, {Component} from "react";
// import ReactDOM from "react-dom";
import ReactDOM from "react-dom";
import Component from "Component";
import "./index.css";
class ClassComponent extends Component {
render() {
return (
<div className="border">
<p>{this.props.name}</p>
</div>
);
}
}
function FunctionComponent(props) {
return (
<div className="border">
<p>{props.name}</p>
<button
onClick={() => {
console.log("omg"); //sy-log
}}>
click
</button>
</div>
);
}
const jsx = (
<div className="border">
<h1>全栈</h1>
<FunctionComponent name="function" />
<ClassComponent name="class" />
<>
<h1>omg</h1>
<h2>omg</h2>
</>
</div>
);
ReactDOM.render(jsx, document.getElementById("root"));
// console.log("React", React.version); //sy-log
createFiber.js
import {Placement} from "./utils";
/**
* fiber:
* type 标记节点类型
* key 标记节点在当前层级下的唯一性
* props 属性
* index 标记当前层级下的位置
* child 第一个子节点
* sibling 下一个兄弟节点
* return 父节点
* stateNode 如果组件是原生标签则是dom节点,如果是类组件则是类实例
*/
export function createFiber(vnode, returnFiber) {
const newFiber = {
type: vnode.type,
key: vnode.key,
props: vnode.props,
stateNode: null,
child: null,
return: returnFiber,
sibling: null,
alternate: null,
flags: Placement,
};
return newFiber;
}
react-dom.js
import {scheduleUpdateOnFiber} from "./ReactFiberWorkLoop";
let wipRoot = null;
function render(vnode, container) {
const FiberRoot = {
type: container.nodeName.toLocaleLowerCase(),
stateNode: container,
props: {children: vnode},
};
scheduleUpdateOnFiber(FiberRoot);
}
export default {render};
ReactFiberWorkLoop.js
import {
updateClassComponent,
updateFragementComponent,
updateFunctionComponent,
updateHostComponent,
} from "./ReactFiberReconciler";
import {isFn, isStr, NoFlags, Placement} from "./utils";
let wipRoot = null;
let nextUnitOfWork = null;
export function scheduleUpdateOnFiber(fiber) {
wipRoot = fiber;
wipRoot.sibling = null;
nextUnitOfWork = wipRoot;
}
function performUnitOfWork(wip) {
const {type} = wip;
if (isFn(type)) {
type.prototype.isReactComponent
? updateClassComponent(wip)
: updateFunctionComponent(wip);
} else if (isStr(type)) {
updateHostComponent(wip);
} else {
updateFragementComponent(wip);
}
if (wip.child) {
return wip.child;
}
while (wip) {
if (wip.sibling) {
return wip.sibling;
}
wip = wip.return;
}
return null;
}
function workLoop(IdleDeadline) {
while (nextUnitOfWork && IdleDeadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
if (!nextUnitOfWork && wipRoot) {
commitRoot();
}
}
ReactFiberReconciler.js
requestIdleCallback(workLoop);
// 提交
function commitRoot() {
commitWorker(wipRoot.child);
// wipRoot = null;
}
function commitWorker(wip) {
if (!wip) {
return;
}
const {stateNode, flags} = wip;
const parentNode = getParentNode(wip);
if (stateNode && flags & Placement) {
parentNode.appendChild(stateNode);
}
wip.flags = NoFlags;
commitWorker(wip.child);
commitWorker(wip.sibling);
}
function getParentNode(fiber) {
while (fiber) {
if (fiber.return.stateNode) {
return fiber.return.stateNode;
}
fiber = fiber.return;
}
return null;
}
ReactFiberReconciler.js
import {createFiber} from "./fiber";
import {isArray, isStringOrNumber, updateNode} from "./utils";
export function updateHostComponent(wip) {
if (!wip.stateNode) {
wip.stateNode = document.createElement(wip.type);
updateNode(wip.stateNode, {}, wip.props);
}
reconcileChildren(wip, wip.props.children);
}
export function updateFunctionComponent(wip) {
const {type, props} = wip;
const child = type(props);
reconcileChildren(wip, child);
}
utils.js
export function updateClassComponent(wip) {
const {type, props} = wip;
const instance = new type(props);
const child = instance.render();
reconcileChildren(wip, child);
}
export function updateFragementComponent(wip) {
reconcileChildren(wip, wip.props.children);
}
function reconcileChildren(returnFiber, children) {
if (isStringOrNumber(children)) {
return;
}
const newChildren = isArray(children) ? children : [children];
let previousNewFiber = null;
for (let i = 0; i < newChildren.length; i++) {
const child = newChildren[i];
const newFiber = createFiber(child, returnFiber);
if (previousNewFiber === null) {
returnFiber.child = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
}
utils.js
// ! flags
export const NoFlags = /* */ 0b00000000000000000000;
export const Placement = /* */ 0b0000000000000000000010; // 2
export const Update = /* */ 0b0000000000000000000100; // 4
export const Deletion = /* */ 0b0000000000000000001000; // 8
//
********************************************************************************
***********
export function isFn(fn) {
return typeof fn === "function";
}
export function isStr(s) {
return typeof s === "string";
}
export function isStringOrNumber(s) {
return typeof s === "string" || typeof s === "number";
}