useState 解析

useState 使用

通常我们这样来使用 useState 方法

function App() {
  const [num, setNum] = useState(0);
  const add = () => {
    setNum(num   1);
  };
  return (
    <div>
      <p>数字: {num}</p>
      <button onClick={add}>  1 </button>
    </div>
  );
}

useState 的使用过程,我们先模拟一个大概的函数

function useState(initialValue) {
  var value = initialValue
  function setState(newVal) {    
    value = newVal
  }
  return [value, setState]
}

这个代码有一个问题,在执行 useState 的时候每次都会 var _val = initialValue,初始化数据;

于是我们可以用闭包的形式来保存状态。

const MyReact = (function() {
   // 定义一个 value 保存在该模块的全局中
  let value
  return {
    useState(initialValue) {
      value = value || initialValue 
      function setState(newVal) {
        value = newVal
      }
      return [value, setState]
    }
  }
})()

这样在每次执行的时候,就能够通过闭包的形式 来保存 value。

不过这个还是不符合 react 中的 useState。因为在实际操作中会出现多次调用,如下。

function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setNum('Dom');
  };
  const handleAge = () => {
    setAge(age   1);
  };
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
       <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}

因此我们需要在改变 useState 储存状态的方式

useState 模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook  ], setState]
    }
  }
})()

因此当重新渲染 App 的时候,再次执行 useState 的时候传入的参数 kevin , 0 也就不会去使用,而是直接拿之前 hooks 存储好的值。

hooks 规则

官网 hoos 规则中明确的提出 hooks 不要再循环,条件或嵌套函数中使用。

为什么不可以?

我们来看下

下面这样一段代码。执行 useState 重新渲染,和初始化渲染 顺序不一样就会出现如下问题

如果了解了上面 useState 模拟写法的存储方式,那么这个问题的原因就迎刃而解了。相关参考视频:传送门

useEffect 解析

useEffect 使用

初始化会 打印一次 ‘useEffect_execute’, 改变年龄重新render,会再打印, 改变名字重新 render, 不会打印。因为依赖数组里面就监听了 age 的值

import React, { useState, useEffect } from 'react';
function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age   1);
  };
  useEffect(()=>{
    console.log('useEffect_execute')
  }, [age])
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

useEffect 的模拟实现

const MyReact = (function() {
  // 开辟一个储存 hooks 的空间
  let hooks = []; 
  // 指针从 0 开始
  let currentHook = 0 ;
  // 定义个模块全局的 useEffect 依赖
  let deps;
  return {
    // 伪代码 解释重新渲染的时候 会初始化 currentHook
    render(Component) {
      const Comp = Component()
      Comp.render()
      currentHook = 0 // 重新渲染时候改变 hooks 指针
      return Comp
    },      
    useState(initialValue) {
      hooks[currentHook] = hooks[currentHook] || initialValue
      const setStateHookIndex = currentHook
      // 这里我们暂且默认 setState 方式第一个参数不传 函数,直接传状态
      const setState = newState => (hooks[setStateHookIndex] = newState)
      return [hooks[currentHook  ], setState]
    }
    useEffect(callback, depArray) {
      const hasNoDeps = !depArray
      // 如果没有依赖,说明是第一次渲染,或者是没有传入依赖参数,那么就 为 true
      // 有依赖 使用 every 遍历依赖的状态是否变化, 变化就会 true
      const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
      // 如果没有依赖, 或者依赖改变
      if (hasNoDeps || hasChangedDeps) {
        // 执行 
        callback()
        // 更新依赖
        deps = depArray
      }
    },

  }
})()

useEffect 注意事项

依赖项要真实

依赖需要想清楚。

刚开始使用 useEffect 的时候,我只有想重新触发 useEffect 的时候才会去设置依赖

那么也就会出现如下的问题。

希望的效果是界面中一秒增加一岁

import React, { useState, useEffect } from 'react';
function App() {
  const [name, setName] = useState('Kevin');
  const [age, setAge] = useState(0);
  const handleName = () => {
    setName('Don');
  };
  const handleAge = () => {
    setAge(age   1);
  };
  useEffect(() => {
    setInterval(() => {
      setAge(age   1);
      console.log(age)
    }, 1000);
  }, []);
  return (
    <div>
      <p>姓名: {name}</p>
      <button onClick={handleName}> 改名字 </button>
      <p>年龄: {age}</p>
      <button onClick={handleAge}> 加一岁 </button>
    </div>
  );
}
export default App;

其实你会发现 这里界面就增加了 一次 年龄。究其原因:

**在第一次渲染中,age0。因此,setAge(age 1)在第一次渲染中等价于setAge(0 1)。然而我设置了0依赖为空数组,那么之后的 useEffect 不会再重新运行,它后面每一秒都会调用setAge(0 1) **

