likes
comments
collection
share

react hooks 你用对了吗,进阶使用提升你的react hook功底

作者站长头像
站长
· 阅读数 31

React Hooks 进阶使用分享

这篇分享并不过多涉及源码的深度解析,只是希望通过tiny hooks的demo来说明,hooks no magic ,just arrays or list ,从而知道平时我们用hooks应该注意的地方,避免出现闭包引发的BUG

相关文章:

Deep dive: How do React hooks really work?

React hooks: not magic, just arrays

Why React Hooks?

React Hooks 你真的用对了吗?

为什么要上Hooks的车

Why Do We Write super(props)?

为什么要用Hooks?

最初React创建组件的方式是通过React.createClass, 后面随之es6的推出 类 class 关键字,react选择服从ECMAScript标准,实现React Class Component。

举一个我们targeran项目业务魔改的相关例子:

我们根据上层传递的outliner大纲实例,监听了内部的mention$事件,根据发生的mention事件去向服务端拉取对应的mention List,然后展示出来。

class Mention extends React.Component {
  constructor(props) {
    super(props);
    this.updateRepos = this.updateRepos.bind(this);
    this.state = {
      loading: false,
      list:[],
    }
  }
  
  componentDidMount() {
    this.cancel = this.props.outliner.mention$.subscribe(this.updateRepos)
  }
  
  componentDidUpdate(prevProps) {
    if (prevProps.outliner !== this.props.outliner) {
      this.cancel();
      this.props.outliner.mention$.subscribe(this.updateMention)
    }
  }
  
  componentWillMount(){
     this.cancel();
  }

    
  updateMention({id}) {
    this.setState({ loading: true });
    fetchMentionList(id).then((list) =>
      this.setState({
        list,
        loading: false,
      })
    );
  }
  
  render() {
    if (this.state.loading === true) {
      return <Loading />;
    }
    return (
      <ul>
        {this.state.list.map(({ name, handle, stars, url }) => (
          <li key={name}>
            <ul>
              <li>
                <a href={url}>{name}</a>
              </li>
              <li>@{handle}</li>
              <li>{stars} stars</li>
            </ul>
          </li>
        ))}
      </ul>
    );
  }
}

重复的构造函数constructor与重复的函数this绑定

 constructor(props) {
    super(props);
    this.updateRepos = this.updateRepos.bind(this);
    this.xxx =  this.xxx.bind(this)
  }

上面的这段构造函数,相信写过class组件的人不会陌生,甚至很多新手教程都是这么写的,而且我们也不厌其烦地写了又写, super(props)调用,提前绑定this,写了这么久,你有没有疑问?我能不能不写呢?

Q:为什么如果在constructor中要有super(props),难道就不能直接使用this吗 ?为什么要传递props呢,传递其他参数不行吗?

A:对于super(props) ,使用Class组件,可以将constructor函数方法中组件的状态初始化为实例(this)上的state属性。但是,根据ECMAScript规范,如果你要扩展一个子类(在本例中是React.Component),你必须先调用super才能使用它。也就是说,在使用React时,你还必须记住将props传递给super。因为在React.Component的构造函数对this.props = props进行了赋值。详细文章可以看这个 why-do-we-write-super-props

Q:为什么要把this.updateRepos再绑定一次this重新赋值?

A:对于this.updateRepos = this.updateRepos.bind(this); 是为了防止在后面函数调用,里面的this指向的react 实例对象 丢失了,例如这块

this.props.outliner.mention$.on(this.updateMention)

等到最终调用的时候,由于this是运行时确定的,最后this指向的并不是该 React 组件实例,导致里面调用到的this.setState 报错,特别是我们把这个函数传给dom事件的时候,我们也会因此类似的现象,如果没有提前将this绑定,导致this丢失了也会this.setState报错。

总之,我们在编写业务代码的时候,这些this的指向问题不应该是我们关心的,实际是使用了class引入的。

虽然随着js的发展跟react完善,后面class Component提供了默认的constructor,帮我们做了super(props)这件事情,然后我们可以通过箭头函数解决this绑定的问题,class 提供了Class Field 帮我们解决constructor中初始化state属性的烦恼。

但是 class Component带来的问题不仅仅是这些,我们经常用react,就能理解react万物皆是组件的思想,问题不是出现在组件模型,而是出现如何实现这个组件模型上。

一直以来,我们构建的React组件的非UI逻辑,一直被耦合到组件的生命周期中,如下

componentDidMount() {
    this.cancel = this.props.outliner.mention$.subscribe(this.updateRepos)
  }
  
componentDidUpdate(prevProps) {
    if (prevProps.outliner !== this.props.outliner) {
      this.cancel();
      this.props.outliner.mention$.subscribe(this.updateMention)
    }
}

componentWillMount(){
     this.cancel();
}


updateMention({id}) {
    this.setState({ loading: true });
    fetchMentionList(id).then((list) =>
      this.setState({
        list,
        loading: false,
      })
    );
}

一个事件监听的绑定解绑,要在componentDidMount, componentDidUpdate,componentWillMount

都需要处理一下,如果我们我们在分享页面或者精选页面,想复用这块的逻辑的话,那么我们不得不通过高阶组件/Render Props的方式进行处理

