react.js源码二

三、调度Scheduler
scheduling(调度)是fiber reconciliation的一个过程,主要决定应该在何时做什么?在stack reconciler中,reconciliation是“一气呵成”,对于函数来说,这没什么问题,因为我们只想要函数的运行结果,但对于UI来说还需要考虑以下问题:
并不是所有的state更新都需要立即显示出来,比如屏幕之外的部分的更新;
并不是所有的更新优先级都是一样的,比如用户输入的响应优先级要比通过请求填充内容的响应优先级更高;
理想情况下,对于某些高优先级的操作,应该是可以打断低优先级的操作执行的,比如用户输入时,页面的某个评论还在reconciliation,应该优先响应用户输入。比如18版本里提示一些不安全的生命周期主要时它被打断了可能会被执行多次。
所以理想状况下reconciliation的过程应该是每次只做一个很小的任务,做完后能够“喘口气儿”,回到主线程看下有没有什么更高优先级的任务需要处理,如果有则先处理更高优先级的任务,没有则继续执行(cooperative scheduling 合作式调度)。
当用户操作时,调用setState,react会把当前的更新送入对应组件对应的update queue中。但是react并不会立即执行对比并修改DOM的操作。而是交给scheduler去处理。
scheduler会根据当前主线程的使用情况去处理这次update。为了实现这种特性,最开始考虑使用了requestIdelCallback API

总的来讲,通常,客户端线程执行任务时会以帧的形式划分,大部分设备控制在30-60帧是不会影响用户体验;在两个执行帧之间,主线程通常会有一小段空闲时间,requestIdleCallback可以在这个空闲期(Idle Period)调用空闲期回调(Idle Callback),执行一些任务。
在这里插入图片描述
低优先级任务由requestIdleCallback处理;
高优先级任务,如动画相关的由requestAnimationFrame处理;
requestIdleCallback可以在多个空闲期调用空闲期回调,执行任务;
requestIdleCallback方法提供deadline,即任务执行限制时间,以切分任务,避免长时间执行,阻塞UI渲染而导致掉帧;
但是由于requestIdleCallback有以下两个问题就采用了messageChannel模拟实现了requestIdleCallback。
1)兼容性;
2)50ms 渲染问题;(可能在一些任务很长时这个回调不会执行)
|— task queue —|— micro task —|— raf —|— render —|— requestIdleCallback – -|
requestIdleCallback是宏任务,messageChannel也宏任务。
为什么没有⽤ generator ?因为它是有状态的,无法从中间中断。
为什么没有⽤ setTimeout ?因为setTimeout有4-5ms的延时。
模拟了requestIdleCallback行为:

/*** schedule —> 把我的任务放进一个队列里,然后以某一种节奏进行执行;* */// task 的任务队列
const queue = [];
const threshold = 1000 / 60;const transtions = [];
let deadline = 0;// 获取当前时间, bi  date-now 精确
const now = () => performance.now(); // 时间 ,精确
// 从任务queue中,选择第一个 任务 
const peek = arr => arr.length === 0 ? null : arr[0];// schedule —> 把我的任务放进一个队列里,然后以某一种节奏进行执行;
export function schedule (cb) {queue.push(cb);startTranstion(flush);
}// 此时,是否应该交出执行权
function shouldYield() {return navigator.scheduling.isInputPending() || now() >= deadline;
}// 执行权的切换
function startTranstion(cb) {transtions.push(cb) && postMessage();
}// 执行权的切换
const postMessage = (() => {const cb = () => transtions.splice(0, 1).forEach(c => c());const { port1, port2 } = new MessageChannel();port1.onmessage = cb;return () => port2.postMessage(null);
})()// 模拟实现 requestIdleCallback 方法
function flush() {// 生成时间,用于判断deadline = now() + threshold;let task = peek(queue);// 我还没有超出 16.666ms 同时,也没有更高的优先级打断我while(task && !shouldYield()) {const { cb } = task;const next = cb();// 相当于有一个约定,如果,你这个task 返回的是一个函数,那下一次,就从你这里接着跑// 那如果 task 返回的不是函数,说明已经跑完了。不需要再从你这里跑了if(next && typeof next === "function") {task.cb = next;} else {queue.shift()}task = peek(queue);}// 如果我的这一个时间片,执行完了,到了这里。task && startTranstion(flush)
}

