Time is flying, it arrives at the end of year again. This is my first year working in PinDuoDuo inc and it seems I arrive in the company yesterday. This point is a good chance to talk with(recognize) myself again. I try to conclude from work
, output
, life
and destination
.
The first course I learn from my team is about starting with the end
. It means everything we do today is prepared for the tomorrow. In the other word, we should realize which period the thing we’re doing is in, and our team write summary every week to make sure the established goals become closer and closer.
The second course I learn from the work is to try to use English more
. There are so much rich resource in Medium, MDN, Frontend Masters and even in twitter. It’s the first time I met with so many foreigners engineer in the JSConf but I can’t talk with them fluently. It’s obviously using English skillfully will open your eyes and improve efficiency looking for some information.
The last but most important is how to communicate
. It’s artistic to describe something easily to make others understand. When arriving at our team at first, one of my leader’s advice is the best way to join in the team is to share —— sharing ideas, knowledge and feeling. My colleagues are so excellent I learn a lot from them in these technical sharing parties every week. At the meanwhile I also share some ideas in it.
There are ten new articles added to my blog, the direction is to talk about React, JavaScript, CSS and so on. If there are mistakes in it, welcome point out.
There will be more creative articles to add in the future. Let’s grow up together.
I enjoy the quiet time sitting in the Cafe the whole day in Saturday and it’s also nice to chat with friends or family there.
And in other some weekend days, I take part in some FE conferences this year —— FDConf
, VUE Conf
, D2
, SEE Conf
and so on. From these activities I learn some some new knowledge. For example, the article How to make page run smoothly is written after listening the sharing of Liu Bowen in FDConf. And luckily, I get the chance to take photo with Evan You and get the
signature from him.
At the same time, it’s happy to meet with old friends and make with new friends in these activities.
Checking the plan of last year before making plans in new year.
five
technical books and a not technical book, React
communityNode.Js
Keeping going on in the new year. Here lists some direction.
When I look up css properties in MDN’s specifications section, there are some properties in it and it seems unfamiliar to me. So I try to find out some of them today.
The follow list try to give one word to specify the meaning of them.
And this article is to discuss the topic about the knowledge of inherited and non-inherited.
The initial value of a CSS property is its default value.
The Initial value has two different behavior between inherited properties
and non-inherited properties
.
For inherited properties
, the initial value is used on the root element only
.
1 | <span style="color: red"> |
The result is the color of both span
and em
element are red. Because the color is an inherited property, the element will get the color property from the parent recursively until to document.
For non-inherited properties
, the initial value is used on every element
. When no value for a non-inherited property has been specified on an element, the element gets the initial value directly.
1 | <span style="border: 1px solid black"> |
The result is the border is only effected on span
element, not em
. Because the border is a non-inherited property, and there is no border property specified on the em, so the em get the border’s initial value none
.
It mentioned much times about inherited value and non-inherited value above, and now we sort out css properties.
I’ve collected some inherited types in css, they are listed as follow:
Font Type
: font-style、font-variant、font-weight、font-stretch、font-size、font-family、color、line-height;Space Type
: letter-spacing、word-spacing、white-space;Letter Type
: text-align、text-indent、text-shadow、text-transform;List Type
: list-style、list-style-type、list-style-position;Others
: visibility、cursor;In the opposite of inherited type, the non-inherited types are listed as follow:
Layout Type
: float、position、left、right、top、bottom、z-index、displayBox Type
: width、max-width、min-width、height、max-height、min-height、margin、padding、border;Background Type
: background-size、background-image、background-clip、background-color、background-origin、background-position、background-repeat;Others
: overflow、text-overflow、vertical-align;These section introduces the inherited concept in CSS, and distinguish some inherited type and some non-inherited type from css properties. Hope it’s helpful for you.
]]>React 官网中对非受控组件与受控组件作了如图中下划线的边界定义。一经推敲, 该定义是缺乏了些完整性
和严谨性
的, 比如针对非表单组件(弹框、轮播图)如何划分受控与非受控的边界? 又比如非受控组件是否真的如文案上所说的数据的展示与变更都由 dom 自身接管呢?
在非受控组件中, 通常业务调用方只需传入一个初始默认值
便可使用该组件。以 Input 组件为例:
1 | // 组件提供方 |
在受控组件中, 数值的展示与变更
则分别由组件的 state
与 setState
接管。同样以 Input 组件为例:
1 | // 组件提供方 |
有意思的一个问题来了, Input
组件到底是受控的还是非受控的? 我们甚至还可以对代码稍加改动成 <Input defaultValue={1} />
的最初调用方式:
1 | // 组件提供方 |
尽管此时 Input 组件本身是一个受控组件, 但与之相对的调用方失去了更改 Input 组件值的控制权
, 所以对调用方而言, Input 组件是一个非受控组件。值得一提的是, 以非受控组件的使用方式去调用受控组件
是一种反模式, 在下文中会分析其中的弊端。
如何做到不管对于组件提供方还是调用方 Input 组件都为受控组件呢? 提供方让出控制权即可, 调整代码如下codesandbox:
1 | // 组件提供方 |
经过上述代码的推演后, 概括如下: 受控以及非受控组件的边界划分取决于当前组件对于子组件值的变更是否拥有控制权
。如若有则该子组件是当前组件的受控组件; 如若没有则该子组件是当前组件的非受控组件。
基于调用方对于受控组件拥有控制权这一认知, 因此受控组件相较非受控组件能赋予调用方更多的定制化职能。这一思路与软件开发中的开放/封闭原则有异曲同工之妙, 同时让笔者受益匪浅的 Inversion of Control 也是类似的思想。
借助受控组件的赋能, 以 Input 组件为例, 比如调用方可以更为自由地对值进行校验限制, 又比如在值发生变更时执行一些额外逻辑。
1 | // 组件提供方 |
因此综合基础组件扩展性
与通用性
的考虑, 受控组件的职能相较非受控组件更加宽泛, 建议优先使用受控组件
来构建基础组件。
首先何谓反模式? 笔者将其总结为增大隐性 bug 出现概率的模式
, 该模式是最佳实践的对立经验
。如若使用了反模式就不得不花更多的精力去避免潜在 bug。官网对反模式也有很好的概括总结。
缘何上文提到以非受控组件的使用方式去调用受控组件是一种反模式? 观察 Input 组件的第一行代码, 其将 defaultValue 赋值给 value, 这种将 props 赋值给 state
的赋值行为在一定程度上会增加某些隐性 bug 的出现概率。
比如在切换导航栏的场景中, 恰巧两个导航中传进组件的 defaultValue 是相同的值, 在导航切换的过程中便会将导航一中的 Input 的状态值带到导航二中, 这显然会让使用方感到困惑。codesandbox
1 | // 组件提供方 |
如何避免使用该反模式同时有效解决问题呢? 官方提供了两种较为优质的解法, 笔者将其留给大家思考。
测试用例的书写是一个风险驱动的行为, 每当收到 Bug 报告时, 先写一个单元测试来暴露这个 Bug, 在日后的代码提交中, 若该测试用例是通过的, 开发者就能更为自信地确保程序不会再次出现此 bug。
测试的动机是有效地提高开发者的自信心。
前端测试中有两种模型, 金字塔模型
与奖杯模型
。
金字塔模型摘自 Martin Fowler’s blog, 模型示意图如下:
金字塔模型自下而上分为单元测试、集成测试、UI 测试, 之所以是金字塔结构是因为单元测试的成本最低, 与之相对, UI 测试的成本最高。所以单元测试写的数量最多, UI 测试写的数量最少。同时需注意的是越是上层的测试, 其通过率给开发者带来的信心是越大的。
奖杯模型摘自 Kent C. Dots 提出的 The Testing Trophy, 该模型是笔者比较认可的前端现代化测试模型, 模型示意图如下:
奖杯模型中自下而上分为静态测试、单元测试、集成测试、e2e 测试, 它们的职责大致如下:
静态测试
: 在编写代码逻辑阶段时进行报错提示。(代表库: eslint、flow、TypeScript)单元测试
: 在奖杯模型中, 单元测试的职责是对一些边界情况或者特定的算法进行测试。(代表库: jest、mocha)集成测试
: 模拟用户的行为进行测试, 对网络请求、获取数据库的数据等依赖第三方环境的行为进行 mock。(代表库: jest、react-testing-library)e2e 测试
: 模拟用户在真实环境上操作行为(包括网络请求、获取数据库数据等)的测试。(代表库: cypress)越是上层的测试给开发者带来的自信是越大的, 与此同时, 越是下层的测试测试的效率是越高的。奖杯模型综合考虑了这两点因素, 可以看到其在集成测试中的占比是最高的。
书写测试用例是为了提高开发者对程序的自信心的, 但是很多时候书写测试用例给开发者带来了觉得在做无用功的沮丧。导致沮丧的感觉出现往往是因为开发者对组件的具体实现细节进行了测试, 如果换个角度站在用户的行为上进行测试则能极大提高测试效率。
测试组件的具体细节会带来的两个问题:
错误否定
;错误肯定
;以轮播图组件
为例, 依次来看上述问题。轮播图组件伪代码如下:
1 | class Carousel extends React.Component { |
如下是基于 enzyme
的 api 写的测试用例:
1 | import { mount } from 'enzyme' |
恭喜, 测试通过✅。某一天开发者觉得 index
的命名不妥, 对其重构将 index
更名为 currentPage
, 此时代码如下:
1 | class Carousel extends React.Component { |
再次跑测试用例, 此时在 expect(wrapper.state('index')).toBe(0)
的地方抛出了错误❌, 这就是所谓的测试用例对代码进行了错误否定
。因为这段代码对于使用方来说是不存在问题的, 但是测试用例却抛出错误, 此时开发者不得不做’无用功’来调整测试用例适配新代码。调整后的测试用例如下:
1 | describe('Carousel Test', () => { |
然后在某一天粗心的小明同学对代码做了以下改动:
1 | class Carousel extends React.Component { |
小明同学跑了上述单测, 测试通过✅, 于是开心地提交了代码。结果上线后线上出现了问题! 这就是所谓测试用例对代码进行了错误肯定
。因为测试用例测试了组件内部细节(此处为 jump
函数), 让小明误以为已经覆盖了全部场景。
测试用例错误否定
以及错误肯定
都给开发者带来了挫败感与困扰, 究其原因是测试了组件内部的具体细节所至。而一个稳定可靠的测试用例应该脱离组件内部的实现细节, 越接近用户行为的测试用例能给开发者带来越充足的自信。相较于 enzyme, react-testing-library 所提供的 api 更加贴近用户的使用行为, 使用其对上述测试用例进行重构:
1 | import { render, fireEvent } from '@testing-library/react' |
关于 react-testing-Library
的用法总结将在下一章节 Jest 与 react-testing-Library 具体介绍。如果对 React 技术栈感兴趣, 欢迎关注个人博客。
本文是 React Hooks 深入系列的后续。此篇详细介绍了 Hooks 相对 class 的优势所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子作了阐述。
React 的 logo 是一个原子图案, 原子组成了物质的表现。类似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 其更接近 React 本质的样子, 但是直到 4 年后的今天才被真正设计出来。 —— Dan in React Conf(2018)
一: 多个组件间逻辑复用
: 在 Class 中使用 React 不能将带有 state 的逻辑给单独抽离成 function, 其只能通过嵌套组件的方式来解决多个组件间逻辑复用的问题, 基于嵌套组件的思想存在 HOC 与 render props
两种设计模式。但是这两种设计模式是否存在缺陷呢?
二: 单个组件中的逻辑复用
: Class 中的生命周期 componentDidMount
、componentDidUpdate
甚至 componentWillUnMount
中的大多数逻辑基本是类似的, 必须拆散在不同生命周期中维护相同的逻辑对使用者是不友好的, 这样也造成了组件的代码量增加。
三: Class 的其它一些问题: 在 React 使用 Class 需要书写大量样板, 用户通常会对 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 当结合 class 与 TypeScript 一起使用时, 需要对 defaultValue 做额外声明处理; 此外 React Team 表示 Class 在机器编译优化方面也不是很理想。
原因是数组的解构比对象更加方便, 可以观察以下两种数据结构解构的差异。
返回数组时, 可以直接解构成任意名字。
1 | [name, setName] = useState('路飞') |
返回对象时, 却需要多一层的命名。
1 | {value: name, setValue: setName} = useState('路飞') |
Hooks 是否可以设计成在组件中通过函数传参来使用? 比如进行如下调用?
1 | const SomeContext = require('./SomeContext) |
使用传递的劣势是会出现冗余的传递。(可以联想 context 解决了什么)
Hooks 中的 setState 与 Class 中最大区别在于 Hooks 不会对多次 setState 进行合并操作。如果要执行合并操作, 可执行如下操作:
1 | setState(prevState => { |
此外可以对 class 与 Hooks 之间 setState
是异步还是同步的表现进行对比, 可以先对以下 4 种情形 render 输出的个数进行观察分析:
在 React 16.8 版本之后, 针对不是特别复杂
的业务场景, 可以使用 React 提供的 useContext
、useReducer
实现自定义简化版的 redux, 可见 todoList 中的运用。核心代码如下:
1 | import React, { createContext, useContext, useReducer } from "react" |
但是针对特别复杂的场景目前不建议使用此模式, 因为 context 的机制会有性能问题。具体原因可见 react-redux v7 回退到订阅的原因
React 官方在未来很可能会提供一个 usePrevious
的 hooks 来获取之前的 props 以及 state。
usePrevious
的核心思想是用 ref 来存储先前的值。
1 | function usePrevous(value) { |
在 Hooks 中使用 useRef() 等价于在 Class 中使用 this.something。
1 | /* in a function */ |
在 React 暗器百解 中提到了 getDerivedStateFromProps
是一种反模式, 但是极少数情况还是用得到该钩子, Hooks 没有该 api, 那其如何达到 getDerivedStateFromProps 的效果呢?
1 | function ScrollView({row}) { |
可以使用 useReducer
来 hack forceUpdate
, 但是尽量避免 forceUpdate 的使用。
1 | const [ignored, forceUpdate] = useReduce(x => x + 1, 0) |
在 Hooks 中可以使用 useMemo
来作为 shouldComponentUpdate
的替代方案, 但 useMemo
只对 props 进行浅比较。
1 | React.useMemo((props) => { |
1 | useMemo(() => <component />) 等价于 useCallback(<component />) |
通常来说依赖列表中移除函数是不安全的。观察如下 demo
1 | const { useState, useEffect } = React |
在该 demo 中, 点击 button 按钮, 并没有打印出 2。解决上述问题有两种方法。
方法一: 将函数放入 useEffect
中, 同时将相关属性放入依赖项中。因为在依赖中改变的相关属性一目了然, 所以这也是首推的做法。
1 | function Example({ someProp }) { |
方法二: 把函数加入依赖列表中
1 | function Example({ someProp }) { |
方案二基本上不会单独使用, 它一般结合 useCallback
一起使用来处理某些函数计算量较大的函数。
1 | function Example({ someProp }) { |
useState
的懒初始化, 用法如下1 | const [value, setValue] = useState(() => createExpensiveObj) |
1 | function Image(props) { |
本篇是对 FDCon2019 上《让你的网页更丝滑》课题的复盘文。该课题也是博主感兴趣的领域, 后续对该文的细节进行进一步补充。
当前市面上的设备频率在 60 HZ 以上。
跑如下界面 https://code.h5jun.com/pojob
结合如下代码块, 可以看到 100ms 以下的点击是顺畅的, 而超过 100ms 的点击就会有卡顿现象。
1 | var observer = new PerformanceObserver(function(list) { |
衡量一个网页/App 是否流畅有个比较好用的 Rail 模型, 它大概有以下几个评判标准值。
1 | Response —— 100ms |
像素管道一般由 5 个部分组成。JavaScript、样式、布局、绘制、合成。如下图所示:
1 | function App() { |
一般超过 50 ms 认为是 long task(长任务)
, long task
会阻塞 main thread
的运行, 如下是两种解决方案。
app.js
代码如下:
1 | import React, {useEffect} from 'react' |
worker.js
代码如下:
1 | const workerCode = () => { |
此时在输入框输入时没有卡顿的感觉。
下面是另外一种使页面流畅的方法 —— Time Slicing
(时间分片)。
观察 Chrome 的 Performance, 火焰图如下,
从火焰图可以看出主线程被拆分为了多个时间分片, 所以不会造成卡顿。时间分片的代码片段如下所示:
1 | function timeSlicing(gen) { |
该函数虽然代码量不长, 但却不易理解。前置知识 Generator
下面对该函数进行分析:
timeSlicing
中传入 generator
函数;performance.now() - start < 1000
则继续 ②、③, 如果 performance.now() - start >= 1000
则跳出循环执行 ④、⑤);针对 long task
会阻塞 main thread
的运行的情形, 给出两种解决方案:
Web Worker
: 使用 Web Worker
提供的多线程环境来处理 long task
;Time Slicing
: 将主线程上的 long task
进行时间分片;保证 16.7ms
有新的一帧传输到界面上。除去用户的逻辑代码, 一帧内留给浏览器整合的时间大概只有 6ms
左右, 回到像素管道上来, 我们可以从这几方面进行优化:
Style 这部分的优化在 css 样式选择器的使用, css 选择器使用的层级越多, 耗费的时间越多。以下是测试 css 选择器不同层级筛选相同元素的一次测试结果。
1 | div.box:not(:empty):last-of-type span 2.25ms |
1 | // 先修改值 |
这段代码有什么问题呢?
可以看到它会造成布局重排。
应对的策略是调整它们的执行顺序,
1 | // 先取值 |
可以看到经过调换顺序后, 后执行的 el.style.width 会新开一个像素管道, 而不会在原先的像素管道进行重排。
此外不要在循环中执行如下的操作,
1 | for (var i = 0; i < 1000; i++) { |
可以在火焰图中看到它发生了重绘的警告,
执行顺序是 ①②①②①②①…, 假若我们在第一个 ① 后面插入一条竖线后 ①|②①②①②①, 其就变成先修改值后取值的情景, 所以也就发生了重绘!
正确的使用姿势应该如下:
1 | const newWidth = container.offsetWidth; |
创建 Layers(图层) 可以避免重绘,
1 | { |
本文为对 hooks 碎片化的理解。同时欢迎关注基于 hooks 构建的 UI 组件库 —— snake-design。
在 class 已经融入 React 生态的节点下, React 推出的 Hooks 具有如下优势:
HOC
与 render Props
, Hooks 拥有更加自由地组合抽象的能力;在 hooks
中每一次 render
都有自己的 state
和 props
, 这与 class
中存在差异, 见 Hooks 每次渲染都是闭包
写出 useEffect 的所用到的依赖
在以下 demo 中, useEffect
的第二个参数传入 []
, 希望的是 useEffect
里的函数只执行一次(类似在 componentDidMount
中执行一次, 但是注意这里仅仅是类似
, 详细原因见上一条注意项), 页面上每隔 1s 递增 1。
1 | function Demo() { |
但这样达到我们预期的效果了么? demo, 可以看到界面上只增加到 1 就停止了。原因就是传入的第二个参数 []
搞的鬼, []
表示没有外界状态对 effect
产生干扰。流程大致如下:
useEffect
传入的 count
为 0, 于是 setCount(0 + 1)
;useEffect
第二个参数 []
的影响,count
仍然为 0, 所以相当于还是 setCount(0 + 1)
;那如何修正上述问题呢? 方法有两个(方法一为主, 方法二为辅):
[]
改为 [count]
setCount(count + 1)
改为 setCount(count => count + 1)
。这种方法的思想是修正状态的值而不依赖外面传进的状态。不过遇到 setCount(count => count + 1)
的情况就可以考虑使用 useReducer
了。
使用 useState
的地方都能用 useReducer
进行替代。相较 useState
, useReducer
有如下优势:
useReducer
将 how
(reducer) 和 what
(dispatch(action)) 进行抽离; 使用 reducer
逻辑状态进行集中化维护;1 | function Demo() { |
此时 useEffect
中传入的第二个参数 getFetchUrl
相当于每次都是新的, 所以每次都会请求数据, 那除了 [getFetchUrl]
将改为 []
这种不推荐的写法外,有两种解决方法:
*. 方法一: 提升 getFetchUrl
的作用域;
*. 方法二: 使用 useCallback
或者 useMemo
来包裹 getFetchUrl;
React.memo
修饰一个函数组件,useMemo
修饰一个函数。它们本质都是运用缓存。
为了理解 React Hooks 内部实现原理, 对 useState
、useEffect
进行了简单的实现。
使用闭包来实现 useState
的简单逻辑:
1 | // 这里使用闭包 |
测试如下:
1 | function Counter() { |
1 | var React = (function() { |
测试代码如下:
1 | var {useState, useEffect} = React |
为了在 hooks
中能使用多次 useState
, useEffect
, 将各个 useState
, useEffect
的调用存进一个数组中, 在上面基础上进行如下改造:
1 | const React = (function() { |
测试代码如下:
1 | var {useState, useEffect} = React |
本文副标题是 Request Schedule 源码解析一
。在本章中会介绍 requestIdleCallback
的用法以及其缺陷, 接着对 React 团队对该 api 的 hack 部分的源码进行剖析。在下一篇中会结合优先级对 React 的调度算法进行宏观的解释, 欢迎关注个人博客。
React 调度算法
与 requestIdleCallback
这个 api 息息相关。requestIdleCallback
的作用是是在浏览器一帧的剩余空闲时间内执行优先度相对较低的任务, 其用法如下:
1 | var tasksNum = 10000 |
deadline
有两个参数
timeRemaining()
: 当前帧还剩下多少时间didTimeout
: 是否超时另外 requestIdleCallback
后如果跟上第二个参数 {timeout: ...}
则会强制浏览器在当前帧执行完后执行。
requestIdleCallback is called only 20 times per second - Chrome on my 6x2 core Linux machine, it’s not really useful for UI work。—— from Releasing Suspense
也就是说 requestIdleCallback
的 FPS 只有 20
, 这远远低于页面流畅度的要求!(一般 FPS 为 60 时对用户来说是感觉流程的, 即一帧时间为 16.7 ms), 这也是 React 需要自己实现 requestIdleCallback
的原因。
在不能操作 DOM 的环境下, 可以借助 setTimeout
来模拟 requestIdleCallback
的实现。
1 | requestIdleCallback = (callback) => { |
下面将 React 源码中关于服务端的实现也呈现出来:
1 | let _callback = null; |
在浏览器端的环境下, 介绍一个与 requestIdleCallback
功能相近的 api —— requestAnimationFrame(callback)
, 其会在下次重绘前执行指定的回调函数,因此这个 api 在动效领域得到了广泛的使用。下面通过一个简单的 demo 来认识它:
1 | let frame |
执行上述代码, 控制台(chrome)打印如下数据:
1 | 先输出 5 次 '测试执行顺序' |
可以看到在浏览器上一帧的时间大致为 16ms
。同时可以看到 requestAnimation(callback)
中的 callback 也是异步的(只不过它是基于帧与帧间的异步), 所以上述打印结果是先打印出 5 次 ‘测试执行顺序’ 后再依次打印出 5 个时间戳。
requestHostCallback
(也就是 requestIdleCallback) 这部分源码的实现比较复杂, 可以将其分解为以下几个重要的步骤(有一些细节点可以看注释):
postMessage
触发步骤四, 否则如果 requestAnimationFrame
在当前帧没有安排任务, 则开始一个帧的流程;requestAnimationFrameWithTimeout
函数, 该函数调用了 requestAnimationFrame
, 并对执行时间超过 100ms
的任务用 setTimeout
放到下一个事件队列中处理;requestAnimationFrame
中的回调函数 animationTick
, 在该回调函数中得到当前帧的截止时间 frameDeadline
, 并通过 postMessage
触发步骤四;onmessage
接受 postMessage
指令, 触发消息事件的执行。在 onmessage
函数中根据 frameDeadline - currentTime <= 0
判断任务是否可以在当前帧执行,如果可以的话执行该任务, 否则进入下一帧的调用。1 | export let requestHostCallback; |
本文介绍与 Suspense
在三种情景下使用方法,并结合源码进行相应解析。欢迎关注个人博客。
在 16.6 版本之前,code-spliting
通常是由第三方库来完成的,比如 react-loadble(核心思路为: 高阶组件 + webpack dynamic import), 在 16.6 版本中提供了 Suspense
和 lazy
这两个钩子, 因此在之后的版本中便可以使用其来实现 Code Spliting
。
目前阶段, 服务端渲染中的
code-spliting
还是得使用react-loadable
, 可查阅 React.lazy, 暂时先不探讨原因。
Code Spliting
在 React
中的使用方法是在 Suspense
组件中使用 <LazyComponent>
组件:
1 | import { Suspense, lazy } from 'react' |
源码中 lazy
将传入的参数封装成一个 LazyComponent
1 | function lazy(ctor) { |
观察 readLazyComponentType 后可以发现 dynamic import
本身类似 Promise
的执行机制, 也具有 Pending
、Resolved
、Rejected
三种状态, 这就比较好理解为什么 LazyComponent
组件需要放在 Suspense
中执行了(Suspense
中提供了相关的捕获机制, 下文会进行模拟实现`), 相关源码如下:
1 | function readLazyComponentType(lazyComponent) { |
为了解决获取的数据在不同时刻进行展现的问题(在 suspenseDemo 中有相应演示), Suspense
给出了解决方案。
下面放两段代码,可以从中直观地感受在 Suspense
中使用 Async Data Fetching
带来的便利。
1 | export default class Demo extends Component { |
Suspense
中进行数据获取的代码如下:1 | const resource = unstable_createResource((id) => { |
可以看到在 Suspense
中进行数据获取的代码量相比正常的进行数据获取的代码少了将近一半!少了哪些地方呢?
loading
状态的维护(在最外层的 Suspense 中统一维护子组件的 loading)当前 Suspense
的使用分为三个部分:
第一步: 用 Suspens
组件包裹子组件
1 | import { Suspense } from 'react' |
第二步: 在子组件中使用 unstable_createResource
:
1 | import { unstable_createResource } from 'react-cache' |
第三步: 在 Component
中使用第一步创建的 resource
:
1 | const data = resource.read('demo') |
来看下源码中 unstable_createResource
的部分会比较清晰:
1 | export function unstable_createResource(fetch, maybeHashInput) { |
结合该部分源码, 进行如下推测:
throw
一个 thenable
对象, Suspense
组件内的 componentDidCatch
捕获之, 此时展示 Loading
组件;Promise
态的对象变为完成态后, 页面刷新此时 resource.read()
获取到相应完成态的值;LRU
缓存算法, 跳过 Loading
组件返回结果(缓存算法见后记);官方作者是说法如下:
所以说法大致相同, 下面实现一个简单版的 Suspense
:
1 | class Suspense extends React.Component { |
进行如下调用
1 | <Suspense fallback={<div>loading...</div>}> |
效果调试可以点击这里, 在 16.6
版本之后, componentDidCatch
只能捕获 commit phase
的异常。所以在 16.6
版本之后实现的 <PromiseThrower>
又有一些差异(即将 throw thenable
移到 componentDidMount
中进行)。
当网速足够快, 数据立马就获取到了,此时页面存在的 Loading
按钮就显得有些多余了。(在 suspenseDemo 中有相应演示), Suspense
在 Concurrent Mode
下给出了相应的解决方案, 其提供了 maxDuration
参数。用法如下:
1 | <Suspense maxDuration={500} fallback={<Loading />}> |
该 Demo 的效果为当获取数据的时间大于(是否包含等于还没确认) 500 毫秒, 显示自定义的 <Loading />
组件, 当获取数据的时间小于 500 毫秒, 略过 <Loading>
组件直接展示用户的数据。相关源码。
需要注意的是 maxDuration
属性只有在 Concurrent Mode
下才生效, 可参考源码中的注释。在 Sync 模式下, maxDuration
始终为 0。
LRU
算法: Least Recently Used
最近最少使用算法(根据时间);LFU
算法: Least Frequently Used
最近最少使用算法(根据次数);若数据的长度限定是 3, 访问顺序为 set(2,2),set(1,1),get(2),get(1),get(2),set(3,3),set(4,4)
, 则根据 LRU
算法删除的是 (3, 3)
, 根据 LFU
算法删除的是 (1, 1)
。
react-cache
采用的是 LRU
算法。
Suspense
开发进度此章节会通过两个 demo
来展示 Stack Reconciler
以及 Fiber Reconciler
的数据结构。
首先用代码表示上图节点间的关系。比如 a1 节点
下有 b1、b2、b3 节点
, 就可以把它们间的关系写成 a1.render = () => [b1, b2, b3]
;
1 | var a1 = { name: 'a1', render = () => [b1, b2, b3] } |
在 React 16
之前,节点之间的关系可以用数据结构中树的深度遍历
来表示。
如下实现 walk
函数, 将深度遍历的节点打印出来。
1 | walk(a1) |
输出结果为: a1 b1 c1 d1 d2 b2 c2 b3
在 React 16
中,节点之间的关系可以用数据结构中的链表
来表示。
节点之间的链表有三种情形, 用图表示如下:
父节点指向第一个子节点, 每个子节点都指向父节点,同层节点间是单向链表。
首先, 构建节点的数据结构, 如下所示:
1 | var FiberNode = function(instance) { |
然后创建一个将节点串联起来的 connect
函数:
1 | var connect = function(parent, childList) { |
在 JavaScript 中实现链表的数据结构可以巧用 reduceRight
connect
函数中实现了上述链表关系。可以像这样使用它:
1 | var parent = new FiberNode(a1) |
这样子便完成了 a1 节点
指向 b1 节点
的链表、b1、b2、b3 节点间
的单向链表以及 b1、b2、b3 节点
指向 a1 节点
的链表。
最后剩下 goWalk
函数将全部节点给遍历完。
1 | // 打印日志以及添加列表 |
打印结果为 a1 b1 c1 d1 d2 b2 c2 b3
通过分析上述两种数据结构实现的代码,可以得出下面结论:
while(true) {}
的循环中, 可以通过 currentNode
的赋值重新得到需要操作的节点,而在赋值之前便可以’暂停’来执行其它逻辑, 这也是 requestIdleCallback
能得以在 Fiber Reconciler
的原因。该系列会有 3 篇文章,分别介绍什么是函数式编程、剖析函数式编程库、以及函数式编程在 React 中的应用,欢迎关注我的 blog
拿泡茶这个事例进行区分命令式编程和声明式编程
1.烧开水(为第一人称)
2.拿个茶杯
3.放茶叶
4.冲水
1.给我泡杯茶(为第二人称)
举个 demo
1 | // 命令式编程 |
函数式编程是声明式编程的范式。在函数式编程中数据在由纯函数组成的管道中传递。
函数式编程可以用简单如
交换律、结合律、分配律
的数学之法来帮我们简化代码的实现。
它具有如下一些特性:
1 | // 反面示例 |
1 | // 反面示例 |
在后记 1 中对数组字符串方法是否对原值有影响作了整理
1 | const add = a => b => c => a + b + c |
1 | const add = a => (b, c) => a + b + c |
1 | const add = (x) => x + x |
如下是一个加法函数:
1 | var add = (a, b, c) => a + b + c |
假如有这样一个 curry
函数, 用其包装 add
函数后返回一个新的函数 curryAdd
, 我们可以将参数 a、b
进行分开传递进行调用。
1 | var curryAdd = curry(add) |
核心思路: 若传进去的参数个数未达到 curryAdd
的个数,则将参数缓存在闭包变量 lists 中:
1 | function curry(fn, ...args) { |
现在有 toUpperCase
、reverse
、head
三个函数, 分别如下:
1 | var toUpperCase = (str) => str.toUpperCase() |
接着使用它们实现将数组末位元素大写化输出, 可以这样做:
1 | var reverseHeadUpperCase = (arr) => toUpperCase(head(reverse(arr))) |
此时在构建 reverseHeadUpperCase
函数的时候, 必须手动声明传入参数 arr, 是否能提供一个 compose
函数让使用者更加友好的使用呢? 类似如下形式:
1 | var reverseHeadUpperCase = compose(toUpperCase, head, reverse) |
此外 compose
函数符合结合律
, 我们可以这样子使用:
1 | compose(compose(toUpperCase, head), reverse) |
以上两种写法与 compose(toUpperCase, head, reverse)
的效果完全相同, 都是依次从右到左执行传参中的函数。
此外 compose
和 map
一起使用时也有相关的结合律, 以下两种写法效果相等
1 | compose(map(f), map(g)) |
代码精华集中在一行之内, 其为众多开源库(比如 Redux) 所采用。
1 | var compose = (...args) => (initValue) => args.reduceRight((a, c) => c(a), initValue) |
范畴论是数学中的一个分支。可以将范畴理解为一个容器, 把原来对值的操作,现转为对容器的操作。如下图:
学习函数式编程就是学习各种函子的过程。
函数式编程中, 函子(Functor)
是实现了 map
函数的容器, 下文中将函子视为范畴,模型可表示如下:
1 | class Functor { |
但是在函数式编程中, 要避免使用 new
这种面向对象的编程方式, 取而代之对外暴露了一个 of
的接口, 也称为 pointed functor
。
1 | Functor.of = value => new Functor(value) |
Maybe 函子
是为了解决 this.value
为 null 的情形, 用法如下:
1 | Maybe.of(null).map(r => r.toUpperCase()) // null |
实现代码如下:
1 | class Maybe { |
Either 函子
是为了对应 if...else...
的语法, 即非左即右
。因此可以将之拆分为 Left
和 Right
两个函子, 它们的用法如下:
1 | Left.of(1).map(r => r + 1) // Left {value: 1} |
Left 函子
实现代码如下:
1 | class Left { |
Right 函子
实现代码如下(其实就是上面的 Functor
):
1 | class Right { |
具体 Either
函数只是对调用 Left 函子
或 Right 函子
作一层筛选, 其接收 f
、g
两个函数以及一个函子(Left or Right
)
1 | var Either = function(f, g, functor) { |
使用 demo:
1 | Either((v) => console.log('left', v), (v) => console.log('def', v), left) // left 1 |
函子会发生嵌套, 比如下面这样:
1 | Functor.of(Functor.of(1)) // Functor { value: Functor { value: 1 } } |
Monad 函子
对外暴露了 join
和 flatmap
接口, 调用者从而可以扁平化嵌套的函子。
1 | class Monad { |
使用方法:
1 | // join |
Monad 函子可以运用在 I/O 这种不纯的操作上将之变为纯函数的操作,目前比较懵懂,日后补充。
1 | var test = [1, 2, 3] |
1 | var test = [1, 2, 3] |
1 | var test = [1, 2, 3] |
1 | var arr = [2, 1, 3, 4] |
1 | var test = [1, 2, 3] |
1 | var test = [1, 2, 3] |
1 | // substr |
度过在点我达两年欢快的时光,开启在拼多多的新的旅程。
Siren
是星巴克 Logo
上双尾女海妖的名字。本意是希望星巴克的咖啡就像 Siren
的歌声那样美妙, 除此之外它还有个引申意 —— 克制欲望。
18 年下半年的周末大多待在下城区金逸影城(也就是 18 年 VueConf
的举办地)的星巴克里看看书之类, 这家星巴克在喧嚣和安静之间保持了恰当好处, 坐在周围的可能是谈论艺术的大学教授,或者是带小朋友做作业的家长,又或者是看书的同学。自由、温馨,时间在这里可快可慢。
以下是对 18 年计划的 review
flag | 完成情况 |
---|---|
对知识点采取思维脑图的方式进行学习 | 创建了 blog 项目进行了知识点的整理输出 |
参与到一个千星 Star 的开源项目的改善,提高阅读源码的能力,阅读 6 本技术书籍,1 本非技术书籍 | 在开源项目的参与深度上有所欠缺, 书籍阅读指标基本达成 |
课外学习积累相关方面知识并尝试用到公司项目中 | 指标达成 |
避免讲话结巴,加强语言组织能力和逻辑能力,没想清楚问题之前不要轻易回答 | 沟通交流能力需持续加强 |
加强自控能力, 规定的时间做规定的事情,做到按时起睡,不晚于 8 点半起床 | 自控能力有所提高,按时起睡指标未达成 |
React.js
社区,提一次 prnode.js
文档感恩亦师亦友的愚安 boss, 以及给我帮助的点我达小伙伴们; 感激对我给予肯定的刃捷、明江前辈; 感激与我聊到凌晨的兵长兄; 感谢给我提供前行指引的死月、芙兰姐; 感谢掘金社区让我结识了小小倩、染陌、相学长等好友。感谢所有内推、面试过我的前辈, 让我认识到自己的不足。最后感谢所有杭州的朋友们,期待下次更好的遇见!
]]>本文整理了 React 16.x 出现的耳目一新的概念与 api 以及应用场景。
更多 React 系列文章可以订阅blog
he
在 16 之前的版本的渲染过程可以想象成一次性潜水 30 米,在这期间做不了其它事情(Stack Reconciler);
痛点概括:
在 16 版本上, React 带来了 Fiber 的架构, 接着拿上面的潜水例子为例,现在变为可以每次潜 10 米,分 3 个 chunk 进行; chunk 和 chunk 之间通过链表连接; chunk 间插入优先级更高的任务, 先前的任务被抛弃。
开启 Fiber 后,获取异步数据的方法应放到 render 后面的生命周期钩子里(phase 2 阶段)进行, 因为 render 前面的生命周期钩子(phase 1阶段)会被执行多次
注意: 并没有缩短原先组件的渲染时间(甚至还加长了),但用户却能感觉操作变流畅了。
在 React16 版本中 render() 增加了一些返回类型,到目前为止支持的返回类型如下:
[render](https://reactjs.org/docs/react-component.html#render]
其中 render() 支持返回 Arrays 能让我们少写一个父节点, 如下所示:
1 | const renderArray = () => [ |
个人认为 render() 支持返回数组完全可以取代 Fragments
将 react 子节点渲染到指定的节点上
案例:实现一个 Modal 组件,demo
另外关于 Portals 做到冒泡到父节点的兄弟节点这个现象, demo, 我想可以这样子实现:如果组件返回是 Portal 对象,则将该组件的父组件的上的事件 copy 到该组件上。其实并不是真的冒泡到了父节点的兄弟节点上。
React 16 提供了一个新的错误捕获钩子 componentDidCatch(error, errorInfo)
, 它能将子组件生命周期里所抛出的错误捕获, 防止页面全局崩溃。demo
componentDidCatch 并不会捕获以下几种错误
服务端渲染一般是作为最后的优化手段, 这里浅显(缺乏经验)谈下 React 16 在其上的优化。
在 React 16 版本中引入了 React.hydrate()
, 它的作用主要是将相关的事件注水
进 html
页面中, 同时会比较前端生成的 html
和服务端传到前端的 html
的文本内容的差异, 如果两者不一致将前端产生的文本内容替换服务端生成的(忽略属性)。
在 React 16 版本中, 支持自定义属性(推荐 data-xxx
), 因而 React 可以少维护一份 attribute 白名单, 这也是 React 16 体积减少的一个重要因素。
在 React 16.3 的版本中,新加入了两个生命周期:
getDerivedStateFromProps(nextProps, prevState)
: 更加语义化, 用来替代 componentWillMount、componentWillReceiveProps(nextProps);
getSnapshotBeforeUpdate(prevProps, prevState)
: 可以将结果传入 componentDidUpdate 里, 从而达到 dom 数据统一。用来替代 componentWillUpdate()(缺点是 React 开启异步渲染后,componentWillUpdate() 与 componentDidUpdate() 间获取的 dom 会不统一;
在 React 16.7 之前,React 有两种形式的组件,有状态组件(类)和无状态组件(函数)。Hooks 的意义就是赋能先前的无状态组件,让之变为有状态。这样一来更加契合了 React 所推崇的函数式编程。
接下来梳理 Hooks 中最核心的 2 个 api, useState
和 useEffect
useState 返回状态和一个更新状态的函数
1 | const [count, setCount] = useState(initialState) |
使用 Hooks 相比之前用 class 的写法最直观的感受是更为简洁
1 | function App() { |
在每次 render 后都会执行这个钩子。可以将它当成是 componentDidMount
、componentDidUpdate
、componentWillUnmount
的合集。因此使用 useEffect 比之前优越的地方在于:
componentDidMount、componentDidUpdate
书写重复的代码;useEffect
;(在以前得写进不同生命周期里);]]>在上述提到的生命周期钩子之外,其它的钩子是否在 hooks 也有对应的方案或者舍弃了其它生命周期钩子, 后续进行观望。
InversityJS 是一个 IoC 框架。IoC(Inversion of Control) 包括依赖注入(Dependency Injection) 和依赖查询(Dependency Lookup)。
相比于类继承的方式,控制反转解耦了父类和子类的联系。
1 | import 'reflect-metadata' |
上述案例可以抽象为下图:
虚线表示可以注入,但在代码中没有表现出来。
代码流程可概括如下:
1.将所有相关类(这里指 Music、popMusic、classicMusic) 通过 @injectable
声明进 container
容器;
2.通过 container.get()
获取 container.bind().to(target)
中的目标对象(这里指 Music);
3.如果目标对象中的 constructor() 里有 @inject()
, 则将相应的实例(这里指 PopMusic 与 classicalMusic 的实例)当作构造函数的参数’注入’;
inject 源码简化如下:
1 | // 这是一个属性装饰器 |
injectable 源码简化如下:
1 | // 这是一个类装饰器 |
从简化版源码中可以看到 inject/injectable 最终是对 Reflect.defineMetadata()
的一个使用。可以将 metadata 看成是一种相对高效的数据结构。
InversityJS 深度结合了 reflect-metadata, reflect-metadata 在 Reflect 基础上对其 api 进行了扩展。
metadata 本质上是一个
WeakMap
对象。扩展:Map 和 WeakMap 的区别
Reflect.defineMetadata(metadataKey, metadataValue, target[, propertyKey])
简化版实现如下:
1 | const Metadata = new WeakMap() |
Reflect.getOwnMetadata(metadataKey, target[, propertyKey])
简化版实现如下:
1 | function getOwnMetadata(metadataKey, target, propertyKey) { |
其数据结构可表示如下:
1 | WeakMap { |
阅读完本文可以了解到 0.1 + 0.2
为什么等于 0.30000000000000004
以及 JavaScript 中最大安全数是如何来的。
拿 173.8125 举例如何将之转化为二进制小数。
①. 针对整数部分 173,采取除 2 取余,逆序排列
;
1 | 173 / 2 = 86 ... 1 |
得整数部分的二进制为 10101101
。
②. 针对小数部分 0.8125,采用乘 2 取整,顺序排列
;
1 | 0.8125 * 2 = 1.625 | |
得小数部分的二进制为 1101
。
③. 将前面两部的结果相加,结果为 10101101.1101
;
根据上面的知识,将十进制小数 0.1
转为二进制:
1 | 0.1 * 2 = 0.2 |
可以发现有限十进制小数 0.1
却转化成了无限二进制小数 0.00011001100...
,可以看到精度在转化过程中丢失了!
能被转化为有限二进制小数的十进制小数的最后一位必然以 5 结尾(因为只有 0.5 * 2 才能变为整数)。所以十进制中一位小数 0.1 ~ 0.9
当中除了 0.5
之外的值在转化成二进制的过程中都丢失了精度。
在 JavaScript 中所有数值都以 IEEE-754 标准的 64 bit
双精度浮点数进行存储的。先来了解下 IEEE-754 标准下的双精度浮点数。
这幅图很关键,可以从图中看到 IEEE-754 标准下双精度浮点数由三部分组成,分别如下:
推荐阅读 JavaScript 浮点数陷阱及解法,阅读完该文后可以了解到以下公式的由来。
精度位总共是 53 bit,因为用科学计数法表示,所以首位固定的 1 就没有占用空间。即公式中 (M + 1) 里的 1。另外公式里的 1023 是 2^11 的一半。小于 1023 的用来表示小数,大于 1023 的用来表示整数。
指数可以控制到 2^1024 - 1,而精度最大只达到 2^53 - 1,两者相比可以得出 JavaScript 实际可以精确表示的数字其实很少。
0.1
转化为二进制为 0.0001100110011...
,用科学计数法表示为 1.100110011... x 2^(-4)
,根据上述公式,S
为 0
(1 bit),E
为 -4 + 1023
,对应的二进制为 01111111011
(11 bit),M
为 1001100110011001100110011001100110011001100110011010
(52 bit,另外注意末尾的进位),0.1
的存储示意图如下:
同理,0.2
转化为二进制为 0.001100110011...
,用科学计数法表示为 1.100110011... x 2^(-3)
,根据上述公式,E
为 -3 + 1023
,对应的二进制为 01111111100
, M
为 1001100110011001100110011001100110011001100110011010
, 0.2
的存储示意图如下:
0.1 + 0.2
即 2^(-4) x 1.1001100110011001100110011001100110011001100110011010 与 2^(-3) x 1.1001100110011001100110011001100110011001100110011010 之和
1 | // 计算过程 |
0.01001100110011001100110011001100110011001100110011001110
转化为十进制就是 0.30000000000000004
。验证完成!
根据双精度浮点数的构成,精度位数是 53 bit
。安全数的意思是在 -2^53 ~ 2^53
内的整数(不包括边界)与唯一的双精度浮点数互相对应。举个例子比较好理解:
1 | Math.pow(2, 53) === Math.pow(2, 53) + 1 // true |
Math.pow(2, 53)
竟然与 Math.pow(2, 53) + 1
相等!这是因为 Math.pow(2, 53) + 1 已经超过了尾数的精度限制(53 bit),在这个例子中 Math.pow(2, 53)
和 Math.pow(2, 53) + 1
对应了同一个双精度浮点数。所以 Math.pow(2, 53)
就不是安全数了。
最大的安全数为
Math.pow(2, 53) - 1
,即9007199254740991
。
本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/…) 项目地址
接上一章 HOC 探索 抛出的问题 ———— react 中的 onChange 事件和原生 DOM 事件中的 onchange 表现不一致,举例说明如下:
1 | // React 中的 onChange 事件 |
我们来看下 React 的一个 issue React Fire: Modernizing React DOM。有两点信息和这篇文章的话题相关。
从这两点内容我们可以得知下面的信息:
React 实现了一套合成事件机制,也就是它的事件机制和原生事件间会有不同。比如它目前 onChange 事件其实对应着原生事件中的 input 事件。在这个 issue 中明确了未来会使用 onInput 事件替代 onChange 事件,并且会大幅度地简化合成事件。
有了以上信息后,我们对 onChange 事件(将来的 onInput 事件)的代码作如下更改:
1 | function setAttribute(dom, attr, value) { |
区分自由组件以及受控组件在于表单的值是否由 value
这个属性控制,比较如下代码:
1 | const case1 = () => <input /> // 此时输入框内可以随意增减任意值 |
case3
的情形即为简化版的受控组件。
题目可以换个问法:当 input
的传入属性为 value
时(且没有 onChange 属性),如何禁用用户的输入事件的同时又能获取焦点?
首先想到了 html 自带属性 readonly、disable,它们都能禁止用户的输入,但是它们不能满足获取焦点这个条件。结合前文 onChange
的实现是监听 input
事件,代码分为以下两种情况:
1.dom 节点包含 value
属性、onChange
属性
2.dom 节点包含 value
属性,不包含 onChange
属性
代码如下:
1 | function vdomToDom(vdom) { |
可以发现它们的核心都在这段代码上:
1 | dom.addEventListener('input', (e) => { |
区别是当有 onChange 属性
时,能提供相应的回调函数 changeCb
通过事件循环机制改变表单的值。看如下两个例子的比较:
1 | const App = () => <input value={123} /> |
效果如下:
1 | class App extends Component { |
这段代码中的 change
函数即上个段落所谓的 changeCb
函数,通过 setState
的事件循环机制改变表单的值。
效果如下:
至此,模拟了受控组件的实现。
]]>本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/…) 项目地址
使用 PureComponent 是优化 React 性能的一种常用手段,相较于 Component, PureComponent 会在 render 之前自动执行一次 shouldComponentUpdate() 函数,根据返回的 bool 值判断是否进行 render。其中有个重点是 PureComponent 在 shouldComponentUpdate() 的时候会进行 shallowEqual(浅比较)。
PureComponent 的浅比较策略如下:
对 prevState/nextState 以及 prevProps/nextProps 这两组数据进行浅比较:
1.对象第一层数据未发生改变,render 方法不会触发;
2.对象第一层数据发生改变(包括第一层数据引用的改变),render 方法会触发;
照着上述思路我们来实现 PureComponent 的逻辑
1 | function PureComponent(props) { |
测试用例用 在 React 上提的一个 issue 中的案例,我们期望点击增加按钮后,页面上显示的值能够加 1。
1 | class B extends PureComponent { |
然而,我们点击上述代码,页面上显示的 0 分毫不动!!!
揭秘如下:
1 | click() { |
当点击增加按钮,控制台显示 t === this.state.count
为 true, 也就说明了 setState 前后的状态是统一的,所以 shallowEqual(浅比较) 返回的是 true,致使 shouldComponentUpdate 返回了 false,页面因此没有渲染。
类似的,如下写法也是达不到目标的,留给读者思考了。
1 | click() { |
那么如何达到我们期望的目标呢。揭秘如下:
1 | click() { |
感悟:小小的一行代码里蕴藏着无数的 bug。
高阶组件(Higher Order Component) 不属于 React API 范畴,但是它在 React 中也是一种实用的技术,它可以将常见任务抽象成一个可重用的部分
。这个小节算是番外篇,会结合 cpreact(前文实现的类 react 轮子) 与 HOC 进行相关的实践。
它可以用如下公式表示:
1 | y = f(x), |
f()
的实现有两种方法,下面进行实践。
这类实现也是装饰器模式的一种运用,通过装饰器函数给原来函数赋能。下面例子在装饰器函数中给被装饰的组件传递了额外的属性 { a: 1, b: 2 }。
声明:下文所展示的 demo 均已在 cpreact 测试通过
1 | function ppHOC(WrappedComponent) { |
要是将 { a: 1, b: 2 } 替换成全局共享对象,那么不就是 react-redux 中的 Connect 了么?
改进上述 demo,我们就可以实现可插拔的受控组件,代码示意如下:
1 | function ppDecorate(WrappedComponent) { |
效果如下图:
这里有个坑点,当我们在输入框输入字符的时候,并不会立马触发 onChange 事件(我们想要让事件立即触发,然而现在要按下回车键或者点下鼠标才触发),在 react 中有个合成事件 的知识点,下篇文章会进行探究。
顺带一提在这个 demo 中似乎看到了双向绑定的效果,但是实际中 React 并没有双向绑定的概念,但是我们可以运用 HOC 的知识点结合 setState 在 React 表单中实现伪双向绑定的效果。
继承反转的核心是:传入 HOC 的组件会作为返回类的父类来使用。然后在 render 中调用 super.render()
来调用父类的 render 方法。
在 《ES6 继承与 ES5 继承的差异》中我们提到了作为对象使用的 super 指向父类的实例。
1 | function iiHOC(WrappedComponent) { |
在这个 demo 中,在 HOC 内实现了渲染劫持,页面上最终显示如下:
可能会有疑惑,使用
属性代理
的方式貌似也能实现渲染劫持呀,但是那样做没有继承反转
这种方式纯粹。
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react
数据结构在开发中是一种编程思想的提炼,无关于用何种语言开发或者是哪种端开发。下列将笔者涉猎到的与前端相关的数据结构案例作如下总结:
数据结构 | 案例 |
---|---|
栈 | FILO: 其它数据结构的基础,redux/koa2 中间件机制 |
队列 | FIFO:其它数据结构的基础 |
链表 | React 16 中的 Fiber 的优化 |
集合 | 对应 JavaScript 中的 Set |
字典 | 对应 JavaScript 中的 Map |
哈希表 | 一种特殊的字典,可以用来存储加密数据 |
树 | DOM TREE / HTML TREE / CSS TREE |
图 | 暂时没遇到,不过里面的 BFS/DFS 蛮常见 |
本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/…) 项目地址
而在现有 setState 逻辑实现中,每调用一次 setState 就会执行 render 一次。因此在如下代码中,每次点击增加按钮,因为 click 方法里调用了 10 次 setState 函数,页面也会被渲染 10 次。而我们希望的是每点击一次增加按钮只执行 render 函数一次。
1 | export default class B extends Component { |
查阅 setState 的 api,其形式如下:
1 | setState(updater, [callback]) |
它能接收两个参数,其中第一个参数 updater 可以为对象或者为函数 ((prevState, props) => stateChange
),第二个参数为回调函数;
确定优化思路为:将多次 setState 后跟着的值进行浅合并,并借助事件循环等所有值合并好之后再进行渲染界面。
1 | let componentArr = [] |
此时,每点击一次增加按钮 render 函数只执行一次了。
在 react 中并不建议使用 ref 属性,而应该尽量使用状态提升,但是 react 还是提供了 ref 属性赋予了开发者操作 dom 的能力,react 的 ref 有 string
、callback
、createRef
三种形式,分别如下:
1 | // string 这种写法未来会被抛弃 |
React ref 的前世今生 罗列了三种写法的差异,下面对上述例子中的第二种写法(比较通用)进行实现。
首先在 setAttribute 方法内补充上对 ref 的属性进行特殊处理,
1 | function setAttribute(dom, attr, value) { |
针对这个例子中 this.myRef.focus()
的 focus 属性需要异步处理,因为调用 componentDidMount 的时候,界面上还未添加 dom 元素。处理 renderComponent 函数:
1 | function renderComponent(component) { |
刷新页面,可以发现 input 框已为选中状态。
处理完普通元素的 ref 后,再来处理下自定义组件的 ref 的情况。之前默认自定义组件上是没属性的,现在只要针对自定义组件的 ref 属性做相应处理即可。稍微修改 vdomToDom 函数如下:
1 | function vdomToDom(vdom) { |
跑如下测试用例:
1 | class A extends Component { |
效果如下:
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react
]]>本系列文章在实现一个 cpreact 的同时帮助大家理顺 React 框架的核心内容(JSX/虚拟DOM/组件/生命周期/diff算法/setState/PureComponent/HOC/…) 项目地址
先来回顾 React 的生命周期,用流程图表示如下:
该流程图比较清晰地呈现了 react 的生命周期。其分为 3 个阶段 —— 生成期,存在期,销毁期。
因为生命周期钩子函数存在于自定义组件中,将之前 _render 函数作些调整如下:
1 | // 原来的 _render 函数,为了将职责拆分得更细,将 virtual dom 转为 real dom 的函数单独抽离出来 |
我们可以在 setProps 函数内(渲染前)加入 componentWillMount
,componentWillReceiveProps
方法,setProps 函数如下:
1 | function setProps(component) { |
而后我们在 renderComponent 函数内加入 componentDidMount
、shouldComponentUpdate
、componentWillUpdate
、componentDidUpdate
方法
1 | function renderComponent(component) { |
测试如下用例:
1 | class A extends Component { |
页面加载时输出结果如下:
1 | componentWillMount |
点击按钮时输出结果如下:
1 | shouldComponentUpdate |
在 react 中,diff 实现的思路是将新老 virtual dom 进行比较,将比较后的 patch(补丁)渲染到页面上,从而实现局部刷新;本文借鉴了 preact 和 simple-react 中的 diff 实现,总体思路是将旧的 dom 节点和新的 virtual dom 节点进行了比较,根据不同的比较类型(文本节点、非文本节点、自定义组件)调用相应的逻辑,从而实现页面的局部渲染。代码总体结构如下:
1 | /** |
下面根据不同比较类型实现相应逻辑。
首先进行较为简单的文本节点的比较,代码如下:
1 | // 对比文本节点 |
对比非文本节点,其思路为将同层级的旧节点替换为新节点,代码如下:
1 | // 对比非文本节点 |
对比自定义组件的思路为:如果新老组件不同,则直接将新组件替换老组件;如果新老组件相同,则将新组件的 props 赋到老组件上,然后再对获得新 props 前后的老组件做 diff 比较。代码如下:
1 | // 对比自定义组件 |
遍历对比子节点的策略有两个:一是只比较同层级的节点,二是给节点加上 key 属性。它们的目的都是降低空间复杂度。代码如下:
1 | // 对比子节点 |
在生命周期的小节中,componentWillReceiveProps 方法还未跑通,稍加修改 setProps 函数即可:
1 | /** |
来测试下生命周期小节中最后的测试用例:
Especially thank simple-react for the guidance function of this library. At the meantime,respect for preact and react
]]>