对于经常使用的框架,若是仅限于会用,我以为仍是远远不够,至少要理解它的思想,这样才不会掉入各类坑里面,这篇文章是基于react-lite源码来写的。javascript
在react里面,通过babel的解析后,jsx会变成createElement执行后的结果。java
const Test = (props) => <h1>hello, {props.name}</h1>;
<Test name="world" />
复制代码
<Test name="world" />
通过babel解析后会变为createElement(Test, {name: "world}),这里的Test就是上面的Test方法,name就是Test方法里面接受的props中的name。 实际上当咱们从开始加载到渲染的时候作了下面几步:react
// 1. babel解析jsx
<Test name="world"> -> createElement(Test, {name: "world"})
// 2. 对函数组件和class组件进行处理
// 若是是类组件,不作处理,若是是函数组件,增长render方法
const props = {name: world};
const newTest = new Component(props);
newTest.render = function() {
return Test(props);
}
// 3. 执行render方法
newTest.render();
复制代码
这样也很容易理解,const Test = <div>hello, world</div>
和const Test = () => <div>hello, world</div>
的区别了。git
react中的diff会根据子组件的key来对比先后两次virtual dom(即便先后两次子组件顺序打乱),因此这里的key最好使用不会变化的值,好比id之类的,最好别用index,若是有两个子组件互换了位置,那么index改变就会致使diff失效。github
为何布尔类型和null类型的值能够这么写,而数字类型却不行?数组
showLoading && <Loading />
复制代码
若是showLoading是个数字0,那么最后渲染出来的竟然是个0,可是showLoading是个false或者null,最后就什么都不渲染,这个是为何? 首先上述代码会被babel编译为以下格式:bash
showLoading && React.createElement(Loading, null)
复制代码
而若是showLoading是false或者0的时候,就会短路掉后面的组件,最后渲染出来的应该是个showLoading。 可是react-lite在渲染子组件的时候(递归渲染虚拟dom),会判断当前是否为布尔类型和null,若是是布尔类型或者null,则会被直接过滤掉。babel
function collectChild(child, children) {
if (child != null && typeof child !== 'boolean') {
if (!child.vtype) {
// convert immutablejs data
if (child.toJS) {
child = child.toJS()
if (_.isArr(child)) {
_.flatEach(child, collectChild, children)
} else {
collectChild(child, children)
}
return
}
child = '' + child
}
children[children.length] = child
}
}
复制代码
原来对cloneElement的理解就是相似cloneElement(App, {})这种写法,如今看了实现以后才理解。原来第一个参数应该是一个reactElement,而不是一个reactComponent,应该是<App />
,而不是App,这个也确实是我没有好好看文档。框架
当shouldComponentUpdate返回false的时候,组件没有从新渲染,可是更新后的state和props已经挂载到了组件上面,这个时候若是打印state和props,会发现拿到的已是更新后的了。dom
react里面setState后不会当即更新,但在某些场景下也会当即更新,下面这几种状况打印的值你都能回答的上来吗?
class App extends React.Component {
state = {
count: 0;
}
test() {
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为0
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为0
}
test2() {
setTimeout(() => {
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为1
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为2
})
}
test3() {
Promise.resolve().then(() => {
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为1
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为2
})
}
test4() {
this.setState(prevState => {
console.log(prevState.count); // 0
return {
count: prevState.count + 1
};
});
this.setState(prevState => {
console.log(prevState.count); // 1
return {
count: prevState.count + 1
};
});
}
async test4() {
await 0;
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为1
this.setState({
count: this.state.count + 1
});
console.log(this.state.count); // 此时为2
}
}
复制代码
在react中为了防止屡次setState致使屡次渲染带来没必要要的性能开销,会将待更新的state放到队列中,等到合适的时机(生命周期钩子和事件)后进行batchUpdate,因此在setState后没法当即拿到更新后的state。因此不少人说setState是异步的,setState表现确实是异步,可是里面没有用异步代码实现。并且不是等主线程代码执行结束后才执行的,而是须要手动触发。 若是是给setState传入一个函数,这个函数是执行前一个setState后才被调用的,因此函数返回的参数能够拿到更新后的state。 可是若是将setState在异步方法中(setTimeout、Promise等等)调用,因为这些方法是异步的,会致使生命周期钩子或者事件方法先执行,执行完这些后会将更新队列的pending状态置为false,这个时候在执行setState后会致使组件当即更新。从这里也能说明setState本质并非异步的,只是模拟了异步的表现。
ref用到原生的标签上,能够直接在组件内部用this.refs.xxx的方法获取到真实DOM。 ref用到组件上,须要用ReactDOM.findDOMNode(this.refs.xxx)的方式来获取到这个组件对应的DOM节点,this.refs.xxx获取到的是虚拟DOM。
react里面将能够冒泡的事件委托到了document上,经过向上遍历父节点模拟了冒泡的机制。 好比当触发onClick事件时,会先执行target元素的onClick事件回调函数,若是回调函数里面阻止了冒泡,就不会继续向上查找父元素。不然,就会继续向上查找父元素,并执行其onClick的回调函数。 当跳出循环的时候,就会开始进行组件的批量更新(若是没有收到新的props或者state队列为空就不会进行更新)。
参考: