hook源码其实不多,但是实现的比较精巧;在diff/index.js中会有一些optison.diff这种钩子函数,hook中就用到了这些钩子函数。
 
在比如options._diff中将currentComponent设置为null
options._diff = vnode => {

    currentComponent = null;

    if (oldBeforeDiff) oldBeforeDiff(vnode);

};
比如这里的options._render,会拿到vnode的_component属性,将全局的currentComponent设置为当前调用hook的组件。
同时这里将currentIndex置为0。
options._render = vnode => {

    if (oldBeforeRender) oldBeforeRender(vnode);

    currentComponent = vnode._component;

    currentIndex = 0;

    const hooks = currentComponent.__hooks;

    if (hooks) {

        hooks._pendingEffects.forEach(invokeCleanup);

        hooks._pendingEffects.forEach(invokeEffect);

        hooks._pendingEffects = [];

    }

};
同时注意getHookState方法,第一次如果currentComponent上没有挂载__hooks属性,就会新建一个__hooks,同时将_list用作存储该hook的state(state的结构根据hook不同也不一样),_pendingEffects主要用作存放useEffect 生成state
function getHookState(index, type) {

    if (options._hook) {

        options._hook(currentComponent, index, currentHook || type);

    }

    currentHook = 0; // 可能有别的用,目前在源码中没有看到用处

    // Largely inspired by:

    // * https://github.com/michael-klein/funcy.js/blob/f6be73468e6ec46b0ff5aa3cc4c9baf72a29025a/src/hooks/core_hooks.mjs

    // * https://github.com/michael-klein/funcy.js/blob/650beaa58c43c33a74820a3c98b3c7079cf2e333/src/renderer.mjs

    // Other implementations to look at:

    // * https://codesandbox.io/s/mnox05qp8

    const hooks = // 如果没有用过hook就在组件上添加一个__hooks属性

        currentComponent.__hooks ||

        (currentComponent.__hooks = {

            _list: [],

            _pendingEffects: []

        });

    // 如果index大于当前list长度就产生一个新的对象
// 所以除了useEffect外其他都不会用到_pendingEffects属性
if (index >= hooks._list.length) { hooks._list.push({}); } return hooks._list[index]; // 返回当前的hook state }
上面中也可以看到hook是通过数组的形式挂载到component中,这也是hook为什么不能在一些if语句中存在;当第一次渲染时,currentIndex为0,随着后续useXXX方法的使用,当初次渲染结束后已经形成了一个list数组,每一个元素就是一个hook产生的state;那么在后续的渲染中会重置currentIndex,那么当本次hook的方法调用与上次顺序不同时,currentIndex的指向就会出现问题。拿到一个错误的结果。
 
 
hook中有四种是比较重要的
 
第一种useMemo系列,衍生出useCallback、useRef
所以这里也可以看到当参数发生改变,每一次都会产生一个新的state或者在之前的基础上修改
export function useMemo(factory, args) {

    /** @type {import('./internal').MemoHookState} */

    const state = getHookState(currentIndex++, 7); // 获取一个hook的state

    if (argsChanged(state._args, args)) { // 可以看到只有当参数改变时,hook的state会被重新修改;旧的参数被存储在state中

        state._value = factory(); // 通过factory生成,如果args不变那么久不会执行factory

        state._args = args;

        state._factory = factory;

    }

    return state._value; // 返回状态值

}
通过useMemo衍生的两个hook也就比较好理解了
export function useRef(initialValue) {

    currentHook = 5;
// 可以看到useRef只是一个有current的一个对象;
return useMemo(() => ({ current: initialValue }), []); } export function useCallback(callback, args) { currentHook = 8; return useMemo(() => callback, args); }
上面中可以看到useRef返回的是一个有current属性的对象,同时内部调用useMemo时传递的第二个参数是空数组,这样就保证每次调用useRef返回的是同一个hook state;为什么每次传递一个新数组而返回值是不同的呢,这就要看argsChanged的实现;
/**

 * @param {any[]} oldArgs

 * @param {any[]} newArgs

 */

function argsChanged(oldArgs, newArgs) {

    return (

        !oldArgs ||

        oldArgs.length !== newArgs.length ||

        newArgs.some((arg, index) => arg !== oldArgs[index])

    );

}
 
可以看到这种实现方式下,及时每次传递一个不同的空数组,那么argsChanged也会返回false。这也解释了为什么useEffect的第二个参数传递空数组就会产生类似componentDidMount效果。
 
 
第二种是useEffect和useLayoutEffect
useEffect是异步执行在每次渲染之后执行,useLayoutEffect是同步执行在浏览器渲染之前执行。
可以看到两者代码中最直接的差异是,useEffect将state放置到component.__hooks._pendingEffects中,而useLayoutEffect将state放置到compoent的_renderCallbacks中。_renderCallbacks会在 diff后的commitRoot中执行
/**

 * @param {import('./internal').Effect} callback

 * @param {any[]} args

 */

export function useEffect(callback, args) {

    /** @type {import('./internal').EffectHookState} */

    const state = getHookState(currentIndex++, 3);

    if (!options._skipEffects && argsChanged(state._args, args)) {

        state._value = callback;

        state._args = args;

        currentComponent.__hooks._pendingEffects.push(state);

    }

}

/**

 * @param {import('./internal').Effect} callback

 * @param {any[]} args

 */

export function useLayoutEffect(callback, args) {

    /** @type {import('./internal').EffectHookState} */

    const state = getHookState(currentIndex++, 4);

    if (!options._skipEffects && argsChanged(state._args, args)) {

        state._value = callback;

        state._args = args;

        currentComponent._renderCallbacks.push(state);

    }

}
当然这里的useLayoutEffect的设置的_renderCallbacks是通过在options中重写了_commit来实现
options._commit = (vnode, commitQueue) => {

    commitQueue.some(component => {

        try {

            component._renderCallbacks.forEach(invokeCleanup);

            component._renderCallbacks = component._renderCallbacks.filter(cb =>
// 如果是useLayoutEffect产生的,就直接执行,否则返回true保证其他的renderCallbacks在正常的阶段执行
cb._value ? invokeEffect(cb) : true ); } catch (e) { commitQueue.some(c => { if (c._renderCallbacks) c._renderCallbacks = []; }); commitQueue = []; options._catchError(e, component._vnode); } }); if (oldCommit) oldCommit(vnode, commitQueue); };
再来看下_pendingEffects的执行时机:
涉及到pendingEffects的执行是两个options的钩子函数,_render和diffed;diffed在组件diff完成时触发,_render在组件的render函数调用之前触发;
options._render = vnode => {

    if (oldBeforeRender) oldBeforeRender(vnode);

    currentComponent = vnode._component;

    currentIndex = 0;

    const hooks = currentComponent.__hooks;

    if (hooks) {

        hooks._pendingEffects.forEach(invokeCleanup);

        hooks._pendingEffects.forEach(invokeEffect);

        hooks._pendingEffects = [];

    }

};

options.diffed = vnode => {

    if (oldAfterDiff) oldAfterDiff(vnode);

    const c = vnode._component;
// 如果hooks中存在pendingEffects数组,那么就在渲染结束后执行
if (c && c.__hooks && c.__hooks._pendingEffects.length) { afterPaint(afterPaintEffects.push(c)); } currentComponent = previousComponent; };
这里得先看diffed函数,如果hooks中存在pendingEffects数组,那么就在渲染结束后执行
afterPaint函数是用来做异步调用的
function afterPaint(newQueueLength) {

    if (newQueueLength === 1 || prevRaf !== options.requestAnimationFrame) {

        prevRaf = options.requestAnimationFrame;

        (prevRaf || afterNextFrame)(flushAfterPaintEffects);

    }

}
afterNextFrame也是利用了requestAnimationFrame函数,其中也可以看到setTimeout函数,这是因为,如果浏览器切换tab页或者变为后台进程时,requestAnimationFrame会暂停,但是setTimeout会正常进行;同时HAS_RAF也是考虑到应用到非浏览器环境时能够正常执行
let HAS_RAF = typeof requestAnimationFrame == 'function';

function afterNextFrame(callback) {

    const done = () => {

        clearTimeout(timeout);

        if (HAS_RAF) cancelAnimationFrame(raf);

        setTimeout(callback);

    };

    const timeout = setTimeout(done, RAF_TIMEOUT);

    let raf;

    if (HAS_RAF) {

        raf = requestAnimationFrame(done);

    }

}
flushAfterPaintEffects是统一来在渲染结束时,处理所有的组件;
并且一次执行完毕之后会清空组件的pendingEffects。
function flushAfterPaintEffects() {

    afterPaintEffects.forEach(component => {

        if (component._parentDom) { // 有父组件的组件才会进行,第一次渲染如果么有挂载到父组件可能不会执行

            try {

                component.__hooks._pendingEffects.forEach(invokeCleanup);

                component.__hooks._pendingEffects.forEach(invokeEffect);

                component.__hooks._pendingEffects = [];

            } catch (e) {

                component.__hooks._pendingEffects = [];

                options._catchError(e, component._vnode);

            }

        }

    });

    afterPaintEffects = [];

}
同时也看到options._render,中如果存在_hooks也会对其中的pendingEffects重新执行一次;这里我理解是对如果渲染阶段没有component._parentDom的一个补偿
options._render = vnode => {

    if (oldBeforeRender) oldBeforeRender(vnode);

    currentComponent = vnode._component;

    currentIndex = 0;

    const hooks = currentComponent.__hooks;

    if (hooks) {

        hooks._pendingEffects.forEach(invokeCleanup);

        hooks._pendingEffects.forEach(invokeEffect);

        hooks._pendingEffects = [];

    }

};
从中也可以看到useEffect设计会带来一些天然的坑,比如useEffect需要清除功能时,不能设置第二个参数为空数组;
  • 如果设置第二个参数为空数组,这种情况下在diffed和_render中都会将pendingEffects进行清除,永远不会执行到清除函数。
  • 当useEffect没有第二个参数,那么第一次渲染后options.diffed函数中的state._value执行,生成state._cleanup,清除pendingEffects;如果函数任意状态改变,在options._render阶段没有pendingEffects不会执行cleanup和state._value;在组件render阶段,state._value被重新改变,将state装入pendingEffects中;在options.diffed中执行invokeCleanup和invokeEffect
  • 当useEffect设置第二个参数为非空数组,那么第一次渲染后options.diffed函数中的state._value执行,生成state._cleanup,清除pendingEffects;只有当useEffect的依赖项改变时(非依赖项变动不会执行该useEffect的清除函数),在options._render阶段没有pendingEffects不会执行cleanup和state._value;在组件render阶段,state._value被重新改变,将state装入pendingEffects中;在options.diffed中执行invokeCleanup和invokeEffect
 
第三种是useReducer,以及衍生的useState
useReducer代码不对,有几个地方需要重点关注一下:
主要是action函数内部这一段:
            action => {
// 通过action来执行reducer获取到下一个状态
const nextValue = hookState._reducer(hookState._value[0], action);
// 状态不等就进行重新赋值,并且触发渲染,新的渲染还是返回hookState._value,但是_value的值已经被修改了
if (hookState._value[0] !== nextValue) {
hookState._value = [nextValue, hookState._value[1]];
// 在diff/index.js中可以看到如果是函数组件没有render方法,那么会对PReact.Component进行实例化
// 这时候调用setState方法同样会触发组件的渲染流程
hookState._component.setState({});
}
}
export function useReducer(reducer, initialState, init) {
const hookState = getHookState(currentIndex++, 2);
hookState._reducer = reducer; // 挂载reducer if (!hookState._component) { // hookState么有_component属性代表第一次渲染
hookState._value = [
!init ? invokeOrReturn(undefined, initialState) : init(initialState), action => {
// 通过action来执行reducer获取到下一个状态
const nextValue = hookState._reducer(hookState._value[0], action);
// 状态不等就进行重新赋值,并且触发渲染,新的渲染还是返回hookState._value,但是_value的值已经被修改了
if (hookState._value[0] !== nextValue) {
hookState._value = [nextValue, hookState._value[1]];
// 在diff/index.js中可以看到如果是函数组件没有render方法,那么会对PReact.Component进行实例化
// 这时候调用setState方法同样会触发组件的渲染流程
hookState._component.setState({});
}
}
]; hookState._component = currentComponent; } return hookState._value; }
而useState就很简单了,只是调用一下useReducer,
而useState就很简单了,只是调用一下useReducer,
export function useState(initialState) { currentHook = 1; return useReducer(invokeOrReturn, initialState); } function invokeOrReturn(arg, f) { return typeof f == 'function' ? f(arg) : f; }
第四种 useContext
在diff中得到了componentContext挂载到了组件的context属性中
export function useContext(context) {
// create-context中返回的是一个context对象,得到provide对象
// Provider组件在diff时,判断没有render方法时,会先用Compoent来实例化一个对象
// 并将render方法设置为doRender,并将constructor指向newType(当前函数),在doRender中调用this.constructor方法
const provider = currentComponent.context[context._id]; const state = getHookState(currentIndex++, 9); state._context = context; // 挂载到state的_context属性中 if (!provider) return context._defaultValue; // 如果么有provider永远返回context的初始值。 if (state._value == null) { // 初次渲染则将组件对provider进行订阅 state._value = true; provider.sub(currentComponent); } return provider.props.value; } useContext使用示例:
import React, { useState ,,useContext, createContext} from 'react';
import './App.css'; // 创建一个 context
const Context = createContext(0) // 组件一, useContext 写法
function Item3 () {
const count = useContext(Context);
return (
<div>{ count }</div>
)
} function App () {
const [ count, setCount ] = useState(0)
return (
<div>
点击次数: { count }
<button onClick={() => { setCount(count + 1)}}>点我</button>
<Context.Provider value={count}>
{/* <Item1></Item1>
<Item2></Item2> */}
<Item3></Item3>
</Context.Provider>
</div>
)
} export default App;

博客园我也真是服了,一个以技术为主的博客网站,竟然每次进入编辑器,插入代码功能只能用一次,第二次死活提交不上,必须关闭浏览器重新打开,我真特么服!!!!

这是TMD把人往掘金、简书、知乎、segmentfault上逼啊!!!!!!!!!!!!!!!!!!

PReact10.5.13源码理解之hook的更多相关文章

  1. PReact10.5.13源码理解

    React源码看过几次,每次都没有坚持下来,索性学习一下PReact部分,网上讲解源码的不少,但是基本已经过时,所以自己来梳理下 render.js部分 import { EMPTY_OBJ, EMP ...

  2. .NET Core 3.0之深入源码理解Startup的注册及运行

    原文:.NET Core 3.0之深入源码理解Startup的注册及运行   写在前面 开发.NET Core应用,直接映入眼帘的就是Startup类和Program类,它们是.NET Core应用程 ...

  3. HashMap源码理解一下?

    HashMap 是一个散列桶(本质是数组+链表),散列桶就是数据结构里面的散列表,每个数组元素是一个Node节点,该节点又链接着多个节点形成一个链表,故一个数组元素 = 一个链表,利用了数组线性查找和 ...

  4. Caffe源码理解2:SyncedMemory CPU和GPU间的数据同步

    目录 写在前面 成员变量的含义及作用 构造与析构 内存同步管理 参考 博客:blog.shinelee.me | 博客园 | CSDN 写在前面 在Caffe源码理解1中介绍了Blob类,其中的数据成 ...

  5. 基于SpringBoot的Environment源码理解实现分散配置

    前提 org.springframework.core.env.Environment是当前应用运行环境的公开接口,主要包括应用程序运行环境的两个关键方面:配置文件(profiles)和属性.Envi ...

  6. jedis的源码理解-基础篇

    [jedis的源码理解-基础篇][http://my.oschina.net/u/944165/blog/127998] (关注实现关键功能的类)   基于jedis 2.2.0-SNAPSHOT   ...

  7. VUEJS2.0源码理解--优

    VUEJS2.0源码理解 http://jiongks.name/blog/vue-code-review/#pingback-112428

  8. AdvanceEast源码理解

    目录 文章思路 源码理解 一. 标签点形式 按顺序排列四个点,逆时针旋转,且第一个点为左上角点(刚开始选择最左边的点, 二. 标签切边 三. loss计算 四. NMS 最后说明 文章思路 大神的gi ...

  9. Pytorch学习之源码理解:pytorch/examples/mnists

    Pytorch学习之源码理解:pytorch/examples/mnists from __future__ import print_function import argparse import ...

随机推荐

  1. Vue Big Changes All in One

    Vue Big Changes All in One Vue 重大更新 Vue Versions Vue 版本变更 Vue 3.x Vue 2.x refs vue lifecycle https:/ ...

  2. auto responsive rem

    auto responsive rem 移动端适配 ;(function(win, lib) { var doc = win.document; var docEl = doc.documentEle ...

  3. js 如何获取某一个月的第一天是周几

    js 如何获取某一个月的第一天是周几 calendar ??? padding dates // day = 1 const firstMonthDate = new Date(year + mont ...

  4. flutter 自定义TabBar

    这里有个工作示例 import 'dart:async'; import 'package:flutter/material.dart'; import 'package:rxdart/subject ...

  5. OpenCVE-开源漏洞预警平台

    0x01简介 主程序主要是通过使用NVD提供的JSON数据来更新CVE数据,并在前端进行展示.然后通过邮件进行通知,目前也只支持邮件.这个开源预警平台看上去并不是很完善,因为CVE本身就具有预警滞后性 ...

  6. Java HashMap源码分析(含散列表、红黑树、扰动函数等重点问题分析)

    写在最前面 这个项目是从20年末就立好的 flag,经过几年的学习,回过头再去看很多知识点又有新的理解.所以趁着找实习的准备,结合以前的学习储备,创建一个主要针对应届生和初学者的 Java 开源知识项 ...

  7. Linux下的进程控制块(PCB)

    本文转载自Linux下的进程控制块(PCB) 导语 进程在操作系统中都有一个户口,用于表示这个进程.这个户口操作系统被称为PCB(进程控制块),在linux中具体实现是 task_struct数据结构 ...

  8. oracle impdp ORA-02304 invalid object identifier literal

    reference: https://webgeest.blogspot.com/2015/07/ora-39083-ora-02304-on-impdp-datapump.html     解决方法 ...

  9. Kubernetes中分布式存储Rook-Ceph的使用:一个ASP.NET Core MVC的案例

    在<Kubernetes中分布式存储Rook-Ceph部署快速演练>文章中,我快速介绍了Kubernetes中分布式存储Rook-Ceph的部署过程,这里介绍如何在部署于Kubernete ...

  10. 基于docker搭建DNSmasq

    一.概述 DNSmasq是一个小巧且方便地用于配置DNS和DHCP的工具,适用于小型网络,它提供了DNS功能和可选择的DHCP功能.它服务那些只在本地适用的域名,这些域名是不会在全球的DNS服务器中出 ...