Redux官方示例:
1 | import { createStore } from 'redux' |
createStore
简易实现:
1 | const randomString = () => |
上述是createStore
的简单实现,调用createStore()
函数以后返回一个store
对象,该对象有4个方法,如下:
1:dispatch
:分发action,通过currentReducer(currentState, action)
来生成新的state,并触发事件。
2: subscribe
: 监听事件,实际上就是把事件添加到事件数组中,并返回移除事件函数。
3: getState
:获取当前的状态。
4: replaceReducer
:替换reducer。
最新的源码与我们的实现理念大致相同,只是多了类型的校验,另外事件采用双map形式(防止dispatch中调用subscribe/unsubscribe)而不是我们简单的数组,最后在事件触发时会使用变量标记,防止在分发过程中出现不合理的操作。
写一个redux中间件很简单,比如写一个打印日志的中间件。
1 | const logger = function(store) { |
中间件是一个嵌套三层的函数,每一层都有一个参数,参数分别是store
、next
、action
。上面是redux-logger
中间件的简单实现,常用的中间件还有redux-thunk
,核心代码如下:
1 | const thunk = ({ dispatch, getState }) => next => action => { |
redux-thunk
的逻辑也很简单,通过对store
解构获取dispatch
和getState
函数,如果action
是函数则调用action
,否则调用next(action)
进行下一个中间件。在action
函数中可以通过dispatch
来触发action
,哪怕是在异步的回调中,所以redux-thunk
通常用来处理异步操作。
1 | import { createStore, applyMiddleware } from 'redux' |
需要注意的是applyMiddleware
是有顺序的,它会从做左到右依次执行,next后是从右到左执行。如果上述示例修改为applyMiddleware(logger, thunk)
会发生什么事呢?那么在调用store.dispatch(() => {})
的时候也会打印日志,里面的dispatch
又会打印一次。
由上createStore
函数可知,当传入中间件的时候会通过enhancer
来生成store。enhancer
实际上就是applyMiddleware(logger, thunk)
的结果,它是一个两层函数,第一层接受的参数是createStore
第二次接受的参数是reducer
和preloadedState
,代码大致如下:
1 | function compose(...funcs) { |
首先通过createStore(reducer, preloadedState)
不传中间件来创建store
,applyMiddleware
内层函数的返回值只有dispatch
是处理过的函数,其他的都是与store
中的一致,也就是说中间件的作用实际上是强化dispatch
函数。middlewareAPI
实际上就是中间件的第一层函数的参数store
,这里需要注意的是dispatch
调用的时候,下面的代码已经走完了,所以里面的dispatch
函数是加强后的dispatch
而不是上面定义的抛出异常的函数。通过middlewares.map(middleware => middleware(middlewareAPI))
去掉了中间件第一层函数。compose
核心逻辑是funcs.reduce((a, b) => (...args) => a(b(...args)))
对于函数数组返回嵌套执行的组合函数,compose(...chain)(store.dispatch)
最后参数是store.dispatch
,比如有两个中间件a
和b
,这里相当于变成a(b(store.dispatch))
,相当于a这一层的next
函数是b(store.dispatch)
函数。
Koa的核心代码只有4个文件,如图。
各个文件的作用:
application.js
:Koa的核心,对应Koa App类。context.js
:对应上下文对象ctx。request.js
:对应ctx.request对象。response.js
:对应ctx.response对象。
Koa使用如下:
1 | const Koa = require('koa'); |
Koa底层是基于原生http模块,原生http模块怎么启动一个服务呢?如下:
1 | const http = require('http'); |
观察上面的代码,两者是不是挺像的。
为了方便查看application的核心逻辑,下面是我去掉了部分非核心代码的application源码:
1 | const onFinished = require('on-finished') |
当调用app.use
的时候,实际上是把中间件函数加入到this.middleware
数组当中。
当调用app.listen
的时候,通过http.createServer
来创建http服务并使用server.listen
来监听服务。
这里比较难理解的是callback
函数,它使用compose
将中间件合并成一个调用函数,具体怎么合并的我们稍后再说。如果error
事件没有监听的话,添加一个默认的监听函数,默认的onerror
函数实际上就是打印错误信息;this.listenerCount
是从哪里来的呢?实际上Application
类是继承自node中的Emitter
,该方法也是Emitter的方法。最后返回了一个handleRequest
函数,该函数做了2件事,首先通过req
和res
构建ctx
,然后调用this.handleRequest
,注意this.handleRequest
是Application
类的属性而不是callback
中的handleRequest
,也就是这里并没有递归调用。
在this.handleRequest
函数中调用了中间件函数fnMiddleware(ctx)
,当中间件函数都调用完了以后调用respond(ctx)
,respond
通过不同的情况去处理res的结果;失败的时候调用ctx.onerror(err)
。另外在中间件处理之前会调用onFinished(res, onerror)
来监听出错的情况,onFinished的代码请看这里。
在讲述源码之前我们先看看koa-compose
中间件是怎么使用的。
1 | const Koa = require('koa'); |
客户端打印:
1 | 第1个中间件开始 |
这就是Koa中间件著名的洋葱模型。
我们先不谈Koa只看看koa-compose
做了什么事。
1 | const compose = require('koa-compose'); |
上面打印:
1 | 第1个中间件开始 |
koa-compose
把多个中间件合并成一个函数,通过await next()
来调用下一个中间件,其源码如下:
1 | function compose (middleware) { |
首先对middleware
做类型检查,middleware
必须是数组,同时每一个中间件必须是函数。然后返回一个函数,这个函数第一个参数是上下文对象,第二个参数是下个中间件执行的next函数。核心逻辑是上面的dispatch
方法,在dispatch
方法中会返回Promise。dispatch
方法实际上就是next方法,首次会调用dispatch(0)
来触发第一个中间件函数。当一个中间件中调用next方法后会把index
标记为当前的索引,如果一个中间件多次调用next
方法,那么由于第一次调用是index
会标记为i
,那么第二次调用的时候i
和index
是相等的,也就是第二次的时候会走if (i <= index) return Promise.reject(new Error('next() called multiple times'))
逻辑,也就是会报错。每次调用的时候根据索引获取当前要执行的中间件函数,在第18行会执行当前中间件,并把下一个dispatch
当作第二个参数next
传入到下一个中间件中。当执行到最后一个中间件的时候,设置fn = next
由于Application代码的第52行并没有传递第二个参数,所以此时next
是undefined
,那么compose
中将会走第16行if (!fn) return Promise.resolve()
的逻辑。如果传递了函数那么会执行传入的函数,当此函数中调用next以后,由于索引已经超过了middleware
的长度,所以下次函数执行事也会走第16行的逻辑。
context
是对上下文对象的封装,具体代码如下:
1 | const util = require('util') |
可见context
实际上就是一个对象,它对Cookie
和onerror
做了一个封装。最后使用delegate()
来代理request
和response
对象,delegate
不了解的同学可以看下面这个示例。
1 | const delegate = require('delegates'); |
上面代理了obj
对象的aaa
属性,所以直接可以通过obj
来访问aaa
中代理的属性和方法,其中method
表示代理方法,getter
表示代理get方法,setter
表示代理set方法,access
表示不但代理了get同时也代理了set。delegates的实现也不难:
1 | function Delegator(proto, target) { |
request
与response
就是一个简单的对象,没什么好说的,比如request
代码大致如下:
1 | module.exports = { |
这里需要注意的是有一个this.req
对象,这个对象是从哪里来的?请看Application
的createContext
方法的第61行,在这里把node的req
挂载了上来,res
同理。
back()
、forward()
、go()
三个方法,H5新增了pushState()
和replaceState()
用来无刷新页面来更新URL地址,本章所说的H5 history API也指的是这两个方法。H5 history API浏览器兼容情况请看这里。
PS:虽然H5已经不是什么新东西了,但学学总没害处。
pushState根据API的意思是向浏览器的历史栈中添加一个状态,这个函数本来是用来添加状态的,而附加能力是修改URL,所以第一个参数是状态,最后一个参数才是URL,URL是可选的,如果不写URL则URL不会变,但是仍然会在历史栈中添加一条数据,点击浏览器的回退按钮会出栈这条历史信息,相当于页面回到原来的状态,页面内容并没有变化。其签名如下:
1 | history.pushState(state, title[, url]) |
pushState第一个参数是状态,这个状态state可以是可结构化拷贝的任意类型,传对象、传数字、传布尔都没问题。第二个是标题,但是浏览器压根不鸟它,你设置了也不会修改浏览器标题,所以我们一般传入null
就可以了。第三个是URL,一般是一个字符串,新版本浏览器对URL对象(new URL()
这种)也是支持的,但考虑到兼容性还是使用字符串比较好,需要注意的是URL必须是跟当前页面地址是同源的。
来看一个简单的例子:
1 | const state = { |
上述state是一个对象,调用pushState会在历史栈中增加一条信息,同时修改URL的pathName为/a
。如果把上述state的值修改为123
,那么调用pushState后history.state
就变成了123
。但是需要注意的是history.state !== state
,这里采用了结构化深拷贝,相当于调用了structuredClone(state)
,结构化深拷贝不仅可以拷贝基本类型,而且对象在存在循环引用也是可以拷贝成功的,比JSON.parse(JSON.stringify(state))
要更加安全。浏览器原生的structuredClone方法目前兼容性还不够好,其相关内容可以看这里。当然结构化深拷贝也不是万能的,对于dom节点,error对象、function函数来说也会拷贝失败,上述stete传这些类型的值时,会报错。
第三个参数URL又可分为这几种情况,比如当前路径为https://www.kai666666.com/2023/04/18/H5-history-API/?1=1#more
(这里为了方便查看search的情况,添加了一个1=1
的参数)。
URL情况 | 处理情况 | url值 | url处理结果 |
---|---|---|---|
URL全路径 | 整体替换 | https://www.kai666666.com/a | https://www.kai666666.com/a |
/ 开头 | 替换pathName部分 | /a | https://www.kai666666.com/a |
./ 开头 | 替换当前路径,也就是最后一位 | ./a | https://www.kai666666.com/2023/04/18/H5-history-API/a |
../ 开头 | 替换上一级 | ../a | https://www.kai666666.com/2023/04/18/a |
? 开头 | 替换search部分 | ?2=2 | https://www.kai666666.com/2023/04/18/H5-history-API/?2=2 |
# 开头 | 替换hash部分 | #hash | https://www.kai666666.com/2023/04/18/H5-history-API/?1=1#hash |
单词或数字 开头 | 替换当前路径部分,等价与./单词或数字 开头 | aaa | https://www.kai666666.com/2023/04/18/H5-history-API/aaa |
这里有几点需要注意一下。首先地址最后一位有没有/
会影响到当前路径和上一级路径的判断,如果有/
则认为当前路径是斜杠后面的路径,虽然后面是空的。举个例子URLhttps://www.kai666666.com/2023/04/18/H5-history-API/
和https://www.kai666666.com/2023/04/18/H5-history-API
调用history.pushState(state, null, './a');
得到的结果分别是https://www.kai666666.com/2023/04/18/H5-history-API/a
和https://www.kai666666.com/2023/04/18/a
。
第二点需要注意的是/
开头的路径将会把search和hash部分也替换掉;?
开头的路径会把hash部分也替换的,如果需要保留则需要手动添加对应的部分,如`/a${location.search}${location.hash}`。
当前路径与上一级路径也可以混用:
1 | history.pushState(null, null, './a/../b/c'); |
对了URL路径中只有./
或者../
可以省略后面的/
。
1 | history.pushState(null, null, '..'); |
最后如果URL中含有中文,调用location.pathname
得到的是转码后的内容。
1 | history.pushState(null, null, '/你好'); |
replaceState与pushState用法一模一样,区别是replaceState是把当前的历史栈替换了,而pushState是添加了一个历史栈,这样就导致replaceState点返回按钮会回到上一个历史栈中。replaceState的签名如下:
1 | history.replaceState(stateObj, title[, url]); |
HTML中base元素提供了基础的路径,如果设置了base,那么相对路径都是基于base元素中的路径来算出新的路径的。
1 | <base href="/base/aaa"> |
如添加如上base元素后,上述情况处理的结果如下:
URL情况 | url值 | url处理结果 |
---|---|---|
URL全路径 | https://www.kai666666.com/a | https://www.kai666666.com/a |
/ 开头 | /a | https://www.kai666666.com/a |
./ 开头 | ./a | https://www.kai666666.com/base/a |
../ 开头 | ../a | https://www.kai666666.com/a |
? 开头 | ?2=2 | https://www.kai666666.com/base/aaa?2=2 |
# 开头 | #hash | https://www.kai666666.com/base/aaa#hash |
单词或数字 开头 | bbb | https://www.kai666666.com/base/bbb |
popstate事件是浏览器历史栈返回或者前进的时候会触发,调用history.pushState()
和history.replaceState()
方法的时候并不会触发popstate事件,只有hash改变或者调用这两个函数后并点击浏览器的前进/后退或者使用JS API前进/后退(如调用history.back()
、history.go(-1)
或history.forward()
)的时候才会触发。
1 | window.addEventListener('popstate', (event) => { |
如果不修改hash或不调用者两个函数的时候,直接前进后退一般都是会刷新页面,也就不会触发事件回调函数。当然你也可以手动触发popstate事件。
1 | window.dispatchEvent(new PopStateEvent('popstate')); |
编写函数,使用随机数生成器估算π。
各种计算机语音中都会给出π的具体值,如JavaScript中就有Math.PI
,但是如何不使用该值来粗略估算π的值呢?
整体思路:如图所示,在坐标系(x, y)的作用域[-1,1]与值域[-1,1]中随机生成点,点在圆x² + y² = 1
内的概率为π / 4
,那么π可以由4 * 点在圆内的数量 / 所有点的数量
得到。实际上我们不需要计算整个圆,我们只要计算作用域[0,1]与值域[0,1]的部分,也就是第一象限,也就是π = 4 * 点在第一象限圆内的数量 / 第一象限所有点的数量
。
代码:
1 | function estimatePi(iterations = 100000000) { |
上述代码中,iterations越大耗时越长,结果越准确。
]]>编写一个函数,确定给定整数的二进制表示中各个1位的数目。
举例:给定一个数字是7,假设是8位操作系统,二进制表示为00000111
,其中有3个1,则调用函数返回3
。
整体思路:循环统计,检测二进制表示中的最后一位,如果最后一位是1的时候计数器加1,然后把数字右移一位,直到整个数字全部移完。
代码:
1 | function numOnesInBinary(number) { |
上述算法已经很不错了,不过还有可以优化的部分。
一个数的二进制跟这个数减1的二进制相比,前半部分是相同的,只是翻转了最低位的1以及之后的各个位。例如有个数的二进制位01110000
(十进制112),该值减去1以后的二进制是01101111
(十进制111),可以看到前三位是相同的,后面的位数是想反的。一个数的二进制跟这个数减1的二进制相与(&)会发生什么呢?实际上就是该二进制去掉最后一个1,如01110000
& 01101111
= 01100000
,01100000
实际上就是01110000
去掉最后一个1的结果。
有了上面的知识,我们可以稍微改造一下代码:
1 | function numOnesInBinary(number) { |
上述得出一个重要的结论number & (number - 1)的值就是number二进制去掉最后一个1的结果。利用这个结论我们还可以最很多事,比如有题目:
给你一个正整数 n,请你判断该正整数是否是 2 的幂次方。如果是,返回 true ;否则,返回 false。
比如,n=4的时候就返回ture,如果n=3的时候就返回false。
整体思路:由于一个正整数是2的幂次方,那么它的二进制一定是1后面好多0这种格式,比如4的二进制就是100
,8的二进制就是1000
。所以按照这个思路我们可以去掉最后一个1,如果结果是0的时候就说明这个正整数是2的幂次方。
1 | function isPowerOfTwo(n) { |
1 | <script type="text/JavaScript"> |
1 | <script type="text/JavaScript"> |
2.常用DOM操作:
1 | document.getElementById(elementId); // 根据id来获取DOM元素 |
3.不同的设置属性方式:
1 | // 给input设置值 |
4.打开一个打开一个320px * 480px的小窗口:
1 | window.open(url, "popup", "width=320,height=480"); |
5.实现一个addLoadEvent函数,支持添加多个window.onload函数:
1 | function addLoadEvent(func) { |
6.实现一个insertAfter函数支持把元素追加到某个元素之后:
1 | function insertAfter(newElement, targetElement) { |
7.实现一个addClass函数给指定节点追加类名:
1 | function addClass(element, value) { |
8.实现一个moveElement动画函数,可以把指定元素移动到目标位置:
1 | function moveElement(elementID, final_x, final_y, interval) { |
9.使用JavaScript实现把指定图片设置为黑白图片,鼠标经过时候图片变成彩色的效果:
1 | function convertToGS(img) { |
Web性能主要指网站的加载速度。你可以通过提高网站速度来加快内容的传输,从而改善用户的体验。电子商务网站上近一半的用户希望在2秒内完成。如果加载时间超过3秒,40%的用户将会退出。页面相应时间每延迟1秒就意味着7%的用户不再进一步操作。
加载时间是用户请求网站到网站出现在用户屏幕上所经历的时间。
本节从减少传输的数据量入手,简单的简绍了3中提高性能的方法:缩小资源、使用服务器压缩、压缩图像。
缩小(minification)文本资源是从基于文本的资源中去除所有空白和非必要字符的过程,因而不会影响资源的工作方式。
下面命令-o
表示输入的文件路径,通过使用下面命令缩小资源后 CSS文件缩小了14%,JS文件缩小了66%,HTML缩小了19%,缩小的还是挺可观的。
1 | 缩小CSS |
服务器压缩的工作方式是用户从服务器请求网页,用户的请求会附带一个Accept-Encoding
的头信息,向服务器告知浏览器可以使用的压缩格式。如果服务器按照Accept-Encoding
头信息中的内容进行编码,它将用一个Content-Encoding
响应头信息进行回复,其值是所使用的压缩方式。gzip
是比较常用的一种压缩格式,express服务配置gzip
如下。
1 | var express = require("express"); |
通过上述两行代码,压缩后资源缩小了66%。
压缩图像书中简绍了使用常用的TinyPNG去压缩,大小缩小了60%左右。
通过这三种方式,网站的加载速度提高了近70%,还是非常可观的。
第一个评估工具就是Google PageSpeed Insights,去该网站输入你要分析的网站,它会给你一些优化的建议,当然网页得是已经在线上跑的网页,另外国内需要翻墙。
第二个评估工具是Google Analytics,这个工具比较全面,但是需要在网页中注入JS脚本,如果是大公司的开发者,注入Google的代码往往需要走法律审查,因为安装跟踪代码时,需要接受法律协议条款。
首字节时间(Time to First Byte,TTFB):从用户请求网页到相应第一个字节到达之间的时间。首字节时间往往跟队列请求、DNS查找、连接设置和SSL握手等有关。
页面创建过程:解析HTML以创建DOM -> 解析CSS以创建CSSOM -> 布局元素 -> 绘制页面。
移动优先响应式设计:默认样式为移动设备定义,并且随着屏幕宽度的增大而增加复杂性。
桌面优先响应式设计:默认样式为桌面设备定义,并且随着屏幕宽度的减小而降低复杂性。
通常响应式设计中使用移动优先的响应式设计会更好一点,主要的原因有:
1.通常移动设备的处理能力和内存通常低于桌面设备,使用移动优先不需要解析媒体查询。
2.从开发角度出发,扩大样式规模更容易实现。
3.手机用户量激增,搜索引擎对移动设备逐渐更友好。
CSS中可以使用@import来引入一个样式,使用方式如下:
1 | @import url(fonts.css); |
最好不要使用@import
来引入一个样式,因为@import
是串行的,会增加页面的总体加载和渲染时间。可以使用<link>
标签来代替,因为<link>
标签是并行的。
Less中的@import
最终编译到css中的并不是CSS语法中的@import
,所以可以使用。
在<head>
标签中放置CSS要比在<body>
标签中放置CSS有两个好处:
如果CSS放在<body>
标签中,如果放在页面HTML结构的下方那么就会先渲染一个没有自定义样式的页面,等加载完CSS以后才会有自定义样式,所以会有无样式内容闪烁的问题。
放在<body>
中还有一个问题是页面加载完<body>
中的样式以后会重新渲染和绘制整个DOM,页面渲染性能较差。
CSS过渡的优点:
如果动画可以使用CSS过渡来实现的话,最好使用CSS过渡而不是JS来改变DOM(减少回流)。
will-change使用方法:
1 | will-change: 属性1, [属性2]... |
will-change可以告诉浏览器哪个属性将会过渡,但是不要使用will-change: all;
。
其他的优化点:
关键CSS,即折叠之上的内容,这些是用户会立即看到的内容,需要尽快加载。
非关键CSS,即折叠之下的内容,这些是用户开始向下滚动页面之前看不到的内容样式,这种CSS也应该尽快加载,但不能在关键CSS之前加载。
书中的折叠是指屏幕的底部,实际上关键CSS就是首屏样式,非关键CSS就是非首屏的样式。
渲染阻塞指的是阻止浏览器将内容绘制到屏幕的任务活动,这是Web中不可避免的事情。无论使用<link>
还是@import
引入样式都会产生渲染阻塞(虽然<link>
下载是并行的)。
加载首屏样式:为了减少渲染阻塞时间可以直接把关键CSS样式放在<style>
标签中。
加载非首屏样式:非首屏样式也会遇到渲染阻塞的问题,可以使用preload来减少阻塞渲染时间。
1 | <link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'"> |
1 | /* 正常屏幕 */ |
-webkit-min-device-pixel-ratio
是旧的浏览器对高DPI的支持,如果比率是1相当于是96DPI,2相当于是192DPI;min-resolution
是现代浏览器所支持的直接显示值就行了,最后min-width
根据屏幕的宽度来加载不同大小的图片。
CSS可以直接把SVG当做图片来使用,实际上本身也可以看成图片:
1 | background-image: url(../img/masthead.svg); |
图片全局max-width规则:在响应式网站中图片往往最大是屏幕的宽度,所以显示最大宽度100%会很有用的。
1 | img { |
商品可以通过媒体查询根据设备分别率来使用不同的图片,实际上H5中,img标签的srcset和sizes属性也可以实现类似的功能。srcset可以根据屏幕的宽度来加载不同的图片。sizes可以通过屏幕的宽度设置图片的宽度,如下。
1 | <img |
上述代码中在512px像素宽度的时候图片是img/amp-small.jpg
,768px像素宽度的时候图片是img/amp-medium.jpg
,1280px像素宽度的时候图片是img/amp-large.jpg
,可见后面的w指的是屏幕宽度为多少。同样的在最小宽度704px的时候图片的宽度是宽度的50%,最小宽度是480px的时候图片的宽度是75%,最小宽度更小的时候图片的宽度是100%。
如果需要在相同的宽度的时候,根据设备分别率来显示不同的图片,那么srcset和sizes就不能做了,此时可以考虑功能更强大的picture标签,如下。
1 | <picture> |
可以使用type属性来加载webp图片,如下。如果支持的话使用图片img/apm-small.webp
,不支持的话使用图片img/amp-small.jpg
。
1 | <picture> |
picture很好用,如果不支持的时候可以走img标签做兜底处理,如果在低版本浏览器也希望使用picture标签该怎么办,就得靠picturefill了,使用方式如下。下面第一个script用来创建一个picture元素,防止因为没有该元素而导致解析错误,第二个script用来异步下载库文件。
1 | <script>document.createElement("picture");</script> |
雪碧图的好处:将大量图片缩减为单个图片,可以更加高效地传递资源,并通过减少到Web服务器的连接数来缩短页面的加载时间。
注:使用雪碧图可以减少HTTP请求,但在HTTP2中是反模式。
生成雪碧图:
1 | npm install -g svg-sprite |
将雪碧图回退为图片可以使用这个工具:https://github.com/filamentgroup/grumpicon
书中减少使用imagemin来缩小图片jpeg和png的图片,同时也支持生成webp图片: https://github.com/imagemin/imagemin
imagemin提供了大量的插件:https://www.npmjs.com/search?q=keywords:imageminplugin
使用svgo来压缩svg图片:https://github.com/svg/svgo
懒加载简单实现:
1 | (function(window, document){ |
上述懒加载的使用:图片上添加lazy样式,同时使用data-src代替src,src使用默认未加载图片来代替,如:
1 | <img src="img/blank.png" data-src="img/red-snapper-1x.jpg" class="recipeImage lazy"> |
将ttf字体转换为其他字体:
1 | npm install -g ttf2eot ttf2woff ttf2woff2 |
CSS中使用自定义字体:
1 | /* 定义字体 */ |
使用unicode-range加载字体子集,如下面把Open Sans Light
拆分成BasicLatin部分和Cyrillic部分,使用unicode-range
定义字符范围,如果文字中只有BasicLatin部分那么只会下载上面的文字,如果只有Cyrillic的文字那么下载下面的文字,都有会都下载。
1 | @font-face{ |
字体下载和字体实际显示直接可能有一段时间,那么这段时间内的字体是怎么显示呢?可以使用font-display来控制:
font-display: auto;
默认值,类似于block。font-display: block;
阻塞文本渲染,直到关联的字体加载完成。font-display: swap;
显示回退文本,加载字体后显示自定义字体。font-display: fallback;
auto和swap的折中方案,短时间(100ms)内显示空白,之后显示回退文本,如果字体加载完后,显示自定义字体。font-display: optional;
几乎和fallback一样,只是浏览器有更大的自由度来控制。
但是font-display
并未得到广泛的支持,使用JS来做回退处理:
1 | (function(document){ |
script标签会阻塞页面的渲染,放在body最后面有助于加快页面加载速度。
script带有async属性与不带async的区别:
不带async:下载脚本->脚本下载完成->浏览器等待其他脚本->执行脚本
带有async:下载脚本->脚本下载完成->执行脚本
带有async脚本下载完会立即执行而不会阻塞渲染。
使用async时需要注意,async下载完会立即执行那么,有可能执行的顺序跟script标签的顺序不同,从而导致JS执行报错。如有一个jquery.min.js文件,还有一个behaviors.js文件,其中behaviors.js引用到jquery.min.js中的$(jQuery对象),那么两个都用async就可能就会在behaviors.js中的$出现没有定义的情况。
解决方法:
1.可以把两个文件文件合并成一个
1 | linux 合并两个文件: |
1 | <script src="js/alameda.js" data-main="js/behaviors" async > |
1 | // js/behaviors.js 中使用AMD模块 |
书中还简绍了jQuery的替代方案和用原生JS代替jQury,现在MVVM时代很少用jQuery了,就不简绍了,原生方案可以看这里:https://github.com/nefe/You-Dont-Need-jQuery。
Service Worker在单独的线程上工作,无法访问window对象,但可以通过中介(如postMessage API)间接访问。
使用方式:
1 | if ("serviceWorker" in navigator) { |
1 | // 文件/sw.js |
上面在使用gzip的时候使用了compression
中间件,实际上compression
是支持传入压缩等级的 范围是0~9,默认是6,但并不是越高越好,往往默认值的效果是比较好的。
另外compression
也支持压缩特定的资源,可以使用filter,返回为true的时候表示压缩,false不压缩。
1 | app.use(compression({ |
Accept-Encoding
中如果有br
说明支持Brotli压缩,express可以使用shrink-ray
来开启Brotli压缩。
1 | // 假设你已经运行过: npm install https shrink-ray |
brotli中的quality范围为0~11,值越大文件越小,默认是4,一般也够用了。
设置Cache-Control头部的max-age指令
1 | app.use(express.static(path.join(__dirname, pubDir), { |
响应头会带有:Cache-Control: max-age=10
,注意max-age的单位是秒。
Cache-Control:no-cache
: 向浏览器表明,下载的任何资源都可以储存在本地,但浏览器必须始终通过服务器重新验证资源。Cache-Control:no-store
: 比no-cache
更近一步,它表示浏览器不应存储受影响的资源。要求浏览器每次访问页面时下载所有受影响的资源。Cache-Control:stale-while-revalidate=10
: 与max-age
类似,单位也是秒,当资源过期后仍然使用过期的资源,同时发出请求并缓存新的资源,下次再请求的时候使用新的资源。
在CDN的Cache-Control
有时会与privite
和public
连用,如Cache-Control: privite, max-age=10
,其中privite表示中介(CDN)不在其服务器上缓存资源,public则缓存。
对不同资源设置不同的缓存策略:
资源类型 | 修改频率 | Cache-Control头部值 |
---|---|---|
HTML | 可能频繁修改,但需要尽可能保持最新 | private, no-cache, max-age=3600 |
CSS和JS | 可能每月修改 | public, max-age=2592000 |
图片 | 几乎不会修改 | public, max-age=31536000, |
代码实现:
1 | app.use(express.static(path.join(__dirname, pubDir), { |
preconnect、prefetch与preload的使用:
1 | <link ref="preconnect" src="https://code.jquery.com"> |
preconnect可以提供更早的DNS查询,但是如果跟HTML是同域名的时候是没用什么用的,因为已经查询过DNS了,preconnect主要是为了查询其他域名,由于HTML是自上而下解析的,通常把preconnect放在HTML的head中的上面的位置。
prefetch告诉浏览器下载特定的资源,并将其存储到浏览器缓存中。通常用来预取同一个页面的资源,或者优先缓存下一页的资源。缓存下一页的资源使用时要小心,不要下载下页没有的资源,否则会造成过多的请求。
preload如果没有as属性,可能会导致请求2次的情况,另外preload只会缓存本页面的资源。
HTTP1的问题:
HTTP2对上述问题的处理
HTTP2对不支持HTTP2的浏览器的处理:每个HTTP2服务器底层都应有一个HTTP1服务器在等待一个不支持HTTP2的客户端出现。
HTTP2的简单使用:
1 | var fs = require("fs"), |
this.$nextTick
大家也再熟悉不过了,今天我们就来看看自创的nextTick
相关的几道面试题,看看你是否真正理解Vue的nextTick
。1 | <template> |
给你3秒,请你仔细考虑一下这道题输入什么?3、2、1…OK,你的答案是3
吗?如果是的话,那么恭喜你,答错了!正确答案是0
。什么?this.$nextTick
不是等DOM处理完后才执行吗,这里怎么不适用了?等等我们再来一题,至于为什么最后再讨论。
1 | <template> |
题目2和题目1就多了一行this.text = 4
,这个打印的是多少呢?再给你3秒,3、2、1…OK,正确答案是3
,跟你的答案一样吗?如果一样那么恭喜你了,我们再来一道过过瘾。
1 | <template> |
题目3和题目2唯一的区别是把this.text = 4
替换成了this.text = 0
,你的答案是多少呢?3、2、1…OK,正确答案是0
!此刻你的内心:**”啊…这!!!”**。别急,还有呢!!
1 | <template> |
本题增加了一个text1变量,并且修改了text1的值,这次打印的是多少呢?3、2、1…OK,正确答案是3
!
1 | <template> |
题目5比题目4模板中少了一行,你再想想这次打印的是多少?3、2、1…OK,正确答案是0
!
好了,我们这里总共5道题,答对3道算及格,你及格了吗?接下来我们分析一下。
这节我们会粘贴大量Vue源码,大家只要看关键的代码就可以了,觉得看源码枯燥难懂的同学可以直接看本节最后的总结。
像如this.text = 1
来设置值的时候,Vue会帮助我们异步的去更新视图,这里涉及Vue响应式原理,最终会调用nextTick
来更新视图,本题中主要考察的是nextTick
先后的顺序。哪怕你没看过Vue的源码也肯定知道Vue响应式原理是通过Object.defineProperty
这个API来实现的吧,他是怎么做的呢?源码如下:
1 | export function defineReactive ( |
这里的代码有点长,但是你不要怕,你只要看29行和57行就行了,在第29行说明当调用get的时候会调用dep.depend()
,在第57行的说明当调用set的时候会调用dep.notify()
。
那么dep的depend()
和notify()
又做了什么呢?
1 | export default class Dep { |
这里的Dep.target
和sub
都是Watcher
对象的实例。这里我们先捋一捋,当我们调用属性的set的时候,会调用dep.notify()
而该函数又调用了subs[i].update()
也就是Watcher
对象的update()
方法,那么Watcher
的update
和上面给到的addDep
方法又做了什么呢?
1 | addDep (dep: Dep) { |
addDep
方法中可以看到当Watcher
对象调用addDep
的时候,实际上是传入的Dep
对象把自己当做sub添加进去,这样在Dep
对象调用notify
才能通知到对应的Watcher,也就是说组件的data在调用set前一定要调用get才会通知对应的Watcher来更新视图,实际上只要模板中用到了变量就会调用变量的get。
update
方法中我们看到有一个queueWatcher(this)
的方法,这个又是搞什么呢?
1 | export function queueWatcher (watcher: Watcher) { |
queueWatcher
我们可以看到如果has[id] == null
就把has[id]
设置为true,然后把watcher
插入到队列中。由于一个组件对应一个Watcher(当然计算属性也会对应Watcher,这里说的是组件级别的Watcher),当一个属性改变后就会调用has[id] = true
,这样当再把当前组件的属性改了以后,由于has[id]
已经是true了,就没必要再加入到队列中了,毕竟更新视图,一次性直接全改了。最后你看到最重要的一行代码,就是nextTick(flushSchedulerQueue)
,我们终于看到nextTick
的影子了。调用nextTick
的时候会把传入的函数push进回调队列里面,也就是这里把flushSchedulerQueue
放在队列的尾部了,这个函数又做了什么呢?
1 | function flushSchedulerQueue () { |
可以看到flushSchedulerQueue
先给queue进行了排序,queue中存放的就是watcher,如果有watcher.before
的话则调用一下,处理完后把has[id]
置为null,最关键的一行是调用了watcher.run()
,我们再看看watcher.run()
做了什么。
1 | run () { |
run
方法貌似就设置了一下value的值,另外执行了一个this.cb.call(this.vm, value, oldValue)
方法,这个cb是Watcher构造函数的第三个参数,通常情况下是一个空函数。这里最重要的是const value = this.get()
这行代码,这里调用了一下Watcher的get方法,这个get方法是什么呢?
1 | get () { |
这里需要注意的是get方法开始的时候调用pushTarget
,结束的时候调用popTarget
,那么在这两段中间的代码调用Dep.target
时指向的就是当前的Watcher对象。另外get方法中有一个this.getter
,这个的值如下根据Watcher第二个参数expOrFn
来定的,我们可以在Watcher的构造方法中看到getter
的取值逻辑:
1 | class Watcher { |
如果第二个参数expOrFn
是函数的话this.getter
就是它,如果是a.b.c
这种字符串的话则进行解析,否则给个空函数。那么这第二个参数expOrFn
又是什么呢,请看下面:
1 | function mountComponent ( |
mountComponent
代码写的是非常漂亮!首先调用了beforeMount
生命周期方法,然后初始化Watcher
,最后调用mounted
生命周期方法。我们注意看Watcher
的第二个参数updateComponent
,该函数的实现是vm._update(vm._render(), hydrating);
也就是说Watcher
调用get方法的时候实际上是去更新视图了。这里你需要注意一点,Watcher
的constructor中最后会调用this.get()
而这时最终也会调用updateComponent
方法,这也就是在beforeMount
和mounted
之间会把视图更新在DOM上的代码。
总结:
- Vue会在beforeMount和mounted生命周期之间创建
Watcher
,并更新视图,当组件的Watcher
对象调用run方法的时候,最终会调用vm._update(vm._render(), hydrating);
来更新视图;- 当数据改变的时候,会调用
Object.defineProperty
中的set,这时除了赋值以外,还会调用dep.notify()
来通知已收集依赖的Watcher
调用update
方法进行更新;Watcher
调用update
方法进行更新时,会调用queueWatcher(this)
把当前的Watcher
对象加入到队列中,同时执行nextTick(flushSchedulerQueue)
;- 当下一个tick执行的时候会调用
flushSchedulerQueue
方法,该方法会调用watcher.run()
方法,进而调用watcher.get()
用来更新视图;- 只有先调用get收集了依赖的data,在set时才可能会引起视图的更新。
通过源码分析我们对Vue修改视图的逻辑有了更深的认识,现在我们再回过头来看看前面的题。
题目1:由于先调用的this.$nextTick
后修改的数据,这样数据后引起视图更改的nextTick
会在this.$nextTick
之后,所以打印未修改前的值,所以是0
。
题目2:由于先修改的数据后调用的this.$nextTick
,这样数据后引起视图更改的nextTick
会在this.$nextTick
之前,由于nextTick
是异步的,当nextTick
执行的时候,值已经是最后一次修改的值了,所以是3
。
题目3:虽然首先调用的赋值,但是值并没有改变,在Object.defineProperty
的set
方法中可以看到,如果值相同直接return
了,所以本题和题目1其实是一样的,也是0
。
题目4:修改text1的时候也会使用nextTick
来更新视图,而this.$nextTick
中的函数排在更新完视图后执行,所以结果是更新后的值,也就是3
。
题目5:在模板中把text1部分去掉了,那么text1相当于就没有调用get了,这样就不会再调用dep.depend()
来收集依赖了,所以text1修改并不会引起nextTick
去更新视图,所以此题的情况跟题目1也是一样的了结果是0
。
通过本章的学习,估计你已经收获满满,现在来一道最难的题:
1 | <template> |
这题是题目5的变种,在设置前通过console.info(this.text1)
打印了一下text1的值,那么就会调用get
方法,那么问题来了,此时结果是什么?3、2、1…OK,什么!你说3
?哈哈,当然不对,这里还是0
,为什么呢?这里虽然调用get
方法了,但是Dep.target
是undefined
,所以也没有收集到依赖,毕竟在get方法中只有Dep.target
不为空才去调用dep.depend()
。那么为什么Dep.target
是undefined
的呢?之前说过Watcher
的get
方法开始的时候调用pushTarget
,结束的时候调用popTarget
,而这个时候打印的时早就popTarget
了,所以Dep.target
是undefined
。那么为什么写在模板里面就有了呢?实际上在mountComponent
方法中创建Watcher
时,构造方法最下面会调用Watcher
的get
方法,get
方法不是先会调用一下pushTarget
吗?此时的Dep.target
指向的是当前的Watcher
对象,这个时候this.getter.call(vm, vm)
实际调用的是vm._update(vm._render(), hydrating);
,而vm._render
就会处理模板中的变量,那么模板中变量的get
也就会被调用了,所以放在模板中的变量在会被收集依赖。
for…of语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。
– MDN
for…of的基本使用比较简单:
1 | // 遍历数组 |
for…of的语法比较简单,上面我们遍历了这么多数据,现在我们使用for…of遍历一下对象:
1 | let object = { |
结果很不幸,使用for…of遍历对象报错了。为什么报错了,报错的错误提示写的很清楚,因为object对象不是可迭代的,也就是说它不是可迭代对象。
这里遇到一个新的名词,什么是可迭代对象呢?
要成为可迭代对象, 这个对象必须实现@@iterator
方法,并且该方法返回一个符合迭代器协议的对象
。
这里有2个问题,第一怎么去实现一个@@iterator
方法?看到@@xxx
这样的方法,想都不用想就是指[Symbol.xxx]
方法,这里也就是一个方法的key是[Symbol.iterator]
就可以了,比如:
1 | let object = { |
第二个问题什么是符合迭代器协议的对象
?首先迭代器协议的对象
是一个对象,这个对象有一个next
方法,这个next
方法每次调用有会返回一个对象,这个返回的对象又有一个done
属性和一个value
属性。其中done
属性表示是否完成,如果是true
则表示完成,false或者不写
则表示没有完成;value表示值,也就是for…of循环时每次使用的值,如果done为true时候则可以不写。举个可迭代对象的例子:
1 | let loop10 = { |
迭代器协议的对象
也可以自己调用着玩玩:
1 | let iterator = loop10[Symbol.iterator](); |
当然迭代器协议的对象
不仅仅只能用在for-of循环中,也可以用在数组的解构上:
1 | let arr = [...loop10]; // arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] |
当我们看到一个个可迭代对象的next方法,再看看一个个的{value: 0, done: false}
这种符合迭代器协议的对象
,这时不想跟generator没点关系都不行了,没错generator函数返回的正是可迭代对象。我们先使用常规方法实现一下对象的for…of遍历。
1 | let object = { |
使用generator函数可以简化上述步骤:
1 | let object = { |
是不是很方便?这里偷偷告诉你一个小秘密:generator函数调用后的对象也可以用在for…of上。
1 | let loop10Gen = function *() { |
上面不是说了,可迭代对象要实现一个@@iterator
方法,这里实现了吗?没错,这里还真实现了!你可以试试:
1 | let itarator = loop10Gen(); |
于是我们就得到一个比较绕的真理:generator调用后的对象,既是可迭代对象,也是符合迭代器协议的对象
。
由于for…in遍历的是对象的可枚举属性,所以对于数组来说打印的是键,而不是值:
1 | let array = ['a', 'b', 'c']; |
1 | let array = ['a', 'b', 'c']; |
通常为了避免for…in遍历原型和原型链上无关的可枚举属性,使用Object.hasOwnProperty()
方法来判断:
1 | let array = ['a', 'b', 'c']; |
可迭代对象除了next方法外还有return方法,主要用在循环中断的时候会调用,比如是用break
关键字、或者抛出一个Error:
1 | let loop10 = { |
简单的组件:
1 | <div id="app"> |
上面的组件需要在components
中引入,当然也可以定义一个全局的组件:
1 | Vue.component('custom-button', { |
上面组件只有一个template属性,一般的组件还有data、方法、计算属性等。这里需要注意的是组件的data需要是一个方法,并且返回一个对象,而Vue实例的data是一个对象,如果组件的data是一个对象的话,那么多个组件将会使用一份数据,这样所有组件数据都是一样的,某个组件修改数据会影响到其他的同类组件。
1 | Vue.component('positive-numbers', { |
组件的Props可以声明父组件要传递到子组件的数据:
1 | <div id="app"> |
props可以添加校验和默认值以及是否必须,校验失败在开发环境会报错:
1 | Vue.component('price-display', { |
模板中组件的props如果是带横线的属性,最后在组件内部将会自动转化为驼峰形式,如下
1 | <div id="app"> |
这里需要注意一点percentage-discount="20"
它的值最后是字符串的’20’而不是数字的20,所以上面代码会报错,如果要是数字的则需要:percentage-discount="20"
,同样的Boolean类型的也一样。
子组件中不建议直接修改父组件传下来的props,通常使用子组件的$emit方法来修改告诉父组件要修改值。某些情况下可以使用aync操作符来简化,赋值操作。
1 | <count-from-number :number.sync="numberToDisplay"/> |
组件定义:
1 | Vue.component('count-from-number', { |
实际上sync操作符只是一个语法糖,上面使用sync操作符的代码等同于:
1 | <count-from-number |
假设现在需要写一个只能输入小写字母的组件input-username,它需要支持v-model:
1 | <input-username |
上述代码等同于:
1 | <input-username |
所以要使得自定义组件支持v-model
可以这样写:
1 | Vue.component('input-username', { |
简单使用:
1 | Vue.component('custom-button', { |
上面定义了一个custom-button
的组件,则组件中间的部分将会给到插槽的位置:
1 | <custom-button>Press me!</custom-button> |
渲染后:
1 | <button class="custom-button">Press me!</button> |
插槽也可以给默认内容,只要下载slot
标签中间:
1 | Vue.component('custom-button', { |
这是如果自定义组件没有内容的时候将会使用默认内容,如:
1 | <custom-button></custom-button> |
渲染后:
1 | <button class="custom-button"> |
具名插槽就是给插槽起一个名字,如下面是blog-post
组件的模板,其中<slot name="header"></slot>
就是一个具名插槽,
1 | <section class="blog-post"> |
使用方式也很简单:
1 | <blog-post :author="author"> |
最终渲染的结果是:
1 | <section class="blog-post"> |
作用域插槽,组件的插槽可以向外面暴露数据:
1 | Vue.component('user-data', { |
上面相当于给defailt作用域的值定义为aaa变量,aaa.user就能获取到插槽的user属性了。当然默认插槽的也可以简写:v-slot="aaa"
。
mixin的简单使用:
1 | const loggingMixin = { |
当组件被创建后先后打印:Logged from mixin
和Logged from component
。说白了mixin对象就是一个普通的JavaScript对象,它可以混入属性、方法、生命周期等,其中属性和方法如果组件中也有同名的则组件中的会覆盖mixin中的,但是生命周期都会执行。
当使用vue-loader了以后就可以创建.vue
文件了。
之前的组件书写形式是:
1 | Vue.component('display-number', { |
.vue
文件组件的书写形式是:
1 | <template> |
代码更加之目了然了,组件的引用如下:
1 | <div id="app"> |
vue对非prop属性的处理是放在组件的外层上,并覆盖原有的。对于class和style则会合并。
1 | <div id="app"> |
最后渲染为:
1 | <button type="submit">Click me!</button> |
可以使用this.$attr
获取所有的非props属性。
上面的例子中都是使用template来定义组件的,实际上还可以写render函数来定义组件。假设有一个组件:
1 | const CustomButton = { |
使用render定义如下:
1 | const CustomButton = { |
上述代码是不是很难理解?render中代码的编写通常是由JSX来生成的,项目中使用babel-plugin-transform-vue-jsx
插件可以把JSX转换成类似于上述的函数内容。上述代码如果使用了JSX编写如下,是不是React很像?
1 | const CustomButton = { |
如果render和template同时出现,那么优先会使用render。
]]>HTML只要引入Vue就可以直接使用了,这里可以使用CDN地址https://unpkg.com/vue来引入Vue:
1 | <div id="app"></div> |
除了HTML直接引入Vue以外,也可以使用vue-loader与webpack配合,来使用.vue
文件来编写代码,webpack中配置vue-loader如下(只截取vue-loader配置部分):
1 | module: { |
当然最简单的还是使用vue-cli来初始化一个项目:
1 | npm install -g vue-cli |
data中的数据可以使用双花括号引入,也可以用在v-
开头的指令中,如下:
1 | <div id="app"> |
1 | <div v-if="true">v-if true</div> |
最终输出的是HTML如下:
1 | <div>v-if true</div> |
可见,v-show是使用display
来控制是否显示的,而v-if则直接控制是有有该元素。
v-if多条件判断:
1 | <div v-if="state === 'loading'">加载中..。</div> |
v-for是循环指令,可以用在数组,对象和数字上。
数组循环:
1 | <div id="app"> |
对象循环:
1 | <div id="app"> |
数字循环,这里打印1~10,注意v-for用在数字上,索引会从1开始而不是0:
1 | <div id="app"> |
属性绑定使用v-bind:
,或者简写形式:
冒号
1 | <div id="app"> |
由于Vue是使用Object.defineProperty来做的响应式的,所以对于给对象添加新的属性、使用数组下标修改数组值、修改数组长度来删除数组元素这三种操作是无法做到响应式,所以Vue提供了set和delete方法:
1 | Vue.set(data, 'key', value); |
如果需要把一段字符串当做HTML元素来使用的时候可以使用v-html,否则HTML标签会被转移。需要注意的是v-html的内容一点要保证是安全的,否则容易受到XSS攻击。
1 | <div v-html="yourHtml"></div> |
1 | <div id="app"> |
1 | <div id="app"> |
1 | <div id="app"> |
计算属性与方法的区别:计算属性会被缓存,如果在模板中多次调用一个方法,方法中的代码在每一次调用时都会执行一遍,但是如果计算属性被多次调用,其中的代码会执行一次,之后每次调用都会使用被缓存的值。只有当计算属性的依赖发生变化时,代码才会被再次执行。此外方法可以携带参数,但是计算属性可以设置值,如下:
1 | <div id="app"> |
1 | new Vue({ |
注意:在watch中一定不要去修改当前监听的值,否则容易造成死循环。如上面count方法中不要写this.count = XXX
。
watch也可以监听对象的某个属性:
1 | new Vue({ |
当然也可以使用函数来监听:
1 | this.$watch('formData.username', function (newValue, oldValue) { |
对于watch监听对象的情况如果对象的引用不变,则不会调用监听的方法,如上面formData.username改变如果只监听formData则不会调用,如果这种情况需要调用的时候,可以传递deep参数:
1 | new Vue({ |
1 | <div id="app"> |
上述最后渲染出的是Product one cost: $9.98
,filter也可以不传参数,如下
1 | <p>Product one cost: {{ productOneCost | formatCost }}</p> |
上述渲染出的是Product one cost: ¥9.98
除此之外还可以定义全局的过滤器:
1 | Vue.filter('formatCost', function (value, symbol = '¥') { |
1 | <canvas ref="myCanvas"/> |
最简单的使用:
1 | <button v-on:click="counter++">Click to increase counter</button> |
使用方法:
1 | <div id="app"> |
v-on:可以使用@来代替简写:
1 | <button @click="increase">Click to increase counter</button> |
修饰符:
1 | <button @click.stop.prevent="increase">Click to increase counter</button> |
Vue的修饰符有很多,不同修饰符间可以连用,修饰符的顺序不同左右可能也会不同。这里简单的列举一下常用的修饰符:
常用修饰符 | 说明 |
---|---|
.stop | 阻止事件冒泡 |
.prevent | 阻止默认行为 |
.capture | 捕获模式 |
.self | 只监听元素自身而不监听子元素 |
.once | 只调用一次方法 |
.passive | 提前告知不阻止默认行为(可以提高移动端性能),不可与.prevent一起使用,如果同时存在则忽略.prevent |
.exact | 准确地触发,如@click.ctrl.exact 则只有当ctrl按下并且点击的时候才触发;再如@click.exact 则只有点击切不能按任何其他键才触发 |
.ctrl | ctrl按下时 |
.alt | alt按下时 |
.shift | shift按下时 |
.meta | Command或者Windows键被按下时 |
鼠标按钮修饰符:
修饰符 | 说明 |
---|---|
.left | 鼠标左键,如@mousedown.left 表示鼠标左键被按下 |
.right | 鼠标右键 |
.middle | 鼠标滚轮 |
对于keyup等按键事件来说,提供了专门的按键修饰符,如:.enter
、.tab
、.delete
(捕获“删除”和“退格”键)、.esc
、.space
、.up
、.down
、.left
、.right
和.数字
(数字对应的是keyCode)
也可以自定义键名,如v-on:keyup.f1
1 | Vue.config.keyCodes.f1 = 112 |
1 | new Vue({ |
1 | <p v-blink>This content will blink</p> |
指令有5个钩子函数:bind、inserted、update、componentUpdated、unbind。
update指的是当前组件被更新时调用,此时可能子组件还没有更新。componentUpdated是所有子组件都更新后调用。
定义指令还有一种简写的形式:
1 | Vue.directive('my-directive', (el) => { |
钩子函数的参数有el、binding、vnode 和 oldVnod。其中binding又是一个对象,它的属性有:name、value、oldValue(仅在 update 和 componentUpdated钩子中可用)、expression、arg、modifiers。
1 | <transition name="fade"> |
过渡类名:{name}-enter、{name}-enter-active、{name}-enter-to、{name}-leave、{name}-leave-active、{name}-leave-to。
过渡也可以使用JS的钩子函数来做:
1 | <transition |
静态使用class:
1 | <div class="foo bar"> |
可以使用数组:
1 | <div :class="['foo', 'bar']"> |
也可以使用对象,如果值为true的时候则使用该类:
1 | <div :class="{foo: false, bar: true}"> |
还可以混着使用数组和对象:
1 | <div :class="['class1', {foo: false, bar: true}]"> |
还可以写一个静态的和一个动态的class,最终的结果是两者的合并:
1 | <div class="class1" :class="{foo: false, bar: true}"> |
style的绑定和class有些类似,需要注意的是对于font-weight
这种带有连字符的属性需要使用驼峰形式,如fontWeight
。
对象形式:
1 | <div :style="{ fontWeight: 'bold', color: 'red' }"></div> |
数组形式,可以串联多个对象:
1 | <div :style="[{fontWeight: 'bold'}, {color: 'red'}]">...</div> |
一般情况下,Vue会自动加上浏览器的前缀的,当然也可以自己设置多重置:
1 | <div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }">...</div> |
在.vue文件中样式默认是全局的,如果在style标签中加入scoped则只会应用在本组件内:
1 | <template> |
为什么会只改变本组件呢?实际上当写有scoped后,打包出来的组件元素都将会有一个data-v-{hash}的属性,而样式也会加上该属性,如下(hash可能是其他的值):
1 | <p data-v-e0e8ddca>The number is <span data-v-e0e8ddca class="number">123</span></p> |
style标签中使用module属性,表示使用了CSS Modules,如下:
1 | <template> |
首先需要安装SCSS,使用命令:
1 | npm install --save-dev sass-loader node-sass |
然后在style上使用lang="scss"
属性就可以了:
1 | <style lang="scss" scoped> |
CSS选择器可分为4类:选择器(如body{})、选择符(如相邻兄弟关系选择符+)、伪类(如:hover)和伪元素(如::before)。
CSS只有一个全局作用域,但是Shadow DOM中的样式不会影响外面的样式。
等级 | 选择器 | 例子 |
---|---|---|
0级 | 通配选择器、选择符和逻辑组合伪类 | 通配选择器*、选择符(+、~、空格、>)、伪类如:not等 |
1级 | 标签选择器 | body {} |
2级 | 类选择器、属性选择器和伪类 | .foo{} [foo]{} :hover |
3级 | ID选择器 | #foo{} |
4级 | style属性内联 | <span style="color:red;">文字</span> |
5级 | !important | .foo{color:red !important;} |
举例:
1 | <style> |
这里html
与body
等级相同,.foo
也是一样的,所有优先级是相同的,如果优先级相同时,就符合后来居上的原则,所以是蓝色的。
部分资料上优先级是按照计数来算的,但是并不意味着10个类选择器和一个id选择器优先级相同,上一级比下一级有永远无法逾越的差距,但是IE浏览器256个上一级选择器要比下一级的优先级大(老式浏览器8字节存储所导致的),现代浏览器则没有此问题。
选择器大小写敏感问题:
选择器类型 | 示例 | 是否大小写敏感 |
---|---|---|
标签选择器 | div{} | 不敏感 |
属性选择器-纯属性 | [attr] | 不敏感 |
属性选择器 | [attr=val] | 属性不敏感、值敏感 |
类选择器 | .container | 敏感 |
ID选择器 | #id | 敏感 |
选择器命名可以以数字开头,但是在CSS中需要转义,如下面是合法的:
1 | <style> |
CSS的命名可以是中文、中文标点符号甚至Emoji.
四大选择符:后代选择符(空格),孩子选择器(>),相邻兄弟选择符(+)、后面兄弟选择符(~)。
思考题:
1 | <style> |
上面1和2是什么颜色呢?由于颜色都是继承自父标签的,所有应该取距离近的父标签的颜色,所以第一个是蓝色,第二个是红色。修改此题如下:
1 | <style> |
本题稍微变化了一下,这里1和2的颜色不是继承来的,而是匹配到了CSS样式,并且2个样式都可以匹配到,此时就得看优先级了,由于优先级相同,所以后来居上故都是蓝色的。
JS获取元素补充说明:
1 | <div id="myId"> |
相邻兄弟选择符(+),选择的是元素,会忽略中间的文本和注释,如下面3个p标签都是红色的:
1 | <style> |
元素选择器包括标签选择器和通配符选择器。
多个选择器时,元素选择器必须写在前面,如这样是不合法的[type=radio]input {} 或者 [type=radio]* {}
7种属性选择器格式:
选择器格式 | 匹配规则 | 举例 | 说明 |
---|---|---|---|
[attr] | 包含指定属性就可以了 | [disabled] {} | boolean型属性用的比较多 |
[attr=”val”] | 属性和值都需要匹配 | [type="radio"] {} | 值可以是单引号、双引号或者不写,结果都是一样的 |
[attr~=”val”] | 值包含则匹配(val必须是一个值,不能是值里面的一部分) | [rel~="noopener"] {} | 匹配:<a rel="noopener nofollow"></a> 不匹配: <a rel="123noopener123"></a> |
[attr|=”val”] | 值起始片段相同(片段是指用-连接的属性值) | [attr|=val] {} | 匹配:<div attr="val"></div> 匹配: <div attr="val-us val2"></div> 不匹配: <div attr="val val2"></div> 不匹配: <div attr="value"></div> |
[attr^=”val”] | 属性值以val开头的元素 | [href^="https"] {} | 匹配href以https开头的元素 |
[attr$=”val”] | 属性值以val结尾的元素 | [href$=".pdf"] {} | 匹配href以.pdf结尾的元素 |
[attr*=”val”] | 属性值包含val的元素 | [attr*=val] {} | 匹配:<div attr="value"></div> \n 匹配:<div attr="aaa val bbb"></div> |
之前说过属性选择器,属性是忽略大小写的,属性的值是大小写敏感的,如果需要属性值也忽略大小写的话可以在属性中加一个i或者I,则表示大小写不敏感,如:[attr*="val" i]
。
手型经过伪类:hover
。
激活状态伪类:active
。
焦点伪类:focus
可以生效的元素:
<area>
元素,不过可生效的CSS属性有限;<summary>
元素。其他元素不能生效,非要生效可以设置属性contenteditable="true"
或者添加属性tabindex="数字"
。
一个页面最多只有一个元素响应:focus
。
整体焦点伪类:focus-within
,在当前元素或者当前元素的任意子元素处于聚焦状态的时候都会匹配。(少有的子元素行为决定父元素的伪类选择器)
键盘焦点伪类:focus-visible
,元素聚焦,同时浏览器认为聚焦轮廓应该显示。(目前只有Chrome支持)
:link
伪类用来匹配页面上href链接没有访问过的<a>
元素,已访问的元素则不匹配(用处不大,通常直接用a标签选择符就可以了)。
a标签相关的四个伪类的优先级::link
> :visited
> :hover
> :active
(LVHA lova-hate)。
:visited
标签访问过选择器,该选择器由于安全考虑有以下特性:
background-color
则a标签的样式需要设置过background-color
);getComputedStyle()
方法无法获取到色值。:any-link
不兼容IE11,其他浏览器兼容性良好,匹配规则如下:
<a>
,<link>
和<area>
;:link
伪类或者:visited
伪类的元素。:target
:当浏览器是有锚点与当前元素相同时则匹配,这里的锚点也就是路由上hash指向的id所对应的元素。该伪类有一个特性,就是当元素不显示的时候也能匹配,但是不显示的时候设置当前元素的样式也不会有什么效果,毕竟不显示嘛,但是可以操作他后面的兄弟节点(可以利用该伪类选择器实现“显示全部”的功能)。
:target-within
:匹配:target
伪类匹配的元素,或者后代存在匹配:target
伪类的元素的元素。目前还没有浏览器支持!😢
:enabled
元素可用,:disabled
元素不可用,他们是对立的,readonly
的表单是:enabled
的,另外:enabled
可以用在a标签上,a标签没有:disabled
状态,哪怕给a标签设置了disabled属性。
:read-only
表单只读,:read-write
表单可读可写(默认就是,比较鸡肋)。表单disabled的时候匹配的是:read-write
,虽然此刻也不能写😂。
:placeholder-show
:占位符显示时匹配,由于占位符是在输入内容为空的时候出现,所以可以使用:placeholder-show
来判断表单是否为空。
:default
:默认状态的表单选中元素,如select标签下的option可以给一个默认值,这个默认值就可以用:default
匹配。这里需要注意的时候如果option标签没有给默认值的时候:default
并不会匹配,但是浏览器会默认选中第一个元素。
:checked
:checkbox选中时的伪类。
checkbox的:checked
伪类比[checked]
属性选择器优势在于JS控制选中的时候(checkbox.chencked = true
这种情况),由于没有改变checked属性,所以[checked]
不准确,而:checked
则不会有问题。同样的:disabled
和[disabled]
也一样,另外:disabled
是表单元素实际是否被禁用,比如表单外面包裹着一层<fieldset disabled>
,里面的表单元素则是禁用状态,此时:disabled
能匹配到,但是由于里面的表单元素没有加disabled属性所以[disabled]
匹配不到。
:indeterminate
:不确定值伪类,实际上就是当JS设置checkbox.indeterminate = true
的时候则会匹配,也就是浏览器常见的复选框一个横线的时候的那种状态。该伪类也可以用于单选框,当单选框的组没有一个选中的时候则单选框的每一项都匹配。
:valid
:输入验证有效的时候匹配。:invalid
:输入严重无效的时候匹配。就是我们在<input>
标签中设置required或者pattern等属性的时候,会判断是否有效,匹配对应的伪类。由于首次进来的时候往往没有输入内容,这时如果有required属性,此时:invalid
会匹配,这样就有点不太友好了,更好的伪元素就是:user-invalid
,可以避免首次判断,但是目前兼容性非常不好。
范围验证伪类:in-range
和:out-of-range
使用于type=number和type=range的input标签。
:required
表示表单必填,:optional
表示表单可选(非必填)。
:root
匹配根元素,IE9以上才支持,在XHTML中根元素就是html,另外也可以匹配的SVG的根元素,但不能匹配Shadow DOM的的根元素,Shadow DOM的的根元素是:host
。:root
最常用的是声明CSS变量。
:empty
用来匹配空元素,这里的空元素包括前后闭合的空元素,甚至<input>
这种非闭合的标签。如果标签内有空格、换行、注释则不能匹配:empty
。具有::before
或者::after
的空元素可以匹配:empty
。
:first-child
第一个子元素;:last-child
最后一个子元素;:only-child
唯一的子元素。
:nth-child()
与:nth-last-child()
表示第几个子元素和倒数第几个子元素,从1开始,也可以是odd
(基数)和even
(偶数),也可以An+B这种形式,如:nth-child(3n + 4)
匹配第4、7、10…
:first-of-type
当前类型元素的第一个;:last-of-type
当前类型元素最后一个;:only-of-type
当前类型只有一个。:only-child
匹配到的元素一定是:only-of-type
,但是反之不成立。:nth-of-type()
和:nth-last-of-type()
与上面的类似,不再重复。
否定伪类:not()
:如果当前元素与括号里面的选择器不匹配,则:not()
后会匹配。需要注意的是:not()
括号里面的只能出现一个选择器,如:not(p)
,不可以:not(p.class1)
或者:not(p, div)
,可以写多个:not()
来支持这种情况如::not(p):not(class1)
。另外:not()
本身的优先级权重是0,整体权重则由括号内的权重来决定。
与:not()
相反的伪类是:is()
,:is()
是老版本浏览器的:matches()
和:any()
演化来的。:is()
的权重与:not()
一样由括号内的权重来决定。还有一个伪类:where()
匹配规则与:is()
相同,但是整体权重是0,不管括号内的权重是多少。
:scope
作用域选择伪类,由于CSS只有一个全局作用域,所以:scope
与:root
一样,都相当于html
。不过JS倒是支持的,详见上面精通CSS选择符
最后部分。
:fullscreen
用来匹配全屏状态下的DOM元素(调用dom.requestFullScreen()方法的dom元素),:backdrop
伪元素用来匹配浏览器默认的黑色全屏背景元素的。
:dir(ltr|rtl)
匹配从左到右,还是从右到左。
:lang()
语言类伪类,如:lang(zh)
。
video或者audio播放状态伪类:playing
和:paused
。
我们前几章,讲的都是小球相关的操作,这里的小球就是圆,那么首先讲的当然是圆的碰撞检测了。在说碰撞检测之前我们先把拖拽相关的代码复制一份,这样我们就可以边拖拽边检测物体是否碰撞检测了。拖拽相关的代码如下,为了简化拖拽的代码,这里我们只考虑2个小球的情况,如果对拖拽还不了解的同学可以参考这篇文章。
1 | function captureMouse (element) { |
都是之前的东西,效果如下。:
圆与圆之间碰撞其实很简单,只要比较两圆圆心之间的距离和两圆半径之和的大小就可以了,若两圆圆心之间的距离大于两圆半径之和那么说明两圆没有发生碰撞,如果相等则表示刚好碰撞了,如果小于的时候,则说明两圆相交。这个在前面的内容你应该早有体会,毕竟我们可是研究过小球碰撞。
圆与圆碰撞检测的代码:
1 | function isCollisionBallAndBall(ball1, ball2) { |
然后我们在animate方法中添加碰撞检测的逻辑,如果碰撞了则把绿色的小球变成红色,否则显示绿色。
1 | function animate (){ |
此时的效果如下:
长方形与长方形的碰撞检测是FC游戏中用的最多的,FC好多游戏为了简化碰撞检测把一些看着不规则的物体也当做长方形来检测了,就是因为长方形好计算。例如,《超级玛丽》就是基于长方形检测的,你能看的出来吗?
在讲长方形之前我们先写一个长方形的类,并创建2个长方形对象:
1 | class Rect { |
长方形没有碰撞的时候无非就只有四种情况就是,一个方块在另一个方块上边的外面,或者右边的外面,或者下边外面,或者左边外面,具体如下。除了这四种情况外,其余的情况都是相交的。
有了上面的知识,写代码就容易了:
1 | function isCollisionRectAndRect(rect1, rect2) { |
添加上拖拽以后,大概是下面这个样子:
在类似于FC的游戏中,为了提高计算效率很少用到圆与长方形的碰撞检测,当然随着计算机性能的提高,圆与长方形的碰撞检测也变得越来越常见了。圆与长方形的碰撞检测首先是下面几种肯定是不会碰撞的。
当然除了这种情况以外,是不是一定会碰撞呢?答案是否定的,在四个角的时候,即使不满足这几种情况也没有碰撞,如下:
所以代码就是这几种情况综合考虑了:
1 | function isCollisionBallAndRect(ball1, rect1) { |
现在的效果如下:
]]>promisify
是什么意思呢?在英语中ify
结尾的单词一般为动词,表示“使……化”,那么很显然promisify
就是“使Promise化”,通俗一点就是把回调函数转化为Promise这种形式。promisify
的代码相对来说比较简单,这里直接给出代码:
1 | function promisify(fn) { |
假设我们在node中的fs.readFile
方法上使用它,那么有:
1 | const fs = require('fs') |
由于Promise比回调方式更优雅,所以很少有人会把Promise再转回回调方法,在讲unpromisify
之前我们先写一个Promise版本的delay
函数:
1 | function delay(timeout) { |
现在定义一个unpromisify
函数,将delay
函数改成回调函数的版本:
1 | function unpromisify (p, done) { |
node的util模块提供了promisify
函数,可以直接拿来使用。由于很少需要unpromisify
,所以该模块中并没有提供unpromisify
。
1 | const fs = require('fs') |
有一次我搜索公众号的时候无意中看到一个“杭州湘湖马拉松”的公众号,但是这个公众号没有任何信息,网上也搜不到任何杭州湘湖马拉松相关资料,我想着估计是首次举办吧,果然没过多久首届湘湖马拉松开始报名了。
2019年10月20日,湘湖马拉松正式开赛,湘湖这边风景非常优美,不过赛道上还是有些桥。在苏州湾国际马拉松后我逐渐增加了速度训练,5公里成绩可以稳定在了23分左右,我计算了一下如果用5分钟的配速跑完半马的话,大概需要1小时45分钟,按照我平时4分35秒的配速跑完半马只要1小时36分41秒,想想都觉得厉害。但是现实却狠狠地给了我一巴掌,毕竟很难用5公里的速度坚持个21.0975公里。当我跑到10公里的时候,基本上还是可以用鼻子呼吸,这是一个好现象,毕竟鼻子做深而长的呼吸将会更省力。但是后面就不太行了,有时候必须用嘴巴呼吸,一用嘴巴呼吸后,速度就开始下降。15公里的时候我感觉脚底貌似踩了个小石头,又跑了2公里决定还是处理一下,当我在路边上脱掉鞋子后惊奇地发现竟然没有小石头,那我大概就明白了,十有八九是脚起泡了,好在还可以跑。在这半年间的训练中,我逐渐适应了快速奔跑,这个时候的我感觉身体还是充满了能量,最后跑到终点,净成绩是01:53:04,大幅PB之前的个人成绩。跑完以后看赛事照片的时候,我发现女子第一名是一个哪吒造型,她就是后来著名的哪吒滚滚儿——唐红芳。
杭州湘湖马拉松的奖牌是我最喜欢的奖牌之一,前面是杭州湘湖马拉松的赛事路线,后面是著名的越王勾践剑,整体呈黄金色配色,简约中带点奢华。
跑完杭州湘湖马拉松后的一周后,我继续背靠背征战杭州女子马拉松,没错我参加的是女子马拉松!为什么我一个男生可以参加女子马拉松呢?因为这次我作为秋芹的私兔来参赛的。这里需要了解一下私兔和普通选手的区别:
上一次湘湖时脚底起泡恢复的也差不多了,为了防止再起泡我买了一个厚鞋垫,别说还是挺管用的。比赛开始后我发现好些女生还是很给力,他们的速度简直飞快,当然在大众选手中我还是勉强可以追一些选手的,跑到17公里的时候感觉马拉松确实很累,前面有个女生都有点不行了,但还是咬牙切齿地跑,太猛了。作为一名私兔,我显然是不合格的,一直跑到终点都没见到秋芹,最终秋芹的成绩是1:52:06。跑完以后,我们私兔的成绩短信并没有立刻发过来,比我先完赛的秋芹笑我跑得太慢,大概过了三四个小时,我的成绩短信过来了,净成绩是1:52:03,比秋芹快了3秒,前一秒还数落我的她瞬间不说话了。
杭女马后我可以休息2周,2周后再次来到美丽的绍兴,这是我参加的第二届绍兴马拉松,这次打算跟秋芹一起征服一次全马。42.195是多少跑马时爱好者崇敬的数字。
2019年11月10日,绍兴马拉松开赛。我深知全马的难,所以我尽可能地放慢速度,要跑一个很长的历程,最简单的就是放慢速度,只要速度够慢基本上都可以跑完,实在跑不完,走也可以。这次马拉松前半程我和秋芹都用了2小时完成,后来秋芹就不知道去哪了,由于有了之前的教训我知道耐力型的秋芹肯定能追上来,果不其然,没过多久她追上来了。就这样我们跑到了30公里,这个时候,我发现我开始掉速度了,每一步都是煎熬,看着秋芹渐渐远去的背影,我发现这一幕似曾相识,虽然我在半马领域比秋芹快了3秒,但是全马领域估计要被她碾压了。33公里的时候,我已经看不到秋芹了,后来跑着跑着看到一个补给点,当时已经极度乏力,此刻我极度厌烦马拉松了,实在太虐了,真正的体验到了花钱遭罪受的感觉!在这个补给点补了一些水后,我想着要不走一下吧,结果一走,发现还是走路香啊!算了算了,还是走到终点吧!!这次给我深刻的教训全马!==半马*2,全马比两个半马难多了,当身体的糖分消耗的差不多的时候,简直是灾难。途中遇到一个年轻的观众说我不好好跑,都开始走路了,真想说你上来跑个30公里再试试。路上遇到一个同样崩了的小哥,和他聊了半公里,最后他还是决定继续跑完,想着路程也不远了,再跑一下步,这个时候430的兔子也追上来了,领先的兔子是个女生,边跑边打气,于是我就和好几个跑者跟在了兔子们的后面。又遇到一个补给点,美女兔子说:“你们先去喝点水吧,我们慢慢跑。”天真的我以为他们真的会慢慢跑,当我喝完水后跟他们差了10米,后来他们慢慢跑,我们的距离变成了20米,30米…直到我都追不到兔子了!不是说好了慢慢跑的吗,怎么跑那么快,算了算了还是走路吧。这一路上心里只有一个想法,再也不来了!!!最后凭借坚强的意志我走到了终点!!!最终净成绩是04:42:00,秋芹比我快了半个小时,最终净成绩04:10:47。
第二年绍兴马拉松终于获得了一个金牌,感受到绍兴全马的虐,我决定以后再也不参加绍兴马拉松了!!
绍兴马拉松跑崩以后,对于后一周举行的诸暨马拉松就没报多少希望了,诸暨马拉松又叫西施马拉松,比赛路线是心形赛道。比赛第12公里的时候有个很大的上坡,这个上坡让很多选手痛苦万分,跑到坡顶后又是一个很长的隧道,第一次在隧道里面跑步,我也是够厉害的,哈哈。隧道出来后我全力冲刺了一公里,后面就泄气了。最后2公里还走了一段距离,最终净成绩:02:02:30。差点打破自己最差成绩。
2019最终参加了7个线下马拉松,想着2020年也好好参加几个,2020年元旦过后,我报了2020年的宜兴马拉松,毕竟宜兴马拉松有着PB赛道,结果因为新冠疫情,2020年宜兴马拉松最终取消了。同样地,上半年因为疫情,马拉松这样的大型赛事全部停赛了。2020年下半年,国内新冠疫情逐渐得到了控制,马拉松赛事也逐渐开始恢复,江浙地区第一个恢复报名的赛事就是绍兴马拉松,想想上次绍兴跑崩的我,对绍兴马拉松都有点恐惧了,奈何现在没有选择了,只能报名去了。绍兴马拉松预报名开启后,官网瞬间挤炸了,在等待了一个多小时后,才逐渐恢复,预报名成功后可以不用抽签,因为我一直守在电脑前,所以官网一恢复就报名了,这次我报名了一个半马,同样的秋芹也报名了,她报名了一个全马!😂
绍兴马拉松的赛事服务做的非常棒,非江浙沪选手来参赛的话,现场可以免费做核酸检测。因为我去的比较晚,所以也没得到什么羊毛,第三年来绍兴参加马拉松,感觉绍兴奥体中心都很熟悉了。2020年11月8日比赛正式开始,半年多没跑了,好多人都铆足了劲,由于跑前一天没怎么睡好,这天各种不在状态,当我跑到15公里的时候,感觉身体有点不适,计算了一下基本上无缘PB了,于是我就放弃了,开始走了。貌似我最近几个马拉松都有走的😇!当我走的时候,我发现周围的人没有一个在走,甚至还有坐轮椅的把我超越了。终于有时间可以把最后的大坡拍个照了:
最终成绩是2:03:16,差点创造了自己最差的马拉松记录。秋芹最终以3:54:18完成了全马,瞬间感觉她和我已经不在一个档次了。
一金一银奖牌:
自从绍兴马拉松失利后,我一直质疑我是不是水平下降了,想想去年湘湖马拉松那种充满体力的感觉,好像已经很遥远了,后来又报名杭州马拉松结果还是没中签,不过梦想小镇马拉松开始报名后我就对杭州马拉松彻底无感了,毕竟去年梦想小镇马拉松办的是非常好,去年因为和湘湖马拉松时间重叠了我最终选择了风景更好的湘湖马拉松,后来发现参加梦想小镇马拉松也很不错,衣服的赞助商都是斯凯奇,所以今年肯定要参加一下梦想小镇的马拉松。
这次和秋芹共同参加了梦想小镇马拉松,由于我的耐力比较差,全马比不过她,那我就在半马上找回点颜面!2020年11月29日,梦想小镇马拉松开赛,由于我的上传的成绩是绍兴全马时候的成绩,导致我被安排在了C区,秋芹稳稳地被安排在了A区。比赛前为了更好的进入状态,我做了充足的热身。今天的策略就是找一个人给我破风。比赛开始后我以5分半的配速跑了一公里,这个时候我看到有个人飞快的跑向前去,他的个子比我低一点不过看着比我壮,我知道机会来了,给我破风的人就是他。于是我紧紧地跟在他的后面,他的配速一般是4分50秒上下,当然这个时间是Keep记录的时间,因为Keep有点偏差,实际配速应该在5分零几秒上下,这个速度我完全可以PB了。虽然我感觉我可以更快点,但是为了保存足够的体力,还是没加速。跑到5公里的时候他竟然不去补给,我想着我去最后一个补给点去喝点水吧,结果当我跑到最后一个补给点才发现最后一个补给点并不是补给点,只是放了一个桌子,就这样我错过了5公里的补给。10公里的时候,他还是没有去拿补给,我都纳闷了,这么强的吗,都不需要补给?第11公里跟着他跑完用时5分03秒,我感觉他体力下降了,或许已经不能再满足我的速度了,于是我就把他抛弃了,想找另外一个给我破风的人,结果到最后也没找到。期间看到一个身体健硕的老大爷,跟他跑了一段,后来我拿补给时被他甩开了,后来也没追上。再后面我感觉我可以再加点速,最终还是控制住了,完赛净成绩:1:47:47,成功PB!
梦想小镇马拉松的奖牌也很不错:
由于一路上我都没看到秋芹,所以我知道她的成绩肯定比我好,结果后来一问1:40:39,我们足足差了7分多,看来我们的差距真的是越来越大😂。
]]>自从上次挑战半程马拉松成功以后,跑步的路上我有了特大的信心,有一天我在Keep上看到马拉松报名中(现在的Keep貌似已经没有这个模块了)有一个宜兴马拉松,因为是首届举办所以不需要抽签,先到先得!看到这我很欣喜地就报名了;当然也把秋芹拉上了。
宜兴离杭州很近,大概四五十分钟左右的高铁。2018年4月14日,宜兴马拉松的前一天,我来到了美丽的宜兴,本来想着可以像杭马那样薅很多羊毛,结果来了大失所望,可能因为是首届马拉松吧,赞助商少得可怜,几乎就没有羊毛。他们的领物袋是一个很大的透明袋子,看着也是不值钱的样子;参加过杭马的秋芹看到这破袋子都有点想放弃了。
2018年4月15日早上,我早早的就起来准备迎接马上的比赛,宜兴马拉松的赛事服务做的很不错,可以坐免费的接驳车。来到存物的地点我才发现这个透明的大袋子原来这么有用,它不仅可以放基本的参赛物品,还可以放书包衣服等行李,如果像杭马那种小型束口袋肯定放不下我这么多东西,就连秋芹也倒戈了:“还好有个大袋子!!!”。7点钟,我做了一遍Keep上的跑前拉伸课程,赛前还有唱国歌等仪式,7点半比赛正式开始!由于参赛人数比较多,第一公里都跑不开,后面慢慢步入正轨,线下赛有个好处就是有补给,一般的是5公里后每2.5公里有一个补给点,有的可以喝水,有的可以喝电解质饮料,还有一些可以吃香蕉之类的水果。宜兴这里的赛道非常宽,跑起来很舒服,最重要的是宜兴的桥少,几乎一路平坦,这就为PB创造了有利的条件。可能由于是首届举办的缘故,宜兴的市民也很有激情,看着大爷大妈们大喊加油,你都不好意思走两步。在大爷大妈的热情鼓励下,我完成了第一个线下半程马拉松,最终用时2:03:54,大幅PB之前的记录。
首届宜兴的奖牌是黑褐色的紫砂奖牌,是采用宜兴最有名的紫砂壶的原料制成的,但是黑黑的奖牌我是不怎么喜欢,某宝上搜了一下紫砂壶,一个也好几百块钱呢,还是好好把奖牌收藏着吧。
宜兴马拉松后我发现身体确实比原来强了,第一次跑完半马休整了一个月,宜兴半马后只休整了一周就几乎没什么事了。转眼间18年下半年就到了,很高心地可以再参加马拉松了,我带着秋芹、党等伙伴一起报名了杭州马拉松,结果悲催的是我们一波人,全军覆没了,一个都没中签!!!这个时候绍兴马拉松开始报名了,一想都绍兴,就会想到一位大文豪——鲁迅!绍兴马拉松有自己独特的简称——越马!刚开始我一直想为什么不叫鲁迅马呢?后来才知道绍兴是一座千古名城!绍兴,简称“越”,越国古都,是卧薪尝胆的越王勾践的那个越国,所以绍兴马拉松简称越马!越马报名后等了好久终于出签了,结果我和党中签了,秋芹只能看着我们跑了。
因为越马,我第一次来到美丽的绍兴,比赛前一天我参观了周恩来祖居、鲁迅故居等景点,增长了两个知识点,一是鲁迅和周总理竟然是远房亲戚,二是鲁迅家真的很有钱!小时候课文上闰土叫鲁迅老爷感觉闰土很势力,毕竟小时候,刺猹的闰土也是很有能耐的孩子,长大后竟然变了!参观完鲁迅故居后我发现我也变了,原来看到鲁迅写的百草园感觉很有趣,如今看到真实的百草园,只能羡慕鲁迅有钱了,想想现代人100多平米的房子,如果不靠父母,好多90后得用大半辈子去还贷,再看看鲁迅家,占地面积就好几百平米,大到家里都可以修一个小院子!怪不得鲁迅能有那么大的气魄!!
扯得有点远,继续回来聊聊马拉松的事。2018年11月25日,越马开赛!绍兴是一座被水环绕的城市,正如越马的口号一样“千年越州行,如在镜中游”,群水环绕景色确实怡人,但同样有一个缺点就是桥太多了;桥多了就会有更多的上下坡,跑起来比较困难。好在这次准备充分,没有掉太大的链子。期间看到有个跑者不顾医生的劝阻执意要跑步,后来才知道这个跑者跑着跑着直接晕到了,好在医生救治及时没有生命危险,但是该跑者拖着还没完全恢复的身体还要继续去跑,医生拦到拦不住,然后名场面诞生,只见一个跑者在跑,后面医生在追。果不其然,没跑几百米该跑者又晕倒了,这次医生很聪明,边救他边让救护车开往医院。后来了解到该跑者是因为心脏骤停而晕倒的,心脏…骤停!!!太恐怖了!!!跑马拉松还是要量力而行啊!!!那么怎么能避免这种情况的发生呢?最简单的就是要做好充足的跑前热身。为什么热身可以避免这种问题呢?其实当跑步的时候,心脏并不能立刻就进入跑步模式,好多选手从静息状态下直接剧烈跑步,这样的话,心脏瞬间跳动加速对心脏的压力是非常大的,正像我们冬天把热水直接灌进瓶子里一样,如果直接倒进去很可能瓶子会炸裂,但是如果先倒一点,给瓶子预热一下,瓶子就不容易炸裂了。另外心脏有问题的跑者并不适合跑马拉松,可以根据家里人的心脏病史来进行评估。回到马拉松中来,就这样一公里一公里地跑下去,最后一公里处有一个超级大坡,有意思的是,看到这个大坡的选手几乎都会说一句:“卧槽”,而且是用不同的方言说的,实在是有趣。最终完赛后净成绩是1:59:01秒,成功挑战了一次神兽路线,这也是我首次半马破二!当时跑完估计这次以后应该很多年都不会再PB了,毕竟半马破2是初跑选手的一个坎,且行且珍惜。
最终的奖牌也很有特色,如果参加的是全马的话会给一个金牌,半马就是一个银牌,小马是一个铜牌。我参加的是半马,所以给了一个银牌。
2018年跑了2场马拉松后我感觉我好像上瘾了,19年3月份报了2个马拉松,分别是无锡马拉松和横店马拉松,有点跑马去旅行的感觉。无锡马拉松我领悟了一个诀窍就是,跑马的时候刚开始的速度一定不要过快,刚开始过快后面很可能会没有体力的,这次也一样,都是以慢速开始的。无锡马拉松的人更多,几乎用了10分钟才走到了起点,它的半程的终点是江南大学,一座很美丽的大学。这次跑的也比较水,最后坚持到了江南大学,因为跑步的时候感觉也不怎么快,成绩也就那样,跟其他的伙伴比感觉还是差了一点点,最终净成绩是1:58:57。等等,58分?额,没错确实58分!难道PB了?!!卧槽竟然PB了4秒,哈哈哈!本以为很水的马拉松,没想到竟然是个人最好成绩!再想想当初觉得跑进2小时就是极限,简直可笑😀。
美丽的江南大学:
非常有特色的奖牌:
2019年03月31日,无锡马拉松结束的一周,我背靠背参加了横店马拉松,无锡马拉松基本上两天左右就恢复的差不多了,第一次背靠背参赛,这次参加比赛的朋友挺多的,秋芹、党、海洋都参赛了。横店是一座影视名“城”,本想着这里可以看看明星的,结果到最后也没看到明星,据说许凯参加了一个小马,不过我不认识。横店的赛道穿越过了好几个影视景点,好多观众都是穿着古装的演员,非常有特色。比赛中,我一路绝尘,早早地把其他几个小伙伴甩在了后面,13公里的时候,经过我再三确认发现小伙伴没有追上来。这个时候我就在想,小伙伴中当我一个人最快达到终点是一种什么样的体验?按照这个速度我肯定会PB,是不是可以稍微放轻松一点?跑完后小伙伴是不是会很崇拜我?当我还沉浸在自己思绪中的时候,只听到有人在我耳边说:“Hello~”,我定睛一看原来是秋芹,没想到她竟然追上来了,于是我跟秋芹一起跑了2公里,2公里以后我发现我逐渐跟不上她的速度了,我们俩的距离一米两米地逐渐落后,落后到20米的时候,我想不能再落后了,于是一股脑地冲上去,可能因为跑得太快了,马上速度就降下来了,于是我只能望着她远去的背影。最后2公里的时候我感觉脚有点沉,这时候想着我跑得每一步都是PB,一定要坚持住啊。最终以净成绩01:56:32成功PB了。
横店马拉松的奖牌是木头做的生肖奖牌,六个奖牌可以拼接一个环,不过可能不怎么值钱。
本来想着19年上半年就跑2场就可以了,后来看到苏州湾国际马拉松开始报名了就去报了一个,之前每次线下马拉松我都PB了,这次看着苏州湾马拉松的赛道非常平坦,参加人数也不多,非常适合PB,于是我就欣然报名了。苏州湾国际马拉松前身是女王跑,也就是只有女生才能参加的马拉松,这一届开始男生也可以参加了。赛前一天,我去了苏州玩了一下,见识过了传说中的秋裤楼。
2019年5月19日正值比赛当天,气温适宜,简直天助我也。因为苏州湾国际马拉松的比赛路线是折返的,比赛中途跑的很累的时候看到大神们已经折返了,相当鼓舞自己。当女子第一名潘红跑过后其他跑者更是沸腾了。跑到15公里的时候由于体力不支,速度下降严重,有种跑不动的感觉,每次这个时候就会想到“又到了最难熬的6公里了”,想了下还是安全完赛的好,最后就以一个比较慢的速度跑到了终点。最终净成绩是1:58:40,也是我首次没有PB的比赛。
另外苏州湾的奖牌也是非常好看的,如下:
]]>小时候一直认为随着社会的发展,脑力劳动将成为主流的工作方式,从小也被教育着要好好学习,将来成为伟大的人,所以一直认为好好学习和成为伟大的人有着非常重要的关系,就是在这种思维的认知下,渐渐地忽略了体育运动的重要性,以至于一直都很瘦弱。后来觉得运动是一件非常有意义的事,但并没有过多的时间去运动,高中时巴不得每天都出去跑跑步,但是由于学业繁重,这件事一直搁浅着,直到搁浅了三四年才勉强的去实践了。那是大学的一个学期,但是几乎每天晚自习后就去操场跑5圈,后来渐渐地发现一次可以跑到7圈,最强的一次跑了10圈,可能因为这次跑的有点多、也可能临近期末快要考试了,所以从这次跑完就再也没跑了(其实就是懒)。
大学毕业后,我成了一名苦逼的程序员,刚开始的时候几乎都是9105
地工作着,由于高强度的工作,导致身体每况愈下。刚毕业的第一年,长时间地低着头伏案工作,使得有段时间总感觉头沉沉地难受的狠,甚至有点晕眩头疼,后来去了趟医院,医生说这就是长时间久坐并且颈椎压迫导致脑供血不足导致的,需要以后多运动。这使得我对健康的认知上了一个新的台阶,原来总以为大病离我远之又远,没想到低头工作都这么危险!有的时候,即使赚再多的钱,如果没有了健康,那也没有任何用;况且,努力工作并不能让我赚更多的钱;反正工资又不会涨,刚毕业的学生最快涨工资的办法当然是跳槽,而不是努力工作,而这要让其他公司认可的前提条件就是拥有一个好身体。人总不能坐以待毙,在危险来临的时候要想办法化解危险。也就在这个时候我又开始了跑步。西安的夏天是非常炎热的,那天我去了离软件园的不远的唐城墙遗址公园
开始跑步,当出去跑后才发现,原来有那么多的人在跑,甚至一些女生都比我跑的厉害,当时一颗要好好锻炼身体的种子就这样埋在了我的心中。
工作一年后,我离开了古城西安,来到了另一个美丽的城市——杭州。有一次,我和我女朋友秋芹一起去西溪湿地跑步,当初跑了3.5公里,这是我少有的几次跑步超过3公里的时候,还记得当初我在西溪湿地南门的桥上喘着租气的感觉,虽然有种快要不行的感觉但是很爽,看了一眼秋芹的样子,一脸鄙视的眼神:“不就是3公里嘛,竟然累成这样!”每次跑完步,总有种成功的喜悦,但是看到别人比自己厉害的时候总是有点自尊心受到打击的感觉,就像贝吉塔一样。
有一天秋芹告诉我,杭州马拉松要开始报名了,希望我跟她一起参加一个。一想到马拉松那么虐,我是肯定不会参加的,秋芹说可以报一个小马,只有7公里。我想了想7公里,那么远!!!我最多也就能跑三四公里,7公里岂不是要老命吗?我坚决不参加,并且说服秋芹也不要去参加,毕竟一下跑这么多不好,但是秋芹还是没听我的自己去参加了。杭马那天,我也早早的跟着秋芹去现场了,秋芹作为选手参加了7公里的小马,我作为观众去给她加油,在现场看到那么多形形色色的选手参加马拉松,感觉马拉松也挺好的,杭马鸣枪后,看着浩浩荡荡的人群跑向前方非常的震撼!秋芹开跑后我就去黄龙体育场里面看看有没有什么好玩的,结果发现里面有好多可以薅羊毛的地方,比如免费吃肯德基的炸鸡,免费喝矿泉水,最好的羊毛还要数康师傅的方便面,本来早上也没吃多少东西,结果现场可以免费吃泡好的方便面,吃了一碗不禁有王境泽的感慨:真香!反正是免费的,吃了一碗再来一碗,当我第二碗吃完的时候我看到一个跑小马的7旬老大爷已经跑完了,看了下时间已经过去了30多分了,也差不多跑完了,正当我还在震惊老大爷都这么猛的时候,秋芹也跑完了!我还说她挺厉害的,她说:“小马就是个打酱油的,好多情侣都走着拍照呢…”。这个时候我才意识到原来我恐惧的小马,竟然是这么菜的级别。这个时候我暗暗下定决心,第二年的杭马我一定要了,而且还要是一个半马!
杭马事件后我开始真正意义上的跑步了,跑了几次后我可以半小时左右跑完5公里。那段时间非常开心,每次跑到keep提醒说打破了个人最远跑步记录才停下,虽然每次只打破了十几米,很开心这种能突破自己的感觉,正像西湖区体育馆挂的一个横幅:“跟别人相比我永远都是最差的,跟自己相比我每次都有提高!”。又跑了几次后我发现5公里并没有那么大的难度,当时信心爆棚,刚好keep举办了首个线上赛活动,我一时兴起就报了一个半程马拉松。
2017年11月26日,我打算征战自己第一个半马,这天阳光明媚,我选择当初跑3.5公里的那个地方(西溪湿地)作为我首个半马的场地,绕西溪湿地一圈大概需要16公里,这就需要我有点折返的地方,后来我打算跑进福堤里面然后从南门再绕过来。
如果只是为了完成马拉松的话,那么最简单的一个办法就是慢,只要够慢基本上就可以跑下来,正因为有这办法所以我才敢去挑战。刚开始的配速基本上是6分30秒,就这样跑了10公里,感觉还好。13公里的时候感觉脚有点沉,渐渐地感觉迈向了一个极限。16公里的时候,我的内心在挣扎:“这是我这辈子最艰难的5公里!”当时每一步都感觉很艰难,路上没有补给,又累有渴,感觉马拉松真不是人跑的。慢慢的4公里,3公里,2公里,最后一公里了,当时连走的心都有了,但还是坚持着,咬咬牙,21公里跑到了,当我停下来的时候,Keep说我已经打破了自己的半马记录,当时终于有种如释重负的感觉。这次跑步总的来说还是经验太少了,跑前没有足够的热身,完了也没有更好的拉伸,跑前能量储备也不多,这次跑完膝盖也疼了好一段时。前前后后缓了将近1个月才恢复如初。最终成绩是2:23:29。自己还是挺满意的,毕竟完成了自己的首个半马目标。
函数式编程是一种范式,我们能够以此创建仅依赖输入就可以完成自身逻辑的函数。这保证了当函数多次调用时仍然返回相同的结果。函数不会改变任何外部环境的变量,这将产生可缓存,可测试的代码库。
引用透明性:函数对于相同的输入都将返回相同的值。
纯函数:相同的输入返回相同输出的函数,该函数不应依赖任何外部变量,也不应改变任何外部变量。
高阶函数:接收函数作为参数或者返回函数作为输出的函数。
高阶函数举例(为了讲清楚内容,这里的函数都是低效的):
every(数组通过计算,若所有元素是否为true,则为true)
1 | const every = (arr,fn) => { |
some(数组通过计算,只要有一个为true,那么结果为true)
1 | const some = (arr,fn) => { |
unless(如果传入值是false时,则执行函数)
1 | const unless = (predicate,fn) => { |
times(从0开始迭代多少次)
1 | const times = (times, fn) => { |
闭包就是一个内部函数。如下,其中函数inner被称为闭包函数。
1 | function outer() { |
闭包可访问的作用域:
如下:
1 | let a = "全局作用域"; |
闭包可以记住上下文:
1 | var fn = (arg) => { |
上述代码需要注意closureFn函数已经在fn外部了,但是仍可以访问fn的变量outer,这个是闭包最重要的特征。
高阶函数举例(续):
tap(接收一个value返回一个函数,当函数执行时第一个参数是value)
1 | const tap = (value) => |
unary (将多参函数转化为一个参数的函数)
1 | const unary = (fn) => |
once (函数只运行一次)
1 | const once = (fn) => { |
memoized (函数记忆化)
1 | const memoized = (fn) => { |
map(将数组转化为一个新的数组)
1 | const map = (array,fn) => { |
filter(过滤函数)
1 | const filter = (array,fn) => { |
concatAll(数组扁平化,实际上就是我们常用的flatten,作用是将多个数组,合并成一个数组)
1 | const concatAll = (array) => { |
reduce(累计计算)
1 | const reduce = (array, fn, initialValue) => { |
zip(合并两个指定的函数)
1 | const zip = (leftArr,rightArr,fn) => { |
一元函数:只接受一个参数的函数。
二元函数:只接受两个参数的函数。
变参函数:接受可变数量参数的函数。
如下:
1 | const identify = (x) => x; // 一元函数 |
柯里化:柯里化是把一个多参数函数转化为可嵌套的一元函数的过程。
一元柯里化:
1 | const curry = (binaryFn) => { |
多元柯里化:
1 | const curryN =(fn) => { |
回顾上面柯里化的例子,由于柯里化的参数是从左往右的,所以我们不得不定义一个转化函数setTimeoutWrapper将函数转化为多个嵌套函数,也就是curryN调用完curryN(setTimeoutWrapper)
再调用一下返回的函数,并传递参数1000
。那么能否提供一个函数,使得函数某几个参数始终都是相同的呢,这里就用到了偏函数,如下:
1 | const partial = function (fn,...partialArgs){ |
Unix中使用管道符号“|”来组合一些命令,使得前一个命令的输出是后一个命令的输入。如我们要统计某个文本文件中“World”出现的次数,可以使用下面的命令。
1 | cat test.txt | grep "World" | wc |
函数的组合:将一个函数的输出当成另一个函数的输入,最终把两者合并成一个函数。
1 | var compose = (a, b) => (c) => a(b(c)) |
组合多个函数:
1 | const composeN = (...fns) => |
上述组合函数参数是从右往左依次调用的,如果是从左往右那么就叫做管道了,也有称为序列。管道是组合的复制品,唯一修改的地方就是数据流的方向。
1 | const pipe = (...fns) => |
组合满足结合律:compose(f, compose(g, h)) === compose(compose(f, g), h)
函子:函子是一个普通对象(在其他语言中可能是一个类),它实现了map函数,在遍历每个对象值的时候生成一个新的对象。
实际上数组就是函子!下面一步一步实现一个普通的函子:
1 | // 首先定义一个容器 由于需要new一个对象 所以这里没使用箭头函数 |
现在简绍一种新的函子,叫MayBe。MayBe函子是用来处理函数式编程空值问题的,实现如下:
1 | // 定义一个容器 跟上面一样的 就是改了一个名字 |
MayBe函子中每一个map函数都会执行,但是如果某一个map返回的是空,那么它后面的map函数的参数函数就都不会执行了,单map函数仍然会执行。
MayBe函子解决了空值的问题,Either函子解决或运算,Either函子实现如下:
1 | const Nothing = function(val) { |
Either函子在实际应用时,如果值在计算中不再参与计算的时候就使用Either.Nothing
否则使用Either.Some
。
Point函子:Point函子是函子的子集,它具有of方法。
我们写的MayBe函子和Either都实现了of方法,所以这两个都是Point函子。另外我们常用的数组,ES6也新增了of方法,所以它也是Point函子。
Monad也是一种函子,估计你看到Monad这个词你就头大了。此时你的内心:“卧槽!又要学习一个新的函子,真心学不动了,求别更新了!!!”
其实,函子这块就是纸老虎,各种名字天花乱坠,实际上都是很简单的,Monad也不例外,先看看Monad的定义。
Monad就是一个含有chain方法的函子。
是不是纸老虎,在说chain方法之前我们先简单的说一下另一个方法join,上面我们创建MayBe
函子以后最后都要调用.value
来返回真正的值,这里添加一个join方法,如果不为空的时候就返回函子的value
否则返回MayBe.of(null)
,如下:
1 | MayBe.prototype.join = function() { |
我们一般使用MayBe
的时候都会调用map函数的,大多数情况最后一个map调用完我们还会调用上面的join方法来获取value。为了简化最后这两步我们引入了chain方法:
1 | MayBe.prototype.chain = function(f){ |
这就是Monad的全部内容,没错就这!相信你已经理解的很深入了!
我们回顾一下这两节的内容:有map方法的对象就是函子,有of方法的函子就是Point函子,有chain方法的函子就是Monad函子。
本书最后一章介绍了ES6的Generator的使用,这里就简述一下:
1 | // 创建Generator(就是函数名和function之间加一个*) |
考拉兹猜想:任取一个自然数,如果它是一个奇数,那么就把它乘以3再加1;如果是偶数,就把他除以2。将得到的结果再重复上述过程(这个过程也叫考拉兹变换),最后无论你取什么样的自然数,只要多次重复上述步骤,最后值会进入一个4,2,1的循环。
举例:比如选择6,由于6是偶数,所以除以2,得到3;由于3是基数,所以乘以3并加1,此时得到10;由于10是偶数,所以除以2,得到5;后面的序列是16、8、4、2、1、4、2、1、4、2、1…
考拉兹猜想又叫“奇偶归一猜想”或“3n+1”猜想。你能证明或者找出反例吗?
]]>解答:一个家庭中有两个小孩只有4种可能:{男,男}、{男,女}、{女,男}、{女,女}。记事件A为“其中一个是女孩”,事件B为“另一个是男孩”,则:
A={(男,女),(女,男),(女,女)},
B={(男,女),(女,男),(男,男)},AB={(男,女),(女,男)}。
可知,P(A)=3/4,P(AB)=2/4
由条件概率公式:P(BA)=P(AB)/P(A)=(2/4)/(3/4)=2/3
通俗一点讲两个小孩的4种可能中,由于一个孩子已经是女孩了,所以排除{男,男}这种可能。也就是总过有3种可能,而带有男孩的有2种,所以概率是2/3。这里很容易答成1/2,如果题目修改为“一个家庭中生了一个孩子是女孩,那么再生一个是男孩的概率是多少?”,这种情况概率就是1/2了,两者的不同是原题是一个条件概率事件,而修改后的题目是两个独立事件。
2、假设一个班有50个同学,那么他们中有人生日相同的概率是多少?(假设一年有365天,即不考虑闰年的情况)
解答:直接上答案,约等于97%!!
我们先考虑简单的情况,如果房子里有1个人,那么其他人与他生日相同的概率,很显然是0,因为就没有其他人。
另一个极端情况,如果房子里有366个人,由于一年只有365天,那么至少有1人会跟其他人生日一样,所以有人生日相同的概率是1。
慢慢推到,如果房子里有2个人,两者生日各不相同的概率很显然是364/365,那么两者有生日相同的概率就是1-364/365。
我们再推广到三个人,第三个人与前两个人生日不相同的概率是363/365,那么三个人生日都不相同的概率是(364/365)*(363/365),此时三者有人生日相同的概率就是1-(364/365)*(363/365)。
貌似你已经发现规律了,如果有n(1~365之间)个人,那么他们生日都不相同的概率是(364/365)*(363/365)*(362/365)…*((365-n)/365),此时n个人有生日相同的概率就是1-(364/365)*(363/365)*(362/365)…*((365-n)/365)。
这个式子懂了,我们就计算吧,为了方便计算我把n个人生日都不相同的概率封装了一个JavaScript方法:
1 | const birthdayProbability = (num) => { |
那么n个人中有人生日相同的概率,就是1 - birthdayProbability(n)
,我们现在算一下n=50的情况,得到的结果是0.9703735795779884
。下面列出一些n的不同时,1 - birthdayProbability(n)
的值:
n | 1 - birthdayProbability(n) |
---|---|
23 | 0.5072972343239857 |
30 | 0.7063162427192688 |
35 | 0.8143832388747153 |
41 | 0.9031516114817354 |
50 | 0.9703735795779884 |
由上可见,每23个人中生日相同的概率竟然超过了50%,是不是跟我们想的有点不一样呢?
]]>