H5W3
当前位置:H5W3 > JavaScript > 正文

【JS】前端基础知识总结(三)

前端基础知识总结(三)

时倾发布于 今天 09:28

react 16做了哪些更新

  • react作为一个ui库,将前端编程由传统的命令式编程转变为声明式编程,即所谓的数据驱动视图,但如果简单粗暴的操作,比如讲生成的html直接采用innerHtml替换,会带来重绘重排之类的性能问题。为了尽量提高性能,React团队引入了虚拟dom,即采用js对象来描述dom树,通过对比前后两次的虚拟对象,来找到最小的dom操作(vdom diff),以此提高性能。
  • 上面提到的reactDom diff,在react 16之前,这个过程我们称之为stack reconciler,它是一个递归的过程,在树很深的时候,单次diff时间过长会造成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。所以为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将以前的stack reconciler替换为fiber reconciler。采用增量式渲染。引入了任务优先级(expiration)requestIdleCallback的循环调度算法,简单来说就是将以前的一根筋diff更新,首先拆分成两个阶段:reconciliationcommit;第一个reconciliation阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期做小任务diff。然后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。
  • 由于reconciliation是可以被打断的,且存在任务优先级的问题,所以会导致commit前的一些生命周期函数多次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官方已申明这些问题,并将其标记为unsafe,在React17中将会移除
  • 由于每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存workInprogressTree(future vdom) 与 oldTree(current vdom)两个链表,两个链表相互引用。这无形中又解决了另一个问题,当workInprogressTree生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在

React hooks原理

在React 16前,函数式组件不能拥有状态管理?因为16以前只有类组件有对应的实例,而16以后Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力。

Hooks的本质就是闭包两级链表

hooks 链表

一个组件包含的hooks 以链表的形式存储在fiber节点的memoizedState属性上,currentHook链表就是当前正在遍历的fiber节点的。nextCurrentHook 就是即将被添加到正在遍历fiber节点的hooks的新链表

let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 最新的state
baseUpdate: Update<any> | null,
// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// 可以让state变化的,即update或dispach产生的update
next: Hook | null, // link 到下一个 hooks
}

state链表

其实state 链表不是hooks独有的,类操作的setState也存在。

  • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
    react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
  • 为什么只能在函数最外层调用 Hook?

    memoizedState 是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

  • 自定义的 Hook 是如何影响使用它的函数组件的?
    共享同一个 memoizedState,共享同一个顺序。
  • “Capture Value” 特性是如何产生的?
    每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

react setState 异步更新

setState 实现原理

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 状态队列,而不会立即更新 state,队列机制可以高效的批量更新 state。如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。

setState()有的同步有的异步?

在React中, 如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

调用风险

当调用 setState 时,实际上是会执行 enqueueSetState 方法,并会对 partialState_pendingStateQueue 队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

performUpdateIfNecessary 获取 _pendingElement _pendingStateQueue_pendingForceUpdate,并调用 reaciveComponentupdateComponent 来进行组件更新。

但,如果在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 方法,就会造成崩溃。 这是因为在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 时,this._pendingStateQueue!=null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdatecomponentWillUpdate 方法,因此造成循环调用,使得浏览器内存占满后崩溃。

React Fiber

掉帧:在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象,其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI,整个过程不能被打断。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

如何解决主线程长时间被 JS 运算?将JS运算切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

React 15 及以下版本通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 表征reconciliation阶段所能拆分的最小工作单元,其实指的是一种链表树,它可以用一个纯 JS 对象来表示:

const fiber = {
stateNode: {},    // 节点实例
child: {},        // 子节点
sibling: {},      // 兄弟节点
return: {},       // 表示处理完成后返回结果所要合并的目标,通常指向父节点
};

Reconciler区别

  • 以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑;
  • Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行;

Stack ReconcilerFiber Reconciler,源码层面其实就是干了一件递归改循环的事情。

scheduling(调度)

scheduling(调度)是fiber reconciliation的一个过程,主要是进行任务分配,达到分段执行。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行

优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。

Fiber Reconciler 在执行过程中,会分为 2 个阶段:

  • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
  • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。

阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

参考:

React Fiber 原理介绍

React Fiber

HOC 与render props区别

Render Props: 把将要包裹的组件作为props属性传入,然后容器组件调用这个属性,并向其传参

实现方式:

  1. 通过props.children(props),props.children返回的是UI元素。<RenderProps> JSX 标签中的所有内容都会作为一个 children prop 传递给 RenderProps组件。因为 RenderProps{props.children} 渲染在一个 <div> 中,被传递的这些子组件最终都会出现在输出结果中。
// 定义
const RenderProps = props => <div>
{props.children(props)}
</div>
// 调用
<RenderProps>
{() => <>Hello RenderProps</>}
</RenderProps>
  1. 通过props中的任何函数, 自行定义传入内容
