_improve-3

createElement过程

React.createElement(): 根据指定的第一个参数创建一个React元素

React.createElement(
  type,
  [props],
  [...children]
)
  • 第一个参数是必填,传入的是似HTML标签名称,eg: ul, li
  • 第二个参数是选填,表示的是属性,eg: className
  • 第三个参数是选填, 子节点,eg: 要显示的文本内容
//写法一:

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')
      );

----------@----------

调和阶段 setState内部干了什么

  • 当调用 setState 时,React会做的第一件事情是将传递给 setState 的对象合并到组件的当前状态
  • 这将启动一个称为和解(reconciliation)的过程。和解(reconciliation)的最终目标是以最有效的方式,根据这个新的状态来更新UI。 为此,React将构建一个新的 React 元素树(您可以将其视为 UI 的对象表示)
  • 一旦有了这个树,为了弄清 UI 如何响应新的状态而改变,React 会将这个新树与上一个元素树相比较( diff )

通过这样做, React 将会知道发生的确切变化,并且通过了解发生什么变化,只需在绝对必要的情况下进行更新即可最小化 UI 的占用空间

----------@----------

setState

在了解setState之前,我们先来简单了解下 React 一个包装结构: Transaction:

事务 (Transaction)

是 React 中的一个调用结构,用于包装一个方法,结构为: initialize - perform(method) - close。通过事务,可以统一管理一个方法的开始与结束;处于事务流中,表示进程正在执行一些操作

  • setState: React 中用于修改状态,更新视图。它具有以下特点:

异步与同步: setState并不是单纯的异步或同步,这其实与调用时的环境相关:

  • 合成事件生命周期钩子 (除 componentDidUpdate) 中,setState是"异步"的;
    • 原因: 因为在setState的实现中,有一个判断: 当更新策略正在事务流的执行中时,该组件更新会被推入dirtyComponents队列中等待执行;否则,开始执行batchedUpdates队列更新;
      • 在生命周期钩子调用中,更新策略都处于更新之前,组件仍处于事务流中,而componentDidUpdate是在更新之后,此时组件已经不在事务流中了,因此则会同步执行;
      • 在合成事件中,React 是基于 事务流完成的事件委托机制 实现,也是处于事务流中;
    • 问题: 无法在setState后马上从this.state上获取更新后的值。
    • 解决: 如果需要马上同步去获取新值,setState其实是可以传入第二个参数的。setState(updater, callback),在回调中即可获取最新值;
  • 原生事件 和 setTimeout 中,setState是同步的,可以马上获取更新后的值;
    • 原因: 原生事件是浏览器本身的实现,与事务流无关,自然是同步;而setTimeout是放置于定时器线程中延后执行,此时事务流已结束,因此也是同步;
  • 批量更新 : 在 合成事件 和 生命周期钩子 中,setState更新队列时,存储的是 合并状态(Object.assign)。因此前面设置的 key 值会被后面所覆盖,最终只会执行一次更新;
  • 函数式 : 由于 Fiber 及 合并 的问题,官方推荐可以传入 函数 的形式。setState(fn),在fn中返回新的state对象即可,例如this.setState((state, props) => newState);
    • 使用函数式,可以用于避免setState的批量更新的逻辑,传入的函数将会被 顺序调用;

注意事项:

  • setState 合并,在 合成事件 和 生命周期钩子 中多次连续调用会被优化为一次;
  • 当组件已被销毁,如果再次调用setState,React 会报错警告,通常有两种解决办法
    • 将数据挂载到外部,通过 props 传入,如放到 Redux 或 父级中;
    • 在组件内部维护一个状态量 (isUnmounted),componentWillUnmount中标记为 true,在setState前进行判断;

总结

setState 并非真异步,只是看上去像异步。在源码中,通过 isBatchingUpdates 来判断

  • setState 是先存进 state 队列还是直接更新,如果值为 true 则执行异步操作,为 false 则直接更新。
  • 那么什么情况下 isBatchingUpdates 会为 true 呢?在 React 可以控制的地方,就为 true,比如在 React 生命周期事件和合成事件中,都会走合并操作,延迟更新的策略。
  • 但在 React 无法控制的地方,比如原生事件,具体就是在 addEventListenersetTimeoutsetInterval 等事件中,就只能同步更新。

一般认为,做异步设计是为了性能优化、减少渲染次数,React 团队还补充了两点。

  • 保持内部一致性。如果将 state 改为同步更新,那尽管 state 的更新是同步的,但是 props不是。
  • 启用并发更新,完成异步渲染。

_improve-3_第1张图片

  1. setState 只有在 React 自身的合成事件和钩子函数中是异步的,在原生事件和 setTimeout 中都是同步的
  2. setState 的异步并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数中没法立马拿到更新后的值,形成了所谓的异步。当然可以通过 setState 的第二个参数中的 callback 拿到更新后的结果
  3. setState 的批量更新优化也是建立在异步(合成事件、钩子函数)之上的,在原生事件和 setTimeout 中不会批量更新,在异步中如果对同一个值进行多次 setState,setState 的批量更新策略会对其进行覆盖,去最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新
  • 合成事件中是异步
  • 钩子函数中的是异步
  • 原生事件中是同步
  • setTimeout中是同步

_improve-3_第2张图片 _improve-3_第3张图片 _improve-3_第4张图片 _improve-3_第5张图片 _improve-3_第6张图片 _improve-3_第7张图片
这是一道经常会出现的 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
  • 所以此时 count1,那么后续在 setTimeout中的输出则是 23

所以完整答案是 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 了。

----------@----------

setState原理分析

1. setState异步更新

  • 我们都知道,React通过this.state来访问state,通过this.setState()方法来更新state。当this.setState()方法被调用的时候,React会重新调用render方法来重新渲染UI
  • 首先如果直接在setState后面获取state的值是获取不到的。在React内部机制能检测到的地方, setState就是异步的;在React检测不到的地方,例如setInterval,setTimeoutsetState就是同步更新的

_improve-3_第8张图片

因为setState是可以接受两个参数的,一个state,一个回调函数。因此我们可以在回调函数里面获取值

_improve-3_第9张图片

  • 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更新。 isBatchingUpdatestrue,则把当前组件(即调用了 setState的组件)放入 dirtyComponents 数组中;否则 batchUpdate 所有队列中的更新
  • 而在原生事件和异步操作中,不会执行pre钩子,或者生命周期的中的异步操作之前执行了pre钩子,但是pos钩子也在异步操作之前执行完了,isBatchingUpdates必定为false,也就不会进行批量更新

_improve-3_第10张图片

enqueueUpdate包含了React避免重复render的逻辑。mountComponentupdateComponent方法在执行的最开始,会调用到batchedUpdates进行批处理更新,此时会将isBatchingUpdates设置为true,也就是将状态标记为现在正处于更新阶段了。 isBatchingUpdatestrue,则把当前组件(即调用了 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的机制。比如那前端比较火的ReactvuenextTick机制,视图的更新以及实现)

1.4 setState之后发生的事情

  • setState操作并不保证是同步的,也可以认为是异步的
  • ReactsetState之后,会经对state进行diff,判断是否有改变,然后去diff dom决定是否要更新UI。如果这一系列过程立刻发生在每一个setState之后,就可能会有性能问题
  • 在短时间内频繁setStateReact会将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,并调用receiveComponentupdateComponent方法进行组件更新
  • 如果在shouldComponentUpdate或者componentWillUpdate方法中调用setState,此时this._pending-StateQueue != null,就会造成循环调用,使得浏览器内存占满后崩溃

3 事务

  • 事务就是将需要执行的方法使用wrapper封装起来,再通过事务提供的perform方法执行,先执行wrapper中的initialize方法,执行完perform之后,在执行所有的close方法,一组initializeclose方法称为一个wrapper
  • 那么事务和setState方法的不同表现有什么关系,首先我们把4setState简单归类,前两次属于一类,因为它们在同一调用栈中执行,setTimeout中的两次setState属于另一类
  • setState调用之前,已经处在batchedUpdates执行的事务中了。那么这次batchedUpdates方法是谁调用的呢,原来是ReactMount.js中的_renderNewRootComponent方法。也就是说,整个将React组件渲染到DOM中的过程就是处于一个大的事务中。而在componentDidMount中调用setState时,batchingStrategyisBatchingUpdates已经被设为了true,所以两次setState的结果没有立即生效
  • 再反观setTimeout中的两次setState,因为没有前置的batchedUpdates调用,所以导致了新的state马上生效

4. 总结

  • 通过setState去更新this.state,不要直接操作this.state,请把它当成不可变的
  • 调用setState更新this.state不是马上生效的,它是异步的,所以不要天真以为执行完setStatethis.state就是最新的值了
  • 多个顺序执行的setState不是同步地一个一个执行滴,会一个一个加入队列,然后最后一起执行,即批处理

----------@----------

React事务机制

_improve-3_第11张图片 _improve-3_第12张图片 _improve-3_第13张图片
----------@----------

React组件和渲染更新过程

渲染和更新过程

  • jsx如何渲染为页面
  • setState之后如何更新页面
  • 面试考察全流程

JSX本质和vdom

  • JSX即createElement函数
  • 执行生成vnode
  • patch(elem,vnode)patch(vnode,newNode)

组件渲染过程

  • props state
  • render()生成vnode
  • patch(elem, vnode)

组件更新过程

  • setState-->dirtyComponents(可能有子组件)
  • render生成newVnode
  • patch(vnode, newVnode)

----------@----------

如何解释 React 的渲染流程

_improve-3_第14张图片

  • React 的渲染过程大致一致,但协调并不相同,以 React 16 为分界线,分为 Stack ReconcilerFiber 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的基本单位是 FiberFiber 基于过去的 React Element 提供了二次封装,提供了指向父、子、兄弟节点的引用,为 diff 工作的双链表实现提供了基础。
  • 在新的架构下,整个生命周期被划分为 Render 和 Commit 两个阶段Render 阶段的执行特点是可中断、可停止、无副作用,主要是通过构造 workInProgress 树计算出 diff。以 current 树为基础,将每个 Fiber作为一个基本单位,自下而上逐个节点检查并构造 workInProgress 树。这个过程不再是递归,而是基于循环来完成
  • 在执行上通过 requestIdleCallback 来调度执行每组任务,每组中的每个计算任务被称为 work,每个 work 完成后确认是否有优先级更高的 work 需要插入,如果有就让位,没有就继续。优先级通常是标记为动画或者 high 的会先处理。每完成一组后,将调度权交回主线程,直到下一次 requestIdleCallback 调用,再继续构建 workInProgress
  • commit 阶段需要处理 effect 列表,这里的 effect 列表包含了根据 diff 更新 DOM 树回调生命周期响应 ref 等。
  • 但一定要注意,这个阶段是同步执行的,不可中断暂停,所以不要在 componentDidMountcomponentDidUpdatecomponentWiilUnmount中去执行重度消耗算力的任务
  • 如果只是一般的应用场景,比如管理后台、H5 展示页等,两者性能差距并不大,但在动画、画布及手势等场景下,Stack Reconciler 的设计会占用占主线程,造成卡顿,而 fiber reconciler 的设计则能带来高性能的表现

----------@----------

diff算法是怎么运作

每一种节点类型有自己的属性,也就是prop,每次进行diff的时候,react会先比较该节点类型,假如节点类型不一样,那么react会直接删除该节点,然后直接创建新的节点插入到其中,假如节点类型一样,那么会比较prop是否有更新,假如有prop不一样,那么react会判定该节点有更新,那么重渲染该节点,然后在对其子节点进行比较,一层一层往下,直到没有子节点

  • 把树形结构按照层级分解,只比较同级元素。
  • 给列表结构的每个单元添加唯一的key属性,方便比较。
  • React 只会匹配相同 classcomponent(这里面的class指的是组件的名字)
  • 合并操作,调用 componentsetState 方法的时候, React 将其标记为 - dirty.到每一个事件循环结束, React 检查所有标记 dirtycomponent重新绘制.
  • 选择性子树渲染。开发人员可以重写shouldComponentUpdate提高diff的性能

优化⬇️

为了降低算法复杂度,Reactdiff会预设三个限制:

  1. 只对同级元素进行Diff。如果一个DOM节点在前后两次更新中跨越了层级,那么React不会尝试复用他。
  2. 两个不同类型的元素会产生出不同的树。如果元素由div变为p,React会销毁div及其子孙节点,并新建p及其子孙节点。
  3. 开发者可以通过 key prop来暗示哪些子元素在不同的渲染下能保持稳定。考虑如下例子:

Diff的思路

该如何设计算法呢?如果让我设计一个Diff算法,我首先想到的方案是:

  1. 判断当前节点的更新属于哪种情况
  2. 如果是新增,执行新增逻辑
  3. 如果是删除,执行删除逻辑
  4. 如果是更新,执行更新逻辑
  • 按这个方案,其实有个隐含的前提——不同操作的优先级是相同的
  • 但是React团队发现,在日常开发中,相较于新增删除更新组件发生的频率更高。所以Diff会优先判断当前节点是否属于更新

基于以上原因,Diff算法的整体逻辑会经历两轮遍历:

  • 第一轮遍历:处理更新的节点。
  • 第二轮遍历:处理剩下的不属于更新的节点。

_improve-3_第15张图片

diff算法的作用

计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面。

传统diff算法

通过循环递归对节点进行依次对比,算法复杂度达到 O(n^3) ,n是树的节点数,这个有多可怕呢?——如果要展示1000个节点,得执行上亿次比较。。即便是CPU快能执行30亿条命令,也很难在一秒内计算出差异。

React的diff算法

  1. 什么是调和?

将Virtual DOM树转换成actual DOM树的最少操作的过程 称为 调和 。

  1. 什么是React diff算法?

diff算法是调和的具体实现。

diff策略

React用 三大策略 将O(n^3)复杂度 转化为 O(n)复杂度

策略一(tree diff):

  • Web UI中DOM节点跨层级的移动操作特别少,可以忽略不计。

策略二(component diff):

  • 拥有相同类的两个组件 生成相似的树形结构,
  • 拥有不同类的两个组件 生成不同的树形结构。

策略三(element diff):

对于同一层级的一组子节点,通过唯一id区分。

tree diff

  • React通过updateDepth对Virtual DOM树进行层级控制。
  • 对树分层比较,两棵树 只对同一层次节点 进行比较。如果该节点不存在时,则该节点及其子节点会被完全删除,不会再进一步比较。
  • 只需遍历一次,就能完成整棵DOM树的比较。

_improve-3_第16张图片

那么问题来了,如果DOM节点出现了跨层级操作,diff会咋办呢?

答:diff只简单考虑同层级的节点位置变换,如果是跨层级的话,只有创建节点和删除节点的操作。

_improve-3_第17张图片

如上图所示,以A为根节点的整棵树会被重新创建,而不是移动,因此 官方建议不要进行DOM节点跨层级操作,可以通过CSS隐藏、显示节点,而不是真正地移除、添加DOM节点

component diff

React对不同的组件间的比较,有三种策略

  1. 同一类型的两个组件,按原策略(层级比较)继续比较Virtual DOM树即可。
  2. 同一类型的两个组件,组件A变化为组件B时,可能Virtual DOM没有任何变化,如果知道这点(变换的过程中,Virtual DOM没有改变),可节省大量计算时间,所以 用户 可以通过 shouldComponentUpdate() 来判断是否需要 判断计算。
  3. 不同类型的组件,将一个(将被改变的)组件判断为dirty component(脏组件),从而替换 整个组件的所有节点。

注意:如果组件D和组件G的结构相似,但是 React判断是 不同类型的组件,则不会比较其结构,而是删除 组件D及其子节点,创建组件G及其子节点。

element diff

当节点处于同一层级时,diff提供三种节点操作:删除、插入、移动。

  • 插入:组件 C 不在集合(A,B)中,需要插入
  • 删除:
    • 组件 D 在集合(A,B,D)中,但 D的节点已经更改,不能复用和更新,所以需要删除 旧的 D ,再创建新的。
    • 组件 D 之前在 集合(A,B,D)中,但集合变成新的集合(A,B)了,D 就需要被删除。
  • 移动:组件D已经在集合(A,B,C,D)里了,且集合更新时,D没有发生更新,只是位置改变,如新集合(A,D,B,C),D在第二个,无须像传统diff,让旧集合的第二个B和新集合的第二个D 比较,并且删除第二个位置的B,再在第二个位置插入D,而是 (对同一层级的同组子节点) 添加唯一key进行区分,移动即��。

总结

  1. tree diff:只对比同一层的 dom 节点,忽略 dom 节点的跨层级移动

如下图,react 只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点不存在时,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。

这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。

image-20210302195610674

这就意味着,如果 dom 节点发生了跨层级移动,react 会删除旧的节点,生成新的节点,而不会复用。

  1. component diff:如果不是同一类型的组件,会删除旧的组件,创建新的组件

_improve-3_第18张图片

  1. element diff:对于同一层级的一组子节点,需要通过唯一 id 进行来区分
  • 如果没有 id 来进行区分,一旦有插入动作,会导致插入位置之后的列表全部重新渲染
  • 这也是为什么渲染列表时为什么要使用唯一的 key。

