Coder Social home page Coder Social logo

blog's People

Contributors

afterthreeyears avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

blog's Issues

TCP传输连接管理

三次握手

  1. 首先客户端给服务器发送一个syn = 1, ack = 0包并且带上每个数据包的最大值,和缓存区的最大值,
  2. 服务端接受到了给客户端发送一个syn = 1 + ack = 1包,并且带上每个数据包的最大值,和缓存区的最大值,
  3. 然后客户端在给服务端发送一个ack = 1的包,表示三次握手完成

四次挥手

  1. 客户端给服务端发送一个带有fin = 1的数据包,然后
  2. 服务器收到以后会发送一个ack = 1的数据包,然后
  3. 当服务器给客户端发送完数据以后会发送一个fin = 1,并ack = 1的包,然后
  4. 客户端收到以后在发送一个ack = 1的包给服务器

问题

1. 为什么TCP客户端最后还要发送一次确认呢?

一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。

如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。

如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。

2. 为什么客户端最后还要等待2MSL?

MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。

第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。

第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。

3. 为什么建立连接是三次握手,关闭连接确是四次挥手呢?

建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。

4. 如果已经建立了连接,但是客户端突然出现故障了怎么办?

TCP还设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。

Webview和Bridge

  1. 为什么要混合开发
    原生开发优点
    1。 APP的数据都保存在本地,APP能及时调取,所以相应速度及流畅性有保障
    缺点。
    效率低
    需要发布
    html5开发
    优点1.效率快
    缺点1.速度流畅性相对较低

混合开发就是融合以上两者的优点,去掉缺点

市面上的方案

  1. hybrid 基于webview
  2. rn/weex
  3. phonegap/inicon
  4. fulber

2.需要提供什么东西?

基础:JSBridge
进阶:
1.离线包
2. 动态数据、容器预加载

3.怎么做
3.1 webview 分为三块

WebView简介

为了方便开发者实现在app内展示网页并与网页交互的需求,Android SDK提供了WebView组件。

它继承自AbsoluteLayout,展示网页的同时,也可以在其中放入其他的子View。

从Android 4.4(KitKat)开始,原本基于WebKit的WebView开始基于Chromium内核,这一改动大大提升了WebView组件的性能以及对HTML5,CSS3,JavaScript的支持。

在WebView中,有几个地方是我们可以使用来定制我们的WebView各种行为的,分别是:WebSettings、JavaScriptInterface、WebViewClient以及WebChromeClient。这些我都会在接下来的文章中一一介绍。

简单理解 就是在app中的一个可被app定制化的浏览器

WebView基本使用

<WebView
  android:id="@+id/web_view"
  android:layout_width="match_parent"
  android:layout_height="match_parent" />

然后在Activity的onCreate方法里写入如下代码:

String url = "https://www.example.com";
WebView webView = (WebView) findViewById(R.id.web_view);
webView.loadUrl(url);

这样就可以在webview里打开网站了

webView类有一些常用的方法

  1. String getUrl():获取当前页面的URL。
  2. reload():重新reload当前的URL,即刷新。
  3. boolean canGoBack():用来确认WebView里是否还有可回退的历史记录。通常我们会在WebView里重写返回键的点击事件,通过该方法判断WebView里是否还有历史记录,若有则返回上一页。
  4. goBack():在WebView历史记录后退到上一项。
  5. destroy():销毁WebView。
  6. pageUp(boolean top):将WebView展示的页面滑动至顶部。
  7. getContentHeight():该方法返回整个HTML页面的高度
    更多api
    https://developer.android.com/reference/android/webkit/WebView

WebSettings介绍
WebSettings是用来管理WebView配置的类。当WebView第一次创建时,内部会包含一个默认配置的集合。若我们想更改这些配置,便可以通过WebSettings里的方法来进行设置。
WebSettings对象可以通过WebView.getSettings()获得,它的生命周期是与它的WebView本身息息相关的,如果WebView被销毁了,那么任何由WebSettings调用的方法也同样不能使用。

获取WebSettings对象

WebSettings webSettings = webView.getSettings();

WebSettings有一些常用的方法

  1. setJavaScriptEnabled(boolean flag):设置WebView是否可以运行JavaScript。
  2. setAllowFileAccess(boolean allow):启用或禁用WebView访问文件数据。
  3. setDefaultEncodingName(String encoding):设置编码格式,通常都设为“UTF-8”。
  4. setUserAgentString(String ua):设置WebView的UserAgent值。
    还有一些设置字体和缓存之类的
    更多api
    https://developer.android.com/reference/android/webkit/WebSettings

WebViewClient介绍
帮助WebView处理各种通知和请求事件

常用方法

  1. onPageStarted(WebView view, String url, Bitmap favicon):该方法在WebView开始加载页面调用,我们可以在这个方法里设定开启一个加载的动画,例如开启顶部的progressBar

  2. onPageFinished(WebView view, String url):该方法只在WebView完成一个页面加载时调用一次,我们可以可以在此时关闭加载动画,例如关闭顶部的progressBar

  3. onReceivedError(WebView view, WebResourceRequest request, WebResourceError error):该方法在web页面加载错误时回调,这些错误通常都是由无法与服务器正常连接引起的,最常见的就是网络问题。 可以用在断网的时候显示一个离线页面

  4. WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request):当WebView需要请求某个数据时,这个方法可以拦截该请求来告知app并且允许app本身返回一个数据来替代我们原本要加载的数据。用来拦截资源做离线包

  5. boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request):当我们没有给WebView提供WebViewClient时,WebView如果要加载一个url会向ActivityManager寻求一个适合的处理者来加载该url(比如系统自带的浏览器),可以用来进行bridge通信

使用

webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        Uri u = Uri.parse(request.getUrl().toString());
    }
});

WebChromeClient
是辅助WebView处理Javascript的对话框,网站图标,网站title,加载进度等

常用方法

  1. onProgressChanged(WebView view, int newProgress):当页面加载的进度发生改变时回调,用来告知主程序当前页面的加载进度。

  2. onReceivedIcon(WebView view, Bitmap icon):用来接收web页面的icon,我们可以在这里将该页面的icon设置到Toolbar。

  3. onReceivedTitle(WebView view, String title):用来接收web页面的title,我们可以在这里将页面的title设置到Toolbar。

  4. boolean onJsAlert(WebView view, String url, String message, JsResult result):处理Javascript中的Alert对话框。

  5. boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):处理Javascript中的Prompt对话框。

  6. boolean onJsConfirm(WebView view, String url, String message, JsResult result):处理Javascript中的Confirm对话框

  7. boolean onShowFileChooser(WebView webView, ValueCallback filePathCallback, WebChromeClient.FileChooserParams fileChooserParams):该方法在用户进行了web上某个需要上传文件的操作时回调。我们应该在这里打开一个文件选择器,如果要取消这个请求我们可以调用filePathCallback.onReceiveValue(null)并返回true。
    更多
    https://developer.android.com/reference/android/webkit/WebChromeClient

3.3.1安卓做法

对于Android调用JS代码的方法有2种:

  1. 通过WebView的loadUrl()
// 调用到window上的callJS()方法, 无法拿到返回值,兼容性好
webView.loadUrl("javascript:callJS()");
  1. 通过WebView的evaluateJavascript()
// 调用到window上的callJS()方法, 可以拿到返回值
// Android 4.4 后才可使用
webView.evaluateJavascript("javascript:callJS()", new ValueCallback<String>() {
  @Override
  public void onReceiveValue(String value) {
      //此处为 js 返回的结果
  }
});

对于JS调用Android代码的方法有3种:

  1. 通过WebView的addJavascriptInterface()进行对象映射

优点:使用简单
缺点:存在严重的漏洞问题,Android 4.2后修复

public class JsApi  {
    @JavascriptInterface
    public void callNative(String msg) {
        System.out.println(msg);
    }
}
// AndroidtoJS类对象映射到js的jsApi对象
webView.addJavascriptInterface(new JsApi(), "jsApi");
// js调用
jsApi.callNative("js调用了android方法");
  1. 通过 WebViewClient 的shouldOverrideUrlLoading ()方法回调拦截 url

具体原理:
Android通过 WebViewClient 的回调方法shouldOverrideUrlLoading ()拦截 url
解析该 url 的协议
如果检测到是预先约定好的协议,就调用相应方法

优点:不存在方式1的漏洞;
缺点:JS获取Android方法的返回值复杂。

function createIframeCall(url) {
  setTimeout(function() {
    var iframe = document.createElement('iframe');
    iframe.style.width = '1px';
    iframe.style.height = '1px';
    iframe.style.display = 'none';
    iframe.src = url;
    document.body.appendChild(iframe);
    setTimeout(function() {
        document.body.removeChild(iframe);
    }, 100);
  }, 0);
}

function call(funcName, data, cb) {
  const callbackId = `callback_${uid}_${Date.now()}`;
  if (cb) {
    window[callbackId] = cb;
  }
  createIframeCall(`weixin://invoke?method=${funcName}&data=${data}&callbackId=${callbackId}`);
}

const data = encodeURIComponent(JSON.stringify({title:"我是标题"}));
call('openWebView', data, () => {});
webView.setWebViewClient(new WebViewClient() {
    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        Uri u = Uri.parse(request.getUrl().toString());
        if (u.getScheme().equals("weixin") && u.getAuthority().equals("invoke")) {
            String method = u.getQueryParameter("method");
            String callbackId = u.getQueryParameter("callbackId");
            String data = u.getQueryParameter("data");
            switch (method) {
                case "openWebView":
                    openWebView(callbackId, data);
                    break;
                default:
                    return false;
            }
        }
        return true;
    }
});
public void openWebView(String callbackId, String data) {
    // 打开webview
    if (callbackId != null) {
        // 执行js监听的回调函数
        execJS(callbackId, data);
    }
}
  1. 通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调拦截JS对话框alert()、confirm()、prompt() 消息
    原理:Android通过 WebChromeClient 的onJsAlert()、onJsConfirm()、onJsPrompt()方法回调分别拦截JS对话框
    (即上述三个方法),得到他们的消息内容,然后解析即可。

常用的拦截是:拦截 JS的输入框(即prompt()方法)
因为只有prompt()可以返回任意类型的值,操作最全面方便、更加灵活;而alert()对话框没有返回值;confirm()对话框只能返回两种状态(确定 / 取消)两个值

  1. 三种方式的对比 & 使用场景

3.3.2 ios做法

UIWebview的delegate也支持schame方式

另外ios7以上支持JavaScriptCore和安卓的addJavascriptInterface很相似

3.3.3 js做法

JSBridge的最佳实践是:

协议规范都使用:hybrid://action/method?arg1=xxx&arg2=xxx
iOS使用Universal Link和UIWebview的delegate
安卓使用shouldOverrideUrlLoading和Applink

根据以上的讨论的出js需要设计一个通用两边的bridge

