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. js type automatic conversion

    js type automatic conversion String & Number `255` < 16; false `15` < 16; true `25` < 1 ...

  2. http cache & 浏览器缓存,存储位置的优先级,条件?

    http cache & 浏览器缓存,存储位置的优先级,条件? memory cache disk cache 浏览器缓存,存储位置的优先级,条件, 机制,原理是什么? from memory ...

  3. css-next & grid layout

    css-next & grid layout css3 demo https://alligator.io/ @media only screen and (max-width: 30em) ...

  4. 2021 NGK生态所体验好、交易快 引人注目!

    据悉,NGK计划于2021年2月15日正式上线自己的生态所(时间待定),目的在于满足NGK生态建设者对于NGK几大币种的交易等需求,如NGK.BGV.SPC.USDN.VAST等.只要上NGK生态所, ...

  5. 炒币亏损一万美金?不如抢SPC空投!

    币圈的市场可以用风云变幻来形容,1月9日的时候比特币震荡,其他币种争先上涨,连平时都不涨的币种都拉出了10%-20%的领先涨幅,市场惊呼牛市来了,但喜悦还没有维持一天,1月10日(昨天)市场就走向另一 ...

  6. golang官方嵌入文件到可执行程序

    目录 前言 示例程序 嵌入文件直接访问 嵌入文件列表 总结 前言 在go官方出嵌入文件的方法前我在网上找过,并且自己还研究过,虽然没有问题,但是既然官方支持还是用起来吧. 看了下go源码embed/e ...

  7. 1047 Student List for Course ——PAT甲级真题

    1047 Student List for Course Zhejiang University has 40,000 students and provides 2,500 courses. Now ...

  8. Java基础语法:运算符

    Java 运算符(operator)根据功能分类: 算术运算符:+,-,*,/,%,++,-- 赋值运算符:= 关系运算符:>,<,>=,<=,==,!=,instanceof ...

  9. 【HTB系列】靶机Netmon的渗透测试

    出品|MS08067实验室(www.ms08067.com) 本文作者:是大方子(Ms08067实验室核心成员) 总结和反思: win中执行powershell的远程代码下载执行注意双引号转义 对po ...

  10. c++ vector对象

    下面随笔讲解c++ vector对象. vector对象 为什么需要vector? 封装任何类型的动态数组,自动创建和删除. 数组下标越界检查. 封装的如ArrayOfPoints也提供了类似功能,但 ...