React:hooks

函数组件实质时一个render函数,只能传递props,无法维持state,也不具备生命周期,通过hooks可以实现对函数组件的增强。

通过HOC可以实现类组件状态逻辑复用,但HOC增加了组件嵌套层级;通过hooks可以实现函数组件状态逻辑复用且不改变组件结构。

在类组件中对于每一个生命周期的逻辑,会随着其复杂度提升而变得维护困难,容易产生bug;使用hooks则可以将组件中相互关联的部分拆分成更小的函数实现,而并非强制按照生命周期划分。

hooks使用注意事项:

  • 仅在函数组件内部调用:外部调用不会被执行
  • 仅在函数组件顶层调用:保证调用次数和次序相同

useState

useState使用示例:

function Counter() {
  const [state, setState] = useState(0)
  setState(state => state ++)
  return (
    <h1 onClick={() => setState(num => num ++)}>
      Count: {state}
    </h1>
  )
}

在上面的示例中,使用 useState方法 获取 state值和更新state方法。

useState原理示例:

// 当前fiber
let wipFiber = null
// hook索引
let hookIndex = null

// 函数组件更新方法
function updateFunctionComponent(fiber) {
  // 更新当前fiber
  wipFiber = fiber
  // 重置hook索引
  hookIndex = 0
  // 重置当前fiber的hooks数组
  wipFiber.hooks = []
  // 执行函数组件方法返回一个虚拟DOM对象,创建数组作为children 
  const children = [fiber.type(fiber.props)]
  // children 执行 reconcile ,进行diffing计算...
  reconcileChildren(fiber, children) 
}

// useState 钩子函数
function useState(initial){
  // 从当前fiber的上一次更新的fiber的hooks数组的对应hook获取老hook
  const oldHook =
    wipFiber.alternate && // 获取上一次更新的fiber
    wipFiber.alternate.hooks && // 获取上一次更新的fiber的hooks数组
    wipFiber.alternate.hooks[hookIndex] // 根据hook索引获取对应hook

  // 首次渲染时,使用初始值,
  // 下一次渲染开始,使用上一次更新的fiber的hook的state值
  const hook = { 
    state: oldHook ? oldHook.state : initial,
    queue: [], // 重置当前hook的回调函数队列queue数组
  }
  
  // 首次渲染时,actions为空数组,下一次渲染时,actions为此次hook的queue
  const actions = oldHook ? oldHook.queue : []
  // 通过遍历actions来逐个顺序执行action
  actions.forEach(action => { 
    hook.state = action(hook.state) // 传入state执行setState的回调函数
  })

  // 更新state的方法
  const setState = action => {
  	// 将传入setState的回调函数加入hook的回调函数队列queue数组中
    hook.queue.push(action)

	/* 
	注意:在下面的代码中,执行了与render方法类似的逻辑
	更新wipRoot并设置为nextUnitOfWork,使workLoop从下一个工作单元开始新render
	这将导致 如果在render过程中接受到setState的更新操作,
	会将当前工作中的fiber tree直接丢弃,从setState创建的新fiber tree开始重新渲染。
	
	事实上,React使用expiration timestamp来标记每一次更新并以此确定其更新的优先级。
	*/
    // 使用上一次更新的根fiber的创建新fiber
    wipRoot = { // 将新fiber更新到工作中的根fiber
      dom: currentRoot.dom,
      props: currentRoot.props,
      alternate: currentRoot, 
    }
    // 将新fiber设置为下一个工作单元
    nextUnitOfWork = wipRoot
    // 清空将要删除的fiber列表
    deletions = []
  }

  // 更新当前​fiber的hooks数组
  wipFiber.hooks.push(hook)
  // 更新hook索引
  hookIndex++
  return [hook.state, setState]
}
const React = { useState }

在上面的代码示例中,当一个函数组件内的useState被调用时,将这次useState调用记录创建为一个名为hook的对象,hook对象保存了一次useState调用返回的state值和queue数组,queue数组保存了当前render过程中针对当前useState的所有action,一个action为执行一次useState后返回的setState方法的一次调用,通过在该组件的fiber上维护的名为hooks的数组,找出该组件函数执行时将会被调用的全部useState记录的hook,使用hookIndex可以从hooks数组中找到当前正在执行的useState记录的hook,通过该组件fiber中保存的上一次更新的fiber的hooks数组中的hookIndex对应的useState调用记录的hook,通过遍历执行hook的queue中所有setState调用的action并传入hook的state值,从而获取更新后的state值,最后返回state值和setState方法。

