React Hooks 学习

104 阅读6分钟

函数式组件中我们每一次执行的返回值就是 UI

  • 这个函数会从上到下依次执行
  • 当这个树的节点数据或数据类型不同时会更新,如果想优化性能,在不需要的时候我们就要将这些不改变的值缓存起来,useMemouseCallback 就是这个作用。当然在子组件中要使用 memo,不然的话也不会生效。
    • 使用了 memo 子组件,就会先进行浅比较,如果发现是一样的,就不会执行子组件的函数。最好是使用的子组件都使用!
const Test = () => {
    return (
        <View>
            <Text>测试</Text>
        </View>
    )
}
import React, {
  FC,
  useMemo,
  memo,
  useState,
  useRef,
  useEffect,
  useReducer,
  useCallback
} from "react"
import { createContext } from "react"
import { useContext } from "react"
import { View, Text, AppRegistry, Button } from "react-native"

// const Test = () => {
//   // let test = '123'
//   // 如果想数据响应式的变化,需要使用 useState
//   const [test, setTest] = useState('123')
//   // TODO: 这里有一点疑问,就是按钮按了两次,这个函数会被执行两次
//   // console.log(test, '外层')

//   const testFn = () => {
//     console.log(test, '外层')
//   }
//   testFn()

//   return (
//     <View>
//       <Text>{test}</Text>
//       <Button title="test" onPress={() => {
//         // test = "456"
//         setTest('456')
//         console.log('buttondidclick', test)
//       }} />
//     </View>
//   )
// }

const Parent = () => {
  // 官网原话:React 会确保 setState 函数的标识是稳定的,并且不会在组件重新渲染时发生变化。这就是为什么可以安全地从 useEffect 或 useCallback 的依赖列表中省略 setState。
  // 如果你的更新函数返回值与当前 state 完全相同,则随后的重渲染会被完全跳过。
  const [a, setA] = useState(0)
  const [b, setB] = useState(0)
  
  // const doSomething = useCallback(() => {
  //   console.log(a, '======')
  // }, [a])

  const doSomething = () => {
    console.log(a, "======")
  }

  // 下面的例子可以证明,如果传的是 () => {},那么无论如何这个缓存其实都是不会起作用的
  // 因为每次他们的引用都不会是相同的
  console.log((() => {}) === (() => {}))
  const testeq = () => {}
  console.log(testeq === testeq)

  // 等价于上面的写法,但是如果想缓存函数的话,还是用上面的吧,下面的没啥用
  // const doSomething = useMemo(() => () => {
  //   console.log(a, '======')
  // }, [a])

  const myb = () => {
    console.log("这里每次都会被执行的,即使我们依赖的值没有发生变化")
    let sum = 0
    for (let i = 0; i < 100000000; i++) {
      sum += i
    }
    console.log("sum:", sum)
    return b + 100
  }

  // 当我们的计算比较复杂的时候我们就要使用这种方式
  // 或者当我们的页面需要多次使用到我们计算的值
  const memob = useMemo(() => {
    console.log("只有当 B 发生变化时这个才会重新被计算")
    let sum = 0
    for (let i = 0; i < 100000000; i++) {
      sum += i
    }
    console.log("sum:", sum)
    return b + 100
  }, [b])

  const memob1 = useMemo(() => {
    return memob + 10
  }, [memob])

  return (
    <View>
      {/* 下面几种写法完全等价 */}
      {/* <Pure onChange={doSomething} />
      <MemoPure onChange={doSomething} /> */}
      {/* <Pure onChange={useCallback(doSomething, [a]) } />
      <Pure onChange={useCallback(() => { console.log(a) }, [a]) } />
      <MemoPure onChange={useCallback(() => { console.log(a) }, [a]) } />
      <MemoPure onChange={useCallback(() => doSomething(a), [a]) } />
      <MemoPure onChange={useCallback(doSomething, [a])} /> */}
      {/* <Pure1 b={myb()} />
      <Text>{myb()}</Text> */}
      <Text>{memob}</Text>
      <Text>{memob1}</Text>
      <Button
        title="A + 1"
        onPress={() => {
          setA(a + 1)
        }}
      />
      <Button
        title="B + 1"
        onPress={() => {
          setB(b + 1)
        }}
      />
    </View>
  )
}

const Pure = props => {
  const { onChange } = props
  console.log("pure")

  return <Button title="Pure - 测试" onPress={onChange} />
}

