React 为什么会在 DOMContentLoaded 事件之前执行

339 阅读4分钟

起因

起初项目中有一些google,bing 的脚本需要提前放置,领英,机器人等脚本后置的需求

然后找到了 有关script 标签 async , defer 的属性与行为;

第一次做法

不改html 在provider 层逻辑注入,因为觉得写在html中太捞了,也没有抽离的思想

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link rel="preconnect" href="https://fonts.googleapis.com" />
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
    <link href="https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet" />
      /// 其他逻辑 
  </head>
  <body>
  /// spa 位置
    <div id="root"></div>
  </body>
</html>


在provider 层注入 但写在了useEffect 中,获取插入head节点第一个

/**
 * 该文件为脚本注入文件provider
 * 2024-4-20需求
 * google/bing 等用户统计资源需要  async拿脚本(即便会阻塞)
 * 页脚加载:paypal - 领英 - salesmartly - tiktok defer加载
 * 众所周知,async defer 参数需要在head中添加,否则不会生效
 **/

import React, { useEffect } from 'react';


function bingScript() {
  // 获取 <head> 元素
  var head = document.head || document.getElementsByTagName('head')[0];

  // 获取 <head> 元素的第一个子节点作为参考节点
  var firstChild = head.firstChild;
  const bingHtml = `(function (w, d, t, r, u) {
      //logic 省略
  })(window, document, 'script', '//bat.bing.com/bat.js', 'uetq');`;
  const scriptele = document.createElement('script');
  scriptele.innerHTML = bingHtml;
  head.insertBefore(scriptele, firstChild);
}

const ScriptProvider = ({ children }: { children: React.ReactNode }) => {
  // const location = useLocation();
  // const history = createBrowserHistory();
  useEffect(() => {
    
    // bing
    bingScript();

  }, []);

  return <>{children}</>;
};
export default ScriptProvider;

结论 : 等js包和脚本执行后才执行到provider 层,哪怕注入到head 顶层,也已经慢了

单独建个项目看下async defer 的脚本在什么时候执行,react 在什么时候执行(DCL 前后?)

这次放在html页面中测试,自己启动node搭建

<!DOCTYPE html>
<html lang="en">
	<head>
		<script>
                // 注入监听
			window.addEventListener('DOMContentLoaded',()=>{
				console.log('DOMContentLoaded done')
			})
			window.addEventListener('load',()=>{
				console.log('load done')
			})
			console.log('html head 开始 ')</script>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Rspack + React</title>
                // defer 的脚本 
		<script defer src="http://localhost:3000/1" ></script>
                // async 的脚本
		<script async src="http://localhost:3000/2" ></script>
		<script> console.log('html head 执行结束')</script>
	</head>
	<body>
		<script>console.log('body 开始')</script>
		<div id="root"></div>
		<script> console.log('body 结束')</script>
                // 放在body 尾部的脚本
		<script defer src="http://localhost:3000/3" ></script>
	</body>
</html>

在main.js 中打印开始状态

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import './index.css'

console.log('main js 开始')
ReactDOM.createRoot(document.getElementById('root')).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
)

在app中打印状态

import React, { useEffect } from "react";
import { useState } from "react";
import reactLogo from "./assets/react.svg";
import "./App.css";

console.log('app js 执行')


function App() {
	useEffect(()=>{
		console.log('app js mounted')
	},[])
	return (
		<div className="App">
		</div>
	);
}

export default App;

启动个express 模拟延时脚本

const express = require('express');
const fs = require('fs');

const app = express();
const port = 3000;

app.get('/1', (req, res) => {
  // 读取 index.js 文件内容
  fs.readFile('js1.js', 'utf8', (err, data) => {
    if (err) {
      console.error('Error reading file:', err);
      res.status(500).send('Internal Server Error');
      return;
    }
    // 返回 index.js 文件内容
    res.setHeader('Content-Type', 'application/javascript');
    res.send(data);
  });
});


app.listen(port, () => {
  console.log(`Proxy server listening at http://localhost:${port}`);
});

js1 的实际内容


(function blockExecution(ms) {
globalThis.console.log('外部1脚本执行开始')
  const start = Date.now();
  console.log(new Date(),'start');
  while (Date.now() - start < ms) {
    // 这里什么也不做,只是阻塞执行
  }
  console.log(new Date(),'end');
})(1000)

console.log('外部1脚本执行完毕')

启动!

screenshot-20240508-175417.png

没有测量出async 和 defer 的行为,因为同步代码阻塞9秒,但看到了react执行竟然在DCL之前

openai =》

image.png

React 的具体实现是开源的,你可以在 React 的 GitHub 仓库中找到源代码。React 通过调度器(scheduler)来控制任务的执行顺序,从而实现了在 DOMContentLoaded 之前开始执行的效果。

以下是 React 中一些关键的源码片段,用于解释 React 如何在 DOMContentLoaded 之前执行:

  1. 调度器(Scheduler) :React 使用调度器来控制任务的优先级和执行顺序。调度器会根据任务的优先级决定何时执行任务,并且可以中断和恢复任务的执行。
  2. 任务调度(Task Scheduling) :React 将需要执行的任务(如渲染组件、处理事件等)添加到调度器中,并根据任务的优先级安排执行顺序。
  3. 异步执行(Asynchronous Execution) :React 利用浏览器提供的异步 API(如 requestAnimationFrame、setTimeout 等)来延迟任务的执行,从而使得任务可以在 DOMContentLoaded 之前开始执行。

以下是伪代码示例,展示了 React 中任务调度的简化实现:

// 任务调度器
const Scheduler = {
  scheduleTask(task, priority) {
    // 将任务添加到调度队列中,按优先级排序
    // 省略具体实现
  },
  runTasks() {
    // 执行调度队列中的任务
    // 省略具体实现
  }
};

// React 组件
class Component {
  constructor() {
    this.state = {};
  }

  // 更新组件状态
  setState(newState) {
    // 更新组件状态并调度重新渲染
    this.state = newState;
    Scheduler.scheduleTask(() => this.render(), Scheduler.Priority.UPDATE);
  }

  // 渲染组件
  render() {
    // 渲染组件并将结果插入到 DOM 中
    // 省略具体实现
  }
}

// 页面加载时初始化 React
document.addEventListener('DOMContentLoaded', () => {
  // 创建根组件并渲染到页面中
  const rootComponent = new RootComponent();
  rootComponent.render();

  // 开始执行任务调度器中的任务
  Scheduler.runTasks();
});

在这个示例中,Scheduler.scheduleTask 方法用于将任务添加到调度队列中,并根据任务的优先级进行排序。在页面加载完毕后,React 创建根组件并将其渲染到页面中,然后开始执行调度器中的任务。由于调度器会根据任务的优先级决定执行顺序,并利用异步 API 来延迟任务的执行,因此 React 可以在 DOMContentLoaded 之前开始执行任务,从而实现更快的页面加载和渲染。

总结

自己试了下有关DCL事件 在html解析后的执行顺序,没有给async 和 defer 测实际行为,但发现了react的异步处理会在DCL事件之前,也算是优先于白屏时间。

参考: blog.csdn.net/upgrade_bro…

回复 map ={ 1: 一坨, 2: 下次别写了, 3: 求断更 }