记录一些零碎的片段
另一个博客 https://i.shaoyaoju.org
记录一些零碎的片段
Home Page: https://blog.shaoyaoju.org/
License: Creative Commons Attribution Share Alike 4.0 International
记录一些零碎的片段
另一个博客 https://i.shaoyaoju.org
xingbofeng/xingbofeng.github.io#15
在初始化的时候
首先通过 Object.defineProperty
改写 getter/setter
为 Data
注入观察者能力
在数据被调用的时候,getter
函数触发,调用方(会为调用方创建一个 Watcher
)将会被加入到数据的订阅者序列;
当数据被改写的时候,setter
函数触发,变更将会通知到订阅者(Watcher
)序列中,并由 Watcher
触发 re-render
。后续的事情就是通过 render function code
生成虚拟 DOM
,进行 diff
比对,将不同反应到真实的 DOM
中。
指程序源代码中定义变量的区域,它规定了如何查找变量,即确定当前执行代码对变量的访问权限。JavaScript 采用词法(静态)作用域。
bash
便采用动态作用域。const value = 1
function foo() {
console.log(value)
}
function bar() {
var value = 2
foo()
}
bar() // 结果是?
因为JavaScript采用静态作用域,因此执行foo函数时,先从作用域内部查找是否有变量value:如果没有,则根据书写的位置,查找上一层代码,即value等于1,因此会打印1。
若JavaScript采用动态作用域,执行foo函数时,依然是从foo函数内部查找是否有变量value:如果没有,就从调用函数的作用域,即函数bar内部查找value变量,因此会打印2。
根据 七牛云文档,可构造如下自定义函数
const urlSafeBase64 = str => {
str = Base64.encode(str)
str = str.replace(/\+/g, '-')
str = str.replace(/\//g, '_')
return str
}
改自 https://cloud.tencent.com/developer/article/1006029
Vue 的核心是双向数据绑定与虚拟 DOM (以下称为 vdom),vdom 是树状结构,其节点为 vnode,vnode 与浏览器 DOM 中的 Node 一一对应,通过 vnode 的 elm 属性可以访问到对应的 Node
vdom 是纯粹的 JavaScript 对象,因此操作它十分高效,但是 vdom 的变更最终会变为 DOM 操作,为了实现高效的 DOM 操作,一套高效的虚拟 DOM diff 算法很有必要
Vue 的 diff 算法如图所示,即仅在同级的vnode间做diff,递归地进行同级 vnode 的 diff,最终实现整个 DOM 树的更新
我们在下文中将使用这个简化的例子来讲述 diff 算法的过程
如上图所示,更新前是1到10排列的 Node 列表,更新后是乱序排列的 Node 列表。图中有以下几种类型的变化情况:
简单的 diff 算法可以这样设计:逐个遍历 newVDOM 的节点,找到它在 oldVDOM 中的位置,如果找到了就移动对应的 DOM 元素;如果没有找到说明是新增节点,则新建一个节点插入;遍历完成之后,若 oldVDOM 中还有没有处理过的节点,则说明这些节点在 newVDOM 中被删除了,删除它们即可。
存在一个问题:几乎每一步都需要移动 DOM 的操作,这在 DOM 整体结构变化不大时开销是很大的,实际上 DOM 变化不大的情况在现实中经常发生,很多时候我们仅仅需要变更某个节点的文本而已
上图例子中,存在 oldStart、oldEnd 与 newStart、newEnd 两对儿指针,分别对应 oldVDOM 与 newVDOM 的起点与终点。起止点之前的节点是待处理的节点,Vue 不断对 VNode 进行处理,同时移动指针到其中任意一对起点与终点相遇。处理过的节点 Vue 会在 oldVDOM 与 newVDOM 中同时将它们标记为已处理。Vue 通过以下措施来提升 diff 性能:
头部的同类型节点,尾部的同类型节点
这类节点更新前后位置没有发生变化,所以不用移动它们对应的 DOM
头尾/尾头的同类型节点
这类节点位置明确,不需要花心思查找,直接移动 DOM 就好
处理了以上场景后,一方面一些不需要做移动的 DOM 得到了快速处理,另一方面待处理节点变少、缩小了后续操作的处理范围,性能也得到提升
这是指 Vue 会尽可能复用 DOM,尽可能不发生 DOM 的移动。Vue 在判断更新前后指针是否指向同一个节点,其实不要求它们真实引用同一个 DOM 节点,实际上仅判断指向的是否是同类节点(如2个不同的 DIV,在 DOM 上它们是不一样的,但是它们属于同类节点)。如果是同类节点,那么 Vue 会直接复用 DOM,这样的好处是不需要移动 DOM
在看上面的实例,加入10个节点都是 DIV,那么整个 diff 过程就没有移动 DOM 的操作了
整个 diff 分两部分:
第一部分是一个循环,循环内部是一个分支逻辑,每次循环只会进入其中的一个分支,每次循环会处理一个节点,处理之后将节点标记为已处理(oldVDOM 与 newVDOM 都要标记;若节点只出现在其中一个 VDOM 中,则另一个 VDOM 不需要标记)。标记方法有两种:当节点正好处于 VDOM 的指针处,移动指针将它排除到未处理列表之外即可,否则 Vue 会将其节点设置为 undefined
循环结束后,newVDOM 或 oldVDOM 中还存在未处理的节点,如果是 newVDOM 有未处理的节点,则这些节点是新增节点,做新增处理;若 oldVDOM 中还有未处理节点,则这些是需要删除的节点,相应在 DOM 树种删除即可
整个过程是逐步找到更新前后 VDOM 差异,然后将差异反映在 DOM 树中(即 PATCH)。Vue 的 PATCH 是即时的,并不是打包所有修改、最后一起操作 DOM(React 中则是将更新放入队列后集中处理)。现代浏览器对这样的 DOM 操作做了优化,性能上无差异
即 oldStart 与 newStart 指向同类节点的情况,如下图中节点1。这种情况下,将节点1的变更更新到 DOM,然后对其进行标记,标记方法是 oldStart 与 newStart 后移1位即可,过程中不需要移动 DOM,但可能更新 DOM,如属性变更、文本变更等
即 oldEnd 与 newEnd 指向同类节点的情况,如下图中节点10。与情况1类型,这种情况下,将节点10的变更更新到 DOM,然后 oldEnd 与 newEnd前移1位并进行标记,同样不需要移动 DOM
即 oldStart 与 newEnd 以及 oldEnd 与 newStart 指向同类节点的情况,如下图中节点2与9。先看节点2,发生了后移,移动到了 oldEnd 指向的节点(节点9)后,移动之后标记该节点,将 oldStart 后移1位,newEnd 前移一位
操作结束之后如下图:
newStart 来到了节点11的位置,在 oldVDOM 中找不到节点11,说明它是新增的。那么久创建一个新的节点,插入 DOM 树,插到 oldStart指向的节点(节点3)前,然后将 newStart 后移1位标记为已处理(注意 oldVDOM 中没有节点11,所以标记过程中它的指针不需要移动),处理后如下图:
经过第4步后,newStart 来到了节点7的位置,在 oldVDOM 中能找到它而且不在指针位置(查找 oldVDOM 中 oldStart 到 oldEnd 区间内的节点),说明它位置移动了。那么需要在 DOM 树种移动它到 oldStart 指向的节点(节点3)前,与此同时将节点标记为已处理。与前面几种情况不同,newVDOM 中该节点在指针下,可以移动 newStart 进行标记,而在 oldVDOM 中该节点不在指针处,所以采用设置为 undefined 的方式进行标记
处理后就变为下图:
经过第5步处理后,我们看到 newStart 与 oldStart 又指向了同一个节点(节点3),很简单,按照1中的做法只需要移动指针即可,非常高效。3、4、5、6都是如此,处理完成后如下图:
经过前6步处理后(前6步是循环进行的),newStart 跨过了 newEnd,它们相遇啦!而这个时候,oldStart 与 oldEnd 还没有相遇,说明这2个指针之间的节点(包括它们指向的节点,即上图中节点7、8)是此次更新中被删除的节点,那我们在 DOM 树中进行删除。再回到前面,我们对节点7做了标记,标记是为了告诉 Vue 我们已经处理过它了,是需要出现在新 DOM 中的节点,不要删除它,所以在这里只需删除节点8
在应用中也可能会遇到 oldVDOM 的起止点相遇了、但是 newVDOM 的起止点没有相遇的情况,这时需要对 newVDOM 中的未处理节点进行处理,这类节点属于更新中被加入的节点,需要将他们插入到 DOM 树中
至此,整个 diff 过程结束了
console.log显示结果时程序已执行完毕,并非是程序正在执行时的结果。要想看到程序正在执行时每一步的结果,把其中的console.log换成alert语句,您就会看到所期待的结果
改自 dwqs/blog#61
有如下题目:
new Promise(resolve => {
resolve(1)
Promise.resolve().then(() => console.log(2))
console.log(4)
}).then(t => {
console.log(t)
})
console.log(3)
// 输出 4 3 2 1
JavaScript 是 单线程 的,即同一时间只能做一件事。为了协调事件、用户交互、脚本、UI 渲染与网络处理等行为以及防止主线程被阻塞,事件循环(Event Loop)应运而生
Event Loop 包含两类:
二者运行是独立的,即每一个 JavaScript 运行的线程环境都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop
事件循环是通过任务队列机制协调的:
在事件循环中,每进行一次循环操作称之为 Tick,每一次 Tick 的任务处理模型是复杂的,关键步骤如下:
阅读规范可知,异步任务分为 Task(MacroTask 宏任务) 与 MicroTask(微任务) 两类,不同的 API 注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行
MacroTask 包括:
MicroTask
注意:
setTimeout、Promise 等 API 便是任务源,而进入任务队列的是它们指定的具体任务,来自不同任务源的任务会进入到不同的任务队列:
console.log("Script Start")
setTimeout(() => {
console.log("timeout1")
}, 10)
new Promise(resolve => {
console.log("promise1")
resolve()
setTimeout(() => console.log("timeout2"), 10)
}).then(() => {
console.log("then1")
})
console.log("Script End")
接着遇到了 console.log()
语句,直接输出 Script Start
。输出之后,Script 任务继续往下执行,遇到了 setTimeout
,它作为一个 MacroTask,会将其任务分发到对应队列中:
Script 任务继续执行,遇到了 Promise
实例,Promise 构造函数中第一个参数是在 new 的时候执行的,执行时其中的参数进入执行栈执行;而后续的 .then
则被分发到 MicroTask 的 Promise 队列中去。因此会输出 Promise1,接着执行 resolve 并将 then1 分发到对应队列
构造函数继续执行,遇到了 setTimeout,然后将其对应的任务分发到对应队列:
Sciprt 任务继续执行,最后输出 Sciprt End
,至此全局任务执行完毕。当执行完一个 MacroTask 后,会检查是否存在 MicroTasks,若存在则执行 MicroTasks 直至清空 MicroTasks
因此,在 Script 执行完毕后,开始查找清空 MicroTask Queue。此时,MicroTasks 中只有 Promise 队列的一个任务 then1,因此直接执行,输出 then1。当所有的 MicroTasks 执行完毕后,表示第一轮循环结束
此后开始第二轮循环,第二轮循环依然从 MacroTask 开始。此时,有两个任务:
取出 timeout1 执行,输出 timeout1。此时 MicroTask Queue 已经没有可执行的任务了,直接开始第三轮循环:
第三轮循环依旧从 MacroTask Queue 开始,此时 MacroTask Queue 只有一个 timeout2,取出并直接输出。此时,MacroTask Queue 与 MicroTask Queue 都没有任务了,因此将不会在输出其它东西。综上,输出结果如下:
Script Start
promise1
Script End
then1
timeout1
timeout2
本文最上方题目:
new Promise(resolve => {
resolve(1)
Promise.resolve().then(() => console.log(2))
console.log(4)
}).then(t => {
console.log(t)
})
console.log(3)
// 输出 4 3 2 1
这段代码流程如下:
为什么 t2 先执行呢?
console.log(1)
setTimeout(() => {
console.log(2)
}, 0)
new Promise(resolve => resolve(3)).then(arg => console.log(arg))
setTimeout(() => {
console.log(4)
}, 300)
console.log(5)
for (var i = 0; i < 10; i++) {
console.log(i)
}
for (let i = 0; i < 10; i++) {
console.log(i)
}
import
与require
差异;怎么导出模块const fn = (f, obj) => f.bind(obj)
每一趟从待排序的数据元素中选择最小(或 最大)的元素作为首元素,直到所有元素排序完为止。
// 待排序数组
let arr = [5, 3, 1, 4, 2]
// 缓存数组长度
const length = arr.length
for (let i = 0; i < length - 1; i++) {
let min = i
for (let j = i + 1; j < length; j++) {
if (arr[j] < arr[min]) {
min = j
}
}
if (min !== i) {
const temp = arr[i] arr[i] = arr[min] arr[min] = temp
}
}
console.log(arr)
改自 https://www.w3cplus.com/javascript/scope-closures.html
4个概念:
[[scope]]
属性[[scope]]
属性JavaScript 中每个函数都表示为一个函数对象(函数实例),因此它拥有对象的属性与方法。除了正常的属性,它还拥有仅供 JavaScript 引擎内部使用、但不能通过代码访问的一系列内部属性。其中一个便是[[scope]]
属性。
内部的[[scope]]
属性,包含了该函数在创建时作用域中的所有对象的集合,该集合成为函数的作用域链(scope chain)。当创建一个函数时,其作用域链中保存的对象,就是在创建该函数时,作用域中所有可访问的数据。
例如以下全局函数:
function add(num1, num2) {
const sum = num1 + num2;
return sum;
}
当定义add
函数后,其作用域链就创建了。函数所在的全局作用域的全局对象被放置到add
函数作用域链([[scope]]
属性)中。
查看下图,可以看到作用域链的第一个对象保存的是全局对象,全局对象中保存了诸如this
、window
、document
以及全局对象中的add
函数(自身)。因此我们可以在全局作用域下的函数中访问window
、this
、全局变量
、函数自身
。
执行以下代码:
const total = add(5, 10);
执行该函数创建一个内部对象,称为 Execution Context。执行上下文 定义了一个函数正在执行时的 作用域 环境。
应当区分执行上下文和函数创建时的作用域链对象[[scope]]
,因为函数定义时的作用域链对象[[scope]]
是固定的,而执行上下文会根据不同的运行时环境变化,且函数每执行一次,都会创建单独的执行上下文,因此多次调用函数便会创建多个执行上下文。一旦函数执行完毕,执行上下文便被销毁。
执行上下文对象有自己的作用域链,当创建执行上下文时,其作用域链将使用执行函数[[scope]]
属性所包含的对象(即函数定义时作用域链对象)进行初始化。这些值将按照它们在函数中出现的顺序复制到执行上下文作用域链中。
随后,在执行其上下文中创建一个 Activation Object 的新对象。这个激活对象保存了函数中所有形参、实参、局部变量、this 指针 等函数执行时函数内部的数据情况。然后将这个激活对象推送到执行其上下文作用域链的顶部。
激活对象是一个可变对象,里面的数据随着函数执行时数据的变化而变化。当函数执行结束之后,执行上下文及其作用域链将被销毁,同时销毁激活对象。但如果存在闭包,激活对象就会以另外一种方式存在(闭包产生原因)。
从左往右看,第一部分为函数执行时创建的执行上下文,它有自己的作用域链;第二部分为作用域链中的对象,索引1
的对象是从[[scope]]
作用域链中复制过来的,索引为0
的对象是在函数执行时创建的激活对象;第三部分是作用域链中的对象的内容:Activation Object 和 Global Object。
函数在执行时,每遇到一个变量,都会去执行上下文的作用域链顶部、执行函数的激活对象开始向下搜索,如果在第一个作用域链(Activation Object)中找到了,那么就返回这个变量;如果没找到,继续向下寻找、直到找到为止。如果在整个执行上下文中都没有找到这个变量,则该变量被认为是未定义的。因此,函数可以访问全局变量且当局部变量与全局变量同名时,将会使用局部变量。
有如下代码:
function assignEvents() {
const id = 'xdi9592';
document.getElementById('save-btn').onclick = function (event) {
saveDocument(id);
}
}
assignEvents
函数为 DOM 元素分配了一个事件处理程序,这个处理函数就是闭包。为了使该闭包访问变量id,必须创建一个特定的作用域链。
形成过程:
assignEvents
函数创建并词法解析后,函数对象assignEvents
的[[scope]]
属性被初始化,作用域链形成。作用域链中包含了全局对象的所有属性与方法(因函数未执行,因此闭包函数未解析)。
assignEvents
开始执行时,创建 执行上下文,在执行上下文的作用域链中创建激活对象,并将激活对象推送到作用域链顶部,在其中保存了函数执行时所有可访问函数内部的数据。激活对象包含变量id
。
当执行闭包时,JavaScript 引擎发现闭包的存在,闭包函数将被解析,为闭包函数创建[[scope]]
属性、初始化作用域链。此时,闭包函数对象的作用域链中有两个对象:assignEvents
函数执行时的激活对象 与 全局对象(如上图)。
图中闭包函数对象的作用域链与assignEvens
函数的执行上下文的作用域链是相同的。因为闭包函数是在assignEvents
函数被执行的过程中被定义并且解析的,而函数执行时的作用域是激活对象,闭包函数被解析时作用域正是assignEvents
作用域链中的第一个作用域对象:激活对象。同时,由于作用域链的关系,全局对象作用域也被引入到闭包函数的作用域链中。
本段落有问题,需要更正
在词法分析时,闭包函数的[[scope]]
属性就已经在作用域链中保存了对assignEvents
函数的激活对象的引用,所以当assignEvents
函数执行完毕后,闭包函数虽然还未执行,但依然可以访问assignEvents
的局部数据,并不是因为闭包函数要访问 因为在词法分析时,闭包函数没有执行,函数内部根本不知道是否要对assignEvents
的局部变量id
,因此当assignEvents
函数执行完毕后,依然保持了对局部变量id
的引用。而是不管是否存在变量引用,都会保存对assignEvents
的激活对象的作用域对象的引用。assignEvents
的局部变量进行访问和操作,所以只能先把assignEvents
的激活对象作用域保存起来,当闭包函数执行时,需要访问assignEvents
的局部变量,那么再去作用域链中查找。
正因为如此,造成了一个副作用:当有闭包引用时,激活对象不会被销毁,因为它仍然被引用。因此,闭包将会需要更多的内存。
闭包函数执行时创建了自己的执行上下文,其作用域链使用了[[scope]]
属性,引用了assignEvents
函数的激活对象与全局对象,并且为闭包本身创建了一个新的激活对象。因此,在闭包函数的执行上下文的作用域链中保存了自己的激活对象、外层函数的 执行上下文 的 激活对象 与 全局对象(如下图所示)。
JavaScript 引擎内部使用 hook 跟踪函数定义与执行上下文的作用域链,在函数执行时,变量标识符按照从上到下的顺序通过作用域链解析。如果在最后没有找到相同的变量标识符,则抛出undefined
的错误。闭包的开销使其作用域链保持了对其执行上下文的激活对象的引用,从而防止激活对象被正常地销毁。因此,闭包函数通常需要更多的内存。
已知有两种解决方案
// 生成卡片
const cardRes = await axios.post(REDIS_API + '/qiniu/watermark/image', {
"originURL": "https://staticfile.shaoyaoju.org/20180613/bg3.jpg",
"paramsData": [
{
"type": "image",
// 小程序码地址
"image": qrcodeURL,
"gravity": "SouthWest",
"dx": "30",
"dy": "20",
"ws": "0.2"
}, {
"type": "image",
// 封面图地址
"image": cover,
"gravity": "North",
"dx": "0",
"dy": "0",
"ws": "1"
}, {
"type": "text",
// 卡片标题
"text": name,
"font": "黑体",
"fontsize": "1500",
"fill": "#000000",
"dissolve": "100",
"gravity": "West",
"dx": "30",
"dy": "200"
}, {
"type": "image",
// 头像地址
"image": avatar,
"gravity": "West",
"dx": "50",
"dy": "0",
"ws": "0.1"
}, {
"type": "text",
// 昵称
"text": nickname,
"font": "黑体",
"fontsize": "800",
"fill": "#333333",
"dissolve": "100",
"gravity": "West",
"dx": "180",
"dy": "-30"
}, {
"type": "text",
// 时间
"text": `${time} 正在浏览『微师大』`,
"font": "黑体",
"fontsize": "800",
"fill": "#908c8c",
"dissolve": "100",
"gravity": "West",
"dx": "180",
"dy": "30"
}
]
})
其中
await axios.post(REDIS_API + '/qiniu/watermark/image', {})
为自己封装的一个“中间件”,用于处理七牛水印接口,过程如下
const getImageWaterMark = async ctx => {
let { originURL, paramsData } = ctx.request.body
let url = originURL + '?watermark/3'
for (let i of paramsData) {
let str = ''
if (i.type === 'image') {
let { image = '', dissolve = '100', gravity = 'SouthEast', dx = '10', dy = '10', ws = '0' } = i
image = urlSafeBase64(image)
dx = String(dx)
dy = String(dy)
ws = String(ws)
str = `/image/${image}/dissolve/${dissolve}/gravity/${gravity}/dx/${dx}/dy/${dy}/ws/${ws}`
}
if (i.type === 'text') {
let { text = '', font = '黑体', fontsize = '240', fill = '#000000', dissolve = '100', gravity = 'SouthEast', dx = '10', dy = '10' } = i
text = urlSafeBase64(text)
font = urlSafeBase64(font)
fill = urlSafeBase64(fill)
str = `/text/${text}/font/${font}/fontsize/${fontsize}/fill/${fill}/dissolve/${dissolve}/gravity/${gravity}/dx/${dx}/dy/${dy}`
}
url += str
}
// 由于最后生成的URL会非常长,便调用了阿里云短网址API将URL缩短
url = await wechat.shortURL(url)
ctx.body = {
url
}
}
urlSafeBase64 可参考https://github.com/juzhiyuan/blog/issues/3
改自 https://segmentfault.com/a/1190000015375217
Observer
,使用 Object.defineProperty()
重写数据的get
、set
,值更新时将调用set
(通知订阅者更新数据)Compile
,深度遍历 DOM 树
,对每个元素的指令模板进行数据替换以及订阅Watcher
用于连接 Observer
与 Compile
,能够订阅并收到每个属性变化的通知,执行指令绑定的相应回调函数,从而更新视图MVVM
入口函数整合以上三者https://jsfiddle.net/juzhiyuan/oh460f3a/82/
window.onload = function() {
new customVue({
el: '#app',
data: {
testDataA: '模仿Vue',
testDataB: '双向数据绑定',
name: '琚致远'
}
})
}
customVue
函数整合数据监听器this._observer()
、指令解析器this._compile()
以及连接Observer
与Compile
的_watcherTpl
的watcher池
function customVue(options = {}) {
// 配置挂载
this.$options = options
// 获取DOM
this.$el = document.querySelector(options.el)
// 数据挂载
this._data = options.data
// watcher池
this._watcherTpl = {}
// 传入数据、执行函数、重写数据的get、set
this._observer(this._data)
// 传入DOM、执行函数、编译模板、发布订阅
this._compile(this.$el)
}
Watcher
函数用于连接Observer
与Compile
:
_compile()
阶段发布订阅function Watcher(el, vm, val, attr) {
// 指令对应的DOM元素
this.el = el
// customVue 实例
this.vm = vm
// 指令对应的值
this.val = val
// DOM获取值:value 获取 INPUT 值、innerHTML 获取 DOM 值
this.attr = attr
// 更新视图
this.update()
}
Watcher.prototype.update = function() {
// 获取 data 的最新值并赋给 DOM、更新视图
this.el[this.attr] = this.vm._data[this.val]
}
使用 Object.defineProperty()
遍历 data 、重写所有属性的 Get、Set 操作,当给对象的某个属性赋值时,将触发 Set,因此我们可以在 Set 中监听数据变化,然后触发 Watch 更新视图
customVue.prototype._observer = function (obj) {
const _this = this
// 遍历数据
Object.keys(obj).forEach(key => {
// 每个数据的订阅池
_this._watcherTpl[key] = {
_directives: []
}
// 获取属性值
let value = obj[key]
// 数据订阅池
const watcherTpl = _this._watcherTpl[key]
// 重写数据的 Get、Set
Object.defineProperty(_this._data, key, {
// 可删除
configurable: true,
// 可枚举(遍历)
enumerable: true,
get() {
console.log(`${key}获取值:${value}`)
// 获取值得时候直接返回
return value
},
set(newValue) {
// 改变值得时候,触发 Set
console.log(`${key}更新值:${newValue}`)
if (value !== newValue) {
value = newValue
// 遍历订阅池、遍历所有订阅的地方(v-model、v-bind、{})并触发this._compile()中发布的订阅 Watcher 更新视图
watcherTpl._directives.forEach(item => item.update())
}
}
})
})
}
Compile
模板编译customVue.prototype._compile = function(el) {
const _this = this
// 获取 #app 的 DOM
const nodes = el.children
// 遍历 DOM节点
for (let i = 0, len = nodes.length; i < len; i++) {
const node = nodes[i]
// 递归、深度遍历DOM树
if (node.children.length) _this._compile(node)
// 若存在 v-model 属性,且元素为 INPUT 或 TEXTAREA,则监听其 INPUT 事件
if (node.hasAttribute('v-model') && (node.tagName = 'INPUT' || node.tagName == 'TEXTAREA')) {
node.addEventListener('input', (function(key) {
// 获取 v-model 绑定的值
const attVal = node.getAttribute('v-model')
// 将 DOM 替换成属性的数据并发布订阅、在 Set 时更新数据
_this._watcherTpl[attVal]._directives.push(new Watcher(node, _this, attVal, 'value'))
return function() {
// INPUT 值改变时,将新值赋给数据、触发 Set-->Watch、更新视图
_this._data[attVal] = nodes[key].value
}
})(i))
}
if (node.hasAttribute('v-bind')) {
var attrVal = node.getAttribute('v-bind')
_this._watcherTpl[attrVal]._directives.push(new Watcher(node, _this, attrVal, 'innerHTML'))
}
const reg = /\{\{\s*([^}]+\S)\s*\}\}/g
const txt = node.textContent
if (reg.test(txt)) {
node.textContent = txt.replace(reg, (matched, placeholder) =>{
// matched匹配的文本节点包括{{}}, placeholder 是{{}}中间的属性名
// 所有绑定watch的数据
let getName = _this._watcherTpl
// 获取对应watch 数据的值
getName = getName[placeholder]
// 没有事件池 创建事件池
if (!getName._directives) getName._directives = []
// 将dom替换成属性的数据并发布订阅 在set的时候更新数据
getName._directives.push(new Watcher(node, _this, placeholder, 'innerHTML'))
return placeholder.split('.').reduce((val, key) => {
// 获取数据的值 触发 Get 返回当前值
return _this._data[key]
},
_this.$el)
})
}
}
}
.element {
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
// 高度一半
margin-top: -50px;
// 宽度一半
margin-left: -50px;
}
.element {
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
// 50% 为自身尺寸一半
transform: translate(-50%. -50%);
}
.element {
width: 100px;
height: 100px;
position: absolute;
left: 50%;
top: 50%;
right: 0;
bottom: 0;
margin: auto;
}
<div class="container">
<div class="box">一些元素</div>
</div>
.container {
display: flex;
justify-content: center;
align-items: center;
}
父级元素设置text-aligin: center;
即可
.container {
display: grid;
grid-template-columns: 50px;
justify-content: center;
}
将接受多个参数的函数 转换为 接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数
缩小适用范围,创建一个针对性更强的函数
fn(a, b, c) --> fn(a)(b)(c)()
ES6 多次柯里化的结果
a => b => c => {xxx}
例如定义函数add
const add = (x, y) => x + y
add(2, 3) // 5
上述方法柯里化后
const add = x => y => x + y
使用方法
add(2)(3)
// 或者
const add2 = add(2)
add2(3) // 5
扩大适用范围,创建一个应用范围更广的函数
fn(a)(b)(c)() --> fn(a, b, c)
预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新周期
保证如果电梯第一个人进来后,10秒后准时运送一次,这个时间从第一个人上电梯开始计时,不等待,如果没有人,则不运行
当页面滚动到底部时,将触发 Ajax 请求新的数据。当页面滚动频繁时,有可能出现之前请求尚未结束、又开始新请求的情况。此时,应当使用函数节流。
const getListData = () => {
// 是否在发送 ajax 请求
let onAjax = false
return cb => {
if (!onAjax) {
onAjax = true
$.get('/XXX', data => {
// 将新数据 push 到列表内
cb(data)
onAjax = false
})
}
}
}
$(window).scroll(() => {
if (滚动到加载新数据的范围之内) {
getListData()
}
})
function throttle(fn, delay) {
let last
let deferTimer
return function(args) {
let that = this
let _args = arguments
let now = +new Date()
if (last && now < (last + delay)) {
clearTimeout(deferTimer)
deferTimer = setTimeout(function() {
last = now
fn.apply(that, _args)
}, delay)
} else {
last = now
fn.apply(that, _args)
}
}
}
当调用动作T时间后,才会执行该动作,若在这T时间内又调用此动作则将重新计算执行时间
如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。
function debounce(fn, wait) {
var timer = null
return function(){
clearTimeout(timer)
timer = setTimeout(()=>{
fn()
}, wait)
}
}
function log() {
console.log(1)
}
window.onscroll = debounce(log, 500)
// 支持 Promise 的 防抖方法
const debounce = (fn, ms = 0) => {
let timer = null
let resolves = []
return (...args) => {
clearTimeout(timer)
timer = setTimeout(() => {
let result = fn(...args)
resolves.forEach(r => r(result))
resolves = []
}, ms)
return new Promise(r => resolves.push(r))
}
}
解决函数频繁执行产生的性能问题
在保持主逻辑代码不变的前提下,进行额外的功能扩展
const service = () => {
console.log('函数 service 开始执行')
}
const test = (() => {
let timeStart
return {
before: () => {
timeStart = (+new Date())
console.log('计时开始')
},
after: () => {
const end = (+new Date()) - timeStart
console.log('计时结束,用时:', end)
}
}
})()
const aop = (fn, proxy) => {
proxy.before && proxy.before()
fn()
proxy.after && proxy.after()
}
aop(service, test)
// 计时开始
// 函数 service 开始执行
// 计时结束:1
将 函数作为参数 或 返回值是函数 的 函数
改自 https://scotch.io/bar-talk/copying-objects-in-javascript
一个对象就是一组属性值的集合,一组属性值关联着一对 Key 与 Value。在 JavaScript 中,几乎所有的对象都来自于 Object
,它在原型链的顶端
分配符(=
)并不会真正拷贝一个对象,它仅仅拷贝了对象的 引用:
let obj = {
a: 1,
b: 2
}
let copy = obj
obj.a = 5
console.log(copy.a) // 输出 5
变量copy
引用该对象,因此对象{a: 1, b: 2}
标明:有两种方式(obj
或者copy
)操作我,不管操作哪一个都会改变我
循环原对象,拷贝它的每一个属性:
function copy(mainObj) {
let objCopy = {}
let key
for (key in mainObj) {
objCopy[key] = mainObj[key]
}
return objCopy
}
const mainObj = {
a: 2,
b:5,
c: {
x: 7,
y: 4
}
}
console.log(copy(mainObj))
objCopy
存在一个不同于对象mainObj
原型的Object.prototype
,这不是我们想要得到的。我们希望拷贝一个完整的原对象false
的writable
描述符,在对象objCopy
中将为true
mainObj
的可枚举属性在没有任何引用的情况下复制源顶级属性时,对象被称为浅层复制,并且存在一个源属性,其值为对象并作为引用复制。如果源值是对象的引用,则它仅将该引用值复制到目标对象。
浅拷贝将复制顶级属性,但嵌套对象在原始(源)和副本(目标)之间共享。
Object.assign()
方法用来拷贝一个或多个源对象自身所拥有的(不包含原型链)且可枚举属性到目标对象
let obj = {
a: 1,
b: 2
}
let objCopy = Object.assign({}, obj)
console.log(objCopy) // 输出:{a: 1, b: 2}
objCopy.b = 89
console.log(objCopy) // 输出:{a: 1, b: 89}
console.log(obj) // 输出:{a: 1, b: 2}
上方代码中,我们实现了对obj
的拷贝,当改变了对象objCopy
中b
属性的值为89并打印它后,发现本次修改只针对于对象objCopy
,没有改变对象obj
。这表明我们成功的从原对象生成了一个拷贝对象,并且没有拷贝其引用。
别着急,尽管看上去一切都没问题,但是还记得我们说过的浅拷贝吗?看下面的例子:
let obj = {
a: 1,
b: {
c: 2
}
}
let newObj = Object.assign({}, obj)
console.log(newObj) // 输出:{a: 1, b: {c: 2}}
obj.a = 10
console.log(obj) // 输出:{a: 10, b: {c: 2}}
console.log(newObj) // 输出:{a: 1, {b: {c: 2}}}
newObj.a = 20
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 20, b: { c: 2} }
newObj.b.c = 30
console.log(obj); // { a: 10, b: { c: 30} }
console.log(newObj); // { a: 20, b: { c: 30} }
当执行obj.b.c = 30
后,对象obj
与对象newObj
的对象属性b
的属性c
值均发生了改变,这是因为Object.assign
方法仅实现了浅拷贝。newObj.b
与obj.b
都引用了同一个对象,而不是单独对值进行复制。任何对引用值得改变都会影响引用它的其它对象。
注意:在原型链上的属性或者不可枚举的属性都不会被拷贝
let someObj = {
a: 2
}
let obj = Object.create(someObj, {
b: {
value: 2
},
c: {
value: 3,
enumerable: true
}
})
let objCopy = Object.assign({}, obj)
console.log(objCopy)
对象someObj
在对象obj
原型链上,它不会被拷贝属性 b
是不可枚举属性属性 c
存在可枚举属性描述符,因此它可以被拷贝深拷贝将复制它遇到的每个对象,副本和原始对象不会共享任何内容,因此它将是原始副本。下面我们解决下使用Object.assign()
进行浅拷贝遇到的问题:
JSON.parse(JSON.stringify(object))
这解决了我们之前遇到的问题。 现在newObj.b
有副本而不是引用! 这是深拷贝对象的一种方法:
let obj = {
a: 1,
b: {
c: 2
}
}
let newObj = JSON.parse(JSON.stringify(obj))
obj.b.c = 20
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }
但是上述方法不适用于拷贝用户定义的对象内方法
方法是作为函数对象的属性,我们尝试拷贝对象内方法:
let obj = {
name: 'jjzhiyuan',
exec: function() {
return true
}
}
let method1 = Object.assign({}, obj)
let method2 = JSON.parse(JSON.stringify(obj))
console.log(method1)
// 输出
/*
* {
* exec: function exec() {
* return true
* },
* name: "jjzhiyuan"
* }
*/
console.log(method2)
// 输出
/*
* {
* name: "jjzhiyuan"
* }
*/
结果显示:Object.assign()
方法可以用于拷贝对象内方法,JSON.parse(JSON.stringify(obj))
无法拷贝
指存在引用自身属性的对象,我们尝试实现对其的拷贝:
JSON.parse(JSON.stringify(object))
let obj = {
a: 'a',
b: {
c: 'c',
d: 'd',
},
}
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
let newObj = JSON.parse(JSON.stringify(obj));
console.log(newObj);
输出如下:
因此 JSON.parse(JSON.stringify(obj))
不能用于拷贝嵌套对象
Object.assign()
let obj = {
a: 'a',
b: {
c: 'c',
d: 'd',
},
}
obj.c = obj.b;
obj.e = obj.a;
obj.b.c = obj.c;
obj.b.d = obj.b;
obj.b.e = obj.b.c;
let newObj2 = Object.assign({}, obj);
console.log(newObj2);
输出如下:
Object.assign()
可以实现对嵌套对象的浅拷贝
const array = [
"a",
"c",
"d", {
four: 4
},
];
const newArray = [...array];
console.log(newArray);
// 输出
// ["a", "c", "d", { four: 4 }]
对象初始值设定项中的Spread属性将源对象中的自身可枚举属性复制到目标对象上:
let obj = {
one: 1,
two: 2,
}
let newObj = { ...obj }
// { one: 1, two: 2 }
改自 https://www.haorooms.com/post/js_neicun_hjcthis
有如下代码:
let a = 20
let b = a
b = 30
console.log(a) // 应输出多少?
let m = {
a: 10,
b: 20
}
let n = m
n.a = 15
console.log(m.a) // 应输出多少?
let a = 20
let obj = {
a: 10,
c: this.a + 20,
fn: function () {
return this.a
}
}
console.log(obj.c) // 应输出多少?
console.log(obj.fn()) // 应输出多少?
let _obj = obj.fn
console.log(_obj()) // 应输出多少?
JavaScript 并没有严格意义地区分栈内存与堆内存
var a = 1;
var b = true;
console.log(a == b); // true 只进行值的比较
console.log(a === b); // false 不仅进行值得比较,还要进行数据类型的比较
var obj1 = {}; // 新建一个空对象 obj1
var obj2 = {}; // 新建一个空对象 obj2
console.log(obj1 == obj2); // false
console.log(obj1 === obj2); // false
因此上方题目可表示为
为引用复制,n 与 m 指向同一个对象,修改了堆内的对象后,输出为:15。
针对函数调用:
obj 是全局声明,在非严格模式下,this 指向全局对象,因此:
this.a + 20
输出:40
fn() 是 obj 对象下的函数,this 指向 obj,输出:10
_obj() 函数独立调用,this 指向全局,输出:20
Node.js 基于 非阻塞I/O 与 事件驱动 模型
改自 http://www.ruanyifeng.com/blog/2014/10/event-loop.html
参照 #41
单线程即同一时间只能做一件事,这与它最初的用途有关。早期作为浏览器脚本语言,主要用于与用户交互、操作 DOM。这决定了它只能是单线程,否则(多线程)会带来不可预测的同步问题。
假设 JavaScript 有两个线程A、B,A线程在某个 DOM 节点上添加内容,B线程在这个DOM节点上删除内容,这时浏览器该以哪个线程为准?因此,JavaScript 从诞生时就是单线程。
为了利用多核CPU计算能力,HTML5 提出了 Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM,因此本质上还是单线程。
单线程意味着所有任务需要排队,前一个任务结束、才会执行下一个任务。如果前一个任务耗时很长,后一个任务不得不等着。
如果排队是因为计算量大,CPU忙不过来,那可以理解;但是CPU通常是闲着的,因为IO设备很慢(例如ajax从网络读取数据),不得不等着结果出来,再往下执行。
JavaScript 的设计者意识到:主线程可以不管IO设备并挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回结果,再去执行被挂起的任务。
因此,任务可以分两种:同步任务与异步任务。
异步执行运行机制如下(同步执行也是如此,因为它可以被视为没有异步任务的异步执行)
只要主线程空了,就会重复不断地去读取任务队列,这就是JavaScript的运行机制。
任务队列是一个事件队列,IO设备完成一项任务,就在任务队列中添加一个事件,表示相关异步任务可以进入执行栈了。主线程读取任务队列,就是读取有哪些事件。
任务队列中的事件,除了IO设备的事件以外,还包括一些用户产生的事件,如 鼠标点击、页面滚动等。只要指定过回调函数,这些事件发生时就会进入任务队列,等待主线程读取。
所谓回调函数,就是会被主线程挂起的代码。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
任务队列是一个先进先出的数据结构,排在前面的事件会优先被主线程读取。主线程读取过程是自动的,只要执行栈清空,任务队列上第一位的事件就自动进入主线程。但是,由于存在后文提到的定时器功能,主线程首先要检查执行时间,某些事件只有到了规定的时间,才能返回主线程。
主线程从任务队列中读取事件,这个过程是循环不断的,因此这种运行机制被称为Event Loop。
上图中,主线程运行时,产生堆 与 栈,栈中代码调用各种外部API,它们在任务队列中添加各种事件(click、load、done)。只要栈中代码执行完毕,主线程就会去读取任务队列,依次执行那些事件对应的回调函数。
执行栈中的代码(同步任务),总是在读取任务队列(异步任务)之前运行。请看下面的例子:
const req = new XMLHttpRequest()
req.open('GET', url)
req.onload = function () {}
req.onerror = function () {}
req.send()
上面代码中的req.send
方法是ajax向服务器发送数据,它是一个异步任务,意味着只有当前脚本所有代码执行完毕后,系统才会去读取任务队列。因此,它与下面的写法等价:
const req = new XMLHttpRequest()
req.open('GET', url)
req.send()
req.onload = function () {}
req.onerror = function () {}
即指定回调函数的部分(onload与onerror),在send()方法的前后都可以。因为它们属于执行栈的一部分,系统总是执行完它们才会去读取任务队列。
除了放置异步任务的事件,任务队列还可以放置定时器事件,即指定某些代码在多少时间之后执行。
定时器功能主要由**setTimeout()与setInterval()**这两个函数完成,它们的内部运行机制完全一样,区别在于前者指定的代码是一次性执行,后者则为反复执行。
以下主要讨论setTimeout():
setTimeout()接受两个参数,第一个是回调函数,第二个是推迟执行的毫秒数:
console.log(1)
setTimeout(function() {
console.log(2)
}, 1000)
console.log(3)
上方执行结果输出为1、3、2,因为setTimeout()将第二行推迟到1000ms之后才执行。
如果将setTimeout()的第二个参数设置为0,则表示当前代码执行完(执行栈清空)以后,立即执行(0ms间隔)指定的回调函数。(仍旧是最后执行setTimeout)
setTimeout(function() {
console.log(1)
}, 0)
console.log(2)
上面代码输出总是2、1,因为只有在执行完下方代码后,才会去执行任务队列中的回调函数。因此,setTimeout(fn, 0)的含义是:指定某个任务在主线程最早可得的空闲时间执行。它在任务队列的尾部添加一个事件,因此要等到同步任务和任务队列现有的事件都处理完,才会得到执行。
HTML5 规定了 setTimeout() 的第二个参数的最小值(最短间隔),不得低于4ms,若低于4ms,则自动增加。另外,对于DOM的变动(尤其是设计页面重新渲染的部分),通常不会立即执行,而是每16ms执行一次。这时使用requestAnimation()的效果好于setTimeout()
需要注意的是,setTimeout()只是将事件插入了任务队列,必须等到当前代码(执行栈)执行完,主线程才会执行它指定的回调函数。如果当前代码耗时很长,有可能要等很久,所以没办法保证回调函数一定会在setTimeout()指定的时间执行。
const qiniu = require('qiniu')
const axios = require('axios')
/*
* 获取二维码
* https://developers.weixin.qq.com/miniprogram/dev/api/qrcode.html
*/
const genQRcode = async ({
// B接口 生成无限量的短路径小程序码
type = 'limit',
path = '',
scene = '',
width = 430,
auto_color = true,
is_hyaline = false
}) => {
// 在此处获取 AccessToken,这是自己封装的获取微信 AccessToken 函数
const { access_token } = await getAccessToken()
// 发起请求,生成小程序码
const qrCodeRes = await axios.post(`https://api.weixin.qq.com/wxa/getwxacodeunlimit?access_token=${access_token}`, {
// 不同类型的获取二维码接口 此处的参数是不一样的
page: path,
scene,
width,
auto_color,
is_hyaline
}, {
// 一定要添加返回类型
responseType: 'stream'
})
const { hash, key } = await qiniu.uploadReadableStream(qrCodeRes.data)
return {
hash, key
}
}
七牛云上传文件流
/**
* 文件流上传
* https://developer.qiniu.com/kodo/sdk/1289/nodejs#form-upload-stream
*/
const uploadReadableStream = readableStream => {
return new Promise((resolve, reject) => {
const config = new qiniu.conf.Config()
config.zone = qiniu.zone.Zone_z0
config.useHttpsDomain = true
config.useCdnDomain = true
// 自己封装的 获取上传凭证的函数,它将返回 Token 与唯一的 Key(UUID)
const { uptoken, uuid } = getUploadToken()
const formUploader = new qiniu.form_up.FormUploader(config)
const putExtra = new qiniu.form_up.PutExtra()
formUploader.putStream(uptoken, uuid, readableStream, putExtra, (respErr, respBody, respInfo) => {
if (respErr) {
reject(respErr)
} else {
resolve(respBody)
}
})
})
}
七牛云 获取上传凭证
const uuid = require('uuid/v1')
/*
* 七牛云 获取上传凭证
*/
const getUploadToken = () => {
// putPolicy规则请查阅 七牛云 Node.js SDK 文档
const uptoken = putPolicy.uploadToken(mac)
return {
uuid: uuid(),
uptoken
}
}
需要注意的是,在获取小程序码时
responseType: 'stream'
由于浏览器的同源策略限制,在目标域名、端口、协议不一致的情况下,将无法访问目标提供的资源。但是,HTML某些标签提供的src属性,不受该策略影响,因此可以通过该种方式获取目标资源。
需要服务端配合
<script type="text/javascript">
const customFn = function (data) {
// 在获取到目标资源后,将调用该函数
console.log(data)
}
// 目标API
const targetURL = 'https://api.shaoyaoju.org/jsonp?callback=customFn';
// 动态创建 script 标签,并设置 src 属性
const script = document.createElement('script');
script.setAttribute('src', targetURL);
// 将 script 标签插入 body,开始调用目标资源
document.getElementsByTagName('body')[0].appendChild(script);
</script>
const get = async ctx => {
// 获取 callback 参数
const { callback } = ctx.query
// 包裹数据
const data = callback + `({"data": {"key": "value"}})`
ctx.body = data
}
对相邻元素进行比较,顺序相反则对换位置。这样子,每一趟都会将最小(或最大)的元素“浮动”到顶端,最终达到完全有序。
// 初始化数组
let arr = [5, 1, 3, 2, 4]
// 缓存数组长度
const length = arr.length
for (let i = 0; i < length - 1; i++) {
for (let j = 0; j < length - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
const temp = arr[j]
arr[j] = arr[j + 1]
arr[j + 1] = temp
}
}
}
console.log(arr)
https://segmentfault.com/a/1190000008789039
居中在布局中常见,假设 DOM 文档结构如下,子元素要在父元素中居中:
<div class="parent">
<div class="child"></div>
</div>
子元素为行内元素或块状元素、宽度是否一定,采取的布局方案不同,下面进行分析:
text-align: center;
margin
值为 auto
display: inline;
,然后设置父元素为text-align:center;
垂直居中对于子元素是单行内联文本、多行内联文本以及块状元素采用的方案是不同的
height
等于行高line-height
display:table-cell;
或display: inline-block;
,再设置vertical-align: middle;
position: fixed
或 position: absolute;
,然后设置margin: auto;
特征:定宽、水平居中
常见单列布局有两种:
header
、content
、footer
宽度相同,其一般不会占满浏览器最宽宽度,但当浏览器宽度缩小低于其最大宽度时,宽度会自适应header
、footer
宽度为浏览器宽度,但content
不会占满浏览器宽度对于第一种,通过对header
、content
、footer
设置统一的width
或max-width
、并设置margin: auto;
实现居中
<div class="layout">
<div id="header">头部</div>
<div id="content">内容</div>
<div id="footer">尾部</div>
</div>
.layout{
/* width: 960px; *//*设置width当浏览器窗口宽度小于960px时,单列布局不会自适应。*/
max-width: 960px;
margin: 0 auto;
}
对于第二种,heade
r、footer
的内容宽度为100%,但header
、footer
的内容区以及content
统一设置max-width
,并通过margin: auto
实现居中
<div id="header">
<div class="layout">头部</div>
</div>
<div id="content" class="layout">内容</div>
<div id="footer">
<div class="layout">尾部</div>
</div>
.layout{
/* width: 960px; *//*设置width当浏览器窗口宽度小于960px时,单列布局不会自适应。*/
max-width: 960px;
margin: 0 auto;
}
设置两个侧栏分别向左右浮动,中间列通过外边距给两个侧栏腾出空间、其宽度根据浏览器窗口自适应
<div id="content">
<div class="sub">sub</div>
<div class="extra">extra</div>
<div class="main">main</div>
</div>
margin-left
的值为左侧栏宽度;margin-right
的值为右侧栏宽度.sub {
width: 100px;
float: left;
}
.extra {
width: 200px;
float: right;
}
.main {
margin-left: 100px;
margin-right: 200px;
}
注意:
这种布局简单明了,但是渲染时先渲染了侧边栏,而不是较为重要的主面板
若是左边带有侧栏的二列布局,则去掉右侧栏,不要设置主面板margin-right
值;其它操作相同,反之亦然
通过绝对定位,将两个侧栏固定,同样通过外边距给两个侧栏腾出空间,中间列自适应
<div class="sub">left</div>
<div class="main">main</div>
<div class="extra">right</div>
top: 0;
,设置左侧栏left: 0
、右侧栏right: 0;
margin-left
值为左侧栏宽度;margin-right
值为右侧栏宽度.sub, .extra {
position: absolute;
top: 0;
width: 200px;
}
.sub {
left: 0;
}
.extra {
right: 0;
}
.main {
margin: 0 200px;
}
margin-right
值,其他操作相同。反之亦然主面板设置宽度为100%,主面板与两个侧栏都设置浮动,常见为左浮动。这时两个侧栏会被主面板挤压下去,通过负边距将浮动的侧栏拉上来,左侧栏的负边距为100%,刚好是窗口的宽度,因此会从主面板下面的左边跑到与主面板对齐的左边、右侧栏此时浮动在主面板下面的左边,设置负边距为负的自身宽度刚好浮动为左右侧栏的宽度,给侧栏腾出空间,此时主面板宽度减小
由于侧栏的负margin 都是相对主面板的,两个侧栏并不会停靠在左右两边,而是跟着缩小的主面板一起向中间靠拢,此时使用相对布局,调整两个侧栏到相应位置
<div id="bd">
<div class="main"></div>
<div class="sub"></div>
<div class="extra"></div>
</div>
.main {
float: left;
width: 100%;
}
.sub {
float: left;
width: 190px;
margin-left: -100%;
position: relative;
left: -190px;
}
.extra {
float: left;
width: 230px;
margin-left: -230px;
position: relative;
right: -230px;
}
#bd {
padding: 0 230px 0 190px;
}
注意:
双飞翼布局和圣杯布局的**有些相似,都利用了浮动和负边距,但双飞翼布局在圣杯布局上做了改进,在main元素上加了一层div, 并设置margin,由于两侧栏的负边距都是相对于main-wrap而言,main的margin值变化便不会影响两个侧栏,因此省掉了对两侧栏设置相对布局的步骤
<div id="main-wrap" class="column">
<div id="main">#main</div>
</div>
<div class="sub"></div>
<div class="extra"></div>
.main-wrap {
float: left;
width: 100%;
}
.sub {
float: left;
width: 190px;
margin-left: -100%;
}
.extra {
float: left;
width: 230px;
margin-left: -230px;
}
.main {
margin: 0 230px 0 190px;
}
注意:
类似于现实生活中的工厂,可产生大量相似的商品、实现同样的效果
const CreatePerson = (name, age, sex) => {
const obj = new Object()
obj.name = name
obj.age = age
obj.sex = sex
obj.sayName = function () {
return this.name
}
return obj
}
const personA = new CreatePerson('琚致远', '21', '男')
const personB = new CreatePerson('晨阳', '18', '女')
console.log(personA.name) // 琚致远
console.log(personA.age) // 21
console.log(personA.sex) // 男
console.log(personA.sayName()) // 琚致远
console.log(personB.name) // 晨阳
console.log(personB.age) // 18
console.log(personB.sex) // 女
console.log(personB.sayName()) // 晨阳
// 类型均为 object,无法识别对象类型、无法识别它们属于哪个对象的实例
console.log(typeof personA) // object
console.log(typeof personB) // object
console.log(personA instanceof Object) // true
将其成员对象的实例化推迟到子类中,子类可以重写父类接口方法以便创建的时候指定自己的对象类型
// 父类
const BicycleShop = () => {}
BicycleShop.prototype = {
constructor: BicycleShop,
/**
* 卖自行车
* @param {model} 自行车型号
*/
sellBicycle: function (model) {
const bicycle = this.createBicycle(model)
// 执行 A 业务逻辑
bicycle.A()
// 执行 B 业务逻辑
bicycle.B()
return bicycle
},
createBicycle: function (model) {
throw new Error('父类是抽象类,不可直接调用,需要子类重写该方法')
}
}
// 自行车构造函数
function BicycleShop (name) {
this.name = name
this.method = () => {
return this.name
}
}
BicycleShop.prototype = {
constructor: BicycleShop,
/**
* 卖自行车
* @param {model} 自行车型号
*/
sellBicycle: function (model) {
const bicycle = this.createBicycle(model)
// 执行 A 业务逻辑
bicycle.A()
// 执行 B 业务逻辑
bicycle.B()
return bicycle
},
createBicycle: function (model) {
throw new Error('父类是抽象类,不可直接调用,需要子类重写该方法')
}
}
/**
* 实现原型继承
* @param { Sub } 子类
* @param { Sup } 超类
*/
const extend = (Sub, Sup) => {
// 定义空函数
const F = function () {}
// 设置空函数原型为超类的原型
F.prototype = Sup.prototype
// 实例化空函数,并将超类原型引用传递给子类
Sub.prototype = new F()
// 重置子类原型的构造器为子类本身
Sub.prototype.constructor = Sub
// 在子类中保存超类原型,避免子类与超类耦合
Sub.sup = Sup.prototype
// 检查超类原型的构造器是否为原型自身
if (Sup.prototype.constructor === Object.prototype.constructor) Sup.prototype.constructor = Sup
}
function BicycleChild (name) {
this.name = name
// 继承构造函数父类中的属性与方法
BicycleShop.call(this.name)
}
// 子类继承父类原型方法
extend(BicycleChild, BicycleShop)
// BicycleChild 子类重写父类方法
BicycleChild.prototype.createBicycle = () => {
const A = () => {
console.log('执行 A 业务逻辑')
}
const B = () => {
console.log('执行 B 业务逻辑')
}
return {
A,
B
}
}
const childClass = new BicycleChild('飞鸽')
console.log(childClass)
产生一个“类”的唯一实例(注意:JavaScript 中并没有真正的类)
有如下场景:点击某个按钮的时候,弹出遮罩层,比如web.qq.com点击登录的时候
// 生成灰色背景遮罩层
var createMask = function() {
return document.body.appendChild(document.createElement("div"));
}
$('button').click(function() {
var mask = createMask();
mask.show();
})
上方代码的问题是:这个遮罩层是全局唯一的,每次调用createMask
都会创建一个新的div
,虽然可以在遮罩层隐藏的时候将它移除掉,但这是不合理的。
在页面一开始就创建好这个div
,然后用一个变量引用它。
var mask = document.body.appendChild(document.createElement('div'));
$('button').click(function() {
mask.show()
})
这样确实在页面只会创建一个遮罩层div
,但是问题是:我们或许永远也不会使用它,因此就会浪费一个div
,对dom
节点的任何操作应当是吝啬的。
如果借助一个变量,来判断是否已经创建过div
呢?
var mask;
var createMask = function() {
if (mask) return mask;
mask = document.body.appendChild(document.createElement('div'));
return mask;
}
上方代码完成了一个产生单例对象的函数,不过我们分析下它有什么不妥:
首先,这个函数存在一定副作用,函数体内改变了外界mask
的引用,在多人协作时,createMask
是一个不安全的函数;此外,mask
这个全局变量并不是必须非要不可,修改如下:
var createMask = (function() {
var mask;
return function() {
return mask || (mask = document.body.appendChild(document.createElement('div')));
}
})()
用 闭包 将变量mask
包起来,至少对函数createMask
来讲,它是封闭的。上方的单例模式还是有缺点的,它只能用于创建遮罩层,加入我又写了一个函数,用来创建唯一的XHR对象,能不能找到一个通用的单例包装器?
JavaScript 中,函数是第一型,意味着函数可以当做参数来传递,以下为最终代码:
var wrapper = function(fn) {
var result;
return function() {
return result || (result = fn.apply(this, arguments));
}
}
var createMask = wrapper(function() {
return document.body.appendChild(document.createElement('div'));
})
用一个变量来保存第一次返回值,如果它已经被赋值过了,那么在以后的调用中优先返回该变量,而真正创建遮罩层的代码是通过回调函数的方式传入到wrapper包装器
的,这种方式叫桥接模式。
然而wrapper函数
还是不完美,它始终需要一个变量result
来寄存div
的引用,遗憾的是JavaScript 的函数式特性不足以完全消除声明与语句。
注意:
先从数列中选择一个数作为基准数
分区:将比这个数大的数放在其右边、小于或等于它的数放在其左边
对左右区间重复第二步,直到各区间只有一个数
const quickSort = arr = >{
if (arr.length <= 1) return arr
// 中间值的索引
const middleIndex = Math.floor(arr.length / 2)
// 中间值
const middle = arr.splice(middleIndex, 1)
// 分为左右两数组
const left = []
const right = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] < middle) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat(middle, quickSort(right))
}
console.log(quickSort([5, 1, 4, 2, 3]))
为了理解事件委托是什么,你首先需要理解什么是事件监听:事件监听指监听一个事件,当触发时将产生一些行为。
以下是一些常见的 JavaScript 事件:
为了给 HTML 某个元素添加事件监听,你需要使用 addEventListener 方法
如下是事件监听的例子:
const character = document.getElementById("disney-character");
character.addEventListener("click", showCharactersName);
第一部分:document.getElementById
这部分是 event target,在上述例子中指目标元素。但是在 JavaScript 中,event target 形式多种多样:在上述例子中,它是 HTML 元素
;在网页被加载到浏览器中,它可以是文档(document)本身,甚至可以是窗口(window);在客户端 JavaScript 中,它可以是包含所有内容的顶层对象。但是在大多数情况下,它就是 HTML 元素
第二部分:事件监听(event listener)
当用户点击带有 id
属性、值为disney-character
的 HTML 元素时,将触发事件监听并调用showCharactersName
函数
事件监听被设置在页面加载时,因此当你打开一个网站时,浏览器下载、读取、运行 JavaScript
const character = document.getElementById("disney-character");
character.addEventListener("click", showCharactersName);
上方代码中,当页面加载时,事件监听查找id
属性值为disney-character
的元素并且设置它的点击事件,当页面加载时若元素存在,那么上方代码将没有问题。但是当页面加载后,又添加了新的元素时,事件监听该怎么实现呢?
事件委托解决了上述问题,为了理解事件委托,我们先看下方的Disney Charaters
列表
上方列表有一些基本的函数方法,可以实现添加人物、勾选人物等功能
这个列表是动态的,当页面加载完毕后,输入值(Mickey、Minnie、Goofy)将被添加进去,但是这将不能够为它们添加事件监听。
查看下方代码:
const checkBoxes = document.querySelectorAll("input");
checkBoxes.forEach(input => input.addEventListener("click", () => alert("Hi"))});
// 当点击某个人物时,应当弹出 Hi 的提示
但是我们看下页面加载时的 HTML:
<ul class="characters">
</ul>
页面加载后的 HTML:
<ul class=”characters”>
<li>
<input type=”checkbox” data-index=”0" id=”item0">
<label for=”item0">Mickey</label>
</li>
<li>
<input type=”checkbox” data-index=”1" id=”item1">
<label for=”item1">Minnie</label>
</li>
<li>
<input type=”checkbox” data-index=”2" id=”item2">
<label for=”item2">Goofy</label>
</li>
</ul>
// 当页面加载后,输入值被填充进去,但是并没有绑定事件监听
因为上述元素并没有在页面加载时填入,因此点击任意的人物都不会弹出 Hi 的提示,因为它们没有绑定事件监听。
事件委托
事件委托的**是:与其监听(列表的)每一个元素,不如寻找一个在页面加载时就存在的元素
在我们的例子中,class
属性值为characters
的列表将在页面加载时存在,我们可以在这个元素上实现事件监听
<ul class=”characters”> // PARENT - ALWAYS ON THE PAGE
<li>
<input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
<label for=”char0">Mickey</label>
</li>
<li>
<input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
<label for=”char1">Minnie</label>
</li>
<li>
<input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
<label for=”char2">Goofy</label>
</li>
</ul>
我们可以将事件委托理解为负责任的父母与粗心的孩子,无论父母说什么,孩子都应当听从。最妙的一点是,如果我们添加更多的孩子(子节点),父母总是有一致的响应,因为父母一直存在(页面加载前后)
我们来实现事件监听:
<ul class="characters">
</ul>
<script>
function toggleDone(event) {
console.log(event.target)
}
const characterList = document.querySelector(".characters")
characterList.addEventListener("click", toogleDone)
</script>
目前我们在一个列表上添加了事件监听,是在characters
上而不是每个子元素
event.target
是对调度事件的对象的引用,即它标志发生事件的 HTML 元素
在我们的例子中,事件便为click
,发生事件的对象是<input />
。label
被认为是input
对象的一部分,因此出现了两次
它不同于上方的console.log(event.target)
当事件遍历 DOM 时,event.currentTarget
标志事件的当前目标,它始终引用事件监听器附加到的元素。 在我们的例子中,事件监听器被附属到列表characters
上,因此我们在控制台看到了它。
因为我们现在知道EVENT.TARGET
标识了事件发生的HTML元素,并且我们也知道我们想要侦听的元素(输入元素),所以在 JavaScript 中解决这个问题相对容易
fucntion toggleDone(event) {
if (!event.target.matches('input')) return
console.log(event.target)
}
基本上上面的代码说明:如果单击的事件目标与输入元素(input)不匹配,则退出该函数;如果单击的事件目标与输入元素(input)匹配,则调用console.log(event.target)
并在该子节点上执行后续JavaScript 代码
这很重要,现在我们可以确信用户单击了正确的子节点,即使在初始页面加载后才加入输入元素
每当用户点击它时,它会一直向上移动到 DOM 的顶部,并在被点击的元素的所有父元素上触发点击事件。你并不总是看到这些点击事件,因为并不是一直在监听这些元素,但确实发生了冒泡事件。
由于它的冒泡性质,事件传播基本上意味着无论何时单击 DOM 上的一个输入,你都可以有效地触发整个 Document 上的事件
查看如下例子:
<div class=”one”>
<div class=”two”>
<div class=”three”>
</div>
</div>
</div>
<script>
const divs = document.querySelectorAll('div');
function logClassName(event) {
console.log(this.classList.value);
}
divs.forEach(div => div.addEventListener('click', logClassName));
</script>
上述例子中:我们有三个DIV
元素:div #1
、div #2
、div #3
。每个div
都有自己的事件监听,当我们在浏览器中点击任意一个div
时,都会调用logClassName()
这个函数
当我点击了 div #3
时,将会执行div #3
、div #2
、div #1
的监听函数,这就是事件冒泡!
<ul class=”characters”> // PARENT -- This is where the listener is!
<li>
<input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
<label for=”char0">Mickey</label>
</li>
<li>
<input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
<label for=”char1">Minnie</label>
</li>
<li>
<input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
<label for=”char2">Goofy</label>
</li>
</ul>
<script>
const characterList = document.querySelector('.characters');
characterList.addEventListener('click', toggleDone);
</script>
回到我们在事件委托部分中的示例 - 我们只有一个事件监听器,它是在我们的无序列表元素上设置的。当我们单击该父 HTML 元素的子元素(input)时,它触发了我们设置的绑定到无序列表的事件监听函数
由于事件冒泡,你可以将事件监听函数放置在位于HTML子节点上方的单个父HTML元素上,并且只要事件发生在任何子节点上,该事件监听函数就会被执行 - 即使这些子节点在页面加载后被添加
如果没有事件委托,则必须将click事件监听器重新绑定到加载到页面的每个新元素,这是复杂且繁琐的。 首先,它会大大增加页面上事件监听器的数量,而更多的事件监听器会增加页面的总内存占用量。 拥有更大的内存占用会降低性能......而糟糕的性能则是一件坏事。 其次,可能存在与绑定和解除绑定事件监听器以及从DOM 中删除元素相关联的内存泄漏问题。
for (let i = 0; i < 10000000000; i++) {
console.log(i)
}
// 第一步:定义变量
let a = 1;
// 第二步:发出指令,然后把回调函数加入异步队列(回调函数并没有执行)
setTimeout(() => {
console.log(a);
}, 0)
// 第三步:赋值,回调函数没有执行
a = 2;
// 第四步:发出指令,然后把回调函数加入异步队列(回调函数并没有执行)
setTimeout(() => {
console.log(a);
}, 0)
// 第五步:赋值,回调函数没有执行
a = 3;
// 当所有代码执行完毕,cpu空闲下来了,就会开始执行异步队列里面的回调函数
// 所以最后控制台输出:3 3
<!DOCTYPE html>
<html>
<head>
<title>The "Click the button" page</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css" />
</head>
<body>
<h1>
Click the button.
</h1>
<button type="button">Click me</button>
<script>
var button = document.querySelector("button");
button.style.fontWeight = "bold";
button.addEventListener("click", function () {
alert("Well done.");
});
</script>
</body>
</html>
浏览器从上到下读取 HTML 标签,并将其分成节点,形成 DOM
当浏览器发现任何与节点有关的样式时(外部、内部、行内),停止渲染DOM,并利用这些节点创建CSSOM(阻塞渲染)
//外部样式
<link rel="stylesheet" href="styles.css">
// 内部样式
<style>
h1 {
font-size: 18px;
}
</style>
// 行内样式
<button style="background-color: blue;">Click me</button>
CSSOM 节点创建 与 DOM 节点创建类似,随后,二者合并如下
浏览器不断构建 DOM 或 CSSOM 节点,直到发现外部或行内脚本。由于脚本可能访问或操作之前的 HTML 或 样式,因此浏览器必须停止解析节点、完成构建 CSSOM、执行脚本、然后继续重复(解析器阻塞)。
一旦所有节点已被解析、DOM 与 CSSOM 准备合并、浏览器便会构建渲染树。假设节点是单词,那么对象模型是句子、渲染树是整篇文章(整个页面)。
改自 https://www.studytonight.com/data-structures/linked-list-vs-array
Array | Linked List |
---|---|
数组是相似类型元素的集合 | 链表是相同类型元素的有序集合,它们使用指针互相连接 |
数组支持随机访问,这意味着可以使用索引直接访问元素,如第一个元素是arr [0],第七个元素是arr [6]等。因此,访问数组中的元素很快,时间复杂度为O(1) | 链表支持顺序访问,这意味着访问链表中的任何元素/节点,我们必须顺序遍历完整的链表,直到该元素。访问有n个元素的链表,时间复杂度为O(n)。 |
在数组中,元素以连续的方式存储在存储器中。 | 分配给新元素的存储器位置地址存储在链表的先前节点中,从而形成两个节点/元素之间的链接。 |
在数组中,插入和删除操作需要更多时间,因为内存位置是连续且固定的。 | 在链表中,新元素存储在第一个空闲、可用的存储位置,只有一个开销步骤,即将存储位置的地址存储在链表的前一个节点中。 |
在编译时,一旦声明了数组,就会分配内存。 它也被称为静态内存分配。 | 在添加新节点时,在运行时分配内存。 它也被称为动态内存分配。 |
每一个元素都是独立的,可以通过索引来访问 | 每一个节点/元素都指向了下一个、上一个或双方节点 |
数组可以是一维、二维或者多维的 | 链表可以是单向的、双向的以及循环的 |
数组大小必须在声明时被指定 | 链表在运行时随着节点增减大小是可变的。 |
数组获取栈中分配的内存 | 链表从堆中获取内存 |
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.