// High Order Component
function WithMentionEvent(Component) {
  return class extends React.Component {
    state = {
      list: [],
      loading: true,
    };
    componentDidMount() {
      this.cancel = this.props.outliner.mention$.subscribe(this.updateRepos);
    }
    componentDidUpdate(prevProps) {
      if (prevProps.outliner !== this.props.outliner) {
        this.cancel();
        this.props.outliner.mention$.subscribe(this.updateMention);
      }
    }
    componentWillMount(){
     this.cancel();
    }    
    updateMention({ id }) {
      this.setState({ loading: true });
      fetchMentionList(id).then((list) =>
        this.setState({
          list,
          loading: false,
        })
      );
    }
    render() {
      return <Component {...this.props} {...this.state} />;
    }
  };
}

// Render Props
 class WithMentionEventComp extends React.Component {
  state = {
    list: [],
    loading: true,
  };
  componentDidMount() {
    this.cancel = this.props.outliner.mention$.subscribe(this.updateRepos);
  }
  componentDidUpdate(prevProps) {
    if (prevProps.outliner !== this.props.outliner) {
      this.cancel();
      this.props.outliner.mention$.subscribe(this.updateMention);
    }
  }
  updateMention({ id }) {
    this.setState({ loading: true });
    fetchMentionList(id).then((list) =>
      this.setState({
        list,
        loading: false,
      })
    );
  }
  render() {
    return this.props.children({ ...this.props, ...this.state });
  }
}


// High order ShareView
class ShareView extends React.Component { 
   // do something 
   render() {
      return <div>share view </div>
   }
}
export default WithMentionEvent(ShareView)

// Render Props ShareView
class ShareView extends React.Component {
  // do something
  render() {
    return (
      <WithMentionEventComp>
        {(props) => <div>share view </div>}
      </WithMentionEventComp>
    );
  }
}

随着我们的组件(例如分享页面),想复用的逻辑越来越多,并且我们的组件有一些定制化需要,最终我们的Hight Order Component或者Render Props 组件会出现不停的封装跟重构, 也许可能是这样的。光是看着就觉得复杂跟难以理解。

// 如果是使用render props
<WithHover>
    // 省略render props..
  <WithTheme hovering={false}>
      // 省略render props..
    <WithAuth hovering={false} theme='dark'>
        // 省略render props..
        <WithMentionEvent {...props} hovering={false} theme='dark' authed={true}>
         (props) => {
           return <ShareView 
                  {...props}
                  id='JavaScript'
                  loading={true} 
                  repos={[]}
                  authed={true}
                  theme='dark'
                  hovering={false} />
         }
      </WithMentionEvent >
    </WithAuth>
  <WithTheme>
</WithHover>

// 使用high order component
WithHover(WithTheme(WithAuth(WithMention(ShareView))))

结合React 官网的 Hook 简介中也列举了推出 Hook 的原因,这里做一下总结:

在组件之间复用状态逻辑很难

class component在组件之间复用状态逻辑很难, 也许使用High Order Component / Render Props 可以复用,但是随着想复用的逻辑越来越多,最后会变成难以理解复杂的 嵌套地狱 一层套一层,复杂组件变得越来越复杂

难以理解的 class

class component 需要考虑super(props)跟this绑定的问题,要去理解class,(实际上js的class是原型继承的语法糖)但我们关心的仅仅只是 props,state 和自顶向下的数据流。

历史原因引入class带来的问题

class 组件会无意中鼓励开发者使用一些让优化措施无效的方案。class 也给目前的工具带来了一些问题。例如,class 不能很好的压缩,并且会使热重载出现不稳定的情况刚也说了是实现组件的模型,也就是class component本身带来的问题,就意味着不能在这个基础上解决,只能另起炉罩,用一种新的方式去解决。

Hook 使我们在非 class 的情况下可以使用更多的 React 特性,赋予了functional component 有状态,生命周期(应该说根据依赖响应式执行hooks),自定义hook抽象非视图逻辑,我们可以将我们上面的代码通过hooks重构

