stormqx / front-end-learning

12 stars 0 forks source link

React #13

Open stormqx opened 4 years ago

stormqx commented 4 years ago

推崇组合(组合大于继承)

React有十分强大的组合模式,react也推崇了多种组合的方式来实现组件复用性和可扩展性。和 vue 的 slot 概念一致,Vue为了增强 slot 的能力,引入了很多概念和语法糖(Slots / Named Slots / Scoped Slots),但都能从 react 上找到对应实现。 React 的灵活性更高,相对给用户带来的心智负担小。 例如:

function FancyBorder(props) {
  return (
    <div className={'FancyBorder FancyBorder-' + props.color}>
      {props.children}
    </div>
  );
}
function SplitPane(props) {
  return (
    <div className="SplitPane">
      <div className="SplitPane-left">
        {props.left}
      </div>
      <div className="SplitPane-right">
        {props.right}
      </div>
    </div>
  );
}

function App() {
  return (
    <SplitPane
      left={
        <Contacts />
      }
      right={
        <Chat />
      } />
  );
}
<DataProvider render={data => (
  <h1>Hello {data.target}</h1>
)}/>

React Router 也使用了 render props,利用它可以解决横切关注点(Cross-Cutting Concerns)。但使用 render props 有一些注意事项:

class MouseTracker extends React.Component {
  // 定义为实例方法,`this.renderTheCat`始终
  // 当我们在渲染中使用它时,它指的是相同的函数
  renderTheCat(mouse) {
    return <Cat mouse={mouse} />;
  }

  render() {
    return (
      <div>
        <h1>Move the mouse around!</h1>
        <Mouse render={this.renderTheCat} />
      </div>
    );
  }
}
stormqx commented 4 years ago

避免调停(Reconciliation)

React 和 Vue 都使用了 Virtual DOM 技术。但他们在 侦查变化 和 更新 等方面使用了不同的策略,我们曾经在virtual dom diff算法原理概述中详细的讨论过差异点。这里主要聊聊 React 的调停策略和日常编码中需要注意的点。

从信息获取的角度来看,

举个具体的例子,当一个用户触发了一个点击事件,React并不知道它会引起多少改变。所以只能在内存中渲染整个Virtual DOM,然后在对Virtual DOM进行diff,在将差异点 patch 到真实DOM中。虽然减少了真实DOM操作,但是每次的 virtual DOM 计算也是一笔不小的开销。所以React 提供了 shouldComponentUpdate方法、PureComponentMemo Hooks等手段,允许开发者自行跳过整个渲染过程。他们的底层原理其实是一致的,

ShouldComponentUpdate

shouldComponentUpdate 为例子。当 C1 组件发生变化时,其下面对应的所有子组件都会进入 Render 流程(如果不做干预)。如果我们已知,C2和C7组件是不需要Render的,直接 shouldComponentUpdate 函数返回false,对应的组件及其子组件就不会Render。C1、C3、C6和C8组件由于未对shouldComponentUpdate做处理,Virtual DOM会重新渲染然后diff,在比对了VDOM后,其中C8组件不用改变真实DOM。C1、C3和C6的VDOM有变化,需要改变真实DOM。

Vue 能做到局部更新vdom,是因为在组件初始化时进行了依赖收集,这种方法是一种tradeoff,它能保证开发者不需要做过多的性能优化就可以收获不错的性能,但由于依赖收集的存在,所以在初始化时会引入更多的时间,尤其是对于数据量复杂且首屏敏感的应用来说,会有很大的影响。

React的做法就相当于给用户提供了性能优化的手段,将 vue 中依赖收集的步骤分摊到每个组件,每次render环节中去。强大的灵活性也对应着开发者要做更多的性能优化相关的工作。

当然上述两种的vdom方法并不是最高效的。从前面描述的信息获取角度来讲,两者对于新旧dom的比较,都需要进行diff算法做patch处理。当然还有其他处理方式,Svelte的编译风格是将模板编译为命令式 (imperative) 的原生 DOM 操作,省掉了vdom的diff/patch操作。