// 定义
const LoginForm = props => {
const flag = false;
const allProps = { flag, ...props };
if (flag) {
return <>{props.login(allProps)}</>
} else {
return <>{props.notLogin(allProps)}</>
}
}
// 调用
<LoginForm
login={() => <h1>LOGIN</h1>}
noLogin={() => <h1>NOT LOGIN</h1>}
/>

HOC: 接受一个组件作为参数,返回一个新的组件的函数。

class Home extends React.Component {
// UI
}
export default Connect()(Home);

高阶组件由于每次都会返回一个新的组件,对于react来说,这是不利于diff和状态复用的,所以高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。

总的来说,render props其实和高阶组件类似,就是在puru component上增加state,响应react的生命周期。

React 通信

react的数据流是单向的,最常见的就是通过props由父组件向子组件传值。

  • 父向子通信: 传入props
  • 子向父通信:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
  • 父向孙通信:利用context传值。React.createContext()
  • 兄弟间通信:

​ 1、找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。
​ 2、用一些全局机制去实现通信,比如redux等
​ 3、发布订阅模式

react合成事件

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。

为什么要使用合成事件?

  1. 进行浏览器兼容,实现更好的跨平台
    React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。
  2. 避免垃圾回收
    事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)
  3. 方便事件统一管理和事务机制

实现原理
在 React 中,“合成事件”会以事件委托方式绑定在 document 对象上,并在组件卸载(unmount)阶段自动销毁绑定的事件。

合成事件和原生事件

当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行 document 上挂载的事件。

合成事件和原生事件最好不要混用。 原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上,所有的 React 事件都将无法被注册。

合成事件的事件池

合成事件对象池,是 React 事件系统提供的一种性能优化方式合成事件对象在事件池统一管理不同类型的合成事件具有不同的事件池

react与vue区别

1. 监听数据变化的实现原理不同

Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。

2. 数据流不同

Vue1.0中可以实现两种双向绑定:父子组件之间props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。
Vue2.x中父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改)。
React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。

3. HoC和mixins

Vue组合不同功能的方式是通过mixin,Vue中组件是一个被包装的函数,并不简单的就是我们定义组件的时候传入的对象或者函数。
React组合不同功能的方式是通过HoC(高阶组件)。

4. 模板渲染方式的不同

模板的语法不同,React是通过JSX渲染模板, Vue是通过一种拓展的HTML语法进行渲染。
模板的原理不同,React通过原生JS实现模板中的常见语法,比如插值,条件,循环等。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如 v-if 。

举个例子,说明React的好处:react中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们import 一个组件完了之后,还需要在 components 中再声明下。

5. 渲染过程不同

Vue可以更快地计算出Virtual DOM的差异,这是由于它会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
React当状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。

6. 框架本质不同

Vue本质是MVVM框架,由MVC发展而来;
React是前端组件化框架,由后端组件化发展而来。

性能优化

1. 静态资源使用 CDN

CDN是一组分布在多个不同地理位置的 Web 服务器。当服务器离用户越远时,延迟越高。

2. 无阻塞

头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载

3. 压缩文件

压缩文件可以减少文件下载时间。

  1. 在 webpack 可以使用如下插件进行压缩:
  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin
  1. 使用 gzip 压缩。通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。

4. 图片优化

  1. 图片懒加载
  2. 响应式图片:浏览器根据屏幕大小自动加载合适的图片。
  3. 降低图片质量:方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

5. 减少重绘重排

  • 降低 CSS 选择器的复杂性
  • 使用 transform 和 opacity 属性更改来实现动画
  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

6. 使用 requestAnimationFrame 来实现视觉变化

7. webpack 打包, 添加文件缓存

index.html 设置成 no-cache,这样每次请求的时候都会比对一下 index.html 文件有没变化,如果没变化就使用缓存,有变化就使用新的 index.html 文件。
其他所有文件一律使用长缓存,例如设置成缓存一年 maxAge: 1000 * 60 * 60 * 24 * 365
前端代码使用 webpack 打包,根据文件内容生成对应的文件名,每次重新打包时只有内容发生了变化,文件名才会发生变化。

输入url后发生了什么

  1. DNS域名解析;
  2. 建立TCP连接(三次握手);
  3. 发送HTTP请求;
  4. 服务器处理请求;
  5. 返回响应结果;
  6. 关闭TCP连接(四次握手);
  7. 浏览器解析HTML;
  8. 浏览器布局渲染;

1. DNS域名解析: 拿到服务器ip

客户端收到你输入的域名地址后,它首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。

2. 建立TCP链接: 客户端链接服务器

TCP提供了一种可靠、面向连接、字节流、传输层的服务。对于客户端与服务器的TCP链接,必然要说的就是『三次握手』。“3次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的

客户端发送一个带有SYN标志的数据包给服务端,服务端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息,最后客户端再回传一个带ACK标志的数据包,代表握手结束,连接成功。