function Mention(props) {
  const [loading, setLoading] = useState(false);
  const [list, setList] = useState([]);
  const updateRepos = useCallback(() => {
    setLoading(true);
    fetchMentionList(id).then((list) => {
      setLoading(true);
      setList(list);
    });
  }, []);
  useEffect(() => {
    const unsubscribe = this.props.outliner.mention$.subscribe(updateRepos);
    return () => {
      unsubscribe();
    };
  }, [props.outliner, updateRepos]);
  if (loading === true) {
    return <Loading />;
  }
  return (
    <ul>
      {list.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li>
              <a href={url}>{name}</a>
            </li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  );
}

很完美,我们只要把需要的依赖丢进去,处理函数丢进去,我们不需要考虑是挂载阶段还是更新阶段,只要我们的依赖变了,那么我们就会重新执行一次事件的绑定与解绑。同时我们也不用再为this,再为super(props)遗留问题烦恼,仅仅去setState就可以了。

甚至我们可以把这部分抽离成一个通用的hooks,其他地方复用就引入这个hooks就可以了

function useMentionEvent(outliner) {
  const [loading, setLoading] = useState(false);
  const [list, setList] = useState([]);
  const updateRepos = useCallback((keyword) => {
    setLoading(true);
    fetchMentionList<MentionList>(keyword).then((list) => {
      setLoading(false);
      setList(list);
    });
  }, []);
  useEffect(() => {
    const subscribtion = this.props.outliner.mention$.subscribe(updateRepos);
    return () => {
      subscribtion.unsubscribe();
    };
  }, [outliner, updateRepos]);
  return [loading, list];
}


function Mention(props) {
  const [loading, list] = useMentionEvent(props.outliner);
  
  if (loading === true) {
    return <Loading />;
  }
  return (
    <ul>
      {list.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li>
              <a href={url}>{name}</a>
            </li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  );
}

当我们需要复用其他逻辑的时候,只要把他们做成一个个自定义hooks,然后在组件中,useXxxhooks(依赖),一切是那么得简单与通俗易懂。

没有 Hooks 之前,高阶组件和 Render Props 本质上都是将复用逻辑提升到父组件中。而 Hooks 出现之后,我们将复用逻辑提取到组件顶层,而不是强行提升到父组件中。这样就能够避免 HOC 和 Render Props 带来的「嵌套地狱」,Hooks 之于 React 就像 async / await 之于 Promise 一样。

function Mention(props) {
  const [loading, list] = useMentionEvent(props.outliner);
  const [perimission,isAuth] = useAuth(props.user); // 根据用户信息返回用户权限身份以及当前是否有权限
  const [titleTheme,contentTheme] = useTheme(props.setting) // 根据是否传入的配置,返回title主题,跟内容主题
  
  if (loading === true) {
    return <Loading />;
  }
  
  return (
    <ul>
      {list.map(({ name, handle, stars, url }) => (
        <li key={name}>
          <ul>
            <li>
              <a href={url}>{name}</a>
            </li>
            <li>@{handle}</li>
            <li>{stars} stars</li>
          </ul>
        </li>
      ))}
    </ul>
  );
}

怎么用Hooks?

为什么有时候项目中hooks 中的useEffect用得混乱?有时候useEffect会出现奇怪的bug(表现在引用到的state不是最新的)

项目逻辑复杂,使用到的state变量特别多,就会出现useEffect依赖过多,调用混乱的问题。

这种情况体现在:有时候出现在一个useEffect 调用处理的事情太多,不够单一职责上。

该例子表示的是:

  1. 搜索相关的参数有改变的时候,会去调用doSearch
  2. refresh相关的参数有改变的时候,去调用doRefresh
function Example({id, name, address, status, personA, personB, progress}) {
  const [page, setPage] = useState();
  const [size, setSize] = useState();
  const doSearch = useCallback(() => {
    // ...
  }, []);
  const doRefresh = useCallback(() => {
    // ...
  }, []);
  useEffect(() => {
    id && doSearch({name, address, status, personA, personB, progress});
    page && doRefresh({name, page, size});
  }, [id, name, address, status, personA, personB, progress, page, size]);
}

我们希望能在一个useEffect里面处理完我们手头的所有事情,不想用太多useEffect去做,但是其实过犹不及,中庸才是最好的。

在 useEffect 中有两段逻辑,这两段逻辑是相互独立的,因此我们可以将这两段逻辑放到不同 useEffect 中:某些相关依赖负责配合处理各自的useEffect

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);

useEffect(() => {
  page && doRefresh({name, page, size});
}, [name,  page, size]);

好的,我们的useEffect各自的依赖减少了,明确职责了。但是在分离了我们的各个职责useEffect , 拆开各自的依赖之后,发现个别useEffect还是依赖很多,并且这些依赖都是必要的,怎么办 ?

useEffect(() => {
  id && doSearch({name, address, status, personA, personB, progress});
}, [id, name, address, status, personA, personB, progress]);

因为我们这些变量都是搜索相关的,可以考虑把我们分散的变量去聚合起来。

const [searchParams,setSearchParams] = useState({name, address, status, personA, personB, progress})
useEffect(() => {
  id && doSearch(searchParams);
}, [id, searchParams]);

以上例子总结为:useEffect在职责上要做到分散独立,在依赖上做到聚合统一。

当把我们的依赖聚合起来后,我们有时候会需要使用当前state的一部分属性去更新下一个state,例如业务中我们需要去调用某个地址选择器组件的回调函数需要去设置 searchParams的一部分属性: address 的时候,正常情况下,为了保证拿到其他最新的searchParams参数,我们一般会这么做

const handleAddress = useCallback((e) => {
    setSearchParams({... searchParams,address: e.target.value })
},[searchParams])

但是我们发现任何时候searchParams的其中一个属性改变,都会导致整个searchParams被重写,也会导致handleAddress重写更新,我们仅仅是为了拿到最新的searchParams而已,这个时候我们可以通过setState 回调函数去更新我们的值,让我们的handleAddress变成一个无依赖的不变函数。

const handleAddress = useCallback((e) => {
    setSearchParams((prevState)=> ({... prevState,address: e.target.value}))
},[])

同时,利用Ref,我们也能达到我们想要减少依赖的一个效果,我们让handleAddress依然是一个无依赖的函数,但是paramsRef帮我们保存最新的引用,在我们需要的时候再去取出来就可以了

const [searchParams,setSearchParams] = useState({name, address, status, personA, personB, progress})
const paramsRef =  useRef({});

useEffect(() => {
 paramsRef.current = searchParams;
})
paramsRef.current = searchParams;

const handleAddress = useCallback((e) => {
    setSearchParams({... paramsRef.current,address: e.target.value})
},[])

我们还可以把这块抽离成一个自定义的hooks: usePropsRef

下面useEffect会在我们的id,或者searchParams改变的时候重写执行doSearch函数

function usePropsRef(props){
  const propsRef = useRef(props);
  useEffect(() => {
     propsRef.current = props;
  },[props])
  return propsRef;
}
function usePropsRef(val) {
  const propsRef = useRef(val);
  propsRef.current = val;
  return propsRef;
}

const [searchParams,setSearchParams] = useState({name, address, status, personA, personB, progress})
const [refreshParams,setRefreshParams] =  useState({name,  page, size})
const searchparamsRef =  useRef(searchParams);

const handleAddress = useCallback((e) => {
    setSearchParams({... paramsRef.current, address: e.target.value})
},[])

 useEffect(() => {
    id && doSearch(searchParams);
 }, [id, searchParams]);
 
 useEffect(() => {
    id && doRefresh(refreshParams)
 },[id, refreshParams])

但有时候我们仅仅只是在id改变的时候,去触发doSearch 跟doRefresh 函数的执行,其他参数改变的时候,我们并不想要去触发,我们仅仅是想在函数调用的时候拿到他们的最新值,这个时候我们可以利用Ref来减少我们的依赖数组变量, 并保证我们的useEffect仅受传入id的影响。

这种情况在我们业务过程中也经常会遇到:

举一个在我们的业务中常见的例子,我们会因为model的实例变化(例如重置文档,更新当前文档如模板文档),然后去执行某一些操作,例如重新实例化outliner或者minder,但是outliner跟minder除了依赖model去实例化之外,还依赖到的某些state变量作为传入配置或者绑定的事件监听函数,但是我们并不想其他变量依赖改变,导致outliner跟minder不断销毁重建,对于我们来说代价是不小的,我们有代价更小的方式去做,如调用实例的方法,如outliner.config.setConfig(ConfigKey,val)。我们仅仅只需要在model实例改变的时候,去重新实例化minder跟outliner,并保证拿到的config的state参数,还有绑定的事件监听函数是最新的就可以了。

function Example({id, name, address, status, personA, personB, progress}) {
  const [page, setPage] = useState();
  const [size, setSize] = useState();
  const searchparamsRef = usePropsRef({name, address, status, personA, personB, progress})
  const freshParamsRef = usePropsRef({name,  page, size})
  const doSearch = useCallback(() => {
    // ...
  }, []);
  const doRefresh = useCallback(() => {
    // ...
  }, []);
  useEffect(() => {
    id && doSearch(searchparamsRef.current);
    id && doRefresh(freshParamsRef.current);
  }, [id]);
}

// 在我们的业务中,也是类似的用法
function Editor(props) {
  const [settting, setSetting] = useState({})
  const handleMention = useCallback(() => {
    //do somthing
  },[theme,hovering])
  const settingRef =  usePropsRef(settting);
  const handleMentionRef  = usePropsRef(handleMention)
  useEffect(() => {
    const outliner = new Outliner(settingRef.current);
    const minder =  new Minder(settingRef.current);
    return () => {
    outliner.mention$.subscribe(handleMentionRef)
    minder.mention$.subscribe(handleMentionRef)
    }
  },[props.model])
}

useEffect确实很方便,只要依赖改变了,我们就可以用来做一些事情,但有时候会稍不注意,就会用处问题。

  1. 我们过多地使用依赖改变去处理其他事情,应该说是不相关的事情,不符合我们的相关依赖对应单一职责约定,让组件逻辑变得负责复杂跟混乱。
  2. 有时候用的方式跟依赖没对,也会引入其他的bug,如state无更新。

例如下面的例子:

过度使用index这个state依赖去调用多个useEffect, 然后去做了太多的事,包括事件绑定(其实并不需要去这么做)以及多余的分散调用拆分,对应了1。

同时也没有处理好useEffect中的state,或者callback更新,引发闭包陷阱问题。对应了2。

function DoSomthingByIndex() {
  const [index, setIndex] = useState(-1);
  const [openModal, setOpenModal] = useState(false);
  const [editable, setEditable] = useState(false);
  const doSomething = () => {
    if (openModal && editable) {
      setOpenModal(true);
    }
  };
  const doAnotherSomething = () => {
    // do doSomething
  };
  const handleDocumentClick = () => {
    if (index !== 0) return;
    // 处理全局点击的逻辑
    // 然后走到下一步
    setIndex(prev => prev + 1);
  };
  const handleNodeDrill = () => {
    if (index !== 2) return;
    // 处理钻入之后的一些逻辑
    // 然后走到下一步
    setIndex(prev => prev + 1);
  };
  // 处理index为0,对应可能是打开一个新手面板
  useEffect(() => {
    if (index === 0) {
      doSomething();
      setIndex(1);
    }
  }, [index]);
  // 处理index为1,对应可能是打开一个新手气泡提示
  useEffect(() => {
    if (index === 1) {
      doAnotherSomething();
      setIndex(2);
    }
  }, [index]);
  // 处理index为2
  useEffect(() => {
    if (index === 2) {
      doSomething2();
      doAnotherSomething();
      setIndex(3);
    }
  }, [index]);
  // index 大于0 要去绑定全局的点击判断
  useEffect(() => {
    if (index >= 0) {
      document.addEventListener('click', handleDocumentClick);
      return () => {
        document.removeEventListener('click', handleDocumentClick);
      };
    }
    return () => {
      document.removeEventListener('click', handleDocumentClick);
    };
  }, [index]);
  // index 等于2 ,要去绑定处理节点钻入的事件
  useEffect(() => {
    if (index === 2) {
      outliner.nodeDrill$.subscribe(handleNodeDrill);
      return () => {
        outliner.nodeDrill$.subscribe(handleNodeDrill);
      };
    }
  }, [index]);
}
  1. 我们可以把index改变导致的执行函数聚合起来,符合相关依赖对应单一职责,按照条件去调用。
  2. 我们可以提前绑定我们想处理的函数,再在函数里面根据index去处理相关逻辑。这样能保证我们的事件绑定一直是正常的,毕竟把事件是否要绑定交由useEffect的依赖改变去做确实很不可控,而且index这个本来就是doSomthing的逻辑,应该跟事件绑定这件事没有挂钩,不符合我们的依赖->职责约定。
  3. 我们要保证我们的调用函数,在useEffect里面是最新的,这样调用的函数依赖到的state才是基于最新ui的,这样才不会出现bug。
function DoSomthingByIndex() {
  const [index, setIndex] = useState(-1);
  const [openModal, setOpenModal] = useState(false);
  const [editable, setEditable] = useState(false);
  const indexRef = usePropsRef(index);
  const handleDocumentClick = useCallback(() => {
    if (indexRef.current !== 0) return;
    // 处理全局点击的逻辑
    // 然后走到下一步
    setIndex(prev => prev + 1);
  }, []);
  const handleNodeDrill = useCallback(() => {
    if (indexRef.current !== 3) return;
    // 处理钻入之后的一些逻辑
    // 然后走到下一步
    setIndex(prev => prev + 1);
  }, []);
  const doSomething = useCallback(() => {
    if (openModal && editable) {
      setOpenModal(true);
    }
  }, [openModal, editable]);
  const doAnotherSomething = useCallback(() => {
    setEditable(true);
  }, []);
  const doThingsRef = usePropsRef({
    doSomething,
    doAnotherSomething,
  });
  useEffect(() => {
    const things = doThingsRef.current;
    if (index === 0) {
      things.doSomething();
      setIndex(1);
    } else if (index === 1) {
      things.doAnotherSomething();
      setIndex(2);
    } else if (index === 2) {
    }
  }, [index]);
  useEffect(() => {
    document.addEventListener('click', handleDocumentClick);
    return () => {
      document.removeEventListener('click', handleDocumentClick);
    };
  }, [handleDocumentClick]);
  useEffect(() => {
    outliner.nodeDrill$.subscribe(handleNodeDrill);
    return () => {
      outliner.nodeDrill$.subscribe(handleNodeDrill);
    };
  }, [outliner, handleNodeDrill]);
}

由于上面的例子涉及的逻辑比较多,这里单独就useEffect 跟useCallback里面用不正确,导致的闭包陷阱问题,表现为: useEffect或者useCallback的依赖数组,跟调用期间使用到的元素项不一致,导致用到的函数或者state,是之前的旧值.(但在我们的预期中应该是最新值)

举一个简单的例子

const [loading,setLoading] = useState(true);
const handleMention = useCallback(() => {
  if (loading) {
    return;
  }
 // do somthing;
}, [loading]);


useEffect(() => {
  const unsubscribe = outliner.mention$.subscribe(handleMention);
  return () => {
      unsubscribe();
  }
}, [outliner]);

当outliner的事件触发的时候,因为闭包陷阱,handleMention的loading会保持在最开始为true的情况,if(loading)会一直return ,不会往下执行了。

仅从这个demo看,好像很容易看出来问题并做提前修改,但是当我们用ref就减少我们的依赖,或者有时候我们的依赖越多,就越容易忽略某个函数在依赖中进行更新了。

怎么有效避免这种情况呢,我们是程序员,不能仅仅依靠肉眼就保证,也不能保证其他人去这么做,那么我们就需要借助工具eslint-plugin-react-hooks 可以帮我们做这些提醒(比如warning警告,或者error报错),我们去根据这些提醒,保证每个人用hooks写的程序不会出问题。

总结 : useEffect在职责上要做到分散独立,在依赖上做到聚合统一。

Hooks 是什么 ?

Q:了解到hooks带给我们的好处,我们一直在使用的hooks ,是怎么实现的呢 ,是不是有很神奇的魔法 ,让为functional 组件没有实例,却能维护那些state状态 ?

Q:为什么只能在函数最外层调用 Hook,为什么不要在循环、条件判断或者子函数中调用?

常见的React Hooks Api:

  • useState
  • useRef
  • useCallback
  • useMemo
  • useReducer
  • useEffect

这里用数组模拟实际上的hook单链表,hookIndex++代表实际链表hook.next,来进行我们通俗的原理讲解,而不是解读源码的长篇大论,因为大部分同学并没有去看过react的源码(包括我也是,只看了一点),讲了之后,除了有点印象,有时候并不能带来对hooks使用的深入理解,也不能有一个完整的流程印象,甚至会更加蒙蔽,不知道说了啥。

useState

let hookIndex = 0;
const hookStates = [];

function useState(initState) {
  const currentIndex = hookIndex;
  hookStates[currentIndex] = hookStates[currentIndex] || initState;
  function setState(val) {
    hookStates[currentIndex] = val;
    render();  // 这里模拟实际情况是调用了setState 会触发当前组件的render
  }
  return [hookStates[hookIndex++], setState];
}

function App() {
  const [num, setNum] = useState(0);
  return (
    <div>
      <h1>app</h1>
      <p>current num: {num}</p>
      <button
        onClick={() => {
          setNum(num + 1);
        }}
      >
        点击+1
      </button>
    </div>
  );
}
function render() {
  cursor = 0;
  ReactDOM.render(<App />, document.getElementById("root"));
}
render();

Q:为什么只能在函数最外层调用 Hook?为什么不要在循环、条件判断或者子函数中调用。

A:memoizedState 数组(在实际实现是hook单链表)是按 hook定义的顺序(实际是调用next)来放置数据的,如果 hook 顺序变化,memoizedState 数组 并不会感知到,就会出现调用混乱,难以预料的bug。

useCallback vs useMemo

import React from "react";
import ReactDOM from "react-dom";
// 匿名闭包,其实是看不见的,这里用数组模拟实际上的单链表,hookIndex++代表实际上的链表的next
let hookIndex = 0;
const hookStates = [];

function useState(initState) {
  const currentIndex = hookIndex;
  hookStates[currentIndex] = hookStates[currentIndex] || initState;
  function setState(val) {
    hookStates[currentIndex] = val;
    render();
  }
  return [hookStates[hookIndex++], setState];
}

function useCallback(callback, dependencies) {
  if (hookStates[hookIndex]) {
    const [lastCallback, lastDependencies] = hookStates[hookIndex];
    const same = dependencies.every(
      (item, index) => item === lastDependencies[index]
    );
    if (same) {
      hookIndex++;
      return lastCallback;
    } else {
      hookStates[hookIndex++] = [callback, dependencies];
      return callback;
    }
  } else {
    hookStates[hookIndex++] = [callback, dependencies];
    return callback;
  }
}

function useMemo(factory, dependencies) {
  if (hookStates[hookIndex]) {
    const [lastMemo, lastDependencies] = hookStates[hookIndex];
    const same = dependencies.every(
      (item, index) => item === lastDependencies[index]
    );
    if (same) {
      hookIndex++;
      return lastMemo;
    } else {
      const newMemo = factory();
      hookStates[hookIndex++] = [newMemo, dependencies];
      return newMemo;
    }
  } else {
    const newMemo = factory();
    hookStates[hookIndex++] = [newMemo, dependencies];
    return newMemo;
  }
}

const Child = memo((props) => {
  console.log("render Child");
  return <button onClick={props.onClick}>{props.data.num}</button>;
});

function App() {
  const [num, setNum] = useState(0);
  const [name, setName] = useState("");
  const onClick = useCallback(() => {
    setNum(num + 1);
  }, [num]);
  const data = useMemo(() => ({ num }), [num]);
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <Child onClick={onClick} data={data} />
    </div>
  );
}
function render() {
  hookIndex = 0;
  ReactDOM.render(<App />, document.getElementById("root"));
}
render();