使用示例解析:

function Counter() {
  const [state, setState] = useState(0)
  /*
  此次调用useState,对应类组件constructor生命周期,
  效果为初始化state为0:
  初始化当前hook为{state: 0, queue: []},
  设置组件fiber的hooks数组为[{state: 0, queue: []}],
  此次调用的hookIndex为0。
  */
  setState(state => state ++)
  /*
  此次调用setState,对应类组件getDerivedStateFromProps生命周期,
  效果为更新state值为1:
  更新当前hook为{state: 0, queue: [state => state ++]},
  中断当前render执行,立即开始一次新的render,
  在新的render运行到当前fiber时,
  从组件fiber找到上一次更新时的fiber的hooks中hookIndex为0的hook,
  即{state: 0, queue: [state => state ++]},
  初始化当前hook为{state: 0, queue: []},
  运行上一次更新时的hook中的queue中的全部action,即state => state ++,
  更新当前hook为{state: 1, queue: []},
  设置组件fiber的hooks数组为[{state: 1, queue: []}]。
  注意:由于此操作会导致丢弃当前render,重新执行render,应尽量避免。
  */
  return (
    <h1 onClick={() => setState(num => num ++)}>
      Count: {state}
    </h1>
  )
  /*
  页面渲染完成,h1的对应fiber的hooks为[{state: 1, queue: []}],
  当用户点击h1时,回调函数调用setState方法:
  更新当前hook为{state: 1, queue: [num => num ++]},
  开始重新render,在运行到当前fiber时,
  从组件fiber找到上一次更新时的fiber的hooks中hookIndex为0的hook,
  即{state: 1, queue: [num => num ++]},
  初始化当前hook为{state: 1, queue: []},
  运行上一次更新时的hook中的queue中的全部action,即num => num ++,
  更新当前hook为{state: 2, queue: []},
  设置组件fiber的hooks数组为[{state: 2, queue: []}]。
  */
}

对于维护复杂状态,推荐使用useReducer,用法和useState基本一致。
useReducer使用示例:

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      state.push( action.item)
      return [...state];
    case 'remove':
      state.splice(action.index, 1);
      return [...state];
    default:
      throw new Error();
  }
}

function ItemList() {
  const [state, dispatch] = useReducer(reducer, [{index: 0, value: 'value0'}]);
  return [
	<button onClick={()=>{ 
	  const index = state.length;
	  dispatch({type: 'add', item: {index, value: 'value'+index}})
	}}>add</button>,
	<ul>
	  {state.map((item, key) => <li onClick={()=>{
	  	dispatch({type: 'remove', index: key})
	  }}>{item}</li>)}	
    </ul>
 ]
}

useEffect

useEffect使用示例:

function Counter({num}) {
  useEffect(()=>{
    consolr.log('component mounted or updated');
    return ()=>{
      consolr.log('component unmounted');
    }
  })
  useEffect(()=>{
    consolr.log('component mounted');
  }, [])
  useEffect(()=>{
    consolr.log('props num changed');
  }, [num])
  return (
    <h1>
      Count: {num}
    </h1>
  )
}

在上面的示例中,使用 useEffect 方法在函数组件中实现类似类组件生命周期函数中的逻辑。
useEffect原理示例:

// useState 钩子函数
function useEffect(
  create: () => (() => void) | void,
  deps: Array<mixed> | void | null
){
  // 从当前fiber的上一次更新的fiber的hooks数组的对应hook获取老hook
  const oldHook =
    wipFiber.alternate && // 获取上一次更新的fiber
    wipFiber.alternate.hooks && // 获取上一次更新的fiber的hooks数组
    wipFiber.alternate.hooks[hookIndex] // 根据hook索引获取对应hook

  // 从老Hook获取老effect
  const oldEffect = oldHook ? oldHook.effect : null
  
  // 初始化deps参数
  const nextDeps = deps === undefined ? null : deps;
  
  // 首次渲染时,创建新effect,下一次渲染开始,effect为当前的fiber的hook的effect
  
  const hook = {
     effect: {
		tag: 'NO_EFFECT', 
	    create, // 传入的create
	    destroy: oldEffect ? oldEffect.destroy: undefined, 
	    nextDeps // 传入的deps
     }
  }
  if (
    wibHook === null || // 首次渲染
    nextDeps === null || // 没有传入deps
    nextDeps !==  wibHook.deps // deps发生变化 
  ) {  
    /*
    此处的判断逻辑符合使用示例中的预期结果:
      首次渲染时,运行effect回调,即mount
      首次渲染或更新渲染时,不传入deps时,运行effect回调,即mount和update
      更新渲染时,传入deps且deps发生变化,运行effect回调,即deps变化时的update
      更新渲染时,传入deps且deps没有变化,不运行,即传入deps为[]
    */
    effect.effectTag = 'HOOK_EFFECT';
  }
  wipFiber.hooks.push(hook)
  hookIndex++
  /* 
  注意:
  事实上,React将所有useEffect创建在hook上的effect的保存在memorizeState属性上,
  并构建成名为componentUpdateQueue环形链表。
  */
}

