React源码解析————ReactElementValidator,useMutableSource,startTransition

2021SC@SDUSC

2021SC@SDUSC

ReactElementValidator

ReactElementValidator.js 模块中的方法对 ReactElement 中的
createElement、createFactory、cloneElement方法进行了包装。这些方法会在创建 React 元素时进行一些额外的检查,
并在检查不通过时打印一些警告信息。这些方法被用在开发环境中。这些检查包括:
1.key 检查
2.props 与元素类型是否匹配

对于这部分的学习,我们应该从另一方面认识一下这些警告信息。
这些警告是 React 传达给我们的很有用的信息,说明我们在使用 React 的过程中忽略了一些细节。
因此,我们需要尽可能修复这些警告。

createElementWithValidation

export function createElementWithValidation(type, props, children) {
  const validType = isValidElementType(type);
  
  // 我们在这种情况下发出警告,但不会抛出。
  // 我们期望元素创建成功,在渲染中可能会出现错误。
  if (!validType) {
    // 在开发模式下打印警告
  }

  const element = createElement.apply(this, arguments);

  // 如果使用模拟或自定义函数,结果可能为空。
  // TODO: 当这些不再被允许作为 type 参数时,删除它。
  if (element == null) {
    return element;
  }

  // 如果 type 无效,那么跳过键警告
  // 因为key 验证逻辑不期望非字符串/函数类型,并且可能抛出让人困惑的错误
  // 我们不希望异常行为在 dev 和 prod 之间有所不同。
  // (渲染将抛出一条有用的消息,一旦 type 被修复,就会出现键警告。)
  if (validType) {
    for (let i = 2; i < arguments.length; i++) {
      validateChildKeys(arguments[i], type);
    }
  }

  if (type === REACT_FRAGMENT_TYPE) {
    validateFragmentProps(element);
  } else {
    validatePropTypes(element);
  }

  return element;
}

其中isValidElementType
用于判断目标是否是有效的 React 元素类型

createFactoryWithValidation

export function createFactoryWithValidation(type) {
  const validatedFactory = createElementWithValidation.bind(null, type);
  validatedFactory.type = type;
  // Legacy hook: remove it
  if (__DEV__) {
    // do something
  }

  return validatedFactory;
}

cloneElementWithValidation

export function cloneElementWithValidation(element, props, children) {
  const newElement = cloneElement.apply(this, arguments);
  for (let i = 2; i < arguments.length; i++) {
    validateChildKeys(arguments[i], newElement.type);
  }
  validatePropTypes(newElement);
  return newElement;
}

useMutableSource

useMutableSource 能够让 React 组件在 Concurrent Mode 模式下安全地有效地读取外接数据源,在组件渲染过程中能够检测到变化,并且在数据源发生变化的时候,能够调度更新。

而外部数据源就要从 state 和更新说起 ,无论是 React 还是 Vue 这种传统 UI 框架中,虽然它们都采用虚拟 DOM 方式,但是还是不能够把更新单元委托到虚拟 DOM 身上来,所以更新的最小粒度还是在组件层面上,由组件统一管理数据 state,并参与调度更新。
回到我们的主角 React 上,既然由组件 component 管控着状态 state。那么在 v17 和之前的版本,React 想要视图上的更新,那么只能通过更改内部数据 state 。纵览 React 的几种更新方式,无一离不开自身 state 。先来看一下 React 的几种更新模式。

1.组件本身改变 state 。函数 useState | useReducer ,类组件 setState | forceUpdate 。
2.props 改变,由组件更新带来的子组件的更新。
3.context 更新,并且该组件消费了当前 context 。

无论是上面哪种方式,本质上都是 state 的变化。因为props 改变来源于父级组件的 state 变化。而context 变化来源于 Provider 中 value 变化,而 value 一般情况下也是 state 或者是 state 衍生产物。

从上面可以概括出:state和视图更新的关系 Model => View 。但是 state 仅限于组件内部的数据,如果 state 来源于外部(脱离组件层面)。那么如何完成外部数据源转换成内部状态, 并且数据源变化,组件重新 render 呢?
常规模式下,先把外部数据 external Data 通过 selector 选择器把组件需要的数据映射到 state | props 上。这算是完成了一步,接下来还需要 subscribe 订阅外部数据源的变化,如果发生变化,那么还需要自身去强制更新 forceUpdate 。下面两幅图表示数据注入和数据订阅更新。
图源:掘金社区的“我不是外星人”老师
在这里插入图片描述
在这里插入图片描述
典型的外部数据源就是 redux 中的 store ,redux 是如何把 Store 中的 state ,安全的变成组件的 state 的。
可以用一段代码来表示从 react-redux 中 state 改变到视图更新的流程。

const store = createStore(reducer,initState)

