代数效应与 React

323 阅读8分钟

什么叫代数效应

比较唬人的概念

代数效应是一种编程模型,它通过表示程序的副作用来管理和控制这些副作用。 代数效应的核心思想是将副作用视为一种代数结构,可以通过组合这些代数结构来创建新的副作用,并通过代数运算来控制这些副作用的执行顺序和结果。

简单理解

可以通过代数效应将副作用函数作为普通函数一样调用。

如何消除 async 的传染性

async function getUser() {
    return await fetch("http://localhost:3000/user");
}

async function fn1() {
    const user = await getUser();
    //  做一些其他操作
    return user;
}

async function fn2() {
    const user = await fn1();
    // 做一些其他操作
    return user;
}

async function main() {
    const user = await fn2();
    console.log(user);
}
main();

这块代码其实没什么问题,但是在函数式编程的环境下这就不合适,因为一个副作用函数,把其它的纯函数全部都变成了副作用函数,为什么要说函数式编程呢?因为代数效应的概念就来自于函数式编程。

那为什么一个副作用函数会把后面纯函数全部都变成副作用函数了呢?是因为 async 的具有传染性的,那我们就需要干掉它,把代码写成这样。

function getUser() {
    return fetch("http://localhost:3000/user");
}

function fn1() {
    const user = getUser();
    //  做一些其他操作
    return user;
}

function fn2() {
    const user = fn1();
    // 做一些其他操作
    return user;
}

function main() {
    const user = fn2();
    console.log(user);
}
main();

但是这么写的话,功能肯定就不对了,你不能因为想把代码写好看,代码能不能跑也不管吧,看一下此时的调用栈。

image.png

所以说问题就在于由于 feach 函数要等待网络请求,导致其他的函数一定要等,可是为什么一定是其他函数等待网络请求的结果,不能是网络请求有结果之后通知一下我们呢?这样我们不就不用等了吗,不就解决这个问题了吗?但是调用栈已经说这样了,怎么才能让后续的函数不等呢?函数除了调用完成会结束,报错不也会结束吗?所以可以有以下的结果。

image.png 改造之后很明显可以看出来需要改造的是 fetch 函数,需要加两个功能一个是报错,一个是缓存。

image.png

根据上面的代码就可以写出下面的代码,就消除的 async 的传染性

const PENDING = "PENDING",
    FULFILLED = "FULFILLED",
    REJECTED = "REJECTED",
    LINK = "http://localhost:3000/user"
function run(fn) {
    const nativeFetch = window.fetch;
    // 设置缓存的格式,仅考虑 fetch 调用一次的情况
    const cache = {
        state: PENDING,
        value: null
    }
    // 劫持 fetch
    window.fetch = function (...args) {
        // 如果有缓存则返回缓存
        if (cache.state === FULFILLED) {
            return cache.value;
        } else if (cache.state === REJECTED) {
            throw cache.value;
        }
        // 拿到 promise 用于注册回调后续重新调用函数
        const pro = nativeFetch(...args).then(resp => resp.json()).then(resp => {
            cache.state = FULFILLED;
            cache.value = resp;
        }, error => {
            cache.state = REJECTED;
            cache.value = error;
        })
        // 抛出 Promise 用于后续注册。
        throw pro;
    }
    try {
        fn();
    } catch (err) {
        // 判断是否为 Promise
        if (isPromise(err)) {
            // 重新调用函数
            err.then(fn, fn).finally(() => {
                // 设置回来
                window.fetch = nativeFetch;
            })
        }
    }
}
// 判断是否为 Promise;
function isPromise(pro) {
    return pro !== null && (typeof pro === "object" || typeof pro === "function") && typeof pro.then === "function";
}
function getUser() {
    return fetch(LINK);
}

function fn1() {
    const user = getUser();
    //  做一些其他操作
    return user;
}

function fn2() {
    const user = fn1();
    // 做一些其他操作
    return user;
}

function main() {
    const user = fn2();
    console.log(user);
}

run(main)

结果

image.png

数据使用的是 json-server 简单起了一个服务器。

{
  "user": [
    {
      "name": "小明",
      "age": 10
    },
    {
      "name": "小明",
      "age": 10
    },
    {
      "name": "小明",
      "age": 10
    },
    {
      "name": "小明",
      "age": 10
    }
  ]
}

理解

其实 React 内部使用的并不是真正的代数效应,但是也是受到了代数效应启发。当然目前 JS 也并没有语法去支持代数效应,因此下面的语法都是虚构的,用文章的话来说就是语法并不重要,重要的是其背后的原理。

function getName(user) {
  let name = user.name;
  if (name === null) {
    // 定义要做什么
    name = perform 'ask_name';
  }
  return name;
}

function makeFriends(user1, user2) {
  user1.friendNames.add(getName(user2));
  user2.friendNames.add(getName(user1));
}

const arya = { name: null };
const gendry = { name: 'Gendry' };
try {
  makeFriends(arya, gendry);
} handle (effect) {
  if (effect === 'ask_name') {
    // 这块是怎么做,然后把结果返回
    resume with 'Arya Stark';
  }
}

然后可以把上面的例子改成这种结构如下。

function fetch (cache){
	if(cache===null){
    // 定义要做什么,怎么做我不管
    cache = perform 'request';
  }
  return cache;
}
function main(){
  const resp = fetch();
  console.log(resp);
}
function run (fn){
	try{
  }handle (effect){
    if(effect === 'request'){
    	// 进行网络请求
      request.then(resp=>resp.json()).then(resp=>{
        resume with resp;
      })
    }
  }
}
run(main);

这种方式和我们自己写的方式有什么区别呢?

区别在于这种写法不会把函数调用两次,而且这种的话是语法支持的,把做什么和怎么做完全隔离开了,也可以这么理解,做什么是纯函数,怎么做是副作用函数,所以说代数效应隔离了纯函数和副作用函数。

在举一个例子,如下。


    function enumerateFiles(dir) {
      // 打开文件
      const contents = perform OpenDirectory(dir);
      // 日志
      perform Log('Enumerating files in ', dir);
      for (let file of contents.files) {
        // 处理文件
        perform HandleFile(file);
      }
      // 日志
      perform Log('Enumerating subdirectories in ', dir);
      for (let directory of contents.dir) {
        // 我们可以递归或者调用别的有效应的函数
        enumerateFiles(directory);
      }
      perform Log('Done');
    }

    let files = [];
    try {
      enumerateFiles('C:\');
    } handle (effect) {
      if (effect instanceof Log) {
        myLoggingLibrary.log(effect.message);
        resume;
      } else if (effect instanceof OpenDirectory) {
        myFileSystemImpl.openDir(effect.dirName, (contents) => {
          resume with contents;
        });
      } else if (effect instanceof HandleFile) {
        files.push(effect.fileName);
        resume;
      }
    }
    // `files`数组里现在有所有的文件了


    import { withMyLoggingLibrary } from 'my-log';
    import { withMyFileSystem } from 'my-fs';

    function ourProgram() {
      enumerateFiles('C:\');
    }
    // 自己的日志系统
    withMyLoggingLibrary(() => {
      //自己的文件系统
      withMyFileSystem(() => {
        ourProgram();
      });
    });

讲到这里我觉得代数效应的作用就非常明显了,我觉得可以拿它做跨平台,并且使用它会让自己的代码更加内聚。

看一下 react 的源码,当然这块是我提取过后的。


 function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
   // 遍历effectList
   while (nextEffect !== null) {
     const effectTag = nextEffect.effectTag;
     // 根据 ContentReset effectTag重置文字节点
     if (effectTag & ContentReset) {
        commitResetTextContent(nextEffect);
     }
     // 根据 effectTag 分别处理
     const primaryEffectTag =
       effectTag & (Placement | Update | Deletion | Hydrating);
     switch (primaryEffectTag) {
       // 插入DOM
       case Placement: {
         commitPlacement(nextEffect);
         nextEffect.effectTag &= ~Placement;
         break;
       }
       // 插入DOM 并 更新DOM
       case PlacementAndUpdate: {
         // 插入
         commitPlacement(nextEffect);

         nextEffect.effectTag &= ~Placement;

         // 更新
         const current = nextEffect.alternate;
         commitWork(current, nextEffect);
         break;
       }
       // 更新DOM
       case Update: {
         const current = nextEffect.alternate;
         commitWork(current, nextEffect);
         break;
       }
       // 删除DOM
       case Deletion: {
         commitDeletion(root, nextEffect, renderPriorityLevel);
         break;
       }
     }

     nextEffect = nextEffect.nextEffect;
   }
 }

只定义做什么的话,我根本就不需要定义那么多逻辑,因为做什么是跨平台通用的,但是怎么做就是平台之间都有不同了,这样可以非常直观的看到逻辑,我看到这里我就只知道他在遍历副作用链表,做对副作用做不同的操作,可以更加直观的去理解它的核心逻辑,降低学习成本。


    function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
      // 遍历effectList
      while (nextEffect !== null) {
        const effectTag = nextEffect.effectTag;
        	perform {
            effectTag,
            fiber:FiberRoot
          }
        nextEffect = nextEffect.nextEffect;
      }
    }

代数效应在 React 中的应用

说实话,不结合 react 本身使用代数效应的例子来看上面的东西就是纯纯炫技

Suspense

react 官网 Suspense 示例,主要代码如下

image.png

其实主要思路都是一样的。

react-fetch

react-fetch 是一个 react 在进行实验的一个库,并没有发布,生存环境是不建议使用的。

