这是我参与8月更文挑战的第15天,活动详情查看:8月更文挑战
虚拟dom
什么是虚拟dom
虚拟dom 是一种编程概念,在这个概念里,UI以一种理想化的,或者说‘虚拟的’表现形式被保存于内存中,并通过如ReactDom等类库使之与‘真实的’DOM同步,这一过程叫做协调。
用 JavaScript 对象表示 DOM 信息和结构,当状态变更的时候,重新渲染这个 JavaScript 的对 象结构。这个 JavaScript 对象称为virtual dom
为什么使用虚拟dom
DOM操作很慢,轻微的操作都可能导致页面重新排版,非常耗性能。相对于DOM对象,js对象 处理起来更快,而且更简单。通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行 dom操作,从而提高性能。
jsx
什么是JSX
- 语法糖
- React 使用 JSX 来替代常规的 JavaScript。
- JSX 是一个看起来很像 XML 的 JavaScript 语法扩展。
为什么需要JSX
- 开发效率:使用 JSX 编写模板简单快速。
- 执行效率:JSX编译为 JavaScript 代码后进行了优化,执行更快。
- 类型安全:在编译过程中就能发现错误。
react 16和react 17使用区别
React 16原理:babel-loader会预编译JSX为React.createElement(...)
React 17原理:React 17中的 JSX 转换不会将 JSX 转换为 React.createElement,而是自动从 React 的 package 中引入新的入口函数并调用。另外此次升级不会改变 JSX 语法,旧的 JSX 转换 也将继续工作
fiber
- 处理时间切片
- 链表结构的js对象
- 对象里的一些关键key
fiber 的一些关键key
fiber{
stateNode:当前的dom 节点
tag:在react 中定义的节点值
type:字符串,标记类型,eg:'div'
child:dom 节点里的第一个子节点
sibling:下一个兄弟节点,
}
ReactDOM渲染的使用方法
- jsx :直接渲染,
- 函数组件:直接执行函数,
- 类组件:会实例化一下,然后调用render()函数
判断组件是类组件还是函数组件 Component.prototype.isReactComponent = {}
手动实现reactDom
通过create-react-app 创建一个文件,打开index.js文件
import ReactDOM from './kreact/react-dom';
const jsx=(
<div>
<h1>learn react</h1>
<p>k-react</p>
<a href='https://learn.kaikeba.com/video/411409'>this is a link</a>
<FunctionComponent name='functionComponent' />
<>
<p>Fragments</p>
</>
<App name='react component'/>
</div>
)
ReactDOM.render(
jsx,
document.getElementById('root')
);
创建kreact 文件,创建react-dom js文件。
ReactDOM.render()
ReactDOM.render(element, container[, callback])
当首次调用时,容器节点里的所有 DOM 元素都会被替换,后续的调用则会使用 React 的 DOM 差分 算法(DOM diffing algorithm)进行高效的更新。
如果提供了可选的回调函数,该回调将在组件被渲染或更新之后被执行。
主要实现:返回一个render 函数
import {scheduleUpdateOnFiber} from './ReactFiberWorkloop';
function render(element, container) {
console.log('react-dom', element);
console.log('container', container.nodeName);
// 设置根fiber 节点
const FiberRoot = {
type: container.nodeName.toLowerCase(),
props: {children: element},
stateNode: container,
};
scheduleUpdateOnFiber(FiberRoot);
}
export default {render};
创建一个reactFiberworkloop js 文件 处理节点的操作 react 里是根据Scheduler(调度器),目的是当浏览器有剩余时间时通知我们。 这里我们通过浏览器api ,window.requestIdleCallback(callback[, options]) ,可以返回浏览器的空闲时间来实现
react为何不使用requestIdleCallback
- 浏览器兼容性
- 触发频率不稳定,受很多因素影响。比如当我们的浏览器切换tab后,之前tab注册的
requestIdleCallback触发的频率会变得很低
import {isStr} from './utils';
import {updateHostCompose} from './ReactFiberReconclier';
// 设置节点更新 之后要根据更新好的fiber 节点再去更新dom 节点
let wipRoot = null; // 当前正在工作中的根节点
let nextUnitOfWork = null; // 下一个fiber 节点
export function scheduleUpdateOnFiber(fiber) {
wipRoot = fiber;
nextUnitOfWork = wipRoot; // 从根fiber 开始更新
}
//处理节点更新
function workloop(IdleDeadline) {
while (nextUnitOfWork && IdleDeadline.timeRemaining() > 0) {
nextUnitOfWork = performUnitOfWork(nextUnitOfWork);
}
}
// 当浏览器有空闲时间时,去处理节点的更新
requestIdleCallback(workloop);
// performUnitOfWork目的
// 1 更新wip
//2 返回下一个要更新的任务
function performUnitOfWork(wip) {
const {type} = wip;
if (isStr(type)) {
// 代表是原生标签
updateHostCompose(wip);
}
// 返回下一个要更新的任务,深度优先遍历
if (wip.child) {
return wip.child;
}
// 没有子节点,要找兄弟节点,若没有兄弟节点,再找父的兄弟节点,一直往上找,直到找到树的最顶层节点
let next = wip;
while (next) {
if (next.sibling) {
// sibling 兄弟节点
return next.sibling;
}
next = next.return; // next 的父节点 通过next.return 获得的
}
return null;
}
不同组件的更新方式不同,创建ReactFiberReconclier.js文件
import { isStr,isArray} from "./utils";
import {createFiber} from './createFiber'
// 处理节点上的属性值
export function updateNodeOne(node,nextVal) {
Object.keys(nextVal).forEach(k=>{
if(k === 'children'){
if(isStringOrNumber(nextVal[k])) {
node.textContent = nextVal[k];
}
}else if(k.slice(0,2) === 'on'){
const eventName = k.slice(2).toLocaleLowerCase();
node.addEventListener(eventName, nextVal[k]);
}else{
node[k] = nextVal[k];
}
})
}
// 更新原生标签
export function updateHostCompose(wip){
if(!wip.stateNode){ // 初次渲染,还没有真实的dom
wip.stateNode = document.createElement(wip.type);
updateNodeOne(wip.stateNode,wip.props); //处理节点上的属性值
}
// 协调子节点,就是diff
reconcileChildren(wip,wip.props.children);
}
// 更新函数组件
export function updateFunctionCompose(wip){
// 函数的children 是函数组件调用,返回的值
const {type,props} = wip;
const children = type(props);
// 协调子节点,就是diff
reconcileChildren(wip,children);
updateNodeOne(wip.stateNode,wip.return.props); //处理节点上的属性值
}
// 更新类组件
export function updateClassCompose(wip){
// 函数的children 是函数组件调用,返回的值
const {type,props} = wip;
const instance = new type(props);
const children = instance.render();
// 协调子节点,就是diff
reconcileChildren(wip,children);
// updateNodeOne(wip.stateNode,wip.return.props); //处理节点上的属性值
}
// 更新fragment
export function updateFragmentsCompose(wip){
// 协调子节点,就是diff
console.log('======45',wip)
reconcileChildren(wip,wip.props.children);
}
// 处理子节点
function reconcileChildren(returnFiber,children) {
//returnFiber父节点,children 子节点
let previousNewFiber = null;
if(isStr(children)){
// 若是文本节点
return;
}
const newChildren = isArray(children) ? children :[children];
for(let i =0;i<newChildren.length;i++){
const newChild = newChildren[i];
const newFiber = createFiber(newChild,returnFiber)
if(previousNewFiber === null){
// 上一个节点不存在,说明是初始节点
returnFiber.child = newFiber
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber= newFiber;
}
}
按照fiber结构创建节点
import { Placement } from "./utils";
export function createFiber(vnode,returnFiber){
/**
* fiber:
* type 标记节点类型
* key 标记节点在当前层级下的唯一性
* props 属性
* index 标记当前层级下的位置
* child 第一个子节点
* sibling 下一个兄弟节点
* return 父节点
* stateNode 如果组件是原生标签则是dom节点,如果是类组件则是类实例
*/
const newFiber={
type: vnode.type,
key:vnode.key,
props:vnode.props,
stateNode:null,
child:null,
return:returnFiber,
sibling:null,
alternate:null,
flags:Placement , // 当前fiber 要做什么事情,eg 插入,删除,更新呀
}
return newFiber
}
总结
ReactDOM 实现路径
-
render 函数
-
scheduleUpdateOnFiber :定义节点
-
workloop:浏览器空闲时间对节点进行处理 requestIdleCallback(workloop)
-
performUnitOfWork:更新节点,返回下一个节点,节点分为不同的类型(原生,函数,类组件)
-
updateHostCompose: 更新原生节点
-
createFiber: 改造节点,按照fiber 的格式
-
节点改造完成,没有下一个子节点,并且wipRoot 存在,说明可以提交节点了 ,要进行提交节点
-
commitRoot:提交节点
-
commitWork(wipRoot.child) // 从子节点开始提交,自己提交自己,提交子节点,提交兄弟节点