Joyce


  • Home

  • Archives

  • Tags

  • About

  • Search

Canvas VS. SVG

Posted on 2022-07-26
| Words count in article: 791

Canvas与SVG的主要区别

  1. 图像类别:Canvas是基于像素的位图,SVG基于XML的矢量图形;
  2. 渲染模式:Canvas是逐像素进行渲染,一旦绘制完成就不会被浏览器继续关注。而SVG是通过DOM操作来显示,SVG中被绘制的图形都被视为对象,若对象的属性发生变化,浏览器能自动重现图形,但当节点过多时渲染会很慢;
  3. SVG支持分层和事件,Canvas不支持,要使用库来实现;

Canvas的使用

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
<canvas id="myCanvas" width="150" height="150" style="border:1px solid #c3c3c3;">您的浏览器不支持canvas</canvas>

var canvas = document.getElementById("myCanvas");
if(canvas.getContext("2d")) {
var ctx = canvas.getContext("2d");

ctx.fillStyle = "rgb(200,0,0)";
ctx.fillRect (10, 10, 55, 50);

canvas.addEventListener("mousedown", function(event){
console.log("hi");
});
}

判断点击事件

1
2
3
4
5
6
7
8
9
10
11
function getEventPosition(event) {
let x, y;
if(event.layerX || event.layerY == 0){
x = event.layerX;
y = event.layerY;
} else if (event.offsetX || event.offsetY == 0) {
x = event.offsetX;
y = event.offsetY;
}
return { x: x, y: y };
}

加载图片

1
2
3
4
5
6
ctx = clearRect(0,0, canvas.width, canvas.height);
const image = new Image();
image.onload = ()=>{
ctx.drawImage(image,left,top,width, height);
};
image.src = src;

计算文字宽度及长度

1
2
3
4
5
6
function measureText(fontSize, fontFamily, fontWeight, text) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;
return context.measureText(text).width;
}

SVG的使用

基本形状

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<svg> 包裹并定义整个矢量图形
<line> 直线:x1、y1、x2、y2
<rect> 矩形:x、y、width、height
<circle> 圆形:cx、cy、r
<ellipse> 椭圆:cx、cy、rx(x 方向半径)、ry(y 方向半径)

<polyline> 创建相连折线段:points(一些列 x/y 坐标)
<polygon> 创建封闭图形:points(一系列 x/y 坐标)

<path> 通过指定 点 以及 点和点之间的线 来创建任意形状
<defs> 定义一个可复用的图形,初始情况下 <defs> 里面的内容是不可见的
<g> 将多种形状组合起来。将组合后的形状置于 <defs> 中可以让它能够被复用
<symbol> 类似于一个组合,但拥有一些额外的特性。通常被置于 <defs> 标签中便于复用
<use> 获取在 <defs> 中定义的复用对象并在 SVG 中显示出来

画笔特性

1
2
3
4
5
6
7
stroke 画笔颜色,默认为 none
stroke-width 画笔宽度,默认为 1
stroke-opacity 0.0 完全透明,1.0 完全不透明(默认)
stroke-dasharray 虚线间距,默认为 none
stroke-linecap 线头尾形状,butt(默认)、round、square
stroke-linejoin 圆形的棱角,miter(尖的,默认)、round、bevel(平的)
stroke-miterlimit 相交处显示宽度与线宽最大比例,默认为 4

填充特性

1
2
3
fill	默认为 black
fill-opacity 0.0 完全透明,1.0 完全不透明(默认)
fill-rule nonzero(默认值)、evenodd

示例

1
2
3
4
5
6
7
8
9
10
<body>
<svg width="300px" height="300px">
<circle cx="100" cy="100" r="60" stroke="#c3c3c3" stroke-width="5" fill="green" />
</svg>

<svg xmlns="http://www.w3.org/2000/svg" version="1.1" height="190" @click="svgClick">
<polygon points="100,10 40,180 190,60 10,60 160,180" @click="domClick"
style="fill:lime; stroke:purple; stroke-width:5; fill-rule:evenodd;" />
</svg>
</body>

SVG实例

JS手写篇

Posted on 2022-06-20
| Words count in article: 1,471

节流 & 防抖

节流:用户频繁点击按钮,但只能3秒(timer)才执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 节流:技能冷却中
const throttle = (f, time) => {
let timer = null
return (...args) => {
if(timer) {return}
f.call(undefined, ...args)
timer = setTimeout(()=> {
timer = null
}, time)
}
}

const f = throttle(()=> {
console.log('hello')
}, 3000)

f() // 打印hello
f() // 技能冷却中

防抖:用户频繁拖动操作,希望拖动停止后再实现一个效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 回城被打断
const debounce = (fn, time) => {
let timer = null
return(...args) => {
if(timer != null) {
clearTimeout(timer) // 打断回城
}
// 重新回城
timer = setTimeout(()=>{
fn.call(undefined, ...args) // 回城后调用fn
timer = null
}, time)
}
}

const f = debounce(()=> {
console.log('回城成功');
}, 3000)

发布订阅模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const eventHub = {
map: {
// 事件队列
// click: [f1, f2]
// mouseDown: [f3, f4]
},
// 监听事件
on: (name, fn) => {
eventHub.map[name] = eventHub.map[name] || [] // 初始化
eventHub.map[name].push(fn)
},
// 触发事件
emit: (name, data) => {
const q = eventHub.map[name]
if(!q) {return}
q.map(f => f.call(null, data))
return undefined
},
// 取消监听
off: (name, fn)=> {
const q = eventHub.map[name]
if(!q) {return}
const index = q.indexOf(fn)
if(index < 0) {return}
q.splice(index,1)
}
}

eventHub.on('click', console.log);
eventHub.on('click',console.error);

setTimeout(()=>{
eventHub.emit('click', 'joyce')
}, 3000)

用class实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class EventHub {
map = {}
on(name, fn) {
this.map[name] = this.map[name] || []
this.map[name].push(fn)
}
emit(name, data) {
const fnList = this.map[name] || []
fnList.forEach(fn => fn.call(undefined, data))
}
off(name, fn) {
const fnList = this.map[name] || []
const index = fnList.indexOf[fn]
if(index < 0) return
fn.splice(index, 1)
}
}

const e = new EventHub()
e.on('click', (name) => {
console.log('hi' + name)
})
e.on('click', (name) => {
console.log('hello' + name)
})
setTimeout(()=>{
e.emit('click', 'joyce')
},3000)

手写AJAX

1
2
3
4
5
6
7
8
9
10
11
12
var request = new XMLHttpRequest()
request.open('GET','/xxx')
request.onreadystatechange = function(){
if(request.readyState === 4){
if(request.status >= 200 || request.status === 304){
success(request)
}else{
fail(request)
}
}
}
request.send()

简易版Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class Promise2 {
#status = 'pending' // 私有属性,防止用户更改状态
constructor(fn){
this.q = []
const resolve = (data)=>{
this.#status = 'fulfilled'
const f1f1 = this.q.shift()
if(!f1f2 || !f1f2[0]) return
const x = f1f2[0].call(undefined, data)
if(x instanceof Promise2){
x.then((data)=> {
resolve(data)
}, (reason)=> {
reject(reason)
})
}else {
resolve(x)
}
}
const reject = (reason)=>{
this.#status = 'rejected'
const f1f1 = this.q.shift()
if(!f1f2 || !f1f2[1]) return
const x = f1f2[1].call(undefined, reason)
if(x instanceof Promise2){
x.then((data)=> {
resolve(data)
}, (reason)=> {
reject(reason)
})
}else {
resolve(x)
}
}
fn.call(undefined, resolve, reject)
}
then(f1,f2){
this.q.push([f1,f2])
}
}

const p = new Promise(function(resolve,reject){
setTimeout(function(){
reject('出错啦') // resolve('hello')
}, 3000)
})

