Giter Site home page Giter Site logo

blog's Introduction

Hi there!

Hey! I am mysterven, I'd like to do some open source stuff. 👋👋

Find me on Discord : mysteryven.

blog's People

Contributors

mysteryven avatar

Stargazers

 avatar

Watchers

 avatar  avatar

blog's Issues

手写 redux

今天带大家从 0 到 1 实现一个在 React 中使用的 Hooks 版 Redux。Redux 的原理比较简单,源码读起来对大家来说都不会有难度,如果大家有时间,可以自己看一下。

尽管源码算是比较简单的,落实到亲自动手实现还是有一点难度的。如果你也有兴趣,不妨试一试。这可以让你更深刻的理解、记忆整个逻辑。

我们今天讲述的内容是来自于 redux 和 react-redux 这两个库。

虽然 Redux 并不是一个必须要配合 React 使用的状态管理库,但是它和 React 的结合是最常见的。

我们今天的计划:先不绑定任何框架,把 Redux 基本的 API 实现;然后再基于在 React 上的使用,继续完善。最终实现的结果就是一个可用的状态管理器。

基础实现

在开始之前,我们得先看一个例子,看它怎么在原生 JS 应用下使用。这有助于我们理解 Redux 最原始的 API。毕竟在 React 中使用也是继续在这个层次上封装。

且看下面这段代码,我是从官网拷贝过来的。网络允许的话,可以在线预览

代码有点多,好在逻辑比较简单。大家重点关注 createStore 方法创造出来的 store:

  1. 使用 store.dispatch 去派发一个 action
  2. 可以使用 store.getState() 拿到最新的值
  3. 视图层使用 store.subscribe 订阅更新,等计算出新的值会调用订阅了更新的函数

image.png

import { createStore } from 'redux'

function counter(state, action) {
  if (typeof state === 'undefined') {
    return 0
  }

  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
}

const store = createStore(counter)
const valueEl = document.getElementById('value')

function render() {
  valueEl.innerHTML = store.getState().toString()
}

render()
store.subscribe(render)

document.getElementById('increment')
  .addEventListener('click', function () {
    store.dispatch({ type: 'INCREMENT' })
  })

document.getElementById('decrement')
  .addEventListener('click', function () {
    store.dispatch({ type: 'DECREMENT' })
  })

document.getElementById('incrementIfOdd')
  .addEventListener('click', function () {
    if (store.getState() % 2 !== 0) {
      store.dispatch({ type: 'INCREMENT' })
    }
  })

document.getElementById('incrementAsync')
  .addEventListener('click', function () {
    setTimeout(function () {
      store.dispatch({ type: 'INCREMENT' })
    }, 1000)
  })

明白了基本的使用,现在我们就来实现 createStore 方法:

入参是一个 reducer 对象,首先我们先定义入参类型:

export interface Action<T = any> {
    type: T
}

export interface AnyAction extends Action {
    [extraProps: string]: any
}

type Reducer<S = any, A extends Action = AnyAction> = (
    state: S | undefined,
    action: A
) => S

刚才我们也介绍了,createStore 返回的结果是一个 store 对象,来定义一下它的类型:

export interface Store<S = any, A extends Action = AnyAction> {
    dispatch: Dispatch<A>;
    getState: () => S;
    subscribe(listener: () => void): Unsubscribe 
    // 返回值是取消订阅的函数,等会看怎么用,先写着
}

export interface Unsubscribe {
    (): void
}

接下来我们来实现 createStore 内部的逻辑。

我们知道,Redux 采用的是单项数据流,当我们使用 dispatch 函数派发一个 action 的时候,它会把这个 action 入参给 reducer 函数,reducer 函数产生的返回值会作为新的 state。

ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

也就是说,dispatch 函数的原理很简单,就是调用 reducer 产生新的 state ,如下:

function dispatch(action) => {
  state = reducer(state, action);
}

那怎么解决数据更新完了通知 UI 的功能呢?我们可以在内部维护一个订阅者数组,初期把需要更新视图的函数加入进来。

当我们新的 state 产生完了,可以通知所有的订阅者,这就达到了更新视图的功能。添加订阅者就是通过 subscribe 函数。当组件注销的时候,我们就不必在通知了,就可以注销掉他们。

下面是整体的代码:

const randomString = () =>
    Math.random().toString(36).substring(7).split('').join('.')

const ActionTypes = {
    INIT: `@@redux/INIT${/* #__PURE__ */ randomString()}`,
}

export default function createStore<S, A extends Action>(reducer: Reducer<S, A>) {
    let state: S;
    let listeners: (() => void)[] = []

    // 初始化派发一下不存在的 action,作用是为了让 state 不为空
    // 这也就是我们必须要在 reducer 写 default 分支的原因。
    dispatch({ type: ActionTypes.INIT } as A)

    function dispatch(action: A) {
        state = reducer(state, action);

        for (let i = 0; i < listeners.length; i++) {
            const listener = listeners[i]
            listener()
        }

        return action;
    }

    function subscribe(listener: () => void): Unsubscribe {
        // 添加订阅者
        listeners.push(listener);

        return () => {
            // 返回的是取消订阅的函数,在销毁组件的时候用
            const index = listeners.indexOf(listener)
            listeners.splice(index, 1)
        }
    }

    return {
        getState() { return state; },
        dispatch,
        subscribe
    }
}

真实的 createStore 依次接受三个参数:reducer、preloadedState、enhancer。

第一个我们刚才已经讲述了。第二个其实是为我们的 state 赋一个初始值,第三个,字面意思,就是增强 store,最常用的就是使用中间件。可能听起来比较高端,其实不复杂。

举一个例子,我们想每次调用 dispatch 方法的时候都打印一下日志,那可以这么去做:

// 省略前面的代码...
// 这是我们的 createStore 方法
const newDispatch = function log(action) => {
    console.log('日志')
    dispatch(action)
}; 

return {
    getState() { return state; },
    dispatch: newDispatch,
    subscribe
}

硬编码进肯定不合适,为了能让别人自定义的增强我们的 dispatch,可以把入参一个 enhancer,让它来做一些增加 dispatch 的事情:

const newDispatch =  enhancer(dispatch) 

这就是中间件的基本思路。实际实现考虑的细节会多一点,但是无关紧要。

提示:事实上,enhancer 并不是接受一个 dispatch ,它接受的是一组中间件,我这么写只是为了简单的表示它的原理。源代码地址在这里:enhancer 调用位置 。 实际使用中,enhancer 接受的是 applyMiddleware 这个函数的返回值,而它的源码也比较简单,我就直接抛在这里了:点我查看