我们什么时候才需要去用useMemo或者useCallback?

我们有一些计算值需要传递给子组件,特别是当这些值是数组,对象或者函数,即使是相同的输入,输出也仍然会不同(引用更新),如果使用了props diff (memo / PureComponent)子组件有依赖到这些值, 由于下传的props属性值 每次都会生成新的引用,就会导致触发子组件频繁的re-render跟diff,这点在复杂的子组件就显得更为严重。

由于useMemo或者useCallback的memorizedState 特性,我们就可以用他们来缓存我们计算过的值(引用),虽然使用他们会增加hooks的链表长度跟因为缓存占用更多内存,但没有做memorized 复杂的子组件重新做 virtual dom diff 的代价, 毫无疑问比使用useMemo的代价大多了。

但是如果该计算函数运算逻辑简单,并且返回值只是普通类型,那么我们没必要做缓存,因为每次运算返回的基本类型,不会生成新的引用,也就是传给函数的输入不变,那么输出也是不变的,也不会引发子组件的对比变化,反而useMemo后缓存跟计算diff useMemo的state是否需要更新,代价更大。

// 一般书写方式 每次App.jsx组件触发render,那么子组件TableComp都会跟着触发render
// TableComp.jsx
function TableComp(props) {
   return 
   (<table>
       {props.data.map(...)}
       <button onClick={props.onClick}>...</button> 
   </table>)
}
// App.jsx
const [inputVal, setInputVal] = useState("");
const data = [
  { name: result.name1 },
  { name: result.name2 },
  { name: result.name3 },
];
const computedValue = getComputedValue(inputVal) // 这种仅仅只是返回简单的字符串(基本类型)
return (
  <div>
    <TableComp
      data={data}
      value={computedValue}
      onClick={() => {
        console.log('');
      }}
      styleName={classnames({ active: activePath === item.path })}
    >
    </TableComp>
    <input
      value={inputVal}
      onChange={(e) => {
        setInputVal(e.target.value);
      }}
    />
  </div>
);