一旦reconciliation过程得到时间片,就开始进入work loop。work loop机制可以让react在计算状态和等待状态之间进行切换。为了达到这个目的,对于每个loop而言,需要追踪两个东西:下一个工作单元(下一个待处理的fiber);当前还能占用主线程的时间。第一个loop,下一个待处理单元为根节点。
每个工作单元(fiber)执行完成后,都会查看是否还继续拥有主线程时间片,如果有继续下一个,如果没有则先处理其他高优先级事务,等主线程空闲下来继续执行
react17版本有时间切片ric,但是没有使用。18版本里才使用了。
宏任务微任务执行示例

四、diff算法
react diff算法最好时是O(n), 最差的话,是 O(mn),而传统的diff算法是O(n^3)。
react 是如何将 diff 算法的复杂度降下来的?
其实就是在算法复杂度、虚拟 dom 渲染机制、性能中找了⼀个平衡,react 采⽤了启发式的算法,做了如下最优假设:
a. 如果节点类型相同,那么以该节点为根节点的 tree 结构,⼤概率是相同的,所以如果类型不同,可以直接「删除」原节点,「插⼊」新节点;
b. 跨层级移动⼦ tree 结构的情况⽐较少⻅,或者可以培养⽤户使⽤习惯来规避这种情况,遇到这种情况同样是采⽤先「删除」再「插⼊」的⽅式,这样就避免了跨层级移动
c. 同⼀层级的⼦元素,可以通过 key 来缓存实例,然后根据算法采取「插⼊」「删除」「移动」的操作,尽量复⽤,减少性能开销
d. 完全相同的节点,其虚拟 dom 也是完全⼀致的;

react为什么不去优化diff算法?
因为新版本下,diff算法不是约束性能瓶颈的问题了。

为什么要有key?
在⽐较时,会以 key 和 type 是否相同进⾏⽐较,如果相同,则直接复制

vue diff算法和react diff算法相同/不同点:
共同点:
vue和diff算法,都是不进行跨层级比较,只做同级比较
不同点:
1.vue进行diff时,调用patch打补丁函数,一边比较一边给真实的dom打补丁,vue对比节点时,当节点元素类型相同,类名不同时,认为是不同的元素,删除重新创建,而react认为是同类型的节点,进行修改操作
2.vue列表对比的时候,采用从两端到中间的方式,旧集合和新集合两端各存在两个指针,两两进行比较,每次对比结束后,指针向队列中间移动;react则是从左往右一次对比,利用元素的index和lastindex进行比较
3.当一个集合把最后一个节点移动到最前面,react会把前面的节点依次向后移动,而Vue只会把最后一个节点放在最前面,这样的操作来看,Vue的diff性能是高于react的。

四、模拟实现react流程
react.js


const normalize = (children = []) => children.map(child => typeof child === 'string' ? createVText(child): child)export const NODE_FLAG = {EL: 1, // 元素 elementTEXT: 1 << 1
};
// El & TEXT  = 0const createVText = (text) => {return {type: "",props: {nodeValue: text + ""},$$: { flag: NODE_FLAG.TEXT }}
}const createVNode = (type, props, key, $$) => {return {type, props,key,$$,}
}export const createElement = (type, props, ...kids) => {props = props || {};let key = props.key || void 0;kids = normalize(props.children || kids);if(kids.length) props.children = kids.length === 1? kids[0] : kids;// 定义一下内部的属性const $$ = {};$$.staticNode = null;$$.flag = type === "" ? NODE_FLAG.TEXT: NODE_FLAG.EL;return createVNode(type, props, key, $$)
}

path.js