core

// api设计
interface IMessage {
  cbId: string, // "cb_(:id)_(:timeStamp)", 回调函数的id
  status: number, // 状态数据 (0:失败, 1:成功)
  msg: string, // 'ok'
  data: object, // {}
}
// 监听客户端调用
JSBridge.handlerFromApp(IMessage: message)
// 主动调用native
JSBridge.invoke(nativeFuncName, data, cb)
// 主动监听native
JSBridge.listen(h5FuncName, data, cb)

/*****其他方法****/
// 注册回调,callNative
function callNative(type, funcName, data, cb) {}
// 创建iframe
function createIframeCall(url)

二次封装,业务调用

window.JSBridge.showToast = function({ data }) {
  return new Promise(resolve => {
    window.JSBridge.invoke('showToast', data, resolve);
  });
};

window.JSBridge.showToast({
  data: { title: 1 },
}).then((data) => {
  console.log('调用toast', data);
})

4.总结

  1. webview的使用
  2. 设计bridge核心api

源码https://github.com/AfterThreeYears/simpleBridge

4.1例子
4.2业界方案
5.继续优化
5.1离线包

CORS

从前端a.com里通过xhr访问接口b.com

  1. cookie
    需要在前端设置
    xhr.withCredentials = true;
    或者fetch
    credentials: 'include',
    并且服务端设置
    Access-Control-Allow-Credentials: true,并且
    Access-Control-Allow-Origin不为*
    才能携带b.com的cookie,
    默认情况下a.com的cookie没办法带给接口b.com,需要手动添加到参数

浏览器和Node.js的Event-loop

浏览器

浏览器的event loop在html5的规范中明确定义。

因为JS是单线程,在浏览器中是为了避免DOM渲染冲突,如果有多线程的话,a线程修改了DOM,b线程也修改了DOM,那就不知道以谁为准了,所有在JS中是使用异步来进行解决,具体的方式是使用event-loop

task

  • script代码
  • setTimeout/setInterval
  • UI交互/UI事件等
  • IO
  • history traversal任务源:当调用history.back()等类似的api时,将任务插进task队- setImmediate(nodejs环境中)

Microtask

一个EL中只有一个microtask队列,通常下面几种任务被认为是microtask

  • promise(promise的then和catch才是microtask,本身其内部的代码并不是)
  • MutationObserver
  • process.nextTick(nodejs环境中)

EL循环过程

一个EL只要存在,就会不断执行下边的步骤:

  1. 在所有task队列中选择一个最早进队列的task,用户代理可以选择任何task队列,如果没有可选的任务,则跳到6Microtasks步骤
  2. 将前一步选择的task设置为 currently running task
  3. Run: 运行被选择的task
  4. 运行结束之后,将event loop的 currently running task 置为 null
  5. 从task队列里移除前边Run里运行的task
  6. Microtasks: 执行microtasks任务检查点。(也就是执行microtasks队列里的任务)
  7. 更新渲染(可能会发生,改部分细致解释可在【拓展】部分看详细解释)
  8. 如果这是一个worker event loop,但是task队列中没有任务,并且WorkerGlobalScope对象的closing标识为true,则销毁EL,中止这些步骤,然后 run a worker
  9. 返回到第1步

所以说在Vue中nextTick优先使用minrotack原因是

  • 执行完microtask队列里的任务,有可能会渲染更新。在一帧以内的多次dom变动浏览器不会立即响应,而是会积攒变动以最高60HZ的频率更新视图
// 类似这样
while (true) {
    宏任务队列.shift()
    微任务队列全部任务()
    UI render() // 这里不一定每次loop都会render
}

Node.js

nodejs的event是基于libuv

个人理解,它与浏览器中的轮询机制(一个task,所有microtasks;一个task,所有microtasks…)最大的不同是,node轮询有phase(阶段)的概念,不同的任务在不同阶段执行,进入下一阶段之前执行process.nextTick() 和 microtasks。

Node事件轮询中的几个阶段

┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

timers

在timer阶段其实使用一个最小堆而不是队列来保存所有元素(其实也可以理解,因为timeout的callback是按照超时时间的顺序来调用的,并不是先进先出的队列逻辑),然后循环取出所有到期的callback执行。

I/O callbacks

根据libuv的文档,一些应该在上轮循环poll阶段执行的callback,因为某些原因不能执行,就会被延迟到这一轮的循环的I/O callbacks阶段执行。换句话说这个阶段执行的callbacks是上轮残留的。

idle, prepare内部使用,忽略

poll

处理 poll 队列的事件
当有已超时的 timer,执行它的回调函数

even loop将同步执行poll队列里的回调,直到队列为空或执行的回调达到系统上限(上限具体多少未详),接下来even loop会去检查有无预设的setImmediate(),分两种情况:

  1. 若有预设的setImmediate(), event loop将结束poll阶段进入check阶段,并执行check阶段的任务队列
  2. 若没有预设的setImmediate(),event loop将阻塞在该阶段等待

注意一个细节,没有setImmediate()会导致event loop阻塞在poll阶段,这样之前设置的timer岂不是执行不了了?所以咧,在poll阶段event loop会有一个检查机制,检查timer队列是否为空,如果timer队列非空,event loop就开始下一轮事件循环,即重新进入到timer阶段

check 阶段

setImmediate()的回调会被加入check队列中, 从event loop的阶段图可以知道,check阶段的执行顺序在poll阶段之后。

close callbacks

关闭I/O的动作,比如文件描述符的关闭,连接断开等
如果socket突然中断,close事件会在这个阶段被触发

while (true) {
  loop.forEach((阶段) => {
    阶段全部任务()
    nextTick全部任务()
    microTask全部任务()
  })
  loop = loop.next
}

setTimeout 和 setImmediate 的区别

setImmediate 一旦当前poll阶段结束(poll queue为空或执行任务到达上限)就执行一次脚本

setTimeout 设定一个最短的调度该脚本的时间阈值

不在同一个I/O cycle中的时候,回调的调度顺序是不被保证的

// timeout_vs_immediate.js
setTimeout(() => {
    console.log('timeout');
}, 0);

setImmediate(() => {
    console.log('immediate');
});

// terminal
$ node timeout_vs_immediate.js
timeout
immediate

$ node timeout_vs_immediate.js
immediate
timeout

在同一个I/O cycle中,immediate 总比 timeout 更早被调度

// timeout_vs_immediate.js
const fs = require('fs');

fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0);
    setImmediate(() => {
        console.log('immediate');
    });
});

// terminal
$ node timeout_vs_immediate.js
immediate
timeout

$ node timeout_vs_immediate.js
immediate
timeout

process.nextTick()

process.nextTick() 不是Node的EL中的一部分(虽然它也是异步API),但是,任意阶段的操作结束之后 nextTickQueue 就会被处理。

process.nextTick会比promise优先级高

process.nextTick() 和 setImmediate()

官方推荐使用 setImmediate(),因为更容易推理,也兼容更多的环境,例如浏览器环境

process.nextTick() 在当前循环阶段结束之前触发
setImmediate() 在下一个事件循环中的check阶段触发
通过process.nextTick()触发的回调也会在进入下一阶段前被执行结束,这会允许用户递归调用 process.nextTick() 造成I/O被榨干,使EL不能进入poll阶段

因此node作者推荐我们尽量使用setImmediate,因为它只在check阶段执行,不至于导致其他异步回调无法被执行到

mongodb auth笔记

使用守护进程启动

mongod --config ./mongodb.conf --fork

创建全局admin用户

> use admin
switched to db admin
> show collection
> db.createUser(
...   {
...     user: "root",
...     pwd: "root",
...     roles: [ { role: "userAdminAnyDatabase", db: "admin" }, "readWriteAnyDatabase" ]
...   }
... )
Successfully added user: {
	"user" : "root",
	"roles" : [
		{
			"role" : "userAdminAnyDatabase",
			"db" : "admin"
		},
		"readWriteAnyDatabase"
	]
}

使用admin登录

mongo --port 6930 -u root -p root --authenticationDatabase "admin"

创建eve用户

> use eve
switched to db eve
> db.createUser(
...   {
...     user : "testuser",
...     pwd: "!!!aaa",
...     roles : [
...         'readWrite'
...     ]
...   }
... )
Successfully added user: { "user" : "testuser", "roles" : [ "readWrite" ] }

用eve用户登录

mongo --port 6930 -u testuser -p '!!!aaa' --authenticationDatabase "eve"

对应的配置文件

# mongodb.conf

# Where to store the data.
dbpath=/data/db

#where to log
logpath=/Users/shell/shell/soft/mongodb-osx-x86_64-4.0.4/mongodb.log

logappend=true

bind_ip = 0.0.0.0
port = 6930

# Enable journaling, http://www.mongodb.org/display/DOCS/Journaling
journal=true

# Enables periodic logging of CPU utilization and I/O wait
#cpu = true

# Turn on/off security.  Off is currently the default
#noauth = true
auth = true

# Verbose logging output.
#verbose = true

# Inspect all client data for validity on receipt (useful for
# developing drivers)
#objcheck = true

# Enable db quota management
#quota = true

# Set diagnostic logging level where n is
#   0=off (default)
#   1=W
#   2=R
#   3=both
#   7=W+some reads
#diaglog = 0

# Diagnostic/debugging option
#nocursors = true

# Ignore query hints
#nohints = true

# Disable the HTTP interface (Defaults to localhost:27018).
#nohttpinterface = true

# Turns off server-side scripting.  This will result in greatly limited
# functionality
#noscripting = true

# Turns off table scans.  Any query that would do a table scan fails.
#notablescan = true

# Disable data file preallocation.
#noprealloc = true

# Specify .ns file size for new databases.
# nssize = <size>

# Accout token for Mongo monitoring server.
#mms-token = <token>

# Server name for Mongo monitoring server.
#mms-name = <server-name>

# Ping interval for Mongo monitoring server.
#mms-interval = <seconds>

# Replication Options

# in replicated mongo databases, specify here whether this is a slave or master
#slave = true
#source = master.example.com
# Slave only: specify a single database to replicate
#only = master.example.com
# or
#master = true
#source = slave.example.com

# Address of a server to pair with.
#pairwith = <server:port>
# Address of arbiter server.
#arbiter = <server:port>
# Automatically resync if slave data is stale
#autoresync
# Custom size for replication operation log.
#oplogSize = <MB>
# Size limit for in-memory storage of op ids.
#opIdMem = <bytes>

# SSL options
# Enable SSL on normal ports
#sslOnNormalPorts = true
# SSL Key file and password
#sslPEMKeyFile = /etc/ssl/mongodb.pem
#sslPEMKeyPassword = pass

DNS 简介

  1. 什么是DNS
  2. 解析过程
