Code Monkey home page Code Monkey logo

blog's Introduction

hello,我是落落落洛克

交流学习联系:vnues123(wechat)

blog's People

Contributors

vnues avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

Typescript三斜线指令的使用场景

在全局变量的声明文件中,我们不能使用import export 不然就会被认为是默认 那么在全局变量的声明文件中需要引入对应的依赖的时候 如何解决呢 这时候就需要三斜线命令了

  • 当我们在书写一个全局变量的声明文件时
  • 当我们需要依赖一个全局变量的声明文件时

早期没有import export模块化

都是declare+namespace

image

再用三斜线导入依赖

但是现在 namespace 类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。
但是在声明文件中,它还是有一定的用武之地。

也就是你如果想用三斜线命令导入 该导入的文件就不能具有import export 直接用import直接导入依赖吧

image

go mod命令

download download modules to local cache (下载依赖的module到本地cache))
edit edit go.mod from tools or scripts (编辑go.mod文件)
graph print module requirement graph (打印模块依赖图))
init initialize new module in current directory (再当前文件夹下初始化一个新的module, 创建go.mod文件))
tidy add missing and remove unused modules (增加丢失的module,去掉未用的module)
vendor make vendored copy of dependencies (将依赖复制到vendor下)
verify verify dependencies have expected content (校验依赖)
why explain why packages or modules are needed (解释为什么需要依赖)

5个实用的CSS技巧

:where() 伪类函数

image
上面的代码可以用:where() 伪类函数优化为

image

Conic gradients(圆锥渐变)函数

image

image

Scroll Snap

CSS Scroll Snap是CSS中一个独立的模块,可以让网页容器滚动停止的时候,自动平滑定位到指定元素的指定位置,包含scroll-*以及scroll-snap-*等诸多CSS属性。

image

image

aspect ratio

image

aspect-ratio CSS 属性为box容器规定了一个期待的纵横比,这个纵横比可以用来计算自动尺寸以及为其他布局函数服务。

CSS aspect-ratio属性可以明确元素的高宽比例,日后一定是一个高频使用的CSS属性。

在过去,想要让元素等比例缩放,两种方式:

百分比padding,详见:“CSS百分比padding实现比例固定图片自适应布局”
vw单位,例如:

.box {
    width: 50vw; height: 15vw;
}

但上面这些方法使用的时候均有局限性。

现在有了aspect-ratio属性,开发者对于元素比例的控制就非常容易实现了。

目前Chrome 88已经支持了aspect-ratio属性,各大浏览器大规模支持只是时间问题,我的Chrome现在版本正好是88,可以体验效果了,于是赶快尝鲜,了解下相关的细节。

aspect-ratio的兼容性
image

padding实现图片等比例自适应