import { mount } from "./mount";
import { diff } from './diff';function patchChildren(prev, next, parent) {// diff 整个的逻辑还是耗性能的,所以,我们可以先提前做一些处理。if(!prev) {if(!next) {// nothing} else {next = Array.isArray(next) ? next : [next];for(const c of next) {mount(c, parent);}}} else if (prev && !Array.isArray(prev)) {// 只有一个 childrenif(!next) parent.removeChild(prev.staticNode);else if(next && !Array.isArray(next)) {patch(prev, next, parent)} else {// 如果prev 只有一个节点,next 有多个节点parent.removeChild(prev.staticNode);for(const c of next) {mount(c, parent);}}} else diff(prev, next, parent);
}export function patch (prev, next, parent) {// type: 'div' -> 'ul'if(prev.type !== next.type) {parent.removeChild(prev.staticNode);mount(next, parent);return;}// type 一样,diff props // 先不看 children const { props: { children: prevChildren, ...prevProps}} = prev;const { props: { children: nextChildren, ...nextProps}} = next;// patch Porpsconst staticNode = (next.staticNode = prev.staticNode);for(let key of Object.keys(nextProps)) {let prev = prevProps[key],next = nextProps[key]patchProps(key, prev, next, staticNode)}for(let key of Object.keys(prevProps)) {if(!nextProps.hasOwnProperty(key)) patchProps(key, prevProps[key], null, staticNode);}// patch Children !!!patchChildren(prevChildren,nextChildren,staticNode)}export function patchProps(key, prev, next, staticNode) {// style if(key === "style") {// margin: 0 padding: 10if(next) {for(let k in next) {staticNode.style[k] = next[k];}}if(prev) {// margin: 10; color: redfor(let k in prev) {if(!next.hasOwnProperty(k)) {// style 的属性,如果新的没有,老的有,那么老的要删掉。staticNode.style[k] = "";}}}}else if(key === "className") {if(!staticNode.classList.contains(next)) {staticNode.classList.add(next);}}// eventselse if(key[0] === "o" && key[1] === 'n') {prev && staticNode.removeEventListener(key.slice(2).toLowerCase(), prev);next && staticNode.addEventListener(key.slice(2).toLowerCase(), next);} else if (/\[A-Z]|^(?:value|checked|selected|muted)$/.test(key)) {staticNode[key] = next} else {staticNode.setAttribute && staticNode.setAttribute(key, next);}
}

mount.js

import { patchProps } from "./patch";
import { NODE_FLAG } from "./react";export function mount(vnode, parent, refNode) {// 为什么会有一个 refNode?/**                   |* 假如: ul ->  li  li  li(refNode) */if(!parent) throw new Error('no container');const $$ = vnode.$$;if($$.flag & NODE_FLAG.TEXT) {// 如果是一个文本节点const el = document.createTextNode(vnode.props.nodeValue);vnode.staticNode = el;parent.appendChild(el);} else if($$.flag & NODE_FLAG.EL) {// 如果是一个元素节点的情况,先不考虑是一个组件的情况;const { type, props } = vnode;const staticNode = document.createElement(type);vnode.staticNode = staticNode;// 我们再来处理,children 和后面的内容const { children, ...rest} = props;if(Object.keys(rest).length) {for(let key of Object.keys(rest)) {// 属性对比的函数patchProps(key, null, rest[key], staticNode);}}if(children) {// 递归处理子节点const __children = Array.isArray(children) ? children : [children];for(let child of __children) {mount(child, staticNode);}}refNode ? parent.insertBefore(staticNode, refNode) : parent.appendChild(staticNode);}}

diff.js

import { mount } from './mount.js'
import { patch } from './patch.js'export const diff = (prev, next, parent) => {let prevMap = {}let nextMap = {}// 遍历我的老的 childrenfor (let i = 0; i < prev.length; i++) {let { key = i + '' } = prev[i]prevMap[key] = i}let lastIndex = 0// 遍历我的新的 childrenfor (let n = 0; n < next.length; n++) {let { key = n + '' } = next[n]// 老的节点let j = prevMap[key]// 新的 childlet nextChild = next[n]nextMap[key] = n// 老的children      新的children// [b, a]           [c, d, a]  =>  [c, b, a]  --> c// [b, a]           [c, d, a]  =>  [c, d, b, a]  --> dif (j == null) {// 从老的里面,没有找到。新插入let refNode = n === 0 ? prev[0].staticNode : next[n - 1].staticNode.nextSiblingmount(nextChild, parent, refNode)}else {// [b, a]           [c, d, a]  =>  [c, d, a, b]  --> a// 如果找到了,我 patch patch(prev[j], nextChild, parent)if (j < lastIndex) {// 上一个节点的下一个节点的前面,执行插入let refNode = next[n - 1].staticNode.nextSibling;parent.insertBefore(nextChild.staticNode, refNode)}else {lastIndex = j}}}// [b, a]           [c, d, a]  =>  [c, d, a]  --> bfor (let i = 0; i < prev.length; i++) {let { key = '' + i } = prev[i]if (!nextMap.hasOwnProperty(key)) parent.removeChild(prev[i].staticNode)}
}

render.js

import { mount } from "./mount";
import { patch } from "./patch";// step 1
// setTimeout(() => render(vnode, document.getElementById("app")))// step 2
// setTimeout(() => render(null, document.getElementById("app")),5000)export function render(vnode, parent) {let prev = parent.__vnode;if(!prev) {mount(vnode, parent);parent.__vnode = vnode;} else {if(vnode) {// 新旧两个patch(prev, vnode, parent);parent.__vnode = vnode;} else {parent.removeChild(prev.staticNode)}} 
}

