React Native Hook浅析——state处理

React Native Hook浅析——state处理,第1张

前言

前提知识:函数式组件在每次props、state变动时,都会重新执行整个函数,重新渲染页面。
在使用React的class组件时,我们可以使用state,this.xxx,以及生命周期(componentDidMount、componentDidUpdate、componentWillUnmount)等钩子,但函数式组件却无法使用这些,为解决这个问题,React在函数式组件中引入了hooks(class组件无法使用hooks)。Hook是指是一些可以在函数组件中“钩入”React state及生命周期等特性的函数,其实现原理在此不细述,关键就是闭包+单链表。
hook函数在一个函数组件中可以调用多次,不需要统一在一起:

export default (props: any) => {
  const [num, setNum] = useState(0)
  const [name, setName] = useState("")
  useEffect(()=>{
    // 启动时才执行的 *** 作,相当于componentDidMount
  },[])
  useEffect(()=>{
    // 渲染后执行的 *** 作,相当于componentDidMount+componentDidUpdate
  })
}
只能在函数组件最外层调用 Hook,切记不要在循环、条件判断或者子函数中调用,因为会导致单链表错乱只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook useState

在class组件中可以使用this.state存储页面数据:

class Hooks1Class extends Component {
  constructor(props: any) {
    super(props)
    this.state = {
      num: 0
    }
  }

  render(): React.ReactNode {
    return <SafeAreaView style={styles.root}>
      <Text>{this.state.num}</Text>

      <TouchableOpacity style={styles.button} onPress={() => {
        this.setState({
          num: this.state.num + 1
        })
      }}>
        <Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
      </TouchableOpacity>
    </SafeAreaView>
  }
}

在函数组件中,等加写法就是:

export default (props: any) => {
  const [num, setNum] = useState(0)
  return (
    <SafeAreaView style={styles.root}>
      <Text>{num}</Text>

      <TouchableOpacity style={styles.button} onPress={() => { setNum(num + 1) }}>
        <Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
      </TouchableOpacity>
    </SafeAreaView>
  );
};

这里的setNum与class组件的setState区别就是,函数组件更新state变量总是替换整个state,而不是class组件的合并state。
每个函数组件可以定义多个state,而在不同函数中引用相同组件,组件间的state是完全独立的:

const Component1 = (props: any) => {
  const [num, setNum] = useState(0)
  return (
    <SafeAreaView style={styles.root}>
      <Text>{num}</Text>

      <TouchableOpacity style={styles.button} onPress={() => { setNum(num + 1) }}>
        <Text style={{ fontSize: 16, color: 'white' }} >Add</Text>
      </TouchableOpacity>
    </SafeAreaView>
  );
}

export default (props: any) => {
  return (
    <SafeAreaView style={{ ...styles.root, flexDirection: 'row' }}>
      <Component1 />
      <Component1 />
    </SafeAreaView>
  );
};


可以看到,左右两个Component1组件的state是相互独立的。
以上设置state的方法setNum(num + 1)还可以修改为函数式设置:

 ...
   <TouchableOpacity style={styles.button} onPress={() => { setNum(n => n + 1) }}>
 ...

此方式可以使设置时不会依赖于num的state,导致在useEffect等hooks里产生副作用(下文细述)。
在实际使用useState时,需要适度拆分state,避免冗余 *** 作:

