react 中顺序加载script 标签
export default class Script extends React.Component {
static defaultProps = {
attributes: {},
onCreate: () => {},
onError: () => {},
onLoad: () => {},
}
static scriptObservers = {};
// 特定的URL是否已经加载完成
// this.constructor.scriptObservers[url][this.scriptLoaderId] = this.props;
// 每一个URL对应于多个scriptLoaderId,但是只会检查一个是否已经加载完毕
static loadedScripts = {};
// this.constructor.loadedScripts[url] = true;
static erroredScripts = {};
// this.constructor.erroredScripts[url] = true;
static idCount = 0;
// 该组件已经被实例化了多少个对象
constructor(props) {
super(props);
this.scriptLoaderId = `id${this.constructor.idCount++}`;
//1.如果某一个页面有多个该Script标签,那么其特定的this.scriptLoaderId都是唯一的
}
componentDidMount() {
const { onError, onLoad, url } = this.props;
//fix 1:如果该URL已经加载过了,然后又在页面其他地方要求加载,因为this.constructor.loadedScripts[url]已经被设置为true,那么直接调用onLoad方法
if (this.constructor.loadedScripts[url]) {
onLoad();
return;
}
//fix 2:如果该URL已经加载过了,而且加载出错,然后又在页面其他地方要求加载,因为tthis.constructor.erroredScripts[url]已经被设置为true,那么直接调用onError方法
if (this.constructor.erroredScripts[url]) {
onError();
return;
}
// If the script is loading, add the component to the script's observers
// and return. Otherwise, initialize the script's observers with the component
// and start loading the script.
// fix 3:如果某一个URL已经在加载了,即this.constructor.scriptObservers[url]被设置为特定的值了,那么如果还要求该URL那么直接返回,防止一个组件被加载多次
if (this.constructor.scriptObservers[url]) {
this.constructor.scriptObservers[url][this.scriptLoaderId] = this.props;
return;
}
//8.this.constructor.scriptObservers用于注册某一个URL特定的对象,其值为为该组件添加的所有的props对象,而key为该组件实例的this.scriptLoaderId
this.constructor.scriptObservers[url] = {
[this.scriptLoaderId]: this.props
};
this.createScript();
}
componentWillUnmount() {
const { url } = this.props;
const observers = this.constructor.scriptObservers[url];
// If the component is waiting for the script to load, remove the
// component from the script's observers before unmounting the component.
// componentWillUnmount只是卸载当前的组件实例而已,所以直接delete当前实例的this.scriptLoaderId
if (observers) {
delete observers[this.scriptLoaderId];
}
}
createScript() {
const { onCreate, url, attributes } = this.props;
//1.onCreate在script标签创建后被调用
const script = document.createElement('script');
onCreate();
// add 'data-' or non standard attributes to the script tag
// 2.所有attributes指定的属性都会被添加到script标签中
if (attributes) {
Object.keys(attributes).forEach(prop => script.setAttribute(prop, attributes[prop]));
}
script.src = url;
// default async to true if not set with custom attributes
// 3.如果script标签没有async属性,表示不是异步加载的
if (!script.hasAttribute('async')) {
script.async = 1;
}
//5.shouldRemoveObserver(observers[key])用于移除特定的监听器并触发onLoad
const callObserverFuncAndRemoveObserver = (shouldRemoveObserver) => {
const observers = this.constructor.scriptObservers[url];
//监听当前URL的scriptObservers,然后获取该Observer的key,即对应于this.scriptLoaderId,每一个组件实例都是唯一的,一个URL可能多个this.scriptLoadedId相对应:
// if (this.constructor.scriptObservers[url]) {
// this.constructor.scriptObservers[url][this.scriptLoaderId] = this.props;
// return;
// }
Object.keys(observers).forEach((key) => {
//如果某一个特定的key对应的,传入的observers[key]就是该组件实例的this.props
if (shouldRemoveObserver(observers[key])) {
delete this.constructor.scriptObservers[url][this.scriptLoaderId];
}
});
};
//4.onload将该URL已经加载的状态设置为true
script.onload = () => {
this.constructor.loadedScripts[url] = true;
callObserverFuncAndRemoveObserver((observer) => {
//6.调用用户自己的onLoad表示脚本加载完成
observer.onLoad();
return true;
});
}
script.onerror = () => {
this.constructor.erroredScripts[url] = true;
callObserverFuncAndRemoveObserver((observer) => {
//7.调用用户自己的onError表示加载错误
observer.onError();
return true;
});
};
document.body.appendChild(script);
}
render() {
return null;
}
}
handleScriptLoad = value => {
++this.scriptLoaderCount;
//两个js脚本
if (this.scriptLoaderCount == 2) {
this.map = new AMap.Map("my__amp--container", {
resizeEnable: true,
zoom: 13,
center: [116.39, 39.9]
});
window.AMap.plugin("AMap.Geocoder", () => {
this.geocoder = new AMap.Geocoder({
//city: "010" //城市,默认:“全国”
});
this.marker = new AMap.Marker({
map: this.map,
bubble: true
});
});
render(){
return <div>
<Script
url=" https://webapi.amap.com/maps?v=1.4.2&key=eafedbd654c4c2996d778d04f3cba020"
onLoad={this.handleScriptLoad}
/>
<Script
url="https://webapi.amap.com/demos/js/liteToolbar.js"
onLoad={this.handleScriptLoad}
/>
</div>
}
比如有一次在页面中接入高德地图,需要保证当其依赖的js都加载完毕以后才渲染地图,所以有如下的方法:
//两个js脚本
if (this.scriptLoaderCount == 2) {
this.map = new AMap.Map("my__amp--container", {
resizeEnable: true,
zoom: 13,
center: [116.39, 39.9]
});
window.AMap.plugin("AMap.Geocoder", () => {
this.geocoder = new AMap.Geocoder({
//city: "010" //城市,默认:“全国”
});
this.marker = new AMap.Marker({
map: this.map,
bubble: true
});
});
render(){
return <div>
<Script
url=" https://webapi.amap.com/maps?v=1.4.2&key=eafedbd654c4c2996d778d04f3cba020"
onLoad={this.handleScriptLoad}
/>
<Script
url="https://webapi.amap.com/demos/js/liteToolbar.js"
onLoad={this.handleScriptLoad}
/>
</div>
}
生命周期调用顺
import React from 'react';
export default class Parent extends React.Component {
state = {
count: 0
};
/**
* (1)componentWillReceiveProps签名知道只有当组件的props发生改变后才会调用该方法
* (2)componentWillReceiveProps在shouldComponentUpdate之前调用的
*/
componentWillReceiveProps(nextProps) {
console.log("Parent的nextProps为", nextProps);
}
/**
* (1)组件state或者props发生改变都会触发这个方法,但是如果组件没有调用setState那么不会调用。
* 同时从函数的签名可以看到:组件的渲染收到两个方面的影响:父组件传递的props改变+组件自己state
* (2)如组件如果shouldComponentUpdate返回false,那么render方法不会重新渲染
* (3)父组件的SCU一定在子组件的SCU之前调用,组件的componentWillReceiveProps在SCU之前调用
*/
shouldComponentUpdate(nextProps, nextState) {
console.log("Parent的shouldComponentUpdate为", nextProps, nextState);
return true;
}
/**
* (1)组件只会被挂载一次,父组件的componentDidMount一定在子组件的ComponentDidMount之后被触发
*/
componentDidMount() {
console.log("Parent被挂载");
}
/**
* (1)组件调用了setState,那么会让Parent走一次shouldComponentUpdate
*
*/
parentRender = () => {
this.setState({
count: ++this.state.count
});
};
render() {
return (
<div style={{ border: "1px solid red" }}>
我是Parent
<div style={{ border: "1px solid pink" }}>
<Child1 style={{ border: "1px solid yellow" }} />
<Child2 />
</div>
<button onClick={this.parentRender}>
点击我让父组件重新渲染{this.state.count}
</button>
</div>
);
}
}
class Child1 extends React.Component {
/**
* (1)子组件重新渲染的时候componentWillReceiveProps在SCU之前被调用
*/
componentWillReceiveProps(nextProps) {
console.log("Child1的nextProps为", nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
console.log("Child1的shouldComponentUpdate为", nextProps, nextState);
}
componentDidMount() {
console.log("Child1被挂载");
}
render() {
return <div>我是Child1</div>;
}
}
class Child2 extends React.Component {
componentWillReceiveProps(nextProps) {
console.log("Child2的nextProps为", nextProps);
}
shouldComponentUpdate(nextProps, nextState) {
console.log("Child2的shouldComponentUpdate为", nextProps, nextState);
}
componentDidMount() {
console.log("Child2被挂载");
}
render() {
return <div>我是Child1</div>;
}
}
// ReactDOM.render(<Parent/>, document.getElementById("example"));
componentWillReceiveProps触发的条件
使用React-Router后hash跳转会触发componentWillReceiveProps的坑
通常对于componentWillReceiveProps,认为是外层Update时才会触发。
对于使用了React-Router的场景,通常也会理解为,当路由发生跳转时才会触发。
而实际上,当页面发生hash跳转(例如点击了XXX)时,虽然路由没有跳转,但也会触发componentWillReceiveProps。
场景
使用了componentWillReceiveProps
在点击诸如<a href='#xxx'>XXX</a>
时会触发componentWillReceiveProps
实际不希望此时触发
原因
react的componentWillReceiveProps触发实际受2个条件制约
Component.props更新
Component.context更新
而每次URL的pathname变化或hash变化,React-Router都会触发context里的router变化。
从而会触发componentWillReceiveProps
setState 的‘同步’与‘异步’
- setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
- setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
- setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。