diff的不足与待优化的地方

尽量减少类似将最后一个节点移动到列表首部的操作,当节点数量过大或更新操作过于频繁时,会影响React的渲染性能

与其他框架相比,React 的 diff 算法有何不同?

_improve-3_第19张图片

diff 算法探讨的就是虚拟 DOM 树发生变化后,生成 DOM 树更新补丁的方式。它通过对比新旧两株虚拟 DOM 树的变更差异,将更新补丁作用于真实 DOM,以最小成本完成视图更新

_improve-3_第20张图片

具体的流程是这样的:

  • 真实 DOM 与虚拟 DOM 之间存在一个映射关系。这个映射关系依靠初始化时的 JSX 建立完成;
  • 当虚拟 DOM 发生变化后,就会根据差距计算生成 patch,这个 patch 是一个结构化的数据,内容包含了增加、更新、移除等;
  • 最后再根据 patch 去更新真实的 DOM,反馈到用户的界面上。

在回答有何不同之前,首先需要说明下什么是 diff 算法。

  • diff 算法是指生成更新补丁的方式,主要应用于虚拟 DOM 树变化后,更新真实 DOM。所以 diff 算法一定存在这样一个过程:触发更新 → 生成补丁 → 应用补丁
  • React 的 diff 算法,触发更新的时机主要在 state 变化与 hooks 调用之后。此时触发虚拟 DOM 树变更遍历,采用了深度优先遍历算法。但传统的遍历方式,效率较低。为了优化效率,使用了分治的方式。将单一节点比对转化为了 3 种类型节点的比对,分别是树、组件及元素,以此提升效率。
    • 树比对:由于网页视图中较少有跨层级节点移动,两株虚拟 DOM 树只对同一层次的节点进行比较。
    • 组件比对:如果组件是同一类型,则进行树比对,如果不是,则直接放入到补丁中。
    • 元素比对:主要发生在同层级中,通过标记节点操作生成补丁,节点操作对应真实的 DOM 剪裁操作。同一层级的子节点,可以通过标记 key 的方式进行列表对比。
  • 以上是经典的 React diff 算法内容。自 React 16 起,引入了 Fiber 架构。为了使整个更新过程可随时暂停恢复,节点与树分别采用了 FiberNode 与 FiberTree 进行重构fiberNode 使用了双链表的结构,可以直接找到兄弟节点与子节点
  • 然后拿 Vue 和 Preact 与 React 的 diff 算法进行对比
    • PreactDiff 算法相较于 React,整体设计思路相似,但最底层的元素采用了真实 DOM 对比操作,也没有采用 Fiber 设计。Vue 的 Diff 算法整体也与 React 相似,同样未实现 Fiber 设计
  • 然后进行横向比较,React 拥有完整的 Diff 算法策略,且拥有随时中断更新的时间切片能力,在大批量节点更新的极端情况下,拥有更友好的交互体验。
  • Preact 可以在一些对性能要求不高,仅需要渲染框架的简单场景下应用。
  • Vue 的整体 diff 策略与 React 对齐,虽然缺乏时间切片能力,但这并不意味着 Vue 的性能更差,因为在 Vue 3 初期引入过,后期因为收益不高移除掉了。除了高帧率动画,在 Vue 中其他的场景几乎都可以使用防抖和节流去提高响应性能。

**学习原理的目的就是应用。那如何根据 React diff 算法原理优化代码呢?**这个问题其实按优化方式逆向回答即可。

  • 根据 diff 算法的设计原则,应尽量避免跨层级节点移动。
  • 通过设置唯一 key 进行优化,尽量减少组件层级深度。因为过深的层级会加深遍历深度,带来性能问题。
  • 设置 shouldComponentUpdate 或者 React.pureComponet 减少 diff 次数。

----------@----------

合成事件原理

为了解决跨浏览器兼容性问题,React 会将浏览器原生事件(Browser Native Event)封装为合成事件(SyntheticEvent)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差异,保证了行为的一致性。另外有意思的是,React 并没有直接将事件附着到子元素上,而是以单一事件监听器的方式将所有的事件发送到顶层进行处理。这样 React 在更新 DOM 的时候就不需要考虑如何去处理附着在 DOM 上的事件监听器,最终达到优化性能的目的

  • 所有的事件挂在document上,DOM 事件触发后冒泡到 document;React 找到对应的组件,造出一个合成事件出来;并按组件树模拟一遍事件冒泡。
  • event不是原生的,是SyntheticEvent合成事件对象
  • 和Vue事件不同,和DOM事件也不同

React 17 之前的事件冒泡流程图

_improve-3_第21张图片

所以这就造成了,在一个页面中,只能有一个版本的 React。如果有多个版本,事件就乱套了。值得一提的是,这个问题在 React 17 中得到了解决,事件委托不再挂在 document 上,而是挂在 DOM 容器上,也就是 ReactDom.Render 所调用的节点上。

React 17 后的事件冒泡流程图

_improve-3_第22张图片

那到底哪些事件会被捕获生成合成事件呢?可以从 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

  1. 当用户在为onClick添加函数时,React并没有将Click绑定到DOM上面
  2. 而是在document处监听所有支持的事件,当事件发生并冒泡至document处时,React将事件内容封装交给中间层 SyntheticEvent (负责所有事件合成)
  3. 所以当事件触发的时候, 对使用统一的分发函数 dispatchEvent 将指定函数执行

_improve-3_第23张图片

为何要合成事件

  • 兼容性和跨平台
  • 挂在统一的document上,减少内存消耗,避免频繁解绑
  • 方便事件的统一管理(事务机制)
  • dispatchEvent事件机制

----------@----------

JSX语法糖本质

JSX是语法糖,通过babel转成React.createElement函数,在babel官网上可以在线把JSX转成React的JS语法

  • 首先解析出来的话,就是一个createElement函数
  • 然后这个函数执行完后,会返回一个vnode
  • 通过vdom的patch或者是其他的一个方法,最后渲染一个页面

script标签中不添加text/babel解析jsx语法的情况下

<script>
  const ele = React.createElement("h2", null, "Hello React!");
  ReactDOM.render(ele, document.getElementById("app"));
script>

JSX的本质是React.createElement()函数

_improve-3_第24张图片

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;
}

_improve-3_第25张图片

react通过babel把JSX转成createElement函数,生成ReactElement对象,然后通过ReactDOM.render函数把ReactElement渲染成真实的DOM元素

为什么 React 使用 JSX

_improve-3_第26张图片

  • 在回答问题之前,我首先解释下什么是 JSX 吧。JSX 是一个 JavaScript 的语法扩展,结构类似 XML。
  • JSX 主要用于声明 React 元素,但 React 中并不强制使用 JSX。即使使用了 JSX,也会在构建过程中,通过 Babel 插件编译为 React.createElement。所以 JSX 更像是 React.createElement 的一种语法糖
  • 接下来与 JSX 以外的三种技术方案进行对比
    • 首先是模板,React 团队认为模板不应该是开发过程中的关注点,因为引入了模板语法、模板指令等概念,是一种不佳的实现方案
    • 其次是模板字符串,模板字符串编写的结构会造成多次内部嵌套,使整个结构变得复杂,并且优化代码提示也会变得困难重重
    • 所以 React 最后选用了 JSX,因为 JSX 与其设计思想贴合,不需要引入过多新的概念,对编辑器的代码提示也极为友好。

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 函数体拆解

_improve-3_第27张图片

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 又把它交回到了开发者手中

_improve-3_第28张图片

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 对象实例

_improve-3_第29张图片

这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是老生常谈的“虚拟 DOM”(准确地说,是虚拟 DOM 中的一个节点)

----------@----------

为什么 React 元素有一个 $$typeof 属性

_improve-3_第30张图片

目的是为了防止 XSS 攻击。因为 Synbol 无法被序列化,所以 React 可以通过有没有 $$typeof 属性来断出当前的 element 对象是从数据库来的还是自己生成的。

  • 如果没有 $$typeof 这个属性,react 会拒绝处理该元素。
  • 在 React 的古老版本中,下面的写法会出现 XSS 攻击:
// 服务端允许用户存储 JSON
let expectedTextButGotJSON = {
  type: 'div',
  props: {
    dangerouslySetInnerHTML: {
      __html: '/* 把你想的搁着 */'
    },
  },
  // ...
};
let message = { text: expectedTextButGotJSON };

// React 0.13 中有风险
<p>
  {message.text}
</p>

----------@----------

Virtual DOM 的工作原理是什么