p.then( (data)=>{console.log(data)}, (reason)=>{console.error(reason)} )

Promise.all

  • 在Promise上写,而不是原型
  • 参数是一个Promise数组,返回值是一个新Promise对象
  • 用一个数组来记录结果
  • 只要有一个reject,就整体reject
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Promise.myAll = function(list){
    const results = []
    let count = 0 // 表示成功resolve的数量,当const等于list的长度时,说明全部成功
    return new Promise((resolve,reject) =>{
    list.map((promise,index) => {
    promise.then((result) => {
    results[index] = result // 将成功的结果记录在results里
    count += 1
    if(count === list.length) { // 全部成功后调用resolve
    resolve(results)
    }
    }, (reason) => {
    reject(reason)
    })
    })
    })
    }

深拷贝

  • 方法一:用JSON
  1. 该方法不支持日期、正则、函数、undefined等数据
  2. 不支持引用
    1
    const b = JSON.parse(JSON.stringify(a))
  • 方法二:用递归
  1. 检查环:a.self = a 造成的无法退出递归的问题,使用缓存cache
  2. 不拷贝原型上的属性(hasOwnProperty)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    const deepClone = (a, cache) => {
    if(!cache){
    // 只有第一次深拷贝时创建缓存,后面递归的不会再创建
    // 使用Map而不是Object,Map的键可以是对象、函数,而Object的键只能是string/symbol
    cache = new Map() // 使用Map做映射,只要曾经拷贝过,就不会重复拷贝
    }
    if(a instanceof Object){
    // 判断是对象的实例
    if(cache.get(a)) { return cache.get(a) } // 检查环,因为a.self = a 无法退出递归
    let result
    if(a instanceof Function){
    if(a.prototype) { // 有 prototype 就是普通函数
    result = function(){ return a.apply(this, arguments) }
    } else { // 否则就是箭头函数
    result = (...args) => { return a.call(undefined, ...args) }
    }
    } else if (a instanceof Array) {
    result = []
    } else if (a instanceof Date) {
    result = new Date(a-0)
    } else if (a instanceof RegExp) {
    result = new RegExp(a.source, a.flags)
    } else {
    result = {}
    }

    cache.set(a, result)
    for(let key in a) {
    if(a.hasOwnProperty(key)){ // 只要自身属性,不拷贝原型上(继承)的属性
    result[key] = deepClone(a[key], cache)
    }
    }
    return result
    } else {
    // 其他基本类型
    return a
    }
    }

    const a = {
    number:1, bool:false, str:'1', empty1:undefined, empty2:null,
    array: [
    {name:'tony', age: 18},
    {name:'joyce', age: 24}
    ],
    date: new Date(2000,0,1,20,30,0),
    regex: /\.(j|t)sx/i,
    object: {name:'joyce', age: 24},
    f1: (a,b) => a+b,
    f2: function(a,b){ return a+b }
    }
    a.self = a

    const b = deepClone(a)

    b.self === b // true
    b.self = 'hello'
    a.self !== 'hello' // true

数组去重

  1. 方法一:使用Set

    1
    2
    3
    4
    5
    6
    var a1 = [1,2,2,3,3,3]
    var unique = function(a){
    return Array.from(new Set(a)) // return [...new Set(a)]
    }

    unique(a1) // 打印 [1,2,3]
  2. 方法二:使用计数排序,但只支持字符串

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var a1 = [1,2,2,3,3,3]
    var unique = function(a){
    var map = {}
    for(let i=0; i<a.length; i++) {
    let number = a[i] // 1 ~ 3
    if(number === undefined) { continue }
    if(number in map) { continue }
    map[number] = true
    }
    // map = {1:true, 2:true, 3:true}
    return Object.keys(map)

    // 不用Object.keys的写法
    const result = []
    for(let key in map) {
    result.push(key)
    }
    return result
    }
  3. 方法三:使用Map,但兼容性稍差

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var a1 = ['1',1,,2,,2,3,3,3,4,1,2]

    var unique = function(a){
    var map = new Map()
    for(let i=0; i<a.length; i++){
    let number = a[i] // 1 ~ 3
    if(number === undefined) { continue }
    if(map.has(number)){
    continue
    }
    map.set(number, true)
    }
    return [...map.keys()]
    }

Vue3

Posted on 2022-06-17
| Words count in article: 316

Vue3 为什么使用 Proxy

  1. 弥补Object.defineProperty的两个不足
  • 动态创建的data属性需要用 Vue.set 来赋值,用了Proxy就不需要了
  • 基于性能考虑,Vue2篡改了数组的7个API,用了Proxy就不需要了
  1. defineProperty需要提前递归地遍历data做到响应式,而Proxy可以在真正用到深层数据时再做到响应式

Vue3 为什么使用 Composition API

  1. Composition API 相比 mixins、高阶组件、Renderless Components更好,因为这些模式存在下面三个问题
  • 模板中数据来源不清晰。当一个组件中使用了多个mixin,只看模板很难分清一个属性到底来自哪个mixin
  • 命名空间冲突。不同开发者开发的mixin无法保证不会正好用到一样的属性或方法名
  • 性能。
  1. 更适合 TypeScript

推荐阅读 => 尤雨溪 Vue Function-based API RFC

推荐阅读 => 组合API问题解答

相比于 Vue2 做了哪些改动

  1. 根元素可以不只有一个
  2. createApp()代替了new Vue()
  3. v-model代替了以前的v-model和.sync
  4. 新增 Teleport 传送门
  5. ref属性支持函数
  6. destroyed 改名为 unmounted、beforeDestroy 改名为 beforeUnmount

推荐阅读 => 非兼容的变更

Vue2

Posted on 2022-06-15
| Words count in article: 1,313

Vue2 的生命周期钩子有哪些?数据请求放在哪

  • beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed
  • activated、deactivated、errorCaptured
  • 数据请求一般放在 mounted 里面
  • 如果放在created里,会在后端SSR(Server-Side Render)和前端都执行一次,不符合预期
  • updated钩子触发太过频繁,一般数据请求只触发一次

Vue2 组件间通信方式

  1. 父子通信:使用props和事件进行通信
  2. 爷孙通信:
  • 使用两次父子组件通信
  • 使用provide和inject
  1. 任意组件:使用eventBus = new Vue()
  • 主要API是eventBus.$on(监听) 和 eventBus.$emit(触发)
  • 缺点:事件多了容易乱,难以维护
  1. 任意组件:使用Vuex通信(Vue3可用Pinia代替Vuex)

Vuex用过吗?如何理解

  1. 描述:Vuex是一个专为vuejs应用程序开发的 状态管理模式 + 库
  2. 核心概念:
    1
    2
    3
    4
    5
    6
    a. store是一个大容器,包含以下所有内容
    b. State 用来读取状态,带有一个 mapState 辅助函数
    c. Getter 用来读取派生(计算)状态,带有一个mapGetters 辅助函数
    d. Mutation 用于同步提交状态变更(写),带有一个mapMutations 辅助函数
    e. Action 用于异步变更状态(写),但它提交的是mutation,而不是直接变更状态
    f. Module 用来给store划分模块,方便代码维护

VueRouter用过吗?如何理解

描述:官方推荐的路由,适用于做单页面应用

router-link

进行导航,传递to来指定链接,最终会呈现一个带有href属性的a标签

router-view

路由出口,路由匹配到的组件将在这里渲染

嵌套路由

在一个路由下还分有子路由,他们分别又对应不同的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const routes = [
{
path: '/',
redirect: '/account'
},
{
path: '/account',
component: account,
// 使用children属性,实现子路由
// 子路由的path前不带'/',否则永远以根路径开始请求
children: [
{
path: 'login',
component: Login
},
{
path: 'register',
component: Register
}
]
}
]

Hash模式 && History模式

前端路由:找到与地址相匹配的组件并将它渲染出来