也就是当我们需要 依赖 age 的时候我们 就必须再 依赖数组中去记录他的依赖。这样useEffect 才会正常的给我们去运行。

所以我们想要每秒都递增的话有两种方法

方法一:

真真切切的把你所依赖的状态填写到 数组中

  // 通过监听 age 的变化。来重新执行 useEffect 内的函数
  // 因此这里也就需要记录定时器,当卸载的时候我们去清空定时器,防止多个定时器重新触发
  useEffect(() => {
    const id = setInterval(() => {
      setAge(age   1);
    }, 1000);
    return () => {
      clearInterval(id)
    };
  }, [age]);

方法二

useState 的参数传入 一个方法。

注:上面我们模拟的 useState 并没有做这个处理 后面我会讲解源码中去解析。

useEffect(() => {
    setInterval(() => {
      setAge(age => age   1);
    }, 1000);
  }, []);

useEffect 只运行了一次,通过 useState 传入函数的方式它不再需要知道当前的age值。因为 React render 的时候它会帮我们处理

这正是setAge(age => age 1)做的事情。再重新渲染的时候他会帮我们执行这个方法,并且传入最新的状态。

所以我们做到了去时刻改变状态,但是依赖中却不用写这个依赖,因为我们将原本的使用到的依赖移除了。(这句话表达感觉不到位)

接口无限请求问题

刚开始使用 useEffect 的我,在接口请求的时候常常会这样去写代码。

props 里面有 页码,通过切换页码,希望监听页码的变化来重新去请求数据

// 以下是伪代码 
// 这里用 dva 发送请求来模拟
import React, { useState, useEffect } from 'react';
import { connect } from 'dva';
function App(props) {
  const { goods, dispatch, page } = props;
  useEffect(() => {
    // 页面完成去发情请求
   dispatch({
      type: '/goods/list',
      payload: {page, pageSize:10},
    });
    // xxxx 
  }, [props]);
  return (
    <div>
      <p>商品: {goods}</p>
     <button>点击切下一页</button>
    </div>
  );
}
export default connect(({ goods }) => ({
  goods,
}))(App);

然后得意洋洋的刷新界面,发现 Network 中疯狂循环的请求接口,导致页面的卡死。

究其原因是因为在依赖中,我们通过接口改变了状态 props 的更新, 导致重新渲染组件,导致会重新执行 useEffect 里面的方法,方法执行完成之后 props 的更新, 导致重新渲染组件,依赖项目是对象,引用类型发现不相等,又去执行 useEffect 里面的方法,又重新渲染,然后又对比,又不相等, 又执行。因此产生了无限循环。

Hooks 源码解析

该源码位置: react/packages/react-reconciler/src/ReactFiberHooks.js

const Dispatcher={
  useReducer: mountReducer,
  useState: mountState,
  // xxx 省略其他的方法
}

mountState 源码

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    /*    mountWorkInProgressHook 方法 返回初始化对象    {        memoizedState: null,        baseState: null,         queue: null,        baseUpdate: null,        next: null,      }    */
  const hook = mountWorkInProgressHook();
 // 如果传入的是函数 直接执行,所以第一次这个参数是 undefined
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
    /*    定义 dispatch 相当于    const dispatch = queue.dispatch =    dispatchAction.bind(null,currentlyRenderingFiber,queue);    */ 
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
 // 可以看到这个dispatch就是dispatchAction绑定了对应的 currentlyRenderingFiber 和 queue。最后return:
  return [hook.memoizedState, dispatch];
}

dispatchAction 源码

function dispatchAction<A>(fiber: Fiber, queue: UpdateQueue<A>, action: A) {
  //... 省略验证的代码
  const alternate = fiber.alternate;
    /*    这其实就是判断这个更新是否是在渲染过程中产生的,currentlyRenderingFiber只有在FunctionalComponent更新的过程中才会被设置,在离开更新的时候设置为null,所以只要存在并更产生更新的Fiber相等,说明这个更新是在当前渲染中产生的,则这是一次reRender。所有更新过程中产生的更新记录在renderPhaseUpdates这个Map上,以每个Hook的queue为key。对于不是更新过程中产生的更新,则直接在queue上执行操作就行了,注意在最后会发起一次scheduleWork的调度。    */
  if (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    didScheduleRenderPhaseUpdate = true;
    const update: Update<A> = {
      expirationTime: renderExpirationTime,
      action,
      next: null,
    };
    if (renderPhaseUpdates === null) {
      renderPhaseUpdates = new Map();
    }
    const firstRenderPhaseUpdate = renderPhaseUpdates.get(queue);
    if (firstRenderPhaseUpdate === undefined) {
      renderPhaseUpdates.set(queue, update);
    } else {
      // Append the update to the end of the list.
      let lastRenderPhaseUpdate = firstRenderPhaseUpdate;
      while (lastRenderPhaseUpdate.next !== null) {
        lastRenderPhaseUpdate = lastRenderPhaseUpdate.next;
      }
      lastRenderPhaseUpdate.next = update;
    }
  } else {
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);
    const update: Update<A> = {
      expirationTime,
      action,
      next: null,
    };
    flushPassiveEffects();
    // Append the update to the end of the list.
    const last = queue.last;
    if (last === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // Still circular.
        update.next = first;
      }
      last.next = update;
    }
    queue.last = update;
    scheduleWork(fiber, expirationTime);
  }
}

