React源码解析之ReactDOM.render()

2019-08-14 admin

一、React更新的方式有三种: (1)ReactDOM.render() || hydrate(ReactDOMServer渲染) (2)setState (3)forceUpdate

接下来,我们就来看下ReactDOM.render()源码

二、ReactDOM.render(element, container[, callback])

作用: 在提供的container里渲染一个React元素,并返回对该组件的引用

常见的用法是这个:

ReactDOM.render(<App />, document.getElementById('root'));

官网网址: https://zh-hans.reactjs.org/docs/react-dom.html#render

源码:

const ReactDOM: Object = {
  //服务端使用hydrate方法渲染节点
  hydrate(element: React$Node, container: DOMContainer, callback: ?Function) {
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );

    // TODO: throw or warn if we couldn't hydrate?
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      //true是让服务端尽可能复用节点,提高性能
      true,
      callback,
    );
  },

  render(
    //元素
    element: React$Element<any>,
    //容器
    container: DOMContainer,
    //应用渲染结束后,调用的函数
    callback: ?Function,
  ) {
    //错误抓取
    invariant(
      isValidContainer(container),
      'Target container is not a DOM element.',
    );

    //render方法本质是返回了函数legacyRenderSubtreeIntoContainer
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      //render不会复用节点,因为是前端渲染
      false,
      callback,
    );
  },

}

解析: (1)render()方法本质是返回了函数legacyRenderSubtreeIntoContainer()

(2)hydrate()render()的唯一区别是传入legacyRenderSubtreeIntoContainer()的第四个参数不一样: hydrate()true,表示在服务端尽可能复用节点,提高性能; render()false,表示在浏览器端不会去复用节点,而是全部替换掉。

三、legacyRenderSubtreeIntoContainer()

作用: 初始化Container

源码:

// null, element, container, false, callback,
function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,
  children: ReactNodeList,
  container: DOMContainer,
  forceHydrate: boolean,
  callback: ?Function,
) {
  // TODO: Without `any` type, Flow says "Property cannot be accessed on any
  // member of intersection type." Whyyyyyy.

  //render中一般渲染的是DOM标签,所以不会有_reactRootContainer存在,
  // 所以第一次渲染,root是不存在的
  let root: _ReactSyncRoot = (container._reactRootContainer: any);
  let fiberRoot;
  if (!root) {
    // Initial mount
    //创建一个ReactRooter
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    fiberRoot = root._internalRoot;

    //判断是否有callback
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        //根据fiberRoot获取公共Root实例
        //就是fiberRoot.current.child.stateNode
        const instance = getPublicRootInstance(fiberRoot);
        //通过该实例instance 去调用originalCallback方法
        originalCallback.call(instance);
      };
    }
    // Initial mount should not be batched.
    //初始化安装不应该批量更新
    unbatchedUpdates(() => {
      //element,fiberRoot,null,callback
      updateContainer(children, fiberRoot, parentComponent, callback);
    });
  } else {
    fiberRoot = root._internalRoot;
    if (typeof callback === 'function') {
      const originalCallback = callback;
      callback = function() {
        const instance = getPublicRootInstance(fiberRoot);
        originalCallback.call(instance);
      };
    }
    // Update
    updateContainer(children, fiberRoot, parentComponent, callback);
  }
  return getPublicRootInstance(fiberRoot);
}

解析: (1)由于是第一次渲染更新,所以rootnull,只需看!root的情况

(2)legacyCreateRootFromDOMContainer(container,false,)的作用是创建ReactRooter,稍后会讲解

(3)unbatchedUpdates(fn)的简化源码如下:

unbatchedUpdates(fn){
  return fn()
}

(4)updateContainer()的作用是更新container,稍后讲解

四、legacyCreateRootFromDOMContainer(container,forceHydrate,)

作用: 创建一个ReactRooter

源码:

//创建ReactRooter
function legacyCreateRootFromDOMContainer(
  container: DOMContainer,
  forceHydrate: boolean,
): _ReactSyncRoot {
  //是否是服务端渲染
  const shouldHydrate =
    //render的forceHydrate是false,所以会调用shouldHydrateDueToLegacyHeuristic方法来判断是否是服务端渲染
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // First clear any existing content.
  //如果不是服务端渲染的话
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    //循环删除container的子节点
    //为什么要删除?因为React认为这些节点是不需要复用的
    while ((rootSibling = container.lastChild)) {

      container.removeChild(rootSibling);
    }
  }

  // Legacy roots are not batched.
  //container是空的container,0,false
  //ReactRoot是同步的
  //sync 同步
  //async 异步
  return new ReactSyncRoot(container, LegacyRoot, shouldHydrate);
}

解析: (1)render()forceHydratefalse,所以看shouldHydrateDueToLegacyHeuristic(container)是否返回false