本质:改变浏览器地址(更新视图)但不向服务器发出请求

有两种方法可以做到:

  • hash模式 利用URL中的hash(“#”)
  • history模式 利用 history.pushState() API来完成URL跳转而无须重新加载页面,但是需要后端将所有前端路由都渲染同一页面,且IE8以下的浏览器不支持

两种模式的对比:

  • Hash模式只可以更改 # 后面的内容(也就是字符串),History模式可以通过 API 设置任意的同源 URL
  • Hash模式无需后端配置且兼容性好,History模式在用户手动输入地址或者刷新页面的时候会发起URL请求,后端需要配置 index.html页面 用于匹配不到静态资源的时候

路由守卫实现:权限控制 & 加载进度

  • 在系统路由跳转前做权限校验
  • Vue-Router提供了两个钩子函数,前置守卫beforeEach 和 后置守卫afterEach,前者在路由跳转前触发,后者在路由跳转后触发
  • 注册全局路由守卫。当路由跳转时,路由守卫按顺序异步解析执行,只有当所有守卫resolved才会进入resolved状态,否则处在pending状态中

    1
    2
    3
    4
    5
    6
    7
    8
    const router = new VueRouter({ ... })

    router.beforeEach((to,from,next) => {
    // ...
    })
    router.afterEach((to,from,next) => {
    // ...
    })
  • 守卫的回调函数接受三个参数

  1. to:即将进入的路由,一个Route对象
  2. from:正要离开的路由,一个Route对象
  3. next:一个Function,回调函数内部必须执行该方法,令守卫进入resolved状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    router.beforeEach((to,from,next) => {
    // 鉴权操作
    const token = getToken()

    if(token) {
    if(to.path === '/login'){
    next('/')
    } else {
    // 其他鉴权操作,如资源是否可访问
    let menus = store.getters['user/menus']
    if(menus.includes(to.path)) {
    next()
    } else {
    next({path: '/403', replace: true}) // replace 替换当前路由,避免用户后退操作导致重复跳转
    }
    }
    } else {
    if(whiteList.includes(to.path)){
    next()
    } else {
    next(`/login?redirect =${to.path}`)
    }
    }
    })

参考文章巧用Vue-Router路由守卫实现路由权限控制和加载进度

路由懒加载

将不同路由对应的不同组件分割成不同的代码块,然后当路由被访问时才加载对应的组件

1
2
3
// 用动态导入代替静态导入
// 将 import UserDetails from './views/UserDetails' 替换成
const UserDetails = () => import('./views/UserDetails')

Vue2 如何实现双向绑定?

  1. 一般使用 v-model/.sync 实现,v-model实际上是v-bind:value和v-on:input的语法糖
    a. v-bind:value => 绑定value,当data改变就去更新UI
    b. v-on:input => 监听input事件,当input被触发就去更新data
  2. 上面两个单向绑定如何实现
    a. 前者通过 Object.defineProperty API 给data的每个属性创建 getter 和 setter,用于监听data的改变。当data发生改变,就会安排UI变
    b. 后者通过 template compiler 给DOM添加事件监听,当DOM input的值变了就会去修改data

参考文章vue的双向绑定原理及实现

TypeScript相关

Posted on 2022-06-14
| Words count in article: 1,522

TS基础语法

1
2
3
4
5
6
7
8
9
const a:string = 'hi'
const b:number = 111
const c:boolean = true
const d:null = null
const e:undefined = undefined
const f:symbol = Symbol('hi')
const g:bigint = 123n
const obj:Object = {}
const arr:Array<string | number | null> = ['1','2', 3, null]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 函数的类型声明
// 1. 类型写在函数体里
const add1 = (a:number, b:number) => a+b
// 2. 写在 :后面
const add2:(a:number, b:number) => number = (a,b)=>a+b
// 3. type 缩写,本质与第二种写法相同
type Add = (a:number, b:number) => number
const add3:Add = (a,b)=>a+b
// 4. 有属性的函数,只能用interface
interface AddWithProps {
(a:number, b:number) : number
xxx: string
}
const add2:AddWithProps = (a,b) => a+b
add2.xxx = 'Hello World'

联合类型

  • 联合类型:由两种或多种其他类型组成的类型,表示可能是这些类型中的任何一种的值
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    type A = {
    name: 'a';
    age: number
    }
    type B = {
    name: 'b',
    gender: string
    }
    // 联合A与B两种类型
    // 使用要点:A、B两个类型中必须至少有一个相同的key,然后才能根据这个key的值做区分
    const f = (n: A|B) => {
    if(n.name === 'a'){ // 类型A与类型B中有相同的key,即name
    n.age
    } else {
    n.gender
    }
    }

TS vs. JS

  • 语法层面:TypeScript = JavaScript + Type(TS是JS的超集)
  • 执行环境:浏览器、nodejs可以直接执行JS,但不能执行TS(Deno可以执行TS)
  • 编译层面:TS有编译阶段,JS没有编译阶段(只有转译和ESlint阶段)
  • 编写层面:TS更难写一些,但类型更安全
  • 文档层面:TS代码写出来就是文档,IDE可以完美提示。JS提示主要靠TS

将TS转译为JS的方法

  1. webpack + babel
  2. Vite 2
  3. tsc 即 typescript compiler

使用TS的好处

  1. 检查bug:增加类型检查,可以在写代码(编译时)就报错,不需要等到代码运行时
  2. 智能提示:要求你做好类型判断(收窄类型),写代码更安全
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import React from 'react'
    import ReactDOM from 'react-dom'
    const root = document.getElementById('root')
    // 这里root可能的类型为 HTMLElement || null

    if(root !== null) { // 判断 root不为null 才执行
    ReactDOM.render(<div>hi</div>, root)
    } else {
    report('root为空')
    }

any vs. unknown

  • 两者都是顶级类型(top type),任何类型的值都可以赋值给顶级类型变量

    1
    2
    let foo:any = 123;  // 不报错
    let bar:unknown = 123; // 不报错
  • 但 unknown 比 any 的类型检查更严格,any什么检查都不做,unknown要求在使用前先明确类型(类型收窄)

    1
    2
    const value:unknown = "Hello World";
    const someString:string = value; //报错:Type 'unknown' is not assignable to type 'string'
1
2
const value: unknown = "Hello World";
const someString:string = value as string; // 不报错
  • 若改为any,基本在哪都不报错。所以优先用unknown,类型更安全

never

  • never是底类型,表示不应该出现的类型(什么都不是,出现never表示代码有问题)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    interface A {
    type: 'a'
    }
    interface B {
    type: 'b'
    }
    type All = A | B

    function handleValue(val: All) {
    switch (val.type) {
    case 'a':
    // 这里 val 被收窄为 A
    break

    case 'b':
    // val 在这里是 B
    break

    default:
    // val 在这里是 never
    const exhaustiveCheck: never = val
    break
    }
    }

type 和 interface 的区别?

  1. 组合方式:interface 使用 extends 来实现继承,type 使用 & 来实现联合类型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // interface的继承方式
    interface B {
    b: string
    }
    interface A extends B {
    a: string
    }

    const a:A = {
    a: 'hi',
    b: 'hi'
    }
    // type的继承方式
    type BB = {
    bb: string
    }
    type AA = {
    aa: string
    } & BB

    const aa:AA = {
    aa: 'hi',
    bb: 'hi'
    }
  2. 扩展方式:interface 可以重复声明用来扩展,type 一个类型只能声明一次

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface A {
    a:string
    }
    // interface可以重复声明,且两次声明会自动合并
    interface A {
    b:string
    }
    const a:A {
    a: 'hi',
    b: 'hi'
    }
1
2
3
4
5
6
7
type A = {
a: string
}
// 再次声明A会报错,用type声明变量后不可变
type A = {
b: string
}
  1. 范围不同:type 适用于基本类型,interface 一般不行
  2. 命名方式:interface 会创建新的类型名,type 只是创建类型别名,并没有新创建类型
    1
    2
    3
    type X = number  // 给number取了一个别名为 X
    const x:X = 1
    type Y = typeof x // Y === number

TS工具类型的作用和实现

  1. Partial(部分类型)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 创建一个用户,但用户id需要上传服务器后才能拿到
    interface User {
    id: string;
    name: string;
    }

    const user: Partial<User> = {
    name: 'joyce' // 创建用户时可以先不传id
    }
  2. Required(必填类型)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    interface User {
    id: string;
    name: string;
    }

    const user: Required<User> = { // id和name都必填
    id: '111',
    name: 'joyce'
    }
  3. Readonly(只读类型)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    interface User {
    id: string;
    name: string;
    }

    const user: Readonly<User> = {
    id: '111',
    name: 'joyce'
    }

    user.id='222' // 报错,user是只读的,不可更改
  4. Exclude(排除类型)&& Extract(提取类型) => 后接基本类型

    1
    2
    3
    4
    type Dir = '东' | '南' | '西' | '北'

    type Dir2 = Exclude<Dir, '北'> // 排除 '北'
    type Dir3 = Extract<Dir, '东'|'西'> // 只要 '东' '西'
  5. Omit(排除key类型)&& Pick(提取key类型) => 后接对象类型

    1
    2
    3
    4
    5
    6
    7
    8
    interface User {
    id: string;
    name: string;
    age: number;
    }

    type God1 = Omit<User, 'age' | 'id'> // 排除age和id,God此时只有name属性
    type God2 = Pick<User, 'name'> // God2只要name属性
  6. ReturnType(返回值类型)

    1
    2
    3
    4
    5
    6
    // 获取函数返回值类型
    function f(a:number, b:number){
    return a+b
    }

    type A = ReturnType<typeof f>

TS泛型

1
2
3
4
5
type Add<T> = (a:T, b:T) => T

// 泛型的调用
const addN:Add<number> = (a,b) => a+b
const addS:Add<string> = (a,b) => a + '' + b

用泛型封装网络请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type T = Partial<Omit<User, 'id'>>
type createResource = (path:string) => {
create: (attrs: Omit<Partial<User>, 'id'>) => Promise<Response<User>>;
delete: (id: User['id']) => Promise<Response<never>>;
update: (id: User['id'], attrs: Omit<Partial<User>, 'id'>) => Promise<Response<User>>;
get: (id: User['id']) => Promise<Response<User>>;
getPage: (page:number) => Promise<Response<User[]>>
}

const createResource:createResource = (path) => {
return {
create(attrs){
const url = path + 's'
return axios.post<User>(url, {data:attrs})
},
delete(){},
update(){},
get(){},
getPage(){}
}
}

var userResource = createResource('/api/v1/user')

JS

Posted on 2022-05-26
| Words count in article: 1,524

一、8种数据类型

1
string、number、boolean、undefined、null、object、symbol、bigint
  1. null vs.undefined
    null在设置对象为空时使用,undefined在设置非对象为空时用
  2. number vs. bigint(大整数)
    当数字非常大时使用bigint,用法:在数字后加 n,且bigint只支持整数不支持小数
  3. typeof用来检测数据类型(但有两个bug)
    1
    2
    typeof 函数  // 'function'
    typeof null // 'object'

二、原型链

  • 什么是原型
    假设有一个普通对象 x={ },x会拥有一个隐藏属性proto,这个属性会指向Object.prototype,即 x.proto = Object.prototype,此时可以说 x的原型 是 Object.prototype,而 proto 属性的作用就是指向x的原型
  • 举例说明原型链
    比方有一个数组对象 a=[ ],a有一个隐藏属性proto指向Array.prototype,Array.prototype也有一个隐藏属性指向Object.prototype,于是a就有了两层原型,中间通过proto形成一个链条,即
    a ===> Array.prototype ===> Object.prototype
  • 如何改变原型(链)

    1
    2
    3
    const x = Object.create
    // 或
    const x = new 构造函数() // 会导致 x.__proto = 构造函数.prototype
  • 解决了什么问题
    在没有class的情况下实现了继承,以下面代码为例

    1
    a ===> Array.prototype ===> Object.prototype
  1. a是Array的实例,a拥有Array.prototype里的属性(实例化)
  2. Array继承了Object
  3. a是Object的间接实例,a拥有Object里的属性(继承)
  • 缺点
    不支持私有属性,若要支持私有属性需使用class

三、this的指向

详见我的另一篇博客this指向及绑定方法

四、new的过程

  1. 创建临时对象/空对象/新对象,作为要返回的对象实例
  2. 绑定原型(绑定共有属性),将临时对象的原型指向构造函数的prototype属性
  3. 指定this = 临时对象
  4. 执行构造函数(绑定私有属性),将私有属性绑在临时对象上
  5. 返回这个临时对象
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 直观上理解
    let Foo = function(name) {
    let this = Object.create(Foo.prototype)
    this.name = name
    return this
    }
    Foo()

    // 代码底层上理解
    let objectFactory() {
    let obj = {}, // 从 Object.prototype 上克隆一个空的对象
    Constructor = [].shift.call(arguments) // 取得外部传入的构造器
    obj.__proto__ = Constructor.prototype // 指向正确的原型
    let ret = Constructor.apply(obj, arguments) // 借用外部传入的构造器给 obj 设置属性
    return typeof ret === 'object' ? ret : obj // 确保构造器总是会返回一个对象
    }
    let a = objectFactory(Person, 'seven')
    console.log(Object.getPrototypeOf(a) === Person.prototype) // true

五、立即执行函数

  • 声明一个匿名函数,然后立即执行它,写法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    (function(){alert('我是匿名函数')}())  // 括号包住整个表达式
    (function(){alert('我是匿名函数')}()) () // 括号包住函数
    !function(){alert('我是匿名函数')}()
    +function(){alert('我是匿名函数')}()
    -function(){alert('我是匿名函数')}()
    ~function(){alert('我是匿名函数')}()
    void function(){alert('我是匿名函数')}()
    new function(){alert('我是匿名函数')}()
    var x = function(){return '我是匿名函数'}
  • 在ES6之前,只能通过立即执行函数来创建局部作用域,且兼容性好

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建局部变量 ES5
    !function(){
    var a
    }()

    // ES6的 block+let 语法
    {
    var a
    console.log(a) // 可读取到a
    }
    console.log(a) // 不能读取到a

六、闭包

  • 闭包是JS的一种语法特性,JS里的函数都支持闭包
  • 闭包 = 函数 + 自由变量(非全局、局部变量的变量)
  • 下面的代码放在了一个非全局环境中(通过立即执行函数可以制造一个非全局环境),这样就是一个闭包的应用

    1
    2
    3
    4
    5
    6
    7
    8
    const add = function(){
    var count
    return () => { // 访问了外部变量的函数
    count += 1
    }
    }()

    add() // 相当于 count += 1
  • 闭包的作用在于:

  1. 避免污染全局环境
  2. 提供对局部变量的间接访问
  3. 维持变量,不被垃圾回收
  • 闭包使用不当可能造成内存泄漏
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function test(){
    var x = {name: 'x'};
    var y = {name: 'y', content: "-- -- -- --这里有很长很长很长的代码-- -- --"}
    return function fn(){
    return x
    }
    }

    const myFn = test() // myFn 就是 fn 了
    const myX = myFn() // myX 就是 x 了

对于一个正常的浏览器来说,y会在一段时间后自动消失(被垃圾回收掉)

但旧版本的IE浏览器不会回收,这是IE的问题

七、JS如何实现类

方法一:基于原型的class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Dog(name){
this.name = name
this.legsNumber = 4
}

Dog.prototype.kind = '狗子'
Dog.prototype.say = function(){
console.log(`汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}
Dog.prototype.run = function(){
console.log(`${this.legsNumber}条腿跑起来。`)
}

const d1 = new Dog('哮天犬') // Dog函数 就是一个类
d1.say()

方法二:基于class关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Dog {
kind = '狗子' // 等价于在 constructor 里写 this.kind = '狗子'
// class未提供在原型上添加非函数属性的方法
constructor(name){
this.name = name
this.legsNumber = 4
}
say(){
console.log(`汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}
run(){
console.log(`${this.legsNumber}条腿跑起来。`)
}
}

const d1 = new Dog('哮天犬')
d1.say()

八、如何实现继承(子类如何继承父类)

使用原型链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 声明父类Animal
function Animal(legsNumber){
this.legsNumber = legsNumber
}
Animal.prototype.kind = '动物'

// 声明子类Dog
function Dog(name){
this.name = name
Animal.call(this, 4) // 关键代码1:继承父类的私有属性
}
Dog.prototype.__proto__ = Animal.prototype // 关键代码2:继承父类的原型,但如果这句被禁用了怎么办

Dog.prototype.kind = '狗'
Dog.prototype.say = function(){
console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}

const d1 = new Dog('哮天犬')
console.dir(d1)
1
2
3
4
// 可替换 关键代码2
var f = function(){}
f.prototype = Animal.prototype
Dog.prototype = new f()

使用class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Animal{
constructor(legsNumber){
this.legsNumber = legsNumber
}
run(){}
}

class Dog extends Animal{
constructor(name){
super(4)
this.name = name
}
say(){
console.log(`汪汪汪~ 我是${this.name},我有${this.legsNumber}条腿。`)
}
}

CSS

Posted on 2022-05-26
| Words count in article: 1,076

BFC

  • BFC(Block formatting context),即块级格式化上下文
  • 一个新创建的BFC盒子是独立布局的,盒子内元素的布局不会影响到盒子外元素
  • 有时会有副作用,可以使用最新的 display: flow-root 来触发BFC

触发条件

  • float不为none(浮动元素)
  • overflow不为visible
  • position: absolute/fixed
  • display: inline-block/table-cell/flex/inline-flex
  • display: flex/inline-flex(弹性元素的直接子元素)

用途

  • 防止margin合并
  • 清除浮动(用BFC可以包住它内部的浮动元素)
  • float+div 左右自适应布局

eg.1 创建BFC包住内部浮动元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<div id="demo1">
<div class="child"></div>
</div>

<style>
#demo1 {
border: 5px solid red;
overflow: hidden;
}
.child {
background: green;
float: left;
height: 100px;
width: 100px;
margin-top: 10px;
}
</style>