对于绝大多数都布局,我们并不要求非要比例固定,但是有一种情况例外,那就是图片,因为图片原始尺寸它是固定的。在传统的固定宽度的布局下,我们会通过给图片设定具体的宽度和高度值,来保证我们的图片占据区域稳固;但是在移动端或者在响应式开发情况下,图片最终展现的宽度很可能是不确定的,例如手机端的一个通栏广告,iPhone7下宽度是375,iPhone7 Plus下是414,还有360等尺寸,此时需要的不是对图片进行固定尺寸设定,而是比例设定`。

banner图可能是按照比例设置的,这时候如何按照比例固定图片呢,利用padding来做,因为padding的百分比是参照宽度的

Scss variables and Mixins

mixin可以让你制作一些你想在整个网站上重复使用的CSS声明组。你甚至可以传入数值,使你的混合器更加灵活。

image

image

参考文献

什么是Server Component?

解决什么问题

image

Dan 开门见山,丢出了我们业务开发中需要权衡三个点:体验(user experience)、可维护性(maintenance)、性能(performance),然后用一个例子来说明为什么这三个点很难权衡。

image

这是一个很常见的组件化组合,问题在于每个组件都需要不同的数据,但是就体验而言我们更希望这些组件的渲染尽量同时,而且如果关注性能的话,我们也会考虑并行的去 fetch 数据,于是我们通常会 fetch 逻辑放到顶层,然后通过 Props 或者 Context 传递下去。

image
这样会把可维护性变差,除了看起来恶心,每个组件从逻辑上就不那么解耦了,我们于是会考虑每个组件自己处理 fetch 逻辑。
image

这又会让体验变差,因为浏览器从服务端 fetch 数据是比较贵的 IO,抽象一下就是下面这样:
image

我们之所以需要从服务端 fetch 数据,是因为我们把所有渲染操作放到了客户端,那如果我们把部分渲染逻辑放服务端呢?

image

于是就有了 React Server Components!

总结:Server Component解决的痛点就是

Server Component解决的痛点就是项目存在瀑布流请求,导致用户体验差,如果我们把组件放在服务端执行,数据请求会非常快

Server Component一些注意点

image
image

  • 容器组件才能在服务端执行(Server端并不能处理交互,交互还是得交给客户端处理),有交互和State状态的组件只能跑在客户端

  • Server Component传递到客户端组件的数据,是可以经过序列化的(用于网络传输)(
    比如已经转换好后的jsx)

  • Server Component是0 bundle,打包的时候不会被引入到客户端
    本地可以看到没有Server端的文件

image

  • 与SSR的区别,可以保持state状态,之所以可以实现这种,因为返回的不是HTML,而是序列化的“指令”

image

  • Server Component和Suspense是互补的

6.Server Component和Suspense是互补的,Server Component是让组件在服务端运行,这样数据请求非常快,Suspense是局部水合,可以形成互补

疑问点

image

我总感觉首次渲染后,后面只进行数据请求,不涉及序列化的“指令”(HTML的生成那样),应该是由客户端客户端进行接管,岂不是更好(比如我进行搜索)

接口返回的序列化的“指令”
这些数据我感觉挺大的啊

M1:{"id":"./src/SearchField.client.js","chunks":["client5"],"name":""}
M2:{"id":"./src/EditButton.client.js","chunks":["client1"],"name":""}
S3:"react.suspense"
J0:["$","div",null,{"className":"main","children":[["$","section",null,{"className":"col sidebar","children":[["$","section",null,{"className":"sidebar-header","children":[["$","img",null,{"className":"logo","src":"logo.svg","width":"22px","height":"20px","alt":"","role":"presentation"}],["$","strong",null,{"children":"React Notes"}]]}],["$","section",null,{"className":"sidebar-menu","role":"menubar","children":[["$","@1",null,{}],["$","@2",null,{"noteId":null,"children":"New"}]]}],["$","nav",null,{"children":["$","$3",null,{"fallback":["$","div",null,{"children":["$","ul",null,{"className":"notes-list skeleton-container","children":[["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}],["$","li",null,{"className":"v-stack","children":["$","div",null,{"className":"sidebar-note-list-item skeleton","style":{"height":"5em"}}]}]]}]}],"children":"@4"}]}]]}],["$","section","4",{"className":"col note-viewer","children":["$","$3",null,{"fallback":["$","div",null,{"className":"note skeleton-container","role":"progressbar","aria-busy":"true","children":[["$","div",null,{"className":"note-header","children":[["$","div",null,{"className":"note-title skeleton","style":{"height":"3rem","width":"65%","marginInline":"12px 1em"}}],["$","div",null,{"className":"skeleton skeleton--button","style":{"width":"8em","height":"2.5em"}}]]}],["$","div",null,{"className":"note-preview","children":[["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}],["$","div",null,{"className":"skeleton v-stack","style":{"height":"1.5em"}}]]}]]}],"children":"@5"}]}]]}]
J4:["$","div",null,{"className":"notes-empty","children":["Couldn't find any notes titled \"1\"."," "]}]
J5:["$","div",null,{"className":"note","children":[["$","div",null,{"className":"note-header","children":[["$","h1",null,{"className":"note-title","children":"react"}],["$","div",null,{"className":"note-menu","role":"menubar","children":[["$","small",null,{"className":"note-updated-at","role":"status","children":["Last updated on ","9 Sep 2021 at 11:15 AM"]}],["$","@2",null,{"noteId":4,"children":"Edit"}]]}]]}],["$","div",null,{"className":"note-preview","children":["$","div",null,{"className":"text-with-markdown","dangerouslySetInnerHTML":{"__html":"<p>hello React Component</p>\n"}}]}]]}]

参考文献

babel automation with app.js

when i run the command "babel src/app.js --out-file=public/scripts/app.js --presets=env,react" in the application i am trying to code so that babel compiles it using reaadable code in the app.js located in src and scripts folder i always get an error """
"""Error: Cannot find package 'babel-preset-env' imported from /Users/hammoud/Desktop/react/indecision-app/babel-virtual-resolve-base.js
at new NodeError (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/vendor/import-meta-resolve.js:203:5)
at packageResolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/vendor/import-meta-resolve.js:872:9)
at moduleResolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/vendor/import-meta-resolve.js:901:20)
at defaultResolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/vendor/import-meta-resolve.js:984:15)
at resolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/vendor/import-meta-resolve.js:998:12)
at resolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/config/files/import-meta-resolve.js:13:10)
at tryImportMetaResolve (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/config/files/plugins.js:123:45)
at resolveStandardizedNameForImport (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/config/files/plugins.js:145:19)
at resolveStandardizedName (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/config/files/plugins.js:154:12)
at loadPreset (/usr/local/Cellar/babel/7.21.8/libexec/lib/node_modules/@babel/cli/node_modules/@babel/core/lib/config/files/plugins.js:56:20) {
code: 'ERR_MODULE_NOT_FOUND'
}
"""
please may have any help solving this error

读《深入浅出搞定 React》

JSX

JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。

image

React 16 要更改组件的生命周期

当组件更新时,会再次通过调用 render 方法生成新的虚拟 DOM,然后借助 diff定位出两次虚拟 DOM 的差异,从而针对发生变化的真实 DOM 作定向更新。

image

  • componentWillMount 会在执行 render 方法前被触发,一些同学习惯在这个方法里做一些初始化的操作,但这些操作往往会伴随一些风险或者说不必要性(这一点大家先建立认知,具体原因将在“03 课时”展开讲解)。

  • 组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新。这两种更新对应的生命周期流程如下图所示:

image

componentReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的,这个结论,请你谨记。

React 16 生命周期工作流详解

image

image

  • 而 getDerivedStateFromProps 这个 API,其设计的初衷不是试图替换掉 componentWillMount,而是试图替换掉 componentWillReceiveProps,因此它有且仅有一个用途:使用 props 来派生/更新 state。

static getDerivedStateFromProps(props, state)

值得一提的是,getDerivedStateFromProps 在更新和挂载两个阶段都会“出镜”(这点不同于仅在更新阶段出现的 componentWillReceiveProps)。这是因为“派生 state”这种诉求不仅在 props 更新时存在,在 props 初始化的时候也是存在的。React 16 以提供特定生命周期的形式,对这类诉求提供了更直接的支持

getDerivedStateFromProps 的返回值之所以不可或缺,是因为 React 需要用这个返回值来更新(派生)组件的 state。因此当你确实不存在“使用 props 派生 state ”这个需求的时候,最好是直接省略掉这个生命周期方法的编写,否则一定记得给它 return 一个 null。

React 16.4 的挂载和卸载流程都是与 React 16.3 保持一致的,差异在于更新流程上:

在 React 16.4 中,任何因素触发的组件更新流程(包括由 this.setState 和 forceUpdate 触发的更新流程)都会触发 getDerivedStateFromProps;

而在 v 16.3 版本时,只有父组件的更新会触发该生命周期。

到这里,你已经对 getDerivedStateFromProps 相关的改变有了充分的了解。接下来,我们就基于这层了解,问出生命周期改变背后的第一个“Why”。

改变背后的第一个“Why”:为什么要用 getDerivedStateFromProps 代替 componentWillReceiveProps?
对于 getDerivedStateFromProps 这个 API,React 官方曾经给出过这样的描述:

与 componentDidUpdate 一起,这个新的生命周期涵盖过时componentWillReceiveProps 的所有用例。

在这里,请你细细品味这句话,这句话里蕴含了下面两个关键信息:

getDerivedStateFromProps 是作为一个试图代替 componentWillReceiveProps 的 API 而出现的;

getDerivedStateFromProps不能完全和 componentWillReceiveProps 画等号,其特性决定了我们曾经在 componentWillReceiveProps 里面做的事情,不能够百分百迁移到getDerivedStateFromProps 里。

接下来我们就展开说说这两点。

关于 getDerivedStateFromProps 是如何代替componentWillReceiveProps 的,在“挂载”环节已经讨论过:getDerivedStateFromProps 可以代替 componentWillReceiveProps 实现基于 props 派生 state。

至于它为何不能完全和 componentWillReceiveProps 画等号,则是因为它过于“专注”了。这一点,单单从getDerivedStateFromProps 这个 API 名字上也能够略窥一二。原则上来说,它能做且只能做这一件事。

  • 消失的 componentWillUpdate 与新增的 getSnapshotBeforeUpdate

细说生命周期“废旧立新”背后的思考
在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。

Diff算法

经常做算法题的人都知道,OJ 中相对理想的时间复杂度一般是 O(1) 或 O(n)。当复杂度攀升至 O(n2) 时,我们就会本能地寻求性能优化的手段,更不必说是人神共愤的 O(n3) 了!我们看不下去,React 自然也看不下去。React 团队结合设计层面的一些推导,总结了以下两个规律, 为将 O (n3) 复杂度转换成 O (n) 复杂度确立了大前提:

若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树形结构;

处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性。

除了这两个“板上钉钉”的规律之外,还有一个和实践结合比较紧密的规律,它为 React 实现高效的 Diff 提供了灵感:DOM 节点之间的跨层级操作并不多,同层级操作是主流。

setState

setState 并不是单纯同步/异步的,它的表现会因调用场景的不同而不同:在 React 钩子函数及合成事件中,它表现为异步;而在 setTimeout、setInterval 等函数中,包括在 DOM 原生事件中,它都表现为同步。这种差异,本质上是由 React 事务机制和批量更新机制的工作方式来决定的。

“同步现象”的本质
下面结合对事务机制的理解,我们继续来看在 ReactDefaultBatchingStrategy 这个对象。ReactDefaultBatchingStrategy 其实就是一个批量更新策略事务,它的 wrapper 有两个:FLUSH_BATCHED_UPDATES 和 RESET_BATCHED_UPDATES。

话说到这里,一切都变得明朗了起来:isBatchingUpdates 这个变量,在 React 的生命周期函数以及合成事件执行前,已经被 React 悄悄修改为了 true,这时我们所做的 setState 操作自然不会立即生效。当函数执行完毕后,事务的 close 方法会再把 isBatchingUpdates 改为 false。

以开头示例中的 increment 方法为例,整个过程像是这样:

reduce = () => {

  // 进来先锁上

  isBatchingUpdates = true

  setTimeout(() => {

    console.log('reduce setState前的count', this.state.count)

    this.setState({

      count: this.state.count - 1

    });

    console.log('reduce setState后的count', this.state.count)

  },0);

  // 执行完函数再放开

  isBatchingUpdates = false

}

带着这个结论,我们再来看看 React 16 打算废弃的是哪些生命周期:

componentWillMount;

componentWillUpdate;

componentWillReceiveProps。

image

我们先来看下三个阶段各自有哪些特征(以下特征翻译自上图)。

render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。

pre-commit 阶段:可以读取 DOM。

commit 阶段:可以使用 DOM,运行副作用,安排更新。

总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。

React Hook

因为虽然 props 本身是不可变的,但 this 却是可变的,this 上的数据是可以被修改的,this.props 的调用每次都会获取最新的 props,而这正是 React 确保数据实时性的一个重要手段。

多数情况下,在 React 生命周期对执行顺序的调控下,this.props 和 this.state 的变化都能够和预期中的渲染动作保持一致。但在这个案例中,我们通过 setTimeout 将预期中的渲染推迟了 3s,打破了 this.props 和渲染动作之间的这种时机上的关联,进而导致渲染时捕获到的是一个错误的、修改后的 this.props。这就是问题的所在。

  • 两个钩子的区别在于,useEffect 是异步的,要等到浏览器将所有变化渲染到屏幕后才会被执行;而useLayoutEffect 是同步的——这是执行时机上的区别,这块你应该是理解的。问题在于:1.React官方真的不建议使用 useEffect 操作 DOM 吗?我在官方网站上找到了相反的描述:“尽可能使用标准的 useEffect 以避免阻塞视觉更新(出自 https://zh-hans.reactjs.org/docs/hooks-reference.html#uselayouteffect)”。 2. useEffect 会造成视觉阻塞吗?恰恰相反,因为 useLayoutEffect 是同步渲染的机制,而 useEffect 是异步非阻塞的渲染,所以说阻塞渲染的恰恰是 useLayoutEffect 而不是 useEffect。如果你有一段逻辑确实存在“阻塞渲染”这个同步的需求,那么可以使用 useLayoutEffect。否则就应该像 React 官网原文所说的那样,“尽可能使用标准的 useEffect 以避免阻塞视觉更新”

requestIdleCallback

Fiber 架构核心:“可中断”“可恢复”与“优先级”

diff算法跟新DOM逻辑
Vue基于snabbdom库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边对比,边更新DOM。

React主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM

setState --> 得到 vdom树(基于pull或者push的模式不同)

margin/padding百分比值的计算

问题

场景:保持 item 元素宽高比恒定(如高是宽的 1.618 倍)的情况下,使得 item 元素可以和父元素同比缩放

<!DOCTYPE html>
<html lang="en">
	<head>
		<meta charset="UTF-8" />
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>Document</title>
	</head>
	<body>
		<style>
			/* 
			本质:
			首先需要知道,一个元素的 padding,如果值是一个百分比,
			那这个百分比是相对于其父元素的宽度而言的,
			即使对于 padding-bottom 和 padding-top 也是如此。 */
			/* box 用来控制宽度 
			*/
			.box {
				width: 80%;
			}
			/* scale 用来实现宽高等比例 */
			.scale {
				width: 100%;
				/* 
				怎么算的 9/16
				相对于box这个盒子来说的
			    */
				padding-bottom: 56.25%;
				height: 0;
				position: relative;
			}
			/* item 用来放置全部的子元素 */
			.item {
				width: 100%;
				height: 100%;
				background-color: aquamarine;
				position: absolute;
			}
		</style>
		<!-- CSS实现父元素不知道宽高,子元素固定宽高比(比如16:9) -->
		<div class="box">
			<div class="scale">
				<div class="item">item</div>
			</div>
		</div>
	</body>
</html>

Vue中的slot

前言

插槽(slot),是组件的一块HTML模板,这块模板显示不显示,怎么显示由父组件来决定。

在 2.6.0 中,我们为具名插槽和作用域插槽引入了一个新的统一的语法 (即 v-slot 指令)。它取代了 slot 和 slot-scope 这两个目前已被废弃但未被移除且仍在文档中的 attribute。新语法的由来可查阅这份 RFC。

注意可以写成v-slot:header和v-slot:header="slotProps"

意思是这个插槽的name为header和这个插槽的name为header,传递过来的数据为slotProps ,可以传递数据也可以不传

明白这点就不饶了

为什么有作用域插槽

// 创建一个submit-button组件
<button type="submit">
  <slot></slot>
</button>

我们在父组件使用它

<div class="container">
    <submit-button>
        <template>{{user.firstName }}</template>
    </submit-button>
</div>

上面代码的user是拿不到的

要知道我们自定义的template内容是在父组件下创建的,但是只有submit-button组件才能访问到user,也就是有作用域的限制,们提供的内容是在父级渲染的

为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 元素的一个 attribute 绑定上去

<button type="submit">
  <slot v-bind:user="user"></slot>
</button>

绑定在 <slot> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot 来定义我们提供的插槽 prop 的名字

<div class="container">
    <submit-button>
        <template v-slot:default="user">{{user.firstName }}</template>
    </submit-button>
</div>
  • v-slot 替代 slot slot-scope这些属性

  • slot-scope 子组件把数据交给父组件

  • 插槽组件 和 插槽属性 slot slot-scope 还是不同的

v-slot(别名:#)

在接下来所有的 2.x 版本中 slot 和 slot-scope attribute 仍会被支持,但已经被官方废弃且不会出现在 Vue 3 中

https://github.com/vuejs/rfcs/blob/master/active-rfcs/0001-new-slot-syntax.md
这个rfc提案说的是

<foo>
  <template slot-scope="{ msg }">
    <div>{{ msg }}</div>
  </template>
</foo>

When we first introduced scoped slots, it was verbose because it required always using :

每次使用slot-socpe都要声明一个template 挺繁琐的

后面可以不用包裹一层template ,但是会造成映射不清楚

<foo>
  <bar slot-scope="{ msg }">
    {{ msg }}
  </bar>
</foo>

However, the above usage leads to a problem: the placement of slot-scope doesn't always clearly reflect which component is actually providing the scope variable. Here slot-scope is placed on the component, but it's actually defining a scope variable provided by the default slot of .

意思是这个msg是提供给foo组件的 不是bar组件的

具名插槽

跟 v-on 和 v-bind 一样,v-slot 也有缩写,即把参数之前的所有内容 (v-slot:) 替换为字符 #。例如 v-slot:header 可以被重写为 #header:

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>

  <p>A paragraph for the main content.</p>
  <p>And another one.</p>

  <template #footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

slot 原理

<!-- 父组件模板 -->
<MyComponent>
  <div slot="xxoo" slot-scope="scopeData">
    {{ scopeData.a }}
  </div>
</MyComponent>

则会把它编译成:
render() {
  return h('MyComponent', {
    scopedSlots: {
      xxoo: function(scopeData) {
        return h('div', scopeData.a)
      }
    }
  })
}

同样的,在创建子组件实例的时候一样可以拿到 scopedSlots 数据,并把 scopedSlots 数据添加到组件实例对象上,所以在子组件的渲染函数中可以这样拿到作用域插槽要渲染的内容:
// 子组件的渲染函数

render() {
  return h('section', this.$scopedSlots.xxoo())
}

因为 this.$scopedSlots.xxoo 是一个函数,所以我们需要执行它,而正式因为它是函数所以才给了我们为它传递参数的机会,例如:
// 子组件的渲染函数

render() {
  return h('section', this.$scopedSlots.xxoo({
    a: this.a,
    b: this.b
  }))
}

如上代码所示,我们传递了一个对象过去。这里是关键,大家注意,上面的代码是子组件的渲染函数,所以我们可以把子组件的数据传递过去,再回过头来看一下 xxoo 函数:

xxoo: function(scopeData) {
        return h('div', scopeData.a)
      }

这里的 scopeData 就是我们从子组件传递过来的对象,而 scopeData.a 就是子组件的数据,这就是作用域插槽的原理。

至于作用域插槽,与普通插槽唯一的区别就是,编译器会把作用域插槽编译成函数,一个返回 VNode 的函数,而非像普通插槽一样直接编译成 VNode

参考文献

Typescript中infer关键字

image

有时候我们想从A extends B 中 获取B部分参数 这时候就可以使用infer进行推断

推断部分类型 分解

其实B就是限制A的,也可以理解成从A推断出部分参数,这部分的参数的具体怎么来,表达式怎么拆解的,依赖于B

一文讲懂堆排序,解决topK问题

image.png

解题思路

堆排序整个流程可以总结为:上浮下沉

为什么解决本题需要用到堆?

很多同学可能会想到这样一种解决,我把数组全部排序好,这样就可以拿到第k大的元素,这样是一种解法,但是我们是需要第K大的元素,不一定要全部排序好再去拿,只针对部分元素进行排序,这样的复杂度显然可以降低的

也就是可以转化为:使用堆排序来解决这个问题——建立一个大顶堆,做k−1 次删除操作后,堆顶元素就是我们要找的答案(堆排序过程中,不全部下沉,下沉nums.length-k+1,然后堆顶可以拿到我们top k答案了)

堆排序

基本介绍

堆排序是利用 这种 数据结构 而设计的一种排序算法,它是一种选择排序,最坏 、最好、平均时间复杂度均为 O(nlogn),它是不稳定排序。

注意因为完全二叉树的性质,可以用数组表示对应的树结构(所以,堆排序过程中,你是看不到树这数据结构的,用数组进行映射了),这叫顺序存储

顺序存储二叉树

特点

  • 第 n 个元素的 左子节点 为 2*n+1
  • 第 n 个元素的 右子节点 为 2*n+2
  • 第 n 个元素的 父节点 为 (n-1)/2
  • 最后一个非叶子节点为 Math.floor(arr.length/2)-1

堆是具有以下性质的完全二叉树:

  • 大顶堆:每个节点的值都 大于或等于 其左右孩子节点的值

    注:没有要求左右值的大小关系

  • 小顶堆:每个节点的值都 小于或等于 其左右孩子节点的值

举例说明:

大顶堆举例

对堆中的节点按层进行编号,映射到数组中如下图

大顶堆特点:arr[i] >= arr[2*i+1] && arr[i] >= arr[2*i+2],i 对应第几个节点,i 从 0 开始编号

小顶堆举例

小顶堆特点:arr[i] <= arr[2*i+1] && arr[i] <= arr[2*i+2],i 对应第几个节点,i 从 0 开始

排序说明

  • 升序:一般采用大顶堆
  • 降序:一般采用小顶堆

基本**

  1. 将待排序序列构造成一个大顶堆

    注意:这里使用的是数组,而不是一颗二叉树

  2. 此时:整个序列的 最大值就是堆顶的根节点

  3. 将其 与末尾元素进行交换,此时末尾就是最大值

  4. 然后将剩余 n-1 个元素重新构造成一个堆,这样 就会得到 n 个元素的次小值。如此反复,便能的得到一个有序序列。

堆排序步骤图解

对数组 4,6,8,5,9 进行堆排序,将数组升序排序。

步骤一:构造初始堆

  1. 给定无序序列结构 如下:注意这里的操作用数组,树结构只是参考理解

将给定无序序列构造成一个大顶堆。

  1. 此时从最后一个非叶子节点开始调整,从左到右,从上到下进行调整。

叶节点不用调整,第一个非叶子节点 arr.length/2-1 = 5/2-1 = 1 ,也就是 元素为 6 的节点。

比较时:先让 5 与 9 比较,得到最大的那个,再和 6 比较,发现 9 大于 6,则调整他们的位置。

  1. 找到第二个非叶子节点 4,由于 [4,9,8] 中,9 元素最大,则 4 和 9 进行交换

  1. 此时,交换导致了子根 [4,5,6] 结构混乱,将其继续调整。[4,5,6] 中 6 最大,将 4 与 6 进行调整。

此时,就将一个无序序列构造成了一个大顶堆。

步骤二:将堆顶元素与末尾元素进行交换

将堆顶元素与末尾元素进行交换,使其末尾元素最大。然后继续调整,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。

  1. 将堆顶元素 9 和末尾元素 4 进行交换

  1. 重新调整结构,使其继续满足堆定义

  1. 再将堆顶元素 8 与末尾元素 5 进行交换,得到第二大元素 8

  1. 后续过程,继续进行调整、交换,如此反复进行,最终使得整个序列有序

总结思路

  1. 将无序序列构建成一个堆,根据升序降序需求选择大顶堆
  2. 将堆顶元素与末尾元素交换,将最大元素「沉」到数组末端
  3. 重新调整结构,使其满足堆定义,然后继续交换堆顶与当前末尾元素,反复执行调整、交换步骤,直到整个序列有序。

步骤

这里想说的几点注意事项(代码实现的关键思路):

  1. 第一步构建初始堆:是自底向上构建,从最后一个非叶子节点开始

  2. 第二步就是下沉操作让尾部元素与堆顶元素交换,最大值被放在数组末尾,并且缩小数组的length,不参与后面大顶堆的调整

  3. 第三步就是调整是从上到下,从左到右,因为堆顶元素下沉到末尾了,要重新调整这颗大顶堆

代码模板

官方的代码模板我参考了下,比一些书籍写的都好记,所以可以参考作为堆排序的模板

/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number}
 */
 // 整个流程就是上浮下沉
var findKthLargest = function(nums, k) {
   let heapSize=nums.length
    buildMaxHeap(nums,heapSize) // 构建好了一个大顶堆
    // 进行下沉 大顶堆是最大元素下沉到末尾
    for(let i=nums.length-1;i>=nums.length-k+1;i--){
        swap(nums,0,i)
        --heapSize // 下沉后的元素不参与到大顶堆的调整
        // 重新调整大顶堆
         maxHeapify(nums, 0, heapSize);
    }
    return nums[0]
   // 自下而上构建一颗大顶堆
   function buildMaxHeap(nums,heapSize){
     for(let i=Math.floor(heapSize/2)-1;i>=0;i--){
        maxHeapify(nums,i,heapSize)
     }
   }
   // 从左向右,自上而下的调整节点
   function maxHeapify(nums,i,heapSize){
       let l=i*2+1
       let r=i*2+2
       let largest=i
       if(l < heapSize && nums[l] > nums[largest]){
           largest=l
       }
       if(r < heapSize && nums[r] > nums[largest]){
           largest=r
       }
       if(largest!==i){
           swap(nums,i,largest) // 进行节点调整
           // 继续调整下面的非叶子节点
           maxHeapify(nums,largest,heapSize)
       }
   }
   function swap(a,  i,  j){
        let temp = a[i];
        a[i] = a[j];
        a[j] = temp;
   }
};

进行堆排序

findKthLargest(nums,nums.length)
// 或者调整一下 let i=nums.length-1;i>=nums.length-k+1;的条件就行

babel

https://zhuanlan.zhihu.com/p/405456997

useBuiltIns

这样配置的原因是:target 下设置我们业务项目所需要支持的最低环境配置,useBuiltIns 设置为 entry,将最低环境不支持的所有 polyfill 都引入到入口文件(即使你在你的业务代码中并未使用)。这是一种兼顾最终打包体积和稳妥的方式,为什么说稳妥呢,因为我们很难保证引用的三方包有处理好 polyfill 这些问题。

当然如果你能充分保证你的三方依赖 polyfill 处理得当,那么也可以把 useBuiltIns 设置为 usage。

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "chrome": "58" // 按自己需要填写
        },
        "useBuiltIns": "entry",
        "corejs": {
          "version": 3,
          "proposals": true
        }
      }
    ]
  ],
  "plugins": []
}

对 BFC 规范(块级格式化上下文:block formatting context)的理解?

  1. 对 BFC 规范(块级格式化上下文:block formatting context)的理解?

(W3C CSS 2.1 规范中的一个概念,它是一个独立容器,决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。)
一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型。

不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染,也就是说BFC内部的元素和外部的元素不会互相影响。

BFC 规定了内部的 Block Box 如何布局。

定位方案:

  • 内部的 Box 会在垂直方向上一个接一个放置。
  • Box 垂直方向的距离由 margin 决定,属于同一个 BFC 的两个相邻 Box 的 margin 会发生重叠。
  • 每个元素的 margin box 的左边,与包含块 border box 的左边相接触。
  • BFC 的区域不会与 float box 重叠。
  • BFC 是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。
  • 计算 BFC 的高度时,浮动元素也会参与计算。

满足下列条件之一就可触发 BFC

  • 根元素,即 html
  • float 的值不为none(默认)
  • overflow 的值不为 visible(默认)
  • display 的值为 inline-block、table-cell、table-caption
  • position 的值为 absolute 或 fixed

npm的版本号范围规则

整理下npm中版本号范围规则,当备忘录

See semver for more details about specifying version ranges.

  • version Must match version exactly
  • version Must be greater than version

  • =version etc

  • <version
  • <=version
  • ~version "Approximately equivalent to version" See semver
  • ^version "Compatible with version" See semver
  • 1.2.x 1.2.0, 1.2.1, etc., but not 1.3.0
  • http://... See 'URLs as Dependencies' below
    • Matches any version
  • "" (just an empty string) Same as *
  • version1 - version2 Same as >=version1 <=version2.
  • range1 || range2 Passes if either range1 or range2 are satisfied.
  • git... See 'Git URLs as Dependencies' below
  • user/repo See 'GitHub URLs' below
  • tag A specific version tagged and published as tag See npm dist-tag
  • path/path/path See Local Paths below
{
  "dependencies": {
    "foo": "1.0.0 - 2.9999.9999",
    "bar": ">=1.0.2 <2.1.2",
    "baz": ">1.0.2 <=2.3.4",
    "boo": "2.0.1",
    "qux": "<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0",
    "asd": "http://asdf.com/asdf.tar.gz",
    "til": "~1.2",
    "elf": "~1.2.3",
    "two": "2.x",
    "thr": "3.3.x",
    "lat": "latest",
    "dyl": "file:../dyl"
  }
}

版本的格式

major.minor.patch

主版本号.次版本号.修补版本号

version

必须匹配某个版本

如:1.1.2,表示必须依赖1.1.2版

>version

必须大于某个版本

如:>1.1.2,表示必须大于1.1.2版

>=version

可大于或等于某个版本

如:>=1.1.2,表示可以等于1.1.2,也可以大于1.1.2版本

<version

必须小于某个版本

如:<1.1.2,表示必须小于1.1.2版本

<=version

可以小于或等于某个版本

如:<=1.1.2,表示可以等于1.1.2,也可以小于1.1.2版本

~version

大概匹配某个版本

如果minor版本号指定了,那么minor版本号不变,而patch版本号任意

如果minor和patch版本号未指定,那么minor和patch版本号任意

如:~1.1.2,表示>=1.1.2 <1.2.0,可以是1.1.2,1.1.3,1.1.4,.....,1.1.n

如:~1.1,表示>=1.1.0 <1.2.0,可以是同上

如:~1,表示>=1.0.0 <2.0.0,可以是1.0.0,1.0.1,1.0.2,.....,1.0.n,1.1.n,1.2.n,.....,1.n.n

^version

兼容某个版本

版本号中最左边的非0数字的右侧可以任意

如果缺少某个版本号,则这个版本号的位置可以任意

如:^1.1.2 ,表示>=1.1.2 <2.0.0,可以是1.1.2,1.1.3,.....,1.1.n,1.2.n,.....,1.n.n

如:^0.2.3 ,表示>=0.2.3 <0.3.0,可以是0.2.3,0.2.4,.....,0.2.n

如:^0.0,表示 >=0.0.0 <0.1.0,可以是0.0.0,0.0.1,.....,0.0.n

x-range

x的位置表示任意版本

如:1.2.x,表示可以1.2.0,1.2.1,.....,1.2.n

*-range

任意版本,""也表示任意版本

如:*,表示>=0.0.0的任意版本

version1 - version2

大于等于version1,小于等于version2

如:1.1.2 - 1.3.1,表示包括1.1.2和1.3.1以及他们件的任意版本

range1 || range2

满足range1或者满足range2,可以多个范围

如:<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0,表示满足这3个范围的版本都可以

关于npm-install

还有npm add别名

npm install (with no args, in package dir)
npm install [<@scope>/]<name>
npm install [<@scope>/]<name>@<tag>
npm install [<@scope>/]<name>@<version>
npm install [<@scope>/]<name>@<version range>
npm install <alias>@npm:<name>
npm install <git-host>:<git-user>/<repo-name>
npm install <git repo url>
npm install <tarball file>
npm install <tarball url>
npm install <folder>

aliases: npm i, npm add
common options: [-P|--save-prod|-D|--save-dev|-O|--save-optional|--save-peer] [-E|--save-exact] [-B|--save-bundle] [--no-save] [--dry-run]

安装指定的版本号

npm install [<@scope>/]@

npm install [email protected]

参考文献

配合 yarn Workspace 食用的 Lerna 入门指南

Source: jsilvax. A Beginner's Guide to Lerna with Yarn Workspaces. Oct/6/2018

当结合在一起时,Lerna和Yarn Workspaces可以简化和优化对多包仓库的管理。 Lerna 通过提供有用的实用命令来处理跨多个包的任务执行,使版本管理和将包发布到NPM Org中成为一种轻松的体验。 Yarn Workspaces 管理我们的依赖关系。它不需要多个node_modules目录,而是智能地优化了依赖关系,将他们一并安装,并允许在一个monorepo中交叉链接依赖关系。Yarn Workspaces提供了像Lerna这样的工具来管理多包仓库。

为了开始,让我们启用Yarn Workspaces吧

yarn config set workspaces-experimental true

现在我们可以通过创建一个模拟项目来说明这些概念了

mkdir my-design-system && cd my-design-system

然后,我们初始化项目

yarn init

并将Lerna添加为开发依赖。

yarn add lerna --dev

然后你会想要初始化Lerna,这将创建一个lerna.json和一个包目录

lerna init

为了设置Lerna开启Yarn工作空间,我们需要配置lerna.json。 让我们添加yarn作为我们的npm客户端,并指定我们使用yarn工作空间。在本教程中,我们将独立地版本化我们的包。

// lerna.json
{
  "packages": ["packages/*"],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true
}

此时我们应该只有一个根 package.json。在这个根 package.json中,我们需要添加workspaces和private为true。将private设置为true将阻止根项目被发布到NPM。

// package.json
{
  "name": "my-design-system",
  "private": true,
  "workspaces": [
     "packages/*"
  ]  
}

创建一个新包的流程

需要在包目录下创建新的包。让我们创建一个模拟表单包

cd packages

一旦我们进入了正确的目录,我们就可以创建并cd到我们的新包中了

mkdir my-design-system-form && cd my-design-system-form

然后我们通过运行 yarn init 来创建一个新的package.json

yarn init

新包的名称应该遵循我们的NPM Org scope命名方式,例如:@my-scope-name。 同样重要的是,新的包要从0.0.0这样的版本开始,因为一旦我们使用Lerna进行第一次发布,它就会发布成0.1.0或1.0.0。

// package.json
{
  "name": "@my-scope-name/my-design-system-form",
  "version" : "0.0.0",
  "main" : "index.js"
}

如果您有一个支持私有包的NPM Org账户,您可以在您的模块的独立包.json中添加以下内容。

"publishConfig": {
    "access": "restricted"
}

将本地的兄弟依赖关系添加到特定的包中

现在我们知道了创建新包的流程,假设说我们最后的结构是这样的。

my-design-system/
    packages/
        my-design-system-core/
        my-design-system-form/
        my-design-system-button/

如果我们想把my-design-system-button作为依赖关系添加到my-design-system-form中,并让Lerna将它们进行符号链接,我们可以通过cd到该包中来实现。

cd my-design-system-form 

然后运行以下内容。

lerna add @my-scope-name/design-system-button --scope=@my-scope-name/my-design-system-form

这将更新@my-scope-name/my-design-system-form的package.json。 我们的package.json应该是这样的。

// package.json
{
  "name": "@my-scope-name/my-design-system-form",
  "version": "1.0.0",
  "main": "index.js",
  "dependencies": {
    "@my-scope-name/my-design-system-button": "^1.0.0"
  }
}

现在,你可以在index.js中引用这个本地依赖关系,如

import Button from '@my-scope-name/my-design-system-button';

为所有的包添加一个 "共同 "的依赖关系

做法和前面的命令类似。不过这是针对/packages/* 的。不管你要加的依赖是本地的同级依赖还是来自NPM的依赖,都没关系。

lerna add the-dep-name

如果你有常见的开发依赖,最好在 workspace 的 root package.json中指定。例如,可以是Jest、Husky、Storybook、Eslint、Prettier等依赖项

yarn add husky --dev -W

*添加-W标志,就可以明确表示我们要把依赖关系添加到工作区根目录。

删除依赖

如果有一个所有包都使用的依赖,但你想删除,Lerna有exec命令,可以在每个包中运行一个任意命令。有了这些知识,我们就可以使用exec来删除所有包的依赖关系。

lerna exec -- yarn remove dep-name

运行测试

Lerna提供了run命令,它将在每个包含了npm脚本的包中运行该脚本。 例如,假设我们所有的包都遵循my-design-system-form的结构。

my-design-system-form/
    __tests__/
        Form.test.js

在每个package.json中,我们都有测试的npm脚本。

"name": "@my-scope-name/my-design-system-form",
"scripts": {
    "test": "jest"
}

然后Lerna可以通过运行每个测试脚本来执行。

lerna run test --stream

*-stream 这个flag提供子进程的输出。

发布到NPM

手动

首先,你需要确保你已经登录了。你可以通过以下操作来验证你是否已经登录。

npm whoami // myusername

如果你没有登录,请运行以下内容并按照提示操作。

npm login

登录后,您可以通过运行Lerna发布。

lerna publish

Lerna会提示你更新版本号。

自动

Lerna支持使用Conventional Commits Standard在CI环境中自动进行语义版本管理。 这使开发人员能够像下面这样提交

git commit -m "fix: JIRA-1234 Fixed minor bug in foo"

然后在CI环境中,包的版本可以根据上面的提交更新并发布到NPM。这可以通过配置你的CI环境来完成。

lerna publish --conventional-commits --yes 

如果你不想传递flag,可以在你的lerna.json文件中添加以下内容。

"command": {
    "publish": {
       "conventionalCommits": true, 
       "yes": true
    }
}

强制执行 Conventional Commits

如果你想强制执行 Conventional Commits 标准,我建议在项目的ROOT中加入Commitlint。

yarn add @commitlint/cli @commitlint/config-conventional husky cross-env --dev

然后在根package.json中创建一个发布脚本

"scripts": {
    "release": "cross-env HUSKY_BYPASS=true lerna publish"
}

这个发布脚本将在CI环境中运行。请注意,我们在 lerna.json 文件中配置了传统的提交和 "yes "标志。由于这个CI环境将会把版本的变更提交,我们不希望触发提交消息的inting。我们通过添加一个名为HUSKY_BYPASS的环境变量来实现,我们将使用cross-env将其设置为true。 我们还需要在root package.json中添加进一步的配置。

"husky": {
    "hooks": {
    "commit-msg": "[[ -n $HUSKY_BYPASS ]] || commitlint -E HUSKY_GIT_PARAMS"
    }
},
"commitlint": {
    "extends": ["@commitlint/config-conventional"]
}

对于husky,我们添加了一个commitlint/config-conventional的commit-msg钩子,它将检查我们在上面添加的HUSKY_BYPASS环境变量,如果这个变量是假的,那么我们通过@commitlint/config-conventional来精简提交消息。

分离版本控制与发布

如果出于任何原因,你想完全掌控版本控制,Lerna有能力将版本控制和发布分成两个命令。 你可以手动运行。

lerna version

然后按照提示更新各个版本号。 然后你就可以有一个步骤,读取最新的标签(是手动更新的)发布到NPM。

lerna publish from-git --yes

多贡献者参与的本地开发

每当有新的贡献者对你的项目进行git克隆,或者你需要拉取你团队的最新变化时,你必须运行yarn命令。

yarn

在大多数的Lerna教程中,提倡使用lerna bootstrap命令,然而当启用yarn工作空间时,这是不必要的,也是多余的

lerna bootstrap when you're using Yarn workspaces is literally redundant? All lerna bootstrap --npm-client yarn --use-workspaces (CLI equivalent of your lerna.json config) does is call yarn install in the root. — Issue 1308

更多信息见github.com/lerna/lerna…

跨项目的本地开发

在我们的例子中,我们正在构建一个多包设计系统。如果开发人员想在设计系统中创建一个新的组件,但在发布之前也要在本地客户端应用程序中进行测试,他们可以通过使用yarn的链接命令来实现。

建立本地依赖关系的symlink

假设我们想在my-client-app中使用我们本地的my-design-system-core。 我们先cd到我们要在另一个项目中用到的软件包。

cd ~/path/to/my-design-system/my-design-system-core

然后我们创建一个symlink

yarn link

你应该看到这样的输出

success Registered "@my-scope-name/my-design-system-core".
info You can now run `yarn link "@my-scope/my-design-system-core"` in the projects where you want to use this module and it will be used instead.

现在我们的包已经有了符号链接,我们可以进入my-client-app中使用。

cd ~/path/to/my-client-app 
yarn link @my-scope-name/my-design-system-core

在  /packages/my-design-system-core 中的任何变化都会反映在my-client-app中。现在,开发人员可以很容易地在两个项目上进行本地开发,并看到它的反映。

解除本地依赖关系的链接

当开发者完成后,不再想使用本地的包时,我们需要解除链接。 cd到入我们要解除链接的包中

cd ~/path/to/my-design-system/my-design-system-core

运行unlink删除本地symlink

yarn unlink

你会看到这样的输出

success Unregistered "@my-scope-name/my-design-system-core".
info You can now run `yarn unlink "@my-scope-name/my-design-system-core"` in the projects where you no longer want to use this module.

现在,我们可以cd到my-client-app中解除链接。

cd ~/path/to/my-client-app
yarn unlink @my-scope-name/my-design-system-core

总结

  • Lerna管理版本发布
  • Yarn Workspaces处理安装和依赖

Lerna与Yarn Workspaces是一个很好的组合。Lerna 在 Yarn Workspaces 的基础上增加了实用功能,用于处理多个包。yarn wrospace使得所有的依赖关系可以一起安装,使得缓存和安装速度更快。它让我们可以通过一个命令轻松地在NPM上发布依赖关系,当依赖关系的版本发生变化时,自动更新兄弟依赖关系的package.json,一般来说,安装、版本管理和发布都是一种无痛的体验。

typescript中的as const

const assertions

typescript3.4加入这个功能

const 断言顾名思义也是一种类型断言方式,不同于断言到具体的类型 'foo' as 'foo',它的写法是 'foo' as const 或者 { value: 'foo' } as const 。

它主要有以下特性:

  • string number boolean 字面量类型都不会被扩展;
  • 对象字面量的属性会变成只读的;
  • 数组字面量会变成只读的元组;
// Type '"hello"'
let x = "hello" as const;
// Type 'readonly [10, 20]'
let y = [10, 20] as const;
// Type '{ readonly text: "hello" }'
let z = { text: "hello" } as const;

image

除了tsx文件以外,也可以使用尖括号的断言语法。

// Type '"hello"'
let x = <const>"hello";
// Type 'readonly [10, 20]'
let y = <const>[10, 20];
// Type '{ readonly text: "hello" }'
let z = <const>{ text: "hello" };

基于 const assertions 的强大功能,推断出来的值都是确定的,我们不需要显式地声明更多定义来进行类型推断(比如省略readonly,也不需要给result注明类型,会自动推断):

image

// Works with no types referenced or declared.
// We only needed a single const assertion.
function getShapes() {
  let result = [
    { kind: "circle", radius: 100 },
    { kind: "square", sideLength: 50 }
  ] as const;

  return result;
}

for (const shape of getShapes()) {
  // Narrows perfectly!
 // 可以大胆使用,不用担心kind会变化
  if (shape.kind === "circle") {
    console.log("Circle radius", shape.radius);
  } else {
    console.log("Square side length", shape.sideLength);
  }
}

代替枚举类型

如果你选择不使用TypeScript的枚举类型,这甚至可以用来在普通的JavaScript代码中启用类似枚举的模式。

export const Colors = {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;
// or use an 'export default'
export default {
  red: "RED",
  blue: "BLUE",
  green: "GREEN",
} as const;

注意事项

有一点需要注意的是,const断言只能立即应用于简单的字面表达。

// Error! A 'const' assertion can only be applied to a
// to a string, number, boolean, array, or object literal.
let a = (Math.random() < 0.5 ? 0 : 1) as const;
let b = (60 * 60 * 1000) as const;
// Works!
let c = Math.random() < 0.5 ? (0 as const) : (1 as const);
let d = 3_600_000 as const;

const上下文并不能立即将表达式转换为完全不可变的。

let arr = [1, 2, 3, 4];
let foo = {
  name: "foo",
  contents: arr,
} as const;
foo.name = "bar"; // error!
foo.contents = []; // error!
foo.contents.push(5); // ...works! <---注意这里

参考文献

typescript中的type关键字

首先我们从官方的定义中可以看出,对 type 的定义叫 type alias 而非直接就叫 type

Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you’d otherwise have to write by hand.

type Second = number;
 
let timeInSecond: number = 10;
let time: Second = 10;

从上面的例子中不难看出,其实 type 运算的本质就是类型别名,将 number 这个基本类型别名为 Second ,但是实际 Second 还是 number 类型,理解了这个你就明白为什么 interface 不具有直接定义基本数据类型的能力了,因为接口从本质上讲就跟类型没有关系,这个我们放到后面说。那对于“对象”的定义又怎么解释,比如上面提到的:

interface Animal {
  name: string;
  age: number;
}

这里的本质就是把 {name: string; age: number;}这个类型别名(请注意这里是别名而非创建)为 Animal 类型,这样之后我们不需要每次都重复写复杂的类型定义了。

到这里,我们了解到:

  • Typescript 的 type 关键字表示的是类型别名;
  • 被 type 关键词声明的变量表示的还是 Types (比如 Animal 就是动物类型);

这是官网得到的解释,但是从类型编程的角度还可以认为,type具有定义util type(我的理解就是js util function)的能力

// 声明了一个Utility Types
type ParamType<T> = T extends (...args: infer P) => any ? P : T;

// 调用
ParamType<string>

参考文献

webpack模块联邦

什么是模块联邦
先引用一下其他人的总结:
模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享。
它支持直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。npm 包方式和模块联邦区别如下图:

image

如何使用
联邦模块有两个主要概念:Host(消费其他 Remote)和 Remote(被 Host 消费), 每个项目可以是 Host 也可以是 Remote。模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:

name 当前应用名称,需要全局唯一,必填。
remotes 可以将 Remote 中的 exposes 配置的模块映射到当前项目中,作为 Host 时必填。
exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用,作为 Remote 时必填。
shared 主要是用来避免项目出现多个公共依赖,若是配置了这个属性,webpack在加载的时候会先判断本地应用是否存在对应的包,若是不存在,则加载远程应用的依赖包。

image

链接:https://juejin.cn/post/7119298510247165965

精读《Webpack5 新特性 - 模块联邦》 https://zhuanlan.zhihu.com/p/115403616

https://www.zhangxinxu.com/wordpress/2020/08/js-customevent-pass-param/

微前端在得物客服域的实践 | 那么多微前端框架,为啥我们选Qiankun + MF
https://www.modb.pro/db/413745

微前端现有的落地方案可以分为三类,自组织模式、基座模式以及模块加载模式。

与基座模式相比,模块加载模式没有中心容器,这就意味着,我们可以将任意一个微应用当作项目入口,整个项目的微应用与微应用之间相互串联,打破项目的固定加载模式,彻底释放项目的灵活机动性,这样的模式,也被称为去中心化模式。

image

circleci学习

概念

Your CircleCI configuration can be adapted to fit many different needs of your project. The following terms, sorted in order of granularity and dependence, describe the components of most common CircleCI projects:

  • Pipeline: Represents the entirety of your configuration. Available in CircleCI Cloud only.
  • Workflows: Responsible for orchestrating multiple jobs.
  • Jobs: Responsible for running a series of steps that perform commands.
  • Steps: Run commands (such as installing dependencies or running tests) and shell scripts to do the work required for your project.

Workflows工作流是组织一系列Jobs(用于编排所有 job),规定哪些分支可以触发哪些jobs

image

Jobs包含steps 具体执行哪些步骤

image

image

org

org config组件化 2.1版本才有

image

config参考

  • only ignore 进行筛选
version: 2

defaults: &defaults
  working_directory: ~/repo
  docker:
    - image: circleci/node:14.8

jobs:
  test:
    <<: *defaults
    steps:
      - checkout
      - run: npm install
      - run: npm test
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - persist_to_workspace:
          root: ~/repo
          paths: .
  build:
    <<: *defaults
    steps:
      - add_ssh_keys:
          fingerprints: 'b9:ef:45:6e:fc:1b:03:f8:3c:56:48:d0:d7:59:fe:ea'
      - attach_workspace:
          at: ~/repo
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "package.json" }}
            - v1-dependencies-
      - run: ls -al
      - run:
          name: Avoid hosts unknown for github
          command: mkdir ~/.ssh/ && echo -e "Host github.com\n\tStrictHostKeyChecking no\n" > ~/.ssh/config
      - run: npm run build
      - run: ls -al
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ checksum "package.json" }}
      - save_cache:
          paths:
            - dist
          key: dist
  # deploy:
  #   <<: *defaults
  #   steps:
  #     - attach_workspace:
  #         at: ~/repo
  #     - restore_cache:
  #         keys:
  #           - dist
  #           - v1-dependencies-{{ checksum "package.json" }}
  #           - v1-dependencies-
  #     - run: echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" > ~/repo/.npmrc
  #     - run: ls -al
  #     - run: # git config
  #         name: git-config
  #         command: |
  #           git config --global user.email "[email protected]"
  #           git config --global user.name "vnues"
  #     - run: npm run release
  #     - run:
  #         name: Add github.com to known hosts
  #         command: |
  #           mkdir -p ~/.ssh
  #           ssh-keyscan github.com >> ~/.ssh/known_hosts
  #     - run: # git push
  #         name: push-github
  #         command: |
  #           git checkout develop
  #           git rebase master
  #           git push
  #           git push --tags
  #     - run: npm publish

workflows:
  version: 2
  build_test_and_deploy:
    jobs:
      - test:
          filters:
            tags:
              only: 
                /.*/
      - build:
          requires:
            - test
          filters:
            tags:
              only: 
                /.*/
          branches:
              only:
                master
      - deploy:
          requires:
            - build
          filters:
            tags:
              only:
                - /^v.*/
            branches:
              ignore:
                /.*/

参考资料

模块化原理

1.2 CommonJS
CommonJS 是一种使用广泛的JavaScript模块化规范,核心**是通过require方法来同步地加载依赖的其他模块,通过 module.exports 导出需要暴露的接口。

1.2.1 用法

采用 CommonJS 导入及导出时的代码如下:

// 导入
const someFun= require('./moduleA');
someFun();

// 导出
module.exports = someFunc;
1.2.2 原理

// a.js

let fs = require('fs');
let path = require('path');
let b = req('./b.js');
function req(mod) {
    let filename = path.join(__dirname, mod);
    let content = fs.readFileSync(filename, 'utf8');
    let fn = new Function('exports', 'require', 'module', '__filename', '__dirname', content + '\n return module.exports;');
   // 用一句话来说明就是,require方能看到的只有module.exports这个对象,它是看不到exports对象的,而我们在编写模块时用 
   // 到的exports对象实际上只是对module.exports的引用。
    let module = {
        exports: {}
    };

    return fn(module.exports, req, module, __filename, __dirname);
}

// b.js

console.log('bbb');
exports.name = 'zfpx';

1.4 ES6 模块化
ES6 模块化是ECMA提出的JavaScript模块化规范,它在语言的层面上实现了模块化。浏览器厂商和Node.js 都宣布要原生支持该规范。它将逐渐取代CommonJS和AMD`规范,成为浏览器和服务器通用的模块解决方案。 采用 ES6 模块化导入及导出时的代码如下