上面就已经把 Redux 基本的原理都表示出来了,上面的那个例子也完全可以正常运行:

image.png

配合 React 使用

接下来,开始探讨如何能在 React 中更好的使用。如果,我们什么也不改,也可以直接在 React 中使用,但是需采用类似下面这种写法:

const store = createStore(counter)
const rootEl = document.getElementById('root')

const render = () => ReactDOM.render(
    <Counter
        value={store.getState()}
        onIncrement={() => store.dispatch({ type: 'INCREMENT' })}
        onDecrement={() => store.dispatch({ type: 'DECREMENT' })}
    />,
    rootEl
)

render()
store.subscribe(render)

有什么问题呢?就在于 Counter 组件的入参 value,每次更新后,它拿到的都是一个新值,这就会使它及其它的子组件重新渲染。

除此之外,目前我们只是把 store 的值挂载到了顶级父组件上,子组件想使用的话,还是得通过 props 获取。当组件层级深了之后,这无疑很麻烦。

如何解决跨级传递难的问题呢?解决方案想必大家也都知道了,使用 context。这样就能在任意层级的子组件里取到值了。

Redux 中的使用是这样子的:

<Provider store={store}>
    <Todos/>
</Provider>

在 Redux 中,Provider 还可以接受自定义 context。为了方便起见,我们这里实现的 Provider 函数就不接受指定自己的 context 了。我们直接在全局直接声明了一个 ReactReduxContext ,后面取值都会在这个 context 下面操作。

请你放心,原理都是一样的。

先根据用法实现一个最基础的版本:

interface ReactReduxContextValue<SS = any,   A extends Action = AnyAction> {
    store: Store<SS, A>
}

// 全局的 context
const ReactReduxContext = React.createContext<ReactReduxContextValue>(null as any);

interface ProviderProps {
    store: Store;
    children: React.ReactNode
}

function Provider({store, children}: ProviderProps) {

    // 组装传递给 context 的值
    const contextValue = {
        store: store
    }

    const Context = ReactReduxContext;

    return (
        <Context.Provider value={contextValue}>
            {children}
        </Context.Provider>
    )
}

export default Provider;

好了,数据我们是已经存好了,但是还有两大问题没有解决:

  1. 子组件怎么取数据
  2. 子组件怎么派发事件

下面我们就来解决这两个问题。解决了这两个问题,我们再去优化性能。

Redux 中是怎么用的呢?我们不采用 connect 高阶函数的方案了,而是采用自定义 Hooks 的方式,写法是这样子的:

export function Counter() {
  // 从 store 中取数据
  const count = useSelector(state => state.counter.value) 
    
  // 派发事件的函数
  const dispatch = useDispatch() 

  return (
      <div>
        <div>
          <button onClick={() => dispatch({ type: 'increment'})}>
            Increment
          </button>
          <span>{count}</span>
          <button onClick={ {type: 'decrement' }}>
            Decrement
          </button>
        </div>
      </div>
  )
}

这段代码并不能直接运行,但是如果使用过 redux 应该就能大概了解它做了什么。

也就是说,我们:

  1. 使用 useDispatch 取到了 store 中的 dispatch 函数
  2. 使用 useSelector 取到了 store 中的 state 的数据

其实,这两个方法的基础实现也非常简单,最开始的时候,我们定义了一个全局的 context,现在它就要派上用场了:

function useDispatch() {
    const contextValue = useContext(ReactReduxContext)
    return contextValue.store.dispatch;
}

function useSelector<TState, Selected extends unknown>(
    selector: (state: TState) => Selected
 ): Selected {
    const contextValue = useContext(ReactReduxContext);
    return selector((contextValue.store.getState()))
}

但是仅仅这样还是不行,我们在派发事件后,没法收到更新。有一种最简单的办法,更新我们 Provider 最上层的 store,把它完全更新成新的,这样,所有读取此 context 的组件都会更新。

但这也是 React 中 context 的问题。

我们往往这样使用 context:

<MyContext.Provider value={/* some value */}>
function Button1() {
  const name = useContext(MyContext);
  
  retur <div>{name}</div>
}

function Button2() {
  const name = useContext(MyContext);
  
  retur <div>{name}</div>
}

当这个情况下, context 中 value 发生变化,使用当前上下文的组件都会重新渲染。

放到我们 Redux 中,如果我们贸然更改了 store 的引用,无疑会引起我们所有用到当前 context 的组件树都会重新渲染,这对性能来说是不能接受的,我们不想要这种效果。

那怎么办呢?想一想,我们是不是仅仅把使用了 useSelector 的组件加入订阅者不就好了?只有当 useSelector 上一次取到的值和这一次取到的值有变化我们才更新。

现在,我们需要完善一下 useSelector 这个自定义 hook 函数了。

function usePrevious<T>(value: T | undefined): T | undefined {
    const ref = useRef<T>();
    useEffect(() => {
        ref.current = value;
    }, [value]);
    return ref.current;
}
function useSelector<TState, Selected extends unknown>(selector: (state: TState) => Selected): Selected {
    const contextValue = useContext(ReactReduxContext);
    const subscribe = contextValue.store.subscribe
    const state = contextValue.store.getState();
    
    const nextSelected = selector(state);
    const prevSelected = usePrevious(nextSelected);

    // 我们使用简单的计数器来触发组件的更新
    // redux 源码中引用的是来自于
    // react 一个内置 hook: `useSyncExternalStore`
    // 地址:https://github.com/facebook/react/blob/ceee524a8f45b97c5fa9861aec3f36161495d2e1/packages/react-reconciler/src/ReactFiberHooks.new.js#L2633
    // 原理基本一致。
    
    const [_, setCount] = useState(0);

    useEffect(() => {
        // 增加当前组件到订阅者列表
        const unsubscribe = subscribe(() => { 
            // 前后两次值不一样,走更新
            if (prevSelected !== nextSelected) { 
                forceUpdate();
            }
        })

        function forceUpdate() {
            setCount((prev) => prev+1);
        }

        // 组件注销的时候,取消订阅
        return () => {
            unsubscribe();
        }
        
    }, [subscribe])

    return nextSelected;
}

到这里我们基本就实现完了。实验一下下面这个例子,也能确实只更新变化的组件:

import React from 'react';
import Provider, {useDispatch, useSelector} from './Provider';
import createStore from './createStore';

interface State {
    count: number
}

const store = createStore<State, any>(counter);

function counter(state: any, action: any) {
    if (typeof state === 'undefined') {
        return {
            count: 0
        };
    }

    switch (action.type) {
        case 'INCREMENT':
            return {
                ...state,
                count: state.count + 1
            };
        default:
            return state;
    }
}