# 查看示例
# 1. 客户端查询本机DNS缓存www.baidu.com对应的IP地址(hosts文件配置,机器一启动就会加载到本机DNS缓存中)
# 如果没有找到,客户端查询本地配置的域名服务器(即:本地域名服务器,可以是第三方提供的公共域名服务器,例如8.8.8.8)~ dig +trace www.shop.com
;; Warning: Message parser reports malformed message packet.

; <<>> DiG 9.10.6 <<>> +trace www.shop.com
# 如果本地域名服务器没有查到,则由本地域名服务器迭代查询根域名服务器(根域名服务器有13台)
;; global options: +cmd
.			193283	IN	NS	f.root-servers.net.
.			193283	IN	NS	g.root-servers.net.
.			193283	IN	NS	j.root-servers.net.
.			193283	IN	NS	c.root-servers.net.
.			193283	IN	NS	k.root-servers.net.
.			193283	IN	NS	m.root-servers.net.
.			193283	IN	NS	l.root-servers.net.
.			193283	IN	NS	a.root-servers.net.
.			193283	IN	NS	h.root-servers.net.
.			193283	IN	NS	d.root-servers.net.
.			193283	IN	NS	e.root-servers.net.
.			193283	IN	NS	i.root-servers.net.
.			193283	IN	NS	b.root-servers.net.
;; Received 512 bytes from 192.168.0.1#53(192.168.0.1) in 6 ms

com.			172800	IN	NS	d.gtld-servers.net.
com.			172800	IN	NS	a.gtld-servers.net.
com.			172800	IN	NS	b.gtld-servers.net.
com.			172800	IN	NS	e.gtld-servers.net.
com.			172800	IN	NS	i.gtld-servers.net.
com.			172800	IN	NS	f.gtld-servers.net.
com.			172800	IN	NS	c.gtld-servers.net.
com.			172800	IN	NS	l.gtld-servers.net.
com.			172800	IN	NS	m.gtld-servers.net.
com.			172800	IN	NS	h.gtld-servers.net.
com.			172800	IN	NS	g.gtld-servers.net.
com.			172800	IN	NS	k.gtld-servers.net.
com.			172800	IN	NS	j.gtld-servers.net.
com.			86400	IN	DS	30909 8 2 E2D3C916F6DEEAC73294E8268FB5885044A833FC5459588F4A9184CF C41A5766
com.			86400	IN	RRSIG	DS 8 1 86400 20181222170000 20181209160000 2134 . TW4j9reZLul1IeHaY7+7eYic9IhW7lxu3YUg4iXUR4vHSzMOGAYkRUew LmDmsg/nF2jol6TkY75fHFTG3WWT1Uve1+RpGqjX0szAcWmnuX4mzHIn swokDhyqwqK80DayeC67gOZ6w6Z6agsUkWi6oHy4UVrRe1ZaEOQIn5BX UMQJO6qj+1zKC9sx+LaKRZR41o1/nF0tWr7EwpQDOvYPm83LDWnTNoWf 5FnNdykkBSoy5+s0WURInY8wl+fBAmBZD/CNW6lgS277e3xM8FNwKWUX cxTPuAOzaxfr+roFvfDI/D1NcRGmeLkYqgLr5SgarWRLVx3NQaVp0hx4 1i46Yg==
# 根域名服务器返回.com顶级域名对应的NS记录 (这边通过m.root-servers.net解析出.com)
;; Received 1172 bytes from 202.12.27.33#53(m.root-servers.net) in 91 ms

shop.com.		172800	IN	NS	a1.verisigndns.com.
shop.com.		172800	IN	NS	a2.verisigndns.com.
shop.com.		172800	IN	NS	a3.verisigndns.com.
shop.com.		172800	IN	NS	u1.verisigndns.com.
shop.com.		172800	IN	NS	u2.verisigndns.com.
shop.com.		172800	IN	NS	u3.verisigndns.com.
CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN NSEC3 1 1 0 - CK0Q1GIN43N1ARRC9OSM6QPQR81H5M9A  NS SOA RRSIG DNSKEY NSEC3PARAM
CK0POJMG874LJREF7EFN8430QVIT8BSM.com. 86400 IN RRSIG NSEC3 8 2 86400 20181214054228 20181207043228 37490 com. P5rP4UEg+Pg8162pH4TQLffEZn37Wxzy6xfzBIhh9GXQ+H/rde4BPeyy 3fWCoB2zGr1+ULzAqeC7kmdK0Ve7Yx7IGPF5/lpt0BHlt3iarkESu9eG /LXmpL4qchLrAX/adlNZiEfFqXDcQZNgrkbqhYMPaoB/BjghAqejawoh T5I=
RHIGKK6J44K3P1INED5B1T82OJ5CP1GA.com. 86400 IN NSEC3 1 1 0 - RHIH371DI109N4S42FKH8F5N3BB5L5UD  NS DS RRSIG
RHIGKK6J44K3P1INED5B1T82OJ5CP1GA.com. 86400 IN RRSIG NSEC3 8 2 86400 20181214052646 20181207041646 37490 com. bBPz8q0FtuFUtbli7L3nL7FnbGJNkdVBv+cTkEG8N0UQ5FaEOhcD7973 pz0avmURlxDANtTXOZIh693NDDGDQ5nfkgq0/pKOnoi3ljjakY6HNpsW yW/qVbGhOZdYs9lE8i2B57Kzd4pHG9qzX9scV4LtimaSikB5H8jLkCyh YFs=

### 本地域名服务器根据.com的NS记录,查询出shop.com.二级域名的NS记录(即shop.com.二级域名的权威域名服务器)###
;; Received 820 bytes from 192.52.178.30#53(k.gtld-servers.net) in 203 ms

www.shop.com.		43200	IN	CNAME	wildcard.shop.com.edgekey.net.

### 本地域名服务器根据shop.com的NS记录,查询出www.shop.com域名的IP地址并缓存起来 ###
;; Received 84 bytes from 209.112.113.33#53(a1.verisigndns.com) in 208 ms
  1. 有哪些问题
  • DNS劫持:劫持了DNS服务器,通过某些手段取得某域名的解析记录控制权,进而修改此域名的解析结果,导致对该域名的访问由原IP地址转入到修改后的指定IP
  • DNS污染
  1. 怎么解决
    对于DNS劫持可以用HTTPDNS去解决

  2. 总结
    DNS劫持就是指用户访问一个被标记的地址时,DNS服务器故意将此地址指向一个错误的IP地址的行为。范例,网通、电信、铁通的某些用户有时候会发现自己打算访问一个地址,却被转向了各种推送广告等网站,这就是DNS劫持。
     DNS污染,指的是用户访问一个地址,国内的服务器(非DNS)监控到用户访问的已经被标记地址时,服务器伪装成DNS服务器向用户发回错误的地址的行为。范例,访问Youtube、Facebook之类网站等出现的状况。

React-Router

  1. withRouter分析
function withRouter(Component) {
  const C = props => {
    const { wrappedComponentRef, ...remainingProps } = props;

    // 使用Route包裹,然后通过children属性肯定会调用的特性来把routeComponentProps传入给
    // 业务组件
    // wrappedComponentRef 用来接收ref函数
    return (
      <Route
        children={routeComponentProps => (
          <Component
            {...remainingProps}
            {...routeComponentProps}
            ref={wrappedComponentRef}
          />
        )}
      />
    );
  };

  // 用来不丢失静态属性和方法
  return hoistStatics(C, Component);
}

为什么Vue不能观察到数组length的变化?

官网解释如下

由于 JavaScript 的限制,Vue 不能检测以下变动的数组:
当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
当你修改数组的长度时,例如:vm.items.length = newLength

因为vue的响应式是通过Object.defineProperty来实现的,但是数组的length属性是不能添加getter和setter,所有无法通过观察length来判断。

例子

如下代码,虽然看起来数组的length是10,但是for in的时候只能遍历出0, 1, 2,导致了只有前三个索引被加上了getter 和setter

var a = [0, 1, 2]
a.length = 10
// 只是显示的给length赋值,索引3-9的对应的value也会赋值undefined
// 但是索引3-9的key都是没有值的
// 我们可以用for-in打印,只会打印0,1,2
for (var key in a) {
  console.log(key) // 0,1,2
}

那么vue提供了一些解决方法

使用内置的Vue.$set

让数组显式的进行某个索引的观察
Vue.set(array, indexOfItem, newValue)

实际上是调用了

Object.defineProperty(array, indexOfItem, {
  enumerable: true,
  configurable: true,
  get() { },
  set(newVal) { }
})

这样可以手动指定需要观察的key,那么就可以达到预期的效果。

重写了 push, pop, shift, unshift, splice, sort, reverse方法

Vue源码

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

/**
 * Intercept mutating methods and emit events
 */
;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

这些是在Array.prototype上 进行了方法重写或者添加

并且对添加属性的方法如push,unshift,splice所添加进来的新属性进行手动观察,源码为

  if (inserted) ob.observeArray(inserted)

对以上方法进行了手动的进行消息触发

  ob.dep.notify()

结论

vue对数组的length直接改变无法直接进行观察,提供了vue.$set 进行显式观察,并且重写了 push, pop, shift, unshift, splice, sort, reverse方法来进行隐式观察。

webpack笔记 简单的webpack

webpack流程

  1. Complier 对象代表了完整的webpack的环境配置。
  2. Complier.run() -> Compliation 对象代表了一次资源版本构建。
  3. Compilation 对象也提供了很多关键步骤的回调,带来了一次版本的chunk。
  4. Compilation.buildModule -> loader -> chunk。
  5. Parser -> Dependency (负责处理依赖)。
  6. Template自带的代码模板直接生成最后的解析结果。
  7. Compiler / Compilation都继承Tapable

优化点

  1. 通过dll构建公共包例如react redux
    1.1 通过new webpack.optimize.CommonsChunkPlugin提取公共代码,例如工具包,antd 详情
  2. 抽象runtime代码
  3. 使用import() 进行懒加载
  4. tree-sharking
  5. PurifyCSSPlugin 进行csstree-sharking
  6. 使用chunkHash
  7. 通过HtmlWebpackPlugin插入runtime代码,减少请求
  8. 开启多进程模式
  9. manifest
  10. 通过SpeedMeasurePlugin来观察优化情况
  11. 开启Scope Hoisting

简易的webpack

// 流程
// chunk -> module -> loader -> dependency -> template
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const ejs = require('ejs');
const entry = process.argv[2] || './index.js';
const output = './mywebpack-output.js';
const modules = [];