SYN —— 用于初如化一个连接的序列号。
ACK —— 确认,使得确认号有效。
RST —— 重置连接。
FIN —— 该报文段的发送方已经结束向对方发送数据。

3. 发送HTTP请求

4. 服务器处理请求

5. 返回响应结果

6. 关闭TCP连接(需要4次握手)

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。

关闭连接时,服务器收到对方的FIN报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务器也未必全部数据都发送给客户端,所以服务器可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

7. 浏览器解析HTML

浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS,以及还要加载图片、视频等其他媒体资源。

浏览器通过解析HTML,生成DOM树,解析CSS,生成CSSOM树,然后通过DOM树和CSSPOM树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。

8. 浏览器渲染页面

根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。最后浏览器绘制各个节点,将页面展示给用户。

参考:

细说浏览器输入URL后发生了什么

浏览器输入 URL 后发生了什么?

Babel Plugin与preset区别

Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性。

原始代码 --> [Babel Plugin] --> 转换后的代码

Plugin

实现Babel代码转换功能的核心,就是Babel插件(plugin)。Babel插件一般尽可能拆成小的力度,开发者可以按需引进, 既提高了性能,也提高了扩展性。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。开发者想要体验ES6的箭头函数特性,那只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。

Preset

可以简单的把Babel Preset视为Babel Plugin的集合。想要将所有ES6的代码转成ES5,逐个插件引入的效率比较低下, 就可以采用Babel Preset。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。

Plugin与Preset执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

比如.babelrc配置如下,那么执行的顺序为:

  1. Plugin:transform-react-jsx、transform-async-to-generator
  2. Preset:es2016、es2015
{
"presets": [
"es2015",
"es2016"
],
"plugins": [
"transform-react-jsx",
"transform-async-to-generator"
]
}

webpack hash区别

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

  • hash

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改。同一次构建过程中生成的哈希都是一样的。

output:{
path:path.join(__dirname, '/dist'),
filename: 'bundle.[name].[hash].js',
}
  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

output:{
path:path.join(__dirname, '/dist/js'),
filename: 'bundle.[name].[chunkhash].js',
}

采用chunkhash,项目主入口文件Index.js及其对应的依赖文件Index.css由于被打包在同一个模块,共用相同的chunkhash。由于公共库是不同的模块,有单独的chunkhash。所以Index文件的更改不会影响公共库。如果index.js更改了代码,css未改变,由于该模块发生了改变,导致css文件会重复构建。

  • contenthash

根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 才会发生变化。

output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
}

typeof 如何判断的

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息:

对象: 000
浮点数: 010
字符串: 100
布尔: 110
整数: 1
null:所有机器码均为0
undefined:用 −2^30 整数来表示

// JavaScript 诞生以来便如此
typeof null === 'object';

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"

怎样开发和部署前端代码

为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。当需要更新静态资源的时候,同时也会更新html中的引用。

如果同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,是先上线页面,还是先上线静态资源?

  1. 先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。
  2. 先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。

这个奇葩问题,起源于资源的 覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决它也好办,就是实现 非覆盖式发布。用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。

大公司的静态资源优化方案,基本上要实现这么几个东西:

  1. 配置超长时间的本地缓存 —— 节省带宽,提高性能
  2. 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  3. 静态资源CDN部署 —— 优化网络请求
  4. 更改资源发布路径实现非覆盖式发布 —— 平滑升级

大数相加

function add(a, b){
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, 0);
b = b.padStart(maxLength, 0);
let t = 0;
let f = 0;
let sum = "";
for (let i = maxLength - 1; i >= 0; i--) {
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t / 10);
sum = `${t % 10}${sum}`;
}
if (f === 1){
sum = "1" + sum;
}
return sum;
}

斐波那契数列求和

function fib(n) {
if (n <= 0) {
return 0;
}
let n1 = 1;
let n2 = 1;
let sum = 1;
for(let i = 3; i <= n; i++) {
[n1, n2] = [n2, sum];
sum = n1 + n2;
}
return sum;
};
javascript前端react.js
阅读 86发布于 今天 09:28
本作品系原创,采用《署名-非商业性使用-禁止演绎 4.0 国际》许可协议
avatar

时倾

把梦想放在心中

190 声望
1.4k 粉丝

0 条评论
得票时间

avatar

时倾

把梦想放在心中

190 声望
1.4k 粉丝

宣传栏