function A() {
    return (
        <Provider store={store}>
            <B/>
            <C/>
        </Provider>
    );
}

function B() {
    console.log('b');
    const count = useSelector<State, number>((state) => state.count);
    const dispatch = useDispatch();
    return (
        <>
            <button onClick={() => dispatch({type: 'INCREMENT'})}>
                +
            </button>
            <div>{count}</div>
        </>
    );
}

function C() {
    console.log('c');
    return <div>hello world</div>;
}

别急,到这里还没完,我们还要再完善一下我们的 Provider 组件,有一种情况会导致我们 Provider 组件的 store 值重新计算,那就是它不在根节点:

function App() {

    return (
        <Provider> ... </Provider>
    )
}

如果我在 App 级别做了更新,会触发Provider 的重新渲染,此时 contextValue 重新生成,我们刚才做到就全白做了。我们希望 Provider 不在根目录,也不让 store 重新渲染,怎么做呢?用 useMemo:

function Provider({store, children}: ProviderProps) {
    const contextValue = useMemo(() => {
        return {
            store
        };
    }, [store]);

    const Context = ReactReduxContext;

    return (
        <Context.Provider value={contextValue}>
            {children}
        </Context.Provider>
    );
}

到这里,我们整个 redux 的核心源码就都涉及到了。不知道是否对你有帮助呢。

http1.1 vs http 2.0 vs http 3.0

http 1.1 的问题

  1. 同时只能保持一个连接,一个发完了才能下一个
  2. 有最大连接数的限制(一个域名只能建立 6 个 TCP 连接)
  3. 请求头部过大

http 1.1 下的优化方案

  1. 雪碧图
  2. 打包工具压缩合并文件
  3. keep-alive
  4. 内联小图片、样式
  5. 把请求分域名,比如用 CDN

http 2.0 优点

  1. 只使用一个 TCP 连接,多个请求可以多路复用
  2. 请求头压缩
  3. 服务端主动推送消息

http 2.0 的缺点

  1. 毕竟是基于 TCP 的,握手过多
  2. TCP 丢包造成重传,引发队头阻塞问题

http 3.0 优点

  1. 没有队头阻塞
  2. 1 个 RTT 就完全握手了

写给前端工程师看的 cookie 教程

正在施工中

预备知识

HTTP 1.x
Node.js
Express.js

基本使用

为了 HTTP 协议的可伸缩性、可扩展性,它被设计成无状态的协议,但是记录用户状态太需要了,于是 cookie 就出来了。那 cookie 是怎么记录用户状态的呢?我们将通过实际的案例来演示这件事。

这一部分,我会用写一个登录接口来看一下 cookie 是怎么记录用户状态的

cookie 是怎么记录登录状态的

安全与防范

通过实际的案例更能理解 cookie 的这些属性有什么作用。毕竟,知其所以然才更好的记住它。

中间人攻击和 cookie 劫持

XSS

CSFR

cookie

前言

我是一个整天只会 CRUD 的页面仔,之前,我始终对形如 XSS 、CSRF 攻击这类的攻击手段很迷惑,没有一个直观的概念,看过文章之后也只是似懂非懂,直到后面把它和 cookie 联系起来,再实操一遍,才算真正的理解了,同时对 cookie 为什么会有那些字段的意义也有了更好的理解。今天,我想把这些分享给你,相信对你也会有帮助 ~

关于 Cookie 的基本知识,MDN 比较详细,我便不在赘述了,本文把重点放在实战上面,让你可以有一个更加直观、深刻的认识。

本篇是第一节,内容比较基础、简单,不过这都是再给下一篇铺垫,下一篇将在今天的基础上讲解安全与防范的内容。

预备知识

为了让你更流畅的阅读,我先帮你简单的回顾或介绍一些前置知识。

cookie

cookie 是服务端发送给客户端的一段数据,并且交由客户端(浏览器)保存。服务端通过 set-cookie 响应头设置,随后客户端的每个请求都会自动携带上 cookie 字段。

响应头的 set-cookie 每次只能携带一个 name/value 格式的名值对,所以,你可能在响应里看到多个 set-cookie 字段。而 cookie 是可以有多个名值对的,所以请求里就一个。

至于 set-cookie、cookie 的具体格式是什么,它们是如何起作用的,将在文章下面的内容结合实例进行讲解。

Node.js

可以使用 readFileSync 读文件,writeFileSync 写文件。

Express.js

我们只会用一点点它的知识,了解官网的 Get Started 章节,了解中间件的使用。

基本使用

为了 HTTP 协议的可伸缩性、可扩展性,它被设计成无状态的协议,但是记录用户状态几乎是每一个网站都需要的事情,此时,HTTP 需要一个状态管理机制(HTTP State Management Mechanism),于是 cookie 就出来了。那 cookie 是怎么记录用户状态的呢?我们将通过实际的案例来演示这件事。

这一部分,我会用写一个登录接口作为例子,来看一下如何借助 cookie 记录用户状态。

1. 初始化项目

mkdir cookie-playground
cd cookie-playground
yarn init -y
yarn add express
touch app.js
mkdir public
touch public/index.html public/main.js

完成之后,目录如下所示:

image.png

这里采用了前后端分离,public 目录是前端的资源目录,app.js 是后端的主入口文件。

2. 填充基本的页面结构

接下来就是把下面的内容填到各个文件:

// filename: app.js
const express = require('express')
const app = express()
const port = 3000

app.use(express.static('public'))

app.listen(port, () => {
  console.log(`请打开浏览器的 http://localhost:3000`)
})
<!-- filename: public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <h1>你好,<span id="userName">_</span></h1>
  <script src="./mian.js"></script>
</body>
</html>
// filename: public/main.js
document.querySelector('#userName').textContent = '用户名'

如果上面操作都正确的话,在当前目录的命令行运行 node app.js 就会成功在本机的 3000 端口启动一个 server,同时当我们访问 http://localhost:3000 的时候可以得到下图所示的页面。

image.png

3. 写登录接口

此时在 public 目录新建关于登录的页面:

touch public/login.html public/login.js

填入以下内容:

<!-- filename: public/login.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <form id="loginForm">
    <input id="name" type="text">
    <input id="password" type="password">
    <button>登录</button>
  </form>
  <script src="./login.js"></script> 
</body>
</html>
// filename: login.js
document.querySelector('#loginForm').addEventListener('submit', function (e) {
  e.preventDefault();
  const name = document.querySelector('#name').value
  const password = document.querySelector('#password').value

  console.log(name, password)
})

效果如下:

image.png

