本文基于 PReact 10.5.13 版本,深入剖析 Context 与 Diff 核心源码。详细讲解 Context 中 Provider 与 Consumer 的数据传递机制,包括 globalContext 的构建、订阅者管理以及 shouldComponentUpdate 中的值更新逻辑。同时梳理 Diff 阶段的复杂流程,通过源码片段解析组件实例化与渲染差异对比过程,帮助开发者理解 PReact 内部实现原理。
此前多次尝试研读 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 作为组件所需的上下文信息。此时,Consumer 会调用 Provider 的 sub 方法进行订阅。若后续在 Provider 的 shouldComponentUpdate 中检测到 value 发生变化,系统会将所有订阅者加入 enqueueRender 函数队列以触发重新渲染。

综上所述,源码中 globalContext 对象的每个键值指向一个 Context.Provider;而 componentContext 则代表组件所在 Consumer 所传递的上下文信息,即与其配对的 Provider 的 props.value。
此外,在 Provider 的 shouldComponentUpdate 方法中使用了 this.props.value !== _props.value 这一判断条件。这里需要明确的是,Provider 组件本身并未直接定义 props 属性,那么 this.props 究竟从何而来?
这主要源于代码中的特定处理逻辑:当判断组件没有 render 方法时,系统会先使用 Component 类实例化一个对象,将 render 方法设置为 doRender,并将 constructor 指向 newType(当前函数)。在 doRender 执行时,会调用 this.constructor 方法,从而间接获取到 props。
// 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 部分的逻辑较为复杂,整体流程已整理为一张大图以便直观理解。

不得不吐槽的是,博客园的编辑器存在诸多 Bug,特别是在 Mac 系统下使用体验较差,例如第二次上传代码无法提交、赋值粘贴功能失效等问题频发。
唯有对技术的情怀支撑着我继续在此更新内容。
至此,关于 PReact 10.5.13 源码的解析文章介绍完毕。如需了解更多 PReact 10.5.13 相关内容,请搜索本站历史文章或继续浏览后续文章,感谢大家的支持!