Open 5Mi opened 7 years ago
/*
* 经典面试题
* 函数参数不定回调函数数目不定
* 编写函数实现:
* add(1,2,3,4,5)==15
* add(1,2)(3,4)(5)==15
*/
function add() {
// 第一次执行时,定义一个数组专门用来存储所有的参数
var _args = [].slice.call(arguments);
// 在内部声明一个函数,利用闭包的特性保存_args并收集所有的参数值
var adder = function () {
var _adder = function() {
[].push.apply(_args, [].slice.call(arguments));
return _adder;
};
// 利用隐式转换的特性,当最后执行时隐式转换,并计算最终的值返回
_adder.toString = function () {
return _args.reduce(function (a, b) {
return a + b;
});
}
return _adder;
}
return adder.apply(null, _args);
}
// 输出结果,可自由组合的参数
console.log(add(1, 2, 3, 4, 5)); // 15
console.log(add(1, 2, 3, 4)(5)); // 15
console.log(add(1)(2)(3)(4)(5)); // 15
function Parent() {
this.a = 1;
this.b = [1, 2, this.a];
this.c = { demo: 5 };
this.show = function () {
console.log(this.a , this.b , this.c.demo );
}
}
function Child() {
this.a = 2;
this.change = function () {
this.b.push(this.a);
this.a = this.b.length;
this.c.demo = this.a++;
}
}
Child.prototype = new Parent();
var parent = new Parent();
var child1 = new Child();
var child2 = new Child();
child1.a = 11;
child2.a = 12;
parent.show();
child1.show();
child2.show();
child1.change();
child2.change();
parent.show();
child1.show();
child2.show();
(function () {
var x,y; // 外部变量提升
try {
throw new Error();
} catch (x/* 内部的x */) {
x = 1; //内部的x,和上面声明的x不是一回事!!
y = 2; //内部没有声明,作用域链向上找,外面的y
console.log(x); //当然是1
}
console.log(x); //只声明,未赋值,undefined
console.log(y); //就是2了
})();
// 1
// undefined
// 2
js 异步执行的运行机制。
- 所有任务都在主线程上执行,形成一个执行栈。
- 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
- 主线程不断重复上面的第三步。
宏任务与微任务:
异步任务分为 宏任务(macrotask) 与 微任务 (microtask),不同的API注册的任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。
- 宏任务(macrotask): script(整体代码)、setTimeout、setInterval、UI 渲染、 I/O、postMessage、 MessageChannel、setImmediate(Node.js 环境)
- 微任务(microtask): Promise、 MutaionObserver、process.nextTick(Node.js环境)
Event Loop(事件循环)中,每一次循环称为 tick, 每一次tick的任务如下:
- 执行栈选择最先进入队列的宏任务(通常是script整体代码),如果有则执行
- 检查是否存在 Microtask,如果存在则不停的执行,直至清空 microtask 队列
- 更新render(每一次事件循环,浏览器都可能会去更新渲染)
- 重复以上步骤
function testSometing() {
console.log("执行testSometing");
return "testSometing";
}
async function testAsync() {
console.log("执行testAsync");
return Promise.resolve("hello async");
}
async function test() {
console.log("test start...");
const v1 = await testSometing();//关键点1
console.log(v1);
const v2 = await testAsync();
console.log(v2);
console.log(v1, v2);
}
test();
var promise = new Promise((resolve)=> { console.log("promise start.."); resolve("promise");});//关键点2
promise.then((val)=> console.log(val));
console.log("test end...")
// VM256:12 test start...
// VM256:2 执行testSometing
// VM256:22 promise start..
// VM256:25 test end...
// VM256:14 testSometing
// VM256:7 执行testAsync
// VM256:23 promise
// VM256:16 hello async
// VM256:17 testSometing hello async
性能优化相关
减少http请求
布局背景图片使用样式background
css sprites雪碧图
延时加载,预加载,按需加载
减少dom数量与dom操作
服务端静态资源长缓存 静态资源加MD5戳
减少Cookie 去除无必要Cookie
样式表置顶 --> <body>
内容 --> <script>(脚本置底)
--></body>
使用外部css和js方便浏览器缓存
精简代码 代码压缩混淆 去除空格注释 优化压缩图片
200(from cache) 替换 304
异步无阻塞加载JS: defer,async
css关键样式提取,内联, 使用骨架屏, 静态页面无头浏览器预渲染
preload,prefetch
dns-prefetch,preconnect
code
Object.freeze
, 纯展示可用函数式组件runtime
webworker
多线程计算will-change
transform:translateZ(0);
等requestAnimationframe
requestIdleCallback
deploy & http
tree shaking
, scope hosting
, 资源压缩, 图片格式webp
等Service Worker
Memory Cache
(内存)Disk Cache
(硬盘)cookie
,storage
,indexDB
离线缓存Service Worker
cookie
传输preload
, prefatch
, dns-prefetch
, preconnect
, prerender
async
, defer
keep-alive
, 多域名主资源,比如 HTML 页面,或者下载项,一类是派生资源,比如 HTML 页面中内嵌的图片或者脚本链接
不访问服务器,直接读缓存,从内存中读取缓存。此时的数据时缓存到内存中的,当kill进程后,也就是浏览器关闭以后,数据将不存在。仅派生资源(缓存 js脚本文件,css样式表文件,font字体文件,图片文件等静态文件)
不访问服务器,直接读缓存,从磁盘中读取缓存,当kill进程时,数据还是存在。这种方式也只能缓存派生资源
访问服务器,服务器返回此状态码表示资源仍可用, 然后从缓存中读取数据
先去内存看,如果有,直接加载 如果内存没有,择取硬盘获取,如果有直接加载 如果硬盘也没有,那么就进行网络请求 加载到的资源缓存到硬盘和内存
所以我们可以来解释这个现象 ,图片为例:
访问-> 200 -> 退出浏览器 再进来-> 200(from disk cache) -> 刷新 -> 200(from memory cache)
<script src="script.js">
没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。(加载后,执行完,才能继续渲染)
<script async src="script.js">
有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。(async加载完就执行,但加载时不影响后续渲染)
<script defer src="myscript.js">
有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但是 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。(defer执行按加载顺序)
dns 预解析
<link rel="dns-prefetch" href="//yuchengkai.cn" />
DNS 解析也是需要时间的,可以通过预解析的方式来预先获得域名所对应的 IP。
preconnect
<!-- CORS 的跨域请求,那么也要加上 crossorigin 的属性 -->
<link href="https://cdn.domain.com" rel="preconnect" crossorigin />
preconnect 允许浏览器在一个 HTTP 请求正式发给服务器前预先执行一些操作,这包括 DNS 解析,TLS 协商,TCP 握手,这消除了往返延迟并为用户节省了时间。
preload 与 prefetch
<link rel="preload" href="/path/to/style.css" as="style" />
preload 提前加载 顾名思义就是一种预加载的方式,它通过声明向浏览器声明一个需要提交加载的资源,当资源真正被使用的时候立即执行,就无需等待网络的消耗
这种方式比通过 Link 方式加载资源方式更快,请求在返回还没到解析页面的时候就已经开始预加载资源了。
<link rel="prefetch" href="/path/to/style.css" as="style" />
prefetch 预判加载 prefetch 跟 preload 不同,
prerender
<link rel="prerender" href="http://example.com" />
可以通过预渲染将下载的文件预先在后台渲染,可以使用以下代码开启预渲染
预渲染虽然可以提高页面的加载速度,但是要确保该页面百分百会被用户在之后打开,否则就白白浪费资源去渲染
cookie一般用于保存信息,你向同一个服务器发请求时会带上浏览器保存的对于那个服务器的cookie,而不管你从哪个网站发请求。所以如果用cookie校验权限则会导致csrf攻击的成立, 即 当你在当前网站(A网站)登录后, 浏览第三方页面(伪造网站等), 在第三方页面上发起对A网站的请求
但目前一般权限校验采用JWT,请求头Authoritarian传递token校验用户权限,规避cookie自动携带的隐患
再就是目前 cookie same-site
属性的使用, 限制跨站请求时 携带cookie的行为. 即加上之前 cookie的 domain
属性和path
属性, 当向服务器发起请求时满足 domain
和 path
才携带传递, same-site
则跟进一步要校验当前访问的网站和网站内请求的服务地址是否跨站, 下图为 same-site: Lax
等值之间的区别
再就是第三方cookie Third-party cookies
But when you visit a domain such as www.somedomain.com, the web pages on that domain may feature content from a third-party domain. For instance, there may be an advertisement run by www.anotherdomain.com showing graphic advert banners. When your web browser asks for the banner image from www.anotherdomain.com, that third-party domain is allowed to set a cookie. Each domain can only read the cookie it created, so there should be no way of www.anotherdomain.com reading the cookie created by www.somedomain.com. So what's the problem?
Some people don't like third-party cookies for the following reason: suppose that the majority of sites on the internet have banner adverts from www.anotherdomain.com. Now it's possible for the advertiser to use its third-party cookie to identify you as you move from one site with its adverts to another site with its adverts.
Even though the advertiser from www.anotherdomain.com may not know your name, it can use the random ID number in the cookie to build up an anonymous profile of the sites you visit. Then, when it spots the unique ID in the third-party cookie, it can say to itself: "visitor 3E7ETW278UT regularly visits a music site, so show him/her adverts about music and music products".
Some people don't like the idea of advertising companies building up profiles about their browsing habits, even if the profile is anonymous.
如果大多网站都有嵌入类似 阿里妈妈, 百度分析,google分析,faceboo广告等第三方请求(js,跨域请求,链接等), 则same-site
普及前,不同站点间浏览时,即浏览 A网站->B网站->C网站, 如果A,B,C都有同一个第三方分析的js, 那么第三方的cookie就会在多个网站的第三方请求中携带, 则第三方虽然不清楚你具体的用户信息,但仍可分析当前特定用户的浏览行为,访问习惯, 做到精准推送,用户行为分析
DOM 和 CSSOM通常是并行构建的,所以 「CSS 加载不会阻塞 DOM 的解析」。 然而由于Render Tree 是依赖DOM Tree和 CSSOM Tree的,所以它必须等到两者都加载完毕后,完成相应的构建,才开始渲染,因此, 「CSS加载会阻塞DOM渲染」。 由于 JavaScript 是可操纵 DOM 和 css 样式 的,如果在修改这些元素属性同时渲染界面(即 JavaScript 线程和 UI 线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。 因此为了防止渲染出现不可预期的结果,浏览器设置 「GUI 渲染线程与 JavaScript 引擎为互斥」 的关系
Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。
DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载
成功
重定向
301和302状态码都表示重定向,就是说浏览器在拿到服务器返回的这个状态码后会自动跳转到一个新的URL地址,这个地址可以从响应的Location首部中获取(用户看到的效果就是他输入的地址A瞬间变成了另一个地址B)——这是它们的共同点。
他们的不同在于。301表示旧地址A的资源已经被永久地移除了(这个资源不可访问了),搜索引擎在抓取新内容的同时也将旧的网址交换为重定向之后的网址;302表示旧地址A的资源还在(仍然可以访问),这个重定向只是临时地从旧地址A跳转到地址B,搜索引擎会抓取新的内容而保存旧的网址 参考
请求错误 (资源访问受限)
服务器错误
强缓存
Cache-Control:max-age=6000
表示资源返回后6000秒内,可以直接使用缓存当Expires和Cache-Control同时存在时,优先考虑Cache-Control。 当然了,当缓存资源失效了,也就是没有命中强缓存,接下来就进入协商缓存
协商缓存
这个字段表示的是「最后修改时间」。在浏览器第一次给服务器发送请求后,服务器会在响应头中加上这个字段。
浏览器接收到后,「如果再次请求」,会在请求头中携带If-Modified-Since字段,这个字段的值也就是服务器传来的最后修改时间
服务器拿到请求头中的If-Modified-Since的字段后,其实会和这个服务器中该资源的最后修改时间对比:
如果请求头中的这个值小于最后修改时间,说明是时候更新了。返回新的资源,跟常规的HTTP请求响应的流程一样。否则返回304,告诉浏览器直接使用缓存。
ETag是服务器根据当前文件的内容,对文件生成唯一的标识,比如MD5算法,只要里面的内容有改动,这个值就会修改,服务器通过把响应头把该字段给浏览器。
浏览器接受到ETag值,会在下次请求的时候,将这个值作为「If-None-Match」这个字段的内容,发给服务器。
服务器接收到「If-None-Match」后,会跟服务器上该资源的「ETag」进行比对👇
如果两者一样的话,直接返回304,告诉浏览器直接使用缓存如果不一样的话,说明内容更新了,返回新的资源,跟常规的HTTP请求响应的流程一样
性能上,Last-Modified优于ETag,Last-Modified记录的是时间点,而Etag需要根据文件的MD5算法生成对应的hash值。精度上,ETag优于Last-Modified。ETag按照内容给资源带上标识,能准确感知资源变化,Last-Modified在某些场景并不能准确感知变化,比如👇
编辑了资源文件,但是文件内容并没有更改,这样也会造成缓存失效。Last-Modified 能够感知的单位时间是秒,如果文件在 1 秒内改变了多次,那么这时候的 Last-Modified 并没有体现出修改了。
最后,「如果两种方式都支持的话,服务器会优先考虑ETag」。
同源策略与跨域
如果协议、域名或者端口有一个不同就是跨域,Ajax 请求会失败(发送成功,服务器可接收,浏览器会拦截返回由于跨域失败)
image
,style
,script
标签没有跨域限制
不同源的客户端脚本 (如当前源与跨域iframe) ,在没有明确授权的情况下,不能读写对方的资源。
跨域解决方案
JSONP 使用简单且兼容性不错,但是只限于 get 请求, 利用 <script>
标签没有跨域限制的漏洞。通过 <script>
标签指向一个需要访问的地址并提供一个回调函数来接收数据
<!-- 服务器将返回js内容为 一个回调函数的执行 -->
<!-- jsonp(参数写有服务器返回的数据) -->
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
function jsonp(url, jsonpCallback, success) {
let script = document.createElement('script')
script.src = url
script.async = true
script.type = 'text/javascript'
window[jsonpCallback] = function(data) {
success && success(data)
}
document.body.appendChild(script)
}
jsonp('http://xxx', 'callback', function(value) {
console.log(value)
})
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源
nginx,devserver等服务代理 proxy
iframe间使用postMessage [实现跨文档消息传输]
<!-- 父页面中 -->
<iframe id="iframe" src="http://someIframePage.com/index.html"></iframe>
<script>
// https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
var iframe = document.getElementById('iframe');
// 向iframe传送跨域数据
iframe.contentWindow.postMessage(JSON.stringify(data), 'http://someIframePage.com.com');
// 接受postMessage消息, 监听message事件
window.addEventListener("message", receiveMessage, false)
function receiveMessage(event){
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://someIframePage.com.com') {
console.log('验证通过')
}
}
</script>
<!-- iframe页面中 -->
<script>
window.parent.postMessage(JSON.stringify(data), 'http://parentPage.com')
// 接受postMessage消息, 监听message事件
window.addEventListener("message", receiveMessage, false)
function receiveMessage(event){
var origin = event.origin || event.originalEvent.origin
if (origin === 'http://parentPage.com') {
console.log('验证通过')
}
}
</script>
捕获 -> 目标 -> 冒泡
简要实现双向事件绑定:
监听器 Observer 实现
/**
* 循环遍历数据对象的每个属性
*/
function observable(obj) {
if (!obj || typeof obj !== 'object') {
return;
}
let keys = Object.keys(obj);
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj;
}
/**
* 将对象的属性用 Object.defineProperty() 进行设置
*/
defineReactive: function(data, key, val) {
var dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function getter () {
// 收集依赖
if (Dep.target) {
dep.addSub(Dep.target);
}
return val;
},
set: function setter (newVal) {
if (newVal === val) {
return;
}
val = newVal;
// 通知订阅者
dep.notify();
}
});
}
消息订阅器 Dep
,用来容纳所有的“订阅者”。订阅器 Dep
主要负责收集订阅者,然后当数据变化的时候后执行对应订阅者的更新函数
function Dep () {
// 储存订阅者 watcher
this.subs = [];
}
Dep.prototype = {
addSub: function(sub) {
this.subs.push(sub);
},
notify: function() {
this.subs.forEach(function(sub) {
sub.update();
});
}
};
Dep.target = null;
订阅者 Watcher
初始化的时候触发watcher
的 get
函数去执行添加订阅者操作, 通过赋值 Dep.target
缓存下订阅者,接着获取响应对象的值,触发其getter
使watcher
被 dep
收集 , 收集依赖后再将Dep.target
清空,
function Watcher(vm, exp, cb) {
this.vm = vm;
this.exp = exp;
this.cb = cb;
this.value = this.get(); // 将自己添加到订阅器的操作
}
Watcher.prototype = {
update: function() {
this.run();
// 实际中会区分更新是 同步还是异步的
// 异步的话 执行异步watcher队列 queueWatcher(this)
},
run: function() {
var value = this.vm.data[this.exp];
var oldVal = this.value;
if (value !== oldVal) {
this.value = value;
this.cb.call(this.vm, value, oldVal);
}
},
get: function() {
Dep.target = this; // 全局变量 订阅者 赋值
var value = this.vm.data[this.exp] // 强制执行监听器里的get函数,使dep收集依赖
Dep.target = null; // 全局变量 订阅者 释放
return value;
}
};
订阅者 Watcher
是一个 类,在它的构造函数中,定义了一些属性:
Vue
的实例对象;v-model
等指令的属性值 或者插值符号中的属性。如 v-model="name"
,exp
就是name;Watcher
绑定的更新函数;当我们去实例化一个渲染 watcher
的时候,首先进入 watcher
的构造函数逻辑,就会执行它的 this.get()
方法,进入 get
函数,首先会执行
Dep.target = this; // 将自己赋值为全局的订阅者
实际上就是把 Dep.target
赋值为当前的渲染 watcher
,接着又执行了
// 强制执行响应对象的get函数, 使dep收集Dep.target 即当前watcher
let value = this.vm.data[this.exp]
在这个过程中会对 vm
上的数据访问,其实就是为了触发数据对象的 getter
每个对象值的 getter
都持有一个 dep
,在触发 getter
的时候会调用 dep.depend()
方法,也就会执行this.addSub(Dep.target)
,即把当前的 watcher
订阅到这个数据持有的 dep
的 watchers
中,这个目的是为后续数据变化时候能通知到哪些 watchers
做准备。
这样实际上已经完成了一个依赖收集的过程。那么到这里就结束了吗?其实并没有,完成依赖收集后,还需要把 Dep.target
恢复成上一个状态,即
Dep.target = null; // 释放自己
而 update()
函数是用来当数据发生变化时调用 Watcher 自身的更新函数进行更新的操作。先通过 let value = this.vm.data[this.exp]
; 获取到最新的数据,然后将其与之前 get()
获得的旧数据进行比较,如果不一样,则调用更新函数 cb
进行更新
通过监听器 Observer
订阅器 Dep
和订阅者 Watcher
的实现,其实就已经实现了一个双向数据绑定的例子,但是整个过程都没有去解析 dom
节点,而是直接固定某个节点进行替换数据的,所以接下来需要实现一个解析器 Compile
来做解析和绑定工作。
解析器 Compile
实现步骤:
我们下面对 {{变量}}
这种形式的指令处理的关键代码进行分析,感受解析器 Compile
的处理逻辑,关键代码如下:
compileText: function(node, exp) {
var self = this;
var initText = this.vm[exp]; // 获取属性值
this.updateText(node, initText); // dom 更新节点文本值
// 将这个指令初始化为一个订阅者,后续 exp 改变时,就会触发这个更新回调,从而更新视图
new Watcher(this.vm, exp, function (value) {
self.updateText(node, value);
});
}
简单理解为:
Compile
解析组件模板, 将更新函数传入 new watcher
实例的回调watcher
实例调用自身get
赋值监听器dep.target
,再获取响应对象的值, 来触发可响应对象的get
get
中 将当前存放有watcher
实例的 dep.target
存入此属性对应的dep
实例的sub
数组observer
)的值改动,触发对应的set
函数,此属性对应的dep
实例将调用dep.notify()
遍历所有订阅者watcher
实例watcher
实例调用其run()
方法,执行new watcher
时传入的回调, 即 Complie解析时 {{变量}}
对应的更新函数(修改对应的dom内容等)<template>
<div class="box">{{msg}}</div>
</template>
export default {
name: 'index',
data () {
return {
msg: 'hello'
}
},
mounted () {
this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
console.log(box.innerHTML) // hello
}
}
以上代码可以看到,修改数据后dom并没有立刻更新,vue中dom的更新机制是异步的,(会对异步更新队列中的watcher
去重再统一更新dom) 无法通过同步代码获取,需要使用nextTick,在下一次事件循环中获取
this.msg = 'world'
let box = document.getElementsByClassName('box')[0]
// 如果我们需要获取数据更新后的dom信息,
// 比如动态获取宽高、位置信息等,需要使用nextTick
this.$nextTick(() => {
console.log(box.innerHTML) // world
})
双向绑定原理: setter
->Dep
->Watcher
->update
触发至watcher.update()
执行异步队列 queueWatcher
// watcher update
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// dom操作 执行异步队列 queueWatcher
queueWatcher(this)
}
}
queueWatcher
-> nextTick(flushSchedulerQueue)
function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
// 通过waiting 保证nextTick只执行一次
waiting = true
// 最终queueWatcher 方法会把flushSchedulerQueue 传入到nextTick中执行
nextTick(flushSchedulerQueue)
}
}
}
其中 flushSchedulerQueue
-> watcher.run()
function flushSchedulerQueue () {
currentFlushTimestamp = getNow()
flushing = true
let watcher, id
...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before()
}
id = watcher.id
has[id] = null
// 遍历执行渲染watcher的run方法 完成视图更新
watcher.run()
}
// 重置waiting变量
resetSchedulerState()
...
}
当数据变化最终会把flushSchedulerQueue
传入到nextTick
中执行flushSchedulerQueue
函数会遍历执行watcher.run()
方法,watcher.run()
方法最终会完成视图更新,接下来我们看关键的nextTick
方法到底是啥
nextTick
方法会将传进来的回调push进callbacks
数组,然后执行timerFunc
方法
export function nextTick (cb?: Function, ctx?: Object) {
let _resolve
// push进callbacks数组
callbacks.push(() => {
cb.call(ctx)
})
if (!pending) {
pending = true
// 执行timerFunc方法
timerFunc()
}
}
timerFunc
决定将 nexttick
回调 推入微任务
还是 宏任务
let timerFunc
// 判断是否原生支持Promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
const p = Promise.resolve()
timerFunc = () => {
// 如果原生支持Promise 用Promise执行flushCallbacks
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
// 判断是否原生支持MutationObserver
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
// PhantomJS and iOS 7.x
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
let counter = 1
// 如果原生支持MutationObserver 用MutationObserver执行flushCallbacks
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
// 判断是否原生支持setImmediate
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
// 如果原生支持setImmediate 用setImmediate执行flushCallbacks
setImmediate(flushCallbacks)
}
// 都不支持的情况下使用setTimeout 0
} else {
timerFunc = () => {
// 使用setTimeout执行flushCallbacks
setTimeout(flushCallbacks, 0)
}
}
// flushCallbacks 最终执行nextTick 方法传进来的回调函数
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
响应式对象设置属性值 响应式触发setter
->Dep
->Watcher
->update
-> queueWatcher
-> nextTick(flushSchedulerQueue)
-> nexttick
中 timerFunc
决定如何执行nextTickHandler
将 flushSchedulerQueue
推入微任务还是 宏任务 -> flushSchedulerQueue
队列根据watcherId
去重后 执行每一个watcher.run()
直到清空队列, 此处watcher.run()
则含有同步更新dom操作(diff算法patch之后dom已被操作更新)
之后用户再使用vue.nextTick(callback)则可确保回调函数在dom跟新后执行
由于宏任务耗费的时间是大于微任务的,所以在浏览器支持的情况下,优先使用微任务。如果浏览器不支持微任务,再使用宏任务 延迟调用优先级如下: Promise > MutationObserver > setImmediate > setTimeout
macrotask
-> microtask queue
-> UI render
-> macrotask
(nextTick 循环)
注意不要把“UI渲染”跟“DOM更新”这2个混为一谈了,“UI渲染”确实是所有微任务完成之后,是异步的。而“DOM更新”是同步的(程序中),只不过用户想在页面观察到变化需要等待UI渲染之后
开启严格模式,仅需在创建 store 的时候传入 strict: true; 在严格模式下,无论何时发生了状态变更且不是由 mutation 函数引起的,将会抛出错误。这能保证所有的状态变更都能被调试工具跟踪到。
vuex 设置严格模式参数为true, 调用了 $watch
函数来观察state
的变化。当state
变化时,会判断 store._committing(这个值在调用commit方法时才修改) 的值,如果不为 true,就会报出异常
if(process.env.NODE_ENV !== 'production'){
assert(store._committing,`Do not mutate vuex store state outside mutation handlers.`)
}
通过commit
调用mutation
才修改_committing
开关
虽然直接修改state
,state
可以修改成功,并且依然是响应式的, 哪怕在严格模式下也只是抛出错误(依旧可修改,并可响应),但是还是应该按照规范使用commit
提交mutation
的方式, 这样才能被vuex
及其vue开发工具更好的管理记录,数据的流向还原才更加清晰
重新渲染,就需要重新生成布局和重新绘制。前者叫做重排(reflow 或 回流),后者叫做重绘(repaint)
需要注意的是,重绘不一定需要重排,重排必然导致重绘。为了提高网页性能,就要降低"重排"和"重绘"的频率和成本,尽量少触发重新渲染
浏览器为了重新渲染部分或整个页面,重新计算页面元素位置和几何结构的进程叫做reflow
DOM
元素的几何属性变化,常见的几何属性有width、height、padding、margin、left、top、border
等等, 这个很好理解。DOM
节点发生增减或者移动。offset
族、scroll
族和client
族属性的时候,浏览器为了获取这些值,需要进行回流操作。window.getComputedStyle
方法。一些常用且会导致回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
scrollIntoView()、scrollIntoViewIfNeeded()
getComputedStyle()
getBoundingClientRect()
scrollTo()
当你获取布局信息的操作的时候,会强制队列刷新,比如当你访问以上属性或者使用以上方法,以上属性和方法都需要返回最新的布局信息,因此浏览器不得不清空队列,触发回流重绘来返回正确的值。因此,我们在修改样式的时候,最好避免使用上面列出的属性,他们都会刷新渲染队列。如果要使用它们,最好将值缓存起来
触发回流的时候,如果 DOM 结构发生改变,则重新渲染 DOM 树,然后将后面的流程(包括主线程之外的任务)全部走一遍
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘
于没有导致 DOM 几何属性的变化,因此元素的位置信息不需要更新,从而省去布局的过程,流程如下:
跳过了布局树
和建图层树
,直接去绘制列表,然后在去分块,生成位图等一系列操作。可以看到,重绘不一定导致回流,但回流一定发生了重绘
还有一种情况:就是更改了一个既不要布局也不要绘制的属性,那么渲染引擎会跳过布局和绘制,直接执行后续的合成操作,这个过程就叫合成。
举个例子:比如使用CSS的transform
来实现动画效果,避免了回流跟重绘,直接在非主线程中执行合成动画操作。显然这样子的效率更高,毕竟这个是在非主线程上合成的,没有占用主线程资源,另外也避开了布局和绘制两个子阶段,所以相对于重绘和重排,合成能大大提升绘制效率。
利用这一点好处:
提升合成层的最好方式是使用 CSS 的 will-change
属性
css3的translate会引起重排吗, 并不会,不是同一个复合图层
不会,因为 GPU 进程会为其开启一个新的复合图层,不会影响默认复合图层(就是普通文档流),所以并不会影响周边的 DOM 结构,而属性的改变也会交给 GPU 处理,不会进行重排。使 GPU 进程开启一个新的复合图层的方式还有 3D 动画,过渡动画,以及 opacity 属性,还有一些标签,这些都可以创建新的复合图层。这些方式叫做硬件加速方式。你可以想象成新的复合图层和默认复合图层是两幅画,相互独立,不会彼此影响。降低重排的方式:要么减少次数,要么降低影响范围,创建新的复合图层就是第二种优化方式。绝对布局虽然脱离了文档流,但不会创建新的复合图层,因此当绝对布局改变时,不会影响普通文档流的 render tree,但是依然会绘制整个默认复合图层,对普通文档流是有影响的。普通文档流就是默认复合图层,不要介意我交换使用它们如果你要使用硬件加速方式降低重排的影响,请不要过度使用,创建新的复合图层是有额外消耗的,比如更多的内存消耗,并且在使用硬件加速方式时,配合 z-index 一起使用,尽可能使新的复合图层的元素层级等级最高
scrollWidth,clientWidth,offsetWidth的区别
无滚动 有滚动
转自前端进阶
MS
ECMAScript中的所有参数传递的都是值,不可能通过引用传递参数。 被传递的值会被复制给一个局部变量(arguments)
js slice 与 splice
字符串回文与匹配 与乱序
javascript 中的
this
引用的是函数据以执行的环境对象——或者也可以说是 this 值(当在网页的全局作用域中调用函数时, this 对象引用的就是 window)。运算符优先级表
优先级 高到低 同级不同运算从左到右 20 圆括号
19 成员访问
.
19 需计算的成员访问
[]
19
new
(带参数列表 例: new...(...) )19 函数调用 例: ...(...)
18
new
(无参数列表) 例: new ...17 后置自增,后置自减
a++
a--
...
(new foo 等同于 new foo(), 只能用在不传递任何参数的情况)
函数声明提升 和 变量声明提升
变量提升也有优先级, 函数声明 > arguments > 变量声明
函数作用域链包含两个对象:的作用域链包含两个对象:它自己的变量对象(其中 定义着 arguments 对象) 和 全局环境的变量对象。
作用域链是定死的,函数引用的变量在哪里定义,引用的就是哪里的变量.
function fn(){ console.log(a); var a = 5; console.log(a);
a++; var a; fn3(); fn2(); console.log(a);
}
function fn3(){ console.log(a) a = 200; }
fn(); console.log(a); //输出 //undefined //5 //1 //6 //20 //200
函数的隐式转换
对象通过valueOf方法,把自己转换成数字,通过toString方法,把自己转换成字符串
如果字符串和数字相加,JavaScript会自动把数字转换成字符的,不管数字在前还是字符串在前,字符串和数字相加结果是字符串
一个对象同时存在valueOf方法和toString方法,那么,valueOf方法总是会被优先调用的
参考
==
与===
与Object.is()
获取页面用到哪些元素
document.getElementsByTagName('*');
document.all
能取得当前页面所有的element,判断nodeType===1
就是element了,取nodeName
就是标签名称js 中的
new
访问属性对象
.
与[]
[]
中可以用变量this
函数调用可等价转化为call形式;
数组去重
模拟call , apply函数
参考call,apply,bind
模拟实现
模拟bind函数
bind方法
返回类型