共计 16702 个字符,预计需要花费 42 分钟才能阅读完成。
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';
// 创立一个 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;