eg.2 做左右自适应布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<div id="demo2">
<div class="left"></div>
<div class="right"></div>
</div>

<style>
#demo2 .left{
border: 5px solid green;
min-height: 300px;
width: 400px;
float: left;
margin-right: 20px;
}
#demo2 .right{
border: 5px solid pink;
min-height: 300px;
overflow: auto; /*触发BFC*/
}
</style>


垂直居中

  • 若父元素的 height 没写死,只需padding: 10px 0; 就可将子元素垂直居中
  • 若父元素的高度写死,则需要其他方式来实现垂直居中
  • 一般建议能不写死height就不写死

实现垂直居中的方式

  1. flex布局

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <div class="parent">
    <div class="child">
    一串文字一串文字一串文字一串文字一串文字一串文字一串文字一串文字一串文字一串文字
    </div>
    </div>

    <style>
    .parent{
    height: 600px;
    border: 3px solid red;

    display: flex;
    justify-content: center;
    align-items: center;
    }
    .child{
    border: 3px solid green;
    width: 300px;
    }
    </style>
  2. translate -50%

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    .parent{
    height: 600px;
    border: 1px solid red;
    position: relative;
    }
    .child{
    border: 1px solid green;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%,-50%);
    }
  3. margin-top -50%

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    .parent{
    height: 600px;
    border: 1px solid red;
    position: relative;
    }
    .child{
    border: 1px solid green;
    width: 300px;
    position: absolute;
    top: 50%;
    left: 50%;
    margin-left: -150px;
    height: 100px;
    margin-top: -50px;
    }
  4. 绝对定位 + margin: auto + 上下左右 0

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .parent{
    height: 600px;
    border: 1px solid red;
    position: relative;
    }
    .child{
    border: 1px solid green;
    position: absolute;
    width: 300px;
    height: 200px;
    margin: auto;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    }
  5. table自带功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    <table class="parent">
    <tr>
    <td class="child">
    一些内容一些内容
    </td>
    </tr>
    </table>

    <style>
    .parent {
    border: 2px solid green;
    height: 600px; /* 父元素写死高度 */
    }
    .child {
    border: 2px solid pink;
    }
    </style>
  6. 让div变成table

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <div class="table">
    <div class="td">
    <div class="child">
    一些文字一些文字一些文字
    </div>
    </div>
    </div>

    <style>
    div.table {
    display: table;
    height: 600px;
    border: 2px solid green;
    }
    div.td {
    display: table-cell;
    border: 2px solid yellow;
    vertical-align: middle;
    }
    div.child {
    border: 1px solid pink;
    }
    </style>
  7. 利用父元素的伪类(100%高度的伪类 + inline block)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <div class="parent">
    <div class="child">
    一些内容一些内容一些内容一些内容一些内容一些内容一些内容一些内容一些内容一些内容
    </div>
    </div>

    <style>
    .parent {
    border: 2px solid green;
    height: 600px;
    text-align: center;
    }
    .child {
    border: 2px solid pink;
    display: inline-block;
    width: 200px;
    vertical-align: middle;
    }
    .parent:before {
    content: '';
    outline: 1px solid blue;
    display: inline-block;
    height: 100%;
    vertical-align: middle;
    }
    .parent:after {
    content: '';
    outline: 1px solid blue;
    display: inline-block;
    height: 100%;
    vertical-align: middle;
    }
    </style>