mountReducer 源码

多勒第三个参数,是函数执行,默认初始状态 undefined

其他的和 上面的 mountState 大同小异

function mountReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  const hook = mountWorkInProgressHook();
  let initialState;
  if (init !== undefined) {
    initialState = init(initialArg);
  } else {
    initialState = ((initialArg: any): S);
  }
    // 其他和 useState 一样
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: reducer,
    lastRenderedState: (initialState: any),
  });
  const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
    null,
    // Flow doesn't know this is non-null, but we do.
    ((currentlyRenderingFiber: any): Fiber),
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

通过 react 源码中,可以看出 useState 是特殊的 useReducer

  • 可见useState不过就是个语法糖,本质其实就是useReducer
  • updateState 复用了 updateReducer(区别只是 updateState 将 reducer 设置为 updateReducer)
  • mountState 虽没直接调用 mountReducer,但是几乎大同小异(区别只是 mountState 将 reducer 设置为basicStateReducer)

注:这里仅是 react 源码,至于重新渲染这块 react-dom 还没有去深入了解。

更新:

分两种情况,是否是 reRender,所谓reRender就是说在当前更新周期中又产生了新的更新,就继续执行这些更新知道当前渲染周期中没有更新为止

他们基本的操作是一致的,就是根据 reducerupdate.action 来创建新的 state,并赋值给Hook.memoizedState 以及 Hook.baseState

注意这里,对于非reRender得情况,我们会对每个更新判断其优先级,如果不是当前整体更新优先级内得更新会跳过,第一个跳过得Update会变成新的baseUpdate,他记录了在之后所有得Update,即便是优先级比他高得,因为在他被执行得时候,需要保证后续的更新要在他更新之后的基础上再次执行,因为结果可能会不一样。

来源

preact 中的 hooks

Preact 最优质的开源 React 替代品!(轻量级 3kb)

注意:这里的替代是指如果不用 react 的话,可以使用这个。而不是取代。

useState 源码解析

调用了 useReducer 源码

export function useState(initialState) {
    return useReducer(invokeOrReturn, initialState);
}

useReducer 源码解析

// 模块全局定义
/** @type {number} */
let currentIndex; // 状态的索引,也就是前面模拟实现 useState 时候所说的指针
let currentComponent; // 当前的组件
export function useReducer(reducer, initialState, init) {
    /** @type {import('./internal').ReducerHookState} */
    // 通过 getHookState 方法来获取 hooks 
    const hookState = getHookState(currentIndex  );
    // 如果没有组件 也就是初始渲染
    if (!hookState._component) {
        hookState._component = currentComponent;
        hookState._value = [
            // 没有 init 执行 invokeOrReturn
                // invokeOrReturn 方法判断 initialState 是否是函数
                // 是函数 initialState(null) 因为初始化没有值默认为null
                // 不是函数 直接返回 initialState
            !init ? invokeOrReturn(null, initialState) : init(initialState),
            action => {
                // reducer == invokeOrReturn
                const nextValue = reducer(hookState._value[0], action);
                // 如果当前的值,不等于 下一个值
                // 也就是更新的状态的值,不等于之前的状态的值
                if (hookState._value[0]!==nextValue) {
                    // 储存最新的状态
                    hookState._value[0] = nextValue;
                    // 渲染组件
                    hookState._component.setState({});
                }
            }
        ];
    }
    // hookState._value 数据格式也就是 [satea:any, action:Function] 的数据格式拉
    return hookState._value;
}

getHookState 方法

function getHookState(index) {
    if (options._hook) options._hook(currentComponent);
    const hooks = currentComponent.__hooks || (currentComponent.__hooks = { _list: [], _pendingEffects: [], _pendingLayoutEffects: [] });

    if (index >= hooks._list.length) {
        hooks._list.push({});
    }
    return hooks._list[index];
}

invokeOrReturn 方法

function invokeOrReturn(arg, f) {
    return typeof f === 'function' ? f(arg) : f;
}

