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;
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。