react 16做了哪些更新

  • react作为一个ui库,将前端编程由传统的命令式编程转变为声明式编程,即所谓的数据驱动视图,但如果简单粗暴的操作,比如讲生成的html直接采用innerHtml替换,会带来重绘重排之类的性能问题。为了尽量提高性能,React团队引入了虚拟dom,即采用js对象来描述dom树,通过对比前后两次的虚拟对象,来找到最小的dom操作(vdom diff),以此提高性能。
  • 上面提到的reactDom diff,在react 16之前,这个过程我们称之为stack reconciler,它是一个递归的过程,在树很深的时候,单次diff时间过长会造成JS线程持续被占用,用户交互响应迟滞,页面渲染会出现明显的卡顿,这在现代前端是一个致命的问题。所以为了解决这种问题,react 团队对整个架构进行了调整,引入了fiber架构,将以前的stack reconciler替换为fiber reconciler。采用增量式渲染。引入了任务优先级(expiration)requestIdleCallback的循环调度算法,简单来说就是将以前的一根筋diff更新,首先拆分成两个阶段:reconciliationcommit;第一个reconciliation阶段是可打断的,被拆分成一个个的小任务(fiber),在每一侦的渲染空闲期做小任务diff。然后是commit阶段,这个阶段是不拆分且不能打断的,将diff节点的effectTag一口气更新到页面上。
  • 由于reconciliation是可以被打断的,且存在任务优先级的问题,所以会导致commit前的一些生命周期函数多次被执行, 如componentWillMount、componentWillReceiveProps 和 componetWillUpdate,但react官方已申明这些问题,并将其标记为unsafe,在React17中将会移除
  • 由于每次唤起更新是从根节点(RootFiber)开始,为了更好的节点复用与性能优化。在react中始终存workInprogressTree(future vdom) 与 oldTree(current vdom)两个链表,两个链表相互引用。这无形中又解决了另一个问题,当workInprogressTree生成报错时,这时也不会导致页面渲染崩溃,而只是更新失败,页面仍然还在

React hooks原理

在React 16前,函数式组件不能拥有状态管理?因为16以前只有类组件有对应的实例,而16以后Fiber 架构的出现,让每一个节点都拥有对应的实例,也就拥有了保存状态的能力。

Hooks的本质就是闭包两级链表

hooks 链表

一个组件包含的hooks 以链表的形式存储在fiber节点的memoizedState属性上,currentHook链表就是当前正在遍历的fiber节点的。nextCurrentHook 就是即将被添加到正在遍历fiber节点的hooks的新链表

let currentHook: Hook | null = null;
let nextCurrentHook: Hook | null = null;
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 最新的state
baseUpdate: Update<any> | null,
// 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null,// 可以让state变化的,即update或dispach产生的update
next: Hook | null, // link 到下一个 hooks
}

state链表

其实state 链表不是hooks独有的,类操作的setState也存在。

  • memoizedState,cursor 是存在哪里的?如何和每个函数组件一一对应的?
    react 会生成一棵组件树(或Fiber 单链表),树中每个节点对应了一个组件,hooks 的数据就作为组件的一个信息,存储在这些节点上,伴随组件一起出生,一起死亡。
  • 为什么只能在函数最外层调用 Hook?

    memoizedState 是按 hook定义的顺序来放置数据的,如果 hook 顺序变化,memoizedState 并不会感知到。

  • 自定义的 Hook 是如何影响使用它的函数组件的?
    共享同一个 memoizedState,共享同一个顺序。
  • “Capture Value” 特性是如何产生的?
    每一次 ReRender 的时候,都是重新去执行函数组件了,对于之前已经执行过的函数组件,并不会做任何操作。

react setState 异步更新

setState 实现原理

setState 通过一个队列机制来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 状态队列,而不会立即更新 state,队列机制可以高效的批量更新 state。如果不通过setState,直接修改this.state 的值,则不会放入状态队列,当下一次调用 setState 对状态队列进行合并时,之前对 this.state 的修改将会被忽略,造成无法预知的错误。

setState()有的同步有的异步?

在React中, 如果是由React引发的事件处理(比如通过onClick引发的事件处理),调用setState不会同步更新this.state,除此之外的setState调用会同步执行this.state 。所谓“除此之外”,指的是绕过React通过addEventListener直接添加的事件处理函数,还有通过setTimeout/setInterval产生的异步调用。

原因: 在React的setState函数实现中,会根据一个变量isBatchingUpdates判断是直接更新this.state还是放到队列中回头再说,而isBatchingUpdates默认是false,也就表示setState会同步更新this.state,但是,有一个函数batchedUpdates,这个函数会把isBatchingUpdates修改为true,而当React在调用事件处理函数之前就会调用这个batchedUpdates,造成的后果,就是由React控制的事件处理过程setState不会同步更新this.state

调用风险

当调用 setState 时,实际上是会执行 enqueueSetState 方法,并会对 partialState_pendingStateQueue 队列进行合并操作,最终通过 enqueueUpdate 执行 state 更新。

performUpdateIfNecessary 获取 _pendingElement _pendingStateQueue_pendingForceUpdate,并调用 reaciveComponentupdateComponent 来进行组件更新。

但,如果在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 方法,就会造成崩溃。 这是因为在 shouldComponentUpdatecomponentWillUpdate 方法里调用 this.setState 时,this._pendingStateQueue!=null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新,而 updateComponent 方法又会调用 shouldComponentUpdatecomponentWillUpdate 方法,因此造成循环调用,使得浏览器内存占满后崩溃。