function destroyEffects(fiber) {
  if (fiber.hooks) {
    fiber.hooks
      .filter(
        hook => hook.effect.tag === "HOOK_EFFECT" && hook.effect.destroy
      )
      .forEach(effectHook => {
        effectHook.effect.destroy()
      })
  }
}

function createEffects(fiber) {
  if (fiber.hooks) {
    fiber.hooks
      .filter(
        hook => hook.effect.tag === "HOOK_EFFECT" && hook.effect.create
      )
      .forEach(effectHook => {
        effectHook.effect.destroy = effectHook.effect.create()
      })
  }
}

// 提交fiber
function commitWork(fiber) {
  	if (!fiber) { // fiber tree 遍历完成
    	return
  	}
	let domParentFiber = fiber.parent // 父fiber
 	// 函数组件和类组件自身没有对应的真实DOM
  	while (!domParentFiber.dom) { // 判断是否存在真实DOM
  		// 查找最近的具有真实DOM的父fiber,作为当前fiber的真实DOM的容器
    	domParentFiber = domParentFiber.parent
  	}
  	const domParent = domParentFiber.dom // 父fiber真实DOM
	if (fiber.effectTag === "PLACEMENT") {
	    if(fiber.dom !== null){
		    domParent.appendChild(fiber.dom) // 添加当前fiber真实DOM到父级
	    }
	    createEffects(fiber)
	} else if (fiber.effectTag === "UPDATE") {
		destroyEffects(fiber)
		if(fiber.dom !== null){
			updateDom( // 根据当前fiber更新真实DOM
		      	fiber.dom,
		      	fiber.alternate.props,
		      	fiber.props
		    )
		}
	  	createEffects(fiber)
	} else if (fiber.effectTag === "DELETION") {
		destroyEffects(fiber)
		commitDeletion(fiber, domParent) // 移除需要删除的真实DOM
	}

	/*
	通过两次递归调用commitWork,
	先后分别传入子fiber和兄弟fiber,
	实现深度优先遍历,
	即有子fiber则先遍历子fiber,直到fiber tree最深层级子fiber,
	此时没有子fiber,开始遍历兄弟fiber,直到同级fiber遍历完成,
	此时没有兄弟fiber,如此直到整个fiber tree遍历完成。

	React在fiber tree中实际发生改变的fiber上创建effect,
	将effect构建成一个收集了全部fiber变更的effect list链表,
	在commit阶段执行effect list的全部effect,
	实现了只对实际发生改变的fiber的对应DOM进行更新,
	避免了遍历整个fiber tree造成的性能浪费。
	*/
  	commitWork(fiber.child) // 提交第一个子fiber
  	commitWork(fiber.sibling) // 提交下一个兄弟fiber
}
const React = { useState }

在上面的代码示例中,当一个函数组件内的useEffect被调用时,会将这次useEffect调用记录在组件fiber中创建为一个名为effect的对象,effect对象保存了一次useEffect调用的tag标签、create回调函数、destroy回调函数、deps依赖数据数组,tag标签用来标记effect操作类型,create为传入useEffect的第一个参数,destroy为create函数return值,即组件的unmount回调函数、deps为传入useEffect的第二个参数。通过在该组件的fiber上维护的名为hooks的数组,找出该组件函数执行时将会被调用的全部useEffect记录的hook,使用hookIndex可以从hooks数组中找到当前正在执行的useEffect记录的hook。
在commit时,区分fiber中不同的更新操作类型,通过遍历该组件fiber中的hooks数组中的hook,找出hook中的effect,对于mount和update组件:执行tag为’HOOK_EFFECT’的effect的create回调函数,将返回值更新到该effect的destroy;对于unmount组件:执行tag为’HOOK_EFFECT’的effect的destroy回调函数。