// 导入
import { name } from './person.js';
// 导出
export const name = 'zfpx';
ES6模块虽然是终极模块化方案,但它的缺点在于目前无法直接运行在大部分 JavaScript 运行环境下,必须通过工具转换成标准的 ES5 后才能正常运行。

五 Commonjs 和 Es Module 总结
接下来贯穿全文,讲一下 Commonjs 和 Es Module 的特性。

Commonjs 总结
Commonjs 的特性如下:

CommonJS 模块由 JS 运行时实现。
CommonJs 是单个值导出(值拷贝),本质上导出的就是 exports 属性。
CommonJS 是可以动态加载的,对每一个加载都存在缓存,可以有效的解决循环引用问题。
CommonJS 模块同步加载并执行模块文件。
es module 总结
Es module 的特性如下:

ES6 Module 静态的,不能放在块级作用域内,代码发生在编译时。
ES6 Module 的值是动态绑定的,可以通过导出方法修改,可以直接访问修改结果(值引用)。
ES6 Module 可以导出多个属性和方法,可以单个导入导出,混合导入导出。
ES6 模块提前加载并执行模块文件,
ES6 Module 导入模块在严格模式下。
ES6 Module 的特性可以很容易实现 Tree Shaking 和 Code Splitting。

https://zhuanlan.zhihu.com/p/397999271