shouldHydrateDueToLegacyHeuristic()

作用: 判断是否是服务端渲染

源码:

//判断是否是服务端渲染
function shouldHydrateDueToLegacyHeuristic(container) {
  //获取container的第一个节点(根节点)
  //也就是 id='root' 的节点
  const rootElement = getReactRootElementInContainer(container);
  return !!(
    rootElement &&
    rootElement.nodeType === ELEMENT_NODE &&
    //判断是否是服务端渲染
    rootElement.hasAttribute(ROOT_ATTRIBUTE_NAME)
  );
}

getReactRootElementInContainer()

作用: 获取container中的第一个节点(或文档节点)

源码:

//获取Container里的RectRoot元素
//返回document节点或第一个子节点
function getReactRootElementInContainer(container: any) {
  if (!container) {
    return null;
  }
  //DOCUMENT_NODE 即 window.document
  if (container.nodeType === DOCUMENT_NODE) {
    return container.documentElement;
  } else {
    return container.firstChild;
  }
}

也就是说,判断是否是服务端渲染的标志是: 在获取container中的第一个节点(或文档节点)后,看该节点是否有属性**ROOT_ATTRIBUTE_NAME**

ROOT_ATTRIBUTE_NAME是什么呢?

//服务端渲染的话,会在React App的第一个元素上添加该属性
//以标识是服务端渲染的
export const ROOT_ATTRIBUTE_NAME = 'data-reactroot';

data-reactroot

(2)由(1)可知,render()container的首节点是没有data-reactroot属性的,所以会进行while循环,依次删除container的子节点,删除完毕后,new 一个ReactSyncRoot()的实例

(3)ReactSyncRoot()

作用: 创建ReactRoot实例

源码:

// container,0,false
function ReactSyncRoot(
  container: DOMContainer,
  tag: RootTag,
  hydrate: boolean,
) {
  // Tag is either LegacyRoot or Concurrent Root
  const root = createContainer(container, tag, hydrate);
  this._internalRoot = root;
}

把创建的root作为legacyCreateRootFromDOMContainer()__internalRoot属性

createContainer

作用: 创建React容器

源码:

//创建React容器
export function createContainer(
  containerInfo: Container,
  tag: RootTag,
  hydrate: boolean,
): OpaqueRoot {
  //创建FiberRoot
  return createFiberRoot(containerInfo, tag, hydrate);
}

也就是说legacyCreateRootFromDOMContainer()的本质是创建了FilberRoot

五、updateContainer()

作用: 创建更新container

源码:

//更新Container
export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  const current = container.current;
  //1073741823
  const currentTime = requestCurrentTime();

  const suspenseConfig = requestCurrentSuspenseConfig();
  //计算过期时间,这是React优先级更新非常重要的点
  const expirationTime = computeExpirationForFiber(
    currentTime,
    current,
    suspenseConfig,
  );
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    suspenseConfig,
    callback,
  );
}

解析:

(1)requestCurrentTime()

作用: 计算新开始的时间

源码不用看,只需要知道该时间,是以V8引擎上最大31位整数1073741823为根据的:

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
//整型最大数值,是V8中针对32位系统所设置的最大值
export default 1073741823;

(2)requestCurrentSuspenseConfig()computeExpirationForFiber()以后会讲解

(3)updateContainerAtExpirationTime()

作用: 每到过期时间,就更新container,过期时间单位为 10ms

源码:

//在过期时间内,更新container
export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
  callback: ?Function,
) {
  // TODO: If this is a nested container, this won't be the root.
  const current = container.current;

  //由于parentComponent为null,所以返回空对象{}
  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }
  //计划更新Root
  return scheduleRootUpdate(
    current,
    element,
    expirationTime,
    suspenseConfig,
    callback,
  );
}

解析:

scheduleRootUpdate()

作用: 计划更新Root

源码:

//计划更新Root
function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  suspenseConfig: null | SuspenseConfig,
  callback: ?Function,
) {

  //创建更新的时间节点
  const update = createUpdate(expirationTime, suspenseConfig);
  // 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;
  }

  if (revertPassiveEffectsChange) {
    flushPassiveEffects();
  }
  //一整个React应用中,会有多次更新,而这多次更新均在更新队列中
  enqueueUpdate(current, update);
  //进行任务调度
  //当React进行Update后,就要进行调度
  //即 根据任务的优先级去调度任务
  //先执行优先级高的任务,
  scheduleWork(current, expirationTime);

  return expirationTime;
}

解析: 任务调度是React中最重要、复杂的内容,之后会慢慢来解析。 这里可以看到,React将初始化的Update放入了更新队列中,并进行任务调度,最终返回了一个expirationTime

也就是说,updateContainer()本质是返回了expirationTime

六、getPublicRootInstance()

作用: 获取root实例

源码:

//获取root实例
export function getPublicRootInstance(
  container: OpaqueRoot,
): React$Component<any, any> | PublicInstance | null {
  //看到container.current,我就想到了ref(xxx.current)
  //获取当前节点
  const containerFiber = container.current;
  if (!containerFiber.child) {
    return null;
  }
  switch (containerFiber.child.tag) {
    case HostComponent:
      return getPublicInstance(containerFiber.child.stateNode);
    default:
      return containerFiber.child.stateNode;
  }
}

解析: 由于是 React 初始化,所以container.current是没有子节点的,所以该方法返回 null

七、ReactDOM.render()流程图

总结: ReactDOM.render() 的更新步骤 (1)创建 ReactRoot,ReactRoot 是创建整个React应用的根对象

(2)创建 FiberRoot 和 RootFiber

(3)创建更新 (创建更新后,就会进入调度阶段,调度阶段由调度器进行管理)

GitHub: https://github.com/AttackXiaoJinJin/reactExplain/blob/master/react16.8.6/packages/react-dom/src/client/ReactDOM.js


(完)

[转载]原文链接:https://segmentfault.com/a/1190000020064411

本站文章除注明转载外,均为本站原创或编译。欢迎任何形式的转载,但请务必注明出处。

转载请注明:文章转载自 JavaScript中文网 [https://www.javascriptcn.com]

本文地址:https://www.javascriptcn.com/read-72422.html

文章标题:React源码解析之ReactDOM.render()

相关文章
jQuery中DOM树操作之复制元素的方法
本文实例讲述了jQuery中DOM树操作之复制元素的方法。分享给大家供大家参考。具体分析如下: 复制元素 前面提到的操作包括:插人新创建的元素、将元素从文档中的一个位置移动 到另一个位置,以及通过新元素来包装已有的元素。可是,有时候也会用到...
2015-11-13
Node.js的不足之处
跨平台编程能力不够强大 Bowery团队指出Go能很方便地在不同系统里进行程序编译,这是他们转入Go的重要原因之一。 作为开发平台,对Linux,Windows,OSX等常见操作系统提供支援是能否吸引开发者的基本要素。在Go中,开发者可以针...
2015-11-12
[翻译]基于Webpack4使用懒加载分离打包React代码
原文地址:https://engineering.innovid.com/code-splitting-using-lazy-loading-with-react-redux-typescript-and-webpack-4-3ec601...
2018-03-11
VSCode配置react开发环境的步骤
vscode 默认配置对于 react 的 JSX 语法不友好,体现在使用自动格式化或者粘贴后默认缩进错误,尽管可以通过改变 language mode 缓解错误,但更改 language mode 后的格式化依然不够理想。 通过搭配使用 ...
2017-12-28
JavaScript正则进阶之路——活学妙用奇淫正则表达式
有些童鞋肯定有所疑惑,花了大量时间学习正则表达式,却发现没有用武之地,正则不就是验证个邮箱嘛,其他地方基本用不上,其实,大部分人都是这种感觉,所以有些人干脆不学,觉得又难又没多大用处。殊不知,想要成为编程大牛,正则表达式必须玩转,GitH...
2017-05-31
ajax教程之ajax使用Http请求
ajax中是如何让使用http请求的呢? 在传统的JS编程中,如果您希望从服务器上的文件或数据库中得到任何的信息,或者向服务器发送信息的话,就必须利用一个 HTML 表单向服务器 GET 或 POST 数据。而用户则需要单击“提交”按钮来发...
2015-11-12
bootstrap table之通用方法( 时间控件,导出,动态下拉框, 表单验证 ,选中与获取信息)代码分享
1.bootstrap-table 单击单行选中 $(&#x27;#gzrwTable&#x27;).on(&#x27;click-row.bs.table&#x27;, function(e, row, $element) { $(&#x...
2017-02-17
JS教程之基础
javascript教程之什么是 JavaScript? JavaScript 被设计用来向 HTML 页面添加交互行为。JavaScript 是一种脚本语言(脚本语言是一种轻量级的编程语言)。JavaScript 由数行可执行计算机代码组...
2015-11-12
DOM之通俗易懂讲解
DOM 是所有前端开发每天打交道的东西,但是随着 jQuery 等库的出现,大大简化了 DOM 操作,导致大家慢慢的 “遗忘” 了它的本来面貌。不过,要想深入学习前端知识,对 DOM 的了解是不可或缺的,所以本文力图系统的讲解下 DOM 的...
2016-01-13
Vue.js原理分析之observer模块详解
介绍 observer是Vue核心中最重要的一个模块(个人认为),能够实现视图与数据的响应式更新,底层全凭observer的支持。 **注意:**本文是针对Vue@2.1.8进行分析 observer模块在Vue项目中的代码位置是src/c...
2017-03-16
回到顶部