React.createElement(): 根据指定的第一个参数创建一个React元素
React.createElement(
type,
[props],
[...children]
)
//写法一:
var child1 = React.createElement('li', null, 'one');
var child2 = React.createElement('li', null, 'two');
var content = React.createElement('ul', { className: 'teststyle' }, child1, child2); // 第三个参数可以分开也可以写成一个数组
ReactDOM.render(
content,
document.getElementById('example')
);
//写法二:
var child1 = React.createElement('li', null, 'one');
var child2 = React.createElement('li', null, 'two');
var content = React.createElement('ul', { className: 'teststyle' }, [child1, child2]);
ReactDOM.render(
content,
document.getElementById('example')
);
----------@----------
reconciliation
)的过程。和解(reconciliation
)的最终目标是以最有效的方式,根据这个新的状态来更新UI
。 为此,React
将构建一个新的 React
元素树(您可以将其视为 UI
的对象表示)通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间
----------@----------
在了解setState之前,我们先来简单了解下 React 一个包装结构: Transaction:
事务 (Transaction)
是 React 中的一个调用结构,用于包装一个方法,结构为: initialize - perform(method) - close。通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作
异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:
注意事项:
总结
setState 并非真异步,只是看上去像异步。在源码中,通过
isBatchingUpdates
来判断
setState
是先存进 state
队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。isBatchingUpdates
会为 true
呢?在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。addEventListener
、setTimeout
、setInterval
等事件中,就只能同步更新。一般认为,
做异步设计是为了性能优化、减少渲染次数
,React 团队还补充了两点。
setState
只有在 React 自身的合成事件和钩子函数中是异步的,在原生事件和 setTimeout 中都是同步的setState
的异步并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的异步。当然可以通过 setState 的第二个参数中的 callback 拿到更新后的结果setState
的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在异步中如果对同一个值进行多次 setState,setState 的批量更新策略会对其进行覆盖,去最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新
这是一道经常会出现的 React setState 笔试题:下面的代码输出什么呢?
class Test extends React.Component {
state = {
count: 0
};
componentDidMount() {
this.setState({count: this.state.count + 1});
console.log(this.state.count);
this.setState({count: this.state.count + 1});
console.log(this.state.count);
setTimeout(() => {
this.setState({count: this.state.count + 1});
console.log(this.state.count);
this.setState({count: this.state.count + 1});
console.log(this.state.count);
}, 0);
}
render() {
return null;
}
};
我们可以进行如下的分析:
console.log
,都在 React 的生命周期事件中,所以是异步的处理方式,则输出都为 0
;setTimeout
中的 console.log
处于原生事件中,所以会同步的处理再输出结果,但需要注意,虽然 count
在前面经过了两次的 this.state.count + 1
,但是每次获取的 this.state.count
都是初始化时的值,也就是 0
;count
是 1
,那么后续在 setTimeout
中的输出则是 2
和 3
。所以完整答案是 0,0,2,3
同步场景
异步场景中的案例使我们建立了这样一个认知:setState 是异步的,但下面这个案例又会颠覆你的认知。如果我们将 setState 放在 setTimeout 事件中,那情况就完全不同了。
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState({ count: this.state.count + 1 });
console.log(this.state.count);
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log("setTimeout: " + this.state.count);
}, 0);
}
render(){
...
}
}
那这时输出的应该是什么呢?如果你认为是 0,0,那么又错了。
正确的结果是 0,2
。因为 setState
并不是真正的异步函数,它实际上是通过队列延迟执行操作实现的,通过 isBatchingUpdates 来判断 setState 是先存进 state 队列还是直接更新。值为 true 则执行异步操作,false 则直接同步更新
接下来这个案例的答案是什么呢
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
this.setState({
count: this.state.count + 1
}, () => {
console.log(this.state.count)
})
}
render(){
...
}
}
如果你觉得答案是 1,2
,那肯定就错了。这种迷惑性极强的考题在面试中非常常见,因为它反直觉。
如果重新仔细思考,你会发现当前拿到的 this.state.count
的值并没有变化,都是 0
,所以输出结果应该是 1,1
。
当然,也可以在 setState
函数中获取修改后的 state
值进行修改。
class Test extends Component {
state = {
count: 0
}
componentDidMount(){
this.setState(
preState=> ({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
this.setState(
preState=>({
count:preState.count + 1
}),()=>{
console.log(this.state.count)
})
}
render(){
...
}
}
这些通通是异步的回调,如果你以为输出结果是 1,2,那就又错了,实际上是 2,2
。
为什么会这样呢?当调用 setState
函数时,就会把当前的操作放入队列中
。React 根据队列内容,合并 state 数据,完成后再逐一执行回调,根据结果更新虚拟 DOM,触发渲染。所以回调时,state 已经合并计算完成了
,输出的结果就是 2,2
了。
----------@----------
1. setState异步更新
React
通过this.state
来访问state
,通过this.setState()
方法来更新state
。当this.setState()
方法被调用的时候,React
会重新调用render
方法来重新渲染UI
setState
后面获取state
的值是获取不到的。在React
内部机制能检测到的地方, setState
就是异步的;在React
检测不到的地方,例如setInterval
,setTimeout
,setState
就是同步更新的因为
setState
是可以接受两个参数的,一个state
,一个回调函数。因此我们可以在回调函数里面获取值
setState
方法通过一个队列机制实现state
更新,当执行setState
的时候,会将需要更新的state
合并之后放入状态队列,而不会立即更新this.state
setState
而是使用this.state.key
来修改,将不会触发组件的re-render
。this.state
赋值给一个新的对象引用,那么其他不在对象上的state
将不会被放入状态队列中,当下次调用setState
并对状态队列进行合并时,直接造成了state
丢失1.1 setState批量更新的过程
在
react
生命周期和合成事件执行前后都有相应的钩子,分别是pre
钩子和post
钩子,pre
钩子会调用batchedUpdate
方法将isBatchingUpdates
变量置为true
,开启批量更新,而post
钩子会将isBatchingUpdates
置为false
isBatchingUpdates
变量置为true
,则会走批量更新分支,setState
的更新会被存入队列中,待同步代码执行完后,再执行队列中的state
更新。 isBatchingUpdates
为 true
,则把当前组件(即调用了 setState
的组件)放入 dirtyComponents
数组中;否则 batchUpdate
所有队列中的更新pre
钩子,或者生命周期的中的异步操作之前执行了pre
钩子,但是pos
钩子也在异步操作之前执行完了,isBatchingUpdates
必定为false
,也就不会进行批量更新
enqueueUpdate
包含了React
避免重复render
的逻辑。mountComponent
和updateComponent
方法在执行的最开始,会调用到batchedUpdates
进行批处理更新,此时会将isBatchingUpdates
设置为true
,也就是将状态标记为现在正处于更新阶段了。isBatchingUpdates
为true
,则把当前组件(即调用了setState
的组件)放入dirtyComponents
数组中;否则batchUpdate
所有队列中的更新
1.2 为什么直接修改this.state无效
setState
本质是通过一个队列机制实现state
更新的。 执行setState
时,会将需要更新的state合并后放入状态队列,而不会立刻更新state
,队列机制可以批量更新state
。setState
而直接修改this.state
,那么这个state
不会放入状态队列中,下次调用setState
时对状态队列进行合并时,会忽略之前直接被修改的state
,这样我们就无法合并了,而且实际也没有把你想要的state
更新上去1.3 什么是批量更新 Batch Update
在一些
mv*
框架中,,就是将一段时间内对model
的修改批量更新到view
的机制。比如那前端比较火的React
、vue
(nextTick
机制,视图的更新以及实现)
1.4 setState之后发生的事情
setState
操作并不保证是同步的,也可以认为是异步的React
在setState
之后,会经对state
进行diff
,判断是否有改变,然后去diff dom
决定是否要更新UI
。如果这一系列过程立刻发生在每一个setState
之后,就可能会有性能问题setState
。React
会将state
的改变压入栈中,在合适的时机,批量更新state
和视图,达到提高性能的效果1.5 如何知道state已经被更新
传入回调函数
setState({
index: 1
}}, function(){
console.log(this.state.index);
})
在钩子函数中体现
componentDidUpdate(){
console.log(this.state.index);
}
2. setState循环调用风险
setState
时,实际上会执行enqueueSetState
方法,并对partialState
以及_pending-StateQueue
更新队列进行合并操作,最终通过enqueueUpdate
执行state
更新performUpdateIfNecessary
方法会获取_pendingElement
,_pendingStateQueue
,_pending-ForceUpdate
,并调用receiveComponent
和updateComponent
方法进行组件更新shouldComponentUpdate
或者componentWillUpdate
方法中调用setState
,此时this._pending-StateQueue != null
,就会造成循环调用,使得浏览器内存占满后崩溃3 事务
wrapper
封装起来,再通过事务提供的perform
方法执行,先执行wrapper
中的initialize
方法,执行完perform
之后,在执行所有的close
方法,一组initialize
及close
方法称为一个wrapper
。setState
方法的不同表现有什么关系,首先我们把4
次setStat
e简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout
中的两次setState
属于另一类setState
调用之前,已经处在batchedUpdates
执行的事务中了。那么这次batchedUpdates
方法是谁调用的呢,原来是ReactMount.js
中的_renderNewRootComponent
方法。也就是说,整个将React
组件渲染到DOM
中的过程就是处于一个大的事务中。而在componentDidMount
中调用setState
时,batchingStrategy
的isBatchingUpdates
已经被设为了true
,所以两次setState
的结果没有立即生效setTimeout
中的两次setState
,因为没有前置的batchedUpdates
调用,所以导致了新的state
马上生效4. 总结
setState
去更新this.state
,不要直接操作this.state
,请把它当成不可变的setState
更新this.state
不是马上生效的,它是异步的,所以不要天真以为执行完setState
后this.state
就是最新的值了setState
不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理----------@----------
渲染和更新过程
JSX本质和vdom
createElement
函数patch(elem,vnode)
和patch(vnode,newNode)
组件渲染过程
props state
render()
生成vnode
patch(elem, vnode)
组件更新过程
setState-->dirtyComponents
(可能有子组件)render
生成newVnode
patch(vnode, newVnode)
----------@----------
React 16
为分界线,分为 Stack Reconciler
和 Fiber Reconciler
。这里的协调从狭义上来讲,特指 React 的 diff 算法,广义上来讲,有时候也指 React 的 reconciler
模块,它通常包含了 diff
算法和一些公共逻辑。Stack Reconciler
中,Stack Reconciler
的核心调度方式是递归
。调度的基本处理单位是事务
,它的事务基类是 Transaction
,这里的事务是 React 团队从后端开发中加入的概念
。在 React 16 以前,挂载主要通过 ReactMount 模块完成
,更新通过 ReactUpdate
模块完成,模块之间相互分离,落脚执行点也是事务。React 16
及以后,协调改为了 Fiber Reconciler
。它的调度方式主要有两个特点,第一个是协作式多任务模式
,在这个模式下,线程会定时放弃自己的运行权利,交还给主线程,通过requestIdleCallback
实现。第二个特点是策略优先级
,调度任务通过标记 tag
的方式分优先级执行,比如动画,或者标记为 high
的任务可以优先执行。Fiber Reconciler
的基本单位是 Fiber
,Fiber
基于过去的 React Element
提供了二次封装,提供了指向父、子、兄弟节点的引用,为 diff
工作的双链表实现提供了基础。Render 和 Commit 两个阶段
。Render 阶段的执行特点是可中断、可停止、无副作用
,主要是通过构造 workInProgress
树计算出 diff
。以 current
树为基础,将每个 Fiber
作为一个基本单位,自下而上逐个节点检查并构造 workInProgress 树。这个过程不再是递归,而是基于循环来完成requestIdleCallback
来调度执行每组任务,每组中的每个计算任务被称为 work
,每个 work
完成后确认是否有优先级更高的 work
需要插入,如果有就让位,没有就继续。优先级通常是标记为动画或者 high
的会先处理。每完成一组后,将调度权交回主线程,直到下一次 requestIdleCallback
调用,再继续构建 workInProgress
树commit
阶段需要处理 effect
列表,这里的 effect
列表包含了根据 diff 更新 DOM 树
、回调生命周期
、响应 ref
等。componentDidMount
、componentDidUpdate
、componentWiilUnmount
中去执行重度消耗算力的任务Stack Reconciler
的设计会占用占主线程,造成卡顿,而 fiber reconciler
的设计则能带来高性能的表现----------@----------
每一种节点类型有自己的属性,也就是prop,每次进行diff的时候,react会先比较该节点类型,假如节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较prop是否有更新,假如有prop不一样,那么react会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点
key
属性,方便比较。React
只会匹配相同 class
的 component
(这里面的class
指的是组件的名字)component
的 setState
方法的时候, React
将其标记为 - dirty
.到每一个事件循环结束, React
检查所有标记 dirty
的 component
重新绘制.shouldComponentUpdate
提高diff
的性能优化⬇️
为了降低算法复杂度,
React
的diff
会预设三个限制:
Diff
。如果一个DOM节点
在前后两次更新中跨越了层级,那么React
不会尝试复用他。div
变为p
,React会销毁div
及其子孙节点,并新建p
及其子孙节点。key prop
来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:Diff的思路
该如何设计算法呢?如果让我设计一个Diff算法
,我首先想到的方案是:
新增
,执行新增逻辑删除
,执行删除逻辑更新
,执行更新逻辑React团队
发现,在日常开发中,相较于新增
和删除
,更新
组件发生的频率更高。所以Diff
会优先判断当前节点是否属于更新
。基于以上原因,Diff算法
的整体逻辑会经历两轮遍历:
更新
的节点。更新
的节点。diff算法的作用
计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。
传统diff算法
通过循环递归对节点进行依次对比,算法复杂度达到
O(n^3)
,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。
React的diff算法
将Virtual DOM树转换成actual DOM树的最少操作的过程 称为 调和 。
diff
算法是调和的具体实现。
diff策略
React用 三大策略 将O(n^3)复杂度 转化为 O(n)复杂度
策略一(tree diff):
策略二(component diff):
策略三(element diff):
对于同一层级的一组子节点,通过唯一id区分。
tree diff
那么问题来了,如果DOM节点出现了跨层级操作,diff会咋办呢?
答:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。
如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点
component diff
React对不同的组件间的比较,有三种策略
shouldComponentUpdate()
来判断是否需要 判断计算。dirty component
(脏组件),从而替换 整个组件的所有节点。注意:如果组件D和组件G的结构相似,但是 React判断是 不同类型的组件,则不会比较其结构,而是删除 组件D及其子节点,创建组件G及其子节点。
element diff
当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。
总结
tree diff
:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。
这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。
component diff
:如果不是同一类型的组件,会删除旧的组件,创建新的组件element diff
:对于同一层级的一组子节点,需要通过唯一 id 进行来区分diff的不足与待优化的地方
尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能
与其他框架相比,React 的 diff 算法有何不同?
diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新
具体的流程是这样的:
在回答有何不同之前,首先需要说明下什么是 diff 算法。
diff 算法是指生成更新补丁的方式
,主要应用于虚拟 DOM 树变化后,更新真实 DOM
。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁
将单一节点比对转化为了 3 种类型节点的比对
,分别是树、组件及元素
,以此提升效率。
树比对
:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。组件比对
:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。元素比对
:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。同一层级的子节点,可以通过标记 key 的方式进行列表对比。自 React 16 起,引入了 Fiber 架构
。为了使整个更新过程可随时暂停恢复
,节点与树分别采用了 FiberNode 与 FiberTree 进行重构
。fiberNode 使用了双链表的结构
,可以直接找到兄弟节点与子节点Preact
的 Diff
算法相较于 React
,整体设计思路相似,但最底层的元素采用了真实 DOM
对比操作,也没有采用 Fiber
设计。Vue 的 Diff
算法整体也与 React
相似,同样未实现 Fiber
设计React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力
,在大批量节点更新的极端情况下,拥有更友好的交互体验。diff 策略与 React 对齐
,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。**学习原理的目的就是应用。那如何根据 React diff 算法原理优化代码呢?**这个问题其实按优化方式逆向回答即可。
diff
算法的设计原则,应尽量避免跨层级节点移动。key
进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。shouldComponentUpdate
或者 React.pureComponet
减少 diff
次数。----------@----------
为了解决跨浏览器兼容性问题,
React
会将浏览器原生事件(Browser Native Event
)封装为合成事件(SyntheticEvent
)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React
并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样React
在更新DOM
的时候就不需要考虑如何去处理附着在DOM
上的事件监听器,最终达到优化性能的目的
React 17 之前的事件冒泡流程图
所以这就造成了,在一个页面中,只能有一个版本的 React。如果有多个版本,事件就乱套了。值得一提的是,这个问题在 React 17 中得到了解决,事件委托不再挂在 document 上,而是挂在 DOM 容器上,也就是
ReactDom.Render
所调用的节点上。
React 17 后的事件冒泡流程图
那到底哪些事件会被捕获生成合成事件呢?可以从 React 的源码测试文件中一探究竟。下面的测试快照中罗列了大量的事件名,也只有在这份快照中的事件,才会被捕获生成合成事件。
// react/packages/react-dom/src/__tests__/__snapshots__/ReactTestUtils-test.js.snap
Array [
"abort",
"animationEnd",
"animationIteration",
"animationStart",
"auxClick",
"beforeInput",
"blur",
"canPlay",
"canPlayThrough",
"cancel",
"change",
"click",
"close",
"compositionEnd",
"compositionStart",
"compositionUpdate",
"contextMenu",
"copy",
"cut",
"doubleClick",
"drag",
"dragEnd",
"dragEnter",
"dragExit",
"dragLeave",
"dragOver",
"dragStart",
"drop",
"durationChange",
"emptied",
"encrypted",
"ended",
"error",
"focus",
"gotPointerCapture",
"input",
"invalid",
"keyDown",
"keyPress",
"keyUp",
"load",
"loadStart",
"loadedData",
"loadedMetadata",
"lostPointerCapture",
"mouseDown",
"mouseEnter",
"mouseLeave",
"mouseMove",
"mouseOut",
"mouseOver",
"mouseUp",
"paste",
"pause",
"play",
"playing",
"pointerCancel",
"pointerDown",
"pointerEnter",
"pointerLeave",
"pointerMove",
"pointerOut",
"pointerOver",
"pointerUp",
"progress",
"rateChange",
"reset",
"scroll",
"seeked",
"seeking",
"select",
"stalled",
"submit",
"suspend",
"timeUpdate",
"toggle",
"touchCancel",
"touchEnd",
"touchMove",
"touchStart",
"transitionEnd",
"volumeChange",
"waiting",
"wheel",
]
如果DOM上绑定了过多的事件处理函数,整个页面响应以及内存占用可能都会受到影响。React为了避免这类DOM事件滥用,同时屏蔽底层不同浏览器之间的事件系统的差异,实现了一个中间层 - SyntheticEvent
为何要合成事件
----------@----------
JSX是语法糖,通过babel转成
React.createElement
函数,在babel官网上可以在线把JSX转成React的JS语法
createElement
函数vnode
script标签中不添加
text/babel
解析jsx语法的情况下
<script>
const ele = React.createElement("h2", null, "Hello React!");
ReactDOM.render(ele, document.getElementById("app"));
script>
JSX的本质是React.createElement()函数
createElement
函数返回的对象是ReactEelement
对象。
createElement
的写法如下
class App extends React.Component {
constructor() {
super()
this.state = {}
}
render() {
return React.createElement("div", null,
/*第一个子元素,header*/
React.createElement("div", { className: "header" },
React.createElement("h1", { title: "\u6807\u9898" }, "\u6211\u662F\u6807\u9898")
),
/*第二个子元素,content*/
React.createElement("div", { className: "content" },
React.createElement("h2", null, "\u6211\u662F\u9875\u9762\u7684\u5185\u5BB9"),
React.createElement("button", null, "\u6309\u94AE"),
React.createElement("button", null, "+1"),
React.createElement("a", { href: "http://www.baidu.com" },
"\u767E\u5EA6\u4E00\u4E0B")
),
/*第三个子元素,footer*/
React.createElement("div", { className: "footer" },
React.createElement("p", null, "\u6211\u662F\u5C3E\u90E8\u7684\u5185\u5BB9")
)
);
}
}
ReactDOM.render(<App />, document.getElementById("app"));
实际开发中不会使用createElement
来创建ReactElement
的,一般都是使用JSX的形式开发。
ReactElement
在程序中打印一下
render() {
let ele = (
<div>
<div className="header">
<h1 title="标题">我是标题</h1>
</div>
<div className="content">
<h2>我是页面的内容</h2>
<button>按钮</button>
<button>+1</button>
<a href="http://www.baidu.com">百度一下</a>
</div>
<div className="footer">
<p>我是尾部的内容</p>
</div>
</div>
)
console.log(ele);
return ele;
}
react通过babel把JSX转成
createElement
函数,生成ReactElement
对象,然后通过ReactDOM.render函
数把ReactElement
渲染成真实的DOM
元素
为什么 React 使用 JSX
JavaScript
的语法扩展,结构类似 XML。React
元素,但 React 中并不强制使用 JSX
。即使使用了 JSX
,也会在构建过程中,通过 Babel 插件编译为 React.createElement
。所以 JSX 更像是 React.createElement
的一种语法糖Babel 插件如何实现 JSX 到 JS 的编译? 在 React 面试中,这个问题很容易被追问,也经常被要求手写。
它的实现原理是这样的。Babel 读取代码并解析,生成 AST,再将 AST 传入插件层进行转换,在转换时就可以将 JSX 的结构转换为 React.createElement
的函数。如下代码所示:
module.exports = function (babel) {
var t = babel.types;
return {
name: "custom-jsx-plugin",
visitor: {
JSXElement(path) {
var openingElement = path.node.openingElement;
var tagName = openingElement.name.name;
var args = [];
args.push(t.stringLiteral(tagName));
var attribs = t.nullLiteral();
args.push(attribs);
var reactIdentifier = t.identifier("React"); //object
var createElementIdentifier = t.identifier("createElement");
var callee = t.memberExpression(reactIdentifier, createElementIdentifier)
var callExpression = t.callExpression(callee, args);
callExpression.arguments = callExpression.arguments.concat(path.node.children);
path.replaceWith(callExpression, path.node);
},
},
};
};
React.createElement源码分析
/**
101. React的创建元素方法
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 最后返回一个调用ReactElement执行方法,并传入刚才处理过的参数
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
入参解读:创造一个元素需要知道哪些信息
export function createElement(type, config, children)
createElement 有 3 个入参,这 3 个入参囊括了 React 创建一个元素所需要知道的全部信息。
type
:用于标识节点的类型。它可以是类似“h1”“div”这样的标准 HTML 标签字符串,也可以是 React 组件类型或 React fragment
类型。config
:以对象形式传入,组件所有的属性都会以键值对的形式存储在 config 对象中。children
:以对象形式传入,它记录的是组件标签之间嵌套的内容,也就是所谓的“子节点”“子元素”React.createElement("ul", {
// 传入属性键值对
className: "list"
// 从第三个入参开始往后,传入的参数都是 children
}, React.createElement("li", {
key: "1"
}, "1"), React.createElement("li", {
key: "2"
}, "2"));
这个调用对应的 DOM 结构如下:
<ul className="list">
<li key="1">1li>
<li key="2">2li>
ul>
createElement 函数体拆解
createElement 中并没有十分复杂的涉及算法或真实 DOM 的逻辑,它的每一个步骤几乎都是在格式化数据。
现在看来,
createElement
原来只是个“参数中介”。此时我们的注意力自然而然地就聚焦在了ReactElement
上
出参解读:初识虚拟 DOM
createElement
执行到最后会 return 一个针对 ReactElement 的调用。这里关于 ReactElement,我依然先给出源码 + 注释形式的解析
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// REACT_ELEMENT_TYPE是一个常量,用来标识该对象是一个ReactElement
$$typeof: REACT_ELEMENT_TYPE,
// 内置属性赋值
type: type,
key: key,
ref: ref,
props: props,
// 记录创造该元素的组件
_owner: owner,
};
//
if (__DEV__) {
// 这里是一些针对 __DEV__ 环境下的处理,对于大家理解主要逻辑意义不大,此处我直接省略掉,以免混淆视听
}
return element;
};
ReactElement
其实只做了一件事情,那就是“创建”,说得更精确一点,是“组装”:ReactElement
把传入的参数按照一定的规范,“组装”进了element
对象里,并把它返回给了eact.createElement
,最终React.createElement
又把它交回到了开发者手中
const AppJSX = (<div className="App">
<h1 className="title">I am the title</h1>
<p className="content">I am the content</p>
</div>)
console.log(AppJSX)
你会发现它确实是一个标准的 ReactElement
对象实例
这个 ReactElement
对象实例,本质上是以 JavaScript 对象形式存在的对 DOM
的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM
中的一个节点)
----------@----------
目的是为了防止 XSS 攻击。因为 Synbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。
// 服务端允许用户存储 JSON
let expectedTextButGotJSON = {
type: 'div',
props: {
dangerouslySetInnerHTML: {
__html: '/* 把你想的搁着 */'
},
},
// ...
};
let message = { text: expectedTextButGotJSON };
// React 0.13 中有风险
<p>
{message.text}
</p>
----------@----------
通过 JS 对象模拟 DOM 的节点
。在 Facebook 构建 React 初期时,考虑到要提升代码抽象能力、避免人为的 DOM 操作、降低代码整体风险等因素,所以引入了虚拟 DOMPlain Object
,以 React 为例,在 render
函数中写的 JSX
会在 Babel
插件的作用下,编译为 React.createElement
执行 JSX
中的属性参数React.createElement
执行后会返回一个 Plain Object
,它会描述自己的 tag
类型、props
属性以及 children
情况等。这些 Plain Object
通过树形结构组成一棵虚拟 DOM
树。当状态发生变更时,将变更前后的虚拟 DOM
树进行差异比较,这个过程称为 diff
,生成的结果称为 patch
。计算之后,会渲染 Patch
完成对真实 DOM
的操作。改善大规模
DOM操作的性能
、规避 XSS 风险
、能以较低的成本实现跨平台开发
。DOM
除了渲染页面,虚拟 DOM 还有哪些应用场景?
这个问题考验面试者的想象力。通常而言,我们只是将虚拟 DOM 与渲染绑定在一起,但实际上虚拟 DOM 的应用更为广阔。比如,只要你记录了真实 DOM 变更,它甚至可以应用于埋点统计与数据记录等。
SSR原理
借助虚拟dom,服务器中没有dom概念的,react巧妙的借助虚拟dom,然后可以在服务器中nodejs可以运行起来react代码。
----------@----------
类组件中的优化手段
PureComponent
作为基类。shouldComponentUpdate
生命周期函数来自定义渲染逻辑。方法组件中的优化手段
React.memo
高阶函数包装组件,React.memo
可以实现类似于 shouldComponentUpdate
或者 PureComponent
的效果useMemo
React.useMemo
精细化的管控,useMemo 控制的则是是否需要重复执行某一段逻辑
,而React.memo 控制是否需要重渲染一个组件
useCallBack
。其他方式
Suspense
和 lazy 进行懒加载,例如:import React, { lazy, Suspense } from "react";
export default class CallingLazyComponents extends React.Component {
render() {
var ComponentToLazyLoad = null;
if (this.props.name == "Mayank") {
ComponentToLazyLoad = lazy(() => import("./mayankComponent"));
} else if (this.props.name == "Anshul") {
ComponentToLazyLoad = lazy(() => import("./anshulComponent"));
}
return (
<div>
<h1>This is the Base User: {this.state.name}</h1>
<Suspense fallback={<div>Loading...</div>}>
<ComponentToLazyLoad />
</Suspense>
</div>
)
}
}
----------@----------
在 Redux 的整个工作过程中,数据流是严格单向的
。这一点一定一定要背下来,面试的时候也一定一定要记得说
为什么要用redux
在
React
中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props
),所以,两个非父子组件之间通信就相对麻烦,redux
的出现就是为了解决state
里面的数据问题
Redux设计理念
Redux
是将整个应用状态存储到一个地方上称为store
,里面保存着一个状态树store tree
,组件可以派发(dispatch
)行为(action
)给store
,而不是直接通知其他组件,组件内部通过订阅store
中的状态state
来刷新自己的视图
如果你想对数据进行修改,
只有一种途径:派发 action
。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。
Redux三大原则
整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中
state
是只读的,唯一改变state
的方法就是触发action
,action
是一个用于描述以发生时间的普通对象
使用纯函数来执行修改,为了描述
action
如何改变state
的,你需要编写reducers
从编码的角度理解 Redux 工作流
createStore 来完成 store 对象的创建
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
reducer,
initial_state,
applyMiddleware(middleware1, middleware2, ...)
);
createStore 方法是一切的开始,它接收三个入参:
reducer;
初始状态内容;
指定中间件
reducer 的作用是将新的 state 返回给 store
一个 reducer 一定是一个纯函数,它可以有各种各样的内在逻辑,但它最终一定要返回一个 state:
const reducer = (state, action) => {
// 此处是各种样的 state处理逻辑
return new_state
}
当我们基于某个 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则:
// 更新规则全都写在 reducer 里
const store = createStore(reducer)
要想让 state 发生改变,就必须用正确的 action 来驱动这个改变。
const action = {
type: "ADD_ITEM",
payload: 'text '
}
action 对象中允许传入的属性有多个,但只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”。
action 本身只是一个对象,要想让 reducer 感知到 action,还需要“派发 action”这个动作,这个动作是由 store.dispatch 完成的
。这里我简单地示范一下:
import { createStore } from 'redux'
// 创建 reducer
const reducer = (state, action) => {
// 此处是各种样的 state处理逻辑
return new_state
}
// 基于 reducer 创建 state
const store = createStore(reducer)
// 创建一个 action,这个 action 用 “ADD_ITEM” 来标识
const action = {
type: "ADD_ITEM",
payload: 'text '
}
// 使用 dispatch 派发 action,action 会进入到 reducer 里触发对应的更新
store.dispatch(action)
以上这段代码,是从编码角度对 Redux 主要工作流的概括,这里我同样为你总结了一张对应的流程图:
Redux源码
let createStore = (reducer) => {
let state;
//获取状态对象
//存放所有的监听函数
let listeners = [];
let getState = () => state;
//提供一个方法供外部调用派发action
let dispath = (action) => {
//调用管理员reducer得到新的state
state = reducer(state, action);
//执行所有的监听函数
listeners.forEach((l) => l())
}
//订阅状态变化事件,当状态改变发生之后执行监听函数
let subscribe = (listener) => {
listeners.push(listener);
}
dispath();
return {
getState,
dispath,
subscribe
}
}
let combineReducers=(renducers)=>{
//传入一个renducers管理组,返回的是一个renducer
return function(state={},action={}){
let newState={};
for(var attr in renducers){
newState[attr]=renducers[attr](state[attr],action)
}
return newState;
}
}
export {createStore,combineReducers};
聊聊 Redux 和 Vuex 的设计思想
首先两者都是处理全局状态的工具库,大致实现思想都是:全局
state
保存状态---->dispatch(action)
------>reducer
(vuex
里的mutation
)----> 生成newState
; 整个状态为同步操作;
最大的区别在于处理异步的不同,vuex里面多了一步
commit
操作,在action
之后commit(mutation)
之前处理异步,而redux
里面则是通过中间件处理
redux 中间件
中间件提供第三方插件的模式,自定义拦截 action -> reducer 的过程。变为 action -> middlewares -> reducer 。这种机制可以让我们改变数据流,实现如异步 action ,action 过 滤,日志输出,异常报告等功能
常见的中间件:
redux-logger
:提供日志输出;redux-thunk
:处理异步操作;redux-promise
: 处理异步操作;actionCreator
的返回值是 promise
redux中间件的原理是什么
applyMiddleware
为什么会出现中间件?
function applyMiddleware(middlewares) {
middlewares = middlewares.slice()
middlewares.reverse()
let dispatch = store.dispatch
middlewares.forEach(middleware =>
dispatch = middleware(store)(dispatch)
)
return Object.assign({}, store, { dispatch })
}
上面的
middleware(store)(dispatch)
就相当于是const logger = store => next => {}
,这就是构造后的dispatch,继续向下传递。这里middlewares.reverse()
,进行数组反转的原因,是最后构造的dispatch
,实际上是最先执行的。因为在applyMiddleware
串联的时候,每个中间件只是返回一个新的dispatch
函数给下一个中间件,实际上这个dispatch
并不会执行。只有当我们在程序中通过store.dispatch(action)
,真正派发的时候,才会执行。而此时的dispatch
是最后一个中间件返回的包装函数。然后依次向前递推执行。
action、store、reducer分析
redux的核心概念就是store、action、reducer,从调用关系来看如下所示
store.dispatch(action) --> reducer(state, action) --> final state
// reducer方法, 传入的参数有两个
// state: 当前的state
// action: 当前触发的行为, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
switch (action.type) {
case 'add_todo':
return state.concat(action.text);
default:
return state;
}
};
// 创建store, 传入两个参数
// 参数1: reducer 用来修改state
// 参数2(可选): [], 默认的state值,如果不传, 则为undefined
var store = redux.createStore(reducer, []);
// 通过 store.getState() 可以获取当前store的状态(state)
// 默认的值是 createStore 传入的第二个参数
console.log('state is: ' + store.getState()); // state is:
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '读书'});
// 打印出修改后的state
console.log('state is: ' + store.getState()); // state is: 读书
store.dispatch({type: 'add_todo', text: '写作'});
console.log('state is: ' + store.getState()); // state is: 读书,写作
store
store
在这里代表的是数据模型,内部维护了一个state
变量store
有两个核心方法,分别是getState
、dispatch
。前者用来获取store
的状态(state
),后者用来修改store
的状态// 创建store, 传入两个参数
// 参数1: reducer 用来修改state
// 参数2(可选): [], 默认的state值,如果不传, 则为undefined
var store = redux.createStore(reducer, []);
// 通过 store.getState() 可以获取当前store的状态(state)
// 默认的值是 createStore 传入的第二个参数
console.log('state is: ' + store.getState()); // state is:
// 通过 store.dispatch(action) 来达到修改 state 的目的
// 注意: 在redux里,唯一能够修改state的方法,就是通过 store.dispatch(action)
store.dispatch({type: 'add_todo', text: '读书'});
action
redux
里是一个普通的js
对象action
必须有一个type
字段来标识这个行为的类型{type:'add_todo', text:'读书'}
{type:'add_todo', text:'写作'}
{type:'add_todo', text:'睡觉', time:'晚上'}
reducer
store
的状态。传入两个参数 state
、action
state
为当前的状态(可通过store.getState()
获得),而action
为当前触发的行为(通过store.dispatch(action)
调用触发)reducer(state, action)
返回的值,就是store
最新的state
值// reducer方法, 传入的参数有两个
// state: 当前的state
// action: 当前触发的行为, {type: 'xx'}
// 返回值: 新的state
var reducer = function(state, action){
switch (action.type) {
case 'add_todo':
return state.concat(action.text);
default:
return state;
}
};
actionCreator
actionCreator(args) => action
var addTodo = function(text){
return {
type: 'add_todo',
text: text
};
};
addTodo('睡觉'); // 返回:{type: 'add_todo', text: '睡觉'}
异步Action及操作
Action
是数据从应用传递到store
/state
的载体,也是开启一次完成数据流的开始
普通的action对象
const action = {
type:'ADD_TODO',
name:'poetries'
}
dispatch(action)
封装action creator
function actionCreator(data){
return {
type:'ADD_TODO',
data:data
}
}
dispatch(actionCreator('poetries'))
bindActionCreators合并
function a(name,id){
reurn {
type:'a',
name,
id
}
}
function b(name,id){
reurn {
type:'b',
name,
id
}
}
let actions = Redux.bindActionCreators({a,b},store.dispatch)
//调用
actions.a('poetries','id001')
actions.b('jing','id002')
action创建的标准
在Flux的架构中,一个Action要符合 FSA(Flux Standard Action) 规范,需要满足如下条件
type
、payload
、error
和 meta
中的一个或者多个属性。type
字段不可缺省,其它字段可缺省Action
报错,error
字段不可缺省,切必须为 true
payload
是一个对象,用作Action携带数据的载体
标准action示例
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
{
type: 'ADD_TODO',
payload: new Error(),
error: true
}
https://github.com/acdlite/flux-standard-action
Action
是否符合FSA标准// every有一个匹配不到返回false
let isFSA = Object.keys(action).every((item)=>{
return ['payload','type','error','meta'].indexOf(item) > -1
})
最简单的方式就是使用同步的方式来异步,将原来同步时一个
action
拆分成多个异步的action
的,在异步开始前、异步请求中、异步正常返回(异常)操作分别使用同步的操作,从而模拟出一个异步操作了。这样的方式是比较麻烦的,现在已经有redux-saga
等插件来解决这些问题了
异步action的实现方式一:setTimeout
redux-thunk
中间处理解析
function thunkAction(data) {
reutrn (dispatch)=>{
setTimeout(function(){
dispatch({
type:'ADD_TODO',
data
})
},3000)
}
}
异步action的实现方式二:promise实现异步action
redux-promise
中间处理这种action
function promiseAction(name){
return new Promise((resolve,reject) => {
setTimeout((param)=>{
resolve({
type:'ADD_TODO',
name
})
},3000)
}).then((param)=>{
dispatch(action("action2"))
return;
}).then((param)=>{
dispatch(action("action3"))
})
}
首先发起一个action,然后通过中间件,这里为什么要用中间件呢,因为这样dispatch
的返回值才能是一个函数。
通过store.dispatch
,将状态的的改变传给store
的小弟reducer
,reducer
根据action
的改变,传递新的状态state
。
最后将所有的改变告诉给它的大哥,store
。store
保存着所有的数据,并将数据注入到组件的顶部,这样组件就可以获得它需要的数据了
redux-thunk
Redux
本身只能处理同步的Action
,但可以通过中间件来拦截处理其它类型的action
,比如函数(Thunk
),再用回调触发普通Action
,从而实现异步处理
action
其实是被中间件捕获的,函数类型的action就被middleware
捕获。至于怎么定义异步的action
要看你用哪个中间件,根据他们的实例来定义,这样才会正确解析action
Redux
本身不处理异步行为,需要依赖中间件。结合redux-actions
使用,Redux
有两个推荐的异步中间件
redux-thunk
redux-promise
redux-thunk
的源码如下
function createThunkMiddleware(extraArgument) {
return ({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState, extraArgument);
}
return next(action);
};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
源码可知,
action creator
需要返回一个函数给redux-thunk
进行调用,示例如下
export let addTodoWithThunk = (val) => async (dispatch, getState)=>{
//请求之前的一些处理
let value = await Promise.resolve(val + ' thunk');
dispatch({
type:CONSTANT.ADD_TO_DO_THUNK,
payload:{
value
}
});
};
//action types
const GET_DATA = 'GET_DATA',
GET_DATA_SUCCESS = 'GET_DATA_SUCCESS',
GET_DATA_FAILED = 'GET_DATA_FAILED';
//action creator
const getDataAction = (id) => (dispatch, getState) => {
dispatch({
type: GET_DATA,
payload: id
})
api.getData(id) //注:本文所有示例的api.getData都返回promise对象
.then(response => {
dispatch({
type: GET_DATA_SUCCESS,
payload: response
})
})
.catch(error => {
dispatch({
type: GET_DATA_FAILED,
payload: error
})
})
}
}
//reducer
const reducer = (oldState, action) => {
switch(action.type) {
case GET_DATA :
return oldState;
case GET_DATA_SUCCESS :
return successState;
case GET_DATA_FAILED :
return errorState;
}
}
这已经是最简单的场景了,请注意:我们甚至还没写一行业务逻辑,如果每个异步处理都像这样,重复且无意义的工作会变成明显的阻碍
GET_DATA_SUCCESS
、GET_DATA_FAILED
这样的字符串声明也非常无趣且易错 上例中,GET_DATA
这个action
并不是多数场景需要的redux-promise
由于
redux-thunk
写起来实在是太麻烦了,社区当然会有其它轮子出现。redux-promise
则是其中比较知名的
middleware
,当检测到有action
的payload
属性是Promise
对象时,就会
resolve
,触发一个此action
的拷贝,但payload
为promise
的value
,并设status
属性为"success"
reject
,触发一个此action
的拷贝,但payload
为promise
的reason
,并设status
属性为"error"
//action types
const GET_DATA = 'GET_DATA';
//action creator
const getData = function(id) {
return {
type: GET_DATA,
payload: api.getData(id) //payload为promise对象
}
}
//reducer
function reducer(oldState, action) {
switch(action.type) {
case GET_DATA:
if (action.status === 'success') {
return successState
} else {
return errorState
}
}
}
redux-promise
为了精简而做出的妥协非常明显:无法处理乐观更新
场景解析之:乐观更新
多数异步场景都是悲观更新的,即等到请求成功才渲染数据。而与之相对的乐观更新,则是不等待请求成功,在发送请求的同时立即渲染数据
redux-thunk
的例子中,我们看到了GET_DATA
, GET_DATA_SUCCESS
、GET_DATA_FAILED
三个action
,分别表示初始动作、异步成功和异步失败,其中第一个action
使得redux-thunk
具备乐观更新的能力redux-promise
中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux
认可的action
对象是 plain JavaScript objects
,即简单对象,而在redux-promise
中,初始action
的payload
是个Promise
redux-promise-middleware
redux-promise-middleware
相比redux-promise
,采取了更为温和和渐进式的思路,保留了和redux-thunk
类似的三个action
//action types
const GET_DATA = 'GET_DATA',
GET_DATA_PENDING = 'GET_DATA_PENDING',
GET_DATA_FULFILLED = 'GET_DATA_FULFILLED',
GET_DATA_REJECTED = 'GET_DATA_REJECTED';
//action creator
const getData = function(id) {
return {
type: GET_DATA,
payload: {
promise: api.getData(id),
data: id
}
}
}
//reducer
const reducer = function(oldState, action) {
switch(action.type) {
case GET_DATA_PENDING :
return oldState; // 可通过action.payload.data获取id
case GET_DATA_FULFILLED :
return successState;
case GET_DATA_REJECTED :
return errorState;
}
}
action/index.js
import fetch from 'isomorphic-fetch'
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
//获取新闻成功的action
function receivePosts(reddit, json) {
return {
type: RECEIVE_POSTS,
reddit: reddit,
posts: json.data.children.map(child =>child.data)
}
}
function fetchPosts(subreddit) {
return function (dispatch) {
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json =>
dispatch(receivePosts(subreddit, json))
)
}
}
//如果需要则开始获取文章
export function fetchPostsIfNeeded(subreddit) {
return (dispatch, getState) => {
return dispatch(fetchPosts(subreddit))
}
}
fetchPostsIfNeeded
这里就是一个中间件。redux-thunk
会拦截fetchPostsIfNeeded
这个action
,会先发起数据请求,如果成功,就将数据传给action
从而到达reducer
那里
reducers/index.js
import { combineReducers } from 'redux'
import {
RECEIVE_POSTS
} from '../actions'
function posts(state = {
items: []
}, action) {
switch (action.type) {
case RECEIVE_POSTS:
// Object.assign是ES6的一个语法。合并对象,将对象合并为一个,前后相同的话,后者覆盖强者。详情可以看这里
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
return Object.assign({}, state, {
items: action.posts //数据都存在了这里
})
default:
return state
}
}
// 将所有的reducer结合为一个,传给store
const rootReducer = combineReducers({
postsByReddit
})
export default rootReducer
这个跟正常的
reducer
差不多。判断action
的类型,从而根据action
的不同类型,返回不同的数据。这里将数据存储在了items
这里。这里的reducer
只有一个。最后结合成rootReducer
,传给store
store/configureStore.js
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import rootReducer from '../reducers'
const createStoreWithMiddleware = applyMiddleware(
thunkMiddleware,
createLogger()
)(createStore)
export default function configureStore(initialState) {
const store = createStoreWithMiddleware(rootReducer, initialState)
if (module.hot) {
// Enable Webpack hot module replacement for reducers
module.hot.accept('../reducers', () => {
const nextRootReducer = require('../reducers')
store.replaceReducer(nextRootReducer)
})
}
return store
}
dispatch
机制中引入 Redux Thunk middleware
的呢? 我们使用了applyMiddleware()
middleware
,action creator
除了返回 action
对象外还可以返回函数action creator
就成为了 thunk
界面上的调用:在containers/App.js
//初始化渲染后触发
componentDidMount() {
const { dispatch} = this.props
// 这里可以传两个值,一个是 reactjs 一个是 frontend
dispatch(fetchPostsIfNeeded('frontend'))
}
改变状态的时候也是需要通过
dispatch
来传递的
provider
,将store
里面的数据注入给组件。让顶级组件提供给他们的子孙组件调用。代码如下:import 'babel-core/polyfill'
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './containers/App'
import configureStore from './store/configureStore'
const store = configureStore()
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
这样就完成了
redux
的异步操作。其实最主要的区别还是action
里面还有中间件的调用,其他的地方基本跟同步的redux
差不多的。搞懂了中间件,就基本搞懂了redux
的异步操作
----------@----------
Dispatcher
、 Store
、View
、Action
。Store
存储了视图层所有的数据,当 Store
变化后会引起 View 层的更新。如果在视图层触发一个 Action
,就会使当前的页面数据值发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层,Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,触发 View 层更新。Flux 的优点是单向数据流,解决了 MVC 中数据流向不清的问题
,使开发者可以快速了解应用行为。从项目结构上简化了视图层设计,明确了分工,数据与业务逻辑也统一存放管理,使在大型架构的项目中更容易管理、维护代码。其次是 Redux
,Redux 本身是一个 JavaScript 状态容器,提供可预测化状态的管理。社区通常认为 Redux 是 Flux 的一个简化设计版本,它提供的状态管理,简化了一些高级特性的实现成本,比如撤销、重做、实时编辑、时间旅行、服务端同构等。单一数据源、纯函数 Reducer、State 是只读的
。Dispatch
的时候会有一个 middleware 中间件层
,拦截分发的 Action 并添加额外的复杂行为
,还可以添加副作用。第一类方案的流行框架有 Redux-thunk、Redux-Promise、Redux-Observable、Redux-Saga
等。Reducer
层中直接处理副作用,采取该方案的有 React Loop
,React Loop
在实现中采用了 Elm 中分形的思想,使代码具备更强的组合能力。rematch 或 dva
,提供了更详细的模块架构能力,提供了拓展插件以支持更多功能。Action
触发的方式,可以在调试器中使用时间回溯,定位问题更简单快捷;最后是 Mobx
,Mobx 通过监听数据的属性变化,可以直接在数据上更改触发UI 的渲染。在使用上更接近 Vue,比起 Flux 与 Redux
的手动挡的体验,更像开自动挡的汽车。Mobx 的响应式实现原理与 Vue 相同
,以 Mobx 5
为分界点,5 以前采用 Object.defineProperty
的方案,5 及以后使用 Proxy
的方案。它的优点是样板代码少、简单粗暴、用户学习快、响应式自动更新数据
让开发者的心智负担更低。----------@----------
1. connect用法
作用:连接
React
组件与Redux store
connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])
// 这个函数允许我们将 store 中的数据作为 props 绑定到组件上
const mapStateToProps = (state) => {
return {
count: state.count
}
}
Redux
的 store
,我们从中摘取了 count
属性。你不必将 state
中的数据原封不动地传入组件,可以根据 state
中的数据,动态地输出组件需要的(最小)属性ownProps
,是组件自己的 props
当
state
变化,或者ownProps
变化的时候,mapStateToProps
都会被调用,计算出一个新的stateProps
,(在与ownProps merge
后)更新给组件
mapDispatchToProps(dispatch, ownProps): dispatchProps
connect
的第二个参数是mapDispatchToProps
,它的功能是,将action
作为props
绑定到组件上,也会成为MyComp
的 `props
2. 原理解析
首先
connect
之所以会成功,是因为Provider
组件
Provider
的子组件Redux
的store
作为props
,通过context
对象传递给子孙组件上的connect
connect做了些什么
它真正连接
Redux
和React
,它包在我们的容器组件的外一层,它接收上面Provider
提供的store
里面的state
和dispatch
,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件
3. 源码
connect
是一个高阶函数,首先传入mapStateToProps
、mapDispatchToProps
,然后返回一个生产Component
的函数(wrapWithConnect
),然后再将真正的Component
作为参数传入wrapWithConnect
,这样就生产出一个经过包裹的Connect
组件,该组件具有如下特点
props.store
获取祖先Component
的store props
包括stateProps
、dispatchProps
、parentProps
,合并在一起得到nextState
,作为props
传给真正的Component
componentDidMount
时,添加事件this.store.subscribe(this.handleChange)
,实现页面交互shouldComponentUpdate
时判断是否有避免进行渲染,提升页面性能,并得到nextState
componentWillUnmount
时移除注册的事件this.handleChange
// 主要逻辑
export default function connect(mapStateToProps, mapDispatchToProps, mergeProps, options = {}) {
return function wrapWithConnect(WrappedComponent) {
class Connect extends Component {
constructor(props, context) {
// 从祖先Component处获得store
this.store = props.store || context.store
this.stateProps = computeStateProps(this.store, props)
this.dispatchProps = computeDispatchProps(this.store, props)
this.state = { storeState: null }
// 对stateProps、dispatchProps、parentProps进行合并
this.updateState()
}
shouldComponentUpdate(nextProps, nextState) {
// 进行判断,当数据发生改变时,Component重新渲染
if (propsChanged || mapStateProducedChange || dispatchPropsChanged) {
this.updateState(nextProps)
return true
}
}
componentDidMount() {
// 改变Component的state
this.store.subscribe(() = {
this.setState({
storeState: this.store.getState()
})
})
}
render() {
// 生成包裹组件Connect
return (
<WrappedComponent {...this.nextState} />
)
}
}
Connect.contextTypes = {
store: storeShape
}
return Connect;
}
}
----------@----------
React 中通常使用 类定义 或者 函数定义 创建组件:
在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。
函数组件与类组件的对比:无关“优劣”,只谈“不同”
但是类组件它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的
react hooks的好处:
跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;
类定义更为复杂
不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;
时刻需要关注this的指向问题;
代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;
注意:
重要钩子
// useState 只接受一个参数: 初始状态
// 返回的是组件名和更改该组件对应的函数
const [flag, setFlag] = useState(true);
// 修改状态
setFlag(false)
// 上面的代码映射到类定义中:
this.state = {
flag: true
}
const flag = this.state.flag
const setFlag = (bool) => {
this.setState({
flag: bool,
})
}
类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。
useEffect(callback, [source])接受两个参数
useEffect(() => {
// 组件挂载后执行事件绑定
console.log('on')
addEventListener()
// 组件 update 时会执行事件解绑
return () => {
console.log('off')
removeEventListener()
}
}, [source]);
// 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):
// --- DidMount ---
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- DidUpdate ---
// 'off'
// 'on'
// --- WillUnmount ---
// 'off'
通过第二个参数,我们便可模拟出几个常用的生命周期:
const useMount = (fn) => useEffect(fn, [])
const useUnmount = (fn) => useEffect(() => fn, [])
const useMounted = () => {
const [mounted, setMounted] = useState(false);
useEffect(() => {
!mounted && setMounted(true);
return () => setMounted(false);
}, []);
return mounted;
}
const mounted = useMounted()
useEffect(() => {
mounted && fn()
})
useContext
: 获取 context 对象
useReducer
: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:
useCallback
: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;
useMemo
: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;
useRef
: 获取组件的真实节点;
useLayoutEffect
function useTitle(title) {
useEffect(
() => {
document.title = title;
});
}
// 使用:
function Home() {
const title = '我是首页'
useTitle(title)
return (
<div>{title}</div>
)
}
React Hooks 的限制
循环、条件
或嵌套函数中调用 Hook
;Hook
那为什么会有这样的限制呢?就得从 Hooks 的设计说起。Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。
render props
及状态管理框架。这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。
那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现
。在调用时按顺序加入数组中
,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表
。
这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。
useEffect 与 useLayoutEffect 区别在哪里
mountEffectImpl
,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用。useEffect
在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而 LayoutEffect
会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect
做计算量较大的耗时任务从而造成阻塞。useEffect
,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect
即可。----------@----------
<FInput value = {x} onChange = {fn} />
// 上面的是受控组件 下面的是非受控组件
<FInput defaultValue = {x} />
----------@----------
一般而言,ajax请求的数据都放在redux中存取。
----------@----------
context如何运用
在跨层级通信中,主要分为一层或多层的情况
父组件向子组件通信
,子组件向父组件通信
以及平级的兄弟组件间互相通信
。Props
即可。那么场景体现在容器组件与展示组件之间,通过 Props
传递 state
,让展示组件受控。通过 React 的 ref API 获取子组件的实例
,然后是通过实例调用子组件的实例函数
。这种方式在过去常见于 Modal 框的显示与隐藏Context API
,最常见的用途是做语言包国际化----------@----------
shouldComponentUpdate
阻断渲染来提升性能,而函数组件依靠 React.memo
缓存渲染结果来提升性能。----------@----------
React 组件应从设计与工程实践
两个方向进行探讨
从设计上而言,社区主流分类的方案是展示组件与灵巧组件
展示组件内部没有状态管理,仅仅用于最简单的展示表达
。展示组件中最基础的一类组件称作代理组件。代理组件常用于封装常用属性、减少重复代码。很经典的场景就是引入 Antd 的 Button 时,你再自己封一层。如果未来需要替换掉 Antd 或者需要在所有的 Button 上添加一个属性,都会非常方便。基于代理组件的思想还可以继续分类,分为样式组件与布局组件两种,分别是将样式与布局内聚在自己组件内部。----------@----------
为什么要进行组件的协同
什么是组件的协同使用?
如何实现组件的协同使用
React
中的Mixin
,更多的是实现代码的复用组件嵌套的含义
组件嵌套的优缺点
Mixin
Mixin的含义
Mixin=一组方法
。Mixin的优缺点
Mixin
来开发自己的代码Mixin
可能被用在各种环境中,想要兼容多种环境就需要更多的 - 码与逻辑,通用的代价是提高复杂度Mixin
本质上会分散逻辑,理解起来难度大不可控组件
defaultValue
的值是固定的,这就是一个不可控组件input
的value
值,只有使用ref
获取节点来获取值可控组件
defaultValue
的值是根据状态确定了,只需要拿到this.state.value
的值就可以了value
的值是不可修改的,defaultValue
的值是可以修改的可控组件的优点
React
的数据流state
中,便于获取----------@----------
React Router
路由的基础实现原理分为两种,如果是切换 Hash
的方式,那么依靠浏览器 Hash
变化即可;如果是切换网址中的 Path
,就要用到 HTML5 History API
中的 pushState
、replaceState
等。在使用这个方式时,还需要在服务端完成 historyApiFallback
配置React Router
内部主要依靠 history
库完成,这是由 React Router
自己封装的库,为了实现跨平台运行的特性,内部提供两套基础 history
,一套是直接使用浏览器的 History API
,用于支持 react-router-dom
;另一套是基于内存实现的版本,这是自己做的一个数组,用于支持 react-router-native
。React Router
的工作方式可以分为设计模式与关键模块两个部分。从设计模式的角度出发,在架构上通过 Monorepo
进行库的管理。Monorepo
具有团队间透明、迭代便利的优点。其次在整体的数据通信上使用了 Context API 完成上下文传递。第一类是 Context 容器
,比如 Router 与 MemoryRouter;第二类是消费者组件,用以匹配路由
,主要有 Route、Redirect、Switch 等;第三类是与平台关联的功能组件
,比如 Link、NavLink、DeepLinking
等。----------@----------
最重要的是以下三点:
JSX
转换逻辑Lane 模型
的引入1. 重构 JSX 转换逻辑
在过去,如果我们在 React 项目中写入下面这样的代码:
function MyComponent() {
return <p>这是我的组件</p>
}
React 是会报错的,原因是 React 中对 JSX 代码的转换依赖的是 React.createElement
这个函数。因此但凡我们在代码中包含了 JSX,那么就必须在文件中引入 React,像下面这样:
import React from 'react';
function MyComponent() {
return <p>这是我的组件</p>
}
而 React 17 则允许我们在不引入 React 的情况下直接使用 JSX
。这是因为在 React 17 中,编译器会自动帮我们引入 JSX 的解析器,也就是说像下面这样一段逻辑:
function MyComponent() {
return <p>这是我的组件</p>
}
会被编译器转换成这个样子:
import {jsx as _jsx} from 'react/jsx-runtime';
function MyComponent() {
return _jsx('p', { children: '这是我的组件' });
}
react/jsx-runtime
中的 JSX 解析器将取代 React.createElement
完成 JSX
的编译工作,这个过程对开发者而言是自动化、无感知的。因此,新的 JSX 转换逻辑带来的最显著的改变就是降低了开发者的学习成本。
react/jsx-runtime
中的 JSX 解析器看上去似乎在调用姿势上和 React.createElement
区别不大,那么它是否只是 React.createElement
换了个马甲呢?当然不是,它在内部实现了 React.createElement
无法做到的性能优化和简化。在一定情况下,它可能会略微改善编译输出内容的大小
2. 事件系统重构
事件系统在 React 17 中的重构要从以下两个方面来看:
2.1 卸掉历史包袱:放弃利用 document 来做事件的中心化管控
React 16.13.x 版本中的事件系统会通过将所有事件冒泡到 document 来实现对事件的中心化管控
这样的做法虽然看上去已经足够巧妙,但仍然有它不聪明的地方——document 是整个文档树的根节点,操作 document 带来的影响范围实在是太大了,这将会使事情变得更加不可控
在 React 17 中,React 团队终于正面解决了这个问题:事件的中心化管控不会再全部依赖
document
,管控相关的逻辑被转移到了每个 React 组件自己的容器 DOM 节点中。比如说我们在 ID 为 root 的 DOM 节点下挂载了一个 React 组件,像下面代码这样:
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
那么事件管控相关的逻辑就会被安装到 root 节点
上去。这样一来, React 组件就能够自己玩自己的,再也无法对全局的事件流构成威胁了
2.2 拥抱新的潮流:放弃事件池
在 React 17 之前,合成事件对象会被放进一个叫作“事件池”的地方统一管理。这样做的目的是能够实现事件对象的复用,进而提高性能:每当事件处理函数执行完毕后,其对应的合成事件对象内部的所有属性都会被置空,意在为下一次被复用做准备。这也就意味着事件逻辑一旦执行完毕,我们就拿不到事件对象了,React 官方给出的这个例子就很能说明问题,请看下面这个代码
function handleChange(e) {
// This won't work because the event object gets reused.
setTimeout(() => {
console.log(e.target.value); // Too late!
}, 100);
}
异步执行的
setTimeout
回调会在handleChange
这个事件处理函数执行完毕后执行,因此它拿不到想要的那个事件对象e
。
要想拿到目标事件对象,必须显式地告诉 React——我永远需要它,也就是调用 e.persist()
函数,像下面这样:
function handleChange(e) {
// Prevents React from resetting its properties:
e.persist();
setTimeout(() => {
console.log(e.target.value); // Works
}, 100);
}
在 React 17 中,我们不需要 e.persist()
,也可以随时随地访问我们想要的事件对象。
3. Lane 模型的引入
初学 React 源码的同学由此可能会很自然地认为:优先级就应该是用 Lane 来处理的
。但事实上,React 16 中处理优先级采用的是 expirationTime 模型
。
expirationTime
模型使用expirationTime
(一个时间长度) 来描述任务的优先级;而Lane 模型
则使用二进制数来表示任务的优先级
:
lane 模型
通过将不同优先级赋值给一个位,通过 31 位的位运算
来操作优先级。
Lane 模型
提供了一个新的优先级排序的思路,相对于 expirationTime
来说,它对优先级的处理会更细腻,能够覆盖更多的边界条件。
----------@----------
DNS
解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP
<link rel="dns-prefetch" href="//blog.poetries.top">
强缓存
实现强缓存可以通过两种响应头实现:
Expires
和Cache-Control
。强缓存表示在缓存期间不需要请求,state code
为200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires
是HTTP / 1.0
的产物,表示资源会在Wed, 22 Oct 2018 08:41:00 GMT
后过期,需要再次请求。并且Expires
受限于本地时间,如果修改了本地时间,可能会造成缓存失效
Cache-control: max-age=30
Cache-Control
出现于HTTP / 1.1
,优先级高于Expires
。该属性表示资源会在30
秒后过期,需要再次请求
协商缓存
Last-Modified
和 If-Modified-Since
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将 Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来Last-Modified
被修改,所以在 HTTP / 1.1
出现了 ETag
ETag
和 If-None-Match
ETag
类似于文件指纹,If-None-Match
会将当前 ETag
发送给服务器,询问该资源 ETag 是否变动,有变动的话就将新的资源发送回来。并且 ETag
优先级比 Last-Modified
高选择合适的缓存策略
对于大部分的场景都可以使用强缓存配合协商缓存解决,但是在一些特殊的地方可能需要选择特殊的缓存策略
Cache-control: no-store
,表示该资源不需要缓存Cache-Control: no-cache
并配合 ETag
使用,表示该资源已被缓存,但是每次都会发送请求询问资源是否更新。Cache-Control: max-age=31536000
并配合策略缓存使用,然后对文件进行指纹处理,一旦文件名变动就会立刻下载新的文件HTTP / 1.1
时代,每个请求都需要建立和断开,消耗了好几个 RTT
时间,并且由于 TCP
慢启动的原因,加载体积大的文件会需要更多的时间HTTP / 2.0
中引入了多路复用,能够让多个请求使用同一个 TCP
链接,极大的加快了网页的加载速度。并且还支持 Header
压缩,进一步的减少了请求的数据大小fetch
,强制浏览器请求资源,并且不会阻塞 onload
事件,可以使用以下代码开启预加载<link rel="preload" href="http://example.com">
预加载可以一定程度上降低首屏的加载时间,因为可以将一些不影响首屏但重要的文件延后加载,唯一缺点就是兼容性不好
可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
<link rel="prerender" href="http://poetries.com">
总结
defer
和 async
在网络读取的过程中都是异步解析defer
是有顺序依赖的,async
只要脚本加载完后就会执行preload
可以对当前页面所需的脚本、样式等资源进行预加载prefetch
加载的资源一般不是用于当前页面的,是未来很可能用到的这样一些资源懒执行
懒加载
懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的
src
属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为src
属性,这样图片就会去下载资源,实现了图片懒加载
图片优化
对于如何优化图片,有 2 个思路
图片加载优化
CSS
去代替。base64
格式WebP
格式的浏览器尽量使用 WebP
格式。因为 WebP
格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好PNG
,其实对于大部分图标这类图片,完全可以使用 SVG
代替JPEG
其他文件优化
CSS
文件放在 head
中script
标签放在 body
底部,因为 JS
文件执行会阻塞渲染。当然也可以把 script
标签放在任意位置然后加上 defer
,表示该文件会并行下载,但是会放到 HTML
解析完成后顺序执行。对于没有任何依赖的 JS
文件可以加上 async
,表示加载和渲染后续文档元素的过程将和 JS
文件的加载与执行并行无序进行。 执行 JS
代码过长会卡住渲染,对于需要很多时间计算的代码Webworker
。Webworker
可以让我们另开一个线程执行脚本而不影响渲染。CDN
静态资源尽量使用
CDN
加载,由于浏览器对于单个域名有并发请求上限,可以考虑使用多个CDN
域名。对于CDN
加载静态资源需要注意CDN
域名要与主站不同,否则每次请求都会带上主站的Cookie
使用 Webpack 优化项目
Webpack4
,打包项目使用 production
模式,这样会自动开启代码压缩ES6
模块来开启 tree shaking
,这个技术可以移除没有使用的代码base64
的方式写入文件中监控
对于代码运行错误,通常的办法是使用
window.onerror
拦截报错。该方法能拦截到大部分的详细报错信息,但是也有例外
Script error
. 对于这种情况我们需要给 script
标签添加 crossorigin
属性arguments.callee.caller
来做栈递归catch
的方式捕获错误。比如 Promise
可以直接使用 catch 函数,async await
可以使用 try catch
sourceMap
文件便于 debug
。img
标签的 src
发起一个请求性能优化API
Performance
。performance.now()
与new Date()
区别,它是高精度的,且是相对时间,相对于页面加载的那一刻。但是不一定适合单页面场景window.addEventListener("load", "");
window.addEventListener("domContentLoaded", "");
Img
的onload
事件,监听首屏内的图片是否加载完成,判断首屏事件RequestFrameAnmation
和 RequestIdleCallback
IntersectionObserver
、MutationObserver
,PostMessage
Web Worker
,耗时任务放在里面执行检测工具
Chrome Dev Tools
Page Speed
Jspref
前端指标
window.onload = function(){
setTimeout(function(){
let t = performance.timing
console.log('DNS查询耗时 :' + (t.domainLookupEnd - t.domainLookupStart).toFixed(0))
console.log('TCP链接耗时 :' + (t.connectEnd - t.connectStart).toFixed(0))
console.log('request请求耗时 :' + (t.responseEnd - t.responseStart).toFixed(0))
console.log('解析dom树耗时 :' + (t.domComplete - t.domInteractive).toFixed(0))
console.log('白屏时间 :' + (t.responseStart - t.navigationStart).toFixed(0))
console.log('domready时间 :' + (t.domContentLoadedEventEnd - t.navigationStart).toFixed(0))
console.log('onload时间 :' + (t.loadEventEnd - t.navigationStart).toFixed(0))
if(t = performance.memory){
console.log('js内存使用占比 :' + (t.usedJSHeapSize / t.totalJSHeapSize * 100).toFixed(2) + '%')
}
})
}
DNS预解析优化
dns解析是很耗时的,因此如果解析域名过多,会让首屏加载变得过慢,可以考虑dns-prefetch优化
DNS Prefetch
应该尽量的放在网页的前面,推荐放在 后面。具体使用方法如下:
<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.zhix.net">
<link rel="dns-prefetch" href="//api.share.zhix.net">
<link rel="dns-prefetch" href="//bdimg.share.zhix.net">
request请求耗时
1. 概述
PC
优化手段在Mobile
侧同样适用Mobile
侧我们提出三秒种渲染完成首屏指标3
秒完成或使用Loading
338KB/s(2.71Mb/s
),所以首屏资源不应超过1014KB
Mobile
侧因手机配置原因,除加载外渲染速度也是优化重点2. 加载优化
加载过程是最为耗时的过程,可能会占到总耗时的
80%
时间,因此是优化的重点
2.1 缓存
使用缓存可以减少向服务器的请求数,节省加载时间,所以所有静态资源都要在服务器端设置缓存,并且尽量使用长
Cache
(长Cache
资源的更新可使用时间戳)
2.2 压缩HTML、CSS、JavaScript
减少资源大小可以加快网页显示速度,所以要对
HTML
、CSS
、JavaScript
等进行代码压缩,并在服务器端设置GZip
GZip
2.3 无阻塞
写在
HTML
头部的JavaScript
(无异步),和写在HTML
标签中的Style
会阻塞页面的渲染,因此CSS
放在页面头部并使用Link
方式引入,避免在HTML
标签中写Style
,JavaScript
放在页面尾部或使用异步方式加载
2.4 使用首屏加载
首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化。
2.5 按需加载
将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量。
PS:按需加载会导致大量重绘,影响渲染性能
LazyLoad
Media Query
加载2.6 预加载
大型重资源页面(如游戏)可使用增加
Loading
的方法,资源加载完成后再显示页面。但Loading
时间过长,会造成用户流失。
对用户行为分析,可以在当前页加载下一页资源,提升速度。
Loading
Loading
(如提前加载下一页)2.7 压缩图片
图片是最占流量的资源,因此尽量避免使用他,使用时选择最合适的格式(实现需求的前提下,以大小判断),合适的大小,然后使用智图压缩,同时在代码中用
Srcset
来按需显示
PS:过度压缩图片大小影响图片显示效果
CSS3
2. 使用SVG
3. 使用IconFont
)Srcset
webP
优于JPG
2. PNG8
优于GIF
)1014KB
2. 不宽于640
(基于手机屏幕一般宽度))2.8 减少Cookie
Cookie
会影响加载速度,所以静态资源域名不使用Cookie
。
2.9 避免重定向
重定向会影响加载速度,所以在服务器正确设置避免重定向。
2.10 异步加载第三方资源
第三方资源不可控会影响页面的加载和显示,因此要异步加载第三方资源
2.11 减少HTTP请求
因为手机浏览器同时响应请求为4个请求(
Android
支持4个,iOS
5后可支持6个),所以要尽量减少页面的请求数,首次加载同时请求数不能超过4个
CSS
、JavaScript
3. 三、脚本执行优化
脚本处理不当会阻塞页面加载、渲染,因此在使用时需当注意
CSS
写在头部,JavaScript
写在尾部或异步iFrame
等的空Src
,空Src
会重新加载当前页面,影响速度和效率。CSS
、JavaScript
等中多次重置图片大小,多次重设图片大小会引发图片的多次重绘,影响性能DataURL
,DataURL
图片没有使用图片的压缩算法文件会变大,并且要解码后再渲染,加载慢耗时长4. CSS优化
尽量避免写在HTML标签中写
Style
属性
4.1 css3过渡动画开启硬件加速
.translate3d{
-webkit-transform: translate3d(0, 0, 0);
-moz-transform: translate3d(0, 0, 0);
-ms-transform: translate3d(0, 0, 0);
transform: translate3d(0, 0, 0);
}
4.2 避免CSS表达式
CSS表达式的执行需跳出CSS树的渲染,因此请避免CSS表达式。
4.3 不滥用Float
Float在渲染时计算量比较大,尽量减少使用
4.4 值为0时不需要任何单位
为了浏览器的兼容性和性能,值为0
时不要带单位
5. JavaScript执行优化
5.1 减少重绘和回流
Class
而不是Style
,使用classList
代替className
document.write
drawImage
5.2 TOUCH事件优化
使用
touchstart
、touchend
代替click
,因快影响速度快。但应注意Touch
响应过快,易引发误操作
6. 渲染优化
6.1 HTML使用Viewport
Viewport可以加速页面的渲染,请使用以下代码
<meta name=”viewport” content=”width=device-width, initial-scale=1″>
6.2 动画优化
CSS3
动画requestAnimationFrame
动画代替setTimeout
Canvas
动画 5
个元素以内使用css
动画,5
个以上使用Canvas
动画(iOS8
可使用webGL
)6.3 高频事件优化
Touchmove
、Scroll
事件可导致多次渲染
requestAnimationFrame
监听帧变化,使得在正确的时间进行渲染6.4 GPU加速
CSS
中以下属性(CSS3 transitions
、CSS3 3D transforms
、Opacity
、Canvas
、WebGL
、Video
)来触发GPU
渲染,请合理使用
----------@----------
核心概念
entry
:入口。webpack是基于模块的,使用webpack首先需要指定模块解析入口(entry),webpack从入口开始根据模块间依赖关系递归解析和处理所有资源文件。output
:输出。源代码经过webpack处理之后的最终产物。loader
:模块转换器。本质就是一个函数,在该函数中对接收到的内容进行转换,返回转换后的结果。因为 Webpack 只认识 JavaScript,所以 Loader 就成了翻译官,对其他类型的资源进行转译的预处理工作。plugin
:扩展插件。基于事件流框架 Tapable
,插件可以扩展 Webpack 的功能,在 Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果。module
:模块。除了js范畴内的es module、commonJs、AMD
等,css @import、url(...)
、图片、字体等在webpack中都被视为模块。解释几个 webpack 中的术语
module
:指在模块化编程中我们把应用程序分割成的独立功能的代码模块chunk
:指模块间按照引用关系组合成的代码块,一个 chunk
中可以包含多个 module
chunk group
:指通过配置入口点(entry point
)区分的块组,一个 chunk group
中可包含一到多个 chunkbundling
:webpack 打包的过程asset/bundle
:打包产物webpack 的打包思想可以简化为 3 点:
Loader
转换为 JS 模块 (module
),模块之间可以互相引用。entry point
)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle)
文件。chunk group
),在不考虑分包的情况下,一个 chunk group
中只有一个 chunk
,该 chunk 包含递归分析后的所有模块。每一个 chunk
都有对应的一个打包后的输出文件(asset/bundle
)打包流程
Compiler
对象,加载所有配置的插件,执行对象的 run
方法开始执行编译。entry
找出所有的入口文件。loader
对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。chunk
,再把每个 chunk
转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。简版
Compiler
对象;Compiler
对象开始编译整个项目;在以上过程中,
Webpack 会在特定的时间点广播出特定的事件
,插件在监听到相关事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果
构建流程核心概念:
Tapable
:一个基于发布订阅的事件流工具类,Compiler
和 Compilation
对象都继承于 Tapable
Compiler
:compiler对象是一个全局单例,他负责把控整个webpack打包的构建流程。在编译初始化阶段被创建的全局单例,包含完整配置信息、loaders
、plugins以及各种工具方法Compilation
:代表一次 webpack 构建和生成编译资源的的过程,在watch
模式下每一次文件变更触发的重新编译都会生成新的 Compilation
对象,包含了当前编译的模块 module
, 编译生成的资源,变化的文件, 依赖的状态等AST
语法树。每个模块文件在通过Loader解析完成之后,会通过acorn
库生成模块代码的AST语法树,通过语法树就可以分析这个模块是否还有依赖的模块,进而继续循环执行下一个模块的编译解析。最终Webpack
打包出来的bundle
文件是一个IIFE
的执行函数。
// webpack 5 打包的bundle文件内容
(() => { // webpackBootstrap
var __webpack_modules__ = ({
'file-A-path': ((modules) => { // ... })
'index-file-path': ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { // ... })
})
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})
webpack详细工作流程
常用 Loader:
file-loader
: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;url-loader
: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;babel-loader
: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;ts-loader
: 加载 ts / tsx 文件,编译 TypeScript;style-loader
: 将 css 代码以
标签的形式插入到 html 中;css-loader
: 分析@import和url(),引用 css 文件与对应的资源;postcss-loader
: 用于 css 的兼容性处理,具有众多功能,例如 添加前缀,单位转换 等;less-loader / sass-loader
: css预处理器,在 css 中新增了许多语法,提高了开发效率;编写原则:
----------@----------
插件系统是 Webpack 成功的一个关键性因素。在编译的整个生命周期中,Webpack 会触发许多事件钩子,Plugin 可以监听这些事件,根据需求在相应的时间点对打包内容进行定向的修改。
一个最简单的 plugin 是这样的:
class Plugin{
// 注册插件时,会调用 apply 方法
// apply 方法接收 compiler 对象
// 通过 compiler 上提供的 Api,可以对事件进行监听,执行相应的操作
apply(compiler){
// compilation 是监听每次编译循环
// 每次文件变化,都会生成新的 compilation 对象并触发该事件
compiler.plugin('compilation',function(compilation) {})
}
}
注册插件:
// webpack.config.js
module.export = {
plugins:[
new Plugin(options),
]
}
事件流机制:
Webpack 就像工厂中的一条产品流水线。原材料经过 Loader 与 Plugin 的一道道处理,最后输出结果。
Webpack 事件流编程范式的核心是基础类 Tapable,是一种 观察者模式 的实现事件的订阅与广播:
const { SyncHook } = require("tapable")
const hook = new SyncHook(['arg'])
// 订阅
hook.tap('event', (arg) => {
// 'event-hook'
console.log(arg)
})
// 广播
hook.call('event-hook')
Webpack
中两个最重要的类Compiler
与Compilation
便是继承于Tapable
,也拥有这样的事件流机制。
Compilation
: 可以称为 编译实例。当监听到文件发生改变时,Webpack 会创建一个新的 Compilation 对象,开始一次新的编译。它包含了当前的输入资源,输出资源,变化的文件等,同时通过它提供的 api,可以监听每次编译过程中触发的事件钩子;Compiler
全局唯一,且从启动生存到结束;Compilation
对应每次编译,每轮编译循环均会重新创建;apply
函数传入 compiler
对象compiler
对象监听事件loader和plugin有什么区别?
webapck默认只能打包JS和JOSN模块,要打包其它模块,需要借助loader,loader就可以让模块中的内容转化成webpack或其它laoder可以识别的内容。
loader
就是模块转换化,或叫加载器。不同的文件,需要不同的loader
来处理。plugin
是插件,可以参与到整个webpack打包的流程中,不同的插件,在合适的时机,可以做不同的事件。webpack中都有哪些插件,这些插件有什么作用?
html-webpack-plugin
自动创建一个HTML文件,并把打包好的JS插入到HTML文件中clean-webpack-plugin
在每一次打包之前,删除整个输出文件夹下所有的内容mini-css-extrcat-plugin
抽离CSS代码,放到一个单独的文件中optimize-css-assets-plugin
压缩css----------@----------
HMR 的基本流程图
webpack
;webpack
重新编译构建一个或多个模块,并通知 HMR 服务器进行更新;HMR Server
使用 webSocket
通知 HMR runtime
需要更新,HMR
运行时通过 HTTP
请求更新 jsonp
HMR
运行时替换更新中的模块,如果确定这些模块无法更新,则触发整个页面刷新----------@----------
优化前的准备工作
speed-measure-webpack-plugin
分析插件加载的时间webpack-bundle-analyzer
分析产物内容代码优化:
无用代码消除,是许多编程语言都具有的优化手段,这个过程称为 DCE (dead code elimination),即 删除不可能执行的代码;
例如我们的 UglifyJs
,它就会帮我们在生产环境中删除不可能被执行的代码,例如:
var fn = function() {
return 1;
// 下面代码便属于 不可能执行的代码;
// 通过 UglifyJs (Webpack4+ 已内置) 便会进行 DCE;
var a = 1;
return a;
}
摇树优化 (Tree-shaking),这是一种形象比喻。我们把打包后的代码比喻成一棵树,这里其实表示的就是,通过工具 “摇” 我们打包后的 js 代码,将没有使用到的无用代码 “摇” 下来 (删除)。即 消除那些被 引用了但未被使用 的模块代码。
tree-shaking
webpack-deep-scope-plugin
,可以进行作用域分析,减少此类情况的发生,但仍需要注意;code-spliting: 代码分割技术 ,将代码分割成多份进行 懒加载 或 异步加载,避免打包成一份后导致体积过大,影响页面的首屏加载;
Webpack
中使用 SplitChunksPlugin
进行拆分;scope hoisting : 作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;
编译性能优化:
webpack
,能有效提升编译性能;dev-server
/ 模块热替换 (HMR
) 提升开发体验;
modules
: 指定模块路径,减少递归搜索;mainFields
: 指定入口文件描述字段,减少搜索;noParse
: 避免对非模块化文件的加载;includes/exclude
: 指定搜索范围/排除不必要的搜索范围;alias
: 缓存目录,避免重复寻址;babel-loader
node_moudles
,避免编译第三方库中已经被编译过的代码cacheDirectory
,可以缓存编译结果,避免多次重复编译webpack-parallel-uglify-plugin
: 可多进程并发压缩 js 文件,提高压缩速度;HappyPack
: 多进程并发文件的 Loader
解析;DLLPlugin
和 DLLReferencePlugin
可以提前进行打包并缓存,避免每次都重新编译;Webpack Analyse / webpack-bundle-analyzer
对打包后的文件进行分析,寻找可优化的地方source-map
:
cheap-module-eval-source-map
hidden-source-map
;优化webpack打包速度
loader
的 test
,include & exclude
Webpack4
默认压缩并行Happypack
并发调用babel
也可以缓存编译Resolve
在构建时指定查找模块文件的规则DllPlugin
,不用每次都重新构建externals
和 DllPlugin
解决的是同一类问题:将依赖的框架等模块从构建过程中移除。它们的区别在于
externals
更简单,而 DllPlugin
需要独立的配置文件。DllPlugin
包含了依赖包的独立构建流程,而 externals
配置中不包含依赖框架的生成方式,通常使用已传入 CDN 的依赖包externals
配置的依赖包需要单独指定依赖模块的加载方式:全局对象、CommonJS、AMD 等DllPlugin
无须更改,而 externals
则会将子模块打入项目包中优化打包体积
UglifyJsPlugin
gzip
压缩require.ensure
devtool
中的source-map
css
文件,单独打包Tree Shaking
在构建打包过程中,移除那些引入但未被使用的无效代码scope hosting
----------@----------
对tree-shaking的了解
作用:
它表示在打包的时候会去除一些无用的代码
原理 :
ES6
的模块引入是静态分析的,所以在编译时能正确判断到底加载了哪些模块特点:
babel
编译全部模块被封装成IIFE
,它存在副作用无法被tree-shaking
掉package.json
中配置sideEffects
来指定哪些文件是有副作用的。它有两种值,一个是布尔类型,如果是false
则表示所有文件都没有副作用;如果是一个数组的话,数组里的文件路径表示改文件有副作用rollup
和webpack
中对tree-shaking
的层度不同,例如对babel
转译后的class
,如果babel
的转译是宽松模式下的话(也就是loose
为true
),webpack
依旧会认为它有副作用不会tree-shaking
掉,而rollup
会。这是因为rollup
有程序流分析的功能,可以更好的判断代码是否真正会产生副作用。原理
ES6 Module
引入进行静态分析,故而编译的时候正确判断到底加载了那些模块依赖于
import/export
通过导入所有的包后再进行条件获取。如下:
import foo from "foo";
import bar from "bar";
if(condition) {
// foo.xxxx
} else {
// bar.xxx
}
ES6的import语法完美可以使用tree shaking,因为可以在代码不运行的情况下就能分析出不需要的代码
CommonJS的动态特性模块意味着tree shaking不适用 。因为它是不可能确定哪些模块实际运行之前是需要的或者是不需要的。在ES6中,进入了完全静态的导入语法:import。这也意味着下面的导入是不可行的:
// 不可行,ES6 的import是完全静态的
if(condition) {
myDynamicModule = require("foo");
} else {
myDynamicModule = require("bar");
}
----------@----------
作用域提升,将分散的模块划分到同一个作用域中,避免了代码的重复引入,有效减少打包后的代码体积和运行时的内存损耗;
----------@----------
1. 是什么
webpack proxy
,即webpack
提供的代理服务
基本行为就是接收客户端发送的请求后转发给其他服务器
其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)
想要实现代理首先需要一个中间服务器,webpack
中提供服务器的工具为webpack-dev-server
2. webpack-dev-server
webpack-dev-server
是 webpack
官方推出的一款开发工具,将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起
目的是为了提高开发者日常的开发效率,「只适用在开发阶段」
关于配置方面,在webpack
配置对象属性中通过devServer
属性提供,如下:
// ./webpack.config.js
const path = require('path')
module.exports = {
// ...
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
proxy: {
'/api': {
target: 'https://api.github.com'
}
}
// ...
}
}
devServetr
里面proxy
则是关于代理的配置,该属性为对象的形式,对象中每一个属性就是一个代理的规则匹配
属性的名称是需要被代理的请求路径前缀,一般为了辨别都会设置前缀为/api
,值为对应的代理匹配规则,对应如下:
target
:表示的是代理到的目标地址pathRewrite
:默认情况下,我们的 /api-hy
也会被写入到URL中,如果希望删除,可以使用pathRewrite
secure
:默认情况下不接收转发到https
的服务器上,如果希望支持,可以设置为false
changeOrigin
:它表示是否更新代理后请求的 headers
中host
地址2. 工作原理
proxy
工作原理实质上是利用http-proxy-middleware
这个http
代理中间件,实现请求转发给其他服务器
举个例子:
在开发阶段,本地地址为http://localhost:3000
,该浏览器发送一个前缀带有/api
标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true}));
app.listen(3000);
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar
3. 跨域
在开发阶段,
webpack-dev-server
会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在localhost
的一个端口上,而后端服务又是运行在另外一个地址上
所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题
通过设置webpack proxy
实现代理请求后,相当于浏览器与服务端中添加一个代理者
当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地
在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据
注意:
「服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制」
----------@----------
babel
的编译过程分为三个阶段: parsing 、 transforming 、 generating ,以 ES6 编译为 ES5 作为例子:
ES6
代码输入;babylon
进行解析得到 AST;plugin
用 babel-traverse
对 AST
树进行遍历编译,得到新的 AST
树;babel-generator
通过 AST
树生成 ES5
代码。----------@----------
Rollup 是一款 ES Modules 打包器。它也可以将项目中散落的细小模块打包为整块代码,从而使得这些划分的模块可以更好地运行在浏览器环境或者 Node.js 环境。
Rollup优势:
缺点
HMR
;Require.js
这样的 AMD
库
- 我们发现如果我们开发的是一个应用程序,需要大量引用第三方模块,同时还需要 HMR 提升开发体验,而且应用过大就必须要分包。那这些需求 Rollup 都无法满足。
- 如果我们是开发一个 JavaScript 框架或者库,那这些优点就特别有必要,而缺点呢几乎也都可以忽略,所以在很多像 React 或者 Vue 之类的框架中都是使用的 Rollup 作为模块打包器,而并非 Webpack
总结一下 :Webpack 大而全,Rollup 小而美
。
在对它们的选择上,我的基本原则是:应用开发使用 Webpack,类库或者框架开发使用 Rollup
。
不过这并不是绝对的标准,只是经验法则。因为 Rollup 也可用于构建绝大多数应用程序,而 Webpack 同样也可以构建类库或者框架。
----------@----------
GET
的重定向,但是不能确保 POST
会重定向为 POST
GET
的重定向307
和 302
一样,除了不允许 POST
到 GET
的重定向----------@----------
HTTP
协议始于三十年前蒂姆·伯纳斯 - 李的一篇论文HTTP/0.9
是个简单的文本协议,只能获取文本资源;HTTP/1.0
确立了大部分现在使用的技术,但它不是正式标准;HTTP/1.1
是目前互联网上使用最广泛的协议,功能也非常完善;HTTP/2
基于 Google 的 SPDY
协议,注重性能改善,但还未普及;HTTP/3
基于 Google 的 QUIC
协议,是将来的发展方向----------@----------
HTTP
协议传输;User Agent
;Apache
和 Nginx
;CDN
位于浏览器和服务器之间,主要起到缓存加速的作用;User Agent
,是自动访问网络资源的程序。TCP/IP
是网络世界最常用的协议,HTTP
通常运行在 TCP/IP
提供的可靠传输基础上DNS
域名是 IP
地址的等价替代,需要用域名解析实现到 IP
地址的映射;URI
是用来标记互联网上资源的一个名字,由“协议名 + 主机名 + 路径”构成,俗称 URL;HTTPS
相当于“HTTP+SSL/TLS+TCP/IP
”,为 HTTP
套了一个安全的外壳;HTTP
传输过程中的“中转站”,可以实现缓存加速、负载均衡等功能----------@----------
TCP/IP
里无对应;TCP/IP
的链接层;TCP/IP
的网际层;TCP/IP
的传输层;TCP/IP
的应用层总结
TCP/IP
分为四层,核心是二层的 IP
和三层的 TCP
,HTTP
在第四层;OSI
分为七层,基本对应 TCP/IP
,TCP
在第四层,HTTP
在第七层;OSI
可以映射到 TCP/IP
,但这期间一、五、六层消失了;OSI
模型,用四层、七层等术语;HTTP
利用 TCP/IP
协议栈逐层打包再拆包,实现了数据传输,但下面的细节并不可见有一个辨别四层和七层比较好的(但不是绝对的)小窍门,“两个凡是”:凡是由操作系统负责处理的就是四层或四层以下,否则,凡是需要由应用程序(也就是你自己写代码)负责处理的就是七层
----------@----------
HTTP 协议的请求报文和响应报文的结构基本相同,由三大部分组成
key-value
形式更详细地说明报文;这其中前两部分起始行和头部字段经常又合称为“请求头”或“响应头”,消息正文又称为“实体”,但与“header”对应,很多时候就直接称为“body”。
一个完整的 HTTP 报文就像是下图的这个样子,注意在 header 和 body 之间有一个“空行”
----------@----------
URI
是用来唯一标记服务器上资源的一个字符串,通常也称为 URL;URI
通常由 scheme
、host:port
、path
和 query
四个部分组成,有的可以省略;scheme
叫“方案名”或者“协议名”,表示资源应该使用哪种协议来访问;host:port
”表示资源所在的主机名和端口号;path
标记资源所在的位置;query
表示对资源附加的额外要求;URI
里对“@&/
”等特殊字符和汉字必须要做编码,否则服务器收到 HTTP
报文后会无法正确处理----------@----------
1. 数据类型与编码
text/html
了,表示超文本文档,此外还有纯文本 text/plain
、样式表 text/css
等。image
:即图像文件,有 image/gif
、image/jpeg
、image/png
等。audio/video
:音频和视频数据,例如 audio/mpeg
、video/mp4
等。application
:数据格式不固定,可能是文本也可能是二进制,必须由上层应用程序来解释。常见的有 application/json
,application/javascript
、application/pdf
等,另外,如果实在是不知道数据是什么类型,像刚才说的“黑盒”,就会是 application/octet-stream
,即不透明的二进制数据但仅有
MIME type
还不够,因为HTTP
在传输时为了节约带宽,有时候还会压缩数据,为了不要让浏览器继续“猜”,还需要有一个“Encoding type”,告诉数据是用的什么编码格式,这样对方才能正确解压缩,还原出原始的数据。
比起 MIME type
来说,Encoding type
就少了很多,常用的只有下面三种
gzip
:GNU zip
压缩格式,也是互联网上最流行的压缩格式;deflate
:zlib
(deflate
)压缩格式,流行程度仅次于 gzip
;br
:一种专门为 HTTP
优化的新压缩算法(Brotli
)2. 数据类型使用的头字段
有了 MIME type
和 Encoding type
,无论是浏览器还是服务器就都可以轻松识别出 body
的类型,也就能够正确处理数据了。
HTTP
协议为此定义了两个 Accept
请求头字段和两个 Content
实体头字段,用于客户端和服务器进行“内容协商”。也就是说,客户端用 Accept
头告诉服务器希望接收什么样的数据,而服务器用 Content
头告诉客户端实际发送了什么样的数据
Accept
字段标记的是客户端可理解的MIME
type,可以用“,”做分隔符列出多个类型,让服务器有更多的选择余地,例如下面的这个头:
Accept: text/html,application/xml,image/webp,image/png
这就是告诉服务器:“我能够看懂 HTML、XML 的文本,还有 webp
和 png
的图片,请给我这四类格式的数据”。
相应的,服务器会在响应报文里用头字段Content-Type
告诉实体数据的真实类型:
Content-Type: text/html
Content-Type: image/png
这样浏览器看到报文里的类型是“text/html”就知道是 HTML 文件,会调用排版引擎渲染出页面,看到“image/png”就知道是一个 PNG 文件,就会在页面上显示出图像。
Accept-Encoding字段标记的是客户端支持的压缩格式,例如上面说的 gzip、deflate 等,同样也可以用“,”列出多个,服务器可以选择其中一种来压缩数据,实际使用的压缩格式放在响应头字段Content-Encoding
里
Accept-Encoding: gzip, deflate, br
Content-Encoding: gzip
不过这两个字段是可以省略的,如果请求报文里没有 Accept-Encoding
字段,就表示客户端不支持压缩数据;如果响应报文里没有 Content-Encoding
字段,就表示响应数据没有被压缩
3. 语言类型使用的头字段
同样的,HTTP 协议也使用 Accept
请求头字段和 Content
实体头字段,用于客户端和服务器就语言与编码进行“内容协商”。
Accept-Language
字段标记了客户端可理解的自然语言,也允许用“,”做分隔符列出多个类型,例如:
Accept-Language: zh-CN, zh, en
这个请求头会告诉服务器:“最好给我 zh-CN
的汉语文字,如果没有就用其他的汉语方言,如果还没有就给英文”。
相应的,服务器应该在响应报文里用头字段Content-Language
告诉客户端实体数据使用的实际语言类型
Content-Language: zh-CN
HTTP
里使用的请求头字段是Accept-Charset
,但响应头里却没有对应的 Content-Charset
,而是在Content-Type
字段的数据类型后面用“charset=xxx
”来表示,这点需要特别注意。GBK
或 UTF-8
的字符集,然后服务器返回的是 UTF-8
编码,就是下面这样Accept-Charset: gbk, utf-8
Content-Type: text/html; charset=utf-8
不过现在的浏览器都支持多种字符集,通常不会发送 Accept-Charset
,而服务器也不会发送 Content-Language
,因为使用的语言完全可以由字符集推断出来,所以在请求头里一般只会有 Accept-Language
字段,响应头里只会有 Content-Type
字段
4. 内容协商的质量值
在 HTTP 协议里用 Accept
、Accept-Encoding
、Accept-Language
等请求头字段进行内容协商的时候,还可以用一种特殊的“q”参数表示权重来设定优先级,这里的“q”是“quality factor”的意思。
权重的最大值是 1,最小值是 0.01,默认值是 1,如果值是 0 就表示拒绝。具体的形式是在数据类型或语言代码后面加一个“;”,然后是“q=value”。
这里要提醒的是“;”的用法,在大多数编程语言里“;”的断句语气要强于“,”,而在 HTTP 的内容协商里却恰好反了过来,“;”的意义是小于“,”的。
例如下面的 Accept 字段:
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
它表示浏览器最希望使用的是 HTML 文件,权重是 1,其次是 XML 文件,权重是 0.9,最后是任意数据类型,权重是0.8。服务器收到请求头后,就会计算权重,再根据自己的实际情况优先输出 HTML 或者 XML
5. 内容协商的结果
内容协商的过程是不透明的,每个 Web 服务器使用的算法都不一样。但有的时候,服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段,给出一点信息,例如:
Vary: Accept-Encoding,User-Agent,Accept
这个 Vary
字段表示服务器依据了 Accept-Encoding
、User-Agent
和 Accept
这三个头字段,然后决定了发回的响应报文。
Vary
字段可以认为是响应报文的一个特殊的“版本标记”。每当 Accept
等请求头变化时,Vary
也会随着响应报文一起变化。也就是说,同一个 URI
可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务,这个之后讲“HTTP 缓存”时还会再提到
6. 小结
MIME type
,相关的头字段是 Accept
和 Content-Type
;Accept-Encoding
和 Content-Encoding
;Accept-Language
和 Content-Language
;Accept-Charset
和 Content-Type;Accept
等头字段与服务器进行“内容协商”,要求服务器返回最合适的数据; Accept
等头字段可以用“,”顺序列出多个可能的选项,还可以用“;q=
”参数来精确指定权重----------@----------
超文本传输协议, HTTP 是一个在计算机世界里专门在两点之间传输文字、图片、音频、视频等超文本数据的约定和规范 。
TCP
连接,当某个请求时间过长时,其他的请求只能处于阻塞状态,这就是队头阻塞问题。http 无状态无连接
http
协议对于事务处理没有记忆能力url
请求没有上下文关系http协议无状态中的 状态 到底指的是什么?!
cookie
和session
机制,现在的网络请求其实是有状态的http
协议下,服务器也一定会保留你每次网络请求对数据的修改,但这跟保留每次访问的数据是不一样的,保留的只是会话产生的结果,而没有保留会话----------@----------
http/1.1
规定了以下请求方法(注意,都是大写):
从应用场景角度来看,Get 多用于无副作用,幂等的场景,例如搜索关键字。Post 多用于副作用,不幂等的场景,例如注册。
options 方法有什么用
----------@----------
本质上,只是语义上的区别,GET 用于获取资源,POST 用于提交资源。
具体差别
----------@----------
什么是队头阻塞?
对于每一个HTTP请求而言,这些任务是会被放入一个任务队列中串行执行的,一旦队首任务请求太慢时,就会阻塞后面的请求处理,这就是HTTP队头阻塞
问题。
有什么解决办法吗
并发连接
我们知道对于一个域名而言,是允许分配多个长连接的,那么可以理解成增加了任务队列,也就是说不会导致一个任务阻塞了该任务队列的其他任务,在
RFC规范
中规定客户端最多并发2个连接,不过实际情况就是要比这个还要多,举个例子,Chrome中是6个。
域名分片
TianTian.com
,可以分出很多二级域名,比如Day1.TianTian.com
,Day2.TianTian.com
,Day3.TianTian.com
,这样子就可以有效解决队头阻塞问题。----------@----------
大概遇到的情况就分为定长数据 与 不定长数据的处理吧。
定长数据
对于定长的数据包而言,发送端在发送数据的过程中,需要设置Content-Length
,来指明发送数据的长度。
当然了如果采用了Gzip压缩的话,Content-Length设置的就是压缩后的传输长度。
我们还需要知道的是
Content-Length
如果存在并且有效的话,则必须和消息内容的传输长度完全一致,也就是说,如果过短就会截断,过长的话,就会导致超时。Content-Length
,若是非Keep-alive,跟前面情况一样,Content-Length可有可无。那怎么来设置Content-Length
举个例子来看看
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', 10);
res.write("你好,使用的是Content-Length设置传输数据形式");
}
})
server.listen(3000, () => {
console.log("成功启动--TinaTian");
})
不定长数据
现在采用最多的就是HTTP/1.1版本,来完成传输数据,在保存Keep-alive状态下,当数据是不定长的时候,我们需要设置新的头部字段
Transfer-Encoding: chunked
通过chunked机制,可以完成对不定长数据的处理,当然了,你需要知道的是
Transfer-Encoding
,优先采用Transfer-Encoding里面的方法来找到对应的长度。那我们来模拟一下吧
const server = require('http').createServer();
server.on('request', (req, res) => {
if(req.url === '/index') {
// 设置数据类型
res.setHeader('Content-Type', 'text/html; charset=utf8');
res.setHeader('Content-Length', 10);
res.setHeader('Transfer-Encoding', 'chunked');
res.write("你好,使用的是Transfer-Encoding设置传输数据形式");
setTimeout(() => {
res.write("第一次传输数据给您
");
}, 1000);
res.write("骚等一下");
setTimeout(() => {
res.write("第一次传输数据给您");
res.end()
}, 3000);
}
})
server.listen(3000, () => {
console.log("成功启动--TinaTian");
})
上面使用的是nodejs中
http
模块,有兴趣的小伙伴可以去试一试,以上就是HTTP对定长数据和不定长数据传输过程中的处理手段。
----------@----------
session
: 是一个抽象概念,开发者为了实现中断和继续等操作,将 user agent
和 server
之间一对一的交互,抽象为“会话”,进而衍生出“会话状态”,也就是 session
的概念cookie
:它是一个世纪存在的东西,http
协议中定义在 header
中的字段,可以认为是 session
的一种后端无状态实现现在我们常说的
session
,是为了绕开cookie
的各种限制,通常借助cookie
本身和后端存储实现的,一种更高级的会话状态实现
session
的常见实现要借助cookie
来发送 sessionID
----------@----------
HTTPS 要比 HTTPS 多了 secure 安全性这个概念,实际上, HTTPS 并不是一个新的应用层协议,它其实就是 HTTP + TLS/SSL 协议组合而成,而安全性的保证正是 SSL/TLS 所做的工作。
SSL
安全套接层(Secure Sockets Layer)
TLS
(传输层安全,Transport Layer Security)
现在主流的版本是 TLS/1.2, 之前的 TLS1.0、TLS1.1 都被认为是不安全的,在不久的将来会被完全淘汰。
HTTPS 就是身披了一层 SSL 的 HTTP 。
那么区别有哪些呢
我觉得记住以下两点HTTPS主要作用就行
HTTPS的缺点
----------@----------
总结
----------@----------
我们可以把HTTPS理解成HTTPS = HTTP + SSL/TLS
TLS/SSL 的功能实现主要依赖于三类基本算法:
散列函数
、对称加密
和非对称加密
,其利用非对称加密实现身份认证和密钥协商,对称加密算法采用协商的密钥对数据加密,基于散列函数验证信息的完整性。
1. 对称加密
加密和解密用同一个秘钥的加密方式叫做对称加密。Client客户端和Server端共用一套密钥,这样子的加密过程似乎很让人理解,但是随之会产生一些问题。
问题一: WWW万维网有许许多多的客户端,不可能都用秘钥A进行信息加密,这样子很不合理,所以解决办法就是使用一个客户端使用一个密钥进行加密。
问题二:既然不同的客户端使用不同的密钥,那么对称加密的密钥如何传输? 那么解决的办法只能是 一端生成一个秘钥,然后通过HTTP传输给另一端 ,那么这样子又会产生新的问题。
问题三: 这个传输密钥的过程,又如何保证加密?如果被中间人拦截,密钥也会被获取, 那么你会说对密钥再进行加密,那又怎么保存对密钥加密的过程,是加密的过程?
到这里,我们似乎想明白了,使用对称加密的方式,行不通,所以我们需要采用非对称加密
2. 非对称加密
通过上面的分析,对称加密的方式行不通,那么我们来梳理一下非对称加密。采用的算法是RSA,所以在一些文章中也会看见 传统RSA握手 ,基于现在TLS主流版本是1.2,所以接下来梳理的是 TLS/1.2握手过程 。
非对称加密中,我们需要明确的点是
3. 主要工作流程
梳理起来,可以把TLS 1.2 握手过程分为主要的五步
接下来,就可以通过该对称密钥对传输的信息加密/解密啦,从上面图举个例子
接下来考虑一个问题,如果公钥被中间人拿到纂改怎么办呢?
客户端可能拿到的公钥是假的,解决办法是什么呢?
3. 第三方认证
客户端无法识别传回公钥是中间人的,还是服务器的,这是问题的根本,我们是不是可以通过某种规范可以让客户端和服务器都遵循某种约定呢?那就是通过第三方认证的方式
在HTTPS中,通过 证书 + 数字签名来解决这个问题。
这里唯一不同的是,假设对网站信息加密的算法是MD5,通过MD5加密后, 然后通过第三方机构的私钥再次对其加密,生成数字签名 。
这样子的话,数字证书包含有两个特别重要的信息某网站公钥+数字签名
我们再次假设中间人截取到服务器的公钥后,去替换成自己的公钥,因为有数字签名的存在,这样子客户端验证发现数字签名不匹配,这样子就防止中间人替换公钥的问题。
那么客户端是如何去对比两者数字签名的呢?
4. 数字签名作用
数字签名:将网站的信息,通过特定的算法加密,比如MD5,加密之后,再通过服务器的私钥进行加密,形成 加密后的数字签名 。
第三方认证机构是一个公开的平台,中间人可以去获取。
如果没有数字签名的话,这样子可以就会有下面情况
从上面我们知道,如果只是对网站信息进行第三方机构私钥加密的话,还是会受到欺骗。
因为没有认证,所以中间人也向第三方认证机构进行申请,然后拦截后把所有的信息都替换成自己的,客户端仍然可以解密,并且无法判断这是服务器的还是中间人的,最后造成数据泄露。
5. 总结
----------@----------
一共有两种方法来恢复断开的 SSL 连接,一种是使用 session ID,一种是 session ticket。
通过session ID
使用 session ID 的方式,每一次的会话都有一个编号,当对话中断后,下一次重新连接时,只要客户端给出这个编号,服务器如果有这个编号的记录,那么双方就可以继续使用以前的秘钥,而不用重新生成一把。目前所有的浏览器都支持这一种方法。但是这种方法有一个缺点是,session ID 只能够存在一台服务器上,如果我们的请求通过负载平衡被转移到了其他的服务器上,那么就无法恢复对话。
通过session ticket
另一种方式是 session ticket 的方式,session ticket 是服务器在上一次对话中发送给客户的,这个 ticket 是加密的,只有服务器能够解密,里面包含了本次会话的信息,比如对话秘钥和加密方法等。这样不管我们的请求是否转移到其他的服务器上,当服务器将 ticket 解密以后,就能够获取上次对话的信息,就不用重新生成对话秘钥了。
----------@----------
首先补充一下,http 和 https 的区别,相比于 http,https 是基于 ssl 加密的 http 协议
简要概括:http2.0
是基于 1999 年发布的 http1.0
之后的首次更新
http1.1
中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限 制(连接数量),超过限制会被阻塞头部压缩
HTTP 1.1版本会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。
HTTP 2.0 使用 HPACK
算法进行压缩。
多路复用
HTTP2中:
Stream ID
,流标识符,有了它,接收方就能从乱序的二进制帧中选择ID相同的帧,按照顺序组装成请求/响应报文。服务器推送
浏览器发送一个请求,服务器主动向浏览器推送与这个请求相关的资源,这样浏览器就不用发起后续请求。
相比较http/1.1的优势
二进制分帧
之前是明文传输,不方便计算机解析,对于回车换行符来说到底是内容还是分隔符,都需要内部状态机去识别,这样子效率低,HTTP/2采用二进制格式,全部传输01串,便于机器解码。
这样子一个报文格式就被拆分为一个个二进制帧,用Headers帧存放头部字段,Data帧存放请求体数据。这样子的话,就是一堆乱序的二进制帧,它们不存在先后关系,因此不需要排队等待,解决了HTTP队头阻塞问题。
在客户端与服务器之间,双方都可以互相发送二进制帧,这样子 双向传输的序列 ,称为流
,所以HTTP/2中以流来表示一个TCP连接上进行多个数据帧的通信,这就是多路复用概念。
那乱序的二进制帧,是如何组装成对于的报文呢?
优先级
和流量控制
等功能,这样子的话,就可以设置数据帧的优先级,让服务器处理重要资源,优化用户体验。HTTP2的缺点
----------@----------
Google 在推SPDY的时候就已经意识到了这些问题,于是就另起炉灶搞了一个基于 UDP 协议的“QUIC”协议,让HTTP跑在QUIC上而不是TCP上。主要特性如下:
----------@----------
keep-alive
来设置Range
来实现。二进制分帧
这是一次彻底的二进制协议,头信息和数据体都是二进制,并且统称为"帧":头信息帧和数据帧。头部压缩
HTTP 1.1版本会出现 User-Agent、Cookie、Accept、Server、Range 等字段可能会占用几百甚至几千字节,而 Body 却经常只有几十字节,所以导致头部偏重。HTTP 2.0 使用 HPACK
算法进行压缩。多路复用
复用TCP连接,在一个连接里,客户端和浏览器都可以同时发送多个请求或回应,且不用按顺序一一对应,这样子解决了队头阻塞的问题。服务器推送
允许服务器未经请求,主动向客户端发送资源,即服务器推送。请求优先级
可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验。----------@----------
DNS 的作用就是通过域名查询到具体的 IP。DNS 协议提供的是一种主机名到 IP 地址的转换服务,就是我们常说的域名系统。是应用层协议,通常该协议运行在UDP协议之上,使用的是53端口号。
因为 IP 存在数字和英文的组合(IPv6),很不利于人类记忆,所以就出现了域名。你可以把域名看成是某个 IP 的别名,DNS 就是去查询这个别名的真正名称是什么。
当你在浏览器中想访问 www.google.com
时,会通过进行以下操作:
com
这个一级域名的服务器google.com
这个二级域名www.google.com
这个三级域名的地址我们通过一张图来看看它的查询过程吧
这张图很生动的展示了DNS在本地DNS服务器是如何查询的,一般向本地DNS服务器发送请求是递归查询的
本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程
递归查询和迭代查询
所以一般而言, 本地服务器查询是递归查询 ,而本地 DNS 服务器向其他域名服务器请求的过程是迭代查询的过程
DNS缓存
缓存也很好理解,在一个请求中,当某个DNS服务器收到一个DNS回答后,它能够回答中的信息缓存在本地存储器中。返回的资源记录中的 TTL 代表了该条记录的缓存的时间。
DNS实现负载平衡
它是如何实现负载均衡的呢?首先我们得清楚DNS 是可以用于在冗余的服务器上实现负载平衡。
原因: 这是因为一般的大型网站使用多台服务器提供服务,因此一个域名可能会对应 多个服务器地址。
举个例子来说
DNS 为什么使用 UDP 协议作为传输层协议?
DNS 使用 UDP 协议作为传输层协议的主要原因是为了避免使用 TCP 协议时造成的连接时延
总结
—>>
本地hosts文件 —>>
本地DNS解析器 —>>
本地DNS服务器 —>>
其他域名服务器请求。 接下来的过程就是迭代过程。----------@----------
1. 短轮询
短轮询的基本思路:
优缺点
2. 长轮询
长轮询的基本思路:
优缺点
3. WebSocket
----------@----------
正向代理
我们常说的代理也就是指正向代理,正向代理的过程,它隐藏了真实的请求客户端,服务端不知道真实的客户端是谁,客户端请求的服务都被代理服务器代替来请求。
反向代理
这种代理模式下,它隐藏了真实的服务端,当我们向一个网站发起请求的时候,背后可能有成千上万台服务器为我们服务,具体是哪一台,我们不清楚,我们只需要知道反向代理服务器是谁就行,而且反向代理服务器会帮我们把请求转发到真实的服务器那里去,一般而言反向代理服务器一般用来实现负载平衡。
负载平衡的两种实现方式?
----------@----------
什么是keep-alive
我们知道HTTP协议采用“请求-应答”模式,当使用普通模式,即非KeepAlive模式时,每个请求/应答客户和服务器都要新建一个连接,完成 之后立即断开连接(HTTP协议为无连接的协议);
当使用Keep-Alive模式(又称持久连接、连接重用)时,Keep-Alive功能使客户端到服 务器端的连接持续有效,当出现对服务器的后继请求时,Keep-Alive功能避免了建立或者重新建立连接。
为什么要使用keep-alive
keep-alive技术的创建目的,能在多次HTTP之前重用同一个TCP连接,从而减少创建/关闭多个 TCP 连接的开销(包括响应时间、CPU 资源、减少拥堵等),参考如下示意图
客户端如何开启
在HTTP/1.0协议中,默认是关闭的,需要在http头加入"Connection: Keep-Alive”,才能启用Keep-Alive;
Connection: keep-alive
http 1.1中默认启用Keep-Alive,如果加入"Connection: close “,才关闭。
Connection: close
目前大部分浏览器都是用http1.1协议,也就是说默认都会发起Keep-Alive的连接请求了,所以是否能完成一个完整的Keep- Alive连接就看服务器设置情况。
----------@----------
1.0 协议缺陷:
TCP 3
次握手head of line blocking
: 线头阻塞,导致请求之间互相影响1.1 改进:
keep-alive
),复用host
字段指定对应的虚拟站点cache
缓存
Cache-Control
Expires
Last-Modified
Etag
2.0:
https: 较为安全的网络传输协议
SSL
加密443
TCP:
缓存策略: 可分为 强缓存 和 协商缓存
Cache-Control/Expires
: 浏览器判断缓存是否过期,未过期时,直接使用强缓存,Cache-Control
的 max-age
优先级高于 Expires
Etag
(response
携带) & If-None-Match
(request
携带,上一次返回的 Etag
): 服务器判断资源是否被修改Last-Modified(response) & If-Modified-Since
(request
,上一次返回的Last-Modified
)
Last-Modified
缺点:
s
, s
以内的改动无法检测到Etag
的优先级高于Last-Modified
----------@----------
客户端和服务端都需要直到各自可收发,因此需要三次握手
你可以能会问,2 次握手就足够了?。但其实不是,因为服务端还没有确定客户端是否准备好了。比如步骤 3 之后,服务端马上给客户端发送数据,这个时候客户端可能还没有准备好接收数据。因此还需要增加一个过程
TCP有6种标示:SYN(建立联机) ACK(确认) PSH(传送) FIN(结束) RST(重置) URG(紧急)
举例:已失效的连接请求报文段
client
发送了第一个连接的请求报文,但是由于网络不好,这个请求没有立即到达服务端,而是在某个网络节点中滞留了,直到某个时间才到达server
server
端接收到这个请求报文后,还是会想client
发出确认的报文,表示同意连接。server
发出确认,新的建立就连接了,但其实这个请求是失效的请求,client
是不会理睬server
的确认信息,也不会向服务端发送确认的请求server
认为新的连接已经建立起来了,并一直等待client
发来数据,这样,server的很多资源就没白白浪费掉了client
并没有建立连接。这就是三次握手的作用三次握手过程中可以携带数据吗
ESTABLISHED (已建立连接状态)
,对于客户端来说,已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也是没问题的。为什么建立连接只通信了三次,而断开连接却用了四次?
服务端经过一个等待,确定可以关闭连接了,再发一条 FIN 给客户端
。为了确保数据能够完成传输。因为当服务端收到客户端的 FIN 报文后,发送的 ACK 报文只是用来应答的,并不表示服务端也希望立即关闭连接。
当只有服务端把所有的报文都发送完了,才会发送 FIN 报文,告诉客户端可以断开连接了,因此在断开连接时需要四次挥手。
SOCKET
,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK
报文和FIN报文多数情况下都是分开发送的。----------@----------
已经有了被广泛应用的 HTTP 协议,为什么要再出一个 WebSocket 呢?它有哪些好处呢?
其实 WebSocket 与 HTTP/2 一样,都是为了解决 HTTP 某方面的缺陷而诞生的。HTTP/2 针对的是“队头阻塞”,而
WebSocket 针对的是“请求 - 应答”通信模式
。
那么,“请求 - 应答”有什么不好的地方呢?
WebSocket 的特点
采用了二进制帧结构
,语法、语义与 HTTP 完全不兼容,但因为它的主要运行环境是浏览器,为了便于推广和应用,就不得不“搭便车”,在使用习惯上尽量向 HTTP 靠拢,这就是它名字里“Web”的含义。WebSocket 的默认端口也选择了 80 和 443
,因为现在互联网上的防火墙屏蔽了绝大多数的端口,只对 HTTP 的 80、443 端口“放行”,所以 WebSocket 就可以“伪装”成 HTTP 协议,比较容易地“穿透”防火墙,与服务器建立连接ws://www.chrono.com
ws://www.chrono.com:8080/srv
wss://www.chrono.com:445/im?user_id=xxx
WebSocket 的握手
和 TCP、TLS 一样,WebSocket 也要有一个握手过程,然后才能正式收发数据。
这里它还是搭上了 HTTP 的“便车”,利用了 HTTP 本身的“协议升级”特性,“伪装”成 HTTP,这样就能绕过浏览器沙盒、网络防火墙等等限制,这也是 WebSocket 与 HTTP 的另一个重要关联点。
WebSocket 的握手是一个标准的 HTTP GET 请求,但要带上两个协议升级的专用头字段:
另外,为了防止普通的 HTTP 消息被“意外”识别成 WebSocket,握手消息还增加了两个额外的认证用头字段(所谓的“挑战”,Challenge):
Sec-WebSocket-Key
:一个 Base64 编码的 16 字节随机数,作为简单的认证密钥;Sec-WebSocket-Version
:协议的版本号,当前必须是 13。服务器收到 HTTP 请求报文,看到上面的四个字段,就知道这不是一个普通的 GET 请求,而是 WebSocket 的升级请求,于是就不走普通的 HTTP 处理流程,而是构造一个特殊的“101 Switching Protocols”响应报文,通知客户端,接下来就不用 HTTP 了,全改用 WebSocket 协议通信
小结
浏览器是一个“沙盒”环境,有很多的限制,不允许建立 TCP 连接收发数据,而有了 WebSocket,我们就可以在浏览器里与服务器直接建立“TCP 连接”,获得更多的自由。
不过自由也是有代价的,WebSocket 虽然是在应用层,但使用方式却与“TCP Socket”差不多,过于“原始”,用户必须自己管理连接、缓存、状态,开发上比 HTTP 复杂的多,所以是否要在项目中引入 WebSocket 必须慎重考虑。
HTTP
的“请求 - 应答”模式不适合开发“实时通信”应用,效率低,难以实现动态页面,所以出现了 WebSocket;WebSocket
是一个“全双工”的通信协议,相当于对 TCP 做了一层“薄薄的包装”,让它运行在浏览器环境里;WebSocket
使用兼容 HTTP 的 URI 来发现服务,但定义了新的协议名“ws”和“wss”,端口号也沿用了 80 和 443
;WebSocket
使用二进制帧,结构比较简单,特殊的地方是有个“掩码”操作,客户端发数据必须掩码,服务器则不用;WebSocket
利用 HTTP 协议实现连接握手,发送 GET 请求要求“协议升级”,握手过程中有个非常简单的认证机制,目的是防止误连接。----------@----------
----------@----------
外观模式是最常见的设计模式之一,它为子系统中的一组接口提供一个统一的高层接口,使子系统更容易使用。简而言之外观设计模式就是把多个子系统中复杂逻辑进行抽象,从而提供一个更统一、更简洁、更易用的API。很多我们常用的框架和库基本都遵循了外观设计模式,比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题,从而提供了一个更高级更易用的版本。其实在平时工作中我们也会经常用到外观模式进行开发,只是我们不自知而已
兼容浏览器事件绑定
let addMyEvent = function (el, ev, fn) {
if (el.addEventListener) {
el.addEventListener(ev, fn, false)
} else if (el.attachEvent) {
el.attachEvent('on' + ev, fn)
} else {
el['on' + ev] = fn
}
};
封装接口
let myEvent = {
// ...
stop: e => {
e.stopPropagation();
e.preventDefault();
}
};
场景
优点
缺点
不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。
是为一个对象提供一个代用品或占位符,以便控制对它的访问
假设当A 在心情好的时候收到花,小明表白成功的几率有60%,而当A 在心情差的时候收到花,小明表白的成功率无限趋近于0。小明跟A 刚刚认识两天,还无法辨别A 什么时候心情好。如果不合时宜地把花送给A,花被直接扔掉的可能性很大,这束花可是小明吃了7 天泡面换来的。但是A 的朋友B 却很了解A,所以小明只管把花交给B,B 会监听A 的心情变化,然后选择A 心情好的时候把花转交给A,代码如下:
let Flower = function() {}
let xiaoming = {
sendFlower: function(target) {
let flower = new Flower()
target.receiveFlower(flower)
}
}
let B = {
receiveFlower: function(flower) {
A.listenGoodMood(function() {
A.receiveFlower(flower)
})
}
}
let A = {
receiveFlower: function(flower) {
console.log('收到花'+ flower)
},
listenGoodMood: function(fn) {
setTimeout(function() {
fn()
}, 1000)
}
}
xiaoming.sendFlower(B)
场景
HTML元 素事件代理
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul');
ul.addEventListener('click', event => {
console.log(event.target);
});
</script>
优点
缺点
处理请求速度可能有差别,非直接访问存在开销
工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。
class Product {
constructor(name) {
this.name = name
}
init() {
console.log('init')
}
fun() {
console.log('fun')
}
}
class Factory {
create(name) {
return new Product(name)
}
}
// use
let factory = new Factory()
let p = factory.create('p1')
p.init()
p.fun()
场景
优点
缺点
什么时候不用
当被应用到错误的问题类型上时,这一模式会给应用程序引入大量不必要的复杂性.除非为创建对象提供一个接口是我们编写的库或者框架的一个设计上目标,否则我会建议使用明确的构造器,以避免不必要的开销。
由于对象的创建过程被高效的抽象在一个接口后面的事实,这也会给依赖于这个过程可能会有多复杂的单元测试带来问题。
顾名思义,单例模式中Class的实例个数最多为1。当需要一个对象去贯穿整个系统执行某些任务时,单例模式就派上了用场。而除此之外的场景尽量避免单例模式的使用,因为单例模式会引入全局状态,而一个健康的系统应该避免引入过多的全局状态。
实现单例模式需要解决以下几个问题:
我们一般通过实现以下两点来解决上述问题:
getInstance()
方法来创建/获取唯一实例Javascript中单例模式可以通过以下方式实现:
// 单例构造器
const FooServiceSingleton = (function () {
// 隐藏的Class的构造函数
function FooService() {}
// 未初始化的单例对象
let fooService;
return {
// 创建/获取单例对象的函数
getInstance: function () {
if (!fooService) {
fooService = new FooService();
}
return fooService;
}
}
})();
实现的关键点有:
我们可以验证下单例对象是否创建成功:
const fooService1 = FooServiceSingleton.getInstance();
const fooService2 = FooServiceSingleton.getInstance();
console.log(fooService1 === fooService2); // true
场景例子
优点
缺点
策略模式简单描述就是:对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。把它们一个个封装起来,并且使它们可以互相替换
<html>
<head>
<title>策略模式-校验表单</title>
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
</head>
<body>
<form id = "registerForm" method="post" action="http://xxxx.com/api/register">
用户名:<input type="text" name="userName">
密码:<input type="text" name="password">
手机号码:<input type="text" name="phoneNumber">
<button type="submit">提交</button>
</form>
<script type="text/javascript">
// 策略对象
const strategies = {
isNoEmpty: function (value, errorMsg) {
if (value === '') {
return errorMsg;
}
},
isNoSpace: function (value, errorMsg) {
if (value.trim() === '') {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.trim().length < length) {
return errorMsg;
}
},
maxLength: function (value, length, errorMsg) {
if (value.length > length) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) {
if (!/^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|17[7]|18[0|1|2|3|5|6|7|8|9])\d{8}$/.test(value)) {
return errorMsg;
}
}
}
// 验证类
class Validator {
constructor() {
this.cache = []
}
add(dom, rules) {
for(let i = 0, rule; rule = rules[i++];) {
let strategyAry = rule.strategy.split(':')
let errorMsg = rule.errorMsg
this.cache.push(() => {
let strategy = strategyAry.shift()
strategyAry.unshift(dom.value)
strategyAry.push(errorMsg)
return strategies[strategy].apply(dom, strategyAry)
})
}
}
start() {
for(let i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
let errorMsg = validatorFunc()
if (errorMsg) {
return errorMsg
}
}
}
}
// 调用代码
let registerForm = document.getElementById('registerForm')
let validataFunc = function() {
let validator = new Validator()
validator.add(registerForm.userName, [{
strategy: 'isNoEmpty',
errorMsg: '用户名不可为空'
}, {
strategy: 'isNoSpace',
errorMsg: '不允许以空白字符命名'
}, {
strategy: 'minLength:2',
errorMsg: '用户名长度不能小于2位'
}])
validator.add(registerForm.password, [ {
strategy: 'minLength:6',
errorMsg: '密码长度不能小于6位'
}])
validator.add(registerForm.phoneNumber, [{
strategy: 'isMobile',
errorMsg: '请输入正确的手机号码格式'
}])
return validator.start()
}
registerForm.onsubmit = function() {
let errorMsg = validataFunc()
if (errorMsg) {
alert(errorMsg)
return false
}
}
</script>
</body>
</html>
场景例子
优点
缺点
如果你看到这,ES6中的迭代器 Iterator 相信你还是有点印象的,上面第60条已经做过简单的介绍。迭代器模式简单的说就是提供一种方法顺序一个聚合对象中各个元素,而又不暴露该对象的内部表示。
迭代器模式解决了以下问题:
一个迭代器通常需要实现以下接口:
为Javascript的数组实现一个迭代器可以这么写:
const item = [1, 'red', false, 3.14];
function Iterator(items) {
this.items = items;
this.index = 0;
}
Iterator.prototype = {
hasNext: function () {
return this.index < this.items.length;
},
next: function () {
return this.items[this.index++];
}
}
验证一下迭代器是否工作:
const iterator = new Iterator(item);
while(iterator.hasNext()){
console.log(iterator.next());
}
//输出:1, red, false, 3.14
ES6提供了更简单的迭代循环语法 for…of,使用该语法的前提是操作对象需要实现 可迭代协议(The iterable protocol),简单说就是该对象有个Key为 Symbol.iterator 的方法,该方法返回一个iterator对象。
比如我们实现一个 Range 类用于在某个数字区间进行迭代:
function Range(start, end) {
return {
[Symbol.iterator]: function () {
return {
next() {
if (start < end) {
return { value: start++, done: false };
}
return { done: true, value: end };
}
}
}
}
}
验证一下:
for (num of Range(1, 5)) {
console.log(num);
}
// 输出:1, 2, 3, 4
观察者模式又称发布-订阅模式(Publish/Subscribe Pattern),是我们经常接触到的设计模式,日常生活中的应用也比比皆是,比如你订阅了某个博主的频道,当有内容更新时会收到推送;又比如JavaScript中的事件订阅响应机制。观察者模式的思想用一句话描述就是:被观察对象(subject)维护一组观察者(observer),当被观察对象状态改变时,通过调用观察者的某个方法将这些变化通知到观察者。
观察者模式中Subject对象一般需要实现以下API:
用JavaScript手动实现观察者模式:
// 被观察者
function Subject() {
this.observers = [];
}
Subject.prototype = {
// 订阅
subscribe: function (observer) {
this.observers.push(observer);
},
// 取消订阅
unsubscribe: function (observerToRemove) {
this.observers = this.observers.filter(observer => {
return observer !== observerToRemove;
})
},
// 事件触发
fire: function () {
this.observers.forEach(observer => {
observer.call();
});
}
}
验证一下订阅是否成功:
const subject = new Subject();
function observer1() {
console.log('Observer 1 Firing!');
}
function observer2() {
console.log('Observer 2 Firing!');
}
subject.subscribe(observer1);
subject.subscribe(observer2);
subject.fire();
//输出:
Observer 1 Firing!
Observer 2 Firing!
验证一下取消订阅是否成功:
subject.unsubscribe(observer2);
subject.fire();
//输出:
Observer 1 Firing!
场景
document.body.addEventListener('click', function() {
console.log('hello world!');
});
document.body.click()
优点
缺点
过度使用会导致对象与对象之间的联系弱化,会导致程序难以跟踪维护和理解
场景
例如购物车需求,存在商品选择表单、颜色选择表单、购买数量表单等等,都会触发change事件,那么可以通过中介者来转发处理这些事件,实现各个事件间的解耦,仅仅维护中介者对象即可。
var goods = { //手机库存
'red|32G': 3,
'red|64G': 1,
'blue|32G': 7,
'blue|32G': 6,
};
//中介者
var mediator = (function() {
var colorSelect = document.getElementById('colorSelect');
var memorySelect = document.getElementById('memorySelect');
var numSelect = document.getElementById('numSelect');
return {
changed: function(obj) {
switch(obj){
case colorSelect:
//TODO
break;
case memorySelect:
//TODO
break;
case numSelect:
//TODO
break;
}
}
}
})();
colorSelect.onchange = function() {
mediator.changed(this);
};
memorySelect.onchange = function() {
mediator.changed(this);
};
numSelect.onchange = function() {
mediator.changed(this);
};
聊天室成员类:
function Member(name) {
this.name = name;
this.chatroom = null;
}
Member.prototype = {
// 发送消息
send: function (message, toMember) {
this.chatroom.send(message, this, toMember);
},
// 接收消息
receive: function (message, fromMember) {
console.log(`${fromMember.name} to ${this.name}: ${message}`);
}
}
聊天室类:
function Chatroom() {
this.members = {};
}
Chatroom.prototype = {
// 增加成员
addMember: function (member) {
this.members[member.name] = member;
member.chatroom = this;
},
// 发送消息
send: function (message, fromMember, toMember) {
toMember.receive(message, fromMember);
}
}
测试一下:
const chatroom = new Chatroom();
const bruce = new Member('bruce');
const frank = new Member('frank');
chatroom.addMember(bruce);
chatroom.addMember(frank);
bruce.send('Hey frank', frank);
//输出:bruce to frank: hello frank
优点
缺点
系统中会新增一个中介者对象,因为对象之间交互的复杂性,转移成了中介者对象的复杂性,使得中介者对象经常是巨大的。中介 者对象自身往往就是一个难以维护的对象。
访问者模式 是一种将算法与对象结构分离的设计模式,通俗点讲就是:访问者模式让我们能够在不改变一个对象结构的前提下能够给该对象增加新的逻辑,新增的逻辑保存在一个独立的访问者对象中。访问者模式常用于拓展一些第三方的库和工具。
// 访问者
class Visitor {
constructor() {}
visitConcreteElement(ConcreteElement) {
ConcreteElement.operation()
}
}
// 元素类
class ConcreteElement{
constructor() {
}
operation() {
console.log("ConcreteElement.operation invoked");
}
accept(visitor) {
visitor.visitConcreteElement(this)
}
}
// client
let visitor = new Visitor()
let element = new ConcreteElement()
elementA.accept(visitor)
访问者模式的实现有以下几个要素:
简单的代码实现如下:
Receiving Object:
function Employee(name, salary) {
this.name = name;
this.salary = salary;
}
Employee.prototype = {
getSalary: function () {
return this.salary;
},
setSalary: function (salary) {
this.salary = salary;
},
accept: function (visitor) {
visitor.visit(this);
}
}
Visitor Object:
function Visitor() { }
Visitor.prototype = {
visit: function (employee) {
employee.setSalary(employee.getSalary() * 2);
}
}
验证一下:
const employee = new Employee('bruce', 1000);
const visitor = new Visitor();
employee.accept(visitor);
console.log(employee.getSalary());//输出:2000
场景
对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作
需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作"污染"这些对象的类,也不希望在增加新操作时修改这些类。
优点
缺点