const MemoPure = memo(props => {
  const { onChange } = props
  console.log("memopure")

  return <Button title="MemoPure - 测试" onPress={onChange} />
})

const Pure1 = props => {
  const { b } = props

  return <Text>Pure1 -- {b}</Text>
}

// 下面是学习 memo 的用法
const Parent1 = props => {
  const [a, setA] = useState(0)
  const [person, setPerson] = useState({ name: "name", age: 10 })

  return (
    <View>
      <Child1 a={a} />
      <Child2 person={person} />
      <Child3 person={person} />
      <Button
        title="setA"
        onPress={useCallback(() => {
          setA(a + 1)
        }, [a])}
      />
      <Button
        title="setPerson"
        onPress={useCallback(() => {
          person.name += "a"
          person.age += 1
          setPerson(person => ({ ...person }))
          // setPerson(person)
        }, [person])}
      />
    </View>
  )
}

// 从下面的例子就可以看出,如果 props 是一个值,那么使用 memo 就可以直接更改了,而且缓存也起作用
// 如果这里面是一个对象,那么这个 memo 就没什么意义了,因为这个比较总是成功或者失败的
// 我们可以重写这个比较函数,如果不同引用,但是值相同的话也不会更改了
// shallowEqual 的代码:https://github.com/facebook/react/blob/cb7075399376f4b913500c4e377d790138b31c74/packages/shared/shallowEqual.js#L19

// 如果这里我不写的话,这个 Child1 里面的所有东西还会被执行一次
const Child1 = memo(props => {
  const { a } = props
  console.log("child1")
  return (
    <View>
      <Text>{a}</Text>
    </View>
  )
})

// 这种情况不管你怎么弄,都会再次执行,除非你赋值不正确
// 这里其实就是 ts 类型的检查了,保证了我们函数式组件的安全性
// 当然由于这个是闭包,所以他是不能支持泛型的,要支持的话需要使用 function
// 这里面要是不想约束参数类型的话,需要使用 any
const Child2: FC<{ person?: { name?: any; age?: number } | undefined }> = memo(
  props => {
    const { person } = props
    console.log("child2")
    return (
      <View>
        <Text>
          name: {person?.name}- age: {person?.age}
        </Text>
      </View>
    )
  }
)

// 由于我重写了这个比较函数
// TODO: 这块还是有疑问
// 这里会有一个总是,就是 Child3 的值是跳动的,后续再看看这个东西,恶心的很
const Child3 = memo(
  props => {
    const { person } = props
    console.log("child3")
    return (
      <View>
        <Text>
          name: {person.name}- age: {person.age}
        </Text>
      </View>
    )
  },
  (pre, next) => {
    console.log(pre, next)
    return (
      pre.person.name == next.person.name && pre.person.age == next.person.age
    )
  }
)

// 测试 useContext,这个地方好像缓存失效了~
// TODO: 后期继续验证这个问题
const B = createContext(null)

const Parent2 = props => {
  const [a, setA] = useState(10)
  const [b, setB] = useState(20)

  const doSomething = useCallback(() => {
    setB(b + 10)
  }, [])

  return (
    <B.Provider value={{ b, setB, doSomething }}>
      <View>
        <Child21 />
        <Child22 />
        <Button title="Parent2-SetA" onPress={() => setA(a + 1)} />
      </View>
    </B.Provider>
  )
}

// 我不需要 a 这个数据,但是为了我儿子,那么我也就做个中间人
const Child21 = memo(props => {
  console.log("Child21")
  return (
    <View>
      <GrandChild21 />
    </View>
  )
})

const Child22 = memo(props => {
  const { b, setB, doSomething } = useContext(B)
  console.log("Child22")
  return (
    <View>
      <Text>b: {b}</Text>
      <Button title="Child22-SetB" onPress={() => setB(b + 1)} />
      <Button title="Child22-SetB" onPress={doSomething} />
    </View>
  )
})

// 我需要爷爷的数据
const GrandChild21 = memo(props => {
  const { b, setB, doSomething } = useContext(B)
  console.log("GrandChild21")
  return (
    <View>
      <Text>b: {b}</Text>
      <Button title="GrandChild21-SetB" onPress={() => setB(b + 1)} />
      <Button title="GrandChild21-SetB" onPress={doSomething} />
    </View>
  )
})

