Hi there, I'm fantasticit! 👋
Languages and Tools:
编程技术学习笔记 https://coding.fantasticit.vercel.app
Home Page: https://fantasticit.gitee.io/coding
Languages and Tools:
最近看了不少关于 h5 页面制作工具。端午闲来无事,决定尝试下一个页面搭建工具。效果如下:
gif 录制效果不佳,可以访问以下链接进行体验。
前端开发组件库,完善组件类型,编辑器读取组件完成页面搭建,将页面数据发送至服务端保存。
访问页面,从服务端拉取页面数据,前端渲染页面即可。
.
|____index.tsx
|____plugins ## 组件库管理
|____Editor.tsx ## 编辑器
|____type.ts ## 类型定义
|____components
| |____Pannel ## 左侧组件面板
| |____Preview ## 中间预览面板
| |____Editor ## 组件编辑器实现
| | |____index.tsx
| | |____PropsEditor
| | | |____index.tsx
| | | |____components
| | | | |____SwitchEditor
| | | | |____RadioEditor
| | | | |____ImgEditor
| | | | |____ColorEditor
| | | | |____TextEditor
| | | | |____TextareaEditor
| | | | |____NumberEditor
| | | |____renderEditorItem.tsx
| | | |____UnionEditor.tsx
| | |____FunctionEditor
| | |____SettingEditor
|____renderPage.tsx
既然是可视化页面搭建,那么页面必须可以以某种数据结构进行描述。比如:
{
setting: {
} // 页面设置
components: []; // 页面使用到的组件
}
页面核心是由组件搭建而成的,那么就要定义组件的数据结构。
import React from 'react';
export const Title = ({ title, style }) => {
return <h1>{title}</h1>
};
Title.defaultProps = {
title: '标题',
};
Title.schema = {
title: {
title: '标题',
type: 'text'
},
};
核心可以抽象为:
{
name: 'Title'; // 对应组件名
props: {
}
schema: {
}
}
name
不可能把组件源代码保存到服务端,所以这里只保存组件的名称,前端渲染时,根据该名称找到对应组件渲染即可(类似 Vue.js 的动态组件)
props
React 组件的 props,这里使用 defaultProps
赋值默认值
schema
对应 props
各个属性的描述,用于编辑器针对进行渲染。进行组件编辑,实际上编辑的是组件的 props
,props
改变组件的渲染结果自然改变。为了对 props
进行编辑,需要定义 props
的描述语言,通过 props
描述来进行属性编辑。这里使用如下的 schema
。
{
title: '标题';
type: 'text';
}
对应组件 props.title
,通过 type
可以决定如何渲染编辑器组件。
可能光能渲染组件是不够的,也许需要更多的功能包装,比如埋点。这一类函数本质上也是组件。可以通过 schema
定义进行 props
编辑。举个例子:
import React from 'react';
export const Tracking = ({ op, pageSn, pageElSn, children }) => {
const onClick = () => {
alert('埋点测试:' + op + '_' + pageSn + '_' + pageElSn);
};
return <div onClick={onClick}>{children}</div>;
};
Tracking.defaultProps = {
op: 'click',
pageSn: null,
pageElSn: null
};
Tracking.schema = {
op: {
title: '曝光方式',
type: 'radio',
options: ['click', 'pv']
},
pageSn: {
title: '页面编号',
type: 'number'
},
pageElSn: {
title: '元素编号',
type: 'number'
}
};
Github:传送门
在 client
和 server
分别执行 yarn dev
即可。
今天中午刷掘金沸点时,看到一个 Jerry Menu,看着效果不错,就像学(抄)习(袭)一下。效果图见下:
这里我要学(抄)习(袭)的就是这个菜单效果,这个 dom 结构还是很简单的。
div.blobs
div.circle.main
div.circle.sub.first
div.circle.sub.second
div.circle.sub.last
用CSS美化一下:
.blobs {
display: flex;
justify-content: center;
align-items: center;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.circle {
position: absolute;
width: 90px;
height: 90px;
transform: translate(0, -48px);
background: hsl(337, 70%, 58%);
clip-path: circle(42px at center);
}
.circle.main {
z-index: 2;
}
为了更直接到达目标效果,先不做动画,先把各个菜单的位置写好:
.first {
transform: translate(-100px, -120px);
background: hsl(307, 70%, 58%);
}
.second {
transform: translate(0px, -150px);
background: hsl(277, 70%, 58%);
}
.last {
transform: translate(100px, -120px);
background: hsl(247, 70%, 58%);
}
这时候效果就出来了,大致长这样:
最开始的效果是有交互的,那我们就用JS加一点交互:
const button = document.querySelector(".circle.main");
const circles = document.querySelectorAll(".circle.sub");
button.addEventListener("click", function() {
circles.forEach(element => {
element.classList.toggle("show");
});
});
相应地,CSS也要作出变更:
.first {
transition: transform 0.5s 100ms ease-out;
background: hsl(307, 70%, 58%);
}
.second {
transition: transform 0.5s 300ms ease-out;
background: hsl(277, 70%, 58%);
}
.last {
transition: transform 0.5s 500ms ease-out;
background: hsl(247, 70%, 58%);
}
.first.show {
transform: translate(-100px, -120px);
}
.second.show {
transform: translate(0px, -150px);
}
.last.show {
transform: translate(100px, -120px);
}
这时候效果就差不多了:
但是总感觉差了点什么,粘连效果没了,看一下原作者的效果:
赶紧回头看下了作者的源代码,发现作者加了 .blobs { filter: url(#goo); }
这样的滤镜效果,翻看文档看了下:
CSS滤镜属性,可以在元素呈现之前,为元素的渲染提供一些效果,如模糊、颜色转移之类的。滤镜常用于调整图像、背景、边框的渲染。SVG滤镜资源(SVG Filter Sources)是指以xml文件格式定义的svg滤镜效果集,可以通过URL引入并且通过锚点(#element-id)指定具体的一个滤镜元素。
再设置 filter
滤镜并加上相应的 svg 代码之后,整个 Jerry Menu 的效果就学(抄)习(袭)完了,效果如下:
这里附上 MDN上关于 filter 的文档。
interface T1 {
name: string;
}
interface T2 {
age: number;
}
interface T3 extends T1, T2 {
sex: string;
}
interface Animal {
eat(): void;
}
interface Dog extends Animal {
bite(): void;
}
// 如果前面的类型可以赋值给后面的类型,则为 true
type A = Dog extends Animal ? string : number; // string
type A1 = 'x' extends 'x' ? string : number; // string
type A2 = 'x' | 'y' extnds 'x' ? string : number; // number
type P<T> = T extends 'x' ? string : number;
type A3 = P<'x' | 'y'>; // string | number; 满足分配律,被拆开赋值,type A3 = 'x' extends 'x' | 'y' extends 'x'
type A4 = never extends 'x' ? string : number; // string
type P1<T> = T extends 'x' ? string : number;
type A5 = P1<never>; // never,在条件分配类型中 never 被认为是空的联合类型
type P2<T> = [T] extends ['x'] ? string : number; // 通过 [] 阻止条件分配类型
type A6 = P2<"x" | 'y'>; // number
type A7 = P2<never>; // string,never 被认为是所有类型的子类型
type Exclude<T, U> = T extends U ? never : T;
type B = Exclude<'key1' | 'key2', "key2"> // 相当于 type B = Exclude<key1, key2> | Exclude<key2, key2>
type Extract<T, U> = T extends U ? T : never;
type C = Extract<'key1' | 'key2', 'key2'> // 相当于 type C = Extract<key1, key2> | Extract<key2, key2>
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
interface Point {
x: number;
y: number;
}
interface SetPoint {
(x: number, y: number): void;
}
type Point = {
x: number;
y: number;
};
type SetPoint = (x: number, y: number) => void;
type Name = string;
type X = { x: number };
type Y = { y: number };
type Z = X | Y;
type D = [X, Y];
interface X {
x: number;
}
interface Point extends X {
y: number;
}
-----
type X = { x: number };
type Point = X & { y: number };
----
interface X {
x: number;
}
type Point = X & { y: number };
----
type X = { x: number };
interface Point extends X {
y: number;
}
interface Point {
x: number;
}
interface Point {
y: number;
}
const p: Point = { x: 0, y: 0 };
缓存是性能优化中简单高效的一种方式。优先的缓存策略可以缩短网页请求资源的距离,减少延迟,并且可以缓存文件进行复用,还可以减少带宽,降低网络负荷。
对于一个数据请求来说,可以分为发起请求、后端处理、浏览器响应 3 个阶段,缓存可以在 1、2 两步优化性能。
service worker 是运行在浏览器背后的独立线程。使用 service worker 必须使用 https 传输协议。service worker 可以让用户自由控制缓存哪些文件,如何匹配缓存、如何读取缓存,同时缓存还是持久性的。
memory cache 是内存中的缓存,主要包含当前页面中已经抓取到的资源。
disk cache 是硬盘中的缓存。
Push Cache 是推送缓存,是 HTTP/2 中的内容,当以上 3 种缓存都没有命中时,才会被使用。该缓存在 会话(session)中存在,会话结束就会被释放。
通常浏览器缓存分为 2 种:强缓存 和 协商缓存,并且缓存策略都是通过设置 HTTP Header 实现的。
强缓存:不会向服务器发生请求,直接从缓存中读取资源。在 chrome 控制台可以看到该请求返回 200 的状态码,并且 size 显示 from disk cache 或 from memory cache。
强缓存可以通过设置 2 中 HTTP Header 实现:expires 和 Cache-Control。
expires 是 HTTP 1.0 的产物,cache-control 是 HTTP 1.1 的产物,两者同时存在时,cache-control 优先级高些。
强缓存判断缓存是否超出某个时间或者范围,但是不关心服务器是否已经更新内容。为了获取服务器更新,就需要使用协商缓存。
协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器决定是否使用缓存。主要有 2 中情况:
协商缓存通过 2 中 HTTP Heeader 设置:last-modified 和 etag。
浏览器在第一次访问资源时,服务器返回资源的同时,在 响应头添加 last-modified,值是这个资源在服务器上的最后修改时间。
浏览器下一次请求,服务器检测到 last-modified 的值,再添加个 if-modified-since 值是 last-modified 的值。服务器再次收到请求,会根据 last-modified-since 的值于服务器中这个资源的最后修改时间对比。相同 304,如果 if-modified-since 的值比服务器上资源最后修改时间小,返回新的资源和 200。
last-modified 存在的弊端:
因为根据文件修改时间决定缓存命中有欠缺,所以根据文件内容是否修改, HTTP 1.1 出现了 etag 和 if-none-match。
etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(有服务器生成),只要资源有变化,etag 就会重新生成。
浏览器下一次请求时,只需要把上一次 etag 的值写到 if-none-match 中,服务器再比较 if-none-match 和当前资源的 etag 是否一致。
精度上,etag 优于 last-modified。
Last-Modified的时间单位是秒,如果某个文件在1秒内改变了多次,那么他们的Last-Modified其实并没有体现出来修改,但是Etag每次都会改变确保了精度;如果是负载均衡的服务器,各个服务器生成的Last-Modified也有可能不一致。
强缓存优先于协商缓存,若强缓存生效则直接使用缓存,若不生效使用协商缓存(last-modified/if-modified-since 和 etag/if-none-match)。协商缓存由服务器决定是否使用缓存。使用缓存返回 304,不使用 返回 200 和 新的资源。
cache-control: no-cache
通过设置 no-cache 使浏览器每次都请求服务器,然后配合使用 etag 或者 last-modified 协商缓存。
cache-control: max-age=315360000
通过设置一个很大的过期时间,浏览器就会使用强缓存。而为了解决更新的问题,就需要在文件名(或者路径)中添加 hash, 版本号等动态字符,之后更改动态字符,从而达到更改引用 URL 的目的,让之前的强制缓存失效 (其实并未立即失效,只是不再使用了而已)。
今天,从 MDN 上看到了 SpeechSynthesis
这个API。看了下它的介绍。
网页语音 API 的 SpeechSynthesis
接口是语音服务的控制接口;它可以用于获取设备上关于可用的合成声音的信息,开始、暂停语音,或除此之外的其他命令。
卧槽,这一看不得了啊,这个接口可以调用设备接口读出文字,那这样岂不是就可以在web页面上实现朗读功能。来,试一下:
function speak() {
let synth = window.speechSynthesis;
let voices = synth.getVoices().filter(voice => voice.lang === 'zh-CN');
if (voices.length == 0) return;
let utterThis = new SpeechSynthesisUtterance(document.querySelector('#issue-589242640 > div > div.edit-comment-hide > task-lists > table > tbody > tr > td').textContent);
utterThis.voice = voices[0];
synth.speak(utterThis);
}
可在本博客页面打开控制台,并执行代码,体验一下“朗读功能”。(如果没有朗读,可以再调一次 speak()❤️😄😁)
在控制台打印一下 voices
。
应该是不同的硬件设备,不同的操作系统,可用的 voices 不一样,比如 mac 和 windows。
最后,看一下兼容性还不错。😹💓
本文将学习 createStore,并实现一个简单的 createStore。
接下来就按照 createStore
的内部流程来解读。
createStore 引入的函数有:
createStore接受三个参数:
createStore返回一个 redux store 用以读取 state、触发 action 和 订阅 changes
首先,判断 preloadedState 和 enhancer:
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
所以 enhancer
应是一个 柯里化函数,如果给定了 enhancer
则 直接调用 reducer
和 preloadedState
返回 enhanced
的 store
(当然也是 createstore生成 )。
其次,判断 reducer 是否为函数,不是则报错:
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
紧接着,声明了以下变量:
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
然后 dispatch 初始化 action(dispatch({ type: ActionTypes.INIT })) (这样每个 reducer都会返回自己的初始状态,这样就有效地填充了 初始的 state tree)
最后返回一个对象:
return {
dispatch,
subscribe,
getState
}
dispatch 是唯一的触发 状态变化(state change)的途径,它只接受一个 isPlainObject 的 action,具体流程如下:
action
是否 isPlainObject
,否则报错action.type
,没有则报错isDispatching
,如果是报错(目前无法 dispatch action
)currentReducer
(传入的 reducer
)try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
// 依次调用 listeners
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
subscribe 接收一个 listener
函数,它首先 ensureCanMutateNextListeners
(通过 nextListeners = currentListeners.slice()
保存当前的监听函数 ),当初次 ensures
时,nextListeners
为 [],然后将传入的 listener
加入到 nextListeners
,这样 nextListeners
中便有了 listeners
,这样在dispacth
中便有了 listeners便可以循环调用 listeners
,同时subscribe返回一个包含取消监听函数的对象。
目前可以简单总结下 createStore
返回一个主要包含(我说的主要) getState
、subscribe
和 dispatch
。通过dispatch ation
调用 reducer(currentState, action)
以完成 change state
,那么接下来按照这个思路实现一个简单的 createStore
:
function createStore(reducer, preloadedState) {
let currentState = preloadedState
let listeners = []
function getState() {
return currentState
}
function subscribe(listener) {
listeners.push(listener)
return {
unsubscribe() {
let index = listeners.indexOf(listener)
listeners.splice(index, 1)
}
}
}
function dispatch(action) {
try {
currentState = reducer(currentState, action)
} finally {}
// 遍历监听函数
for (let i = 0; i < listeners.length; i++) {
listeners[i]()
}
return action
}
dispatch({ type: 'INIT' })
return {
getState,
dispatch,
subscribe
}
}
测试以下:
function reducer (state, action) {
switch (action.type) {
case 'ADD':
let count = state.count
count++
return { ...state, count }
default:
return state
}
}
const store = createStore(reducer, { count: 0 })
let listener = store.subscribe(() => console.log('订阅store.count: ', store.getState().count))
store.dispatch({ type: 'ADD' })
store.dispatch({ type: 'ADD' })
listener.unsubscribe()
store.dispatch({ type: 'ADD' })
输出如下:
订阅store.count: 1
订阅store.count: 2
通过阅读 createStore
的源码可以发现这部分的核心主要就是在 reducer
、dispatch
以及 state
,理清楚这些便明白了 createStore
的简单与巧妙。
安装koa2
安装koa-router
koa-router 提供了 .get、.post、.put 和 .del 接口来处理各种请求
这里按照MVC
的**来组织代码结构:
server
├── app.js
├── controller
├── middleware
├── package.json
├── package-lock.json
└── router
hello world
。在 controller 文件下新建index.js
,写入以下代码:module.exports = {
hello: async (ctx, next) => {
ctx.response.body = 'Hello World'
}
}
index.js
,写入以下代码:const logger = () => {
return async (ctx, next) => {
const start = Date.now()
await next()
const responseTime = (Date.now() - start)
console.log(`响应时间为: ${responseTime / 1000}s`)
}
}
module.exports = (app) => {
app.use(logger())
}
注意,中间件只能是函数。
index.js
,写入以下代码:const router = require('koa-router')()
module.exports = app => {
router.get('/', Controller.hello) // 注意是在controller编写的hello函数
app.use(router.routes()).use(router.allowedMethods())
}
const koa = require('koa')
const app = new koa()
const middleWare = require('./middleware')
const router = require('./router')
middleWare(app)
router(app)
app.listen(port, () => {
console.log('server is running at http://localhost:3000')
})
node app.js
然后打开浏览器,访问http://localhost:3000
就可以看到Hello World
了。至此,使用koa2
编写接口的基本思路就说完了,一般都是在controller
对数据库进行CRUD
,然后配置相关路由,就完成了一个接口服务的开发。
在 Docker 网站下载安装 Docker 。
安装完成后,打开命令行输入 docker version
,如果有相关输出即安装成功。
docker 中与 container
相关的命令主要有:
docker container run ...
:运行容器docker container ls
: 列出当前运行容器docker container start [container_id | container_name]
: 开始运行指定 id(或名称) 的容器docker container stop [container_id | container_name]
: 停止运行指定 id(或名称) 的容器docker container rm [container_id| container_name]
: 删除指定 id(或名称) 的容器其中,也可以使用 docker ps
查看正在运行的容器。
打开命令行,键入:
docker container run -it -p 8080:81 nginx
如果报错无法拉取,执行 docker login
输入账号密码登录 docker hub 账号后再执行。
在浏览器访问网站:http://localhost:8080
,如果可以访问到 nginx 相关即表示运行成功。
-p 8080:81
指定镜像在容器内运行的端口为:81
,同时将端口映射到本地机器的 8080
端口,所以需要在浏览器访问 http://localhost:8080
才可。
不同与 nginx ,运行 mysql 时可能需要设置一些变量,比如数据库密码之类的,这就需要用到 --env
参数。
docker container run -d -p 3006:3036 --env MYSQL_ROOT_PASSWORD=123456 mysql
使用 docker container -it exec [container_id | container_name]
可以运行指定的容器内的程序
首先,运行一个名为 mynginx
的容器:
docker container run -d -p 8080:80 --name mynginx nginx
然后,调用 mynginx
的 bash
程序:
docker container exec -it mynginx bash
如此,便可以在 container 内部操作环境执行命令,就像是进入了这个 container 操作环境内一样。
在上一步中,可以知道 container mynginx 的文件路径为:/usr/share/nginx/html
。如果想要修改这些文件,就需要进入 container 内部进行修改,这样便会很麻烦。docker 提供了绑定挂载点的功能,这样便可以把本地机器的文件映射到 container 内部。
docker container run -d -p 8080:81 -v ~/test:/usr/share/nginx/html --nmae mynginx nginx
在本地机器的 test 文件夹,新建一个 index.html 文件,然后浏览器访问 http://localhost:8080
,便可以看到刚刚保存的内容。
docker container run
的过程是首先拉取镜像,然后运行这个镜像。如此便可以制作自己的镜像。例如:
首先新建一个文件夹,然后新建一个 index.html
文件并写入内容,然后再编写 Dockerfile
文件。
FROM nginx:latest
WORKDIR /usr/share/nginx/html
COPY . .
然后运行 docker image build . -t mynginx-web
(.
是指当前路径),便完成了镜像制作。
运行这个镜像只需要:docker container run -it -p 8080:80 mynginx-web
。
假设开发中,遇到这样一个需求:“接口返回一片地区内所有的小区的电子围栏,将小区绘制到高德地图上”。很容易写出下面这样的代码:
const map = new Amap.Map();
for (const item of data) {
const polygon = new AMap.Polygon(item);
map.add(polygon);
}
效果大致可能就是这样,在实际运行中,很有可能会非常卡顿,因为绘制耗了大量时间,如果在地图上还有事件交互,也可能会非常卡顿。实际业务根本无法使用,这时候就要找办法性能优化,翻阅高德地图的文档示例,可能会发现有“集群”、“海量点”渲染优化等示例,但是实际上在项目中可能还是没法使用(比如这个需求是绘制小区)。
从接口层面来看,很有可能是后端吐出大规模地理信息数据,前端拿到数据后根据产品需求进行渲染,本质上都是在消费数据。最直接的方式是“单次消费全部数据进行全部渲染”,基本上会带来卡顿问题。让我们回到地图本身,当我们在地图上进行交互(比如移动地图、滚动缩放)时,地图看起来好像才会绘制当前视口能看到的地方,或者说就是这一片的瓦片。
所以,从地图本身的瓦片式渲染来看,我们对数据的消费也可以是这种形式,展示“当前视口内可以渲染的数据,当前缩放等级可以看到的数据”,进而大幅减少单次需要渲染的数据,性能自然就上去了。总结一下:
通过地图当前的视口、缩放登记,获取当前可以渲染的数据、被聚合的数据
站在巨人的肩膀上,通过 kd-brush
和 supercluster
对数据进行消费。
function sortKD(ids, coords, nodeSize, left, right, depth) {
if (right - left <= nodeSize) {
return;
}
const m = (left + right) >> 1;
select(ids, coords, m, left, right, depth % 2);
sortKD(ids, coords, nodeSize, left, m - 1, depth + 1);
sortKD(ids, coords, nodeSize, m + 1, right, depth + 1);
}
function select(ids, coords, k, left, right, inc) {
while (right > left) {
if (right - left > 600) {
const n = right - left + 1;
const m = k - left + 1;
const z = Math.log(n);
const s = 0.5 * Math.exp((2 * z) / 3);
const sd = 0.5 * Math.sqrt((z * s * (n - s)) / n) * (m - n / 2 < 0 ? -1 : 1);
const newLeft = Math.max(left, Math.floor(k - (m * s) / n + sd));
const newRight = Math.min(right, Math.floor(k + ((n - m) * s) / n + sd));
select(ids, coords, k, newLeft, newRight, inc);
}
const t = coords[2 * k + inc];
let i = left;
let j = right;
swapItem(ids, coords, left, k);
if (coords[2 * right + inc] > t) {
swapItem(ids, coords, left, right);
}
while (i < j) {
swapItem(ids, coords, i, j);
i++;
j--;
while (coords[2 * i + inc] < t) {
i++;
}
while (coords[2 * j + inc] > t) {
j--;
}
}
if (coords[2 * left + inc] === t) {
swapItem(ids, coords, left, j);
} else {
j++;
swapItem(ids, coords, j, right);
}
if (j <= k) {
left = j + 1;
}
if (k <= j) {
right = j - 1;
}
}
}
function swapItem(ids, coords, i, j) {
swap(ids, i, j);
swap(coords, 2 * i, 2 * j);
swap(coords, 2 * i + 1, 2 * j + 1);
}
function swap(arr, i, j) {
const tmp = arr[i];
arr[i] = arr[j];
arr[j] = tmp;
}
function range(ids, coords, minX, minY, maxX, maxY, nodeSize) {
const stack = [0, ids.length - 1, 0];
const result = [];
let x;
let y;
while (stack.length) {
const axis = stack.pop();
const right = stack.pop();
const left = stack.pop();
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
x = coords[2 * i];
y = coords[2 * i + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
result.push(ids[i]);
}
}
continue;
}
const m = Math.floor((left + right) / 2);
x = coords[2 * m];
y = coords[2 * m + 1];
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
result.push(ids[m]);
}
const nextAxis = (axis + 1) % 2;
if (axis === 0 ? minX <= x : minY <= y) {
stack.push(left);
stack.push(m - 1);
stack.push(nextAxis);
}
if (axis === 0 ? maxX >= x : maxY >= y) {
stack.push(m + 1);
stack.push(right);
stack.push(nextAxis);
}
}
return result;
}
function within(ids, coords, qx, qy, r, nodeSize) {
const stack = [0, ids.length - 1, 0];
const result = [];
const r2 = r * r;
while (stack.length) {
const axis = stack.pop();
const right = stack.pop();
const left = stack.pop();
if (right - left <= nodeSize) {
for (let i = left; i <= right; i++) {
if (sqDist(coords[2 * i], coords[2 * i + 1], qx, qy) <= r2) {
result.push(ids[i]);
}
}
continue;
}
const m = Math.floor((left + right) / 2);
const x = coords[2 * m];
const y = coords[2 * m + 1];
if (sqDist(x, y, qx, qy) <= r2) {
result.push(ids[m]);
}
const nextAxis = (axis + 1) % 2;
if (axis === 0 ? qx - r <= x : qy - r <= y) {
stack.push(left);
stack.push(m - 1);
stack.push(nextAxis);
}
if (axis === 0 ? qx + r >= x : qy + r >= y) {
stack.push(m + 1);
stack.push(right);
stack.push(nextAxis);
}
}
return result;
}
function sqDist(ax, ay, bx, by) {
const dx = ax - bx;
const dy = ay - by;
return dx * dx + dy * dy;
}
const defaultGetX = function (p) {
return p[0];
};
const defaultGetY = function (p) {
return p[1];
};
const KDBush = function KDBush(points, getX, getY, nodeSize, ArrayType) {
if (getX === void 0) getX = defaultGetX;
if (getY === void 0) getY = defaultGetY;
if (nodeSize === void 0) nodeSize = 64;
if (ArrayType === void 0) ArrayType = Float64Array;
this.nodeSize = nodeSize;
this.points = points;
const IndexArrayType = points.length < 65536 ? Uint16Array : Uint32Array;
const ids = (this.ids = new IndexArrayType(points.length));
const coords = (this.coords = new ArrayType(points.length * 2));
for (let i = 0; i < points.length; i++) {
ids[i] = i;
coords[2 * i] = getX(points[i]);
coords[2 * i + 1] = getY(points[i]);
}
sortKD(ids, coords, nodeSize, 0, ids.length - 1, 0);
};
KDBush.prototype.range = function range$1(minX, minY, maxX, maxY) {
return range(this.ids, this.coords, minX, minY, maxX, maxY, this.nodeSize);
};
KDBush.prototype.within = function within$1(x, y, r) {
return within(this.ids, this.coords, x, y, r, this.nodeSize);
};
const defaultOptions = {
minZoom: 3, // min zoom to generate clusters on
maxZoom: 18, // max zoom level to cluster the points on
minPoints: 4, // minimum points to form a cluster
radius: 80, // cluster radius in pixels
extent: 512, // tile extent (radius is calculated relative to it)
nodeSize: 64, // size of the KD-tree leaf node, affects performance
log: false, // whether to log timing info
// whether to generate numeric ids for input features (in vector tiles)
generateId: false,
// a reduce function for calculating custom cluster properties
reduce: null, // (accumulated, props) => { accumulated.sum += props.sum; }
// properties to use for individual points when running the reducer
map: (props) => props, // props => ({sum: props.my_value})
};
const fround =
Math.fround ||
((tmp) => (x) => {
tmp[0] = +x;
return tmp[0];
})(new Float32Array(1));
class Supercluster {
constructor(options) {
this.options = extend(Object.create(defaultOptions), options);
this.trees = new Array(this.options.maxZoom + 1);
}
load(points) {
const { log, minZoom, maxZoom, nodeSize } = this.options;
if (log) console.time('total time');
const timerId = `prepare ${points.length} points`;
if (log) console.time(timerId);
this.points = points;
// generate a cluster object for each point and index input points into a KD-tree
let clusters = [];
for (let i = 0; i < points.length; i++) {
// if (!points[i].geometry) continue;
clusters.push(createPointCluster(points[i], i));
}
this.trees[maxZoom + 1] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
if (log) console.timeEnd(timerId);
// cluster points on max zoom, then cluster the results on previous zoom, etc.;
// results in a cluster hierarchy across zoom levels
for (let z = maxZoom; z >= minZoom; z--) {
const now = +Date.now();
// create a new set of clusters for the zoom and index them with a KD-tree
clusters = this._cluster(clusters, z);
this.trees[z] = new KDBush(clusters, getX, getY, nodeSize, Float32Array);
if (log) console.log('z%d: %d clusters in %dms', z, clusters.length, +Date.now() - now);
}
if (log) console.timeEnd('total time');
return this;
}
getClusters(bbox, zoom) {
let minLng = ((((bbox[0] + 180) % 360) + 360) % 360) - 180;
const minLat = Math.max(-90, Math.min(90, bbox[1]));
let maxLng = bbox[2] === 180 ? 180 : ((((bbox[2] + 180) % 360) + 360) % 360) - 180;
const maxLat = Math.max(-90, Math.min(90, bbox[3]));
if (bbox[2] - bbox[0] >= 360) {
minLng = -180;
maxLng = 180;
} else if (minLng > maxLng) {
const easternHem = this.getClusters([minLng, minLat, 180, maxLat], zoom);
const westernHem = this.getClusters([-180, minLat, maxLng, maxLat], zoom);
return easternHem.concat(westernHem);
}
const tree = this.trees[this._limitZoom(zoom)];
const ids = tree.range(lngX(minLng), latY(maxLat), lngX(maxLng), latY(minLat));
const clusters = [];
for (const id of ids) {
const c = tree.points[id];
clusters.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]);
}
return clusters;
}
getChildren(clusterId) {
const originId = this._getOriginId(clusterId);
const originZoom = this._getOriginZoom(clusterId);
const errorMsg = 'No cluster with the specified id.';
const index = this.trees[originZoom];
if (!index) {
console.error(errorMsg);
return [];
}
const origin = index.points[originId];
if (!origin) {
console.error(errorMsg);
return [];
}
const r = this.options.radius / (this.options.extent * Math.pow(2, originZoom - 1));
const ids = index.within(origin.x, origin.y, r);
const children = [];
for (const id of ids) {
const c = index.points[id];
if (c.parentId === clusterId) {
children.push(c.numPoints ? getClusterJSON(c) : this.points[c.index]);
}
}
return children;
}
getLeaves(clusterId, limit, offset) {
limit = limit || 10;
offset = offset || 0;
const leaves = [];
this._appendLeaves(leaves, clusterId, limit, offset, 0);
return leaves;
}
getTile(z, x, y) {
const tree = this.trees[this._limitZoom(z)];
const z2 = Math.pow(2, z);
const { extent, radius } = this.options;
const p = radius / extent;
const top = (y - p) / z2;
const bottom = (y + 1 + p) / z2;
const tile = {
features: [],
};
this._addTileFeatures(
tree.range((x - p) / z2, top, (x + 1 + p) / z2, bottom),
tree.points,
x,
y,
z2,
tile
);
if (x === 0) {
this._addTileFeatures(tree.range(1 - p / z2, top, 1, bottom), tree.points, z2, y, z2, tile);
}
if (x === z2 - 1) {
this._addTileFeatures(tree.range(0, top, p / z2, bottom), tree.points, -1, y, z2, tile);
}
return tile.features.length ? tile : null;
}
getClusterExpansionZoom(clusterId) {
let expansionZoom = this._getOriginZoom(clusterId) - 1;
while (expansionZoom <= this.options.maxZoom) {
const children = this.getChildren(clusterId);
expansionZoom++;
if (children.length !== 1) break;
clusterId = children[0].properties.cluster_id;
}
return expansionZoom;
}
_appendLeaves(result, clusterId, limit, offset, skipped) {
const children = this.getChildren(clusterId);
for (const child of children) {
const props = child.properties;
if (props && props.cluster) {
if (skipped + props.point_count <= offset) {
// skip the whole cluster
skipped += props.point_count;
} else {
// enter the cluster
skipped = this._appendLeaves(result, props.cluster_id, limit, offset, skipped);
// exit the cluster
}
} else if (skipped < offset) {
// skip a single point
skipped++;
} else {
// add a single point
result.push(child);
}
if (result.length === limit) break;
}
return skipped;
}
_addTileFeatures(ids, points, x, y, z2, tile) {
for (const i of ids) {
const c = points[i];
const isCluster = c.numPoints;
const f = {
type: 1,
geometry: [
[
Math.round(this.options.extent * (c.x * z2 - x)),
Math.round(this.options.extent * (c.y * z2 - y)),
],
],
tags: isCluster ? getClusterProperties(c) : this.points[c.index].properties,
};
// assign id
let id;
if (isCluster) {
id = c.id;
} else if (this.options.generateId) {
// optionally generate id
id = c.index;
} else if (this.points[c.index].id) {
// keep id if already assigned
id = this.points[c.index].id;
}
if (id !== undefined) f.id = id;
tile.features.push(f);
}
}
_limitZoom(z) {
return Math.max(this.options.minZoom, Math.floor(Math.min(+z, this.options.maxZoom + 1)));
}
_cluster(points, zoom) {
const clusters = [];
const { radius, extent, reduce, minPoints } = this.options;
const r = radius / (extent * Math.pow(2, zoom));
// loop through each point
for (let i = 0; i < points.length; i++) {
const p = points[i];
// if we've already visited the point at this zoom level, skip it
if (p.zoom <= zoom) continue;
p.zoom = zoom;
// find all nearby points
const tree = this.trees[zoom + 1];
const neighborIds = tree.within(p.x, p.y, r);
const numPointsOrigin = p.numPoints || 1;
let numPoints = numPointsOrigin;
// count the number of points in a potential cluster
for (const neighborId of neighborIds) {
const b = tree.points[neighborId];
// filter out neighbors that are already processed
if (b.zoom > zoom) numPoints += b.numPoints || 1;
}
if (numPoints >= minPoints) {
// enough points to form a cluster
let wx = p.x * numPointsOrigin;
let wy = p.y * numPointsOrigin;
let clusterProperties = reduce && numPointsOrigin > 1 ? this._map(p, true) : null;
// encode both zoom and point index on which the cluster originated -- offset by total length of features
const id = (i << 5) + (zoom + 1) + this.points.length;
for (const neighborId of neighborIds) {
const b = tree.points[neighborId];
if (b.zoom <= zoom) continue;
b.zoom = zoom; // save the zoom (so it doesn't get processed twice)
const numPoints2 = b.numPoints || 1;
wx += b.x * numPoints2; // accumulate coordinates for calculating weighted center
wy += b.y * numPoints2;
b.parentId = id;
if (reduce) {
if (!clusterProperties) clusterProperties = this._map(p, true);
reduce(clusterProperties, this._map(b));
}
}
p.parentId = id;
clusters.push(
createCluster(wx / numPoints, wy / numPoints, id, numPoints, clusterProperties)
);
} else {
// left points as unclustered
clusters.push(p);
if (numPoints > 1) {
for (const neighborId of neighborIds) {
const b = tree.points[neighborId];
if (b.zoom <= zoom) continue;
b.zoom = zoom;
clusters.push(b);
}
}
}
}
return clusters;
}
// get index of the point from which the cluster originated
_getOriginId(clusterId) {
return (clusterId - this.points.length) >> 5;
}
// get zoom of the point from which the cluster originated
_getOriginZoom(clusterId) {
return (clusterId - this.points.length) % 32;
}
_map(point, clone) {
if (point.numPoints) {
return clone ? extend({}, point.properties) : point.properties;
}
const original = this.points[point.index].properties;
const result = this.options.map(original);
return clone && result === original ? extend({}, result) : result;
}
}
function createCluster(x, y, id, numPoints, properties) {
return {
x: fround(x), // weighted cluster center; round for consistency with Float32Array index
y: fround(y),
zoom: Infinity, // the last zoom the cluster was processed at
id, // encodes index of the first child of the cluster and its zoom level
parentId: -1, // parent cluster id
numPoints,
properties,
};
}
function createPointCluster(p, id) {
const x = 'x' in p ? p.x : p.position && p.position[0];
const y = 'y' in p ? p.y : p.position && p.position[1];
return {
x: fround(lngX(x)), // projected point coordinates
y: fround(latY(y)),
zoom: Infinity, // the last zoom the point was processed at
index: id, // index of the source feature in the original input array,
parentId: -1, // parent cluster id
};
}
function getClusterJSON(cluster) {
return {
isClutser: true,
id: cluster.id,
...getClusterProperties(cluster),
x: xLng(cluster.x),
y: yLat(cluster.y),
position: [xLng(cluster.x), yLat(cluster.y)],
};
}
function getClusterProperties(cluster) {
const count = cluster.numPoints;
const abbrev =
count >= 10000
? `${Math.round(count / 1000)}k`
: count >= 1000
? `${Math.round(count / 100) / 10}k`
: count;
return extend(extend({}, cluster.properties), {
cluster: true,
cluster_id: cluster.id,
count,
abbreviatedCount: abbrev,
});
}
// longitude/latitude to spherical mercator in [0..1] range
function lngX(lng) {
return lng / 360 + 0.5;
}
function latY(lat) {
const sin = Math.sin((lat * Math.PI) / 180);
const y = 0.5 - (0.25 * Math.log((1 + sin) / (1 - sin))) / Math.PI;
return y < 0 ? 0 : y > 1 ? 1 : y;
}
// spherical mercator to longitude/latitude
function xLng(x) {
return (x - 0.5) * 360;
}
function yLat(y) {
const y2 = ((180 - y * 360) * Math.PI) / 180;
return (360 * Math.atan(Math.exp(y2))) / Math.PI - 90;
}
function extend(dest, src) {
for (const id in src) dest[id] = src[id];
return dest;
}
function getX(p) {
return p.x;
}
function getY(p) {
return p.y;
}
function debounce(func, wait, immediate) {
let timeout;
const debounced = function () {
const context = this;
const args = arguments;
const later = function () {
timeout = null;
if (!immediate) {
func.apply(context, args);
}
};
const callNow = immediate && !timeout;
clearTimeout(timeout);
timeout = setTimeout(later, wait);
if (callNow) {
func.apply(context, args);
}
};
debounced.cancel = () => {
clearTimeout(timeout);
};
return debounced;
}
const data = areas.map((area, index) => {
return {
...area,
path: area.lnglat,
name: `模拟社区${index}`,
index,
x: area.lnglat[0][0],
y: area.lnglat[0][1],
};
});
const map = new AMap.Map(document.querySelector('#app'), {
mapStyle: 'amap://styles/grey',
zoom: 14,
center: [116.467987, 39.992613],
});
const index = new Supercluster();
index.load(data);
let markers = [];
let polygons = [];
const render = () => {
let bounds = map.getBounds();
if (bounds.toBounds) {
bounds = bounds.toBounds();
}
const bbox = [
bounds.southWest.lng,
bounds.southWest.lat,
bounds.northEast.lng,
bounds.northEast.lat,
];
const views = index.getClusters(bbox, map.getZoom());
const clusters = views.filter((view) => view.isClutser);
const data = views.filter((view) => !view.isClutser);
map.remove(markers);
markers = clusters.map((cluster) => {
const marker = new AMap.Marker({
...cluster,
content: `<div style="width: 32px; height: 32px; line-height: 32px; border-radius: 50%; background-color: green; color: #fff; text-align: center;">${cluster.count}</div>`,
});
return marker;
});
map.add(markers);
map.remove(polygons);
polygons = data.map((item) => {
const marker = new AMap.Polygon({
...item,
fillColor: 'rgba(256, 0, 0, 0.2)', // 多边形填充颜色
borderWeight: 2, // 线条宽度,默认为 1
strokeColor: 'rgba(256, 0, 0, 1)', // 线条颜色
});
return marker;
});
map.add(polygons);
};
render();
const listener = debounce(render, 200);
map.on('zoom', listener);
map.on('moveend', listener);
Storage Event
实现页面间通信最近总是看到这种需求,故记录下作为笔记。
我们可能经常会在浏览器中使用多个 Tab
打开同一个网页。例如,在线聊天网页每个 Tab 页面的消息同步。其实原理真的很简单,监听 storage
事件即可。
window.addEventListener("storage", evt => handler(evt));
就这么短短一行代码即可。
storage
事件的触发条件为:
localStorage.setItem(key, value)
保存值localStorage.setItem(key, newValue)
更新值PS:
sessionStorage
也可以。
需要注意的是当前页面监听不到这个事件。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
</head>
<body>
<p></p>
<button>发送随机消息</button>
<script>
const oP = document.querySelector("p");
const oBtn = document.querySelector("button");
window.addEventListener("storage", evt => {
console.log(evt);
oP.innerHTML = JSON.stringify(evt, null, 2);
});
oBtn.onclick = () => {
window.localStorage.setItem("hello", window.location + new Date());
};
</script>
</body>
</html>
"Fira Code", SFMono-Regular, Consolas, "Liberation Mono", Menlo, Courier, monospace, Operator Mono Ssm, Consolas, 'Courier New', monospace
二叉树中的节点最多只能有 2 个子节点:左侧子节点 和 右侧子节点。 二叉搜索树是二叉树的一种,但是它只允许在左侧子节点存储比父节点小的值,在右侧子节点存储大于等于父节点的值。
class Node {
constructor(value) {
this.left = null;
this.value = value;
this.right = null;
}
}
class BinarySearchTree {
constructor() {
this.root = null;
}
/**
* 插入节点
* @param {} value
*/
insert(value) {
const newNode = new Node(value);
if (!this.root) {
this.root = newNode;
} else {
let root = this.root;
insertNode(root, newNode);
function insertNode(node, newNode) {
if (node.value < newNode.value) {
if (node.right === null) {
node.right = newNode;
} else {
insertNode(node.right, newNode);
}
} else {
if (node.left === null) {
node.left = newNode;
} else {
insertNode(node.left, newNode);
}
}
}
}
return true;
}
/**
* 按值查找节点
* @param {*} value
*/
search(value) {
const _search = (node, value) => {
if (node === null) {
return false;
}
if (node.value > value) {
return _search(node.left, value);
} else if (node.value < value) {
return _search(node.right, value);
} else {
return node;
}
};
return _search(this.root, value);
}
}
先序遍历的顺序是:根 -> 左 -> 右。
function preTraverse(bsTree) {
const traverse = (node, queue) => {
if (!node) {
return queue;
} else {
queue.push(node);
traverse(node.left, queue);
traverse(node.right, queue);
}
};
const queue = [];
traverse(bsTree.root, queue);
return queue;
}
function preTraverse(bsTree) {
let stack = [];
let queue = [];
let node = bsTree.root;
stack.push(node);
while (stack.length > 0) {
let currentNode = stack.pop();
queue.push(currentNode);
if (currentNode.right) {
stack.push(currentNode.right);
}
if (currentNode.left) {
stack.push(currentNode.left);
}
}
return queue;
}
中序遍历的顺序是:左 -> 根 -> 右。
function inorderTraverse(bsTree) {
const traverse = (node, queue) => {
if (!node) {
return;
}
traverse(node.left, queue);
queue.push(node);
traverse(node.right, queue);
};
const queue = [];
traverse(bsTree.root, queue);
return queue;
}
function inorderTraverse(bsTree) {
const stack = [];
const queue = [];
let currentNode = bsTree.root;
while (currentNode || stack.length > 0) {
if (currentNode != null) {
stack.push(currentNode);
currentNode = currentNode.left;
} else {
currentNode = stack.pop();
queue.push(currentNode);
currentNode = currentNode.right;
}
}
return queue;
}
循环版的思路:
function inorderTraverse(bsTree) {
const stack = [];
const queue = [];
let currentNode = bsTree.root;
while (currentNode) {
currentNode = currentNode.left;
stack.push(currentNode);
}
while (stack.length) {
queue.push(stack.pop());
}
currentNode = bsTree.root;
while (currentNode) {
queue.push(currentNode);
currentNode = currentNode.right;
}
return queue;
}
后序遍历的顺序是:左 -> 右 -> 根。
function postorderTraverse(bsTree) {
const traverse = (node, queue) => {
if (!node) {
return;
}
traverse(node.left, queue);
traverse(node.right, queue);
queue.push(node);
};
const queue = [];
traverse(bsTree.root, queue);
return queue;
}
function postorderTraverse(bsTree) {
let stack = []
let queue = []
let node = bsTree.root
stack.push(node)
while (stack.length) {
node = stack.pop()
if (node.left) {
stack.push(node.left)
}
if (node.right) {
stack.push(node.right)
}
queue.unshift(node)
}
return queue
}
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
11
/ \
7 15
/ \ / \
3 6 12 16
层级遍历结果:
[
[11],
[7, 15],
[3, 6, 12, 16]
]
function levelTraverse(bsTree) {
let queue = []
let ret = []
let node = bsTree.root
queue.push(node)
while (queue.length) {
const count = queue.length
const subRet = []
while (count > 0) {
node = queue.pop()
subRet.push(node)
if (node.left) queue.push(node.left)
if (node.right) queue.push(node.right)
}
ret.push(subRet)
}
return subRet
}
相信大部分前端同学之前早已无数次听过或了解过 vnode
(虚拟节点),那么什么是 vnode
? vnode
应该是什么样的?
如果不使用前端框架,我们可能会写出这样的页面:
<html>
<head>
<title></title>
</head>
<body>
<div></div>
<script></script>
</body>
</html>
不难发现,整个文档树的根节点只有一个 html
,然后嵌套各种子标签,如果使用某种数据结构来表示这棵树,那么它可能是这样。
{
tagName: 'html',
children: [
{
tagName: 'head',
children: [
{
tagName: 'title'
}
]
},
{
tagName: 'body',
children: [
{
tagName: 'div'
},
{
tagName: 'script'
}
]
}
]
}
但是实际开发中,整个文档树中head
和 script
标签基本不会有太大的改动。频繁交互可能改动的应当是 body
里面的除 script
的部分,所以构建 虚拟节点树 应当是整个 HTML 文档树的一个子树,而这个子树应当保持和 HTML 文档树一致的数据结构。它可能是这样。
<html>
<head>
<title></title>
</head>
<body>
<div id="root">
<div class="header"></div>
<div class="main"></div>
<div class="footer"></div>
</div>
<script></script>
</body>
</html>
这里应当构建的 虚拟节点树 应当是 div#root
这棵子树:
{
tagName: 'div',
children: [
{
tagName: 'div',
},
{
tagName: 'div',
},
{
tagName: 'div',
},
]
}
到这里,vnode 的概念应当很清晰了,vnode 是用来表示实际 dom 节点的一种数据结构,其结构大概长这样。
{
tagName: 'div',
attrs: {
class: 'header'
},
children: []
}
一般,我们可能会这样定义 vnode
。
// vnode.js
export const vnode = function vnode() {}
使用 React
会经常写 JSX
,那么如何将 JSX
表示成 vnode
?这里可以借助 @babel/plugin-transform-react-jsx
这个插件来自定义转换函数,
只需要在 .babelrc
中配置:
{
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"pragma": "window.h"
}
]
]
}
然后在 window
对象上挂载一个 h
函数:
// h.js
const flattern = arr => [].concat.apply([], arr)
window.h = function h(tagName, attrs, ...children) {
const node = new vnode()
node.tagName = tagName
node.attrs = attrs || {}
node.children = flattern(children)
return node
}
测试一下:
现在我们已经知道了如何构建 vnode
,接下来就是将其渲染成真正的 dom 节点并挂载。
// 将 vnode 创建为真正的 dom 节点
export function createElement(vnode) {
if (typeof vnode !== 'object') {
// 文本节点
return document.createTextNode(vnode)
}
const el = document.createElement(vnode.tagName)
setAttributes(el, vnode.attrs)
vnode.children.map(createElement).forEach(el.appendChild.bind(el))
return el
}
// render.js
export default function render(vnode, parent) {
parent = typeof parent === 'string' ? document.querySelector(parent) : parent
return parent.appendChild(createElement(vnode))
}
这里的逻辑主要为:
vnode.tagName
创建元素vnode.attrs
设置元素的 attributes
vnode.children
并将其创建为真正的元素,然后将真实子元素节点 append 到第 1 步创建的元素第 2 步已经实现了 vnode
到 dom
节点的转换与挂载,那么接下来某一个时刻 dom
节点发生了变化,如何更新 dom
树?显然不能无脑卸载整棵树,然后挂载新的树,最好的办法还是找出两棵树之间的差异,然后应用这些差异。
在写 diff
之前,首先要定义好,要 diff
什么,明确 diff
的返回值。比较上图两个 vnode,可以得出:
li
的内容ul
下创建两个 li
,这两个 li 为 第 4 个和 第 5 个子节点那么可能得返回值为:
{
"type": "UPDATE",
"children": [
{
"type": "UPDATE",
"children": [
{
"type": "REPLACE",
"newVNode": 0
}
],
"attrs": []
},
{
"type": "UPDATE",
"children": [
{
"type": "REPLACE",
"newVNode": 1
}
],
"attrs": []
},
{
"type": "UPDATE",
"children": [
{
"type": "REPLACE",
"newVNode": 2
}
],
"attrs": []
},
{
"type": "CREATE",
"newVNode": {
"tagName": "li",
"attrs": {},
"children": [
3
]
}
},
{
"type": "CREATE",
"newVNode": {
"tagName": "li",
"attrs": {},
"children": [
4
]
}
}
],
"attrs": []
}
diff
的过程中,要保证节点的父节点正确,并要保证该节点在父节点 的子节点中的索引正确(保证节点内容正确,位置正确)。diff
的核心流程:
/**
* diff 新旧节点差异
* @param {*} oldVNode
* @param {*} newVNode
*/
export default function diff(oldVNode, newVNode) {
if (isNull(oldVNode)) {
return { type: CREATE, newVNode }
}
if (isNull(newVNode)) {
return { type: REMOVE }
}
if (isDiffrentVNode(oldVNode, newVNode)) {
return { type: REPLACE, newVNode }
}
if (newVNode.tagName) {
return {
type: UPDATE,
children: diffVNodeChildren(oldVNode, newVNode),
attrs: diffVNodeAttrs(oldVNode, newVNode)
}
}
}
知道了两棵树之前的差异,接下来如何应用这些更新?在文章开头部分我们提到 dom
节点树应当只有一个根节点,同时 diff
算法是保证了虚拟节点的位置和父节点是与 dom
树保持一致的,那么 patch 的入口也就很简单了,从 虚拟节点的挂载点开始递归应用更新即可。
/**
* 根据 diff 结果更新 dom 树
* 这里为什么从 index = 0 开始?
* 因为我们是使用树去表示整个 dom 树的,传入的 parent 即为 dom 挂载点
* 从根节点的第一个节点开始应用更新,这是与整个dom树的结构保持一致的
* @param {*} parent
* @param {*} patches
* @param {*} index
*/
export default function patch(parent, patches, index = 0) {
if (!patches) {
return
}
parent = typeof parent === 'string' ? document.querySelector(parent) : parent
const el = parent.childNodes[index]
/* eslint-disable indent */
switch (patches.type) {
case CREATE: {
const { newVNode } = patches
const newEl = createElement(newVNode)
parent.appendChild(newEl)
break
}
case REPLACE: {
const { newVNode } = patches
const newEl = createElement(newVNode)
parent.replaceChild(newEl, el)
break
}
case REMOVE: {
parent.removeChild(el)
break
}
case UPDATE: {
const { attrs, children } = patches
patchAttrs(el, attrs)
for (let i = 0, len = children.length; i < len; i++) {
patch(el, children[i], i)
}
break
}
}
}
至此,vdom
的核心 diff
与 patch
都已基本实现。在测试 demo 中,不难发现 diff
其实已经很快了,但是 patch
速度会比较慢,所以这里留下了一个待优化的点就是 patch
。
本文完整代码均在这个仓库。
栈遵循 后进先出 的原则。
/**
* 栈
* 后进先出
*/
class Stack {
constructor() {
this.items = [];
}
// 添加元素
push(value) {
this.items.push(value);
}
// 返回栈顶的元素
peek() {
return this.items[this.items.length - 1];
}
// 移出栈顶的元素
pop() {
return this.items.pop();
}
/**
* 判断栈是否为空
*/
isEmpty() {
return this.items.length <= 0;
}
}
队列遵循 先进先出 的原则。
/**
* 队列
* 先进先出
*/
class Queue {
constructor() {
this.items = [];
}
/**
* 添加元素
*/
enqueue(value) {
this.items.push(value);
}
/**
* 移出队列的第一个元素
*/
dequeue() {
return this.items.shift();
}
}
给定一个只包括 '(',')','{','}','[',']'
的字符串,判断字符串是否有效。
使用 栈 来解决。
false
false
/**
* 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。
* @param {*} str
*/
function isValidStr(str) {
let arr = str.split("");
let stack = new Stack();
function isMatchStr(a, b) {
return (
(a === "(" && b === ")") ||
(a === "[" && b === "]") ||
(a === "{" && b === "}")
);
}
while (arr.length) {
let char = arr.shift();
if (char === "(" || char === "[" || char === "{") {
stack.push(char);
} else {
if (stack.isEmpty()) {
return false;
} else if (isMatchStr(stack.pop(), char)) {
continue;
} else {
return false;
}
}
}
return true;
}
用两个栈来实现一个队列,完成队列的 enqueue
和 dequeue
操作。
in
栈用于处理 入列 操作,out
栈用于处理 出列 操作。
in
栈in
栈的元素出栈压入到 out
栈,然后返回 out
栈栈顶的元素(这样便保持了队列 先进先出
的原则)。class TwoStackQueue {
constructor() {
this.inStack = new Stack();
this.outStack = new Stack();
}
enqueue(value) {
this.inStack.push(value);
}
dequeue() {
while (!this.inStack.isEmpty()) {
this.outStack.push(this.inStack.pop());
}
return this.outStack.pop();
}
}
输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列 1,2,3,4,5 是某栈的压入顺序,序列 4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的弹出序列。(注意:这两个序列的长度是相等的)
初始化一个辅助栈,遍历压入顺序,执行入栈操作,入栈的同时,如果辅助栈非空,同时弹出序列的当前值与辅助栈栈顶元素相同,则辅助栈出栈。最后判断辅助是否为空,为空这是正确的顺序,否则不是。例如:
入栈:1, 2, 3, 4, 5
出栈:4, 5, 3, 2, 1
首先,1 入栈,此时栈顶元素 1 != 4,继续入栈;
2 != 4,入栈;
3 != 4 入栈;
4 == 4,入栈后出栈(这时候栈内元素为 1, 2, 3);
3 != 5,入栈(这时候栈内元素为 1, 2, 3, 5),5 == 5 出栈,3 == 3 出栈...
最后栈为空,顺序匹配。
function isPopOrder(pushOrder, popOrder) {
let stack = new Stack();
let popIndex = 0;
for (let pushIndex = 0; pushIndex < pushOrder.length; pushIndex++) {
stack.push(pushOrder[pushIndex]);
while (!stack.isEmpty() && stack.peek() === popOrder[popIndex]) {
stack.pop();
popIndex++;
}
}
return stack.isEmpty();
}
mac 系统自带 php
,命令行输入 php -v
即可查看版本信息。
php-composer
的安装:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === '48e3236262b34d30969dca3c37281b3b4bbe3221bda826ac6a9a62d6444cdb0dcd0615698a5cbe587c3f0fe57a54d8f5') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"
mv composer.phar /usr/local/bin/composer
完成后,键入 composer
,如果有相关信息输出即安装成功。
团队使用 TypeScript 进行 React 组件开发。开发组件的同时,需要为组件撰写文档(使用 Markdown 编写文档)。文档中需要对组件的 props 定义进行说明。
在开发组件的时候,是编写组件 props 的接口定义。这时候就希望能够偷懒了,直接抽取组件源代码中的接口定义和注释来生成这部分文档。
基于上面背景进行分析,可以发现核心需求是抽取组件接口定义和注释形成特定格式的文档。
import React from "react";
export interface IProps {
// 文字
text: string;
// 点击事件
onClick: () => void;
}
const Button = ({ text }) => {
return <button>{text}</button>;
};
export default Button;
其实就是抽取 IProps
定义然后转换成其他格式文档。
在人人都说 AST
的今天,我们肯定是要使用 AST
来完成。
AST
AST
抽取 interface
定义interface
抽取各个字段定义 definition
code -> ast -> interface -> definitions
AST
站在巨人的肩膀上,使用 babel
解析源代码即可。
const parser = require("@babel/parser");
function transformCode2Ast(code) {
return parser.parse(code, {
sourceType: "module",
plugins: [
"jsx",
"typescript",
"asyncGenerators",
"bigInt",
"classProperties",
"classPrivateProperties",
"classPrivateMethods",
["decorators", { decoratorsBeforeExport: false }],
"doExpressions",
"dynamicImport",
"exportDefaultFrom",
"exportNamespaceFrom",
"functionBind",
"functionSent",
"importMeta",
"logicalAssignment",
"nullishCoalescingOperator",
"numericSeparator",
"objectRestSpread",
"optionalCatchBinding",
"optionalChaining",
["pipelineOperator", { proposal: "minimal" }],
"throwExpressions",
"topLevelAwait",
"estree",
],
});
}
转换上面的 Button 组件,AST
大概长这样:
Node {
type: 'File',
start: 0,
end: 207,
loc:
SourceLocation {
start: Position { line: 1, column: 0 },
end: Position { line: 16, column: 0 } },
errors: [],
program:
Node {
type: 'Program',
start: 0,
end: 207,
loc: SourceLocation { start: [Position], end: [Position] },
sourceType: 'module',
interpreter: null,
body: [ [Node], [Node], [Node], [Node] ] },
comments:
[ { type: 'CommentLine',
value: ' 文字',
start: 57,
end: 62,
loc: [SourceLocation] },
{ type: 'CommentLine',
value: ' 点击事件',
start: 81,
end: 88,
loc: [SourceLocation] } ] }
AST
同样站在巨人的肩膀上,使用 ast-types
对 ast
进行遍历。
const { visit } = require("ast-types");
function findInterface(ast) {
let ret = Object.create(null);
let currentInterface = null;
visit(ast, {
visitTSInterfaceDeclaration(nodePath) {
currentInterface = nodePath.value.id.name;
this.traverse(nodePath);
},
visitTSPropertySignature(nodePath) {
ret[currentInterface] = ret[currentInterface] || [];
ret[currentInterface].push(nodePath.value);
return false;
},
});
return ret;
}
对 AST
遍历抽取 Interface
后的结果大概长这样。
Node {
type: 'TSPropertySignature',
start: 65,
end: 78,
loc: [SourceLocation],
key: [Node],
computed: false,
typeAnnotation: [Node],
leadingComments: [Array],
trailingComments: [Array] },
Node {
type: 'TSPropertySignature',
start: 91,
end: 111,
loc: [SourceLocation],
key: [Node],
computed: false,
typeAnnotation: [Node],
leadingComments: [Array] } ] }
会发现各个定义在 typeAnnotation
中,这时候对它进行解析即可。
通过分析 typeAnnotation
很容易写出。
const get = require("lodash/get");
function parseTSTypeReference(typeName) {
const type = get(typeName, "type");
switch (type) {
case "TSQualifiedName":
return `${get(typeName, "left.name")}.${get(typeName, "right.name")}`;
default:
return `Unknown ReferenceType`;
}
}
function parseTSFunctionType(parameters, typeAnnotation) {
const parseTSFunctionParameters = (parameters) => {
if (!parameters || !parameters.length) {
return `()`;
}
let args = parameters.map((parameter) => {
return `${get(parameter, "name")}: ${parseTypeAnnotation(
get(parameter, "typeAnnotation.typeAnnotation")
)}`;
});
return "( " + args.join(", ") + ")";
};
const parseTSFunctionReturn = (typeAnnotation) => {
const type = get(typeAnnotation, "type");
switch (type) {
case "TSVoidKeyword":
return "void";
case "TSTypeReference":
return parseTSTypeReference(get(typeAnnotation, "typeName"));
default:
return `Unknown FunctionType`;
}
};
return `${parseTSFunctionParameters(parameters)} => ${parseTSFunctionReturn(
typeAnnotation
)}`;
}
function parseTSTypeLiteral(members) {
const ret = parseInterfaceDefinitions(members);
let args = ret.map((t) => `${t.name}: ${t.type}`);
return "{ " + args.join(", ") + " }";
}
function parseTypeAnnotation(typeAnnotation) {
const type = get(typeAnnotation, "type");
switch (type) {
case "TSNumberKeyword":
case "TSStringKeyword":
case "TSBoleanKeyword":
case "TSNullKeyword":
case "TSUndefinedKeyword":
case "TSSymbolKeyword":
case "TSAnyKeyword":
return type.match(/TS(\w+)Keyword/)[1].toLowerCase();
case "TSUnionType":
return get(typeAnnotation, "types", [])
.map((type) => get(type, "literal.value"))
.join(" | ");
case "TSFunctionType":
return parseTSFunctionType(
get(typeAnnotation, "parameters"),
get(typeAnnotation, "typeAnnotation.typeAnnotation")
);
case "TSTypeReference":
return parseTSTypeReference(get(typeAnnotation, "typeName"));
case "TSTypeLiteral":
return parseTSTypeLiteral(get(typeAnnotation, "members"));
default:
return "UnKnowType";
}
}
function parseInterfaceDefinitions(nodePaths) {
const parseInterfaceDefinitionsNode = (nodePath) => {
const name = get(nodePath, "key.name");
const comments = get(nodePath, "leadingComments.0.value", "")
.trim()
.split(/[\r\n]/)
.map((str) => str.trim().replace(/^\*/g, "").trim())
.filter(Boolean);
const typeAnnotation = get(nodePath, "typeAnnotation.typeAnnotation");
const type = parseTypeAnnotation(typeAnnotation);
return { name, type, comments };
};
return nodePaths.map(parseInterfaceDefinitionsNode);
}
至此,可以得到 Button
的接口定义。
[
[
{
name: "text",
type: "string",
comments: ["文字"],
},
{
name: "onClick",
type: "() => void",
comments: ["点击事件"],
},
],
];
接下来只要将解析后的结果转成想要的格式即可。
function parseTypeScriptComponentInterface(code) {
let ast = transformCode2Ast(code);
let interfaces = findInterfaces(ast);
let definitions = Object.keys(interfaces).reduce((a, c) => {
a[c] = a[c] || [];
a[c].push(parseInterfaceDefinitions(interfaces[c]));
return a;
}, Object.create(null));
return definitions;
}
const code = `
import React from 'react';
export interface IProps {
/**
* button
* 显示文字
*/
text: string;
// 点击事件
onClick: () => void;
// 属性 3
props3: (arg: any) => void;
// 属性 4
props4: (arg: { name: string, age: number }) => React.Node
}
const Button = ({ text }) => {
return <button>{text}</button>;
};
export default Button;
`;
let ret = parseTypeScriptComponentInterface(code);
输出:
{
"IProps": [
[
{
"name": "text",
"type": "string",
"comments": [
"button",
"显示文字"
]
},
{
"name": "onClick",
"type": "() => void",
"comments": [
"点击事件"
]
},
{
"name": "props3",
"type": "( arg: any) => void",
"comments": [
"属性 3"
]
},
{
"name": "props4",
"type": "( arg: { name: string, age: number }) => React.Node",
"comments": [
"属性 4"
]
}
]
]
}
冒泡排序重复遍历数组,依次比较 2 个元素,如这 2 个元素顺序错误,就交换位置。
function bubbleSort(arr) {
let len = arr.length;
for (let i = 0; i < len - 1; i++) {
for (let j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
[arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
}
}
}
return arr;
}
选择排序时间复杂度 O(n²)
。
function selectSort(arr) {
let len = arr.length
let minIndex = 0
for (let i = 0; i < len; i++) {
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
}
return arr
}
插入排序首先构建有序数列,然后在有序数列中从后向前插入未排序元素。
function insertSort(arr) {
let len = arr.length
for (let i = 1; i < len; i++) {
let j = i - 1;
let tmp = arr[i]
while (j >= 0 && arr[j] > tmp) {
arr[j + 1] = arr[j]
j--
}
arr[j + 1] = tmp
}
return arr
}
希尔排序是插入排序的更高效改进版。建立于插入排序的 2 点特性:
希尔排序的步骤。
function shellSort(arr) {
let len = arr.length;
let gap = 1;
while (gap < len / 3) {
gap = gap * 3 + 1;
}
for (; gap > 0; gap = Math.floor(gap / 3)) {
for (let i = gap; i < len; i++) {
let tmp = arr[i];
let j = i - gap;
while (j >= 0 && arr[j] > tmp) {
arr[j + gap] = arr[j];
j -= gap;
}
arr[j + gap] = tmp;
}
}
return arr;
}
归并排序是采用分治法的典型例子。
function mergeSort(arr) {
let len = arr.length
if (len <= 1) {
return arr
}
let middle = len >> 1
let left = arr.slice(0, middle)
let right = arr.slice(middle)
const merge = (left, right) => {
let ret = []
while (left.length && right.length) {
if (left[0] < right[0]) {
ret.push(left.shift())
} else {
ret.push(right.shift())
}
}
while (left.length) {
ret.push(left.shift())
}
while (right.length) {
ret.push(right.shift())
}
return ret
}
return merge(mergeSort(left), mergeSort(right))
}
快速排序采用分治法将 1 个数列分成 2 个子数列进行处理。
function quickSort(arr, left, right) {
if (arr.length <= 1) {
return arr;
}
let pivotIndex = Math.floor(arr.length / 2);
let pivot = arr.splice(pivotIndex, 1)[0];
let left = [];
let right = [];
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
假设场景:
在某个项目下,我新建了一个本地分支 test
,并在该分支做出了一些改动,但是我在没有 push
该分支代码到远程仓库之前手滑删除了该分支,那么如何在本地恢复这个分支?该场景的相关操作如下:
git branch test
git checkout test
# 做了一些改动
git status
git add .
git commit -m "some info"
git chekout - # 切到上一个分支
git branch -D test # 手滑删了 test
经过以上操作,本地更改的代码是没有了的。但是 git
中有日志(因为 git commit 到本地了),通过 git log -g
查看 commit
的记录 hash
值,然后执行 git branch test hash
即可恢复。
总结一下:
git log -g # 查找本地提交记录 hash
git branch new_branch_name hash # 新建分支
有句代码有点疑惑
this.listener.set(type, [].concat(subs, fn))
感觉直接用
subs.push(fn)
就行了吧?
本文接下来会一步一步模仿造一个低配版的 Element 的对话框和弹框组件
。
当使用vue-cli
初始化一个项目的时候,会发现 src/components
文件夹下有一个 HelloWorld.vue
文件,这便是单文件组件的基本开发模式。
// 注册
Vue.component("my-component", {
template: "<div>A custom component!</div>"
});
// 创建根实例
new Vue({
el: "#example"
});
接下来,开始写一个 dialog
组件。
目标对话框组件的基本样式如图:
根据目标样式,可以总结出:
title
props 来标示弹窗标题确定
按钮时 发射
出 确定
事件(即告诉父组件确定
了)发射
出 取消
事件那么,编码如下:
<template>
<div class="ta-dialog__wrapper">
<div class="ta-dialog">
<div class="ta-dialog__header">
<span>{{ title }}</span>
<i class="ios-close-empty" @click="handleCancel()"></i>
</div>
<div class="ta-dialog__body">
<slot></slot>
</div>
<div class="ta-dialog__footer">
<button @click="handleCancel()">取消</button>
<button @click="handleOk()">确定</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Dialog",
props: {
title: {
type: String,
default: "标题"
}
},
methods: {
handleCancel() {
this.$emit("cancel");
},
handleOk() {
this.$emit("ok");
}
}
};
</script>
这样便完成了dialog
组件的开发,使用方法如下:
<ta-dialog title="弹窗标题" @ok="handleOk" @cancel="handleCancel">
<p>我是内容</p>
</ta-dialog>
这时候发现一个问题,通过使用 v-if
或者 v-show
来控制弹窗的展现时,没有动画!!!,看上去很生硬。教练,我想加动画
,这时候就该 transition
组件上场了。使用 transition
组件结合 css
能做出很多效果不错的动画。接下来增强 dialog
组件动画,代码如下:
<template>
<transition name="slide-down">
<div class="ta-dialog__wrapper" v-if="isShow">
<!-- 省略 -->
</div>
</transition>
</template>
<script>
export default {
data() {
return {
isShow: true
};
},
methods: {
handleCancel() {
this.isShow = false;
this.$emit("cancel");
},
handleOk() {
this.isShow = true;
this.$emit("ok");
}
}
};
</script>
可以看到 transition
组件接收了一个 name
props,那么怎么编写 css
完成动画呢?很简单的方式,写出两个
关键 class
(css 的 className)样式即可:
.slide-down-enter-active {
animation: dialog-enter ease 0.3s;
}
.slide-down-leave-active {
animation: dialog-leave ease 0.5s;
}
@keyframes dialog-enter {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes dialog-leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
就是这么简单就开发出了效果还不错的动效,注意 transition
组件的 name
为 slide-down
,而编写的动画的关键 className
为 slide-down-enter-active
和 slide-down-leave-active
。
Dialog
做MessageBox
Element 的MessageBox
的使用方法如下:
this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$message({
type: "success",
message: "删除成功!"
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除"
});
});
看到这段代码,我的感觉就是好神奇好神奇好神奇(惊叹三连)。仔细看看,这个组件其实就是一个封装好的 dialog
,
接下来,我也要封装一个这样的组件。首先,整理下思路:
this.$confirm
,这不就是挂到 Vue
的 prototype
上就行了then
是确定,catch
是取消,promise
就可以啦整理好思路,我就开始编码了:
import Vue from "vue";
import MessgaeBox from "./src/index";
const Ctur = Vue.extend(MessgaeBox);
let instance = null;
const callback = action => {
if (action === "confirm") {
if (instance.showInput) {
instance.resolve({ value: instance.inputValue, action });
} else {
instance.resolve(action);
}
} else {
instance.reject(action);
}
instance = null;
};
const showMessageBox = (tip, title, opts) =>
new Promise((resolve, reject) => {
const propsData = { tip, title, ...opts };
instance = new Ctur({ propsData }).$mount();
instance.reject = reject;
instance.resolve = resolve;
instance.callback = callback;
document.body.appendChild(instance.$el);
});
const confirm = (tip, title, opts) => showMessageBox(tip, title, opts);
Vue.prototype.$confirm = confirm;
至此,可能会疑惑怎么 callback
呢,其实我编写了一个封装好的 dialog
并将其命名为 MessageBox
,
它的代码中,有这样两个方法:
onCancel() {
this.visible = false
this.callback && (this.callback.call(this, 'cancel'))
},
onConfirm() {
this.visible = false
this.callback && (this.callback.call(this, 'confirm'))
},
没错,就是确定
和取消
时进行callback
。我还想说一说Vue.extend
,代码中引入了MessageBox
,
我不是直接new MessageBox
而是借助new Ctur
,因为这样可以定义数据(不仅仅是props)
,例如:
instance = new Ctur({ propsData }).$mount();
这时候,页面上其实是还没有MessageBox
的,我们需要执行:
document.body.appendChild(instance.$el);
如果你直接这样,你可能会发现 MessageBox
打开的时候没有动画,而关闭的时候有动画。解决方法也很简单,
appendChild
的时候让其仍是不可见,然后使用类这样的代码:
Vue.nextTick(() => (instance.visible = true));
这样就有动画了。
transition
和 css
实现不错的动画。其中,transition
组件的 name
决定了编写 css
的两个关键[name]-enter-active
和 [name]-leave-active
Vue.extend
继承一个组件的构造函数(不知道怎么说合适,就先这样说),然后通过这个构造函数,便可以document.body.appendChild
然后 Vue.nextTick(() => instance.visible = true)
字节跳动面试题目:利用已知函数 add
实现 asyncSum
。
function add (a, b, callback) {
callback(a + b)
}
async function asyncSum(...args) {
// 具体实现
}
// await asyncSum(1, 2, 3, 4, 5) 的结果应当为 1 + 2 + 3 + 4+ 5 = 15
解题思路:
[1, 2, 3, 4, 5] // 传参
[[1, 2], [3, 4], [5, 0]] // 分组
[3, 7, 5] // 各组求和
[[3, 7], [5, 0]] // 继续分组
[10, 5] // 继续各组求和
[[10, 5]] // 继续分组
[15] // 求和
首先,实现分组函数,将参数分成 2 个一组。
function chunk(arr) {
let ret = []
for (let i = 0; i < arr.length; i += 2) {
ret.push([arr[i], arr[i + 1] ? arr[i + 1] : 0])
}
return ret
}
chunk([1, 2, 3, 4, 5]) // [[1, 2], [3, 4], [5, 0]]
然后,实现 sum 函数对分组的数字依次求和。由于需要强依赖 add
函数,首先将 add
函数 promisify
。
function add (a, b, callback) {
callback(a + b)
}
function asyncAdd(a, b) {
return new Promise(resolve => add(a, b, sum => resolve(sum)))
}
实现 sum
函数。
function sum(nums) {
return Promise.all(chunk(nums).map(([a, b]) => asyncAdd(a, b)))
}
// await sum([1, 2, 3, 4, 5]) 结果 [3, 7, 5]
至此,可以看出数字长度在缩减,所以只需要继续拆分数组调用 sum
函数直到数字长度为 1 即为最终求和。
async function asyncSum(...args) {
let ret = await sum(args)
while (ret.length > 1) {
ret = await sum(ret)
}
return ret[0]
}
完整实现。
function chunk(arr) {
let ret = []
for (let i = 0; i < arr.length; i += 2) {
ret.push([arr[i], arr[i + 1] ? arr[i + 1] : 0])
}
return ret
}
function add (a, b, callback) {
callback(a + b)
}
function asyncAdd(a, b) {
return new Promise(resolve => add(a, b, sum => resolve(sum)))
}
function sum(nums) {
return Promise.all(chunk(nums).map(([a, b]) => asyncAdd(a, b)))
}
async function asyncSum(...args) {
let ret = await sum(args)
while (ret.length > 1) {
ret = await sum(ret)
}
return ret[0]
}
单例模式需确保只有一个实例且可以全局访问。
let __instance = (function() {
let instance;
return newInstance => {
if (newInstance) instance = newInstance;
return instance;
};
})();
class Singleton {
constructor(name) {
this.name = name;
if (!__instance()) {
__instance(this);
}
return __instance();
}
}
测试:
const s1 = new Singleton("s1");
const s2 = new Singleton("s2");
assert.strictEqual(s1, s2);
assert.strictEqual(s1.name, s2.name);
单例模式需要满足只有一个实例且可全局访问即可,可以使用 JavaScript 的闭包来实现。接下来以弹窗为例:
function createPopup(content) {
const div = document.createElement("div");
div.innerHTML = content;
return div;
}
将单例模式和创建弹窗代码解耦:
function createSingleton (fn) {
let result
return function () {
return result || result = fn.apply(this, arguments)
}
}
const createSingletonPopup = createSingleton(createPopup)
事件发布订阅模式可以帮助完成更松的解耦。
class EventEmitter {
constructor() {
this.listener = new Map();
}
on(type, fn) {
const subs = this.listener.get(type);
if (!subs) {
this.listener.set(type, [fn]);
} else {
this.listener.set(type, [].concat(subs, fn));
}
}
emit(...args) {
const type = args[0];
for (let listener of this.listener.get(type)) {
listener(...args.slice(1));
}
}
}
测试代码:
const em = new EventEmitter();
em.on("click", counter => {
assert.equal(counter, 1);
});
em.emit("click", 1);
代理对象和本体对象具有一致的接口,对使用者友好。代理模式的种类有很多种,在 JavaScript
中常见的是:虚拟代理和缓存代理。
const myImg = (() => {
const img = document.appendChild("img");
document.body.appendChild(img);
return {
setSrc: src => (img.src = src)
};
})();
const proxyImage = (() => {
const img = new Image();
img.onload = function() {
myImg.setSrc(this.src);
};
return {
setSrc: src => {
myImg.setSrc("loading.png");
img.src = src;
}
};
})();
proxyImage.setSrc("loaded.jpg");
const multiply = function(...args) {
return args.reduce((accu, curr) => (accu *= curr), 1);
};
const proxyMultiply = (() => {
const cache = {};
return (...args) => {
let tag = args.join(",");
if (cache[tag]) {
return cache[tag];
}
return (cache[tag] = multiply.apply(null, args));
};
})();
顾名思义,根据不同的参数(或配置)有不同的策略(函数)。
以表单验证为例,不同的字段应有不同的验证方法,即不同的策略。
class Checker {
constructor(check, message) {
this.check = check;
this.message = message;
}
}
class Validator {
constructor(config) {
this.config = config;
this.messages = [];
}
validate(data) {
for (let [k, v] of Object.entries(data)) {
const type = this.config.get(k);
const checker = Validator[type];
const result = checker.check(v);
if (!result) {
this.messages.push(checker.message(v));
}
}
return this;
}
isError() {
return this.messages.length > 0;
}
}
测试代码:
const data = {
name: "startegy",
age: 0
};
const config = new Map([["name", "isNotEmpty"], ["age", "isGreaterThan"]]);
Validator.isNotEmpty = new Strategy.Checker(
val => val.length > 0,
val => `The ${val} is empty`
);
Validator.isGreaterThan = new Strategy.Checker(
number => number > 20,
number => `The number ${number} is less than 20`
);
assert.equal(new Validator(config).validate(data).isError(), true);
能访问到聚合对象的顺序和元素。
iterator
接口const data = {
data: [1, 2, 3, 4, 5, 6],
[Symbol.iterator]() {
const len = this.data.length;
let index = 0;
return {
next: () => {
return index < len
? { value: this.data[index++], done: false }
: { value: undefined, done: true };
},
rewind: () => (index = 0)
};
}
};
svg-sprite-loader
在vue中的使用svg sprite
类似于CSS中的雪碧图。将svg整合在一起,呈现的时候根据symbolId
来显示特定的图标。
svg sprite
与symbol
元素可以这样简单理解,symbol
是一个个svg图标,而svg sprite
则是symbol
的集合,我们可以通过use
来指定使用哪一个svg
。
vue
中使用安装svg-sprite-loader
执行npm install --save-dev svg-sprite-loader
修改webpack.base.conf.js
在rules下添加并修改以下配置:
{
test: /\.svg$/,
loader: 'svg-sprite-loader',
include: [resolve('src/icons')],
options: {
symbolId: '[name]'
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
exclude: [resolve('src/icons')],
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
}
配置说明:
svg-sprite-loader:
这里用include: [resolve('src/icons')]
来假设项目中所用到svg
图标文件在src/icons
文件目录下,svg-sprite-loader
将只处理这里的文件
url-loader:
这里用xclude: [resolve('src/icons')]
来告诉url-loader
不要处理src/icons
下的图片文件(因为这里已经交给svg-sprite-loader
了)
icon
组件src/components
文件夹下新建文件夹icon
,并新建index.vue
文件,写入内容如下:<template>
<svg :width="width" :height="height">
<use :xlink:href="iconName"/>
</svg>
</template>
<script>
export default {
name: 'Icon',
props: {
type: {
default: 'sad'
},
width: {
default: 50
},
height: {
default: 50
}
},
computed: {
iconName() {
return '#' + this.$props.type
}
}
}
</script>
<style scoped>
svg {
fill: currentColor;
overflow: hidden;
}
</style>
src/icons
文件夹svg
文件都放在src/icons/svg
文件夹下,那么新建src/icons/index.js
文件,写入:import Vue from 'vue';
import Icon from '@/components/icon';
Vue.component('icon', Icon);
// 导入所有的svg(参照webpack文档: http://webpack.github.io/docs/context.html#dynamic-requires )
~function (requireContext) {
return requireContext.keys().map(requireContext)
}(require.context('./svg', false, /\.svg$/))
至此,如有不理解,可参照我的src
目录结构示意:
├── App.vue
├── assets
│ └── logo.png
├── components
│ └── icon
│ └── index.vue
├── icons
│ ├── index.js
│ └── svg
│ ├── more.svg
│ ├── navicon.svg
│ ├── pause.svg
│ ├── play.svg
│ ├── sad.svg
│ └── wifi.svg
└── main.js
main.js
引入src/icons/index.js
import Vue from 'vue'
import App from './App'
import './icons/index'
Vue.config.productionTip = false
new Vue({
el: '#app',
template: '<App/>',
components: { App }
})
App.vue
文件举个例子:<template>
<div id="app">
<icon type="play"></icon>
<icon type="sad"></icon>
<icon type="more"></icon>
<icon type="pause"></icon>
<icon type="wifi"></icon>
</div>
</template>
<script>
export default {
name: 'app'
}
</script>
<style>
#app {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
</style>
链表存储有序的元素集合,但不同于数组,链表中的元素在内存中不是连续放置的。每个元素由一个存储元素本身的节点和指向下一个元素的 引用 组成。
------------------------------------------------------------------------
| |
| node node null |
| --------------- --------------- -------- |
| head -> + item | next + -> + item | next + -> + null + |
| --------------- --------------- -------- |
| |
------------------------------------------------------------------------
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkList {
constructor() {
this.head = null;
this.length = 0;
}
/**
* 添加节点
* @param {*} value
*/
append(value) {
return this.insert(value, this.length);
}
/**
* 添加节点到指定位置
* @param {*} value
* @param {*} position
*/
insert(value, position) {
if (position < 0 || position > this.length) {
return false;
}
let node = new Node(value);
if (position === 0) {
node.next = this.head;
this.head = node;
} else {
let prev = null;
let current = this.head;
let i = 0;
while (i++ < position) {
prev = current;
current = current.next;
}
prev.next = node;
node.next = current;
}
this.length++;
return true;
}
/**
* 移出指定位置的节点
* @param {*} position
*/
removeAt(position) {
if (position < 0 || position > this.length) {
return false;
}
if (position === 0) {
this.head = this.head.next;
} else {
let prev = null;
let current = this.head;
let i = 0;
while (i++ < position) {
prev = current;
current = current.next;
}
prev.next = current.next;
}
this.length--;
return true;
}
/**
* 查找给定值所在索引
* @param {*} value
*/
indexOf(value) {
let current = this.head;
let index = -1;
while (current) {
indeex++;
if (current.value === value) {
return index;
} else {
current = current.next;
}
}
return -1;
}
reverse() {
let prev = null;
let current = this.head;
let next = null;
while (current) {
next = current.next;
current.next = prev;
prev = current;
current = next;
}
this.head = prev;
}
traverseFromTailToHead() {
let stack = [];
let current = this.head;
while (current) {
stack.push(current);
current = current.next;
}
while (stack.length) {
stack.pop();
}
}
getKthNodeFromTailToHead(k) {
let p1 = this.head;
let p2 = this.head;
let i = 0;
while (i++ < k) {
p1 = p1.next;
}
while (p1) {
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
}
题目:输入一个单链表,输出此链表中的倒数第 K 个节点。(去除头结点,节点计数从 1 开始)。
倒数第 k 个节点即为整数第 n - k 个节点(n 为链表长度)
function findKthTail(linkList, k) {
let p1 = linkList.head;
let p2 = linkList.head;
let i = 0;
while (i++ < k) {
p1 = p1.next;
}
while (p1) {
p1 = p1.next;
p2 = p2.next;
}
return p2;
}
单链表中的环是指链表末尾的节点的 next 指针不为 NULL ,而是指向了链表中的某个节点,导致链表中出现了环形结构。
0 -> 1 -> 2 -> 3 -> 4
| |
6 <- 5
链表中尾节点 6
指向了 节点 3
而非 null
,导致出现了环形结构。
null
,但若是有环,2 指针必定在一个节点处相遇,该节点不为 null
/**
* 判断链表是否存在环
* @param {*} linkList
*/
function isExistLoop(linkList) {
let p1 = linkList.head;
let p2 = linkList.head;
while (p1 && p2.next) {
p1 = p1.next;
p2 = p2.next.next;
if (p1 === p2) {
return true;
}
}
return false;
}
定位环的起点。
定义2个快慢指针,第一次先找到 2 个指针在环中的相遇点,然后令 p1 指向 相遇点,p2 指向头节点,同时出发(每次走过的节点相同),当 2 指针指向的节点相同时,p1 即为环的起点。
function getMeetingNode(linkList) {
let p1 = linkList.head;
let p2 = linkList.head;
while (p1 && p2.next) {
p1 = p1.next;
p2 = p2.next.next;
if (p1 == p2) {
return p1;
}
}
return null;
}
function getEntryOfLoop(linkList) {
let meetingNode = linkList.getMeetingNode();
if (!meetingNode) {
return null;
}
let p1 = meetingNode;
let p2 = linkList.head;
while (p1 != p2) {
p1 = p1.next;
p2 = p2.next;
}
return p1;
}
最近,使用 Vue 开发组件时遇到了这样一个问题:开发的组件所能够自定义的 props
比较多,导致使用该组件时需要传入太多属性,数据、样式控制什么的属性都在一起了,看起来很不美观,像下面这样:
<some-component title="我是标题" :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" :data="testData" />
虽然看起来可能还好,但是实际使用时可以自定义的属性是不止这些的,这样使用起来就很不美观了。于是就有了这样一个想法:定义一个类似于 React
中常写的 theme
组件,将一些非数据相关的 props
定义到 theme
组件上,theme
组件再自动将 props
透传给其他组件使用即可。theme
组件使用起来像这样:
<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
<some-component title="我是标题":data="testData" />
</thmem>
theme
组件在 React
中,开发这样一个高阶组件 theme
是很简单的。但是在 vue
中如何开发 theme
组件以达到上面设想的使用效果?通过翻阅 Vue
的文档,发现借助 $slots
和 render
函数可以做到。
export default {
name: 'theme',
render(h) {
const theme = this.$attrs // 通过 $attrs 可以拿到使用该组件时定义的 props,而无需声明有哪些 props
const merge = vNode => {
if (!vNode.tag) {
return
}
if (vNode.componentOptions) {
let props = vNode.componentOptions.propsData
props = Object.assign({}, theme, props)
vNode.componentOptions.propsData = props
} else {
if (!vNode.data) {
return
}
let attrs = vNode.data.attrs || {}
attrs = Object.assign({}, theme, attrs)
vNode.data.attrs = attrs
}
}
this.$slots.default.map(vNode => merge(vNode))
Object.keys(this.$attrs).forEach(key => {
this.$attrs[key] = null
})
return this.$slots.default[0] // 直接返回,无需额外渲染
}
}
如此便达到了这样的使用效果:
<theme :pos="[0, 0]" :size="[500, 400]" :radius="[0, 0.5]" :borderWidth="2" :borderColor="'#fff'" >
<some-component title="我是标题":data="testData" />
</thmem>
很显然 theme
组件是没有渲染的,它所做的也只不过是透传 props 给其它组件而已,称之为 无渲染组件
。
slot-scope
在 Vue文档 中提到了 slot-scope
可以使用作用域插槽变得更干净。那么结合 theme
组件的经验,可以写出这样一款 axios
组件。
Vue.component('s-axios', {
props: ['url'],
data() {
return {
loading: true,
response: null
}
},
created() {
axios.get(this.url)
.then(response => {
this.loading = false
this.response = response
})
},
render() {
return this.$scopedSlots.default({
loading: this.loading,
response: this.response
})
}
})
使用起来也很方便:
<div id="app">
<s-axios url="https://api.github.com/orgs/reactjs/repos">
<div slot-scope="{ loading, response }">
<div v-if="loading">loading</div>
<div v-else>响应数据为:${{ response.data }}</div>
</div>
</s-axios>
</div>
可以点击查看在线Demo。
通过 $slots
、$scopedSlots
结合 render
可以创造很多好玩的组件,比如本篇文章中说到的 无渲染组件 ,关键就在于使用者怎么想。
在网络请求、耗时操作等异步场景下,一般都会给用户一个指示(如 loading 加载中指示器)。但是有时候异步操作可能并没有想象的那么耗时,比如一个 ajax 请求可能会在 200ms 内完成,也可能会超过 200ms 才能完成,如果不管三七二十一直接上 loading,反而对用户体验不好。
总结一下:
针对 React
,可以封装 useAsyncLoading
hook,来完成这种操作。
useAsyncLoading
使用方式const [wrappedPromiseAction, loading] = useAsyncLoading(promiseAction, 200)
解释:针对 promiseAction
如果 200ms 内完成 loading
为 false
,反之为 true
。
useAsyncLoading
代码实现import React, { useState, useCallback, useEffect, useRef } from "react";
type PromiseAction = (...args: any[]) => Promise<any>;
function useAsyncLoading<A extends PromiseAction>(
action: A,
wait: number
): [A, boolean] {
const timerRef = useRef(null);
const [pending, setPending] = useState(false);
const [loading, setLoading] = useState(false);
const actionWithPending = useCallback(
(...args: Parameters<A>) => {
setPending(true);
const promise = action(...args);
promise.then(
() => setPending(false),
() => setPending(false)
);
return promise;
},
[action]
);
useEffect(() => {
clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
setLoading(pending);
}, wait);
return () => {
clearTimeout(timerRef.current);
};
}, [wait, pending]);
return [actionWithPending as A, loading];
}
useAsyncLoading
demo 示例import React, { useState, useCallback } from "react";
const mockApi = (): Promise<string> => {
const time = Math.random() * 400;
return new Promise((resolve) => {
setTimeout(() => {
resolve("此次请求耗时: " + time + "ms");
}, time);
});
};
export default function App() {
const [mock, loading] = useAsyncLoading(mockApi, 200);
const [data, setData] = useState("");
const getData = useCallback(() => {
mock().then((res) => {
setData(res);
});
}, [mock]);
return (
<div className="App">
<button onClick={getData}>发起请求</button>
{loading && <h1>loading</h1>}
{!loading && <p>{data}</p>}
</div>
);
}
数据可视化的主要任务是将数据转换为易于感知的图形。
很多人认为数据可视化无非就是数据几组数据,生成各自图表(或图形)等等。其实数据可视化大致可分为:
之前所提到的简单图表只是信息可视化中最常见的几种。面对不同的数据体积以及不同的可视化目标,可视化系统的复杂度很可能就会超出想象。
可视化整体可分为三步:分析 -》 处理 -》 生成。
分析分为三部分:任务、数据和领域。
首先,要分析该次可视化的出发点和目标是什么。遇到什么问题、展示什么信息、要得出什么结论、验证什么假说等等。数据承载的信息是多种多样的,不同的展示方式的侧重点也是不一样的(说白了,想清楚要干什么,才能确定要过滤什么数据、怎样处理数据最后怎样展示数据)。
其次,分析数据(见数据模型)。
最后要针对不同的领域,进行响应的分析。可视化的侧重点要跟随领域做出相应变化。
处理可分为两部分:对数据的处理、对视觉编码的处理。
在可视化之前,要对数据进行数据清洗、数据规范、数据分析等数据处理。
所谓视觉编码即指如何使用位置、尺寸、灰度值、纹理、色彩、方向、形状等视觉通道,以映射要展示的数据维度。
将之前的分析和设计实现。
数据说白了就是可定性或可量化的一组数据。为了更准确更形象地表达数据,先了解一些数据相关的概念。
数据为什么可以代表世界?带着这个问题,来了解数据和概念两个模型。
数据模型 是一组数字或符号的组合,其包含着数据的定义、类型等,可以进行各类数学操作。
概念模型 描述的是事物的语义或状态行为等。
现实世界 =》 概念模型 =》 数据模型
现实世界可以用概念模型描述,而概念模型又可以用数据模型来描述。经过两层抽象,数据便可以描述现实世界。
一个东西属于哪一类,取决于用什么标准划分,数据亦然。
按数据在计算机中的存储,数据可分为浮点数、整数、字符等;从关系模型的角度来说,数据可以分为实体和关系两类;从数据结构来说,数据可以分为一维、二维、三维、多维、时间序列、空间序列、树型、图型等等。接下来说一说和数据可视化有关的分类方法。
按照测量标度来分,数据一般分为四类:类别型、有序型、区间型和比值型。
在数据可视化中,通常不区分区间型和比值型,通一称为 数值型 。
id | 类型 | 款式 | 尺码 | 销量 | 年增长 |
---|---|---|---|---|---|
1 | 男款 | 上衣 | L | 50 | 10% |
2 | 女款 | 上衣 | S | 35 | 5% |
3 | 女款 | 裤子 | M | 40 | 20% |
4 | 男款 | 上衣 | XL | 30 | 15% |
如表所示,不难看出:
视觉编码描述的是将数据映射到最终可视化结果上的过程。
编码二字,编可以说是指设计、映射的过程,码是指一些图形符号。图形符号和信息间的映射关系可以使人迅速获取信息。可以说图形符号中携带了信息(称之为编码了一些信息)。而人从这些符号中读取信息时,可以称作时解码了一些信息。
人解码信息靠的是眼睛,人的视觉系统。如果说图形符号是编码信息的工具或通道,那么人的视觉系统便是解码信息的通道。通常把这种 图形符号 《--》 信息 《--》 视觉系统 的对应过程称为 视觉通道。
1967 年,Jacques Bertin 初版的《Semiology of Graphics》一书提出了图形符号与信息的对应关系,奠定了可视化编码的理论基础。该书中把图形符号分为两种:
后来又补充了 长度、面积、体积、透明度、模糊/聚焦 和 动画 等视觉通道。
首先说一下视觉通道的性质:
最后说一下视觉编码设计的两大原则:
数据可视化编码除了视觉通道还需考虑:
等等。
最近接到了奇舞团的电话面试,总共进行了两轮电话面试,其中有几个问题印象比较深刻,其中一个便是:“如何实现依次执行异步任务”(最近脑子不太好使了,愣是想不起来两轮面试问了哪些问题,故就不记录此次奇舞团面试笔记)。
现有 n 个异步任务,这 n 个异步任务是依次执行且下一个异步任务依赖上一个异步任务的结果作参数,问如何实现。
简单的 for 循环是依次进行循环的,利用这一特点加 async / await 很容易写出下面这样的代码:
(async () => {
const sleep = delay => {
return new Promise((resolve, reject) => {
setTimeout(_ => resolve(), delay)
})
}
const task = (i) => {
return new Promise(async (resolve, reject) => {
await sleep(500)
console.log(`now is ${i}`)
++i
resolve(i)
})
}
let param = 0
for (let i = 0; i < 4; i++) {
param = await task(param)
}
})()
输出:
now is 0
now is 1
now is 2
now is 3
效果虽然做到了,但是看到 param 这个局部变量就很不爽,请看解法2。
关于 Array.prototype.reduce
方法相信大部分小伙伴初见时都是用来数组求和。不了解的小伙伴可以点击链接了解下 reduce
。
reduce
有 初始值 ,积累值,以及 当前值 的概念。其中 “积累值”可以看作是 前一个值
,通过 返回 积累值
又可以看作是 下一个值(可能说的比较绕,可以参照 Redux 的 中间件执行顺序 的源码,也是用的 reduce
)。使用 reduce
来解决问题的代码为:
const sleep = delay => {
return new Promise((resolve, reject) => {
setTimeout(_ => resolve(), delay)
})
}
const task = (i) => {
return new Promise(async (resolve, reject) => {
await sleep(500)
console.log(`now is ${i}`)
++i
resolve(i)
})
}
[task, task, task, task].reduce(async (prev, task) => {
const res = await prev
return task(res)
}, 0)
输出:
now is 0
now is 1
now is 2
now is 3
可以这样理解 prev
和 task
:
当前的异步任务需要上一个异步任务的结果作参数,故很显然要 await prev
。
要学好ES6,要不断尝试写出优雅的代码。
使用vue-cli
可以直接生成一个包含unit & e2e
测试的开发环境。这里我们主要针对unit
文件进行单元测试
。
├── coverage
├── jest.conf.js
├── setup.js
└── specs
├── api-test.spec.js
├── click-test.spec.js
├── data-test.spec.js
├── dom-test.spec.js
├── input-test.spec.js
├── mock-test.spec.js
└── props-test.spec.js
npm run unit
后,可在该文件夹下打开index.html
文件查看覆盖率)。假设我们编写了这样一个组件:
<!-- data-test.vue -->
<template>
<span>{{ msg }}</span>
</template>
<script>
export default {
name: 'TestData',
data() {
return {
msg: 'hello'
}
},
created() {
this.msg = 'bye'
}
}
</script>
那么,我们可以这样编写断言
// data.spec.js
// 导入vue.js和组件,进行测试
import Vue from 'vue'
import TestDataComponent from '@/components/data-test.vue'
describe('TestDataComponent', () => {
// 检查原始组件选项
it('has a created hook', () => {
expect(typeof TestDataComponent.created).toEqual('function')
})
// 评估原始组件选项中的函数的结果
it('should set the correct default data', () => {
expect(typeof TestDataComponent.data).toEqual('function')
const data = TestDataComponent.data()
expect(data.msg).toEqual('hello')
})
// 检查mount中的组件实例
it('should correctly set the msg when created', () => {
const vm = new Vue(TestDataComponent).$mount()
expect(vm.msg).toEqual('bye')
})
// 创建一个实例并检查渲染输出
it('should render correct msg', () => {
const Ctor = Vue.extend(TestDataComponent)
const vm = new Ctor().$mount()
expect(vm.$el.textContent).toEqual('bye')
})
})
很多组件的渲染输出由它的 props 决定。事实上,如果一个组件的渲染输出完全取决于它的 props,那么它会让测试变得简单,就好像断言不同参数的纯函数的返回值。例子:
<template>
<!-- props-test.vue -->
<p>{{ msg }}</p>
</template>
<script>
export default {
props: ['msg']
}
</script>
那么我们就可以在不同的 props
中,通过 propsData 选项断言它的渲染输出:
// props.spec.js
import Vue from 'vue'
import TestPropsComponent from '@/components/props-test.vue'
const Ctor = Vue.extend(TestPropsComponent);
// 针对props,可以通过propsData选项断言它的渲染输出
const getRenderText = (propsData) => {
const vm = new Ctor({propsData}).$mount()
return vm.$el.textContent
}
describe('TestPropsComponent', () => {
it('should render correctly with different props', () => {
expect(getRenderText({msg: 'Hello'})).toEqual('Hello')
expect(getRenderText({msg: 'Bye'})).toEqual('Bye')
})
})
由于 Vue 进行 异步更新 DOM 的情况,一些依赖 DOM 更新结果的断言必须在 Vue.nextTick 回调中进行:
<template>
<!-- dom-test.vue -->
<span>{{ msg }}</span>
</template>
<script>
export default {
data() {
return {
msg: 'Hello'
}
}
}
</script>
断言的编写:
// dom.spec.js
import Vue from 'vue'
import DomUpdateComponent from '@/components/dom-test.vue'
describe('DomUpdateComponent', () => {
it('the msg should be Hello', () => {
const data = DomUpdateComponent.data()
expect(data.msg).toEqual('Hello')
})
// 在状态更新后检查生成的HTML
it('the msg should change to Bye', () => {
const vm = new Vue(DomUpdateComponent).$mount()
vm.msg = 'Bye'
// 在状态改变后和断言 DOM 更新前等待一刻
Vue.nextTick(() => {
expect(vm.$el.textContent).toEqual('Bye')
})
})
})
假设我们编写了这样一个组件,其中有一个按钮,按钮有一个点击事件
:
<!-- click-test.vue -->
<template>
<div>
<p>现在是{{ num }}</p>
<button @click="increase">change</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
},
methods: {
increase() {
this.num++
}
}
}
</script>
那么,我们可以这样在断言中触发事件:
// click.spec.js
import Vue from 'vue'
import ClickTestComponent from '@/components/click-test.vue'
describe('ClickTestComponent', () => {
const Ctor = Vue.extend(ClickTestComponent)
const vm = new Ctor().$mount()
// 获取按钮
const oBtn = vm.$el.querySelector('button')
// 创建点击事件
const clickEvent = new window.Event('click')
it("the num should be 1", () => {
// 触发按钮点击事件
oBtn.dispatchEvent(clickEvent)
// 需要手动监听更新
vm._watcher.run()
expect(vm.$el.textContent).toContain('1')
})
it("the num should be 2", () => {
// 触发按钮点击事件
oBtn.dispatchEvent(clickEvent)
// 需要手动监听更新
vm._watcher.run()
expect(vm.$el.textContent).toContain('2')
})
})
假设我们有一个输入框,并且绑定了一些事件:
<template>
<!-- input-test.vue -->
<div>
<p>{{ msg }}</p>
<input type="text" v-model="input">
<button @click="handle"></button>
</div>
</template>
<script>
export default {
data() {
return {
msg: '',
input: 'hi'
}
},
methods: {
handle() {
this.msg = this.input
}
}
}
</script>
很显然,我们是要利用v-model
模拟input
进行断言:
// input.spec.js
import Vue from 'vue'
import InputTestComponent from '@/components/input-test.vue'
describe('InputTestComponent', () => {
const Ctor = Vue.extend(InputTestComponent)
const vm = new Ctor().$mount()
const oBtn = vm.$el.querySelector('button')
const clickEvent = new window.Event('click')
it('the msg should be equal with input', () => {
vm.input = 'let me change msg to what I say'
oBtn.dispatchEvent(clickEvent)
// 手动监听更新
vm._watcher.run()
expect(vm.$el.textContent).toContain('let me change msg to what I say')
})
});
mock.js
模拟后端接口src
文件夹下新建mock
文件夹,编写index.js
:// mock/index.js
import Mock from 'mockjs'
export default Mock.mock('http://api.rs.com/v1/test', {
'greeting': 'Hi, Vue'
});
axios
进行ajaxsrc
文件夹下新建provider
文件夹,编写index.js
:// provider/index.js
import axios from 'axios'
import '../mock/index'
const getGreeting = async (url = 'http://api.rs.com/v1/test') => {
try {
const res = await axios.get(url)
return res.data
} catch (err) {
throw new Error('获取问候语失败')
}
}
export default getGreeting
<template>
<!-- mock-test.vue -->
<p>{{ msg }}</p>
</template>
<script>
import getGreeting from '@/provider/index'
export default {
data() {
return {
msg: 'waiting'
}
},
created() {
this.getMsg()
},
methods: {
async getMsg(url) {
try {
const data = await getGreeting(url)
this.msg = data.greeting
} catch (err) {
console.log(err.message)
}
}
}
}
</script>
provider/index.js
进行单元测试// api.spec.js
import getGreeting from '@/provider/index'
describe('test greeting', async () => {
it('res should be "Hi, Vue"', async () => {
const res = await getGreeting();
expect(res.greeting).toEqual('Hi, Vue')
})
it('err should be catched', async () => {
try {
const res = await getGreeting('wrongUrl')
} catch (err) {
expect(err.message).toEqual('获取问候语失败')
}
})
})
// mock-test.spec.js
import Vue from 'vue'
import MockTestComponet from '@/components/mock-test.vue'
describe('MockTestComponet', () => {
it('should have the created hook', () => {
expect(typeof MockTestComponet.created).toEqual('function')
})
it('the msg should be waiting', () => {
const data = MockTestComponet.data()
expect(data.msg).toEqual('waiting')
})
it('the msg should change when created', async () => {
// const vm = new Vue(MockTestComponet).$mount()
const Ctor = Vue.extend(MockTestComponet)
const vm = new Ctor().$mount()
setTimeout(() => {
expect(vm.msg).toEqual('Hi, Vue')
}, 0)
})
// 返回正确的结果
it('test methods getMsg by correct url', async () => {
const vm = new Vue(MockTestComponet)
await vm.getMsg();
expect(vm.msg).toEqual('Hi, Vue')
})
// 抛出异常
it('test methods getMsg by wrong url', async () => {
try {
const vm = new Vue(MockTestComponet)
await vm.getMsg('fuck');
} catch (err) {
expect(err.message).toEqual('获取问候语失败')
}
})
})
最近学习了 react-redux-typescript-guide,在此记录一下。
npm i -D @types/react @types/react-dom
React.FC<props> | React.FunctionComponent<Props>
例如:
const MyComponent: React.FC<Props> = ...
React.Component<Props, State>
例如:
class MyComponent extends React.Component<Props, State> { ... }
React.ComponentType<Props>
例如:
const withState = <P extends WrappedComponentProps>(
WrappedComponent: React.ComponentType<P>,
) => { ... }
props
React.ComponentProps<typeof XXX>
例如:
type MyComponentProps = React.ComponentProps<typeof MyComponent>;
React.ReactElement | JSX.Element;
例如:
const elementOnly: React.ReactElement = <div /> || <MyComponent />;
React.ReactNode;
例如:
const elementOrPrimitive: React.ReactNode = 'string' || 0 || false || null || undefined || <div /> || <MyComponent />
const Component = ({ children: React.ReactNode }) => { ... }
React.CSSProperties;
例如:
const style: React.CSSProperties = { color: "red" };
const element: React.ReactElement = <div style={style} />;
React.HTMLProps;
例如:
const Input: React.FC<Props & React.HTMLProps<HTMLInputElement>> = props => { ... }
<Input accept={...} alt={...} />
React.ReactEventHandler<HTMLXXXElement>
例如:
const handleChange: React.ReactEventHandler<HTMLInputElement> = (evt) => { ... }
<input onChange={handleChange} />
React.XXXEvent<HTMLXXXElement>
例如:
const handleChange = (ev: React.MouseEvent<HTMLDivElement>) => { ... }
<div onMouseMove={handleChange} />
import * as React from "react";
type Props = {
label: string,
count: number,
onIncrement: () => void
};
export const FCCounter: React.FC<Props> = props => {
const { label, count, onIncrement } = props;
return (
<div>
<span>
{label}: {count}
</span>
<button onClick={onIncrement}>Increment</button>
</div>
);
};
使用:
<FCCounter
label={"FCCounter"}
count={this.state.count}
onIncrement={() => this.setState({ count: this.state.count + 1 })}
/>
import * as React from "react";
type Props = {
label: string;
initialCount: number;
};
type State = {
count: number;
};
export class StatefulCounter extends React.Component<Props, State> {
static defaultProps = {
initialCount: 0
};
readonly state: State = {
count: this.props.initialCount
};
componentWillReceiveProps({ initialCount }: Props) {
this.setState({ count: initialCount });
}
onIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
const { label } = this.props;
const { count } = this.state;
return (
<div>
<span>
{label}: {count}
</span>
<button onClick={this.onIncrement}>Increment</button>
</div>
);
}
}
使用:
<StatefulCounter label={"StatefulCounter"} initialCount={10} />
import * as React from "react";
export interface IGenericListProps<T> {
items: T[];
itemRenderer: (item: T) => React.ReactElement;
}
export class GenericList<T> extends React.Component<IGenericListProps<T>, {}> {
render() {
const { items, itemRenderer } = this.props;
return <div>{items.map(itemRenderer)}</div>;
}
}
使用:
interface IUser {
name: string;
}
const users: IUser[] = [{ name: "Mike" }, { name: "Tom" }, { name: "David" }];
class UserList extends GenericList<IUser> {}
<UserList
items={users}
itemRenderer={({ name }) => <p key={name}>{name}</p>}
/>;
import * as React from "react";
export interface IMouseProviderProps {
render: (state: IMouseProviderState) => React.ReactNode;
}
interface IMouseProviderState {
readonly x: number;
readonly y: number;
}
export class MouseProvider extends React.Component<
IMouseProviderProps,
IMouseProviderState
> {
readonly state: IMouseProviderState = {
x: 0,
y: 0
};
handleMousemove = (evt: React.MouseEvent<HTMLDivElement>) => {
this.setState({
x: evt.clientX,
y: evt.clientY
});
};
render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMousemove}>
{this.props.render(this.state)}
</div>
);
}
}
使用:
<div style={{ height: 80, border: "1px solid #000" }}>
<MouseProvider render={({ x, y }) => `x: ${x}, y: ${y}`} />
</div>
import * as React from "react";
import { Subtract } from "utility-types";
interface IInjectedProps {
count: number;
onIncrement: () => void;
}
export const withState = <BaseProps extends IInjectedProps>(
_BaseComponent: React.ComponentType<BaseProps>
) => {
const BaseComponent = (_BaseComponent as unknown) as React.ComponentType<
IInjectedProps
>;
type HOCProps = Subtract<BaseProps, IInjectedProps> & {
initialCount?: number;
};
type HOCState = { readonly count: number };
return class HOC extends React.Component<HOCProps, HOCState> {
readonly state: HOCState = {
count: Number(this.props.initialCount) || 0
};
onIncrement = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<BaseComponent
count={this.state.count}
onIncrement={this.onIncrement}
{...this.props}
/>
);
}
};
};
使用:
const FCCounterWithState = withState(FCCounter);
<FCCounterWithState initialCount={20} label={"FCCounter withState"} />;
本文接下来会一步一步模仿造一个低配版的 Element 的对话框和弹框组件
。
当使用vue-cli
初始化一个项目的时候,会发现 src/components
文件夹下有一个 HelloWorld.vue
文件,这便是单文件组件的基本开发模式。
// 注册
Vue.component("my-component", {
template: "<div>A custom component!</div>"
});
// 创建根实例
new Vue({
el: "#example"
});
接下来,开始写一个 dialog
组件。
目标对话框组件的基本样式如图:
根据目标样式,可以总结出:
title
props 来标示弹窗标题确定
按钮时 发射
出 确定
事件(即告诉父组件确定
了)发射
出 取消
事件那么,编码如下:
<template>
<div class="ta-dialog__wrapper">
<div class="ta-dialog">
<div class="ta-dialog__header">
<span>{{ title }}</span>
<i class="ios-close-empty" @click="handleCancel()"></i>
</div>
<div class="ta-dialog__body">
<slot></slot>
</div>
<div class="ta-dialog__footer">
<button @click="handleCancel()">取消</button>
<button @click="handleOk()">确定</button>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Dialog",
props: {
title: {
type: String,
default: "标题"
}
},
methods: {
handleCancel() {
this.$emit("cancel");
},
handleOk() {
this.$emit("ok");
}
}
};
</script>
这样便完成了dialog
组件的开发,使用方法如下:
<ta-dialog title="弹窗标题" @ok="handleOk" @cancel="handleCancel">
<p>我是内容</p>
</ta-dialog>
这时候发现一个问题,通过使用 v-if
或者 v-show
来控制弹窗的展现时,没有动画!!!,看上去很生硬。教练,我想加动画
,这时候就该 transition
组件上场了。使用 transition
组件结合 css
能做出很多效果不错的动画。接下来增强 dialog
组件动画,代码如下:
<template>
<transition name="slide-down">
<div class="ta-dialog__wrapper" v-if="isShow">
<!-- 省略 -->
</div>
</transition>
</template>
<script>
export default {
data() {
return {
isShow: true
};
},
methods: {
handleCancel() {
this.isShow = false;
this.$emit("cancel");
},
handleOk() {
this.isShow = true;
this.$emit("ok");
}
}
};
</script>
可以看到 transition
组件接收了一个 name
props,那么怎么编写 css
完成动画呢?很简单的方式,写出两个
关键 class
(css 的 className)样式即可:
.slide-down-enter-active {
animation: dialog-enter ease 0.3s;
}
.slide-down-leave-active {
animation: dialog-leave ease 0.5s;
}
@keyframes dialog-enter {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes dialog-leave {
from {
opacity: 1;
transform: translateY(0);
}
to {
opacity: 0;
transform: translateY(-20px);
}
}
就是这么简单就开发出了效果还不错的动效,注意 transition
组件的 name
为 slide-down
,而编写的动画的关键 className
为 slide-down-enter-active
和 slide-down-leave-active
。
Dialog
做MessageBox
Element 的MessageBox
的使用方法如下:
this.$confirm("此操作将永久删除该文件, 是否继续?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning"
})
.then(() => {
this.$message({
type: "success",
message: "删除成功!"
});
})
.catch(() => {
this.$message({
type: "info",
message: "已取消删除"
});
});
看到这段代码,我的感觉就是好神奇好神奇好神奇(惊叹三连)。仔细看看,这个组件其实就是一个封装好的 dialog
,
接下来,我也要封装一个这样的组件。首先,整理下思路:
this.$confirm
,这不就是挂到 Vue
的 prototype
上就行了then
是确定,catch
是取消,promise
就可以啦整理好思路,我就开始编码了:
import Vue from "vue";
import MessgaeBox from "./src/index";
const Ctur = Vue.extend(MessgaeBox);
let instance = null;
const callback = action => {
if (action === "confirm") {
if (instance.showInput) {
instance.resolve({ value: instance.inputValue, action });
} else {
instance.resolve(action);
}
} else {
instance.reject(action);
}
instance = null;
};
const showMessageBox = (tip, title, opts) =>
new Promise((resolve, reject) => {
const propsData = { tip, title, ...opts };
instance = new Ctur({ propsData }).$mount();
instance.reject = reject;
instance.resolve = resolve;
instance.callback = callback;
document.body.appendChild(instance.$el);
});
const confirm = (tip, title, opts) => showMessageBox(tip, title, opts);
Vue.prototype.$confirm = confirm;
至此,可能会疑惑怎么 callback
呢,其实我编写了一个封装好的 dialog
并将其命名为 MessageBox
,
它的代码中,有这样两个方法:
onCancel() {
this.visible = false
this.callback && (this.callback.call(this, 'cancel'))
},
onConfirm() {
this.visible = false
this.callback && (this.callback.call(this, 'confirm'))
},
没错,就是确定
和取消
时进行callback
。我还想说一说Vue.extend
,代码中引入了MessageBox
,
我不是直接new MessageBox
而是借助new Ctur
,因为这样可以定义数据(不仅仅是props)
,例如:
instance = new Ctur({ propsData }).$mount();
这时候,页面上其实是还没有MessageBox
的,我们需要执行:
document.body.appendChild(instance.$el);
如果你直接这样,你可能会发现 MessageBox
打开的时候没有动画,而关闭的时候有动画。解决方法也很简单,
appendChild
的时候让其仍是不可见,然后使用类这样的代码:
Vue.nextTick(() => (instance.visible = true));
这样就有动画了。
transition
和 css
实现不错的动画。其中,transition
组件的 name
决定了编写 css
的两个关键[name]-enter-active
和 [name]-leave-active
Vue.extend
继承一个组件的构造函数(不知道怎么说合适,就先这样说),然后通过这个构造函数,便可以document.body.appendChild
然后 Vue.nextTick(() => instance.visible = true)
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.