function App({ selector }){
    const [ state , setReduxState ] = React.useState({})
    const contextValue = useMemo(()=>{
        /* 订阅 store 变化 */
        store.subscribe(()=>{
             /* 用选择器选择订阅 state */
             const value = selector(data.getState())
             /* 如果发生变化  */
             if(ifHasChange(state,value)){
                 setReduxState(value)
             }
        })
    },[ store ])    
    return <div>...</div>
}

redux 和 react 本质上是这样工作的。

通过 store.subscribe 来订阅 state 变化,但是本质上要比代码片段中复杂的多,通过 selector (选择器)找到组件需要的 state。 我在这里先解释一下selector,因为在业务组件往往不需要整个 store 中的 state 全部数据,而是仅仅需要下面的部分状态,这个时候就需要从 state 中选择‘有用的’,并且和 props 合并,细心的同学应该发现,选择器需要和 react-redux 中 connect 第一参数 mapStateToProps 联动。对于细节,无关紧要,因为今天重点是 useMutableSource。

如上是没有 useMutableSource 的情况,现在用 useMutableSource 不在需要把订阅到更新流程交给组件处理。如下:

/* 创建 store */
const store = createStore(reducer,initState)
/* 创建外部数据源 */
const externalDataSource = createMutableSource( store ,store.getState() )
/* 订阅更新 */
const subscribe = (store, callback) => store.subscribe(callback);
function App({ selector }){
    /* 订阅的 state 发生变化,那么组件会更新 */
    const state = useMutableSource(externalDataSource,selector,subscribe)
}

通过 createMutableSource 创建外部数据源,通过 useMutableSource 来使用外部数据源。外部数据源变化,组件自动渲染。

功能原理

createMutableSource

createMutableSource 的原理非常简单,和 createContext , createRef 类似, 就是创建一个 createMutableSource 对象,

export function createMutableSource<Source: $NonMaybeType<mixed>>(
  source: Source,
  getVersion: MutableSourceGetVersionFn,
): MutableSource<Source> {
  const mutableSource: MutableSource<Source> = {
    _getVersion: getVersion,
    _source: source,
    _workInProgressVersionPrimary: null,
    _workInProgressVersionSecondary: null,
  };

  if (__DEV__) {
    mutableSource._currentPrimaryRenderer = null;
    mutableSource._currentSecondaryRenderer = null;

    // Used to detect side effects that update a mutable source during render.
    // See https://github.com/facebook/react/issues/19948
    mutableSource._currentlyRenderingFiber = null;
    mutableSource._initialVersionAsOfFirstRender = null;
  }

  return mutableSource;
}

useMutanleSource

对于 useMutableSource 原理也没有那么玄乎,原来是由开发者自己把外部数据源注入到 state 中,然后写订阅函数。 useMutableSource 的原理就是把开发者该做的事,自己做了,这样省着开发者去写相关的代码了。本质上就是 useState + useEffect :
1.useState 负责更新。
2.useEffect 负责订阅。

参数:
hook:不再解释
source:MutableSource < Source > 可以理解为带记忆的数据源对象。
getSnapshot:( source : Source ) => Snapshot :一个函数,数据源作为函数的参数,获取快照信息,可以理解为 selector ,把外部的数据源的数据过滤,找出想要的数据源。

function useMutableSource(hook,source,getSnapshot){
    /* 获取版本号 */
    const getVersion = source._getVersion;
    const version = getVersion(source._source);
    /* 用 useState 保存当前 Snapshot,触发更新。 */
    let [currentSnapshot, setSnapshot] = dispatcher.useState(() =>
       readFromUnsubscribedMutableSource(root, source, getSnapshot),
    );
    dispatcher.useEffect(() => {
        /* 包装函数  */
        const handleChange = () => {
            /* 触发更新 */
            setSnapshot()
        }
        /* 订阅更新 */
        const unsubscribe = subscribe(source._source, handleChange);
        /* 取消订阅 */
        return unsubscribe;
    },[source, subscribe])
}

上述代码中保留了最核心的逻辑:

首先通过 getVersion 获取数据源版本号,用 useState 保存当前 Snapshot,setSnapshot 用于触发更新。
在 useEffect 中,进行订阅,绑定的是包装好的 handleChange 函数,里面调用 setSnapshot 真正的更新组件。
所以 useMutableSource 本质上还是 useState 。

useMutableSource 和 useSubscription 功能类似:

两者都需要带有记忆化的‘配置化对象’,从而从外部取值。
两者都需要一种订阅和取消订阅源的方法 subscribe。

除此之外 useMutableSource 还有一些特点:

useMutableSource 需要源作为显式参数。也就是需要把数据源对象作为第一个参数传入。
useMutableSource 用 getSnapshot 读取的数据,是不可变的。

GitHub例子详解

链接: GitHub.
redux 中 useMutableSource 使用
redux 可以通过 useMutableSource 编写自定义 hooks —— useSelector,useSelector 可以读取数据源的状态,当数据源改变的时候,重新执行快照获取状态,做到订阅更新。我们看一下 useSelector 是如何实现的。