/// 优化书写  这么写的好处是,只要没有发生必要的state相关依赖,如reuslt更新导致传递data改变,那么子组件就不会触发新的render
/// 但是现状是我们团队写的组件,很少会使用memo,关注是否会重新render,如果是开发的通用组件库,那么有memo的话,性能是否会更加高效一点 ?
/// App.jsx
const data = useMemo(() => [{name: result.name1},{name: result.name2},{name: result.name3}],[result]);
const onClick = useCallback(() =>  {},[]);
const computedValue = getComputedValue(inputVal) 
return (
  <div>
    <TableComp
      data={data}
      computedValue={computedValue}
      onClick={onClick}
      styleName={classnames({ active: activePath === item.path })}
    >
    </TableComp>
    <input
      value={inputVal}
      onChange={(e) => {
        setInputVal(e.target.value);
      }}
    />
  </div>
);
// TableComp.jsx
const TableComp = memo(() => {
 return (
   <table>
       {props.data.map(...)}
       <button onClick={props.onClick}>...</button> 
   </table>)
})

memo 与 useMemo 与 PureComponent

memo的作用与PureComponent相同,会返回一个 进行props浅比较从而决定是否进行re-render的组件。

useMemo是一个hook,会根据依赖是否改变,从而更新并返回之前记录的factory执行后的结果。

// 相当于使用了PureComponent
function memo(OldFuncionComp) {
  return class NewFuncionComp extends React.PureComponent {
    render() {
      return <OldFuncionComp {...this.props} />;
    }
  };
}

useEffect 与 useLayoutEffect 区别

Q:如果在useEffect或useLayoutEffect,有一个执行js动画的操作,那么页面渲染会有什么区别 ?

react hooks 你用对了吗,进阶使用提升你的react hook功底

赋值给 useEffect 的函数会在组件渲染到屏幕之后执行。

我们可以通俗地理解为,在每次evenLoop之间,useEffect是向宏任务队列推进了一次新的红任务,也就是在当前的宏任务执行结束,布局渲染之后。

useLayoutEffect会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。

我们可以通俗地理解为,在每次eventLoop之间,useLayoutEffect 是向当前宏任务对应的微任务队列,推进了一次新的微任务,也就是在当前的event loop 执行过程,布局渲染之前。

import React, { useRef } from "react";
import ReactDOM from "react-dom";
// 匿名闭包,其实是看不见的,这里用数组模拟实际上的单链表,hookIndex++代表实际上的链表的next
let hookIndex = 0;
const hookStates = [];

function useEffect(callback, dependencies) {
  if (hookStates[hookIndex]) {
    const [lastCancel, lastDependencies] = hookStates[hookIndex];
    const same = dependencies.every(
      (item, index) => item === lastDependencies[index]
    );
    if (same) {
      hookIndex++;
    } else {
      typeof lastCancel === "function" && lastCancel();
      const currentIndex = hookIndex;
      hookStates[hookIndex++] = [, dependencies];
      setTimeout(() => {
        hookStates[currentIndex][0] = callback();
      });
    }
  } else {
    const currentIndex = hookIndex;
    hookStates[hookIndex++] = [, dependencies];
    setTimeout(() => {
      hookStates[currentIndex][0] = callback();
    });
  }
}