image

在谈到CommonJS和ESM处理模块循环引用的区别时,经常会看到这样一个结论:
CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。 ESM模块对导出模块,变量,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。 ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

总结
本文从webpack的模块实现角度,通俗解释了模块的循环问题中的一些常见疑惑。结合一下上面代码,总结一下这里谈到的问题:

commonJS和ESM的webpack实现都是通过缓存来处理循环引用问题的。
commonJS和ESM处理循环引用的时机不同,一个是运行时,一个是在编译时。

链接:https://juejin.cn/post/7085029980899377160

require 避免循环引用
那么接下来这个循环引用问题,也就很容易解决了。为了让大家更清晰明白,那么我们接下来一起分析整个流程。

① 首先执行 node main.js ,那么开始执行第一行 require(a.js);
② 那么首先判断 a.js 有没有缓存,因为没有缓存,先加入缓存,然后执行文件 a.js (需要注意 是先加入缓存, 后执行模块内容);
③ a.js 中执行第一行,引用 b.js。
④ 那么判断 b.js 有没有缓存,因为没有缓存,所以加入缓存,然后执行 b.js 文件。
⑤ b.js 执行第一行,再一次循环引用 require(a.js) 此时的 a.js 已经加入缓存,直接读取值。接下来打印 console.log('我是 b 文件'),导出方法。
⑥ b.js 执行完毕,回到 a.js 文件,打印 console.log('我是 a 文件'),导出方法。
⑦ 最后回到 main.js,打印 console.log('node 入口文件') 完成这个流程。

链接:https://juejin.cn/post/6994224541312483336

加入缓存了就不需要再执行一遍require函数了 就避免循环了

Flutter学习

组件通信

父子组件之间通信:父组件可以通过构造函数将数据传递给子组件,并通过子组件的属性来接收和使用这些数据。子组件可以通过回调函数将数据传递回父组件。

import 'package:flutter/material.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Parent-Child Communication',
      home: ParentWidget(),
    );
  }
}

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => _ParentWidgetState();
}

class _ParentWidgetState extends State<ParentWidget> {
  String message = 'Hello from parent';

  void updateMessage(String newMessage) {
    setState(() {
      message = newMessage;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Parent Widget'),
      ),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text(message),
          ChildWidget(message: message, updateMessage: updateMessage),
        ],
      ),
    );
  }
}

class ChildWidget extends StatelessWidget {
  final String message;
  final Function(String) updateMessage;

  ChildWidget({this.message, this.updateMessage});

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: [
          Text('Child Widget'),
          RaisedButton(
            child: Text('Update Message'),
            onPressed: () {
              updateMessage('New message from child');
            },
          ),
        ],
      ),
    );
  }
}

在上面的例子中,父组件是ParentWidget,子组件是ChildWidget。父组件通过构造函数将message数据传递给子组件,子组件接收并显示这个数据。子组件中的按钮被点击后,调用父组件传递的updateMessage回调函数,将新的消息传递给父组件并更新界面。

这样,父子组件之间就通过构造函数和属性实现了数据的传递和更新。

TypeScript 中的逆变、协变和双向协变

前言

为什么需要引入逆变、协变和双向协变这些概念

因为考虑到类型兼容,详情参考https://www.typescriptlang.org/docs/handbook/type-compatibility.html

在 TypeScript 中,有两种兼容性机制:子类型和赋值 (意思是理解成在子类型和赋值这种操作下才会触发兼容性,比如比较该类型是不是其子类型)

出于实际目的,类型兼容性由赋值兼容性决定,即使在implements and extends子句的情况下也是如此

基础

TypeScript中的类型兼容性可以用于确定一个类型是否可以赋值给其他类型。这里要了解两个概念:

官方文档说到TS 是结构性的类型系统(Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing. Consider the following code)

  • 结构类型:一种只使用其成员来描述类型的方式(类型 ducking type);
  • 名义类型:明确的指出或声明其类型,如c#,java。