index.js

import { render } from "./render";
import { createElement } from "./react";// 用户的开发:
// react / preact / vueconst vnode = createElement("ul",{id: "ul-test",className: "padding-20",style: {padding: "10px",},},createElement("li", { key: "li-0" }, "this is li 01")
);const nextVNode = createElement("ul",{style: {width: "100px",height: "100px",backgroundColor: "green",},},[createElement("li", { key: "li-a" }, "this is li a"),createElement("li", { key: "li-b" }, "this is li b"),createElement("li", { key: "li-c" }, "this is li c"),createElement("li", { key: "li-d" }, "this is li d"),]
);const lastVNode = createElement("ul",{style: {width: "100px",height: "200px",backgroundColor: "pink",},},[createElement("li", { key: "li-a" }, "this is li a"),createElement("li", { key: "li-c" }, "this is li c"),createElement("li", { key: "li-d" }, "this is li d"),createElement("li", { key: "li-f" }, "this is li f"),createElement("li", { key: "li-b" }, "this is li b"),]
);setTimeout(() => render(vnode, document.getElementById("app")))
setTimeout(() => render(nextVNode, document.getElementById("app")),6000)
setTimeout(() => render(lastVNode, document.getElementById("app")),8000)
console.log(nextVNode);

使用rollup进行编译运行:
下载rollup插件,创建rollup.config.js文件