const Parent3 = props => {
  // 下面的这两种写法函数地址都是不会改变的
  // 换而言之,我们可以很安心的把他传递给子视图,而不用担心重新渲染的问题

  // 但是子视图如果不想被重新渲染,memo 这个也是必须要加的,HOC 组件
  // 会先判断 shouldUpdate,should update 里面使用的就是浅比较算法

  // const [count, dispatch] = useReducer((state, action) => {
  //   switch (action) {
  //     case "d":
  //       return state + 1
  //     case "s":
  //       return state - 1
  //   }
  // }, 0)
  // const [count, dispatch] = useState(0)
  const [value, setValue] = useState(0)

  let dispatch;

  // useEffect 是在 dom 渲染完成后才执行的,所以这个 dispatch 其实就是 undefined
  // 其实也就不存在 dispatch 的问题
  // React 保证了每次运行 effect 的同时,DOM 都已经更新完毕。
  useEffect(() => {
    // 这样写可以赋值,但是子视图的指针指向并不会更改
    console.log('初次会执行')
    dispatch = () => {
      console.log('===============')
    }
    // 这个清除动作是当,这个 useeffect 被重新执行的时候会再次执行,如果不执行,那么好像这个 cleanup 也不会执行
    return () => {
      console.log('卸载会执行')
    }
  }, [value])

  return (
    <View>
      <Text>{value}</Text>
      {/* <Text>{count}</Text> */}
      {/* <ZButton title="add" dispatch={useCallback(() => dispatch('d'), [])} />
      <ZButton title="sub" dispatch={useCallback(() => dispatch('s'), [])} /> */}
      {/* 从上面的和下面的写法比较,就知道 dispatch 的地址是稳定的,不会改变 */}
      <ZButton title="add" dispatch={dispatch} />
      <ZButton title="sub" dispatch={dispatch} />
      <Button title="setValue" onPress={() => {
        console.log('1111')
        setValue(value + 1)
      }} />
    </View>
  )
}

const ZButton = memo(({title, dispatch}) => {
  console.log('渲染了')
  return (
    <Button title={title} onPress={() => dispatch(10)} />
  )
})

// 如何处理函数
// 如何处理数组频繁变化时的措施
const Parent4 = () => {
  const [count, setCount] = useState(0)
  const [another, setAnother] = useState(0)
  const countRef = useRef(null)
  countRef.current = () => {
    console.log(count)
    setAnother(another + 1)
  }
  console.log(count, '每次都是最新值')

  // 这种情况下,我们是无论如何都不需要的
  useEffect(() => {
    // 这种写法也不会因为重复设置,导致定时器多次取消重置而不准确
    const id = setInterval(() => {
      countRef.current()
    }, 1000)

    // 这个函数只会被执行一次
    // 那么这个 current 被捕获了,以后更改了也不知道
    // 所以这个地方只能使用闭包
    // const id = setInterval(countRef.current, 1000)
    return () => clearInterval(id)
  }, [])

  return (
    <View>
      <Text style={{fontSize: 50}}>{count}</Text>
      <Text style={{fontSize: 50}}>{another}</Text>
      <Button title="another" onPress={() => {
        // setAnother(another + 1)
        setCount(count => count + 1)
      }} />
    </View>
  )
}

const Parent5 = () => {
  const [obj, setObj] = useState({name: '', age: 0})
  const [obj1, setObj1] = useObjState({name: '', age: 0})

  return (
    <View>
      <Text>{obj.name}{obj.age}</Text>
      <Text>{obj1.name}{obj1.age}</Text>
      <Button title="setObj" onPress={() => {
        // 如果这里是对象的话,如果只是更改其中的一个值,那么就需要 ...obj,把其他的值也复制过来
        // 这个显然是一个可以复用的逻辑,所以可以抽成一个自定义 hooks
        setObj({
          ...obj,
          name: obj.name + 'a'
        })
      }} />
      <Button title="setObj1" onPress={() => {
        setObj1({name: obj1.name + 'a'})
      }} />
    </View>
  )
}

const useObjState = (initialObj?: any) => {
  const [obj, setObj] = useState(initialObj)
  // 这个包一下,确保是稳定的,和 useState 的 useReducer 一样
  const setObjState = useCallback((value) => {
    setObj({
      ...obj, 
      ...value
    })
  }, [])
  return [obj, setObjState]
}

AppRegistry.registerComponent("Test", () => Parent5)

上面是我验证很多困惑写的 demo,感兴趣的可以一起交流!

相关资料