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部分比較複雜,整體整理了一張大圖
真是不得不吐槽,部落格園的編輯器bug太多了,尤其是mac上使用,比如第二次上傳程式碼提交不了;賦值貼上用不了。。。
只有情懷讓我繼續在這裡更新