为了方便起见,我就不把登录接口、注册接口分开写了,且把他们混为一个接口,如果当前用户不存在,就帮他注册一下。当且仅当用户名存在但是密码不正确的时候才不允许登录。如果用户成功登录了,我们会跳转回 index.html 页面。

为了记住用户有没有注册过,持久的保存住用户的登录信息,我们需要引入数据库,为了方便起见,我只使用一个本地文件作为我们的数据库

touch db.txt

内容格式类似于:

username1=123
username2=456
...

准备的差不多了,接下来就让我们来写登录接口:

app.use(express.json());

app.post('/login', (req, res) => {
  const { name, password } = req.body;
  // 文末的所有代码有这个函数的实现,这里就不占空间了
  // 从数据库拿用户信息,格式是:Record<string, string>
  const users = readFromDB(); 

  if (users[name] && password !== users[name]) {
    res.send({ success: false, message: '密码不正确' });
    return;
  }

  // 用户名不存在,新用户直接注册
  if (!users[name]) {
    users[name] = password;
  }

  // 文末的所有代码有这个函数的实现,这里就不占空间了
  // 把用户信息再同步到数据库
  syncUsersToDB(users)

  res.redirect('/')
})

在前端,我们点击登录按钮,如果新用户或者老用户输入密码正确,就跳转到 index.html 页面,也就是:

document.querySelector('#loginForm').addEventListener('submit', function (e) {
  e.preventDefault();
  const name = document.querySelector('#name').value
  const password = document.querySelector('#password').value

  fetch('/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name, password })
  }).then(res => {
    if (res.redirected) {
      window.location.href = res.url;
    } else {
      res.text().then(err => { alert(err); });
    }
  })
})

这样子登录接口就写好了。

4. 用 cookie 保存登录状态

用户刚开始肯定是访问我们首页,此时我们要判断用户有没有登录,如果没有登录,就跳转到登录页面,如果已经登录了,就把用户名展示在页面上。

重点来了,那怎么记录用户有没有登录了?这里就要用 cookie 了。这里其实没有前端啥事,都是后端通过 set-cookie 字段在操作。

我们先来看一下 set-cookie 的格式是什么?

当我们访问 www.baidu.com 时,可以看到响应头里有如下图所示的 Set-Cookie 字段。

截屏2022-02-12 下午2.43.21.png

这格式,怪奇怪的。不急,来和我一起梳理一下。

拿 Set-Cookie: BD_HOME=1; path=/ 为例。

Set-Cookie 字段的格式是:cookie-name=cookie-value,在上面那段中,BD_HOME 就是 cookie-name, 1 就是 cookie-value,也可以移步浏览器检查:

截屏2022-02-12 下午9.08.19.png

可以注意到,在上图,value 后面还有一些项,比如 Domain、Path、Expires。这些设置是怎么控制的呢?其实这些也是在 Set-Cookie 中设置的,跟在 cookie-name=cookie-value 这组名值对后面,且以 ; 分隔。path=/ 就是。一条 Set-Cookie 中,控制cookie 的名值对可以有多个,我们后面的安全部分将于它们打交道,现在先知道即可。

在当前小节,我们只需要设置 cookie 的 value 以便记住状态即可,暂不需要考虑其他的。

在登录接口添加如下的代码:

const crypto = require('crypto')
const NodeCache = require("node-cache");

// 为了方便模拟,设置 cookie 10 秒过期
const cookieCache = new NodeCache({ stdTTL: 10 }); 

// 省略不要的代码....

app.post('/login', (req, res) => {
 
  // 省略不要的代码....
  
  const uuid = crypto.randomUUID();
  if (!cookieCache.has(name)) {
    cookieCache.set(uuid, name);
  }
  res.cookie('sessionid', uuid)
  // 省略不要的代码....
  res.redirect('/')
})

根据有没有登录而访问主页面做区别跳转:

const cookieParser = require("cookie-parser");

app.use(cookieParser());
app.get('/', function (req, res) {
  const cookies = req.cookies;
  const hasLogin = cookieCache.has(cookies.sessionid);
  if (hasLogin) {
    return res.redirect('/index.html');
  } else {
    return res.redirect('/login.html')
  }
});

这时,检查 NetWork 面板,就会看到设置的 sessionid:

image.png

我们页面还留了一个空位,叫做用户名。

image.png

我们写一个 getUserInfo 接口,进入首页的时候就会访问,如果是登录状态,就拿到当前用户名,否则重定向到登录页。

app.get('/getUserInfo', function (req, res) {
  const cookies = req.cookies;
  const hasLogin = cookieCache.has(cookies.sessionid);
  if (hasLogin) {
    res.send({
      name: cookieCache.get(cookies.sessionid)
    })
  } else {
    return res.redirect('/login.html')
  }
})

使用的地方也比较简单:

fetch('/getUserInfo', {
  method: 'GET',
}).then(res => {
  if (!res.redirected) {
    res.text().then(r => {
      document.querySelector('#userName').textContent = JSON.parse(r).name;
    });
  } else {
    window.location.href = res.url
  }
})

成功登录后效果如下图所示:

image.png

等 cookie 过期,则就会重定向到 login.html 页面。

整个过程画一个流程图的话,就是:

image.png

基本使用就这些,下一篇,将在此基础上讲解安全与防范的内容,包括中间人攻击、 cookie 劫持、XSS攻、CSFR 攻击等。

介绍两种存储数值类型的方式:定点数和浮点数

在计算机的领域中,有两种表示数值的方法,一种叫做定点数,另外一种是浮点数。我们今天首先来介绍定点数,然后再介绍浮点数(或许算是一句没有信息量的废话)。

相信大家可能或多或少听过浮点数的问题,表示的数据可能会不准。在像银行这样的金融领域,它们的系统可一点点数字的误差也不允许。所以他们往往采取定点数的方式存储他们的数据。定点数存储数据的时候就不会有误差了。

计算机是采用二进制存储数据的,为了将一个数存到计算机中,我们先要有一个对照表。

且看下面这张表:

十进制 BCD 代码
0 0000
1 0001
2 0010
3 0011
4 0100
5 0101
6 0110
7 0111
8 1000
9 1001

上面的 BCD 代码是一种编码规则,我们把十进制转化为定点数的规则可以是对照着上面的表,把十进制数字一个一个的转。

假如有一个数字是 100.20。

通过查表,我们可以转为:

1    0    0    2    0
0001 0000 0000 0010 0000

在上面,小数点是不存储在这的,一般来说,使用定点数存数值的人,会在程序中额外的地方存它。怎么存储小数点,我们可以暂且不关心。

由于数字还有正负之分,所以,我们还要在最左边开辟四个位,标识符号位,和往常一样,1 就是整数,0 就是负数。上面那个 100.20 就会变成:

