交流学习联系:vnues123(wechat)
vnues / blog Goto Github PK
View Code? Open in Web Editor NEW收录blog
收录blog
交流学习联系:vnues123(wechat)
在全局变量的声明文件中,我们不能使用import export 不然就会被认为是默认 那么在全局变量的声明文件中需要引入对应的依赖的时候 如何解决呢 这时候就需要三斜线命令了
早期没有import export模块化
都是declare+namespace
再用三斜线导入依赖
但是现在 namespace 类似,三斜线指令也是 ts 在早期版本中为了描述模块之间的依赖关系而创造的语法。随着 ES6 的广泛应用,现在已经不建议再使用 ts 中的三斜线指令来声明模块之间的依赖关系了。
但是在声明文件中,它还是有一定的用武之地。
也就是你如果想用三斜线命令导入 该导入的文件就不能具有import export 直接用import直接导入依赖吧
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 (解释为什么需要依赖)
CSS Scroll Snap是CSS中一个独立的模块,可以让网页容器滚动停止的时候,自动平滑定位到指定元素的指定位置,包含scroll-*以及scroll-snap-*等诸多CSS属性。
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,可以体验效果了,于是赶快尝鲜,了解下相关的细节。
对于绝大多数都布局,我们并不要求非要比例固定,但是有一种情况例外,那就是图片,因为图片原始尺寸它是固定的。在传统的固定宽度的布局下,我们会通过给图片设定具体的宽度和高度值,来保证我们的图片占据区域稳固;但是在移动端或者在响应式开发情况下,图片最终展现的宽度很可能是不确定的,
例如手机端的一个通栏广告,iPhone7下宽度是375,iPhone7 Plus下是414,还有360等尺寸,此时需要的不是对图片进行固定尺寸设定,而是比例设定`。
banner图可能是按照比例设置的,这时候如何按照比例固定图片呢,利用padding来做,因为padding的百分比是参照宽度的
mixin可以让你制作一些你想在整个网站上重复使用的CSS声明组。你甚至可以传入数值,使你的混合器更加灵活。
Dan 开门见山,丢出了我们业务开发中需要权衡三个点:体验(user experience)、可维护性(maintenance)、性能(performance),然后用一个例子来说明为什么这三个点很难权衡。
这是一个很常见的组件化组合,问题在于每个组件都需要不同的数据,但是就体验而言我们更希望这些组件的渲染尽量同时,而且如果关注性能的话,我们也会考虑并行的去 fetch 数据,于是我们通常会 fetch 逻辑放到顶层,然后通过 Props 或者 Context 传递下去。
这样会把可维护性变差,除了看起来恶心,每个组件从逻辑上就不那么解耦了,我们于是会考虑每个组件自己处理 fetch 逻辑。
这又会让体验变差,因为浏览器从服务端 fetch 数据是比较贵的 IO,抽象一下就是下面这样:
我们之所以需要从服务端 fetch 数据,是因为我们把所有渲染操作放到了客户端,那如果我们把部分渲染逻辑放服务端呢?
于是就有了 React Server Components!
总结:Server Component解决的痛点就是
Server Component解决的痛点就是项目存在瀑布流请求,导致用户体验差,如果我们把组件放在服务端执行,数据请求会非常快
容器组件才能在服务端执行(Server端并不能处理交互,交互还是得交给客户端处理),有交互和State状态的组件只能跑在客户端
Server Component传递到客户端组件的数据,是可以经过序列化的(用于网络传输)(
比如已经转换好后的jsx)
Server Component是0 bundle,打包的时候不会被引入到客户端
本地可以看到没有Server端的文件
6.Server Component和Suspense是互补的,Server Component是让组件在服务端运行,这样数据请求非常快,Suspense是局部水合,可以形成互补
我总感觉首次渲染后,后面只进行数据请求,不涉及序列化的“指令”(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"}}]}]]}]
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
JSX 语法糖允许前端开发者使用我们最为熟悉的类 HTML 标签语法来创建虚拟 DOM,在降低学习成本的同时,也提升了研发效率与研发体验。
当组件更新时,会再次通过调用 render 方法生成新的虚拟 DOM,然后借助 diff定位出两次虚拟 DOM 的差异,从而针对发生变化的真实 DOM 作定向更新。
componentWillMount 会在执行 render 方法前被触发,一些同学习惯在这个方法里做一些初始化的操作,但这些操作往往会伴随一些风险或者说不必要性(这一点大家先建立认知,具体原因将在“03 课时”展开讲解)。
组件的更新分为两种:一种是由父组件更新触发的更新;另一种是组件自身调用自己的 setState 触发的更新。这两种更新对应的生命周期流程如下图所示:
componentReceiveProps 并不是由 props 的变化触发的,而是由父组件的更新触发的,这个结论,请你谨记。
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 名字上也能够略窥一二。原则上来说,它能做且只能做这一件事。
细说生命周期“废旧立新”背后的思考
在 Fiber 机制下,render 阶段是允许暂停、终止和重启的。当一个任务执行到一半被打断后,下一次渲染线程抢回主动权时,这个任务被重启的形式是“重复执行一遍整个任务”而非“接着上次执行到的那行代码往下走”。这就导致 render 阶段的生命周期都是有可能被重复执行的。
经常做算法题的人都知道,OJ 中相对理想的时间复杂度一般是 O(1) 或 O(n)。当复杂度攀升至 O(n2) 时,我们就会本能地寻求性能优化的手段,更不必说是人神共愤的 O(n3) 了!我们看不下去,React 自然也看不下去。React 团队结合设计层面的一些推导,总结了以下两个规律, 为将 O (n3) 复杂度转换成 O (n) 复杂度确立了大前提:
若两个组件属于同一个类型,那么它们将拥有相同的 DOM 树形结构;
处于同一层级的一组子节点,可用通过设置 key 作为唯一标识,从而维持各个节点在不同渲染过程中的稳定性。
除了这两个“板上钉钉”的规律之外,还有一个和实践结合比较紧密的规律,它为 React 实现高效的 Diff 提供了灵感:DOM 节点之间的跨层级操作并不多,同层级操作是主流。
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。
我们先来看下三个阶段各自有哪些特征(以下特征翻译自上图)。
render 阶段:纯净且没有副作用,可能会被 React 暂停、终止或重新启动。
pre-commit 阶段:可以读取 DOM。
commit 阶段:可以使用 DOM,运行副作用,安排更新。
总的来说,render 阶段在执行过程中允许被打断,而 commit 阶段则总是同步执行的。
因为虽然 props 本身是不可变的,但 this 却是可变的,this 上的数据是可以被修改的,this.props 的调用每次都会获取最新的 props,而这正是 React 确保数据实时性的一个重要手段。
多数情况下,在 React 生命周期对执行顺序的调控下,this.props 和 this.state 的变化都能够和预期中的渲染动作保持一致。但在这个案例中,我们通过 setTimeout 将预期中的渲染推迟了 3s,打破了 this.props 和渲染动作之间的这种时机上的关联,进而导致渲染时捕获到的是一个错误的、修改后的 this.props。这就是问题的所在。
Fiber 架构核心:“可中断”“可恢复”与“优先级”
diff算法跟新DOM逻辑
Vue基于snabbdom库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边对比,边更新DOM。
React主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM
setState --> 得到 vdom树(基于pull或者push的模式不同)
场景:保持 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>
插槽(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 还是不同的
在接下来所有的 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>
<!-- 父组件模板 -->
<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
堆排序整个流程可以总结为:
上浮下沉
很多同学可能会想到这样一种解决,我把数组全部排序好,这样就可以拿到第k大的元素,这样是一种解法,但是我们是需要第K大的元素,
不一定要全部排序好再去拿,只针对部分元素进行排序
,这样的复杂度显然可以降低的
也就是可以转化为:使用堆排序来解决这个问题——建立一个大顶堆,做k−1 次删除操作后,堆顶元素就是我们要找的答案(堆排序过程中,不全部下沉,下沉
nums.length-k+1,然后堆顶可以拿到我们top k答案了)
堆排序是利用 堆 这种 数据结构 而设计的一种排序算法,它是一种选择排序,最坏 、最好、平均时间复杂度均为 O(nlogn)
,它是不稳定排序。
注意因为完全二叉树的性质,可以用数组表示对应的树结构(所以,堆排序过程中,你是看不到树这数据结构的,用数组进行映射了),这叫
顺序存储
堆是具有以下性质的完全二叉树:
大顶堆:每个节点的值都 大于或等于 其左右孩子节点的值
注:没有要求左右值的大小关系
小顶堆:每个节点的值都 小于或等于 其左右孩子节点的值
举例说明:
对堆中的节点按层进行编号,映射到数组中如下图
大顶堆特点: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 开始
将待排序序列构造成一个大顶堆
注意:这里使用的是数组,而不是一颗二叉树
此时:整个序列的 最大值就是堆顶的根节点
将其 与末尾元素进行交换,此时末尾就是最大值
然后将剩余 n-1
个元素重新构造成一个堆,这样 就会得到 n 个元素的次小值。如此反复,便能的得到一个有序序列。
对数组 4,6,8,5,9
进行堆排序,将数组升序排序。
将给定无序序列构造成一个大顶堆。
叶节点不用调整,第一个非叶子节点 arr.length/2-1 = 5/2-1 = 1
,也就是 元素为 6 的节点。
比较时:先让 5 与 9 比较,得到最大的那个,再和 6 比较,发现 9 大于 6,则调整他们的位置。
[4,9,8]
中,9 元素最大,则 4 和 9 进行交换[4,5,6]
结构混乱,将其继续调整。[4,5,6]
中 6 最大,将 4 与 6 进行调整。此时,就将一个无序序列构造成了一个大顶堆。
将堆顶元素与末尾元素进行交换,使其末尾元素最大。然后继续调整,再将堆顶元素与末尾元素交换,得到第二大元素。如此反复进行交换、重建、交换。
这里想说的几点注意事项(代码实现的关键思路):
第一步构建初始堆:是自底向上构建,从最后一个非叶子节点开始。
第二步就是下沉操作
让尾部元素与堆顶元素交换,最大值被放在数组末尾,并且缩小数组的length,不参与后面大顶堆的调整
第三步就是调整
:是从上到下,从左到右,因为堆顶元素下沉到末尾了,要重新调整这颗大顶堆
官方的代码模板我参考了下,比一些书籍写的都好记,所以可以参考作为堆排序的模板
/**
* @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;的条件就行
https://zhuanlan.zhihu.com/p/405456997
这样配置的原因是:target 下设置我们业务项目所需要支持的最低环境配置,useBuiltIns 设置为 entry,将最低环境不支持的所有 polyfill 都引入到入口文件(即使你在你的业务代码中并未使用)。这是一种兼顾最终打包体积和稳妥的方式,为什么说稳妥呢,因为我们很难保证引用的三方包有处理好 polyfill 这些问题。
当然如果你能充分保证你的三方依赖 polyfill 处理得当,那么也可以把 useBuiltIns 设置为 usage。
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"chrome": "58" // 按自己需要填写
},
"useBuiltIns": "entry",
"corejs": {
"version": 3,
"proposals": true
}
}
]
],
"plugins": []
}
(W3C CSS 2.1 规范中的一个概念,它是一个独立容器,决定了元素如何对其内容进行定位,以及与其他元素的关系和相互作用。)
一个页面是由很多个 Box 组成的,元素的类型和 display 属性,决定了这个 Box 的类型。
不同类型的 Box,会参与不同的 Formatting Context(决定如何渲染文档的容器),因此Box内的元素会以不同的方式渲染,也就是说BFC内部的元素和外部的元素不会互相影响。
BFC 规定了内部的 Block Box 如何布局。
整理下npm中版本号范围规则,当备忘录
See semver for more details about specifying version ranges.
version Must be greater than version
=version etc
{
"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
主版本号.次版本号.修补版本号
必须匹配某个版本
如:1.1.2,表示必须依赖1.1.2版
必须大于某个版本
如:>1.1.2,表示必须大于1.1.2版
可大于或等于某个版本
如:>=1.1.2,表示可以等于1.1.2,也可以大于1.1.2版本
必须小于某个版本
如:<1.1.2,表示必须小于1.1.2版本
可以小于或等于某个版本
如:<=1.1.2,表示可以等于1.1.2,也可以小于1.1.2版本
大概匹配某个版本
如果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
兼容某个版本
版本号中最左边的非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的位置表示任意版本
如:1.2.x,表示可以1.2.0,1.2.1,.....,1.2.n
任意版本,""也表示任意版本
如:*,表示>=0.0.0的任意版本
大于等于version1,小于等于version2
如:1.1.2 - 1.3.1,表示包括1.1.2和1.3.1以及他们件的任意版本
满足range1或者满足range2,可以多个范围
如:<1.0.0 || >=2.3.1 <2.4.5 || >=2.5.2 <3.0.0,表示满足这3个范围的版本都可以
还有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]
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 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 标准,我建议在项目的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
在我们的例子中,我们正在构建一个多包设计系统。如果开发人员想在设计系统中创建一个新的组件,但在发布之前也要在本地客户端应用程序中进行测试,他们可以通过使用yarn的链接命令来实现。
假设我们想在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 的基础上增加了实用功能,用于处理多个包。yarn wrospace使得所有的依赖关系可以一起安装,使得缓存和安装速度更快。它让我们可以通过一个命令轻松地在NPM上发布依赖关系,当依赖关系的版本发生变化时,自动更新兄弟依赖关系的package.json,一般来说,安装、版本管理和发布都是一种无痛的体验。
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;
除了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注明类型,会自动推断):
// 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! <---注意这里
首先我们从官方的定义中可以看出,对 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 类型,这样之后我们不需要每次都重复写复杂的类型定义了。
到这里,我们了解到:
这是官网得到的解释,但是从类型编程
的角度还可以认为,type具有定义util type
(我的理解就是js util function
)的能力
// 声明了一个Utility Types
type ParamType<T> = T extends (...args: infer P) => any ? P : T;
// 调用
ParamType<string>
什么是模块联邦
先引用一下其他人的总结:
模块联邦让 Webpack 达到了线上 Runtime 的效果,让代码直接在项目间利用 CDN 直接共享,不再需要本地安装 Npm 包、构建再发布了!我们知道 Webpack 可以通过 DLL 或者 Externals 做代码共享时 Common Chunk,但不同应用和项目间这个任务就变得困难了,我们几乎无法在项目之间做到按需热插拔。模块联邦是 Webpack5 新内置的一个重要功能,可以让跨应用间真正做到模块共享。
它支持直接将一个应用的包应用于另一个应用,同时具备整体应用一起打包的公共依赖抽取能力。npm 包方式和模块联邦区别如下图:
如何使用
联邦模块有两个主要概念:Host(消费其他 Remote)和 Remote(被 Host 消费), 每个项目可以是 Host 也可以是 Remote。模块联邦本身是一个普通的 Webpack 插件 ModuleFederationPlugin,插件有几个重要参数:
name 当前应用名称,需要全局唯一,必填。
remotes 可以将 Remote 中的 exposes 配置的模块映射到当前项目中,作为 Host 时必填。
exposes 表示导出的模块,只有在此申明的模块才可以作为远程依赖被使用,作为 Remote 时必填。
shared 主要是用来避免项目出现多个公共依赖,若是配置了这个属性,webpack在加载的时候会先判断本地应用是否存在对应的包,若是不存在,则加载远程应用的依赖包。
链接: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
微前端现有的落地方案可以分为三类,自组织模式、基座模式以及模块加载模式。
与基座模式相比,模块加载模式没有中心容器,这就意味着,我们可以将任意一个微应用当作项目入口,整个项目的微应用与微应用之间相互串联,打破项目的固定加载模式,彻底释放项目的灵活机动性,这样的模式,也被称为去中心化模式。
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:
Workflows工作流是组织一系列Jobs(用于编排所有 job),规定哪些分支可以触发哪些jobs
Jobs包含steps 具体执行哪些步骤
org config组件化 2.1版本才有
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
在谈到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函数了 就避免循环了
父子组件之间通信:父组件可以通过构造函数将数据传递给子组件,并通过子组件的属性来接收和使用这些数据。子组件可以通过回调函数将数据传递回父组件。
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回调函数,将新的消息传递给父组件并更新界面。
这样,父子组件之间就通过构造函数和属性实现了数据的传递和更新。
为什么需要引入逆变、协变和双向协变这些概念
因为考虑到类型兼容
,详情参考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)
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 是类型系统中很重要的一个概念,当你把一个变量赋值给另一个变量时,就要检查这两个变量的类型之间是否可以相互赋值。
let animal: Animal
let dog: Dog
animal = dog // ✅ok
dog = animal // ❌error! animal 实例上缺少属性 'bark'
如何处理类型兼容呢?通过协变和逆变原则
协变与逆变(covariance and contravariance)是在计算机科学中,描述具有父/子型别关系的多个型别通过型别构造器、构造出的多个复杂型别之间是否有父/子型别关系的用语。
维基百科上关于协变和逆变的解释有点晦涩难懂。这里,我们用更通俗一点的语言来表述:
// 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 来修复这个问题。设置之后,函数参数就不再是双向协变的了。
in在ts中用处还是非常广泛,但官方文档并没有解释的很清楚,借此总结一下
interface A {
x: number;
}
interface B {
y: string;
}
let q: A | B = ...;
if ('x' in q) {
// q: A
} else {
// q: B
}
// 例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、样式计算、布局、图层、绘制、光栅化、合成和显示。下面我用一张图来总结下这整个渲染流程
结合上图,一个完整的渲染流程大致可总结为如下:
渲染进程将 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解析,关于详细信息我们会在后面的章节中详细介绍。
最近写的
移动端业务
经常跟弹框
打交道,偶尔处理对于多个弹框的显示
问题也是捉襟见肘
,特别是产品经常改需求
,那么有没有一种优雅的解决方案去处理上面这种问题
,或者说,淘宝
、拼多多
等是怎么处理这种问题的
由于项目一开始没有做好规划
或者说一开始就不是你维护的
,导致首页的弹窗组件可能放了十多个甚至更多
,不仅是首页有,首页内又引入了十多个个子组件
,这些子组件内也有弹框
,另外子组件的子组件也可能存在弹框
,每个弹窗都有对应的一组控制显隐逻辑
,但是你不可能让所有符合显示条件的弹窗都全都一下子在首页弹出来,如何有顺序的管理这些弹框是重中之重
的事情
上面这么分析可能有同学还是不了解这个
业务痛点
,我们举个例子,假设首页页面有个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-弹框的唯一idname
: 弹框名称-可以根据名称很快找到该页面上的弹框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 () {
// ...
}
}
实现这个弹框类
,我们来拆分实现这四个方法
就行了
根据以上思路,
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值
我们以弹框的
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
这个方法很简单,单纯的用来判断当前页面的弹框
是否都订阅完成
if (this.modalList.length === Object.values(this.modalFlatMap).length) {
this.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()
}
到上面的步骤,其实我们的
弹框管理类
已经差不多完成了,但是考虑到弹框可能分布在子组件或者孙组件等等
,这时候如果都在每个组件实例化弹框类
,那么他们实际是没有关联的
,此时单例模式就派上用场了
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,执行以下操作就可
git clone git@github.com:vnues/modal-control.git
git checkout feature/first
yarn
yarn serve
第一版的
ModalControl
可以解决我们开发中遇到的场景
,但是我们还要考虑一下复杂场景
接下来,我们来完善我们的弹框类ModalControl
,我们先来分析下需要注意哪些问题吧
一对一的关系
,而是多对一的关系,多个接口都可以控制这个弹框的显示
,这里通过apiFlag
来标识弹框,不再使用name
得益于我们的modalConfig配置
,我们只需要补充一个apiFlag
字段,便可以解决上述问题,是不是很方便,其实后续的复杂场景,也在这里补充字段完善就行
增加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,执行以下操作就可
git clone git@github.com:vnues/modal-control.git
git checkout feature/second
yarn
yarn serve
细心的童鞋可能会发现,竟然第一版和第二版分别实现了
一对一
和多对一
的关系,那么一对多的关系
如何实现呢?也即是多个接口一起决定弹框是否展示
这里我给出两种思路
:
多个接口一起决定弹框是否展示
,我们完全可以在接口层做合并,最终实现出来的效果就是一对一
handler
实现多个接口一起决定弹框是否展示
,个人还是推荐第一种解决方案最近花了点时间把笔记整理到语雀上
了,方便童鞋们阅读
我们平时写的 .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。
目前可以使用Volar可以在单文件组件检测TypeScript
ES2021 或 ES12 在今年夏天早些时候发布(具体的ES2021新特性
,可以查看这里),现在我们来看看ES2022 会带来什么有意思的新特性
。
在本文中中将介绍并解释在规范的最新草案中已被接受的提案
。
注:每个特性提案都遵循一个过程,在这个过程中,它经历了不同的阶段,直到
stage 4
,这表明新增功能已准备好包含在正式的ECMAScript 标准
中,并将包含在最快的实用标准修订版中。以下功能已经完成,处于stage 4
并已添加到ECMAScript 最新草案中。
到目前为止,在ES规范中,类的字段定义和初始化是在类的构造函数中完成的。但是在新的提案中,类字段可以在类的顶层被定义和初始化
用
#前缀
来定义类的私有方法和字段。
该提案提供了一个新的
/d
flag,以获得关于输入字符串中每个匹配的开始和索引位置结束的额外信息。
举个例子:
注:包含 begin,但不包含
end
顶层的await允许在异步函数之外使用
await关键字
。这个提案允许模块当做大型异步函数
,所以这些ECMAScript模块可以等待资源加载,这样其他导入这些模块的模块在开始执行自己的代码之前也要等待资源加载完再去执行
当我们试图访问一个没有被声明的公共字段时
,会得到未定义的结果
,同时访问私有字段
会抛出一个异常
。我们根据这两个行为来判断是否含有公共字段和私有字段
。但是这个建议引入了一个更有趣的解决方案
,它包括使用in操作符
,如果指定的属性/字段在指定的对象/类中,则返回真
,并且也能判断私有字段
新增一个新的数组方法,通过给定的索引来获取一个元素。当给定的索引为正数时,这个新方法的行为与使用括号符号的访问相同,但是当我们给定一个负整数的索引时,它就像python的 "负数索引 "一样工作,这意味着at()方法以负整数为索引,从数组的最后一项往后数。所以该方法可以被执行为array.at(-1),它的行为与array[array.length-1]相同,在下面的例子中可以看到
简单讲就是使用Object.hasOwn
来替代Object.prototype.hasOwnProperty.call
(太长了,不好看)
类静态块提议提供了一种优雅的方式,在类声明/定义期间评估静态初始化代码块,可以访问类的私有字段
可能你乍一想好像不太容易理解,那你可以做一个假设:假设我们在开发页面上的某个局部功能时,需要用到一个样式模块和一个图片文件。如果你还是将这些资源文件单独引入到 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 目录。
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 的频率不停的更新消息内容,每行消息内容都有 ❤️ 的图标,没错这就是一个心跳请求。
其实我们在实现webpack-dev-server热更新的时候,已经把webpack-hot-middleware的功能都实现了。
他们的最大区别就是浏览器和服务器之间的通信方式,webpack-dev-server使用的是websocket,webpack-hot-middleware使用的是eventSource;以及通信过程的事件名不一样了,webpack-dev-server是利用hash和ok,webpack-dev-middleware是build(构建中,不会触发热更新)和sync(判断是否开始热更新流程)
本文讨论的React Router版本是V5以上的
写法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
所有路由器组件的通用低级接口。通常情况下,应用程序会使用其中一个高级别路由器来代替
<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。
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 :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也可以用于服务端渲染,因为它本身可以用于非浏览器环境
// 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 的基础,虽然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);
}, []);
我们习惯于使用 React 中的 ref 机制作为访问元素的 DOM 节点的手段,无论是因为我们需要它来计算其大小、设置焦点状态,或者基本上做任何 React 自然不能做的事情。但是 refs 也可以用于不同的目的——我们可以使用类组件非常容易·实现这一点,但我们不能使用函数式组件——保留一个不会在每次渲染时重新创建的静态变量
。
点击demo
在函数式组件中我们可以使用ref存储静态变量
写入DOM的成本非常高。这就是为什么我们通常不想重新mount 组件,除非绝对必要。但是有时我们必须,出于各种原因。那么在那种情况下,我们如何告诉 react 卸载并立即重新mount 组件?用一个简单的技巧——为我们的组件提供一个key,并改变它的值。
key prop 是一个特殊的 React 属性
著名的 React 警告
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>;
});
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
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
之前写移动端项目的时候,使用骨架屏来解决首屏渲染时出现短暂空白现象,采用了就是饿了么
page-skeleton-webpack-plugin
方法
但是page-skeleton-webpack-plugin
需要puppeteer
这个依赖,这玩意会导致整个项目在开发阶段很笨重
,而且不是所有的页面都要用到骨架屏,后面找了套方案,决定使用谷歌插件
代替puppeteer
最新版本下载地址,还未通过谷歌官方审核, PS: 谷歌插件如何安装,自行谷歌
同饿了么骨架屏文档保持一样,如下图
饿了么骨架屏原理,具体可以看看这篇文章
其实思路很简单,我们可以根据已有的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
是否为容器
饿了么
那套解决方案是有对冗余节点和样式做了处理,但是效果并不是很明显,我们换种思路想,竟然我们已经知道那个节点是容器,那个节点是块
,那么我们是不是对于容器
这种节点做剔除,因为真正展示在页面的是对应的骨架屏块
,而对于具体位置,可以使用绝对定位,通过getBoundingClientRect
这个api获取
最近花了点时间把笔记整理到语雀上
了,方便童鞋们阅读
在项目中,通常都需要跟服务端进行异步的数据交互,基本都是用到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 缺少的数据获取(data-fetching)库,但是从更广泛的角度来看,它使 React 程序中的获取,缓存,同步和更新服务器状态变得轻而易举。
服务端状态有以下特点:
存储在远端,本地无法直接控制
需要异步 API 来查询和更新
可能在不知情的情况下,被另一个请求方更改了数据,导致数据不同步
现有的状态管理库(如 Mobx、Redux等)适用于管理客户端状态,但它们并不关心客户端是如何异步请求远端数据的,所以他们并不适合处理异步的、来自服务端的状态。
而 React Query 就是为了解决服务端状态带来的上述问题而出现的,除此之外它还带来了以下特性:
更方便地控制缓存
把对于相同数据的多个请求简化成一个
在后台更新过期数据
知道数据什么时候会「过期」
对于数据的变化尽可能快得做出响应
分页查询和懒加载等请求性能优化
管理服务器状态的内存和垃圾回收
通过结构共享(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!
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仓库翻到
作者说会在一个讲座分析,后面我再深入研究,先留个坑
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 {
kind: string;
}
interface Dog extends Animal {
bark(): void;
}
// Dog => { name: string; bark(): void }
在书写泛型的时候,我们往往需要对类型参数作一定的限制,比如希望传入的参数都有 name 属性的数组我们可以这么写:
function getCnames<T extends { name: string }>(entities: T[]):string[] {
return entities.map(entity => entity.cname)
}
这里extends
对传入的参数作了一个限制,就是 entities 的每一项可以是一个对象,但是必须含有类型为string
的cname
属性。再比如,redux 里 dispatch 一个 action,必须包含 type
属性:
interface Dispatch<T extends { type: string }> {
(action: T): T
}
SomeType extends OtherType ? TrueType : FalseType;
When the type on
the left
of theextends
isassignable 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 = {
name: string;
}
type Duck = {
name: string;
}
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 = {
name: string;
occupation: string;
}
type Duck = {
name: string;
}
type Bool = Duck extends Human ? 'yes' : 'no'; // Bool => 'no'
当我们给Human
加上一个occupation
属性,发现此时Bool
是'no'
,这是因为 Duck 没有类型为string
的occupation
属性,类型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是所有类型的子类型
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
是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
和上面的Exclude
刚好相反,它是将第二个参数的联合项从第一个参数的联合项中提取出来
,当然,第二个参数可以含有第一个参数没有的项。
下面是其定义和一个例子,有兴趣可以自己推导一下
type Extract<T, U> = T extends U ? T : never
type A = Extract<'key1' | 'key2', 'key1'> // 'key1'
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
关键字的常用场景。
原文地址:https://blog.vuejs.org/posts/vue-3.2.html
Vue也提供了一个apidefineCustomElement
来创建native custom elements,也就是创建Web Components
由于 @basvanmeurs 的出色工作,3.2 对 Vue 的反应性系统进行了一些重大的性能改进。具体如下:
模板编译器也得到了一些改进:
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.