_improve-3_第31张图片

  • 虚拟 DOM 的工作原理是通过 JS 对象模拟 DOM 的节点。在 Facebook 构建 React 初期时,考虑到要提升代码抽象能力、避免人为的 DOM 操作、降低代码整体风险等因素,所以引入了虚拟 DOM
  • 虚拟 DOM 在实现上通常是 Plain Object,以 React 为例,在 render 函数中写的 JSX 会在 Babel 插件的作用下,编译为 React.createElement 执行 JSX 中的属性参数
  • React.createElement 执行后会返回一个 Plain Object,它会描述自己的 tag 类型、props 属性以及 children 情况等。这些 Plain Object 通过树形结构组成一棵虚拟 DOM 树。当状态发生变更时,将变更前后的虚拟 DOM 树进行差异比较,这个过程称为 diff,生成的结果称为 patch。计算之后,会渲染 Patch 完成对真实 DOM 的操作。
  • 虚拟 DOM 的优点主要有三点:改善大规模DOM操作的性能规避 XSS 风险能以较低的成本实现跨平台开发
  • 虚拟 DOM 的缺点在社区中主要有两点
    • 内存占用较高,因为需要模拟整个网页的真实 DOM
    • 高性能应用场景存在难以优化的情况,类似像 Google Earth 一类的高性能前端应用在技术选型上往往不会选择 React

除了渲染页面,虚拟 DOM 还有哪些应用场景?

这个问题考验面试者的想象力。通常而言,我们只是将虚拟 DOM 与渲染绑定在一起,但实际上虚拟 DOM 的应用更为广阔。比如,只要你记录了真实 DOM 变更,它甚至可以应用于埋点统计与数据记录等。

SSR原理

借助虚拟dom,服务器中没有dom概念的,react巧妙的借助虚拟dom,然后可以在服务器中nodejs可以运行起来react代码。

----------@----------

React有哪些优化性能的手段

类组件中的优化手段

  • 使用纯组件 PureComponent 作为基类。
  • 使用 shouldComponentUpdate 生命周期函数来自定义渲染逻辑。

方法组件中的优化手段

  • 使用 React.memo 高阶函数包装组件,React.memo 可以实现类似于 shouldComponentUpdate 或者 PureComponent 的效果
  • 使用 useMemo
    • 使用React.useMemo精细化的管控,useMemo 控制的则是是否需要重复执行某一段逻辑,而React.memo 控制是否需要重渲染一个组件
  • 使用 useCallBack

其他方式

  • 在列表需要频繁变动时,使用唯一 id 作为 key,而不是数组下标。
  • 必要时通过改变 CSS 样式隐藏显示组件,而不是通过条件判断显示隐藏组件。
  • 使用 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 的整个工作过程中,数据流是严格单向的。这一点一定一定要背下来,面试的时候也一定一定要记得说

为什么要用redux

React中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(通过props),所以,两个非父子组件之间通信就相对麻烦,redux的出现就是为了解决state里面的数据问题

Redux设计理念

Redux是将整个应用状态存储到一个地方上称为store,里面保存着一个状态树store tree,组件可以派发(dispatch)行为(action)给store,而不是直接通知其他组件,组件内部通过订阅store中的状态state来刷新自己的视图

_improve-3_第32张图片

如果你想对数据进行修改,只有一种途径:派发 action。action 会被 reducer 读取,进而根据 action 内容的不同对数据进行修改、生成新的 state(状态),这个新的 state 会更新到 store 对象里,进而驱动视图层面做出对应的改变。

Redux三大原则

  • 唯一数据源

整个应用的state都被存储到一个状态树里面,并且这个状态树,只存在于唯一的store中

  • 保持只读状态

state是只读的,唯一改变state的方法就是触发actionaction是一个用于描述以发生时间的普通对象

  • 数据改变只能通过纯函数来执行

使用纯函数来执行修改,为了描述action如何改变state的,你需要编写reducers

从编码的角度理解 Redux 工作流

  1. 使用 createStore 来完成 store 对象的创建
// 引入 redux
import { createStore } from 'redux'
// 创建 store
const store = createStore(
    reducer,
    initial_state,
    applyMiddleware(middleware1, middleware2, ...)
);

createStore 方法是一切的开始,它接收三个入参:

  • reducer;

  • 初始状态内容;

  • 指定中间件

  1. reducer 的作用是将新的 state 返回给 store

一个 reducer 一定是一个纯函数,它可以有各种各样的内在逻辑,但它最终一定要返回一个 state:

const reducer = (state, action) => {
    // 此处是各种样的 state处理逻辑
    return new_state
}

当我们基于某个 reducer 去创建 store 的时候,其实就是给这个 store 指定了一套更新规则:

// 更新规则全都写在 reducer 里 
const store = createStore(reducer)
  1. action 的作用是通知 reducer “让改变发生”

要想让 state 发生改变,就必须用正确的 action 来驱动这个改变。

const action = {
  type: "ADD_ITEM",
  payload: '
  • text
  • '
    }

    action 对象中允许传入的属性有多个,但只有 type 是必传的。type 是 action 的唯一标识,reducer 正是通过不同的 type 来识别出需要更新的不同的 state,由此才能够实现精准的“定向更新”。

    1. 派发 action,靠的是 dispatch

    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 主要工作流的概括,这里我同样为你总结了一张对应的流程图:

    _improve-3_第33张图片

    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

    为什么会出现中间件?

    • 它只是一个用来加工dispatch的工厂,而要加工什么样的dispatch出来,则需要我们传入对应的中间件函数
    • 让每一个中间件函数,接收一个dispatch,然后返回一个改造后的dispatch,来作为下一个中间件函数的next,以此类推。
    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: 读书,写作
    
    1. store、reducer、action关联

    store

    • store在这里代表的是数据模型,内部维护了一个state变量
    • store有两个核心方法,分别是getStatedispatch。前者用来获取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的状态。传入两个参数 stateaction
    • 其中,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;
        }
    };
    
    1. 关于actionCreator
    actionCreator(args) => action
    
    var addTodo = function(text){
        return {
            type: 'add_todo',
            text: text
        };
    };
    
    addTodo('睡觉');  // 返回:{type: 'add_todo', text: '睡觉'}
    

    异步Action及操作

    1. 创建同步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) 规范,需要满足如下条件

    • 是一个纯文本对象
    • 只具备 typepayloaderrormeta中的一个或者多个属性。type 字段不可缺省,其它字段可缺省
    • Action 报错,error 字段不可缺省,切必须为 true

    payload 是一个对象,用作Action携带数据的载体

    标准action示例

    • A basic Flux Standard Action:
    {
      type: 'ADD_TODO',
      payload: {
        text: 'Do something.'  
      }
    }
    
    • An FSA that represents an error, analogous to a rejected Promise
    {
      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
    })
    
    1. 创建异步action的多种方式

    最简单的方式就是使用同步的方式来异步,将原来同步时一个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"))
        })
    }
    
    1. redux异步流程

    _improve-3_第34张图片

    • 首先发起一个action,然后通过中间件,这里为什么要用中间件呢,因为这样dispatch的返回值才能是一个函数。

    • 通过store.dispatch,将状态的的改变传给store的小弟reducerreducer根据action的改变,传递新的状态state

    • 最后将所有的改变告诉给它的大哥,storestore保存着所有的数据,并将数据注入到组件的顶部,这样组件就可以获得它需要的数据了

    1. Redux异步方案选型

    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_SUCCESSGET_DATA_FAILED这样的字符串声明也非常无趣且易错 上例中,GET_DATA这个action并不是多数场景需要的

    redux-promise

    由于redux-thunk写起来实在是太麻烦了,社区当然会有其它轮子出现。redux-promise则是其中比较知名的

    • 它自定义了一个middleware,当检测到有actionpayload属性是Promise对象时,就会
      • resolve,触发一个此action的拷贝,但payloadpromisevalue,并设status属性为"success"
      • reject,触发一个此action的拷贝,但payloadpromisereason,并设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为了精简而做出的妥协非常明显:无法处理乐观更新

    场景解析之:乐观更新

    多数异步场景都是悲观更新的,即等到请求成功才渲染数据。而与之相对的乐观更新,则是不等待请求成功,在发送请求的同时立即渲染数据

    • 由于乐观更新发生在用户操作时,要处理它,意味着必须有action表示用户的初始动作
    • 在上面redux-thunk的例子中,我们看到了GET_DATA, GET_DATA_SUCCESSGET_DATA_FAILED三个action,分别表示初始动作、异步成功和异步失败,其中第一个action使得redux-thunk具备乐观更新的能力
    • 而在redux-promise中,最初触发的action被中间件拦截然后过滤掉了。原因很简单,redux认可的action对象是 plain JavaScript objects,即简单对象,而在redux-promise中,初始actionpayload是个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;
        }
    }
    
    1. redux异步操作代码演示
    • 根据官网的async例子分析 https://github.com/lewis617/react-redux-tutorial/tree/master/redux-examples/async

    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()
    • 通过使用指定的 middlewareaction 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的异步操作

    _improve-3_第35张图片

    ----------@----------

    谈谈你对状态管理的理解

    • 首先介绍 Flux,Flux 是一种使用单向数据流的形式来组合 React 组件的应用架构。
    • Flux 包含了 4 个部分,分别是 DispatcherStoreViewActionStore 存储了视图层所有的数据,当 Store 变化后会引起 View 层的更新。如果在视图层触发一个 Action,就会使当前的页面数据值发生变化。Action 会被 Dispatcher 进行统一的收发处理,传递给 Store 层,Store 层已经注册过相关 Action 的处理逻辑,处理对应的内部状态变化后,触发 View 层更新。
    • Flux 的优点是单向数据流,解决了 MVC 中数据流向不清的问题,使开发者可以快速了解应用行为。从项目结构上简化了视图层设计,明确了分工,数据与业务逻辑也统一存放管理,使在大型架构的项目中更容易管理、维护代码。
    • 其次是 Redux,Redux 本身是一个 JavaScript 状态容器,提供可预测化状态的管理。社区通常认为 Redux 是 Flux 的一个简化设计版本,它提供的状态管理,简化了一些高级特性的实现成本,比如撤销、重做、实时编辑、时间旅行、服务端同构等。
    • Redux 的核心设计包含了三大原则:单一数据源、纯函数 Reducer、State 是只读的
    • Redux 中整个数据流的方案与 Flux 大同小异
    • Redux 中的另一大核心点是处理“副作用”,AJAX 请求等异步工作,或不是纯函数产生的第三方的交互都被认为是 “副作用”。这就造成在纯函数设计的 Redux 中,处理副作用变成了一件至关重要的事情。社区通常有两种解决方案:
      • 第一类是在 Dispatch 的时候会有一个 middleware 中间件层,拦截分发的 Action 并添加额外的复杂行为,还可以添加副作用。第一类方案的流行框架有 Redux-thunk、Redux-Promise、Redux-Observable、Redux-Saga 等。
      • 第二类是允许 Reducer 层中直接处理副作用,采取该方案的有 React LoopReact Loop 在实现中采用了 Elm 中分形的思想,使代码具备更强的组合能力。
      • 除此以外,社区还提供了更为工程化的方案,比如 rematch 或 dva,提供了更详细的模块架构能力,提供了拓展插件以支持更多功能。
    • Redux 的优点很多:
      • 结果可预测;
      • 代码结构严格易维护;
      • 模块分离清晰且小函数结构容易编写单元测试;
      • Action 触发的方式,可以在调试器中使用时间回溯,定位问题更简单快捷;
      • 单一数据源使服务端同构变得更为容易;社区方案多,生态也更为繁荣。
    • 最后是 Mobx,Mobx 通过监听数据的属性变化,可以直接在数据上更改触发UI 的渲染。在使用上更接近 Vue,比起 Flux 与 Redux 的手动挡的体验,更像开自动挡的汽车。Mobx 的响应式实现原理与 Vue 相同,以 Mobx 5 为分界点,5 以前采用 Object.defineProperty 的方案,5 及以后使用 Proxy 的方案。它的优点是样板代码少、简单粗暴、用户学习快、响应式自动更新数据让开发者的心智负担更低。
    • Mobx 在开发项目时简单快速,但应用 Mobx 的场景 ,其实完全可以用 Vue 取代。如果纯用 Vue,体积还会更小巧

    _improve-3_第36张图片

    ----------@----------

    connect组件原理分析

    1. connect用法

    作用:连接React组件与 Redux store

    connect([mapStateToProps], [mapDispatchToProps], [mergeProps],[options])
    // 这个函数允许我们将 store 中的数据作为 props 绑定到组件上
    const mapStateToProps = (state) => {
      return {
        count: state.count
      }
    }
    
    • 这个函数的第一个参数就是 Reduxstore,我们从中摘取了 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的子组件
    • 接收Reduxstore作为props,通过context对象传递给子孙组件上的connect

    connect做了些什么

    它真正连接 ReduxReact,它包在我们的容器组件的外一层,它接收上面 Provider提供的 store 里面的 statedispatch,传给一个构造函数,返回一个对象,以属性形式传给我们的容器组件

    3. 源码

    connect是一个高阶函数,首先传入mapStateToPropsmapDispatchToProps,然后返回一个生产Component的函数(wrapWithConnect),然后再将真正的Component作为参数传入wrapWithConnect,这样就生产出一个经过包裹的Connect组件,该组件具有如下特点

    • 通过props.store获取祖先Componentstore props包括statePropsdispatchPropsparentProps,合并在一起得到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 Hooks

    • 代码逻辑聚合,逻辑复用
    • HOC嵌套地狱
    • 代替class

    React 中通常使用 类定义 或者 函数定义 创建组件:

    在类定义中,我们可以使用到许多 React 特性,例如 state、 各种组件生命周期钩子等,但是在函数定义中,我们却无能为力,因此 React 16.8 版本推出了一个新功能 (React Hooks),通过它,可以更好的在函数定义组件中使用 React 特性。

    函数组件与类组件的对比:无关“优劣”,只谈“不同”

    • 类组件需要继承 class,函数组件不需要;
    • 类组件可以访问生命周期方法,函数组件不能;
    • 类组件中可以获取到实例化后的 this,并基于这个 this 做各种各样的事情,而函数组件不可以;
    • 类组件中可以定义并维护 state(状态),而函数组件不可以;

    但是类组件它太重了,对于解决许多问题来说,编写一个类组件实在是一个过于复杂的姿势。复杂的姿势必然带来高昂的理解成本,这也是我们所不想看到的

    react hooks的好处:

    1. 跨组件复用: 其实 render props / HOC 也是为了复用,相比于它们,Hooks 作为官方的底层 API,最为轻量,而且改造成本小,不会影响原来的组件层次结构和传说中的嵌套地狱;

    2. 类定义更为复杂

    • 不同的生命周期会使逻辑变得分散且混乱,不易维护和管理;

    • 时刻需要关注this的指向问题;

    • 代码复用代价高,高阶组件的使用经常会使整个组件树变得臃肿;

    1. 状态与UI隔离: 正是由于 Hooks 的特性,状态逻辑会变成更小的粒度,并且极容易被抽象成一个自定义 Hooks,组件中的状态和 UI 变得更为清晰和隔离。

    注意:

    • 避免在 循环/条件判断/嵌套函数 中调用 hooks,保证调用顺序的稳定;
    • 只有 函数定义组件 和 hooks 可以调用 hooks,避免在 类组件 或者 普通函数 中调用;
    • 不能在useEffect中使用useState,React 会报错提示;
    • 类组件不会被替换或废弃,不需要强制改造类组件,两种方式能并存;

    重要钩子

    1. 状态钩子 (useState): 用于定义组件的 State,其到类定义中this.state的功能;
    // useState 只接受一个参数: 初始状态
    // 返回的是组件名和更改该组件对应的函数
    const [flag, setFlag] = useState(true);
    // 修改状态
    setFlag(false)
    
    // 上面的代码映射到类定义中:
    this.state = {
        flag: true    
    }
    const flag = this.state.flag
    const setFlag = (bool) => {
        this.setState({
            flag: bool,
        })
    }
    
    1. 生命周期钩子 (useEffect):

    类定义中有许多生命周期函数,而在 React Hooks 中也提供了一个相应的函数 (useEffect),这里可以看做componentDidMount、componentDidUpdate和componentWillUnmount的结合。

    useEffect(callback, [source])接受两个参数

    • callback: 钩子回调函数;
    • source: 设置触发条件,仅当 source 发生改变时才会触发;
    • useEffect钩子在没有传入[source]参数时,默认在每次 render 时都会优先调用上次保存的回调中返回的函数,后再重新调用回调;
    useEffect(() => {
        // 组件挂载后执行事件绑定
        console.log('on')
        addEventListener()
    
        // 组件 update 时会执行事件解绑
        return () => {
            console.log('off')
            removeEventListener()
        }
    }, [source]);
    
    
    // 每次 source 发生改变时,执行结果(以类定义的生命周期,便于大家理解):
    // --- DidMount ---
    // 'on'
    // --- DidUpdate ---
    // 'off'
    // 'on'
    // --- DidUpdate ---
    // 'off'
    // 'on'
    // --- WillUnmount --- 
    // 'off'
    

    通过第二个参数,我们便可模拟出几个常用的生命周期:

    • componentDidMount: 传入[]时,就只会在初始化时调用一次
    const useMount = (fn) => useEffect(fn, [])
    
    • componentWillUnmount: 传入[],回调中的返回的函数也只会被最终执行一次
    const useUnmount = (fn) => useEffect(() => fn, [])
    
    • mounted: 可以使用 useState 封装成一个高度可复用的 mounted 状态;
    const useMounted = () => {
        const [mounted, setMounted] = useState(false);
        useEffect(() => {
            !mounted && setMounted(true);
            return () => setMounted(false);
        }, []);
        return mounted;
    }
    
    • componentDidUpdate: useEffect每次均会执行,其实就是排除了 DidMount 后即可;
    const mounted = useMounted() 
    useEffect(() => {
        mounted && fn()
    })
    
    1. 其它内置钩子:
    • useContext: 获取 context 对象

    • useReducer: 类似于 Redux 思想的实现,但其并不足以替代 Redux,可以理解成一个组件内部的 redux:

      • 并不是持久化存储,会随着组件被销毁而销毁;
      • 属于组件内部,各个组件是相互隔离的,单纯用它并无法共享数据;
      • 配合useContext`的全局性,可以完成一个轻量级的 Redux;(easy-peasy)
    • useCallback: 缓存回调函数,避免传入的回调每次都是新的函数实例而导致依赖组件重新渲染,具有性能优化的效果;

    • useMemo: 用于缓存传入的 props,避免依赖的组件每次都重新渲染;

    • useRef: 获取组件的真实节点;

    • useLayoutEffect

      • DOM更新同步钩子。用法与useEffect类似,只是区别于执行时间点的不同
      • useEffect属于异步执行,并不会等待 DOM 真正渲染后执行,而useLayoutEffect则会真正渲染后才触发;
      • 可以获取更新后的 state;
    1. 自定义钩子(useXxxxx): 基于 Hooks 可以引用其它 Hooks 这个特性,我们可以编写自定义钩子,如上面的useMounted。又例如,我们需要每个页面自定义标题:
    function useTitle(title) {
      useEffect(
        () => {
          document.title = title;
        });
    }
    
    // 使用:
    function Home() {
        const title = '我是首页'
        useTitle(title)
    
        return (
            <div>{title}</div>
        )
    }
    

    React Hooks 的限制

    _improve-3_第37张图片

    • 不要在循环、条件嵌套函数中调用 Hook
    • 在 React 的函数组件中调用 Hook

    那为什么会有这样的限制呢?就得从 Hooks 的设计说起。Hooks 的设计初衷是为了改进 React 组件的开发模式。在旧有的开发模式下遇到了三个问题。

    • 组件之间难以复用状态逻辑。过去常见的解决方案是高阶组件、render props 及状态管理框架。
    • 复杂的组件变得难以理解。生命周期函数与业务逻辑耦合太深,导致关联部分难以拆分。
    • 常见的有 this 的问题,但在 React 团队中还有类难以优化的问题,他们希望在编译优化层面做出一些改进。

    这三个问题在一定程度上阻碍了 React 的后续发展,所以为了解决这三个问题,Hooks 基于函数组件开始设计。然而第三个问题决定了 Hooks 只支持函数组件。

    那为什么不要在循环、条件或嵌套函数中调用 Hook 呢?因为 Hooks 的设计是基于数组实现。在调用时按顺序加入数组中,如果使用循环、条件或嵌套函数很有可能导致数组取值错位,执行错误的 Hook。当然,实质上 React 的源码里不是数组,是链表

    这些限制会在编码上造成一定程度的心智负担,新手可能会写错,为了避免这样的情况,可以引入 ESLint 的 Hooks 检查插件进行预防。

    useEffect 与 useLayoutEffect 区别在哪里

    _improve-3_第38张图片

    • 它们的共同点很简单,底层的函数签名是完全一致的,都是调用的 mountEffectImpl,在使用上也没什么差异,基本可以直接替换,也都是用于处理副作用。
    • 那不同点就很大了,useEffect 在 React 的渲染过程中是被异步调用的,用于绝大多数场景,而 LayoutEffect 会在所有的 DOM 变更之后同步调用,主要用于处理 DOM 操作、调整样式、避免页面闪烁等问题。也正因为是同步处理,所以需要避免在 LayoutEffect 做计算量较大的耗时任务从而造成阻塞。
    • 在未来的趋势上,两个 API 是会长期共存的,暂时没有删减合并的计划,需要开发者根据场景去自行选择。React 团队的建议非常实用,如果实在分不清,先用 useEffect,一般问题不大;如果页面有异常,再直接替换为 useLayoutEffect 即可。

    ----------@----------

    受控组件和非受控组件

    <FInput value = {x} onChange = {fn} /> 
    // 上面的是受控组件 下面的是非受控组件
    <FInput defaultValue = {x} />
    
    • 当你一个组件同时传递一个value以及onChange事件时,它就是一个受控组件,收入输出都是我来控制的。
    • 第二个只是传递了默认的初时值,并没有传onchange事件,
    • 非受控组件是一种反模式,它的值不受组件自身的state或props控制

    ----------@----------

    如何避免ajax数据请求重新获取

    一般而言,ajax请求的数据都放在redux中存取。

    ----------@----------

    组件之间通信

    • 父子组件通信
    • 自定义事件
    • redux和context

    context如何运用

    • 父组件向其下所有子孙组件传递信息
    • 如一些简单的信息:主题、语言
    • 复杂的公共信息用redux

    在跨层级通信中,主要分为一层或多层的情况

    • 如果只有一层,那么按照 React 的树形结构进行分类的话,主要有以下三种情况:父组件向子组件通信子组件向父组件通信以及平级的兄弟组件间互相通信
    • 在父与子的情况下 ,因为 React 的设计实际上就是传递 Props 即可。那么场景体现在容器组件与展示组件之间,通过 Props 传递 state,让展示组件受控。
    • 在子与父的情况下 ,有两种方式,分别是回调函数与实例函数。回调函数,比如输入框向父级组件返回输入内容,按钮向父级组件传递点击事件等。实例函数的情况有些特别,主要是在父组件中通过 React 的 ref API 获取子组件的实例,然后是通过实例调用子组件的实例函数。这种方式在过去常见于 Modal 框的显示与隐藏
    • 多层级间的数据通信,有两种情况 。第一种是一个容器中包含了多层子组件,需要最底部的子组件与顶部组件进行通信。在这种情况下,如果不断透传 Props 或回调函数,不仅代码层级太深,后续也很不好维护。第二种是两个组件不相关,在整个 React 的组件树的两侧,完全不相交。那么基于多层级间的通信一般有三个方案。
      • 第一个是使用 React 的 Context API,最常见的用途是做语言包国际化
      • 第二个是使用全局变量与事件。
      • 第三个是使用状态管理框架,比如 Flux、Redux 及 Mobx。优点是由于引入了状态管理,使得项目的开发模式与代码结构得以约束,缺点是学习成本相对较高

    _improve-3_第39张图片

    ----------@----------

    类组件与函数组件有什么区别呢?

    • 作为组件而言,类组件与函数组件在使用与呈现上没有任何不同,性能上在现代浏览器中也不会有明显差异
    • 它们在开发时的心智模型上却存在巨大的差异。类组件是基于面向对象编程的,它主打的是继承、生命周期等核心概念;而函数组件内核是函数式编程,主打的是 immutable、没有副作用、引用透明等特点。
    • 之前,在使用场景上,如果存在需要使用生命周期的组件,那么主推类组件;设计模式上,如果需要使用继承,那么主推类组件。
    • 但现在由于 React Hooks 的推出,生命周期概念的淡出,函数组件可以完全取代类组件。
    • 其次继承并不是组件最佳的设计模式,官方更推崇“组合优于继承”的设计概念,所以类组件在这方面的优势也在淡出。
    • 性能优化上,类组件主要依靠 shouldComponentUpdate 阻断渲染来提升性能,而函数组件依靠 React.memo 缓存渲染结果来提升性能。
    • 从上手程度而言,类组件更容易上手,从未来趋势上看,由于React Hooks 的推出,函数组件成了社区未来主推的方案。
    • 类组件在未来时间切片与并发模式中,由于生命周期带来的复杂度,并不易于优化。而函数组件本身轻量简单,且在 Hooks 的基础上提供了比原先更细粒度的逻辑组织与复用,更能适应 React 的未来发展。

    _improve-3_第40张图片

    ----------@----------

    如何设计React组件

    React 组件应从设计与工程实践两个方向进行探讨

    从设计上而言,社区主流分类的方案是展示组件与灵巧组件

    • 展示组件内部没有状态管理,仅仅用于最简单的展示表达。展示组件中最基础的一类组件称作代理组件。代理组件常用于封装常用属性、减少重复代码。很经典的场景就是引入 Antd 的 Button 时,你再自己封一层。如果未来需要替换掉 Antd 或者需要在所有的 Button 上添加一个属性,都会非常方便。基于代理组件的思想还可以继续分类,分为样式组件与布局组件两种,分别是将样式与布局内聚在自己组件内部。
    • 从工程实践而言,通过文件夹划分的方式切分代码。我初步常用的分割方式是将页面单独建立一个目录,将复用性略高的 components 建立一个目录,在下面分别建立 basic、container 和 hoc 三类。这样可以保证无法复用的业务逻辑代码尽量留在 Page 中,而可以抽象复用的部分放入 components 中。其中 basic 文件夹放展示组件,由于展示组件本身与业务关联性较低,所以可以使用 Storybook 进行组件的开发管理,提升项目的工程化管理能力

    _improve-3_第41张图片

    ----------@----------

    组件的协同及(不)可控组件

    为什么要进行组件的协同

    • 我们在实际的开发项目的时候,不会只用几个组件,有时候遇到大型的项目,可能会有成千上百的组件,难免会遇到有功能重复的组件。要进行修改,就会修改大部分的文件。所以我们需要进行组件的协同开发。

    _improve-3_第42张图片

    什么是组件的协同使用?

    • 组件的协同本质上是对组件的一种组织、管理的方式。
    • 目的:
      • 逻辑清晰:这是组件与组件之间的逻辑
      • 代码模块化
      • 封装细节:像面向对象一样将常用的方法以及数据封装起来
      • 提高代码的复用性:因为是组件,相当于一个封装好的东西,用的时候直接调用

    如何实现组件的协同使用

    • 第一种:增加一个父组件,将其他的组件进行嵌套,更多的是实现代码的封装
    • 第二种:通过一些操作从后台获取数据,React中的Mixin,更多的是实现代码的复用

    组件嵌套的含义

    • 组件嵌套的本质是父子关系

    _improve-3_第43张图片

    组件嵌套的优缺点

    • 优点:
      • 逻辑清晰:父子关系类似于人类中的父子关系
      • 模块化开发:每个模块对应一个功能,不同的模块可以同步开发
      • 封装细节:开发者必须要关注组件的功能,不需要了解细节
    • 缺点:
      • 编写难度高:父子组件的关系需要经过深思熟虑,贸然编写可能导致关系混乱,代码难以维护
      • 无法掌握所有细节:使用者只知道组件的用法,不知道实现细节,遇到问题难以修复

    Mixin

    Mixin的含义

    • Mixin=一组方法
    • 他的目的是横向抽离出组件的相似代码,把组件的共同作用以及效果的代码提出来

    _improve-3_第44张图片

    Mixin的优缺点

    • 优点
      • 代码复用:抽离出通用的代码,减少开发成本,提高开发效率
      • 即插即用:可以使用许多现有的Mixin来开发自己的代码
      • 适应性强:改动一次代码,影响多个组件
    • 缺点
      • 编写难度高:Mixin可能被用在各种环境中,想要兼容多种环境就需要更多的 - 码与逻辑,通用的代价是提高复杂度
      • 降低代码的可读性:组件的优势在于将逻辑与是界面直接结合在一起,Mixin本质上会分散逻辑,理解起来难度大

    不可控组件

    • 上图:defaultValue的值是固定的,这就是一个不可控组件
    • 如果要获取inputvalue值,只有使用ref获取节点来获取值

    可控组件

    • defaultValue的值是根据状态确定了,只需要拿到this.state.value的值就可以了
    • 这里需要注意一下:使用value的值是不可修改的,defaultValue的值是可以修改的

    可控组件的优点

    • 符合React的数据流
    • 数据存储在state中,便于获取
    • 便于处理数据

    ----------@----------

    React-Router 的实现原理及工作方式分别是什么

    • React Router 路由的基础实现原理分为两种,如果是切换 Hash 的方式,那么依靠浏览器 Hash 变化即可;如果是切换网址中的 Path,就要用到 HTML5 History API 中的 pushStatereplaceState 等。在使用这个方式时,还需要在服务端完成 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 等。

    _improve-3_第45张图片

    _improve-3_第46张图片

    ----------@----------

    React 17 带来了哪些改变

    最重要的是以下三点:

    • 新的 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 预解析

    • DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP
    <link rel="dns-prefetch" href="//blog.poetries.top">
    

    缓存

    • 缓存对于前端性能优化来说是个很重要的点,良好的缓存策略可以降低资源的重复加载提高网页的整体加载速度
    • 通常浏览器缓存策略分为两种:强缓存和协商缓存

    强缓存

    实现强缓存可以通过两种响应头实现:ExpiresCache-Control 。强缓存表示在缓存期间不需要请求,state code200

    Expires: Wed, 22 Oct 2018 08:41:00 GMT
    

    ExpiresHTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT 后过期,需要再次请求。并且 Expires 受限于本地时间,如果修改了本地时间,可能会造成缓存失效

    Cache-control: max-age=30
    

    Cache-Control 出现于 HTTP / 1.1,优先级高于 Expires 。该属性表示资源会在 30 秒后过期,需要再次请求

    协商缓存

    • 如果缓存过期了,我们就可以使用协商缓存来解决问题。协商缓存需要请求,如果缓存有效会返回 304
    • 协商缓存需要客户端和服务端共同实现,和强缓存一样,也有两种实现方式

    Last-ModifiedIf-Modified-Since

    • Last-Modified 表示本地文件最后修改日期,If-Modified-Since 会将 Last-Modified的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来
    • 但是如果在本地打开缓存文件,就会造成 Last-Modified 被修改,所以在 HTTP / 1.1 出现了 ETag

    ETagIf-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 / 2.0

    • 因为浏览器会有并发请求限制,在 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">
    
    • 预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染

    总结

    • deferasync在网络读取的过程中都是异步解析
    • defer是有顺序依赖的,async只要脚本加载完后就会执行
    • preload 可以对当前页面所需的脚本、样式等资源进行预加载
    • prefetch 加载的资源一般不是用于当前页面的,是未来很可能用到的这样一些资源

    懒执行与懒加载

    懒执行

    • 懒执行就是将某些逻辑延迟到使用时再计算。该技术可以用于首屏优化,对于某些耗时逻辑并不需要在首屏就使用的,就可以使用懒执行。懒执行需要唤醒,一般可以通过定时器或者事件的调用来唤醒

    懒加载

    • 懒加载就是将不关键的资源延后加载

    懒加载的原理就是只加载自定义区域(通常是可视区域,但也可以是即将进入可视区域)内需要加载的东西。对于图片来说,先设置图片标签的 src 属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为 src 属性,这样图片就会去下载资源,实现了图片懒加载

    • 懒加载不仅可以用于图片,也可以使用在别的资源上。比如进入可视区域才开始播放视频等

    文件优化

    图片优化

    对于如何优化图片,有 2 个思路

    • 减少像素点
    • 减少每个像素点能够显示的颜色

    图片加载优化

    • 不用图片。很多时候会使用到很多修饰类图片,其实这类修饰图片完全可以用 CSS 去代替。
    • 对于移动端来说,屏幕宽度就那么点,完全没有必要去加载原图浪费带宽。一般图片都用 CDN 加载,可以计算出适配屏幕的宽度,然后去请求相应裁剪好的图片
    • 小图使用 base64格式
    • 将多个图标文件整合到一张图片中(雪碧图)
    • 选择正确的图片格式:
      • 对于能够显示 WebP 格式的浏览器尽量使用 WebP 格式。因为 WebP 格式具有更好的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量,缺点就是兼容性并不好
      • 小图使用 PNG,其实对于大部分图标这类图片,完全可以使用 SVG 代替
      • 照片使用 JPEG

    其他文件优化

    • CSS文件放在 head
    • 服务端开启文件压缩功能
    • script 标签放在 body 底部,因为 JS 文件执行会阻塞渲染。当然也可以把 script 标签放在任意位置然后加上 defer ,表示该文件会并行下载,但是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS文件可以加上 async ,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。 执行 JS代码过长会卡住渲染,对于需要很多时间计算的代码
    • 可以考虑使用 WebworkerWebworker可以让我们另开一个线程执行脚本而不影响渲染。

    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发起一个请求

    如何根据chrome的timing优化

    性能优化API

    • Performanceperformance.now()new Date()区别,它是高精度的,且是相对时间,相对于页面加载的那一刻。但是不一定适合单页面场景
    • window.addEventListener("load", ""); window.addEventListener("domContentLoaded", "");
    • Imgonload事件,监听首屏内的图片是否加载完成,判断首屏事件
    • RequestFrameAnmationRequestIdleCallback
    • IntersectionObserverMutationObserverPostMessage
    • Web Worker,耗时任务放在里面执行

    检测工具

    • Chrome Dev Tools
    • Page Speed
    • Jspref

    前端指标

    _improve-3_第47张图片

    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请求耗时

    • 不请求,用cache(最好的方式就是尽量引用公共资源,同时设置缓存,不去重新请求资源,也可以运用PWA的离线缓存技术,可以帮助wep实现离线使用)
    • 前端打包时压缩
    • 服务器上的zip压缩
    • 图片压缩(比如tiny),使用webp等高压缩比格式
    • 把过大的包,拆分成多个较少的包,防止单个资源耗时过大
    • 同一时间针对同一域名下的请求有一定数量限制,超过限制数目的请求会被阻塞。如果资源来自于多个域下,可以增大并行请求和下载速度
    • 延迟、异步、预加载、懒加载
    • 对于非首屏的资源,可以使用 defer 或 async 的方式引入
    • 也可以按需加载,在逻辑中,只有执行到时才做请求
    • 对于多屏页面,滚动时才动态载入图片

    移动端优化

    _improve-3_第48张图片

    1. 概述

    • PC优化手段在Mobile侧同样适用
    • Mobile侧我们提出三秒种渲染完成首屏指标
    • 基于第二点,首屏加载3秒完成或使用Loading
    • 基于联通3G网络平均338KB/s(2.71Mb/s),所以首屏资源不应超过1014KB
    • Mobile侧因手机配置原因,除加载外渲染速度也是优化重点
    • 基于第五点,要合理处理代码减少渲染损耗
    • 基于第二、第五点,所有影响首屏加载和渲染的代码应在处理逻辑中后置
    • 加载完成后用户交互使用时也需注意性能

    2. 加载优化

    加载过程是最为耗时的过程,可能会占到总耗时的80%时间,因此是优化的重点

    2.1 缓存

    使用缓存可以减少向服务器的请求数,节省加载时间,所以所有静态资源都要在服务器端设置缓存,并且尽量使用长Cache(长Cache资源的更新可使用时间戳)

    2.2 压缩HTML、CSS、JavaScript

    减少资源大小可以加快网页显示速度,所以要对HTMLCSSJavaScript等进行代码压缩,并在服务器端设置GZip

    • a) 压缩(例如,多余的空格、换行符和缩进)
    • b) 启用GZip

    2.3 无阻塞

    写在HTML头部的JavaScript(无异步),和写在HTML标签中的Style会阻塞页面的渲染,因此CSS放在页面头部并使用Link方式引入,避免在HTML标签中写StyleJavaScript放在页面尾部或使用异步方式加载

    2.4 使用首屏加载

    首屏的快速显示,可以大大提升用户对页面速度的感知,因此应尽量针对首屏的快速显示做优化。

    2.5 按需加载

    将不影响首屏的资源和当前屏幕资源不用的资源放到用户需要时才加载,可以大大提升重要资源的显示速度和降低总体流量。

    PS:按需加载会导致大量重绘,影响渲染性能

    • a) LazyLoad
    • b) 滚屏加载
    • c) 通过Media Query加载

    2.6 预加载

    大型重资源页面(如游戏)可使用增加Loading的方法,资源加载完成后再显示页面。但Loading时间过长,会造成用户流失。

    对用户行为分析,可以在当前页加载下一页资源,提升速度。

    • a)可感知Loading
    • b)不可感知的Loading(如提前加载下一页)

    2.7 压缩图片

    图片是最占流量的资源,因此尽量避免使用他,使用时选择最合适的格式(实现需求的前提下,以大小判断),合适的大小,然后使用智图压缩,同时在代码中用Srcset来按需显示

    PS:过度压缩图片大小影响图片显示效果

    • a)使用智图( http://zhitu.tencent.com/ )
    • b)使用其它方式代替图片(1. 使用CSS3 2. 使用SVG 3. 使用IconFont
    • c)使用Srcset
    • d)选择合适的图片(1. webP优于JPG2. PNG8优于GIF
    • e)选择合适的大小(1. 首次加载不大于1014KB 2. 不宽于640(基于手机屏幕一般宽度))

    2.8 减少Cookie

    Cookie会影响加载速度,所以静态资源域名不使用Cookie

    2.9 避免重定向

    重定向会影响加载速度,所以在服务器正确设置避免重定向。

    2.10 异步加载第三方资源

    第三方资源不可控会影响页面的加载和显示,因此要异步加载第三方资源

    2.11 减少HTTP请求

    因为手机浏览器同时响应请求为4个请求(Android支持4个,iOS 5后可支持6个),所以要尽量减少页面的请求数,首次加载同时请求数不能超过4个

    • a)合并CSSJavaScript
    • b)合并小图片,使用雪碧图

    3. 三、脚本执行优化

    脚本处理不当会阻塞页面加载、渲染,因此在使用时需当注意

    • CSS写在头部,JavaScript写在尾部或异步
    • 避免图片和iFrame等的空Src,空Src会重新加载当前页面,影响速度和效率。
    • 尽量避免重设图片大小
    • 重设图片大小是指在页面、CSS、JavaScript等中多次重置图片大小,多次重设图片大小会引发图片的多次重绘,影响性能
    • 图片尽量避免使用DataURLDataURL图片没有使用图片的压缩算法文件会变大,并且要解码后再渲染,加载慢耗时长

    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 减少重绘和回流

    • 避免不必要的Dom操作
    • 尽量改变Class而不是Style,使用classList代替className
    • 避免使用document.write
    • 减少drawImage

    5.2 TOUCH事件优化

    使用touchstarttouchend代替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 高频事件优化

    TouchmoveScroll 事件可导致多次渲染

    • 使用requestAnimationFrame监听帧变化,使得在正确的时间进行渲染
    • 增加响应变化的时间间隔,减少重绘次数

    6.4 GPU加速

    CSS中以下属性(CSS3 transitionsCSS3 3D transformsOpacityCanvasWebGLVideo)来触发GPU渲染,请合理使用

    ----------@----------

    工程化

    介绍一下 webpack 的构建流程

    核心概念

    • 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 中可包含一到多个 chunk
    • bundling:webpack 打包的过程
    • asset/bundle:打包产物

    webpack 的打包思想可以简化为 3 点:

    • 一切源代码文件均可通过各种 Loader 转换为 JS 模块 (module),模块之间可以互相引用。
    • webpack 通过入口点(entry point)递归处理各模块引用关系,最后输出为一个或多个产物包 js(bundle) 文件。
    • 每一个入口点都是一个块组(chunk group),在不考虑分包的情况下,一个 chunk group 中只有一个 chunk,该 chunk 包含递归分析后的所有模块。每一个 chunk 都有对应的一个打包后的输出文件(asset/bundle

    _improve-3_第49张图片

    打包流程

    1. 初始化参数:从配置文件和 Shell 语句中读取并合并参数,得出最终的配置参数。
    2. 开始编译:从上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译。
    3. 确定入口:根据配置中的 entry 找出所有的入口文件。
    4. 编译模块:从入口文件出发,调用所有配置的 loader 对模块进行翻译,再找出该模块依赖的模块,这个步骤是递归执行的,直至所有入口依赖的模块文件都经过本步骤的处理。
    5. 完成模块编译:经过第 4 步使用 loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系。
    6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 chunk,再把每个 chunk 转换成一个单独的文件加入到输出列表,这一步是可以修改输出内容的最后机会。
    7. 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。

    _improve-3_第50张图片

    简版

    • Webpack CLI 启动打包流程;
    • 载入 Webpack 核心模块,创建 Compiler 对象;
    • 使用 Compiler 对象开始编译整个项目;
    • 从入口文件开始,解析模块依赖,形成依赖关系树;
    • 递归依赖树,将每个模块交给对应的 Loader 处理;
    • 合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

    在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到相关事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果

    构建流程核心概念:

    • Tapable:一个基于发布订阅的事件流工具类,CompilerCompilation 对象都继承于 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详细工作流程

    _improve-3_第51张图片
    ----------@----------

    介绍 Loader

    常用 Loader:

    • file-loader: 加载文件资源,如 字体 / 图片 等,具有移动/复制/命名等功能;
    • url-loader: 通常用于加载图片,可以将小图片直接转换为 Date Url,减少请求;
    • babel-loader: 加载 js / jsx 文件, 将 ES6 / ES7 代码转换成 ES5,抹平兼容性问题;
    • ts-loader: 加载 ts / tsx 文件,编译 TypeScript;
    • style-loader: 将 css 代码以