// 摘自官方例子
function Box() {
  // 位置、大小信息
  const [state, setState] = useState({ left: 0, top: 0, width: 100, height: 100 });
  
  useEffect(() => {
    // 模拟鼠标移动时,改变Box坐标信息 
    function handleWindowMouseMove(e) {
      // 展开 「...state」 以确保我们没有 「丢失」 width 和 height
      setState(state => ({ ...state, left: e.pageX, top: e.pageY }));
    }
    // 注意:这是个简化版的实现
    window.addEventListener('mousemove', handleWindowMouseMove);
    return () => window.removeEventListener('mousemove', handleWindowMouseMove);
  }, []);
  // ...

可以看到在setState时,需要使用…state以确保不会丢失Box大小, *** 作冗余,所以可以改成

function Box() {
  const [position, setPosition] = useState({ left: 0, top: 0 });
  const [size, setSize] = useState({ width: 100, height: 100 });
  
  useEffect(() => {
    function handleWindowMouseMove(e) {
      setPosition({ left: e.pageX, top: e.pageY });
    }
    // ...

而对于较复杂的state,如state逻辑较复杂且包含多个子值,或者下一个state依赖于之前的state等,可以使用useReducer替代。

Hook简单原理

先看个小例子:

const f = () => {
    for (var i = 5; i >= 1; i--) {
      setNum(num + 1)
      // 输出啥?for之后num是啥?改成setNum(num=>num + 1)呢?
      console.log(num)
    }
  }

以useState为例,此处需要简单说一下hook的实现思想:

// const [num, setNum] = useState(0)

// 闭包,决定了state只在当前组件内有效,且可以像class组件的state一样“记住”上次的值
// useState可以在组件内多次使用以构建多个state,同时根据函数组件的useState等hook顺序,可以区分出不同的hook,是因为使用了单链表,此处使用数组和index模拟 
let state = [], index = 0;
// 利用Promise微任务,把刷新放到js的事件队列后面
const defer = (fn) => Promise.resolve().then(fn);
function useState(initialValue) {
    // 保存当前的索引
    let currentIndex = index;
    if (typeof initialValue === "function") {
        // 函数式初始化
        initialValue = initialValue();
    }
    // render时更新state(初始化)
    state[currentIndex] = state[currentIndex] === undefined ? initialValue : state[currentIndex];
    const setState = newValue => {
        if (typeof newValue === "function") {
            // 函数式更新
            newValue = newValue(state[currentIndex]);
        }
        state[currentIndex] = newValue;
        // 同时setState的话,index = 0会阻断多次renderComponent
        // 因此for循环setState会只执行一次,且会出现设置不上去的情况(除非用函数式更新)
        if (index!==0) {
            defer(renderComponent);
        }
        // 同时setState的话,index会阻断多次renderComponent
        index = 0;
    };
    // 每个useState递增当前下标
    index += 1;
    return [state[currentIndex], setState];
}
useReducer

没错,这个就是简化版的组件级Redux,实现复杂state管理:

const [state, dispatch] = useReducer(reducer, initialArg, init);
state:当前state值,当使用Object.is(浅层比较)比较两次渲染的state相同时,React将跳过子组件的渲染及副作用的执行,优化性能dispatch:函数组件中可使用形如dispatch({type[, newStateValue]})函数替换state。dispatch在useEffect中无需依赖,React保证其不变性,同时,dispatch也可结合useContext实现避免向下传递回调(下文细述)。reducer:实际处理dispatch的函数,根据 *** 作类型type返回新的stateinitialArg:state的初始值init:当init被设置时,state的初始值会惰性初始化,在被调用时会执行init(initialArg)得到初始的state值
以下实现简单的选项卡:
 // 惰性初始化函数
 // initialArg为页面传递的初始化参数
 function init(initialArg) {
  return {
    ...initialArg,
    list: [{ id: 0, txt: "第0项", checked: false },
    { id: 1, txt: "第1项", checked: false },
    { id: 2, txt: "第2项", checked: false }],
    allFlag: { id: -1, txt: "全选", checked: false }
  }
}

// 根据type执行 *** 作,返回替换的state
function reducer(state, action) {
  switch (action.type) {
    case 'clickItem': {
      let index = action.item.id
      state.list[index].checked = !state.list[index].checked
      let all = state.list.every(item => item && (item.checked == true))
      let newState = { ...state, allFlag: { ...state.allFlag, checked: all } }
      return newState
    }
    case 'clickAll': {
      let all = !(state.allFlag.checked)
      state.list.forEach((item, index) => item.checked = all)
      let newState = { ...state, allFlag: { ...state.allFlag, checked: all } }
      return newState
    }
    default:
      return state;
  }
}

// 控件、函数等建议可以的话尽量定义到外部,避免不小心引用了外部组件属性导致依赖
const ListItem = ({ item, setSelection }) => {
  return (
    <View style={styles.checkboxContainer}>
      <CheckBox
        value={item.checked}
        onValueChange={setSelection}
        style={styles.checkbox}
      />
      <Text style={styles.label}>{item.txt}</Text>
    </View>
  )
}

export default (props) => {
  // 使用useReducer,暴露state、dispatch供使用
  const [state, dispatch] = useReducer(reducer, props.initData, init)

  const renderItem = ({ item }) => {
    return (
      <ListItem
        item={item}
        setSelection={(newValue) => {
          // 触发事件
          dispatch({ type: 'clickItem', item })
        }}
      />
    );
  };

  return (
    <SafeAreaView style={{ ...styles.root }}>
      <ListItem
        // 使用数据
        item={state.allFlag}
        setSelection={(newValue) => dispatch({ type: 'clickAll'})}
      /> 
      <FlatList
        data={state.list}
        renderItem={renderItem}
        keyExtractor={item => item.id} />
    </SafeAreaView>
  );
};
useContext

useContext主要用于省略一步步向下传递属性、依赖等,比如控制底层子组件主题、回调方法等,调用了useContext的组件总会在context值变化时重新渲染,因此需注意若组件渲染耗时,需要使用useMemo等技术提升性能(下文细述)。
使用useContext+useReducer就可以实现轻量级的Redux,可用于避免向下传递回调。假设上个例子的FlatList外层还有一堆包浆层,则需要将state与dispatch一层层传递下去,而结合useContext的话,则可以改造成:

...
// 使用createContext,参数为context的默认值
// createStore像不像?
const ReduxContext = React.createContext({})

const ListView = () => {
  // 引入定义的context,由上层组件中距离当前组件最近的的value属性决定
  const redux = useContext(ReduxContext)
  const renderItem = ({ item }) => {
    return (
      <ListItem
        item={item}
        setSelection={(newValue) => {
          // 使用context
          redux.dispatch({ type: 'clickItem', item })
        }}
      />
    );
  };

  return (
    <FlatList
      data={redux.state.list}
      renderItem={renderItem}
      keyExtractor={item => item.id} />
  )
}

export default (props) => {
  const [state, dispatch] = useReducer(reducer, props.initData, init)

  return (
    <SafeAreaView style={styles.root}>
      <ListItem
        // 使用数据
        item={state.allFlag}
        setSelection={(newValue) => dispatch({ type: 'clickAll' })}
      />
      
      {/* 是不是很眼熟?对,就是类似redux最外层的Provider */}
      <ReduxContext.Provider value={{ state, dispatch }}>
        {/* 模拟多层包浆,此处不需要一层层把state和dispatch传递给ListView */}
        <View style={styles.root}>
          <View>
            <View>
              <View>
                <ListView />
              </View>
            </View>
          </View>
        </View>
      </ReduxContext.Provider>

    </SafeAreaView>
  );
};
参考资料

Hook简介
React Hooks原理探究
React setState、useState核心实现原理–模拟实现

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/1297672.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-06-10
下一篇 2022-06-10

发表评论

登录后才能评论

评论列表(0条)

保存