对于常用的框架, 如果仅限于会用, 我觉得还是远远不够, 至少要理解它的思想, 这样才不会掉入各种坑里面, 这篇文章是基于 react-lite 源码来写的.
createElement 和 component
在 react 里面, 经过 babel 的解析后, jsx 会变成 createElement 执行后的结果.
- 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. 实际上当我们从开始加载到渲染的时候做了下面几步:
- // 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 > 的区别了.
key
react 中的 diff 会根据子组件的 key 来对比前后两次 virtual dom(即使前后两次子组件顺序打乱), 所以这里的 key 最好使用不会变化的值, 比如 id 之类的, 最好别用 index, 如果有两个子组件互换了位置, 那么 index 改变就会导致 diff 失效.
cloneElement
原来对 cloneElement 的理解就是类似 cloneElement(App, {})这种写法, 现在看了实现之后才理解. 原来第一个参数应该是一个 reactElement, 而不是一个 reactComponent, 应该是 < App />, 而不是 App, 这个也确实是我没有好好看文档.
shouldComponentUpdate
当 shouldComponentUpdate 返回 false 的时候, 组件没有重新渲染, 但是更新后的 state 和 props 已经挂载到了组件上面, 这个时候如果打印 state 和 props, 会发现拿到的已经是更新后的了.
setState
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
ref 用到原生的标签上, 可以直接在组件内部用 this.refs.xxx 的方法获取到真实 DOM. ref 用到组件上, 需要用 ReactDOM.findDOMNode( http://this.refs.xxx )的方式来获取到这个组件对应的 DOM 节点, this.refs.xxx 获取到的是虚拟 DOM.
合成事件
react 里面将可以冒泡的事件委托到了 document 上, 通过向上遍历父节点模拟了冒泡的机制. 比如当触发 onClick 事件时, 会先执行 target 元素的 onClick 事件回调函数, 如果回调函数里面阻止了冒泡, 就不会继续向上查找父元素. 否则, 就会继续向上查找父元素, 并执行其 onClick 的回调函数. 当跳出循环的时候, 就会开始进行组件的批量更新(如果没有收到新的 props 或者 state 队列为空就不会进行更新).
参考:
https://github.com/Lucifier129/react-lite
从零写一个 react https://github.com/hujiulong/blog/issues/4
来源: https://juejin.im/post/5c0b913b6fb9a049c15f0850