TypeScript的类型兼容性就是基于结构子类型的。下面的例子:

interface IName {
    name: string;
}

class Man {
    name: string;
    constructor() {
        this.name = "鸣人";
    }
}

let p: IName;
p = new Man();
p.name;

上面的代码在TypeScript不会出错,但是在java等语言中就会报错,因为Man类没有明确的说明实现了IName 接口

结构化

在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。而结构性类型系统是基于类型的组成结构,且不要求明确地声明。

TS 是结构性的类型系统所谓结构化就是对值所具有的结构进行类型检查。简单来说,要判断两个类型是否是兼容的,只需要看两个类型的结构是否兼容就可以了,不需要关心类型的名称是否相同。比如:

interface Pet {
  name: string;
}
class Dog {
  name: string;
}
let pet: Pet;
// OK, because of structural typing
pet = new Dog();

子类型

比如考虑如下接口:

interface Animal {
  age: number
}

interface Dog extends Animal {
  bark(): void
}

在这个例子中,Animal 是 Dog 的父类,Dog是Animal的子类型,子类型的属性比父类型更多,更具体。

在类型系统中,属性更多的类型是子类型。

在集合论中,属性更少的集合是子集。

也就是说,子类型是父类型的超集,而父类型是子类型的子集,这是直觉上容易搞混的一点。

记住一个特征,子类型比父类型更加具体,这点很关键。

可赋值性 assignable

assignable 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。

let animal: Animal
let dog: Dog

animal = dog //  ✅ok
dog = animal //  ❌error! animal 实例上缺少属性 'bark'

协变和逆变

如何处理类型兼容呢?通过协变和逆变原则

协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。

维基百科上关于协变和逆变的解释有点晦涩难懂。这里,我们用更通俗一点的语言来表述:

  • 协变: 允许子类型转换为父类型(可以里式替换LSP原则进行理解)
  • 逆变: 允许父类型转换为子类型

逆变

// Dog ≼ Animal

var feedAnimal = (o: Animal) => {};
var feedDog = (o: Dog) => {
  o.bark();
};
feedDog = feedAnimal; // 成立,feedAnimal ≼ feedDog
feedAnimal = feedDog; // 严格模式下报错,因为可能animal并不能保证存在bark()

// 也就是存在如下场景
function func(f: typeof feedDog) {
  var d: Dog;
  f(d);
}
func(feedAnimal);

在函数的参数类型中,是符合逆变的函数的关系和参数的关系是相反的。但在TS中,参数类型是双向协变的(详见下文3.1小节),如果项目里开启了"strict": true,意味着,会来带开启 strictFunctionType ,此时,才按照逆变处理

双向协变

在老版本的 TS 中,函数参数是双向协变的。也就是说,既可以协变又可以逆变,但是这并不是类型安全的。
在新版本 TS (2.6+) 中 ,你可以通过开启 strictFunctionTypes 或 strict 来修复这个问题。设置之后,函数参数就不再是双向协变的了。

参考资料

typescript中in的关键字

in在ts中用处还是非常广泛,但官方文档并没有解释的很清楚,借此总结一下

in 操作符

  • 在 Typescript 中,in 操作符还充当类型保护
  • 用于映射类型(主要用以申明索引签名。)

类型保护

interface A {
  x: number;
}
interface B {
  y: string;
}

let q: A | B = ...;
if ('x' in q) {
  // q: A
} else {
  // q: B
}

用于映射类型(mapped-types)

// 例1:

type Index = 'a' | 'b' | 'c' 
type FromIndex = { [K in Index]?: number }

const good: FromIndex = { b: 1, c: 2 } // OK
const bad: FromIndex = { b: 1, c: 2, d: 3 } // Error. 不能添加 d 属性

// 例2:

type FromSomeIndex<K extends string> = { [key in K]: number } // 在这里使用泛型限制了 K 的类型为 string,因此可以作为索引

签名

再比如一些TS内置的映射类型当中:

type Readonly<T> = {
  readonly [K in keyof T]: T[K] // 首先通过 keyof 操作符获取类型 T 上的字符串联合类型,然后通过 in 操作符遍历这个联合类型,并依次将联合类型当中每个值绑定到这个映射类型的属性上
}

注:类型映射是这样的语法的:[K in keyof T]: T[K]

参考文献

读《浏览器工作原理与实践》

渲染流水线大总结

好了,我们现在已经分析完了整个渲染流程,从 HTML 到 DOM、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程

image

结合上图,一个完整的渲染流程大致可总结为如下:
渲染进程将 HTML 内容转换为能够读懂的 DOM 树结构。
渲染引擎将 CSS 样式表转化为浏览器可以理解的 styleSheets,计算出 DOM 节点的样式。
创建布局树,并计算元素的布局信息。
对布局树进行分层,并生成分层树。为每个图层生成绘制列表,并将其提交到合成线程。合成线程将图层分成图块,并在光栅化线程池中将图块转换成位图。
合成线程发送绘制图块命令 DrawQuad 给浏览器进程。浏览器进程根据 DrawQuad 消息生成页面,并显示到显示器上。

当从服务器接收HTML页面的第一批数据时,DOM解析器就开始工作了,在解析过程中,如果遇到了JS脚本,如下所示:

极客时间 <script> document.write("--foo") </script> 那么DOM解析器会先执行JavaScript脚本,执行完成之后,再继续往下解析。

那么第二种情况复杂点了,我们内联的脚本替换成js外部文件,如下所示:

极客时间 <script type="text/javascript" src="foo.js"></script> 这种情况下,当解析到JavaScript的时候,会先暂停DOM解析,并下载foo.js文件,下载完成之后执行该段JS文件,然后再继续往下解析DOM。这就是JavaScript文件为什么会阻塞DOM渲染。

我们再看第三种情况,还是看下面代码:

<style type="text/css" src = "theme.css" />

极客时间

<script> let e = document.getElementsByTagName('p')[0] e.style.color = 'blue' </script> 当我在JavaScript中访问了某个元素的样式,那么这时候就需要等待这个样式被下载完成才能继续往下执行,所以在这种情况下,CSS也会阻塞DOM的解析。

所以JS和CSS都有可能会阻塞DOM解析,关于详细信息我们会在后面的章节中详细介绍。

如何优雅的处理C端多个弹框展示场景

前言

最近写的移动端业务经常跟弹框打交道,偶尔处理对于多个弹框的显示问题也是捉襟见肘,特别是产品经常改需求,那么有没有一种优雅的解决方案去处理上面这种问题,或者说,淘宝拼多多等是怎么处理这种问题的

由于项目一开始没有做好规划或者说一开始就不是你维护的,导致首页的弹窗组件可能放了十多个甚至更多,不仅是首页有,首页内又引入了十多个个子组件,这些子组件内也有弹框,另外子组件的子组件也可能存在弹框,每个弹窗都有对应的一组控制显隐逻辑,但是你不可能让所有符合显示条件的弹窗都全都一下子在首页弹出来,如何有顺序的管理这些弹框是重中之重的事情

一个小场景

上面这么分析可能有同学还是不了解这个业务痛点,我们举个例子,假设首页页面有个A组件,A组件有一个弹框A_Modal需要在打开首页显示出来,enen...很简单,我们按照平时的逻辑请求后端接口拿到数据去控制弹框显示就行,我们继续接着迭代,此时遇到了一个B组件,同样也是要显示在首页因为是新活动,所以优先级比较大需要显示B_Modal弹框,这时候你可能要去找找控制A组件的接口找到后端说这个组件不显示了或者说自己手动重置为false,一个组件可以这样搞,但是几十个呢?,不太现实

如下图:

这些弹框是都要在首页上显示的弹框

小误区

❗️注意以下这种交互弹框不在我们讨论范围之内,比如通过按钮弹出弹框这种,像这类弹框通过交互事件我们控制就行,我们要处理的弹框场景是通过后端接口来显示弹框,所以后面我们所说的弹框都是这种情况,注意即可

带着这个业务痛点,我去踩坑了几种方案,下面来分享下以下这种配置化弹框方案(借鉴了动态表单的思路来实现

配置化弹框

之前写管理后台系统的时候有了解过动态表单,实际就是通过一串JSON数据渲染出表单,那么我们是不是可以基于这种思路,通过可配置化的数据来控制弹框的显示,显然是可以的

// modalConfig.js
export default {
  // 首页
  index: {
    // 弹框列表
    modalList: [{
      id: 1, // 弹框的id
      name: 'modalA',
      level: 100,
      // 弹框的优先级
      // 由前端控制弹框是否显示
      // 当我们一个活动过去了废弃一个弹框时候,可以不需要通过后端去更改
      frontShow: true
    }, {
      id: 2,
      name: 'modalB',
      level: 122,
      frontShow: true
    }, {
      id: 3,
      name: 'modalC',
      level: 70,
      frontShow: true
    }]
  }
}

这样做的好处就是利于管理弹框,并且最重要的一点,我可以知道我的页面有多少弹框一目了然的去配置,这里我们先讲解下每个弹框modal的属性

  • id:弹框id-弹框的唯一id
  • name: 弹框名称-可以根据名称很快找到该页面上的弹框
  • level: 弹框优先级-杜绝一个页面可能提示展示多个弹窗的情况
  • frontShow: 前端控制弹框显示的字段-默认为true
  • backShow: 后端控制弹框显示的字段-通过接口请求获取

发布订阅模式来管理弹框

配置完弹框数据,我们还缺少一个调度系统去统一管理这些弹框,这时候自然而然就可以想到发布订阅这种设计模式

// modalControl.js
class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.nodify()
  }
  // 发布
  notify () {
    // ...
  }
}

正常情况下,后端单个接口会返回给我们字段来控制弹框的显示,当然也可能存在多个接口去控制弹框的显示,对于这些情况,我们前端自己去做一层合并,只要保证最后得出一个控制弹框是否展示的字段就行,此时我们就可以在相应的位置取注册我们的弹框类即可

那什么时候发布呢

注意这里的发布跟我们平时的发布判断情况可能不一样,以前我们可能通过在一个生命周期钩子或者按钮触发等事件去发布,但是我们仔细想想,进入首页由接口控制显示,这样动作的发生需要2个条件

  • 每次发生一次订阅操作都伴随着一次执行一次预检测操作,检测所有的弹框是否都订阅完
  • 真正触发的时机是当前页面的弹框都订阅完了,因为只有这样才能拿到所有弹框的优先级,才能判断显示哪个弹框

第一版实现

根据上面的分析单个接口返回的就是一个订阅,而发布是等到所有的弹框都订阅完才执行,于是我们可以快速写出以下代码结构

class ModalControl {
  constructor () {
    // ...
  }
  // 订阅
  add () {
    // ...
    this.preCheck()
  }
  // 预检测
   preCheck(){
    if(this.modalList.length === n){
      // ...
      this.notify()
    }
  }
  // 发布
  notify () {
    // ...
  }
}

实现这个弹框类,我们来拆分实现这四个方法就行了

constructor构造函数

根据以上思路,ModalControl类的 constructor方法中需要设置的初始值差不多也就知道了

// 上述弹框配置
import modalMap from './modalMap'
constructor (type) {
  this.type = type // 页面类型
 this.modalFlatMap = {} // 用于缓存所有已经订阅的弹窗的信息
 this.modalList = getAllModalList(modalMap[this.type]) // 该页面下所有需要订阅的弹框列表,数组长度就是n值
}
// 弹框信息
modalInfo = {
    name: modalItem.name,
    level: modalItem.level,
    frontShow: modalItem.frontShow,
    backShow: infoObj.backShow,
    handler: infoObj.handler // 表示选择出了需要展示的弹窗时,该执行的函数
 }

constructor构造函数接收一个所有弹框的配置项,里面声明两个属性,modalFlatMap用于缓存所有已经订阅的弹窗的信息modalList表示该页面下所有需要订阅的弹框列表,数组长度就是n值

add订阅

我们以弹框的id的作为唯一key值,当请求后端数据接口成功后,在该请求方法相应的回调里进行订阅操作,并且每次订阅都会去检测下调用preCheck方法来判断当前页面的所有弹框是否已经订阅完,如果,则触发notify

  add (modalItem, infoObj) {
    this.modalFlatMap[modalItem.name] = {
      id: modalItem.id,
      level: modalItem.level,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

preCheck检测

preCheck这个方法很简单,单纯的用来判断当前页面的弹框是否都订阅完成

 if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
  }

notify发布

当我们页面上的弹框全部都订阅完后就会触发notify发布,这个notify主要做了这么一件事情:过滤不需要显示的弹框,筛选出当前页面需要显示并且优先级最高的弹框,然后触发其handler方法

  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }

单例模式完善ModalControl

到上面的步骤,其实我们的弹框管理类已经差不多完成了,但是考虑到弹框可能分布在子组件或者孙组件等等,这时候如果都在每个组件实例化弹框类,那么他们实际是没有关联的,此时单例模式就派上用场了

const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

第一版代码

第一版的代码就这样完成了,是不是很简单,搭配modalConfig发布订阅模式,我们可以处理大部分问题了,为自己打个call😊

class ModalControl {
  constructor (type) {
    this.type = type
    this.modalFlatMap = {}
    this.modalList = getAllModalList(modalMap[this.type])
  }

  add (modalItem, infoObj) {
    this.modalFlatMap[modalItem.name] = {
      id: modalItem.id,
      level: modalItem.level,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

  preCheck () {
    if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
    }
  }

  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }
}

const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

demo验证一下

第一版的代码例子🌰在该仓库下demo,执行以下操作就可

git clone git@github.com:vnues/modal-control.git

git checkout feature/first

yarn 

yarn serve

第二版

第一版的ModalControl可以解决我们开发中遇到的场景,但是我们还要考虑一下复杂场景

接下来,我们来完善我们的弹框类ModalControl,我们先来分析下需要注意哪些问题吧

  • 可能存在多个接口控制弹框显示(比如A接口也可以调取这个弹框,后面持续迭代,B接口也可能调取这个弹框),所以不再是那种一对一的关系,而是多对一的关系,多个接口都可以控制这个弹框的显示,这里通过apiFlag来标识弹框,不再使用name

得益于我们的modalConfig配置,我们只需要补充一个apiFlag字段,便可以解决上述问题,是不是很方便,其实后续的复杂场景,也在这里补充字段完善就行

modalConfig

增加apiFlag字段,由name字段对应弹框变为apiFlag对应弹框,实现多对一的关系

export default {
  // 首页
  index: {
    // 弹框列表
    modalList: [{
      id: 1, // 弹框的id
      name: 'modalA',
      level: 100,
      frontShow: true,
      apiFlag: ['mockA_1', 'mockA_2']
    }, {
      id: 2,
      name: 'modalB',
      level: 122,
      frontShow: true,
      apiFlag: ['mockB_1', 'mockB_2']
    }, {
      id: 3,
      name: 'modalC',
      level: 70,
      frontShow: true,
      apiFlag: ['mockC_1']
    }]
  }
}

第二版代码

/* eslint-disable no-console */
/* eslint-disable no-unused-vars */
import modalMap from './modalConfig'