function styleLoader(source) {
  const styleTemplate = `const style = document.createElement('style');
  style.innerHTML = ${JSON.stringify(source).replace(/\\n/g, '')};
  document.head.appendChild(style);`
  return styleTemplate;
}
function getDep(result) {
  result.replace(/require\(['"](.+?)['"]\)/g, (...args) => {
    if (!args) return;
    const script = args[1];
    let content = fs.readFileSync(path.join('./src', script), 'utf-8');
    if (/\.css$/.test(script)) {
      content = styleLoader(content);
    }
    if (modules.find((d) => d.script === script)) {
      return;
    }
    modules.push({
      script,
      content,
    });
    getDep(content);
  });
}

const result = fs.readFileSync(path.join('./src', entry), 'utf-8');
getDep(result);

const template = `(function(modules) {
	function require(moduleId) {
		var module = {
			exports: {}
		};
		modules[moduleId](module, module.exports, require);
		return module.exports;
	}
	return require(\`<%-entry%>\`);
})
({
  "<%-entry%>":
  (function(module, exports, require) {
    eval(\`<%-result%>\`);
    }),
  <%for(let i = 0; i < modules.length; i += 1) {
    let { script, content } = modules[i];%>
    "<%-script%>": 
    (function(module, exports, require) {
      eval(\`<%-content%>\`);
    }),
  <%}%>
});`;

const outContent = ejs.render(template, {
  entry,
  result,
  modules,
});

// console.log(outContent);

fs.writeFileSync(path.join('./dist', output), outContent);

// 首先读取入口文件,然后对入口文件进行依赖解析,如果解析到有require函数的话,再次进行文件读取,返回内容,放入modules数组中备用,
// 然后把主入口文件首先用模板渲染完成以后,再去对modules数组,进行循环渲染
// 其中匹配文件的时候如果遇到了css文件,那么使用style loader进行处理




// (function main(modules) {
//   function require(moduleId) {
//     var module = {
// 			exports: {}
// 		};
//     modules[moduleId].call(module.exports, module, module.exports, require);
//     return module.exports;
//   }
//   require('./index.js');
// })({
//   './index.js': (() => {
//     eval("alert(1)")
//   })
// })

前端监控的思考

1.捕获js中抛出error,不包括语法错误和promise reject

可以用重写window.onerror 进行捕获,
类似代码

const tmpError = window.onerror;
window.onerror = function (...args) {
  // 如果有业务事件
  if (tmpError) tmpError.call(args);
  console.log('error is catch');
  // 需要return false,不能return true; 会让错误出现在浏览器控制台
  return false
}

2.捕获资源加载异常img, script, css, jsonp, link

需要使用addEventListener,需要注意的是error时间不会冒泡,需要在捕获阶段监听,第三个参数为true

window.addEventListener('error', (e) => {
  // JS语法报错这里也会被捕获,需要过滤掉
  const { type, target } = e;
  if (target === window) return;
  // some-code
}, true);

其中还有框架的错误接口如 Vue的错误总结

3.获取资源加载时间

这里分为两种一种是js加载前已经在页面上有DOM节点了,第二种是动态加入资源,第三种是使用performance。

第二种比较简单,先说第二种的思路
通过MutationObserver API去监听DOM的变动,从而给资源标签加上load事件,获取加载时间
简单代码如下

const observer = new MutationObserver(function (mutations) {
  mutations.forEach(function (mutation) {
    if (mutation.type === 'childList') {
      mutation.addedNodes.forEach((target) => {
        if (target.nodeName !== 'IMG') return;
          on(target);
          waitMap[target.src] = {
            start: Date.now(),
          };
      });
    }
  });
});
observer.observe(document.querySelector('body'), { subtree: true, childList: true });
function on(el) {
  el.addEventListener('load', handleSuccess);
}
function handleSuccess({ target }) {
  if (!waitMap[target.src]) return;
  successs.push({
    src: target.src,
    end: Date.now(),
    start: waitMap[target.src].start,
    duration: Date.now() - waitMap[target.src].start,
  });
  console.log(waitMap, successs);
  this.removeEventListener('load', handleSuccess);
}

第一种需要在第二种的基础上
添加对已有存在页面上的资源进行绑定事件

document.querySelectorAll('img').forEach((img) => {
  waitMap[img.src] = {
    start: Date.now(),
  };
  on(img);
});

但是这个会有弊端,如果在绑定事件之前这个资源已经下载完了,那该怎么处理

第三种的话兼容性不太好,实测微信浏览器下无法访问到资源的加载情况

performance.getEntries('resource');

4.如何获取ajax和fetch的请求信息

xhr

可以通过添加一个ajax-hook来进行处理
原理是在window上重新构造一个XMLHttpRequest对象,然后把原生的XMLHttpRequest作为一个属性赋值给他,然后对原生XMLHttpRequest的所有属性进行重写,如果是函数的话,先调用hook函数,然后调用原本的函数

function hookfun(fun) {
  return function () {
    var args = [].slice.call(arguments)
    if (funs[fun] && funs[fun].call(this, args, this.xhr)) {
      return;
    }
    return this.xhr[fun].apply(this.xhr, args);
  }
}

然后对于所有原生xhr的属性进行重写getter setter,getter获取this.xhr上的属性,setter进行判断是属性的话在新构造的XHR对象添加一个私有属性,如果是函数的话使用用户定义的hook或者使用用户赋值的函数

以上参考开源库Ajax-hook

fetch

通过重写fetch来进行监控

if (!window.fetch) return;
// 保留原先的fetch
const tmpFetch = fetch;
window.fetch = function rewriteFetch(...args) {
    // some code
    return tmpFetch.apply(this, args)
      .then(res => res)
      .catch(err => err);
  };

跨域的JS

  1. 需要在script添加corssorgin属性,
  2. 并且在资源响应头添加Acess-Control-Allow-Origin: "*"
    否则错误信息是script error 并且没有错误行号和列号

错误监控

使用img标签上报

简单介绍HTTPS加密过程

  1. 正常的TCP三次握手以后,client发送一个clinet hello数据包给服务器,其中带有SSL协议版本,随机数,加密套件等。

  2. server收到以后,发送一个server hello请求,并且带有SSL的协议版本,随机数,加密算法,数字证书和公钥

  3. 客户端收到后,首先通过数字证书验证公钥是否被修改,没有修改的话,客户端生成随机数2,通过公钥加密,发送给server,这一步是非对称加密。

  4. 接下来客户端和服务端分别使用两个客户端随机数和一个服务器随机数生成预主秘钥,接下来的数据包传输都通过这个预主秘钥进行对称加解密

第三3步是非对称加密,第4步和以后都是对称加密,所以HTTPS前期协商预主秘钥的时候会比较慢

公钥和内容的鉴定

  1. 首先服务器向权威机构申请数字证书
    (公钥 + 域名) -> 权威机构私钥加密 -> 数字证书
  2. 生成摘要通过私钥加密给客户端
    内容(hash) -> 摘要(digest) -> 私钥加密 -> (数字签名(signature) + 数字证书)(服务器打包给客户端)
  3. 客户端校验公钥真实性和内容是否伪造
    权威机构公钥解密数字证书 -> (公钥 + 域名) -> 1. 通过hash计算内容的摘要 通过公钥解密数字签名,把两个摘要对比,一样的话没有被篡改

koa-compose

async function f1(ctx, next) {
  console.log(1);
  await next();
  console.log(6);
}

async function f2(ctx, next) {
  console.log(2);
  await next();
  console.log(5);
}

async function f3(ctx, next) {
  console.log(3);
  await next();
  console.log(4);
}

function compose(mids) {
  return function(ctx, next) {
    let index = -1;
    return dispatch(0);
    function dispatch(i) {
      index = i;
      let fn = mids[i];
      if (!fn) return Promise.resolve();
      const result = fn(ctx, dispatch.bind(null, i + 1));
      return Promise.resolve(result);
    }
  }
}

compose([f1, f2, f3])(); // 1 2 3 4 5 6

snabbdom核心逻辑源码分析

snabbdom核心逻辑源码分析

https://github.com/snabbdom/snabbdom/blob/v0.2.2/snabbdom.js

// h函数
var VNode = require('./vnode');
module.exports = function h(sel, b, c) {
  var data = {}, children, text, i;
  if (is.array(children)) {
    for (i = 0; i < children.length; ++i) {
      if (is.primitive(children[i])) children[i] = VNode(undefined, undefined, undefined, children[i]);
    }
  }
  return VNode(sel, data, children, text, undefined);
};
// vnode函数
module.exports = function(sel, data, children, text, elm) {
  var key = data === undefined ? undefined : data.key;
  return {sel: sel, data: data, children: children,
          text: text, elm: elm, key: key};
};
// 简化版updateChildren函数
function updateChildren(vnode, newVnode) {
  const children = vnode.children;
  const newChildren = newVnode.children;

  children.forEach((child, index) => {
    const newChild = newChildren[index];
    if (!newChild) return;
    if (child.tag === newChild.tag) {
      updateChildren(child, newChild);
    } else {
      replaceNode(child, newChild);
    }
  })
}

diff 算法

首先把创建oldVNode和VNode,然后调用patch方法,
这里会先调用pre钩子,然后判断oldVNode是否是VNode,如果不是的话,那么说明是dom,然后把这个dom更新成vnode,然后把这个dom的父级dom获取到,然后把vnode.elm插入到parent中,把原来的oldVNode.elm从页面中删除
如果两个都是vnode,根据tag和key都一样的话,那么调用patchVnode,这里会调用update钩子,其中首先判断如果有text,没有chidren,根据新text和老text对比更新,接下来判断children,如果有新的ch没有老的ch,那么执行更新操作,如果有老的ch,没有新的ch,那么执行删除操作,如果新老都有的话,那么调用updateChildren,其中逻辑为双指针从两边往中间走,

  1. 两个start指针指向的vnode tag和key一样,那么调用patchVnode进行递归处理,然后指针往后走1
  2. 两个end指针指向的vnode tag和key一样,那么调用patchVnode进行递归处理,然后指针往前走1
  3. 如果老的start等于新的end,那么把那么调用patchVnode进行递归处理,然后把老的elm移动到oldEndVnode的右边
  4. 如果老的end等于新的start,那么把那么调用patchVnode进行递归处理,然后把老的elm移动到oldStartVnode的左边
  5. 如果前后都不一样,那么把老的vnode做成map,key是vnode的key,值是vnode,然后用新的vnode去map中查找,
  6. 如果找到了,并且key,tag一样,然后调用patchVnode进行递归处理,然后插入到查找到的vnode.elm的前面,如果key不一样,那么重新创建,进行插入
  7. 如果没有找到,那么也是直接插入,
  8. 如果oldVnode的指针过界了,那么直接停止,把还剩下的新的vnode插入到最后
  9. 如果newVNode的指针过界了,那么把oldVNode开始结束指针之间的vnode直接删除

怎么从 vnode 创建其对应的 DOM 树?即 Virtual DOM 到真实 DOM。

createElm函数逻辑
首先判断vnode的type
如果type === 'comment', 那么调用createComment创建一个注释节点,挂载到vnode.elm属性
如果type === '', 那么直接生成一个文本节点,挂载到vnode.elm属性
如果是其他type的话通过createElement去创建dom,然后挂载到vnode.elm属性, 并且如果有children,并且是一个数组的话,去递归的创建相关的子节点, 然后通过appendChild函数挂在父节点上
如果children没有值得情况下使用createTextNode创建一个文本节点,进行挂载,
当所有的dom创建完成以后,挂载到浏览器中


怎么比较 oldVnode 与 newVnode 两个 vnode,并实现 DOM 树更新?diff 算法应该尽量高效,更新应该尽量复用已有 DOM(最小更新)?

patch 流程

oldVNode = document.querySelect('#app');
首先我们调用patch(oldVNode, vnode)
首先会判断oldVNode是不是一个vnode,如果不是话会调用用第一个参数创建一个vnode,新的vnode的elment为第一个参数的引用,如果是的话调用patchVnode函数

接下来如果发现oldVNode和第二个参数vnode的key和tag是否一致,如果不一致的话会判断oldVNode是否有父级,如果有父级的话会去把本身删掉,重新插入之前生成的新的vnode,并且会把第二个参数vnode通过createElm函数来创建一个实体dom,并且挂载在该vnode上,然后把vnode推入insertedVnodeQueue队列


patchVnode函数工作流程
patchVnode(oldVnode, vnode);
ch = vnode.children
oldCh = oldVnode.children
首先创建一个变量elm = oldVnode.elm = vnode.elm,因为他们是同一个vnode
if (oldVnode === vnode),他们的引用一致,可以认为没有变化。
if(oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用修改 elm.text = vnode.text。
if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用 updateChildren 函数比较子节点
if (ch),只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点,(方式为对 ch 中每个 vnode 递归创建 dom 并插入到 elm)
if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点。(把oldCh每一个从elm中删除)


updateChildren函数工作原理

/**
  * 比较新旧 children 并更新
  * @param  {Element} parentElm          父 dom,children 对应的 dom 将要挂载的
  * @param  {Array} oldCh              旧 children,vnode 数组
  * @param  {Array} newCh              新 children,vnode 数组
  * @param  {Array} insertedVnodeQueue 用于存储将要插入的 dom 节点
  * @return {Undefined}
  */
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue) {
  let oldStartIdx = 0, newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx
  let idxInOld
  let elmToMove
  let before

  // 遍历 oldCh 和 newCh 来比较和更新
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    // 1⃣️ 首先检查 4 种情况,保证 oldStart/oldEnd/newStart/newEnd
    // 这 4 个 vnode 非空,左侧的 vnode 为空就右移下标,右侧的 vnode 为空就左移 下标。
    if (oldStartVnode == null) {
      oldStartVnode = oldCh[++oldStartIdx]
    } else if (oldEndVnode == null) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (newStartVnode == null) {
      newStartVnode = newCh[++newStartIdx]
    } else if (newEndVnode == null) {
      newEndVnode = newCh[--newEndIdx]
    }
    /**
      * 2⃣️ 然后 oldStartVnode/oldEndVnode/newStartVnode/newEndVnode 两两比较,
      * 对有相同 vnode 的 4 种情况执行对应的 patch 逻辑。
      * - 如果同 start 或同 end 的两个 vnode 是相同的(情况 1 和 2),
      *   说明不用移动实际 dom,直接更新 dom 属性/children 即可;
      * - 如果 start 和 end 两个 vnode 相同(情况 3 和 4),
      *   那说明发生了 vnode 的移动,同理我们也要移动 dom。
      */
    // 1. 如果 oldStartVnode 和 newStartVnode 相同(key相同),执行 patch
    else if (isSameVnode(oldStartVnode, newStartVnode)) {
      // 不需要移动 dom
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    }
    // 2. 如果 oldEndVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newEndVnode)) {
      // 不需要移动 dom
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 3. 如果 oldStartVnode 和 newEndVnode 相同,执行 patch
    else if (isSameVnode(oldStartVnode, newEndVnode)) {
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      // 把获得更新后的 (oldStartVnode/newEndVnode) 的 dom 右移,移动到
      // oldEndVnode 对应的 dom 的右边。为什么这么右移?
      // (1)oldStartVnode 和 newEndVnode 相同,显然是 vnode 右移了。
      // (2)若 while 循环刚开始,那移到 oldEndVnode.elm 右边就是最右边,是合理的;
      // (3)若循环不是刚开始,因为比较过程是两头向中间,那么两头的 dom 的位置已经是
      //     合理的了,移动到 oldEndVnode.elm 右边是正确的位置;
      // (4)记住,oldVnode 和 vnode 是相同的才 patch,且 oldVnode 自己对应的 dom
      //     总是已经存在的,vnode 的 dom 是不存在的,直接复用 oldVnode 对应的 dom。
      api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    }
    // 4. 如果 oldEndVnode 和 newStartVnode 相同,执行 patch
    else if (isSameVnode(oldEndVnode, newStartVnode)) {
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      // 这里是左移更新后的 dom,原因参考上面的右移。
      api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    }

    // 3⃣️ 最后一种情况:4 个 vnode 都不相同,那么我们就要
    // 1. 从 oldCh 数组建立 key --> index 的 map。
    // 2. 只处理 newStartVnode (简化逻辑,有循环我们最终还是会处理到所有 vnode),
    //    以它的 key 从上面的 map 里拿到 index;
    // 3. 如果 index 存在,那么说明有对应的 old vnode,patch 就好了;
    // 4. 如果 index 不存在,那么说明 newStartVnode 是全新的 vnode,直接
    //    创建对应的 dom 并插入。
    else {
      // 如果 oldKeyToIdx 不存在,创建 old children 中 vnode 的 key 到 index 的
      // 映射,方便我们之后通过 key 去拿下标。
      if (oldKeyToIdx === undefined) {
        oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      }
      // 尝试通过 newStartVnode 的 key 去拿下标
      idxInOld = oldKeyToIdx[newStartVnode.key]
      // 下标不存在,说明 newStartVnode 是全新的 vnode。
      if (idxInOld == null) {
        // 那么为 newStartVnode 创建 dom 并插入到 oldStartVnode.elm 的前面。
        api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        newStartVnode = newCh[++newStartIdx]
      }
      // 下标存在,说明 old children 中有相同 key 的 vnode,
      else {
        elmToMove = oldCh[idxInOld]
        // 如果 type 不同,没办法,只能创建新 dom;
        if (elmToMove.type !== newStartVnode.type) {
          api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm)
        }
        // type 相同(且key相同),那么说明是相同的 vnode,执行 patch。
        else {
          patchVnode(elmToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
        }
        newStartVnode = newCh[++newStartIdx]
      }
    }
  }

  // 上面的循环结束后(循环条件有两个),处理可能的未处理到的 vnode。
  // 如果是 new vnodes 里有未处理的(oldStartIdx > oldEndIdx
  // 说明 old vnodes 先处理完毕)
  if (oldStartIdx > oldEndIdx) {
    before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm
    addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  }
  // 相反,如果 old vnodes 有未处理的,删除 (为处理 vnodes 对应的) 多余的 dom。
  else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

TCP流量控制

什么是流量控制?流量控制的目的?

如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面。

如何实现流量控制?

TCP的滑动窗口的可靠性也是建立在“确认重传”基础上的。

TCP 利用滑动窗口实现流量控制的机制。
滑动窗口(Sliding window)是一种流量控制技术。TCP 中采用滑动窗口来进行传输控制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据。当滑动窗口为 0 时,发送方一般不能再发送数据报,但有两种情况除外,一种情况是可以发送紧急数据,例如,允许用户终止在远端机上的运行进程。另一种情况是发送方可以发送一个 1 字节的数据报来通知接收方重新声明它希望接收的下一字节及发送方的滑动窗口大小。

总结

由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 数据包中除了ack,还会包含自己的接收窗口的大小rwnd,通过这个字段来控制发送方滑动窗口的大小

[算法]排序

桶排序 复杂度为 O(M+N), 限制为只能是正整数排序,而且n不能太大,因为需要开辟一个n长度的数组

function bucketSort(arr, n) {
    const bucket = Array.from(new Array(n)).map(() => 0);
    for (let i = 0; i <= arr.length; i += 1) {
      bucket[arr[i]] += 1;
    }
    const reslut = bucket.map((item, index) => item ? index : 0).filter(item => item);
    return reslut;
  }

快速排序 复杂度为O(NlogN)

  /* 基准是6
     i                           j
     6, 1, 2, 7, 9, 3, 4, 5, 10, 8
     i                    j
     6, 1, 2, 7, 9, 3, 4, 5, 10, 8
              i           j
     6, 1, 2, 7, 9, 3, 4, 5, 10, 8
              i           j
     6, 1, 2, 5, 9, 3, 4, 7, 10, 8
                 i     j   
     6, 1, 2, 5, 9, 3, 4, 7, 10, 8
                 i     j   
     6, 1, 2, 5, 4, 3, 9, 7, 10, 8
                    ij  
     6, 1, 2, 5, 4, 3, 9, 7, 10, 8
                    ij
     3, 1, 2, 5, 4, 6, 9, 7, 10, 8

     left, i - 1
     i           j
     3, 1, 2, 5, 4
           ij   
     3, 1, 2, 5, 4
           ij     
     2, 5, 3, 1, 4

     left, i - 1
     i  j
     2, 5
     
     right, i + 1
     i  j   
     1, 4

  */
  
  function quickSort(left, right, arr) {
    let i, j, t, temp;
    if (left > right) {
      return;
    }
    temp = arr[left];
    i = left;
    j = right;
    // i 往右边移动 j往左边移动 直到两个碰头
    while(i !== j) {
      // j往右移动,寻找比基准值temp小的值 并且ij两个不能相遇
      while(arr[j] >= temp && i < j) {
        j--;
      }
      // i往做移动,寻找比基准值temp大的值 并且ij两个不能相遇
      while(arr[i] <= temp && i < j) {
        i++;
      }
      // 如果两位哨兵还没相遇,那么进行相互交换值
      if (i < j) {
        t = arr[i];
        arr[i] = arr[j];
        arr[j] = t;
      }
    }
    // 相遇了以后把基准值和当前ij所在的位置的值交换
    arr[left] = arr[i];
    arr[i] = temp;

    // 然后根据这个基准值分成两个数据 分别进行之前的操作
    quickSort(left, i - 1, arr);
    quickSort(i + 1, right, arr);
  }
  var arr = [6, 1, 2, 7, 9, 3, 4, 5, 10, 8];
  
  quickSort(0, arr.length - 1, arr);

  console.log(arr);

冒泡排序 复杂度为O(n²)

function bubbleSort(arr) {
    const n = arr.length;
    for (var i = 0; i <= n - 1; i++) {
      for (let j = 0; j <= n - i; j++) {
        if (arr[j] < arr[j + 1]) {
          var t = arr[j];
          arr[j] = arr[j + 1];
          arr[j + 1] = t;
        }
      }
    }
    console.log(arr);
  }

冒泡排序优化

function sort(array) {
  for (let i = array.length - 1; i > 0; i -= 1) {
    let exchange = false;
    for (let j = 0; j < i; j++) {
      if (array[j] > array[j + 1]) {
        swap(array, j, j + 1);
        exchange = true;
      }
    }
    if (!exchange) return;
  }
}

const array = [9, 1, 4, 6, 7];

sort(array);
console.log(array);

function swap(array, i, j) {
  const tmp = array[i];
  array[i] = array[j];
  array[j] = tmp;
}

其中用exchange来判断是否数组中是否还有乱序的item,没有的话直接停止排序

TCP可靠传输的实现

TCP 如何控制可靠传输

最基本的传输可靠性来源于确认重传机制。

确认丢失

如上图a. B发送出的对M1的确认丢失了.A在规定时间内没有接收到确认,无法知道是自己发出的分组出错,丢失,或B的确认丢失.A需要在计时器到期后重传M1.这时B又收到了M1,需要

  • 丢弃该分组M1,不向上层交付.
  • 向A发送确认.以免A再次发送M1.

确认迟到

如上图b. 由于网络延迟等原因,B发出的对M1的确认没能在指定时间内到达A,而是在以后的某个时间到达了.这时A会收到重复 的确认(因为A会超时重传).对于这样的确认,A只需丢弃.对于重传的M1,B也需要确认,并丢弃M1.

上述的确认和重传机制,可以在不可靠的网络上,进行可靠的通信,叫做自动重传请求ARQ

nginx

  1. 对静态资源使用proxy_pass的时候,其中有css,js等文件的话,需要对这些资源单独进行proxy_pass代理
  2. 使用proxy_pass的时候如果需要把location的路径不匹配进去的话,需要在转发的地址后面加上\

Vue的错误处理总结

vue/src/core/util/error.js

  1. 全局错误/警告
    在Vue.config 提供了errorHandler和warnHandler接口,分别返回error/wran信息,当前错误的组件实例,错误消息(具体指在哪个地方抛出的错误,例如 "mounted hook"),代码如下
export function handleError (err: Error, vm: any, info: string) {
/**生命周期错误,暂时忽略**/
  if (vm) {
    let cur = vm
    while ((cur = cur.$parent)) {
      const hooks = cur.$options.errorCaptured
      if (hooks) {
        for (let i = 0; i < hooks.length; i++) {
          try {
            const capture = hooks[i].call(cur, err, vm, info) === false
            if (capture) return
          } catch (e) {
            globalHandleError(e, cur, 'errorCaptured hook')
          }
        }
      }
    }
  }
/**生命周期错误,暂时忽略**/
  globalHandleError(err, vm, info)
}

function globalHandleError (err, vm, info) {
 // 根据用户是否定义了errorHandler函数。
  if (config.errorHandler) {
    try {
      return config.errorHandler.call(null, err, vm, info)
    } catch (e) {
      logError(e, null, 'config.errorHandler')
    }
  }
  // 这里就是如果有conosle对象就调用,否则就抛出error
  logError(err, vm, info)
}

  1. 子组件错误冒泡
    提供了errorCaptured生命周期
/* 这里通过不停的寻找父级和是否有errorCaptured方法来进行调用,
如果errorCaptured返回false那么停止往上查找,
如果这里报错,直接调用全局的errorHandler函数。
**注意**:如果这里停止向上了,那么全局函数errorHandler无法被调用 */
 if (vm) {
    let cur = vm
    while ((cur = cur.$parent)) {
      const hooks = cur.$options.errorCaptured
      if (hooks) {
        for (let i = 0; i < hooks.length; i++) {
          try {
            const capture = hooks[i].call(cur, err, vm, info) === false
            if (capture) return
          } catch (e) {
            globalHandleError(e, cur, 'errorCaptured hook')
          }
        }
      }
    }
  }
  1. 哪些地方会进行错误的try catch
  • 执行用户emit事件的时候
  • 调用hook的时候
  • 调用render函数的时候
  • getData的时候
  • watcher函数有回调的时候
  • 计算属性有get的时候
  • nextTick回调
  • 指令生命周期的时候-bind-inserted-update-componentUpdated-unbind
  1. 其他
  • render函数出现错误的时候可以用renderError来进行捕获,这样就不会调用errorHandler了

postcss-module 源码解析

postcss-module源码

postcss-module依赖了很多相关的lib,
css-modules-loader-core
postcss-modules-extract-imports
postcss-modules-local-by-default
postcss-modules-scope
postcss-modules-values
icss-utils 用来生成和解析css

module.exports = postcss.plugin(PLUGIN_NAME, (opts = {}) => {
  // 传入用户的write json方法或者使用默认的json方法,默认为在当前css的目录下写入一个同名的json文件
  const getJSON = opts.getJSON || saveJSON;
  return async (css, result) => {
    // 传入的源css文件地址
    const inputFile     = css.source.input.file;
    // 过滤掉本身插件
    const resultPlugins = result.processor.plugins.filter(isResultPlugin);
    /**
     * 返回一个插件列表
     * 有两种情况local 和 GLOBAL
     * 如下
     * const plugins = {
        [behaviours.LOCAL]: [
          postcss-modules-values
          postcss-modules-local-by-default,
          postcss-modules-extract-imports,
          postcss-modules-scope,
        ],
        [behaviours.GLOBAL]: [
          postcss-modules-values
          postcss-modules-extract-imports,
          postcss-modules-scope,
        ],
      };
      具体功能后面讲
     */
    const pluginList    = getDefaultPluginsList(opts, inputFile);
    // 组成一个plugin列表, 顺序从前往后
    // postcss-modules-values
    // postcss-modules-local-by-default,
    // postcss-modules-extract-imports,
    // postcss-modules-scope,
    // 传入给postcss的其他plugin
    // 以上顺序
    const plugins       = [...pluginList, ...resultPlugins];
    // 使用用户传入的loader或者默认的FileSystemLoader,生成loader实例
    // 这个loader需要去读取到源css文件的内容
    const loader        = getLoader(opts, plugins);
    // Parser内置了三个pulgin fetchAllImports, linkImportedSymbols, extractExports
    const parser        = new Parser(loader.fetch.bind(loader));
    // 这里一共有7 + N个plugin, 进行一系列的语法处理
    await postcss([...plugins, parser.plugin]).process(css, { from: inputFile });

    // finalSource是一个计算属性,可以从缓存的loader获取处理完毕的css代码
    const out = loader.finalSource;
    // 插入到css中
    if (out) css.prepend(out);

    // getJSON may return a promise
    // exportTokens 为一个json,内容是css处理完成以后的映射内容
    return getJSON(css.source.input.file, parser.exportTokens, result.opts.to);
  };
});

TCP拥塞控制

拥塞控制是什么

拥塞控制是一种用来调整传输控制协议(TCP)连接单次发送的分组数量cwnd的算法

慢开始和拥塞避免算法

发送方维持拥塞窗口cwnd
发送方控制拥塞窗口的原则是:

  • 只要网络没有出现拥塞,拥塞窗口就在增大一些,以便把更多的分组发送出去。
  • 只要把网络出现拥塞,拥塞窗口就减少一些,以减少注入到网络中的分组数。

如上图所示

  1. 发送一个数据包
  2. 发送二个数据包
  3. 发送四个数据包
  4. 发送八个数据包
  5. 发送十六个数据包
  6. 发现到达了慢开始门限,开始一个一个数据包的网上加
  7. 直到发现有丢包现象,比如24的时候,调整慢开始门限为12,进行慢开始算法,恢复到1个数据包,重新进行指数增长
  8. 到12个数据包的时候,再次一个一个往上叠加数据包

以上就是慢开始和拥塞避免算法过程

拥塞避免并非指完全能够避免拥塞,利用以上的措施要完全避免拥塞还是不可能的

拥塞避免是说在拥塞避免阶段把拥塞窗口控制为按线性规律增长,使得网络比较不容易出现拥塞

快重传和快恢复

快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期

快重传配合使用的还有快恢复算法,有以下两个要点:

当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞)。但是接下去并不执行慢开始算法
考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。如下图:TCP Reno版本是目前使用最广泛的版本。