React Fiber

掉帧:在页面元素很多,且需要频繁刷新的场景下,React 15 会出现掉帧的现象,其根本原因,是大量的同步计算任务阻塞了浏览器的 UI 渲染。默认情况下,JS 运算、页面布局和页面绘制都是运行在浏览器的主线程当中,他们之间是互斥的关系。如果 JS 运算持续占用主线程,页面就没法得到及时的更新。当我们调用setState更新页面的时候,React 会遍历应用的所有节点,计算出差异,然后再更新 UI,整个过程不能被打断。如果页面元素很多,整个过程占用的时机就可能超过 16 毫秒,就容易出现掉帧的现象。

如何解决主线程长时间被 JS 运算?将JS运算切割为多个步骤,分批完成。在完成一部分任务之后,将控制权交回给浏览器,让浏览器有时间进行页面的渲染。等浏览器忙完之后,再继续之前未完成的任务。

React 15 及以下版本通过递归的方式进行渲染,使用的是 JS 引擎自身的函数调用栈,它会一直执行到栈空为止。而Fiber实现了自己的组件调用栈,它以链表的形式遍历组件树,可以灵活的暂停、继续和丢弃执行的任务。实现方式是使用了浏览器的requestIdleCallback

React 框架内部的运作可以分为 3 层:

  • Virtual DOM 层,描述页面长什么样。
  • Reconciler 层,负责调用组件生命周期方法,进行 Diff 运算等。
  • Renderer 层,根据不同的平台,渲染出相应的页面,比较常见的是 ReactDOM 和 ReactNative。

Fiber 表征reconciliation阶段所能拆分的最小工作单元,其实指的是一种链表树,它可以用一个纯 JS 对象来表示:

const fiber = {
stateNode: {},    // 节点实例
child: {},        // 子节点
sibling: {},      // 兄弟节点
return: {},       // 表示处理完成后返回结果所要合并的目标,通常指向父节点
};

Reconciler区别

  • 以前的 Reconciler 被命名为Stack Reconciler。Stack Reconciler 运作的过程是不能被打断的,必须一条道走到黑;
  • Fiber Reconciler 每执行一段时间,都会将控制权交回给浏览器,可以分段执行;

Stack ReconcilerFiber Reconciler,源码层面其实就是干了一件递归改循环的事情。

scheduling(调度)

scheduling(调度)是fiber reconciliation的一个过程,主要是进行任务分配,达到分段执行。任务的优先级有六种:

  • synchronous,与之前的Stack Reconciler操作一样,同步执行
  • task,在next tick之前执行
  • animation,下一帧之前执行
  • high,在不久的将来立即执行
  • low,稍微延迟执行也没关系
  • offscreen,下一次render时或scroll时才执行

优先级高的任务(如键盘输入)可以打断优先级低的任务(如Diff)的执行,从而更快的生效。

Fiber Reconciler 在执行过程中,会分为 2 个阶段:

  • 阶段一,生成 Fiber 树,得出需要更新的节点信息。这一步是一个渐进的过程,可以被打断。
  • 阶段二,将需要更新的节点一次过批量更新,这个过程不能被打断。

阶段一可被打断的特性,让优先级更高的任务先执行,从框架层面大大降低了页面掉帧的概率。

参考:

React Fiber 原理介绍

React Fiber

HOC 与render props区别

Render Props: 把将要包裹的组件作为props属性传入,然后容器组件调用这个属性,并向其传参

实现方式:

  1. 通过props.children(props),props.children返回的是UI元素。<RenderProps> JSX 标签中的所有内容都会作为一个 children prop 传递给 RenderProps组件。因为 RenderProps{props.children} 渲染在一个 <div> 中,被传递的这些子组件最终都会出现在输出结果中。
// 定义
const RenderProps = props => <div>
{props.children(props)}
</div>
// 调用
<RenderProps>
{() => <>Hello RenderProps</>}
</RenderProps>
  1. 通过props中的任何函数, 自行定义传入内容
// 定义
const LoginForm = props => {
const flag = false;
const allProps = { flag, ...props };
if (flag) {
return <>{props.login(allProps)}</>
} else {
return <>{props.notLogin(allProps)}</>
}
}
// 调用
<LoginForm
login={() => <h1>LOGIN</h1>}
noLogin={() => <h1>NOT LOGIN</h1>}
/>

HOC: 接受一个组件作为参数,返回一个新的组件的函数。

class Home extends React.Component {
// UI
}
export default Connect()(Home);

高阶组件由于每次都会返回一个新的组件,对于react来说,这是不利于diff和状态复用的,所以高阶组件的包装不能在render 方法中进行,而只能像上面那样在组件声明时包裹,这样也就不利于动态传参。

总的来说,render props其实和高阶组件类似,就是在puru component上增加state,响应react的生命周期。

React 通信

