大概半个多月前就有用户反馈:同时打开文件A和文件B,在A的某个单元格中写几行执行起来非常耗时的代码,在等待A执行完成的时候,我再切到文件B,去执行B中的代码,发现无法执行,刷新整个页面之后才OK。
听到这个反馈,我第一反应是,有可能是执行某些耗时长,占用资源大的代码时,内核挂掉了,于是我立马重启一下内核,发现内核能够重启成功,重启之后照样不能执行代码。
排掉这个因素,我想,是不是socket受不住断掉了,于是试了一下代码提示功能,发现代码提示功能正常,说明socket连接也没问题。
在看socket的时候我顺便看了一下Frames,点击执行按钮的时候,事件并没有触发,那这意味着run方法没有执行:
// 触发socket 执行代码
run(focusLine) {
this.setState({ runLine: focusLine, kernelStatus: 'busy' });
const { byId, socket } = this.props;
console.log('待执行代码:', byId[focusLine].source.join('\n'), focusLine);
// 如果这一句执行了,那么肯定可以看到对应Frame
socket.emit('codeExe', byId[focusLine].source.join('\n'), focusLine);
}
run方法其实并不是点击按钮时的回调,单步执行的回调是:
// 单步运行
runStep = needGoNext => {
this.needGoNext = needGoNext;
this.msgObj = {};
const { focusLine } = this.props;
console.log('runstep', finishFlag);
if (!this.finishFlag) return;
this.finishFlag = false;
this.run(focusLine);
};
那难道是runStep没有执行?可是console.log('runstep', finishFlag);能看到打印结果,说明点击事件绝对是生效了,那么就只有一个可能了:finishFlag这把锁造成的。
这把锁干什么用的?
finishFlag就是为了保证上一次执行完成才能触发下一次的执行。
如果不加锁,会造成代码执行结果的重复输出。
这把锁的逻辑很简单:
// 初始化锁为已完成状态
let finishFlag = true;
// notebook组件
export default class Notebook extends Component {
// 单步运行
runStep = needGoNext => {
this.needGoNext = needGoNext;
this.msgObj = {};
const { focusLine } = this.props;
console.log('runstep', finishFlag);
// 上一次执行未完成,直接返回
if (!this.finishFlag) return;
// 上锁
this.finishFlag = false;
this.run(focusLine);
// 不在这儿解锁,解锁的时机是收到idel片段时
};
return (<div>...notebook...<div>)
}
// 父组件
import Notebook from '../...'
export default class EditorTabs extends Component {
return
( <div>
<Notebook />
<Notebook />
</div>)
}
再想想错误现象,一个notebook无法执行的时候,另一个也没法执行了,那说明两个Notebook组件共享了这一个finishFlag,也就是说所有的Notebook都是是用的同一把锁!
为什么会这样呢?会不会有别的原因??
来做个实验好了,用create-react-app建一个空的项目,然后自己写一个这样的测试组件:
import React, { Component } from 'react';
let flag = true;
class Test extends Component {
changeFlag = () => {
console.log('----before', flag);
flag = !flag;
console.log('----after', flag);
}
render() {
return <button onClick={this.changeFlag}>Test</button>
}
}
export default Test;
import React, { Component } from 'react';
import Test from './Test';
class App extennds Component{
return (
<div>
<Test />
<Test />
<Test />
<Test />
</div>
);
}
如果每次点击输出都为
----before true
----after false
那说明是flag独立不受影响的。
但实验结果为:
image.png
再次验证了我的想法。
接下来把flag从组件外挪到组件内:
class Test extends Component {
constructor(props) {
super(props);
this.flag = true;
}
changeFlag = () => {
console.log('----before', flag);
this.flag = !this.flag;
console.log('----after', flag);
}
render() {
return <button onClick={this.changeFlag}>Test</button>
}
}
这样就没毛病了。
照葫芦画瓢我把finishFlag放到组件里,经过测试没有再出现用户反应的问题。
疑问
到这里,问题虽然解决了,但是我还是有些疑惑,这个flag到底在哪个作用域里?为什么没有被组件单独拥有呢?
看看Test模块的输出输入,然后在网上看了一些js模块化开发的博客,我终于懂了:
其实,flag属于这个Test.js模块,但是不属于这个App组件
export default Test;
import Test from '../..'
引申
- js模块化开发
js不像java有package把作用域隔开,但是开发中我们又经常需要分隔作用域,人为制造模块。
最古老的方式有:用function制造独立作用域,用对象集中管理不同作用域,立即执行函数制造独立作用域等;这样写起来很麻烦,需要开发人员去跟踪管理所有依赖关系;
随后出现了CommonJS, 个人人为CommonJS最大的改进是:规定一个单独的文件就是一个模块,每一个模块都是一个单独的作用域。CommonJS使用require和module.exports函数来导入导出模块,但是使用require获取依赖模块的时候,由于同步的原因产生阻塞, 这样的缺点显而易见。
随后出现了AMD和CMD,AMD使用requireJS,主要解决异步加载和前置引用的问题,CMD使用seaJs, 可以用到的时候再去require。但说实话这个时候JS都还没有原声支持“模块”,都要借助第三方库去做,直到ES6出来。
从ES6出来后,有了新的模块化开发方式,即ES6模块化。也就是我现在用得最多的这种。ES6模块化其实很多引擎还不支持,需要用babel转成CommonJS规范的那种。那ES6模块化和CommonJS有什么区别呢?
1.CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
总结
以后不要轻易在组件外写变量!写之前想清楚能不能这样写。
参考
[前端模块化]http://www.cnblogs.com/dolphinX/p/4381855.html
[Javascript模块化编程(一):模块的写法
]http://www.ruanyifeng.com/blog/2012/10/javascript_module.html
[js模块化进程]https://blog.csdn.net/u014168594/article/details/77099315
[Module 的加载实现]http://es6.ruanyifeng.com/#docs/module-loader
[深入 JSX]http://www.css88.com/react/docs/jsx-in-depth.html
网友评论