拥塞控制和流量控制 #19 的区别

拥塞控制:拥塞控制是作用于网络的,它是防止过多的数据注入到网络中,避免出现网络负载过大的情况;常用的方法就是:

  • 慢开始、拥塞避免
  • 快重传、快恢复。

流量控制:流量控制是作用于接收者的,它是控制发送者的发送速度从而使接收者来得及接收,防止分组丢失的。

ES6 Iterator要点总结

1. Iterator(遍历器)的概念

  • 遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

2. 目的

  • 一是为各种数据结构,提供一个统一的、简便的访问接口;
  • 二是使得数据结构的成员能够按某种次序排列;
  • 三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

3. 默认 Iterator 接口

原生具备 Iterator 接口的数据结构如下。

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函数的 arguments 对象
  • NodeList 对象

例子

  • 数组Symbol.iterator的属性
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }
  • 下面是另一个类似数组的对象调用数组的Symbol.iterator方法的例子。
let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

4. 调用 Iterator 接口的场合

  • 解构赋值
  • 扩展运算符
  • yield*
  • for...of
  • Array.from()
  • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
  • Promise.all()
  • Promise.race()

5. 遍历器对象的 return(),throw()

遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。

6. 手动部署Iterator

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};
console.log(...obj); // hello, world

7. 对对象使用for of

  • 一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。