const getAllModalList = mapObj => {
  let currentList = []
  if (mapObj.modalList) {
    currentList = currentList.concat(
      mapObj.modalList.reduce((t, c) => t.concat(c.id), [])
    )
  }
  if (mapObj.children) {
    currentList = currentList.concat(
      Object.values(mapObj.children).reduce((t, c) => {
        return t.concat(getAllModalList(c))
      }, [])
    )
  }
  return currentList
}

const getModalItemByApiFlag = (apiFlag, mapObj) => {
  let mapItem = null
  // 首先查找 modalList
  const isExist = (mapObj.modalList || []).some(item => {
    if (item.apiFlag === apiFlag || (Array.isArray(item.apiFlag) && item.apiFlag.includes(apiFlag))) {
      mapItem = item
    }
    return mapItem
  })
  // modalList没找到,继续找 children
  if (!isExist) {
    Object.values(mapObj.children || []).some(mo => {
      mapItem = getModalItemByApiFlag(apiFlag, mo)
      return mapItem
    })
  }
  return mapItem
}
class ModalControl {
  constructor (type) {
    this.type = type
    this.modalFlatMap = {} // 用于缓存所有已经订阅的弹窗的信息
    this.modalList = getAllModalList(modalMap[this.type]) // 该页面下所有需要订阅的弹框列表,数组长度就是n值
  }

  add (apiFlag, infoObj) {
    const modalItem = getModalItemByApiFlag(apiFlag, modalMap[this.type])
    console.log('modalItem', modalItem)
    this.modalFlatMap[apiFlag] = {
      level: modalItem.level,
      name: modalItem.name,
      frontShow: modalItem.frontShow,
      backShow: infoObj.backShow,
      handler: infoObj.handler
    }
    this.preCheck()
  }

  preCheck () {
    if (this.modalList.length === Object.values(this.modalFlatMap).length) {
      this.notify()
    }
  }

  notify () {
    const highLevelModal = Object.values(this.modalFlatMap).filter(item => item.backShow && item.frontShow).reduce((t, c) => {
      return c.level > t.level ? c : t
    }, { level: -1 })
    highLevelModal.handler && highLevelModal.handler()
  }
}

const controlTypeMap = {}
// 获取单例
function createModalControl (type) {
  if (!controlTypeMap[type]) {
    controlTypeMap[type] = new ModalControl(type)
  }
  console.log('controlTypeMap[type]', controlTypeMap[type])
  return controlTypeMap[type]
}

export default createModalControl

demo验证一下

第一版的代码例子🌰在该仓库下demo,执行以下操作就可

git clone git@github.com:vnues/modal-control.git

git checkout feature/second

yarn 

yarn serve

待解决问题

细心的童鞋可能会发现,竟然第一版和第二版分别实现了一对一多对一的关系,那么一对多的关系如何实现呢?也即是多个接口一起决定弹框是否展示

这里我给出两种思路

  • 多个接口一起决定弹框是否展示,我们完全可以在接口层做合并,最终实现出来的效果就是一对一
  • 订阅方法做去重,利用高阶函数再次封装对应的handler实现多个接口一起决定弹框是否展示,个人还是推荐第一种解决方案

我的前端学习笔记📒

最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读

总结

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌

什么是Vue的SFC? 怎么解析成SFCDescriptor?

我们平时写的 .vue 文件称为 SFC(Single File Components)。vue 会先对 .vue 文件进行解析,分成 template、script、styles、customBlocks 四个部分,称为 descriptor。之后,再对这四个部分分别进行编译最终得到可以在浏览器中执行的 .js 文件。本文介绍将 SFC 解析为 descriptor 这一过程。

SFCDescriptor,是表示 .vue 各个代码块的对象,为以下数据格式:

// an object format describing a single-file component.
declare type SFCDescriptor = {
    template: ?SFCBlock;
    script: ?SFCBlock;
    styles: Array<SFCBlock>;
    customBlocks: Array<SFCBlock>;
};

vue 提供了一个 compiler.parseComponent(file, [options])方法,来将 .vue 文件解析成一个 SFCDescriptor。

image

目前可以使用Volar可以在单文件组件检测TypeScript

参考文献

即将到来的ECMAScript 2022标准

前言

ES2021 或 ES12 在今年夏天早些时候发布(具体的ES2021新特性,可以查看这里),现在我们来看看ES2022 会带来什么有意思的新特性

在本文中中将介绍并解释在规范的最新草案中已被接受的提案

注:每个特性提案都遵循一个过程,在这个过程中,它经历了不同的阶段,直到stage 4,这表明新增功能已准备好包含在正式的 ECMAScript 标准中,并将包含在最快的实用标准修订版中。以下功能已经完成,处于stage 4并已添加到ECMAScript 最新草案中

声明类的字段

到目前为止,在ES规范中,类的字段定义和初始化是在类的构造函数中完成的。但是在新的提案中,类字段可以在类的顶层被定义和初始化

私有方法和字段

#前缀来定义类的私有方法和字段。

类的静态公共方法和字段

在之前的类的字段私有方法提案的基础上,为JavaScript类增加了静态公共字段静态私有方法静态私有字段的特性。

正则匹配索引

该提案提供了一个新的/dflag,以获得关于输入字符串中每个匹配的开始和索引位置结束的额外信息。

举个例子:

注:包含 begin,但不包含 end

Top-level await

顶层的await允许在异步函数之外使用await关键字。这个提案允许模块当做大型异步函数,所以这些ECMAScript模块可以等待资源加载,这样其他导入这些模块的模块在开始执行自己的代码之前也要等待资源加载完再去执行

检测私有字段

当我们试图访问一个没有被声明的公共字段时,会得到未定义的结果,同时访问私有字段抛出一个异常。我们根据这两个行为来判断是否含有公共字段和私有字段。但是这个建议引入了一个更有趣的解决方案,它包括使用in操作符如果指定的属性/字段在指定的对象/类中,则返回真,并且也能判断私有字段

在所有内置的可索引数据上新增.at()方法

新增一个新的数组方法,通过给定的索引来获取一个元素。当给定的索引为正数时,这个新方法的行为与使用括号符号的访问相同,但是当我们给定一个负整数的索引时,它就像python的 "负数索引 "一样工作,这意味着at()方法以负整数为索引,从数组的最后一项往后数。所以该方法可以被执行为array.at(-1),它的行为与array[array.length-1]相同,在下面的例子中可以看到

Object.hasOwn(object, property)

简单讲就是使用Object.hasOwn来替代Object.prototype.hasOwnProperty.call(太长了,不好看)

ECMAScript类静态初始化块

类静态块提议提供了一种优雅的方式,在类声明/定义期间评估静态初始化代码块,可以访问类的私有字段

注:Typescript4.4也做了支持

参考文献

webpack

Loader

可能你乍一想好像不太容易理解,那你可以做一个假设:假设我们在开发页面上的某个局部功能时,需要用到一个样式模块和一个图片文件。如果你还是将这些资源文件单独引入到 HTML 中,然后再到 JS 中添加对应的逻辑代码。试想一下,如果后期这个局部功能不用了,你就需要同时删除 JS 中的代码和 HTML 中的资源文件引入,也就是同时需要维护这两条线。而如果你遵照 Webpack 的这种设计,所有资源的加载都是由 JS 代码控制,后期也就只需要维护 JS 代码这一条线了。

所以说,通过 JavaScript 代码去引入资源文件,或者说是建立 JavaScript 和资源文件的依赖关系,具有明显的优势。因为 JavaScript 代码本身负责完成整个应用的业务功能,放大来说就是驱动了整个前端应用,而 JavaScript 代码在实现业务功能的过程中需要用到样式、图片等资源文件。如果建立这种依赖关系:

一来逻辑上比较合理,因为 JS 确实需要这些资源文件配合才能实现整体功能;

二来配合 Webpack 这类工具的打包,能确保在上线时,资源不会缺失,而且都是必要的。

最后说一句题外话,学习新事物不是说学会它的所有用法你就能提高,因为这些照着文档操作基本上谁都可以做到,很多时候它的**才是突破点。能搞明白新事物为什么这样设计,基本上你就算出道了。

// ./markdown-loader.js

const marked = require('marked')



module.exports = source => {

  const html = marked(source)

  // const code = `module.exports = ${JSON.stringify(html)}`

  const code = `export default ${JSON.stringify(html)}`

  return code 

}



// ./webpack.config.js

module.exports = {

  entry: './src/main.js',

  output: {

    filename: 'bundle.js',

  },

  module: {

    rules: [

      {

        test: /\.md$/,

        use: [

          'html-loader',

          './markdown-loader'

        ]

      }

    ]

  }

}

loader执行顺序是从后往前,也就是说我们应该把先执行的 markdown-loader 放在后面,html-loader 放在前面

开发一个插件

实说起来也非常简单,Webpack 的插件机制就是我们在软件开发中最常见的钩子机制。

钩子机制也特别容易理解,它有点类似于 Web 中的事件。在 Webpack 整个工作过程会有很多环节,为了便于插件的扩展,Webpack 几乎在每一个环节都埋下了一个钩子。这样我们在开发插件的时候,通过往这些不同节点上挂载不同的任务,就可以轻松扩展 Webpack 的能力。

[Compiler Hooks](https://webpack.js.org/api/compiler-hooks/);

[Compilation Hooks](https://webpack.js.org/api/compilation-hooks/);

[JavascriptParser Hooks](https://webpack.js.org/api/parser/)
// ./remove-comments-plugin.js

class RemoveCommentsPlugin {

  apply (compiler) {

    compiler.hooks.emit.tap('RemoveCommentsPlugin', compilation => {

      // compilation => 可以理解为此次打包的上下文

      for (const name in compilation.assets) {

        // console.log(name)

        console.log(compilation.assets[name].source()) // 输出文件内容

      }

    })

  }

}

通过 Loader 处理特殊类型资源的加载,例如加载样式、图片;
通过 Plugin 实现各种自动化的构建任务,例如自动压缩、自动发布

这里我们先提炼出 Webpack 核心工作过程中的关键环节,明确“查阅”源码的思路:

Webpack CLI 启动打包流程;
载入 Webpack 核心模块,创建 Compiler 对象;
使用 Compiler 对象开始编译整个项目;
从入口文件开始,解析模块依赖,形成依赖关系树;
递归依赖树,将每个模块交给对应的 Loader 处理;
合并 Loader 处理完的结果,将打包结果输出到 dist 目录。

对于 make 阶段后续的流程,这里我们概括一下:

SingleEntryPlugin 中调用了 Compilation 对象的 addEntry 方法,开始解析入口;
addEntry 方法中又调用了 _addModuleChain 方法,将入口模块添加到模块依赖列表中;
紧接着通过 Compilation 对象的 buildModule 方法进行模块构建;
buildModule 方法中执行具体的 Loader,处理特殊资源加载;
build 完成过后,通过 acorn 库生成模块代码的 AST 语法树;
根据语法树分析这个模块是否还有依赖的模块,如果有则继续循环 build 每个依赖;
所有依赖解析完成,build 阶段结束;
最后合并生成需要输出的 bundle.js 写入 dist 目录。

sideEffects

Webpack 4 中新增了一个 sideEffects 特性,它允许我们通过配置标识我们的代码是否有副作用,从而提供更大的压缩空间。

TIPS:模块的副作用指的就是模块执行的时候除了导出成员,是否还做了其他的事情。

这个特性一般只有我们去开发一个 npm 模块时才会用到。因为官网把对 sideEffects 特性的介绍跟 Tree-shaking 混到了一起,所以很多人误认为它们之间是因果关系,其实它们没有什么太大的关系。

我们先把 sideEffects 特性本身的作用弄明白,你就更容易理解为什么说它跟 Tree-shaking 没什么关系了。

sideEffects 作用
我们打开 Webpack 的配置文件,在 optimization 中开启 sideEffects 特性,具体配置如下:

复制代码
// ./webpack.config.js
module.exports = {
mode: 'none',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
optimization: {
sideEffects: true
}
}
TIPS:注意这个特性在 production 模式下同样会自动开启。

那此时 Webpack 在打包某个模块之前,会先检查这个模块所属的 package.json 中的 sideEffects 标识,以此来判断这个模块是否有副作用,如果没有副作用的话,这些没用到的模块就不再被打包。换句话说,即便这些没有用到的模块中存在一些副作用代码,我们也可以通过 package.json 中的 sideEffects 去强制声明没有副作用。

热更新

websocket + jsonp
https://juejin.cn/post/6844904008432222215#heading-10

Webpack-hot-middleware 插件的作用就是提供浏览器和 Webpack 服务器之间的通信机制、且在浏览器端接收 Webpack 服务器端的更新变化。
源码中有这么一段配置,看到这里瞬间想到了在开发时浏览器的 Network 中总是有一个 __Webpack_hmr 的请求,点开查看会看到EventStream 事件流(服务器端事件流,服务器向浏览器推送消息,除了 websocket 全双工通道双向通信方式还有一种 Server-Sent Events 单向通道的通信方法,只能服务器端向浏览器端通过流信息的方式推送消息;页面可以通过 EventSource 实例接收服务器发送事件通知并触发 onmessage 事件),并且以 2s 的频率不停的更新消息内容,每行消息内容都有 ❤️ 的图标,没错这就是一个心跳请求。

image

  1. 总结

其实我们在实现webpack-dev-server热更新的时候,已经把webpack-hot-middleware的功能都实现了。
他们的最大区别就是浏览器和服务器之间的通信方式,webpack-dev-server使用的是websocket,webpack-hot-middleware使用的是eventSource;以及通信过程的事件名不一样了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(构建中,不会触发热更新)和sync(判断是否开始热更新流程)

  1. webpack-dev-server
    Webpack-Dev-Server 就是内置了 Webpack-dev-middleware 和 Express 服务器,以及利用websocket替代eventSource实现webpack-hot-middleware的逻辑

https://juejin.cn/post/6844904020528594957#heading-46

react router学习

本文讨论的React Router版本是V5以上的

react-router和react-router-dom的区别

image

为什么有时候我们看到如下的写法:

写法1:

import {Switch, Route, Router, browserHistory, Link} from 'react-router-dom';

写法2:

import {Switch, Route, Router} from 'react-router';
import {BrowserRouter as Router, Link} from 'react-router-dom';

先简单说下各自的功能:

  • react-router: 实现了路由的核心功能

  • react-router-dom(用于浏览器环境): 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter 组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。

  • react-router-native: 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。

react-router-dom依赖react-router,所以我们使用npm安装依赖的时候,只需要安装相应环境下的库即可,不用再显式安装react-router。基于浏览器环境的开发,只需要安装react-router-dom

如上所说,我们使用react开发web应用,所以只需要安装react-router-dom。

  npm install react-router-dom --save

Router

所有路由器组件的通用低级接口。通常情况下,应用程序会使用其中一个高级别路由器来代替

  • <BrowserRouter>
  • <HashRouter>
  • <MemoryRouter>
  • <NativeRouter>
  • <StaticRouter>

比如📢<Router history="{browserHistory}"></Router>< BrowserRouter></BrowserRouter >表达的是一样的意思

The most common use-case for using the low-level is to synchronize a custom history with a state management lib like Redux or Mobx. Note that this is not required to use state management libs alongside React Router, it’s only for deep integration.

三种路由模式

本文档中的 "history "和 "history对象 "是指history,包,它是React Router仅有的两个主要依赖项之一(除了React本身),它提供了几种不同的实现,用于在各种环境中管理JavaScript的会话历史。

React Router 是建立在 history 之上的。 简而言之,一个 history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件。

常用的 history 有三种形式, 但是你也可以使用 React Router 实现自定义的 history。

  • browserHistory
  • hashHistory
  • createMemoryHistory

Memory history 不会在地址栏被操作或读取。这就解释了我们是如何实现服务器渲染的。同时它也非常适合测试和其他的渲染环境(像 React Native )。

用法

import React from "react";
import {
  Router,
  Switch,
  Route,
  Link
} from "react-router-dom";
import {createMemoryHistory} from 'history'

// This site has 3 pages, all of which are rendered
// dynamically in the browser (not server rendered).
//
// Although the page does not ever refresh, notice how
// React Router keeps the URL up to date as you navigate
// through the site. This preserves the browser history,
// making sure things like the back button and bookmarks
// work properly.

export default function BasicExample() {
  return (
    <Router history={createMemoryHistory()}>
      <div>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
          <li>
            <Link to="/dashboard">Dashboard</Link>
          </li>
        </ul>

        <hr />

        {/*
          A <Switch> looks through all its children <Route>
          elements and renders the first one whose path
          matches the current URL. Use a <Switch> any time
          you have multiple routes, but you want only one
          of them to render at a time
        */}
        <Switch>
          <Route exact path="/">
            <Home />
          </Route>
          <Route path="/about">
            <About />
          </Route>
          <Route path="/dashboard">
            <Dashboard />
          </Route>
        </Switch>
      </div>
    </Router>
  );
}

可点击demo体验,记得复制粘贴部分代码

确实不会引起地址栏的变化

MemoryRouter与StaticRouter的区别

  • MemoryRouter :A that keeps the history of your “URL” in memory (does not read or write to the address bar). Useful in tests and non-browser environments like React Native.

  • A that never changes location.This can be useful in server-side rendering scenarios when the user isn’t actually clicking around, so the location never actually changes. Hence, the name: static. It’s also useful in simple tests when you just need to plug in a location and make assertions on the render outpu

MemoryRouter主要是用于非浏览器环境,它的历史记录是放在内存中的并不会改变地址栏

StaticRouter主要用于服务端渲染, StaticRouter是无状态的,与BrowserRouter的区别就是这样
BrowserRouter:A <Router> that uses the· HTML5 history API (pushState, replaceState and the popstate event) to keep your UI in sync with the URL.

这里我理解的无状态就是o keep your UI in sync with the URL. StaticRouter不需要保持UI同步(以浏览器来说,我们的url变化,UI对应更新,但可能是局部的,会保留部分状态),由于服务端是无状态的,我只要拿到对应的组件渲染出HTML扔给客户端就行

这是我的理解,欢迎其他同学补充

注意:SSR服务端渲染路由是要HTML5 history ,所以这里也是不拿HashRouter比较的原因

官网是用MemoryRouter做测试用,StaticRouter是用于服务端渲染,但是我觉得MemoryRouter也可以用于服务端渲染,因为它本身可以用于非浏览器环境

Vue官方也有提到
image

image

image

// src/server.tsx
import { Routes } from "@/routes";
import { createStore, StoreCtx } from "@/store";
import * as React from "react";
import { StaticRouter } from "react-router";

function ServerRender(req, context, initStore) {
  return props => {
    // hook 要在这、函数组件内部调用
    const value = createStore(initStore);
    return (
      <StoreCtx.Provider value={value}>
        <StaticRouter location={req.url} context={context}>
          <Routes />
        </StaticRouter>
      </StoreCtx.Provider>
    );
  };
}

参考文献

精读《React — 5 Things That Might Surprise You》

1. 使用之前的状态设置状态是不可预测的

状态管理是 React 的基础,虽然useState可能是最常见的钩子,但可能对其实际行为有些不了解。
让我们来看看以下组件:

import React, { useState } from "react";
import "./styles.css";

export default function App() {
  const [counter, setCounter] = useState(0);
  return (
    <div className="App">
      <h1>Counter: {counter}</h1>
      <button
        onClick={() => {
          setCounter(counter + 1);
          setCounter(counter + 1);
        }}
      >
        +
      </button>
    </div>
  );
}

在用户单击按钮后,您希望计数器状态的值是多少?
A. 2
B. 1 ✔️

点击demo

原因是在我们的状态更新期间,我们使用了之前的状态值:setCounter(count + 1)。本质上,setState函数被包装在功能组件闭包中,因此它提供了在该闭包中捕获的值。这意味着当它最终被执行时(setState函数是异步的),它可能持有一个不再相关的状态值。最重要的是,setState 的连续执行可能会导致 React 的调度算法使用相同的事件处理程序处理多个非常快速的状态更新。
在异步函数中设置状态时也可能出现同样的问题:

onClick={() => { 
   setTimout(() => { setCounter(counter + 1); ), 1000); 
}};

但是,不用担心,React 实际上为这个问题提供了一个简单的解决方案——“functional updates”。
setCounter((prevCounter) => prevCounter + 1);

注意:每当你的状态更新依赖于之前的状态时,请务必使用functional updates

// incorrect
const update = useCallback(() => {
   setCounter(counter + 1);
}, [counter]);

// correct ✔️
const update = useCallback(() => {
   setCounter(prevCounter => prevCounter + 1);
}, []);

2.可以使用useRef来存储静态变量

我们习惯于使用 React 中的 ref 机制作为访问元素的 DOM 节点的手段,无论是因为我们需要它来计算其大小、设置焦点状态,或者基本上做任何 React 自然不能做的事情。但是 refs 也可以用于不同的目的——我们可以使用类组件非常容易·实现这一点,但我们不能使用函数式组件——保留一个不会在每次渲染时重新创建的静态变量

点击demo

在函数式组件中我们可以使用ref存储静态变量

3. React 可以强制重新挂载一个组件

写入DOM的成本非常高。这就是为什么我们通常不想重新mount 组件,除非绝对必要。但是有时我们必须,出于各种原因。那么在那种情况下,我们如何告诉 react 卸载并立即重新mount 组件?用一个简单的技巧——为我们的组件提供一个key,并改变它的值。

key prop 是一个特殊的 React 属性

著名的 React 警告

image

key是帮助 React 跟踪元素的东西,即使我们已经改变了它在组件结构中的位置或重新渲染了父级(否则每次渲染都会导致整个组件数组被重新安装,这是不好的性能)。

使用这种机制,我们可以欺骗 React 认为一个组件与其之前的自己不同,并导致它重新挂载。

点击demo

import React, { useEffect, useState, useCallback } from "react";
import "./styles.css";

export default function App() {
  const [key, setKey] = useState(1);
  const [console, setConsole] = useState([]);

  const onLifecycleChange = useCallback((message) => {
    setConsole((prevConsole) => [message, ...prevConsole]);
  }, []);
  return (
    <div className="App">
      <button
        onClick={() => {
          setKey((oldKey) => oldKey + 1);
        }}
      >
        Remount
      </button>
      <ChildComp key={key} onLifecycleChange={onLifecycleChange} />

      <div className="console">
        {console.map((text, i) => (
          <div key={i}>{text}</div>
        ))}
      </div>
    </div>
  );
}

const ChildComp = React.memo(({ onLifecycleChange }) => {
  useEffect(() => {
    onLifecycleChange("mounting ChildComp");
    return () => {
      onLifecycleChange("ummounting ChildComp");
    };
  }, [onLifecycleChange]);

  return <div style={{ marginTop: 10 }}>Child Comp</div>;
});

4.Context不像你期望的那样工作

Context用来解决 “prop drilling” 问题,但是它会带来性能问题,(context value如果是对象)其中一个属性状态发生变化,会导致其它订阅Context的组件都发生更新,所以context一般用于不频繁更新的场景比如(locale和theme)

  • use-context-selector可以解决context带来的性能问题

  • 频繁更新状态(状态共享)的,推荐使用Redux等状态管理工具

import React, { useState, useContext } from "react";
import "./styles.css";

const SomeContext = React.createContext({});

export default function App() {
  const [contextValue, setContextValue] = useState({ name: "John", age: 55 });

  const onChangeAge = (e) => {
    const age = e.target.value;
    setContextValue((prevContextValue) => ({ ...prevContextValue, age }));
  };

  const onChangeName = (e) => {
    const name = e.target.value;
    setContextValue((prevContextValue) => ({ ...prevContextValue, name }));
  };

  return (
    <div className="App">
      <SomeContext.Provider value={contextValue}>
        <Wrapper />
        <input value={contextValue.age} onChange={onChangeAge} />
        <input value={contextValue.name} onChange={onChangeName} />
      </SomeContext.Provider>
    </div>
  );
}

const Wrapper = () => {
  return (
    <div>
      <Name />
      <Age />
    </div>
  );
};

const Name = () => {
  const { name } = useContext(SomeContext);
  console.log("name rendered");
  return <h1>Name: {name}</h1>;
};

const Age = () => {
  const { age } = useContext(SomeContext);
  console.log("age rendered");
  return <h1>Age: {age}</h1>;
};

点击demo

5. React 有一个完整的 API 来处理 children 属性

React为Children属性提供了一系列API

React.Children.toArray(children)
// If you want to use map/forEach:
React.Children.map(children, fn)
React.Children.forEach(children, fn)
React.Children.count(children)

如果你需要在您的组件中强制执行单个子项(我最近注意到 formik 这样做),你可以简单地在您的组件中包含以下行,React 将为你运行检查和错误处理:

React.Children.only(children)
import React from "react";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <Wrapper>
        <h2 style={{ color: "red", margin: 0 }}>Red</h2>
        <h2 style={{ color: "blue" }}>Blue</h2>
        <h2 style={{ color: "green" }}>Green</h2>
      </Wrapper>
      <Wrapper>hello</Wrapper>
    </div>
  );
}

const Wrapper = ({ children }) => {
  const childrenArray = React.Children.toArray(children);
  console.log(childrenArray);
  return (
    <div style={{ border: "1px solid", padding: 20, margin: 5 }}>
      <div>{children}</div>
      <div>Number of children: {React.Children.count(children)}</div>
      <div>
        children type: <strong>{typeof children}</strong>
      </div>
    </div>
  );
};

点击demo

参考文献

基于饿了么骨架屏方案,使用Chrome扩展程序自动生成网页骨架屏

前言

之前写移动端项目的时候,使用骨架屏来解决首屏渲染时出现短暂空白现象,采用了就是饿了么page-skeleton-webpack-plugin方法

但是page-skeleton-webpack-plugin需要puppeteer这个依赖,这玩意会导致整个项目在开发阶段很笨重,而且不是所有的页面都要用到骨架屏,后面找了套方案,决定使用谷歌插件代替puppeteer

Chrome扩展程序生成网页骨架屏

谷歌插件下载

image.png

最新版本下载地址,还未通过谷歌官方审核, PS: 谷歌插件如何安装,自行谷歌

效果图


如何使用

插件参数

同饿了么骨架屏文档保持一样,如下图

骨架屏原理

饿了么骨架屏原理,具体可以看看这篇文章

其实思路很简单,我们可以根据已有的dom结构,覆盖指定上的颜色,这样就大致实现了,不过这套方案有两个难点

  • 如何辨别容器和块
  • css冗余样式和冗余dom结构处理

容器和块

因为不是所有的dom节点都覆盖指定的背景色,有些dom是作为容器,来看饿了么是怎么处理的

		// 将所有拥有 textChildNode 子元素的元素的文字颜色设置成背景色,这样就不会在显示文字了。
		if (ele.childNodes && Array.from(ele.childNodes).some((n) => n.nodeType === Node.TEXT_NODE)) {
			transparent(ele)
		}
		if (checkHasTextDecoration(styles)) {
			ele.style.textDecorationColor = TRANSPARENT
		}
		// 隐藏所有 svg 元素
		if (ele.tagName === 'svg') {
			return svgs.push(ele)
		}
		// ! 针对于容器中如果有background或者img的 如果有需要当做块处理 否则就以容器为处理
		if (EXT_REG.test(styles.background) || EXT_REG.test(styles.backgroundImage)) {
			return hasImageBackEles.push(ele)
		}
		// export const GRADIENT_REG = /gradient/
		// CSS linear-gradient() 函数用于创建一个表示两种或多种颜色线性渐变的图片
		if (GRADIENT_REG.test(styles.background) || GRADIENT_REG.test(styles.backgroundImage)) {
			return gradientBackEles.push(ele)
		}
		if (ele.tagName === 'IMG' || isBase64Img(ele)) {
			return imgs.push(ele)
		}
		if (
			ele.nodeType === Node.ELEMENT_NODE &&
			(ele.tagName === 'BUTTON' || (ele.tagName === 'A' && ele.getAttribute('role') === 'button'))
		) {
			return buttons.push(ele)
		}
		if (
			ele.childNodes &&
			ele.childNodes.length === 1 &&
			ele.childNodes[0].nodeType === Node.TEXT_NODE &&
			/\S/.test(ele.childNodes[0].textContent)
		) {
			return texts.push(ele)
		}
	})(rootElement)

	// ! dom节点 引用类型  这里统一收集对应类型的dom 然后集中用对应的handler处理
	console.log('button数组', buttons)
	console.log('hasImageBackEles', hasImageBackEles)
	console.log(pseudos, gradientBackEles, grayBlocks)
	svgs.forEach((e) => handler.svg(e, svg, cssUnit, decimal))
	texts.forEach((e) => handler.text(e, text, cssUnit, decimal))
	buttons.forEach((e) => handler.button(e, button))
	console.log('imgs数组', imgs)

	hasImageBackEles.forEach((e) => handler.background(e, image))
	imgs.forEach((e) => handler.image(e, image))
	pseudos.forEach((e) => handler.pseudos(e, pseudo))
	gradientBackEles.forEach((e) => handler.background(e, image))
	grayBlocks.forEach((e) => handler.grayBlock(e, button))

解决的方式很简单,根据该dom是否有background、backgroundImage、linear-gradient是否为容器

css冗余样式和冗余dom结构处理

饿了么那套解决方案是有对冗余节点和样式做了处理,但是效果并不是很明显,我们换种思路想,竟然我们已经知道那个节点是容器,那个节点是,那么我们是不是对于容器这种节点做剔除,因为真正展示在页面的是对应的骨架屏块,而对于具体位置,可以使用绝对定位,通过getBoundingClientRect这个api获取

我的前端学习笔记📒

最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读

总结

  • 文中如有错误,欢迎在评论区指正,如果这篇文章帮到了你,欢迎点赞和关注😊
  • 本文首发于掘金,未经许可禁止转载💌

使用React Query作为axios请求库的上层封装

前言

在项目中,通常都需要跟服务端进行异步的数据交互,基本都是用到axios这个库来做请求,嗯,毕竟拥有80k star,明星项目

