怎样建设一个购物网站,什么网站可以做直播,女生学动漫制作技术好就业吗,两个wordpress单点登录React当中创建更新的主要方式
ReactDOM.render || hydrate 这两个API都是我们要把整个应用第一次进行渲染到我们的页面上面能够展现出来我们整个应用的样子的一个过程这是初次渲染 setState 后续更新应用 forceUpdate 后续更新应用 replaceState 在后续被舍弃
关于 ReactDOM…React当中创建更新的主要方式
ReactDOM.render || hydrate 这两个API都是我们要把整个应用第一次进行渲染到我们的页面上面能够展现出来我们整个应用的样子的一个过程这是初次渲染 setState 后续更新应用 forceUpdate 后续更新应用 replaceState 在后续被舍弃
关于 ReactDOM.render 1 概述
它先要去创建一个 ReactRoot这是一个包含react它整个应用的一个最顶点的一个对象之后是创建 FiberRoot 和 RootFiber第三步就是要创建一个更新创建更新之后应用就可以进入到一个更新调度的阶段 在进入调度之后不管是通过setState还是 ReactDOM.render它们后续的操作都是由调度器去管理的跟我们实际调用的API就已经没有任何关系了也就是后面都交给内部调度器来掌控全局这块先不涉及调度相关内容
2 Demo 示例
App.js
import React, { Component } from react
import ./App.cssclass List extends Component {state {a: 1,b: 2,c: 3,}handleClick () {this.setState(oldState {const { a, b, c } oldStatereturn {a: a * a,b: b * b,c: c * c,}})}render() {const { a, b, c } this.statereturn [span keya{a}/span,span keyb{b}/span,span keyc{c}/span,button keybutton onClick{this.handleClick}click me/button,]}
}class Input extends Component {state {name: wang,}handleChange e {// 这里如果使用方法设置state// 那么需要现在外面读取e.target.value// 因为在React走完整个事件之后会重置event对象// 以复用event对象如果等到方法被调用的时候再读取e.target.value// 那时e.target是nullthis.setState({name: e.target.value,})}render() {return (inputtypetextstyle{{ color: red }}onChange{this.handleChange}value{this.state.name}/)}
}class App extends Component {render() {return (div classNamemainInput /List //div)}
}export default Appindex.js
import React from react
import ReactDOM from react-dom
import ./index.css
import App from ./demos/lazyReactDOM.render(App /, document.getElementById(root))这个demo非常的简单, 它首先有一个App, 它 render 了两个子节点一个是 Input一个是 List最终渲染应用 ReactDOM.render(App /, document.getElementById(root)) 应用渲染出来之后会挂载到这个root的Dom节点上面
这个 React App 小程序通过 ReactElement 形成一个树结构 App|render() return|div/ \/ \children[0] children[1]/ \/ \/ \Input List| \
render() return render() return | \input (span span span button)我们传入的App是一个class component它调用render之后会得到一个div标签这个div标签就是它的children, 也是它 return 的唯一一个节点这个节点它有两个children一个是Input组件另外一个是List组件这个Input组件它调用render之后会返回一个input就是原生的input标签这个List组件它调用render之后 return 的是一个数组 这个数组里面有三个span标签和一个button标签 这就是整个树结构我们完全可以通过一些特定的方式去获取到它的相应的子节点一层一层下来
3 特别说明
ReactDOM.render(App /, document.getElementById(root)) 这个写法中的 App /实际上是调用React.createElement 传递进去的是 App 这个类但并没有去创建它的一个实例这个时候我们还什么东西都没有因为我们只得到了一个ReactElement最终我们要形成一个把页面渲染出来的过程是 ReactDOM.render 这个方法它接下去要做的事情
4 源码解析 ReactDOM.js 链接: https://github.com/facebook/react/blob/v16.6.0/packages/react-dom/src/client/ReactDOM.js 在react-dom 下面会有很多不同的包比如 client, server, shared, 对应的就是渲染平台不一样 server是在 nodejs 平台进行渲染的它的一个工具包, 我们把精力放在 client 下面 在react-dom里面先找到定义的 ReactDOM 对象 const ReactDOM: Object {createPortal,findDOMNode(componentOrElement: Element | ?React$Componentany, any,): null | Element | Text {if (__DEV__) {let owner (ReactCurrentOwner.current: any);if (owner ! null owner.stateNode ! null) {const warnedAboutRefsInRender owner.stateNode._warnedAboutRefsInRender;warningWithoutStack(warnedAboutRefsInRender,%s is accessing findDOMNode inside its render(). render() should be a pure function of props and state. It should never access something that requires stale data from the previous render, such as refs. Move this logic to componentDidMount and componentDidUpdate instead.,getComponentName(owner.type) || A component,);owner.stateNode._warnedAboutRefsInRender true;}}if (componentOrElement null) {return null;}if ((componentOrElement: any).nodeType ELEMENT_NODE) {return (componentOrElement: any);}if (__DEV__) {return DOMRenderer.findHostInstanceWithWarning(componentOrElement,findDOMNode,);}return DOMRenderer.findHostInstance(componentOrElement);},hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {// TODO: throw or warn if we couldnt hydrate?return legacyRenderSubtreeIntoContainer(null,element,container,true,callback,);},render(element: React$Elementany,container: DOMContainer,callback: ?Function,) {return legacyRenderSubtreeIntoContainer(null,element,container,false,callback,);},unstable_renderSubtreeIntoContainer(parentComponent: React$Componentany, any,element: React$Elementany,containerNode: DOMContainer,callback: ?Function,) {invariant(parentComponent ! null ReactInstanceMap.has(parentComponent),parentComponent must be a valid React Component,);return legacyRenderSubtreeIntoContainer(parentComponent,element,containerNode,false,callback,);},unmountComponentAtNode(container: DOMContainer) {invariant(isValidContainer(container),unmountComponentAtNode(...): Target container is not a DOM element.,);if (container._reactRootContainer) {if (__DEV__) {const rootEl getReactRootElementInContainer(container);const renderedByDifferentReact rootEl !ReactDOMComponentTree.getInstanceFromNode(rootEl);warningWithoutStack(!renderedByDifferentReact,unmountComponentAtNode(): The node youre attempting to unmount was rendered by another copy of React.,);}// Unmount should not be batched.DOMRenderer.unbatchedUpdates(() {legacyRenderSubtreeIntoContainer(null, null, container, false, () {container._reactRootContainer null;});});// If you call unmountComponentAtNode twice in quick succession, youll// get true twice. Thats probably fine?return true;} else {if (__DEV__) {const rootEl getReactRootElementInContainer(container);const hasNonRootReactChild !!(rootEl ReactDOMComponentTree.getInstanceFromNode(rootEl));// Check if the container itself is a React root node.const isContainerReactRoot container.nodeType ELEMENT_NODE isValidContainer(container.parentNode) !!container.parentNode._reactRootContainer;warningWithoutStack(!hasNonRootReactChild,unmountComponentAtNode(): The node youre attempting to unmount was rendered by React and is not a top-level container. %s,isContainerReactRoot? You may have accidentally passed in a React root node instead of its container.: Instead, have the parent component update its state and rerender in order to remove this component.,);}return false;}},// Temporary alias since we already shipped React 16 RC with it.// TODO: remove in React 17.unstable_createPortal(...args) {if (!didWarnAboutUnstableCreatePortal) {didWarnAboutUnstableCreatePortal true;lowPriorityWarning(false,The ReactDOM.unstable_createPortal() alias has been deprecated, and will be removed in React 17. Update your code to use ReactDOM.createPortal() instead. It has the exact same API, but without the unstable_ prefix.,);}return createPortal(...args);},unstable_batchedUpdates: DOMRenderer.batchedUpdates,unstable_interactiveUpdates: DOMRenderer.interactiveUpdates,flushSync: DOMRenderer.flushSync,unstable_flushControlled: DOMRenderer.flushControlled,__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {// Keep in sync with ReactDOMUnstableNativeDependencies.js// and ReactTestUtils.js. This is an array for better minification.Events: [ReactDOMComponentTree.getInstanceFromNode,ReactDOMComponentTree.getNodeFromInstance,ReactDOMComponentTree.getFiberCurrentPropsFromNode,EventPluginHub.injection.injectEventPluginsByName,EventPluginRegistry.eventNameDispatchConfigs,EventPropagators.accumulateTwoPhaseDispatches,EventPropagators.accumulateDirectDispatches,ReactControlledComponent.enqueueStateRestore,ReactControlledComponent.restoreStateIfNeeded,ReactDOMEventListener.dispatchEvent,EventPluginHub.runEventsInBatch,],},
};这个对象里面有一个 render 方法它接收3个参数 一个是 element本质是 React$Element 对象第二个是 container就是我们要挂载到哪个dom节点上面第三个是 callback 就是说这个应用它渲染结束之后它会调用这个callback render 方法最终 return 了一个 legacyRenderSubtreeIntoContainer 方法 传入了 null, element, container, false, callback 四个方法主要看下 第一个 null 和 第四个 false 现在定位到 legacyRenderSubtreeIntoContainer 这个方法 function legacyRenderSubtreeIntoContainer(parentComponent: ?React$Componentany, any,children: ReactNodeList,container: DOMContainer,forceHydrate: boolean,callback: ?Function,
) {// TODO: Ensure all entry points contain this checkinvariant(isValidContainer(container),Target container is not a DOM element.,);if (__DEV__) {topLevelUpdateWarnings(container);}// TODO: Without any type, Flow says Property cannot be accessed on any// member of intersection type. Whyyyyyy.let root: Root (container._reactRootContainer: any);if (!root) {// Initial mountroot container._reactRootContainer legacyCreateRootFromDOMContainer(container,forceHydrate,);if (typeof callback function) {const originalCallback callback;callback function() {const instance DOMRenderer.getPublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Initial mount should not be batched.DOMRenderer.unbatchedUpdates(() {if (parentComponent ! null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {root.render(children, callback);}});} else {if (typeof callback function) {const originalCallback callback;callback function() {const instance DOMRenderer.getPublicRootInstance(root._internalRoot);originalCallback.call(instance);};}// Updateif (parentComponent ! null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {root.render(children, callback);}}return DOMRenderer.getPublicRootInstance(root._internalRoot);
}传进来的第一个参数 null 它对应的是叫做 parentComponent 这么一个参数 接着往下它定义一个 root, 即: let root: Root (container._reactRootContainer: any); 获取是否有 _reactRootContainer 这个属性正常来讲一个普通的dom对象肯定不会有这种属性在上面的所以第一次渲染的时候它肯定是不存在的所以我们主要关心的就是下面if里面的这个条件满足的root不存在的情况 如果root不存在则进行创建 // Initial mount
root container._reactRootContainer legacyCreateRootFromDOMContainer(container,forceHydrate,
);这个方法 legacyCreateRootFromDOMContainer 我们也要注意它接受两个参数 container: DOMContainerforceHydrate: boolean 我们在函数调用栈向上溯源传进去的 forceHydrate 是一个 false这是一开始就写死的我们在最顶层对比可知在 626 行的 hydrate 方法的第四个参数传递的是 true因为hydrate跟render方法本质是一样的唯一的一个区别就是是否会调和原来存在于这个dom节点就是我们 container里面的它的HTML的节点, 是否要复用这些节点它主要是在有服务端渲染的情况下会使用 hydrate 这个API因为服务端渲染出来的情况它里面的dom节点应该是跟客户端渲染的时候第一次渲染它得到的节点是一模一样的这个时候如果可以复用这些dom节点可以提高一定的性能所以hydrate跟render内部的唯一的区别就是传的第四个参数是true或false 再回到 legacyCreateRootFromDOMContainer这个函数 function legacyCreateRootFromDOMContainer(container: DOMContainer,forceHydrate: boolean,
): Root {const shouldHydrate forceHydrate || shouldHydrateDueToLegacyHeuristic(container);// First clear any existing content.if (!shouldHydrate) {let warned false;let rootSibling;while ((rootSibling container.lastChild)) {if (__DEV__) {if (!warned rootSibling.nodeType ELEMENT_NODE (rootSibling: any).hasAttribute(ROOT_ATTRIBUTE_NAME)) {warned true;warningWithoutStack(false,render(): Target node has markup rendered by React, but there are unrelated nodes as well. This is most commonly caused by white-space inserted around server-rendered markup.,);}}container.removeChild(rootSibling);}}if (__DEV__) {if (shouldHydrate !forceHydrate !warnedAboutHydrateAPI) {warnedAboutHydrateAPI true;lowPriorityWarning(false,render(): Calling ReactDOM.render() to hydrate server-rendered markup will stop working in React v17. Replace the ReactDOM.render() call with ReactDOM.hydrate() if you want React to attach to the server HTML.,);}}// Legacy roots are not async by default.const isConcurrent false;return new ReactRoot(container, isConcurrent, shouldHydrate);
}在 render 函数中进入了这个函数forceHydrate 参数的值就是 false这边定义了一个 shouldHydrate 来得到是否应该进行 Hydrateconst shouldHydrate forceHydrate || shouldHydrateDueToLegacyHeuristic(container);function shouldHydrateDueToLegacyHeuristic(container) {const rootElement getReactRootElementInContainer(container);return !!(rootElement rootElement.nodeType ELEMENT_NODE rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME) // 这里的 ROOT_ATTRIBUTE_NAME 是 data-reactroot 老版本服务端渲染会在第一个节点上加上这个标识);
}
function getReactRootElementInContainer(container: any) {if (!container) {return null;}// 判断节点类型是否是 DOCUMENT_NODE 类型if (container.nodeType DOCUMENT_NODE) {return container.documentElement;} else {// 否则返回第一个孩子节点return container.firstChild;}
}通过判断有 rootElement 这个节点并且它有这个 ROOT_ATTRIBUTE_NAME 属性来判断它是否需要进行一个合并: 老的html节点和我们客户端第一次渲染出来的应用的所有节点进行合并的一个过程因为是跟服务端渲染相关的跟整体的更新流程没有特别大的关系所以就不是特别重要再回到 shouldHydrate 在下面的一个 if (!shouldHydrate) {} 判断中没有服务端渲染这里是false是会进入判断的执行了一个 while循环也就是循环删除 container 下面的所有子节点因为这些子节点里面的东西不是在我们整个reactApp 渲染出来之后还可以用的节点因为我们不需要去合并它所以就把这些节点全部删了忽略下面 DEV 的判断最终返回了一个 ReactRoot: return new ReactRoot(container, isConcurrent, shouldHydrate); 接下来进入 new ReactRoot 的过程 function ReactRoot(container: Container,isConcurrent: boolean,hydrate: boolean,
) {const root DOMRenderer.createContainer(container, isConcurrent, hydrate);this._internalRoot root;
}通过 const root DOMRenderer.createContainer(container, isConcurrent, hydrate); 创建了一个 root 节点而 DOMRenderer 是在 import * as DOMRenderer from react-reconciler/inline.dom; 在 react-reconciler 这个包中的函数react-reconciler 是在 react 中非常重要的一个模块它处理和平台无关的节点的调和操作和任务调度的操作react-reconciler 中的代码比 react-dom中的还要复杂在 react-reconciler/inline.dom 文件下只有一行代码 export * from ./src/ReactFiberReconciler;打开这个 js 文件找到 createContainerexport function createContainer(containerInfo: Container,isConcurrent: boolean,hydrate: boolean,
): OpaqueRoot {return createFiberRoot(containerInfo, isConcurrent, hydrate);
}最终它创建了一个 FiberRoot, 这里先不展开 回到 ReactRoot, 它这边挂载了一个 _internalRoot, this._internalRoot root; 退回到 legacyCreateRootFromDOMContainer 它最终返回了一个 ReactRoot 再退回到调用 legacyCreateRootFromDOMContainer 的 legacyRenderSubtreeIntoContainer 函数中 root container._reactRootContainer legacyCreateRootFromDOMContainer(container, forceHydrate);接下来判断是否有 callback, 没有则对其进行简单的封装处理if (typeof callback function) {const originalCallback callback;callback function() {const instance DOMRenderer.getPublicRootInstance(root._internalRoot);originalCallback.call(instance);};
}接着进入核心环节 DOMRenderer.unbatchedUpdates// Initial mount should not be batched.
DOMRenderer.unbatchedUpdates(() {if (parentComponent ! null) {root.legacy_renderSubtreeIntoContainer(parentComponent,children,callback,);} else {root.render(children, callback);}
});unbatchedUpdates 涉及到 react 中的一个概念 batchedUpdates 批量更新这里先跳过 可以理解为 改了 scheduler 里的一个全局变量可以暂时忽略这里涉及到更新的过程 然后 unbatchedUpdates 里面的回调直接被执行里面直接走 else也就是执行 root.render(children, callback);这里的 root.render 方法, 实际上是ReactRoot.prototype.render function(children: ReactNodeList,callback: ?() mixed,
): Work {const root this._internalRoot;const work new ReactWork();callback callback undefined ? null : callback;if (__DEV__) {warnOnInvalidCallback(callback, render);}if (callback ! null) {work.then(callback);}DOMRenderer.updateContainer(children, root, null, work._onCommit);return work;
};这里创建了一个 ReactWork, 最终调用了 DOMRenderer.updateContainer 这里的 updateContainer 是在 ReactFiberReconciler.js 中的export function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Componentany, any,callback: ?Function,
): ExpirationTime {const current container.current;const currentTime requestCurrentTime();const expirationTime computeExpirationForFiber(currentTime, current);return updateContainerAtExpirationTime(element,container,parentComponent,expirationTime,callback,);
}这里的第一个参数 element 是上层 ReactRoot.prototype.render的第一个参数还可以向上继续溯源这里最核心的是, 计算 expirationTime, 这里是React 16让我们使用 ConcurrentMode 进行一个优先级的任务更新这里 computeExpirationForFiber 涉及一个非常复杂的计算过程先跳过最后调用 updateContainerAtExpirationTime 来返回结果进入这个方法export function updateContainerAtExpirationTime(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Componentany, any,expirationTime: ExpirationTime,callback: ?Function,
) {// TODO: If this is a nested container, this wont be the root.const current container.current;if (__DEV__) {if (ReactFiberInstrumentation.debugTool) {if (current.alternate null) {ReactFiberInstrumentation.debugTool.onMountContainer(container);} else if (element null) {ReactFiberInstrumentation.debugTool.onUnmountContainer(container);} else {ReactFiberInstrumentation.debugTool.onUpdateContainer(container);}}}const context getContextForSubtree(parentComponent);if (container.context null) {container.context context;} else {container.pendingContext context;}return scheduleRootUpdate(current, element, expirationTime, callback);
}它获取了一个 context, const context getContextForSubtree(parentComponent); 这个先忽略因为 parentComponent 是 null下面的 if else 先忽略简单认为 container.context 和 container.pendingContext 都不存在在 react-dom 的 api 中没有任何方法可以在root节点上提供context的入口先忽略它们最终 scheduleRootUpdate 作为返回值进入这个方法function scheduleRootUpdate(current: Fiber,element: ReactNodeList,expirationTime: ExpirationTime,callback: ?Function,
) {if (__DEV__) {if (ReactCurrentFiber.phase render ReactCurrentFiber.current ! null !didWarnAboutNestedUpdates) {didWarnAboutNestedUpdates true;warningWithoutStack(false,Render methods should be a pure function of props and state; triggering nested component updates from render is not allowed. If necessary, trigger nested updates in componentDidUpdate.\n\n Check the render method of %s.,getComponentName(ReactCurrentFiber.current.type) || Unknown,);}}const update createUpdate(expirationTime);// Caution: React DevTools currently depends on this property// being called element.update.payload {element};callback callback undefined ? null : callback;if (callback ! null) {warningWithoutStack(typeof callback function,render(...): Expected the last optional callback argument to be a function. Instead received: %s.,callback,);update.callback callback;}enqueueUpdate(current, update);scheduleWork(current, expirationTime);return expirationTime;
}跳过里面的 DEV 判断的代码它创建了一个 update const update createUpdate(expirationTime);update 是用来标记 react 应用当中需要更新的地点的接着设置 update 的一些属性如payload, callback最后调用 enqueueUpdate , 是把 update 加入到我们这个Fiber对象上面对应的 updateQueue 里面update它是可以在一次更新当中这个节点上面有多个更新的就是一个整体的react应用的更新过程当中会有很多次更新在某一个节点上产生这跟 batchUpdates 是有一定的关系的最终调用 scheduleWork 就是开始任务调度告诉 react 有更新产生了要进行更新了也要开始调度了 为何要调度react 16之后提供了一个任务优先级的概念, 因为有可能在同一时间, 有各种优先级的任务在应用里面就需要有个调度器来进行指挥调度按照优先级先执行优先级高的任务再执行优先级低的任务这才是react更新中最复杂的逻辑
简单总结
在 ReactDOM.render 过程当中创建了一个 ReactRoot同时在 ReactRoot 创建的过程中创建了 FiberRootFiberRoot 在创建的过程中也会自动去初始化一个 Fiber 对象(上面暂没有涉及)然后又在这个 root 上面去创建了一个 expirationTime之后又创建了一个 update 这个更新的对象然后把这个更新的对象放到我们的 root 的节点上面之后就进入了一个更新的过程, 这就是一个创建更新的过程创建完更新, 再去实际的调度整个任务的更新