function useLayoutEffect(callback, dependencies) {
  if (hookStates[hookIndex]) {
    const [lastCancel, lastDependencies] = hookStates[hookIndex];
    const same = dependencies.every(
      (item, index) => item === lastDependencies[index]
    );
    if (same) {
      hookIndex++;
    } else {
      typeof lastCancel === "function" && lastCancel();
      const currentIndex = hookIndex;
      hookStates[hookIndex++] = [, dependencies];
      Promise.resolve().then(() => {
        hookStates[currentIndex][0] = callback();
      });
    }
  } else {
    const currentIndex = hookIndex;
    hookStates[hookIndex++] = [, dependencies];
    Promise.resolve().then(() => {
      hookStates[currentIndex][0] = callback();
    });
  }
}

function App() {
  const effectRef = useRef(null);
  const layoutEffectRef = useRef(null);
  useEffect(() => {
    effectRef.current.style.transform = "translateX(300px)";
    effectRef.current.style.transition = "all 300ms";
  }, []);
  useLayoutEffect(() => {
    layoutEffectRef.current.style.transform = "translateX(300px)";
    layoutEffectRef.current.style.transition = "all 300ms";
  }, []);
  return (
    <div>
      <div
        ref={effectRef}
        style={{ height: 100, width: 100, backgroundColor: "red" }}
      ></div>
      <div
        ref={layoutEffectRef}
        style={{ height: 100, width: 100, backgroundColor: "red" }}
      ></div>
    </div>
  );
}

function render() {
  hookIndex = 0;
  ReactDOM.render(<App />, document.getElementById("root"));
}

render();

react hooks 你用对了吗,进阶使用提升你的react hook功底

useContext

function useContext(context) {
  return context._currentValue;
}
// 使用了useContext的组件,会根据context. _currentValue是否发生变化而触发render
// 所以保证上层传递的value是一个 memoizedState 是很重要的

useReducer

import React from "react";
import ReactDOM from "react-dom";
// 匿名闭包,其实是看不见的,这里用数组模拟实际上的单链表,hookIndex++代表实际上的链表的next
let hookIndex = 0;
const hookStates = [];
function numberReducer(state, action) {
  switch (action) {
    case "add":
      return state + 1;
    case "decrease":
      return state - 1;
    default:
      return state;
  }
}

function useReducer(reducer, initState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initState;
  const currentIndex = hookIndex;
  function dispatch(action) {
    hookStates[currentIndex] = reducer
      ? reducer(hookStates[currentIndex], action)
      : action;
    render();
  }
  return [hookStates[hookIndex++], dispatch];
}

