React源码看过几次,每次都没有保持下来,索性学习一下PReact局部,网上解说源码的不少,然而根本曾经过期,所以本人来梳理下
render.js局部
import { EMPTY_OBJ, EMPTY_ARR } from './constants';import { commitRoot, diff } from './diff/index';import { createElement, Fragment } from './create-element';import options from './options';/** * Render a Preact virtual node into a DOM element * @param {import('./internal').ComponentChild} vnode The virtual node to render * @param {import('./internal').PreactElement} parentDom The DOM element to * render into * @param {import('./internal').PreactElement | object} [replaceNode] Optional: Attempt to re-use an * existing DOM tree rooted at `replaceNode` */export function render(vnode, parentDom, replaceNode) { if (options._root) options._root(vnode, parentDom); // We abuse the `replaceNode` parameter in `hydrate()` to signal if we are in // hydration mode or not by passing the `hydrate` function instead of a DOM // element.. let isHydrating = typeof replaceNode === 'function'; // To be able to support calling `render()` multiple times on the same // DOM node, we need to obtain a reference to the previous tree. We do // this by assigning a new `_children` property to DOM nodes which points // to the last rendered tree. By default this property is not present, which // means that we are mounting a new tree for the first time. // 为了反对屡次在一个dom节点上调用render函数,须要在dom节点上增加一个饮用,用来获取指向上一次渲染的虚构dom树。 // 这个属性默认是指向空的,也意味着咱们第一次正在配备一颗新的树 // 所以开始时这里的oldVNode是空(不管isHydrating的值),然而如果反复在这个节点上调用render那oldVNode是有值的 let oldVNode = isHydrating ? null : (replaceNode && replaceNode._children) || parentDom._children; // 用Fragment包裹一下vnode,同时给replaceNode和parentDom的_children赋值 vnode = ( (!isHydrating && replaceNode) || parentDom )._children = createElement(Fragment, null, [vnode]); // List of effects that need to be called after diffing. // 用来搁置diff之后须要进行各种生命周期解决的Component,比方cdm、cdu;componentWillUnmount在diffChildren的unmount函数中执行不在commitRoot时执行 let commitQueue = []; diff( parentDom, // 这个应用parentDom的_children属性曾经指向[vnode]了 // Determine the new vnode tree and store it on the DOM element on // our custom `_children` property. vnode, oldVNode || EMPTY_OBJ, // 旧的树 EMPTY_OBJ, parentDom.ownerSVGElement !== undefined, // excessDomChildren,这个参数用来做dom复用的作用 !isHydrating && replaceNode ? [replaceNode] : oldVNode ? null : parentDom.firstChild // 如果parentDom有子节点就会把整个子节点作为待复用的节点应用 ? EMPTY_ARR.slice.call(parentDom.childNodes) : null, commitQueue, // oldDom,在后续办法中用来做标记插入地位应用 !isHydrating && replaceNode ? replaceNode : oldVNode ? oldVNode._dom : parentDom.firstChild, isHydrating ); // Flush all queued effects // 调用所有commitQueue中的节点_renderCallbacks中的办法 commitRoot(commitQueue, vnode);}/** * Update an existing DOM element with data from a Preact virtual node * @param {import('./internal').ComponentChild} vnode The virtual node to render * @param {import('./internal').PreactElement} parentDom The DOM element to * update */export function hydrate(vnode, parentDom) { render(vnode, parentDom, hydrate);}
create-context.js局部
Context的应用:
Provider的props中有value属性
Consumer中间接获取传值
import { createContext, h, render } from 'preact';const FontContext = createContext(20);function Child() { return <FontContext.Consumer> {fontSize=><div style={{fontSize:fontSize}}>child</div>} </FontContext.Consumer>}function App(){ return <Child/>}render( <FontContext.Provider value={26}> <App/> </FontContext.Provider>, document.getElementById('app'));
看一下源码:
import { enqueueRender } from './component';export let i = 0;export function createContext(defaultValue, contextId) { contextId = '__cC' + i++; // 生成一个惟一ID const context = { _id: contextId, _defaultValue: defaultValue, /** @type {import('./internal').FunctionComponent} */ Consumer(props, contextValue) { // return props.children( // context[contextId] ? context[contextId].props.value : defaultValue // ); return props.children(contextValue); }, /** @type {import('./internal').FunctionComponent} */ Provider(props) { if (!this.getChildContext) { // 第一次调用时进行一些初始化操作 let subs = []; let ctx = {}; ctx[contextId] = this; // 在diff操作用,如果判断一个组件在Comsumer中,会调用sub进行订阅; // 同时这个节点后续所有diff的中央都会带上这个context,调用sub办法进行调用 // context具备层级优先级,组件会先退出最近的context中 this.getChildContext = () => ctx; this.shouldComponentUpdate = function(_props) { if (this.props.value !== _props.value) { // I think the forced value propagation here was only needed when `options.debounceRendering` was being bypassed: // https://github.com/preactjs/preact/commit/4d339fb803bea09e9f198abf38ca1bf8ea4b7771#diff-54682ce380935a717e41b8bfc54737f6R358 // In those cases though, even with the value corrected, we're double-rendering all nodes. // It might be better to just tell folks not to use force-sync mode. // Currently, using `useContext()` in a class component will overwrite its `this.context` value. // subs.some(c => { // c.context = _props.value; // enqueueRender(c); // }); // subs.some(c => { // c.context[contextId] = _props.value; // enqueueRender(c); // }); // enqueueRender最终会进入renderComponent函数,进行diff、commitRoot、updateParentDomPointers等操作 subs.some(enqueueRender); } }; this.sub = c => { subs.push(c);// 进入订阅数组, let old = c.componentWillUnmount; c.componentWillUnmount = () => { // 重写componentWillUnmount subs.splice(subs.indexOf(c), 1); if (old) old.call(c); }; }; } return props.children; } }; // Devtools needs access to the context object when it // encounters a Provider. This is necessary to support // setting `displayName` on the context object instead // of on the component itself. See: // https://reactjs.org/docs/context.html#contextdisplayname // createContext最终返回的是一个context对象,带着Provider和Consumer两个函数 // 同时Consumber函数的contextType和Provider函数的_contextRef属性都指向context return (context.Provider._contextRef = context.Consumer.contextType = context);}
所以对于Provider组件,在渲染时会判断有没有getChildContext办法,如果有的话调用失去globalContext并始终向下传递上来
if (c.getChildContext != null) { globalContext = assign(assign({}, globalContext), c.getChildContext()); } if (!isNew && c.getSnapshotBeforeUpdate != null) { snapshot = c.getSnapshotBeforeUpdate(oldProps, oldState); } let isTopLevelFragment = tmp != null && tmp.type === Fragment && tmp.key == null; let renderResult = isTopLevelFragment ? tmp.props.children : tmp; diffChildren( parentDom, Array.isArray(renderResult) ? renderResult : [renderResult], newVNode, oldVNode, globalContext, isSvg, excessDomChildren, commitQueue, oldDom, isHydrating );
当渲染遇到Consumer时,即遇到contextType属性,先从Context中拿到provider,而后拿到provider的props的value值,作为组件要获取的上下文信息。
同时这时候会调用provider的sub办法,进行订阅,当调用到Provider的shouldComponentUpdate中发现value发生变化时就会将所有的订阅者进入enqueueRender函数。
所以源码中,globalContext对象的每一个key指向一个Context.Provider;componentContext代表组件所在的Consumer传递的上下文信息即配对的Provider的props的value;
同时Provider的shouldComponentUpdate办法中用到了 ·this.props.value !== _props.value· 那么这里的this.props是哪来的?Provider中并没有相干属性。
次要是上面这个中央,当判断没有render办法时,会先用Compoent来实例化一个对象,并将render办法设置为doRender,并将constructor指向newType(以后函数),在doRender中调用this.constructor办法
// Instantiate the new component if ('prototype' in newType && newType.prototype.render) { // @ts-ignore The check above verifies that newType is suppose to be constructed newVNode._component = c = new newType(newProps, componentContext); // eslint-disable-line new-cap } else { // @ts-ignore Trust me, Component implements the interface we want newVNode._component = c = new Component(newProps, componentContext); c.constructor = newType; c.render = doRender; }
/** The `.render()` method for a PFC backing instance. */function doRender(props, state, context) { return this.constructor(props, context);}
diff局部
diff局部比较复杂,整体整顿了一张大图
Hook局部
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
不过unmount阶段,所有的useEffect返回的回调都会被执行,因为unmount函数针对的是所有的hooks而不是只进入到pendingEffects中的hook
options.unmount = vnode => { if (oldBeforeUnmount) oldBeforeUnmount(vnode); const c = vnode._component; if (c && c.__hooks) { try { c.__hooks._list.forEach(invokeCleanup); } catch (e) { options._catchError(e, c._vnode); } }};
第三种是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';// 创立一个 contextconst 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;