const livereload = require('rollup-plugin-livereload');
const serve = require('rollup-plugin-serve');module.exports = {input: './react/index.js',output: {file: './dist/bundle.js',format: "iife" // es, umd, amd, cjs,iife以script脚本加载执行},plugins: [livereload(),serve({openPage: "/public/index.html",port: 3020,contentBase:'./'})]
}// rollup -c // 我默认去找根目录下的 rollup.config.js    -w 监听文件变化,重新编译。

执行 rollup -c // 我默认去找根目录下的 rollup.config.js -w 监听文件变化,重新编译。
将输入开始的文件,编译打包到output目录下。这样就可以访问对应端口查看页面。

react 冒泡到fiberroot 而不是到root,是因为render函数可能调用多次,会导致错乱。
react为什么实现合成事件,是因为如果写很多监听事件会导致性能下降,还有兼容性问题。

react18的新特性
React 18 中的重大更改仅限于几个简单的 API 更改,以及对 React 中多个行为的稳定性和一致性的一些改进,比较重要的一点是,不再支持 IE 浏览器。
1、客户端渲染 API
带有 createRoot() 的 root API,替换现有的 render() 函数,提供更好的人体工程学并启用新的并发渲染特性。
2、自动批量处理
以下都是批量处理了,以优化性能并避免重渲染。但是之前的版本在settimeout里是会渲染两次的。

const App = () => {const handleClick = () => {setA((a) => a + 1);setB((b) => b - 1);// Updates batched - single re-render};setTimeout(() => {setA((a) => a + 1);setB((b) => b - 1);// New (v18): Updates batched - single re-render}, 1000);// ...
};

3、并发渲染特性,比图startansition等,它是基于任务优先级,时间分片实现的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/212152.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是CDN?用了CDN一定会更快吗?

文章目录 前言CDN是什么?CDN的工作原理为什么要加个CNAME那么麻烦&#xff1f;怎么知道哪个服务器IP里调用方最近&#xff1f; 回源是什么回源是什么&#xff1f;那还有哪些情况会发生回源呢&#xff1f; 怎么判断是否发生回源用了CDN一定比不用的更快吗&#xff1f;什么情况下…

光伏电站全貌

光伏电站 简介 每一篇文章开篇我都会写一个内容简介&#xff0c;一来梳理自己的写作思路&#xff0c;二来方便读者整体了解文章写作意图和脉络。本篇是新能源方面的开篇之作&#xff0c;我选取了介绍光伏电站基础知识&#xff0c;首先我们要了解光伏电站基础分类&#xff0c;然…

Copilot的11个新功能,你不能错过!

我的新书《Android App开发入门与实战》已于2020年8月由人民邮电出版社出版&#xff0c;欢迎购买。点击进入详情 文章目录 1. PowerPoint2. Excel3. One Note4. Word5. 必应聊天现在变为Copilot6. GPT-4为Copilot聊天提供动力7. Microsoft Teams8. Outlook9. Copilot Studio10.…

磁盘存储器

目录 1.1 磁盘存储器1.2 磁盘的性能指标1.3 磁盘存储器(续)1.4 磁盘阵列 \quad \quad \quad 左南右北为0 左北右南为1 \quad \quad 1.1 磁盘存储器 \quad 磁盘的驱动器 \quad 磁盘的控制器 \quad 主机每次对磁盘进行读和写操作都是以扇区为单位的 现在比较流行的是SATA标准 \…

【kafka实践】12|如何实现exactly once

前面的章节中我们聊到如何避免保证消息丢失&#xff0c;没有印象的同学可以再看看&#xff0c;本节我们将展开如何实现kafka的一次精确。 首先我们需要明白两个概念“幂等”和“事物” 幂等 “幂等”这个词原是数学领域中的概念&#xff0c;指的是某些操作或函数能够被执行多…

基于SpringBoot 2+Layui实现的管理后台系统源码+数据库+安装使用说明

springboot-plus 一个基于SpringBoot 2 的管理后台系统,包含了用户管理&#xff0c;组织机构管理&#xff0c;角色管理&#xff0c;功能点管理&#xff0c;菜单管理&#xff0c;权限分配&#xff0c;数据权限分配&#xff0c;代码生成等功能 相比其他开源的后台系统&#xff0…

vue 实现返回顶部功能-指定盒子滚动区域

vue 实现返回顶部功能-指定盒子滚动区域 html代码css代码返回顶部显示/隐藏返回标志 html代码 <a-icontype"vertical-align-top"class"top"name"back-top"click"backTop"v-if"btnFlag"/>css代码 .top {height: 35px;…

令牌桶算法理解学习(限流算法)

令牌桶算法是网络流量整形&#xff08;Traffic Shaping&#xff09;和速率限制&#xff08;Rate Limiting&#xff09;中最常使用的一种算法。典型情况下&#xff0c;令牌桶算法用来控制发送到网络上的数据的数目&#xff0c;并允许突发数据的发送。 用简单的话语来说就是限制…

Linux中的SNAT与DNAT实践

Linux中的SNAT与DNAT实践 1、SNAT的介绍1.1&#xff0c;SNAT概述1.2&#xff0c;SNAT源地址转换过程1.3&#xff0c;SNAT转换 2、DNAT的介绍2.1&#xff0c;DNAT概述2.2&#xff0c;DNAT转换前提条件2.3&#xff0c;DNAT的转换 3、防火墙规则的备份和还原4、tcpdump抓包工具的运…

腾讯再推互动微短剧,游戏的风吹向了短剧

当你看剧时不再拥有上帝视角&#xff0c;处在女主的位置上&#xff0c;你又会做出什么样的选择&#xff1f; 腾讯最新上线的短剧《摩玉玄奇2》在原版之外还推出了互动版&#xff0c;就给出了这样一个新玩法。 《摩玉玄奇2》原版是普通的后宫职场微短剧&#xff0c;互动版则是…

虚拟机VMware安装centos以及配置网络

目录 1、CentOS7的下载2、CentOS7的配置3、CentOS7的安装4、CentOS7的网络配置 4.1、自动获取IP4.2、固定获取IP 5、XShell连接CentO 准备工作&#xff1a;提前下载和安装好VMware。VMware的安装可以参考这一篇文章&#xff1a;VMware15的下载及安装教程。 1、CentOS7的下载 …

从零开始搭建企业管理系统(五):统一响应结果和全局异常处理

统一响应结果和全局异常处理 前言统一响应结果定义响应结构定义响应对象定义响应状态对象统一返回响应对象定义 Controller 拦截类 全局异常处理 前言 做个功能之前我们想一下为什么要做统一响应结果和全局异常处理呢&#xff1f; 这是因为我们的项目采用的是前后端分离开发&am…

LVGL | Demo实例使用说明

LVGL | Demo实例使用说明 时间&#xff1a;2023年12月10日21:51:17 文章目录 LVGL | Demo实例使用说明Demos for LVGLAdd the examples to your projectsDemosWidgetsMusic playerKeypad and encoderBenchmarkStress Contributing Demos for LVGL Add the examples to your p…

基于SSM的酒店管理旅店系统(Java毕业设计)

大家好&#xff0c;我是DeBug&#xff0c;很高兴你能来阅读&#xff01;作为一名热爱编程的程序员&#xff0c;我希望通过这些教学笔记与大家分享我的编程经验和知识。在这里&#xff0c;我将会结合实际项目经验&#xff0c;分享编程技巧、最佳实践以及解决问题的方法。无论你是…

【软件测试】年薪30万跟年薪15万的面试有些什么区别?

1、什么是兼容性测试&#xff1f;兼容性测试侧重哪些方面&#xff1f; 参考答案&#xff1a; 兼容测试主要是检查软件在不同的硬件平台、软件平台上是否可以正常的运行&#xff0c;即是通常说的软件的可移植性。 兼容的类型&#xff0c;如果细分的话&#xff0c;有平台的兼容…

Numpy数组中数据的排序【sort(),argsort()与 lexsort()】 (第13讲)

Numpy数组中数据的排序【sort(),argsort()与 lexsort()】 (第13讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…

【C++ 程序设计入门基础】- 第3节-循环结构02

目录 while 语句 案例 while 循环 输入一个整数 n &#xff0c;输出 1~n 的所有整数。 查看运行结果&#xff1a; while 语句结构解析 do while 语句 案例 do while 循环 输入一个整数n&#xff0c;输出1&#xff5e;n的所有整数。 查看运行结果 while、do while的区别 …

SpringSecurity6 | 登录成功后的JSON处理

✅作者简介&#xff1a;大家好&#xff0c;我是Leo&#xff0c;热爱Java后端开发者&#xff0c;一个想要与大家共同进步的男人&#x1f609;&#x1f609; &#x1f34e;个人主页&#xff1a;Leo的博客 &#x1f49e;当前专栏&#xff1a; Java从入门到精通 ✨特色专栏&#xf…

Android:java.lang.SecurityException: Provider must not be exporte

java.lang.SecurityException: Provider must not be exporte 解决方案 首先在AndroidManifest.xml中添加provider android:authorities&#xff1a; 是用来标识provider的唯一标识&#xff0c;在同一部手机上一个"authority"串只能被一个app使用&#xff0c;冲突的话…

架构LNMP

目录 1.安装Nginx服务 2.安装 MySQL 服务 3.安装配置 PHP 解析环境 4.部署 Discuz&#xff01;社区论坛 Web 应用 1.安装Nginx服务 实验准备 systemctl stop firewalld systemctl disable firewalld setenforce 0 安装依赖包 yum -y install pcre-devel zlib-devel gcc…