react的数据流是单向的,最常见的就是通过props由父组件向子组件传值。

  • 父向子通信: 传入props
  • 子向父通信:父组件向子组件传一个函数,然后通过这个函数的回调,拿到子组件传过来的值
  • 父向孙通信:利用context传值。React.createContext()
  • 兄弟间通信:

​ 1、找一个相同的父组件,既可以用props传递数据,也可以用context的方式来传递数据。
​ 2、用一些全局机制去实现通信,比如redux等
​ 3、发布订阅模式

react合成事件

React 合成事件(SyntheticEvent)是 React 模拟原生 DOM 事件所有能力的一个事件对象,即浏览器原生事件的跨浏览器包装器。

为什么要使用合成事件?

  1. 进行浏览器兼容,实现更好的跨平台
    React 采用的是顶层事件代理机制,能够保证冒泡一致性,可以跨浏览器执行。React 提供的合成事件用来抹平不同浏览器事件对象之间的差异,将不同平台事件模拟合成事件。
  2. 避免垃圾回收
    事件对象可能会被频繁创建和回收,因此 React 引入事件池,在事件池中获取或释放事件对象。即 React 事件对象不会被释放掉,而是存放进一个数组中,当事件触发,就从这个数组中弹出,避免频繁地去创建和销毁(垃圾回收)
  3. 方便事件统一管理和事务机制

实现原理
在 React 中,“合成事件”会以事件委托方式绑定在 document 对象上,并在组件卸载(unmount)阶段自动销毁绑定的事件。

合成事件和原生事件

当真实 DOM 元素触发事件,会冒泡到 document 对象后,再处理 React 事件;所以会先执行原生事件,然后处理 React 事件;最后真正执行 document 上挂载的事件。

合成事件和原生事件最好不要混用。 原生事件中如果执行了stopPropagation方法,则会导致其他React事件失效。因为所有元素的事件将无法冒泡到document上,所有的 React 事件都将无法被注册。

合成事件的事件池

合成事件对象池,是 React 事件系统提供的一种性能优化方式合成事件对象在事件池统一管理不同类型的合成事件具有不同的事件池

react与vue区别

1. 监听数据变化的实现原理不同

Vue通过 getter/setter以及一些函数的劫持,能精确知道数据变化。
React默认是通过比较引用的方式(diff)进行的,如果不优化可能导致大量不必要的VDOM的重新渲染。

2. 数据流不同

Vue1.0中可以实现两种双向绑定:父子组件之间props可以双向绑定;组件与DOM之间可以通过v-model双向绑定。
Vue2.x中父子组件之间不能双向绑定了(但是提供了一个语法糖自动帮你通过事件的方式修改)。
React一直不支持双向绑定,提倡的是单向数据流,称之为onChange/setState()模式。

3. HoC和mixins

Vue组合不同功能的方式是通过mixin,Vue中组件是一个被包装的函数,并不简单的就是我们定义组件的时候传入的对象或者函数。
React组合不同功能的方式是通过HoC(高阶组件)。

4. 模板渲染方式的不同

模板的语法不同,React是通过JSX渲染模板, Vue是通过一种拓展的HTML语法进行渲染。
模板的原理不同,React通过原生JS实现模板中的常见语法,比如插值,条件,循环等。而Vue是在和组件JS代码分离的单独的模板中,通过指令来实现的,比如 v-if 。

举个例子,说明React的好处:react中render函数是支持闭包特性的,所以我们import的组件在render中可以直接调用。但是在Vue中,由于模板中使用的数据都必须挂在 this 上进行一次中转,所以我们import 一个组件完了之后,还需要在 components 中再声明下。

5. 渲染过程不同

Vue可以更快地计算出Virtual DOM的差异,这是由于它会跟踪每一个组件的依赖关系,不需要重新渲染整个组件树。
React当状态被改变时,全部子组件都会重新渲染。通过shouldComponentUpdate这个生命周期方法可以进行控制,但Vue将此视为默认的优化。

6. 框架本质不同

Vue本质是MVVM框架,由MVC发展而来;
React是前端组件化框架,由后端组件化发展而来。

性能优化

1. 静态资源使用 CDN

CDN是一组分布在多个不同地理位置的 Web 服务器。当服务器离用户越远时,延迟越高。

2. 无阻塞

头部内联的样式和脚本会阻塞页面的渲染,样式放在头部并使用link方式引入,脚本放在尾部并使用异步方式加载

3. 压缩文件

压缩文件可以减少文件下载时间。

  1. 在 webpack 可以使用如下插件进行压缩:
  • JavaScript:UglifyPlugin
  • CSS :MiniCssExtractPlugin
  • HTML:HtmlWebpackPlugin
  1. 使用 gzip 压缩。通过向 HTTP 请求头中的 Accept-Encoding 头添加 gzip 标识来开启这一功能。

4. 图片优化

  1. 图片懒加载
  2. 响应式图片:浏览器根据屏幕大小自动加载合适的图片。
  3. 降低图片质量:方法有两种,一是通过 webpack 插件 image-webpack-loader,二是通过在线网站进行压缩。