css优先级

  • 选择器越具体,优先级越高
  • 相同优先级,出现在后面的覆盖前面的
  • 属性后加 !important 优先级最高,但要少用
  • 加权算法

清除浮动

  • 给父元素加.clearfix

    1
    2
    3
    4
    5
    .clearfix:after {
    content: " ";
    display: block;
    clear: both;
    }
  • 给父元素加 overflow: hidden;(触发BFC)


CSS盒模型

  1. content-box,width指定的是content(仅内容)区域宽度,而非实际宽度。
  • 实际宽度 = width + padding + border
  1. border-box,width指定的是左右边框外侧距离。
  • 实际宽度 = width(包含了padding和border)
  1. 通常来说设置box-sizing: border-box更好用,这样能将border和padding包含进元素的宽度中,例如设置元素宽度300px,那就是300px,里面再怎么折腾都可以

vue模板、指令与修饰符

Posted on 2022-05-09
| Words count in article: 1,615

Vue模板语法

  1. Vue完整版,写在HTML里
    1
    2
    3
    4
    <div id="xxx">
    {{n}}
    <button @click="add">+1</button>
    </div>
1
2
3
4
5
6
7
8
9
10
11
new Vue({
el: '#xxx',
data: {
n: 0
},
methods: {
add(){
this.n += 1
}
}
})
  1. Vue完整版,写在选项里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    <div id="app"></div>

    new Vue({
    data: {
    n: 0
    },
    template: `
    <div>
    {{ n }}
    <button @click="add">+1</button>
    </div>
    `,
    methods: {
    add(){
    this.n += 1
    }
    }
    }).$mount('#app')
  2. Vue非完整版,配合xxx.vue文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    -- .vue文件 --
    <template> // template标签里不是html而是xml(xml必须是闭合标签)
    <div>
    {{n}}
    <button @click="add">+1</button>
    </div>
    </template>

    <script>
    export default {
    data(){ <!--- vue组件里的data必须是函数 -->
    return {n: 0}
    },
    methods: {
    add() {
    this.n += 1
    }
    }
    }
    </script>