使用示例解析:

function Counter({num}) {
  useEffect(()=>{
    console.log('mounted or updated');
    return ()=>{
      console.log('unmounted');
    }
  })
  /*
  此次调用useEffect效果为:
  componentDidMount和componentDidUpdate输出'mounted or updated',
  componentDidUpdate输出'unmounted'。
  初始化当前hook为{effect: {
    tag: 'HOOK_EFFECT',
    create: ()=>{console.log('mounted or updated')
      return ()=>{console.log('unmounted')}
	},
    destroy: undefined, 
    nextDeps: null,
  }},
  设置组件fiber的hooks数组为[{effect: {...}}],
  此次调用的hookIndex为0。
  */
  useEffect(()=>{
    console.log('mounted');
  }, [])
  /*
  此次调用useEffect效果为:
  componentDidMount输出'mounted'。
  初始化当前hook为{effect: {
    tag: 'HOOK_EFFECT',
    create: ()=>{console.log('mounted')},
    destroy: undefined, 
    nextDeps: [],
  }},
  设置组件fiber的hooks数组为[{effect: {...}}, {effect: {...}}],
  此次调用的hookIndex为1。
  */
  useEffect(()=>{
    console.log('props num changed');
  }, [num])
  /*
  此次调用useEffect效果为:
  num属性变更后的componentDidMount输出'props num changed'。
  假设传入的num属性值为0,则初始化当前hook为{effect: {
    tag: 'HOOK_EFFECT',
    create: ()=>{console.log('props num changed')},
    destroy: undefined, 
    nextDeps: [0],
  }},
  设置组件fiber的hooks数组为[{effect: {...}},{effect: {...}},{effect: {...}}
  ],
  此次调用的hookIndex为2。
  */
  return (
    <h1>
      Count: {num}
    </h1>
  )
  /*
  所有fiber运行完成,会从根fiber进行commit,
  在commit当前fiber时,进行"PLACEMENT"操作,
  执行createEffects方法,遍历Counter组件fiber的hooks的hook,
  执行effect.tag为'HOOK_EFFECT'的effect.create方法,
  并将返回赋值到effect.destroy,
  执行效果为:
  输出'mounted or updated'
  hooks[0].effect.destroy更新为()=>{console.log('unmounted')}
  输出'mounted'
  hooks[1].effect.destroy重新赋值为undefined
  输出'props num changed'
  hooks[2].effect.destroy重新赋值为undefined
  */
  /*
  当num属性更新时,开始重新render,
  假设当前num属性值为1,在运行到当前fiber时,
  hooks[1].effect.tag更新为'NO_EFFECT'
  hooks[2].effect.nextDeps更新为[1]

  所有fiber运行完成,再次commit时,进行"UPDATE"操作,
  执行destroyEffects和createEffects方法,执行效果为:
  输出'unmounted'
  输出'mounted or updated'
  hooks[0].effect.destroy重新赋值为()=>{console.log('unmounted')}
  输出'props num changed'
  hooks[2].effect.destroy重新赋值为undefined
  */
  /*
  当组件unmount时,fiber运行commit进行"DELETION"操作,
  执行destroyEffects方法,执行效果为:
  输出'unmounted'
  */
}

通过useEffect自定义hooks方法分离组件逻辑示例:

function useLoginStatus({account}) {
  const [isLogin, setIsLogin] = useState(false);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }
    fetch('/checkLogin',{account}).then(handleStatusChange);
  }, [account]);
  return isLogin;
}

function User({userInfo}){
  const isLogin = useLoginStatus(userInfo.account)  
  return <div className={isLogin? 'online':'offline'}>
    {userInfo.username}
  </div>
}

在上面的代码示例中,useLoginStatus会在account属性变化时重新获取用户登录状态并保存到isLogin,在User组件中使用useLoginStatus的返回值更新用户切换账号时的展示样式,useLoginStatus也可以复用到其他函数组件中,实现逻辑复用和分离,同时也不会增加组件结构的嵌套层级,User组件自身更加简洁,有利于维护。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值