for (var key of Object.keys(someObject)) {
   console.log(key + ': ' + someObject[key]);
 }
  • 另一个方法是使用 Generator 函数将对象重新包装一下。
function* entries(obj) {
   for (let key of Object.keys(obj)) {
     yield [key, obj[key]];
   }
 }

 for (let [key, value] of entries(obj)) {
   console.log(key, '->', value);
 }

vuejs笔记

1.对表单组件绑定v-model使用get set的时候,如果在set中进行输入过滤,会发生表单值和数据值不一致的情情况,需要进行fouceUpdate,如果是进行过包装的表单组件,那么需要在data上放置一个key,每次set更新key让其强制render来确保表单值和数据值保持一致。

中缀表达式转换成后缀表达式

中缀表达式转换成后缀表达式,并且进行计算

参考大话数据结构第四章栈与队列实现

const computed = '9+(3-1)*3+10/2/2/2+2*6+(7*7)'
  .replace(/\s*/g, '')
  .replace(/(\d+)/g, '$1 ')
  .replace(/(\+|\(|\)|\*|\-|\/)/g, '$1 ')
  .trim();

const weightMap = {
  '+': 1,
  '-': 1,
  '*': 2,
  '(': 1,
  '/': 2,
};
const computedArray = computed.split(' ');

