You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
functionpureCheckoutBook(book){letcopy={ ...book}// this change will only affect the copycopy.isCheckedOut=true// gotta return it, otherwise the change will be lostreturncopy}letbook={title: "Tiny Habits",author: "BJ Fogg",isCheckedOut: false}// This function returns a new book,// instead of modifying the existing one,// so replace `book` with the new checked-out onebook=pureCheckoutBook(book);
关于引用的一些小例子
第一个例子:
document.addEventListener('click',()=>console.log('clicked'));document.removeEventListener('click',()=>console.log('clicked'));// won't work
shallowCompare performs a shallow equality check on the current props and nextProps objects as well as the current state and nextState objects.
It does this by iterating on the keys of the objects being compared and returning true when the values of a key in each object are not strictly equal.
React 就提供一些 API 基于 shallowEuqal 的特性进行性能优化, 比如比较常见的一个 React.memo. 官网的介绍也是非常清晰了, 这里直接贴一下:
If your component renders the same result given the same props, you can wrap it in a call to React.memo for a performance boost in some cases by memoizing the result. This means that React will skip rendering the component, and reuse the last rendered result.
React.memo only checks for prop changes. If your function component wrapped in React.memo has a useState, useReducer or useContext Hook in its implementation, it will still rerender when state or context change.
By default it will only shallowly compare complex objects in the props object. If you want control over the comparison, you can also provide a custom comparison function as the second argument.
// If you want to construct a single object with multiple state-picks inside, similar to redux's mapStateToProps, you can tell zustand that you want the object to be diffed shallowly by passing the shallow equality function.importshallowfrom'zustand/shallow'// Object pick, re-renders the component when either state.nuts or state.honey changeconst{ nuts, honey }=useStore(state=>({nuts: state.nuts,honey: state.honey}),shallow)// Array pick, re-renders the component when either state.nuts or state.honey changeconst[nuts,honey]=useStore(state=>[state.nuts,state.honey],shallow)// Mapped picks, re-renders the component when state.treats changes in order, count or keysconsttreats=useStore(state=>Object.keys(state.treats),shallow)// For more control over re-rendering, you may provide any custom equality function.consttreats=useStore(state=>state.treats,(oldTreats,newTreats)=>compare(oldTreats,newTreats))
老生常谈的问题了, 之前看到一篇还挺有趣的文章, 记录一下
什么是引用
考虑如下代码:
我们可以说变量
word
指向(pints to) 一个盒子, 该盒子里装着"hello"
. 注意word
变量不是盒子, 而是指向盒子.现在给 word 重新赋值
这段代码的解释可以为, 现在有一个新的盒子被创建了, 盒子里装着
"world"
,word
变量现在指向这个新的装有"world"
盒子, 而之前装有"hello"
的盒子被garbage collector
清理掉了, 因为没有地方用到他.用图表示的话如下:
引申一下, 如果尝试过给函数参数重新进行赋值, 你会发现他并不能改变函数外的任何值.
简单来讲, 即如果给函数形式参数重新赋值, 那对外部传递的实际参数没有任何影响. 比如以下代码:
上面代码的解释为:
test
变量指向"hello"
reassignFail
函数开始传递实参的时候,test
和word
均指向相同的盒子, 即"hello"
word
指向了新的盒子"world"
, 但该操作对外面的test
变量没有任何影响,test
变量仍旧指向"hello"
盒子在 JavaScript 中变量的赋值可以看做是这么解释的. 当重新给一个变量赋值的时候, 他不会改变别的也指向之前同样"盒子"的变量, 他只改变自己对"盒子"的指向, 且不管这个盒子里装的是什么, boolean, number, object, array, function, 该操作均是这样进行的
两种类型
JavaScript 有两种广泛的类型分类, 他们对于赋值(assignment) 和 referential equality(引用) 有不同的规则
Primitive Type 原始类型
原始类型包括
string
,number
,boolean
,symbol
,undefined
,null
, 这些类型的数据是 immutable 的, 即只可读, 不可被改变(read-only, can’t be changed)当一个变量持有原始类型的数据时, 你不能改变数据本身, 换句话说, 如果盒子里装的是原始类型, 你不能改变盒子里的东西, 你能做的只能创建一个新的盒子, 然后将变量指向这个新的盒子, 就如同之前的图所展示的:
而不是以下所示图这样:
同样的, 举个例子, 所有
string
的方法总是返回一个新的string
而不是改变string
本身, 如果你想要获取新的string
, 你就必须用一个变量指向他把他保存起来, 因为旧的值永远不会发生改变, 例如以下代码片段:Object Type
第二种类型是对象类型, 包括常见的
Object
,Array
,Function
,Map
和Set
. 和原始类型不同的是这些类型是 mutable 的, 形象点说你可以改变盒子里的东西Immutable is Predictable
Immutable 是可控的. 就像之前所说, 如果将一个原始类型作为参数传递给一个函数, 那么可以保证指向这个原始类型的变量是"安全的". 因为调用这个函数永远不会改变到这个变量所对应的值. 即盒子还是原来的盒子, 盒子里的东西永远没变.
但是对于对象类型, 这就存在隐患. 如果将一个对象传递给一个函数, 那么调用这个函数后该对象的值可能会发生改变. 比如如果这个对象是一个数组, 函数内部可以对这个数组进行增加元素或者删除元素操作. 虽然说对于外部一开始指向对象的变量引用没变, 但是由于对象是 mutable 的, 盒子里的值还是变了. 即盒子还是原来的盒子, 但是盒子里的东西最后有可能发生变化...
一个不会改变参数, 或者任何外部的东西的函数可以认为他是一个纯函数. 如果需要改变参数某个值, 那么会选择返回一个新的变量存着新的值
简单回顾: 变量指向盒子, 原始类型是 immutable 的
不管是原始类型还是对象类型, 在进行赋值操作的时候, 我们都认为创建了一个新的盒子, 然后将变量指向那个盒子, 这种操作永远都是成立的, 不管是发生在第一次赋值(assignment)还是后面的重新赋值(reassignment)
原始类型是不可变的, 你没有办法改变盒子里的东西, 你只能重新创建一个盒子有着新的值, 然后让变量指向这个新的盒子.
对象类型: 改变盒子里的内容
假设有这样一个对象
book
:如果对这个对象的某个属性进行修改操作:
虽然
book
这个变量指向没变, 一直指向的盒子是同一个盒子(同一个对象). 但是盒子里的内容还是变了, 因为对象的属性变了.如图所示:
注意这种赋值和之前的规则是一样的, 不同于我们直接使用变量
isCheckedOut
, 这次使用book.isCheckedOut
来代表一个变量, 重新赋值的时候指向新的盒子需要注意的是,
book
这个对象的引用一直没变, 即盒子还是原来的盒子. 如果我们尝试用另一个变量指向这个盒子, 会发现当book
内的属性改变时, 另一个变量也会跟随变化, 因为都是指向同一个对象. 例如:这种操作不是拷贝, 因为没有制造一个新的对象, 只是单纯的让两个变量指向同一个对象地址, 所以最后
console.log
返回的是true
需要注意的是, 变量永远只会指向盒子, 不存在变量指向变量的情况. 比如这个例子里, 当进行
backup = book
操作的时候, JS 会查看book
指向的盒子, 然后让backup
也指向相同的盒子, 不存在backup
指向book
这种说法. 这就很赞, 因为就不会被谁指向谁再指向谁一层一层绕晕了...在函数中 Mutate(修改) 一个对象
之前提到在函数中直接完全修改形式参数是不会影响到实际参数的(换盒子地址). 但是如果改变的是盒子内部的属性, 那么会影响到函数外边对该盒子的变量. 因为形式参数和实际参数都指向同一个盒子. 比如这个例子:
用图来表示和之前的一样:
如果想要阻止这种情况发生, 比较好的做法是对传来的对象做一份浅拷贝, 创造一个新盒子, 新盒子里的属性和旧盒子完全一样. 这样改变新盒子的内容就不会影响到旧盒子了
关于引用的一些小例子
第一个例子:
addEventListener
和removeEventListener
第二个参数必须是一样的引用才能保证该回调函数被成功添加/移除到click
事件上, 上面的例子相当于创建了两个匿名的盒子, 引用当然完全不一样比如可以试一下:
最后结果一定是
false
. 所有的对象(array, function, set, map, etc.) 创建的时候都存在于自己的盒子里第二个例子:
sort()
会改变array
本身. 不注意的话有时候就会造成意想不到的结果...Strict Equal 和 Shallow Equal
两个概念其实经常出现在 React 以及一些衍生库里, 比如一些 APi 会提到默认使用
Strict Equal
进行比较, 如果想要自定义比较过程需要手动写 equal function.Strict Equal
先说 strict equal. 中文翻译过来就是严格比较. 官方也有相关文档. 简单来说如果是原始类型, 那直接比较值就行, 比如
true === true
,1 === 1
,null === null
. 如果比喻成盒子的话, 比较的时候会具体到盒子里存的内容, 比如这样:如果是两个对象进行严格比较, 那么比较的是他们两个地址一不一样, 即两个对象是不是同一个对象, 或者说他们指向的是不是同一个盒子.
比如最简单的:
Shallow Equal
然后是 shallow equal, 浅比较. React 官方有关于这个概念简单的定义:
简单来讲, 就是对一个对象的所有属性进行严格比较, 注意是只有第一层的属性. 所以如果第一层属性对应的值是一个对象的话, 比较结果可能是
false
, 因为两个对象的引用根本不一样. 举一些例子:一些应用
React
React 就提供一些 API 基于
shallowEuqal
的特性进行性能优化, 比如比较常见的一个React.memo
. 官网的介绍也是非常清晰了, 这里直接贴一下:翻译过来就这么几个点:
React.memo
进行优化, 优化方式是这种情况下会跳过重复渲染, 直接采用上次渲染的结果(毕竟 render 结果都是一样的)React.memo
只考虑 props 变化, 如果 state 有变化即使 props 一样组件依旧会被渲染.shallowEqual
对 props 比较, 也就是对 props 这个对象进行浅比较. 如果 props 这个对象是一个很复杂的对象, 比如包含很多深层次的属性, 那可以自定义比较函数说了这么多, 直接看一个例子: https://stackblitz.com/edit/react-wvppng-tfoayr
先看效果:
代码如下:
App
组件里由于有一个输入框, 输入框有内容输入的时候会不断触发setState
继而App
会不断被渲染. 而由于 React 的默认行为是当父组件被渲染时, 子组件也会被渲染(不考虑 bail out 情况). 所以Child
和MemorizedChild
也理应要被渲染. 这里可以注意, 两个子组件的 props 一直都是一样的都是{ count: 0 }
. 为了避免这种无必要重复渲染, 使用React.memo
包裹后的MemorizedChild
就不会被重复渲染了.Redux
Redux 提供的 useSelector 默认的比较行为是严格比较(===), 如果想要通过返回一个对象或者数组拿多个状态的话很有可能造成没必要的重复渲染, 因为可能只是某个子状态发生了改变, 但由于进行的是严格比较, 整个对象的引用都变了, 会导致组件也会被渲染. 一般没有特殊需求的话使用
shallowEqual
就行. 如下:或者直接自定义为一个 hook:
另一个类似的库 zustand 也是提倡类似的概念, 官方文档也更加简洁清晰, 如下:
用这类库的时候有时候多写一行也不是什么坏事, 保证每次获取的状态都是独立, 这样也不用去考虑如何写
compareFn
, 理论上也不会导致不必要的渲染参考
The text was updated successfully, but these errors were encountered: