9分钟了解 <Prompt> 是如何拦截浏览器默认行为的

441 阅读5分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

前言

我们都见过离开前页面前的提示弹窗,比如"您确定要离开当前页面吗?"或者 提示是”当前设定修改后未保存,是否确定离开"等等, 推测肯定是拦截了浏览器的回退功能,但很好奇具体是怎么实现的?现在,就让我们像剥洋葱一样一层一层剥开它的心~~

一、使用方式

  • 先看下基本使用
// users.js
import React from "react"
import { Prompt } from "react-router";

const Users = () => {
   return (
       <div>
           <div style={{color: '#990', fontSize: '40px'}}> users 页面</div>
           <Prompt message="你真的要离开吗?" />
       </div>
   );
}

export default Users

当点击回退按钮之后,页面出现提示

16625389182767.jpg

  • 参数说明
export interface PromptProps {
   message: string | ((location: H.Location, action: H.Action) => string | boolean);
   when?: boolean | undefined;
}
export class Prompt extends React.Component<PromptProps, any> {}

可以看到,<Prompt>只接收两个参数,一个必传的message,就是弹窗消息内容,message本身既可以是string(上例),还可以是个函数;另一个可选的when,可以控制出现提示的触发时机,接收布尔值或undefined

二、剥源码

2.1 一睹为快

下面是react-router V5.3.3的<prompt>源码

16625459809335.jpg 可以看到,<Prompt>组件使用订阅消费模式,拿到context对象,然后<Lifecycle>组件传入了从context传过来的history.block方法,最后在不同的生命周期调用。看起来就是这么简单!嗯,看起来~

洞悉的笑.jpeg

上图标注的四点,是影响我们彻底理解的四点,慢慢来剥。

2.2 【1】RouterContext.Consumer 和 【2】Context

<ReactContext.Consumer>{context => {...}</ReactContext.Consumer> 其实就是react 原生的React.createContext ————一个使用订阅消费模式实现的跨组件共享数据机制。

Consumer就一定会有Provider,凭借修炼已久的顺藤摸瓜大法,在Router.js中我找了它

16625432098625.jpg
其中,RouterContext.Provider的value值,就是<Prompt>中的context。 我们再来看下这里:

16625446904114.jpg (warning除外)用到context的地方只有两处:

  • context.staticContext 主要用于测试和 服务器渲染场景,上面那行if判断也能看出来,如果when是false 或者 context.staticContext 是 true是不会拦截的,想了解的童鞋也可自行看下<StaticRouter>组件
  • context.history.block 这里才是拦截的实现,下面单独来看

2.3【3】context.history.block

  • 首先,在 history 里找到这个方法(当前用的是V4.10.1版本)

16626183043619.jpg

  • 然后看下它的实现(createBrowserHistorycreateHashHistory 是一模一样的实现)

16626278012943.jpg 可以看到,block的代码里关键是标注的[1][2]

  • 先看下 【1】createTransitionManager

16626280320326.jpg
由于上面block里用到的是这个方法setPrompt,其余干扰代码我就删掉了,这简单的几行代码里做了三件事:

  1. 一是 history一次只支持传入一个prompt的提示,warning函数第一个参数为false的时候触发提示。
  2. 二是 在调用setPrompt的时候通过闭包给外层的prompt赋值为传入的提示内容
  3. 三是 setPrompt返回一个函数,可在调用这个返回函数的时候,把变量prompt重新置为null。(所以,前面block的第一行,会给返回函数起名为unblock
  • 再看[2] checkDOMListeners 这个方法嵌套的调用有点多(关于这个方法,后面我单独写一篇,感兴趣的童鞋,敬请期待),只能放部分思路代码了

16626333906842.jpg checkDOMListeners 这个函数事实上做了两件事:

  1. 用一个listenerCount的值是0还是1来控制监听/取消相应事件
  2. 在触发BrowserHistory的时候如果同时触发了HashHistory,也要监听/取消相应事件。

这里我们以看上面圈出来的方法handlePopState为代表

16626334694335.jpg
先是一个忽略 WebKit 中无关的 popstate 事件的判断,然后调用了一个handlePop(getDOMLocation(event.state)), getDOMLocation(event.state)返回的是location对象(不再展开细说),我们看handlePop的实现:

16626338826053.jpg
第一次调用的时候会走到else逻辑,关键方法是t ransitionManager.confirmTransitionTo,坚持✊一下,马上就能看到曙光了~,继续看这个方法:

16626342835373.jpg
该方法里面的prompt就是我们使用时传入的提示内容,比如是个string——您确定要离开当前页面吗?,那标注[1]处的result此时就是这个字符串。而标注[2]处的getUserConfirmation长这个样子,终于见到庐山真面目了~

16626344952725.jpg
这个函数的第二参数是个回调,参数是window.confirm(message),这里就直接看出来了,浏览器拦截弹窗用的是window.confirm而非window.alert。而在getUserConfirmation(result, callback);调用中的callBackconfirmTransitionTo的第四个参数:

16626348928633.jpg
如果在拦截弹窗里点true,那就跳走,否则走revertPop

16626350031099.jpg

至此,history相关内容告一段落,接下来,我们回到Prompt代码,看下标注的最后一点【4】Lifecycle

2.4 【4】Lifecycle

有了上面的铺垫,<Lifecycle>就是洒洒水啦~,组件本身也非常简单,直接上代码:

16626901390927.jpg 可以看到,就是继承了React.Component的三个生命周期,并在各自的生命周期里调用从props传入的方法,返回一个null
再来回忆下我们Prompt里这段代码:

16626898782580.jpg
他调的method就是history.block, message就是我们的提示内容。onMount、onUpdate、onUnmount分别对应React.componentcomponentDidMount、componentDidUpdate、componentWillUnmount

再做下简单解释,当onMount的时候会执行method(message),并把它挂到self.releas上,等到update的时候,判断下前后两次的message是否一致,不一致的话, 执行selef.relase(),(也即是unblock---history.block的返回值),然后再执行一遍self.release = method(message),最后就是Unmount的卸载了。

细心的童鞋可能会发现,<Lifecycle>明明没有接收message参数,为什么<Prompt>还要传个message={message}呢?是不是去掉也可以呢?
答案是~ 当然不可以,<Lifecycle>的确明明没有接收message,但是他暗戳戳的接收了呀,在componentDidUpdate里有个prevProps,就是用于获取上次的message的。

结语

俗话说,一图胜千言,我们就来画个简图:

16626948287966.jpg

最后,祝大家月饼节快乐,希望感觉有一丢丢收获的童鞋可以不吝赐赞,快乐加倍🦆!!。像我一样因为疫情回不了家的,也要好好犒劳自己一顿😄。

白眼.jpeg