/**
 * 规则:从左到右遍历中缀表达式的每个数字和符号,
 * 若是数字就输出,即成为后缀表达式的一部分;
 * 若是符号,则判断其与栈顶符号的优先级,
 * 是右括号或优先级低于找顶符号(乘除优先加减)则栈顶元素依次出找并输出,
 * 并将当前符号进栈,一直到最终输出后缀表达式为止。
**/
function postfix(computedArray) {
    const S1 = [];
    const S2 = [];
    for (let index = 0; index < computedArray.length; index++) {
      const value = computedArray[index];
      // 如果是右括号那么去匹配左括号之间的符号
      if (value === ')') {
        // 对S2进行出栈,直到碰见"("
        let element;
        while (element !== '(') {
          element = S2.shift();
          S1.push(element)
        }
      }
      const symbolWeight = weightMap[value];
      if (symbolWeight) {
        // 如果入栈的符号比栈顶的符号的权重要低的话,那么把这个栈清空放入主栈里,并且把这个符号放入符号栈内
        while(
          symbolWeight <= weightMap[S2[0]] &&
          S2[0] !== '(' &&
          value !== '('
        ) {
          S1.push(S2.shift());
        }
        S2.unshift(value);
      } else {
        S1.push(value);
      }
    }
    return [...S1, ...S2].filter((item) => item !== ')' && item !== '(');
  }


/**
* 从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,
* 用运算符对它们做相应的计算(次栈顶元素 op 栈顶元素),并将结果入栈;
* 重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。
**/
function calc(array) {
  const stack = [];
  for (let index = 0; index < array.length; index++) {
    const value = array[index];
    const symbolWeight = weightMap[value];
    // 如果栈里面有两个以上包括两个的数字,那么把他们取出来,进行运算,然后放回去
    if (symbolWeight && stack.length >= 2) {
      const right = stack.shift();
      const left = stack.shift();
      stack.unshift(Function(`return ${left} ${value} ${right}`)());
    } else {
      stack.unshift(+value);
    }
  }
  return stack[0];
}

console.log(calc(postfix(computedArray))) // 77.25
console.log(Function(`return ${computed}`)()); // 77.25

Vue.js事件机制

原生事件

  1. 解析模板
<child @select="handleSelect($event)" @click.native.prevent="handleNativeClick($event)"/>

"{on:{"select":function($event){handleSelect($event)}},nativeOn:{"click":function($event){$event.preventDefault();handleNativeClick($event)}},"

修饰符会进行字符串拼接

const modifierCode: { [key: string]: string } = {
  stop: '$event.stopPropagation();',
  prevent: '$event.preventDefault();',
  self: genGuard(`$event.target !== $event.currentTarget`),
  ctrl: genGuard(`!$event.ctrlKey`),
  shift: genGuard(`!$event.shiftKey`),
  alt: genGuard(`!$event.altKey`),
  meta: genGuard(`!$event.metaKey`),
  left: genGuard(`'button' in $event && $event.button !== 0`),
  middle: genGuard(`'button' in $event && $event.button !== 1`),
  right: genGuard(`'button' in $event && $event.button !== 2`)
}

这里需要注意的是如果是nativeOn事件,在初始化组件的时候会把on事件赋值给listeners,把nativeOn赋值个on,所以下面获得的是on属性里中原生事件

  1. VNode 进行绑定

在patch函数中如果判断到data中有on属性,那么会去通过CreateHook进行原生事件的绑定

if (isDef(data)) {
  invokeCreateHooks(vnode, insertedVnodeQueue)
}

其中给updateListeners函数传入add和remove函数来自定义具体的绑定和删除操作

export function updateListeners (on, oldOn, add, remove, vm) {
  let name, cur, old, event
  for (name in on) {
    cur = on[name]
    old = oldOn[name]
    // 新添加事件
    if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive)
    } else if (cur !== old) {
      // 对老事件进行更新
      old.fns = cur
      on[name] = old
    }
  }
  // 删除已经被去掉的事件
  for (name in oldOn) {
    if (isUndef(on[name])) {
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

其中原生add事件如下

function add (event, handler, once, capture, passive) {
  if (once) {
    const oldHandler = handler
    const _target = target
    // 如果是once那么调用完毕以后,调用remove来删除事件
    handler = function () {
      oldHandler.apply(null, arguments)
      remove(event, handler, capture, _target)
    }
  }
  target.addEventListener(
    event,
    handler,
    supportsPassive
      ? { capture, passive }
      : capture
  )
}

remove事件

function remove (event, handler, capture, _target) {
  (_target || target).removeEventListener(event, handler, capture)
}

自定义事件

首先在createComponent函数中,从render函数中on属性,赋值到listeners属性, 然后重新初始化这个组件的时候调用initEvents函数,并且传入自定义的add和remove,这里vue自己实现了一套订阅发布的事件机制

export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateListeners(listeners, oldListeners || {}, add, remove, vm)
  }
}

function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

function remove (event, fn) {
  target.$off(event, fn)
}
Vue.prototype.$on = function (event, fn) {
  const vm = this
  (vm._events[event] || (vm._events[event] = [])).push(fn)
  return vm
}