不谈具体背景,直接讨论方案孰优孰劣是不切实际的。更合理的事,结合自身业务需求场景做合适的选择。

stormqx commented 4 years ago

React Hooks

Q:为什么必须在函数组件顶部作用域调用Hooks API? A:

useState

  1. dispatchAction 函数是更新 state 的关键,它会生成一个update挂载到Hooks队列中,并提交一个React更新调度,后续的工作和类组件一致。
  2. 理论上可以同时调用措辞dispatch操作,只有最后一次会生效(queue 的 last 指针指向最后一次 update 的 state)
  3. useState更新数据和setState不同,setState会对 state 进行 merge 操作。useState 则是直接覆盖。

useEffect

  1. useState 传入的是具体 state 不同,useEffect传入的是一个callBack函数。与useState最大的不同是执行时机,useEffect callback 是在 组件被渲染为真实DOM后执行。
  2. useEffect调用也会在当前 Fiber 节点的Hooks链中追加一个 Hook 并返回,它的memoizedState存放的是一个 effect 对象,effect 对象最终会被挂载到 Fiber 节点的updateQueue队列。(当Fiber节点都渲染到页面上后,就会开始执行Fiber节点中的updateQueue中所保存的函数)
  3. 组件re-render时,函数组件是重新执行整个函数,其中也包括所有“注册”过的Hooks,默认情况下useEffect callback也会被重新执行。
  4. useEffect可以接受第二个参数deps,用户在re-render时判断是否重新执行callback,deps必须按照实际依赖传入,不能少传或者多传。
  5. deps数据项应该是mutable,比较也是浅比较,传入对象、函数无意义。使用时,应尽可能都传deps

useReducer

const useReducer = (reducer, initState, initFn) => {
  const [state, setState] = useState(initFn ? initFn(initState) : initState);

  const dispatch = useCallback((action) => setState(prev => reducer(prev, action))), [reducer]);

  return useMemo(() => [state, dispatch), [state, dispatch]);
}

useCallback

useCallback<T>(callback: T, deps: Array<mixed> | void | null): T

由于JavaScript的特殊性,当函数签名被作为deps传入useEffect时,还是会引起re-render(即使函数体没有改变)。这种现象在类组件里面也存在:

// 当Parent组件re-render时,Child组件也会re-render
class Parent extends Component {
  render() {
    const someFn = () => {}; // re-render时,someFn函数会重新实例化

    return (
      <>
        <Child someFn={someFn} />
        <Other />
      </>
    );
  }
}

class Child extends Component {
  componentShouldUpdate(prevProps, nextProps) {
    return prevProps.someFn !== nextProps.someFn; // 函数比较将永远返回false
  }
}

function component:

function App() {
  const [count, setCount] = useState(0);
  const [list, setList] = useState([]);
  const fetchData = async () => {
    setTimeout(() => {
      setList(initList);
    }, 3000);
  };

  useEffect(() => {
    fetchData();
  }, [fetchData]);

  return (
    <>
      <div>click {count} times</div>
      <button onClick={() => setCount(count + 1)}>Add count</button>
      <List list={list} />
    </>
  );
}

解决方法:

  1. 函数移到组件外部(缺点是无法读取组件的状态)
  2. 条件允许的话,把函数体移到 useEffect 内部
  3. 如果函数调用不止是 useEffect 内部(比如需要传递给子组件),可以使用 useCallback API 包裹函数。 useCallback 本质是对函数进行依赖分析,依赖变更时才重新执行。

useMemo

简单理解: useCallback(fn, deps) === useMemo(() => fn, deps)

useRef

useRef Hook返回一个ref对象的可变引用,但useRef的用途比ref更广泛,它可以存储任意javascript值而不仅仅是DOM引用。

  1. useRef是所有hooks API中唯一一个返回mutable数据的
  2. 修改useRef值的唯一方法是修改其current的值,且值的变更不会引起re-render
  3. 每一次组件render时useRef都返回固定不变的值