1
2
3
4
5
6
-- main.js文件 --
import Xxx from './xxx.vue' // Xxx是一个导出的options对象,一般规定首字母大写

new Vue({
render: h => h(Xxx)
}).$mount('#app')

模板里有哪些语法

我们把html模板叫做template

一、展示内容:

1. 表达式

  • 该表达式意为 把构造选项里data的 object.a 显示到HTML里
  • NaN 可以写任何运算
  • 可以调用函数,会在methods里找
  • 若值为 null、undefined 就不显示

2. html内容

  • 假设 data.x 值为 hi ,默认显示的时候就是 hi
  • 若想展示粗体的 hi ,使用

3. 单纯想展示

1
<div v-pre> {{ n }} </div>
  • v-pre 不会对模板进行编译,就只是单纯展示字符

二、绑定属性:

1
2
3
4
<!--绑定src-->
<img v-bind:src="xxx" />
<!--缩写-->
<img :src="xxx" />
1
2
<!--绑定对象-->
<div :style="{ border: '1px solid red', height: 100}"></div>

三、绑定事件:

v-on:事件名

1
2
3
4
5
6
<button v-on:click="add">+1</button>  <!-- 点击后 运行add()-->
<button v-on:click="xxx(1)">xxx</button> <!-- 点击后运行xxx(1)-->
<button v-on:click="n+=1">yyy</button> <!--- 点击后运行 n+=1 -->
<!-- 即发现函数就加括号调用它,否则将直接运行代码 -->
<!--缩写-->
<button @click="add">+1</button>

四、条件判断:什么时候出现在DOM数中

if…else

1
2
3
<div v-if="x>0"> 若x大于0,显示这个div </div>
<div v-else-if="x === 0"> x为0 </div>
<div v-else> x小于0 </div>

for循环

for(value,key) in 数组或对象

1
2
3
4
5
6
7
8
9
10
11
12
<!--数组例子-->
<ul>
<li v-for="(u,index) in users" :key="index"></li>
<!-- 将users里的每一项以u和index代替,u表示数组里的值value,index表示下标
使用时: 索引: {{ index }}、值: {{ u.name }} -->
</ul>

<!-- 对象例子 -->
<ul>
<li v-for="(value,name) in obj" :key="name"></li>
<!-- 一般来说使用了v-for,就必须有一个:key="",且key须用一个不会重复的值 -->
</ul>

显示、隐藏:

1
2
3
<div v-show="n%2 === 0"> n为偶数 </div>
<!-- 若v-show里的条件满足则展示这个div,下面与上面的写法等价 -->
<div :style="{ display: n%2 === 0 ? 'block' : 'none' }"> n为偶数 </div>

指令Directive

什么是指令

1
2
3
<!--以 v- 开头的就是指令-->
<div v-text="x"></div>
<div v-html="x"></div>

语法

  • v-指令名:参数=值,如 v-on:click=’add’
  • 如果值里没有特殊字符,则可以不加引号
  • 有些指令没有参数和值,如 v-pre
  • 有些指令没有值,如 v-on:click.prevent 阻止默认事件
    google

修饰符

一些指令支持修饰符

  • @click.stop=”add” 阻止事件传播/冒泡
  • @click.prevent=”add” 阻止默认动作
  • @click.stop.prevent=”add” 既阻止事件传播,也阻止默认动作
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    new Vue({
    data:{
    n:0
    },
    template:`
    <div>
    {{n}}
    <a @click.prevent="x" href="https://google.com">google</a>
    <input @input="y"/>
    <input @keypress="z" />
    <button @click=add>+1</button>
    </div>
    `,
    methods:{
    add(){
    this.n += 1
    },
    x(){
    console.log('xxx')
    },
    y(e){
    console.log(e.target.value)
    },
    z(e){
    if(e.keyCode === 13)
    console.log('用户打了回车')
    }
    }
    }).$mount("#app")

上述例子用修饰符的简单写法

1
2
3
4
5
6
7
8
9
10
11
12
new Vue({
template:`
<input @keypress.13 === "y" />
<input @keypress.enter === "y" />
`,
methods:{
y(e){
console.log('用户打了回车')
}
}
// keyCode很难记,比如 13是回车,1是49,所以使用 alias别名,如 @keypress.enter
})

具体可参考官方文档:

https://cn.vuejs.org/v2/guide/events.html?#%E6%8C%89%E9%94%AE%E4%BF%AE%E9%A5%B0%E7%AC%A6

有多少修饰符呢?

  • v-on 支持的有 .{keyCode | keyAlias} .stop .prevent .capture(在捕获阶段监听) .self .once(只会触发一次的事件) .passive .native
  • 快捷键相关 .ctrl .alt .shift .meta .exact
  • 鼠标相关 .left .right .middle
  • v-bind 支持的有 .prop .camel .sync
  • v-model 支持的有 .lazy .number .trim

Vue需要重点了解的修饰符

@click.stop=”xxx” 阻止事件冒泡

  • 由于冒泡机制,给元素绑定点击事件的时候,也会触发父级元素的绑定事件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <div @click="shout(2)">
    <button @click="shout(1)">ok</button>
    </div>

    shout(e){
    console.log(e)
    }

    // 1
    // 2
  • @click.stop 可以阻止事件冒泡,相当于调用了 event.stopPropagation()

    1
    2
    3
    4
    <div @click="shout(2)">
    <button @click.stop="shout(1)">ok</button>
    </div>
    // 只输出1

@click.prevent=”xxx” 阻止默认事件

  • @click.prevent用于阻止默认事件,相当于写 event.preventDefault()
    1
    2
    <a @click.prevent href="https://google.com">google</a>
    // 点击链接不会跳转

.sync 修饰符

场景描述:

  • 爸爸给儿子钱,儿子要花钱怎么办? 儿子打电话(触发事件),向爸爸要钱

Vue规则:

  • 组件不能修改 props 外部数据
  • this.$emit 可以触发事件,并传参
  • $event 可以获取 $emit 传来的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 父组件 App.vue
<template>
<div class="app">
App.vue 我现在有 {{total}}
<hr>
<Child :money="total" v-on:update:money="total = $event"/>
// 等价于 <Child :money.sync="total" />
</div>
</template>

<script>
import Child from './Child.vue'
export default {
data(){
return {total:10000}
},
components: {Child}
};
</script>

<style>
.app{
border: 3px solid red;
padding: 10px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 子组件Child.vue
<template>
<div class="child">
{{money}}
<button @click="$emit('update:money',money - 100)">
<span>花钱</span>
</button>
</div>
</template>

<script>
export default {
props:["money"]
}
</script>

<style>
.child {
border:3px solid green
}
</style>
1
2
3
4
5
6
7
8
<button @click="$emit('update:money', money - 100)"></button>
第一步: 儿子用 $emit 触发'update:money'事件,并传入参数 'money - 100', 告诉父亲钱还剩 money - 100

<Child :money="total" v-on:update:money="total = $event"/>
第二步: 父亲用 v-on:'update:money' 绑定事件,监听 update:money, 并用 $event 获取参数

由于这样的代码场景非常常见,为了简化,便有了语法糖 .sync
<Child :money.sync="total" /> 等价于 <Child :money="total" v-on:update:money="total = $event"/>

vue中的computed和watch

Posted on 2022-05-05
| Words count in article: 1,475

Computed–计算属性

  • 被计算出来的就是计算属性

用户名重复展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 用户名多处展示,使用computed计算属性
new Vue({
data: {
user: {
nickname: "小马",
phone: "18988888888",
email: "593205312@qq.com"
}
},
computed: {
displayName: {
get(){
const user = this.user
return user.nickname || user.phone || user.email
},
set(value){
this.user.nickname = value
}
}
},
template: `
<div>
{{ displayName }}
<div>
{{ displayName }}
<button @click="add">set name</button>
</div>
</div>
`,
methods: {
add({
this.displayName = '小芳'
})
}
}).$mount("#app")

列表展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
let id = 0
const createUser = (name,gender) => {
id += 1
return (id, name, gender)
}

new Vue({
data(){
return{
users: [
createUser("方方", "男")
createUser("小圆", "女")
createUser("小新", "男")
createUser("小葵", "女")
],
gender: '' // 用户选择的 gender 默认为空
}
},
computed: {
displayUsers(){
console.log('displayUsers 计算了一次') // 如果依赖的属性未发生变化,就不会重新计算

const hash = {
male: '男',
female: '女'
}

const {users, gender} = this;
if(gender === ''){
return users
} else if (typeOf gender === 'string') { // 也可写作 (gender === 'male' || gender === 'female')
return users.filter(u => u.gender === hash[gender])
} else {
throw new Error ('gender 的值是意外的值')
}
}
},
methods: {
setGender(string) {
this.gender = string
}
},
template: `
<div>
<div>
<button @click="setGender('')">全部</button>
<button @click="setGender('male')">男</button>
<button @click="setGender('female')">女</button>
</div>
<ul>
<li v-for="(u,index) in displayUsers" :key="index">
{{ u.name }} - {{ u.gender }}
</li>
</ul>
</div>
`
})

缓存

  1. 如果computed依赖的属性没有变化,就不会重新计算
  2. getter/setter 默认不会做缓存,Vue做了特殊处理

Watch监听器

  • 当watch监听的数据发生变化时,执行一个函数

撤销操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
new Vue({
data: {
n: 0,
history: [],
inUndoMode: false // 初始化撤销模式
},
watch: {
n(newValue, oldValue){
console.log(`在不在撤销模式:${this.inUndoMode ? '在':'不在'}`)

if(!this.inUndoMode){
this.history.push({from: oldValue, to: newValue})
}
}
},
template: `
<div>
{{ n }}
<hr/>
<button @click="add1">+1</button>
<button @click="add2">+2</button>
<button @click="minus1">-1</button>
<button @click="minus2">-2</button>
<hr/>
{{ history }}
</div>
`,
methods: {
add1() {
this.n += 1
},
add2(){
this.n += 2
},
minus1() {
this.n -= 1
},
minus2() {
this.n -= 2
},
undo() {
const last = this.history.pop()
console.log(last)

const old = last.from
this.inUndoMode = true
this.n = old // watch是异步的
this.$nextTick(()=> { // 使用 this.$nextTick() 使得该操作在watch的异步操作之后
this.inUndoMode = false
})
}
}
}).$mount("#app")

使用watch模拟computed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
new Vue({
data:{
user: {
email: 'xiaofang@gmail.com',
nickname: '小芳',
phone: '13867676767'
},
displayName: ''
},
watch: {
'user.email'(){
const {user: {email,nickname,phone}} = this
this.displayName = nickname || email || phone
},
'user.nickname'(){
const {user: {email,nickname,phone}} = this
this.displayName = nickname || email || phone
},
'user.phone'(){
const {user: {email,nickname,phone}} = this
this.displayName = nickname || email || phone
}
},
template: `
<div>
{{ displayName }}
<button @click="user.nickname=undefined">remove nickname</button>
</div>
`,
methods: {}
}).$mount("#app")

// 运行代码,displayName并未展示,这是因为watch存在默认选项,也就是第一次设置的值是不监听的,因为第一次赋值是从无到有
// 当点击 remove nickname 时,nickname的值被删除了,watch就监听到了变化

解决watch默认选项问题 immediate: true,第一次的值也要监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
new Vue({
data:{
user: {
email: 'xiaofang@gmail.com',
nickname: '小芳',
phone: '13867676767'
},
displayName: ''
},
watch: { // watch默认选项,第一次的值是不监听的
'user.email': {
handler(){
console.log('email 变了')
const {user: {email,nickname,phone}} = this
this.displayName = nickname || email || phone
},
immediate: true // 第一次渲染也触发 watch
},
'user.nickname': {
handler() {
console.log('nickname 变了')
const {user: {email, nickname, phone}} = this
this.displayName = nickname || email || phone
},
immediate: true
},
'user.phone': {
handler() {
console.log('phone 变了')
const {user: {email, nickname, phone}} = this
this.displayName = nickname || email || phone
},
immediate: true
}
},
template: `
<div>
{{ displayName }}
<button @click="user.nickname=undefined">remove nickname</button>
</div>
`,
methods: {}
}).$mount("#app")

什么是数据变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
new Vue({
data: {
n: 0,
obj: {
a: 'a'
}
},
template: `
<div>
<button @click="n+=1">n+1</button>
<button @click=" obj.a += 'hi' ">obj.a + 'hi'</button>
<button @click="obj = {a: 'a'}">obj = 新对象</button>
</div>
`,
watch: {
n(){
console.log('n 变了')
},
obj(){
console.log('obj 变了')
},
'obj.a'() {
console.log('obj.a 变了')
}
}
}).$mount("#app")
  • obj原本是 {a: ‘a’},后来变为 obj= {a: ‘a’}
  • obj变了吗?变了,因为对象的地址变了;obj.a变了吗?没有
  • 所以简单类型看值,复杂类型(对象)看地址

watch监听更深层的值 deep: true

  • deep: true的意思是,watch监听时,是否往更深层去监听变化
  • 数据变化时,我们一般只比较obj的地址,但在Vue可以做到不仅仅比较地址,还比较里面的数据变化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    watch: {
    n(){
    console.log('n 变了')
    },
    obj:{
    handler() {
    console.log('obj 变了')
    },
    deep: true // 深比较,虽然对象地址没变,但obj里属性的值发生变化,也算obj变了
    }
    }

watch的语法

  • watch的几种写法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 写法1
    watch: {
    o1: ()=>{}, // 不要使用箭头函数,因为这里的this是全局对象
    o2: function(value, oldValue){},
    o3: (){},
    o4: [f1,f2],
    o5: 'methodName',
    o6: {
    handler: fn,
    deep: true,
    immediate: true,
    'obj.a': function(){}
    },
    }

    // 写法2
    vm.$watch('n', function(){
    console,log('n 变了')
    }), {immediate: true}


    // 写法3:放在钩子里
    created(){
    this.$watch('n', function(){
    console.log('n 变了')
    }, {immediate: true})
    }

computed VS. watch

  1. computed 用来计算出一个值,这个值在调用时不需要加括号,可以当属性用;并且会根据依赖自动缓存,如果依赖不变,computed的值就不会重新计算
  2. watch 用来监听,若某个属性发生变化,就去执行一个函数。它有两个选项:immediate表示第一次渲染时,是否执行这个函数;deep表示监听一个对象时,是否要监听对象里面的变化

vue数据响应式

Posted on 2022-04-29
| Words count in article: 2,050

问题引入:Vue对data做了什么

data值变了,例子中 myData 在传入前和传入后,两次log出的结果不一样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const myData = {
n: 0
}
console.log(myData) // 第一次log出的结果为 { n: 0 }

const vm = new Vue ({
data: myData,
template: `
<div>
{{ n }}
<button @click="add"> +10 </button>
</div>
`,
methods: {
add() {
this.n += 10
}
}
}).$mount("#app")

setTimeout(() => {
myData.n += 10
console.log(myData) // 第二次log出的结果为 { n:(...) }
}, 3000)

为什么两次log出的myData会不一致?需要先了解一下ES6中的getter和setter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let obj0 = {
姓: "高",
名: "圆圆",
age: 18
}

// 需求一:拿到姓名
let obj1 = {
姓: "高",
名: "圆圆",
姓名() {
return this.姓 + this.名;
},
age: 18
};

console.log(obj1.姓名()); // 姓名() 是一个函数,后面的括号不能省略

// 需求二:函数后面的括号如何去掉
let obj2 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名
},
age: 18
};

console.log(obj2.姓名); // 在函数名前加上get,在调用时函数可以不加括号

// 需求三:函数可以被“写”
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名() {
return this.姓 + this.名;
},
set 姓名(xxx) {
this.姓 = xxx[0]
this.名 = xxx.slice(1)
},
age: 18
}
obj3.姓名 = "高媛媛" // set的用法

console.log(`姓 ${obj3.姓},名 ${obj3.名}`)
console.log(obj3)

  • 虽然在声明obj3时并未声明 姓名 这样的属性,但你确实可以对它进行读、写的操作,打印出的结果为 姓名: (…)
  • n: (…) 的意思就是并不存在n这样的属性,但是它身上有一个 get n 和 set n 来模拟对n的读写操作

Object.defineProperty()

  • 定义完一个对象后,想要在它身上再添加额外的getter和setter时使用Object.defineProperty()
  • 可以给对象添加属性value,也可以添加getter、setter
  • getter、setter用于对属性的读写进行监控
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let _xxx = 0  // 声明一个容器,用来存放 xxx 的值

    Object.defineProperty(obj1, 'xxx', {
    get() {
    return _xxx
    },
    set(value) {
    _xxx = value
    }
    }) // 给obj1增加了一个名为 xxx 的属性,值为0

使用Object.defineProperty()完成几个需求

需求一:用Object.defineProperty()定义n的值

1
2
3
4
5
6
7
8
9
let data0 = {
n: 0
}

let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
console.log(`需求1:${data1.n}`)

需求二:n不能小于0,即data2.n = -1 无效,data2.n = 1有效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let data2 = {}

data2._n = 0 // 用来存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
console.log(`需求2:${data2.n}`)

data2.n = -1
console.log(`需求2:${data2.n} 设置为-1 失败`)

data2.n = 1
console.log(`需求2:${data2.n} 设置为 1 成功`)

// 问题:虽然这样可以满足需求,但如果有人直接修改data2._n的值怎么办?

使用代理,解决上述问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let data3 = proxy({  // 括号里是匿名对象,无法访问
data: {n:0}
})

function proxy({data}){ /* 解构赋值 */
const obj = {}
Object.defineProperty(obj,'n', {
get(){
return data.n
},
set(value) {
if(value < 0) return
data.n = value
}
})
return obj // obj就是代理:对obj做什么,就会同样作用在 data3 身上
}

// data3 就是 obj
console.log(`需求3: ${data3.n}`);

data3.n = -1
console.log(`需求3 : ${data3.n} 设置为-1 失败`);

data3.n = 1
console.log(`需求3 : ${data3.n} 设置为1 成功`);

需求四:解决绕过代理的问题,就算有人修改myData,也要拦截他

下面是可以被用户修改的myData

1
2
3
4
5
6
let myData = { n:0 }  // 声明一个引用,这样就可以绕过代理,直接修改myData
let data4 = proxy({ data: myData }) // 括号里是匿名对象,无法访问

console.log(`杠精:${data4.n}`)
myData.n = -1 // 绕过代理,成功修改了myData
console.log(`杠精:${data4.n}, 设置为-1成功了`)

解决绕过代理的问题,不要让用户擅自修改myData

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 })

function proxy2({data}) {
let value = data.n
// 省略了delete.data.n,使用Object.defineProperty时会覆盖掉
Object.defineProperty(data, 'n', {
get() {
return value
},
set(newValue){
if(newValue < 0) return
value = newValue
}
})
} // 上面几句,可以监听 data

const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})

return obj // 用 obj 对 data 进行代理
console.log(`需求4:${data4.n}`)

myData5.n = -1
console.log(`需求4:${data4.n} 设置为 -1 失败`)

myData5.n = 2
console.log(`需求4:${data4.n} 设置为 1 成功`)

什么是代理

  • 对 myData 对象的属性进行读写,全权由另一个对象vm来负责
  • 那么,vm 就是 myData 的代理
  • 不用 myData.n 偏偏用 this.n 或 vm.n

    new Vue做了什么

    1
    2
    3
    // 看着很相似的两行代码
    let data4 = proxy({data: myData4})
    let vm = new Vue({data: myData})
  • 会让vm成为myData的代理,可以通过 this.myData 访问到 myData

  • 会监听 myData 的所有属性(赋值给value,再通过getter、setter添加虚拟属性)
  • 监听的目的是防止 myData 属性改变,vm不知道
  • 当vm监测到myData发生改变,就调用render(data),刷新视图

Vue的数据响应式

Vue 的 data 是响应式的

  • const vm = new Vue({data: {n:0}})
  • 如果修改 vm.n ,那么UI中的n就会进行响应
  • Vue是通过 Object.defineProperty 来实现数据响应式的

    Vue 的 data 的bug

    问题:如果一开始在data中并未对 b 进行赋值,Vue就不能对它进行代理或监听
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    new Vue({
    data: {
    obj: {
    a: 0 // obj.a 会被Vue监听和代理
    }
    },
    template: `
    <div>
    {{ obj.b }}
    <button @click="setB">set b</button>
    <button @click="addB">b+1</button>
    </div>
    `,
    methods: {
    setB(){
    this.obj.b = 1 // 这种写法页面中不会显示b的值,因为Vue没办法监听一开始就未设置在data中的obj.b

    // 解决方法,使用Vue.set 或 this.$set
    console.log(Vue.set === this.$set) // 下面两种写法等价
    Vue.set(this.obj, 'b', 1)
    this.$set(this.obj, 'b', 1)
    },
    addB() {
    this.obj.b += 1
    }
    }
    }).$mount("#app")

解决方法:使用 Vue.set 或 this.$set,作用是:

  • 新增key
  • 自动创建监听和代理
  • 触发ui进行更新(但并不是立即更新)

eg:this.$set(this.object, ‘m’, 10)

如果data是一个数组呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
new Vue({
data: {
array: ['a', 'b', 'c'] // array: {0:'a', 1:'b', 2:'c'}
},
template: `
<div>
{{array}}
<button @click="setD">setD</button>
</div>
`,
methods: {
setD() { // 3种 setD 的方法
this.array[3] = 'd' // 页面中不会出现'd' Vue不能监测到新增的d
this.$set(this.array, 3, 'd') // 可以用$set的方法,但每次都用set会不会很麻烦
this.array.push('d') // 这个push方法被尤雨溪篡改了,有两个作用:一是调用以前的push方法,二是增加代理和监听
}
}
})

当你将一个数组传给Vue的data,Vue会对这个数组进行篡改,它会在原来数组对象中间增加一层原型,这个原型上有7个方法

来看看push是如何被篡改的?

1
2
3
4
5
6
7
8
9
10
class VueArray extends Array {
push(...args) {
const oldLength = this.length // this就是当前数组
super.push(...args)
console.log('你 push 了')
for(let i = oldLength; i<this.length; i++){ // i为所有push的元素的下标,遍历旧数组到新数组的下标
Vue.set(this, i, this[i]) // 在当前数组this上,添加i项,每项i的值为this[i],并将每个新增的key通过Vue.set告知给Vue
}
}
}

总结

对象中新增的key

  • Vue没办法事先监听和代理
  • 要使用 Vue.set / this.$set 来新增key,创建监听和代理、更新ui
  • 最好的方式是将属性都写出来,不要新增key。但数组没办法做到不新增key

    数组中新增的key

  • 可以用 Vue.set / this.$set 来新增key,创建监听和代理、更新ui
  • 但尤玉溪篡改了数组的7个API方便对数组进行增删
  • 这7个API会自动处理监听和代理、更新ui
12…6
Joyce

Joyce

53 posts
14 tags
RSS
Links
  • 网易云音乐
© 2018 — 2022 Joyce
Theme — NexT.Mist v5.1.4
0%