0001 0001 0000 0000 0010 0000

而计算机中一个字节含有 8 个比特。也就是说上面那个数据存储,一共消耗了三个字节,表示为这样更合理:

00010001 00000000 00100000

定点数会根据系统的需要固定好存储的字节大小,也就是说,会固定好当前定点数用几个字节存储,一般来说会根据最大值和最小值来确定。

假设我们系统最大的数是 999.99,最小数为 -999.99,它可以用三个字节来表示当前系统内部的数。999.99 会表示为

00011001 10011001 10011001

在当前系统存储 -0.01 这个数字

00000000 00000000 00000001

从结果来看,-0.01 这个数浪费了两个半的字节。但是由于在银行呀这种地方,精度是大于一切的。并且,表示钱的数字的长度也不是长的无法想象。举一个更具象化的例子,假设一个系统最多能支撑的是 1 亿人民币,那每一个数需要多少个字节呢,其实也才 5 个字节。

    1    0   0    0   0    0   0    0   0
           
符号位
00010001 00000000 00000000 00000000 00000000

一般来说, int 型数据也是四个字节,长整形 long 要 8 个字节,表示双精度浮点数的 double 也要 8 个字节。

所以,定点数在这种对精度要求贼高但是数据长度又没有很长的场景很有效。

但放在平常使用就不是这样了,我们定义数的时候可能随意设置小数点的位置,可能想定义一
个随意大的数。

定点数不能随意设置小数的位置,而我们可不想定义一个二位小数的数据和定义三位小数的数据使用两个类型。

一门语言为了兼容所有使用者使用数的场景,如果使用定点数,它得为定点数预设多大的空间?难以想象。所以我们再继续在平常的场景下使用定点数可谓是困难重重。

数据存储方式一直在精度和内存两者之间权衡,更注重后者的存放方式就是浮点数了。

如果简单的概括浮点数是什么,就是用科学记数法格式的二进制数表示一个数。

在初中的时候,我们就已经学会怎么在十进制下使用科学技术法了,如果表示 1000,我们会记成:

$$ 1.00 * 10 ^3 $$

数字 1 后面带着两个 0,就代表是精度为小数点后两位(想起因为马虎大意忽略精度,被这道数学题支配的恐惧了吗,哈哈?)

1.00 也叫做有效数,它在十进制中的范围是大于等于 1 且小于 10 。

10 我们暂且叫做基(我忘记了,隐约记得叫做这个名词)

二进制下的科学记数法也是一样的逻辑,只不过,有效数的范围是大于等于二进制的 1 且小于 二进制的 10,也就是说,大于等于十进制的 1 且小于十进制的 2 。

而基也不是 10 了,而是 2。

对于一个二进制数 10001(十进制 17 ),我们可以表示为

$$ 1.0001 * 2^4 $$

其实上面这种记法可能会让一些同学产生误解。

有些同学可能会疑惑,为什么 1.0001 * 16 结果是 16.016 不是 17 ?

大家可别指望上面那个式子的结果是 17,因为按照我们现在的思维计算是十进制的计算思维,二进制下乘法的进位规则和十进制不一样,也就是说按照十进制算的 1.0001 * 16 不是 17 很正常。

要想真正算上面的结果,我们需要也把 16 转为二进制,乘的过程也是满二进制的 10 进 1 。最后真正的结果是:

1.0001 * 10000 = 10001

在上面的二进制的科学记数法中,我们还可以推断出这样一条隐含的规则:二进制的有效数规定大于等于二进制的 1 且 小于二进制的 10,那也就是说有效数小数点左边肯定是 1 。这时候我们在计算机存储的时候可以不存储这一位,只考虑小数点部分就好了。

接下来,介绍浮点数。

浮点数就是按照上面二进制科学记数法的基本结构,还分为了两个基本格式:以 4 个字节表示的单精度浮点数,以 8 个字节表示的双精度浮点数。单精度所用的字节要少,相应的,所表示的精准数字的范围也比双精度的要小。

我们可以借由十进制体会一下这里精度的含义。

有一个数据格式规定有效位是两位,另一个数据格式规定有效位是4位,如果表示一个整数如 10,二者都可以很精准的表示,没有差别。

假设我们要表示 π。前者会用 3.14 表示,后者我们就可以用 3.1415 来表示。虽然二者都有误差,但是后者使用了更多的有效位,比前者更的精度要高。

上面也体现出了单精度和双精度的两个差别:双精度可表示的数字范围更大,对于一个肯定有误差的数据,双精度的误差会小。

接下来我们先来介绍单精度的格式:

上面说了,它有 4 个字节,也就是32 位比特,这四个字节的分配如下:

s=1位符号位 | e=8位指数(0~255) | f=23位有效数

它是如何用这三部分表示一个数的?

$$ (-1)^s * 1.f * 2^{e-127} $$

继续那 10001 的例子,它就是:

$$ (-1)^0 * 1.0001 * 2^{5} $$

再来看一下双精度的格式,它有 8 个字节,64 个比特:

s=1位符号位 | e=11位指数(0~2047) | f=52位有效数

表示数的格式和单精度一致,只不过 f 的位数变多了,e 也变大了:

$$ (-1)^s * 1.f * 2^{e-1023} $$

我们再来依次介绍一下 s、e、f 对一个浮点数格式有什么影响。

s 就是符号位,它只有一位比特,取值只有 0 或 1。

当取 0, $(-1)^s$ 结果为 1;取 1,结果为 -1,从而起到表示符号的作用。

e 为指数部分,直观的说,它控制了有效位的偏移量。就比如 $1.0001 * 2^{5}$,就代表 1.0001 向右偏移 5 位。

f 为科学记数法格式里的有效位,它和精度的关系比较大。我们再直观的理解一下。

且拿单精度举例。

按上面单精度的公式,它可以表达最大的值是:

$$ (-1)^s * 1.f * 2^{127} $$

换算成 10 进制就是 $3.402823466 * 10^{38}$。这个数还是蛮大的,但是在它范围内的一个数 16777216 表示为二进制是:

$$ 1.00000000000000000000000 * 2^{24} $$

由于计算机中能表示的数不是连续的,是离散的,紧接着上面这个二进制,下一个二进制数是(即上一个二进制数变大最小刻度):

$$ 1.00000000000000000000001 * 2^{24} $$

这个数再转换成十进制其实是 16777218。(16777216, 16777218) 这个集合内所有的数据都不见了。事实上,这之间的数都被表示成了一个数,和 16777216 存储结构一样。也就是说,这个区间的数无法精准表示。

