React

useRef 原理理解

useRef相当于受限的全局变量(只能在组件作用域中使用,跟随组件实例的整个生命周期),所以是相当于 组件实例的实例属性。

可以用陈旧闭包来理解 useRef 带来的全局变量效果。

log 函数使用 message 的 value 属性,message 则使用了 value,也就是 createIncrement 的作用域中的 value 变量,message 为每次 inc 作用域生成时生成的变量,所以每次 inc 的执行会生成新的作用域,也就生成新的 log 函数的闭包, 每个 log 闭包纳入当前 inc 作用域中的 message 变量的 value 属性,然后由于 该属性 使用了 value,value 是一个 createIncrement 作用域的基本类型,基本类型直接复制。value 的类型会随着每次 log 的作用域生成而复制。

  1. function createIncrement(i) {
  2. let value = 0
  3. return function inc() {
  4. value += i
  5. console.log(value)
  6. const message = { value: `Current value is ${value}` }
  7. return function log() { // setState相当于logValue函数
  8. console.log(message.value)
  9. }
  10. }
  11. }
  12. const inc = createIncrement(1) // i被固定为1,输入几就被固定为几
  13. inc() // 1
  14. const log = inc() // 2
  15. inc() // 3
  16. log() // "Current value is 2" 未能正确打印3

改变这个效果的话,只能将 message 提升作用域,要么提升到与 value 同级作用域,要么直接提升到顶层作用域,完成 值可变更闭包更新的条件 也就是:

  1. 使用的是一个引用类型;
  2. 引用类型定义的作用域 高于其 属性值被改变时 的作用域; ```typescript // const message = {};

function createIncrement(i) { let value = 0; // const message = {}; // 提升到这里也可以 return function inc() { value += i console.log(value)

  1. message.value = `Current value is ${value}`; // 引用类型 属性值改变
  2. return function log() {
  3. console.log(message.value)
  4. }
  5. }

} const inc = createIncrement(1) // i被固定为1,输入几就被固定为几

inc() // 1 const log = inc() // 2

inc() // 3 log() // “Current value is 3”

  1. #useRef
  2. <a name="nDnCL"></a>
  3. ## 函数props的处理
  4. hooks 让函数可以用 useCallback 这样,只有 依赖 变化时,函数才会变化,这导致了一个问题。组件开发者会产生误解,这样函数就是一个有效依赖了,其实不应该这么做。
  5. 子组件使用 props 传来的 函数 时,根本不知道 函数 依赖了什么东西,必须向上找看看到底依赖了什么东西,才知道它到底什么时候会变,才敢放心地将 这个函数 放到 依赖数组中。所以这只独立开发通用组件时,就是一个不可能的事情,**也就是对于引用类型(不仅仅是函数),在组件开发场景,必然存在隐式依赖**。
  6. ```typescript
  7. // From: https://www.zhihu.com/question/508780830/answer/2289574081
  8. // parent
  9. const fetchData = query => {
  10. const url = 'https://hn.algolia.com/api/v1/search?query=' + query
  11. return fetch(url).then(x => x.text())
  12. }
  13. // child
  14. useEffect(() => {
  15. fetchData(query).then(result => {
  16. setResult(result);
  17. })
  18. },[query])
  19. // 在 query 改变时调用“纯”函数 fetchData
  20. // 这里的 “纯” 指不隐式依赖state

所以对于通用组件的场景,所有来自 props 的函数就应当假设,没有做过任何处理,也就不可作为依赖。而如果有需要通过判断 是否触发该函数的逻辑,则应该让外部传递真正的可用依赖。典型的就是 fecher场景,不依赖 fecher 本身,而依赖 fetcher 调用过程中使用到的 变量,或额外的应该触发 fetcher 调用的 状态。另外通过显式定义 useEffect 的依赖,也能更明确地表达 当前 副作用 会在以下 状态 变更时执行 这个更准确的语义表达。
#useCallback

Programming

CPS & Algebraic Effects

CPS:Continuation-passing style:

怎样理解 Continuation-passing style? (E)CPS 同 Monad 是“等价”的,理论上任何 Monad 都可以通过等价的 CPS 形式表达(或 shift/reset)出来,这部分可以看 The Essence of Functional Programming 和 Representing Monads。

https://zhuanlan.zhihu.com/p/380855727
https://en.wikipedia.org/wiki/Continuation-passing_style

In functional programming, continuation-passing style is a style of programming in which control is passed explicitly in the form of a continuation. A function written in continuation-passing style takes an extra argument: an explicit “continuation”, i.e. a function of one argument. When the CPS function has computed its result value, it “returns” it by calling the continuation function with this value as the argument. That means that when invoking a CPS function, the calling function is required to supply a procedure to be invoked with the subroutine’s “return” value. Expressing code in this form makes a number of things explicit which are implicit in direct style. These include: procedure returns, which become apparent as calls to a continuation; intermediate values, which are all given names; order of argument evaluation, which is made explicit; and tail calls, which simply call a procedure with the same continuation, unmodified, that was passed to the caller.

CPS 是一种编程风格, 通过 回调函数 来 返回结果控制流程。
CPS 风格中, 每一个函数都会显式的接收一个回调函数(continuation),
通过回调函数传递当前函数计算结果并控制下一个函数的调用.

Algebraic Effects: 可以当做参数传递的副作用

函数式编程中的 algebraic effects 是什么?

函数可以用来描述一段操作,且这段操作只在 函数执行 时,才进行,所以通过返回函数,可达到 中断当前操作流程,等待 后续触发 的效果。
函数如果 输入相同,输出相同,则为 Pure。

  • CPS 风格的代码里是通过 next 控制后续流程, 并通过 next 回调返回调用结果
  • JavaScript throw 操作也是一种流程控制手段, 且 throw 具有调用栈穿透的效果

所以实现 Algebraic Effects 需要两个条件:

  1. 可中断(yield),可继续Continuation(next)的语法;
  2. 函数可作为参数传递和返回;

中断时需要保留中断那刻的上下文,在继续时,使用这个上下文。
中断后继续的调用栈可以不是上次调用的栈。

由于函数作用域随着每次调用都是新的,所以保证函数的纯度,就能保证流程的纯度,但流程中具有某些副作用是无法保证纯度的,所以封闭这些逻辑,为其添加额外的调度逻辑,就能隔离出 具有纯度的逻辑,和 满足约束后的具有纯度的副作用。

Algebraic Effects 通过定义 Effect函数 表明做什么, 通过实现 Handler 来决定要如何做. 从而将 副作用隔离,且为副作用添加了额外的调度逻辑,并且如果可以调度逻辑会从主逻辑中隐藏,而是只保留正确处理的连续流程(也就是类似 await 的同步语法的感觉,抛错,等待等与主流程无关的分支被隐藏或提升了)。

衍生到 React 的 useEffect,执行函数 是定义 做什么,依赖数组 决定是否做,Fiber 或其他的某种调度策略决定 如何做(何时做)。每次组件按照树形结构的遍历渲染,并且 render 后的回溯 useEffect 并确认是否触发,中间是没有副作用的,副作用被限制在了 useEffect 的执行中。
所以 React 其实只定义了 如何做(何时做) 的正确性(当前组件render结束时,触发逻辑,是React觉得正确的 如何做,或 何时做),而 是否做,做什么 是交给开发者自行处理的。

所以无论是 CPS 还是 Algebraic Effects 都是为了实现类似的功能:惰性求值、异步、流程控制…… 在完成这些内容的同时,如果还需要保证 语义 可读,则需要一些额外的模式。
#Algebraic_Effects