接下来,我们来回顾下axios在项目中的使用

以查询用户信息为例,我们会这样封装

async function requestUsers(){
  const {data} =await axios.get('/api/users');
  return data;
}

我们再用hooks再封装下这个请求,包括loading等中间态的封装,处理的优雅一点

import React, {useState,useEffect} from 'react';
import axios from 'axios';
function useUsersQuery(){
  const [data,setData] = useState([]);
  const [isLoading,setLoading] = useState(false);
  const [isError,setError] = useState(false)
  useEffect(()=>{
    (async()=>{
       setLoading(true);
       try{
          const {data} = await axios.get('/api/users');
          setData(data);
       } catch((()=>{
          setError(true);
       })
       setLoading(false);
    })()
  })
  return { 
     data,
     isLoading,
     isError
  };
}
function UserList(){
  const {data, isLoading,isError} = useUsersQuery();
  if (isLoading) {
        return <div>loading</div>;
   }
  if (isError) {
        return <div>error</div>;
   }
  return (
    <div>
       {
         data.map((item)=>{
            return <div>{item.name}</div>
         })
       }
    </div>
  )
}

可以看到,我们的项目中基本上是这样封装请求,我们不仅要请求数据,还要处理相应的loading,error这些中间态,这类通用的中间状态处理逻辑可能在不同组件中重复写很多次。

另外,现在的前端项目特别是单页面应用,会使用Flux、Redux、Mobox等状态管理库,会把组件间共享的数据都存放在状态管理库中,这些可以分为两类,一类是用户交互的中间状态,比如isLoading,isClose,modalVisible等等,另外一类就是服务端状态(数据)

我们一般处理的方式都是无差别的存放在全局状态管理上,状态管理库为了兼容异步请求,就有了redux-saga,redux-action这些异步解决方案

其实对于redux等状态管理库,本身是没有异步这个概念,只有mutation这种操作,为了支持异步,硬是强加了异步action这种操作,实际这些异步中间件就是在最后的请求回调透传了dispatch,诸如这些情况,我们不仅将数据一锅炖放在全局状态管理上,写法上也使得项目越来越臃肿了(以至于出现后面rematch、dva方案进行简化),我们有没有想过,服务端的状态就不应该放在全局状态管理上,全局状态管理应该专门处理用户交互的中间状态

接下来,就是引出今天的主角 React Query

React Query

React Query 通常被描述为 React 缺少的数据获取(data-fetching)库,但是从更广泛的角度来看,它使 React 程序中的获取,缓存,同步和更新服务器状态变得轻而易举。

解决了什么问题

服务端状态有以下特点:

  1. 存储在远端,本地无法直接控制

  2. 需要异步 API 来查询和更新

  3. 可能在不知情的情况下,被另一个请求方更改了数据,导致数据不同步

现有的状态管理库(如 Mobx、Redux等)适用于管理客户端状态,但它们并不关心客户端是如何异步请求远端数据的,所以他们并不适合处理异步的、来自服务端的状态。

而 React Query 就是为了解决服务端状态带来的上述问题而出现的,除此之外它还带来了以下特性:

  1. 更方便地控制缓存

  2. 把对于相同数据的多个请求简化成一个

  3. 在后台更新过期数据

  4. 知道数据什么时候会「过期」

  5. 对于数据的变化尽可能快得做出响应

  6. 分页查询和懒加载等请求性能优化

  7. 管理服务器状态的内存和垃圾回收

  8. 通过结构共享(structural sharing)来缓存查询结果

请求中间态处理

 function Todos() {
   const { isLoading, isError, data, error } = useQuery('todos', fetchTodoList)

   if (isLoading) {
     return <span>Loading...</span>
   }

   if (isError) {
     return <span>Error: {error.message}</span>
   }

   // also status === 'success', but "else" logic works, too
   return (
     <ul>
       {data.map(todo => (
         <li key={todo.id}>{todo.title}</li>
       ))}
     </ul>
   )
 }

React query会自动把这些isLoading,isError请求中间态处理好,我们不必写重复逻辑,另外提多一点,对于loading场景的处理,Suspense也支持的不错,特别是局部Loading,简直Nice!

ReactQuery 的状态管理

Fetch, cache and update data in your React and React Native applications all without touching any "global state".

官网对于React Query的简述,注意global state,你会不解,为什么React Query明明是一个请求库,跟数据状态管理又有什么关系,甚至可以处做全局状态管理

那是因为ReactQuery 会在全局维护一个服务端状态树,根据 Query key 去查找状态树中是否有可用的数据,如果有则直接返回,否则则会发起请求,并将请求结果以 Query key 为主键存储到状态树中。

ReactQuery 就将我们所有的服务端状态维护在全局,并配合它的缓存策略来执行数据的存储和更新。借助于这样的特性,我们就可以将所有跟服务端进行交互的数据从类似于 Redux 这样的状态管理工具中剥离,而全部交给 ReactQuery 来管理。

举个例子:

import React from "react";
import { useQuery, queryCache } from "react-query";
import "./styles.css";

export default function App() {
  return (
    <div className="App">
      <h1>Shared state using react-query</h1>
      <Comp1 />
      <Comp2 />
    </div>
  );
}

function useSharedState(key, initialValue) {
  const { data: state } = useQuery(key, () => queryCache.getQueryData(key), {
    initialData: initialValue
  });

  const setState = value => queryCache.setQueryData(key, value);

  return [state, setState];
}

function Comp1() {
  const [count, setCount] = useSharedState("count", 1);

  console.log("comp1 rendered");

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
    </div>
  );
}

function Comp2() {
  const [count, setCount] = useSharedState("count", 2);

  console.log("comp2 rendered");

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>add</button>
    </div>
  );
}

上述方式是可以实现React Query状态管理,但是有性能问题,其实本质还是利用Context透传,我们知道Context处理prop drilling问题,但是有性能问题,详情可查看这篇文章 精读《React — 5 Things That Might Surprise You》

不过令人费解的是官方强调ReactQuery 的状态管理,但是在官网例子并没有给出类似的例子,上述例子还是在官方的github仓库翻到

作者说会在一个讲座分析,后面我再深入研究,先留个坑

参考文献

typescript中的extends关键字

前言

extends关键字在TS编程中出现的频率挺高的,而且不同场景下代表的含义不一样,特此总结一下:

  • 表示继承/拓展的含义
  • 表示约束的含义
  • 表示分配的含义

基本使用

extends是 ts 里一个很常见的关键字,同时也是 es6 里引入的一个新的关键字。在 js 里,extends一般和class一起使用,例如:

  • 继承父类的方法和属性
class Animal {
  kind = 'animal'
  constructor(kind){
    this.kind = kind;
  }
  sayHello(){
    console.log(`Hello, I am a ${this.kind}!`);
  }
}

class Dog extends Animal {
  constructor(kind){
    super(kind)
  }
  bark(){
    console.log('wang wang')
  }
}

const dog = new Dog('dog');
dog.name; //  => 'dog'
dog.sayHello(); // => Hello, I am a dog!

这里 Dog 继承了父类的 sayHello 方法,因为可以在 Dog 实例 dog 上调用。

  • 继承某个类型

在 ts 里,extends除了可以像 js 继承值,还可以继承/扩展类型:

 interface Animal {
   kindstring;
 }

 interface Dog extends Animal {
   bark()void;
 }
 // Dog => { name: string; bark(): void }

泛型约束

在书写泛型的时候,我们往往需要对类型参数作一定的限制,比如希望传入的参数都有 name 属性的数组我们可以这么写:

function getCnames<T extends { namestring }>(entitiesT[]):string[] {
  return entities.map(entity => entity.cname)
}

这里extends对传入的参数作了一个限制,就是 entities 的每一项可以是一个对象,但是必须含有类型为stringcname属性。再比如,redux 里 dispatch 一个 action,必须包含 type属性:

interface Dispatch<T extends { typestring }> {
  (actionT)T
}

条件类型与高阶类型

SomeType extends OtherType ? TrueType : FalseType;

When the type on the left of the extends is assignable to the one on the right, then you’ll get the type in the first branch (the “true” branch); otherwise you’ll get the type in the latter branch (the “false” branch).

extends还有一大用途就是用来判断一个类型是不是可以分配给另一个类型,这在写高级类型的时候非常有用,举个 🌰:

  type Human = {
    namestring;
  }
  type Duck = {
    namestring;
  }
  type Bool = Duck extends Human ? 'yes' : 'no'; // Bool => 'yes'

在 vscode 里或者 ts playground 里输入这段代码,你会发现 Bool 的类型是'yes'。这是因为 Human 和 Duck 的类型完全相同,或者说 Human 类型的一切约束条件,Duck 都具备;换言之,类型为 Human 的值可以分配给类型为 Duck 的值(分配成功的前提是,Duck里面得的类型得有一样的),反之亦然。需要理解的是,这里A extends B,是指类型A可以分配给类型B,而不是说类型A是类型B的子集。 稍微扩展下来详细说明这个问题:

  type Human = {
    namestring;
    occupationstring;
  }
  type Duck = {
    namestring;
  }
  type Bool = Duck extends Human ? 'yes' : 'no'; // Bool => 'no'

当我们给Human加上一个occupation属性,发现此时Bool'no',这是因为 Duck 没有类型为stringoccupation属性,类型Duck不满足类型Human的类型约束。因此,A extends B,是指类型A可以分配给类型B,而不是说类型A是类型B的子集,理解extends在类型三元表达式里的用法非常重要。

继续看示例

  type A1 = 'x' extends 'x' ? string : number; // string
  type A2 = 'x' | 'y' extends 'x' ? string : number; // number
  
  type P<T> = T extends 'x' ? string : number;
  type A3 = P<'x' | 'y'> // ?

A1和A2是extends条件判断的普通用法,和上面的判断方法一样。

P是带参数T的泛型类型,其表达式和A1,A2的形式完全相同,A3是泛型类型P传入参数'x' | 'y'得到的类型,如果将'x' | 'y'带入泛型类的表达式,可以看到和A2类型的形式是完全一样的,那是不是说明,A3和A2的类型就是完全一样的呢?

有兴趣可以自己试一试,这里就直接给结论了

  type P<T> = T extends 'x' ? string : number;
  type A3 = P<'x' | 'y'>  // A3的类型是 string | number

这涉及到分配条件类型(Distributive Conditional Types)的概念

When conditional types act on a generic type, they become distributive when given a union type

对于使用extends关键字的条件类型(即上面的三元表达式类型),如果extends前面的参数是一个泛型类型,当传入该参数的是联合类型,则使用分配律计算最终的结果。分配律是指,将联合类型的联合项拆成单项,分别代入条件类型,然后将每个单项代入得到的结果再联合起来,得到最终的判断结果。

If we plug a union type into ToArray, then the conditional type will be applied to each member of that union.

还是用上面的例子说明

  type P<T> = T extends 'x' ? string : number;
  type A3 = P<'x' | 'y'>  // A3的类型是 string | number

该例中,extends的前参为T,T是一个泛型参数。在A3的定义中,给T传入的是'x'和'y'的联合类型'x' | 'y',满足分配律,于是'x'和'y'被拆开,分别代入P<T>

P<'x' | 'y'> => P<'x'> | P<'y'>

'x'代入得到

'x' extends 'x' ? string : number => string

'y'代入得到

'y' extends 'x' ? string : number => number

然后将每一项代入得到的结果联合起来,得到string | number

总之,满足两个要点即可适用分配律:第一,参数是泛型类型,第二,代入参数的是联合类型

  • 特殊的never
  // never是所有类型的子类型
  type A1 = never extends 'x' ? string : number; // string

  type P<T> = T extends 'x' ? string : number;
  type A2 = P<never> // never

上面的示例中,A2和A1的结果竟然不一样,看起来never并不是一个联合类型,所以直接代入条件类型的定义即可,获取的结果应该和A1一直才对啊?

实际上,这里还是条件分配类型在起作用。never被认为是空的联合类型,也就是说,没有联合项的联合类型,所以还是满足上面的分配律,然而因为没有联合项可以分配,所以P<T>的表达式其实根本就没有执行,所以A2的定义也就类似于永远没有返回的函数一样,是never类型的。

  • 防止条件判断中的分配
  type P<T> = [T] extends ['x'] ? string : number;
  type A1 = P<'x' | 'y'> // number
  type A2 = P<never> // string

在条件判断类型的定义中,将泛型参数使用[]括起来,即可阻断条件判断类型的分配,此时,传入参数T的类型将被当做一个整体,不再分配。

在高级类型中的应用

  • Exclude

Exclude是TS中的一个高级类型,其作用是从第一个联合类型参数中,将第二个联合类型中出现的联合项全部排除,只留下没有出现过的参数。

示例:

type A = Exclude<'key1' | 'key2', 'key2'> // 'key1'

Exclude的定义是

type Exclude<T, U> = T extends U ? never : T

这个定义就利用了条件类型中的分配原则,来尝试将实例拆开看看发生了什么:

type A = `Exclude<'key1' | 'key2', 'key2'>`

// 等价于

type A = `Exclude<'key1', 'key2'>` | `Exclude<'key2', 'key2'>`

// =>

type A = ('key1' extends 'key2' ? never : 'key1') | ('key'2 extends 'key2' ? never : 'key2')

// =>

// never是所有类型的子类型
type A = 'key1' | never = 'key1'
  • Extract

高级类型Extract和上面的Exclude刚好相反,它是将第二个参数的联合项从第一个参数的联合项中提取出来,当然,第二个参数可以含有第一个参数没有的项。

下面是其定义和一个例子,有兴趣可以自己推导一下

type Extract<T, U> = T extends U ? T : never
type A = Extract<'key1' | 'key2', 'key1'> // 'key1'
  • Pick

extends的条件判断,除了定义条件类型,还能在泛型表达式中用来约束泛型参数

// 高级类型Pick的定义
type Pick<T, K extends keyof T> = {
    [P in K]: T[P]
}

interface A {
    name: string;
    age: number;
    sex: number;
}

type A1 = Pick<A, 'name'|'age'>
// 报错:类型“"key" | "noSuchKey"”不满足约束“keyof A”
type A2 = Pick<A, 'name'|'noSuchKey'>

Pick的意思是,从接口T中,将联合类型K中涉及到的项挑选出来,形成一个新的接口,其中K extends keyof T则是用来约束K的条件,即,传入K的参数必须使得这个条件为真,否则ts就会报错,也就是说,K的联合项必须来自接口T的属性。

以上就是ts中 extends 关键字的常用场景。

参考文献

Vue3.2发布的新特性

原文地址:https://blog.vuejs.org/posts/vue-3.2.html

image

主要新增了两个单文件特性

  • <script setup> 编译时的语法糖 可以提高效率
  • <style> v-bind 在`单文件组件`中可以让style标签的样式获取到script标签的变量值(很神奇!)

Web Components

Vue也提供了一个apidefineCustomElement 来创建native custom elements,也就是创建Web Components

image

其他就是一些性能提升

由于 @basvanmeurs 的出色工作,3.2 对 Vue 的反应性系统进行了一些重大的性能改进。具体如下:

模板编译器也得到了一些改进:

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.