如上说的,也就是浮点数面临的精度问题。和单精度浮点数一样,双精度浮点数一样会有精度的问题,但是由于扩大了有效位,比起单精度浮点数,可以说的大大改善了。

不过比较有效位是有限的, 如果存储无理数或者长度超过有效位的数字的话,就会不准,比如 存储 0.1 ,它转换为二进制是一个无限循环小数:

0.0001 1001 1001 1001 1001 1001 ...

为了存储下来,只好截取前面那部分,但这样以来,存储下的数据就已经是不准确的,也不能指望做四则运算是准的。

最后,在 JavaScript 中,数值类型的表示采用的是浮点数的双精度格式存储。除了正常的数值,还有几个特殊的数值 NaN、Infinity。对比这下面这个公式看一下:

$$ (-1)^s * 1.f * 2^{e-1023} $$

  1. 如果 e == 2047 且 f != 0,就被表示为 NaN。
  2. 如果 e == 2047 且 f == 0,就被表示为无穷大或者无穷小(具体看符号位)

对于这些特殊的值,也不用关心它最终算出的结果,它只是一个记法。

好了,这就是本文关于定点数、浮点数的全部内容了。

谢谢阅读,撒花~

先理解原理在手写 Promise

今天,我们要手写一个 promise,不过,重点并不是那段代码,而是更加深入的理解 promise 的用法。

为什么呢?因为如果把重点放在「手写 promise」这件事上,可能只是应试的时候才受益,并且这样死记下来的写法很容易忘记,但是如果我们深刻的掌握了 promise ,会发现「手写promise」是一件顺其自然、水到渠成的事情。

promise 是 ES 6 引入的语法,它的实现规范按照 Promises/A+。本文就主要参考这个规范来实现我们的 promise。

我们就从研究 Promise 对象开始。

通常的,我们会想下面这样构造一个 Promise 对象:

const p = new Promise((resolve, reject) => {
    resolve('hello world')
})

我们使用 new 操作符新建一个对象,构造函数的格式是:

declare function executor(resolve, reject) : void 

值得注意的是,每当我们新构造一个 Promise 对象, 这个 executor 函数也会立即执行一遍。因为它是用来设置 promise 值的。

在上一篇,我们也提过,一个 promise 对象有三种状态:pending、fulfilled、rejected。

众所周知,我们无法在使用 JS 访问 promise 对象来知道它当前处于哪个状态,promise 的状态是维护在内部的。尽管如此,改变一个 promise 的状态却是我们做的。这也就是 executor 函数的作用。

我们可以在初始化 Promise 对象的时候,调用 resolve 把 promise 的状态置为 fulfilled,也可以调用 reject 把 promise 的状态置为 rejected。

const p = new Promise((resolve, reject) => {
    resolve('hello world') // 把 p 改为 fulfilled 态
})

换句话讲,executor 相当于内置的 promise 开放给我们的回调函数,尽管我们无法直接读取 promise 的状态,但是我们通过这个函数与 promise 对象进行沟通。而它的两个参数,我们可以称它们为「置值器」。

这是第一个我们需要改变想法的点。以后要把 Promise 构造函数的入参 executor 看做一个和内置 promise 对象交互的函数。我们经由此函数的两个参数 resolve、reject 来改变 promise 的状态。

接下来我们研究生成的 promise 对象 p。

这个对象也叫做 thenable 对象。什么是 thenable 对象呢?很简单,就是具有 then 方法的函数或者对象,promise 对象就是一个 内置的 thenable 对象,我们也可以马上写一个thenable对象:

const thenableObj = {
    then: () => {}
}

对于 p 来说,它有一个函数 then,并且接受两个参数。通常使用的话就是:

p.then(
    function onfulfilled() {
        console.log('onfulfilled');
    },
    function onRejected() {
        console.log('onRejected');
    }
);

then 函数的调用时机就是 p 的状态到了 fulfilled/rejected。 尽管如此,就算 p 的状态最开始就是 fulfilled/rejected ,也不会立马执行。规范里有这么一句话

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

翻译:在 执行上下文 堆栈仅包含平台代码之前,不得调用 onFulfilledonRejected`。

Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

翻译:这里的“平台代码”是指引擎、环境和promise实现代码。 在实践中,这个要求确保 onFulfilledonRejected 异步执行,在调用 then 的事件循环之后,并使用新的堆栈。 这可以通过“宏任务”机制实现,例如 setTimeoutsetImmediate,或使用“微任务”机制,例如 MutationObserverprocess.nextTick。 由于promise实现被认为是平台代码,它本身可能包含一个任务调度队列或“trampoline”,在其中调用处理程序。

也就是说,then 函数的调用肯定是异步的,等 p 到了 fulfilled/rejected,我们就会把当前这个任务加入到异步任务的队列里面去。至于什么时候取出来执行,就是当前没有平台代码(platform code)执行了。也就是我们所说的执行上下文中的同步代码都执行完毕了。

至于 promise 是当做宏任务还是微任务加进去,规范上面并没有做强制要求,不过,浏览器端的实现加的都是微任务。

then 的返回值也是一个 promise 对象,同样有 then 方法。如此往复,可以一直链下去。

p.then().then().then()

如上所见,then 接受的这两个函数都可以省略,如果then 省略了处理当前 promsie 对象状态的函数,原先 promise 的状态会继续往后传递。举个例子来说明这个问题。

我们最开始生成了一个拒绝态的 promsie 对象,但是再紧跟着它的 then 函数里面没有传递 onRejected,那它的状态就被原样的往后传递,直到碰到有 onRejected 处理函数的。

Promise.reject(1)
    .then(() => {
        console.log('只有成功态'); // 这个不会被打印
    })
    .then()
    .then(undefined, (err) => {
        console.log(err); // 1
    });

明白了上面这些,已经可以开始动手实现一个初版的 promise 了。作为我们第一版的实现,暂且不关注 then 函数的返回值,因为它的返回值还有很多需要考虑的点,我们就先实现生成一个 promise 对象然后调用它的 then 方法。

事不宜迟,现在就动手。

先写一下我们的调用代码

const p = new MyPromise((resolve, reject) => {
    setTimeout(() => {
        resolve('hi') 
    }, 1000) 
})

console.log('a')
p.then((res) => {
    console.log('c')
    console.log(res);
})
console.log('b')

我们期望的结果是:

a
b
// 一秒后
c
hi

我们就对比着用法来实现代码:

type Executor<T> = (
    resolve: (value: T) => void,
    reject: (reason?: any) => void
) => void

type ThenCallbackQueueItem<T> = [OnFulfilled<T> | undefined, OnRejected | undefined]
type OnFulfilled<T> = (value: T | undefined) => void;
type OnRejected = (reason: any) => void

class MyPromise<T> {
    #status: 'pending' | 'fulfilled' | 'rejected' = 'pending';
    #value: T | undefined = undefined;
    #reason: any = undefined;
    #thenCallbackQueue: ThenCallbackQueueItem<T>[] = [];

    constructor(executor: Executor<T>) {
        executor(this.resolveFn.bind(this), this.rejectFn.bind(this));
    }
    
    // 请暂时忽略 then 的返回值,为了方便,我们先把它写成了 void
    // 下一版会改
    then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): void {
        // promise then 的执行顺序也是按照先挂载的先执行
        // 所以我们维护一个队列
        this.#thenCallbackQueue.push(
            [onFulfilled, onRejected]
        );

        // 如果调用 then 方法的时候, promise 已经是解决态了
        // 那就把当前挂载的所有回调都执行一遍
        if (this.isSettled()) {
            this.releaseThenCallbackInOrder();
        }
    }

    resolveFn(value: T) {
        this.#status = 'fulfilled';
        this.#value = value;

        // 说明调用 then 的时候 当前 promise 还是 pending 状态
        // 那个时候只是把 then 接受的回调函数加入队列里去了
        // 我们就在解决了之后再释放队列
        if(this.#thenCallbackQueue.length > 0) {
            this.releaseThenCallbackInOrder()
        }
    }

    rejectFn(reason: any) {
        this.#status = 'rejected';
        this.#reason = reason;

        if(this.#thenCallbackQueue.length > 0) {
            this.releaseThenCallbackInOrder()
        }
    }

    isSettled() {
        return this.#status !== 'pending';
    }

    releaseThenCallbackInOrder() {
        // 把执行 then 的操作加入 event loop
        setTimeout(() => {
            this.#thenCallbackQueue.forEach(([onFulfilled, onRejected]) => {
                if (this.#status === 'fulfilled') {
                    onFulfilled?.(this.#value);
                } else {
                    onRejected?.(this.#reason);
                }
            });
            this.#thenCallbackQueue = [];
        });
    }
}

使用上面那份实现,我们的输出结果正如我们预想的那样:

image.png

这样我们就实现完成了第一版,接下来写第二版

这是我们实现的第一版中的 then 的实现:

then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): void {
    // promise then 的执行顺序也是按照先挂载的先执行
    // 所以我们维护一个队列
    this.#thenCallbackQueue.push(
        [onFulfilled, onRejected]
    );

    // 如果调用 then 方法的时候, promise 已经是解决态了
    // 那就把当前挂载的所有回调都执行一遍
    if (this.#isSettled()) {
        this.#releaseThenCallbackInOrder();
    }
}

之前讨论的时候忽略了它的返回值,现在就先来看一下它返回值到底是什么。

首先需要知道的是,它的类型就是一个 promise 对象,不是什么类似于 promise 的结构(比如 promiseLike 对象)。这就会造成了,当上一个值是非 promise 对象的时候,会被引擎内部,隐式的包装成 promise 对象,再传递下去。

在下面这个示例里:

const p1 = Promise.resolve(1).then(() => {
    return 'hi';
});

p1.then(res => {
    console.log(res); // hi
});

这里首先使用 Promise.resolve 生成了一个成功状态的 promise,然后调用它的 then 方法,并在它的第一个参数里处理了这个成功状态,返回值记作 p1。接着调用 p1.then,这个结果会打印出 'hi'。

then 函数接受两个参数:onFulfiled、onRejected。也就是说,我们会把这两个函数的返回值作为下一个 promise 的值,在上面那个示例中,其实 p1 就是:

Promsie.resolve('hi')

如果没有返回值呢?

const p1 = Promise.resolve(1).then(() => {});

p1.then(res => {
    console.log(res); // undefined
});

这个表现也合理,因为在 JS 中,没有显示的 return 语句,就默认返回 undefined 了。此时如果返回 null,结果就变成了 null 了。

如果我们返回值是一个对象呢?

const obj = {
    a: 'hi'
}

const p1 = Promise.resolve(1).then(() => {
    return obj;
});

p1.then(res => {
    console.log(obj === res); // true
});

这个例子就更直观的反映出了,then 链上下一个 promsie 包装的值,就是上一个 then 函数中某一个处理函数(onFulfiled 或者 onRejected)的返回值,至于是哪一个处理函数的返回值,要看 promise 的状态。

上面讨论的情况都是非 promise 对象,返回的效果就可以想象成使用 Promise.resolve 包装一层,用作下一个 promise。

假设直接返回 promise?就不能想象成用 Promise.resolve 包装了,而是会返回包装了相同值的 promise 对象。

const obj = {
    a: 'hi'
};

const x = Promise.resolve(obj);
const p1 = Promise.resolve(1).then(() => {
    return x;
});

p1.then(res => {
    console.log(res === obj); // true
});

console.log(x === p1); // false 注意这里,和下面 Promise.resolve 的行为会不同

为什么要说 「就不能想象成用 Promise.resolve 包装了」因为 Promsie.resolve 这个 Promise 类的静态方法有些特殊,它会有这样的表现:

const x1 = Promise.resolve(1);
console.log(x1 === Promise.resolve(x1)); // true

它会判断,假如 Promise.resolve 的入参是一个 Promise 实例的时候,就会不加修饰,直接返回。这里的 Promise 实例也包含了继承了 Promise 的类,也就是说,使用 instanceof Promise 判断返回 true 的,就触发这条规则。

继续回到 then 方法返回值的处理,回顾一下:

  1. 在上面的演示中,说明了如果是非 promise 的值,那就会被包装成一个 promise 对象作为返回值,这个包装的过程可以看做是使用 Promise.resolve 包装

  2. 如果是 promise,就会产生一个新的包装了相同值的 promise 作为返回值。我们可以用伪代码稍微来模拟一下这个过程:

return new Promise((resolve) => {
    上一次的Promise.then((value) => {
        resolve(value)
    })

好了,它的规则大概就是这样,最后再来看一个特例:PromiseLike。也就是我们上一篇提到的 thenable 对象中排除 Promise 对象。再来复习一下,这次我们直接放规范里的定义:

“promise” is an object or function with a then method whose behavior conforms to this specification. “thenable” is an object or function that defines a then method.

上面我们看到了普通对象的表现,会直接包装后返回,但是如果是 PromiseLike 呢?它会有 "拆包" 的行为:

const obj = {
    a: 'hi'
};

const wrapperObj = {
    then(resolve: any) {
        resolve(obj);
    }
};

const p1 = Promise.resolve(1).then(() => {
    return wrapperObj;
});

p1.then(res => {
    console.log(res === obj); // true
});

上面就是一系列的行为了,种种情况都列举到了,我们就要准备写代码了,现在我们要做的就是把这些情况都整理成测试用例,然后让我们的实现都能跑通上面的例子。

先来看非 thenable 对象。

const EmptyPromise = new MyPromise((resolve) => {
    setTimeout(() => {
        resolve('hi');
    }, 1000);
});

// 不传,返回 undefined
const p0 = EmptyPromise.then(() => {});
p0.then(res => {
    console.log(res === undefined);
});

// 原始值
const v1 = Math.random();
const p1 = EmptyPromise.then(() => v1);
p1.then(res => {
    console.log(res === v1)
});

// 对象
const v2 = {
    value: Math.random()
};
const p2 = EmptyPromise.then(() => v2);
p2.then(res => {
    console.log(res === v2);
});

等到真正实现 then 函数具有返回值,我们就发现昨天我们实现的那个版本存在问题,因为昨天的那一版只有在处于 settled 情况下才返回,而 then 要求无论什么情况都要返回。也就是说,返回值是确定的。那怎么实现呢?昨天那一份也不是白做,思路会有一点类似。

最根本的我们要记得上一篇文章强调的,executor 是一个和 Promise 交互的回调函数,我们靠它设置值,既然这样的话,我们可以先返回 promise 对象,但是,在解决了才调用 then 的函数,解决了才往我们的 promsie 里面设置值。

实现如下:

type OnFulfilled<T> = (value: T | undefined) => void;
type OnRejected = (reason: any) => void

export default class MyPromise<T> {

    // 省略前面的代码...
    
    then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): MyPromise<any> {
        return new MyPromise((resolve) => {
            // 增加了第三个参数,会在解决的时候靠它
            // 改变当前返回值 promise 的状态
            this.#thenCallbackQueue.push(
                [onFulfilled, onRejected, resolve]
            );

            if (this.#isSettled()) {
                this.#releaseThenCallbackInOrder();
            }
        });

    }

    #releaseThenCallbackInOrder() {
        setTimeout(() => {
            this.#thenCallbackQueue.forEach(([onFulfilled, onRejected, promiseResolve]) => {
                if (this.#status === 'fulfilled') {
                    const ret = onFulfilled?.(this.#value);
                    promiseResolve(ret);
                } else {
                    const ret = onRejected?.(this.#reason);
                    promiseResolve(ret);
                }
            });
            this.#thenCallbackQueue = [];
        });
    }
}

目前,我们的 then 函数签名如下:

then(onFulfilled?: OnFulfilled<T>, onRejected?: OnRejected): MyPromise<any>

返回值是 any,我们得再更新一下它的类型了,我们已经知道,返回值的结果和 onFulFilled / onRejected 有关,于是,我们把它更新为:

type OnFulfilled<T, T1Result> = (value: T | undefined) => T1Result;
type OnRejected<T2Result> = (reason: any) => T2Result

then<T1Result, T2Result>(onFulfilled?: OnFulfilled<T, T1Result>, onRejected?: OnRejected<T2Result>): MyPromise<T1Result | T2Result> {
    return new MyPromise<T1Result | T2Result>((resolve) => {
        this.#thenCallbackQueue.push(
            [onFulfilled, onRejected, resolve]
        );

        if (this.#isSettled()) {
            this.#releaseThenCallbackInOrder();
        }
    });

}

接下来讨论 thenable 对象的情况,先讨论其中的 promise 对象。这个就是检测到返回值是 promise 对象,就取出它包装的值,再返回。

这是我们上面的测试用例,稍微了改编了一下:

const obj = {
    a: 'hi'
};

const v3 = new MyPromise((resolve) => {
    resolve(obj);
});

const p3 = EmptyPromise.then(() => v3);

p3.then((value) => {
    console.log(value === obj); // true
});

我们修改的部分就只在 releaseThenCallbackInOrder

#releaseThenCallbackInOrder() {
    setTimeout(() => {
        this.#thenCallbackQueue.forEach(([onFulfilled, onRejected, promiseResolve]) => {
            if (this.#status === 'fulfilled') {
                const ret = onFulfilled?.(this.#value);

                if (ret instanceof MyPromise) {
                    // onFulfilled 或者 onRejected 可能返回任何状态的 promise
                    // then 的结果可能是 fulfilled/rejected 之一
                    // 由于只可能是其中之一,所有只有一个函数走逻辑
                    // 都兼容一下.
                    ret.then((res) => {
                        promiseResolve(res);
                    }, (err) => {
                        promiseResolve(err);
                    });
                } else {
                    promiseResolve(ret);
                }

            } else {
                const ret = onRejected?.(this.#reason);

                if (ret instanceof MyPromise) {
                    ret.then((res) => {
                        promiseResolve(res);
                    }, (err) => {
                        promiseResolve(err);
                    });
                } else {
                    promiseResolve(ret);
                }
            }
        });
        this.#thenCallbackQueue = [];
    });

剩下的就是拆包 PromiseLike 对象了。

interface PromiseLike<T> {
    then<TResult1, TResult2>(
        onFulfilled?: OnFulfilled<T, TResult1>,
        onRejected?: OnRejected<TResult2>): Promise<TResult1 | TResult2>;
}

先写一个判断 PromiseLike 的工具方法

#isPromiseLike(obj: unknown): obj is PromiseLike<T> {
    if (obj && typeof obj === 'object' && obj.hasOwnProperty('then')) {
        let o = obj as PromiseLike<any>;

        return o.then && (
            typeof o.then === 'function' || typeof o.then === 'object'
        );
    }
    return false;
}

接下来做的事情就简单了,其实处理逻辑和 promise 对象一致,也就意味着,我们只需要更新一下刚才的判断条件即可:

if (ret instanceof MyPromise || this.#isPromiseLike(ret)) {
   ...
} else {
   ...
}

用下面这个测试用例测试一下:

const wrapperObj = {
    then(resolve: any) {
        resolve(obj);
    }
};

const p4 = EmptyPromise.then(() => {
    return wrapperObj;
});

p4.then(res => {
    console.log(res === obj); // true
});

逻辑也是正常,到这里,我们就完成了一个比较完备的 then 函数。

最本质的还是理解它的用法,理解了用法,就可以按照它的用法自己想一下实现。

好了,promise 章节的三篇就完成啦,关于它的一些静态方法,像 Promise.resolve、Promise.reject、Promise.all、Promise.race、Promise.prototype.catch、Promise.prototype.finally,除却 finally ,都比较简单,如果,你有哪一个想了解,可以在下面留言。

谢谢阅读,撒花。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.