Vue.prototype.$once = function (event, fn) {
  const vm = this
  function on () {
    // 首先进行删除,然后执行函数
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  // 这里的fn是在删除的时候会去进行对比
  on.fn = fn
  vm.$on(event, on)
  return vm
}

Vue.prototype.$off = function (event, fn) {
  const vm = this
  // 清空全部
  if (arguments.length === 1) {
    vm._events[event] = null
    return vm
  }
  // 去和所有的事件进行对比,找到了就删除
  const cbs = vm._events[event]
  if (fn) {
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
  }
  return vm
}

Vue.prototype.$emit = function (event) {
  const vm = this
  let cbs = vm._events[event]
  if (cbs) {
    const args = toArray(arguments, 1)
    for (let i = 0, l = cbs.length; i < l; i++) {
      try {
        // 这里是用户传入的函数,所有需要捕获错误
        cbs[i].apply(vm, args)
      } catch (e) {
        handleError(e, vm, `event handler for "${event}"`)
      }
    }
  }
  return vm
}

总结

  • 有两套事件机制,原生事件和发布订阅事件
  • 两套事件都是通过updateListeners来进行绑定,其中分别实现不同的add和remove来区别,
  • 具体的事件在模板解析的时候分别解析在on和nativeOn属性上
  • 原生事件通过removeEventListener和addEventListener实现,其中事件修饰符是通过在事件名之前添加不同的字符串拼接截取来判断更新的,原生事件的绑定和更新是在VNode的create和update hook上
  • 发布订阅事件通过自己实现的事件机制进行实现,这个事件的初始化是在组件initEvent中进行绑定
  • once事件都是利用先保留执行函数,然后调用remove和off在解绑函数,最后调用之前缓存的函数来实现的

一个请求头中host字段引起的坑

背景:

最近开发了一个项目,由于开发的时候都是前端和接口都是localhost,所以本地服务调用接口没有问题。

  • 问题1:

    前端和接口分别上线后配置了不同的域名,然后就发现本地起服务的前端项目,无法代理到线上域名的接口,而是直接返回线上前端的html文件。
  • 问题2:

    项目中使用了webpack-dev-server的before功能,其中before中拦截配置到路径后会发起请求到相关的接口,再由相关的接口来进行查询到需要转发的接口域名,通过axios来进行转发,但是发现也是直接返回线上前端的html文件。

解决过程:

首先直接通过接口域名访问,发现正常,排除了nginx配置有误,排除接口挂了,然后查看nginx日志,发现
返回了如下日志

# 已做敏感信息处理
localhost:7777 - - localhost - - /index.html - - /group/list - - 1.2.3.4 - - [23/Feb/2019:09:00:41 +0800] "GET /group/list HTTP/1.1"

发现请求的接口路径是/group/list,但是却返回了index.html,通过文档查询nginx的server_name匹配规则doc

server_name匹配规则

  • 如果只有一个server,server_name可以任意起(可以为IP/域名/任何字符串),不论用什么访问(IP/域名/字符串),不论server_name是否匹配,都匹配到此server。

  • 有多个server,如果server_name可以匹配到任何一个server的server_name,则使用此server,否则匹配到第一个server块,不论server_name是否匹配!

所以可以理解为什么返回html文件了,那么既然知道了要修改host,查询node-http-proxy 文档发现可以使用changeOrigin来修改host, 完美解决问题

接下来解决了问题1,发现问题2也是由于host和请求的地址不一致导致nginx返回了默认的第一个server块,查询node-http-proxy changeOrigin源码得知内部重写了host

  if (options.changeOrigin) {
    outgoing.headers.host =
      required(outgoing.port, options[forward || 'target'].protocol) && !hasPort(outgoing.host)
        ? outgoing.host + ':' + outgoing.port
        : outgoing.host;
  }

那么我们也照猫画虎重写host即可

  config.headers.host = this.ctx.request.header.host;
  responseData = await axios.request(config);

apollographql-服务端升级(1->2)介绍.md

Error handling

  1. Default information 默认信息
   1.1 默认会返回错误的堆栈 通过 debug为false Apollo server中配置
   1.2 环境变量为‘production’ or ‘test’
  1. 可以配置一个错误可读性好的code

  2. 可以为每个错误定制可读的�message

  3. 提供一个全局formatError error catch

antd笔记

  1. 按需加载需要注意bable-import-plugin的位置,不能放在会对module做转换的插件后面,否则会导致语法分析失败,按需加载失效
// 失效
"plugins": [
    "@babel/plugin-transform-modules-commonjs",
    ["import", {
      "libraryName": "antd",
      "style": "css"
    }],
  ],
// 有效
"plugins": [
    ["import", {
      "libraryName": "antd",
      "style": "css"
    }],
  "@babel/plugin-transform-modules-commonjs",
  ],
  1. getFieldDecorator 不能装饰纯函数,react会警告无法传递ref

 js <FormItem {...formItemProps}> {getFieldDecorator(element.key, fieldProps)( <Widget {...widgetProps}> {element.children || null} </Widget>, )} </FormItem>  

或者可以通过React. createRef()进行包裹

React. forwardRef((props, ref) => <p {...props} ref={ref} >jsx</p>)
  1. form组件性能很差,如果很多复杂的自定义表单组件需要进行form拆分,最后通过访问ref来汇总数据。 详情

  2. Upload组件自定义上传函数需要返回假值或者一个带有abort函数的对象,因为在upload组件销毁前会去调用abort方法,就可能导致报错
    源码
    example

customRequest={this.handleUpload}

  handleUpload = ({ file }) => {
    const { fileList } = this.state;
    upload(file)
      .then((url) => {
        this.setState({
          fileList: [...(fileList || []), {
            uid: Math.random(),
            url,
            name: file.name,
            type: file.type,
            size: file.size,
            lastModified: file.lastModified,
          }],
        }, this.handleSync);
      })
      .catch((error) => {
        message.error(isDev ? error.message : '上传图片失败,请稍后再试');
      });
  }

类似这种需求的话,不能使用table自带的分页,需要外置一个分页
比如数据结构如下

[{
 id: 1,
 children: [{
   id: 1-1
 }, {
   id: 1-2
 }]
}]

如果有合并单元格的需求的话,我会把数据转换如下

[{
 id: 1,
}, {
 id: 1-1,
}, {
 id: 1-2,
}]

这样就会超出之前pageSize了,所以需求自己外置提供分页组件

简易的babel 插件开发入门例子

如果需要使用这个功能的话请用官方的插件

babel-plugin-lodash

这个仓库只是一个学习的例子

仓库地址
主要做的事情是按需加载lodash库
将如下的代码

import _ from 'lodash'
import isArray from 'lodash/isArray'
import { toString } from 'lodash'

isArray([])
_.add(1, 2)
toString(1)
_.add(1, 2)
isArray([1])

↓↓↓↓↓↓↓↓↓↓↓ 转换成如下的代码

import isArray from 'lodash/isArray';
import toString from 'lodash/toString';

isArray([]);
import add from 'lodash/add';
add(1, 2)

toString(1);
add(1, 2)

isArray([1]);

babel-plugin-lodash-extract

平常开发的时候有些同学会不遵守规范或者无意中使用了如下写法

import _ from 'lodash'

import loadsh from 'lodash'

这样会把所有的lodash代码都加载进去
导致大概js文件大小会大70kb左右

如下代码

import _ from 'lodash'

_.add(1, 2)

结果有71.1kb

static/js/vendor.6b317cc799badf6ad864.js    71.1 kB       0  [emitted]  vendor

但是这么写的话

import add from 'lodash/add'
add(1, 2)

发现只有这么大

static/js/vendor.d9f1fb9269405aa51217.js    2.24 kB       0  [emitted]  vendor

下载安装

npm i --save-dev babel-plugin-lodash-extract

示例:

在babel的配置文件里加上

{
  "plugins": ["babel-plugin-lodash-extract"],
}

这样的配置,这里需要注意是plugins的加载顺序是从后往前的,一般的项目都有添加
transform-runtime插件,那么写法需要这样

{
  "plugins": [
    "transform-runtime",
    "babel-plugin-lodash-extract"
  ],
}

CLI

  npm run test // unit test
  npm run debug // development debug
  npm run compile // development compile
  npm run example // run example

原理说明

首先根据babel的插件开发文档

发现所有的babel插件需要按照以下的格式来进行处理代码

module.exports = ({ types: t }) => {
  return {
    visitor: {
      // ...some code
    },
  };
}

然后babel会把你的代码解析成AST

首先建立一个目录example,里面建两个文件index.js, example.js

下列使用这个代码来进行处理

// example.js的内容

const a = 1 + 2;

↓↓↓↓↓↓↓↓
解析成

const b = 2 - 3;

在这个网站https://astexplorer.net进行AST解析

观察一下这个语法树

首先进行变量a的替换
来看一下结果代码

module.exports = ({ types: t }) => {
  return {
    visitor: {
      Identifier(path) {
        if (path.node.name === 'a') {
          path.replaceWith(t.Identifier('b'));
        }
      },
    },
  };
}

// 在package.json里面加入 "example": "babel --plugins ../example/index.js ./example/example.js"
// 运行 npm run example
// 输出 const b = 1 + 2;

然后继续解析 1 + 2 -> 2 - 3

module.exports = ({ types: t }) => {
  return {
    visitor: {
      Identifier(path) {
        ...
      },
      BinaryExpression(path) {
        if (path.node.operator === '+') {
          const left = t.numericLiteral(2);
          const right = t.numericLiteral(3);
          path.replaceWith(t.binaryExpression('-', left, right));
        }
      }
    },
  };
}
// 输出 const b = 2 - 3;
// 目标完成

关于t上面有哪些方法可以根据__babel-types__的文档来进行查阅

还有__path__的一些使用方法在babel插件手册里也有部分解释

完整代码

module.exports = ({ types: t }) => {
  return {
    visitor: {
      Identifier(path) {
        if (path.node.name === 'a') {
          path.replaceWith(t.Identifier('b'));
        }
      },
      BinaryExpression(path) {
        if (path.node.operator === '+') {
          const left = t.numericLiteral(2);
          const right = t.numericLiteral(3);
          path.replaceWith(t.binaryExpression('-', left, right));
        }
      }
    },
  };
}

没了。

二分搜索(折半查找)

二分搜索(折半查找)是一种经典的检索算法。该算法的基本**如下:

  1. 选择数组的中间值。
  2. 如果选中值mid是待搜索值target,那么算法执行完毕(值找到了)。
  3. 如果待搜索值target比选中值mid要小,则返回步骤1, 并且把返回缩小到low, mid - 1 。
  4. 如果待搜索值比选中值要大,则返回步骤1, 并且把返回缩小到mid + 1, high 。
  5. 需要注意的是mid的取值需要是left 和right的中间值, 比如 left = 80 right = 90,那么mid = 85,然后下一次循环的时候发现mid 大于target82,那么就把right设置为 85 - 1,范围就缩小到了80~84
function binarySearch(target, total) {
  var left = 0,
    right = total - 1,
    mid;
  let i = 0;
  while(left <= right){
    i += 1;
    mid = Math.floor((left + right) / 2);
    if (target > mid) {
      left = mid + 1;
    }else if (target < mid){
      right = mid - 1;
    }else{
      console.log(`target is ${target}, use ${i} times`);
      return i;
    }
  }
  return -1;
}

binarySearch(80, 100); // 返回需要的次数

TypeScript

  1. 批量修改.js 文件到.ts
find app/src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;

如何通过postMessgae跨域设置cookie

背景是统一登录只会在example.com 下面设置httponly的cookie,但是本地开发的时候使用localhost来进行开发,所以需要配置nginx来设置开发域名来访问cookie,后面想到可以让登陆完成以后跳转到一个已经部署在example.com下的页面,例如a.example.com来发送请求到node接口,然后接口返回request中的cookie返回,然后通过postMessage把cookie,通过本地localhost的一个接受cookie的页面来写入到localhost,至此就可以不需要配置nginx,也可以在localhost进行开发工作了。

原型链和new

// 自定义new
function new1(func) {
  var o = Object.create(func.prototype);
  var k = func.call(o);
  if (typeof k === 'object') {
    return k;
  } else {
    return o;
  }
}

function foo() {
 this.age = 12
}
function foo1() {
 this.age = 12
 return {name: 66};
}
new1(foo); // { age: 12 }
new1(foo1); // { name: 66 }

React中的二进制类型

在学习react源码时候碰到了一些特别的变量和很多这样的的比较和赋值,类似这样

workInProgress.effectTag |= Ref
(workInProgress.effectTag & DidCapture) !== NoEffect

解读

通过查阅源码得知具体effectTag的值为

// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /*              */ 0b00000000000;
export const PerformedWork = /*         */ 0b00000000001;

// You can change the rest (and add more).
export const Placement = /*             */ 0b00000000010;
export const Update = /*                */ 0b00000000100;
export const PlacementAndUpdate = /*    */ 0b00000000110;
export const Deletion = /*              */ 0b00000001000;
export const ContentReset = /*          */ 0b00000010000;
export const Callback = /*              */ 0b00000100000;
export const DidCapture = /*            */ 0b00001000000;
export const Ref = /*                   */ 0b00010000000;
export const Snapshot = /*              */ 0b00100000000;

// Union of all host effects
export const HostEffectMask = /*        */ 0b00111111111;

export const Incomplete = /*            */ 0b01000000000;
export const ShouldCapture = /*         */ 0b10000000000;

第一句workInProgress.effectTag |= Ref只是做个二进制的或运算,例如PerformedWork | Placement,那么么返回的结果
0b00000000011

接下来第二句(workInProgress.effectTag & DidCapture),就是对二进制做与运算,例如上面的
(PerformedWork | Placement) & Placement 这样会返回一个不为0的二进制数字,然后可以通过结果去判断是否
Placement有过赋值操作,那么第二句代码的含义就是effectTag是否被DidCapture赋值过了。

延伸阅读

权限控制

这样的设计在权限控制中也很常见,提供一个[例子] (https://gist.github.com/AfterThreeYears/817b208772c1a3dda6c0e4828f1b0ebc)
这样设计的好处在于,如果用户已经有某个权限了,再次设置也不会出现问题,而且linux中的权限设计也是按照这种思路来做的

引用

https://segmentfault.com/a/1190000016284033
https://zhuanlan.zhihu.com/p/30103832

Redux

动手实现 Redux 笔记

源码

Redux模式

Store和dispatch

Redux本质是一个共享状态对象, 但是没有任何限制,任何人在任何时候都可以去修改数据,导致最后使用的时候获取的数据是不可预期的,
那么Redux提出的方案是把事情搞复杂一些,提高数据修改的门槛:模块(组件)之间可以共享数据,也可以改数据。但是我们约定,这个数据并不能直接改,你只能执行某些我允许的某些修改,而且你修改的必须大张旗鼓地告诉我。
这个函数就是dispatch, 专门用来负责数据的修改, 其中必须的参数是type字段,用来识别用户具体做了什么操作

function reducer(action) {
  switch (action.type) {
    case 'UPDATE_TITLE_TEXT':
      appState.title.text = action.text
      break
    case 'UPDATE_TITLE_COLOR':
      appState.title.color = action.color
      break
    default:
      break
  }
}

function createStore (state, reducer) {
  const getState = () => state
  const dispatch = (action) => reducer(state, action)
  return { getState, dispatch }
}

reducer需要为一个纯函数

纯函数的概念来自于函数式编程

  1. 一个函数的返回结果只依赖于它的参数
  2. 执行过程里面没有副作用

好处

  1. 输出的结果可以预期,使reducer函数更好调试和测试。

上面的reducer是直接对appState进行了修改,那么会导致无法区分哪些字段进行了修改,哪些字段没有被修改,所以reducer需要每次执行以后返回一个新的appState,其中未修改的部分可以直接使用老数据的引用,可以防止无用的渲染,另外一个好处是一个记录新旧数据的区别,来实现时间旅行功能,

订阅

假如我们的更新代码是

renderTitle(store.getState())

那么当我们每次dispatch以后都需要去手动的执行上面的函数来进行页面上的同步

这里我们可以使用观察者模式来进行自动同步页面,修改createStore函数为

function createStore (state, reducer) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

发现暴露出一个subscribe方法,然后我们的使用方法就变成

// 定一个 reducer
function reducer (state, action) {
  /* 初始化 state 和 switch case */
}

// 生成 store
const store = createStore(state, reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderTitle(store.getState()))

// 首次渲染页面
renderTitle(store.getState()) 

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.