├── docs ├── .nojekyll ├── favicon.ico ├── README.md ├── interview │ ├── 常用项目配置.md │ ├── _navbar.md │ ├── React进阶实践指南 │ │ ├── 1.写给想要进阶的你.md │ │ ├── 28.总结篇-如何有效阅读源码.md │ │ ├── 35.v18特性篇-订阅外部数据源.md │ │ ├── 15.原理篇-事件原理-v18新版本.md │ │ ├── 13.优化篇-处理海量数据.md │ │ ├── 14.优化篇-细节处理(持续).md │ │ ├── 37.v18特性篇-concurrent下的state更新流程.md │ │ ├── 3.基础篇-起源Component.md │ │ ├── 16.原理篇-调度与时间片.md │ │ ├── 22.实践篇-实现mini-Router.md │ │ └── 25.实践篇-自定义弹窗.md │ ├── 前沿技术 │ │ ├── 现代前端工程Monorepo.md │ │ ├── pnpm对npm和yarn优势.md │ │ ├── swc、esbuild和vite前端构建工具浅析.md │ │ ├── 大前端技术.md │ │ ├── 实现一个埋点监控SDK.md │ │ └── hybrid简单了解.md │ ├── _sidebar.md │ ├── 性能优化.md │ ├── sourcemap.md │ ├── webgl.md │ └── excel导出优化.md ├── _navbar.md ├── sidebar.css ├── 生成目录.js ├── sw.js ├── _sidebar.md ├── index.html └── sidebar.js ├── server.sh └── deploy.sh /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server.sh: -------------------------------------------------------------------------------- 1 | echo 'start serve' 2 | docsify serve docs 3 | echo 'success' 4 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kiraraty/fe-doc/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | echo '--------upload files start--------' 2 | 3 | git add . 4 | 5 | git status 6 | 7 | git commit -m 'auto update' 8 | 9 | echo '--------commit successfully--------' 10 | 11 | git push 12 | 13 | echo '--------push to GitHub successfully--------' 14 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | > 前端学习笔记 4 | 主要结合掘金和网上的知识进行汇总的笔记 5 | github:https://github.com/kiraraty/fe-doc 6 | 欢迎star 7 | 8 | # 安装docsify 9 | 10 | ``` 11 | npm install -g docsify-cli 12 | ``` 13 | 14 | # 启动本地服务 15 | ``` 16 | docsify serve docs 17 | ``` -------------------------------------------------------------------------------- /docs/interview/常用项目配置.md: -------------------------------------------------------------------------------- 1 | ## Vue 2 | 3 | [Vue3](https://cn.vuejs.org/guide/quick-start.html) 4 | 5 | ## React 6 | 7 | [React](https://react.docschina.org/docs/getting-started.html) 8 | 9 | ## Typescript 10 | 11 | [Typescript](https://www.typescriptlang.org/zh/) 12 | 13 | ## Webpack 14 | 15 | [Webpack](https://www.webpackjs.com/concepts/) 16 | 17 | ## Vite 18 | 19 | [Vite](https://cn.vitejs.dev/guide/) 20 | 21 | ## Babel 22 | 23 | 24 | 25 | ## Jest 26 | 27 | [Jest](https://www.jestjs.cn/docs/getting-started) 28 | 29 | ## ESlint 30 | 31 | [ESlint](https://eslint.org/docs/latest/user-guide/getting-started) 32 | 33 | ## Prettier 34 | 35 | [Prettier](https://www.prettier.cn/) -------------------------------------------------------------------------------- /docs/_navbar.md: -------------------------------------------------------------------------------- 1 | - 基础 2 | - [Html](interview/html.md) 3 | - [Css](interview/css.md) 4 | - [JavaScript基础](interview/javascript基础.md) 5 | - [JavaScript进阶](interview/javascript进阶.md) 6 | - [TypeScript](interview/typescript.md) 7 | - [Vue.js](interview/vue.md) 8 | - [React.js](interview/react.md) 9 | - 进阶 10 | - [Webpack](interview/webpack.md) 11 | - [Webpack5](interview/webpack5.md) 12 | - [Node.js](interview/node.js.md) 13 | - [Axios](interview/axios.md) 14 | - [浏览器原理](interview/浏览器.md) 15 | - 综合 16 | - [计算机网络](interview/网络.md) 17 | - [数据结构与算法](interview/数据结构与算法基础.md) 18 | - [操作系统原理](interview/操作系统.md) 19 | - [数据库原理](interview/数据库原理.md) 20 | - [计算机基础](interview/常见计算机基础.md) 21 | - [性能优化](interview/性能优化.md) 22 | - [设计模式](interview/设计模式.md) 23 | - [场景题](interview/场景题.md) 24 | - 代码 25 | - [代码题](interview/代码题.md) 26 | - [算法题](interview/算法题.md) 27 | - [输出题](interview/输出题.md) 28 | 29 | -------------------------------------------------------------------------------- /docs/interview/_navbar.md: -------------------------------------------------------------------------------- 1 | - 基础 2 | - [Html](interview/html.md) 3 | - [Css](interview/css.md) 4 | - [JavaScript基础](interview/javascript基础.md) 5 | - [JavaScript进阶](interview/javascript进阶.md) 6 | - [TypeScript](interview/typescript.md) 7 | - [Vue.js](interview/vue.md) 8 | - [React.js](interview/react.md) 9 | - 进阶 10 | - [Webpack](interview/webpack.md) 11 | - [Webpack5](interview/webpack5.md) 12 | - [Node.js](interview/node.js.md) 13 | - [Axios](interview/axios.md) 14 | - [浏览器原理](interview/浏览器.md) 15 | - 综合 16 | - [计算机网络](interview/网络.md) 17 | - [数据结构与算法](interview/数据结构与算法基础.md) 18 | - [操作系统原理](interview/操作系统.md) 19 | - [数据库原理](interview/数据库原理.md) 20 | - [计算机基础](interview/常见计算机基础.md) 21 | - [性能优化](interview/性能优化.md) 22 | - [设计模式](interview/设计模式.md) 23 | - [场景题](interview/场景题.md) 24 | - 代码 25 | - [代码题](interview/代码题.md) 26 | - [算法题](interview/算法题.md) 27 | - [输出题](interview/输出题.md) 28 | 29 | -------------------------------------------------------------------------------- /docs/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar-nav>ul { 2 | margin-left: 10px 3 | } 4 | 5 | .sidebar-nav ul:not(.app-sub-sidebar)>li { 6 | position: relative; 7 | margin: 0; 8 | cursor: pointer; 9 | margin-left: 2px 10 | } 11 | 12 | .sidebar-nav ul:not(.app-sub-sidebar)>li::before { 13 | content: ''; 14 | display: block; 15 | position: absolute; 16 | top: 8px; 17 | left: -16px; 18 | height: 10px; 19 | width: 10px 20 | } 21 | 22 | .sidebar-nav ul:not(.app-sub-sidebar)>li.folder::before { 23 | background: center/contain no-repeat url(data:image/svg+xml;base64,PHN2ZyB0PSIxNTk4NTQ1MTE2ODQ2IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjIyNTciCiAgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPgogIDxwYXRoIGQ9Ik00NDggMTI4aDUxMmE2NCA2NCAwIDAgMSA2NCA2NHYzMjBhNjQgNjQgMCAwIDEtNjQgNjRINDQ4YTY0IDY0IDAgMCAxLTY0LTY0VjE5MmE2NCA2NCAwIDAgMSA2NC02NHoiCiAgICBmaWxsPSIjNzliOGZmIiBwLWlkPSIyMjU4Ij48L3BhdGg+CiAgPHBhdGgKICAgIGQ9Ik02NCA2NGg0MDguNDQ4YTY0IDY0IDAgMCAxIDU3LjIxNiAzNS4zOTJsOTIuNjcyIDE4NS4yMTZhNjQgNjQgMCAwIDAgNTcuMjE2IDM1LjM5Mkg5NjBhNjQgNjQgMCAwIDEgNjQgNjR2NTEyYTY0IDY0IDAgMCAxLTY0IDY0SDY0YTY0IDY0IDAgMCAxLTY0LTY0VjEyOGE2NCA2NCAwIDAgMSA2NC02NHoiCiAgICBmaWxsPSIjNzliOGZmIiBwLWlkPSIyMjU5Ij48L3BhdGg+Cjwvc3ZnPgo=) 24 | } 25 | 26 | .sidebar-nav ul:not(.app-sub-sidebar)>li.file::before { 27 | background: center/contain no-repeat url(data:image/svg+xml;base64,PHN2ZyB0PSIxNTk4NTQ1NjIzOTMxIiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjgxNzMiCiAgd2lkdGg9IjIwMCIgaGVpZ2h0PSIyMDAiPgogIDxwYXRoCiAgICBkPSJNODEyLjggMjY4LjhsLTE4NS42LTE4NS42Yy0xMi44LTEyLjgtMjguOC0xOS4yLTQ0LjgtMTkuMkgyNTZjLTM1LjIgMC02NCAyOC44LTY0IDY0djc2OGMwIDM1LjIgMjguOCA2NCA2NCA2NGg1MTJjMzUuMiAwIDY0LTI4LjggNjQtNjRWMzEzLjZjMC0xNi02LjQtMzItMTkuMi00NC44eiBtLTcwLjQgMTkuMkg2MDhWMTUzLjZMNzQyLjQgMjg4ek03MDQgODk2SDI1NlYxMjhoMjg4djE2MGMwIDM1LjIgMjguOCA2NCA2NCA2NGgxNjB2NTQ0aC02NHoiCiAgICBwLWlkPSI4MTc0Ij48L3BhdGg+Cjwvc3ZnPgo=) 28 | } -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/1.写给想要进阶的你.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # React 进阶实践指南 - 我不是外星人 - 掘金课程 4 | 5 | --- 6 | React 是当前非常流行的**用于构建用户界面的 JavaScript 库**,也是目前最受欢迎的 Web 界面开发工具之一。 7 | 8 | 这主要是得益于它精妙的设计思想,以及多年的更新迭代沉淀而来的经验。 9 | 10 | **首先,React 的出现让创建交互式 UI 变得轻而易举。** 它不仅可以为应用的每一个状态设计出简洁的视图。而且,**当数据变动时,React 还能高效更新并渲染合适的组件**。 11 | 12 | 这是因为,在 React 的世界中,函数和类就是 UI 的载体。我们甚至可以理解为,将数据传入 React 的类和函数中,返回的就是 UI 界面。 13 | 14 | 同时,这种灵活性使得开发者在开发 React 应用的时候,更注重逻辑的处理,所以在 React 中,可以运用多种设计模式,更有效地培养编程能力。 15 | 16 | ![image-20220909193854870](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/image-20220909193854870.png) 17 | 18 | **其次,React 把组件化的思想发挥得淋漓尽致。** 在 React 应用中,一切皆组件,每个组件像机器零件一样,开发者把每一个组件组合在一起,将 React 应用运转起来。 19 | 20 | **最后,React 还具有跨平台能力。** React 支持 Node 进行服务器渲染,还可以用 React Native 进行原生移动应用的开发,随着跨平台构建工具的兴起,比如 Taro,开发者可以写一套 React 代码,适用于多个平台。 21 | 22 | 因此,学好 React,能增强我们自身的职业竞争力。 23 | 24 | ## 跟着小册学习React 25 | 26 | 想要系统学习 React,我建议你跟着小册学。这里,我准备了几个学习中的常见问题,我就结合它们来说说小册优势。 27 | 28 | **① “看会”等于“学会”吗?** 29 | 30 | 我认为**看会不等于学会的。俗话说“好记性不如烂笔头”,前端开发者学习重心还是要放到 coding 上来。** 31 | 32 | 因此,《React进阶实践指南》这本小册,在讲解 React api 高阶用法,和一些核心模块原理的同时,也会列举出很多实践 Demo 去强化知识点。那么,小册的最佳学习方式就是:读者可以结合小册每一章节中的知识点,去亲自体验每一个高阶玩法,亲自尝试实现每一个 Demo。 33 | 34 | **② 有必要掌握小册中的源码吗?** 35 | 36 | 这本小册有很多**原理源码**,我们是否有必要花费大量时间去研究它们呢?这也是很多人在学习 React 的时候比较关心的问题。我想,虽然我们没必要纠结源码中的一些细枝末节,但还是有必要掌握一些核心原理的(**可以不看源码,但需要掌握原理**)。原因有两点: 37 | 38 | 第一,现在前端圈子内卷严重,面试官在面试中为了对比候选人,就会问一些原理/源码层面上的问题。因此,如果应聘者不懂原理/源码,就会很吃亏。 39 | 40 | 比如应聘者在简历上写了用过 mobx 和 redux,那么面试官就很可能会问两者区别。如果这个时候应聘者的答案只是停留在两者使用层面上的区别,肯定是很难让人满意。 41 | 42 | 第二,**更深的理解方可更好的使用**。开发者对框架原理的深入理解可以让其在工作中,更容易发现问题、定位问题、解决问题。就算是面对一些复杂困难的技术场景,也能提供出合理的解决方案。 43 | 44 | **③ 一定要按顺序学习吗?跳着看可以吗?** 45 | 46 | 本小册的难度是由浅入深的,内容是承上启下的。所以我希望每一个读者能够按照章节顺序阅读,不要跳跃式阅读。 47 | 48 | ## React里程碑 49 | 50 | 在正式学习 React 之前,首先看一下 React 发展史中一些重要的里程碑(从 `React16` 开始),《React进阶实践指南》这本小册中,会围绕这些里程碑中的内容展开讨论。 51 | 52 | - **`v16.0`**: 为了**解决之前大型 React 应用一次更新遍历大量虚拟 DOM 带来个卡顿问题**,React **重写了核心模块 Reconciler ,启用了 Fiber 架构**;为了在让节点渲染到指定容器内,更好的实现弹窗功能,推出 createPortal API;为了捕获渲染中的异常,引入 componentDidCatch 钩子,划分了错误边界。 53 | 54 | - **`v16.2`**:推出 Fragment ,解决数组元素问题。 55 | 56 | - **`v16.3`**:增加 React.createRef() API,可以通过 React.createRef 取得 Ref 对象。增加 React.forwardRef() API,解决高阶组件 ref 传递问题;推出新版本 context api,迎接Provider / Consumer 时代;增加 getDerivedStateFromProps 和 getSnapshotBeforeUpdate 生命周期 。 57 | 58 | - **`v16.6`**:增加 React.memo() API,用于控制子组件渲染;增加 React.lazy() API 实现代码分割;增加 contextType 让类组件更便捷的使用context;增加生命周期 getDerivedStateFromError 代替 componentDidCatch 。 59 | 60 | - **`v16.8`**:全新 React-Hooks 支持,使函数组件也能做类组件的一切事情。 61 | 62 | - **`v17`**: 事件绑定由 document 变成 container ,移除事件池等。 63 | 64 | 65 | ## 阅读前的声明 66 | 67 | - 本小册涉及的所有 React 源码版本均为 `v16.13.1` ,为了用最精炼的内容把事情讲明白,本小册涉及的源码均为精简后的,会和真正的源码有出入,敬请谅解。 68 | 69 | - 本小册各个章节是承上启下的,所以请按照目录,渐进式阅读。 70 | 71 | - 所有的实践 Demo 项目,笔者已经整理到 GitHub上,地址为 [《React进阶实践指南》——Demo 项目和代码片段](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2FGoodLuckAlien%2FReact-Advanced-Guide-Pro "https://github.com/GoodLuckAlien/React-Advanced-Guide-Pro"),持续更新中。 72 | -------------------------------------------------------------------------------- /docs/生成目录.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | var ans = [] 4 | function readDir(pathUrl, pathName) { 5 | fs.readdir(pathUrl, (err, fileName) => { 6 | if (err) { 7 | console.log('文件夹读取错误', err) 8 | } else { 9 | for (let i = 0; i < fileName.length; i++) { 10 | let filename = fileName[i].toString().replace(/\s*/g, ""); 11 | if (filename.indexOf('.')>5) { 12 | filename = filename.replace(/^[0-9]+/, function (match, pos, orginText) { 13 | return match + '.' 14 | }) 15 | } 16 | let name = filename.split('.md')[0] 17 | let oldPath = pathUrl + '/' + fileName[i]; 18 | let newPath = pathUrl + '/' + filename; 19 | fs.renameSync(oldPath, newPath, function (err) { 20 | if (!err) { 21 | 22 | } 23 | }) 24 | // let str = `* [${name}](interview/${pathName}/${fileName[i]}) `; 25 | let str = `"/${pathName}/${fileName[i]}",` 26 | ans.push(str) 27 | } 28 | } 29 | /* ans.sort((b, a) => { 30 | let x = +a.split('[')[1].split('.')[0] 31 | let y = +b.split('[')[1].split('.')[0] 32 | return y - x 33 | }) */ 34 | for (const item of ans) { 35 | console.log(`${item}`); 36 | } 37 | }); 38 | 39 | }; 40 | readDir('./interview','interview'); 41 | /* const fs = require("fs"); 42 | let path = '路径' 43 | fs.readdir(path, function (err, files) { 44 | files.forEach(function (filename, index) { 45 | let oldPath = path + '/' + filename; 46 | let newPath = oldPath.replace(/\Y[0-9]*\_/g, 'Y1_') 47 | console.log(oldPath, '----------', newPath); 48 | fs.rename(oldPath, newPath, function (err) { 49 | if (!err) { 50 | console.log(filename + '修改完成!') 51 | } 52 | }) 53 | }) 54 | }) */ 55 | 56 | /* //Dijkstra算法 57 | let graph = [ 58 | [0, 2, 4, 0, 0, 0], 59 | [0, 0, 2, 4, 2, 0], 60 | [0, 0, 0, 0, 3, 0], 61 | [0, 0, 0, 0, 0, 2], 62 | [0, 0, 0, 3, 0, 2], 63 | [0, 0, 0, 0, 0, 0] 64 | ]; 65 | 66 | let INF = Infinity 67 | 68 | function minDistance(dist, visited) { 69 | let min = INF 70 | let minIndex = -1 71 | for (let i = 0; i < dist.length; i++) { 72 | if (!visited[i] && dist[i] < min) { 73 | minIndex = i 74 | min = dist[i] 75 | } 76 | } 77 | return minIndex 78 | }; 79 | 80 | function dijkstra(src) { 81 | let dist = [] 82 | let visited = [] 83 | let length = graph.length 84 | for (let i = 0; i < length; i++) { / 85 | dist[i] = INF 86 | visited[i] = false 87 | } 88 | dist[src] = 0 // (2) 89 | for (let j = 0; j < length - 1; j++) { 90 | let u = minDistance(dist, visited) 91 | visited[u] = true 92 | for (let v = 0; v < length; v++) { 93 | if ( 94 | !visited[v] && 95 | dist[u] != INF && 96 | graph[u][v] != 0 && 97 | dist[u] + graph[u][v] < dist[v] 98 | ) { 99 | dist[v] = dist[u] + graph[u][v] 100 | } 101 | } 102 | } 103 | return dist 104 | }; 105 | 106 | // 求B点到其他个点的最短距离 107 | const res = dijkstra(1); 108 | // Infinity表示点B与点A不相邻,从上图可知A与B相邻,而B与A不相邻 109 | // [ Infinity, 0, 2, 4, 2, 4 ] */ -------------------------------------------------------------------------------- /docs/sw.js: -------------------------------------------------------------------------------- 1 | /* =========================================================== 2 | * docsify sw.js 3 | * =========================================================== 4 | * Copyright 2016 @huxpro 5 | * Licensed under Apache 2.0 6 | * Register service worker. 7 | * ========================================================== */ 8 | 9 | const RUNTIME = 'docsify' 10 | const HOSTNAME_WHITELIST = [ 11 | self.location.hostname, 12 | 'fonts.gstatic.com', 13 | 'fonts.googleapis.com', 14 | 'cdn.jsdelivr.net' 15 | ] 16 | 17 | // The Util Function to hack URLs of intercepted requests 18 | const getFixedUrl = (req) => { 19 | var now = Date.now() 20 | var url = new URL(req.url) 21 | 22 | // 1. fixed http URL 23 | // Just keep syncing with location.protocol 24 | // fetch(httpURL) belongs to active mixed content. 25 | // And fetch(httpRequest) is not supported yet. 26 | url.protocol = self.location.protocol 27 | 28 | // 2. add query for caching-busting. 29 | // Github Pages served with Cache-Control: max-age=600 30 | // max-age on mutable content is error-prone, with SW life of bugs can even extend. 31 | // Until cache mode of Fetch API landed, we have to workaround cache-busting with query string. 32 | // Cache-Control-Bug: https://bugs.chromium.org/p/chromium/issues/detail?id=453190 33 | if (url.hostname === self.location.hostname) { 34 | url.search += (url.search ? '&' : '?') + 'cache-bust=' + now 35 | } 36 | return url.href 37 | } 38 | 39 | /** 40 | * @Lifecycle Activate 41 | * New one activated when old isnt being used. 42 | * 43 | * waitUntil(): activating ====> activated 44 | */ 45 | self.addEventListener('activate', event => { 46 | event.waitUntil(self.clients.claim()) 47 | }) 48 | 49 | /** 50 | * @Functional Fetch 51 | * All network requests are being intercepted here. 52 | * 53 | * void respondWith(Promise r) 54 | */ 55 | self.addEventListener('fetch', event => { 56 | // Skip some of cross-origin requests, like those for Google Analytics. 57 | if (HOSTNAME_WHITELIST.indexOf(new URL(event.request.url).hostname) > -1) { 58 | // Stale-while-revalidate 59 | // similar to HTTP's stale-while-revalidate: https://www.mnot.net/blog/2007/12/12/stale 60 | // Upgrade from Jake's to Surma's: https://gist.github.com/surma/eb441223daaedf880801ad80006389f1 61 | const cached = caches.match(event.request) 62 | const fixedUrl = getFixedUrl(event.request) 63 | const fetched = fetch(fixedUrl, { cache: 'no-store' }) 64 | const fetchedCopy = fetched.then(resp => resp.clone()) 65 | 66 | // Call respondWith() with whatever we get first. 67 | // If the fetch fails (e.g disconnected), wait for the cache. 68 | // If there’s nothing in cache, wait for the fetch. 69 | // If neither yields a response, return offline pages. 70 | event.respondWith( 71 | Promise.race([fetched.catch(_ => cached), cached]) 72 | .then(resp => resp || fetched) 73 | .catch(_ => { /* eat any errors */ }) 74 | ) 75 | 76 | // Update the cache with the version we fetched (only for ok status) 77 | event.waitUntil( 78 | Promise.all([fetchedCopy, caches.open(RUNTIME)]) 79 | .then(([response, cache]) => response.ok && cache.put(event.request, response)) 80 | .catch(_ => { /* eat any errors */ }) 81 | ) 82 | } 83 | }) -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/28.总结篇-如何有效阅读源码.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | > 一流工程师天生充满了责任感和好奇心,他们大都满怀信心但却虚怀若谷,他们直接但不粗鲁,他们不推诿,他们不在乎工作边界,以团队任务而不是自己的工作任务为模板。我不止一次领教过一流工程师的威力,他们不止能把事情做对,还能把事情做好。他们能在完成开发的同时还能把团队不必要的沟通、返工和流程成本降到最低,更能防患于未然,把各种凶险消弭于无形。 --《浪潮之巅》:吴军博士讲述硅谷IT发展史 4 | 5 | 在目前读者的反馈中,说到小册里面的源码部分看起来比较头疼,很难理解。实际我在小册中放了一些源码主要为了让大家了解 React 中一些核心细节和主要流程。只有知道这些,才能更熟练的运用 React ,才能更好地解决工作中一些复杂的场景问题。接下来我把我阅读源码的心得分享给大家,希望能对大家有帮助。 6 | 7 | ## 如何高效阅读源码(彩蛋) 8 | 9 | 在阅读源码过程中,我遵循这几个技巧,感觉很有效果。 10 | 11 | ### 1 知己知彼 12 | 13 | 首先在阅读源码之前,第一步就是要明白看的是什么? 是否熟悉里面的 api? 是否在真实的项目中使用过? 如果对于同一个库,工作中使用过的开发者看源码,要比没有使用过直接看源码,容易的多。 14 | 15 | - 比如想看 `react-redux` 源码,就要先知道 react-redux 中 Provider 是做什么的 ? `connect` 怎么使用的,它有几个参数,每个参数有什么意义? 16 | - 比如想看 `mobx-react` 源码,就要先知道 mobx-react 中 `inject` ,`observer` 的作用。 17 | 18 | 开发者对一个库或者一个框架越熟悉,看源码也就越容易,甚至如果真的精通一个框架本身,那么很有可能不看源码就猜到框架内部是如何实现的,正所谓庖丁解牛。 19 | 20 | ### 2 渐进式|分片式阅读 21 | 22 | 看源码千万不要想着**一口气吃成个胖子**,作者看 React 源码,陆陆续续看了一年,当然不是天天都在看,但是中间也没有间隔太久,如果想要快速看完源码,懂得原理,那么**只能从看源码到放弃**。 23 | 24 | 作者看源码遵循 **渐进式 ,分片式** 原则。 25 | 26 | - 渐进式: 拿 React 为例子🌰,如果一上来就看 React 核心调和调度流程,那么一下就会蒙,就会找不到头绪,看着看着就会变成看天书,所以可以先看一下基础的,比如 fiber 类型,React 如何创建 fiber ,每个 fiber 属性有什么意义。然后再慢慢渗透核心模块,这样效果甚佳。 27 | - 分片式:对于一个框架的源码学习,要制定计划,化整为零,每天学习一点点,每一次学习都做好笔记,两次学习的间隔最好不要太长时间。 28 | 29 | ### 3 带着问题,带着思考去阅读 30 | 31 | 带着问题学习源码是最佳的学习方案,而且作者可以保证,这种方式每一次看都会有收获。即使很多开发者看源码坚持不下去,就是因为没有带着问题,没有去思考。 32 | 33 | 如果没有问题的去看源码,看着看着就会变得盲目,而且很有可能犯困。这样是坚持不下去的。 34 | 35 | 那么如何带着问题去思考呢? 作者这里举了个例子🌰,比如现在想看 React Hooks 源码。那么我写一段 React Hooks 代码片段,看一下可以从中汇总出哪些问题? 36 | 37 | ``` 38 | import React , { useContext , useState , useRef, useEffect, useLayoutEffect, useMemo } from 'react' 39 | const newContext = React.createContext(null) 40 | /* ① React Hooks 必须在函数组件内部执行?,React 如何能够监听 React Hooks 在外部执行并抛出异常。 */ 41 | const value = useContext(newContext) 42 | console.log(value) 43 | function Index(props){ 44 | const [ number, setNumber ] = useState(0) 45 | // ② React Hooks 如何把状态保存起来?保存的信息存在了哪里? 46 | let number1 , setNumber1 47 | // ③ React Hooks 为什么不能写在条件语句中? 48 | if(props.isShow) [ number1 , setNumber1 ] = useState(0) 49 | const cacheState = useRef(0) 50 | const trueValue = useMemo(()=>{ 51 | // ④ useMemo 内部引用 useRef 为什么不需要添加依赖项,而 useState 就要添加依赖项 52 | return number1 + number + cacheState.current 53 | },[ number ,number1 ]) 54 | // ⑤ useEffect 添加依赖项 props.a ,为什么 props.a 改变,useEffect 回调重新执行。 55 | useEffect(()=>{ 56 | console.log(1) 57 | },[ props.a ]) 58 | // ⑥ React 如何区别 useEffect 和 useLayoutEffect ,执行时机有什么不同? 59 | useLayoutEffect(()=>{ 60 | console.log(2) 61 | },[]) 62 | 63 | return
《React 进阶实践指南》
64 | } 65 | ``` 66 | 67 | 从上面的一段代码中,可以提炼出的问题有: 68 | 69 | - ① React Hooks 为什么必须在函数组件内部执行?React 如何能够监听 React Hooks 在外部执行并抛出异常。 70 | - ② React Hooks 如何把状态保存起来?保存的信息存在了哪里? 71 | - ③ React Hooks 为什么不能写在条件语句中? 72 | - ④ useMemo 内部引用 useRef 为什么不需要添加依赖项,而 useState 就要添加依赖项。 73 | - ⑤ useEffect 添加依赖项 props.a ,为什么 props.a 改变,useEffect 回调函数 create 重新执行。 74 | - ⑥ React 内部如何区别 useEffect 和 useLayoutEffect ,执行时机有什么不同? 75 | 76 | 如果带着以上问题去阅读源码,相信阅读之后肯定会有收获。这种带着问题去阅读好处是: 77 | 78 | - **能够从源码中找到问题所在,更一针见血的了解原理**。 79 | - **能让阅读源码的过程变得不是那么枯燥无味,能够更加坚定阅读**。 80 | 81 | ## 披荆斩棘 ,勇往直前! 82 | 83 | 这本小册作者从 2020 年就开始整理,2021 年四月初开始正式写,写了正好四个月。 四个月里作者拒绝一切社交活动,没有一个周末休息过,把 React 知识点,从点到线再到面的串联起来,展现给大家,可能写的比较仓促,有一些错别字,如果给大家阅读带来不便,还请大家见谅,我会一直维护这本小册,修复错别字和不正确的地方,对小册的内容做技术翻新,对小册内容做补充,特别是持续维护的章节(第十四章,第二十六章,第二十七章)。 84 | 85 | **“路漫漫其修远兮,吾将上下而求索”** ,希望阅读到这里的每一个读者,不要把掌握小册的知识点作为学习 React 的终点,而是要当成学习的起点,带着对 React 全新的认识去使用,平时工作中要多敲多练,多学习一些 React 设计模式,多写一些自定义 Hooks 。在 React 技术成长之路上披荆斩棘 ,勇往直前! 86 | 87 | 最后的最后感谢大家支持我,认可我,祝福大家早日进阶 React 技术栈,在成长的道路上我与你们同行。🙏🙏🙏 88 | 89 | 如果在阅读过程中有什么问题欢迎大家加我微信,交个朋友,微信:**TH0000666**,也可以关注笔者公众号 **前端Sharing**,持续分享前端硬核文章~ 90 | 91 | ![11118.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1fe8d3fa5d1440e4af905bb810f29b68~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) -------------------------------------------------------------------------------- /docs/interview/前沿技术/现代前端工程Monorepo.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [![](https://p3-passport.byteimg.com/img/user-avatar/d43ef01b284d645d6d7ab02c8dc32aaa~100x100.awebp)](https://juejin.cn/user/430664257382462) 4 | 5 | 2021年03月29日 09:12 ·  阅读 31363 6 | 7 | 随着前端工程日益复杂,某些业务或者工具库通常涉及到很多个仓库,那么时间一长,多个仓库开发弊端日益显露,由此出现了一种新的项目管理方式——Monorepo。本文主要以 **Monorepo 的概念**、**MultiRepo的弊端**、**Monorepo 的收益**以及**Monorepo 的落地**这几个角度来认识和学习一下 Monorepo,文末会有思考题,欢迎大家来踊跃讨论。 8 | 9 | ## 什么是 Monorepo? 10 | 11 | Monorepo 其实不是一个新的概念,在软件工程领域,它已经有着十多年的历史了。概念上很好理解,就是把**多个项目**放在**一个仓库**里面,相对立的是传统的 MultiRepo 模式,即每个项目对应一个单独的仓库来分散管理。 12 | 13 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/75a56317bdf94794a8b29f6cd184c888~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 14 | 15 | 现代的前端工程已经越来越离不开 Monorepo 了,无论是业务代码还是工具库,越来越多的项目已经采用 Monorepo 的方式来进行开发。Google 宁愿把所有的代码都放在一个 Monorepo 工程下面,Vue 3、Yarn、Npm7 等等知名开源项目的源码也是采用 Monorepo 的方式来进行管理的。 16 | 17 | 一般 Monorepo 的目录如下所示,在 packages 存放多个子项目,并且每个子项目都有自己的`package.json`: 18 | 19 | ``` 20 | ├── packages 21 | | ├── pkg1 22 | | | ├── package.json 23 | | ├── pkg2 24 | | | ├── package.json 25 | ├── package.json 26 | 复制代码 27 | ``` 28 | 29 | 那 Monorepo 究竟有什么魔力,让大家如此推崇,落地如此之广呢? 30 | 31 | ## MultiRepo 之痛 32 | 33 | 要想知道 Monorepo 的优势,首先得弄清楚之前的开发方式有什么痛点。 34 | 35 | 之前传统的方式`MultiRepo`当中,每个项目都对应单独的一个代码仓库。我之前也是用这种方式开发的,是真真切切地感受到了这种方式带来的诸多弊端。现在就和大家一一分享一下。 36 | 37 | ### 1.代码复用 38 | 39 | 在维护多个项目的时候,有一些逻辑很有可能会被多次用到,比如一些基础的组件、工具函数,或者一些配置,你可能会想: 要不把代码直接 copy 过来,多省事儿!但有个问题是,如果这些代码出现 bug、或者需要做一些调整的时候,就得修改多份,维护成本越来越高。 40 | 41 | 那如何来解决这个问题呢?比较好的方式是将公共的逻辑代码抽取出来,作为一个 npm 包进行发布,一旦需要改动,只需要改动一份代码,然后 publish 就行了。 42 | 43 | 但这真的就完美解决了么?我举个例子,比如你引入了 `1.1.0` 版本的 A 包,某个工具函数出现问题了,你需要做这些事情: 44 | 45 | - 1. 去修改一个工具函数的代码 46 | - 2. 发布`1.1.1`版本的新包 47 | - 3. 项目中安装新版本的 A。 48 | 49 | 可能只是改了一行代码,需要走这么多流程。然而开发阶段是很难保证不出 bug 的,如果有个按钮需要改个样式,又需要把上面的流程重新走一遍......停下来想想,这些重复的步骤真的是必须的吗?我们只是想复用一下代码,为什么每次修改代码都这么复杂? 50 | 51 | 上述的问题其实是 `MultiRepo`普遍存在的问题,因为不同的仓库工作区的割裂,导致复用代码的成本很高,开发调试的流程繁琐,甚至在基础库频繁改动的情况下让人感到很抓狂,体验很差。 52 | 53 | ### 2.版本管理 54 | 55 | 在 MultiRepo 的开发方式下,依赖包的版本管理有时候是一个特别玄学的问题。比如说刚开始一个工具包版本是 v1.0.0,有诸多项目都依赖于这个工具包,但在某个时刻,这个工具包发了一个 `break change` 版本,和原来版本的 API 完全不兼容。而事实上有些项目并没有升级这个依赖,导致一些莫名的报错。 56 | 57 | 当项目多了之后,很容易出现这种依赖更新不及时的情况。这又是一个痛点。 58 | 59 | ### 3.项目基建 60 | 61 | 由于在 MultiRepo 当中,各个项目的工作流是割裂的,因此每个项目需要单独配置开发环境、配置 CI 流程、配置部署发布流程等等,甚至每个项目都有自己单独的一套脚手架工具。 62 | 63 | 其实,很容易发现这些项目里的很多基建的逻辑都是重复的,如果是 10 个项目,就需要维护 10 份基建的流程,逻辑重复不说,各个项目间存在构建、部署和发布的规范不能统一的情况,这样维护起来就更加麻烦了。 64 | 65 | ## Monorepo 的收益 66 | 67 | 说清楚 `MultiRepo` 的痛点之后,相信你也大概能理解为什么要诞生`Monorepo`这个技术了。现在就来细致地分析一下`Monorepo`到底给现代的前端工程带来了哪些收益。 68 | 69 | 首先是**工作流的一致性**,由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况。 70 | 71 | 其次是**项目基建成本的降低**,所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低。 72 | 73 | 再者,**团队协作也更加容易**,一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码,另一方面,git commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 commit,现在只需要提交一次,简化了 commit 记录,方便协作。 74 | 75 | ## Monorepo 的落地 76 | 77 | 如果你还从来没接触过 Monorepo 的开发,到这可能你会疑惑了: 刚刚说了这么多 Monorepo 的好处,可是我还是不知道怎么用啊!是直接把所有的代码全部搬到一个仓库就可以了吗? 78 | 79 | 当然不是,在实际场景来落地 Monorepo,需要一套完整的工程体系来进行支撑,因为基于 Monorepo 的项目管理,绝不是仅仅代码放到一起就可以的,还需要考虑项目间依赖分析、依赖安装、构建流程、测试流程、CI 及发布流程等诸多工程环节,同时还要考虑项目规模到达一定程度后的性能问题,比如项目`构建/测试`时间过长需要进行**增量构建/测试**、**按需执行 CI**等等,在实现全面工程化能力的同时,也需要兼顾到性能问题。 80 | 81 | 因此,要想从零开始定制一套完善的 Monorepo 的工程化工具,是一件难度很高的事情。不过社区已经提供了一些比较成熟的方案,我们可以拿来进行定制,或者对于一些上层的方案直接拿来使用。 82 | 83 | 其中比较底层的方案比如 [`lerna`](https://link.juejin.cn/?target=https%3A%2F%2Flerna.js.org%2F "https://lerna.js.org/"),封装了 Monorepo 中的依赖安装、脚本批量执行等等基本的功能,但没有一套构建、测试、部署的工具链,整体 Monorepo 功能比较弱,但要用到业务项目当中,往往需要基于它进行顶层能力的封装,提供全面工程能力的支撑。 84 | 85 | 当然也有一些集成的 Monorepo 方案,比如[`nx`](https://link.juejin.cn/?target=https%3A%2F%2Fnx.dev%2Flatest%2Freact%2Fgetting-started%2Fgetting-started "https://nx.dev/latest/react/getting-started/getting-started")(官网写的真心不错,还有不少视频教程)、[`rushstack`](https://link.juejin.cn/?target=https%3A%2F%2Frushstack.io%2F "https://rushstack.io/"),提供从初始化、开发、构建、测试到部署的全流程能力,有一套比较完整的 Monorepo 基础设施,适合直接拿来进行业务项目的开发。不过由于这些顶层方案内部各种流程和工具链都已经非常完善了,如果要基于这些方案来定制,适配和维护的成本过高,基本是不可行的。 86 | 87 | ## 总结 88 | 89 | 总而言之,Monorepo 的开发模式就是将各自独立的项目,变成一个统一的工程整体,解决 MultiRepo 下出现的各种痛点,提升研发效率和工程质量。那最后我还有有一个问题,采用 Monorepo 解决了之前的痛点之后,产生了哪些新的问题呢?这些问题可以解决吗?欢迎大家在留言区一起讨论。 90 | 91 | > 本文首发于公众号《前端三元同学》欢迎大家关注,原文:[现代前端工程为什么越来越离不开 Monorepo?](https://link.juejin.cn/?target=https%3A%2F%2Fmp.weixin.qq.com%2Fs%3F__biz%3DMzU0MTU4OTU2MA%3D%3D%26mid%3D2247486144%26idx%3D1%26sn%3D9f9248285a315555b52157115f2fa09a%26chksm%3Dfb26e397cc516a81098d5727ca46fc58d85d6e0516828a5f8711e712b8798a654e79fe810a48%26token%3D733579633%26lang%3Dzh_CN%23rd "https://mp.weixin.qq.com/s?__biz=MzU0MTU4OTU2MA==&mid=2247486144&idx=1&sn=9f9248285a315555b52157115f2fa09a&chksm=fb26e397cc516a81098d5727ca46fc58d85d6e0516828a5f8711e712b8798a654e79fe810a48&token=733579633&lang=zh_CN#rd") 92 | 93 | > 字节跳动 IES 前端架构团队急缺人才(p5/p6/p7大量HC),欢迎加我微信 sanyuan0704 一起来搞事情。 94 | 95 | ![](https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/00ba359ecd0075e59ffbc3d810af551d.svg) 626 96 | 97 | ![](https://lf3-cdn-tos.bytescm.com/obj/static/xitu_juejin_web/3d482c7a948bac826e155953b2a28a9e.svg) 收藏 -------------------------------------------------------------------------------- /docs/interview/前沿技术/pnpm对npm和yarn优势.md: -------------------------------------------------------------------------------- 1 | 大家最近是不是经常听到 pnpm,我也一样。今天研究了一下它的机制,确实厉害,对 yarn 和 npm 可以说是降维打击。 2 | 3 | 那具体好在哪里呢? 我们一起来看一下。 4 | 5 | 我们按照包管理工具的发展历史,从 npm2 开始讲起: 6 | 7 | ## npm2 8 | 9 | 用 node 版本管理工具把 node 版本降到 4,那 npm 版本就是 2.x 了。 10 | 11 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b4eef39cebc949859ff12c8d51e747e0~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 12 | 13 | 然后找个目录,执行下 npm init -y,快速创建个 package.json。 14 | 15 | 然后执行 npm install express,那么 express 包和它的依赖都会被下载下来: 16 | 17 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8ad0f0e13d1404c93089bde5ae08112~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 18 | 19 | 展开 express,它也有 node\_modules: 20 | 21 | ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ada5f744720c4cb7b4d846ee2d1bf81b~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 22 | 23 | 再展开几层,每个依赖都有自己的 node\_modules: 24 | 25 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5ff1d1c0cab14b65b905fe1e74db59a1~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 26 | 27 | 也就是说 npm2 的 node\_modules 是嵌套的。 28 | 29 | 这很正常呀?有什么不对么? 30 | 31 | 这样其实是有问题的,多个包之间难免会有公共的依赖,这样嵌套的话,同样的依赖会复制很多次,会占据比较大的磁盘空间。 32 | 33 | 这个还不是最大的问题,致命问题是 windows 的文件路径最长是 260 多个字符,这样嵌套是会超过 windows 路径的长度限制的。 34 | 35 | 当时 npm 还没解决,社区就出来新的解决方案了,就是 yarn: 36 | 37 | ## yarn 38 | 39 | yarn 是怎么解决依赖重复很多次,嵌套路径过长的问题的呢? 40 | 41 | 铺平。所有的依赖不再一层层嵌套了,而是全部在同一层,这样也就没有依赖重复多次的问题了,也就没有路径过长的问题了。 42 | 43 | 我们把 node\_modules 删了,用 yarn 再重新安装下,执行 yarn add express: 44 | 45 | 这时候 node\_modules 就是这样了: 46 | 47 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/71906633d465460183c3eb880391bf2e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 48 | 49 | 全部铺平在了一层,展开下面的包大部分是没有二层 node\_modules 的: 50 | 51 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/52e6392c33f04f7a949c07fa7d65d358~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 52 | 53 | 当然也有的包还是有 node\_modules 的,比如这样: 54 | 55 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/cd0a3971237445aea60f4de1c13250a7~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 56 | 57 | 为什么还有嵌套呢? 58 | 59 | 因为一个包是可能有多个版本的,提升只能提升一个,所以后面再遇到相同包的不同版本,依然还是用嵌套的方式。 60 | 61 | npm 后来升级到 3 之后,也是采用这种铺平的方案了,和 yarn 很类似: 62 | 63 | ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/79f93e2855514117bb73de52284d86fa~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 64 | 65 | 当然,yarn 还实现了 yarn.lock 来锁定依赖版本的功能,不过这个 npm 也实现了。 66 | 67 | yarn 和 npm 都采用了铺平的方案,这种方案就没有问题了么? 68 | 69 | 并不是,扁平化的方案也有相应的问题。 70 | 71 | 最主要的一个问题是幽灵依赖,也就是你明明没有声明在 dependencies 里的依赖,但在代码里却可以 require 进来。 72 | 73 | 这个也很容易理解,因为都铺平了嘛,那依赖的依赖也是可以找到的。 74 | 75 | 但是这样是有隐患的,因为没有显式依赖,万一有一天别的包不依赖这个包了,那你的代码也就不能跑了,因为你依赖这个包,但是现在不会被安装了。 76 | 77 | 这就是幽灵依赖的问题。 78 | 79 | 而且还有一个问题,就是上面提到的依赖包有多个版本的时候,只会提升一个,那其余版本的包不还是复制了很多次么,依然有浪费磁盘空间的问题。 80 | 81 | 那社区有没有解决这俩问题的思路呢? 82 | 83 | 当然有,这不是 pnpm 就出来了嘛。 84 | 85 | 那 pnpm 是怎么解决这俩问题的呢? 86 | 87 | ## pnpm 88 | 89 | 回想下 npm3 和 yarn 为什么要做 node\_modules 扁平化?不就是因为同样的依赖会复制多次,并且路径过长在 windows 下有问题么? 90 | 91 | 那如果不复制呢,比如通过 link。 92 | 93 | 首先介绍下 link,也就是软硬连接,这是操作系统提供的机制,硬连接就是同一个文件的不同引用,而软链接是新建一个文件,文件内容指向另一个路径。当然,这俩链接使用起来是差不多的。 94 | 95 | 如果不复制文件,只在全局仓库保存一份 npm 包的内容,其余的地方都 link 过去呢? 96 | 97 | 这样不会有复制多次的磁盘空间浪费,而且也不会有路径过长的问题。因为路径过长的限制本质上是不能有太深的目录层级,现在都是各个位置的目录的 link,并不是同一个目录,所以也不会有长度限制。 98 | 99 | 没错,pnpm 就是通过这种思路来实现的。 100 | 101 | 再把 node\_modules 删掉,然后用 pnpm 重新装一遍,执行 pnpm install。 102 | 103 | 你会发现它打印了这样一句话: 104 | 105 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1b2d51d9a17743a4bafc42f1bbfd310c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 106 | 107 | 包是从全局 store 硬连接到虚拟 store 的,这里的虚拟 store 就是 node\_modules/.pnpm。 108 | 109 | 我们打开 node\_modules 看一下: 110 | 111 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9b4dc807ca6e4ae7a955c8dd6385cb46~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 112 | 113 | 确实不是扁平化的了,依赖了 express,那 node\_modules 下就只有 express,没有幽灵依赖。 114 | 115 | 展开 .pnpm 看一下: 116 | 117 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/65a69589bd534fdd97bdbeb6e3e1024c~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 118 | 119 | 所有的依赖都在这里铺平了,都是从全局 store 硬连接过来的,然后包和包之间的依赖关系是通过软链接组织的。 120 | 121 | 比如 .pnpm 下的 expresss,这些都是软链接, 122 | 123 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c50d8dc8a2a4466ba9e5eccd5c15614e~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 124 | 125 | 也就是说,所有的依赖都是从全局 store 硬连接到了 node\_modules/.pnpm 下,然后之间通过软链接来相互依赖。 126 | 127 | 官方给了一张原理图,配合着看一下就明白了: 128 | 129 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/326a2090786e4d16b2d6fce25e876680~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 130 | 131 | 这就是 pnpm 的实现原理。 132 | 133 | 那么回过头来看一下,pnpm 为什么优秀呢? 134 | 135 | 首先,最大的优点是节省磁盘空间呀,一个包全局只保存一份,剩下的都是软硬连接,这得节省多少磁盘空间呀。 136 | 137 | 其次就是快,因为通过链接的方式而不是复制,自然会快。 138 | 139 | 这也是它所标榜的优点: 140 | 141 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1ba8815b36b3498ea4a3c2248d192bd6~tplv-k3u1fbpfcp-zoom-in-crop-mark:4536:0:0:0.image?) 142 | 143 | 相比 npm2 的优点就是不会进行同样依赖的多次复制。 144 | 145 | 相比 yarn 和 npm3+ 呢,那就是没有幽灵依赖,也不会有没有被提升的依赖依然复制多份的问题。 146 | 147 | 这就已经足够优秀了,对 yarn 和 npm 可以说是降维打击。 148 | 149 | ## 总结 150 | 151 | pnpm 最近经常会听到,可以说是爆火。本文我们梳理了下它爆火的原因: 152 | 153 | npm2 是通过嵌套的方式管理 node\_modules 的,会有同样的依赖复制多次的问题。 154 | 155 | npm3+ 和 yarn 是通过铺平的扁平化的方式来管理 node\_modules,解决了嵌套方式的部分问题,但是引入了幽灵依赖的问题,并且同名的包只会提升一个版本的,其余的版本依然会复制多次。 156 | 157 | pnpm 则是用了另一种方式,不再是复制了,而是都从全局 store 硬连接到 node\_modules/.pnpm,然后之间通过软链接来组织依赖关系。 158 | 159 | 这样不但节省磁盘空间,也没有幽灵依赖问题,安装速度还快,从机制上来说完胜 npm 和 yarn。 160 | 161 | pnpm 就是凭借这个对 npm 和 yarn 降维打击的。 -------------------------------------------------------------------------------- /docs/interview/_sidebar.md: -------------------------------------------------------------------------------- 1 | * 基础 2 | * [Html](interview/html.md) 3 | * [Css](interview/css.md) 4 | * [JavaScript基础](interview/javascript基础.md) 5 | * [JavaScript进阶](interview/javascript进阶.md) 6 | * [TypeScript](interview/typescript.md) 7 | 8 | * 框架 9 | * [Vue.js](interview/vue.md) 10 | * [Vue.js源码分析](interview/vue源码分析.md) 11 | * [Vue3小结](interview/vue3小结.md) 12 | * [React.js](interview/react.md) 13 | * [React.js源码分析](interview/react源码分析.md) 14 | * React进阶实践指南 15 | * [1.写给想要进阶的你](interview/React进阶实践指南/1.写给想要进阶的你.md) 16 | * [2.基础篇-认识jsx](interview/React进阶实践指南/2.基础篇-认识jsx.md) 17 | * [3.基础篇-起源Component](interview/React进阶实践指南/3.基础篇-起源Component.md) 18 | * [4.基础篇-玄学state](interview/React进阶实践指南/4.基础篇-玄学state.md) 19 | * [5.基础篇-深入props](interview/React进阶实践指南/5.基础篇-深入props.md) 20 | * [6.基础篇-理解lifeCycle](interview/React进阶实践指南/6.基础篇-理解lifeCycle.md) 21 | * [7.基础篇-多功能Ref](interview/React进阶实践指南/7.基础篇-多功能Ref.md) 22 | * [8.基础篇-提供者context](interview/React进阶实践指南/8.基础篇-提供者context.md) 23 | * [9.基础篇-模块化css](interview/React进阶实践指南/9.基础篇-模块化css.md) 24 | * [10.基础篇-高阶组件](interview/React进阶实践指南/10.基础篇-高阶组件.md) 25 | * [11.优化篇-渲染控制](interview/React进阶实践指南/11.优化篇-渲染控制.md) 26 | * [12.优化篇-渲染调优](interview/React进阶实践指南/12.优化篇-渲染调优.md) 27 | * [13.优化篇-处理海量数据](interview/React进阶实践指南/13.优化篇-处理海量数据.md) 28 | * [14.优化篇-细节处理(持续)](interview/React进阶实践指南/14.优化篇-细节处理(持续).md) 29 | * [15.原理篇-事件原理](interview/React进阶实践指南/15.原理篇-事件原理.md) 30 | * [15.原理篇-事件原理-v18新版本.md](interview/React进阶实践指南/15.原理篇-事件原理-v18新版本.md) 31 | * [16.原理篇-调度与时间片](interview/React进阶实践指南/16.原理篇-调度与时间片.md) 32 | * [17.原理篇-调和与fiber](interview/React进阶实践指南/17.原理篇-调和与fiber.md) 33 | * [17.原理篇-更新流程:进入调度任务.md](interview/React进阶实践指南/17.原理篇-更新流程:进入调度任务.md) 34 | * [18.原理篇-Hooks原理](interview/React进阶实践指南/18.原理篇-Hooks原理.md) 35 | * [19.生态篇-React-router](interview/React进阶实践指南/19.生态篇-React-router.md) 36 | * [20.生态篇-React-redux](interview/React进阶实践指南/20.生态篇-React-redux.md) 37 | * [21.生态篇-React-mobx](interview/React进阶实践指南/21.生态篇-React-mobx.md) 38 | * [22.实践篇-实现mini-Router](interview/React进阶实践指南/22.实践篇-实现mini-Router.md) 39 | * [23.实践篇-表单验证上](interview/React进阶实践指南/23.实践篇-表单验证上.md) 40 | * [24.实践篇-表单验证下](interview/React进阶实践指南/24.实践篇-表单验证下.md) 41 | * [25.实践篇-自定义弹窗](interview/React进阶实践指南/25.实践篇-自定义弹窗.md) 42 | * [26.自定义Hooks设计(持续)](interview/React进阶实践指南/26.自定义Hooks设计(持续).md) 43 | * [27.实践篇-自定义Hooks实践(持续)](interview/React进阶实践指南/27.实践篇-自定义Hooks实践(持续).md) 44 | * [28.总结篇-如何有效阅读源码](interview/React进阶实践指南/28.总结篇-如何有效阅读源码.md) 45 | * [29.原理篇-Context原理](interview/React进阶实践指南/29.原理篇-Context原理.md) 46 | * [30.原理篇-beginWork和render全流程](interview/React进阶实践指南/30.原理篇-beginWork和render全流程.md) 47 | * [31.V18特性篇-useMutableSource(已被取缔)](interview/React进阶实践指南/31.V18特性篇-useMutableSource(已被取缔).md) 48 | * [32.V18特性篇-transition](interview/React进阶实践指南/32.V18特性篇-transition.md) 49 | * [33.原理篇-更新流程:进入调度任务](interview/React进阶实践指南/33.原理篇-更新流程:进入调度任务.md) 50 | * [34.v18特性篇-concurrent下的state更新流程](interview/React进阶实践指南/34.v18特性篇-concurrent下的state更新流程.md) 51 | * [35.v18特性篇-订阅外部数据源](interview/React进阶实践指南/35.v18特性篇-订阅外部数据源.md) 52 | * [36.原理篇-v18commit全流程](interview/React进阶实践指南/36.原理篇-v18commit全流程.md) 53 | * [37.v18特性篇-concurrent下的state更新流程.md](interview/React进阶实践指南/37.v18特性篇-concurrent下的state更新流程.md) 54 | * 实习 55 | * [前端文件处理](interview/前端文件处理.md) 56 | * [excel导出优化](interview/excel导出优化.md) 57 | * [分片上传组件](interview/分片上传组件.md) 58 | * [动态表格渲染](interview/动态表格渲染.md) 59 | * [前端监控系统](interview/前端监控系统.md) 60 | * [组件封装思想](interview/组件封装思想.md) 61 | 62 | * 项目 63 | * [管理系统项目](interview/管理系统项目.md) 64 | * [预约系统项目](interview/预约系统项目.md) 65 | * [投票系统项目](interview/投票系统项目.md) 66 | * [三画项目](interview/三画项目面.md) 67 | 68 | * 进阶 69 | * [Webpack](interview/webpack.md) 70 | * [Webpack5](interview/webpack5.md) 71 | * [Node.js](interview/node.js.md) 72 | * [Axios](interview/axios.md) 73 | * [WebComponents](interview/webComponents.md) 74 | * [微前端](interview/微前端.md) 75 | * [微前端实践](interview/微前端治理框架.md) 76 | * [vue-demi](interview/vue-demi.md) 77 | * [langchain](interview/langchian.md) 78 | * [错误分析](interview/错误分析.md) 79 | * [vue-demi实践](interview/vue-demi实践.md) 80 | * [大模型知识库构建](interview/大模型知识库构建.md) 81 | * [flow](interview/workflow.md) 82 | 83 | * 原理 84 | * [浏览器原理](interview/浏览器.md) 85 | * [计算机网络](interview/网络.md) 86 | * [数据结构与算法](interview/数据结构与算法基础.md) 87 | * [操作系统](interview/操作系统.md) 88 | * [数据库原理](interview/数据库原理.md) 89 | * [Linux基础](interview/linux.md) 90 | 91 | * 综合 92 | * [计算机基础](interview/常见计算机基础.md) 93 | * [场景题](interview/场景题.md) 94 | * [性能优化](interview/性能优化.md) 95 | * [设计模式](interview/设计模式.md) 96 | 97 | * 代码 98 | * [代码题](interview/代码题.md) 99 | * [算法题](interview/算法题.md) 100 | * [输出题](interview/输出题.md) 101 | 102 | * 可视化 103 | * [canvas](interview/HTML5-Canvas.md) 104 | * [svg](interview/SVG入门指南.md) 105 | * [webgl](interview/webgl.md) 106 | * [webgl入门与实践](interview/WebGL入门与实践.md) 107 | * [three.js](interview/three.js.md) 108 | * [d3.js](interview/d3.js.md) 109 | * [webgis](interview/webgis.md) 110 | 111 | * 技术前沿 112 | * [大前端技术](interview/前沿技术/大前端技术.md) 113 | * [hybrid简单了解](interview/前沿技术/hybrid简单了解.md) 114 | * [前端监控SDK原理分析](interview/前沿技术/前端监控SDK原理分析.md) 115 | * [实现一个埋点监控SDK](interview/前沿技术/实现一个埋点监控SDK.md) 116 | * [Node性能监控指标获取方法](interview/前沿技术/Node性能监控指标获取方法.md) 117 | * [一文了解Node性能监控](interview/前沿技术/一文了解Node性能监控.md) 118 | * [从0到1搭建前端异常监控系统](interview/前沿技术/从0到1搭建前端异常监控系统.md) 119 | * [现代前端工程Monorepo](interview/前沿技术/现代前端工程Monorepo.md) 120 | * [pnpm对npm和yarn优势](interview/前沿技术/pnpm对npm和yarn优势.md) 121 | * [swc、esbuild和vite前端构建工具浅析](interview/前沿技术/swc、esbuild和vite前端构建工具浅析.md) 122 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | * 基础 2 | * [Html](interview/html.md) 3 | * [Css](interview/css.md) 4 | * [JavaScript基础](interview/javascript基础.md) 5 | * [JavaScript进阶](interview/javascript进阶.md) 6 | * [TypeScript](interview/typescript.md) 7 | 8 | * 框架 9 | * [Vue.js](interview/vue.md) 10 | * [Vue.js源码分析](interview/vue源码分析.md) 11 | * [Vue3小结](interview/vue3小结.md) 12 | * [React.js](interview/react.md) 13 | * [React.js源码分析](interview/react源码分析.md) 14 | * React进阶实践指南 15 | * [1.写给想要进阶的你](interview/React进阶实践指南/1.写给想要进阶的你.md) 16 | * [2.基础篇-认识jsx](interview/React进阶实践指南/2.基础篇-认识jsx.md) 17 | * [3.基础篇-起源Component](interview/React进阶实践指南/3.基础篇-起源Component.md) 18 | * [4.基础篇-玄学state](interview/React进阶实践指南/4.基础篇-玄学state.md) 19 | * [5.基础篇-深入props](interview/React进阶实践指南/5.基础篇-深入props.md) 20 | * [6.基础篇-理解lifeCycle](interview/React进阶实践指南/6.基础篇-理解lifeCycle.md) 21 | * [7.基础篇-多功能Ref](interview/React进阶实践指南/7.基础篇-多功能Ref.md) 22 | * [8.基础篇-提供者context](interview/React进阶实践指南/8.基础篇-提供者context.md) 23 | * [9.基础篇-模块化css](interview/React进阶实践指南/9.基础篇-模块化css.md) 24 | * [10.基础篇-高阶组件](interview/React进阶实践指南/10.基础篇-高阶组件.md) 25 | * [11.优化篇-渲染控制](interview/React进阶实践指南/11.优化篇-渲染控制.md) 26 | * [12.优化篇-渲染调优](interview/React进阶实践指南/12.优化篇-渲染调优.md) 27 | * [13.优化篇-处理海量数据](interview/React进阶实践指南/13.优化篇-处理海量数据.md) 28 | * [14.优化篇-细节处理(持续)](interview/React进阶实践指南/14.优化篇-细节处理(持续).md) 29 | * [15.原理篇-事件原理](interview/React进阶实践指南/15.原理篇-事件原理.md) 30 | * [15.原理篇-事件原理-v18新版本.md](interview/React进阶实践指南/15.原理篇-事件原理-v18新版本.md) 31 | * [16.原理篇-调度与时间片](interview/React进阶实践指南/16.原理篇-调度与时间片.md) 32 | * [17.原理篇-调和与fiber](interview/React进阶实践指南/17.原理篇-调和与fiber.md) 33 | * [17.原理篇-更新流程:进入调度任务.md](interview/React进阶实践指南/17.原理篇-更新流程:进入调度任务.md) 34 | * [18.原理篇-Hooks原理](interview/React进阶实践指南/18.原理篇-Hooks原理.md) 35 | * [19.生态篇-React-router](interview/React进阶实践指南/19.生态篇-React-router.md) 36 | * [20.生态篇-React-redux](interview/React进阶实践指南/20.生态篇-React-redux.md) 37 | * [21.生态篇-React-mobx](interview/React进阶实践指南/21.生态篇-React-mobx.md) 38 | * [22.实践篇-实现mini-Router](interview/React进阶实践指南/22.实践篇-实现mini-Router.md) 39 | * [23.实践篇-表单验证上](interview/React进阶实践指南/23.实践篇-表单验证上.md) 40 | * [24.实践篇-表单验证下](interview/React进阶实践指南/24.实践篇-表单验证下.md) 41 | * [25.实践篇-自定义弹窗](interview/React进阶实践指南/25.实践篇-自定义弹窗.md) 42 | * [26.自定义Hooks设计(持续)](interview/React进阶实践指南/26.自定义Hooks设计(持续).md) 43 | * [27.实践篇-自定义Hooks实践(持续)](interview/React进阶实践指南/27.实践篇-自定义Hooks实践(持续).md) 44 | * [28.总结篇-如何有效阅读源码](interview/React进阶实践指南/28.总结篇-如何有效阅读源码.md) 45 | * [29.原理篇-Context原理](interview/React进阶实践指南/29.原理篇-Context原理.md) 46 | * [30.原理篇-beginWork和render全流程](interview/React进阶实践指南/30.原理篇-beginWork和render全流程.md) 47 | * [31.V18特性篇-useMutableSource(已被取缔)](interview/React进阶实践指南/31.V18特性篇-useMutableSource(已被取缔).md) 48 | * [32.V18特性篇-transition](interview/React进阶实践指南/32.V18特性篇-transition.md) 49 | * [33.原理篇-更新流程:进入调度任务](interview/React进阶实践指南/33.原理篇-更新流程:进入调度任务.md) 50 | * [34.v18特性篇-concurrent下的state更新流程](interview/React进阶实践指南/34.v18特性篇-concurrent下的state更新流程.md) 51 | * [35.v18特性篇-订阅外部数据源](interview/React进阶实践指南/35.v18特性篇-订阅外部数据源.md) 52 | * [36.原理篇-v18commit全流程](interview/React进阶实践指南/36.原理篇-v18commit全流程.md) 53 | * [37.v18特性篇-concurrent下的state更新流程.md](interview/React进阶实践指南/37.v18特性篇-concurrent下的state更新流程.md) 54 | * 实习 55 | * [前端文件处理](interview/前端文件处理.md) 56 | * [excel导出优化](interview/excel导出优化.md) 57 | * [分片上传组件](interview/分片上传组件.md) 58 | * [动态表格渲染](interview/动态表格渲染.md) 59 | * [前端监控系统](interview/前端监控系统.md) 60 | * [组件封装思想](interview/组件封装思想.md) 61 | 62 | * 项目 63 | * [管理系统项目](interview/管理系统项目.md) 64 | * [预约系统项目](interview/预约系统项目.md) 65 | * [投票系统项目](interview/投票系统项目.md) 66 | * [三画项目](interview/三画项目面.md) 67 | 68 | * 进阶 69 | * [Webpack](interview/webpack.md) 70 | * [Webpack5](interview/webpack5.md) 71 | * [Node.js](interview/node.js.md) 72 | * [Axios](interview/axios.md) 73 | * [WebComponents](interview/webComponents.md) 74 | * [微前端](interview/微前端.md) 75 | * [微前端实践](interview/微前端治理框架.md) 76 | * [vue-demi](interview/vue-demi.md) 77 | * [vue-demi实践](interview/vue-demi实践.md) 78 | * [langchain](interview/langchian.md) 79 | * [错误分析](interview/错误分析.md) 80 | * [vue-demi实践](interview/vue-demi实践.md) 81 | * [大模型知识库构建](interview/大模型知识库构建.md) 82 | * [flow](interview/workflow.md) 83 | 84 | * 原理 85 | * [浏览器原理](interview/浏览器.md) 86 | * [计算机网络](interview/网络.md) 87 | * [数据结构与算法](interview/数据结构与算法基础.md) 88 | * [操作系统](interview/操作系统.md) 89 | * [数据库原理](interview/数据库原理.md) 90 | * [Linux基础](interview/linux.md) 91 | 92 | * 综合 93 | * [计算机基础](interview/常见计算机基础.md) 94 | * [场景题](interview/场景题.md) 95 | * [性能优化](interview/性能优化.md) 96 | * [设计模式](interview/设计模式.md) 97 | 98 | * 代码 99 | * [代码题](interview/代码题.md) 100 | * [算法题](interview/算法题.md) 101 | * [输出题](interview/输出题.md) 102 | 103 | * 可视化 104 | * [canvas](interview/HTML5-Canvas.md) 105 | * [svg](interview/SVG入门指南.md) 106 | * [webgl](interview/webgl.md) 107 | * [webgl入门与实践](interview/WebGL入门与实践.md) 108 | * [three.js](interview/three.js.md) 109 | * [d3.js](interview/d3.js.md) 110 | * [webgis](interview/webgis.md) 111 | 112 | * 技术前沿 113 | * [大前端技术](interview/前沿技术/大前端技术.md) 114 | * [hybrid简单了解](interview/前沿技术/hybrid简单了解.md) 115 | * [前端监控SDK原理分析](interview/前沿技术/前端监控SDK原理分析.md) 116 | * [实现一个埋点监控SDK](interview/前沿技术/实现一个埋点监控SDK.md) 117 | * [Node性能监控指标获取方法](interview/前沿技术/Node性能监控指标获取方法.md) 118 | * [一文了解Node性能监控](interview/前沿技术/一文了解Node性能监控.md) 119 | * [从0到1搭建前端异常监控系统](interview/前沿技术/从0到1搭建前端异常监控系统.md) 120 | * [现代前端工程Monorepo](interview/前沿技术/现代前端工程Monorepo.md) 121 | * [pnpm对npm和yarn优势](interview/前沿技术/pnpm对npm和yarn优势.md) 122 | * [swc、esbuild和vite前端构建工具浅析](interview/前沿技术/swc、esbuild和vite前端构建工具浅析.md) 123 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 前端学习文档 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 25 | 26 | 27 | 28 |
29 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/35.v18特性篇-订阅外部数据源.md: -------------------------------------------------------------------------------- 1 | --- 2 | created: 2022-06-11T11:01:29 (UTC +08:00) 3 | tags: [] 4 | source: https://juejin.cn/book/6945998773818490884/section/6948353204413268001 5 | author: 6 | --- 7 | 8 | # React 进阶实践指南 - 我不是外星人 - 掘金课程 9 | 10 | > ## Excerpt 11 | > 在第 31 章节中,讲到了外部数据源,还介绍了外部数据源的处理方式 —— useMutableSource 。在前不久更新的最新 React 18 中,用 useSyncExternalStore 代替了 useMutableSource 。具体内容可以参考 useMutableSource → useSyncExternalStore 。 12 | 13 | --- 14 | ## 一前言 15 | 16 | 在第 31 章节中,讲到了外部数据源,还介绍了外部数据源的处理方式 —— **useMutableSource** 。在前不久更新的最新 React 18 中,用 useSyncExternalStore 代替了 useMutableSource 。具体内容可以参考 [useMutableSource → useSyncExternalStore](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Freactwg%2Freact-18%2Fdiscussions%2F86 "https://github.com/reactwg/react-18/discussions/86") 。 17 | 18 | 言归正传,在之前的章节说到在 concurrent 模式下,render 可能会被执行多次,那么在读取外部数据源的会存在一个问题,比如一个 render 过程中读取了外部数据源状态 1 ,那么中途遇到更高优先级的任务,而中断了此次更新,就在此时改变了外部数据源,然后又恢复了此次更新,那么接下来又读取了数据源,由于中途发生了改变,所以这次读取的是外部数据源状态 2 ,那么一次更新中出现了这种表现不一致的情况。这个问题叫做 tearing 。 19 | 20 | ## 二 useSyncExternalStore 介绍 21 | 22 | 那么 useSyncExternalStore 的诞生并非偶然,和 v18 的更新模式下外部数据的 tearing 有着十分紧密的关联。 23 | 24 | useSyncExternalStore 出现解决了这个问题,我们从 v18 发布的 tag 中,找到这样的描述: 25 | 26 | > useSyncExternalStore is a new hook that allows external stores to support concurrent reads by forcing updates to the store to be synchronous. It removes the need for useEffect when implementing subscriptions to external data sources, and is recommended for any library that integrates with state external to React. 27 | 28 | useSyncExternalStore 能够让 React 组件在 concurrent 模式下安全地有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。当读取到外部状态发生了变化,会触发一个强制更新,来保证结果的一致性。 29 | 30 | 现在用 useSyncExternalStore 不在需要把订阅到更新流程交给组件处理。如下: 31 | 32 | ``` 33 | ) 34 | function App(){ 35 | const state = useSyncExternalStore(store.subscribe,store.getSnapshot) 36 | return
...
37 | } 38 | ``` 39 | 40 | 如上是通过 useSyncExternalStore 实现的订阅更新,这样减少了 APP 内部组件代码,代码健壮性提升,一定程度上也降低了耦合,最重要的它解决了并发模式状态读取问题。但是这里强调的一点是, 正常的 React 开发者在开发过程中不需要使用这个 api ,这个 hooks 主要是对于 React 的一些状态管理库,比如 redux ,通过它的帮助可以合理管理外部的 store,保证数据读取的一致。 41 | 42 | 接下来看一下 useSyncExternalStore 使用: 43 | 44 | ``` 45 | useSyncExternalStore( 46 | subscribe, 47 | getSnapshot, 48 | getServerSnapshot 49 | ) 50 | ``` 51 | 52 | - subscribe 为订阅函数,当数据改变的时候,会触发 subscribe,在 useSyncExternalStore 会通过带有记忆性的 getSnapshot 来判别数据是否发生变化,如果发生变化,那么会强制更新数据。 53 | 54 | - getSnapshot 可以理解成一个带有记忆功能的选择器。当 store 变化的时候,会通过 getSnapshot 生成新的状态值,这个状态值可提供给组件作为数据源使用,getSnapshot 可以检查订阅的值是否改变,改变的话那么会触发更新。 55 | 56 | - getServerSnapshot 用于 hydration 模式下的 getSnapshot。 57 | 58 | 59 | ## 三 useSyncExternalStore 基本使用 60 | 61 | 接下来我们用 useSyncExternalStore 配合 redux ,来简单实现订阅外部数据源功能。 62 | 63 | ``` 64 | import { combineReducers , createStore } from 'redux' 65 | 66 | /* number Reducer */ 67 | function numberReducer(state=1,action){ 68 | switch (action.type){ 69 | case 'ADD': 70 | return state + 1 71 | case 'DEL': 72 | return state - 1 73 | default: 74 | return state 75 | } 76 | } 77 | 78 | /* 注册reducer */ 79 | const rootReducer = combineReducers({ number:numberReducer }) 80 | /* 创建 store */ 81 | const store = createStore(rootReducer,{ number:1 }) 82 | 83 | function Index(){ 84 | /* 订阅外部数据源 */ 85 | const state = useSyncExternalStore(store.subscribe,() => store.getState().number) 86 | console.log(state) 87 | return
88 | {state} 89 | 90 |
91 | } 92 | ``` 93 | 94 | - 点击按钮,会触发 reducer ,然后会触发 store.subscribe 订阅函数,执行 getSnapshot 得到新的 number ,判断 number 是否发生变化,如果变化,触发更新。 95 | 96 | 有了 useSyncExternalStore 这个 hooks ,可以通过外部数据到内部数据的映射,当数据变化的时候,可以通知订阅函数 subscribe 去触发更新。 97 | 98 | ## 四 useSyncExternalStore 原理 99 | 100 | 接下来看一下 useSyncExternalStore 内部是如何实现的。 101 | 102 | > react-reconciler/src/ReactFiberHooks.new.js 103 | 104 | ``` 105 | function mountSyncExternalStore(subscribe,getSnapshot){ 106 | /* 创建一个 hooks */ 107 | const hook = mountWorkInProgressHook(); 108 | /* 产生快照 */ 109 | let nextSnapshot = getSnapshot(); 110 | 111 | /* 把快照记录下来 */ 112 | hook.memoizedState = nextSnapshot; 113 | /* 快照记录在 inst 属性上 */ 114 | const inst = { 115 | value: nextSnapshot, 116 | getSnapshot, 117 | }; 118 | hook.queue = inst; 119 | 120 | /* 用一个 effect 来订阅状态 ,subscribeToStore 发起订阅 */ 121 | mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]); 122 | 123 | /* 用一个 useEffect 来监听组件 render ,只要组件渲染就会调用 updateStoreInstance */ 124 | pushEffect( 125 | HookHasEffect | HookPassive, 126 | updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot), 127 | undefined, 128 | null, 129 | ); 130 | return nextSnapshot; 131 | } 132 | ``` 133 | 134 | mountSyncExternalStore 大致流程是这样的: 135 | 136 | - 第一步:创建一个 hooks 。我们都知道 hooks 更新是分两个阶段的,在初始化 hooks 阶段会创建一个 hooks ,在更新阶段会更新这个 Hook。 137 | - 第二步:调用 getSnapshot 产生一个状态值,并保存起来。 138 | - 第三步:用一个 effect 来订阅状态 `subscribeToStore` 发起订阅 。 139 | - 第四步:用一个 useEffect 来监听组件 render ,只要组件渲染就会调用 `updateStoreInstance` 。这一步是关键所在,在 concurrent 模式下渲染会中断,那么如果中断恢复 render ,那么这个 effect 就解决了这个问题。当 render 就会触发 updateStoreInstance 。 140 | 141 | 接下来看一下 subscribeToStore 和 updateStoreInstance 的实现。 142 | 143 | **subscribeToStore** 144 | 145 | > react-reconciler/src/subscribeToStore.js 146 | 147 | ``` 148 | function checkIfSnapshotChanged(inst) { 149 | const latestGetSnapshot = inst.getSnapshot; 150 | /* 取出上一次的快照信息 */ 151 | const prevValue = inst.value; 152 | try { 153 | /* 最新的快照信息 */ 154 | const nextValue = latestGetSnapshot(); 155 | /* 返回是否相等 */ 156 | return !is(prevValue, nextValue); 157 | } catch (error) { 158 | return true; 159 | } 160 | } 161 | /* 直接发起调度更新 */ 162 | function forceStoreRerender(fiber) { 163 | scheduleUpdateOnFiber(fiber, SyncLane, NoTimestamp); 164 | } 165 | 166 | function subscribeToStore(fiber, inst, subscribe) { 167 | const handleStoreChange = () => { 168 | /* 检查 state 是否发生变化 */ 169 | if (checkIfSnapshotChanged(inst)) { 170 | /* 触发更新 */ 171 | forceStoreRerender(fiber); 172 | } 173 | }; 174 | /* 发起订阅 */ 175 | return subscribe(handleStoreChange); 176 | } 177 | ``` 178 | 179 | subscribeToStore 的流程如下: 180 | 181 | - 通过 subscribe 订阅 handleStoreChange,当 state 改变会触发 handleStoreChange ,里面判断两次快照是否相等,如果不想等那么触发更新。 182 | 183 | **updateStoreInstance** 184 | 185 | > react-reconciler/src/updateStoreInstance.js 186 | 187 | ``` 188 | function updateStoreInstance(fiber,inst,nextSnapshot,getSnapshot) { 189 | inst.value = nextSnapshot; 190 | inst.getSnapshot = getSnapshot; 191 | /* 检查是否更新 */ 192 | if (checkIfSnapshotChanged(inst)) { 193 | /* 强制更新 */ 194 | forceStoreRerender(fiber); 195 | } 196 | } 197 | ``` 198 | 199 | - updateStoreInstance 很简单就是判断 state 是否发生变化,变化就更新。 200 | 201 | 通过如上原理分析,我们知道了 useSyncExternalStore 是如何防止 tearing 的了。为了让大家更清楚其流程 ,接下来我们来模拟一个 useSyncExternalStore 的实现。 202 | 203 | ``` 204 | function useMockSyncExternalStore(subscribe,getSnapshot){ 205 | const [ , forceupdate ] = React.useState(null) 206 | const inst = React.useRef(null) 207 | 208 | const nextValue = getSnapshot() 209 | 210 | inst.current = { 211 | value:nextValue, 212 | getSnapshot 213 | } 214 | /* 检测是否更新 */ 215 | const checkIfSnapshotChanged = () => { 216 | try { 217 | /* 最新的快照信息 */ 218 | const nextValue = inst.current.getSnapshot(); 219 | /* 返回是否相等 */ 220 | return !inst.value === nextValue 221 | } catch (error) { 222 | return true; 223 | } 224 | } 225 | /* 处理 store 改变 */ 226 | const handleStoreChange=()=>{ 227 | if (checkIfSnapshotChanged(inst)) { 228 | /* 触发更新 */ 229 | forceupdate({}) 230 | } 231 | } 232 | React.useEffect(()=>{ 233 | subscribe(handleStoreChange) 234 | },[ subscribe ]) 235 | 236 | /* 注意这个 useEffect 没有依赖项 ,每次更新都会执行该 effect */ 237 | React.useEffect(()=>{ 238 | handleStoreChange() 239 | }) 240 | 241 | return nextValue 242 | } 243 | ``` 244 | 245 | 如上就是 useSyncExternalStore 的模拟实现。 246 | 247 | ## 五 总结 248 | 249 | 本章节介绍了引入外部数据源的 hooks useSyncExternalStore,以及它的介绍,使用,以及原理。 250 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/15.原理篇-事件原理-v18新版本.md: -------------------------------------------------------------------------------- 1 | 2 | > ## Excerpt 3 | > 在上一章节中,我们讲到了老版本的事件原理,老版本的事件原理有一个问题就是,捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的,比如如下例子中: 4 | 5 | --- 6 | ## 一 前言 7 | 8 | 在上一章节中,我们讲到了老版本的事件原理,老版本的事件原理有一个问题就是,捕获阶段和冒泡阶段的事件都是模拟的,本质上都是在冒泡阶段执行的,比如如下例子中: 9 | 10 | ```js 11 | function Index(){ const refObj = React.useRef(null) useEffect(()=>{ const handler = ()=>{ console.log('事件监听') } refObj.current.addEventListener('click',handler) return () => { refObj.current.removeEventListener('click',handler) } },[]) const handleClick = ()=>{ console.log('冒泡阶段执行') } const handleCaptureClick = ()=>{ console.log('捕获阶段执行') } return } 12 | ``` 13 | 14 | 如上通过 onClick,onClickCapture 和原生的 DOM 监听器给元素 button 绑定了三个事件处理函数,那么当触发一次点击事件的时候,处理函数的执行,老版本打印顺序为: 15 | 16 | 老版本事件系统:事件监听 -> 捕获阶段执行 -> 冒泡阶段执行 17 | 18 | 但是老版本的事件系统,一定程度上,不符合事件流的执行时机,但是在新版本 v18 的事件系统中,这个问题得以解决。 19 | 20 | 新版本事件系统:捕获阶段执行 -> 事件监听 -> 冒泡阶段执行 21 | 22 | 那么新版本事件系统有哪里改变呢? 本章节我们来看一下新版本的事件系统原理。 23 | 24 | 对于 React 事件原理挖掘,主要体现在两个方面,那就是**事件绑定**和**事件触发**。 25 | 26 | ## 二 事件绑定——事件初始化 27 | 28 | 在 React 新版的事件系统中,在 createRoot 会一口气向外层容器上注册完全部事件,我们来看一下具体的实现细节: 29 | 30 | > react-dom/client.js 31 | 32 | ```js 33 | function createRoot(container, options) { /* 省去和事件无关的代码,通过如下方法注册事件 */ listenToAllSupportedEvents(rootContainerElement); } 34 | ``` 35 | 36 | 在 createRoot 中,通过 listenToAllSupportedEvents 注册事件,接下来看一下这个方法做了些什么: 37 | 38 | > react-dom/src/events/DOMPluginEventSystem.js 39 | 40 | ```js 41 | function listenToAllSupportedEvents(rootContainerElement) { /* allNativeEvents 是一个 set 集合,保存了大多数的浏览器事件 */ allNativeEvents.forEach(function (domEventName) { if (domEventName !== 'selectionchange') { /* nonDelegatedEvents 保存了 js 中,不冒泡的事件 */ if (!nonDelegatedEvents.has(domEventName)) { /* 在冒泡阶段绑定事件 */ listenToNativeEvent(domEventName, false, rootContainerElement); } /* 在捕获阶段绑定事件 */ listenToNativeEvent(domEventName, true, rootContainerElement); } }); } 42 | ``` 43 | 44 | listenToAllSupportedEvents 这个方法比较核心,主要目的就是通过 listenToNativeEvent 绑定浏览器事件,这里引出了两个常量,allNativeEvents 和 nonDelegatedEvents ,它们分别代表的意思如下: 45 | 46 | allNativeEvents:allNativeEvents 是一个 set 集合,保存了 81 个浏览器常用事件。 nonDelegatedEvents :这个也是一个集合,保存了浏览器中不会冒泡的事件,一般指的是媒体事件,比如 pause,play,playing 等,还有一些特殊事件,比如 cancel ,close,invalid,load,scroll 。 47 | 48 | 接下来如果事件是不冒泡的,那么会执行一次,listenToNativeEvent,第二个参数为 true 。 如果是常规的事件,那么会执行两次 listenToNativeEvent,分别在冒泡和捕获阶段绑定事件。 49 | 50 | 那么 listenToNativeEvent 就是事件监听,这个函数这里给它精简化,listenToNativeEvent 主要逻辑如下 51 | 52 | ```js 53 | var listener = dispatchEvent.bind(null,domEventName,...) if(isCapturePhaseListener){ target.addEventListener(eventType, dispatchEvent, true); }else{ target.addEventListener(eventType, dispatchEvent, false); } 54 | ``` 55 | 56 | 如上代码是源代码精简后的,并不是源码,isCapturePhaseListener 就是 listenToNativeEvent 的第二个参数,target 为 DOM 对象。dispatchEvent 为统一的事件监听函数。 57 | 58 | 如上可以看到 listenToNativeEvent 本质上就是向原生 DOM 中去注册事件,上面还有一个细节,就是 dispatchEvent 已经通过 bind 的方式将事件名称等信息保存下来了。经过这第一步,在初始化阶段,就已经注册了很多的事件监听器了。 59 | 60 | 此时如果发生一次点击事件,就会触发两次 dispatchEvent : 61 | 62 | - 第一次捕获阶段的点击事件; 63 | - 第二次冒泡阶段的点击事件; 64 | 65 | ## 三 事件触发 66 | 67 | 接下来就是重点,当触发一次点击事件,会发生什么,首先就是执行 dispatchEvent 事件,我们来看看这个函数做了些什么? 68 | 69 | dispatchEvent 保留核心的代码如下: 70 | 71 | ```js 72 | batchedUpdates(function () { return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst); }); 73 | ``` 74 | 75 | dispatchEvent 如果是正常的事件,就会通过 batchedUpdates 来处理 dispatchEventsForPlugins ,batchedUpdates 是批量更新的逻辑,在之前的章节中已经讲到通过这种方式来让更新变成可控的。所有的矛头都指向了 dispatchEventsForPlugins ,这个函数做了些什么呢? 76 | 77 | ```js 78 | function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) { /* 找到发生事件的元素——事件源 */ var nativeEventTarget = getEventTarget(nativeEvent); /* 待更新队列 */ var dispatchQueue = []; /* 找到待执行的事件 */ extractEvents(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags); /* 执行事件 */ processDispatchQueue(dispatchQueue, eventSystemFlags); } 79 | ``` 80 | 81 | 这个函数非常重要,首先通过 getEventTarget 找到发生事件的元素,也就是事件源。然后创建一个待更新的事件队列,这个队列做什么,马上会讲到,接下来通过 extractEvents 找到待更新的事件,然后通过 processDispatchQueue 执行事件。 82 | 83 | 上面的信息量比较大,我们会逐一进行解析,先举一个例子如下: 84 | 85 | ```js 86 | function Index(){ const handleClick = ()=>{ console.log('冒泡阶段执行') } const handleCaptureClick = ()=>{ console.log('捕获阶段执行') } const handleParentClick = () => { console.log('div 点击事件') } return
} 87 | ``` 88 | 89 | 如上的例子,有一个 div 和 button 均绑定了一个正常的点击事件 ,div 是 button 的父元素,除此之外 button 绑定了一个在捕获阶段执行的点击事件。 90 | 91 | 当点击按钮,触发一次点击事件的时候,如果 nativeEventTarget 本质上就是发生点击事件的 button 对应的 DOM 元素。 92 | 93 | 那么第一个问题就是 dispatchQueue 是什么? extractEvents 有如何处理的 dispatchQueue。 94 | 95 | 发生点击事件,通过上面我们知道,会触发两次 dispatchEvents,第一次是捕获阶段,第二次是冒泡阶段 ,两次我们分别打印一下 dispatchQueue : 96 | 97 | 第一次打印: 98 | 99 | ![1.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a5087c32b043467eb5525731692b4a24~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 100 | 101 | 第一次打印: 102 | 103 | ![10-8-2.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/20584f9179b24f9c8d9088b379d95f5b~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 104 | 105 | 如上可以看到两次 dispatchQueue 中只有一项元素,也就是在一次用户中,产生一次事件就会向 dispatchQueue 放入一个对象,对象中有两个状态,一个是 event ,一个是 listeners。那么这两个东西是如何来的呢? 106 | 107 | event 是通过事件插件合成的事件源 event,在 React 事件系统中,事件源也不是原生的事件源,而是 React 自己创建的事件源对象。对于不同的事件类型,会创建不同的事件源对象。本质上是在 extractEvents 函数中,有这么一段处理逻辑。 108 | 109 | ```js 110 | var SyntheticEventCtor = SyntheticEvent; /* 针对不同的事件,处理不同的事件源 */ switch (domEventName) { case 'keydown': case 'keyup': SyntheticEventCtor = SyntheticKeyboardEvent; break; case 'focusin': reactEventType = 'focus'; SyntheticEventCtor = SyntheticFocusEvent; break; .... } /* 找到事件监听者,也就是我们 onClick 绑定的事件处理函数 */ var _listeners = accumulateSinglePhaseListeners(targetInst, reactName, nativeEvent.type, inCapturePhase, accumulateTargetOnly); /* 向 dispatchQueue 添加 event 和 listeners */ if(_listeners.length > 0){ var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget); dispatchQueue.push({ event: _event, listeners: _listeners }); } 111 | ``` 112 | 113 | 如上可以看到,首先根据不同事件类型,选用不同的构造函数,通过 new 的方式去合成不同事件源对象。上面还有一个细节就是 \_listeners 是什么? \_listeners 本质上也是一个对象,里面有三个属性。 114 | 115 | currentTarget:发生事件的 DOM 元素。 instance : button 对应的 fiber 元素。 listener :一个数组,存放绑定的事件处理函数本身,上面 demo 中就是绑定给 onClick,onClickCapture 的函数。 116 | 117 | 接下来可以通过 DOM 元素找到对应的 fiber,找到元素对应的 fiber 之后,也就能找到 props 事件了。但是这里有一个细节,就是 listener 可以有多个,比如如上捕获阶段的 listener 只有一个,而冒泡阶段的 listener 有两个,这是因为 div button 上都有 onClick 事件。 118 | 119 | 如上可以总结为: 120 | 121 | **当发生一次点击事件,React 会根据事件源对应的 fiber 对象,根据 return指针向上遍历,收集所有相同的事件**,比如是 onClick,那就收集父级元素的所有 onClick 事件,比如是 onClickCapture,那就收集父级的所有 onClickCapture。 122 | 123 | 得到了 dispatchQueue 之后,就需要 processDispatchQueue 执行事件了,这个函数的内部会经历两次遍历: 124 | 125 | - 第一次遍历 dispatchQueue,通常情况下,只有一个事件类型,所有 dispatchQueue 中只有一个元素。 126 | - 接下来会遍历每一个元素的 listener,执行 listener 的时候有一个特点: 127 | 128 | ```js 129 | /* 如果在捕获阶段执行。 */ if (inCapturePhase) { for (var i = dispatchListeners.length - 1; i >= 0; i--) { var _dispatchListeners$i = dispatchListeners[i], instance = _dispatchListeners$i.instance, currentTarget = _dispatchListeners$i.currentTarget, listener = _dispatchListeners$i.listener; if (instance !== previousInstance && event.isPropagationStopped()) { return; } /* 执行事件 */ executeDispatch(event, listener, currentTarget); previousInstance = instance; } } else { for (var _i = 0; _i < dispatchListeners.length; _i++) { var _dispatchListeners$_i = dispatchListeners[_i], _instance = _dispatchListeners$_i.instance, _currentTarget = _dispatchListeners$_i.currentTarget, _listener = _dispatchListeners$_i.listener; if (_instance !== previousInstance && event.isPropagationStopped()) { return; } /* 执行事件 */ executeDispatch(event, _listener, _currentTarget); previousInstance = _instance; } } 130 | ``` 131 | 132 | 如上在 executeDispatch 会负责执行事件处理函数,也就是上面的 handleClick ,handleParentClick 等。这个有一个区别就是,如果是捕获阶段执行的函数,那么 listener 数组中函数,会从后往前执行,如果是冒泡阶段执行的函数,会从前往后执行,用这个模拟出冒泡阶段先子后父,捕获阶段先父后子。 133 | 134 | 还有一个细节就是如果触发了阻止冒泡事件,上述讲到事件源是 React 内部自己创建的,所以如果一个事件中执行了 e.stopPropagation ,那么事件源中就能感知得到,接下来就可以通过 event.isPropagationStopped 来判断是否阻止冒泡,如果组织,那么就会退出,这样就模拟了事件流的执行过程,以及阻止事件冒泡。 135 | 136 | ## 四 总结 137 | 138 | 以上就是新版本事件系统的原理,这里用一幅图来总结,新老版本事件系统在每个阶段的区别。 139 | 140 | ![8-6-3.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f893c626a7048bd95c7a02046f3a047~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 141 | -------------------------------------------------------------------------------- /docs/interview/性能优化.md: -------------------------------------------------------------------------------- 1 | # 性能优化面试题 2 | 3 | ## 网络层面 4 | 5 | ### DNS预解析 6 | 7 | `DNS-prefetch` 是一种 DNS 预解析技术。它会在请求跨域资源之前,预先解析并进行DNS缓存,以减少真正请求时DNS解析导致的请求延迟。对于打开包含有许多第三方连接的网站,效果明显。 8 | 9 | 添加ref属性为“dns-prefetch”的link标签。一般放在在html的head中。 10 | 11 | ```html 12 | 13 | ``` 14 | 15 | `href`的值就是要预解析的域名,对应后面要加载的资源或用户有可能打开链接的域名。 16 | 17 | ### 应用浏览器缓存 18 | 19 | 浏览器缓存是浏览器存放在本地磁盘或者内存中的请求结果的备份。当有相同请求进来时,直接响应本地备份,而无需每次都从原始服务器获取。这样不仅提升了客户端的响应效率,同时还能缓解服务器的访问压力。 20 | 21 | 其间,约定何时、如何使用缓存的规则,被称为缓存策略。分为强缓存和协商缓存。 22 | 23 | 整个缓存执行的过程大致如下: 24 | 25 | ①. 请求发起,浏览器判断本地缓存,如果有且未到期,则命中**强缓存**。浏览器响应本地备份,状态码为200。控制台Network中size那一项显示disk cache; 26 | 27 | ②. 如果没有缓存或者缓存已过期,则请求原始服务器询问文件是否有变化。服务器根据请求头中的相关字段,判断目标文件新鲜度; 28 | 29 | ③. 如果目标文件没变更,则命中**协商缓存**,服务器设置新的过期时间,浏览器响应本地备份,状态码为304; 30 | 31 | ④. 如果目标文件有变化,则服务器响应新文件,状态码为200。浏览器更新本地备份。 32 | 33 | 以Nginx举例。强缓存的配置字段是`expires`,它接受一个数字,单位是秒。 34 | 35 | ```nginx 36 | server { 37 | listen 8080; 38 | location / { 39 | root /Users/zhp/demo/cache-koa/static; 40 | index index.html; 41 | # 注意try_files会导致缓存配置不生效 42 | # try_files $uri $uri/ /index.html; 43 | expires 60; 44 | } 45 | } 46 | ``` 47 | 48 | 在响应头加上强缓存所需的`Exprise`和`Cache-Control`字段 49 | 50 | ```js 51 | app.use(async (ctx) => { 52 | // 1.根据访问路径读取指定文件 53 | const content = fs.readFileSync(`./static${ctx.path}`, "utf-8"); 54 | // 2.设置缓存 55 | ctx.response.set("Cache-Control", "max-age=60"); 56 | ctx.response.set('Exprise', new Date(new Date().getTime()+60*1000)); 57 | // 3.设置响应 58 | ctx.body = content; 59 | }); 60 | ``` 61 | 62 | ### 静态资源CDN 63 | 64 | **概念** 65 | 66 | > CDN的全称是Content Delivery Network,即[内容分发网络](https://link.juejin.cn/?target=https%3A%2F%2Fbaike.baidu.com%2Fitem%2F%E5%86%85%E5%AE%B9%E5%88%86%E5%8F%91%E7%BD%91%E7%BB%9C%2F4034265)。CDN是构建在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。 67 | 68 | 核心功效总结起来就两点: 69 | 70 | ①. 通过负载均衡技术 ,为用户的请求选择最佳的服务节点; 71 | 72 | ②. 通过内容缓存服务,提高用户访问响应速度。 73 | 74 | ### 使用高版本的HTTP协议 75 | 76 | HTTP/1.1的持久连接和管道化技术、2.0的多路复用和首部压缩 77 | 78 | ## 代码层面 79 | 80 | ### 优化DOM操作 81 | 82 | **概念** 83 | 84 | 众所周知,浏览器的渲染成本是极其昂贵的。通过合并DOM操作,可以避免频繁的触发重排重绘,以提升渲染效率。 85 | 86 | 优化DOM操作的最佳实践,莫过于大名鼎鼎的虚拟DOM。 87 | 88 | > virtual DOM *虚拟DOM*,用普通JS对象来描述DOM结构,因为不是真实DOM,所以称之为*虚拟DOM* 89 | 90 | 它的价值在于: 91 | 92 | ①. 查找 JS 对象的属性要比查询 DOM 树的开销要小; 93 | 94 | ②. 当数据驱动频繁触发DOM操作的时候,所有变化先反映在这个 JS 对象上。最终在一个宏任务(EventLoop机制)中统一执行所有变更,达成合并DOM操作的效果; 95 | 96 | ③. 可以方便的通过比较新旧两个虚拟DOM(Diff算法),最大程度的缩小DOM变更范围 97 | 98 | ### 事件委托 99 | 100 | **概念** 101 | 102 | 简单来讲,就是当我们绑定事件时,不直接绑到目标元素,而是绑到其父/祖先元素上的绑事件策略。 103 | 104 | 这样做有两个好处:①. 页面监听的事件少;②. 当新增子节点时,不需要再绑定事件。 105 | 106 | **实操** 107 | 108 | 以”鼠标放到li上对应的li背景变灰“这个需求场景举例 109 | 110 | - 正常绑事件: 111 | 112 | ```html 113 | 121 | 129 | ``` 130 | 131 | - 利用事件委托: 132 | 133 | ```js 134 | $("ul").on("mouseover", function (e) { 135 | $(e.target) 136 | .css("background-color", "#ddd") 137 | .siblings() 138 | .css("background-color", "white"); 139 | }); 140 | ``` 141 | 142 | ### 防抖和节流 143 | 144 | 防抖与节流都是为了优化单位时间内大量事件触发,存在的性能问题。它们只是效果不同,适用场景不同。 145 | 146 | - 防抖。单位时间多次连续触发,最终只执行最后的那一次。核心原理是延迟执行,期间但凡有新的触发就重置定时器。 147 | 148 | 经典应用场景:搜索框中的实时搜索,等待用户不再输入内容后再做接口查询 149 | 150 | - 节流。单位时间内事件仅触发一次。核心原理是加锁,只有满足一定间隔时间才执行。 151 | 152 | ```js 153 | function throttle(fn) { 154 | // 1、通过闭包保存一个标记 155 | let canRun = true; 156 | return function(...args) { 157 | // 2、在函数开头判断标志是否为 true,不为 true 则中断函数 158 | if(!canRun) { 159 | return; 160 | } 161 | // 3、将 canRun 设置为 false,防止执行之前再被执行 162 | canRun = false; 163 | // 4、定时器 164 | setTimeout( () => { 165 | fn.call(this, args); //如果需要立即执行,把改行移到定时器外层 166 | // 5、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true 167 | canRun = true; 168 | }, 1000); 169 | }; 170 | } 171 | ``` 172 | 173 | 经典应用场景:滚动事件等高频触发的场景;按钮防重复点击等 174 | 175 | ### 图片懒加载 176 | 177 | *图片懒加载*是针对图片加载时机的一种优化,在一些图片量比较大的网站(比如电商网站首页,或者团购网站、小游戏首页等),如果我们尝试在用户打开页面的时候,就把所有的图片资源加载完毕,那么很可能会造成白屏、卡顿等现象。 178 | 179 | 懒加载的意思就是让浏览器只加载可视区内的图片,可视区外的大量图片不进行加载,当页面滚动到后面去的时候再进行加载。避免资源浪费的同时,可以使页面加载更流畅。 180 | 181 | 图片只是载体,懒加载贯彻的是按需加载的思路。举一反三,分页查询、路由懒加载、模块异步加载,都是该类别的常用优化 182 | 183 | ## 构建层面 184 | 185 | ### 路由懒加载 186 | 187 | **概念:** 188 | 189 | > 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。 190 | 191 | **实操** 192 | 193 | 下面是VueRouter关于路由懒加载的官方示例 194 | 195 | ```js 196 | // 将 197 | // import UserDetails from './views/UserDetails' 198 | // 替换成 199 | const UserDetails = () => import('./views/UserDetails') 200 | 201 | const router = createRouter({ 202 | // ... 203 | routes: [{ path: '/users/:id', component: UserDetails }], 204 | }) 205 | ``` 206 | 207 | 核心实现就两点: 208 | 209 | ①. 使用了ES6 的动态导入方法import(),异步的加载模块; 210 | 211 | ②. 打包工具,在构建时自动识别并打包成单独的代码块。 212 | 213 | 我们还可以通过行内注释`/* webpackChunkName: "about" */`(Webpack语法),指定代码块的名称,和把多个路由源码构建到同一个块中。 214 | 215 | ``` 216 | // router.js 217 | { 218 | path: '/about', 219 | name: 'About', 220 | // route level code-splitting 221 | // this generates a separate chunk (about.[hash].js) for this route 222 | // which is lazy-loaded when the route is visited. 223 | component: () => import(/* webpackChunkName: "about" */ '../views/About.vue') 224 | } 225 | ``` 226 | 227 | 228 | 229 | ### Externals排除依赖 230 | 231 | **概念** 232 | 233 | Webpack的`externals`配置项允许我们从输出的 bundle 中排除指定依赖,排除的依赖不参与构建。 234 | 235 | 通常用于配合较大体积第三方依赖使用CDN的场景。 236 | 237 | **实操** 238 | 239 | 以在vue-cli项目中 CDN vue举例 240 | 241 | 1. 首先在public/index.html添加script引用 242 | 243 | ```html 244 | // public/index.html 245 | 246 | 247 | 248 | ... 249 | 250 | 251 | 252 | ... 253 | 254 | 255 | 复制代码 256 | ``` 257 | 258 | 2. 使用webpack配置项externals排除vue的依赖 259 | 260 | ```js 261 | // vue.config.js 262 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; 263 | 264 | module.exports = { 265 | configureWebpack:{ 266 | plugins: [ 267 | new BundleAnalyzerPlugin() // 用于输出下图中的打包分析报告 npm run build --report 268 | ], 269 | externals: { 270 | vue: 'Vue', 271 | }, 272 | } 273 | } 274 | ``` 275 | 276 | ### TreeShaking按需引入 277 | 278 | **概念** 279 | 280 | TreeShaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。 281 | 282 | 概念早就有了,实现的话则是在ES6之后。主要得益于ES6 Module模块的编译时加载,使得静态分析成为可能。 283 | 284 | Webpack 4 正式版本,扩展了该项能力。在vue-cli创建的项目中我们不需要任何额外配置,就有效果。 285 | 286 | 但,当improt第三方插件时,实际并没有生效。比如lodash 287 | 288 | ```js 289 | import debounce from 'lodash/debounce'; // 3.35kb 290 | import { debounce } from 'lodash'; // 72.48kb 291 | ``` 292 | 293 | 因为,它的生效需要满足一些条件: 294 | 295 | > - 使用 ES2015 模块语法(即 `import` 和 `export`)。 296 | > - 确保没有编译器将您的 ES2015 模块语法转换为 CommonJS 的(顺带一提,这是现在常用的 @babel/preset-env 的默认行为,详细信息请参阅[文档](https://link.juejin.cn/?target=https%3A%2F%2Fbabeljs.io%2Fdocs%2Fen%2Fbabel-preset-env%23modules))。 297 | > - 在项目的 `package.json` 文件中,添加 `"sideEffects"` 属性。 298 | > - 使用 `mode` 为 `"production"` 的配置项以启用[更多优化项](https://link.juejin.cn/?target=https%3A%2F%2Fwebpack.docschina.org%2Fconcepts%2Fmode%2F%23usage),包括压缩代码与 tree shaking。 299 | 300 | ## 进阶优化 301 | 302 | ### 服务端渲染 303 | 304 | SSR是Server Side Render(服务端渲染)的简称,与之相对应的是Client Side Render(客户端渲染)。 305 | 306 | - 服务端渲染:在服务端完成页面插值/数据组装,直接返回包含有数据的页面。 307 | - 客户端渲染:客户端分别请求页面静态资源和接口数据,然后操作DOM赋值到页面。 308 | 309 | 其实,Web世界诞生的初始,只有服务端渲染这一种方式。 那时.net、jsp如日中天,那时还只有一种程序员,不分前后端。直到Ajax技术的出现,允许人们不刷新页面的获取数据,客户端渲染的大门就此打开,一发而不可收拾。前后端分离、单页应用的流行,更是一步步的把客户端渲染的疆域推向极致。 310 | 311 | 现如今,SSR一般只存在于对首屏时间有苛刻要求、以静态内容为主和需要SEO的场景。 312 | 313 | webWorkers 314 | 315 | > web worker 是运行在后台的 JavaScript,不会影响页面的性能。 316 | 317 | 原理就是开子线程 318 | 319 | > [`Worker`](https://link.juejin.cn/?target=https%3A%2F%2Fdeveloper.mozilla.org%2Fzh-CN%2Fdocs%2FWeb%2FAPI%2FWorker)接口会生成真正的操作系统级别的线程,线程可以执行任务而不阻塞 UI 线程。 320 | 321 | 一般用于处理像密集型运算等耗费 CPU 资源的任务。 322 | 323 | **实操** 324 | 325 | 无米之炊。我这阅历并没有遇到需要Worker的场景,仅说下自己联想到的唯二信息:①.有些插件比如psfjs有这块的应用,因为它的构建结果中有xxx.worker.js;②. Node有线程相关的API(child_process),在构建的场景有较多应用。 326 | -------------------------------------------------------------------------------- /docs/interview/前沿技术/swc、esbuild和vite前端构建工具浅析.md: -------------------------------------------------------------------------------- 1 | ## 背景 2 | 3 | 过去几年里,前端又推出了一堆新的构建工具🔧,例如像以轻量快速著称的`snowpack`(目前已经不维护了,并推荐使用Vite)、编译速度超越`Babel`几十倍的`SWC`、打包和压缩资源速度惊人的`esbuild`,以及如今呼声最高,被誉为前端下一代构建工具的`Vite`。这么多的工具的推出,让以前一家独大的`webpack`突然失去的声音,甚至前端构建领域掀起了一场`去webpack`的浪潮🌊。今天就让我们简单了解下这些构建工具的代表`SWC`、`esbuild`和`Vite`。 4 | 5 | ## SWC 6 | 7 | ## 简介 8 | 9 | 受限于JS的语言本身效率的问题,近几年前端领域出现了不少工具被`Rust`重写,其中就包括编译`JS/TS`文件速度比`Babel`快不少的`SWC`,其所对标的工具就是`Babel`。 10 | 11 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2ffe0fa17a82400fa15e53379939aa7b~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 12 | 13 | `SWC`全称为`Speed Web Compiler`,其是基于Rust实现的工具,目前被很多前端知名项目(Next.js、Parcel和Deno)所使用。 14 | 15 | ## 核心功能库 16 | 17 | **@swc/cli:** CLI 命令行工具,可通过命令行编译文件。 18 | 19 | **@swc/core:** 编译转码核心的API的集合。 20 | 21 | **swc-loader:** 该模块允许您将 SWC 与 webpack 一起使用。 22 | 23 | **@swc/wasm-web:** 该模块允许您使用 WebAssembly 在浏览器内同步转换代码。 24 | 25 | **@swc/jest:** 该模块可以让jest的tranform速度更快。 26 | 27 | 而这些功能库几乎都能在Babel找到对应的库,例如`@babel/cli`、`@babel/core`、以及`babel-loader`等。也更加印证了SWC的竞争对手就是Babel。 28 | 29 | ## 功能介绍 30 | 31 | ### 编译JS文件 32 | 33 | 通过将一个es6语法的JS文件,编译为es5的语法来比较两款工具编译能力: 34 | 35 | **Babel的编译结果:** ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d36a9b9ab18a414099f8acfb97e2de2c~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 36 | 37 | **SWC的编译结果:** ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e308e1276291455f9bc24991c040efa6~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 最终SWC只花了`0.12s`,Babel花了`1.08s`,SWC的编译速度约为Babel的近9倍🚀。 38 | 39 | ### 在webpack中使用 40 | 41 | 在webpack中SWC也可以和babel掰掰手腕,SWC提供了`swc-loader`,其实也跟`babel-loader`的作用差不多。 42 | 43 | 在没有无缓存的情况下比较babel-loader与swc-loader在webpack中的编译情况: 44 | 45 | **babel-loader的编译耗时:** ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e5bce088e3354117a23398a443979241~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?) 46 | 47 | **swc-loader的编译耗时:** 48 | 49 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5d5621b385b94ce7ba485370b7045212~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?) 50 | 51 | 最终swc-loader只花了`4.89s`就完成了文件编译工作,而babel-loader花了`12.56s`,可见即使是在webpack中,swc的编译效率依旧很高。 52 | 53 | ### swcpack 54 | 55 | swc的打包能力还在建设中,目前只能在`spack.config.js`文件中进行一些简单的配置,预计在V2版本会SWC的bundle能力会有较大的提升。 56 | 57 | ``` 58 | // spack.config.js 59 | const { config } = require('@swc/core/spack') 60 | 61 | module.exports = config({ 62 | entry: { 63 | 'web': __dirname + '/src/index.ts', 64 | }, 65 | output: { 66 | path: __dirname + '/lib' 67 | }, 68 | module: {}, 69 | }); 70 | 复制代码 71 | ``` 72 | 73 | ### 配置文件 74 | 75 | swc有自己的配置文件`.swcrc`,与babel的配置文件不同的是swc的配置文件基本做到开箱即用,不需要进行对插件或预设进行二次安装。 76 | 77 | ``` 78 | { 79 | "jsc": { 80 | "parser": { 81 | // 语法,支持ecmascript和typescript 82 | "syntax": "ecmascript", 83 | // 是否解析jsx,对应插件 @babel/plugin-transform-react-jsx 84 | "jsx": false, 85 | // 动态加载 等同于 @babel/plugin-syntax-dynamic-import 86 | "dynamicImport": true, 87 | // 装饰器 等同于 @babel/plugin-syntax-decorators 88 | "decorators": false, 89 | //顶层await 等同于@babel/plugin-syntax-top-level-await 90 | "topLevelAwait": false, 91 | ... 92 | // 支持多种编译插件的配置 93 | }, 94 | // 编译目标 95 | "target": "es5", 96 | // 等同babel-preset-env的松散配置 97 | "loose": false, 98 | // 输出代码可能依赖于辅助函数来支持目标环境。 99 | "externalHelpers": false 100 | }, 101 | // 压缩代码 102 | "minify": false 103 | } 104 | 复制代码 105 | ``` 106 | 107 | 详细配置见[官网配置](https://link.juejin.cn/?target=https%3A%2F%2Fswc.rs%2Fdocs%2Fconfiguration%2Fswcrc "https://swc.rs/docs/configuration/swcrc")。 108 | 109 | ## 小结 110 | 111 | **优势:** 112 | 113 | - 编译速度快 114 | - 迁移成本低,基本可以从babel无痛迁移并能覆盖基本的使用场景。 115 | 116 | **不足:** 117 | 118 | - 生态相比于babel来说不够完善,用户的覆盖面也不高,某些场景可能会有试错成本。 119 | - 若需要深入开发的话,需要学习Rust,有较高的学习成本。 120 | - SWC虽然有bundle能力,但是bundle能力还不太完善,目前其在工程化领域更像是Compiler(编译工具)。 121 | 122 | ## esbuild 123 | 124 | ## 简介 125 | 126 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d51d171e16434fa981794c07e27d3a9d~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 127 | 128 | `esbuild`基于Golang开发的一款打包构建工具,相比传统的打包构建工具,主打性能优势,在构建速度上可以快 10~100 倍。 129 | 130 | 下图为esbuild和其他的构建工具用默认配置打包10个three.js库所花费时间的对比,我们能看见esbuild比webpack5的构建速度快了很多倍。 ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/63cbbab4f2b24adf8176182be0ca7a2f~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 131 | 132 | ## 为什么能这么快? 133 | 134 | 根据官网esbuild的FAQ给出解释,简要总结下esbuild相比于传统构建工具有以下优势: 135 | 136 | - **语言优势**。esbuild是基于go语言,传统的JS开发的构建工具并不适合资源打包这种 CPU 密集场景下,go更具性能优势。 137 | - **多线程能力**。go具有多线程运行能力,而JS本质上就是一门单线程语言。由于go的多个线程是可以共享内存的,所以可以将解析、编译和生成的工作并行化。 138 | - **从零开始**。从一开始就考虑性能,不使用第三方依赖,从始至终是使用的是一致的数据结构从而避免昂贵的数据转换。 139 | - **内存的有效利用**。webpack的工作机制在经过不同的工具链的时候,都会进行(`string => AST => string => ... => string`)string到AST的不断转换,这样实际上会占用更多的内存并降低速度。而esbuild从头到尾尽可能的共用一份AST,从而降低内存的占用,提升编译速度。 140 | 141 | ## 主要功能 142 | 143 | ### 核心API 144 | 145 | esbuild对外提供了两个核心API——tranform和build,主要功能如下: 146 | 147 | - 支持将js、ts、jsx、tsx、css等一系列文件的转译。 148 | - 支持文件监听和devServer。 149 | - 支持sourcemap。 150 | - 支持code-splitting。 151 | - 支持tree-shaking和文件压缩。 152 | - ... 153 | 154 | 详细的可见[官方文档](https://link.juejin.cn/?target=https%3A%2F%2Fesbuild.github.io%2Fapi%2F%23transform-api "https://esbuild.github.io/api/#transform-api")。 155 | 156 | ### esbuild-loader 157 | 158 | 也许我们单纯去使用esbuild去编译打包我们的项目还是比较麻烦,esbuild所提供的esbuild-loader帮我们解决了这个难题。在webpack中我们可以通过esbuild-loader体验到惊人的JS/TS文件编译的速度和高效的压缩能力。 159 | 160 | 接下来对比下在webpack环境下`esbuild-loader`和`TerserPlugin`的压缩效率: 161 | 162 | **TerserPlugin的压缩耗时:** ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/00cc62c2e33c46169789efb629db0279~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?) 163 | 164 | **esbuild-loader的压缩耗时:** ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/141d7567ef5e4e77971eebe7f53f542c~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?) 165 | 166 | 两者压缩之后的JS产物的大小几乎没有太大差别,但是`TerserPlugin`所花费的时间是`esbuild-loader`的10多倍。 167 | 168 | ## 小结 169 | 170 | **优势:** 171 | 172 | - 构建速度非常快 173 | - 压缩能力也非常强,可支持JS和CSS的压缩。 174 | 175 | **不足:** 176 | 177 | - 其tranform的API不能将产物编译到es5及以下,产物无法兼容低版本的浏览器。 178 | - 直接使用esbuild进行打包具有一定的使用成本,并且不能完全覆盖使用场景。 179 | - 在代码分割和 CSS 处理方面功能还有待完善。 180 | 181 | ## Vite 182 | 183 | ## 简介 184 | 185 | ![image.png](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/067e2f0b8f2b4a7c8e04918ea004c6c5~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) webpack的`冷启动`和`热更新速度`一直被人诟病,也由此被戏称没有一个程序员能拒绝在启动一个webpack项目的时候冲一杯咖啡☕️。而一众`基于浏览器原生ESM支持`实现的`no-bundle`构建工具的问世,让前端开发者又看到了新的希望,其中Vite又属于是`no-bundle`构建工具阵营中的集大成者。 186 | 187 | ## 特点 188 | 189 | - **快速的冷启动**:Vite基于`ESM`的支持实现了`no-bundle`服务,并且通过`esbuild`完成对依赖预构建工作。 190 | - **毫秒级的热更新**:不同于webpack的HMR(热更替),`Vite基于原生ESM实现一套HMR方案`,其可以精确锁定HMR的更新边界,无需重新构建;并利用浏览器缓存策略提升请求速度。 191 | - **开箱即用**:Vite就像CRA一样内置了对大部分文件的支持,并拥有一套,把用户的使用成本降到最低。 192 | - **强大的插件生态**:Vite继承了Rollup优秀的插件接口设计,意味着Vite用户可以利用Rollup强大的生态系统进行功能的扩展。 193 | 194 | ## 预构建的作用 195 | 196 | - **转换文件格式**:由于一些第三方依赖并没有ESM版本,而为了能在Vite上运行他们,则需要将其他格式转化为ESM的格式并缓存入 `node_modules/.vite`(默认路径)。 197 | - **减少HTTP请求**:Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。例如像lodash-es这种库里面许多单独的文件相互导入,当我们执行 `import { debounce } from 'lodash-es'` 时,浏览器同时发出 600 多个 HTTP 请求。 198 | 199 | ## 小结 200 | 201 | **优势:** 前面我们介绍Vite特点的时候就已经介绍了Vite的优势了,这边就不再赘述了。 202 | 203 | **不足:** 204 | 205 | - 项目启动或使用懒加载时页面加载时间过长 206 | 207 | `no-bundle`是一个双刃剑,虽然减少了开发环节构建时间,但是大量未经打包的ESM模块可能导致出现大量HTTP请求,从而导致页面加载会变得很慢。这也是为什么Vite在生产环境还是选择rollup进行打包,而不是直接用原生ESM模块。 208 | 209 | - 预构建影响devServer性能 210 | 211 | 由于首次预构建在devServer启动之前,若页面依赖较多预构建就会长时间阻塞devServer,从而导致页面加载时间过长;当依赖发生变化,导致页面重新加载。 212 | 213 | - 生态相比于webpack还有一定差距,不过差距正在不断缩小。 214 | 215 | 216 | ## 总结 217 | 218 | 未来肯定会有更多的人不断的开始使用这些非JS开发的前端工具链,因为JS本身语言的种种性能问题,随着前端越来越注重性能和体验,像`SWC`和`esbuild`这类工具会越来越多。并且随着浏览器兼容性越来越好,`no-bundle`可能将是未来的一个趋势,未来会有更多的开发者完善相关的生态,而像Vite这类`no-bundle`工具和传统的`bundle`类构建工具终会有一次“时代交接”。 219 | 220 | ## 附录 221 | 222 | - [swc官方文档](https://link.juejin.cn/?target=https%3A%2F%2Fswc.rs%2F "https://swc.rs/") 223 | - [esbuild官方文档](https://link.juejin.cn/?target=https%3A%2F%2Fesbuild.github.io%2F "https://esbuild.github.io/") 224 | - [webpack or esbuild: Why not both?](https://link.juejin.cn/?target=https%3A%2F%2Fblog.logrocket.com%2Fwebpack-or-esbuild-why-not-both%2F "https://blog.logrocket.com/webpack-or-esbuild-why-not-both/") 225 | - [Vite官方文档](https://link.juejin.cn/?target=https%3A%2F%2Fwww.vitejs.net%2F "https://www.vitejs.net/") 226 | - [深入理解Vite核心原理](https://juejin.cn/post/7064853960636989454 "https://juejin.cn/post/7064853960636989454") -------------------------------------------------------------------------------- /docs/sidebar.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory() : 3 | typeof define === 'function' && define.amd ? define(factory) : 4 | (factory()); 5 | }(this, (function () { 6 | 'use strict'; 7 | 8 | function styleInject(css, ref) { 9 | if (ref === void 0) ref = {}; 10 | var insertAt = ref.insertAt; 11 | 12 | if (!css || typeof document === 'undefined') { return; } 13 | 14 | var head = document.head || document.getElementsByTagName('head')[0]; 15 | var style = document.createElement('style'); 16 | style.type = 'text/css'; 17 | 18 | if (insertAt === 'top') { 19 | if (head.firstChild) { 20 | head.insertBefore(style, head.firstChild); 21 | } else { 22 | head.appendChild(style); 23 | } 24 | } else { 25 | head.appendChild(style); 26 | } 27 | 28 | if (style.styleSheet) { 29 | style.styleSheet.cssText = css; 30 | } else { 31 | style.appendChild(document.createTextNode(css)); 32 | } 33 | } 34 | 35 | var css = ".sidebar-nav > ul > li ul {\n display: none;\n}\n\n.app-sub-sidebar {\n display: none;\n}\n\n.app-sub-sidebar.open {\n display: block;\n}\n\n.sidebar-nav .open > ul:not(.app-sub-sidebar),\n.sidebar-nav .active:not(.collapse) > ul {\n display: block;\n}\n\n/* 抖动 */\n.sidebar-nav li.open:not(.collapse) > ul {\n display: block;\n}\n\n.active + ul.app-sub-sidebar {\n display: block;\n}\n"; 36 | styleInject(css); 37 | 38 | function sidebarCollapsePlugin(hook, vm) { 39 | hook.doneEach(function (html, next) { 40 | var activeNode = getActiveNode(); 41 | openActiveToRoot(activeNode); 42 | addFolderFileClass(); 43 | addLevelClass(); 44 | syncScrollTop(activeNode); 45 | next(html); 46 | }); 47 | hook.ready(function () { 48 | document.querySelector('.sidebar-nav').addEventListener('click', handleMenuClick); 49 | }); 50 | } 51 | 52 | function init() { 53 | document.addEventListener('scroll', scrollSyncMenuStatus); 54 | } 55 | 56 | var lastTop; // 侧边栏滚动状态 57 | 58 | function syncScrollTop(activeNode) { 59 | if (activeNode && lastTop != undefined) { 60 | var curTop = activeNode.getBoundingClientRect().top; 61 | document.querySelector('.sidebar').scrollBy(0, curTop - lastTop); 62 | } 63 | } 64 | 65 | function scrollSyncMenuStatus() { 66 | requestAnimationFrame(function () { 67 | var el = document.querySelector('.app-sub-sidebar > .active'); 68 | 69 | if (el) { 70 | el.parentNode.parentNode.querySelectorAll('.app-sub-sidebar').forEach(function (dom) { 71 | return dom.classList.remove('open'); 72 | }); 73 | 74 | while (el.parentNode.classList.contains('app-sub-sidebar')) { 75 | if (el.parentNode.classList.contains('open')) { 76 | break; 77 | } else { 78 | el.parentNode.classList.add('open'); 79 | el = el.parentNode; 80 | } 81 | } 82 | } 83 | }); 84 | } 85 | 86 | function handleMenuClick(e) { 87 | lastTop = e.target.getBoundingClientRect().top; 88 | var newActiveNode = findTagParent(e.target, 'LI', 2); 89 | if (!newActiveNode) return; 90 | 91 | if (newActiveNode.classList.contains('open')) { 92 | newActiveNode.classList.remove('open'); // docsify 默认行为会操作 collapse,我们异步之后修补 93 | 94 | setTimeout(function () { 95 | newActiveNode.classList.add('collapse'); 96 | }, 0); 97 | } else { 98 | removeOpenToRoot(getActiveNode()); 99 | openActiveToRoot(newActiveNode); // docsify 默认行为会操作 collapse,我们异步之后修补 100 | 101 | setTimeout(function () { 102 | newActiveNode.classList.remove('collapse'); 103 | }, 0); 104 | } 105 | 106 | syncScrollTop(newActiveNode); 107 | } 108 | 109 | function getActiveNode() { 110 | var node = document.querySelector('.sidebar-nav .active'); 111 | 112 | if (!node) { 113 | var curLink = document.querySelector(".sidebar-nav a[href=\"".concat(decodeURIComponent(location.hash).replace(/ /gi, '%20'), "\"]")); 114 | node = findTagParent(curLink, 'LI', 2); 115 | 116 | if (node) { 117 | node.classList.add('active'); 118 | } 119 | } 120 | 121 | return node; 122 | } 123 | 124 | function openActiveToRoot(node) { 125 | if (node) { 126 | node.classList.add('open', 'active'); 127 | 128 | while (node && node.className !== 'sidebar-nav' && node.parentNode) { 129 | if (node.parentNode.tagName === 'LI' || node.parentNode.className === 'app-sub-sidebar') { 130 | node.parentNode.classList.add('open'); 131 | } 132 | 133 | node = node.parentNode; 134 | } 135 | } 136 | } 137 | 138 | function removeOpenToRoot(node) { 139 | if (node) { 140 | node.classList.remove('open', 'active'); 141 | 142 | while (node && node.className !== 'sidebar-nav' && node.parentNode) { 143 | if (node.parentNode.tagName === 'LI' || node.parentNode.className === 'app-sub-sidebar') { 144 | node.parentNode.classList.remove('open'); 145 | } 146 | 147 | node = node.parentNode; 148 | } 149 | } 150 | } 151 | 152 | function findTagParent(curNode, tagName, level) { 153 | if (curNode && curNode.tagName === tagName) return curNode; 154 | var l = 0; 155 | 156 | while (curNode) { 157 | l++; 158 | if (l > level) return; 159 | 160 | if (curNode.parentNode.tagName === tagName) { 161 | return curNode.parentNode; 162 | } 163 | 164 | curNode = curNode.parentNode; 165 | } 166 | } 167 | 168 | function addFolderFileClass() { 169 | document.querySelectorAll('.sidebar-nav li').forEach(function (li) { 170 | if (li.querySelector('ul:not(.app-sub-sidebar)')) { 171 | li.classList.add('folder'); 172 | } else { 173 | li.classList.add('file'); 174 | } 175 | }); 176 | } 177 | 178 | function addLevelClass() { 179 | function find(root, level) { 180 | root.childNodes && root.childNodes.forEach(function (child) { 181 | if (child.classList && child.classList.contains('folder')) { 182 | child.classList.add("level-".concat(level)); 183 | 184 | if (window.$docsify && window.$docsify.sidebarDisplayLevel && typeof window.$docsify.sidebarDisplayLevel === 'number' && level <= window.$docsify.sidebarDisplayLevel) { 185 | child.classList.add('open'); 186 | } 187 | 188 | if (child && child.childNodes.length > 1) { 189 | find(child.childNodes[1], level + 1); 190 | } 191 | } 192 | }); 193 | } 194 | 195 | find(document.querySelector('.sidebar-nav > ul'), 1); 196 | } 197 | 198 | init(); 199 | 200 | var css$1 = "@media screen and (max-width: 768px) {\n /* 移动端适配 */\n .markdown-section {\n max-width: none;\n padding: 16px;\n }\n /* 改变原来按钮热区大小 */\n .sidebar-toggle {\n padding: 0 0 10px 10px;\n }\n /* my pin */\n .sidebar-pin {\n appearance: none;\n outline: none;\n position: fixed;\n bottom: 0;\n border: none;\n width: 40px;\n height: 40px;\n background: transparent;\n }\n}\n"; 201 | styleInject(css$1); 202 | 203 | var PIN = 'DOCSIFY_SIDEBAR_PIN_FLAG'; 204 | 205 | function init$1() { 206 | // 响应式尺寸 @media screen and (max-width: 768px) 207 | if (document.documentElement.clientWidth > 768) return; 208 | localStorage.setItem(PIN, false); // 添加覆盖标签 209 | 210 | var btn = document.createElement('button'); 211 | btn.classList.add('sidebar-pin'); 212 | btn.onclick = togglePin; 213 | document.body.append(btn); 214 | window.addEventListener('load', function () { 215 | var content = document.querySelector('.content'); // 点击内容区域收起侧边栏 216 | 217 | document.body.onclick = content.onclick = function (e) { 218 | if (e.target === document.body || e.currentTarget === content) { 219 | if (localStorage.getItem(PIN) === 'true') { 220 | togglePin(); 221 | } 222 | } 223 | }; 224 | }); 225 | } 226 | 227 | function togglePin() { 228 | var pin = localStorage.getItem(PIN); 229 | pin = pin === 'true'; 230 | localStorage.setItem(PIN, !pin); 231 | 232 | if (pin) { 233 | document.querySelector('.sidebar').style.transform = 'translateX(0)'; 234 | document.querySelector('.content').style.transform = 'translateX(0)'; 235 | } else { 236 | document.querySelector('.sidebar').style.transform = 'translateX(300px)'; 237 | document.querySelector('.content').style.transform = 'translateX(300px)'; 238 | } 239 | } 240 | 241 | init$1(); 242 | 243 | function install() { 244 | if (!window.$docsify) { 245 | console.error('这是一个docsify插件,请先引用docsify库!'); 246 | } else { 247 | for (var _len = arguments.length, plugins = new Array(_len), _key = 0; _key < _len; _key++) { 248 | plugins[_key] = arguments[_key]; 249 | } 250 | 251 | $docsify.plugins = plugins.concat($docsify.plugins || []); 252 | } 253 | } 254 | 255 | install(sidebarCollapsePlugin); 256 | 257 | }))); -------------------------------------------------------------------------------- /docs/interview/sourcemap.md: -------------------------------------------------------------------------------- 1 | **sourcemap原理和实践** 2 | 3 | ## **sourcemap的配置分析** 4 | 5 | webpack 的 sourcemap 配置比较麻烦,但其实也是有规律的。 6 | 7 | 它是对一些基础配置按照一定顺序的组合,理解了每个基础配置,知道了怎么组合就理解了各种 devtool 配置。 8 | 9 | - eval:浏览器 devtool 支持通过 sourceUrl 来把 eval 的内容单独生成文件,还可以进一步通过 sourceMappingUrl 来映射回源码,webpack 利用这个特性来简化了 sourcemap 的处理,可以直接从模块开始映射,不用从 bundle 级别。 10 | - cheap:只映射到源代码的某一行,不精确到列,可以提升 sourcemap 生成速度 11 | - source-map:生成 sourcemap 文件,可以配置 inline,会以 dataURL 的方式内联,可以配置 hidden,只生成 sourcemap,不和生成的文件关联 12 | - nosources:不生成 sourceContent 内容,可以减小 sourcemap 文件的大小 13 | - module: sourcemap 生成时会关联每一步 loader 生成的 sourcemap,配合 sourcemap-loader 可以映射回最初的源码 14 | 15 | ## **sourcemap原理** 16 | 17 | sourcemap确定映射关系需要 8 个要素,8 个元素组成了一个映射段: 编译后文件|编译后变量起始行|编译后变量起始列|编译后变量名|源文件|源代码起始行|源代码起始列|源代码变量名,在 sourcemap 中,通过一个 mappings 字段,将所有映射段以 , 链接形成一个字符串,那这个字符串就可以确定完整的代码映射. 18 | 19 | - 编译后文件:一个编译后文件只会指向一个 sourcemap 文件,所以在一个 sourcemap 文件中,编译后文件名都是一样的,sourcemap 标准中就是用一个 file 字段记录下编译后文件名称,就可以在映射段去除掉编译后文件这个要素。 20 | - 编译后变量起始行:我们解析编译后的代码,都是从头到尾按照顺序来的,也就是说行也是从头到尾按照顺序解析的。那么这种顺序结构,我们通常可以用数组记录,例如第一行编译后代码的映射放到数组下标 0、第二行放到下标 1……而 sourcemap 标准中所有映射段都放在一个字符串中,采用 ; 分隔每一行的映射,就可以去掉映射段中的编译后代码起始行。 21 | - 编译后变量起始列:许多时候,编译后代码只有一行,列可能达到几万甚至几十万,意味着到了靠后的映射段我们需要一个很大的数字去记录起始列。我们知道编译后的代码是从头到尾按顺序的,那么根据这个思路,我们可以使用增量来记录,即记录的是当前这个变量相对于同一行上一个变量的所在的起始列的增量,例如function print(variable),print 起始列下标为 9,variable 其实列下标是 11,那么增量就是 variable 相对于 print 的起始列增量就是 2,我们记录 2 来代替 11。 22 | - 编译后变量名:我们都知道,js 变量名都是以字母、数字、$ 或者 _ 组成的连续字符串(不能以数字开始),我们已经有了编译后的代码和编译后变量的起始列,自然知道编译后的变量名是什么,所以不需要再映射段中记录编译后变量名. 23 | - 源文件:因为一个 sourcemap 只对应一个编译后文件,那源文件能省略吗?答案是不能。以 webpack 为例,打包过程中可能将多个源文件打包到一个 chunk 中(编译后文件),也就是说一个 sourcemap 对应多个编译前文件,所以源文件的信息我们需要记录。但是我们可以不必再每个映射段中都记录源文件,因为通常很多个映射段对于一个源文件,所以我们可以将源文件作为一个数组提出来,sourcemap 用 sources 字段记录了源文件的数组,然后每个映射段记录的是对应源文件在 sources 数组总的下标 24 | - 源代码起始行:mappings 中映射段的顺序是按照编译后代码顺序来的,其对应的编译前代码不一定是按序的,所以源代码的起始行我们无法通过数组或者 ; 等形式省略,但是我们同样可以用相对于上一个映射段中源代码起始行的相对增量来记录行数,以减小记录的行数位数. 25 | - 源代码变量名:上面我们通过已知编译后代码和编译后变量的起始位置得到了编译后变量名,所以可以在映射段中省略编译后变量名的记录。但是我们是不知道源代码具体内容的,所以我们无法省略源代码变量名。但是同源文件的思路一样,源代码中同一个变量是被多次使用的,所以 sourcemap 用 names 字段记录了源代码中的变量数组,然后在映射段中记录对应变量名在 names 数组中下标。 26 | 27 | 然后进行进一步精简,采用base64 VLQ 编码 28 | 29 | ![img](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/imgs/out-20231229142742646.png) 30 | 31 | VLQ 通过 6 位二进制数进行存储,第一位表示连续位(1 表示连续,0 表示不连续,也就是说如果是 1 代表这部分还没结束),最后一位表示是正数还是负数(1 是负数,0 是正数),中间的 4 位用了存储数据,所以一个 6 位二进制存储的范围是 [-15, 15],超过就需要用连续 6 位二进制数(连续的 6 位二进制数从第二个开始不需要记录是正负数了,所以第二个之后后 5 位存储数据) 32 | 33 | 一个转换网站 34 | 35 | https://www.murzwin.com/base64vlq.html 36 | 37 | ## **sourcemap的工具** 38 | 39 | 例如restore-source-tree,reverse-sourcemap这两个工具包 可以利用.map文件还原出源代码 40 | 41 | 42 | 43 | ``` 44 | reverse-sourcemap --output-dir sourceCode test.js.map 45 | restore-source-tree --out-dir sourceRestore test.js.map 46 | ``` 47 | 48 | 49 | 50 | **sourcemap实践** 51 | 52 | 实现一个本地批量转换.map文件的函数 53 | 54 | 55 | 56 | ``` 57 | const fs = require('fs') 58 | const { existsSync, mkdirSync, writeFileSync } = require('fs') 59 | const path = require('path') 60 | const sourceMap = require('source-map') 61 | const https = require('https') 62 | const execa = require('execa') 63 | /* const target = 'https://s2-12537.kwimgs.com/kos/nlav12537/micro/original/static/polaris-original/js/908.0a7e0f1e.js' // searchBtnClick 342 12 实际是391行 64 | const lineNumber = 605 65 | const columnNumber = 1183 66 | const afterLineNumber = 1 67 | const beforeLineNumber = 1 68 | 69 | // const target = 'https://s2-12537.kwimgs.com/kos/nlav12537/public/js/vendors.28ca3a66.js' 70 | // const lineNumber = 2 71 | // const columnNumber = 5142356 72 | const afterLineNumber = 3 73 | const beforeLineNumber = 3 74 | 75 | 76 | //https://s2-12537.kwimgs.com/kos/nlav12537/public/js/chunk/index/index.1fd0f6fc.js:1:174034 77 | /* const target = 'https://s2-12537.kwimgs.com/kos/nlav12537/public/js/chunk/2.4955c113.js' 78 | const lineNumber = 1 79 | const columnNumber = 1000 */ 80 | /* const target = 'https://s2-12537.kwimgs.com/kos/nlav12537/public/js/chunk/vendors.ba41ee0c.js' 81 | const lineNumber = 5990 82 | const columnNumber = 99740 83 | const afterLineNumber = 1 84 | const beforeLineNumber = 1 */ 85 | var filepath = path.join(__dirname, './1.js.map'); 86 | 87 | 88 | async function fetch(url) { 89 | return new Promise((resolve, reject) => { 90 | https.get(url, function (response) { 91 | let data = ''; 92 | if (response.statusMessage !== 'OK') { 93 | console.log('ERROR: ', response.statusCode, ' ', response.statusMessage); 94 | reject(); 95 | } 96 | response.setEncoding('utf8'); 97 | response.on('data', function (chunk) { 98 | data += chunk; 99 | }); 100 | response.on('end', function () { 101 | resolve(JSON.parse(data)); 102 | }); 103 | }); 104 | }); 105 | } 106 | async function getSourceCodePosition(target, lineNumber, columnNumber) { 107 | const rawSourceMap = await fetch(target + '.map'); 108 | console.log(rawSourceMap) 109 | let sourceCodePosition = {}; 110 | SourceMapConsumer.with(rawSourceMap, null, (consumer) => { 111 | // 获取函数 `myFunction` 的位置信息 112 | sourceCodePosition = consumer.originalPositionFor({ 113 | line: lineNumber, 114 | column: columnNumber, 115 | }); 116 | console.log(sourceCodePosition); // 案例 { source: 'test.js', line: 1, column: 14, name: 'add' } 117 | }); 118 | 119 | return sourceCodePosition 120 | } 121 | 122 | function createSourceMapConsumer(sourceMapCode) { 123 | const consumer = new sourceMap.SourceMapConsumer(sourceMapCode) 124 | return consumer 125 | } 126 | async function getSourcesBySourceMapCode(sourceMapCode) { 127 | const consumer = await createSourceMapConsumer(sourceMapCode) 128 | const { sources } = consumer 129 | const result = sources.map((source) => { 130 | return { 131 | source, 132 | code: consumer.sourceContentFor(source) 133 | } 134 | }) 135 | return result 136 | } 137 | async function outPutSources( 138 | sources, 139 | outPutDir = 'source-map-result' 140 | ) { 141 | for (const sourceItem of sources) { 142 | const { source, code } = sourceItem 143 | const filepath = path.resolve(process.cwd(), outPutDir, source) 144 | if (!existsSync(path.dirname(filepath))) { 145 | mkdirSync(path.dirname(filepath), { recursive: true }) 146 | } 147 | writeFileSync(filepath, code, 'utf-8') 148 | } 149 | } 150 | 151 | 152 | async function parse(target, lineNumber, columnNumber, afterLineNumber, beforeLineNumber) { 153 | const rawSourceMap = await fetch(target + '.map'); 154 | fs.writeFileSync(filepath, JSON.stringify(rawSourceMap)); 155 | const sourceCode = await getSourcesBySourceMapCode(rawSourceMap) 156 | console.log(sourceCode.length, '个文件') 157 | outPutSources(sourceCode) 158 | const consumer = await new sourceMap.SourceMapConsumer(rawSourceMap); 159 | // 传入要查找的行列数,查找到压缩前的源文件及行列数 160 | const sm = consumer.originalPositionFor({ 161 | line: lineNumber, // 压缩后的行数 162 | column: columnNumber, // 压缩后的列数 163 | }); 164 | if (!sm.source || !sm.line) { 165 | throw new Error('没有找到源文件'); 166 | } 167 | // 压缩前的所有源文件列表 168 | const sources = consumer.sources; 169 | // 根据查到的source,到源文件列表中查找索引位置 170 | const smIndex = sources.indexOf(sm.source); 171 | // 到源码列表中查到源代码 172 | const smContent = consumer.sourcesContent[smIndex]; 173 | // 将源代码串按"行结束标记"拆分为数组形式 174 | const rawLines = smContent.split(/\r?\n/g); 175 | console.log('111', sm.source); 176 | console.log('222', sm); 177 | console.log('333', consumer.sourcesContent.length); 178 | /* const code = JSON.stringify(consumer.sourcesContent.map(item => item.split(/\r?\n/g))); 179 | //const buffer = Buffer.from(consumer.sourcesContent); 180 | fs.writeFile('test.js', code, { encoding: 'utf-8' }, (err) => { 181 | if (err) throw err; 182 | console.log('File saved!'); 183 | }); */ 184 | // 输出源码行,因为数组索引从0开始,故行数需要-1 185 | for (let i = -1 * beforeLineNumber; i <= afterLineNumber; i++) { 186 | /* if (i === 0) { 187 | console.log(i, sm.line + i, "66666", rawLines[sm.line + i]); 188 | } */ 189 | console.log(i, sm.line + i, rawLines[sm.line + i - 1]); 190 | } 191 | console.log(sm.line, rawLines[sm.line - 1]); 192 | const str = 'webpack:///src/store/modules/message-configure.ts'; 193 | const regex = /webpack:\/\/\/(.+?)(?:\?.*)?$/; 194 | const match = sm.source.match(regex); 195 | const path = match ? match[1] : null; 196 | const encodedFilePath = encodeURIComponent(path); 197 | console.log(path); 198 | console.log(encodedFilePath) 199 | } 200 | parse(target, lineNumber, columnNumber, afterLineNumber, beforeLineNumber) 201 | execa('reverse-sourcemap', ['--output-dir', 'sourceCode', '1.js.map']); 202 | // 输出 "/src/store/modules/message-configure.ts" 203 | // 对线上的 204 | /* 205 | { 206 | source: 'webpack:///src/page/reach/component/step-content-smart-to-call.vue?cd5a', 207 | line: 1, 25 208 | column: 736, 30 209 | name: null 210 | } 211 | ``` 212 | -------------------------------------------------------------------------------- /docs/interview/前沿技术/大前端技术.md: -------------------------------------------------------------------------------- 1 | ## 跨端技术对比分析 2 | 3 | ### 一、前端三板斧 4 | 5 | 正式讨论「跨端开发」这个概念前,我们可以先思考一个问题:对大部分前端工作来说,前端主要干些啥? 6 | 7 | 我个人认为,无论环境怎么变,前端基本上就是做三件事情: 8 | 9 | ![Fetch Data、Manage State、Render Page](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3d830831be5f4872bafeb8b6538a2143~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 10 | 11 | - fetch data(数据获取) 12 | - manage state(状态管理) 13 | - render page(页面渲染) 14 | 15 | 没了。 16 | 17 | 也许有人觉得我说的太片面,其实我们可以理一理。往近了说,现在知识付费搞的如火如荼,动不动就搞个「XXX 源码解析」,分析一下这些课程的主题和目录,你就会发现基本都是围绕着这三个方向展开讲的;往远了说,我们可以分析一下 Web 前端的发展历程: 18 | 19 | - 1995 年左右,用 HTTP/1.0 拉取数据,用第一版的 JavaScript 管理几个前端状态,用裸露的 HTML 标签展示页面 20 | - 2005 年左右,用 HTTP/1.1 和 AJAX 拉取数据,用 JavaScript 做做表单画画特效,用 CSS 美化页面 21 | - 2010 年左右,用 HTTP/1.1 和 AJAX 拉取数据,用 jQuery 操作 DOM 处理前端逻辑,用 CSS 美化页面 22 | - 2015 年左右,随着 HTML5 标准的推广和浏览器性能的提升,前端开始进入「**学不动了**」的时代: 23 | - 在 fetch data 层面,除了 HTTP/1.1 和 AJAX,HTTPS 来了,HTTP/2 来了,WebSocket 也来了 24 | - 在 manage state 层面,Angular、React 和 Vue 先后出现,从现在看,React 的状态驱动视图的理念直接影响了 Flutter 和 SwiftUI 的设计 25 | - 在 render page 层面,除了传统的 HTML + CSS,还加入了 CSS3、Canvas 等概念,音视频功能也得到加强 26 | - 最近几年,网络协议趋于稳定,几年内也不会有啥大的变动;国内 React 和 Vue 的地位基本稳固,一堆前端盯着 GitHub 进度条等版本更新;render 层出了不少幺蛾子,好不容易摆脱了 IE6,又来了各种小程序,同一套业务逻辑写好几遍不经济也不现实,这时候各种跨端方案就整出来了 27 | 28 | 经过一番分析,这个三板斧理论看上去已经有些道理了,我们顺着这个方向再向底层思考:这三大功能是怎么实现的? 29 | 30 | - fetch data 方向,最后要靠网络协议栈把数据发出去,但是让一个前端直接搞套接字编程是非常不现实的,所以我们需要把网络操作封装为库,让应用层调用 31 | - render page 方向,最后是把相关图元信息通过各种图形 API(OpenGL/Metal/Vulkan/DirectX)发给 GPU 进行渲染,很多前端的图形学路程最终都止于一个三角形,用这套技术栈去画 UI 也极其不现实,更不要说排版系统这种工程量浩大的工作,所以这些活儿都让相关的**渲染引擎**做了 32 | - manage state 方向,你可以用全局变量管理状态,最后的结局一定被同事打爆,现在主流方案都是采用各种框架和 runtime 进行状态管理,而这个 runtime 的宿主环境,往往就是某个语言的**虚拟机**,同时,fetch data 的起点,也是同一个虚拟机 33 | 34 | ![虚拟机 渲染引擎](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a75c7240a34545ddaffcfb91b0c7c3d7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 35 | 36 | 经过上面的分析我们可以看出,前端的主要技术核心就两个:**虚拟机**和**渲染引擎**,这也意味着,**如果我们想要搞跨端开发,就必须得统一虚拟机和渲染引擎**。 37 | 38 | ### 二、虚拟机和渲染引擎 39 | 40 | #### 1.网页:JS Engine + WebKit 41 | 42 | ![前端三剑客](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3107a6604068499fa3c9dbd5f1a0586e~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 43 | 44 | > 因为谷歌的 Blink 引擎 fork 自苹果的 WebKit,后文为了描述方便,统一用 WebKit 代替浏览器渲染引擎 45 | 46 | 网页是成本最低上手最快的跨端方案了。得益于互联网开放式理念,网页天生就是跨端的,无论什么渲染框架,WebView 都是必不可少的核心组件。 47 | 48 | 开发人员的接入成本也极低,主要技术就是 Web 开发那一套,前端主要头疼的是各个渲染引擎的**适配问题**和**性能问题**。 49 | 50 | 现在主流的 JS Engine 是苹果的 JavaScriptCore 和谷歌的 V8,主流的渲染引擎是苹果的 Webkit 和谷歌的 Blink。虽然 W3C 的规范就摆在那里,各个浏览器厂商再根据规范实现浏览器,这也是网页跨端的基础。问题在于浏览器内核实现总有细微差距,部分实现不合规范,部分实现本身就有 Bug,这也是前端摆脱不了适配需求的本质原因。 51 | 52 | 另一个是性能问题。其实 WebKit 本身的渲染速度还是很快的,但是受限于一些浏览器特性,比如说极其复杂极其动态的 CSS 属性,DOM 树和 CSSOM 的合并,主线程必须挂起等待 JS 的执行,这些都会大大降低性能,前端搞性能优化,一般得依据这些浏览器特性进行减枝处理,但是再怎么优化,在页面性能和交互体验上,和 Native 还是有很大的距离。 53 | 54 | #### 2.网页 PLUS:JS Engine + WebKit + Native 能力 55 | 56 | 直接拿个 URL 扔到 WebView 里是最简单的,其实这样也能解决大部分问题,毕竟前端 90% 的工作都是画 UI 写业务逻辑,但是还有 10% 的功能做不到,比如说要和 Native 同步状态,调用一些系统功能。 57 | 58 | 要实现客户端和网页双向通讯的话,一般都是借助 **JSBridge** 进行通信,[《JSBridge 的原理》](https://juejin.cn/post/6844903585268891662)这篇文章总结的不错,感兴趣的同学可以看一下。 59 | 60 | JSBridge 只是解决了 Native 和 Web 的互相调用问题,如果我想借助 Native 加强 Web 怎么办?这时候就有了一些探索: 61 | 62 | - **预热**:提前创建和初始化 WebView,甚至实现 WebView 容器池,减少 WebView 的启动时间 63 | - **缓存**:把常用的 Web 资源预先存在 Native 本地,然后拦截浏览器网络请求重定向到本地,这样就可以加快 Web 的资源加载速度(也叫“离线包”方案); 64 | - **劫持**:比如说 Web 对网络加载的控制力比较弱,部分有能力的厂商会把所有的网络请求都劫持下来交给 Native 去做,这样做可以更灵活的管理 Web 请求 65 | - **替换**:替换一般指替换 Web 的 `Img` 标签和 `Video` 标签,这个最常见的地方就是各大新闻类客户端。因为新闻的动态性和实时性,新闻都是由各个编辑/自媒体通过后台编辑下发的,这时候要利用 **Web 强大的排版功能**去显示文本内容;但是为了加载速度和观看体验,图片和视频都是 Native 组件替换的 66 | 67 | 经过上面几步,网页的速度基本可以达到秒开的级别,这里面最典型的就是几大新闻客户端,大家可以上手体验一下。 68 | 69 | #### 3.小程序:JS Engine + WebKit 70 | 71 | ![各大小程序平台](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/81379300095e4ed582fcc36a66773ab7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 72 | 73 | 小程序,国内的特色架构,本质上是微信成为流量黑洞后,想成为流量分发市场管理和分发自己的流量,所以这是个商业味道很重的框架。 74 | 75 | 小程序在技术上没什么特别的创新点,**本质上就是阉割版的网页**,所以微信小程序出来后各个流量寡头都推出了自己的小程序,正如有人吐槽的,[小程序的实现方式有 9 种](https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F418571461%2Fanswer%2F1445614841),底层实现多样化,各个厂实现还没有统一的标准,最后就是给开发者喂屎,我也没啥好介绍的,就这样吧。 76 | 77 | #### 4.React Native:JS Engine + Native RenderPipeLine 78 | 79 | ![React Native 和 Hermes](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b0e7ca9cfe5c4cc0bbd7cb0170b1b31a~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 80 | 81 | React 2013 年发布,两年后 React Native 就发布了,前几种跨段方案基本都是基于浏览器技术的,RN 这个跨段方案的创新性在于它保留了 JS Engine,在渲染引擎这条路上,他没有自己造轮子,而是复用了现有的 Native 渲染管线。 82 | 83 | 这样做的好处在于,保留 JS Engine,可以最大程度的复用 Web 生态,毕竟 GitHub 上轮子最多的语言就是 JavaScript 了;复用 Native RenderPipeLine,好处在于脱离 WebKit 的历史包袱,相对来说渲染管线更短,性能自然而然就上去了。 84 | 85 | 那么问题来了,RN 是如何做到跨端的?这个其实全部仰仗于 React 的 vdom。 86 | 87 | **vdom** 88 | 89 | 前端社区上有些文章讨论 vdom,总会从性能和开发便捷性上切入讲解,从纯 Web 前端的角度看,这些的确是 vdom 的特点,但是这不是 vdom 真正火起来的原因。vdom 更大的价值在于,**人们从 vdom 身上看到跨端开发的希望**,所以在 React 出现后 React Native 紧跟着出现是一件非常自然的事情。为什么这么说?这个就要先溯源一下 UI 开发的范式。 90 | 91 | UI 开发主要有两大范式:**[Immediate Mode GUI(立即模式)](https://link.juejin.cn/?target=https%3A%2F%2Fwww.wikiwand.com%2Fen%2FImmediate_mode_(computer_graphics))** 和 **[Retained Mode GUI(保留模式)](https://link.juejin.cn/?target=https%3A%2F%2Fwww.wikiwand.com%2Fen%2FRetained_mode)**。 92 | 93 | 简单来说,IMGUI 每帧都是全量刷新,主要用在实时性很高的领域(游戏 CAD 等);RMGUI 是最广泛的 UI 范式,每个组件都被封装到一个对象里,便于状态管理和复杂的嵌套布局。无论是网页、iOS、Android 还是 Qt 等桌面开发领域,都是基于 RMGUI 的。这两者的具体细节差异,可以看这篇[知乎回答](https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F39093254%2Fanswer%2F1351958747)和这个 [Youtube 视频](https://link.juejin.cn/?target=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DZ1qyvQsjK5Y%26t%3D4s)。 94 | 95 | 我们再回到 React Native 中,既然 iOS Android 的原生渲染管线都是 RMGUI 范式,那么总是有相似点的,比如说 UI 都是树状嵌套布局,都有事件回调等等。这时候 vdom 的作用就出来了: 96 | 97 | vdom 作为一个纯对象,可以清晰的提炼出出布局的嵌套结构,而且**这个抽象描述是平台无关的**,那么我们就可以利用 JS 生成 vdom,然后将 vdom 映射到 Native 的布局结构上,最终让 Native 渲染视图,以达到跨平台开发的目的。 98 | 99 | 到这里如果你有些编译原理的知识,你就会发现 vdom 和 **IR** 有些类似,同样都是抽象于平台的中间态,vdom 上接 React 下接 Native RenderPipeLine,IR 上接编译器前端下接编译器后端,我们只要关心前半段的逻辑处理,脏活累活都让后半部分做。 100 | 101 | **Hermes** 102 | 103 | 2019 年 Facebook 为了优化 React Native 的性能,直接推出了新的 JS Engine——**Hermes**,[FB 官方博文](https://link.juejin.cn/?target=https%3A%2F%2Fengineering.fb.com%2F2019%2F07%2F12%2Fandroid%2Fhermes%2F)介绍了很多的优点,我个人认为最大的亮点是加入 AOT,传统的 JS 加工加载流程是这样的: 104 | 105 | ``` 106 | Babel 语法转换` → `Minify 代码压缩` → `install 下载代码` → `Parse 转为 AST` → `Compile 编译` → `Execute 执行 107 | ``` 108 | 109 | Hermes 加入 AOT 后,`Babel`、`Minify`、`Parse` 和 `Compile` 这些流程全部都在开发者电脑上完成,直接下发字节码让 Hermes 运行就行。 110 | 111 | ![Bytecode precompilation with Hermes](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b8739878bc9840068f0b8478bb3c4859~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 112 | 113 | 这样做的好处在于,可以大大缩短 JS 的编译时间,不信的话大家可以用 Chrome 分析几个大型网站,JS 的解析加载时间基本占时都是 50% 以上,部分重型网站可能占时 90%,这对桌面应用来说还好,对于电量和 CPU 都要弱上一线的移动平台来说,这些都是妥妥的性能杀手,Hermes 的加入可以大大改善这一情况。 114 | 115 | 目前 React Native 0.64 也支持 Hermes 了,如果有做 RN 业务的同学可以玩一玩,看看在 iOS 上的性能提升有多大。 116 | 117 | #### 5.Flutter: Dart VM + Flutter RnderPipeLine 118 | 119 | ![Flutter 和 Dart](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f3d2207d23704d2b8c2c419efc685a54~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 120 | 121 | Flutter 是最近比较火的一个跨端方案,也有不少人认为这是最终的跨端方案,毕竟桌面软件时代,最终胜出跨端方案就是 Qt,他们的共同特点就是自带了一套渲染引擎,可以抹平终端差异。 122 | 123 | Flutter 的创造还是很有意思的,[这里](https://link.juejin.cn/?target=https%3A%2F%2Fzhuanlan.zhihu.com%2Fp%2F52666477)有个 Eric 的访谈,视频中说 Eric 差不多有十几年的 Web 渲染领域工作经验,有一次在 Chrome 内部他们做了个实验,把一些乱七八糟的 Web 规范去掉后,一些基准测试甚至能**快 20 倍**,因此 Google 内部开始立项,Flutter 的出现了。至于 Flutter 选择 Dart 的理由,坊间一直传说 **Flutter 开发组隔壁就是 Dart 开发组**,离得近就好 PY 交易,反正 Dart 也没人用,没啥历史包袱,可以很好的相应 Flutter 的需求。 124 | 125 | Flutter 的架构也是比较清晰的: 126 | 127 | - 虚拟机用的 Dart VM,Dart 同时支持 JIT 和 AOT,可以同时保证开发效率和运行效率 128 | - 渲染引擎先把 Dart 构建的视图数据传递给 Skia,然后 Skia 加工数据交给 OpenGL/Metal 这两个图形 API,最终交给 GPU 渲染,整体上比 WebKit 的渲染流水线清晰不少 129 | 130 | 从纯粹程度上看,Flutter 是做的最彻底的,虚拟机和渲染引擎都没有用业内的成熟方案,而是自造了一套,好处就是没啥适配压力,坏处就是太新了,业务开发时往往会遇到无轮子可用的尴尬状态,如果谷歌大力推广,国内大厂持续跟进,前景还是很光明的。 131 | 132 | #### 6.其它方向的探索:JS Engine + Flutter RnderPipeLine? 133 | 134 | 社区里有一种声音,认为 Flutter 最大的败笔就是不能用 JavaScript 开发。这时候就会有人想,如果我们把 Web 技术和 Flutter 技术结合起来,用 JS Engine 对接世界上最大最活跃的 JS 社区,用 Flutter 渲染引擎对接高性能渲染体验,国安民乐,岂不美哉? 135 | 136 | 目前来说一些大厂还是做了一些探索,我看了一些分析和项目架构,感觉就是做了个低配版的 React Native,React Native 的现有架构有一个性能瓶颈就是跨语言调用成本比较高,而这些大厂的调用链路多达 4 步:`JS` -> `C++` -> `Dart` -> `C++`,更加丧心病狂,目前看无论是上手和推广都是没有直接用 RN or Flutter 方便。 137 | 138 | ### 三、各跨端方案的不足之处 139 | 140 | 跨端方案不可能只有好处的,各个方案的坏处也是很明显的,我下面简单列一下: 141 | 142 | - **网页**:性能是个过去不的坎儿,而且 Apple 明确指出不欢迎 WebView 套壳 APP,有拒审危险 143 | - **网页 PLUS**:技术投入很高,基本只能大厂玩转 144 | - **小程序**:对开发者不友好,技术半衰期极短 145 | - **React Native**:基本只能画 UI,一旦做深了,只会 JS 根本解决不了问题,Java OC 都得学,对开发者要求比较高 146 | - **Flutter**:Android 支持很好,但 iOS 平台的交互割裂感还是很强的,而且和 RN 问题一样,一旦做深了,必须学习客户端开发知识,对开发者要求比较高 147 | 148 | 总的来说,在牺牲一定用户体验的前提下,跨端方案可以提高开发者的开发效率和公司的运行效率,我个人认为,只要某个方案的 ROI 比较高,其实是还是可以投入到生产的。 149 | 150 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/13.优化篇-处理海量数据.md: -------------------------------------------------------------------------------- 1 | ## 前言 2 | 3 | 本章节将要介绍一下 React 对于大量数据的处理方案,对于项目中大量数据通常存在两种情况: 4 | 5 | - 第一种就是**数据可视化**,比如像热力图,地图,大量的数据点位的情况。 6 | - 第二种情况是**长列表渲染**。 7 | 8 | 接下来将重点围绕这两点展开讨论,通过本章节,将收获 React 应用处理大量数据的解决方案。 9 | 10 | ## 实践一 时间分片 11 | 12 | 时间分片主要解决,初次加载,一次性渲染大量数据造成的卡顿现象。**浏览器执 js 速度要比渲染 DOM 速度快的多。**,时间分片,并没有本质减少浏览器的工作量,而是把一次性任务分割开来,给用户一种流畅的体验效果。就像造一个房子,如果一口气完成,那么会把人累死,所以可以设置任务,每次完成任务一部分,这样就能有效合理地解决问题。 13 | 14 | 所以接下来实践一个时间分片的 demo ,一次性加载 20000 个元素块,元素块的位置和颜色是随机的。首先假设对 demo 不做任何优化处理。 15 | 16 | 色块组件: 17 | 18 | ```js 19 | /* 获取随机颜色 */ 20 | function getColor(){ 21 | const r = Math.floor(Math.random()*255); 22 | const g = Math.floor(Math.random()*255); 23 | const b = Math.floor(Math.random()*255); 24 | return 'rgba('+ r +','+ g +','+ b +',0.8)'; 25 | } 26 | /* 获取随机位置 */ 27 | function getPostion(position){ 28 | const { width , height } = position 29 | return { left: Math.ceil( Math.random() * width ) + 'px',top: Math.ceil( Math.random() * height ) + 'px'} 30 | } 31 | /* 色块组件 */ 32 | function Circle({ position }){ 33 | const style = React.useMemo(()=>{ //用useMemo缓存,计算出来的随机位置和色值。 34 | return { 35 | background : getColor(), 36 | ...getPostion(position) 37 | } 38 | },[]) 39 | return
40 | } 41 | ``` 42 | 43 | - 子组件接受父组件的位置范围信息。并**通过 useMemo 缓存计算出来随机的颜色,位置**,并绘制色块。 44 | 45 | 父组件: 46 | 47 | ```json 48 | class Index extends React.Component{ 49 | state={ 50 | dataList:[], // 数据源列表 51 | renderList:[], // 渲染列表 52 | position:{ width:0,height:0 } // 位置信息 53 | } 54 | box = React.createRef() 55 | componentDidMount(){ 56 | const { offsetHeight , offsetWidth } = this.box.current 57 | const originList = new Array(20000).fill(1) 58 | this.setState({ 59 | position: { height:offsetHeight,width:offsetWidth }, 60 | dataList:originList, 61 | renderList:originList, 62 | }) 63 | } 64 | render(){ 65 | const { renderList, position } = this.state 66 | return
67 | { 68 | renderList.map((item,index)=> ) 69 | } 70 |
71 | } 72 | } 73 | /* 控制展示Index */ 74 | export default ()=>{ 75 | const [show, setShow] = useState(false) 76 | const [ btnShow, setBtnShow ] = useState(true) 77 | const handleClick=()=>{ 78 | setBtnShow(false) 79 | setTimeout(()=>{ setShow(true) },[]) 80 | } 81 | return
82 | { btnShow && } 83 | { show && } 84 |
85 | } 86 | ``` 87 | 88 | - 父组件在 **componentDidMount 模拟数据交互,用ref获取真实的DOM元素容器的宽高,渲染列表**。 89 | 90 | 效果: 91 | 92 | ![185fabe653144598a892332eaa812ecd_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/185fabe653144598a892332eaa812ecd_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 93 | 94 | 可以直观看到这种方式渲染的速度特别慢,而且是一次性突然出现,体验不好,所以接下来要用时间分片做性能优化。 95 | 96 | ```js 97 | // TODO: 改造方案 98 | class Index extends React.Component{ 99 | state={ 100 | dataList:[], //数据源列表 101 | renderList:[], //渲染列表 102 | position:{ width:0,height:0 }, // 位置信息 103 | eachRenderNum:500, // 每次渲染数量 104 | } 105 | box = React.createRef() 106 | componentDidMount(){ 107 | const { offsetHeight , offsetWidth } = this.box.current 108 | const originList = new Array(20000).fill(1) 109 | const times = Math.ceil(originList.length / this.state.eachRenderNum) /* 计算需要渲染此次数*/ 110 | let index = 1 111 | this.setState({ 112 | dataList:originList, 113 | position: { height:offsetHeight,width:offsetWidth }, 114 | },()=>{ 115 | this.toRenderList(index,times) 116 | }) 117 | } 118 | toRenderList=(index,times)=>{ 119 | if(index > times) return /* 如果渲染完成,那么退出 */ 120 | const { renderList } = this.state 121 | renderList.push(this.renderNewList(index)) /* 通过缓存element把所有渲染完成的list缓存下来,下一次更新,直接跳过渲染 */ 122 | this.setState({ 123 | renderList, 124 | }) 125 | requestIdleCallback(()=>{ /* 用 requestIdleCallback 代替 setTimeout 浏览器空闲执行下一批渲染 */ 126 | this.toRenderList(++index,times) 127 | }) 128 | } 129 | renderNewList(index){ /* 得到最新的渲染列表 */ 130 | const { dataList , position , eachRenderNum } = this.state 131 | const list = dataList.slice((index-1) * eachRenderNum , index * eachRenderNum ) 132 | return 133 | { 134 | list.map((item,index) => ) 135 | } 136 | 137 | } 138 | render(){ 139 | return
140 | { this.state.renderList } 141 |
142 | } 143 | } 144 | 145 | ``` 146 | 147 | - 第一步:计算时间片,首先用 eachRenderNum 代表一次渲染多少个,那么除以总数据就能得到渲染多少次。 148 | - 第二步:开始渲染数据,通过 `index>times` 判断渲染完成,如果没有渲染完成,那么通过 requestIdleCallback 代替 setTimeout 浏览器空闲执行下一帧渲染。 149 | - 第三步:通过 renderList 把已经渲染的 element 缓存起来,渲染控制章节讲过,这种方式可以直接跳过下一次的渲染。实际每一次渲染的数量仅仅为 demo 中设置的 500 个。 150 | 151 | 完美达到效果(这个是 gif 形式,会出现丢帧的情况,在真实场景,体验感更好): 152 | 153 | ![31b871920902477380879805a066f61d_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/31b871920902477380879805a066f61d_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 154 | 155 | ## 实践二 虚拟列表 156 | 157 | 虚拟列表是一种长列表的解决方案,现在滑动加载是 M 端和 PC 端一种常见的数据请求加载场景,这种数据交互有一个问题就是,如果没经过处理,加载完成后数据展示的元素,都显示在页面上,如果伴随着数据量越来越大,会使页面中的 DOM 元素越来越多,即便是像 React 可以良好运用 diff 来复用老节点,但也不能保证大量的 diff 带来的性能开销。所以虚拟列表的出现,就是解决大量 DOM 存在,带来的性能问题。 158 | 159 | 何为虚拟列表,就是在长列表滚动过程中,只有视图区域显示的是真实 DOM ,**滚动过程中,不断截取视图的有效区域,让人视觉上感觉列表是在滚动。达到无限滚动的效果**。 160 | 161 | 虚拟列表划分可以分为三个区域:**视图区 + 缓冲区 + 虚拟区**。 162 | 163 | e0a19faafac24c3a9be8c49e7f85c259 164 | 165 | - 视图区:视图区就是能够直观看到的列表区,此时的元素都是真实的 DOM 元素。 166 | - 缓冲区:缓冲区是为了防止用户上滑或者下滑过程中,出现白屏等效果。(缓冲区和视图区为渲染真实的 DOM ) 167 | - 虚拟区:对于用户看不见的区域(除了缓冲区),剩下的区域,不需要渲染真实的 DOM 元素。虚拟列表就是通过这个方式来减少页面上 DOM 元素的数量。 168 | 169 | 具体实现思路。 170 | 171 | - 通过 useRef 获取元素,缓存变量。 172 | - useEffect 初始化计算容器的高度。截取初始化列表长度。这里需要 div 占位,撑起滚动条。 173 | - 通过**监听滚动容器的 onScroll 事件**,根据 scrollTop 来计算渲染区域向上偏移量, 这里需要注意的是,当用户向下滑动的时候,为了渲染区域,能在可视区域内,可视区域要向上滚动;当用户向上滑动的时候,可视区域要向下滚动。 174 | - 通过**重新计算 end 和 start 来重新渲染列表**。 175 | 176 | ```js 177 | function VirtualList(){ 178 | const [ dataList,setDataList ] = React.useState([]) /* 保存数据源 */ 179 | const [ position , setPosition ] = React.useState([0,0]) /* 截取缓冲区 + 视图区索引 */ 180 | const scroll = React.useRef(null) /* 获取scroll元素 */ 181 | const box = React.useRef(null) /* 获取元素用于容器高度 */ 182 | const context = React.useRef(null) /* 用于移动视图区域,形成滑动效果。 */ 183 | const scrollInfo = React.useRef({ 184 | height:500, /* 容器高度 */ 185 | bufferCount:8, /* 缓冲区个数 */ 186 | itemHeight:60, /* 每一个item高度 */ 187 | renderCount:0, /* 渲染区个数 */ 188 | }) 189 | React.useEffect(()=>{ 190 | const height = box.current.offsetHeight 191 | const { itemHeight , bufferCount } = scrollInfo.current 192 | const renderCount = Math.ceil(height / itemHeight) + bufferCount 193 | scrollInfo.current = { renderCount,height,bufferCount,itemHeight } 194 | const dataList = new Array(10000).fill(1).map((item,index)=> index + 1 ) 195 | setDataList(dataList) 196 | setPosition([0,renderCount]) 197 | },[]) 198 | const handleScroll = () => { 199 | const { scrollTop } = scroll.current 200 | const { itemHeight , renderCount } = scrollInfo.current 201 | const currentOffset = scrollTop - (scrollTop % itemHeight) 202 | const start = Math.floor(scrollTop / itemHeight) 203 | context.current.style.transform = `translate3d(0, ${currentOffset}px, 0)` /* 偏移,造成下滑效果 */ 204 | const end = Math.floor(scrollTop / itemHeight + renderCount + 1) 205 | if(end !== position[1] || start !== position[0] ){ /* 如果render内容发生改变,那么截取 */ 206 | setPosition([ start , end ]) 207 | } 208 | } 209 | const { itemHeight , height } = scrollInfo.current 210 | const [ start ,end ] = position 211 | const renderList = dataList.slice(start,end) /* 渲染区间 */ 212 | console.log('渲染区间',position) 213 | return
214 |
215 |
216 |
217 | { 218 | renderList.map((item,index)=>
{item + '' } Item
) 219 | } 220 |
221 |
222 |
223 | } 224 | ``` 225 | 226 | **完美达到效果:** 227 | 228 | ![1ea56dfce19c4b7998008d0ac099b24f_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/1ea56dfce19c4b7998008d0ac099b24f_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 229 | 230 | ## 总结 231 | 232 | 对于海量的数据处理,在实际项目中,可能会更加复杂,本章节给了两个海量数据场景的处理方案,时间分片( Time slicing )和虚拟列表( Virtual list ),如果真实项目中有这个场景,希望能给大家一个处理思路。纸上得来终觉浅,绝知此事须躬行。 -------------------------------------------------------------------------------- /docs/interview/前沿技术/实现一个埋点监控SDK.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 这篇文章会讲清楚: 4 | 5 | 1. 埋点监控系统负责处理哪些问题,需要怎么设计api? 6 | 2. 为什么用img的src做请求的发送,sendBeacon又是什么? 7 | 3. 在react、vue的错误边界中要怎么处理? 8 | 9 | ## 什么是埋点监控SDK 10 | 11 | 举个例子,公司开发上线了一个网站,但开发人员不可能预测,用户实际使用时会发生什么:用户浏览过哪几个页面?几成用户会点击某个弹窗的确认按钮,几成会点击取消?有没有出现页面崩溃? 12 | 13 | 所以我们需要一个埋点监控SDK去做数据的收集,后续再统计分析。有了分析数据,才能有针对性对网站进行优化:PV特别少的页面就不要浪费大量人力;有bug的页面赶紧修复,不然要325了。 14 | 15 | 比较有名的埋点监控有Google Analytics,除了web端,还有iOS、安卓的SDK。 16 | 17 | ## 埋点监控的职能范围 18 | 19 | 因为业务需要的不同,大部分公司都会自己开发一套**埋点监控系统**,但基本上都会涵盖这三类功能: 20 | 21 | ## 用户行为监控 22 | 23 | 负责统计PV(页面访问次数)、UV(页面访问人数)以及用户的点击操作等行为。 24 | 25 | 这类统计是用的最多的,有了这些数据才能量化我们的工作成果。 26 | 27 | ## 页面性能监控 28 | 29 | 开发和测试人员固然在上线之前会对这些数据做评估,但用户的环境和我们不一样,也许是3G网,也许是很老的机型,我们需要知道在实际使用场景中的性能数据,比如页面加载时间、白屏时间等。 30 | 31 | ## 错误报警监控 32 | 33 | 获取错误数据,及时处理才能避免大量用户受到影响。除了全局捕获到的错误信息,还有在代码内部被catch住的错误告警,这些都需要被收集到。 34 | 35 | 下面会从api的设计出发,对上述三种类型进一步展开。 36 | 37 | ## SDK的设计 38 | 39 | 在开始设计之前,先看一下SDK怎么使用 40 | 41 | ```js 42 | import StatisticSDK from 'StatisticSDK'; 43 | // 全局初始化一次 44 | window.insSDK = new StatisticSDK('uuid-12345'); 45 | 46 | 47 | 51 | ``` 52 | 53 | 首先把SDK实例挂载到全局,之后在业务代码中调用,这里的新建实例时需要传入一个id,因为这个埋点监控系统往往是给多个业务去使用的,通过id去区分不同的数据来源。 54 | 55 | 首先实现实例化部分: 56 | 57 | ```js 58 | class StatisticSDK { 59 | constructor(productID){ 60 | this.productID = productID; 61 | } 62 | } 63 | ``` 64 | 65 | ## 数据发送 66 | 67 | 数据发送是一个最基础的api,后面的功能都要基于此进行。通常这种前后端分离的场景会使用AJAX的方式发送数据,但是这里使用图片的src属性。原因有两点: 68 | 69 | 1. 没有跨域的限制,像srcipt标签、img标签都可以直接发送跨域的GET请求,不用做特殊处理; 70 | 2. 兼容性好,一些静态页面可能禁用了脚本,这时script标签就不能使用了; 71 | 72 | 但要注意,这个图片不是用来展示的,我们的目的是去「传递数据」,只是借助img标签的的src属性,在其url后面拼接上参数,服务端收到再去解析。 73 | 74 | ``` 75 | class StatisticSDK { 76 | constructor(productID){ 77 | this.productID = productID; 78 | } 79 | send(baseURL,query={}){ 80 | query.productID = this.productID; 81 | let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&') 82 | let img = new Image(); 83 | img.src = `${baseURL}?${queryStr}` 84 | } 85 | } 86 | 复制代码 87 | ``` 88 | 89 | img标签的优点是不需要将其append到文档,只需设置src属性便能成功发起请求。 90 | 91 | 通常请求的这个url会是一张1X1px的GIF图片,网上的文章对于这里为什么返回图片的是一张GIF都是含糊带过,这里查阅了一些资料并测试了: 92 | 93 | 1. 同样大小,不同格式的的图片中GIF大小是最小的,所以选择返回一张GIF,这样对性能的损耗更小; 94 | 2. 如果返回204,会走到img的onerror事件,并抛出一个全局错误;如果返回200和一个空对象会有一个CORB的告警; 95 | 96 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e50e255a2bf34f46a259d26f78ca50a6~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 97 | 98 | > 当然如果不在意这个报错可以采取返回空对象,事实上也有一些工具是这样做的 99 | 100 | 3. 有一些埋点需要真实的加到页面上,比如垃圾邮件的发送者会添加这样一个隐藏标志来验证邮件是否被打开,如果返回204或者是200空对象会导致一个明显图片占位符 101 | 102 | ``` 103 | 104 | ``` 105 | 106 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/5b05d3be1ce842618aa4972bbc86ee31~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 107 | 108 | ### 更优雅的web beacon 109 | 110 | 这种打点标记的方式被称web beacon(网络信标)。除了gif图片,从2014年开始,浏览器逐渐实现专门的API,来更优雅的完成这件事:Navigator.sendBeacon 111 | 112 | 使用很简单 113 | 114 | ``` 115 | Navigator.sendBeacon(url,data) 116 | ``` 117 | 118 | 相较于图片的src,这种方式的更有优势: 119 | 120 | 1. 不会和主要业务代码抢占资源,而是在浏览器空闲时去做发送; 121 | 2. 并且在页面卸载时也能保证请求成功发送,不阻塞页面刷新和跳转; 122 | 123 | 现在的埋点监控工具通常会优先使用sendBeacon,但由于浏览器兼容性,还是需要用图片的src兜底。 124 | 125 | ## 用户行为监控 126 | 127 | 上面实现了数据发送的api,现在可以基于它去实现用户行为监控的api。 128 | 129 | ``` 130 | class StatisticSDK { 131 | constructor(productID){ 132 | this.productID = productID; 133 | } 134 | // 数据发送 135 | send(baseURL,query={}){ 136 | query.productID = this.productID; 137 | let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&') 138 | let img = new Image(); 139 | img.src = `${baseURL}?${queryStr}` 140 | } 141 | // 自定义事件 142 | event(key, val={}) { 143 | let eventURL = 'http://demo/' 144 | this.send(eventURL,{event:key,...val}) 145 | } 146 | // pv曝光 147 | pv() { 148 | this.event('pv') 149 | } 150 | } 151 | 152 | ``` 153 | 154 | 用户行为包括自定义事件和pv曝光,也可以把pv曝光看作是一种特殊的自定义行为事件。 155 | 156 | ## 页面性能监控 157 | 158 | 页面的性能数据可以通过performance.timing这个API获取到,获取的数据是单位为毫秒的时间戳。 159 | 160 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/eabe9749d5b84f2b8ef932cff2076a08~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 上面的不需要全部了解,但比较关键的数据有下面几个,根据它们可以计算出FP/DCL/Load等关键事件的时间点: 161 | 162 | 1. 页面首次渲染时间:`FP(firstPaint)=domLoading-navigationStart` 163 | 2. DOM加载完成:`DCL(DOMContentEventLoad)=domContentLoadedEventEnd-navigationStart` 164 | 3. 图片、样式等外链资源加载完成:`L(Load)=loadEventEnd-navigationStart` 165 | 166 | 上面的数值可以跟performance面板里的结果对应。 167 | 168 | 回到SDK,我们只用实现一个上传所有性能数据的api就可以了: 169 | 170 | ``` 171 | class StatisticSDK { 172 | constructor(productID){ 173 | this.productID = productID; 174 | // 初始化自动调用性能上报 175 | this.initPerformance() 176 | } 177 | // 数据发送 178 | send(baseURL,query={}){ 179 | query.productID = this.productID; 180 | let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&') 181 | let img = new Image(); 182 | img.src = `${baseURL}?${queryStr}` 183 | } 184 | // 性能上报 185 | initPerformance(){ 186 | let performanceURL = 'http://performance/' 187 | this.send(performanceURL,performance.timing) 188 | } 189 | } 190 | 复制代码 191 | ``` 192 | 193 | 并且,在构造函数里自动调用,因为性能数据是必须要上传的,就不需要用户每次都手动调用了。 194 | 195 | ## 错误告警监控 196 | 197 | 错误报警监控分为JS原生错误和React/Vue的组件错误的处理。 198 | 199 | ### JS原生错误 200 | 201 | 除了try catch中捕获住的错误,我们还需要上报没有被捕获住的错误——通过error事件和unhandledrejection事件去监听。 202 | 203 | #### error 204 | 205 | error事件是用来监听DOM操作错误`DOMException`和JS错误告警的,具体来说,JS错误分为下面8类: 206 | 207 | 1. InternalError: 内部错误,比如如递归爆栈; 208 | 2. RangeError: 范围错误,比如new Array(-1); 209 | 3. EvalError: 使用eval()时错误; 210 | 4. ReferenceError: 引用错误,比如使用未定义变量; 211 | 5. SyntaxError: 语法错误,比如var a = ; 212 | 6. TypeError: 类型错误,比如\[1,2\].split('.'); 213 | 7. URIError: 给 encodeURI或 decodeURl()传递的参数无效,比如decodeURI('%2') 214 | 8. Error: 上面7种错误的基类,通常是开发者抛出 215 | 216 | 也就是说,代码运行时发生的上述8类错误,都可以被检测到。 217 | 218 | #### unhandledrejection 219 | 220 | Promise内部抛出的错误是无法被error捕获到的,这时需要用unhandledrejection事件。 221 | 222 | 回到SDK的实现,处理错误报警的代码如下: 223 | 224 | ``` 225 | class StatisticSDK { 226 | constructor(productID){ 227 | this.productID = productID; 228 | // 初始化错误监控 229 | this.initError() 230 | } 231 | // 数据发送 232 | send(baseURL,query={}){ 233 | query.productID = this.productID; 234 | let queryStr = Object.entries(query).map(([key, value]) => `${key}=${value}`).join('&') 235 | let img = new Image(); 236 | img.src = `${baseURL}?${queryStr}` 237 | } 238 | // 自定义错误上报 239 | error(err, etraInfo={}) { 240 | const errorURL = 'http://error/' 241 | const { message, stack } = err; 242 | this.send(errorURL, { message, stack, ...etraInfo}) 243 | } 244 | // 初始化错误监控 245 | initError(){ 246 | window.addEventListener('error', event=>{ 247 | this.error(error); 248 | }) 249 | window.addEventListener('unhandledrejection', event=>{ 250 | this.error(new Error(event.reason), { type: 'unhandledrejection'}) 251 | }) 252 | } 253 | } 254 | 复制代码 255 | ``` 256 | 257 | 和初始化性能监控一样,初始化错误监控也是一定要做的,所以需要在构造函数中调用。后续开发人员只用在业务代码的try catch中调用error方法即可。 258 | 259 | ### React/Vue组件错误 260 | 261 | 成熟的框架库都会有错误处理机制,React和Vue也不例外。 262 | 263 | #### React的错误边界 264 | 265 | 错误边界是希望当应用内部发生渲染错误时,不会整个页面崩溃。我们提前给它设置一个兜底组件,并且可以细化粒度,只有发生错误的部分被替换成这个「兜底组件」,不至于整个页面都不能正常工作。 266 | 267 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d09ecffb71b4439c96950643a5ccd2b1~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 268 | 269 | 它的使用很简单,就是一个带有特殊生命周期的类组件,用它把业务组件包裹起来。 270 | 271 | 这两个生命周期是`getDerivedStateFromError`和`componentDidCatch`, 272 | 273 | 代码如下: 274 | 275 | ``` 276 | // 定义错误边界 277 | class ErrorBoundary extends React.Component { 278 | state = { error: null } 279 | static getDerivedStateFromError(error) { 280 | return { error } 281 | } 282 | componentDidCatch(error, errorInfo) { 283 | // 调用我们实现的SDK实例 284 | insSDK.error(error, errorInfo) 285 | } 286 | render() { 287 | if (this.state.error) { 288 | return