5. 减少重绘重排

  • 降低 CSS 选择器的复杂性
  • 使用 transform 和 opacity 属性更改来实现动画
  • 用 JavaScript 修改样式时,最好不要直接写样式,而是替换 class 来改变样式。
  • 如果要对 DOM 元素执行一系列操作,可以将 DOM 元素脱离文档流,修改完成后,再将它带回文档。推荐使用隐藏元素(display:none)或文档碎片(DocumentFragement),都能很好的实现这个方案。

6. 使用 requestAnimationFrame 来实现视觉变化

7. webpack 打包, 添加文件缓存

index.html 设置成 no-cache,这样每次请求的时候都会比对一下 index.html 文件有没变化,如果没变化就使用缓存,有变化就使用新的 index.html 文件。
其他所有文件一律使用长缓存,例如设置成缓存一年 maxAge: 1000 * 60 * 60 * 24 * 365
前端代码使用 webpack 打包,根据文件内容生成对应的文件名,每次重新打包时只有内容发生了变化,文件名才会发生变化。

输入url后发生了什么

  1. DNS域名解析;
  2. 建立TCP连接(三次握手);
  3. 发送HTTP请求;
  4. 服务器处理请求;
  5. 返回响应结果;
  6. 关闭TCP连接(四次握手);
  7. 浏览器解析HTML;
  8. 浏览器布局渲染;

1. DNS域名解析: 拿到服务器ip

客户端收到你输入的域名地址后,它首先去找本地的hosts文件,检查在该文件中是否有相应的域名、IP对应关系,如果有,则向其IP地址发送请求,如果没有,再去找DNS服务器。

2. 建立TCP链接: 客户端链接服务器

TCP提供了一种可靠、面向连接、字节流、传输层的服务。对于客户端与服务器的TCP链接,必然要说的就是『三次握手』。“3次握手”的作用就是双方都能明确自己和对方的收、发能力是正常的

客户端发送一个带有SYN标志的数据包给服务端,服务端收到后,回传一个带有SYN/ACK标志的数据包以示传达确认信息,最后客户端再回传一个带ACK标志的数据包,代表握手结束,连接成功。

SYN —— 用于初如化一个连接的序列号。
ACK —— 确认,使得确认号有效。
RST —— 重置连接。
FIN —— 该报文段的发送方已经结束向对方发送数据。

3. 发送HTTP请求

4. 服务器处理请求

5. 返回响应结果

6. 关闭TCP连接(需要4次握手)

为了避免服务器与客户端双方的资源占用和损耗,当双方没有请求或响应传递时,任意一方都可以发起关闭请求。

关闭连接时,服务器收到对方的FIN报文时,仅仅表示客户端不再发送数据了但是还能接收数据,而服务器也未必全部数据都发送给客户端,所以服务器可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

7. 浏览器解析HTML

浏览器需要加载解析的不仅仅是HTML,还包括CSS、JS,以及还要加载图片、视频等其他媒体资源。

浏览器通过解析HTML,生成DOM树,解析CSS,生成CSSOM树,然后通过DOM树和CSSPOM树生成渲染树。渲染树与DOM树不同,渲染树中并没有head、display为none等不必显示的节点。

8. 浏览器渲染页面

根据渲染树布局,计算CSS样式,即每个节点在页面中的大小和位置等几何信息。HTML默认是流式布局的,CSS和js会打破这种布局,改变DOM的外观样式以及大小和位置。最后浏览器绘制各个节点,将页面展示给用户。

参考:

细说浏览器输入URL后发生了什么

浏览器输入 URL 后发生了什么?

Babel Plugin与preset区别

Babel是代码转换器,比如将ES6转成ES5,或者将JSX转成JS等。借助Babel,开发者可以提前用上新的JS特性。

原始代码 --> [Babel Plugin] --> 转换后的代码

Plugin

实现Babel代码转换功能的核心,就是Babel插件(plugin)。Babel插件一般尽可能拆成小的力度,开发者可以按需引进, 既提高了性能,也提高了扩展性。比如对ES6转ES5的功能,Babel官方拆成了20+个插件。开发者想要体验ES6的箭头函数特性,那只需要引入transform-es2015-arrow-functions插件就可以,而不是加载ES6全家桶。

Preset

可以简单的把Babel Preset视为Babel Plugin的集合。想要将所有ES6的代码转成ES5,逐个插件引入的效率比较低下, 就可以采用Babel Preset。比如babel-preset-es2015就包含了所有跟ES6转换有关的插件。

Plugin与Preset执行顺序

可以同时使用多个Plugin和Preset,此时,它们的执行顺序非常重要。

  1. 先执行完所有Plugin,再执行Preset。
  2. 多个Plugin,按照声明次序顺序执行。
  3. 多个Preset,按照声明次序逆序执行。