总结

使用 hooks 几个月了。基本上所有类组件我都使用函数式组件来写。现在 react 社区的很多组件,都也开始支持hooks。大概了解了点重要的源码,做到知其然也知其所以然,那么在实际工作中使用他可以减少不必要的 bug,提高效率。

到此这篇关于React深入浅出分析Hooks源码的文章就介绍到这了,更多相关React Hooks内容请搜索Devmax以前的文章或继续浏览下面的相关文章希望大家以后多多支持Devmax!

React深入浅出分析Hooks源码的更多相关文章

  1. ios – React native链接到另一个应用程序

    如果是错误的,有人知道如何调用正确的吗?

  2. ios – React Native – 在异步操作后导航

    我正在使用ReactNative和Redux开发移动应用程序,我正面临着软件设计问题.我想调用RESTAPI进行登录,如果该操作成功,则导航到主视图.我正在使用redux和thunk所以我已经实现了异步操作,所以我的主要疑问是:我应该把逻辑导航到主视图?我可以直接从动作访问导航器对象并在那里执行导航吗?.我对组件中的逻辑没有信心.似乎不是一个好习惯.有没有其他方法可以做到这一点?

  3. 在ios中使用带有React Native(0.43.4)的cocoapods的正确方法是什么?

    我已经挖掘了很多帖子试图使用cocoapods为本地ios库设置一个反应原生项目,但我不可避免地在#import中找到了丢失文件的错误.我的AppDelegate.m文件中的语句.什么是使用反应原生的可可豆荚的正确方法?在这篇文章发表时,我目前的RN版本是0.43.4,而我正在使用Xcode8.2.1.这是我的过程,好奇我可能会出错:1)

  4. ios – React Native WebView滚动行为无法按预期工作

    如何确保滚动事件的行为与ReactNative应用程序中的浏览器相同?

  5. ios – React Native – BVLinearGradient – 找不到’React/RCTViewManager.h’文件

    谢谢.解决方法几天前我遇到了完全相同的问题.问题是在构建应用程序时React尚未链接.试试这个:转到Product=>Scheme=>管理方案…=>点击你的应用程序Scheme,然后点击Edit=>转到Build选项卡=>取消选中ParallelizeBuild然后点击标志添加目标=>搜索React,选择第一个名为React的目标,然后单击Add然后在目标列表中选择React并将其向上拖动到该列表中的第一个.然后转到Product=>再次清理并构建项目.这应该有所帮助.

  6. ios – React Native – NSNumber无法转换为NSString

    解决方法在你的fontWeight()函数中也许变成:

  7. ios – React native error – react-native-xcode.sh:line 45:react-native:command not found命令/ bin/sh失败,退出代码127

    尝试构建任何(新的或旧的)项目时出现此错误.我的节点是版本4.2.1,react-native是版本0.1.7.我看过其他有相同问题的人,所以我已经更新了本机的最新版本,但是我仍然无法通过xcode构建任何项目.解决方法要解决此问题,请使用以下步骤:>使用节点版本v4.2.1>cd进入[你的应用]/node_modules/react-native/packager>$sh./packager.s

  8. 反应原生 – 如何通过Xcode构建React Native iOS应用程序到设备?

    我试图将AwesomeProject应用程序构建到设备上.构建成功并启动屏幕显示,但后来我看到一个红色的“无法连接到开发服务器”屏幕.它表示“确保节点服务器正在运行–从Reactroot运行”npmstart“.看起来节点服务器已经运行,因为当我做npm启动时,我收到一个EADDRINUSE消息,表示该端口已经在使用.解决方法从设备访问开发服务器您可以使用开发服务器快速迭代设备.要做到这一点,你的

  9. 静音iOS推送通知与React Native应用程序在后台

    我有一个ReactNative应用程序,我试图获得一个发送到JavaScript处理程序的静默iOS推送通知.我看到的行为是AppDelegate中的didReceiveRemoteNotification函数被调用,但是我的JavaScript中的处理程序不会被调用,除非应用程序在前台,或者最近才被关闭.我很困惑的事情显然是应用程序正在被唤醒,并且它的didReceiveRemoteNotifi

  10. 如何为iOS的React Native设置分析

    所以我已经完成了一个针对iOS的ReactNative项目,但是我想在其中分析.我尝试了react-native-google-analytics软件包,但是问题阻止了它的正常工作.此外,react-native-cordova-plugin软件包只适用于Android,因此插入Cordova插件进行分析的能力现在已成为问题.我也没有Swift/ObjectiveC的经验,所以将完全失去GA的插入.有没有人有任何建议如何连接GoogleAnalytics的ReactNativeforiOS?

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部