// 其实useState 本质是用了一个简单版的useReducer调用
function useState(initState) {
  return useReducer(null, initState);
}

function App() {
  const [name, setName] = useState("");
  const [num, dispatch] = useReducer(numberReducer, 0);
  return (
    <div>
      <input
        type="text"
        value={name}
        onChange={(e) => setName(e.target.value)}
      />
      <button
        onClick={() => {
          dispatch("add");
        }}
      >
        {num}
      </button>
    </div>
  );
}

function render() {
  hookIndex = 0;
  ReactDOM.render(<App />, document.getElementById("root"));
}
render();

到这里基本的hooks api模拟讲完了

那不禁会问,我们讲了这么多原理性的demo,会不会跟源码差别很大呢 ? 其实思想都是差不多的,在看了原理demo之后,我们对hooks的实现有了比较浅显的理解,再去看举一个源码例子,也显得更加容易理解了。

在实际源码中,我们使用的useState之类的hook,会从dispatcher拿到, dispatcher从ReactCurrentDispatcher.current获得,这是一个对象的引用,会根据运行时进行变化赋值 ,绑定了对应生命周期的Dispatcher, 里面包含了各种hooks的调用方法,以我们最常用到的useState举例

// ReactHooks.js
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current;
  return dispatcher;
}

export function useState<S>(initialState: (() => S) | S) {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

ReactCurrentDispatcher.current 在组件第一次初始化,进行挂载的时候,会调用HooksDispatcherOnMount

const HooksDispatcherOnMount: Dispatcher = {
  readContext,
  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
};

从HooksDispatcherOnMount拿到挂载阶段使用的useState,实际上是调用了mountState, 里面通过mountWorkInProgressHook方法拿到了当前的hook对象,将它绑定在当前组件对应的Fiber上,之后将初始化的state值保存在hook.memoizedState属性里面,相当于我们后面讲的hookStates[index]保存空间,将对state的更新操作,保存在一个queue中,然后返回了可以对state进行修改的dispatch函数

function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = (hook.queue = {
    last: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  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));
  return [hook.memoizedState, dispatch];
}

我们分析的hook对象是什么东西呢,从源码中找到Hook的类型定义,React 对Hook对象的定义是链表,上一个Hook的next指向下一个Hook对象。在mount阶段,每当我们调用Hooks方法,比如useState,也就是mountState就会调用mountWorkInProgressHook 来创建一个Hook对象节点,并把它添加到Hooks链表上。(我们后面的demo会使用一个数组来模拟,使用索引cursor来模拟hook.next的赋值)

// react-reconciler/src/ReactFiberHooks.js
export type Hook = {
  memoizedState: any,// 指向当前渲染节点 Fiber
  baseState: any,
  baseUpdate: Update<any, any> | null,
  queue: UpdateQueue<any, any> | null,
  next: Hook | null,// link 到下一个 hooks,通过 next 串联每一 hooks
};

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    queue: null,
    baseUpdate: null,
    next: null,
  };
  if (workInProgressHook === null) {
    // 当前workInProgressHook链表为空的话,将当前Hook作为第一个Hook
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // 否则将当前Hook添加到Hook链表的末尾
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

dispatch实际上是调用了dispatchAction方法,我们进到里面可以看到

// react-reconciler/src/ReactFiberHooks.js
// 去除特殊情况和与fiber相关的逻辑
function dispatchAction(fiber,queue,action,) {
    const update = {
      action,
      next: null,
    };
    // 将update对象添加到循环链表中
    const last = queue.last;
    if (last === null) {
      // 链表为空,将当前更新作为第一个,并保持循环
      update.next = update;
    } else {
      const first = last.next;
      if (first !== null) {
        // 在最新的update对象后面插入新的update对象
        update.next = first;
      }
      last.next = update;
    }
    // 将表头保持在最新的update对象上
    queue.last = update;
   // 进行调度工作
    scheduleWork();
}

也就是我们每次执行dispatchAction方法,比如setState之类的方法。就会创建一个保存着此次更新信息的update对象,添加到更新链表queue上,通过链表来存放所有的历史更新操作这种方式,以便在update阶段可以通过这些更新记录计算出到最终的值,最后作为我们的hook.memorizedState。

通过对比发现,我们模拟的hooks,state是保存在我们的全局变量hookState, 在源码中,hook的state都是保存在hook.memorizedState上,这里我们模拟的hookState是作为全局变量存在的,那对应实际源码中的hook对象,是放在哪里呢?我们需要放在一个跟组件对应的地方,也就是组件对应React Fiber Node,具体是在它的memorizedState属性上。

Q:说了这么多,我们已经知道hooks的很多好处了,那hooks是万能的吗,能完全取代class component吗?

A:

React官方:长远来看,我们期望 Hook 能够成为人们编写 React 组件的主要方式。hooks还有一些不完善的地方,目前暂时还没有对应不常用的 getSnapshotBeforeUpdate,getDerivedStateFromError 和 componentDidCatch 生命周期的 Hook 等价写法,也就是错误边界的使用。但我们计划尽早把它们加进来。

Q:Hook 对于 Redux connect() 和 React Router 等流行的 API 来说,意味着什么?

A: react-redux 提供了相应的 hooks,如useSelector, useDispatch,

react-router 提供了 useParams,useLocation,useHistory 等等

最后更多的社区支持意味着我们会更好地拥抱hooks!谢谢大家的聆听~