keep-run / blog Goto Github PK
View Code? Open in Web Editor NEW随笔博客,保持上进的心,每天进步一点点
随笔博客,保持上进的心,每天进步一点点
箭头函数和普通函数的区别:
this
普通函数的this是在执行时绑定的,但是箭头函数本身没有this
.所以需要通过查找作用域链来确定this
的值。如果箭头函数被普通函数包含,那么箭头函数中的this就是最近一层非箭头函数的this
;
arguments
对象获取参数的时候,可以用参数名来获取,或者用ES6中的 rest参数获取;
new
关键字调用JS函数有两个内部方法[[Call]]和[[Construct]]
箭头函数没有[[Construct]]方法,因此不能作为构造函数使用,如果用new来调用,会报错;
由于不能使用new调用箭头函数,因此没有构建原型的需求,所以箭头函数没有prototype
super
箭头函数没有原型,所以也不能通过super来访问原型属性。--实际上箭头函数没有spuer;跟 this、arguments、new.target 一样,这些值由外围最近一层非箭头函数决定。
前边两篇文章基本实现了一个简单的promise
。但是除此之外,promise
还有一些常用的基础方法,比如 catch
, finially
以及 resolve
,reject
,race
, any
,all
等静态方法。,这里来谈谈其实现原理。
resolve
& reject
resolve
和reject
这两个方法是promise的静态方法,都返回一个新的promise
,对应的状态分别为 fulfilled
和rejected
。实现起来相对比较简单:
class MyPromise {
...
static resolve (data) {
return new MyPromise(resolve => {
resolve(data)
})
}
static reject (data) {
return new MyPromise((resolve,reject) => {
reject(data)
})
}
}
大家都知道一句话,catch本质上是then的语法糖,这是个什么意思呢,个人理解就是catch是then的某一种使用方法而已,其实finally也一样。都可能通过包装then
来实现,代码如下:
class MyPromise {
...
// 捕获前边未处理的reject。
catch (onRejected) {
return this.then(null,onRejected)
}
// 无论前边promise是何种状态,都会执行的方法
finally (fn) {
return this.then(fn,fn)
}
}
all
也是一个静态方法,将多个promise实例包装成一个新的promise,使用方法如下:
const p = Promise.all([p1,p2,p3])
使用说明如下:
p1
,p2
,p3
都是promise
实例,如果不是会自动使用Promise.all
包装;p1
,p2
,p3
都变成fulfilled
状态时,p
才会变成fulfilled
状态, p1
,p2
,p3
返回值组成一个数组传递给p
的回调函数。p1
,p2
,p3
中只要有一个变为rejected
状态,p
就变成rejected
状态,第一个被reject
的实例返回值传给p
的回掉函数。代码实现
class MyPromise {
...
static all(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const result = []
const Arrlength = promiseArr.length
let tempItem
let resolveNums = 0 //记录fulfilled实例的个数
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then((data) => {
resolveNums++
result[i] = data // 保证前后位置对应
// 全部都变为fulfilled,返回
if (resolveNums === Arrlength) {
resolve(result)
}
}, (data) => {
reject(data)
})
}
})
}
}
测试demo
const p1 = new MyPromise(resolve => {
setTimeout(resolve, 1000, "p1")
})
const p2 = 'p2'
const p3 = MyPromise.resolve('p3')
MyPromise.all([p1, p2, p3]).then(data => {
console.log(data)
}).catch(data => {
console.log(data)
})
一秒后输出 "p1","p2","p3"
any
和all
接受的参数以及规则是一样的,但是功能正好相反,比如以下代码
const p = Promise.all([p1,p2,p3])
p1
,p2
,p3
都变成rejected
状态时, p
才会变成rejected
;p1
,p2
,p3
有一个变为fulfilled
状态时,p
就会变成fulfilled
;代码实现
class MyPromise {
...
static any(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const result = []
const Arrlength = promiseArr.length
let tempItem
let rejectedNums = 0 //记录rejected实例的个数
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then(data => {
resolve(data)
}, data => {
result[i] = data
if (++rejectedNums === Arrlength) {
reject(result)
}
})
}
})
}
}
测试demo
const p1 = new MyPromise((resolve, reject) => {
setTimeout(reject, 1000, "p1")
})
const p2 = MyPromise.reject('p2')
const p3 = MyPromise.reject('p3')
MyPromise.any([p1, p2, p3]).then(data => {
console.log("then", data)
}).catch(data => {
console.log("reject", data)
})
1s后输出
reject [ 'p1', 'p2', 'p3' ]
race
接受的参数和参数的处理规则和any
以及all
是一样的。实现的功能是:参数中的promise
,有任意一个状态改变,新的promise
状态就会改变。率先改变的promise
的返回值将会传递给回调函数;
代码实现
class MyPromise {
...
static race(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const Arrlength = promiseArr.length
let tempItem
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then((data) => {
resolve(data)
}, (data) => {
reject(data)
})
}
})
}
}
测试demo
const p1 = new MyPromise((resolve, reject) => {
setTimeout(reject, 1000, "p1")
})
const p2 = new MyPromise((resolve, reject) => {
setTimeout(reject, 2000, "p2")
})
const p3 = new MyPromise((resolve, reject) => {
setTimeout(reject, 3000, "p3")
})
MyPromise.race([p1, p2, p3]).then(data => {
console.log("then", data)
}).catch(data => {
console.log("reject", data)
})
1s后输出
reject p1
// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'
class MyPromise {
state = PENDING
value = '' // 向后传的value值
callbacks = [] // 回调队列
constructor(fn) {
if (typeof fn !== "function") {
throw new Error('参数必须是函数')
}
fn(this._resolve.bind(this), this._reject.bind(this))
}
static resolve(data) {
return new MyPromise(resolve => {
resolve(data)
})
}
static reject(data) {
return new MyPromise((resolve, reject) => {
reject(data)
})
}
static all(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const result = []
const Arrlength = promiseArr.length
let tempItem
let resolveNums = 0 //记录fulfilled实例的个数
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then((data) => {
resolveNums++
result[i] = data
// 全部都变为fulfilled,返回
if (resolveNums === Arrlength) {
resolve(result)
}
}, (data) => {
reject(data)
})
}
})
}
static race(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const Arrlength = promiseArr.length
let tempItem
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then((data) => {
resolve(data)
}, (data) => {
reject(data)
})
}
})
}
static any(promiseArr) {
if (!Array.isArray(promiseArr)) {
throw new Error('参数必须是数组')
}
return new MyPromise((resolve, reject) => {
const result = []
const Arrlength = promiseArr.length
let tempItem
let rejectedNums = 0 //记录rejected实例的个数
for (let i = 0; i < Arrlength; i++) {
tempItem = promiseArr[i]
if (!(tempItem instanceof MyPromise)) {
tempItem = MyPromise.resolve(tempItem)
}
tempItem.then(data => {
resolve(data)
}, data => {
result[i] = data
if (++rejectedNums === Arrlength) {
reject(result)
}
})
}
})
}
// 捕获前边未处理的reject。
catch(onRejected) {
return this.then(null, onRejected)
}
// 无论前边promise是何种状态,都会执行的方法
finally(fn) {
return this.then(fn, fn)
}
then(onFulfilled, onRejected) {
const _this = this; //_this指向前一个promise对象
return new MyPromise((resolve, reject) => {
_this._handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
_resolve(value) {
if (this.state !== PENDING) {
return
}
this.state = FULFILLED //修改状态
this.value = value // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_reject(error) {
if (this.state !== PENDING) {
return
}
this.state = REJECTED //修改状态
this.value = error // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_handle(callback) {
// prnding状态时,注册函数入栈
if (this.state === PENDING) {
this.callbacks.push(callback)
return
}
// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected
// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject
let ret
if (cb) {
ret = cb(this.value) //计算继续向后传递的值
// 返回一个promise
if (ret instanceof MyPromise) {
ret.then((data) => {
callback.resolve(data)
}, (err) => {
callback.reject(err)
})
} else {
callback.resolve(ret)
}
} else {
cb_changeState(this.value) //then里没有回调函数,把当前value继续向下传递
}
}
}
一般公司的开发机自带nginx;
yum install nginx
;service nginx start
;配置跨域:
location / {
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ($request_method = 'OPTIONS') {
return 204;
}
}
详情:https://segmentfault.com/a/1190000012550346?utm_source=tag-newest
使用索引类型,编译器可以检查使用动态属性名的代码。
keyof
。对任何类型T
,keyof T
为已知类型T
的属性名的联合,比如interface Person {
name: string;
age: number;
}
let personProps:keyof Person //personProps:"name" | "age"
T[K]
interface Person {
name: string;
age: number;
}
function demo<T extends object, K extends keyof T>(obj: T, names: K[]): T[K][] {
return names.map(item => obj[item])
}
let people: Person = {
name: "test",
age: 21
}
let res1 = demo(people, ["name"]) //string[] 类型
let res2 = demo(people, ["name",'age']) //(string|number)[] 类型
T[K]
表明people['name']的类型为Person['name']。
类型别名和接口用法相似,但是还是有细微区别
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
// 有一个变量是Bird或者Fish中的一个,如何区分?
function isFish(pet:Fish|Bird):pet is Fish{
return (<Fish>pet).swim!==undefined
}
program.parse(process.argv)
;-h
只会列出所有的指令,但是这些指令的 options会折叠起来,不会展示, 比如:create [options]
,想要看options的详细信息,可以执行xx create --help
在react的日常开发过程中,ref虽然使用不多,但是却是一个非常重要的属性,可以解决很多实际问题。比如:
useRef
和useImperativeHandle
使用,可以暴露出供父组件访问的属性和方法(注意函数组件没有实例,不能通过获取实例的方式访问子组件);下边就具体看看函数组件和class组件在使用中的区别;
import React, { Component, createRef } from 'react'
import { Button } from 'antd'
class ChildComp extends Component {
state = {
bgColor: 'red'
}
setBgColor = (bgColor) => {
this.setState({ bgColor })
}
render() {
return (<div style={{ width: '200px', height: "50px", backgroundColor: this.state.bgColor }} />)
}
}
export default class Demo extends Component {
constructor(props) {
super(props)
this.childRef = createRef()
}
/** 父组件中调用子组件的方法,从而改变子组件的状态 */
setChildBgColor = (color) => {
this.childRef.current.setBgColor(color)
}
render() {
return (
<>
<Button onClick={() => { this.setChildBgColor('red') }}>red</Button>
<Button onClick={() => { this.setChildBgColor('black') }}>black</Button>
<Button onClick={() => { this.setChildBgColor('green') }}>green</Button>
<ChildComp ref={this.childRef} />
</>
)
}
}
import React, { useRef, forwardRef, useState, useImperativeHandle } from 'react'
import { Button } from 'antd'
function Child(props, ref) {
const [bgcolor, setBgColor] = useState('red')
useImperativeHandle(ref, () => {
return { setBgColor: setBgColor } //区别在这里,手动暴露供父组件访问的属性和方法
})
return <div style={{ width: '200px', height: "50px", backgroundColor: bgcolor }} />
}
const ChildComp = forwardRef(Child) // 注意ref需要用forwardRef承接,不能直接从props中取
export default () => {
const childRef = useRef(null)
const setChildBgColor = (color) => {
childRef.current.setBgColor(color)
}
return (
<>
<Button onClick={() => { setChildBgColor('red') }}>red</Button>
<Button onClick={() => { setChildBgColor('black') }}>black</Button>
<Button onClick={() => { setChildBgColor('green') }}>green</Button>
<ChildComp ref={childRef} />
</>
)
}
实现的功能相似,但是注意实现上的差异,见函数组件的备注。
长列表优化是一个常见的问题,简单来说就是服务一下给你返回一万条数据,前端如果真的全部渲染出来,性能将是一个无法想象的问题;怎么做到只渲染可视区以及附近那几条数据的渲染,其他区域不渲染是一个前端问题,也就是这里所说的长列表优化。一般来讲这种问题有两种解决办法:
分页处理是一个常见的处理方式,技术上没啥难度;虚拟列表处理在社区有很多讨论,也有npm包可以直接使用,这里分析一下其原理和简易实现;
虚拟列表优化的前提条件是:列表每一项的高度一样活着近似,否则会计算不准确;
虚拟列表的核心有以下几点:
scroll
事件,通过scrollTop
的值计算当前可视区应该展示的数据的范围;import React, { useState, useEffect, useRef, } from 'react'
import './index.less'
const height = 40 //列表的每个item的高度为40
const bufferSize = 5 //缓冲区域,实现无缝滚动的效果
export default (props) => {
const [startOffset, setStartOffset] = useState<number>(0)
const [endOffset, setEndOffset] = useState<number>(0)
const [visibleData, setVisibleData] = useState<Array<any>>([])
const container = useRef(null)
const actionData: any = useRef({
startIndex: 0,
endIndex: 0,
visibleCount: 10 + bufferSize,
data: (new Array(1000)).fill('test') //初始化1000条数据做测试
})
//计算 startIndex 和 endIndex 并设置上下边距
const updateBoundary = (scrollTop) => {
const { data, visibleCount } = actionData.current
const dataLength = data.length
const startIndex = Math.min(Math.floor(scrollTop / height), dataLength - visibleCount)
const endIndex = startIndex + visibleCount
actionData.current.startIndex = startIndex
actionData.current.endIndex = endIndex
setStartOffset(startIndex * height)
setEndOffset((dataLength - endIndex) * height)
}
// 通过 startIndex, endIndex 截取真实的数据做渲染
const updateVisibleData = () => {
const { startIndex, endIndex, data } = actionData.current
setVisibleData(data.slice(startIndex, endIndex))
}
const handleScroll = () => {
const scrollTop = container.current.scrollTop
updateBoundary(scrollTop)
updateVisibleData()
}
useEffect(() => {
const { visibleCount, startIndex } = actionData.current
actionData.current.endIndex = visibleCount + startIndex
updateVisibleData()
}, [])
return (<div className='virtual__list-container' onScroll={handleScroll} ref={container}>
<div className='virtual__list-wrap' style={{ paddingTop: `${startOffset}px`, paddingBottom: `${endOffset}px` }} >
{visibleData.map((item, index) => (<div className="virtual__list-item">{item}----{index + actionData.current.startIndex}</div>))}
</div>
</div>
)
}
// index.less
.virtual__list-container{
width: 100%;
height:400px;
overflow: scroll;
background-color: cadetblue;
}
.virtual__list-wrap{
width: 100%;
}
.virtual__list-item{
height: 40px;
line-height: 40px;
border-bottom: 1px solid black;
}
bash中执行: sh online.sh test1 test2
,则在online.sh文件中可以分别得到三个变量:$0 //值为 online.sh
$1 //值为 test1
$2 //值为 test2
背景:访问一个接口,如果返回的值不符合要求,则过几秒去轮询。这个过程需要阻塞后续相关代码的执行。
代码:
// 模拟接口
function req() {
return new Promise((reslove, reject) => {
if (Math.random() > 0.9) {
reslove(1)
} else {
reslove(0)
}
})
}
function sendReq(callback) {
req().then(res => {
console.log('res',res)
if (res) {
callback()
} else {
setTimeout(() => sendReq(callback), 1000)
}
})
}
function waitRes() {
console.log('start')
return new Promise(resolve => {
sendReq(resolve)
})
}
function end() {
console.log('end')
}
async function run() {
await waitRes()
end()
}
run()
在ES5中没有块级作用域的概念。而有变量提升的概念。导致初学者会比较困惑。
先看一个例子。
console.log(a);
var a=2
习惯了Java
等语法的人肯定觉得以上代码会报错,因为变量升明在调用之后,但实际上却输出了undefined
。
这是为什么呢,原因就在于js引擎处理js代码时,会先进行编译,再去执行,而在编译阶段就会找到对应的变量声明,并和对应的作用域关联起来(变量提升)。
上述代码会被编译成如下形式:
var a;
console.log(a);
a=2
执行的时候按顺序执行,输出undefined
也就理所应当了;
注意: 只有声明本身会提升,赋值或者运行等其他逻辑会留在原地;
在js中函数本质上也是一个变量。因此遵守以上变量提升的规则。但是函数会形成一个自己的作用域(函数作用域),所以函数内部定义的变量会在函数作用域中完成变量提升,而不会提升到全局。看一个例子:
foo();
function foo(){
console.log(a);
var a=2
}
以上代码也会输出undefined
;
在编译阶段,变量提升之后,代码转换为如下形式:
function foo(){
var a
console.log(a);
a=2
}
foo();
可以看到,函数本身作为一个变量提升到最外层,而函数内部变量又在函数作用域中完成提升;
这是一个容易犯的错误。比如如下代码:
foo()
var foo=function(){
..........
}
以上代码会报错,TypeError
; 原因在于 以上代码会被编译成如下形式:
var foo;
foo();
foo=function(){
.......
}
在执行foo()
时,foo的值为undefined
。对undefined
执行函数操作,会报TypeError
错误;
当变量和函数名称相同时,函数提升会优先。但是相同名称的函数,后边是可以覆盖前边的。比如:
foo();
function foo(){
console.log(1)
}
var foo=function(){
console.log(2)
}
function foo(){
console.log(3)
}
以上代码会输出3。因为函数首先提升,后边的会覆盖前边。而var foo出现的时候,前边已经有函数foo的声明,这里是个重复声明,因此被忽略。 引擎编译后的代码如下:
function foo(){
console.log(3)
}
foo();
foo=function(){
console.log(2)
}
ES6中引入了块级组作用域的概念。块级作用域主要存在以下两个地方
{
var a=1
}
{
let b=2
}
console.log(a) //1
console.log(b) //Uncaught ReferenceError: b is not defined
JS引擎是怎么知道识别第二个代码块是一个块级作用域,而第一个不是呢?这就涉及到一个块级声明的概念。
块级声明的典型例子就是 let 和 const声明,都知道用这两个关键字声明的变量具有以下特征:
console.log(a);
let a=1
编辑阶段,遇到let声明的a变量,先放入死区,在执行console.log(a)
时,a还在死区没释放出来。因此会报错;
在看下边一个例子:
{
var a=1;
let b=2;
}
console.log(a) // 1
console.log(b) // Uncaught ReferenceError: b is not defined
编译阶段 遇到var声明,会将a提升到顶级作用域,再遇到let声明,此时{}会形成一个块级作用域。因此再外边可以访问a却不能访问b;
源码地址:https://github.com/keep-run/simple-cli
有了前边的准备,下边我们首先规划一下项目的结构,我们预计要写这样三个指令:
simple-cli init //初始化一个项目;
simple-cli start //启动一个项目;
simple-cli build //打包项目;
首先在cli目录下建立如下目录:
cli
├── index.js
└── src
├── clis
│ ├── build.js
│ ├── init.js
│ └── start.js
└── index.js
其中cli/index.js为脚手架的根入口,src/index.js用来获取指令,选择执行clis下边的对应脚本。
为了测试,我们先在build.js、init.js和start.js下分别export一个函数,函数体分别为:console.log('build')
、console.log('init')
、console.log('strat')
。
为了后边可以用一些es6的语法,我们还需要在项目入口处配置babel。简单贴一下cli/index.js的代码。
#!/usr/bin/env node
require('@babel/polyfill')
require('@babel/core')
require('@babel/register')({
presets: [require.resolve('@babel/preset-env')],
ignore: [],
cache: true,
})
require('./src/index')
接下来就要通过输入的指令执行执行相应的脚本了。这里推荐一个npm包optimist
, src/index.js的关键代码如下:
import optimist from 'optimist'
import fs from 'fs'
import path from 'path'
const { argv } = optimist
const commands = argv._
const clis = fs.readdirSync(path.resolve(__dirname, 'clis')).map(item => item.replace('.js', ''))
const cmd = clis.indexOf(commands[0]) > -1 ? commands[0] : ''
if (cmd) {
const command = require(`./clis/${cmd}`).default
argv.cwd = process.cwd()
command(argv)
} else {
console.log('not found')
}
到这里就可以在命令行执行simple-cli start
,simple-cli init
,simple-cli build
指令。会输出clis目录下相应的脚本的console.log内容了。
下一步就是开发各脚本的具体内容了。
关于单链表的常见题目主要有以下几个方面:
如下图所示,即为一个环形链表。写程序判断链表是否有环?
这个题大概有以下两种解法:
从头开始遍历,用一个数组存储每个节点的hash值,每遍历一项,就去数组查找对应hash值,如果找到,则说明有环,否则没有环;而这种方法,需要额外的存储空间,还有大量的数组搜索,实际使用当中不值得推荐;
设置两个指针:
两个指针从头同时开始向后遍历,如果链表有环,那么两个指针总会有相遇的时候。JavaScript
实现如下:
function hasCycle (head) {
let fast = head, slow = head;
while (fast && fast.next) {
fast = fast.next.next;
slow = slow.next
if (fast === slow) {
return true
}
}
return false
};
参考练习题目:leetcode 141题
这个问题是对前一个问题的提升,不光要判断链表是否有环,如果有环,还要找到环的入口位置。我们还是用前边的快慢指针法,接下来我们分析两个指针的相遇点和环的入口点有什么关系。如下图所示:
假设链表的起点为A,环的入口点为B,快慢指针的相遇点在C处。(慢指针在走完第一圈之前一定会被快指针追上,据说这个是一个小学奥数题,网上很多相关分析证明,这里直接拿结论来用了)。相遇的时候:
由于快指针的速度是慢指针的两倍,所以有:s_fast=2*s_slow,由此可以得到:LAB=LCB;
上边这个结论就太重要了。这表明相遇点离环的起点距离和链表的起点与环的起点距离相等。那么,当快慢指针相遇的时候,让快指针变为慢指针,且从链表的头开始遍历,原始慢指针继续向后遍历。 那么两者一定会在环的入口位置相遇。
有了以上分析,具体实现也就不难了,代码如下:
var detectCycle = function (head) {
let slow = head, fast = head;
while (true) {
if (!fast || !fast.next) { return null }
fast = fast.next.next;
slow = slow.next
if (fast === slow) { break }
}
fast = head
while (fast !== slow) {
fast = fast.next;
slow = slow.next
}
return fast
};
参考练习题目:leetcode 142题
分析以上交叉链表的特征,可以得出以下结论:
有了以上结论作支撑,JavaScript
代码实现如下:
var getIntersectionNode = function (headA, headB) {
let lenA = 0;
let lenB = 0;
let difLen = 0;
let first = headA;
let second = headB;
while (first) {
first = first.next;
lenA++;
}
while (second) {
second = second.next;
lenB++;
}
if (lenA > lenB) {
difLen = lenA - lenB;
first = headA;
second = headB;
} else {
difLen = lenB - lenA;
first = headB;
second = headA;
}
while (difLen--) {
first = first.next
}
while (first && second) {
if (first === second) {
return first
}
first = first.next;
second = second.next;
}
return null
};
上边代码虽然易懂,但是不够优雅,来自leetcode的高手写法如下:
var getIntersectionNode = function (headA, headB) {
let pA = headA;
let pB = headB;
while (pA !== pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
};
参考习题:LeetCode 160题
本节我们将陆续实现start
和init
指令;
start
指令start
指令,用于启动一个项目,其原理是基于webpackDevServer
在本地启一个服务。
webpack
配置首先配置入口文件src/index.js
import start from './commanders/start.js'
...
program
.command('start')
.description('start a program')
.action(() => {
start(config)
})
...
src/commanders/start.js
import Webpack from "webpack";
import WebpackDevServer from "webpack-dev-server";
import path from 'path'
import getDevConfig from '../webpack/config.dev.js'
export default (config) => {
const { port = 8000, cwd } = config
const host = '0.0.0.0'
const webpackConfig = getDevConfig(config)
const devServer = {
port,
host,
open: true,
compress: true,
static: {
directory: path.join(cwd, 'dist'),
}
}
const complier = Webpack(webpackConfig)
const server = new WebpackDevServer(complier, devServer)
server.listen(port, host, () => {
console.log(`start server on http://localhost:${port}`)
})
}
src/webpack/config.dev.js
import commonConfig from './config.com.js'
export default (pkg) => {
return {
mode: 'development', //设置开发模式
...commonConfig(pkg) //各个模式下的公共配置
}
}
对于一个前端项目,在浏览器打开是需要有一个html
模板来承接的。webpack
提供了一个插件HtmlWebpackPlugin
,可以实现自动生成一个模板,并在模板内引入打包生成的所有的js
文件。
配置src/babel/plugin.js
import HtmlWebpackPlugin from 'html-webpack-plugin'
...
export default () => {
return [
...
new HtmlWebpackPlugin(), // 自动生成html模板,并引入打包生成的所有js文件
]
}
此时已经可以在本地启项目了。
修改demo/index.js
文件
document.body.innerHTML = '<div>demo</div>'
demo目录下执行simple-cli start
。会自动打开一个网页,显示demo文件。
init
指令作为一个完整的脚手架工具,除了start
,build
之外,init
指令一样的重要,该指令的主要作用是:复制内置的模板到用户目录。
首先建立一个react模板目录cli/templates/react
:该目录下默认有两个文件index.js
和package.json
index.js
:
import React from 'react'
import { render } from 'react-dom'
render(
<div className="container">
<div>this is a react app</div>
</div>,
document.getElementById('root'),
)
package.json
:
{
"name": "demo",
"version": "1.0.0",
"description": "a simple-cli init demo",
"scripts": {
"start": "simple-cli start",
"build": "simple-cli build"
},
"creator": "",
"license": "ISC",
"simple-cli": {
"port": 9000,
"entry": "./index"
},
"dependencies": {
"react": "^16.9.0",
"react-dom": "^16.9.0"
}
}
目前为止,我们的项目还不能解析jsx
语法,我们更新src/babel/jsloader.js
...
{
"test": /\.(m?js|jsx)$/,
"exclude": /(node_modules|bower_components)/,
"use": {
loader: require.resolve('babel-loader'), // require.resolve很重要,在当前项目的node_modules中寻找对应的模块
options: {
presets: [
require.resolve('@babel/preset-env'),
require.resolve('@babel/preset-react') // 支持react
]
}
}
}
用同样的方法,可以配置其他对应的loader以支持less
,stylus
,css
以及文件,图片等内容,这里不再累述,可以查看文末的源码。
此时任意新建一个目录,该目录下依次执行以下指令:
simple-init
npm install
simple-start
就会启动一个项目,并在打开一个默认的页面;
至此,我们的脚手架的基本功能都已经实现了。其中包括init
,start
,build
指令。可以满足日常所需,在生产环境,可以结合公司的技术栈,做相应的扩展。
判断元素是否在可视区,一般会用到以下函数的一种或者几种:
getBoundingClientRect
这个函数会返回一个对象,这个对象中包含以下几个重要属性:
width // 当前元素的宽度
height // 当前元素的高度
left // 当前元素左边离浏览器窗口左侧的距离,如果由于滚动,
// 当前元素左侧超出浏览器左侧窗口,则该值为负值;
right // 当前元素右侧离浏览器窗口左侧的距离
// 如果由于滚动,当前元素从浏览器左侧完全滑出,则该值为负值,
// 如果从浏览器右侧滑出,该值不受影响;仍为`left+width`
top // 当前元素上边缘离浏览器上侧的距离,
// 如果元素从浏览器浏览器上侧滑出,则该值为负值;
bottom // 当前元素下边缘离浏览器上侧的距离,
// 如果元素从浏览器浏览器上侧完全滑出,则该值为负值;
// 如果从下侧完全滑出,该值不受影响;
通过以上分析,可以做如下判断:
但是 只通过这个元素无法判断元素有没有超出浏览器的下侧和右侧,要判断这两种情况就必须借助视窗的宽高信息;
日常开发过程中,通常都是application/json格式的数据交互,但是偶尔也会遇到关于流文件的处理(比如上传下载文件)。
import FormData from 'form-data'
import axios from 'axios'
const form = new FormData();
form.append(key, value);
form.append('file', fs.readFileSync(filePath), filename)
return request({
method: 'post',
url: url
data: form,
headers: form.getHeaders() //FormData会自动设置合适的头
})
const getFiles = async (data, targetDist) => {
const res = await axios({
method: 'post',
baseURL: host,
url: '/api/block/download',
data:data,
responseType: "stream"
})
return new Promise((reslove, reject) => {
const writeStream = tar.extract({
strip: 1,
cwd: targetDist
})
res.data.pipe(writeStream) //res.data就是一个文件流
.on('end', () => {
reslove()
console.log('添加成功')
})
.on('error', (err) => {
console.log('err', err)
process.exit(1)
})
})
}
我们的cli工具现在虽然可以使用了,但是还是很简陋,这一节主要从以下两个方面去丰富其功能:
css打包有两种形式,第一是借助style-loader
.配置如下:
{
test: /\.css$/,
exclude: /node_modules/,
use: [require.resolve('style-loader'), require.resolve('css-loader')],
},
这样会把css
打包进js
文件,最后以style
标签的形式把css
样式渲染在html
中。这种方式在生产环境会产生一些问题:
第二种形式是借助插件MiniCssExtractPlugin
。 把样式单独抽离出来打包成一个文件。在html
中通过一个link
标签引入即可,这也是当下主流的使用方式,配置如下:
配置loader:
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
...
{
test: /\.css$/,
exclude: /node_modules/,
use: [MiniCssExtractPlugin.loader, require.resolve('css-loader')],
},
配置plugin(src/babel/plugins)
export default (config) => {
return [
...
new MiniCssExtractPlugin({
filename: config.mode === 'development' ? 'bundle.css' : 'bundle.[contenthash].css' //输出css文件名称, 开发环境,文件名不要带hash,带hash会做持久缓存,影响开发热更。 生产环境需要带hash,用于做缓存
}),
]
}
这里有一个坑啊。 开发环境的css
文件名不要带hash
值。webpack官网有一个一句话的解释:
使用 filename: "[contenthash].css" 启动长期缓存。根据需要添加 [name]。
这导致一个什么问题呢,如果开发环境文件名称带了hash
值,当我们修改css文件时,页面不会热更,所以这里文件名称会加入当前模式的判断。
对于一个成熟的脚手架工具,开发过程中mock
数据是一项基本且重要的操作。当然实现mock
的方法有很多,比如常用的charles等。我们这里提供一个方法,代理到本地目录。省去各种配置。
基本原理是:devServer
中拦截ajax
请求,去本地读取某个文件作为接口返回。
commanders/strat.js
中增加配置:
...
const { port, cwd, mock } = config
...
{
...
onBeforeSetupMiddleware: function (devServer) {
if (devServer) {
devServer.app.use((req, res, next) => {
// 对于ajax请求,可以设置mock
if (mock && req.headers['x-requested-with']) { //请求头中增加 x-requested-with 早期ajax请求会自动加, 新的fetch没有了,需要手动加
const filePath = path.join(cwd, `/mock/${req._parsedUrl.path}.json`)
let mockData = {}
try {
mockData = require(filePath)
} catch (err) {
mockData = { message: `${filePath} is not a file` }
}
res.set('Content-Type', 'application/json')
res.send(mockData)
}
next()
})
}
}
}
几点解释:
mock
字段作为配置,允许用户自定义。和其他配置字段一样在package.json
中simple_cli
字段下。x-requested-with
字段,以此作为识别ajax
请求的标识。/test
。则会读取本地目录:mock/test.json
的值作为返回。更多内容持续丰富中。
相关文章:
在利用npm start
或者npm run dev
启动项目时经常遇到以下错误:
events.js:85
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE
很显然,这是端口占用了,怎么解决呢?
lsof -i tcp : your-port
,会返回占用该端口的进程,记住相关进程的pid;kill -9 your-pid
。这一句会杀死相关进程;到此,重新启动项目即可;
有的时候,项目的端口确实会被本地某个应用的端口占用,那么只能有以下两种解决方案:
有时候会有另一种情况,没有应用占用端口,正常启动项目,一顿操作之后关闭项目,然后再启动项目,出现端口占用。这是为什么呢?
直观上看是项目停了,但是node服务没停,小白我尝试升级node版本,检查项目,最终发觉是我停止项目的方式有问题。正确方法是使用ctrl+c
停止项目,而我误使用ctrl+z
,两者区别:
ctrl+c
:终止程序执行,结束进程;ctrl+z
:将前台执行的程序放到后台,并处于暂停状态;还有以下后续操作:
bg
:启动当前后台暂停的进程; 该进程仍会后台运行;fg
:后台进程转向前台并启动; 如果有多个后台进程,可以先执行jobs
,拿到各进程的jobsNumber,然后执行fg % jobsNumber
,将指定的后台进程转向前台;const handleSearch = (e:KeyboardEvent) => { .... }
...
<input
type="search"
onChange={(e) => { setKeyWord(e.target.value) }}
onKeyPress={e => handleSearch(e)}
/>
此时ts报错:
类型“KeyboardEvent<HTMLInputElement>”的参数不能赋给类型“KeyboardEvent”的参数.
解决办法:
const handleSearch = (e:React.KeyboardEvent<HTMLInputElement>) => {...}
let key=""
...
this.state[key] //会报 类型any--->string的错误
...
解决办法
const state:pageState = this.state
...
state[key]
...
对象可能存在某些不确定的的属性。为其定义类型
interface demo{
width:string,
[unsurekey:string]:any //除了width属性以为,其他可能存在的属性值类型都为any
}
脚手架在前端工程化领域的重要性不言而喻,每个公司(或团队)都会根据自己的业务特性维护适合自己的脚手架。具体来说其重要性体现在以下几个方面:
既然如此,那作为一个前端人员,得理解脚手架的工作原理以及其基本实现吧。接下来我们一步一步从零开始完成一个简单的脚手架。
mkdir simple-cli
cd simple-cli
mkdir cli // 存放cli源码的目录
mkdir demo // 存放demo的目录
进入到cli
目录(后续操作默认都在cli目录下)
平平无奇,直接执行npm init
。一顿回车就能得到一个项目。但是只有一个package.json
文件。
同一层级新建index.js
#!/usr/bin/env node
// 上边这一行代码很重要,指定开发环境为 node
console.log('---index.js-----')
package.json
文件增加配置:
{
"bin": {
"simple-cli": "index.js"
}
}
bin
的配置很重要,用来指明对应可执行文件的位置。 未来用户通过npm
全局安装我们的脚手架之后,就可以执行simple-cli
指令了。
更多关于package.json配置的解读在这里。
但是这个包还在开发阶段,当然不可能安装了。此时可以借助另一个指令npm link
。控制台进入当前目录下执行:
npm link
此时npm
会将bin
下的指令链接到全局(未来可以通过npm unlink
断开)。
此时在控制台执行:
simple-cli // 输出 ---index.js-----
脚手架的执行环境是node
。而node
是遵循CommonJs
的。 为了能执行ES6
代码,则需要对我们的代码进行转义。继续在package.json
中增加如下配置:
"scripts": {
"dev": "npx babel src --watch --out-dir lib"
}
这个配置的含义是:babel
转义src
目录下的文件,并输出到lib
目录下, --watch
的含义是监听src
目录,当该目录下有代码改动时,就会重新编译一次。
配置 .babelrc, 新建文件cli/.babelrc
:
{
"presets": [
"@babel/preset-env"
]
}
控制台执行:
npm install @babel/cli --save-dev
npm install @babel/core --save-dev
npm install @babel/preset-env --save-dev
npm run dev
新建文件src/index.js
,写入代码:
console.log('src/index.js')
此时会自动新建一个文件lib/index.js
,其内容就是src/index.js
转义后的代码。
修改cli/index.js
文件代码为:
#!/usr/bin/env node
// 上边这一行代码很重要,指定开发环境为 node
require('./lib/index')
此时控制台输入
simple-cli // 输出'src/index.js'
到这里得到如下目录:
.
├── cli
│ ├── index.js
│ ├── lib
│ │ └── index.js
│ ├── package.json
│ ├── .babelrc
│ └── src
│ └── index.js
└── demo
我们的项目基础配置已经完成了。后续在src目录下开发具体功能。
对于一个脚手架工具,应该具有以下基础指令:
simple-cli start
simple-cli build
simple-cli publish
这里借助commander
来实现其定义。 首先npm install commander --save-dev
。修改src/index.js
文件:
import { program } from 'commander'
const pkg = require('../package.json')
// 定义 -v/ --version 指令
program.version(`当前版本: ${pkg.version}`, '-v, --version', 'get current version')
// 自定义指令 start
program
.command('start')
.description('start a program')
.action(() => {
// todo
console.log('command start ')
})
// 自定义指令 build
program
.command('build')
.description('build program')
.action(() => {
// todo
console.log('command build')
})
// 自定义指令 publish
program
.command('publish')
.description('publish program')
.action(() => {
// todo
console.log('command publish')
})
program.parse(process.argv)
此时控制台执行simple-cli help
将得到如下输出:
Options:
-v, --version get current version
-h, --help display help for command
Commands:
start start a program
build build program
publish publish program
help [command] display help for command
这里只做了其定义,每个指令的具体实现将在后面陆续实现。
未完待续
需要注意的几个点:
content: attr(alt);
可以获得当前选中元素的alt属性值;transform
样式后,会具有position:'relative'
的功效;基于promise和递归可以实现红绿灯问题。其中红灯亮3秒,黄灯2秒,绿灯1秒。代码如下:
function red() {
console.log('red')
}
function green() {
console.log('green')
}
function yellow() {
console.log('yellow')
}
function light(callback, timer) {
return new Promise((reslove) => {
setTimeout(function () {
callback();
reslove();
}, timer)
})
}
function step() {
Promise.resolve().then(() => {
return light(red, 3000) //这里一定要return
}).then(() => {
return light(green, 1000)
}).then(() => {
return light(yellow, 2000)
}).then(() => {
step()
})
}
step() //该怎么设置停止这个递归呢?
上一篇文章实现了一个简单的promise
,文章末尾提到一个问题,当前then
的回调怎么只受前一个promise
状态控制;其实问题主要出在这几行代码中
// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected
// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject
cb
取决于当前状态无可厚非,但是cb_changeState
就不应该了。cb_changeState
应该取决于cb
的执行结果,如果结果是一个promise
对象,则依赖该promise
的状态;
class MyPromise {
...
_handle(callback) {
// prnding状态时,注册函数入栈
if (this.state === PENDING) {
this.callbacks.push(callback)
return
}
// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected
// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject
let ret
if (cb) {
ret = cb(this.value) //计算继续向后传递的值
// 返回一个promise
if (ret instanceof MyPromise) {
ret.then((data) => {
callback.resolve(data)
}, (err) => {
callback.reject(err)
})
} else {
callback.resolve(ret)
}
} else {
cb_changeState(this.value) // then里没有回调函数,把当前value继续向下传递
}
}
}
new MyPromise((resolve, reject) => {
resolve('test')
}).then((value) => {
console.log('then1', value)
return new MyPromise((resolve, reject) => {
reject(value)
})
}, (err) => {
console.log('reject1', err)
}).then((value) => {
console.log('then2', value)
}, (err) => {
console.log('reject2', err)
})
如下输出
then1 test
reject2 test
可以看到中途实现了状态的变更;
// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'
class MyPromise {
state = PENDING
value = '' // 向后传的value值
callbacks = [] // 回调队列
constructor(fn) {
if (typeof fn !== "function") {
throw new Error('参数必须是函数')
}
fn(this._resolve.bind(this), this._reject.bind(this))
}
then(onFulfilled, onRejected) {
const _this = this; //_this指向前一个promise对象
return new MyPromise((resolve, reject) => {
_this._handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
_resolve(value) {
if (this.state !== PENDING) {
return
}
this.state = FULFILLED //修改状态
this.value = value // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_reject(error) {
if (this.state !== PENDING) {
return
}
this.state = REJECTED //修改状态
this.value = error // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_handle(callback) {
// prnding状态时,注册函数入栈
if (this.state === PENDING) {
this.callbacks.push(callback)
return
}
// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected
// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject
let ret
if (cb) {
ret = cb(this.value) //计算继续向后传递的值
// 返回一个promise
if (ret instanceof MyPromise) {
ret.then((data) => {
callback.resolve(data)
}, (err) => {
callback.reject(err)
})
} else {
callback.resolve(ret)
}
} else {
cb_changeState(this.value) //then里没有回调函数,把当前value继续向下传递
}
}
}
new MyPromise((resolve, reject) => {
resolve('test')
}).then((value) => {
console.log('then1', value)
return new MyPromise((resolve, reject) => {
reject(value)
})
}, (err) => {
console.log('reject1', err)
}).then((value) => {
console.log('then2', value)
}, (err) => {
console.log('reject2', err)
})
输出结果如下
then1 test
reject2 test
可见第二个then
的回调依赖第一个then
的状态改变;
通常使用new
关键字是为了创建一个自定义对象类型的实例。使用new的时候,会有以下几个过程:
1、创建一个空的简单JavaScript对象(即{});
2、链接该对象(即设置该对象的构造函数)到另一个对象 ;
3、将步骤1新创建的对象作为this的上下文 ;
4、如果该函数(2中设置的构造函数)没有返回对象,则返回this。
原文(中文):https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new
英文(英文):https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/new
以上仅仅是new的过程,如果要创建一个用户自定类型的对象,需要以下两部:
1、写一个函数定义对象类型;
2、通过new关键字来创建对象的实例;
有两点值得注意:
1、如果构造函数没有显示的返回一个对象,则返回this(创建的实例对象)。 如果显示返回一个对象,则返回该对象,且该对象不是构造函数的实例。 如果返回 null string number等,会直接忽略,继续返回this对象。测试一下:
function Demo1(a){
this.a=a
return 'test'
}
Demo1.prototype.getA=function(){
return 'a is :'+this.a
}
const inst1=new Demo1(5)
inst1.getA()
function Demo2(a){
this.a=a
return {
b:3
}
}
Demo2.prototype.getB=function(){
return 'a is :'+this.b
}
const inst2=new Demo2(5)
inst2.a //undefined
inst2.b //2
inst2.getB() // inst2.getB is not a function 显示返回的对象不是Demo2的实例,不能访问原型上的对象。
function _new(constructFunc,...rest){
let obj={}
constructFunc.apply(obj,rest)
obj.__proto__=constructFunc.prototype
return obj
}
function Test(a){
this.a=a
}
Test.prototype.getA=function(){
return this.a
}
const testInst=_new(Test,999)
console.log(testInst.getA()) // 999
注意:定义新对象的时候,以下两种方法是等价的:
let a={}
let a=Object.create({})
如果使用Object.create(null)
创建对象,则会丢失原型,只能通过Object.setPrototypeOf()
重新设置对象的原型。o.__proto__
设置是无效的。
这里要做一次判断,如果constructFunc函数返回一个对象,则直接返回该对象,否则就返回构造的实例对象。
function _new(constructFunc, ...rest) {
let obj = {}
obj.__proto__ = constructFunc.prototype
let res=constructFunc.apply(obj, rest)
return (res && typeof res === 'object') ? res : obj
}
function Test(a) {
this.a = a
return {b:a}
}
Test.prototype.getA = function () {
return this.a
}
const testInst = _new(Test, 999)
testInst.getA() // testInst.getA is not a function 不是实例
参考文章:mqyqingfeng/Blog#13
new.target
new.target
是es6新增的指令, 返回new调用的函数的函数名。基于该指令可以辅助实现限制部分函数必须用new调用。比如定义了一个构造函数,必须先实例化才能继续使用。可以写以下函数来容错。
function Person(name){
if(new.target!==Person){
return new Person(name)
}
this.name=name
}
let p1=new Person('A') //Person {name: "A"}
let P2=Person('B') // Person {name: "B"}
以上写法,不管有没有使用new来调用函数,都可以返回一个Person的实例。增加程序健壮性;
很多文章认为:观察者模式===发布订阅模式。 这个结论有待商榷。至少代码层面是不一样的。
每个实例都有发布任务(publish)和订阅任务(subscribe)的能力。且都各自维护自己的任务列表,列表中存储订阅自己的个体信息。
//假设有个学生群体,有A,B,C,D四个学生,A,B,C同时订阅D。D发布信息时,A,B,C给予响应。
class Student {
constructor(name) {
this.name = name;
this.list = []
}
subscribe(target, callback) {
target.list.push(callback)
}
publish(params) {
this.list.forEach(func => func(params))
}
}
const stuA = new Student("A")
const stuB = new Student("B")
const stuC = new Student("C")
const stuD = new Student("D")
//stuA,stuB,stuC 分别观察stuD。并传入相应的回调函数
stuA.subscribe(stuD, function (params) {
console.log("A receive D:" + params)
})
stuB.subscribe(stuD, function (params) {
console.log("B receive D:" + params)
})
stuC.subscribe(stuD, function (params) {
console.log("C receive D:" + params)
})
stuD.publish('send a message')
有一个事件管理中心,订阅者把自己想订阅的事件注册在事件管理中心,发布者发布事件时,由管理中心统一触发订阅者注册的事件。
//事件管理中心
const union = {
eventObj:{},
subscribe: function (type, callback) {
if (!this.eventObj[type]) {
this.eventObj[type] = []
}
this.eventObj[type].push(callback)
},
publish: function (type, ...items) {
const callbacks = this.eventObj[type]
if (callbacks) {
callbacks.forEach(callback => {
callback.apply(null, items)
});
}
}
}
class Student {
constructor(name) {
this.name = name;
}
subscribe(type, callback) {
union.subscribe(type,callback)
}
publish(type,params) {
union.publish(type,params)
}
}
const stuA = new Student("A")
const stuB = new Student("B")
const stuC = new Student("C")
const stuD = new Student("D")
//stuA,stuB,stuC 分别订阅事件‘demo'。并传入相应的回调函数 stuD触发该事件
stuA.subscribe('demo', function (params) {
console.log("A receive D:" + params)
})
stuB.subscribe('demo', function (params) {
console.log("B receive D:" + params)
})
stuC.subscribe('demo', function (params) {
console.log("C receive D:" + params)
})
stuD.publish('demo','simple demo')
题目大概是实现一个如下功能的程序:
LazyMan('tom')
输出:
"this is tom"
LazyMan('tom').sleep(10).eat('apple')
输出:
"this is tom"
等待10秒...
"eat apple"
LazyMan('tom').eat('apple').eat('banana')
输出:
"this is tom"
"eat apple"
"eat banana"
LazyMan('tom').eat('banana').sleepFirst(5)
输出:
等待 5 秒...
"this is tom"
"eat banana"
从输出结果看有几个特点:
sleepFirst
。后调用却先输出。所以这里更适合采用队列来实现,根据任务类型来判断是添加在队尾还是队首。class _LazyMan {
constructor(name) {
this.name = name;
this.tasks = [this.sayName]; //任务队列 默认添加sayName在队列中
setTimeout(() => this.next(), 0) // 任务添加完毕后,下一个宏任务中执行队列
}
/**
* 添加任务
* @param {function} task 任务
* @param {boolean} addToTail 添加在队尾
*/
addTask(task, addToTail = true) {
if (addToTail) {
this.tasks.push(task)
} else {
this.tasks.unshift(task)
}
}
//每次去任务队列的第一个任务执行
next = () => {
const task = this.tasks.shift()
task && task()
}
sayName = () => {
console.log(`this is ${this.name}`)
this.next()
}
eat = (name) => {
this.addTask(() => {
console.log(`eat ${name}`)
this.next()
})
return this // 必须return this 方便链式调用,继续向队列添加任务
}
sleep = (time) => {
this.addTask(this.sleepTask(time))
return this
}
sleepFirst = (time) => {
this.addTask(this.sleepTask(time), false)
return this
}
sleepTask = (time) => {
return () => {
console.log(`等待${time}秒 ...`)
setTimeout((() => {
this.next()
}), time * 1000)
}
}
}
function LazyMan(name) {
return new _LazyMan(name)
}
参考文档:
lazyMan
掘金看到一个比较有意思的题目(https://juejin.cn/post/6987529814324281380#heading-11) 。大致的意思就是,需要调用接口计算两个数相加。实现一个函数实现任意个数的数字和。
// 第一种,普通遍历,串行执行,效率比较低
async function addFn1(args) {
let res = 0
for (const item of args) {
res = await addRemote(res, item)
}
return res
}
// 递归实现, 串行执行, 和addFn1差距不大
async function addFn2(args = []) {
if (args.length < 2) {
return args[0] || 0
}
args.push(await addRemote(args.shift(), args.shift()))
return addFn2(args)
}
// 基于reduce来实现
async function addFn3(args = []) {
return args.reduce((calculator, current) => {
return calculator.then(cal => addRemote(cal, current))
}, Promise.resolve(0))
}
// 利用promiseAll 两两一组执行
async function addFn4(args = []) {
let promiseChain = []
if (args.length % 2) {
promiseChain.push(Promise.resolve(args.shift()))
}
for (let i = 0; i < args.length / 2; i++) {
promiseChain.push(addRemote(args[2 * i], args[(2 * i) + 1]))
}
return Promise.all(promiseChain).then(res => {
if (res.length === 1) {
return res[0]
}
return addFn4(res)
})
}
let cache = {}
async function addFn5(args = []) {
async function addWithCache(params) {
let promiseChain = []
if (params.length % 2) {
promiseChain.push(Promise.resolve(params.shift()))
}
for (let i = 0; i < params.length / 2; i++) {
let key1 = params[2 * i];
let key2 = params[(2 * i) + 1];
let cacheRes = cache[`${key1}${key2}`] || cache[`${key2}${key1}`]
if (cacheRes) {
promiseChain.push(cacheRes)
} else {
promiseChain.push(addRemote(key1, key2).then(res => {
cache[`${key1}${key2}`] = res;
return res
}))
}
}
return Promise.all(promiseChain).then(res => {
if (res.length === 1) {
return res[0]
}
return addWithCache(res)
})
}
return addWithCache(args)
}
const addRemote = async (a, b) => new Promise(resolve => {
setTimeout(() => resolve(a + b), 500)
});
async function add(...inputs) {
console.log('---------------------分割线---Fn1------------------------------')
console.time('addFn1')
console.log(await addFn1([...inputs]))
console.timeEnd('addFn1')
console.log('---------------------分割线---Fn2------------------------------')
console.time('addFn2')
console.log(await addFn2([...inputs]))
console.timeEnd('addFn2')
console.log('---------------------分割线---Fn3------------------------------')
console.time('addFn3')
console.log(await addFn3([...inputs]))
console.timeEnd('addFn3')
console.log('---------------------分割线---Fn4------------------------------')
console.time('addFn4')
console.log(await addFn4([...inputs]))
console.timeEnd('addFn4')
console.log('---------------------分割线---Fn5------------------------------')
console.time('addFn5')
console.log(await addFn5([...inputs]))
console.timeEnd('addFn5')
console.log('---------------------分割线---Fn5withcache---------------------')
console.time('addFn5--again')
console.log(await addFn5([...inputs]))
console.timeEnd('addFn5--again')
}
add(1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 10, 10, 10, 10)
promise
的使用现在已经非常普及了。 虽然方便了开发者,但是也带了一些八股文考题,常见的就是如何实现一个promise
. 下边从零开始,一步步实现一个完整的promise
。
简易版的promise
应该具有以下基础功能:
promise
;promise
状态正常流转;then
方法;首先,一个promise
具有三个状态pending
、fulfilled
、rejected
。且只能按照如下方式流转
pending
---> fulfilled
pending
---> rejected
状态一旦改变,就不再变了。
其次,创建Promise
的时候,接受一个函数,且会立即执行,该函数接受两个函数参数,resolve
,reject
,用来改变状态;
基于此可以实现如下代码
// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'
class MyPromise {
state = PENDING
value = '' // 向后传的value值
constructor(fn) {
if (typeof fn !== "function") {
throw new Error('参数必须是函数')
}
fn(this._resolve.bind(this), this._reject.bind(this))
}
_resolve(value) {
this.state = FULFILLED //修改状态
this.value = value // 赋值,用于向后传递
}
_reject(error) {
this.state = REJECTED //修改状态
this.value = error // 赋值,用于向后传递
}
}
then
是promise
的一个基本且强大方法,具有以下特点
onFulfilled
, onRejected
;分别承接 resolve
和reject
的回调;then
方法会返回一个新的promise
,因此可以实现链式调用,一直then
下去;实现then
的一个难点就是,每次then
方法会注册两个函数参数onFulfilled
, onRejected
,但是具体执行哪一个,依赖前一个promise
的状态,fulfilled
状态时调用onFulfilled
,rejected
状态时调用onRejected
。 因此onFulfilled
, onRejected
必须注册在前一个promise的回调队列中,以便状态变更时,正确回调;
基于以上分析,完善代码如下
// 先定义三个状态变量
const PENDING = 'pending'
const REJECTED = 'rejected'
const FULFILLED = 'fulfilled'
class MyPromise {
state = PENDING
value = '' // 向后传的value值
callbacks = [] // 回调队列
constructor(fn) {
if (typeof fn !== "function") {
throw new Error('参数必须是函数')
}
fn(this._resolve.bind(this), this._reject.bind(this))
}
then(onFulfilled, onRejected) {
const _this = this; // _this指向前一个promise对象,这个很关键
return new MyPromise((resolve, reject) => {
_this._handle({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
_resolve(value) {
if (this.state !== PENDING) {
return
}
this.state = FULFILLED // 修改状态
this.value = value // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_reject(error) {
if (this.state !== PENDING) {
return
}
this.state = REJECTED // 修改状态
this.value = error // 赋值,用于向后传递
this.callbacks.forEach(cb => this._handle(cb)) //回调then中的注册函数
}
_handle(callback) {
// pending状态时,注册函数入栈
if (this.state === PENDING) {
this.callbacks.push(callback)
return
}
// 按需获取then中注册的回调函数
const cb = this.state === FULFILLED ? callback.onFulfilled : callback.onRejected
// 改变then返回的promise的状态,持续回调后续的then
const cb_changeState = this.state === FULFILLED ? callback.resolve : callback.reject
let ret
if (cb) {
ret = cb(this.value) // 计算继续向后传递的值
}
cb_changeState(ret)
}
}
new MyPromise((resolve, reject) => {
resolve('test')
}).then((value) => {
console.log('then1', value)
}, (err) => {
console.log('reject1', err)
}).then((value) => {
console.log('then2', value)
}, (err) => {
console.log('reject2', err)
})
最终输出如下:
then1,test
then2,undefined
上述代码虽然实现了状态变化以及链式调用等基本能力,但是会存在一个很大的问题,第一个promise
的状态变为fulfilled
,那么会依次调用后续的onFulfilled
函数。这是不符合预期的。
预期是当前then
的回调函数,只取决于前一个promise
的状态,即整个回调链路中可能同时存在onFulfilled
,onRejected
。
在下一篇文章中将专门解决这个问题。
interface Person{
name:string,
age:number
}
// 函数不显示设置返回值的类型,利用动态推导得出
function demo<T extends object, K entends keyof T>(obj:T, key:K){
return obj[k]
}
//测试
let obj:Person={
name:"tea",
age:23
}
let age = demo(obj, "age") // number类型
let name = demo(obj, "name") // string类型
interSection Observer 提供了一种异步检测目标元素与指定祖先元素相交情况变化的方法;基于这个API,最常见的两个使用场景是:
传统的做法都是绑定scroller事件,滚动过程通过Element.getBoundingClientRect()
计算是否出现在可视区,然后加载图片,然而这些都是在主线程频繁触发,会影响性能;
import React, { useState, useEffect, useRef } from 'react';
import { imgs } from './imgs.ts' //这个文件会返回一个很长的图片列表
export default () => {
useEffect(() => {
const interSectionObserver = new IntersectionObserver(entries => {
// 当目标元素与视窗(默认的祖先元素)交叉超过阈值(默认为0)时的回调,entries是所有观测的目标元素
entries.forEach(item => {
if (item.isIntersecting) { // true表示超过交叉阈值,取dataset.src复制给src,实现图片加载
const imgDom = item.target
const imgSrc = imgDom.dataset.src
imgDom.src = imgSrc
interSectionObserver.unobserve(imgDom) // 图片加载完毕之后,取消观测该元素,否则来回滚动会继续触发;
}
})
}, {
rootMargin: "350px 0px" // 这个会使得根元素的矩形大小上下各增加350px; 实现图片没有出现在屏幕可视区,但是开始加载;
})
Array.from(document.querySelectorAll('img')).forEach(dom => {
interSectionObserver.observe(dom) // 找到所有的img元素,并观测;
})
}, [])
return (<div style={{ height: "600px", overflow: "scroll" }}>
{
imgs.map((item, index) => {
return <div style={{ 'justifyContent': "center" }} key={item}>
{/** 先把图片资源存储在data-src中 */}
<img data-src={item} style={{ width: "300px", height: "165px", margin: "20px" }} />
<span>{index}</span>
</div>
})
}
</div>)
}
这个场景在移动端会比较常见,滚定到底部实现自动分页,一般的做法也是绑定scoller
事件,判断距离底部一定的值时发接口。问题和图片懒加载是一样的,利用IntersectionObserver
就可以改善很多,在最底部放一个元素,观测该元素的交叉状况即可实现分页;
import React, { useState, useEffect, useRef } from 'react';
const bgColors = ['#4e61d4', '#ccc', '#999', 'pink', 'yellow']
const generateBlock = (counts: Number) => {
return new Array(counts).fill(0).map(() => {
const backgroundColor = bgColors[Math.floor(Math.random() * bgColors.length)]
return <div style={{ height: "50px", margin: "20px 0", backgroundColor }} />
})
}
export default function () {
const [blocks, setBlocks] = useState(generateBlock(10));
const footerRef = useRef('')
useEffect(() => {
const interSectionObserver = new IntersectionObserver((enteries) => {
// 可见
if (enteries[0].isIntersecting) {
addblocks(10) //增加10个背景色随机的块(模拟分页)
}
},{
rootMargin:"50px 0px"
});
if (footerRef.current) {
interSectionObserver.observe(footerRef.current)
}
}, [])
const addblocks = (counts: Number) => {
setBlocks((prevBlocks) => ([...prevBlocks, ...generateBlock(counts)]))
}
return (
<>
<div>无线向下滚动</div>
{[...blocks]}
<div ref={footerRef} style={{ height: "1px" }}>footer</div>
</>
);
}
bash中执行: sh online.sh test1 test2
,则在online.sh文件中可以分别得到三个变量:$0 //值为 online.sh
$1 //值为 test1
$2 //值为 test2
1、webpack5
中devServer
其中后,打开的页面,控制台显示 socket链接失败, 大概率是端口问题,换一个端口就好了;
2、 利用mini-css-extract-plugin
插件提取css
后,修改js
文件,页面更新,但是修改css
文件,页面不更新。检查该插件的配置:
new MiniCssExtractPlugin({
filename: 'bundle.[contenthash].css' //输出css文件名称
})
开发环境下,不要带hash
或者contenthash
,就可以解决刷新问题。生产环境需要带,可以用来解决缓存问题。 webpack
官网有这样一个一笔带过的解释:
使用 filename: "[contenthash].css" 启动长期缓存。根据需要添加 [name]。
登录开发机:ssh username@ip
查看用户:who am i
创建用户:adduser
删除用户:userdel name
只删除用户,不删除用户家目录, 如果要把家目录一起删了,需要执行:deluser -remove-home name
切换用户:su username
修改主机名称: sudo hostnamectl set-hostname newhostname
在上一节中,已经把基础环境搭建好了,这里就陆续实现各指令。首先我们从打包开始,毕竟能够打包编译js代码,这应该是一个脚手架工具最基础的功能。
webpack
配置webpack
基础配置新建文件src/webpack/config.com.js
,该文件存放公用的webpack
配置:
import jsLoader from '../babel/jsLoader'
import plugin from '../babel/plugin'
const path = require('path')
export default (props) => {
const {
entry = './index.js'
} = props
return {
entry,
output: {
path: path.join(process.cwd(), 'dist'), //输出路径
filename: 'bundle.js'
},
resolve: {
extensions: ['.js', '.jsx', '.ts', '.tsx', '.json', '.vue'],
},
module: {
rules: [...jsLoader()] // 解析文件的各种loader
},
plugins: [...plugin()] // 配置一些常用的插件
}
}
新建文件src/webpack/config.prod.js
,存放打包时的配置信息
import commonConfig from './config.com.js'
export default (pkg) => {
return {
mode: 'production', //设置开发模式
...commonConfig(pkg) //各个模式下的公共配置
}
}
loader
先配置babel-loader
用以解析js
代码
src/babel/jsLoader.js
export default () => {
return [
// babel-loader转义js https://webpack.docschina.org/loaders/babel-loader/#root
{
test: /\.m?js$/,
exclude: /node_modules/, //不用编译 node_modules
use: {
loader: require.resolve('babel-loader'),
options: {
presets: [require.resolve('@babel/preset-env')]
}
}
}
]
}
plugin
plugin是一些辅助插件, 暂时以配置ProgressPlugin
为例,其作用是实时输出打包信息:
src/babel/plugin.js
import ProgressPlugin from "progress-bar-webpack-plugin"
export default () => {
return [new ProgressPlugin()] // 输出打包进度信息
}
src/commander/build.js
import Webpack from 'webpack'
import webpackConfig from '../webpack/config.prod.js'
export default pkg => {
const compiler = Webpack(webpackConfig(pkg))
compiler.run((err, status) => {
if (err) {
console.log('build err', err)
process.exit(1)
}
})
}
src/index.js
import build from './commanders/build'
...
// 自定义指令 build
program
.command('build')
.description('build program')
.action(() => {
build(config)
})
...
新建文件:demo/index.js
,在此目录下执行
simple-cli build
就可以看到输出打包进度信息,并输出结果demo/dist/bundle.js
。此时我们最基础的打包指令已经做好了。
现有的webpack
关键配置,比如entry
都是在工具内部写死的,但是一个合理的交互需要提给供用户自定义的能力。我们约定这些配置在用户项目的package.json
文件的simple-cli
字段下。
cli/src/index.js
...
const cwd = process.cwd()
// 定义默认值
let config = {
port: 8000,
entry: './index.js',
cwd: process.cwd()
}
const userPkgPath = `${cwd}/package.json` //用户目录下package.json文件路径
// 判断是否存在 package.json
if (fs.existsSync(userPkgPath)) {
let userConfig = require(userPkgPath).simple_cli || {}
// 优先使用用户自定义的配置
config = Object.assign(config, userConfig)
}
...
program
.command('build')
.description('build program')
.action(() => {
build(config)
})
在生产环境下,我们都是遵循增量部署的原则。这就意味着,每次打包输出的js
文件名称都是唯一的,webpack
支持带哈希值的输出文件来满足这一要求。
更新src/webpack/config.com.js
:
...
output: {
path: path.join(process.cwd(), 'dist'), // 输出路径
filename: 'bundle.[hash].js'
}
...
此时你会发现每次打包都会生成一个名称类似bundle.1ff2020b12189c7388ec.js
的文件。
但是,这样又引入了另一个问题,你的dist目录下存储的文件越来越多,对于本地开发这显然没必要,保留最新的一份就够了。那么有没有一个办法在写入文件之前,先清空dist目录呢。
webpack
告诉你,‘有’ 。 借助插件clean-webpack-plugin
即可。
更新src/babel/plugin.js
import ProgressPlugin from "progress-bar-webpack-plugin"
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
export default () => {
return [
new CleanWebpackPlugin(), // 清空output目录
new ProgressPlugin() // 输出打包进度信息
]
}
到这里一个简单的打包js
指令已经完成了。其他指令的具体实现,努力更新中。
未完待续
promise
的地位不言而喻,凡是需要异步处理的现在基本都在使用promise
; promise
的出现解决了异步处理中回调地狱的问题,使得开发者可以使用同步的方式去写异步逻辑。开发体验得到极大的提升;同时Promise还提供了很多有用的工具函数。 这里着重说一下Promise.all
方法;
Promise.all
实现将多个Promise
实例包装成一个的功能。
Promise
实例,如果不是,内部会用Promise.resolve
转化。Promise
都reslove
时,Promise.all
才reslove
。且返回的顺序和参数顺序一致(可以保证多个异步任务返回的顺序),其中一个失败时,整体失败,返回失败的那个实例数据。function PromiseAll(promises = []) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promises)) {
reject('PromiseAll 参数必须为数组')
}
let resloveNum = 0 //记录reslove的个数
const promisesLength = promises.length
const result = []
promises.forEach((item, index) => {
let actionItem = item
// 如果不是promise实例,使用Promise.resolve转换成 Promise 实例
if (!(item instanceof Promise)) {
actionItem = Promise.resolve(item)
}
actionItem.then(res => {
resloveNum++;
result[index] = res //确保返回顺序和参数顺序一致
if (resloveNum === promisesLength) {
resolve(result)
}
}).catch(res => {
reject(res)
})
})
})
}
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
})
const p2 = 2
const p3 = Promise.resolve(3)
const p4 = Promise.reject(4)
PromiseAll([p1, p2, p3]).then((res) => {
console.log('resolve',res)
}).catch(res => {
console.log('catch', res)
})
// 1秒后输出 resolve [1,2,3]
PromiseAll([p2, p3, p4]).then((res) => {
console.log('resolve', res)
}).catch(res => {
console.log('catch', res)
})
// 输出 catch 4
q
: 退出;
wq
:修改后保存退出;
q!
:强制退出,不保存修改的内容;
shift+g
:定位到最后
ctrl+a
:回到行手;
ctrl+e
:回到行尾;
源码地址:https://github.com/keep-run/simple-cli
首先,在你的工作目录下建立一个目录simple-cli
。该目录下有两个文件夹cli
,demo
。
simple-cli
├── cli
└── demo
未来会在cli下写我们的脚手架工具,demo中写一些测试用例。
在cli下建立一个index.js
文件,文件中写入
#!/usr/bin/env node // 指定执行环境是node。
console.log('test')
执行pwd
。得到该文件的绝对路径:
/Users/XXXX/Desktop/study/simple-cli/cli //xxxx是你的用户名
现在思考几个问题:
create-react-app
后,为什么就能在工作目录下执行 create-react-app my-app
?这里就出现了一个叫软链接的东西。在create-react-app
源码的package.json中有这么一段:
"bin": {
"create-react-app": "./index.js"
},
这一段的意思就是,全局安装create-react-app
后,会建立一个软链接到项目的.index.js
。所以执行任何create-react-app
的指令,都会走到这个index.js
。
本地看一下,打开你的命令行工具,输入:which create-react-app
, 会返回:/usr/local/bin/create-react-app
。现在我们执行cd /usr/local/bin/
,接着执行ll
。会看到其中一行为:
lrwxr-xr-x 1 root wheel 45B 9 27 2018 create-react-app -> ../lib/node_modules/create-react-app/index.js
可以看到,create-react-app
指向了一个js文件。
现在在该目录下执行指令建立链接:
ln -s /Users/XXXX/Desktop/study/simple-cli/cli/index.js simple-cli
返回到你的工作目录,执行 simple-cli
。如果不出意外的话会得到如下异常:
permission denied: simple-cli
说明权限不够,接下来修改index.js文件的权限,使之可以执行:
chmod 777 index.js
再次执行 simple-cli
。就会输出:test
至此,前期准备工作就完成了。接下来就可以在index.js文件中开发脚手架的具体功能了。
koa是基于node.js的web开发框架,官网:https://koa.bootcss.com/ 。不做详细介绍;在node端与前端交互时,不可避免的就是接口请求。在获取请求参数的时候,koa是基于一些中间件来处理的,而且这些中间件很多,至少有koa-body
,koa-bodyparser
,koa-better-body
,koa-multer
等(这么多真让人头大)。这里以koa-body
为例介绍一些常用的接口参数获取方法;
// server 端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
multipart: true
})
const app = new Koa()
app.use(koaBody)
const koaBodyTest = ctx => {
const query=ctx.request.query
const querystring=ctx.request.querystring
ctx.response.body = {
query, querystring
}
}
app.use(route.get('/test/get', koaBodyTest))
app.listen(3001, () => {
console.log('listen 3001')
})
// client端 demo.js
const axios =require('axios')
axios({
url:'http://0.0.0.0:3001/test/get',
params:{
test:123
}
}).then(res=>{
console.log('response---->',res.data)
})
分别进入个字文件目录执行 node demo.js
,可以看到client端的接口返回为:
response----> { query: { test: '123' }, querystring: 'test=123' }
koa自身没有封装关于post参数解析的方法,所以出现两种办法解析post参数。原生实现&中间件
//server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const app = new Koa()
const handlePost = async (ctx) => {
let data = '';
await new Promise(reslove => {
ctx.req.addListener('data', (chunk) => {
data += chunk
})
ctx.req.addListener('end', (chunk) => {
reslove()
})
})
ctx.body = data
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
console.log('listen 3001')
})
// client端 demo.js
const axios = require('axios')
axios({
url: 'http://0.0.0.0:3001/test/post',
data: {
test: 123
},
method: 'post'
}).then(res => {
console.log('response---->', res.data)
})
分别执行各自的demo.js。client端的返回为:
response----> { test: 123 }
// server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
multipart: true
})
const app = new Koa()
app.use(koaBody)
const handlePost = async (ctx) => {
ctx.response.body = ctx.request.body
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
console.log('listen 3001')
})
const axios = require('axios')
axios({
url: 'http://0.0.0.0:3001/test/post',
data: {
test: 123
},
method: 'post'
}).then(res => {
console.log('response---->', res.data)
})
client返回代码:response----> { test: 123 }
// server端 demo.js
const Koa = require('koa')
const route = require('koa-route')
const koaBody = require('koa-body')({
multipart: true
})
const app = new Koa()
app.use(koaBody)
const handlePost = async (ctx) => {
ctx.response.body = ctx.request.body
}
app.use(route.post('/test/post', handlePost))
app.listen(3001, () => {
console.log('listen 3001')
})
// client端 demo.js
const axios =require('axios')
const FormData = require('form-data');
const form = new FormData();
form.append('file', 'teset');
axios.post('http://0.0.0.0:3001/test/post', form).then(result => {
console.log('response---->',result.data);
});
此时,client端的返回如下:response----> { '----------------------------511028953551316351151647\r\nContent-Disposition: form-data; name': '"file"\r\n\r\nteset\r\n----------------------------511028953551316351151647--\r\n' }
可以看到,koa没能解析formdata的数据,原因在于client发送请求的时候,没指定content-type
。默认为:application/x-www-form-urlencoded
。这个头,koa-body
不能解析。而formdata的强大之处在于,可以执行form.getHeaders()
,为请求添加合适的头。修改client端代码如下:
const axios =require('axios')
const FormData = require('form-data');
const form = new FormData();
form.append('file', 'teset');
axios.post('http://0.0.0.0:3001/test/post', form,{
headers:form.getHeaders()
}).then(result => {
console.log('response---->',result.data);
});
重新执行 client端代码,输出如下:response----> { file: 'teset' }
'函数柯里化可以理解为分步接受函数的参数,等到接受完毕的时候,最终执行'。下边以实现累加为例,具体剖析:
function curry(fn){
let args=[]
return function next(...params){
if(params.length>0){ // 还有参数,则接着返回函数
args=[...args,...params];
return next
}else{ //没有参数,表明接受完所有的参数了
return fn(...args)
}
}
}
let add=curry(function(...items){
return items.reduce((prev,cur)=>{return prev+cur},0)
})
console.log(add(1)(2,3)()) //6
在简易版中,判断最终执行函数的条件是传入了空参数。有没有办法省略这一步呢。其实是有以下两个办法:
toString
或者valueOf
函数。toString
或者valueOf
方法function curry(fn) {
let args = []
function next(...params) {
args = [...args, ...params]
return next
}
next.toString = function () {
return fn(...args)
}
next.valueOf=function(){
return fn(...args)
}
return next
}
let add = curry(function (...items) {
return items.reduce((prev, cur) => { return prev + cur }, 0)
})
//case1
let res = add(1)(2, 3)
console.log(res) //{ [Function: next] toString: [Function], valueOf: [Function] } //没有求值的操作,所以打印了函数。
// case2
let res = add(1)(2, 3)+0 //求值的操作,会调用`valueOf`。
console.log(res) //6
这种方法适用于预先知道需要柯里化的函数的参数个数。
function curry(fn) {
let allArgs = []
let len = fn.length
return function next(...args) {
allArgs = [...allArgs, ...args]
if (allArgs.length === len){
return fn(...allArgs)
}else{
return next
}
}
}
let add = curry(function (p1, p2, p3) {
return p1 + p2 + p3
})
console.log('res',add(1)(2,3)) //6
或者curry函数接受第二个参数,显示指定参数的个数。
function curry(fn,len) {
let allArgs = []
return function next(...args) {
allArgs = [...allArgs, ...args]
if (allArgs.length === len){
return fn(...allArgs)
}else{
return next
}
}
}
let add = curry(function (...items) {
return items.reduce((prev, cur) => { return prev + cur }, 0)
},3)
console.log('res',add(1)(2,3)) //6
上面的例子用到了函数的length属性,length表示函数预期输入的参数个数,那么给了参数默认值的,或者rest参数,都不在length中。且给了默认值后面的没有默认值的参数,也不再length中。比如:
(function(a,b,c){}).length //3 正常使用
(function(a,b,...args){}).length //2 rest参数不计入
(function(a,b,c=1){}).length //2 默认值的参数不计入
(function(a,b=2,c){}).length //1 默认值之后的参数也不计入
参考文献:
1、https://juejin.im/post/5b561426518825195f499772
2、https://es6.ruanyifeng.com/#docs/function#rest-%E5%8F%82%E6%95%B0
git log
,找到合并操作的 commit值;git revert -m 1 <commit>
;传入参数1,会保留当前分支merge前的状态,传入参数2会保留被合并的分支之前的状态;详细解释git archive --remote=xxxxx.git HEAD README.md
,读取仓库中的readme.mdcurl --request GET --header 'PRIVATE-TOKEN:*****' 'http://git.xxxx.com/plat-fe/fe-store-finance/raw/master/package.json?ref=master'
git remote update origin --prune
详情:https://cloud.tencent.com/developer/ask/77349首先http协议是一个无状态的协议。那么对一个服务来说,怎么区分是A访问的还是B访问的,这很重要,与之相关的就是登陆各个网站的登陆系统; 采用的方案大多是cookie和session;
cookie由http响应时服务写入的。下一次请求就会自动携带相应域名下的cookie,作为http请求的一部分发送给服务。这样客户端就可以在cookie中携带自己的身份证明发送给服务;
首先session是放在服务端的,可以在内存中、数据库甚至文件中。说白了就是一个对象,客户端首次访问时,会生成一个session_id(根据服务类型,key默认名称不一样,常见的有jsessionId,connect_id等)。session_id对应一个对象,存储一些信息,比如用户信息等。同时会保存在内存以及存储设备上,然后把这个id作为cookie的一部分返回给客户端, 客户端下次访问时,服务从cookie中读取以上id。并在内存或者存储设备中找,如果找到则认为是同一用户。以此实现不同访问之间的身份识别。
以koa
和koa-session
为例子来说明以上过程。koa-session
中可以设置store,并提供get
,set
,destroy
即可。
const session = require('koa-session');
const Koa = require('koa');
const app = new Koa();
const path = require('path');
const fs = require('fs');
app.keys = ['some secret hurr'];
const store = {
get(key) { /** 这个key就是cookie中的id, 对应CONFIG.key的值 */
const sessionDir = path.resolve(__dirname, './session');
const files = fs.readdirSync(sessionDir);
/** 读取session时,在session目录下找相应的文件,每个文件都为一个session的信息 */
for (let i = 0; i < files.length; i++) {
if (files[i].startsWith(key)) {
const filepath = path.resolve(sessionDir, files[i]);
delete require.cache[require.resolve(filepath)];
const result = require(filepath);
return result;
}
}
},
set(key, session) {
/** 设置session时,在session目录下建一个文件,文件名为session_id。值为对应的其他信息*/
const filePath = path.resolve(__dirname, './session', `${key}.js`);
const content = `module.exports = ${JSON.stringify(session)};`;
fs.writeFileSync(filePath, content);
},
destroy(key){
const filePath = path.resolve(__dirname, './session', `${key}.js`);
fs.unlinkSync(filePath);
}
}
const CONFIG = {
key: 'koa:sess', /** cookie的key 默认 'koa:sess'*/
maxAge: 86400000, /** 有效时间,默认一天 */
overwrite: true, /** (boolean) can overwrite or not (default true) */
httpOnly: true, /** (boolean) httpOnly or not (default true) */
signed: true, /** (boolean) signed or not (default true) */
rolling: false, /** (boolean) Force a session identifier cookie to be set on every response. The expiration is reset to the original maxAge, resetting the expiration countdown. default is false **/
store /** 自定义的store */
};
app.use(session(CONFIG, app));
// or if you prefer all default config, just use => app.use(session(app));
app.use(ctx => {
// ignore favicon
if (ctx.path === '/favicon.ico') return;
let n = ctx.session.views || 0;
ctx.session.views = ++n;
ctx.body = n + ' views';
});
app.listen(3000);
console.log('listening on port 3000');
以上程序实现的是同一个session每次请求,views加1的功能;
客户端第一次访问,session
目录下会建立如下文件
ded82c73-e772-449d-acf4-c1a298eefd42.js
文件内容
module.exports = {"views":1,"_expire":1609926748437,"_maxAge":86400000};
同一个浏览器再次访问,文件内容中的views每次增加1。
同时 客户端浏览器会存储一个cookie。 key为koa:sess
, value为: ded82c73-e772-449d-acf4-c1a298eefd42
清除cookie再次访问,返回1views. 服务端session目录下会新建另一个相同形式的js文件;
mysql -u [username] -p //回车输入密码即可
show databases; //记得有‘;’,否则语句没有结束
show global variables like 'port' //默认3306
很多前端对于Linux指令都不熟,当需要操作开发机、堡垒机的时候就束手无策。这里总结一下基本指令(持续增加中。。。)
cd
pwd
ls
,可以增加一些参数:
ls -l
等价于ll
:查看当前文件夹下的所有文件,并显示文件属性;ls -a
:查看当前文件夹下的隐藏文件;l'scat filename|wc -l
cat filrname|wc -c
du -sh filename
,-sh
会自动换算到合适的单位touch filename
rm -rf filename
vi filename
cat filename
ls -l dirname
mkdir foldername
rm -rf foldername
。(删除真个文件夹)root
目录下搜索 file1
: find /root file1
root
目录下搜索 file1
并删除: find /root file1 -exec rm -rf {}
grep
:
cat filename|grep linux
查找filename中包含linux的行;cat filename|grep -E “linux|php”
查找filename中包含linux或者php的行ps
: Process Status的缩写, 该指令可以列出系统中当前运行的那些进程。该指令可以指定以下参数:
-a
:显示统一终端下的所有程序-H
:显示树状结构;-aux
: 显示所有包含其他使用者的进程;以上参数可以联合使用, 比如ps -auxH
另外linux指令管道化** ps -aux | grep cust
查找进程中包含关键字"cust"的进程。
ln -s 源文件 目标文件
ln 源文件 目标文件
两者的区别:软链接会在指定位置生成一个源文件的镜像,不会占用磁盘空间,但是硬链接会在指定位置生成一个和源文件相同的文件。
cat /etc/passwd
su name
(若要切到root,则需要 sudo su name。 否则可能出现输入密码后,返回鉴定故障)chmod
, 详情
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.