Something went wrong.

289 | } 290 | return this.props.children 291 | } 292 | } 293 | ... 294 | 295 | 296 | 297 | 复制代码 298 | ``` 299 | 300 | > 建了一个在线sandbox可以体验,公众号后台回复「错误边界demo」获取地址 301 | 302 | 回到SDK的整合上,在生产环境下,被错误边界包裹的组件,如果内部抛出错误,全局的error事件是无法监听到的,因为这个错误边界本身就相当于一个try catch。所以需要在错误边界这个组件内部去做上报处理。也就是上面代码中的`componentDidCatch`生命周期。 303 | 304 | #### Vue的错误边界 305 | 306 | vue也有一个类似的生命周期来做这件事,不再赘述:`errorCaptured` 307 | 308 | ``` 309 | Vue.component('ErrorBoundary', { 310 | data: () => ({ error: null }), 311 | errorCaptured (err, vm, info) { 312 | this.error = `${err.stack}\n\nfound in ${info} of component` 313 | // 调用我们的SDK,上报错误信息 314 | insSDK.error(err,info) 315 | return false 316 | }, 317 | render (h) { 318 | if (this.error) { 319 | return h('pre', { style: { color: 'red' }}, this.error) 320 | } 321 | return this.$slots.default[0] 322 | } 323 | }) 324 | ... 325 | 326 | 327 | 328 | 329 | 复制代码 330 | ``` 331 | 332 | 现在我们已经实现了一个完整的SDK的骨架,并且处理了在实际开发时,react/vue项目应该怎么接入。 333 | 334 | 实际生产使用的SDK会更健壮,但思路也不外乎,感兴趣的可以去读一读源码。 335 | 336 | ## 结语 337 | 338 | 文章比较长,但想答好这个问题,这些知识储备都是必须的。 339 | 340 | 我们要设计SDK,首先要清楚它的基本使用方法,才知道后面的代码框架要怎么搭;然后是明确SDK的职能范围:需要能处理用户行为、页面性能以及错误报警三类监控;最后是react、vue的项目,通常会做错误边界处理,要怎么接入我们自己的SDK。 341 | 342 | 如果觉得这篇文章对你有用,点赞关注是对我最大的鼓励! 343 | 344 | ![IMG_6474.JPG](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3a7060e993014a478998941a655d97dc~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp?) 345 | 346 | 你的支持是我创作的动力! -------------------------------------------------------------------------------- /docs/interview/webgl.md: -------------------------------------------------------------------------------- 1 | # WebGL高性能图形编程 2 | 3 | ## 第1章-WebGL基础 4 | 5 | WebGL 是一组基于 JavaScript 语言的图形规范,浏览器厂商按照这组规范进行实现,为 Web 开发者提供一套`3D图形`相关的 API。那么,这些 API 能够帮助 Web 开发者做些什么呢? 6 | 7 | 这些 API 能够让 Web 开发者使用 JavaScript 语言直接和显卡(GPU)进行通信。当然 WebGL 的 GPU 部分也有对应的编程语言,简称 `GLSL`。我们用它来编写运行在 GPU 上的着色器程序。着色器程序需要接收 CPU(WebGL 使用 JavaScript) 传递过来的数据,然后对这些数据进行流水线处理,最终显示在屏幕上,进而实现丰富多彩的 3D 应用,比如 3D 图表,网页游戏,3D 地图,WebVR 等 8 | 9 | WebGL 只能够绘制`点`、`线段`、`三角形`这三种基本图元,但是我们经常看到 WebGL 程序中含有立方体、球体、圆柱体等规则形体,甚至很多更复杂更逼真的不规则模型,那么 WebGL 是如何绘制它们的呢?其实这些模型本质上是由一个一个的`点`组成,GPU 将这些点用`三角形图元`绘制成一个个的微小平面,这些平面之间互相连接,从而组成各种各样的立体模型 10 | 11 | ### 构建模型顶点数据 12 | 13 | 一般情况下,最初的顶点坐标是相对于`模型中心`的,不能直接传递到着色器中,我们需要对`顶点坐标`按照一系列步骤执行`模型转换`,`视图转换`,`投影转换`,转换之后的坐标才是 WebGL 可接受的坐标,即`裁剪空间坐标`。我们把最终的`变换矩阵`和`原始顶点坐标`传递给 `GPU`,GPU 的渲染管线对它们执行流水线作业。 14 | 15 | ### 渲染管线过程 16 | 17 | - 首先进入顶点着色器阶段,利用 GPU 的并行计算优势对顶点逐个进行坐标变换。 18 | - 然后进入图元装配阶段,将顶点按照图元类型组装成图形。 19 | - 接下来来到光栅化阶段,光栅化阶段将图形用不包含颜色信息的像素填充。 20 | - 在之后进入片元着色器阶段,该阶段为像素着色,并最终显示在屏幕上。 21 | 22 | ### 着色器 23 | 24 | GLSL 是用来编写着色器程序的语言,那么新的问题来了,着色器程序是用来做什么的呢? 简单地说,着色器程序是在显卡(GPU)上运行的简短程序,代替了 GPU `固定渲染管线`的一部分,使 GPU 渲染过程中的某些部分允许开发者通过`编程`进行控制 25 | 26 | 着色器程序允许我们通过编程来控制 GPU 的渲染 27 | 28 | ![image-20221009170519482](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/image-20221009170519482.png) 29 | 30 | 上图简单演示了 WebGL 对一个红色三角形的渲染过程,绿色部分为开发者可以通过编程控制的部分: 31 | 32 | - JavaScript 程序 33 | 处理着色器需要的`顶点坐标`、`法向量`、`颜色`、`纹理`等信息,并负责为`着色器`提供这些数据,上图为了演示方便,只是提供了三角形顶点的位置数据。 34 | - 顶点着色器 35 | 接收 JavaScript 传递过来的`顶点信息`,将顶点绘制到对应坐标。 36 | - 图元装配阶段 37 | 将三个顶点装配成指定`图元类型`,上图采用的是三角形图元。 38 | - 光栅化阶段 将三角形内部区域用空像素进行填充。 39 | - 片元着色器 为三角形内部的像素填充颜色信息,上图为暗红色。 40 | 41 | 实际上,对顶点信息的变换操作既可以在 `JavaScript` 中进行,也可以在`着色器程序`中进行。通常我们都是在 `JavaScript` 中生成一个包含了所有变换的最终变换矩阵,然后将该矩阵传递给着色器,利用 GPU 并行计算优势对所有顶点执行变换 42 | 43 | webgl 里面的基本图元是三角形,所有图形都是有很多个三角形组成的,顶点着色器为这些三角形的`顶点`着色,三角形内部区域的颜色会由 gpu 进行`插值填充`,你可以通过`片元着色器`为三角形区域内的`每一个点`着色,从而替代默认的插值方式。 44 | 45 | 顶点着色器**这里的顶点代表的是组成物体的每一个点。** 46 | 47 | 顶点着色器的功能主要是将位置数据经过矩阵变换、计算光照之后生成顶点颜色、变换纹理坐标。并将生成的数据输出到片元着色器。 48 | 49 | 片元着色器的作用是将**光栅化阶段**生成的每个片元,计算出每个片元的最终元素。 50 | 51 | ### 1.3.2 片元着色器 52 | 53 | 片元着色器的作用是将**光栅化阶段**生成的每个片元,计算出每个片元的最终元素。 54 | 55 | ### WebGL数据类型 56 | 57 | - 一般不用bool,bvec,ivec 58 | - 常用就float,sampler2D,samplerCube,mat3,mat4 ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/9634bb00e7cf4c6a9b365ce326de9ead~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 59 | 60 | ### 修饰符(WebGL1.0) 61 | 62 | ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/008a415cedc447d89cfd4a6f598cdcae~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 63 | 64 | ### 修饰符(WebGL2.0) 65 | 66 | - in vec3 normal; 67 | - out vec3 vNormal; 68 | - layout (location = 0) in vec3 position; 69 | - layout (location = 1) in vec3 normal; 70 | - layout(location = 0) out vec4 gColor; 71 | - layout(location = 1) out vec4 gNormal; 72 | 73 | ### 顶点着色器 预定义变量 74 | 75 | ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f03afa3e15df4be4bd64e6d7e02018a7~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 76 | 77 | ### 片段着色器 预定义变量 78 | 79 | ![在这里插入图片描述](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/779bf440a1b24ac39145e491a6f9004c~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 80 | 81 | ### 输入的变量 82 | 83 | ![image.png](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/47adf0b656a741d799453b8f55d1da1e~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/4a5b97800d0b4aff9c559c735e7105de~tplv-k3u1fbpfcp-zoom-in-crop-mark:3024:0:0:0.awebp) 84 | 85 | ### Example 86 | 87 | WebGL1.0 88 | 89 | ```glsl 90 | attribute vec4 a_position; //js一般输入的是 n组数组 每一数组大小跟声明有关 91 | uniform vec4 u_offset; //js一般输入的是 一组数组 这个数组大小跟声明有关 uniform这个可以在片元着色器写 92 | uniform float u_kernel[9]; //js一般输入的是 一组数组 这个数组大小跟声明有关 93 | varying vec4 v_positionWithOffset; //只是用来传值 不用在js里输入 94 | //uniform 不一定要输入 都会有默认值 95 | attribute vec2 a_TexCoord;//顶点纹理坐标 96 | varying vec2 v_TexCoord; 97 | 98 | void main() { 99 | gl_Position = a_position + u_offset + u_kernel[0]; 100 | v_positionWithOffset = a_position + u_offset; 101 | v_TexCoord = a_TexCoord; //uv坐标 102 | } 103 | 复制代码 104 | precision mediump float; 105 | varying vec4 v_positionWithOffset; //要想使用的话 必须要有相同的声明 106 | 107 | struct SomeStruct { //自定义结构 108 | bool active; 109 | vec2 someVec2; 110 | }; 111 | uniform SomeStruct u_someThing; 112 | 113 | 114 | uniform sampler2D u_Sampler; //贴图颜色来的 115 | varying vec2 v_TexCoord; //片元顶点坐标 116 | void main() { 117 | vec4 color = v_positionWithOffset * 0.5 + 0.5 118 | gl_FragColor = texture2D(u_Sampler,v_TexCoord); //texture2D会返回四个通道 119 | } 120 | 121 | ``` 122 | 123 | WebGL2.0 124 | 125 | ```glsl 126 | #version 300 es 127 | in vec4 a_position; 128 | in vec2 a_TexCoord;//纹理坐标 129 | out vec2 v_TexCoord;//插值后纹理坐标 130 | 131 | void main() { 132 | gl_Position = a_position; 133 | v_TexCoord = a_TexCoord; //一般不做处理 134 | } 135 | 复制代码 136 | #version 300 es 137 | precision highp float; 138 | int vec2 vTexcoord; //varing 已经被替代了 139 | out vec4 outColor; // you can pick any name 140 | uniform sampler2D uTexture; 141 | void main() { 142 | outColor = doMathToMakeAColor; 143 | gl_FragColor = texture2D(uTexture,v_TexCoord); 144 | } 145 | ``` 146 | 147 | ## 第2章-初识WebGL 148 | 149 | ### 01-手动绘制一个WebGL图形 150 | 151 | ![image-20190325140143588](https://s2.loli.net/2022/07/09/GEw51QogVDPzSvn.png) 152 | 153 | 实现的步骤: 154 | 155 | 1. 添加一个画布元素 156 | 2. 获取到画布元素的基于webgl上下文环境对象 157 | 3. 使用对象中的API实现图形绘制 158 | 159 | ### 02-使用着色器绘制一个 WebGL图形 160 | 161 | ![image-20190325144201606](https://s2.loli.net/2022/07/09/Adz5Nt4ne1rZkoQ.png) 162 | 163 | 164 | 165 | - WebGL 中的坐标系统: 166 | 167 | ![01-坐标](https://s2.loli.net/2022/07/09/rEwa62hCgyfTPdU.jpg) 168 | 169 | ![02-坐标](https://s2.loli.net/2022/07/09/X1chHqJNa2iS5VC.jpg) 170 | 171 | - 着色器的介绍: 172 | 173 | ​ **着色器是**使用 OpenGL ES Shading Language 语言编写的程序,负责记录像素点的**位置**和**颜色**,并由**顶点着色器和片段着色器**组成,通过用GLSL 编写这些着色器,并将代码文本传递给WebGL执行时编译,另外,顶点着色器和片段着色器的集合我们通常称之为**着色器程序**。 174 | 175 | ​ **顶点着色器**的功能是将输入顶点从原始坐标系转换到WebGL使用的缩放空间坐标系,每个轴的坐标范围从-1.0到1.0,顶点着色器对顶点坐标进行必要的转换后,保存在名称为gl_Position的特殊变量中备用。 176 | 177 | ​ **片段着色器**在顶点着色器处理完图形的顶点后,会被要绘制的每个图形的每个像素点调用一次,它的功能是确定像素的颜色值,并保存在名称为gl_FragColor的特殊变量中,该颜色值将最终绘制到图形像素的对应位置中。 178 | 179 | ## 第3章-绘制三角形 180 | 181 | ### 01-多点绘制的方法 182 | 183 | ![image-20190326133332093](https://s2.loli.net/2022/07/09/BiCwSGvWmz6Q9g7.png) 184 | 185 | - 什么attribute 变量 186 | 187 | 它是一种存储限定符,表示定义一个attribute的全局变量,这种变量的数据将由外部向顶点着色器内传输,并保存**顶点**相关的数据,只有顶点着色器才能使用它。 188 | 189 | - 使用attribute 变量 190 | 191 | 1. 在顶点着色器中,声明一个 attribute 变量。 192 | 193 | 2. 将 attribute 变量赋值给 gl_Position 变量。 194 | 195 | 3. 向 attribute 变量传输数据。 196 | 197 | - 使用缓存区关联attribute变量 198 | 199 | 1. 创建缓存区对象 200 | 2. 绑定缓存区对象 201 | 3. 将数据写入对象 202 | 4. 将缓存区对象分配给attribute变量 203 | 5. 开启attribute变量 204 | 205 | ### 02-绘制三角形的方法 206 | 207 | - 实现代码 208 | 209 | 210 | 211 | 212 | ## 第4章-WebGL动画 213 | 214 | ### 01-图形的移动 215 | 216 | ![image-20190318160443253](https://s2.loli.net/2022/07/09/B6WZjvbkwJ9LfG5.png) 217 | 218 | - 平移原理 219 | 220 | ​ 为了平移一个三角形,只需要对它的每个顶点进行移动,即每个顶点加上一个分量,得到一个新的坐标: 221 | 222 | ` X1=X+TX``Y1=Y+TY``Z1=Z+TZ` 223 | 224 | ​ 只需要着色器中为顶点坐标的每个分量加上一个常量就可以实现,当然这这修改在顶点着色器上。 225 | 226 | - uniform类型变量 227 | 228 | 用于保存和传输一致的数据,既可用于顶点,也可用于片断。 229 | 230 | ### 02-图形的旋转 231 | 232 | ![image-20190326165438537](https://s2.loli.net/2022/07/09/NWOY2HkoT9jcg6s.png) 233 | 234 | 235 | 236 | 旋转原理 237 | 238 | 为了描述一个图形的旋转过程,必须指明以下内容: 239 | 240 | 1. 旋转轴(围绕X和Y轴旋转) 241 | 242 | 2. 旋转的方向(顺时针和逆时针),负值是为顺时针,正值时为逆时针 243 | 244 | 3. 旋转的角度(图形经过的角度) 245 | 246 | ![image-20190319154151430](https://s2.loli.net/2022/07/09/FWszZQXADjTxKLk.png) 247 | 248 | ![image-20190327174612322](https://s2.loli.net/2022/07/09/ag2tspG5wCRxir1.png) 249 | 250 | 251 | 252 | ### 03-图形的缩放 253 | 254 | ![image-20190318160344389](https://s2.loli.net/2022/07/09/yfrpiCXHw3gtRn8.png) 255 | 256 | - 缩放的原理 257 | 258 | ​ 通过改变原有图形中的矩阵值,实现图形的拉大和缩下效果,因此,只需要修改原有图形的矩阵值即可。 259 | 260 | ![image-20190319154240988](https://s2.loli.net/2022/07/09/aGSmhEVOwLsT2I6.png) 261 | 262 | - 动画实现 263 | 264 | 需求:制作一个按旋转三角形的动画 265 | 266 | **屏幕刷新频率** 267 | 268 | 图像在屏幕上更新的速度,也即屏幕上的图像每秒钟出现的次数,一般是60Hz的屏幕每16.7ms刷新一次。 269 | 270 | **动画原理** 271 | 272 | 图像被刷新时,引起以连贯的、平滑的方式进行过渡变化。 273 | 274 | **核心方法** 275 | 276 | ```javascript 277 | requestAnimationFrame(callback) 278 | //执行一个动画,并在下次绘制前调用callback回调函数更新该动画 279 | ``` 280 | 281 | ## 第5章-WebGL颜色 282 | 283 | ### 01-操作步骤介绍 284 | 285 | ![image-20190318162409517](https://s2.loli.net/2022/07/09/KHVsdWTgfx6ZemP.png) 286 | 287 | - 颜色添加步骤 288 | 289 | 1. 在顶点着色器中定义一个接收外部传入颜色值的属性变量a_Color和用于传输获取到的颜色值变量v_Color 290 | 2. 在片段着色器中定义一个同一类型和名称的v_Color变量接收传顶点传入的值。 291 | 3. 重新传入到顶点坐标和颜色值的类型化数组 292 | 4. 将数组值传入缓存中并取出,赋值给顶点的两个变量 293 | 5. 接收缓存值并绘制图形和颜色 294 | 295 | 296 | 297 | - vertexAttribPointer 方法 298 | 299 | - | 参数 | 说明 | 300 | | :-------: | ------------------------------------------------------------ | 301 | | 第1个参数 | 指定待分配attribute变量的存储位置 | 302 | | 第2个参数 | 指定缓存区中每个顶点的分量个数(1~4) | 303 | | 第3个参数 | 类型有,无符号字节,短整数,无符号短整数,整型,无符号整型,浮点型 | 304 | | 第4个参数 | 表示是否将非浮点型的数据归到[0,1][-1,1]区间 | 305 | | 第5个参数 | 相邻两个顶点的字节数。默认为0 | 306 | | 第6个参数 | 表示缓存区对象的偏移量(以字节为单位),attribute 变量从缓冲区中的何处开始存储 | 307 | 308 | - 案例实现 309 | 310 | 1. 添加画布元素,并获取webGL对象,保存在变量中。 311 | 2. 定义着色器内容,并进行附件编译。 312 | 3. 使用缓存对象向顶点传入多个坐标数据。 313 | 4. 根据坐标数据绘制图像。 314 | 315 | ### 02-着色器编译与图像绘制 316 | 317 | - 代码实现 318 | 319 | 320 | ## 第6章-回顾总结 321 | 322 | 回顾 323 | 324 | 1.如何使用画布绘制一个应webgl技术的图形 325 | 326 | colorColor 327 | 328 | 2.有坐标点的图形 329 | 330 | ​ 一个坐标点 331 | 332 | 着色器(顶点、片段) 333 | 334 | 坐标体系 335 | 336 | 3.绘制多个顶点的三角形 337 | 338 | 4.平移,uniform 339 | 340 | ​ 旋转,数学函数计算角度获取坐标值 341 | 342 | ​ 缩放 343 | 344 | 5.各个顶点的绘制颜色 345 | 346 | 步骤 347 | 348 | varying 349 | 350 | 理解和掌握WebGL工作原理的重要基础 351 | 352 | 总结: 353 | 354 | 可绘制简单webgl图形, 355 | 356 | 动画、三维透视方法 357 | 358 | 借助一些比较简单快速上手的框架,three.js -------------------------------------------------------------------------------- /docs/interview/前沿技术/hybrid简单了解.md: -------------------------------------------------------------------------------- 1 | 技术点总有它的来由。文中例子参考[Jockeyjs.js](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ftcoulter%2Fjockeyjs%2Fblob%2Fmaster%2FJockeyJS%2Fjs%2Fjockey.js "https://github.com/tcoulter/jockeyjs/blob/master/JockeyJS/js/jockey.js")容易理解一点。 2 | 3 | 文章概要: 4 | 5 | - 1.hybrid 基本概念 6 | - 2.前端和客户端的交互 7 | - 3.前端和客户端的交互实现 8 | - 4.前端交互实现关注点 9 | - 5.小结 10 | 11 | ## 1.hybrid 基本概念 12 | 13 | ### 1.1什么是hybrid? 14 | 15 | hybrid即“混合”,前端和客户端的混合开发模式,某些环节也可能涉及到 server 端。 hybrid 底层依赖于Native提供的容器**(WebView)**,上层使用html&css&JS做业务开发。 16 | 17 | ### 1.2webview是什么? 18 | 19 | app中的一个组件,类似于小型浏览器内核。native提供的容器盒子,用于加载h5页面。 20 | 21 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409ce9927b838~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 22 | 23 | 图中表示了两种h5页面资源运用的方式 24 | 25 | **①以静态资源打包到app内的方式。** 26 | 27 | 前端将代码提供给native,native客户端拿到前端静态页面,以文件形式存储在 app 中。这种模式,如果前端静态页面需要更新,客户端就需要去server端下载静态资源,即客户端每次打开需要去线上检查有无更新包有就下载压缩包,解压更新静态资源。 28 | 29 |  优点:因为资源在本地,通过file 协议读取,读取速度非常快,且可以做到断网模式下页面合理的展示。 30 | 31 | - 这样就涉及到了一个server端静态资源包管理系统。 32 | - 同时H5的资源是静态的存储在native本地,以file的方式读取,那么H5向远端发起的请求就存在跨域,所以H5的请求需要经过native做一层代理转发。 33 | - 静态资源越多native包就越大(所以这种模式更适用于,产品功能稳定,体验要求又高,且迭代频繁的场景)。 34 | 35 | **②以线上url方式(更偏H5)** 36 | 37 |  将资源部署在线上,native打开一个新的webview请求线上资源展示(同在浏览器中输入url,查看页面过程一致) 38 | 39 |  优点:按需加载,用户使用到的页面才会更新,发请求可以不经过native做代理。 40 | 41 | - 不可避免请求线上资源,都需要时间,所以会出现瞬间白屏(弱网模式特别明显)。 42 | - 断网模式下没有内容显示。 43 | 44 | **③两种模式资源加载** 45 | 46 | **本地读取:过file 协议** 47 | 48 | **线上读取:通过http或着https 请求加载资源** 49 | 50 | ### 1.3.hybrid存在的意义? 51 | 52 | 可以快速迭代开发更新。(无需app审核,哈哈因为对手机的操作权限不高,所以无需审核?) 53 | 54 | hybrid开发效率高,低成本,跨平台,ios和安卓共用一套h5代码。(ios和安卓接口一致) 55 | 56 | hybrid从业务开发上讲,没有版本问题,有BUG能及时修复(相对于app)。 57 | 58 | ## 2.前端和客户端的通讯 59 | 60 | native提供的容器盒子,用于加载h5页面,那么h5的页面要怎么跟native交互? 61 | 62 | 前端和客户端的交互大概描述: 63 | 64 | - JS访问客户端的能力,传递参数和回调函数。 65 | - 客户端通过回调函数返回内容。 66 | 67 | ****前端**的页面跟native交互,是通过******schema**协议。(事实上Native能捕捉webview发出的一切请求,这个协议的意义在于可以在浏览器中直接打开APP)** 68 | 69 | ### 2.1什么是schema协议? 70 | 71 | 大概描述 :scheme是一种页面内跳转协议,通过定义自己的scheme协议,native 拦截这个请求,按需求处理,可以非常方便跳转app中的各个页面。 72 | 73 | 通过执行以下操作支持自定义URL方案: 74 | 75 | - 定义应用程序**schema** URL的格式。 76 | - 注册应用程序**schema** URL方案,以便系统将适当的URL定向到应用程序。 77 | - 应用程序处理接收到的**schema**URL。 78 | 79 | **一些URL Scheme** 80 | 81 | **[www.zhihu.com/question/19…](https://link.juejin.cn/?target=https%3A%2F%2Fwww.zhihu.com%2Fquestion%2F19907735 "https://www.zhihu.com/question/19907735")** 82 | 83 | **![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409ce98ffb0f3~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp)** 84 | 85 | 截取一段代码,代码来源:[github.com/tcoulter/jo…](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ftcoulter%2Fjockeyjs%2Fblob%2Fmaster%2FJockeyJS%2Fjs%2Fjockey.js "https://github.com/tcoulter/jockeyjs/blob/master/JockeyJS/js/jockey.js") 86 | 87 | ``` 88 | // 这段代码主要功能是前端通过一个特殊的协议给客户端发消息,客户端拦截请求然后处理 89 | dispatchMessage: function(type, envelope) { 90 | // We send the message by navigating the browser to a special URL. 91 | // The iOS library will catch the navigation, prevent the UIWebView 92 | // from continuing, and use the data in the URL to execute code 93 | // within the iOS app. 94 | 95 | var src = "jockey://" + type + "/" + envelope.id + "?" + encodeURIComponent(JSON.stringify(envelope)); 96 | var iframe = document.createElement("iframe"); 97 | iframe.setAttribute("src", src); 98 | document.documentElement.appendChild(iframe); 99 | iframe.parentNode.removeChild(iframe); 100 | iframe = null; 101 | }复制代码 102 | ``` 103 | 104 | ### 2.2.具体H5与Native通信,JS to native? 105 | 106 | JS与Native通信一般都是创建这类URL被Native捕获处理。 107 | 108 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409ce9a5944d7~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 109 | 110 | ### 2.3.native到h5页面,native to JS? 111 | 112 | native提供的容器盒子那么native,可否调用它提供的webview中window对象的方法了? 113 | 114 | native 可以 调用之前跟前端约定好的挂载在window对象上面的方法。(具体实现了?) 115 | 116 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409ce9fe307eb~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 117 | 118 | [(图片来源点击)](https://link.juejin.cn/?target=https%3A%2F%2Fblog.csdn.net%2Fgongch0604%2Farticle%2Fdetails%2F80510005 "https://blog.csdn.net/gongch0604/article/details/80510005") 119 | 120 | ``` 121 | / Send an event to JavaScript, passing a payload. 122 | // payload can be an NSDictionary or NSArray, or anything that is serializable to JSON. 123 | // It can be nil. 124 | [Jockey send:@"event-name" withPayload:payload toWebView:webView]; 125 | 126 | // If you want to send an event and also execute code within the iOS app when all 127 | // JavaScript listeners have finished processing. 128 | [Jockey send:@"event-name" withPayload:payload toWebView:webView perform:^{ 129 | // Respond to callback. 130 | }];复制代码 131 | ``` 132 | 133 | ## 3.前端和客户端的交互实现 134 | 135 | 前端与Native两种交互形式: 136 | 137 | ① URL Schema(前端先定义对象,以及交互方法) 138 | 139 | ② 客户端定义对象,注入全局变量(Android本身就支持类似的实现,所以此处讨论ios的JavaScriptCore ) 140 | 141 | JavaScriptCore是一个C++实现的开源项目。使用Apple提供的JavaScriptCore框架,可以在Objective-C或者基于C的程序中执行Javascript代码,也可以向JavaScript环境中插入一些自定义的对象。JavaScriptCore从iOS 7.0之后可以直接使用,资料:https://developer.apple.com/documentation/javascriptcore。 142 | 143 | ### 3.1URL Schema(前端先定义对象,以及交互方法) 144 | 145 | ``` 146 | 147 | 148 | 149 | 150 | 151 | Document 152 | 153 | 154 | 155 | 179 | 180 | 复制代码 181 | ``` 182 | 183 | 通常代码实现会把send函数部分进行封装,前端只需要实现与native约定的功能函数。 184 | 185 | ``` 186 | getPort: function() { 187 | window.send("getPort", {}, (payload) => { 188 | 189 | }); 190 | }, 191 | login: function(args = {}) { 192 | window.send("login", args, (payload) => { 193 | 194 | }); 195 | },复制代码 196 | ``` 197 | 198 | ### 3.2.客户端定义对象,注入全局变量(客户端注入全局变量,供h5使用) 199 | 200 | ``` 201 | // 页面调用了未声明方法,事实上是Native注入给window对象的。(native在本地实现了js方法并注入h5) 202 | // 在页面加载完成前注入全局变量myapp,myapp下面的方法,即app提供的API方法 203 | myapp.getPort(data, (payload) => { 204 | 205 | }); 206 | myapp.login(data, (payload) => { 207 | 208 | });复制代码 209 | ``` 210 | 211 | 两种方式的区别,由客户端还是由h5预先定义对象,以及交互方法 。 212 | 213 | ## 4.前端交互实现关注点 214 | 215 | 代码来源:[github.com/tcoulter/jo…](https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Ftcoulter%2Fjockeyjs%2Fblob%2Fmaster%2FJockeyJS%2Fjs%2Fjockey.js "https://github.com/tcoulter/jockeyjs/blob/master/JockeyJS/js/jockey.js")(以下代码例子,以URL Schema方式为基础) 216 | 217 | ### 4.1.导航栏的设置 218 | 219 | 导航栏通常是native实现,有回按钮退防止页面假死,即页面卡死可以回退。 220 | 221 | 同时native需要提供API供h5进行简单的定制(比如有的需要关闭按钮,分享按钮,收藏按钮等) 222 | 223 | ``` 224 | setBarBack() { 225 | Jockey.send("setBarBack", { 226 | "bar": { 227 | "position": "left", 228 | "cliekEvent": "onBack", 229 | } 230 | });  231 | // 取消监听onBack事件 232 | Jockey.off('onBack'); 233 | // 监听onBack事件 234 | Jockey.on('onBack', () => { 235 | history.back() 236 | }); 237 | },复制代码 238 | ``` 239 | 240 | ### 4.2.跳转是Hybrid必用API之一,对前端来说有以下跳转: 241 | 242 | ① H5跳转Native界面 243 | 244 | ② H5新开Webview跳转H5页面。 245 | 246 | 用native的方法来跳转,一般是为了做页面的动画切换。 247 | 248 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409cea27ed623~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 249 | 250 | 如图我们通常只会使用到导航栏下面部分,但我们关注页面的导航栏,H5端可以注册事件监听native的导航栏的事件(非必须) 251 | 252 | ### 4.3获取基本信息 253 | 254 | 在具备用户体系的模式下,H5页面能从native拿到基本的登录信息,Native本身就保存了用户信息,提供接口给h5使用。 255 | 256 | ``` 257 | function getInfo() { 258 | return new Promise((resolve, reject) => { 259 | Jockey.send("getInfo", {}, (payload) => { 260 | 261 | }); 262 | }); 263 | },复制代码 264 | ``` 265 | 266 | ### 4.4调用native原生具备的功能 267 | 268 | 相机,手机页面横屏显示或者竖屏显示等。 269 | 270 | ### 4.5关于调试 271 | 272 | **Android:输入chrome://inspect/#devices即可(前提native打开了调试模式),当然Android也可以使用模拟器,但与Android的真机表现过于不一样,还是建议使用真机测试。** 273 | 274 | **![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409cedfe5cc3c~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp)** 275 | 276 | **iOS:需一台Mac机,然后打开safari,在偏好设置中将开发模式打开,然后点击打开safari浏览器,查看菜单栏开发菜单(前提native打开了调试模式)...** 277 | 278 | ![](https://p1-jj.byteimg.com/tos-cn-i-t2oaga2asx/gold-user-assets/2019/4/21/16a409cf05288687~tplv-t2oaga2asx-zoom-in-crop-mark:3024:0:0:0.awebp) 279 | 280 | ## 5.小结 281 | 282 | 文章只是一个对hybrid的简单了解,文中例子比较粗糙,理解不准确之处,还请教正。对于有兴趣深入了解Hybrid技术的设计与实现前端部分细节的可以看看参考资料~~ 283 | 284 | 参考资料: 285 | 286 | [www.cnblogs.com/yexiaochai/…](https://link.juejin.cn/?target=http%3A%2F%2Fwww.cnblogs.com%2Fyexiaochai%2Fp%2F4921635.html "http://www.cnblogs.com/yexiaochai/p/4921635.html") 287 | 288 | [www.cnblogs.com/yexiaochai/…](https://link.juejin.cn/?target=http%3A%2F%2Fwww.cnblogs.com%2Fyexiaochai%2Fp%2F5524783.html "http://www.cnblogs.com/yexiaochai/p/5524783.html") 289 | 290 | [www.cnblogs.com/yexiaochai/…](https://link.juejin.cn/?target=http%3A%2F%2Fwww.cnblogs.com%2Fyexiaochai%2Fp%2F5813248.html "http://www.cnblogs.com/yexiaochai/p/5813248.html") -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/14.优化篇-细节处理(持续).md: -------------------------------------------------------------------------------- 1 | ## 一 前言 2 | 3 | 本章节,我将继续补充一些 React 开发中细节问题的解决方案。 4 | 5 | ## 二 细节 6 | 7 | ### 1 React中防抖和节流 8 | 9 | **防抖** 10 | 11 | 防抖和节流在 React 应用中是很常用的,防抖很适合 React 表单的场景,比如点击按钮防抖,search 输入框。举一个简单的例子。 12 | 13 | ```js 14 | export default class Index extends React.Component{ 15 | constructor(props){ 16 | super(props) 17 | } 18 | handleClick= () => { 19 | console.log('点击事件-表单提交-调用接口') 20 | } 21 | handleChange= (e) => { 22 | console.log('搜索框-请求数据') 23 | } 24 | render(){ 25 | return
26 |
27 | 28 |
29 | } 30 | } 31 | ``` 32 | 33 | - 如上,当点击按钮的时候,向服务端发起数据交互;输入 input 时候,同样会向服务端进行数据交互,请求搜索的数据。对于如上的情况如果不做任何优化处理的话,连续点击按钮,或者 input 输入内容的时候,就会出现这种情况。 34 | 35 | ![390bc5aad3454d86a8b6c0b3a2d22b1a_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/390bc5aad3454d86a8b6c0b3a2d22b1a_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 36 | 37 | 如上,会频繁和服务端交互,很显然这种情况是不符合常理的。所以需要防抖处理。 38 | 39 | ```js 40 | constructor(props){ 41 | super(props) 42 | this.handleClick = debounce(this.handleClick,500) /* 防抖 500 毫秒 */ 43 | this.handleChange = debounce(this.handleChange,300) /* 防抖 300 毫秒 */ 44 | } 45 | ``` 46 | 47 | 效果: 48 | 49 | ![2.gif](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58d282f3433b4552bdba0e6e1487acc7~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 50 | 51 | **节流** 52 | 53 | 节流函数一般也用于频繁触发的事件中,比如**监听滚动条滚动**。 54 | 55 | ```js 56 | export default function Index(){ 57 | /* useCallback 防止每次组件更新都重新绑定节流函数 */ 58 | const handleScroll = React.useCallback(throttle(function(){ 59 | /* 可以做一些操作,比如曝光上报等 */ 60 | },300)) 61 | return
62 |
hello,world
63 |
64 | } 65 | ``` 66 | 67 | - 如上将监听滚动函数做节流处理,300 毫秒触发一次。用 useCallback 防止每一次组件更新重新绑定节流函数。 68 | 69 | 防抖节流总结: 70 | 71 | - 防抖函数一般用于表单搜索,点击事件等场景,目的就是为了防止短时间内多次触发事件。 72 | - 节流函数一般为了降低函数执行的频率,比如滚动条滚动。 73 | 74 | ### 2 按需引入 75 | 76 | 按需引入本质上是为项目瘦身,开发者在做 React 项目的时候,会用到 antd 之类的 UI 库,值得思考的一件事是,开发者如果只是用到了 antd 中的个别组件,比如 Button,就要把整个样式库引进来,打包就会发现,体积因为引入了整个样式文件大了很多。所以可以通过 `.babelrc` 实现按需引入。 77 | 78 | 瘦身前体积: 79 | 80 | image-20220922091932659 81 | 82 | .babelrc 增加对 antd 样式按需引入。 83 | 84 | ```js 85 | ["import", { 86 | "libraryName": 87 | "antd", 88 | "libraryDirectory": "es", 89 | "style": true 90 | }] 91 | ``` 92 | 93 | 瘦身后体积: 94 | 95 | image-20220922091955117 96 | 97 | ### 3 React动画 98 | 99 | React 写动画也是一个比较棘手的问题。高频率的 setState 会给应用性能带来挑战,这种情况在 M 端更加明显,因为 M 端的渲染能力受到手机性能的影响较大。所以对 React 动画的处理要格外注意。我这里总结了三种 React 使用动画的方式,以及它们的权重。 100 | 101 | #### ① 首选:动态添加类名 102 | 103 | 第一种方式是通过 transition,animation 实现动画然后写在 class 类名里面,通过动态切换类名,达到动画的目的。 104 | 105 | ```js 106 | export default function Index(){ 107 | const [ isAnimation , setAnimation ] = useState(false) 108 | return
109 | 110 |
111 |
112 | } 113 | ``` 114 | 115 | ```js 116 | .current{ 117 | width: 50px; 118 | height: 50px; 119 | border-radius: 50%; 120 | background: #fff; 121 | border: 1px solid #ccc; 122 | } 123 | .animation{ 124 | animation: 1s changeColor; 125 | background:yellowgreen; 126 | } 127 | @keyframes changeColor { 128 | 0%{background:#c00;} 129 | 50%{background:orange;} 130 | 100%{background:yellowgreen;} 131 | } 132 | ``` 133 | 134 | 效果 135 | 136 | ![4bfef6017c0844e4abea4a79b5ad8db1_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/4bfef6017c0844e4abea4a79b5ad8db1_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 137 | 138 | 这种方式是我最优先推荐的方式,这种方式既不需要频繁 setState ,也不需要改变 DOM 。 139 | 140 | #### ② 其次:操纵原生 DOM 141 | 142 | 如果第一种方式不能满足要求的话,或者必须做一些 js 实现复杂的动画效果,那么可以获取原生 DOM ,然后单独操作 DOM 实现动画功能,这样就避免了 setState 改变带来 React Fiber 深度调和渲染的影响。 143 | 144 | ```js 145 | export default function Index(){ 146 | const dom = useRef(null) 147 | const changeColor = ()=>{ 148 | const target = dom.current 149 | target.style.background = '#c00' 150 | setTimeout(()=>{ 151 | target.style.background = 'orange' 152 | setTimeout(()=>{ 153 | target.style.background = 'yellowgreen' 154 | },500) 155 | },500) 156 | } 157 | return
158 | 159 |
160 |
161 | } 162 | ``` 163 | 164 | 同样达到如上的效果 165 | 166 | #### ③ 再者:setState + css3 167 | 168 | 如果 ① 和 ② 都不能满足要求,一定要使用 setState 实时改变DOM元素状态的话,那么尽量采用 css3 , css3 开启硬件加速,使 GPU (Graphics Processing Unit) 发挥功能,从而提升性能。 169 | 170 | 比如想要改变元素位置 left ,top 值,可以换一种思路通过改变 transform: translate,transform 是由 GPU 直接控制渲染的,所以不会造成浏览器的重排。 171 | 172 | ```js 173 | export default function Index(){ 174 | const [ position , setPosition ] = useState({ left:0,top:0 }) 175 | const changePosition = ()=>{ 176 | let time = 0 177 | let timer = setInterval(()=>{ 178 | if(time === 30) clearInterval(timer) 179 | setPosition({ left:time * 10 , top:time * 10 }) 180 | time++ 181 | },30) 182 | } 183 | const { left , top } = position 184 | return
185 | 186 |
187 |
188 | } 189 | ``` 190 | 191 | **效果** 192 | 193 | ![134a3683658d4440932b8aeccd55591d_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/134a3683658d4440932b8aeccd55591d_tplv-k3u1fbpfcp-zoom-in-crop-mark_1304_0_0_0.gif) 194 | 195 | ### 4 及时清除定时器/延时器/监听器 196 | 197 | 如果在 React 项目中,用到了定时器,延时器和事件监听器,注意要在对应的生命周期,清除它们,不然可能会造成内部泄露的情况。 198 | 199 | 类组件: 200 | 201 | ```js 202 | export default class Index extends React.Component{ 203 | current = null 204 | poll=()=>{} /* 轮训 */ 205 | handleScroll=()=>{} /* 处理滚动事件 */ 206 | componentDidMount(){ 207 | this.timer = setInterval(()=>{ 208 | this.poll() /* 2 秒进行一次轮训事件 */ 209 | },2000) 210 | this.current.addEventListener('scroll',this.handleScroll) 211 | } 212 | componentWillUnmount(){ 213 | clearInterval(this.timer) /* 清除定时器 */ 214 | this.current.removeEventListener('scroll',this.handleScroll) 215 | } 216 | render(){ 217 | return
this.current = node } >hello,let us learn React!
218 | } 219 | } 220 | ``` 221 | 222 | - 在 componentWillUnmount 生命周期及时清除延时器和事件监听器。 223 | 224 | 函数组件: 225 | 226 | ```js 227 | export default function Index(){ 228 | const dom = React.useRef(null) 229 | const poll = ()=>{} 230 | const handleScroll = ()=>{} 231 | useEffect(()=>{ 232 | let timer = setInterval(()=>{ 233 | poll() /* 2 秒进行一次轮训事件 */ 234 | },2000) 235 | dom.current.addEventListener('scroll',handleScroll) 236 | return function(){ 237 | clearInterval(timer) 238 | dom.current.removeEventListener('scroll',handleScroll) 239 | } 240 | },[]) 241 | return
hello,let us learn React!
242 | } 243 | ``` 244 | 245 | - 在 useEffect 或者 useLayoutEffect 第一个参数 create 的返回函数 destory 中,做一些清除定时器/延时器的操作。 246 | 247 | ### 5 合理使用state 248 | 249 | React 并不像 vue 那样响应式数据流。 在 vue 中有专门的 dep 做依赖收集,可以自动收集字符串模版的依赖项,只要没有引用的 data 数据, 通过 `this.aaa = bbb` ,在 vue 中是不会更新渲染的。但是在 React 中只要触发 setState 或 useState ,如果没有渲染控制的情况下,组件就会渲染,暴露一个问题就是,如果视图更新不依赖于当前 state ,那么这次渲染也就没有意义。所以对于视图不依赖的状态,就可以考虑不放在 state 中。 250 | 251 | 打个比方,比如想在滚动条滚动事件中,记录一个 scrollTop 位置,那么在这种情况下,用 state 保存 scrollTop 就没有任何意义而且浪费性能。 252 | 253 | ```js 254 | export default class Index extends React.Component{ 255 | node = null 256 | scrollTop = 0 257 | handleScroll=()=>{ 258 | const { scrollTop } = this.node 259 | this.scrollTop = scrollTop 260 | } 261 | render(){ 262 | return
this.node = node } onScroll={this.handleScroll} >
263 | } 264 | } 265 | ``` 266 | 267 | 上述把 scrollTop 直接绑定在 this 上,而不是通过 state 管理,这样好处是滚动条滚动不需要触发 setState ,从而避免了无用的更新。 268 | 269 | 对于函数组件,因为不存在组件实例,但是函数组件有 hooks ,所以可以通过一个 useRef 实现同样的效果。 270 | 271 | ```js 272 | export default function Index(){ 273 | const dom = useRef(null) 274 | const scrollTop = useRef(0) 275 | const handleScroll = ()=> { 276 | scrollTop.current = dom.current.scrollTop 277 | } 278 | return
279 | } 280 | ``` 281 | 282 | - 如上用 useRef ,来记录滚动条滚动时 scrollTop 的值。 283 | 284 | ### 6 建议不要在 hooks 的参数中执行函数或者 new 实例 285 | 286 | 有一种场景是平时比较容易忽略的,就是在 `hooks` 的参数中执行函数或者 new 实例,比如如下这样: 287 | 288 | ```js 289 | const hook1 = useRef(fn()) 290 | const hook2 = useRef(new Fn()) 291 | ``` 292 | 293 | 不建议这么写。为什么呢? 294 | 295 | - 首先函数每次 `rerender` 都会执行 hooks ,那么在执行 hooks 函数的同时,也会执行函数的参数,比如上面的代码片段中的 `fn()` 和 `new Fn()`,也就是每一次 rerender 都会执行 fn 或者是 new 一个实例。这可能不是开发者期望的,而执行函数,或创建实例也成了一种性能浪费,在一些极端情况下,可能会造成内存泄漏,比如在创建新的 dom 元素,但是没有进行有效的回收。 296 | 297 | - 在 hooks 原理章节讲到过,函数组件在**初始化**和**更新**流程中,会使用不同的 hooks 对象,还是以 `useRef` 为例子,在初始化阶段用的是 `mountRef`函数,在更新阶段用的是 `updateRef`函数,开发者眼睛看见的是 `useRef`,在 React 底层却悄悄的替换成了不同的函数。 更重要的是大部分的 hooks 参数都作为**初始化**的参数,在更新阶段压根没有用到,那么传入的参数也就没有了意义,回到上述代码片段,`fn()` 和 `new Fn()`在更新阶段根本就没有被 `useRef`接收, 无辜的成了流浪者。 298 | 299 | 300 | 还是以 `useRef` 为例子,看一下它在不同阶段的真正面目。 301 | 302 | **初始化** 303 | 304 | ```js 305 | function mountRef(initialValue) { 306 | const hook = mountWorkInProgressHook(); 307 | const ref = {current: initialValue}; 308 | hook.memoizedState = ref; 309 | return ref; 310 | } 311 | ``` 312 | 313 | - 初始化的时候用到了 initialValue ,也就是第一个参数。 314 | 315 | **更新阶段** 316 | 317 | ```js 318 | function updateRef(initialValue) { 319 | const hook = updateWorkInProgressHook(); 320 | return hook.memoizedState; 321 | } 322 | ``` 323 | 324 | - 在更新阶段根本没有用到 initialValue。 325 | 326 | 那么回到最初的目的上来,如果开发者真的想在 hooks 中,以函数组件执行结果或者是实例对象作为参数的话,那么应该怎么处理呢。这个很简单,可以用 useMemo 包装一下。比如: 327 | 328 | ```js 329 | const hook = useRef(null) 330 | const value = useMemo(()=>{ 331 | hook.current = new Fn() 332 | },[ changeValue ]) 333 | ``` 334 | 335 | 如上,通过 useMemo 派生出来的 value ,作为初始化 Ref 的值,这样做还有一个好处,如果 Ref 的值,依赖于 `changeValue` ,当 changeValue 改变的时候,会重新给 Ref 对象赋值。 336 | 337 | ## 三 总结 338 | 339 | 本章补充了前几章没有提到的优化点,实际开发中,还有很多细节,欢迎大家在留言区域补充,然后我统一添加到本章内容里。下一章将开始进入 React 原理篇。 340 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/37.v18特性篇-concurrent下的state更新流程.md: -------------------------------------------------------------------------------- 1 | 2 | > ## Excerpt 3 | > 之前介绍了在 legacy 模式下的 state 更新流程,这种模式下的批量更新原理本质上是通过不同的更新上下文开关 Context ,比如 batch 或者 event 来让更新变成‘可控的’。那么在 v18 conCurrent 下 React 的更新又有哪些特点呢?这就是本章节探讨的问题,本章节涵盖的知识点如下: 4 | 5 | --- 6 | ## 一 前言 7 | 8 | 之前介绍了在 legacy 模式下的 state 更新流程,这种模式下的批量更新原理本质上是通过不同的更新上下文开关 Context ,比如 batch 或者 event 来让更新变成‘可控的’。那么在 v18 conCurrent 下 React 的更新又有哪些特点呢?这就是本章节探讨的问题,本章节涵盖的知识点如下: 9 | 10 | - concurrent 模式下的 state 更新流程是什么 ? 11 | - 在同步异步条件下,state 更新有什么区别 ? 12 | - 主流框架中更新处理方式。 13 | 14 | ## 二 主流框架中更新处理方式 15 | 16 | 在正式讲解 v18 concurrent 之前,先来看一下主流框架中两种批量更新的原理。 17 | 18 | ### 1 第一种:微任务|宏任务实现集中更新 19 | 20 | 第一种批量更新的实现,就是基于**宏任务** 和 **微任务** 来实现。 21 | 22 | 先来描述一下这种方式,比如每次更新,我们先并不去立即执行更新任务,而是先把每一个更新任务放入一个待更新队列 `updateQueue` 里面,然后 js 执行完毕,用一个微任务统一去批量更新队列里面的任务,如果微任务存在兼容性,那么降级成一个宏任务。这里**优先采用微任务**的原因就是微任务的执行时机要早于下一次宏任务的执行。 23 | 24 | 典型的案例就是 vue 更新原理,`vue.$nextTick`原理 ,还有接下来要介绍的 v18 中 `scheduleMicrotask` 的更新原理。 25 | 26 | 以 vue 为例子我们看一下 nextTick 的实现: 27 | 28 | > runtime-core/src/scheduler.ts 29 | 30 | ```js 31 | const p = Promise.resolve() /* nextTick 实现,用微任务实现的 */ export function nextTick(fn?: () => void): Promise { return fn ? p.then(fn) : p } 32 | ``` 33 | 34 | - 可以看到 nextTick 原理,本质就是 `Promise.resolve()` 创建的微任务。 35 | 36 | 大致实现流程图如下所示: 37 | 38 | ![4.jpeg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/820a397dc96e4b66a90ad74251bb02e6~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 39 | 40 | 我们也可以来模拟一下整个流程的实现。 41 | 42 | ```js 43 | class Scheduler { constructor(){ this.callbacks = [] /* 微任务批量处理 */ queueMicrotask(()=>{ this.runTask() }) } /* 增加任务 */ addTask(fn){ this.callbacks.push(fn) } runTask(){ console.log('------合并更新开始------') while(this.callbacks.length > 0){ const cur = this.callbacks.shift() cur() } console.log('------合并更新结束------') console.log('------开始更新组件------') } } function nextTick(cb){ const scheduler = new Scheduler() cb(scheduler.addTask.bind(scheduler)) } /* 模拟一次更新 */ function mockOnclick(){ nextTick((add)=>{ add(function(){ console.log('第一次更新') }) console.log('----宏任务逻辑----') add(function(){ console.log('第二次更新') }) }) } mockOnclick() 44 | ``` 45 | 46 | 我们来模拟一下具体实现细节: 47 | 48 | - 通过一个 Scheduler 调度器来完成整个流程。 49 | - 通过 addTask 每次向队列中放入任务。 50 | - 用 queueMicrotask 创建一个微任务,来统一处理这些任务。 51 | - mockOnclick 模拟一次更新。我们用 nextTick 来模拟一下更新函数的处理逻辑。 52 | 53 | 看一下打印效果: 54 | 55 | ![3.jpeg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/115940d55be44d4797403c97b43b5f46~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 56 | 57 | ### 2 第二种:可控任务实现批量更新 58 | 59 | 还有一种方式,通过拦截把任务变成**可控的**,典型的就是 React v17 之前的 batchEventUpdate 批量更新,这个方式接下来会讲到,这里也不赘述了。这种情况的更新来源于对事件进行拦截,比如 React 的事件系统。 60 | 61 | 以 React 的事件批量更新为例子,比如我们的 onClick ,onChange 事件都是被 React 的事件系统处理的。外层用一个统一的处理函数进行拦截。而我们绑定的事件都是在该函数的执行上下文内部被调用的。 62 | 63 | 那么比如在一次点击事件中触发了多次更新。本质上外层在 React 事件系统处理函数的上下文中,这样的情况下,就可以通过一个开关,证明当前更新是可控的,可以做批量处理。接下来 React 就用一次就可以了。 64 | 65 | 我们用一幅流程图来描述一下原理。 66 | 67 | ![5.jpeg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0ea25cbafb4e4d6ebdfb0bf1531c3e82~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 68 | 69 | 接下来我们模拟一下具体的实现: 70 | 71 | ```html 72 | 73 | ``` 74 | 75 | 打印结果: 76 | 77 | ![6.jpg](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/c070f19898a84b0f8225c1b67f1db74d~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 78 | 79 | 分析一下核心流程: 80 | 81 | - 本方式的核心就是让 handleClick 通过 wrapEvent 变成可控的。首先 wrapEvent 类似于事件处理函数,在内部通过开关 batchEventUpdate 来判断是否开启批量更新状态,最后通过 flushSyncCallbackQueue 来清空待更新队列。 82 | 83 | - 在批量更新条件下,事件会被放入到更新队列中,非批量更新条件下,那么立即执行更新任务。 84 | 85 | 86 | ## 三 与传统 legacy 模式的区别 87 | 88 | 言归正传,回到接下来要介绍的主题上来,首先对于传统的 legacy 模式,有可控任务批量处理的概念,也就是采用了上面第二种批量更新模式,原理第33章讲到主要有两个: 89 | 90 | - 通过不同的更新上下文开关,在开关里的任务是可控的,可以进行批量处理。 91 | - 在事件之行完毕后,通过 `flushSyncCallback` 来进行更新任务之行。 92 | 93 | 那么在 conCurrent 下的更新采用了一个什么方式呢?首先在这种模式下,取消了批量更新的感念。我们以事件系统的更新例子,研究一下两种的区别。 94 | 95 | 在老版本事件系统中: 96 | 97 | > react-dom/src/events/ReactDOMUpdateBatching.js 98 | 99 | ```js 100 | export function batchedEventUpdates(fn,a){ isBatchingEventUpdates = true; //打开批量更新开关 try{ fn(a) // 事件在这里执行 }finally{ isBatchingEventUpdates = false //关闭批量更新开关 if (executionContext === NoContext) { flushSyncCallbackQueue(); // TODO: 这个很重要,用来同步执行更新队列中的任务 } } } 101 | ``` 102 | 103 | - 通过开关 isBatchingEventUpdates 来让 fn 里面的更新变成可控的,所以可以进行批量更新。 104 | - 重点就是 flushSyncCallbackQueue 用来同步执行更新队列中的任务。 105 | 106 | 在最新版本的 v18 alpha 系统中,事件变成了这样 (这个代码和代码仓库的有一些出入,我们这里只关心流程就好): 107 | 108 | ```js 109 | function batchedEventUpdates(){ var prevExecutionContext = executionContext; executionContext |= EventContext; // 运算赋值 try { return fn(a); // 执行函数 }finally { executionContext = prevExecutionContext; // 重置之前的状态 if (executionContext === NoContext) { flushSyncCallbacksOnlyInLegacyMode() // 同步执行更新队列中的任务 } } } 110 | ``` 111 | 112 | 从上述代码中可以清晰的看到,v18 alpha 版本的流程大致是这样的: 113 | 114 | - 也是通过类似开关状态来控制的,在刚开始的时候将赋值给 `EventContext` ,然后在事件执行之后,赋值给 `prevExecutionContext`。 115 | 116 | - 之后同样会触发 flushSyncCallbacksOnlyInLegacyMode ,不过通过函数名称就可以大胆猜想,这个方法主要是针对 legacy 模式的更新,那么 concurrent mode 下也就不会走 flushSyncCallback 的逻辑了。 117 | 118 | 119 | 为了证明这个猜想,一起来看一下 `flushSyncCallbacksOnlyInLegacyMode` 做了些什么事: 120 | 121 | > react-reconciler/src/ReactFiberSyncTaskQueue.js 122 | 123 | ```js 124 | export function flushSyncCallbacksOnlyInLegacyMode(){ if(includesLegacySyncCallbacks){ /* 只有在 legacy 模式下,才会走这里的流程。 */ flushSyncCallbacks(); } } 125 | ``` 126 | 127 | - 验证了之前的猜测,**只有在 legacy 模式下,才会执行 flushSyncCallbacks 来同步执行任务。** 128 | 129 | 在之前的章节讲到过 flushSyncCallbacks 主要作用是,能够在一次更新中,直接同步更新任务,防止任务在下一次的宏任务中执行。那么对于 concurrent 下的更新流程是怎么样的呢? 130 | 131 | ### 一次更新 state 会发生什么? 132 | 133 | 接下来一起研究一下一次更新 state 会发生什么?首先编写一下如下 `demo` : 134 | 135 | ```js 136 | function Index(){ const [ number , setNumber ] = React.useState(0) /* 同步条件下 */ const handleClickSync = () => { setNumber(1) setNumber(2) } /* 异步条件下 */ const handleClick = () => { setTimeout(()=>{ setNumber(1) setNumber(2) },0) } console.log('----组件渲染----') return
{number}
} 137 | ``` 138 | 139 | **在 v17 legacy 下更新:** 140 | 141 | - 点击按钮 `同步环境下`,组件渲染一次。 142 | 143 | ![7.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce67eb14209a4fb8846ce4e50aa29fe0~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 144 | 145 | - 点击按钮 `异步环境下`,组件会渲染二次。相信读过之前章节的同学,都明白原理是什么,在异步条件下的更新任务,不在 React 可控的范围内,所以会触发两次流程。 146 | 147 | ![8.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f4d7fcc26b84a65a3d1b2a669b09639~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 148 | 149 | **重点来了,我们看一下 v18 concurrent 下更新:** 150 | 151 | - 无论点击 **`同步环境下`** 还是 **`异步环境下`** ,组件都会执行一次。 152 | 153 | ![7.jpg](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/35d739049bff4e7ab6f72c48b8a83d09~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 154 | 155 | 首先想一下,在 concurrent 下,如何实现更新合并的呢? 156 | 157 | ## 四 v18 更新原理揭秘 158 | 159 | 按照上面的问题,来探究一下 `concurrent` 下的更新原理。我们还是按照**同步**和**异步**两个方向去探索。 无论是那种条件下,只要触发 React 的 `setState` 或者 `useState`,最终进入调度任务开始更新的入口函数都是 `ensureRootIsScheduled` ,所以可以从这个函数找到线索。 160 | 161 | > react-reconciler/src/ReactFiberWorkLoop.js -> ensureRootIsScheduled 162 | 163 | ```js 164 | function ensureRootIsScheduled(root,currentTime){ var existingCallbackNode = root.callbackNode; var newCallbackPriority = getHighestPriorityLane(nextLanes); var existingCallbackPriority = root.callbackPriority; if (existingCallbackPriority === newCallbackPriority && !( ReactCurrentActQueue.current !== null && existingCallbackNode !== fakeActCallbackNode)) { /* 批量更新退出* */ return; } /* 同步更新条件下,会走这里的逻辑 */ if (newCallbackPriority === SyncLane) { scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root)); /* 用微任务去立即执行更新 */ scheduleMicrotask(flushSyncCallbacks); }else{ newCallbackNode = scheduleCallback( schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root), ); } /* 这里很重要就是给当前 root 赋予 callbackPriority 和 callbackNode 状态 */ root.callbackPriority = newCallbackPriority; root.callbackNode = newCallbackNode; } 165 | ``` 166 | 167 | ### 1 同步条件下的逻辑 168 | 169 | 首先我们来看一下,同步更新的逻辑,上面讲到在 concurrent 中已经没有可控任务那一套逻辑。所以核心更新流程如下: 170 | 171 | 当同步状态下触发多次 useState 的时候。 172 | 173 | - 首先第一次进入到 ensureRootIsScheduled ,会计算出 `newCallbackPriority` 可以理解成执行新的更新任务的优先级。那么和之前的 `callbackPriority` 进行对比,如果相等那么退出流程,那么第一次两者肯定是不想等的。 174 | 175 | - 同步状态下常规的更新 newCallbackPriority 是等于 `SyncLane` 的,那么会执行两个函数,`scheduleSyncCallback` 和 `scheduleMicrotask`。 176 | 177 | 178 | `scheduleSyncCallback` 会把任务 `syncQueue` 同步更新队列中。来看一下这个函数: 179 | 180 | > react-reconciler/src/ReactFiberSyncTaskQueue.js -> scheduleSyncCallback 181 | 182 | ```js 183 | export function scheduleSyncCallback(callback: SchedulerCallback) { if (syncQueue === null) { syncQueue = [callback]; } else { syncQueue.push(callback); } } 184 | ``` 185 | 186 | - **注意:接下来就是 concurrent 下更新的区别了。在老版本的 React 是基于事件处理函数执行的 flushSyncCallbacks ,而新版本 React 是通过 scheduleMicrotask 执行的。** 187 | 188 | 我们看一下 scheduleMicrotask 到底是什么? 189 | 190 | > react-reconciler/src/ReactFiberHostConfig.js -> scheduleMicrotask 191 | 192 | ```js 193 | var scheduleMicrotask = typeof queueMicrotask === 'function' ? queueMicrotask : typeof Promise !== 'undefined' ? function (callback) { return Promise.resolve(null).then(callback).catch(handleErrorInNextTick); } : scheduleTimeout; 194 | ``` 195 | 196 | scheduleMicrotask 本质上就是 `Promise.resolve` ,还有一个 setTimeout 向下兼容的情况。通过 scheduleMicrotask 去进行调度更新。 197 | 198 | - 那么如果发生第二次 useState ,则会出现 `existingCallbackPriority === newCallbackPriority` 的情况,接下来就会 return 退出更新流程了。 199 | 200 | ### 2 异步条件下的逻辑 201 | 202 | 在异步情况下,比如在 `setTimeout` 或者是 `Promise.resolve` 条件下的更新,会走哪些逻辑呢? 203 | 204 | - 第一步也会判断 existingCallbackPriority === newCallbackPriority 是否相等,相等则退出。 205 | - 第二步则就有点区别了。会直接执行 `scheduleCallback` ,然后得到最新的 newCallbackNode,并赋值给 root 。 206 | - 接下来第二次 useState ,同样会 return 跳出 `ensureRootIsScheduled` 。 207 | 208 | 看一下 scheduleCallback 做了哪些事。 209 | 210 | > react-reconciler/src/ReactFiberWorkLoop.js -> scheduleCallback 211 | 212 | ```js 213 | function scheduleCallback(priorityLevel, callback) { var actQueue = ReactCurrentActQueue.current; if (actQueue !== null) { actQueue.push(callback); return fakeActCallbackNode; } else { return scheduleCallback(priorityLevel, callback); } } 214 | ``` 215 | 216 | 最后用一幅流程图描述一下流程: 217 | 218 | ![9.jpg](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b05ae38731f4104a84b70b6ca5d49a9~tplv-k3u1fbpfcp-jj-mark:1890:0:0:0:q75.awebp) 219 | 220 | ## 五 总结 221 | 222 | 通过本章节我们掌握的知识点有一下内容: 223 | 224 | - 主流框架中更新处理方式。 225 | - concurrent 模式下的 state 更新流程。 226 | - 在同步异步条件下,state 更新的区别。 227 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/3.基础篇-起源Component.md: -------------------------------------------------------------------------------- 1 | ## 一 前言 2 | 3 | 在 React 世界里,一切皆组件,我们写的 React 项目全部起源于组件。组件可以分为两类,一类是**类( Class )组件,一类是函数( Function )组件**。 4 | 5 | 本章节,我们将一起探讨 **React 中类组件和函数组件**的定义,不同组件的通信方式,以及常规组件的强化方式,帮助你全方位认识 React 组件,从而对 React 的底层逻辑有进一步的理解。 6 | 7 | 想要理解 React 组件是什么?我们首先要来分析一下组件和常规的函数和类到底有什么本质的区别。 8 | 9 | ```js 10 | /* 类 */ 11 | class textClass { 12 | sayHello=()=>console.log('hello, my name is alien') 13 | } 14 | /* 类组件 */ 15 | class Index extends React.Component{ 16 | state={ message:`hello ,world!` } 17 | sayHello=()=> this.setState({ message : 'hello, my name is alien' }) 18 | render(){ 19 | return
{ this.state.message }
20 | } 21 | } 22 | /* 函数 */ 23 | function textFun (){ 24 | return 'hello, world' 25 | } 26 | /* 函数组件 */ 27 | function FunComponent(){ 28 | const [ message , setMessage ] = useState('hello,world') 29 | return
setMessage('hello, my name is alien') } >{ message }
30 | } 31 | ``` 32 | 33 | 我们从上面可以清楚地看到,组件本质上就是类和函数,但是与常规的类和函数不同的是,**组件承载了渲染视图的 UI 和更新视图的 setState 、 useState 等方法**。React 在底层逻辑上会像**正常实例化类和正常执行函数那样处理的组件**。 34 | 35 | 因此,**函数与类上的特性在 React 组件上同样具有,比如原型链,继承,静态属性**等,所以不要把 React 组件和类与函数独立开来。 36 | 37 | 接下来,我们一起着重看一下 React 对组件的处理流程。 38 | 39 | > 对于类组件的执行,是在react-reconciler/src/ReactFiberClassComponent.js中: 40 | 41 | ```js 42 | function constructClassInstance( 43 | workInProgress, // 当前正在工作的 fiber 对象 44 | ctor, // 我们的类组件 45 | props // props 46 | ){ 47 | /* 实例化组件,得到组件实例 instance */ 48 | const instance = new ctor(props, context) 49 | } 50 | ``` 51 | 52 | > 对于函数组件的执行,是在react-reconciler/src/ReactFiberHooks.js中 53 | 54 | ```js 55 | function renderWithHooks( 56 | current, // 当前函数组件对应的 `fiber`, 初始化 57 | workInProgress, // 当前正在工作的 fiber 对象 58 | Component, // 我们函数组件 59 | props, // 函数组件第一个参数 props 60 | secondArg, // 函数组件其他参数 61 | nextRenderExpirationTime, //下次渲染过期时间 62 | ){ 63 | /* 执行我们的函数组件,得到 return 返回的 React.element对象 */ 64 | let children = Component(props, secondArg); 65 | } 66 | ``` 67 | 68 | 从中,找到了执行类组件和函数组件的函数。那么为了搞清楚 React 底层是如何处理组件的,首先来看一下类和函数组件是什么时候被实例化和执行的? 69 | 70 | **在 React 调和渲染 fiber 节点的时候,如果发现 fiber tag 是 ClassComponent = 1,则按照类组件逻辑处理,如果是 FunctionComponent = 0 则按照函数组件逻辑处理**。当然 React 也提供了一些内置的组件,比如说 Suspense 、Profiler 等。 71 | 72 | ## 二 二种不同 React 组件 73 | 74 | ### 1 class类组件 75 | 76 | **类组件的定义** 77 | 78 | 在 class 组件中,除了继承 React.Component ,底层还加入了 updater 对象,**组件中调用的 setState 和 forceUpdate** 本质上是调用了 **updater 对象**上的 **enqueueSetState 和 enqueueForceUpdate 方法**。 79 | 80 | 那么,React 底层是如何定义类组件的呢? 81 | 82 | > react/src/ReactBaseClasses.js 83 | 84 | ```js 85 | function Component(props, context, updater) { 86 | this.props = props; //绑定props 87 | this.context = context; //绑定context 88 | this.refs = emptyObject; //绑定ref 89 | this.updater = updater || ReactNoopUpdateQueue; //上面所属的updater 对象 90 | } 91 | /* 绑定setState 方法 */ 92 | Component.prototype.setState = function(partialState, callback) { 93 | this.updater.enqueueSetState(this, partialState, callback, 'setState'); 94 | } 95 | /* 绑定forceupdate 方法 */ 96 | Component.prototype.forceUpdate = function(callback) { 97 | this.updater.enqueueForceUpdate(this, callback, 'forceUpdate'); 98 | } 99 | ``` 100 | 101 | 如上可以看出 Component 底层 React 的处理逻辑是,**类组件执行构造函数过程中会在实例上绑定 props 和 context** ,**初始化置空 refs 属性,原型链上绑定setState、forceUpdate 方法**。**对于 updater,React 在实例化类组件之后会单独绑定 update 对象**。 102 | 103 | **|--------问与答---------|** 104 | 105 | 问:如果没有在 constructor 的 super 函数中传递 props,那么接下来 constructor 执行上下文中就获取不到 props ,这是为什么呢? 106 | 107 | ```js 108 | /* 假设我们在 constructor 中这么写 */ 109 | constructor(){ 110 | super() 111 | console.log(this.props) // 打印 undefined 为什么? 112 | } 113 | ``` 114 | 115 | 答案很简单,刚才的 Component 源码已经说得明明白白了,**绑定 props 是在父类 Component 构造函数中**,执行 super 等于执行 Component 函数,此时 props **没有**作为第一个参数传给 super() ,**在 Component 中就会找不到 props 参数,从而变成 undefined ,在接下来 constructor 代码中打印 props 为 undefined** 。 116 | 117 | ```js 118 | /* 解决问题 */ 119 | constructor(props){ 120 | super(props) 121 | } 122 | ``` 123 | 124 | **|---------end----------|** 125 | 126 | **为了更好地使用 React 类组件,我们首先看一下类组件各个部分的功能:** 127 | 128 | ```js 129 | class Index extends React.Component{ 130 | constructor(...arg){ 131 | super(...arg) /* 执行 react 底层 Component 函数 */ 132 | } 133 | state = {} /* state */ 134 | static number = 1 /* 内置静态属性 */ 135 | handleClick= () => console.log(111) /* 方法: 箭头函数方法直接绑定在this实例上 */ 136 | componentDidMount(){ /* 生命周期 */ 137 | console.log(Index.number,Index.number1) // 打印 1 , 2 138 | } 139 | render(){ /* 渲染函数 */ 140 | return
hello,React!
141 | } 142 | } 143 | Index.number1 = 2 /* 外置静态属性 */ 144 | Index.prototype.handleClick = ()=> console.log(222) /* 方法: 绑定在 Index 原型链的 方法*/ 145 | ``` 146 | 147 | 上面把类组件的主要组成部分都展示给大家了。针对 state ,生命周期等部分,后续会有专门的章节进行讲解。 148 | 149 | **|--------问与答---------|** 150 | 151 | 问:上述**绑定了两个 handleClick** ,那么点击 div 之后会打印什么呢? 152 | 153 | 答:结果是 111 。因为在 class 类内部,**箭头函数是直接绑定在实例对象上的**,而第二个 handleClick 是**绑定在 prototype 原型链上的**,它们的优先级是:**实例对象上方法属性 > 原型链对象上方法属性**。 154 | 155 | **|---------end----------|** 156 | 157 | 对于 `pureComponent` 会在 React 渲染优化章节,详细探讨。 158 | 159 | ### 2 函数组件 160 | 161 | ReactV16.8 hooks 问世以来,对函数组件的功能加以强化,可以在 function 组件中,做类组件一切能做的事情,甚至完全取缔类组件。函数组件的结构相比类组件就简单多了,比如说,下面写了一个常规的函数组件: 162 | 163 | ```js 164 | function Index(){ 165 | console.log(Index.number) // 打印 1 166 | const [ message , setMessage ] = useState('hello,world') /* hooks */ 167 | return
setMessage('let us learn React!') } > { message }
/* 返回值 作为渲染ui */ 168 | } 169 | Index.number = 1 /* 绑定静态属性 */ 170 | ``` 171 | 172 | 注意:不要尝试给函数组件 prototype 绑定属性或方法,即使绑定了也没有任何作用,因为通过上面源码中 React 对函数组件的调用,是采用直接执行函数的方式,而不是通过new的方式。 173 | 174 | 那么,函数组件和类组件本质的区别是什么呢? 175 | 176 | **对于类组件来说,底层只需要实例化一次,实例中保存了组件的 state 等状态。对于每一次更新只需要调用 render 方法以及对应的生命周期就可以了。但是在函数组件中,每一次更新都是一次新的函数执行,一次函数组件的更新,里面的变量会重新声明。** 177 | 178 | 为了能让函数组件可以保存一些状态,执行一些副作用钩子,React Hooks 应运而生,它可以帮助记录 React 中组件的状态,处理一些额外的副作用。 179 | 180 | ## 三 组件通信方式 181 | 182 | React 一共有 5 种主流的通信方式: 183 | 184 | 1. props 和 callback 方式。 185 | 2. ref 方式。 186 | 3. React-redux 或 React-mobx 状态管理方式。 187 | 4. context 上下文方式。 188 | 5. event bus 事件总线。 189 | 190 | 这里主要讲一下第1种和第5种,其余的会在对应章节详细解读。 191 | 192 | **① props 和 callback 方式** 193 | 194 | props 和 callback 可以作为 React 组件最基本的通信方式,**父组件可以通过 props 将信息传递给子组件**,**子组件可以通过执行 props 中的回调函数 callback 来触发父组件的方法,实现父与子的消息通讯**。 195 | 196 | 父组件 -> 通过**自身 state 改变,重新渲染,传递 props -> 通知子组件** 197 | 198 | 子组件 -> **通过调用父组件传过来的 props 里面的方法 -> 通知父组件**。 199 | 200 | ```js 201 | /* 子组件 */ 202 | function Son(props){ 203 | const { fatherSay , sayFather } = props 204 | return
205 | 我是子组件 206 |
父组件对我说:{ fatherSay }
207 | sayFather(e.target.value) } /> 208 |
209 | } 210 | /* 父组件 */ 211 | function Father(){ 212 | const [ childSay , setChildSay ] = useState('') 213 | const [ fatherSay , setFatherSay ] = useState('') 214 | return
215 | 我是父组件 216 |
子组件对我说:{ childSay }
217 | setFatherSay(e.target.value) } /> 218 | 219 |
220 | } 221 | ``` 222 | 223 | 224 | 225 | **⑤event bus事件总线** 226 | 227 | 当然利用 eventBus 也可以实现组件通信,但是在 React 中并不提倡用这种方式,我还是更提倡用 props 方式通信。如果说非要用 eventBus,我觉得它更适合用 React 做基础构建的小程序,比如 Taro。接下来将上述 demo 通过 eventBus 方式进行改造。 228 | 229 | ```js 230 | import { BusService } from './eventBus' 231 | /* event Bus */ 232 | function Son(){ 233 | const [ fatherSay , setFatherSay ] = useState('') 234 | React.useEffect(()=>{ 235 | BusService.on('fatherSay',(value)=>{ /* 事件绑定 , 给父组件绑定事件 */ 236 | setFatherSay(value) 237 | }) 238 | return function(){ BusService.off('fatherSay') /* 解绑事件 */ } 239 | },[]) 240 | return
241 | 我是子组件 242 |
父组件对我说:{ fatherSay }
243 | BusService.emit('childSay',e.target.value) } /> 244 |
245 | } 246 | /* 父组件 */ 247 | function Father(){ 248 | const [ childSay , setChildSay ] = useState('') 249 | React.useEffect(()=>{ /* 事件绑定 , 给子组件绑定事件 */ 250 | BusService.on('childSay',(value)=>{ 251 | setChildSay(value) 252 | }) 253 | return function(){ BusService.off('childSay') /* 解绑事件 */ } 254 | },[]) 255 | return
256 | 我是父组件 257 |
子组件对我说:{ childSay }
258 | BusService.emit('fatherSay',e.target.value) } /> 259 | 260 |
261 | } 262 | ``` 263 | 264 | 这样做不仅达到了和使用 props 同样的效果,还能跨层级,不会受到 React 父子组件层级的影响。但是为什么很多人都不推荐这种方式呢?因为它有一些致命缺点。 265 | 266 | - 需要手动绑定和解绑。 267 | - 对于小型项目还好,但是对于中大型项目,这种方式的组件通信,会造成牵一发动全身的影响,而且后期难以维护,组件之间的状态也是未知的。 268 | - 一定程度上违背了 React 数据流向原则。 269 | 270 | ## 四 组件的强化方式 271 | 272 | **①类组件继承** 273 | 274 | 对于类组件的强化,首先想到的是继承方式,之前开发的开源项目 react-keepalive-router 就是通过继承 React-Router 中的 Switch 和 Router ,来达到缓存页面的功能的。因为 React 中类组件,有良好的继承属性,所以可以针对一些基础组件,首先实现一部分基础功能,再针对项目要求进行有方向的**改造**、**强化**、**添加额外功能**。 275 | 276 | 基础组件: 277 | 278 | ```js 279 | /* 人类 */ 280 | class Person extends React.Component{ 281 | constructor(props){ 282 | super(props) 283 | console.log('hello , i am person') 284 | } 285 | componentDidMount(){ console.log(1111) } 286 | eat(){ /* 吃饭 */ } 287 | sleep(){ /* 睡觉 */ } 288 | ddd(){ console.log('打豆豆') /* 打豆豆 */ } 289 | render(){ 290 | return
291 | 大家好,我是一个person 292 |
293 | } 294 | } 295 | /* 程序员 */ 296 | class Programmer extends Person{ 297 | constructor(props){ 298 | super(props) 299 | console.log('hello , i am Programmer too') 300 | } 301 | componentDidMount(){ console.log(this) } 302 | code(){ /* 敲代码 */ } 303 | render(){ 304 | return
305 | { super.render() } { /* 让 Person 中的 render 执行 */ } 306 | 我还是一个程序员! { /* 添加自己的内容 */ } 307 |
308 | } 309 | } 310 | export default Programmer 311 | ``` 312 | 313 | 效果: 314 | 315 | ![image-20220909231303445](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/image-20220909231303445.png) 316 | 317 | 我们从上面不难发现这个继承增强效果很优秀。它的优势如下: 318 | 319 | 1. 可以控制父类 render,还可以添加一些其他的渲染内容; 320 | 2. 可以共享父类方法,还可以添加额外的方法和属性。 321 | 322 | 但是也有值得注意的地方,就是 state 和生命周期会被继承后的组件修改。像上述 demo 中,Person 组件中的 componentDidMount 生命周期将不会被执行。 323 | 324 | **②函数组件自定义 Hooks** 325 | 326 | 在自定义 hooks 章节,会详细介绍自定义 hooks 的原理和编写。 327 | 328 | **③HOC高阶组件** 329 | 330 | 在 HOC 章节,会详细介绍高阶组件 HOC 。 331 | 332 | ## 五 总结 333 | 334 | 从本章节学到了哪些知识: 335 | 336 | - 知道了 React 组件本质——UI + update + 常规的类和函数 = React 组件 ,以及 React 对组件的底层处理逻辑。 337 | - 明白了函数组件和类组件的区别。 338 | - 掌握组件通信方式。 339 | - 掌握了组件强化方式。 340 | 341 | 下一章节,我们将走进 React 状态管理 state 的世界中,一起探讨 State 的奥秘。 -------------------------------------------------------------------------------- /docs/interview/excel导出优化.md: -------------------------------------------------------------------------------- 1 | # WebWorker 优化数据导出下载 Excel 2 | 3 | --- 4 | 5 | 6 | ## 一、业务背景 7 | 8 | ## 1.1 业务概述 9 | 10 | 在项目当中存在着表格数据导出 Excel 文件的业务需求场景,最原始的技术实现流程是: 11 | 12 | 1. 点击按钮发送请求获取要导出 Excel 的原始数据; 13 | 2. 接口返回处理拼装组装导出的形式的数据; 14 | 3. 然后将数据直接使用 excel.js 进行 Excel 文件的导出。 15 | 16 | 上述的过程在优化前仅使用当前网页的 js 单线程进行处理。 17 | 18 | 19 | 20 | ## 1.2 性能瓶颈 21 | 22 | 一开始业务还不算复杂,数据量还不算大的情况下上述的技术实现架构也还说暂时能够应付得来。但是随着业务不断更新迭代,用户的深度使用系统,**数据量急剧上升,这时候技术架构使用的还是单线程 js 处理导出 Excel 文件的形式,此时 js 的 script 处理逻辑进程压力也因此而暴增**,render 渲染进程从而受到阻碍,对系统用户的体验就是灾难性了,点击了下载后页面会直到处理完 Excel 导出数据组装并且 excel.js 成功导出 Excel 文件时都像是卡住了似得,无法在页面当中进行其他的任何操作。 23 | 24 | 25 | 26 | ## 1.3 技术优化改造 27 | 28 | 很明显,限制了用户体验的性能瓶颈就是大量的导出数据,单线程模式下的计算逻辑压力过大全程执行 script 逻辑,导致页面卡顿无法响应用户的其他操作。要想解决这个问题有两个技术优化的方向: 29 | 30 | - **前端利用浏览器的多线程的能力**:开辟单独处理导出数据的子线程,在系统背后多线程处理导出,让出主线程去响应处理用户的其他操作。 31 | - **后台生成对应的 Excel 文件**:这种形式,前端仅需要在点击了相关导出按钮后异步等待后台接口请求返回对应 Excel 文件的下载地址即可。 32 | 33 | 这里最后决策还是选择前端多线程的方式解决这个性能瓶颈的问题,实现性能、用户体验优化的功能;而说到前端多线程就指的是浏览器提供的 **Webworker** 的能力了。 34 | 35 | ___ 36 | 37 | 38 | 39 | ## 二、WebWorker 基础知识 40 | 41 | 篇幅和时间关系,这里就简单介绍相关最基础使用的 api,相关 Webworker 能力和 API 请自行查阅相关资料。 42 | 43 | ## 2.1 基础概念 44 | 45 | Web Worker 是 HTML5 标准的一部分,这一规范定义了一套 API,它允许一段 JavaScript 程序运行在主线程之外的另外一个线程中。 46 | 47 | ### 场景 48 | 49 | **当我们有些任务需要花费大量的时间,进行复杂的运算,就会导致页面卡死**:用户点击页面需要很长的时间才能响应,因为前面的任务还未完成,后面的任务只能排队等待。对用户来说,这样的体验无疑是糟糕的,web worker 就是为了解决这种花费大量时间的复杂运算而诞生的! 50 | 51 | ### WebWorker 的作用:创建 worker 线程 52 | 53 | WebWorker 允许在主线程之外再创建一个 worker 线程,**在主线程执行任务的同时,worker 线程也可以在后台执行它自己的任务,互不干扰。** 54 | 55 | 这样就利用浏览器提供的能力让 JS 变成多线程的环境,可以把高延迟、花费大量时间的运算,分给 worker 线程,最后再把结果返回给主线程就可以了,因为时间花费多的任务被 web worker 承担了,主线程自然会减轻了script 进程计算压力。 56 | 57 | 58 | 59 | ## 2.2 基本使用 60 | 61 | ### 主线程相关 62 | 63 | #### 创建 Worker 实例 64 | 65 | 主线程调用`new Worker()`构造函数,新建一个 worker 线程,构造函数的参数是一个 url,生成这个 url 的方法有两种: 66 | 67 | ##### 1\. 脚本文件: 68 | 69 | ```js 70 | const worker = new Worker('https://xxx.yyy.js'); 71 | ``` 72 | 73 | worker 的两个限制: 74 | 75 | 1. **分配给 Worker 线程运行的脚本文件,必须与主线程的脚本文件同源**。 76 | 2. **worker 不能读取本地的文件**(不能打开本机的文件系统file://),它所加载的脚本必须是网络资源文件。 77 | 78 | 在项目中推荐把文件放在静态文件夹(eg. static 文件夹)中,打包的时候直接拷贝进去。 79 | 80 | ##### 2\. 字符串形式: 81 | 82 | ```js 83 | const data = `worker线程 do something`; 84 | // 转成二进制对象 85 | const blob = new Blob([data]); 86 | // 生成url 87 | const url = window.URL.createObjectURL(blob); 88 | // 加载url 89 | const worker = new Worker(url); 90 | ``` 91 | 92 | 在项目中可以把worker线程的逻辑写在js文件里面,然后字符串化,然后再export、import,配合webpack进行模块化管理,这样也是一种加载 js 模块脚本的方式。 93 | 94 | #### 主线程的其他 API 95 | 96 | ##### 1\. 主线程与 Worker 线程通信 97 | 98 | ```js 99 | worker.postMessage({ 100 | type: 'init', 101 | data: {xxx} 102 | }); 103 | ``` 104 | 105 | 线程之间通过`postMessage/onMessage`api 进行通信。**相互之间的通信可以传递对象和数组**。 106 | 107 | ##### 2\. 主线程监听接收 Worker 线程返回的信息 108 | 109 | ```js 110 | worker.onmessage = function (event) { 111 | console.log(event.data); 112 | } 113 | ``` 114 | 115 | ##### 3\. 主线程关闭 Worker 线程 116 | 117 | ```js 118 | worker.terminate(); // 主线程主动关闭Worker线程 119 | ``` 120 | 121 | Worker 线程一旦新建成功,就会始终运行,这样有利于随时响应主线程的通信。这是 Worker 比较耗费CPU的原因,一旦使用完毕,就应该关闭 worker 线程。 122 | 123 | ##### 4\. 监听错误 124 | 125 | ```js 126 | worker.onerror = error => { 127 | // error.filename - 发生错误的脚本文件名 128 | // error.lineno - 出现错误的行号 129 | // error.message - 可读性良好的错误消息 130 | console.log('onerror', error); 131 | }; 132 | ``` 133 | 134 | ### Worker 线程相关 135 | 136 | #### self 代表 worker 进程自身 137 | 138 | worker 线程的执行上下文是一个叫做WorkerGlobalScope的东西跟主线程的上下文(window)不一样。可以使用self/WorkerGlobalScope 来访问全局对象。 139 | 140 | #### 监听主线程传过来的信息 141 | 142 | ```js 143 | self.onmessage = event => { 144 | console.log(event.data); 145 | }; 146 | ``` 147 | 148 | #### 发送信息给主线程 149 | 150 | ```js 151 | self.postMessage({ 152 | type: 'success', 153 | data: {yyy} 154 | }); 155 | ``` 156 | 157 | #### worker 线程关闭自身 158 | 159 | ```js 160 | self.close() 161 | ``` 162 | 163 | #### worker 线程加载脚本 164 | 165 | Worker 线程能够访问一个全局函数 imprtScripts()来引入脚本,该函数接受 0 个或者多个 URI 作为参数。 166 | 167 | ```js 168 | importScripts('http~.js','http~2.js'); 169 | ``` 170 | 171 | 注:脚本的下载顺序是不固定的,但执行时会按照调用 importScripts 顺序进行,且是同步的。 172 | 173 | #### Worker 线程限制 174 | 175 | **因为 worker 创造了另外一个线程,不在主线程上,浏览器给设定了一些限制** 176 | 177 | **无法使用下列对象**: 178 | 179 | 1. window 对象 180 | 2. document 对象 181 | 3. DOM 对象 182 | 4. parent 对象 183 | 184 | **可以使用下列对象/功能**: 185 | 186 | 1. 浏览器:navigator 对象 187 | 2. URL:location 对象,只读 188 | 3. 发送请求:XMLHttpRequest 对象 189 | 4. 定时器:setTimeout/setInterval 190 | 5. 应用缓存:Application Cache 191 | 192 | 193 | 194 | #### 线程间转移二进制数据 195 | 196 | 因为主线程与 worker 线程之间的通信是拷贝关系,当需要传递一个巨大的二进制文件给 worker 线程处理时(worker 线程就是用来干这个的),这时候使用拷贝的方式来传递数据,无疑会造成性能问题。 197 | 198 | **Web Worker 提供了一种转移数据的方式,允许主线程把二进制数据直接转移给子线程**。这种方式比原先拷贝的方式,有巨大的性能提升。 199 | 200 | **一旦数据转移到其他线程,原先线程就无法再使用这些二进制数据了,这是为了防止出现多个线程同时修改数据的麻烦局面** 201 | 202 | ```js 203 | // 创建二进制数据 204 | var uInt8Array = new Uint8Array(1024*1024*32); // 32MB 205 | for (var i = 0; i < uInt8Array .length; ++i) { 206 | uInt8Array[i] = i; 207 | } 208 | console.log(uInt8Array.length); // 传递前长度:33554432 209 | // 字符串形式创建worker线程 210 | var myTask = ` 211 | onmessage = function (e) { 212 | var data = e.data; 213 | console.log('worker:', data); 214 | }; 215 | `; 216 | 217 | var blob = new Blob([myTask]); 218 | var myWorker = new Worker(window.URL.createObjectURL(blob)); 219 | 220 | // 使用这个格式(a,[a]) 来转移二进制数据 221 | myWorker.postMessage(uInt8Array.buffer, [uInt8Array.buffer]); // 发送数据、转移数据 222 | 223 | console.log(uInt8Array.length); // 传递后长度:0,原先线程内没有这个数据了 224 | ``` 225 | 226 | ___ 227 | 228 | 229 | 230 | ## 三、业务优化的落地 231 | 232 | ## 3.1 技术调研 233 | 234 | 梳理下数据导出的先决条件: 235 | 236 | 1. 在 WebWorker 中需要能调用 ajax 获取接口数据; 237 | 2. 在 WebWorker 中要能动态加载脚本,为了实现不同的 Excel 数据组装格式化及加载 excel.js 脚本进行 excel 文件流转换; 238 | 3. 调用 file-saver 中的 saveAs 功能进行文件导出保存操作; 239 | 240 | 基于以上的条件,逐一讨论答案: 241 | 242 | 1. WebWorker 支持使用 XMLHttpRequest 发起 ajax 请求数据; 243 | 2. WebWorker 中提供了 importScripts() 接口能够动态导入 js 脚本文件,因此在 WebWorker 中也能生成 Excel 的实例; 244 | 3. WebWorker 中是无法使用 DOM 对象, 而 file-saver 正好使用了 DOM,因此只能是子线程中处理完数据后传递数据给主线程由主线程执行文件保存操作; 245 | 246 | 247 | 248 | ## 3.2 实践落地 249 | 250 | ### 设计思路与流程伪代码 251 | 252 | #### 1\. 主线程实例化 Worker 线程 253 | 254 | 实例化 Worker 线程: 255 | 256 | - 使用 statics 不进行打包构建的 js 资源 257 | 258 | ```js 259 | const worker = new Worker(`${window.location.origin}/js/export-excel.js`) 260 | ``` 261 | 262 | 263 | 264 | #### 2\. 主线程与 Worker 线程之间通信 265 | 266 | 主线程通过 worker 实例的 onmessage api 来接受 worker 线程的信息: 267 | 268 | - 通过定义不同的 type 来区分要执行的操作 269 | - 主要划分为三类操作: 270 | - **ready**:worker 线程已经实例化初始化准备好了,主线程可以传输要导出的数据; 271 | - **success**:worker 线程已经处理好要导出的数据,主线程准备接收并且调用 FileSaver.saveAs 导出成 xlsx 文件 272 | - **error**:遇到错误 273 | 274 | ```js 275 | worker.onmessage = (event) => { 276 | const msgType = event.data.type 277 | 278 | switch (msgType) { 279 | case 'ready': 280 | worker.postMessage({ 281 | type: 'init', 282 | data: { 283 | type: 'xxx', 284 | data: {}, 285 | }, 286 | }) 287 | break 288 | 289 | case 'success': 290 | // FileSaver 导出 Excel 文件... 291 | resolve() 292 | break 293 | 294 | case 'error': 295 | reject(event.data.data) 296 | break 297 | 298 | default: break 299 | } 300 | } 301 | ``` 302 | 303 | 304 | 305 | #### 3\. 主线程通知 worker 线程开始,worker 线程加载数据 306 | 307 | worker 接收 init 初始化信号并且利用 XMLHttpRequest 发送请求获取数据 308 | 309 | ```js 310 | onmessage = event => { 311 | const msgType = event.data.type 312 | switch(msgType) { 313 | case 'init': { 314 | getData() 315 | } 316 | } 317 | }; 318 | 319 | function getData() { 320 | //构造表单数据 321 | var formData = new FormData(); 322 | formData.append('key', 'value'); 323 | //创建xhr对象 324 | var xhr = new XMLHttpRequest(); 325 | //设置xhr请求的超时时间 326 | xhr.timeout = 3000; 327 | //设置响应返回的数据格式为 json 328 | xhr.responseType = "json"; 329 | //创建一个 post 请求,采用异步 330 | xhr.open('POST', '/server-api', true); 331 | //注册相关事件回调处理函数 332 | xhr.onload = function(e) { 333 | if(this.status == 200||this.status == 304){ 334 | console.log(this.response); 335 | } 336 | }; 337 | xhr.ontimeout = function(e) { ... }; 338 | xhr.onerror = function(e) { ... }; 339 | xhr.upload.onprogress = function(e) { ... }; 340 | 341 | //发送数据 342 | xhr.send(formData); 343 | } 344 | ``` 345 | 346 | 347 | 348 | #### 4\. Worker 线程处理组装要导出的数据 349 | 350 | 根据不同类型的导出文件进行对数据组装,这里涉及业务就不过多讲述了。 351 | 352 | 加载 excel.js 并进行数据转换 (这里主要介绍 Webworker,因此 excel.js 的用法这里就不累述了。 353 | 354 | ```js 355 | importScripts('/js/xlsx.js'); 356 | 357 | const wbout = XLSX.write({ 358 | SheetNames: ['未命名'], 359 | Sheets: { 360 | '未命名': 'xxx' 361 | } 362 | }, { 363 | bookType: 'xlsx', 364 | bookSST: false, 365 | type: 'binary' 366 | }) 367 | ``` 368 | 369 | 370 | 371 | #### 5\. Worker 线程向主线程通信传输数据 372 | 373 | 这里是使用二进制数据形式在 worker 线程与主线程之间进行传输上述经过 excel.js 转换后的数据 374 | 375 | ```js 376 | self.postMessage({ 377 | type: 'success', 378 | data: { 379 | xlsxBlob: new Blob([wbout], { 380 | type: 'application/octet-stream' 381 | }), 382 | } 383 | }); 384 | ``` 385 | 386 | 387 | 388 | #### 6\. 主线程接受 Worker 线程数据并调用 file-saver saveAs 方法保存为 Excel 文件 389 | 390 | ```js 391 | import FileSaver from 'file-saver' 392 | 393 | worker.onmessage = (event) => { 394 | const msgType = event.data.type 395 | 396 | switch (msgType) { 397 | case 'success': 398 | FileSaver.saveAs(event.data.data.xlsxBlob, `${fileName}.xlsx`) 399 | resolve() 400 | break 401 | 402 | // ... 403 | 404 | default: break 405 | } 406 | } 407 | ``` 408 | 409 | 410 | 411 | ### 遇到的问题 412 | 413 | #### 1\. Worker 实例化参数形式抉择 414 | 415 | 前面提及到 Worker 线程实例化时候有两种传递参数的形式,一种是通过脚本 url 形式,一种是通过 js 逻辑字符串转化成资源 url;因为第二种首先是工程化经过编译、混淆压缩后代码的不好明确拆分并且不同形式的导出 Excel 文件有对应不同的数据处理组装逻辑脚本,因此这里选择的还是第一种,将要运行的脚本放到不会进行构建打包处理的静态资源目录(eg. static 目录)。 416 | 417 | 418 | 419 | #### 2\. 请求的封装逻辑与数据组装格式的逻辑的复用的困境? 420 | 421 | 因为 Worker 线程内部能使用的请求是原生的`XMLHttpRequest`,并且是独立的,因此需要重新调整请求的封装。而数据组装因为技术决策实例化 WebWorker 线程实例时候选择的是使用静态没编译打包的 static 静态脚本资源,因此数组组装这块复用原项目当中的工具 util 函数变的不可能,因此目前也只能调整重新 CP 一份封装在静态资源当中使用 importScripts 来进行加载了。 422 | 423 | 424 | 425 | #### 3\. Worker 往主线程传输大数据优化 426 | 427 | 当要导出的数据量极大的时候,主线程与 Worker 线程之间还使用对象形式进行数据通信就会造成性能问题,所幸 WebWorker 在线程之间的通信支持以二进制形式数据进行通信。项目当中使用的为`new Blob([string])`来对导出的数据进行二进制形式转换,恰巧 file-saver 也可以使用 Blob 二进制数据形式进行对文件的导出。 428 | 429 | ```js 430 | FileSaver.saveAs(new Blob([outputString], { 431 | type: 'application/octet-stream' 432 | }), `${fileName}.xlsx`) 433 | ``` 434 | 435 | 436 | 437 | ## 3.3 优化成效与总结 438 | 439 | 数据导出过程中,页面没有丝毫的卡顿之感。 440 | 441 | 在点击各类下载的按钮后并不会再因为 js 的 script 进程遇到计算量极大的场景而导致对页面造成卡顿感觉,解放了浏览器的主线程,在处理组装导出 Excel 数据的同时能够处理用户的点击、输入等操作,使得用户得到了直线上升的使用体验,收到了客户和 PM 的一致好评! 442 | 443 | 技术的优化还是需要扎根在业务上面,只有真正解决了用户的痛点,这样子的技术优化落地才能说更为有价值所在。 444 | 445 | ___ 446 | 447 | 448 | 449 | ## 参考资料 450 | 451 | - 前端er来学习一下webWorker吧:[juejin.cn/post/684490…](https://juejin.cn/post/6844903725249593352 "https://juejin.cn/post/6844903725249593352") 452 | - Web Worker 使用教程:[www.ruanyifeng.com/blog/2018/0…](https://link.juejin.cn/?target=https%3A%2F%2Fwww.ruanyifeng.com%2Fblog%2F2018%2F07%2Fweb-worker.html "https://www.ruanyifeng.com/blog/2018/07/web-worker.html") 453 | - 前端中如何使用webWorker对户体验进行革命性的提升:[juejin.cn/post/697033…](https://juejin.cn/post/6970336963647766559 "https://juejin.cn/post/6970336963647766559") 454 | -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/16.原理篇-调度与时间片.md: -------------------------------------------------------------------------------- 1 | ## 一 前言 2 | 3 | 接下来的两个章节,我将重点介绍 React 的两大核心模块:调度( Scheduler )和调和( Reconciler )。 4 | 5 | 通过本章节学习,你将理解 React 异步调度的原理,以及 React 调度流程,从而解决面试中遇到的调度问题。 6 | 7 | 在正式讲解调度之前,有个问题可能大家都清楚,那就是 GUI 渲染线程和 JS 引擎线程是相互排斥的,比如开发者用 js 写了一个遍历大量数据的循环,在执行 js 时候,会阻塞浏览器的渲染绘制,给用户直观的感受就是卡顿。 8 | 9 | **请带着这些问题,在本章节中找答案,收获更佳** 10 | 11 | - 异步调度原理? 12 | - React 为什么不用 settimeout ? 13 | - 说一说React 的时间分片? 14 | - React 如何模拟 requestIdleCallback? 15 | - 简述一下调度流程? 16 | 17 | ## 二 何为异步调度 18 | 19 | ### 为什么采用异步调度? 20 | 21 | `v15` 版本的 React 同样面临着如上的问题,由于对于大型的 React 应用,**会存在一次更新,递归遍历大量的虚拟 DOM ,造成占用 js 线程,使得浏览器没有时间去做一些动画效果,伴随项目越来越大,项目会越来越卡**。 22 | 23 | 如何解决以上的问题呢,**首先对比一下 vue 框架,vue 有这 template 模版收集依赖的过程,轻松构建响应式,使得在一次更新中,vue 能够迅速响应,找到需要更新的范围,然后以组件粒度更新组件,渲染视图**。但是在 React 中,一次更新 React 无法知道此次更新的波及范围,所以 React 选择从根节点开始 diff ,查找不同,更新这些不同。 24 | 25 | React 似乎无法打破从 root 开始‘找不同’的命运,但是还是要解决浏览器卡顿问题,那怎么办,解铃还须系铃人,既然更新过程阻塞了浏览器的绘制,那么把 React 的更新,交给浏览器自己控制不就可以了吗,如果浏览器有绘制任务那么执行绘制任务,在空闲时间执行更新任务,就能解决卡顿问题了。与 vue 更快的响应,更精确的更新范围,React 选择更好的用户体验。而今天即将讲的调度( Scheduler )就是具体的实现方式。 26 | 27 | ### 时间分片 28 | 29 | React 如何让浏览器控制 React 更新呢,首先浏览器**每次执行一次事件循环(一帧)**都会做如下事情:**处理事件,执行 js ,调用 requestAnimation ,布局 Layout ,绘制 Paint** ,在一帧执行后,如果没有其他事件,那么浏览器会进入休息时间,那么有的一些不是特别紧急 React 更新,就可以执行了。 30 | 31 | 那么首先就是**如何知道浏览器有空闲时间?** 32 | 33 | requestIdleCallback 是**谷歌浏览器**提供的一个 API, **在浏览器有空余的时间**,浏览器就会调用 requestIdleCallback 的回调。首先看一下 requestIdleCallback的基本用法: 34 | 35 | ```js 36 | requestIdleCallback(callback,{ timeout }) 37 | ``` 38 | 39 | - callback 回调,**浏览器空余时间执行回调函数**。 40 | - timeout 超时时间。如果浏览器长时间没有空闲,那么回调就不会执行,为了解决这个问题,可以通过 requestIdleCallback 的第二个参数指定一个超时时间。 41 | 42 | React 为了防止 requestIdleCallback 中的任务由于浏览器没有空闲时间而卡死,所以**设置了 5 个优先级**。 43 | 44 | - `Immediate` -1 需要立刻执行。 45 | - `UserBlocking` 250ms 超时时间250ms,一般指的是用户交互。 46 | - `Normal` 5000ms 超时时间5s,不需要直观立即变化的任务,比如网络请求。 47 | - `Low` 10000ms 超时时间10s,肯定要执行的任务,但是可以放在最后处理。 48 | - `Idle` 一些没有必要的任务,可能不会执行。 49 | 50 | React 的异步更新任务就是通过类似 requestIdleCallback 去向浏览器做一帧一帧请求,等到浏览器有空余时间,去执行 React 的异步更新任务,这样保证页面的流畅。 51 | 52 | image-20220924101804884 53 | 54 | ### 模拟requestIdleCallback 55 | 56 | 但是 **requestIdleCallback** 目前只有谷歌浏览器支持 ,为了兼容每个浏览器,React需要自己实现一个 requestIdleCallback ,那么就要具备两个条件: 57 | 58 | - 1 实现的这个 requestIdleCallback ,可以**主动让出主线程**,让浏览器去渲染视图。 59 | - 2 一次事件循环**只执行一次,因为执行一个以后,还会请求下一次的时间**片。 60 | 61 | 能够满足上述条件的,就只有 **宏任务**,宏任务是**在下次事件循环中执行,不会阻塞浏览器更新**。而且**浏览器一次只会执行一个宏任务**。首先看一下两种满足情况的宏任务。 62 | 63 | **setTimeout(fn, 0)** 64 | 65 | `setTimeout(fn, 0)` 可以满足创建宏任务,让出主线程,为什么 React 没选择用它实现 Scheduler 呢?原因是**递归执行 setTimeout(fn, 0) 时,最后间隔时间会变成 4 毫秒左右**,而不是最初的 1 毫秒。所以 React 优先选择的并不是 setTimeout 实现方案。 66 | 67 | 接下来模拟一下 **setTimeout 4毫秒延时**的真实场景: 68 | 69 | ```js 70 | let time = 0 71 | let nowTime = +new Date() 72 | let timer 73 | const poll = function(){ 74 | timer = setTimeout(()=>{ 75 | const lastTime = nowTime 76 | nowTime = +new Date() 77 | console.log( '递归setTimeout(fn,0)产生时间差:' , nowTime -lastTime ) 78 | poll() 79 | },0) 80 | time++ 81 | if(time === 20) clearTimeout(timer) 82 | } 83 | poll() 84 | ``` 85 | 86 | 效果: 87 | 88 | image-20220924102414651 89 | 90 | **MessageChannel** 91 | 92 | 为了让视图流畅地运行,**可以按照人类能感知到最低限度每秒 60 帧的频率划分时间片,这样每个时间片就是 16ms** 。也就是这 16 毫秒要完成如上 js 执行,浏览器绘制等操作,而上述 setTimeout 带来的浪费就足足有 4ms,react 团队应该是注意到这 4ms 有点过于铺张浪费,所以才采用了一个新的方式去实现,那就是 `MessageChannel` 。 93 | 94 | MessageChannel 接口允许开发者**创建一个新的消息通道,并通过它的两个 MessagePort 属性发送数据**。 95 | 96 | - MessageChannel.port1 只读返回 channel 的 port1。 97 | - MessageChannel.port2 只读返回 channel 的 port2。 98 | 99 | 下面来模拟一下 MessageChannel 如何触发异步宏任务的。 100 | 101 | ```js 102 | let scheduledHostCallback = null 103 | /* 建立一个消息通道 */ 104 | var channel = new MessageChannel(); 105 | /* 建立一个port发送消息 */ 106 | var port = channel.port2; 107 | 108 | channel.port1.onmessage = function(){ 109 | /* 执行任务 */ 110 | scheduledHostCallback() 111 | /* 执行完毕,清空任务 */ 112 | scheduledHostCallback = null 113 | }; 114 | /* 向浏览器请求执行更新任务 */ 115 | requestHostCallback = function (callback) { 116 | scheduledHostCallback = callback; 117 | if (!isMessageLoopRunning) { 118 | isMessageLoopRunning = true; 119 | port.postMessage(null); 120 | } 121 | }; 122 | ``` 123 | 124 | - 在一次更新中,React 会调用 requestHostCallback ,把更新任务赋值给 scheduledHostCallback ,然后 port2 向 port1 发起 postMessage 消息通知。 125 | - port1 会通过 onmessage ,接受来自 port2 消息,然后执行更新任务 scheduledHostCallback ,然后置空 scheduledHostCallback ,借此达到异步执行目的。 126 | 127 | ## 三 异步调度原理 128 | 129 | 上面说到了时间片的感念和 Scheduler 实现原理。接下来,来看一下调度任务具体的实现细节。**React 发生一次更新,会统一走 ensureRootIsScheduled(调度应用)**。 130 | 131 | - 对于**正常更新**会走 **performSyncWorkOnRoot 逻辑**,最后会走 `workLoopSync` 。 132 | - 对于**低优先级的异步更新**会走 **performConcurrentWorkOnRoot** 逻辑,最后会走 `workLoopConcurrent` 。 133 | 134 | 如下看一下workLoopSync,workLoopConcurrent。 135 | 136 | > react-reconciler/src/ReactFiberWorkLoop.js 137 | 138 | ```js 139 | function workLoopSync() { 140 | while (workInProgress !== null) { 141 | workInProgress = performUnitOfWork(workInProgress); 142 | } 143 | } 144 | ``` 145 | 146 | ```js 147 | function workLoopConcurrent() { 148 | while (workInProgress !== null && !shouldYield()) { 149 | workInProgress = performUnitOfWork(workInProgress); 150 | } 151 | } 152 | ``` 153 | 154 | 在一次更新调度过程中,workLoop 会更新执行每一个**待更新的 fiber** 。**他们的区别就是异步模式会调用一个 shouldYield() ,如果当前浏览器没有空余时间, shouldYield 会中止循环,直到浏览器有空闲时间后再继续遍历,从而达到终止渲染的目的**。这样就解决了一次性遍历大量的 fiber ,导致浏览器没有时间执行一些渲染任务,导致了页面卡顿。 155 | 156 | ### scheduleCallback 157 | 158 | 无论是上述**正常更新任务** `workLoopSync` 还是**低优先级**的任务 `workLoopConcurrent` ,都是由调度器 `scheduleCallback` 统一调度的,那么两者在进入调度器时候有什么区别呢? 159 | 160 | 对于正常更新任务,最后会变成类似如下结构: 161 | 162 | ```js 163 | scheduleCallback(Immediate,workLoopSync) 164 | ``` 165 | 166 | 对于异步任务: 167 | 168 | ```js 169 | /* 计算超时等级,就是如上那五个等级 */ 170 | var priorityLevel = inferPriorityFromExpirationTime(currentTime, expirationTime); 171 | scheduleCallback(priorityLevel,workLoopConcurrent) 172 | ``` 173 | 174 | **低优先级异步任务的处理,比同步多了一个超时等级的概念**。会计算上述那五种超时等级。 175 | 176 | **scheduleCallback 到底做了些什么呢?** 177 | 178 | > scheduler/src/Scheduler.js 179 | 180 | ```js 181 | function scheduleCallback(){ 182 | /* 计算过期时间:超时时间 = 开始时间(现在时间) + 任务超时的时间(上述设置那五个等级) */ 183 | const expirationTime = startTime + timeout; 184 | /* 创建一个新任务 */ 185 | const newTask = { ... } 186 | if (startTime > currentTime) { 187 | /* 通过开始时间排序 */ 188 | newTask.sortIndex = startTime; 189 | /* 把任务放在timerQueue中 */ 190 | push(timerQueue, newTask); 191 | /* 执行setTimeout , */ 192 | requestHostTimeout(handleTimeout, startTime - currentTime); 193 | }else{ 194 | /* 通过 expirationTime 排序 */ 195 | newTask.sortIndex = expirationTime; 196 | /* 把任务放入taskQueue */ 197 | push(taskQueue, newTask); 198 | /*没有处于调度中的任务, 然后向浏览器请求一帧,浏览器空闲执行 flushWork */ 199 | if (!isHostCallbackScheduled && !isPerformingWork) { 200 | isHostCallbackScheduled = true; 201 | requestHostCallback(flushWork) 202 | } 203 | 204 | } 205 | 206 | } 207 | ``` 208 | 209 | 对于调度本身,有几个概念必须掌握。 210 | 211 | - `taskQueue`,里面存的都是过期的任务,依据任务的过期时间( `expirationTime` ) 排序,需要在调度的 `workLoop` 中循环执行完这些任务。 212 | - `timerQueue` 里面存的都是没有过期的任务,依据任务的开始时间( `startTime` )排序,在调度 workLoop 中 会用`advanceTimers`检查任务是否过期,如果过期了,放入 `taskQueue` 队列。 213 | 214 | scheduleCallback 流程如下。 215 | 216 | - 创建一个新的任务 newTask。 217 | - 通过任务的开始时间( startTime ) 和 当前时间( currentTime ) 比较:当 startTime > currentTime, 说明未过期, 存到 timerQueue,当 startTime <= currentTime, 说明已过期, 存到 taskQueue。 218 | - 如果任务过期,并且没有调度中的任务,那么调度 requestHostCallback。本质上调度的是 flushWork。 219 | - 如果任务没有过期,用 requestHostTimeout 延时执行 handleTimeout。 220 | 221 | ### requestHostTimeout 222 | 223 | 上述当一个任务,没有超时,那么 React 把它放入 timerQueue中了,但是它什么时候执行呢 ?这个时候 Schedule 用 requestHostTimeout 让一个未过期的任务能够到达恰好过期的状态, 那么需要延迟 startTime - currentTime 毫秒就可以了。requestHostTimeout 就是通过 setTimeout 来进行延时指定时间的。 224 | 225 | > scheduler/src/Scheduler.js 226 | 227 | ```js 228 | requestHostTimeout = function (cb, ms) { 229 | _timeoutID = setTimeout(cb, ms); 230 | }; 231 | 232 | cancelHostTimeout = function () { 233 | clearTimeout(_timeoutID); 234 | }; 235 | ``` 236 | 237 | - requestHostTimeout 延时执行 handleTimeout,cancelHostTimeout 用于清除当前的延时器。 238 | 239 | ### handleTimeout 240 | 241 | 延时指定时间后,调用的 handleTimeout 函数, handleTimeout 会把任务重新放在 requestHostCallback 调度。 242 | 243 | > scheduler/src/Scheduler.js 244 | 245 | ```js 246 | function handleTimeout(){ 247 | isHostTimeoutScheduled = false; 248 | /* 将 timeQueue 中过期的任务,放在 taskQueue 中 。 */ 249 | advanceTimers(currentTime); 250 | /* 如果没有处于调度中 */ 251 | if(!isHostCallbackScheduled){ 252 | /* 判断有没有过期的任务, */ 253 | if (peek(taskQueue) !== null) { 254 | isHostCallbackScheduled = true; 255 | /* 开启调度任务 */ 256 | requestHostCallback(flushWork); 257 | } 258 | } 259 | } 260 | ``` 261 | 262 | - 通过 advanceTimers 将 timeQueue 中过期的任务转移到 taskQueue 中。 263 | - 然后调用 requestHostCallback 调度过期的任务。 264 | 265 | ### advanceTimers 266 | 267 | > scheduler/src/Scheduler.js advanceTimers 268 | 269 | ```js 270 | function advanceTimers(){ 271 | var timer = peek(timerQueue); 272 | while (timer !== null) { 273 | if(timer.callback === null){ 274 | pop(timerQueue); 275 | }else if(timer.startTime <= currentTime){ /* 如果任务已经过期,那么将 timerQueue 中的过期任务,放入taskQueue */ 276 | pop(timerQueue); 277 | timer.sortIndex = timer.expirationTime; 278 | push(taskQueue, timer); 279 | } 280 | } 281 | } 282 | ``` 283 | 284 | - 如果任务已经过期,那么将 timerQueue 中的过期任务,放入 taskQueue。 285 | 286 | ### flushWork和workloop 287 | 288 | 综上所述要明白两件事: 289 | 290 | - 第一件是 React 的更新任务最后都是放在 taskQueue 中的。 291 | - 第二件是 requestHostCallback ,放入 MessageChannel 中的回调函数是flushWork。 292 | 293 | **flushWork** 294 | 295 | > scheduler/src/Scheduler.js flushWork 296 | 297 | ```js 298 | function flushWork(){ 299 | if (isHostTimeoutScheduled) { /* 如果有延时任务,那么先暂定延时任务*/ 300 | isHostTimeoutScheduled = false; 301 | cancelHostTimeout(); 302 | } 303 | try{ 304 | /* 执行 workLoop 里面会真正调度我们的事件 */ 305 | workLoop(hasTimeRemaining, initialTime) 306 | } 307 | } 308 | ``` 309 | 310 | - flushWork 如果有延时任务执行的话,那么会先暂停延时任务,然后调用 workLoop ,去真正执行超时的更新任务。 311 | 312 | **workLoop** 313 | 314 | 这个 workLoop 是调度中的 workLoop,不要把它和调和中的 workLoop 弄混淆了。 315 | 316 | ```js 317 | function workLoop(){ 318 | var currentTime = initialTime; 319 | advanceTimers(currentTime); 320 | /* 获取任务列表中的第一个 */ 321 | currentTask = peek(); 322 | while (currentTask !== null){ 323 | /* 真正的更新函数 callback */ 324 | var callback = currentTask.callback; 325 | if(callback !== null ){ 326 | /* 执行更新 */ 327 | callback() 328 | /* 先看一下 timeQueue 中有没有 过期任务。 */ 329 | advanceTimers(currentTime); 330 | } 331 | /* 再一次获取任务,循环执行 */ 332 | currentTask = peek(taskQueue); 333 | } 334 | } 335 | ``` 336 | 337 | - workLoop 会依次更新过期任务队列中的任务。**到此为止,完成整个调度过程。** 338 | 339 | ### shouldYield 中止 workloop 340 | 341 | 在 fiber 的异步更新任务 workLoopConcurrent 中,每一个 fiber 的 workloop 都会调用 shouldYield 判断是否有超时更新的任务,如果有,那么停止 workLoop。 342 | 343 | > scheduler/src/Scheduler.js unstable\_shouldYield 344 | 345 | ```js 346 | function unstable_shouldYield() { 347 | var currentTime = exports.unstable_now(); 348 | advanceTimers(currentTime); 349 | /* 获取第一个任务 */ 350 | var firstTask = peek(taskQueue); 351 | return firstTask !== currentTask && currentTask !== null && firstTask !== null && firstTask.callback !== null && firstTask.startTime <= currentTime && firstTask.expirationTime < currentTask.expirationTime || shouldYieldToHost(); 352 | } 353 | ``` 354 | 355 | - 如果存在第一个任务,并且已经超时了,那么 shouldYield 会返回 true,那么会中止 fiber 的 workloop。 356 | 357 | ### 调度流程图 358 | 359 | 整个调度流程,用一个流程图表示: 360 | 361 | ![image-20220924103104875](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/image-20220924103104875.png) 362 | 363 | ### 调和 + 异步调度 流程总图 364 | 365 | 异步调度过程,如下图所示: 366 | 367 | ![image-20220924103124376](https://femarkdownpicture.oss-cn-qingdao.aliyuncs.com/Imgs/image-20220924103124376.png) 368 | 369 | ## 四 总结 370 | 371 | 本章节学习了 React 调度原理和流程,下一节,将学习 React Reconciler 调和流程。 -------------------------------------------------------------------------------- /docs/interview/React进阶实践指南/22.实践篇-实现mini-Router.md: -------------------------------------------------------------------------------- 1 | ## 一 前言 2 | 3 | 本章节,我们会从 0 到 1 实现一个 React 路由功能,这里可以称之为 `mini-Router`。实现的过程中会包含如下知识点: 4 | 5 | - 路由更新流程与原理; 6 | - 自定义 hooks 编写与使用; 7 | - context 实践; 8 | - hoc 编写与使用。 9 | 10 | ## 二 设计思路 11 | 12 | 整个 mini-Router 还是采用 `history` 库,也就是 mini-Router 需要完成的是 `React-Router` 和 `React-Router-DOM` 核心部分。今天编写的 mini-Router 是在 BrowserHistory 模式下。 13 | 14 | ### 1 建立目标 15 | 16 | 接下来要实现的具体功能如下: 17 | 18 | - **组件层面:** 在组件层面,需要实现提供路由状态的 Router ,控制渲染的 Route ,匹配唯一路由的 Switch 。 19 | 20 | - **api层面:** 提供获取 history 对象的 useHistory 方法,获取 location 对象的 useLocation 方法。 21 | 22 | - **高阶组件层面:** 对于不是路由的页面,提供 withRouter,能够获取当前路由状态。 23 | 24 | - **额外功能:** 之前有很多同学问过我,在 React 应用中,可不可以提供有方法监听路由改变,所以 mini-Router 需要做的是增加路由监听器,当路由改变,触发路由监听器。 25 | 26 | 27 | ### 2 设计功能图 28 | 29 | ![2.jpg](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/0a9ca32fec8e40819afeecc05d40cdb5~tplv-k3u1fbpfcp-zoom-in-crop-mark:1304:0:0:0.awebp) 30 | 31 | ## 三 代码实现 32 | 33 | ### 1 组件层面 34 | 35 | **提供路由更新派发——Router** 36 | 37 | ``` 38 | import React ,{ useCallback, useState , useEffect ,createContext, useMemo } from 'react' 39 | import { createBrowserHistory as createHistory } from 'history' 40 | 41 | export const RouterContext = createContext() 42 | export let rootHistory = null 43 | 44 | export default function Router(props){ 45 | /* 缓存history属性 */ 46 | const history = useMemo(() => { 47 | rootHistory = createHistory() 48 | return rootHistory 49 | },[]) 50 | const [ location, setLocation ] = useState(history.location) 51 | useEffect(()=>{ 52 | /* 监听location变化,通知更新 */ 53 | const unlisten = history.listen((location)=>{ 54 | setLocation(location) 55 | }) 56 | return function () { 57 | unlisten && unlisten() 58 | } 59 | },[]) 60 | return 67 | {props.children} 68 | 69 | } 70 | ``` 71 | 72 | Router 设计思路: 73 | 74 | - 创建一个 React Context ,用于保存路由状态。用 Provider 传递 context 。 75 | - 用一个 useMemo 来缓存 BrowserHistory 模式下的产生的路由对象 history ,这里有一个小细节,就是产生 history 的同时,把它赋值给了一个全局变量 rootHistory ,为什么这么做呢,答案一会将揭晓。 76 | - 通过 useEffect 进行真正的路由监听,当路由改变,通过 useState ,改变 location 对象,会改变 Provider 里面 value 的内容,通知消费 context 的 Route ,Switch 等组件更新。 useEffect 的 destory 用于解绑路由监听器。 77 | 78 | **控制更新——Route** 79 | 80 | ``` 81 | import React , { useContext } from 'react' 82 | import { matchPath } from 'react-router' 83 | import { RouterContext } from './Router' 84 | 85 | function Route(props) { 86 | const context = useContext(RouterContext) 87 | /* 获取location对象 */ 88 | const location = props.location || context.location 89 | /* 是否匹配当前路由,如果父级有switch,就会传入computedMatch来精确匹配渲染此路由 */ 90 | const match = props.computedMatch ? props.computedMatch 91 | : props.path ? matchPath(location.pathname,props) : context.match 92 | /* 这个props用于传递给路由组件 */ 93 | const newRouterProps = { ...context, location, match } 94 | let { children, component, render } = props 95 | if(Array.isArray(children) && children.length ===0 ) children = null 96 | let renderChildren = null 97 | if(newRouterProps.match){ 98 | if(children){ 99 | /* 当Router 是 props children 或者 render props 形式。*/ 100 | renderChildren = typeof children === 'function' ? children(newRouterProps) : children 101 | }else if(component){ 102 | /* Route有component属性 */ 103 | renderChildren = React.createElement(component, newRouterProps) 104 | }else if(render){ 105 | /* Route有render属性 */ 106 | renderChildren = render(newRouterProps) 107 | } 108 | } 109 | /* 逐层传递上下文 */ 110 | return 111 | {renderChildren} 112 | 113 | } 114 | export default Route 115 | ``` 116 | 117 | - 用 useContext 提取出路由上下文,当路由状态 location 改变,因为消费context 的组件都会重新渲染,当前Route会组件重新渲染,通过当前的 location 的 pathname 进行匹配,判断当前组件是否渲染,因为 Route 子组件有四种形式,所以会优先进行判断。 118 | - 为了让 Route 的子组件访问到当前 Route 的信息,所以要选择通过 Provider 逐层传递的特点,再一次传递当前 Route 的信息,这样也能够让嵌套路由更简单的实现。 119 | - 因为如果父级元素是 Switch ,就不需要匹配路由了,因为这些都是 Switch 该干的活,所以用 computedMatch 来识别是否上一层的 Switch 已经匹配完成了。 120 | 121 | **匹配正确路由—— Switch** 122 | 123 | ``` 124 | 125 | import React, { useContext } from 'react' 126 | import { matchPath } from 'react-router' 127 | 128 | import { RouterContext } from '../component/Router' 129 | 130 | export default function Switch(props){ 131 | const context = useContext(RouterContext) 132 | const location = props.location || context.location 133 | let children , match 134 | /* 遍历children Route 找到匹配的那一个 */ 135 | React.Children.forEach(props.children,child=>{ 136 | if(!match && React.isValidElement(child) ){ /* 路由匹配并为React.element元素的时候 */ 137 | const path = child.props.path //获取Route上的path 138 | children = child /* 匹配的children */ 139 | match = path ? matchPath(location.pathname,{ ...child.props }) : context.match /* 计算是否匹配 */ 140 | } 141 | }) 142 | /* 克隆一份Children,混入 computedMatch 并渲染。 */ 143 | return match ? React.cloneElement(children, { location, computedMatch: match }) : null 144 | } 145 | ``` 146 | 147 | - Switch 也要订阅来自 context 的变化,然后对 children 元素,进行唯一性的路由匹配。 148 | - 通过`React.Children.forEach`遍历子 Route,然后通过 matchPath 进行匹配,如果匹配到组件,将克隆组件,混入 computedMatch,location 等信息。 149 | 150 | ### 2 hooksAPI层面 151 | 152 | 为了让 mini-Router 每一个组件都能自由获取路由状态,这里编写了两个自定义 hooks。 153 | 154 | **获取history对象** 155 | 156 | ``` 157 | import { useContext } from 'react' 158 | import { RouterContext } from '../component/Router' 159 | /* 用useContext获取上下文中的history对象 */ 160 | export default function useHistory() { 161 | return useContext(RouterContext).history 162 | } 163 | ``` 164 | 165 | - 用 useContext 获取上下文中的 history 对象。 166 | 167 | **获取 location 对象** 168 | 169 | ``` 170 | import { useContext } from 'react' 171 | import { RouterContext } from '../component/Router' 172 | /* 用useContext获取上下文中的location对象 */ 173 | export default function useLocation() { 174 | return useContext(RouterContext).location 175 | } 176 | ``` 177 | 178 | - 用 useContext 获取上下文中的 location 对象。 179 | 180 | 上述的两个 hooks 编写起来非常简单,但是也要注意一个问题,两个 hooks 本质上都是消费了 context ,所以用到上述两个 hook 的组件,当context 变化,都会重新渲染。接下来增加一个新的功能,监听路由改变。 181 | 182 | **监听路由改变**,和上面两种情况不同,不想订阅 context 变化,而带来的更新作用,另外一点就是这种监听有可能在 Router 包裹的组件层级之外,那么如何达到目的呢?这个时候在 Router 中的 rootHistory 就派上了用场,这个 rootHistory 目的就是为了全局能够便捷的获取 history 对象。接下来具体实现一个监听路由变化的自定义 hooks 。 183 | 184 | ``` 185 | import { useEffect } from 'react' 186 | import { rootHistory } from '../component/Router' 187 | 188 | /* 监听路由改变 */ 189 | function useListen(cb) { 190 | useEffect(()=>{ 191 | if(!rootHistory) return ()=> {} 192 | /* 绑定路由事件监听器 */ 193 | const unlisten = rootHistory.listen((location)=>{ 194 | cb && cb(location) 195 | }) 196 | return function () { 197 | unlisten && unlisten() 198 | } 199 | },[]) 200 | } 201 | export default useListen 202 | ``` 203 | 204 | - 如果 rootHistory 不存在,那么这个 hooks 也就没有任何作用,直接返回空函数就可以了。 205 | - 如果 rootHistory 存在,通过 useEffect ,绑定监听器,然后在销毁函数中,解绑监听器。 206 | 207 | ### 3 高阶组件层面 208 | 209 | 希望通过一个 HOC 能够自由获取路由的状态。所以要实现一个 react-router 中 withRouter 功能。 210 | 211 | **获取路由状态——withRouter** 212 | 213 | ``` 214 | import React , { useContext } from 'react' 215 | import hoistStatics from 'hoist-non-react-statics' 216 | 217 | import { RouterContext } from '../component/Router' 218 | 219 | export default function withRouter(Component){ 220 | const WrapComponent = (props) =>{ 221 | const { wrappedComponentRef, ...remainingProps } = props 222 | const context = useContext(RouterContext) 223 | return 227 | } 228 | return hoistStatics(WrapComponent,Component) 229 | ``` 230 | 231 | - 在高阶组件的包装组件中,用useContext获取路由状态,并传递给原始组件。 232 | - 通过`hoist-non-react-statics`继承原始组件的静态属性。 233 | 234 | ### 4 入口文件 235 | 236 | 完成了核心 api 和组件,接下来需要出口文件,把这些方法暴露出去。 237 | 238 | ``` 239 | //component 240 | import Router ,{ RouterContext } from './component/Router' 241 | import Route from './component/Route' 242 | import Switch from './component/Switch' 243 | //hooks 244 | import useHistory from './hooks/useHistory' 245 | import useListen from './hooks/useListen' 246 | import useLocation from './hooks/useLocation' 247 | //hoc 248 | import withRouter from './hoc/withRouter' 249 | 250 | export { 251 | Router, 252 | Switch, 253 | Route, 254 | RouterContext, 255 | useHistory, 256 | useListen, 257 | useLocation, 258 | withRouter 259 | } 260 | ``` 261 | 262 | ## 四 验证效果 263 | 264 | 一个简单的路由库就实现了,接下来验证一下`mini-Router`的效果: 265 | 266 | ### 配置路由 267 | 268 | ``` 269 | import React from 'react' 270 | import { Router, Route, useHistory, useListen, Switch } from './router' 271 | 272 | /* 引用业务组件 */ 273 | import Detail from './testPage/detail' /* 详情页 */ 274 | import Home from './testPage/home' /* 首页 */ 275 | import List from './testPage/list' /* 列表页 */ 276 | import './index.scss' 277 | 278 | const menusList = [ 279 | { 280 | name:'首页', 281 | path:'/home' 282 | }, 283 | { 284 | name:'列表', 285 | path:'/list' 286 | }, 287 | { 288 | name:'详情', 289 | path:'/detail' 290 | } 291 | ] 292 | /**/ 293 | function Nav() { 294 | const history = useHistory() 295 | /* 路由跳转 */ 296 | const RouterGo = (url) => history.push(url) 297 | const path = history.location.pathname 298 | return
299 | { 300 | menusList.map((item=>RouterGo(item.path)} >{item.name})) 302 | } 303 |
304 | } 305 | 306 | function Top() { 307 | /* 路由监听 */ 308 | useListen((location)=>{ 309 | console.log( '当前路由是:', location.pathname) 310 | }) 311 | console.log(111) 312 | return
--------top------
313 | } 314 | function Index() { 315 | console.log('根组件渲染') 316 | return 317 | 318 |