// Somewhere, the Redux store needs to be wrapped in a mutable source object...
const mutableSource = createMutableSource(
  reduxStore,
  // Because the state is immutable, it can be used as the "version".
  () => reduxStore.getState()
);

// It would probably be shared via the Context API...
const MutableSourceContext = createContext(mutableSource);

// Because this method doesn't require access to props,
// it can be declared in module scope to be shared between hooks.
const subscribe = (store, callback) => store.subscribe(callback);

// Oversimplified example of how Redux could use the mutable source hook:
function useSelector(selector) {
  const mutableSource = useContext(MutableSourceContext);

  const getSnapshot = useCallback(store => selector(store.getState()), [
    selector
  ]);

  return useMutableSource(mutableSource, getSnapshot, subscribe);
}

大致流程是这样的:
1.将 redux 的 store 作为数据源对象 mutableSource 。 state 是不可变的,可以作为数据源的版本号。
2.通过创建 context 保存数据源对象 mutableSource。
3.声明订阅函数,订阅 store 变化。store 变化,执行 getSnapshot 。
4.自定义 hooks useSelector 可以在每一个 connect 内部使用,通过 useContext 获取 数据源对象。 用 useCallback 让 getSnapshot 变成有记忆的。
5.最后本质上用的是 useMutableSource 订阅外部 state 变化。



useMutableSource()也可以从非传统来源读取,例如共享位置对象,只要它们可以被订阅并具有"版本"。

// May be created in module scope, like context:
const locationSource = createMutableSource(
  window,
  // Although not the typical "version", the href attribute is stable,
  // and will change whenever part of the Location changes,
  // so it's safe to use as a version.
  () => window.location.href
);

// Because this method doesn't require access to props,
// it can be declared in module scope to be shared between components.
const getSnapshot = window => window.location.pathname;

// This method can subscribe to root level change events,
// or more snapshot-specific events.
//
// Because this method doesn't require access to props,
// it can be declared in module scope to be shared between components.
const subscribe = (window, callback) => {
  window.addEventListener("popstate", callback);
  return () => window.removeEventListener("popstate", callback);
};

function Example() {
  const pathName = useMutableSource(locationSource, getSnapshot, subscribe);

  // ...
}

流程分析:
1.首先通过 createMutableSource 创建一个数据源对象,该数据源对象为 window。 用 location.href 作为数据源的版本号,href 发生变化,那么说明数据源发生变化。
2.获取快照信息,这里获取的是 location.pathname 字段,这个是可以复用的,当路由发生变化的时候,那么会调用快照函数,来形成新的快照信息。
3.通过 popstate 监听 history 模式下的路由变化,路由变化的时候,执行快照函数,得到新的快照信息。
4.通过 useMutableSource ,把数据源对象,快照函数,订阅函数传入,形成 pathName 。

其他的例子,大家可以去GitHub官网上自己去看
useMutableSource是React18的新特性,有可能在未来的React18正式版本中删去,但是它的思想和用法值得我们学习,而且,至少现在它还在package里面嘛,不是吗?

startTransition

React18的新功能:
在React 18中,我们引入了一个新的API,即使你你的应用在大屏幕更新,也能保持更新。
这个新的API让你通过将特定的更新标记为 "transitions "来大幅改善用户互动。React将让你在状态转换时提供视觉反馈,并在转换发生时保持浏览器的响应。

export function startTransition(scope: () => void) {
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = 1;
  try {
    scope();
  } finally {
    ReactCurrentBatchConfig.transition = prevTransition;
    if (__DEV__) {
      if (
        prevTransition !== 1 &&
        warnOnSubscriptionInsideStartTransition &&
        ReactCurrentBatchConfig._updatedFibers
      ) {
        const updatedFibersCount = ReactCurrentBatchConfig._updatedFibers.size;
        if (updatedFibersCount > 10) {
          console.warn(
            'Detected a large number of updates inside startTransition. ' +
              'If this is due to a subscription please re-write it to use React provided hooks. ' +
              'Otherwise concurrent mode guarantees are off the table.',
          );
        }
        ReactCurrentBatchConfig._updatedFibers.clear();
      }
    }
  }
}

简单来说就是用scope来承接传过来的更新,而这个更新可以被打断
举个例子:

import { startTransition } from 'react';


// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});

被startTransition包裹的更新被当作非紧急事件来处理,如果有更紧急的更新,如点击或按键,则会被打断。如果一个过渡被用户打断(例如,连续输入多个字符),React会扔掉未完成的陈旧的渲染工作,只渲染最新的更新。
过渡让你保持大多数交互的敏捷性,即使它们导致了重大的UI变化。它们还可以让你避免浪费时间去渲染那些不再相关的内容。

总结

至此,我对React的源码解析会告一段落,在这个过程中我学到了很多,也对react的各项操作有了更深的理解

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值