比如.babelrc配置如下,那么执行的顺序为:

  1. Plugin:transform-react-jsx、transform-async-to-generator
  2. Preset:es2016、es2015
{
"presets": [
"es2015",
"es2016"
],
"plugins": [
"transform-react-jsx",
"transform-async-to-generator"
]
}

webpack hash区别

hash一般是结合CDN缓存来使用,通过webpack构建之后,生成对应文件名自动带上对应的MD5值。如果文件内容改变的话,那么对应文件哈希值也会改变,对应的HTML引用的URL地址也会改变,触发CDN服务器从源服务器上拉取对应数据,进而更新本地缓存。

  • hash

hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改。同一次构建过程中生成的哈希都是一样的。

output:{
path:path.join(__dirname, '/dist'),
filename: 'bundle.[name].[hash].js',
}
  • chunkhash

根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成哈希值,那么只要我们不改动公共库的代码,就可以保证其哈希值不会受影响。

output:{
path:path.join(__dirname, '/dist/js'),
filename: 'bundle.[name].[chunkhash].js',
}

采用chunkhash,项目主入口文件Index.js及其对应的依赖文件Index.css由于被打包在同一个模块,共用相同的chunkhash。由于公共库是不同的模块,有单独的chunkhash。所以Index文件的更改不会影响公共库。如果index.js更改了代码,css未改变,由于该模块发生了改变,导致css文件会重复构建。

  • contenthash

根据文件内容创建出唯一 hash。当文件内容发生变化时,[contenthash] 才会发生变化。

output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js',
path: path.resolve(__dirname, '../dist'),
}

typeof 如何判断的

js 在底层存储变量的时候,会在变量的机器码的低位1-3位存储其类型信息:

对象: 000
浮点数: 010
字符串: 100
布尔: 110
整数: 1
null:所有机器码均为0
undefined:用 −2^30 整数来表示

// JavaScript 诞生以来便如此
typeof null === 'object';

在 JavaScript 最初的实现中,JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null 的类型标签是 0,typeof null 也因此返回 "object"

怎样开发和部署前端代码

为了进一步提升网站性能,会把静态资源和动态网页分集群部署,静态资源会被部署到CDN节点上,网页中引用的资源也会变成对应的部署路径。当需要更新静态资源的时候,同时也会更新html中的引用。

如果同时改了页面结构和样式,也更新了静态资源对应的url地址,现在要发布代码上线,是先上线页面,还是先上线静态资源?

  1. 先部署页面,再部署资源:在二者部署的时间间隔内,如果有用户访问页面,就会在新的页面结构中加载旧的资源,并且把这个旧版本的资源当做新版本缓存起来,其结果就是:用户访问到了一个样式错乱的页面,除非手动刷新,否则在资源缓存过期之前,页面会一直执行错误。
  2. 先部署资源,再部署页面:在部署时间间隔之内,有旧版本资源本地缓存的用户访问网站,由于请求的页面是旧版本的,资源引用没有改变,浏览器将直接使用本地缓存,这种情况下页面展现正常;但没有本地缓存或者缓存过期的用户访问网站,就会出现旧版本页面加载新版本资源的情况,导致页面执行错误,但当页面完成部署,这部分用户再次访问页面又会恢复正常了。

这个奇葩问题,起源于资源的 覆盖式发布,用 待发布资源 覆盖 已发布资源,就有这种问题。解决它也好办,就是实现 非覆盖式发布。用文件的摘要信息来对资源文件进行重命名,把摘要信息放到资源文件发布路径中,这样,内容有修改的资源就变成了一个新的文件发布到线上,不会覆盖已有的资源文件。上线过程中,先全量部署静态资源,再灰度部署页面,整个问题就比较完美的解决了。

大公司的静态资源优化方案,基本上要实现这么几个东西:

  1. 配置超长时间的本地缓存 —— 节省带宽,提高性能
  2. 采用内容摘要作为缓存更新依据 —— 精确的缓存控制
  3. 静态资源CDN部署 —— 优化网络请求
  4. 更改资源发布路径实现非覆盖式发布 —— 平滑升级

大数相加

function add(a, b){
const maxLength = Math.max(a.length, b.length);
a = a.padStart(maxLength, 0);
b = b.padStart(maxLength, 0);
let t = 0;
let f = 0;
let sum = "";
for (let i = maxLength - 1; i >= 0; i--) {
t = parseInt(a[i]) + parseInt(b[i]) + f;
f = Math.floor(t / 10);
sum = `${t % 10}${sum}`;
}
if (f === 1){
sum = "1" + sum;
}
return sum;
}

斐波那契数列求和

function fib(n) {
if (n <= 0) {
return 0;
}
let n1 = 1;
let n2 = 1;
let sum = 1;
for(let i = 3; i <= n; i++) {
[n1, n2] = [n2, sum];
sum = n1 + n2;
}
return sum;
};

本文地址:H5W3 » 【JS】前端基础知识总结(三)

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址