因为也就 100 来行代码所以我就直接贴了 ,代码如下

    /**
     * Copyright (c) Facebook, Inc. and its affiliates.
     *
     * This source code is licensed under the MIT license found in the
     * LICENSE file in the root directory of this source tree.
     *
     * @flow
     */

    import type {Wakeable} from 'shared/ReactTypes';

    import {readCache} from 'react/unstable-cache';

    const Pending = 0;
    const Resolved = 1;
    const Rejected = 2;

    type PendingResult = {|
      status: 0,
      value: Wakeable,
    |};

    type ResolvedResult = {|
      status: 1,
      value: mixed,
    |};

    type RejectedResult = {|
      status: 2,
      value: mixed,
    |};

    type Result = PendingResult | ResolvedResult | RejectedResult;

    // TODO: this is a browser-only version. Add a separate Node entry point.
    const nativeFetch = window.fetch;
    const fetchKey = {};

    function readResultMap(): Map<string, Result> {
      const resources = readCache().resources;
      let map = resources.get(fetchKey);
      if (map === undefined) {
        map = new Map();
        resources.set(fetchKey, map);
      }
      return map;
    }

    function toResult(thenable): Result {
      const result: Result = {
        status: Pending,
        value: thenable,
      };
      thenable.then(
        value => {
          if (result.status === Pending) {
            const resolvedResult = ((result: any): ResolvedResult);
            resolvedResult.status = Resolved;
            resolvedResult.value = value;
          }
        },
        err => {
          if (result.status === Pending) {
            const rejectedResult = ((result: any): RejectedResult);
            rejectedResult.status = Rejected;
            rejectedResult.value = err;
          }
        },
      );
      return result;
    }

    function readResult(result: Result) {
      if (result.status === Resolved) {
        return result.value;
      } else {
        throw result.value;
      }
    }

    function Response(nativeResponse) {
      this.headers = nativeResponse.headers;
      this.ok = nativeResponse.ok;
      this.redirected = nativeResponse.redirected;
      this.status = nativeResponse.status;
      this.statusText = nativeResponse.statusText;
      this.type = nativeResponse.type;
      this.url = nativeResponse.url;

      this._response = nativeResponse;
      this._arrayBuffer = null;
      this._blob = null;
      this._json = null;
      this._text = null;
    }

    Response.prototype = {
      constructor: Response,
      arrayBuffer() {
        return readResult(
          this._arrayBuffer ||
            (this._arrayBuffer = toResult(this._response.arrayBuffer())),
        );
      },
      blob() {
        return readResult(
          this._blob || (this._blob = toResult(this._response.blob())),
        );
      },
      json() {
        return readResult(
          this._json || (this._json = toResult(this._response.json())),
        );
      },
      text() {
        return readResult(
          this._text || (this._text = toResult(this._response.text())),
        );
      },
    };

    function preloadResult(url: string, options: mixed): Result {
      const map = readResultMap();
      let entry = map.get(url);
      if (!entry) {
        if (options) {
          if (options.method || options.body || options.signal) {
            // TODO: wire up our own cancellation mechanism.
            // TODO: figure out what to do with POST.
            throw Error('Unsupported option');
          }
        }
        const thenable = nativeFetch(url, options);
        entry = toResult(thenable);
        map.set(url, entry);
      }
      return entry;
    }

    export function preload(url: string, options: mixed): void {
      preloadResult(url, options);
      // Don't return anything.
    }

    export function fetch(url: string, options: mixed): Object {
      const result = preloadResult(url, options);
      const nativeResponse = (readResult(result): any);
      if (nativeResponse._reactResponse) {
        return nativeResponse._reactResponse;
      } else {
        return (nativeResponse._reactResponse = new Response(nativeResponse));
      }
    }

其实思路都是一致的。

Hooks

这块使用的代数效应其实没有那么直接,我们来看一下,useState,以及 useEffect

useState

在最开始使用 useState 的时候,我比较奇怪的是 useState 是怎么知道是哪个组件使用了他呢?

当然实际上它是在不同的阶段使用不同的 dispatcher 以及在 fiber 上通过链表来保存 useState 的数据。这种依赖全局可变状态的方式感觉会有点脏,就怕啥时候就被什么不清楚的东西改变了,但是通过代数效应去看会有不同的理解。

    function LikeButton() {
      // useState 怎么知道它在哪个组件里?
      const [isLiked, setIsLiked] = useState(false);
    }

那我可以伪代码的方式进行理解

    function LikeButton() {
      // useState 怎么知道它在哪个组件里?
      const [isLiked, setIsLiked] = perform useState(false);
    }
    ReactDom.render(()=>{
      try{
        LikeButton();
      }handle(effect){
      if (effect instanceof 'State') {
        // 计算得到 state;
        resume getState();
      }
      }
    })

useEffect

然后理解了 useState 再来理解 useEffect 就更加直接了。

    function LikeButton() {
      // useState 怎么知道它在哪个组件里?
      const [isLiked, setIsLiked] = useState(false);
      useEffect(()=>{
        setIsLiked(!isLiked);
      },[])
    }

是不是 能理解成这样呢?

    function LikeButton() {
      // useState 怎么知道它在哪个组件里?
      const [isLiked, setIsLiked] = perform useState(false);
       perform {
         callback()=>{
        		setIsLiked(!isLiked);
      		},
         dep:[]
       } 
    }
    ReactDom.render(()=>{
      try{
        LikeButton();
      }handle(effect){
      if (effect instanceof State) {
        // 计算得到 state;
        resume getState();
      }
          if (effect instanceof Effect) {
        // 计算得到 state;
        resume effect.callback
      }
      }
    })

参考资料

  1. 写给那些搞不懂代数效应的我们(翻译)
  2. React代数效应+Fiber介绍