yvonne

yvonne 查看完整档案

深圳编辑广东工业大学  |  信息管理与信息系统 编辑企鹅  |  小前端 编辑 yvonnevv.github.io 编辑
编辑

少年(゚∀゚ )有兴趣来鹅厂吗?欢迎投简历至yvonnexchen@tencent.com

个人动态

yvonne 赞了文章 · 3月13日

H5与Native交互之JSBridge技术

做过混合开发的很多人都知道Ionic和PhoneGap之类的框架,这些框架在web基础上包了一层Native,然后通过Bridge技术使得js可以调用视频、位置、音频等功能。本文就是介绍这层Bridge的交互原理,通过阅读本文你可以了解到js与ios及android底层的通讯原理及JSBridge的封装技术及调试方法。

一、原理篇

下面分别介绍IOS和Android与Javascript的底层交互原理

IOS

在讲解原理之前,首先来了解下iOS的UIWebView组件,先来看一下苹果官方的介绍:

You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.

上面的意思是说UIWebView是一个可加载网页的对象,它有浏览记录功能,且对加载的网页内容是可编程的。说白了UIWebView有类似浏览器的功能,我们使用可以它来打开页面,并做一些定制化的功能,如可以让js调某个方法可以取到手机的GPS信息。

但需要注意的是,Safari浏览器使用的浏览器控件和UIwebView组件并不是同一个,两者在性能上有很大的差距。幸运的是,苹果发布iOS8的时候,新增了一个WKWebView组件,如果你的APP只考虑支持iOS8及以上版本,那么你就可以使用这个新的浏览器控件了。

原生的UIWebView类提供了下面一些属性和方法

属性:

  • loading:是否处于加载中
  • canGoBack:A Boolean value indicating whether the receiver can move backward. (只读)
  • canGoForward:A Boolean value indicating whether the receiver can move forward. (只读)
  • request:The URL request identifying the location of the content to load. (read-only)

方法:

  • loadData:Sets the main page contents, MIME type, content encoding, and base URL.
  • loadRequest:加载网络内容
  • loadHTMLString:加载本地HTML文件
  • stopLoading:停止加载
  • goBack:后退
  • goForward:前进
  • reload:重新加载
  • stringByEvaluatingJavaScriptFromString:执行一段js脚本,并且返回执行结果

Native(Objective-C或Swift)调用Javascript方法

Native调用Javascript语言,是通过UIWebView组件的stringByEvaluatingJavaScriptFromString方法来实现的,该方法返回js脚本的执行结果。

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

从上面代码可以看出它其实就是调用了window下的一个对象,如果我们要让native来调用我们js写的方法,那这个方法就要在window下能访问到。但从全局考虑,我们只要暴露一个对象如JSBridge对native调用就好了,所以在这里可以对native的代码做一个简单的封装:

//下面为伪代码
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
 webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}

Javascript调用Native(Objective-C或Swift)方法

反过来,Javascript调用Native,并没有现成的API可以直接拿来用,而是需要间接地通过一些方法来实现。UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们就可以在UIWebView内发起一个自定义的网络请求,通常是这样的格式:jsbridge://methodName?param1=value1&param2=value2

于是在UIWebView的delegate函数中,我们只要发现是jsbridge://开头的地址,就不进行内容的加载,转而执行相应的调用逻辑。

发起这样一个网络请求有两种方式:1. 通过localtion.href;2. 通过iframe方式;
通过location.href有个问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。

使用iframe方式,以唤起Native APP的分享组件为例,简单的封闭如下:

var url = 'jsbridge://doAction?title=分享标题&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
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() {
    iframe.remove();
}, 100);

然后Webview就可以拦截这个请求,并且解析出相应的方法和参数。如下代码所示:

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        print("shouldStartLoadWithRequest")
        let url = request.URL
        let scheme = url?.scheme
        let method = url?.host
        let query = url?.query
        
        if url != nil && scheme == "jsbridge" {
            print("scheme == \(scheme)")
            print("method == \(method)")
            print("query == \(query)")

            switch method! {
                case "getData":
                    self.getData()
                case "putData":
                    self.putData()
                case "gotoWebview":
                    self.gotoWebview()
                case "gotoNative":
                    self.gotoNative()
                case "doAction":
                    self.doAction()
                case "configNative":
                    self.configNative()
                default:
                    print("default")
            }
    
            return false
        } else {
            return true
        }
    }

Android

在android中,native与js的通讯方式与ios类似,ios中的通过schema方式在android中也是支持的。

javascript调用native方式

目前在android中有三种调用native的方式:

1.通过schema方式,使用shouldOverrideUrlLoading方法对url协议进行解析。这种js的调用方式与ios的一样,使用iframe来调用native代码。
2.通过在webview页面里直接注入原生js代码方式,使用addJavascriptInterface方法来实现。
在android里实现如下:

class JSInterface {
    @JavascriptInterface //注意这个代码一定要加上
    public String getUserData() {
        return "UserData";
    }
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");

上面的代码就是在页面的window对象里注入了AndroidJS对象。在js里可以直接调用

alert(AndroidJS.getUserData()) //UserDate

3.使用prompt,console.log,alert方式,这三个方法对js里是属性原生的,在android webview这一层是可以重写这三个方法的。一般我们使用prompt,因为这个在js里使用的不多,用来和native通讯副作用比较少。

class YouzanWebChromeClient extends WebChromeClient {
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        // 这里就可以对js的prompt进行处理,通过result返回结果
    }
    @Override
    public boolean onConsoleMessage(ConsoleMessage consoleMessage) {

    }
    @Override
    public boolean onJsAlert(WebView view, String url, String message, JsResult result) {

    }

}

Native调用javascript方式

在android里是使用webview的loadUrl进行调用的,如:

// 调用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");

二、库的封装

js调用native的封装

上面我们了解了js与native通讯的底层原理,所以我们可以封装一个基础的通讯方法doCall来屏蔽android与ios的差异。

YouzanJsBridge = {
    doCall: function(functionName, data, callback) {
        var _this = this;
        // 解决连续调用问题
        if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
            setTimeout(function() {
                _this.doCall(functionName, data, callback);
            }, 100);
            return;
        }
        this.lastCallTime = Date.now();
    
        data = data || {};
        if (callback) {
            $.extend(data, { callback: callback });
        }
    
        if (UA.isIOS()) {
            $.each(data, function(key, value) {
                if ($.isPlainObject(value) || $.isArray(value)) {
                    data[key] = JSON.stringify(value);
                }
            });
            var url = Args.addParameter('youzanjs://' + functionName, data);
            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() {
                iframe.remove();
            }, 100);
        } else if (UA.isAndroid()) {
            window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
        } else {
            console.error('未获取platform信息,调取api失败');
        }
    }
}

上面android端我们使用了addJavascriptInterface方法来注入一个AndroidJS对象。

项目通用方法抽象

在项目的实践中,我们逐渐抽象出一些通用的方法,这些方法基本上都是可以满足项目的需求。如下所示:

1.getData(datatype, callback, extra) H5从Native APP获取数据

使用场景:H5需要从Native APP获取某些数据的时候,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
callbackFunction回调函数
extraObject传递给Native APP的数据对象

示例代码:

JSBridge.getData('userInfo',function(data) {
    console.log(data);
});

2.putData(datatype, data) H5告诉Native APP一些数据

使用场景:H5告诉Native APP一些数据,可以调用这个方法。

参数类型是否必须示例值说明
datatypeStringuserInfo数据类型
dataObject{ username: 'zhangsan', age: 20 }传递给Native APP的数据对象

示例代码:

JSBridge.putData('userInfo', {
    username: 'zhangsan',
    age: 20
});

3.gotoWebview(url, page, data) Native APP新开一个Webview窗口,并打开相应网页

参数类型是否必须示例值说明
urlStringhttp://www.youzan.com网页链接地址,一般都只要传递URL参数就可以了
pageStringweb网页page类型,默认为web
dataObject额外参数对象

示例代码:

// 示例1:打开一个网页
JSBridge.gotoWebview('http://www.youzan.com');

// 示例2:打开一个网页,并且传递额外的参数给Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
    goods_id: 10000,
    title: '这是商品的标题',
    desc: '这是商品的描述'
});

4.gotoNative(page, data) 从H5页面跳转到Native APP的某个原生界面

参数类型是否必须示例值说明
pageStringloginPageNative页面标示符,例如loginPage
dataObject{ username: 'zhangsan', age: 20 }额外参数对象

示例代码:

// 示例1:打开Native APP登录页面
JSBridge.gotoNative('loginPage');

// 示例2:打开Native APP登录页面,并且传递用户名给Native APP
JSBridge.gotoNative('loginPage', {
    username: '张三'
});

5.doAction(action, data) 功能上的一些操作

参数类型是否必须示例值说明
actionStringcopy操作功能类型,例如分享、复制
dataObject{ content: '这是要复制的内容' }额外参数

示例代码:

// 示例1:调用Native APP复制一段文本到剪切板
JSBridge.doAction('copy', {
    content: '这是要复制的内容'
});

// 示例2:调用Native APP的分享组件,分享当前网页到微信
JSBridge.doAction('share', {
    title: '分享标题',
    desc: '分享描述',
    link: 'http://www.youzan.com',
    imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});

三、调试篇

使用Safari进行UIWebView的调试

(1)首先需要打开Safari的调试模式,在Safari的菜单中,选择“Safari”→“Preference”→“Advanced”,勾选上“Show Develop menu in menu bar”选项,如下图所示。
2-1
(2)打开真机或iPhone模拟器的调试模式,在真机或iPhone模拟器中打开设置界面,选择“Safari”→“高级”→“Web检查器”,选择开启即可,如下图所示。
2-2
(3)将真机通过USB连上电脑,或者开启模拟器,Safari的“Develop”菜单下便会多出相应的菜单项,如图所示。

Paste_Image.png

(4)Safari连接上UIWebView之后,我们就可以直接在Safari中直接修改HTML、CSS,以及调试Javascript。

Paste_Image.png

四、参考链接

本文由 @kk @劲风 共同创作,首发于有赞技术博客: http://tech.youzan.com/jsbridge/

查看原文

赞 106 收藏 180 评论 4

yvonne 赞了文章 · 3月13日

可能是全网最全的http面试答案

HTTP有哪些方法?

  • HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法
  • HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT

这些方法的具体作用是什么?

  • GET: 通常用于请求服务器发送某些资源
  • HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源
  • OPTIONS: 用于获取目的资源所支持的通信选项
  • POST: 发送数据给服务器
  • PUT: 用于新增资源或者使用请求中的有效负载替换目标资源的表现形式
  • DELETE: 用于删除指定的资源
  • PATCH: 用于对资源进行部分修改
  • CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器
  • TRACE: 回显服务器收到的请求,主要用于测试或诊断

GET和POST有什么区别?

  • 数据传输方式不同:GET请求通过URL传输数据,而POST的数据通过请求体传输。
  • 安全性不同:POST的数据因为在请求主体内,所以有一定的安全性保证,而GET的数据在URL中,通过历史记录,缓存很容易查到数据信息。
  • 数据类型不同:GET只允许 ASCII 字符,而POST无限制
  • GET无害: 刷新、后退等浏览器操作GET请求是无害的,POST可能重复提交表单
  • 特性不同:GET是安全(这里的安全是指只读特性,就是使用这个方法不会引起服务器状态变化)且幂等(幂等的概念是指同一个请求方法执行多次和仅执行一次的效果完全相同),而POST是非安全非幂等

PUT和POST都是给服务器发送新增资源,有什么区别?

PUT 和POST方法的区别是,PUT方法是幂等的:连续调用一次或者多次的效果相同(无副作用),而POST方法是非幂等的。

除此之外还有一个区别,通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合。

举个例子,我们在开发一个博客系统,当我们要创建一篇文章的时候往往用POST https://www.jianshu.com/articles,这个请求的语义是,在articles的资源集合下创建一篇新的文章,如果我们多次提交这个请求会创建多个文章,这是非幂等的。

PUT https://www.jianshu.com/articles/820357430的语义是更新对应文章下的资源(比如修改作者名称等),这个URI指向的就是单一资源,而且是幂等的,比如你把『刘德华』修改成『蔡徐坤』,提交多少次都是修改成『蔡徐坤』

ps: 『POST表示创建资源,PUT表示更新资源』这种说法是错误的,两个都能创建资源,根本区别就在于幂等性

PUT和PATCH都是给服务器发送修改资源,有什么区别?

PUT和PATCH都是更新资源,而PATCH用来对已知资源进行局部更新。

比如我们有一篇文章的地址https://www.jianshu.com/articles/820357430,这篇文章的可以表示为:

article = {
    author: 'dxy',
    creationDate: '2019-6-12',
    content: '我写文章像蔡徐坤',
    id: 820357430
}

当我们要修改文章的作者时,我们可以直接发送PUT https://www.jianshu.com/articles/820357430,这个时候的数据应该是:

{
    author:'蔡徐坤',
    creationDate: '2019-6-12',
    content: '我写文章像蔡徐坤',
    id: 820357430
}

这种直接覆盖资源的修改方式应该用put,但是你觉得每次都带有这么多无用的信息,那么可以发送PATCH https://www.jianshu.com/articles/820357430,这个时候只需要:

{
    author:'蔡徐坤',
}

http的请求报文是什么样的?

请求报文有4部分组成:

  • 请求行
  • 请求头部
  • 空行
  • 请求体

2019-06-14-11-24-10

  • 请求行包括:请求方法字段、URL字段、HTTP协议版本字段。它们用空格分隔。例如,GET /index.html HTTP/1.1。
  • 请求头部:请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔
  1. User-Agent:产生请求的浏览器类型。
  2. Accept:客户端可识别的内容类型列表。
  3. Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。
  • 请求体: post put等请求携带的数据

2019-06-14-11-33-37

http的响应报文是什么样的?

请求报文有4部分组成:

  • 响应行
  • 响应头
  • 空行
  • 响应体

2019-06-14-11-37-02

  • 响应行: 由协议版本,状态码和状态码的原因短语组成,例如HTTP/1.1 200 OK
  • 响应头:响应部首组成
  • 响应体:服务器响应的数据

聊一聊HTTP的部首有哪些?

内容很多,重点看标『✨』内容

通用首部字段(General Header Fields):请求报文和响应报文两方都会使用的首部

  • Cache-Control  控制缓存 ✨
  • Connection 连接管理、逐条首部 ✨
  • Upgrade  升级为其他协议
  • via 代理服务器的相关信息
  • Wraning 错误和警告通知
  • Transfor-Encoding 报文主体的传输编码格式 ✨
  • Trailer 报文末端的首部一览
  • Pragma 报文指令
  • Date 创建报文的日期

请求首部字段(Reauest Header Fields):客户端向服务器发送请求的报文时使用的首部

  • Accept 客户端或者代理能够处理的媒体类型 ✨
  • Accept-Encoding 优先可处理的编码格式
  • Accept-Language 优先可处理的自然语言
  • Accept-Charset 优先可以处理的字符集
  • If-Match 比较实体标记(ETage) ✨
  • If-None-Match 比较实体标记(ETage)与 If-Match相反 ✨
  • If-Modified-Since 比较资源更新时间(Last-Modified)✨
  • If-Unmodified-Since比较资源更新时间(Last-Modified),与 If-Modified-Since相反 ✨
  • If-Rnages 资源未更新时发送实体byte的范围请求
  • Range 实体的字节范围请求 ✨
  • Authorization web的认证信息 ✨
  • Proxy-Authorization 代理服务器要求web认证信息
  • Host 请求资源所在服务器 ✨
  • From 用户的邮箱地址
  • User-Agent 客户端程序信息 ✨
  • Max-Forwrads 最大的逐跳次数
  • TE 传输编码的优先级
  • Referer 请求原始放的url
  • Expect 期待服务器的特定行为

响应首部字段(Response Header Fields):从服务器向客户端响应时使用的字段

  • Accept-Ranges 能接受的字节范围
  • Age 推算资源创建经过时间
  • Location 令客户端重定向的URI ✨
  • vary  代理服务器的缓存信息
  • ETag 能够表示资源唯一资源的字符串 ✨
  • WWW-Authenticate 服务器要求客户端的验证信息
  • Proxy-Authenticate 代理服务器要求客户端的验证信息
  • Server 服务器的信息 ✨
  • Retry-After 和状态码503 一起使用的首部字段,表示下次请求服务器的时间

实体首部字段(Entiy Header Fields):针对请求报文和响应报文的实体部分使用首部

  • Allow 资源可支持http请求的方法 ✨
  • Content-Language 实体的资源语言
  • Content-Encoding 实体的编码格式
  • Content-Length 实体的大小(字节)
  • Content-Type 实体媒体类型
  • Content-MD5 实体报文的摘要
  • Content-Location 代替资源的yri
  • Content-Rnages 实体主体的位置返回
  • Last-Modified 资源最后的修改资源 ✨
  • Expires 实体主体的过期资源 ✨

聊一聊HTTP的状态码有哪些?

2XX 成功

  • 200 OK,表示从客户端发来的请求在服务器端被正确处理 ✨
  • 201 Created 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立
  • 202 Accepted 请求已接受,但是还没执行,不保证完成请求
  • 204 No content,表示请求成功,但响应报文不含实体的主体部分
  • 206 Partial Content,进行范围请求 ✨

3XX 重定向

  • 301 moved permanently,永久性重定向,表示资源已被分配了新的 URL
  • 302 found,临时性重定向,表示资源临时被分配了新的 URL ✨
  • 303 see other,表示资源存在着另一个 URL,应使用 GET 方法丁香获取资源
  • 304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况
  • 307 temporary redirect,临时重定向,和302含义相同

4XX 客户端错误

  • 400 bad request,请求报文存在语法错误 ✨
  • 401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 ✨
  • 403 forbidden,表示对请求资源的访问被服务器拒绝 ✨
  • 404 not found,表示在服务器上没有找到请求的资源 ✨
  • 408 Request timeout, 客户端请求超时
  • 409 Confict, 请求的资源可能引起冲突

5XX 服务器错误

  • 500 internal sever error,表示服务器端在执行请求时发生了错误 ✨
  • 501 Not Implemented 请求超出服务器能力范围,例如服务器不支持当前请求所需要的某个功能,或者请求是服务器不支持的某个方法
  • 503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求
  • 505 http version not supported 服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本

同样是重定向307,303,302的区别?

302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码又出来了两个303和307。

303明确表示客户端应当采用get方法获取资源,他会把POST请求变为GET请求进行重定向。
307会遵照浏览器标准,不会从post变为get。

HTTP的keep-alive是干什么的?

在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入Connection: keep-alive来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上Connection: keep-alive。

keep-alive的优点:

  • 较少的CPU和内存的使用(由于同时打开的连接的减少了)
  • 允许请求和应答的HTTP管线化
  • 降低拥塞控制 (TCP连接减少了)
  • 减少了后续请求的延迟(无需再进行握手)
  • 报告错误无需关闭TCP连

为什么有了HTTP为什么还要HTTPS?

https是安全版的http,因为http协议的数据都是明文进行传输的,所以对于一些敏感信息的传输就很不安全,HTTPS就是为了解决HTTP的不安全而生的。

HTTPS是如何保证安全的?

过程比较复杂,我们得先理解两个概念

对称加密:即通信的双方都使用同一个秘钥进行加解密,比如特务接头的暗号,就属于对称加密

对称加密虽然很简单性能也好,但是无法解决首次把秘钥发给对方的问题,很容易被hacker拦截秘钥。

非对称加密:

  1. 私钥 + 公钥= 密钥对
  2. 即用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密
  3. 因为通信双方的手里都有一套自己的密钥对,通信之前双方会先把自己的公钥都先发给对方
  4. 然后对方再拿着这个公钥来加密数据响应给对方,等到到了对方那里,对方再用自己的私钥进行解密

非对称加密虽然安全性更高,但是带来的问题就是速度很慢,影响性能。

解决方案:

那么结合两种加密方式,将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。

此时又带来一个问题,中间人问题:

如果此时在客户端和服务器之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥,换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的所有数据。

所以这个时候需要一个安全的第三方颁发证书(CA),证明身份的身份,防止被中间人攻击。

证书中包括:签发者、证书用途、使用者公钥、使用者私钥、使用者的HASH算法、证书到期时间等

2019-06-14-12-30-18

但是问题来了,如果中间人篡改了证书,那么身份证明是不是就无效了?这个证明就白买了,这个时候需要一个新的技术,数字签名。

数字签名就是用CA自带的HASH算法对证书的内容进行HASH得到一个摘要,再用CA的私钥加密,最终组成数字签名。

当别人把他的证书发过来的时候,我再用同样的Hash算法,再次生成消息摘要,然后用CA的公钥对数字签名解密,得到CA创建的消息摘要,两者一比,就知道中间有没有被人篡改了。

这个时候就能最大程度保证通信的安全了。

HTTP2相对于HTTP1.x有什么优势和特点?

二进制分帧

帧:HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。

流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID

HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。

服务器推送

服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。

服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。

头部压缩

HTTP/1.x会在请求和响应中中重复地携带不常改变的、冗长的头部数据,给网络带来额外的负担。

  • HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送
  • 首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;
  • 每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。
你可以理解为只发送差异数据,而不是全部发送,从而减少头部的信息量

2019-06-14-12-52-59

多路复用

HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制。

HTTP2中:

  • 同域名下所有通信都在单个连接上完成。
  • 单个连接可以承载任意数量的双向数据流。
  • 数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装

2019-06-14-12-58-50

拓展阅读:HTTP/2特性及其在实际应用中的表现

公众号

想要实时关注笔者最新的文章和最新的文档更新请关注公众号程序员面试官,后续的文章会优先在公众号更新.

简历模板: 关注公众号回复「模板」获取

《前端面试手册》: 配套于本指南的突击手册,关注公众号回复「fed」获取

2019-08-12-03-18-41

本文由博客一文多发平台 OpenWrite 发布!
查看原文

赞 63 收藏 50 评论 0

yvonne 赞了文章 · 3月9日

React Fiber 渐进式遍历详解

欢迎关注我的公众号睿Talk,获取我最新的文章:
clipboard.png

一、前言

之前写的一篇文章,React Fiber 原理介绍,介绍了 React Fiber 的实现原理,其中的关键是使用Fiber链的数据结构,将递归的Stack Reconciler改写为循环的Fiber Reconciler。今天将手写一个 demo,详细讲解遍历Fiber链的实现方式。

二、Stack Reconciler

假设有以下组件树:

clipboard.png

对应的 JS 代码如下:

const a1 = {name: 'a1'};
const b1 = {name: 'b1'};
const b2 = {name: 'b2'};
const b3 = {name: 'b3'};
const c1 = {name: 'c1'};
const c2 = {name: 'c2'};
const d1 = {name: 'd1'};
const d2 = {name: 'd2'};

a1.render = () => [b1, b2, b3];
b1.render = () => [];
b2.render = () => [c1];
b3.render = () => [c2];
c1.render = () => [d1, d2];
c2.render = () => [];
d1.render = () => [];
d2.render = () => [];

使用Stack Reconciler递归的方式来遍历组件树,大概是这个样子:

function doWork(o) {
    console.log(o.name);
}

function walk(instance) {
    doWork(instance);
    
    const children = instance.render();
    children.forEach(walk);
}

walk(a1);

// 输出结果:a1, b1, b2, c1, d1, d2, b3, c2

二、Fiber Reconciler

下面我们用 Fiber 的数据结构来改写遍历过程。首先定义数据结构,然后在遍历的过程中通过link方法创建节点间的关系:

// 定义 Fiber 数据结构
class Node {
    constructor(instance) {
        this.instance = instance;
        this.child = null;
        this.sibling = null;
        this.return = null;
    }
}

// 创建关系链
function link(parent, children) {
    if (children === null) children = [];

    // child 指向第一个子元素
    parent.child = children.reduceRight((previous, current) => {
        const node = new Node(current);
        node.return = parent;
        // sibling 指向前面处理的元素
        node.sibling = previous;
        return node;
    }, null);

    return parent.child;
}

遍历完成后会得出如下的关系链:

clipboard.png

下面来详细看下遍历的过程。还是沿用之前的walkdoWork方法名:

function doWork(node) {
    console.log(node.instance.name);
    
    // 创建关系链
    const children = node.instance.render();
    return link(node, children);
}

function walk() {
    while (true) {
        let child = doWork(node);

        if (child) {
            node = child;
            continue;
        }

        if (node === root) {
            return;
        }

        while (!node.sibling) {
            if (!node.return || node.return === root) {
                return;
            }

            node = node.return;
        }

        node = node.sibling;
    }
}

const hostNode = new Node(a1);

const root = hostNode;
let node = root;

walk();

// 输出结果:a1, b1, b2, c1, d1, d2, b3, c2

上面就是递归改循环的代码了。可以看到循环的结束条件是当前处理的节点等于根节点。在循环开始的时候,以深度优先一层一层往下递进。当没有子节点和兄弟节点的时候,当前节点会往上层节点回溯,直至根节点为止。

下面再来看看怎么结合requestIdleCallback API,实现渐进式遍历。由于完成这个遍历所需时间实在太短,因此每处理 3 个节点,我们sleep 1 秒,从而达到退出当前requestIdleCallback的目的,然后再创建一个新的回调任务:

function sleep(n) {
    const start = +new Date();
    while(true) if(+new Date() - start > n) break;
}

function walk(deadline) {
    let i = 1;

    while (deadline.timeRemaining() > 0 || deadline.didTimeout) {
        console.log(deadline.timeRemaining(), deadline.didTimeout);

        let child = doWork(node);

        if (i > 2) {
            sleep(1000);
        }
        i++;

        if (child) {
            node = child;
            continue;
        }

        if (node === root) {
            console.log('================ Task End ===============');
            return;
        }

        while (!node.sibling) {
            if (!node.return || node.return === root) {
                console.log('================ Task End ===============');
                return;
            }

            node = node.return;
        }

        node = node.sibling;
    }

    console.log('================ Task End ===============');

    requestIdleCallback(walk);
}

requestIdleCallback(walk);

// 输出结果:
15.845 false
a1
15.14 false
b1
14.770000000000001 false
b2
================ Task End ===============
15.290000000000001 false
c1
14.825000000000001 false
d1
14.485000000000001 false
d2
================ Task End ===============
14.96 false
b3
14.475000000000001 false
c2
================ Task End ===============

三、总结

本文通过一个 demo,讲解了如何利用React Fiber的数据结构,递归改循环,实现组件树的渐进式遍历。

查看原文

赞 17 收藏 10 评论 1

yvonne 赞了文章 · 2月25日

手写Promise - 实现一个基础的Promise

前端开发中经常会用到Promise,不过有部分人并不清楚Promise的原理,本文也是本人在学习Promise时对Promis的一些认识,希望能对各位童鞋有所帮助。

手写Promise - 实现一个基础的Promise
手写Promise - 实例方法catch、finally
手写Promise - 常用静态方法all、any、resolve、reject、race

从认识Promise开始。。。

/* 模拟一个简单的异步行为 */
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve('willem');
        }, 1000);
    });
}

fetchData().then((data) => {
    // after 1000ms
    console.log(data); // willem
    return 'wei';
}, (err) => {}).then((data2) => {
    console.log(data2); // wei
});

上面的例子算是一个最常见的用法,可能在使用的时候更多的使用的是catch来处理异常来替代then方法的第二个参数,但catch也只是一个then的语法糖。

从中我们可以用一些句子来描述Promise。

  1. promise是一个类,它的构造函数接受一个函数,函数的两个参数也都是函数
  2. 在传入的函数中执行resolve表示成功,执行reject表示失败,传入的值会传给then方法的回调函数
  3. promise有一个叫做then的方法,该方法有两个参数,第一个参数是成功之后执行的回调函数,第二个参数是失败之后执行的回调函数。then方法在resolve或者reject执行之后才会执行,并且then方法中的值是传给resolve或reject的参数
  4. promise支持链式调用

有了相应的描述,接下来就是来一步一步实现了。

简单版Promise

1. promise是一个类,它的构造函数接受一个函数,函数的两个参数也都是函数

第一点比较简单

// 这里没有使用Promise作为类名是为了方便测试
class WPromise {
    constructor(executor) {
        // 这里绑定this是为了防止执行时this的指向改变,this的指向问题,这里不过多赘述
        executor(this._resolve.bind(this), this._reject.bind(this));
    }
    
    _resolve() {}
    
    _reject() {}
}

2. 在传入的函数中执行resolve表示成功,执行reject表示失败,传入的值会传给then方法的回调函数

成功、失败,这个很容易想到使用一个状态进行标记,实际上Promise就是这样做的。在Promise中使用了pending、fulfilled、rejected来标识当前的状态。

  • pending 初始状态,既不是成功,也不是失败状态。等待resolve或者reject调用更新状态。
  • fulfilled 意味着操作成功完成。
  • rejected 意味着操作失败。

需要注意的一点是,这三个状态之间只存在两个变换关系:

  • pending转换为fulfilled,只能由resolve方法完成转换
  • pending转换为rejected,只能由reject方法完成转换

传入的值会传给then的回调函数,怎么传递呢?显然我们将对resolve和reject的值做一个保存。

将上面的状态和值添加到Promise

class WPromise {
    static pending = 'pending';
    static fulfilled = 'fulfilled';
    static rejected = 'rejected';

    constructor(executor) {
        this.status = WPromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        executor(this._resolve.bind(this), this._reject.bind(this));
    }
    
    _resolve(value) {
        this.value = value;
        this.status = WPromise.fulfilled; // 将状态设置为成功
    }
    
    _reject(reason) {
        this.reason = reason;
        this.status = WPromise.rejected; // 将状态设置为失败
    }
}

3. Promise有一个叫做then的方法,该方法有两个参数,第一个参数是成功之后执行的回调函数,第二个参数是失败之后执行的回调函数。then方法在resolve或者reject执行之后才会执行,并且then方法中的值是传给resolve或reject的参数

这句话有点长,需要注意的是这句then方法在resolve或者reject执行之后才会执行,我们知道Promise是异步的,也就是说then传入的函数是不能立马执行,需要存储起来,在resolve函数执行之后才拿出来执行。

换句话说,这个过程有点类似于发布订阅者模式:我们使用then来注册事件,那什么时候来通知这些事件是否执行呢?答案就是在resolve方法执行或者reject方法执行时。

ok, 继续完善我们的代码。

class WPromise {
    static pending = "pending";
    static fulfilled = "fulfilled";
    static rejected = "rejected";

    constructor(executor) {
        this.status = WPromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        // 存储then中传入的参数
        // 至于为什么是数组呢?因为同一个Promise的then方法可以调用多次
        this.callbacks = [];
        executor(this._resolve.bind(this), this._reject.bind(this));
    }

    // onFulfilled 是成功时执行的函数
    // onRejected 是失败时执行的函数
    then(onFulfilled, onRejected) {
        // 这里可以理解为在注册事件
        // 也就是将需要执行的回调函数存储起来
        this.callbacks.push({
            onFulfilled,
            onRejected,
        });
    }

    _resolve(value) {
        this.value = value;
        this.status = WPromise.fulfilled; // 将状态设置为成功

        // 通知事件执行
        this.callbacks.forEach((cb) => this._handler(cb));
    }

    _reject(reason) {
        this.reason = reason;
        this.status = WPromise.rejected; // 将状态设置为失败

        this.callbacks.forEach((cb) => this._handler(cb));
    }

    _handler(callback) {
        const { onFulfilled, onRejected } = callback;

        if (this.status === WPromise.fulfilled && onFulfilled) {
            // 传入存储的值
            onFulfilled(this.value);
        }

        if (this.status === WPromise.rejected && onRejected) {
            // 传入存储的错误信息
            onRejected(this.reason);
        }
    }
}

这个时候的Promise已经渐具雏形,现在可以来简单测试一下

function fetchData(success) {
    return new WPromise((resolve, reject) => {
        setTimeout(() => {
            if (success) {
                resolve("willem");
            } else {
                reject('error');
            }
        }, 1000);
    });
}

fetchData(true).then(data => {
    console.log(data); // after 1000ms: willem
});

fetchData(false).then(null, (reason) => {
    console.log(reason); // after 1000ms: error
});

从上面的输出结果来看,暂时是没什么问题的。接下来就是需要重点关注的链式调用问题了。

重难点:链式调用

链式调用 不知道你们看见这个想到了啥,我反正是想到了jQuery。其实链式调用无非就是再返回一个类的实例,那首先想到的肯定就是直接返回this,不过反正自身真的可以吗?

我们不妨在then方法最后添加一行 return this;来进行一个测试

function fetchData() {
    return new WPromise((resolve, reject) => {
        setTimeout(() => {
            resolve('willem');
        }, 1000);
    });
}

const p1 = fetchData().then(data1 => {return data1 + ' wei'});
const p2 = p1.then((data2) => {console.log(data2);}); // willem 正确输出应该是 'willem wei'
const p3 = p1.then((data3) => {console.log(data3);}); // willem 正确输出应该是 'willem wei'

显然,直接返回this是肯定不对,肯定要对函数的返回值做一个处理。
这时候可能会有同学说了,那我处理不就完事了么,我把then回调函数的执行结果赋值给value不就完事。答案当然是否定的,这回引发Promise内部的value和callbacks混乱。

那么,我们采取的当然是另一个方案,每次then方法都将返回一个新的Promise
在这里插入图片描述
这是一个简单的then的数据走向。简单说一下,then函数中返回的Promise的value值来源于当前then函数的onFulfilled函数(第一个参数)的执行结果(为方便理解,暂时只讨论操作成功的情况)。

从我们写的代码来看,value值只会在resolve函数中被赋值,显然我们也将会把onFulfilled执行的结果通过resolve的执行来传入到下一个Promise中。

加入链式调用的处理:

class WPromise {
    static pending = "pending";
    static fulfilled = "fulfilled";
    static rejected = "rejected";

    constructor(executor) {
        this.status = WPromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        // 存储then中传入的参数
        // 至于为什么是数组呢?因为同一个Promise的then方法可以调用多次
        this.callbacks = [];
        executor(this._resolve.bind(this), this._reject.bind(this));
    }

    // onFulfilled 是成功时执行的函数
    // onRejected 是失败时执行的函数
    then(onFulfilled, onRejected) {
        // 返回一个新的Promise
        return new WPromise((nextResolve, nextReject) => {
            // 这里之所以把下一个Promsie的resolve函数和reject函数也存在callback中
            // 是为了将onFulfilled的执行结果通过nextResolve传入到下一个Promise作为它的value值
            this._handler({
                nextResolve,
                nextReject,
                onFulfilled,
                onRejected
            });
        });
    }

    _resolve(value) {
        this.value = value;
        this.status = WPromise.fulfilled; // 将状态设置为成功

        // 通知事件执行
        this.callbacks.forEach((cb) => this._handler(cb));
    }

    _reject(reason) {
        this.reason = reason;
        this.status = WPromise.rejected; // 将状态设置为失败

        this.callbacks.forEach((cb) => this._handler(cb));
    }

    _handler(callback) {
        const { onFulfilled, onRejected, nextResolve, nextReject } = callback;
        
        if (this.status === WPromise.pending) {
            this.callbacks.push(callback);
            return;
        }

        if (this.status === WPromise.fulfilled) {
            // 传入存储的值
            // 未传入onFulfilled时,将undefined传入
            const nextValue = onFulfilled ? onFulfilled(this.value) : undefined;
            nextResolve(nextValue);
            return;
        }

        if (this.status === WPromise.rejected) {
            // 传入存储的错误信息
            // 同样的处理
            const nextReason = onRejected ? onRejected(this.reason) : undefined;
            nextResolve(nextReason);
        }
    }
}

我们再把刚开始的例子拿来测试一下

function fetchData() {
    return new WPromise((resolve, reject) => {
        setTimeout(() => {
            resolve('willem');
        }, 1000);
    });
}

fetchData().then((data) => {
    // after 1000ms
    console.log(data); // willem
    return 'wei';
}, (err) => {}).then((data2) => {
    console.log(data2); // wei
});

哟西,没啥问题。不过上面的版本还有个问题没有处理,当onFulfilled执行的结果不是一个简单的值,而就是一个Promise时,后续的then会等待其执行完成之后才执行。

Promise基础版的最终版:

class WPromise {
    static pending = 'pending';
    static fulfilled = 'fulfilled';
    static rejected = 'rejected';

    constructor(executor) {
        this.status = WPromise.pending; // 初始化状态为pending
        this.value = undefined; // 存储 this._resolve 即操作成功 返回的值
        this.reason = undefined; // 存储 this._reject 即操作失败 返回的值
        // 存储then中传入的参数
        // 至于为什么是数组呢?因为同一个Promise的then方法可以调用多次
        this.callbacks = [];
        executor(this._resolve.bind(this), this._reject.bind(this));
    }

    // onFulfilled 是成功时执行的函数
    // onRejected 是失败时执行的函数
    then(onFulfilled, onRejected) {
        // 返回一个新的Promise
        return new WPromise((nextResolve, nextReject) => {
            // 这里之所以把下一个Promsie的resolve函数和reject函数也存在callback中
            // 是为了将onFulfilled的执行结果通过nextResolve传入到下一个Promise作为它的value值
            this._handler({
                nextResolve,
                nextReject,
                onFulfilled,
                onRejected
            });
        });
    }

    _resolve(value) {
        // 处理onFulfilled执行结果是一个Promise时的情况
        // 这里可能理解起来有点困难
        // 当value instanof WPromise时,说明当前Promise肯定不会是第一个Promise
        // 而是后续then方法返回的Promise(第二个Promise)
        // 我们要获取的是value中的value值(有点绕,value是个promise时,那么内部存有个value的变量)
        // 怎样将value的value值获取到呢,可以将传递一个函数作为value.then的onFulfilled参数
        // 那么在value的内部则会执行这个函数,我们只需要将当前Promise的value值赋值为value的value即可
        if (value instanceof WPromise) {
            value.then(
                this._resolve.bind(this),
                this._reject.bind(this)
            );
            return;
        }

        this.value = value;
        this.status = WPromise.fulfilled; // 将状态设置为成功

        // 通知事件执行
        this.callbacks.forEach(cb => this._handler(cb));
    }

    _reject(reason) {
        if (reason instanceof WPromise) {
            reason.then(
                this._resolve.bind(this),
                this._reject.bind(this)
            );
            return;
        }

        this.reason = reason;
        this.status = WPromise.rejected; // 将状态设置为失败

        this.callbacks.forEach(cb => this._handler(cb));
    }

    _handler(callback) {
        const {
            onFulfilled,
            onRejected,
            nextResolve,
            nextReject
        } = callback;

        if (this.status === WPromise.pending) {
            this.callbacks.push(callback);
            return;
        }

        if (this.status === WPromise.fulfilled) {
            // 传入存储的值
            // 未传入onFulfilled时,value传入
            const nextValue = onFulfilled
                ? onFulfilled(this.value)
                : this.value;
            nextResolve(nextValue);
            return;
        }

        if (this.status === WPromise.rejected) {
            // 传入存储的错误信息
            // 同样的处理
            const nextReason = onRejected
                ? onRejected(this.reason)
                : this.reason;
            nextReject(nextReason);
        }
    }
}

ok,测试一下

function fetchData() {
    return new WPromise((resolve, reject) => {
        setTimeout(() => {
            resolve('willem');
        }, 1000);
    });
}

fetchData().then((data) => {
    return new WPromise(resolve => {
        setTimeout(() => {
            resolve(data + ' wei');
        }, 1000);
    });
}, (err) => {}).then((data2) => {
    console.log(data2); // willem wei
});

至此,一个简单的Promise就完成了,当然还有很多需要处理,比如异常等等。
下一篇文章我们一起再来学习一下finally和catch的实现。

更多:上述模拟Promise的完整代码

查看原文

赞 5 收藏 2 评论 0

yvonne 赞了文章 · 1月13日

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

前言

见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会及时修正。

----------超长文+多图预警,需要花费不少时间。----------

如果看完本文后,还对进程线程傻傻分不清,不清楚浏览器多进程、浏览器内核多线程、JS单线程、JS运行机制的区别。那么请回复我,一定是我写的还不够清晰,我来改。。。

----------正文开始----------

最近发现有不少介绍JS单线程运行机制的文章,但是发现很多都仅仅是介绍某一部分的知识,而且各个地方的说法还不统一,容易造成困惑。
因此准备梳理这块知识点,结合已有的认知,基于网上的大量参考资料,
从浏览器多进程到JS单线程,将JS引擎的运行机制系统的梳理一遍。

展现形式:由于是属于系统梳理型,就没有由浅入深了,而是从头到尾的梳理知识体系,
重点是将关键节点的知识点串联起来,而不是仅仅剖析某一部分知识。

内容是:从浏览器进程,再到浏览器内核运行,再到JS引擎单线程,再到JS事件循环机制,从头到尾系统的梳理一遍,摆脱碎片化,形成一个知识体系

目标是:看完这篇文章后,对浏览器多进程,JS单线程,JS事件循环机制这些都能有一定理解,
有一个知识体系骨架,而不是似懂非懂的感觉。

另外,本文适合有一定经验的前端人员,新手请规避,避免受到过多的概念冲击。可以先存起来,有了一定理解后再看,也可以分成多批次观看,避免过度疲劳。

大纲

  • 区分进程和线程
  • 浏览器是多进程的

    • 浏览器都包含哪些进程?
    • 浏览器多进程的优势
    • 重点是浏览器内核(渲染进程)
    • Browser进程和浏览器内核(Renderer进程)的通信过程
  • 梳理浏览器内核中线程之间的关系

    • GUI渲染线程与JS引擎线程互斥
    • JS阻塞页面加载
    • WebWorker,JS的多线程?
    • WebWorker与SharedWorker
  • 简单梳理下浏览器渲染流程

    • load事件与DOMContentLoaded事件的先后
    • css加载是否会阻塞dom树渲染?
    • 普通图层和复合图层
  • 从Event Loop谈JS的运行机制

    • 事件循环机制进一步补充
    • 单独说说定时器
    • setTimeout而不是setInterval
  • 事件循环进阶:macrotask与microtask
  • 写在最后的话

区分进程和线程

线程和进程区分不清,是很多新手都会犯的错误,没有关系。这很正常。先看看下面这个形象的比喻:

- 进程是一个工厂,工厂有它的独立资源

- 工厂之间相互独立

- 线程是工厂中的工人,多个工人协作完成任务

- 工厂内有一个或多个工人

- 工人之间共享空间

再完善完善概念:

- 工厂的资源 -> 系统分配的内存(独立的一块内存)

- 工厂之间的相互独立 -> 进程之间相互独立

- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务

- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成

- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

然后再巩固下:

如果是windows电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及cpu占有率。

所以,应该更容易理解了:进程是cpu资源分配的最小单位(系统会给它分配内存)

最后,再用较为官方的术语描述一遍:

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

tips

  • 不同进程之间也可以通信,不过代价较大
  • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

浏览器是多进程的

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:(先看下简化理解)

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

关于以上几点的验证,请再第一张图

图中打开了Chrome浏览器的多个标签页,然后可以在Chrome的任务管理器中看到有多个进程(分别是每一个Tab页面有一个独立的进程,以及一个主进程)。
感兴趣的可以自行尝试下,如果再多打开一个Tab页,进程正常会+1以上

注意:在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了
(所以每一个Tab标签对应一个进程并不一定是绝对的)

浏览器都包含哪些进程?

知道了浏览器是多进程后,再来看看它到底包含哪些进程:(为了简化理解,仅列举主要进程)

  1. Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  2. 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  3. GPU进程:最多一个,用于3D绘制等
  4. 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为

    • 页面渲染,脚本执行,事件处理等

强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)

当然,浏览器有时会将多个进程合并(譬如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程),如图

另外,可以通过Chrome的更多工具 -> 任务管理器自行验证

浏览器多进程的优势

相比于单进程浏览器,多进程有如下优点:

  • 避免单个page crash影响整个浏览器
  • 避免第三方插件crash影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果是单进程,插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。。。

当然,内存等资源消耗也会更大,有点空间换时间的意思。

重点是浏览器内核(渲染进程)

重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程

可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行。接下来重点分析这个进程

请牢记,浏览器的渲染进程是多线程的(这点如果不理解,请回头看进程和线程的区分

终于到了线程这个概念了?,好亲切。那么接下来看看它都包含了哪些线程(列举一些主要常驻线程):

  1. GUI渲染线程

    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  2. JS引擎线程

    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运行代码。
    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
    • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  3. 事件触发线程

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

  4. 定时触发器线程

    • 传说中的setIntervalsetTimeout所在线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  5. 异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

看到这里,如果觉得累了,可以先休息下,这些概念需要被消化,毕竟后续将提到的事件循环机制就是基于事件触发线程的,所以如果仅仅是看某个碎片化知识,
可能会有一种似懂非懂的感觉。要完成的梳理一遍才能快速沉淀,不易遗忘。放张图巩固下吧:

再说一点,为什么JS引擎是单线程的?额,这个问题其实应该没有标准答案,譬如,可能仅仅是因为由于多线程的复杂性,譬如多线程操作一般要加锁,因此最初设计时选择了单线程。。。

Browser进程和浏览器内核(Renderer进程)的通信过程

看到这里,首先,应该对浏览器内的进程和线程都有一定理解了,那么接下来,再谈谈浏览器的Browser进程(控制进程)是如何和内核通信的,
这点也理解后,就可以将这部分的知识串联起来,从头到尾有一个完整的概念。

如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程)
然后在这前提下,看下整个的过程:(简化了很多)

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染

    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  • Browser进程接收到结果并将结果绘制出来

这里绘一张简单的图:(很简化)

看完这一整套流程,应该对浏览器的运作有了一定理解了,这样有了知识架构的基础后,后续就方便往上填充内容。

这块再往深处讲的话就涉及到浏览器内核源码解析了,不属于本文范围。

如果这一块要深挖,建议去读一些浏览器内核源码解析文章,或者可以先看看参考下来源中的第一篇文章,写的不错

梳理浏览器内核中线程之间的关系

到了这里,已经对浏览器的运行有了一个整体的概念,接下来,先简单梳理一些概念

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

JS阻塞页面加载

从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。

譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。
然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

WebWorker,JS的多线程?

前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么?

所以,后来HTML5中支持了Web Worker

MDN的官方解释是:

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面

一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 

这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window

因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误

这样理解下:

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程,
只待计算出结果后,将结果通信给主线程即可,perfect!

而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。

其它,关于Worker的详解就不是本文的范畴了,因此不再赘述。

WebWorker与SharedWorker

既然都到了这里,就再提一下SharedWorker(避免后续将这两个概念搞混)

  • WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享

    • 所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
  • SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

    • 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。

看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程

简单梳理下浏览器渲染流程

本来是直接计划开始谈JS运行机制的,但想了想,既然上述都一直在谈浏览器,直接跳到JS可能再突兀,因此,中间再补充下浏览器的渲染流程(简单版本)

为了简化理解,前期工作直接省略成:(要展开的或完全可以写另一篇超长文)

- 浏览器输入url,浏览器主进程接管,开一个下载线程,
然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
随后将内容通过RendererHost接口转交给Renderer进程

- 浏览器渲染流程开始

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

  1. 解析html建立dom树
  2. 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

所有详细步骤都已经略去,渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了

既然略去了一些详细的步骤,那么就提一些可能需要注意的细节把。

这里重绘参考来源中的一张图:(参考来源第一篇)

load事件与DOMContentLoaded事件的先后

上面提到,渲染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?

很简单,知道它们的定义就可以了:

  • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。

(譬如如果有async加载的脚本就不一定完成)

  • 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。

(渲染完毕了)

所以,顺序是:DOMContentLoaded -> load

css加载是否会阻塞dom树渲染?

这里说的是头部引入css的情况

首先,我们都知道:css是由单独的下载线程异步下载的。

然后再说下几个现象:

  • css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
  • 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

这可能也是浏览器的一种优化机制。

因为你加载css的时候,可能会修改下面DOM节点的样式,
如果css加载不阻塞render树渲染的话,那么当css加载完之后,
render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。
所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,
在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

普通图层和复合图层

渲染步骤中就提到了composite概念。

可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层

首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)

其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层

然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源
(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)

可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒

可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息

如下图。可以验证上述的说法

如何变成复合图层(硬件加速)

将该元素变成一个复合图层,就是传说中的硬件加速技术

  • 最常用的方式:translate3dtranslateZ
  • opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),

作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)

  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如以前的flash插件

absolute和硬件加速的区别

可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。
所以,就算absolute中信息改变时不会改变普通文档流中render树,
但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。
(浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)

而硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层
(当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)

复合图层的作用?

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能

但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡

硬件加速时请使用index

使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染

具体的原理时这样的:
**webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能**

简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意

另外,这个问题可以在这个地址看到重现(原作者分析的挺到位的,直接上链接):

http://web.jobbole.com/83575/

从Event Loop谈JS的运行机制

到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS引擎的一些运行机制分析。

注意,这里不谈可执行上下文VOscop chain等概念(这些完全可以整理成另一篇文章了),这里主要是结合Event Loop来谈JS代码是如何执行的。

读这部分的前提是已经知道了JS引擎是单线程,而且这里会用到上文中的几个概念:(如果不是很理解,可以回头温习)

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程

然后再理解一个概念:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

看图:

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,
所以自然有误差。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

上图大致描述就是:

  • 主线程运行时会产生执行栈,

栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

单独说说定时器

上述事件循环机制的核心是:JS引擎线程和事件触发线程

但事件上,里面还有一些隐藏细节,譬如调用setTimeout后,是如何等待特定时间后才添加到事件队列中的?

是JS引擎检测的么?当然不是了。它是由定时器线程控制(因为JS引擎自己都忙不过来,根本无暇分身)

为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

什么时候会用到定时器线程?当使用setTimeoutsetInterval,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

譬如:

setTimeout(function(){
    console.log('hello!');
}, 1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

setTimeout(function(){
    console.log('hello!');
}, 0);

console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

  • 执行结果是:先beginhello!
  • 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

(不过也有一说是不同浏览器有不同的最小时间设定)

  • 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈内空了后才会主动读取事件队列)

setTimeout而不是setInterval

用setTimeout模拟定期计时和直接用setInterval是有区别的。

因为每次setTimeout计时到后就会去执行,然后执行一段时间后才会继续setTimeout,中间就多了误差
(误差多少与代码执行时间有关)

而setInterval则是每次都精确的隔一段时间推入一个事件
(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

而且setInterval有一些比较致命的问题就是:

  • 累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,

就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)

  • 譬如像iOS的webview,或者Safari等浏览器中都有一个特点,在滚动的时候是不执行JS的,如果使用了setInterval,会发现在滚动结束后会执行多次由于滚动不执行JS积攒回调,如果回调执行时间过长,就会非常容器造成卡顿问题和一些不可知的错误(这一块后续有补充,setInterval自带的优化,不会重复添加回调)
  • 而且把浏览器最小化显示等操作时,setInterval并不是不执行程序,

它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行时

所以,鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题。。。

事件循环进阶:macrotask与microtask

这段参考了参考来源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下),
强烈推荐有英文基础的同学直接观看原文,作者描述的很清晰,示例也很不错,如下:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题:

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('script end');

嗯哼,它的正确执行顺序是这样子的:

script start
script end
promise1
promise2
setTimeout

为什么呢?因为Promise里有了一个一个新的概念:microtask

或者,进一步,JS中分为两种任务类型:macrotaskmicrotask,在ECMAScript中,microtask称为jobs,macrotask可称为task

它们的定义?区别?简单点可以按如下理解:

  • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

    • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
    • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(`task->渲染->task->...`)
  • microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

    • 也就是说,在当前task任务后,下一个task之前,在渲染之前
    • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
    • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

分别很么样的场景会形成macrotask和microtask呢?

  • macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
  • microtask:Promise,process.nextTick等

__补充:在node环境下,process.nextTick的优先级高于Promise__,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

参考:https://segmentfault.com/q/1010000011914016

再根据线程来理解下:

  • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护

(这点由自己理解+推测得出,因为它是在主线程下无缝执行的)

所以,总结下运行机制:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

如图:

另外,请注意下Promisepolyfill与官方版本的区别:

  • 官方版本中,是标准的microtask形式
  • polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式
  • 请特别注意这两点区别

注意,有一些浏览器执行结果不一样(因为它们可能把microtask当成macrotask来执行了),
但是为了简单,这里不描述一些不标准的浏览器下的场景(但记住,有些浏览器可能并不标准)

20180126补充:使用MutationObserver实现microtask

MutationObserver可以用来实现microtask
(它属于microtask,优先级小于Promise,
一般是Promise不支持时才会这样做)

它是HTML5中的新特性,作用是:监听一个DOM变动,
当DOM对象树发生任何变动时,Mutation Observer会得到通知

像以前的Vue源码中就是利用它来模拟nextTick的,
具体原理是,创建一个TextNode并监听内容变化,
然后要nextTick的时候去改一下这个节点的文本内容,
如下:(Vue的源码,未修改)

var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))

observer.observe(textNode, {
    characterData: true
})
timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}

对应Vue源码链接

不过,现在的Vue(2.5+)的nextTick实现移除了MutationObserver的方式(据说是兼容性原因),
取而代之的是使用MessageChannel
(当然,默认情况仍然是Promise,不支持才兼容的)。

MessageChannel属于宏任务,优先级是:MessageChannel->setTimeout
所以Vue(2.5+)内部的nextTick与2.4及之前的实现是不一样的,需要注意下。

这里不展开,可以看下https://juejin.im/post/5a1af88f5188254a701ec230

写在最后的话

看到这里,不知道对JS的运行机制是不是更加理解了,从头到尾梳理,而不是就某一个碎片化知识应该是会更清晰的吧?

同时,也应该注意到了JS根本就没有想象的那么简单,前端的知识也是无穷无尽,层出不穷的概念、N多易忘的知识点、各式各样的框架、
底层原理方面也是可以无限的往下深挖,然后你就会发现,你知道的太少了。。。

另外,本文也打算先告一段落,其它的,如JS词法解析,可执行上下文以及VO等概念就不继续在本文中写了,后续可以考虑另开新的文章。

最后,喜欢的话,就请给个赞吧!

附录

博客

初次发布2018.01.21于我个人博客上面

http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html

招聘软广

阿里巴巴钉钉商业化团队大量hc,高薪股权。机会好,技术成长空间足,业务也有很大的发挥空间!

还在犹豫什么,来吧!!!

社招(P6~P7)

职责和挑战

  1. 负责钉钉工作台。工作台是帮助企业实现数字化管理和协同的门户,是拥有亿级用户量的产品。如何保障安全、稳定、性能和体验是对我们的一大挑战。
  2. 负责开放能力建设。针对纷繁的业务场景,提供合理的开放方案,既要做到深入用户场景理解并支撑业务发展,满足企业千人千面、千行千面的诉求,又要在技术上保障用户的安全、稳定和体验。需要既要有技术抽象能力、平台架构能力,又要有业务的理解和分析能力。
  3. 开放平台基础建设。保障链路的安全和稳定。同时对如何保障用户体验有持续精进的热情和追求。

职位要求

  1. 精通HTML5、CSS3、JS(ES5/ES6)等前端开发技术
  2. 掌握主流的JS库和开发框架,并深入理解其设计原理,例如React,Vue等
  3. 熟悉模块化、前端编译和构建工具,例如webpack、babel等
  4. (加分项)了解服务端或native移动应用开发,例如nodejs、Java等
  5. 对技术有强追求,有良好的沟通能力和团队协同能力,有优秀的分析问题和解决问题的能力。

前端实习

面向2021毕业的同学

  1. 本科及以上学历,计算机相关专业
  2. 熟练掌握HTML5/CSS3/Javascript等web前端技术
  3. 熟悉至少一种常用框架,例如React、vue等
  4. 关注新事物、新技术,有较强的学习能力,有强烈求知欲和进取心
  5. 有半年以上实际项目经验,大厂加分

image.png

image.png

内推邮箱

lichun.dlc@alibaba-inc.com

简历发我邮箱,必有回应,符合要求直接走内推!!!

一对一服务,有问必答!

也可加我微信了解更多:a546684355

参考资料

查看原文

赞 852 收藏 966 评论 102

yvonne 收藏了文章 · 1月13日

从浏览器多进程到JS单线程,JS运行机制最全面的一次梳理

前言

见解有限,如有描述不当之处,请帮忙及时指出,如有错误,会及时修正。

----------超长文+多图预警,需要花费不少时间。----------

如果看完本文后,还对进程线程傻傻分不清,不清楚浏览器多进程、浏览器内核多线程、JS单线程、JS运行机制的区别。那么请回复我,一定是我写的还不够清晰,我来改。。。

----------正文开始----------

最近发现有不少介绍JS单线程运行机制的文章,但是发现很多都仅仅是介绍某一部分的知识,而且各个地方的说法还不统一,容易造成困惑。
因此准备梳理这块知识点,结合已有的认知,基于网上的大量参考资料,
从浏览器多进程到JS单线程,将JS引擎的运行机制系统的梳理一遍。

展现形式:由于是属于系统梳理型,就没有由浅入深了,而是从头到尾的梳理知识体系,
重点是将关键节点的知识点串联起来,而不是仅仅剖析某一部分知识。

内容是:从浏览器进程,再到浏览器内核运行,再到JS引擎单线程,再到JS事件循环机制,从头到尾系统的梳理一遍,摆脱碎片化,形成一个知识体系

目标是:看完这篇文章后,对浏览器多进程,JS单线程,JS事件循环机制这些都能有一定理解,
有一个知识体系骨架,而不是似懂非懂的感觉。

另外,本文适合有一定经验的前端人员,新手请规避,避免受到过多的概念冲击。可以先存起来,有了一定理解后再看,也可以分成多批次观看,避免过度疲劳。

大纲

  • 区分进程和线程
  • 浏览器是多进程的

    • 浏览器都包含哪些进程?
    • 浏览器多进程的优势
    • 重点是浏览器内核(渲染进程)
    • Browser进程和浏览器内核(Renderer进程)的通信过程
  • 梳理浏览器内核中线程之间的关系

    • GUI渲染线程与JS引擎线程互斥
    • JS阻塞页面加载
    • WebWorker,JS的多线程?
    • WebWorker与SharedWorker
  • 简单梳理下浏览器渲染流程

    • load事件与DOMContentLoaded事件的先后
    • css加载是否会阻塞dom树渲染?
    • 普通图层和复合图层
  • 从Event Loop谈JS的运行机制

    • 事件循环机制进一步补充
    • 单独说说定时器
    • setTimeout而不是setInterval
  • 事件循环进阶:macrotask与microtask
  • 写在最后的话

区分进程和线程

线程和进程区分不清,是很多新手都会犯的错误,没有关系。这很正常。先看看下面这个形象的比喻:

- 进程是一个工厂,工厂有它的独立资源

- 工厂之间相互独立

- 线程是工厂中的工人,多个工人协作完成任务

- 工厂内有一个或多个工人

- 工人之间共享空间

再完善完善概念:

- 工厂的资源 -> 系统分配的内存(独立的一块内存)

- 工厂之间的相互独立 -> 进程之间相互独立

- 多个工人协作完成任务 -> 多个线程在进程中协作完成任务

- 工厂内有一个或多个工人 -> 一个进程由一个或多个线程组成

- 工人之间共享空间 -> 同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)

然后再巩固下:

如果是windows电脑中,可以打开任务管理器,可以看到有一个后台进程列表。对,那里就是查看进程的地方,而且可以看到每个进程的内存资源信息以及cpu占有率。

所以,应该更容易理解了:进程是cpu资源分配的最小单位(系统会给它分配内存)

最后,再用较为官方的术语描述一遍:

  • 进程是cpu资源分配的最小单位(是能拥有资源和独立运行的最小单位)
  • 线程是cpu调度的最小单位(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)

tips

  • 不同进程之间也可以通信,不过代价较大
  • 现在,一般通用的叫法:单线程与多线程,都是指在一个进程内的单和多。(所以核心还是得属于一个进程才行)

浏览器是多进程的

理解了进程与线程了区别后,接下来对浏览器进行一定程度上的认识:(先看下简化理解)

  • 浏览器是多进程的
  • 浏览器之所以能够运行,是因为系统给它的进程分配了资源(cpu、内存)
  • 简单点理解,每打开一个Tab页,就相当于创建了一个独立的浏览器进程。

关于以上几点的验证,请再第一张图

图中打开了Chrome浏览器的多个标签页,然后可以在Chrome的任务管理器中看到有多个进程(分别是每一个Tab页面有一个独立的进程,以及一个主进程)。
感兴趣的可以自行尝试下,如果再多打开一个Tab页,进程正常会+1以上

注意:在这里浏览器应该也有自己的优化机制,有时候打开多个tab页后,可以在Chrome任务管理器中看到,有些进程被合并了
(所以每一个Tab标签对应一个进程并不一定是绝对的)

浏览器都包含哪些进程?

知道了浏览器是多进程后,再来看看它到底包含哪些进程:(为了简化理解,仅列举主要进程)

  1. Browser进程:浏览器的主进程(负责协调、主控),只有一个。作用有

    • 负责浏览器界面显示,与用户交互。如前进,后退等
    • 负责各个页面的管理,创建和销毁其他进程
    • 将Renderer进程得到的内存中的Bitmap,绘制到用户界面上
    • 网络资源的管理,下载等
  2. 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件时才创建
  3. GPU进程:最多一个,用于3D绘制等
  4. 浏览器渲染进程(浏览器内核)(Renderer进程,内部是多线程的):默认每个Tab页面一个进程,互不影响。主要作用为

    • 页面渲染,脚本执行,事件处理等

强化记忆:在浏览器中打开一个网页相当于新起了一个进程(进程内有自己的多线程)

当然,浏览器有时会将多个进程合并(譬如打开多个空白标签页后,会发现多个空白标签页被合并成了一个进程),如图

另外,可以通过Chrome的更多工具 -> 任务管理器自行验证

浏览器多进程的优势

相比于单进程浏览器,多进程有如下优点:

  • 避免单个page crash影响整个浏览器
  • 避免第三方插件crash影响整个浏览器
  • 多进程充分利用多核优势
  • 方便使用沙盒模型隔离插件等进程,提高浏览器稳定性

简单点理解:如果浏览器是单进程,那么某个Tab页崩溃了,就影响了整个浏览器,体验有多差;同理如果是单进程,插件崩溃了也会影响整个浏览器;而且多进程还有其它的诸多优势。。。

当然,内存等资源消耗也会更大,有点空间换时间的意思。

重点是浏览器内核(渲染进程)

重点来了,我们可以看到,上面提到了这么多的进程,那么,对于普通的前端操作来说,最终要的是什么呢?答案是渲染进程

可以这样理解,页面的渲染,JS的执行,事件的循环,都在这个进程内进行。接下来重点分析这个进程

请牢记,浏览器的渲染进程是多线程的(这点如果不理解,请回头看进程和线程的区分

终于到了线程这个概念了?,好亲切。那么接下来看看它都包含了哪些线程(列举一些主要常驻线程):

  1. GUI渲染线程

    • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等。
    • 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
    • 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行。
  2. JS引擎线程

    • 也称为JS内核,负责处理Javascript脚本程序。(例如V8引擎)
    • JS引擎线程负责解析Javascript脚本,运行代码。
    • JS引擎一直等待着任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
    • 同样注意,GUI渲染线程与JS引擎线程是互斥的,所以如果JS执行的时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞。
  3. 事件触发线程

    • 归属于浏览器而不是JS引擎,用来控制事件循环(可以理解,JS引擎自己都忙不过来,需要浏览器另开线程协助)
    • 当JS引擎执行代码块如setTimeOut时(也可来自浏览器内核的其他线程,如鼠标点击、AJAX异步请求等),会将对应任务添加到事件线程中
    • 当对应的事件符合触发条件被触发时,该线程会把事件添加到待处理队列的队尾,等待JS引擎的处理
    • 注意,由于JS的单线程关系,所以这些待处理队列中的事件都得排队等待JS引擎处理(当JS引擎空闲时才会去执行)

  4. 定时触发器线程

    • 传说中的setIntervalsetTimeout所在线程
    • 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确)
    • 因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
    • 注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  5. 异步http请求线程

    • 在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    • 将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

看到这里,如果觉得累了,可以先休息下,这些概念需要被消化,毕竟后续将提到的事件循环机制就是基于事件触发线程的,所以如果仅仅是看某个碎片化知识,
可能会有一种似懂非懂的感觉。要完成的梳理一遍才能快速沉淀,不易遗忘。放张图巩固下吧:

再说一点,为什么JS引擎是单线程的?额,这个问题其实应该没有标准答案,譬如,可能仅仅是因为由于多线程的复杂性,譬如多线程操作一般要加锁,因此最初设计时选择了单线程。。。

Browser进程和浏览器内核(Renderer进程)的通信过程

看到这里,首先,应该对浏览器内的进程和线程都有一定理解了,那么接下来,再谈谈浏览器的Browser进程(控制进程)是如何和内核通信的,
这点也理解后,就可以将这部分的知识串联起来,从头到尾有一个完整的概念。

如果自己打开任务管理器,然后打开一个浏览器,就可以看到:任务管理器中出现了两个进程(一个是主控进程,一个则是打开Tab页的渲染进程)
然后在这前提下,看下整个的过程:(简化了很多)

  • Browser进程收到用户请求,首先需要获取页面内容(譬如通过网络下载资源),随后将该任务通过RendererHost接口传递给Render进程
  • Renderer进程的Renderer接口收到消息,简单解释后,交给渲染线程,然后开始渲染

    • 渲染线程接收请求,加载网页并渲染网页,这其中可能需要Browser进程获取资源和需要GPU进程来帮助渲染
    • 当然可能会有JS线程操作DOM(这样可能会造成回流并重绘)
    • 最后Render进程将结果传递给Browser进程
  • Browser进程接收到结果并将结果绘制出来

这里绘一张简单的图:(很简化)

看完这一整套流程,应该对浏览器的运作有了一定理解了,这样有了知识架构的基础后,后续就方便往上填充内容。

这块再往深处讲的话就涉及到浏览器内核源码解析了,不属于本文范围。

如果这一块要深挖,建议去读一些浏览器内核源码解析文章,或者可以先看看参考下来源中的第一篇文章,写的不错

梳理浏览器内核中线程之间的关系

到了这里,已经对浏览器的运行有了一个整体的概念,接下来,先简单梳理一些概念

GUI渲染线程与JS引擎线程互斥

由于JavaScript是可操纵DOM的,如果在修改这些元素属性同时渲染界面(即JS线程和UI线程同时运行),那么渲染线程前后获得的元素数据就可能不一致了。

因此为了防止渲染出现不可预期的结果,浏览器设置GUI渲染线程与JS引擎为互斥的关系,当JS引擎执行时GUI线程会被挂起,
GUI更新则会被保存在一个队列中等到JS引擎线程空闲时立即被执行。

JS阻塞页面加载

从上述的互斥关系,可以推导出,JS如果执行时间过长就会阻塞页面。

譬如,假设JS引擎正在进行巨量的计算,此时就算GUI有更新,也会被保存到队列中,等待JS引擎空闲后执行。
然后,由于巨量计算,所以JS引擎很可能很久很久后才能空闲,自然会感觉到巨卡无比。

所以,要尽量避免JS执行时间过长,这样就会造成页面的渲染不连贯,导致页面渲染加载阻塞的感觉。

WebWorker,JS的多线程?

前文中有提到JS引擎是单线程的,而且JS执行时间过长会阻塞页面,那么JS就真的对cpu密集型计算无能为力么?

所以,后来HTML5中支持了Web Worker

MDN的官方解释是:

Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面

一个worker是使用一个构造函数创建的一个对象(e.g. Worker()) 运行一个命名的JavaScript文件 

这个文件包含将在工作线程中运行的代码; workers 运行在另一个全局上下文中,不同于当前的window

因此,使用 window快捷方式获取当前全局的范围 (而不是self) 在一个 Worker 内将返回错误

这样理解下:

  • 创建Worker时,JS引擎向浏览器申请开一个子线程(子线程是浏览器开的,完全受主线程控制,而且不能操作DOM)
  • JS引擎线程与worker线程间通过特定的方式通信(postMessage API,需要通过序列化对象来与线程交互特定的数据)

所以,如果有非常耗时的工作,请单独开一个Worker线程,这样里面不管如何翻天覆地都不会影响JS引擎主线程,
只待计算出结果后,将结果通信给主线程即可,perfect!

而且注意下,JS引擎是单线程的,这一点的本质仍然未改变,Worker可以理解是浏览器给JS引擎开的外挂,专门用来解决那些大量计算问题。

其它,关于Worker的详解就不是本文的范畴了,因此不再赘述。

WebWorker与SharedWorker

既然都到了这里,就再提一下SharedWorker(避免后续将这两个概念搞混)

  • WebWorker只属于某个页面,不会和其他页面的Render进程(浏览器内核进程)共享

    • 所以Chrome在Render进程中(每一个Tab页就是一个render进程)创建一个新的线程来运行Worker中的JavaScript程序。
  • SharedWorker是浏览器所有页面共享的,不能采用与Worker同样的方式实现,因为它不隶属于某个Render进程,可以为多个Render进程共享使用

    • 所以Chrome浏览器为SharedWorker单独创建一个进程来运行JavaScript程序,在浏览器中每个相同的JavaScript只存在一个SharedWorker进程,不管它被创建多少次。

看到这里,应该就很容易明白了,本质上就是进程和线程的区别。SharedWorker由独立的进程管理,WebWorker只是属于render进程下的一个线程

简单梳理下浏览器渲染流程

本来是直接计划开始谈JS运行机制的,但想了想,既然上述都一直在谈浏览器,直接跳到JS可能再突兀,因此,中间再补充下浏览器的渲染流程(简单版本)

为了简化理解,前期工作直接省略成:(要展开的或完全可以写另一篇超长文)

- 浏览器输入url,浏览器主进程接管,开一个下载线程,
然后进行 http请求(略去DNS查询,IP寻址等等操作),然后等待响应,获取内容,
随后将内容通过RendererHost接口转交给Renderer进程

- 浏览器渲染流程开始

浏览器器内核拿到内容后,渲染大概可以划分成以下几个步骤:

  1. 解析html建立dom树
  2. 解析css构建render树(将CSS代码解析成树形的数据结构,然后结合DOM合并成render树)
  3. 布局render树(Layout/reflow),负责各元素尺寸、位置的计算
  4. 绘制render树(paint),绘制页面像素信息
  5. 浏览器会将各层的信息发送给GPU,GPU会将各层合成(composite),显示在屏幕上。

所有详细步骤都已经略去,渲染完毕后就是load事件了,之后就是自己的JS逻辑处理了

既然略去了一些详细的步骤,那么就提一些可能需要注意的细节把。

这里重绘参考来源中的一张图:(参考来源第一篇)

load事件与DOMContentLoaded事件的先后

上面提到,渲染完毕后会触发load事件,那么你能分清楚load事件与DOMContentLoaded事件的先后么?

很简单,知道它们的定义就可以了:

  • 当 DOMContentLoaded 事件触发时,仅当DOM加载完成,不包括样式表,图片。

(譬如如果有async加载的脚本就不一定完成)

  • 当 onload 事件触发时,页面上所有的DOM,样式表,脚本,图片都已经加载完成了。

(渲染完毕了)

所以,顺序是:DOMContentLoaded -> load

css加载是否会阻塞dom树渲染?

这里说的是头部引入css的情况

首先,我们都知道:css是由单独的下载线程异步下载的。

然后再说下几个现象:

  • css加载不会阻塞DOM树解析(异步加载时DOM照常构建)
  • 但会阻塞render树渲染(渲染时需等css加载完毕,因为render树需要css信息)

这可能也是浏览器的一种优化机制。

因为你加载css的时候,可能会修改下面DOM节点的样式,
如果css加载不阻塞render树渲染的话,那么当css加载完之后,
render树可能又得重新重绘或者回流了,这就造成了一些没有必要的损耗。
所以干脆就先把DOM树的结构先解析完,把可以做的工作做完,然后等你css加载完之后,
在根据最终的样式来渲染render树,这种做法性能方面确实会比较好一点。

普通图层和复合图层

渲染步骤中就提到了composite概念。

可以简单的这样理解,浏览器渲染的图层一般包含两大类:普通图层以及复合图层

首先,普通文档流内可以理解为一个复合图层(这里称为默认复合层,里面不管添加多少元素,其实都是在同一个复合图层中)

其次,absolute布局(fixed也一样),虽然可以脱离普通文档流,但它仍然属于默认复合层

然后,可以通过硬件加速的方式,声明一个新的复合图层,它会单独分配资源
(当然也会脱离普通文档流,这样一来,不管这个复合图层中怎么变化,也不会影响默认复合层里的回流重绘)

可以简单理解下:GPU中,各个复合图层是单独绘制的,所以互不影响,这也是为什么某些场景硬件加速效果一级棒

可以Chrome源码调试 -> More Tools -> Rendering -> Layer borders中看到,黄色的就是复合图层信息

如下图。可以验证上述的说法

如何变成复合图层(硬件加速)

将该元素变成一个复合图层,就是传说中的硬件加速技术

  • 最常用的方式:translate3dtranslateZ
  • opacity属性/过渡动画(需要动画执行的过程中才会创建合成层,动画没有开始或结束后元素还会回到之前的状态)
  • will-chang属性(这个比较偏僻),一般配合opacity与translate使用(而且经测试,除了上述可以引发硬件加速的属性外,其它属性并不会变成复合层),

作用是提前告诉浏览器要变化,这样浏览器会开始做一些优化工作(这个最好用完后就释放)

  • <video><iframe><canvas><webgl>等元素
  • 其它,譬如以前的flash插件

absolute和硬件加速的区别

可以看到,absolute虽然可以脱离普通文档流,但是无法脱离默认复合层。
所以,就算absolute中信息改变时不会改变普通文档流中render树,
但是,浏览器最终绘制时,是整个复合层绘制的,所以absolute中信息的改变,仍然会影响整个复合层的绘制。
(浏览器会重绘它,如果复合层中内容多,absolute带来的绘制信息变化过大,资源消耗是非常严重的)

而硬件加速直接就是在另一个复合层了(另起炉灶),所以它的信息改变不会影响默认复合层
(当然了,内部肯定会影响属于自己的复合层),仅仅是引发最后的合成(输出视图)

复合图层的作用?

一般一个元素开启硬件加速后会变成复合图层,可以独立于普通文档流中,改动后可以避免整个页面重绘,提升性能

但是尽量不要大量使用复合图层,否则由于资源消耗过度,页面反而会变的更卡

硬件加速时请使用index

使用硬件加速时,尽可能的使用index,防止浏览器默认给后续的元素创建复合层渲染

具体的原理时这样的:
**webkit CSS3中,如果这个元素添加了硬件加速,并且index层级比较低,
那么在这个元素的后面其它元素(层级比这个元素高的,或者相同的,并且releative或absolute属性相同的),
会默认变为复合层渲染,如果处理不当会极大的影响性能**

简单点理解,其实可以认为是一个隐式合成的概念:如果a是一个复合图层,而且b在a上面,那么b也会被隐式转为一个复合图层,这点需要特别注意

另外,这个问题可以在这个地址看到重现(原作者分析的挺到位的,直接上链接):

http://web.jobbole.com/83575/

从Event Loop谈JS的运行机制

到此时,已经是属于浏览器页面初次渲染完毕后的事情,JS引擎的一些运行机制分析。

注意,这里不谈可执行上下文VOscop chain等概念(这些完全可以整理成另一篇文章了),这里主要是结合Event Loop来谈JS代码是如何执行的。

读这部分的前提是已经知道了JS引擎是单线程,而且这里会用到上文中的几个概念:(如果不是很理解,可以回头温习)

  • JS引擎线程
  • 事件触发线程
  • 定时触发器线程

然后再理解一个概念:

  • JS分为同步任务和异步任务
  • 同步任务都在主线程上执行,形成一个执行栈
  • 主线程之外,事件触发线程管理着一个任务队列,只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  • 一旦执行栈中的所有同步任务执行完毕(此时JS引擎空闲),系统就会读取任务队列,将可运行的异步任务添加到可执行栈中,开始执行。

看图:

看到这里,应该就可以理解了:为什么有时候setTimeout推入的事件不能准时执行?因为可能在它推入到事件列表时,主线程还不空闲,正在执行其它代码,
所以自然有误差。

事件循环机制进一步补充

这里就直接引用一张图片来协助理解:(参考自Philip Roberts的演讲《Help, I'm stuck in an event-loop》)

上图大致描述就是:

  • 主线程运行时会产生执行栈,

栈中的代码调用某些api时,它们会在事件队列中添加各种事件(当满足触发条件后,如ajax请求完毕)

  • 而栈中的代码执行完毕,就会读取事件队列中的事件,去执行那些回调
  • 如此循环
  • 注意,总是要等待栈中的代码执行完毕后才会去读取事件队列中的事件

单独说说定时器

上述事件循环机制的核心是:JS引擎线程和事件触发线程

但事件上,里面还有一些隐藏细节,譬如调用setTimeout后,是如何等待特定时间后才添加到事件队列中的?

是JS引擎检测的么?当然不是了。它是由定时器线程控制(因为JS引擎自己都忙不过来,根本无暇分身)

为什么要单独的定时器线程?因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确,因此很有必要单独开一个线程用来计时。

什么时候会用到定时器线程?当使用setTimeoutsetInterval,它需要定时器线程计时,计时完成后就会将特定的事件推入事件队列中。

譬如:

setTimeout(function(){
    console.log('hello!');
}, 1000);

这段代码的作用是当1000毫秒计时完毕后(由定时器线程计时),将回调函数推入事件队列中,等待主线程执行

setTimeout(function(){
    console.log('hello!');
}, 0);

console.log('begin');

这段代码的效果是最快的时间内将回调函数推入事件队列中,等待主线程执行

注意:

  • 执行结果是:先beginhello!
  • 虽然代码的本意是0毫秒后就推入事件队列,但是W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。

(不过也有一说是不同浏览器有不同的最小时间设定)

  • 就算不等待4ms,就算假设0毫秒就推入事件队列,也会先执行begin(因为只有可执行栈内空了后才会主动读取事件队列)

setTimeout而不是setInterval

用setTimeout模拟定期计时和直接用setInterval是有区别的。

因为每次setTimeout计时到后就会去执行,然后执行一段时间后才会继续setTimeout,中间就多了误差
(误差多少与代码执行时间有关)

而setInterval则是每次都精确的隔一段时间推入一个事件
(但是,事件的实际执行时间不一定就准确,还有可能是这个事件还没执行完毕,下一个事件就来了)

而且setInterval有一些比较致命的问题就是:

  • 累计效应(上面提到的),如果setInterval代码在(setInterval)再次添加到队列之前还没有完成执行,

就会导致定时器代码连续运行好几次,而之间没有间隔。
就算正常间隔执行,多个setInterval的代码执行时间可能会比预期小(因为代码执行需要一定时间)

  • 譬如像iOS的webview,或者Safari等浏览器中都有一个特点,在滚动的时候是不执行JS的,如果使用了setInterval,会发现在滚动结束后会执行多次由于滚动不执行JS积攒回调,如果回调执行时间过长,就会非常容器造成卡顿问题和一些不可知的错误(这一块后续有补充,setInterval自带的优化,不会重复添加回调)
  • 而且把浏览器最小化显示等操作时,setInterval并不是不执行程序,

它会把setInterval的回调函数放在队列中,等浏览器窗口再次打开时,一瞬间全部执行时

所以,鉴于这么多但问题,目前一般认为的最佳方案是:用setTimeout模拟setInterval,或者特殊场合直接用requestAnimationFrame

补充:JS高程中有提到,JS引擎会对setInterval进行优化,如果当前事件队列中有setInterval的回调,不会重复添加。不过,仍然是有很多问题。。。

事件循环进阶:macrotask与microtask

这段参考了参考来源中的第2篇文章(英文版的),(加了下自己的理解重新描述了下),
强烈推荐有英文基础的同学直接观看原文,作者描述的很清晰,示例也很不错,如下:

https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

上文中将JS事件循环机制梳理了一遍,在ES5的情况是够用了,但是在ES6盛行的现在,仍然会遇到一些问题,譬如下面这题:

console.log('script start');

setTimeout(function() {
    console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
    console.log('promise1');
}).then(function() {
    console.log('promise2');
});

console.log('script end');

嗯哼,它的正确执行顺序是这样子的:

script start
script end
promise1
promise2
setTimeout

为什么呢?因为Promise里有了一个一个新的概念:microtask

或者,进一步,JS中分为两种任务类型:macrotaskmicrotask,在ECMAScript中,microtask称为jobs,macrotask可称为task

它们的定义?区别?简单点可以按如下理解:

  • macrotask(又称之为宏任务),可以理解是每次执行栈执行的代码就是一个宏任务(包括每次从事件队列中获取一个事件回调并放到执行栈中执行)

    • 每一个task会从头到尾将这个任务执行完毕,不会执行其它
    • 浏览器为了能够使得JS内部task与DOM任务能够有序的执行,会在一个task执行结束后,在下一个 task 执行开始前,对页面进行重新渲染
(`task->渲染->task->...`)
  • microtask(又称为微任务),可以理解是在当前 task 执行结束后立即执行的任务

    • 也就是说,在当前task任务后,下一个task之前,在渲染之前
    • 所以它的响应速度相比setTimeout(setTimeout是task)会更快,因为无需等渲染
    • 也就是说,在某一个macrotask执行完后,就会将在它执行期间产生的所有microtask都执行完毕(在渲染前)

分别很么样的场景会形成macrotask和microtask呢?

  • macrotask:主代码块,setTimeout,setInterval等(可以看到,事件队列中的每一个事件都是一个macrotask)
  • microtask:Promise,process.nextTick等

__补充:在node环境下,process.nextTick的优先级高于Promise__,也就是可以简单理解为:在宏任务结束后会先执行微任务队列中的nextTickQueue部分,然后才会执行微任务中的Promise部分。

参考:https://segmentfault.com/q/1010000011914016

再根据线程来理解下:

  • macrotask中的事件都是放在一个事件队列中的,而这个队列由事件触发线程维护
  • microtask中的所有微任务都是添加到微任务队列(Job Queues)中,等待当前macrotask执行完毕后执行,而这个队列由JS引擎线程维护

(这点由自己理解+推测得出,因为它是在主线程下无缝执行的)

所以,总结下运行机制:

  • 执行一个宏任务(栈中没有就从事件队列中获取)
  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中
  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)
  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染
  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

如图:

另外,请注意下Promisepolyfill与官方版本的区别:

  • 官方版本中,是标准的microtask形式
  • polyfill,一般都是通过setTimeout模拟的,所以是macrotask形式
  • 请特别注意这两点区别

注意,有一些浏览器执行结果不一样(因为它们可能把microtask当成macrotask来执行了),
但是为了简单,这里不描述一些不标准的浏览器下的场景(但记住,有些浏览器可能并不标准)

20180126补充:使用MutationObserver实现microtask

MutationObserver可以用来实现microtask
(它属于microtask,优先级小于Promise,
一般是Promise不支持时才会这样做)

它是HTML5中的新特性,作用是:监听一个DOM变动,
当DOM对象树发生任何变动时,Mutation Observer会得到通知

像以前的Vue源码中就是利用它来模拟nextTick的,
具体原理是,创建一个TextNode并监听内容变化,
然后要nextTick的时候去改一下这个节点的文本内容,
如下:(Vue的源码,未修改)

var counter = 1
var observer = new MutationObserver(nextTickHandler)
var textNode = document.createTextNode(String(counter))

observer.observe(textNode, {
    characterData: true
})
timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
}

对应Vue源码链接

不过,现在的Vue(2.5+)的nextTick实现移除了MutationObserver的方式(据说是兼容性原因),
取而代之的是使用MessageChannel
(当然,默认情况仍然是Promise,不支持才兼容的)。

MessageChannel属于宏任务,优先级是:MessageChannel->setTimeout
所以Vue(2.5+)内部的nextTick与2.4及之前的实现是不一样的,需要注意下。

这里不展开,可以看下https://juejin.im/post/5a1af88f5188254a701ec230

写在最后的话

看到这里,不知道对JS的运行机制是不是更加理解了,从头到尾梳理,而不是就某一个碎片化知识应该是会更清晰的吧?

同时,也应该注意到了JS根本就没有想象的那么简单,前端的知识也是无穷无尽,层出不穷的概念、N多易忘的知识点、各式各样的框架、
底层原理方面也是可以无限的往下深挖,然后你就会发现,你知道的太少了。。。

另外,本文也打算先告一段落,其它的,如JS词法解析,可执行上下文以及VO等概念就不继续在本文中写了,后续可以考虑另开新的文章。

最后,喜欢的话,就请给个赞吧!

附录

博客

初次发布2018.01.21于我个人博客上面

http://www.dailichun.com/2018/01/21/js_singlethread_eventloop.html

招聘软广

阿里巴巴钉钉商业化团队大量hc,高薪股权。机会好,技术成长空间足,业务也有很大的发挥空间!

还在犹豫什么,来吧!!!

社招(P6~P7)

职责和挑战

  1. 负责钉钉工作台。工作台是帮助企业实现数字化管理和协同的门户,是拥有亿级用户量的产品。如何保障安全、稳定、性能和体验是对我们的一大挑战。
  2. 负责开放能力建设。针对纷繁的业务场景,提供合理的开放方案,既要做到深入用户场景理解并支撑业务发展,满足企业千人千面、千行千面的诉求,又要在技术上保障用户的安全、稳定和体验。需要既要有技术抽象能力、平台架构能力,又要有业务的理解和分析能力。
  3. 开放平台基础建设。保障链路的安全和稳定。同时对如何保障用户体验有持续精进的热情和追求。

职位要求

  1. 精通HTML5、CSS3、JS(ES5/ES6)等前端开发技术
  2. 掌握主流的JS库和开发框架,并深入理解其设计原理,例如React,Vue等
  3. 熟悉模块化、前端编译和构建工具,例如webpack、babel等
  4. (加分项)了解服务端或native移动应用开发,例如nodejs、Java等
  5. 对技术有强追求,有良好的沟通能力和团队协同能力,有优秀的分析问题和解决问题的能力。

前端实习

面向2021毕业的同学

  1. 本科及以上学历,计算机相关专业
  2. 熟练掌握HTML5/CSS3/Javascript等web前端技术
  3. 熟悉至少一种常用框架,例如React、vue等
  4. 关注新事物、新技术,有较强的学习能力,有强烈求知欲和进取心
  5. 有半年以上实际项目经验,大厂加分

image.png

image.png

内推邮箱

lichun.dlc@alibaba-inc.com

简历发我邮箱,必有回应,符合要求直接走内推!!!

一对一服务,有问必答!

也可加我微信了解更多:a546684355

参考资料

查看原文

yvonne 发布了文章 · 2020-12-31

聊聊React的Context

Context概念

一般用在跨组件通信中。

image.png

如上面的组件树中,A组件与B组件之间隔着非常多的组件,假如A组件希望传递给B组件一个属性,那么不得不使用props将属性从A组件历经一系列中间组件传递给B组件。
这样代码不仅非常的麻烦,更重要的是中间的组件可能压根就用不上这个属性,却要承担一个传递的职责,这是我们不希望看见的。
Context出现的目的就是为了解决这种场景,使得我们可以直接将属性从A组件传递给B组件。

旧Context(不推荐使用)

过时的API官方文档传送门

基本用法

假定,ABC三个组件,AC为祖孙关系,AB父子关系,现在使用context由A向C传递属性text

class A extends React.Component {
    getChildContext() {
        return { text: 'Hello孙子' }
    }
    render() {
        return (
            <div>组件A包含<B/></div>
        )
    }
}

A.childContextTypes = { text: PropTypes.string };

class B extends React.Component {
    render() {
        return (
            <div>组件B包含<C/></div>
        )
    }
}

class C extends React.Component {
    render() {
        const { text } = this.context;
        return (
            <div>我是C我接收到A传递的内容是{text}</div>
        )
    } 
}

C.contextTypes = { text: PropTypes.string }

留意上述代码中用以提供contextAPI,只要给上层组件添加getChildContextchildContextTypesReact将自动向下传递信息,子树上的所有组件可以通过定义 contextTypes 来访问 context

带来的问题

假如我们想要更新context,那么只能通过更新state或者props并将其值赋值给context

class A extends React.Component {
    constructor(props) {
        super(props);
        this.state = { text: 'Hello孙子' };
    }
    getChildContext() {
        return { text: this.state }
    }
    updateState = () => {
        this.state = { text: '我是你爷爷' };
    }
    render() {
        return (
            <div onClick={this.updateState} >组件A包含<B /></div>
        )
    }
}

A.childContextTypes = { text: PropTypes.string };

但是,如果A组件提供的一个context发生了变化,而中间父组件(B)的 shouldComponentUpdate 返回 false,那么实际上B和C都不会进行更新。使用了context的组件则完全失控,所以基本上没有办法能够可靠的更新context

新Context

官方文档传送门

还是上述例子,AC祖孙组件通信。使用createContext可以构建一个Provider用以给孙子组件传递props

const defaultVal = { text: 'Hello,孙子' };
const AContext = React.createContext(defaultVal);

class App extends React.Component {
    constructor(props) { 
        ...,
        this.state = { text: '这里是新的内容' }
    }
    render() {
        return (
            <AContext.Provider value={this.state}>
                <B />
            </AContext.Provider>
        )
    }
}

// 接收方式1-定义contextType
import { AContext } from './A';
class C extends React.Component {

    static contextType = AContext;
    
    render() {
        const { text } = this.context;
        return (
            <div>我是C我接收到A传递的内容是{text}</div>
        )
    } 
}

// 接收方式2-使用consumer
// consumer主要用以函数组件渲染
import { AContext } from './A';

class C extends React.Component {
    render() {
        return (
            <AContext.Consumer>
                ({ text }) => (
                    <p>我是C我接收到A传递的内容是{text}</p>
                )
            </AContext.Consumer>
        )
    } 
}

Redux中的Context

// 待续补坑
查看原文

赞 1 收藏 0 评论 0

yvonne 收藏了文章 · 2020-09-10

一篇文章带你过一遍 TypeScript

TypeScript 是 Javascript 的一个超集,提高了代码的可读性和可维护性。Typescript 官网提供的文档已经相当完善,但完整地看一遍需要一定的时间,本文试将 TypeScript 中要点提出,缩略为一文,用于浏览要点、快速复习。

1. 类型

1.1 原始类型定义

boolean/number/string/null/undefined

其中 null/undefined 是 TypeScript 中任何类型的子类型。

1.2 空值、任意值、枚举、Never

void/any/enum/never

void 指空值,若用于变量,则该变量只能赋值为 null/undefined;若用于函数,则该函数返回值为 空/null/undefined。

any 指任意值。TypeScript 中变量赋值后类型是无法改变的,但 any 类型的变量可以改变为任意值。(声明变量且无法类型推论时默认为 any 类型)

enum 指枚举类型,取值可以枚举出来。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

never 指不存在的值的类型,例如异常,函数无限循环无法返回等。

1.3 数组类型定义

TypeScript 中数组类型有多种定义方式,罗列如下:

1.类型 + 方括号

let list: number[] = [1, 2, 3];

2.数组泛型 Array<元素类型>

let list: Array<number> = [1, 2, 3];

3.元组 Tuple

表示一个已知元素数量和类型的数组

let x: [string, number] = ['1', 2]

4.接口定义类型

interface NumberArray {
  [index: number]: number;
}
let x: NumberArray = [1, 1, 2, 3, 5];

1.4 函数类型

TypeScript 中函数类型有多种定义方式,罗列如下:

1.函数声明中类型定义

function add(x: number, y: number): number {
  return x + y;
}

2.函数表达式中类型定义

let myAdd = function(x: number, y: number): number {
  return x + y;
}

3.箭头函数中类型定义

let myAdd = (x: number, y: number): number => {
  return x + y;
}

4.接口定义类型

interface Add {
  (x: number, y: number): number;
}
let myAdd: Add = function(num1, num2) {
  return num1 + num2;
}

1.5 对象类型

TypeScript 中对象类型有多种定义方式,罗列如下:

1.object

let obj: object = {test: 1};

2.接口定义类型

interface SquareConfig {
  color: string;
  width: number;
}
let newSquare: SquareConfig = {
  color: "white",
  width: 100
};

1.6 联合类型

联合类型表示值为多种类型中的一种,用 | 进行类型联合

1.7 泛型

泛型指在定义函数、接口、类时,不预先指定类型,在使用时再指定。泛型通过在函数、接口、类变量名后使用 <> 定义。(类型断言中 <> 位于变量名前)

function identity<T>(arg: T): T {
    return arg;
}

identity<string>('myString')

2. 类型操作

2.1 类型推论

在没有指定类型时,Typescript 会根据类型推论推断出类型。

// 推论类型为 number
let x = 1;

// 推论类型为 any
let y;

2.2 类型断言

类型断言指对变量指定一个比现在类型更明确的类型。

类型断言有两种形式。

1."尖括号"语法:

// 声明 someValue
let someValue: any = "this is a string";

// 对 someValue 类型断言,类型为 string,比原先 any 类型更明确
let strLength: number = (<string>someValue).length;

2."as" 语法(在 tsx 中只能使用 as 语法,因为 jsx 中 <> 会和"尖括号"语法混淆)

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

2.3 类型别名

类型别名不会新建类型,是通过新建名字来引用已有类型。通过 type 进行别名定义。

type Name = string;
let x: Name = '1';

3. 类和接口

3.1 类

类的概念是 ES6 中提出的,类的本质是构造函数的语法糖,通过 class 来创建。

TypeScript 中提供了 publicprivateprotected 三种修饰符,分别代表属性或方法是共有的、私有的、受保护的。

TypeScript 中 static 修饰符修饰属性或方法,代表属性或方法是静态的,即无需实例化,可以直接通过类调用。

TypeScript 中 readonly 修饰符修饰属性,代表属性只读,即初始化之后不可修改。

3.2 抽象类

抽象类指对类或类中部分方法进行抽象,作为其他类继承的基类,不能直接实例化。派生类必须实现抽象类中的抽象方法。

通过 abstract 关键字定义抽象类和抽象类内部定义的抽象方法,extends 来继承类。

abstract class Animal {
  // 必须在派生类中实现
  abstract makeSound(): void;
  // 无须在派生类中实现
  move(): void {
    console.log('roaming the earch...');
  }
}

class Dog extends Animal {
  makeSound (): void {
    console.log('barking');
  }
}

3.3 接口

接口和抽象类有些类似,接口是对属性和方法的抽象,不能直接实例化。接口和抽象类的区别如下:

  1. 接口是 100% 的抽象,不能含有具体的实现。抽象类可以包括具体实现
  2. 一个类只能继承一个类,但是可以实现多个接口。接口可以继承接口、类。

接口通过 interface 定义,implements 实现。

interface ClockInterface {
  alert(): void;
}

class Clock implements ClockInterface {
  alert() {
    console.log('Clock alert');
  }
}

4. 其他

4.1 内置对象

TypeScript 根据 JavaScript 提供了相关的内置对象,如 Date、Document、HTMLElement、Event、NodeList 等。具体可参考定义文件

4.2 声明文件

以 npm 包为例,将第三方包安装到 TypeScript 项目时,需要声明文件,声明该第三方包中导出内容的相关类型,这样 TypeScript 才能进行编译检查。声明文件以 .d.ts 结尾的文件,有以下3个来源:

1.@types
TypeScript 2.0 默认查看 ./node_modules/@types 文件夹,获取模块的类型定义。例如可以通过安装 npm install --save-dev @types/node 获取 node 类型相关声明。该开源项目 DefinitelyTyped 目前由社区进行维护。

2.第三方包已有声明文件
第三方包已有声明文件,则不需要再额外安装包,可以直接使用。通常通过 package.json 中的 types 字段,或者 index.d.ts 声明文件进行声明。

3.书写声明文件
当前面两种方法都无效时,可以在项目中书写声明文件,如创建 types 目录,用来管理声明文件。声明文件写法可以参考 DefinitelyTyped 中相关示例,如下为其中一个示例:

// 声明 createABigTriangle 方法
declare function createABigTriangle(gl: WebGLRenderingContext): void;

// 第三方库是 commonjs 规范的,导出声明使用 export= 这种语法
// 第三方库是 ES6 规范的,导出声明使用 export default 和 export 这种语法
export = createABigTriangle;

本文首发于个人博客:https://www.aquatalking.com/b...

(完)

查看原文

yvonne 收藏了文章 · 2020-05-26

前端面试中常遇到的算法题及考察点

【灵活应对前端面试中的JS算法题】

  • 实现一个函数clone,可以对JavaScript中的5种主要的数据类型(包括Number、String、Object、Array、Boolean)进行值复制
    function clone(obj){
        var result;
        switch(typeof obj){
            case 'undefined':
            break;
            case 'string':
            result = obj+'';
            break;
            case 'number':
            result = obj-0;
            break;
            case 'boolean':
            result =obj;
            break;
            case 'object':
                if(obj ===null){
                    result = null;
                } else {
                    if(Object.prototype.toString.call(obj).slice(8,-1)==='Array'){
                        result=[];
                        for(var i=0;i<obj.length;i++){
                            result.push(clone(obj[i]));
                        }
                    }else{
                        result=[];
                        for(var k in obj){
                            result[k]=clone(obj[k]);
                            }
                        }
                };
                break;
             default:
                 result = obj;
                 break;
            
        }
        return result
    }
  • 判断一个单词是否是回文

回文是指把相同的词汇或句子,在下文中调换位置或颠倒过来,产生首尾回环的情趣,叫做回文,也叫回环。

很多人拿到这样的题目非常容易想到用for将字符串颠倒字母顺序然后匹配就行了。其实重要的考察的是对于reverse的实现。其实我们可以利用现成的函数,将字符串转换成数组,这个思路很重要,我们可以拥有更多的自由度去进行字符串的一些操作。
let reverseStr = function(str) {  
    return str = str.split('').reverse().join('');
}
reverseStr('abcdefg');
//gfedcba
  • 在句子中反转词

如fix this one 变为 one this fix。重点是检测到空格时进行处理。

    function reverseWord(str){
        return str.split(' ').reverse().join(' ')
    }
  • 反转每个单词中字符的顺序

“I am the good boy” 反转成这样 “I ma eht doog yob”

    function reverse(str){
        return str.split(' ').reverse().join(' ').split('').reverse().join('');
    }
  • 去掉一组整型数组重复的值

    题目如下输入: [3,13,24,11,11,14,1,2]
    输出: [3,13,24,11,14,2]
    需要去掉重复的11 和 1 这两个元素。

这道题有多重方法,我理解的主要是考察个人对Object的使用,利用key来进行筛选。
    function unique(arr) {  
        let hashTable = {};
        let data = [];
        for(let i=0,l=arr.length;i<l;i++) {
           if(!hashTable[arr[i]]) {
             hashTable[arr[i]] = true;
             data.push(arr[i]);
            }
         }
         return data
    }
    unique([3,13,24,11,11,14,1,2])
    //[3,13,24,11,14,2]

再来一个其他实现方式,这个方法常在我的项目中出现,用的时候确实觉得代码少了那么几行

     function unique(arr) {  
        let data = [];
        for(let i=0,l=arr.length;i<l;i++) {
           if(data.indexOf(arr[i])==-1) {
             data.push(arr[i]);
            }
         }
         return data
    }
    unique([3,13,24,11,11,14,1,2])
    //[3,13,24,11,14,2]
  • 统计一个字符串出现最多的字母

输入一段英文连续的英文字符串 afjghdfraaaasdenas,找出重复出现次数最多的字母

    function findMaxChar(str) {  
          if(str.length == 1) {
            return str;
          }
          let charObj = {};
          for(let i=0;i<str.length;i++) {
            if(!charObj[str.charAt(i)]) {
              charObj[str.charAt(i)] = 1;
            }else{
              charObj[str.charAt(i)] += 1;
            }
          }
          return compare(charObj);
     };
     function compare(charObj){
         let maxChar = '',
         maxValue = 1;
         for(var k in charObj) {
            if(charObj[k] >= maxValue) {
              maxChar = k;
              maxValue = charObj[k];
            }
         }
         return maxChar;
     }
     findMaxChar('afjghdfraaaasdenas')
    //a
  • 找到字符串中的第一个非重复的字符

遍历字符串,用一个对象当做hash表来存储重复字符。

    function firstNonRepeatChar(str){
        var count = {};
        for(var i=0;i<str.length;i++){
            if(count[str[i]]){
                count[str[i]]++;
            }else{
                count[str[i]] = 1;
            }   
        }
    
        for(var prop in count){
            if(count[prop] === 1){
                return prop;
            }
        }
    
    }
  • 删除字符串中重复的字符

其实是在上一个问题的基础上再进行操作:

    function firstNonRepeatChar(str){
        var count = {};
        var result = [];
        for(var i=0;i<str.length;i++){
            if(count[str[i]]){
                count[str[i]]++;
            }else{
                count[str[i]] = 1;
            }   
        }
    
        for(var prop in count){
            if(count[prop] === 1){
                result.push(prop);
            }
        }
    
        return result.join('');
    }
  • 在n和m之间生成随机整数
    Math.floor(Math.random()*(m-n))+n
  • 排序算法(冒泡排序)

冒泡排序JavaScript代码实现:

    function bubbleSort(arr) {
        var len = arr.length;
        for (var i = 0; i < len; i++) {
            for (var j = 0; j < len - 1 - i; j++) {
                if (arr[j] > arr[j+1]) {        //相邻元素两两对比
                    var temp = arr[j+1];        //元素交换
                    arr[j+1] = arr[j];
                    arr[j] = temp;
                }
            }
        }
        return arr;
    }
    bubbleSort([3,5,2,8,9,7,6])
    //[2, 3, 5, 6, 7, 8, 9]
  • 排序算法(选择排序)
在时间复杂度上表现最稳定的排序算法之一,因为无论什么数据进去都是O(n²)的时间复杂度。。。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。
    function selectionSort(arr) {
        var len = arr.length;
        var minIndex, temp;
        for (var i = 0; i < len - 1; i++) {
            minIndex = i;
            for (var j = i + 1; j < len; j++) {
                if (arr[j] < arr[minIndex]) {     //寻找最小的数
                    minIndex = j;                 //将最小数的索引保存
                }
            }
            temp = arr[i];
            arr[i] = arr[minIndex];
            arr[minIndex] = temp;
        }
        return arr;
    }
     selectionSort([3,5,2,8,9,7,6])
    //[2, 3, 5, 6, 7, 8, 9]
  • 从未排序的整数数组中找出缺失的数字

比如你有1到100的整数,而其中缺了一个,怎么找出这个数字?利用等差数列公式计算这些数应得的和,再计算当前数组所有数字的和,二者的差即为缺少的数。

    function missingNumber(arr){
        var n = arr.length+1;
        var expectedSum = (1+n)*n/2;
        var sum = 0;
        for(var i=0;i<arr.length;i++){
            sum+=arr[i];
        }    
        return expectedSum - sum;
    }
  • 检查是否有任何两个数字的和是给定的数字

最暴力的方法就是两层循环,是O(n2).改进方法使用一个对象作为哈希表,用于存储数,这样在每次搜寻是否有另一个数与当前数的和为sum时就可以在O(1)的时间内找到。

    function twoSum(arr,sum){
        var obj = {};
        var num;
        for(var i=0;i<arr.length;i++){
            num = sum - arr[i];
            if(obj[num]){
                return true;
            }else{
            //若没有的话为当前数字建立索引
                obj[arr[i]] = true;
            }
        }
        return false;
    }
  • 检查是否有任何两个数字的和是给定的数字,有的话将数字和位置以对象的方式返回值,否则返回false

最暴力的方法就是两层循环,是O(n2).改进方法使用一个对象作为哈希表,用于存储数,这样在每次搜寻是否有另一个数与当前数的和为sum时就可以在O(1)的时间内找到。

    function twoSum(arr,sum){
        var obj = {};
        var num;
        for(var i=0;i<arr.length;i++){
            num = sum - arr[i];
            obj[num]=arr.length+1;
            if(obj[num]){
                obj[arr[i]]=i;
                obj[num]=arr.indexOf(num);
                return obj;
            }else{
            //若没有的话为当前数字建立索引
                obj[arr[i]] = i;
            }
        }
        return false;
    }
  • 找到任意两个数字的最大和

找到两个最大的数并返回它们的和。

    function topSum(arr){
        if(arr.length<2) return null;
        var first,second;
        if(arr[0]>arr[1]){
            first = arr[0];
            second=arr[1];
        }else{
            first = arr[1];
            second=arr[0];
        }
    
        for(var i=2;i<arr.length;i++){
            if(arr[i]>first){
                second = first;
                first = arr[i];
            }else if(arr[i]>second){
                second = arr[i];
            }
        }
    
        return first+second;
    }
  • 从1到n中0的总个数
n=50的话,有5个0,分别是10,20,30,40,50。
n = 120的话,分别是10到90,共九个,110到120,共2个,以及100的两个,一共13个。
也就是说10的整数次方会有多个零,如100,1000,那么就要利用现有的数计算包含了多少个10的平方数。
如2014,2014/10=201; 201/10 = 20; 20/10 = 2; 最后表明出现了两次10的三次方,即1000和2000。
    function countZero(n){
        var count = 0;
        while(n>0){
            count+=Math.floor(n/10);
            n/=10;
        }
        return count;
    }
  • 匹配字符串的子字符串
    function substr(str,subStr){
        for(var i=0;i<str.length;i++){
            for(var j=0;j<subStr.length;j++){
                if(str[i+j] != subStr[j]){
                    break;
                }
            }
            if(j == subStr.length){
                return i;
            }
        }
        return -1;
    
    }
  • 字符串的全排列
    var result = [];
    function permutations(str){
        var arr= str.split('');
    
        helper(arr,0,[]);
        return result;
    }
    function helper(arr,index,list){
        if(index === arr.length){
            result.push(list.join(''));
            return;
        }
        for(var i = 0;i<arr.length;i++){
            if(list.indexOf(arr[i]) != -1){
                continue;
            }
            list.push(arr[i]);
            helper(arr,index+1,list)
            list.pop();
        }
    }
  • 不借助临时变量,进行两个整数的交换
把 a = 2, b = 4 变成 a = 4, b =2
这种问题非常巧妙,需要大家跳出惯有的思维,利用 a , b进行置换
主要是利用 + – 去进行运算,类似 a = a + ( b – a) 实际上等同于最后 的 a = b;
    function swap(a , b) {  
      b = b - a;
      a = a + b;
      b = a - b;
      console.log('a='+a);
      console.log('b='+b)
    }
    var a=2,b=4;
    swap(a,b)
    //a=4;b=2
  • 斐波那契数列(黄金分割数列)不说换金分割我也不知道是啥玩意儿啦

斐波那契数列,又称黄金分割数列,指的是这样一个数列:0、1、1、2、3、5、8、13、21、34、……在数学上,斐波纳契数列主要考察递归的调用。

    function getFibonacci(n) {  
      var fibarr = [];
      var i = 0;
      while(i<n) {
        if(i<=1) {
          fibarr.push(i);
        }else{
          fibarr.push(fibarr[i-1] + fibarr[i-2])
        }
        i++;
      }
      return fibarr;
    }
    getFibonacci(9); //拿到9个
    //[0、1、1、2、3、5、8、13、21]
  • 获得第n个斐波那契数字

解法一:迭代

var fibonacci = function(n){
    var fibo = [0,1];
    for(var i=2;i<=n;i++){
        fibo[i] = fibo[i-1]+fibo[i-2];
    }
    return fibo[n];
}

解法二:递归

    var fibonacci = function(n){
        if(n>=2){
            return fibonacci(n-1)+fibonacci(n-2);
        }else{
            return n;
        }
    }
  • 找到两个数的最大公约数

解法一:遍历

    var greatestCommonDivisor= function(a,b){
        if(a<2 || b<2) return 1;
        var divider = 2;
        var greatestDivisor = 1;
        while(divider<=a && divider<=b){
            if(a%divider == 0 && b%divider == 0){
                greatestDivisor = divider;
            }
            divider++;
        }
        return greatestDivisor;
    }

解法二:辗转相除法
又名欧几里德算法(Euclidean algorithm)乃求两个正整数之最大公因子的算法。它是已知最古老的算法……
有解释的,但我选择不去理解……

    function greatestCommonDivisor(a, b){
       if(b == 0)
         return a;
       else 
         return greatestCommonDivisor(b, a%b);
    }
  • 合并两个排序数组
var mergeSortedArray = function(a,b){
    var merge = [];
    var i = 0,j = 0;
    var k = 0;
    while(i<a.length || j<b.length){
        if(i == a.length || (j!=b.length && a[i]>b[j])){
            merge[k++] = b[j++];
        }else{
            merge[k++] = a[i++];
        }
    }
    return merge; 
}
  • 验证一个数是否是质数

质数只能被1和它自己整除,因此令被除数从2开始,若能整除则不是质数,若不能整除则加一,直到被除数到达根号n,此时n则是质数。

    function isPrime(n){
        var divider = 2;
        var limit = Math.sqrt(n);
        while(divider<=limit){
            if(n%divider == 0){
                return false;
            }
            divider++;
        }
        return true;
    }
    isPrime(3);
    //true
  • 查找数字的所有质数因子

divider从2开始,如果n能整除divider,则将divider加入到结果中,n为此次计算后的商,如果n不能整除divider,则divider++

var primeFactors = function(n){
    var factors = [];
    var divider = 2;
    while(n>2){
        if(n%divider == 0){
            factors.push(divider);
            n /= divider;
        }else{
            divider++;
        }
    }
    return factors;
}
  • 找出正数组的最大差值比

这是通过一道题目去测试对于基本的数组的最大值和最小值的查找

     function getMaxProfit(arr) {
        var minPrice = arr[0];
        var maxProfit = 0;
        for (var i = 0; i < arr.length; i++) {
            var currentPrice = arr[i];
            minPrice = Math.min(minPrice, currentPrice);
            var potentialProfit = currentPrice - minPrice;
            maxProfit = Math.max(maxProfit, potentialProfit);
        }
        return maxProfit;
    }
    getMaxProfit([10,5,11,7,8,9])
    //6
  • 随机生成指定长度的字符串
    function randomString(n) {  
      let str = 'abcdefghijklmnopqrstuvwxyz9876543210';
      let tmp = '',
          i = 0,
          l = str.length;
      for (i = 0; i < n; i++) {
        tmp += str.charAt(Math.floor(Math.random() * l));
      }
      return tmp;
    }
    randomString(9);  //指定长度为9
    //4ldkfg9j7
  • 实现类似getElementsByClassName 的功能

自己实现一个函数,查找某个DOM节点下面的包含某个class的所有DOM节点?不允许使用原生提供的 getElementsByClassName querySelectorAll 等原生提供DOM查找函数。

    function queryClassName(node, name) {  
      var starts = '(^|[ \n\r\t\f])',
           ends = '([ \n\r\t\f]|$)';
      var array = [],
            regex = new RegExp(starts + name + ends),
            elements = node.getElementsByTagName("*"),
            length = elements.length,
            i = 0,
            element;
     
        while (i < length) {
            element = elements[i];
            if (regex.test(element.className)) {
                array.push(element);
            }
     
            i += 1;
        }
     
        return array;
    }
    queryClassName()
  • 使用JS 实现二叉查找树

一般叫全部写完的概率比较少,但是重点考察你对它的理解和一些基本特点的实现。 二叉查找树,也称二叉搜索树、有序二叉树(英语:ordered binary tree)是指一棵空树或者具有下列性质的二叉树:

任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值; 任意节点的左、右子树也分别为二叉查找树;
没有键值相等的节点。二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为O(log
n)。二叉查找树是基础性数据结构,用于构建更为抽象的数据结构,如集合、multiset、关联数组等。
    class Node {  
      constructor(data, left, right) {
        this.data = data;
        this.left = left;
        this.right = right;
      }
    }
    
    class BinarySearchTree {
      constructor() {
        this.root = null;
      }
      insert(data) {
        let n = new Node(data, null, null);
        if (!this.root) {
          return this.root = n;
        }
        let currentNode = this.root;
        let parent = null;
        while (1) {
          parent = currentNode;
          if (data < currentNode.data) {
            currentNode = currentNode.left;
            if (currentNode === null) {
              parent.left = n;
              break;
            }
          } else {
            currentNode = currentNode.right;
            if (currentNode === null) {
              parent.right = n;
              break;
            }
          }
        }
      }
     
      remove(data) {
        this.root = this.removeNode(this.root, data)
      }
     
      removeNode(node, data) {
        if (node == null) {
          return null;
        }
        if (data == node.data) {
          // no children node
          if (node.left == null && node.right == null) {
            return null;
          }
          if (node.left == null) {
            return node.right;
          }
          if (node.right == null) {
            return node.left;
          }
          let getSmallest = function(node) {
            if(node.left === null && node.right == null) {
              return node;
            }
            if(node.left != null) {
              return node.left;
            }
            if(node.right !== null) {
              return getSmallest(node.right);
            }
          }
          let temNode = getSmallest(node.right);
          node.data = temNode.data;
          node.right = this.removeNode(temNode.right,temNode.data);
          return node;
        } else if (data < node.data) {
          node.left = this.removeNode(node.left,data);
          return node;
        } else {
          node.right = this.removeNode(node.right,data);
          return node;
        }
      }
     
      find(data) {
        var current = this.root;
        while (current != null) {
          if (data == current.data) {
            break;
          }
          if (data < current.data) {
            current = current.left;
          } else {
            current = current.right
          }
        }
        return current.data;
      }
     
    }
     
    module.exports = BinarySearchTree;
  • 统计数组中每个元素及出现的次数,并输出到页面
  function getArrayMess(arr) {  
          if(arr.length == 1) {
            console.log("{"+arr[0]+":1")
          }
          let charObj = {};
          for(let i=0;i<arr.length;i++) {
            if(!charObj[arr[i]]) {
              charObj[arr[i]] = 1;
            }else{
              charObj[arr[i]] += 1;
            }
          }
            console.log(charObj)
}
     
  
查看原文

yvonne 发布了文章 · 2020-05-11

从发布订阅到双向数据绑定

前言

双向数据绑定已经是一个谈烂的话题,若谈及原理,想必大家都能提到数据劫持defineProperty。但是,对于如何完整地实现一个双向数据绑定伪代码,我想大概很多人都没有去深究。于是,本文借着梳理发布订阅模式由浅到深地实现一下双向数据绑定。

发布订阅模式

双向数据绑定的底层设计模式为:发布订阅模式。
  • 发布订阅模式也称为观察者模式
  • 观察者同时订阅一个对象,当对象发生改变时,每一个观察者都能接收到通知

通俗举例

举一个最通俗的?:小红、小明、小白同时关注拼多多的AJ1,当AJ1一降价,三个人都能接到通知。

class Pinduoduo {
  constructor() {
    // 订阅者
    this.subscribers = [];
  }
  // 订阅方法
  subscribe({name, callback}) {
    if (~this.subscribers.indexOf(name)) return;
    this.subscribers.push({
      name, callback
    });
  }
  // 发布降价消息
  publish() {
    this.subscribers.forEach(({name, callback}) => {
      let prize = 666;
      if (name === '小明') prize = 100;
      callback && callback(name, prize);
    })
  }
}


const pinInstance = new Pinduoduo();
const commonFn = (name, prize) => {
  console.log(`${name}接收到了降价信息,AJ1现在的价格是${prize}`)
}


// 订阅
pinInstance.subscribe({
  name: '小红',
  callback: commonFn
});
pinInstance.subscribe({
  name: '小明',
  callback: commonFn
});
pinInstance.subscribe({
  name: '小白',
  callback: commonFn
});


// 发布
pinInstance.publish();
// 输出
// 小红接收到了降价信息,AJ1现在的价格是666
// 小明接收到了降价信息,AJ1现在的价格是100
// 小白接收到了降价信息,AJ1现在的价格是666

所以——记住实现发布订阅模式的两个要点:
发布(触发) & 订阅(监听)

EventEmitter

借此我们还可以实现一下EventEmitter的伪代码。

function EventEmitter() {
    this.events = Object.create(null);
}

// 实现监听方法
EventEmitter.prototype.on = (type, event) => {
    if (!this.events) this.events = Object.create(null);
    if (!this.events[type]) this.events[type] = [];
    this.events[type].push(event);
}

// 实现触发方法
EventEmitter.prototype.emit = (type, ...args) => {
    if (!this.events[type]) return;
    this.events[type].forEach(event => {
        event.call(this, ...args);
    })
}

// 执行
function Girl() {}
// 实现继承
Girl.prototype = Object.create(EventEmitter.prototype);
const lisa = new Girl();
lisa.on('逛街', () => {
    console.log('买买买!');
});

lisa.emit('逛街');

// console: 买买买!

深入浅出双向数据绑定

下述例子dom结构基于如下代码

<div id="app">
    <input type="text" v-model="data">
    <p v-text="data"></p>
</div>

最最简单的实现

不注释了,相信大家都能看懂。

const inputDom = document.getElementsByTagName('input')[0];
const textDom = document.getElementsByTagName('p')[0];
inputDom.addEventListener('input', e => {
    const val = e.target.value;
    textDom.innerText = val;
});

defineProperty实现

1、极简版

const vm = {
    data: ''
};
const inputDom = document.getElementsByTagName('input')[0];
const textDom = document.getElementsByTagName('p')[0];

Object.defineProperty(vm, 'data', {
    set(newVal) {
        if (vm['data'] === newVal) return;
        // 同时触发视图更新
        textDom.innerText = newVal;
    }
});


inputDom.addEventListener('input', e => {
    vm.data = e.target.value;
});

2、进阶版
假如我们更换个属性,或添加v-model上述代码就不能复用了。我们迭代一下,可以适应多个v-model的情况。

首先梳理一下需要做什么

  • 添加对象劫持器 Object.defineProperty
  • 添加订阅者管理中心,新增订阅者和通知订阅者更新
  • 遍历Dom Treev-modelv-text进行解析。对v-model进行事件绑定监听变化,对v-text添加订阅者,订阅vm变化实现视图更新。
/*
 * 定义对象监听
*/
const vm = {
    data: ''
};

function observe(obj) {
    Object.keys(obj).forEach(key => {
        let val = obj[key];
        Object.defineProperty(obj, key, {
            get() {
                return val;
            },
            set(newVal) {
                if (newVal === val) return;
                // 更新vm中的数据
                val = newVal;
            }
        })
    })
}
/*
 * 加入发布订阅模式
*/

const Dep = {
    target: null,
    subs: [],
    addSubs(sub) {
        this.subs.push(sub)
    },
    notify() {
        this.subs.forEach(sub => {
            sub.update();
        });
    }
}

在getter中,添加watcher
在setter观测到数据变化时,触发所有【订阅者】更新

// ...
get() {
    // 此时的target已经赋值成当前的watcher实例
    if (Dep.target) Dep.addSubs(Dep.target);
    return val;
},
set(newVal) {
    if (newVal === val) return;
    // 更新vm中的数据
    val = newVal;
    Dep.notify();
}
// ...

接下来定义【订阅者】watcher,在本例中可以理解成每一个node节点

function Watcher(node, vm, name) {
    Dep.target = this;
    this.node = node;
    this.vm = vm;
    // name是绑定数据的key
    this.name = name;
    // 将watcher添加进dep中
    this.update();
    Dep.target = null;
}

// Watcher包含update方法和get方法
Watcher.prototype = {
    update() {
        this.get();
        this.node.innerText = this.value;
    },
    // 这里主要是为了触发getter中Dep.addSub
    get() {
        this.value = this.vm[this.name];
    }
}

然后是对相应节点进行解析处理

function complie(node, vm) {
    if (node.nodeType === 1) {
        [...node.attributes].forEach(attr => {
            const name = attr.nodeValue;
            if (attr.nodeName === 'v-model') {
                node.addEventListener('input', e => {
                    vm[name] = e.target.value;
                })
            } else if (attr.nodeName === 'v-text') {
                new Watcher(node, vm, name)
            }
        })
    }
}

现在可以对每个节点进行绑定处理了

function MVVM(id, vm) {
    observe(vm);
    const node = document.getElementById(id);
    // 用fragment缓存节点,节约性能开支
    const fragment = document.createDocumentFragment();
    let child;
    while(child = node.firstChild) {
        fragment.appendChild(child)
    }
}

调用

MVVM(vm);

整个代码初看起来会比较绕,但只要理解observecomplieDepWacther这几个概念,相信就能基本看懂MVVM了。


(未完待续...待更新Proxy的MVVM实现方法)

查看原文

赞 1 收藏 1 评论 0

yvonne 发布了文章 · 2020-04-07

【目标检测从放弃到入门】一篇文章带你入门前端视觉编译技术

前言

在前端领域,目前不断地有 design2code 工具涌现。即给定视觉稿识别出里面的元素并将其转换成代码。它们底层技术都离不开深度学习中的「目标检测」。最近有幸接触到这块的内容,实践下来发现深度学习也并不那么的高深莫测,这里用一篇文章带大家快速入门目标检测技术。并提供一个开箱即用的目标检测框架。

仓库传送门

「【划重点!!】目标检测通俗地来说就是识别出给定视图中的物体,并将其定位。」

准备

团队这边的目标检测是基于tensorflow提供的object detection API。整个训练的过程可以简要概括为训练集的准备和训练。

训练集准备:

  1. 人工标注图片,并转成xml格式
  2. xml转成tf能识别的数据格式即tfrecord

训练过程:

  1. 配置训练参数,这里需要配置

    • 目标检测算法类型
    • 目标类别数量
    • 训练步长
    • 训练部署训练集路径
    • 模型输出路径
    • *以及可以基于前人训练好的模型微调
  2. 基于slim模块实现模型训练

过程

环境配置

我们需要准备好python的环境。系统内置的python一般为2.7.x,而训练需要的版本为3+。这里推荐使用pyenv进行python的版本控制与切换。

$ brew install pyenv
$ brew install pyenv-virtualenv

$ pyenv virtualenv 3.6.5 <your-project-name>
$ pyenv activate <your-project-name>

环境配置完成后,就可以着手开始我们的训练了。

训练集准备

首先我们需要准备大量的训练集,可以针对自己的需求手动标注。我们用的是labelImg这个python工具。

(PS:如果只是想体验下目标检测过程,可以从网上下载已经标注好的训练集和测试集。目标检测训练数据集资料汇总

本文的测试范例用的是扑克牌训练集,来源于github上的大佬 >>> 仓库地址。)

labelImg安装使用具体参考文档。简要步骤如下:

# 基于上步切换到python3环境
# 安装
$ pip install labelImg
# 启动
$ labelImg

手动框选出目标,并储存为xml文件。

最后会得到image和对应的xml(annotations)两个目录。(建议分目录存储)

image.png

训练配置

tf object detection API已经为我们准备了部分已经训练好的模型。我们可以对基于它们做fine-tuning(微调)从而来训练我们的模型

Tensorflow detection model zoo

这里包含了SSD/Faster RCNN/RCNN等目标检测常用算法,比较遗憾的是没有Yolo的。如果有一定深度学习基础的同学可以基于tf训练自己的Yolo模型。

先看一下目录的基本结构
image.png

  • resources:文件转换、训练、模型导出目标代码
  • config:配置文件,包括label_map 和 算法配置文件
  • model:fine_tuning模型
  • object_detection:tf object detection api
  • slim:tf slim目标代码
  • train_assets:训练集
  • train_data:训练过程中输出的文件

1、将xml转成tfrecord
在使用labelImg标注后我们会得到image和对应xml两份文件,要进行训练还需要将其转换成tf能识别的tfrecord格式文件。

tf object detection已经提供了相应的文件转换API

tf/models中目标代码路径 >>> object_detection/dataset_tools/create_pascal_tf_record.py

上面提供的框架中已经将其处理好了

# 根目录下调用
$ python resources/convert.py

这时候就能得到train.record文件。
image.png

2、将slim路径添加到PATH中
这一步框架已经处理了,可以忽略,了解即可。

$ export PYTHONPATH=$PYTHONPATH:'pwd':'pwd'/slim

3、模型选用
模型指的是用以fine-tuning的由前人训练好的模型。可以在上面的model zoo选择下载。这里采用的是SSD。上图model目录。

4、文件配置
训练前我们需要配置检测目标的类别,在config/label_map中定义

# config/label_map.pbtxt

item {
  id: 1
  name: 'nine'
}

item {
  id: 2
  name: 'ten'
}

...more

同时需要配置算法文件(下载fine_tuning模型后对其xxx.config文件进行参数调整)重点配置如下

model {
  ssd {
    num_classes: 6 #用以目标检测的类别数量
    box_coder {...}
    fine_tune_checkpoint: "" #本地fine_tuning模型路径
    num_steps: 10000 #训练步长
 }
 train_input_reader: {
   tf_record_input_reader {
     input_path: "" #tfrecord文件路径
   }
   label_map_path: "" #本地label_map路径
}

完成配置后,在根目录中调用以下命令

$ python resources/train.py

我们这边使用的训练apitf/model/object_detection/legacy/train.pytf/model/object_detection/model_main也可以提供相应的功能,但是在使用cpu训练下会报错,于是换成了legacy/train

如果看到以下输出就说明已经成功进行模型训练了。
这里的step指的是当前训练的步数,loss指当前函数损失值,loss越小,说明模型越接近准确。

image.png

同时可以使用tf提供的命令行工具tensorboard实时查看训练结果。

$ tensorboard --logdir=train_data/ssd

这里可以看到,losses波动特别大,显然这个模型训练得不是特别好。这里等后面模型调优我们再来讲。

image.png

模型导出

如果看到如下输出,说明模型已经训练完成。
image.png

我们此时需要对训练完成的数据进行模型导出。tf object detection也提供了相应的api,具体文件路径是tf/model/object_detection/export_inference_graph.py
使用提供的框架可以直接调用

$ python resources/export.py

正常的话会得到如下结构的文件,其中frozn_inference_graph.pb就是训练所得的模型了。

image.png

预测

在得到了训练好的模型之后,我们就能用它对我们的视图进行目标检测了。

预测这边tf提供的目标代码是tf/model/object_detection/object_detection_tutorial.py
可以自行参考,根据自己需求改写。

也可以在框架代码根目录下执行

$ python resources/detection.py

完成后会在train_detections/ssd目录下生成被标记好的预测图。

image.png

评估

单看上图,我们的预测结果还是准确的,那对于目标物体模糊或者被遮挡的情况呢?我们单纯跑一张图预测的话其实还不足以评估模型的准确性,所以tf也提供了相应模型评估的函数。

具体可以参考tf/model/object_detection/legacy/eval

也可以在框架代码valid_assets/下配置测试集,包括images和对应的xml。完成之后需要:

  • xml转成tfrecord
  • 配置算法文件xxx.config
eval_config: {
  num_examples: 67 # 测试集数量
  # Note: The below line limits the evaluation process to 10 evaluations.
  # Remove the below line to evaluate indefinitely.
  max_evals: 1 # 评估次数,1次即可
}

eval_input_reader: {
  tf_record_input_reader {
    input_path: "" # valid tfrecord文件路径
  }
  label_map_path: "" # label_map路径
  shuffle: false
  num_readers: 1
}

配置完成后在根目录下依次执行

$ python resources/convert.py valid
$ python resources/eval.py

控制台会输出该模型下每个类别的检测准确率。

image.png

微调

如果训练出的模型准确率不是很高,我们需要对配置进行微调。这里主要可以通过改变步长、算法、batch_size等方式。

算法
目标检测的明星算法是SSD/Faster RCNN/YOLO,有关这3个算法的区别可以参考一下这篇文章

【目标检测从放弃到入门】SSD / RCNN / YOLO通俗讲解

步长
步长指训练迭代的总长度,可以通过tensorboard实时观测。步长设置过大可能会带来过拟合的问题,设置不够则会是欠拟合。

batch_size
batch_size指一次迭代使用的样本数量。如果训练集足够小,可以使用全数据集。而对于大数据集,可以使用一个适中的batch_size或者走向另一个极端就是每次只训练一个样本,即 batch_Size = 1。

对于ssd算法来说,使用小的batch_size貌似会造成losses波动大的情况而迟迟达不到一个稳定的losses值。

其他还待深入实践。后续再发一篇文章更新吧~~~

查看原文

赞 3 收藏 0 评论 0

yvonne 发布了文章 · 2020-04-07

【目标检测从放弃到入门】SSD / RCNN / YOLO通俗讲解

最近有幸在项目上接触到了深度学习中的目标检测领域,通过数周来的实践和相关知识的查阅,现在也算是在能在目标检测入门处徘徊了。这篇文章会在三大目标检测经典算法原理上加上自己的理解,希望大家看了此文能不再一脸懵逼地出去。

目标检测

什么是目标检测

一句话概括即:在视图中检测出物体的【类别】和【位置】。

目标检测的三大算法

上面我们已经说了,目标检测就是分类和定位(回归,注:回归即得到确定值)的问题。分类问题我们可以输入图片至分类器处理,而定位问题需要将物体框选出来,那么怎么确定选框呢?这里我们可能需要一堆的「侯选框」。

看到这里我们大概确定了目标检测问题需要两步走:

  1. 侯选框选取;
  2. 将侯选框中的物体分类;(后面需要剔除得分低的框)

这样就得到了【两步走(Two-Stage)】的算法原型,代表是【RCNN】,之后的【Fast-RCNN】、【Faster-RCNN】都是基于它迭代的。

那能不能在侯选框选取的同时预测分类呢?也是可以的,这样就有了【SSD】和【YOLO】(One-Stage)。

下面我们来「稍微」详细地说一下每个算法具体的检测步骤。(非科班同学,有错误还望指正~)

RCNN及其迭代物(2-stage)

1、主要思想

所谓RCNN就是【Region】+【CNN】。完美地对应到了上述说的选框+分类。

关于CNN是什么,如果想要深入的话大家可以看看这篇文章:一文看懂卷积神经网络。这里也简要概括下,CNN即对输入图片通过【降维】(卷积、激活、池化)等操作得到【图片的特征向量】作为【全连接层(就是多个神经网络层合并起来)】的输入,最后输出结果。

可以看下图,[x1, x2...xn]就可以看作是降维后的特征向量,然后通过神经网络处理得到输出的结果。

Region就是我们上面提到的侯选框,RCNN对于候选区域的确定是通过窗口扫描的方式。玩个经典游戏,在鸟中找到乌。我们可能会用眼睛逐行逐字扫描,这里我们可以将每个字看成一个格子,然后判断格子里面的字是不是乌。这个过程其实就是对RCNN的粗糙理解了。前期的选区选择主要采用的是【滑动窗口】技术,就是在视图起始点中确定一个选框,然后每次将选框移动特定的距离,再对窗口中的物体进行检测。

2、RCNN

看到上面,相信大家已经觉察到滑动窗口技术一个明显的弊端,我们不好确定初始窗口是否恰好能包裹到需要检测的物体,这就需要对初始窗口尺寸做非常多次的尝试,如果待检测物体数量较多,待检测窗口的个数更是成倍的增加,而且每次尝试都包含一次完整的CNN运算过程,这显然是非常耗时的。

所以RCNN在侯选框的选择上由滑动窗口技术改成了SS(selective search选择性搜索)技术。意思是通过纹理、颜色将视图划分开,再从中确定大约2000个区域作为候选区域,然后对其进行分类处理。

3、Fast-RCNN

Fast-RCNN的改进点在于:

  1. 首先将图片输入到CNN得到特征图
  2. 划分出特征图中的SS区域(这个在原图中得到)得到特征框,在ROI池化层中将每个特征框池化到统一大小
  3. 最后将特征框输入全连接层进行分类和回归
ROI池化层是将不同尺寸的特征框转化层相同尺寸的特征向量,以便进行后续的分类和输出回归框操作。它可以加速处理速度。

4、Faster-RCNN

Faster—RCNN步骤与Fast-RCNN类似。比较大的突破是:将侯选框提取技术由SS改为RPN
RPN的简要原理是:

  1. 为输入特征图的每个像素点生成9个侯选框,如下图红框处;

  1. 对生成的基础侯选框做修正处理,就是删除不包含目标的候选框;
  2. 对超出图像边界的侯选框做裁剪处理
  3. 忽略掉长或者宽太小的侯选框
  4. 对当前所有候选框做得分高低排序,选取前12000个侯选框
  5. 排除掉重叠的侯选框
  6. 选取前2000个做二次修正

SSD(1-stage)

对比与RCNN,SSD省去了候选区域的选择,而是将图片的feature maps分成n*n(下图n最大为38)个块,在每个块的中心点预设4~6个默认候选区域。

如下图,SSD首先是将图片进行特征提取,然后再加入多个辅助卷积层,设定各层的默认候选区域。设置多个卷积层的原因是为了更好地识别不同尺寸的目标。

训练时,将生成的默认候选区域与实际标注区域做交并集合处理,具体计算为:IoU =(候选 ∩ 实际)/(候选 ∪ 实际),若IoU值大于0.5,即其与实际区域正匹配,这样我们就能得到目标的大致形状。下图为8*8的图片,在检测人时,SSD在单元格(4,5)的中心点(绿色点)处设置了3个默认侯选框(绿框),与真实框(蓝框)对比,可以发现1、2的相似度比较高。

这里简单说明下多卷积层的用途。下图中,b为8 8特征图,c为4 4特征图。对于猫的检测,图b可以较好地确定选择区域。而对于狗的检测,图b的候选区域(右侧)显然没很好地将目标包裹,而c特征图则比较好地处理了对狗的检测。

因此,高分辨率特征图有利于检测小尺寸物体,低分辨率特征图有利于检测大尺寸物体

YOLO(1-stage)

YOLO大致的原理与SSD相似。但YOLO在检测时只利用了最高层的feature maps,而不同于SSD使用了多个辅助卷积层生层多个尺寸的feature maps(金字塔结构)。这里就不再赘述了。

查看原文

赞 4 收藏 1 评论 1

yvonne 发布了文章 · 2020-04-05

【nodejs公众号开发记录】保姆级上云指北

先打个广告!!(不知道能不能打)腾讯IVWEB团队广招前端,大量hc,有兴趣的同学加我:chenxy712联系或发送简历至我邮箱:yvonnexchen@tencent.com

这里使用的是腾讯云的服务器,emmm,可以根据自己需求去申请购买,我这边选用的是广州节点 1核 2G 2M的。(不想太烧钱TAT)。公众号服务技术栈是基于node + mongodb的,下面具体说一下云服务器的的配置。

上一篇文章在这里?
【nodejs公众号开发记录】半小时带你开发微信公众号

服务器登录

$ ssh root@<your-server-host> -p 22

node安装

已经自带了yum

$ curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
$ nvm install v10
$ node -v

mongodb配置

1、配置yummongodb

$ vi /etc/yum.repos.d/mongodb-org-4.2.repo

2、添加配置信息

[mongodb-org-4.2]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/redhat/$releasever/mongodb-org/4.2/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-4.2.asc

3、安装

$ sudo yum install -y mongodb-org

4、启动

$ systemctl start mongod.service

5、远程连接mongodb

$ vi /etc/mongod.conf

bindIp改成0.0.0.0

# network interfaces 
net: 
  port: 27017 
  bindIp: 0.0.0.0 # Enter 0.0.0.0,:: to bind to all IPv4 and IPv6 addresses or, alternatively, use the net.bindIpAll setting.

5、重启mongodb服务

$ sudo service mongod restart

具体参考:Install MongoDB Community Edition on Red Hat or CentOS

nginx配置

# 安装
$ sudo yum install nginx
# 设置开机启动
$ sudo systemctl enable nginx
# 启动
$ sudo systemctl start nginx
# 重启
$ sudo systemctl restart nginx
# 停止
$ sudo systemctl stop nginx

随后可以在/etc/nginx/nginx.conf配置路由。

使用Jenkens持续集成

安装

sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo
sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
$ sudo yum install jenkins -y

jenkins需要依赖java,所以还需要装一下java

$ sudo yum install java-1.8.0-openjdk-devel -y

完成之后使用以下命令启动jenkins

$ systemctl start jenkins.service

启动之后打开<your-host>:8080配置jenkens任务,这里根据提示的步骤进行操作即可。

  • 安装基础插件:这个直接选新手上路的就行了,这一步需要注意由于国内服务器的关系,安装可能会失败,需要确保插件都装好了(没装好在系统配置页会有提示)
  • 用户名/密码配置

GitHub配置

打开系统配置页面,系统管理 > 系统配置。地址:http://<your-host>:8080/configure

配置环境变量
image.png
配置Github
image.png

1、点击添加,首先配置github用户信息,用以登陆校验,点击确定
image.png

2、再次点击添加,此时添加githubtoken,这个token的获取路径是https://github.com/settings/tokens。点击Generate new token,此时就会生成一段token了。需要记下来,因为之后就看不到了。

image.png

3、测试连接
image.png

点击测试连接,若看到Credentials verified for user ...就说明配置成功了。完成之后保存配置就可以了。

4、为项目添加webhook
进入项目仓库,点击Add webhook,注意将Playload URL修改为<your-server-host>:8080/github-webhook
image.png

5、创建任务
打开http://<your-host>:8080/view/all/newJob选择一个自由风格的任务开始创建。

具体配置如下:
image.png

然后需要添加一个可访问你这个项目的用户,具体步骤点击添加,然后添加的内容跟上文?配置Github > 1、配置github用户信息一样。
image.png

勾选GitHub Hook ...
image.png

填写shell命令
image.png

*6、构建失败排查
在构建执行shell命令时,会遇到command xxx not found情况。可以根据以下步骤确认排查。

  • 确保服务器已经配置了相应命令的包,且能正确执行命令
  • 是否已经配置了jenkens的环境变量,上文有提到
  • 是否添加软连接,比如command node not found,可以进入服务器
$ which node

> /root/.nvm/versions/node/v10.19.0/bin/node

# 添加软连接
ln -s /root/.nvm/versions/node/v10.19.0/bin/node /usr/bin/
  • 若出现sudo: no tty present and no askpass program specified
    参考这篇文章解决 > 传送门

这里就已经基本完成服务器配置了,有问题欢迎留言交流~谢谢观看

查看原文

赞 5 收藏 3 评论 0

yvonne 收藏了文章 · 2020-03-31

前端20个真正灵魂拷问,吃透这些你就是中级前端工程师 【上篇】

clipboard.png

网上参差不弃的面试题,本文由浅入深,让你在做面试官的时候,能够辨别出面试者是不是真的有点东西,也能让你去面试中级前端工程师更有底气。但是切记把背诵面试题当成了你的唯一求职方向

另外欢迎大家加入我们的前端交流二群~,里面很多小姐姐哦,下篇将是非常硬核的源码,原理,自己编写框架和库等,如果感觉写得不错,可以关注给个star

clipboard.png

越是开放性的题目,更能体现回答者的水平,一场好的面试,不仅能发现面试者的不足,也能找到他的闪光点,还能提升面试官自身的技术

1.CssHtml合并在第一个题目,请简述你让一个元素在窗口中消失以及垂直水平居中的方法,还有Flex布局的理解

标准答案:百度上当然很多,这里不做阐述,好的回答思路是:

  • 元素消失的方案先列出来, display:nonevisibility: hidden;的区别,拓展到vue框架的v-ifv-show的区别,可以搭配回流和重绘来讲解

回流必将引起重绘,重绘不一定会引起回流

回流(Reflow):

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流
  • 下面内容会导致回流:

    • 页面首次渲染
    • 浏览器窗口大小发生改变
    • 元素尺寸或位置发生改变
    • 元素内容变化(文字数量或图片大小等等)
    • 元素字体大小变化
    • 添加或者删除可见的DOM元素
    • 激活CSS伪类(例如::hover)
    • 查询某些属性或调用某些方法
  • 一些常用且会导致回流的属性和方法:

    • clientWidth、clientHeight、clientTop、clientLeft
    • offsetWidth、offsetHeight、offsetTop、offsetLeft
    • scrollWidth、scrollHeight、scrollTop、scrollLeft
    • scrollIntoView()、scrollIntoViewIfNeeded()
    • getComputedStyle()
    • getBoundingClientRect()
    • scrollTo()

重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

性能影响对比:

clipboard.png

原文出处,感谢作者

  • 列出元素垂直居中的方案,以及各种方案的缺陷

16种居中方案,感谢作者

  • 讲出flex常用的场景,以及flex 1做了什么

阮一峰老师的Flex布局

上面的问题如果答得非常好,在重绘和回流这块要下大功夫。这点是前端性能优化的基础,而性能优化是前端最重要的核心基础技能点,也是面试官最看中的基础之一

2.你对This了解吗,有自己实现过call,apply,bind吗?

50行javaScript代码实现call,apply,bind

这是一个很基础的技能点,考察你对闭包,函数调用的理解程度,我感觉我写得比较简单容易懂

3.如何减少重绘和回流的次数:

clipboard.png

4.你对前端的异步编程有哪些了解呢

这个题目如果回答非常完美,那么可以判断这个人已经脱离了初级前端工程师,前端的核心就是异步编程,这个题目也是体现前端工程师基础是否扎实的最重要依据。

还是老规矩,从易到难吧

传统的定时器,异步编程:

setTimeout(),setInterval()等。

缺点:当同步的代码比较多的时候,不确定异步定时器的任务时候能在指定的时间执行。

例如:

在第100行执行代码 setTimeout(()=>{console.log(1)},1000)//1s后执行里面函数

但是后面可能有10000行代码+很多计算的任务,例如循环遍历,那么1s后就无法输出console.log(1)

可能要到2s甚至更久

setInterval跟上面同理 当同步代码比较多时,不确保每次能在一样的间隔执行代码,

如果是动画,那么可能会掉帧

ES6的异步编程:

promise generator async
 new promise((resolve,reject)=>{ resolve() }).then()....
 缺点: 仍然没有摆脱回掉函数,虽然改善了回掉地狱
 
 generator函数 调用next()执行到下一个yeild的代码内容,如果传入参数则作为上一个

`yield`的
返回值
 缺点:不够自动化

 async await 
 只有async函数内部可以用await,将异步代码变成同步书写,但是由于async函数本身返回一个
promise,也很容易产生async嵌套地狱

requestAnimationFramerequestIdleCallback

传统的javascript 动画是通过定时器 setTimeout 或者 setInterval 实现的。但是定时器动画一直存在两个问题

第一个就是动画的循时间环间隔不好确定,设置长了动画显得不够平滑流畅,设置短了浏览器的重绘频率会达到瓶颈,推荐的最佳循环间隔是17ms(大多数电脑的显示器刷新频率是60Hz,1000ms/60);

第二个问题是定时器第二个时间参数只是指定了多久后将动画任务添加到浏览器的UI线程队列中,如果UI线程处于忙碌状态,那么动画不会立刻执行。为了解决这些问题,H5 中加入了 requestAnimationFrame以及requestIdleCallback

requestAnimationFrame 会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率

在隐藏或不可见的元素中,requestAnimationFrame 将不会进行重绘或回流,这当然就意味着更少的 CPU、GPU 和内存使用量

requestAnimationFrame 是由浏览器专门为动画提供的 API,在运行时浏览器会自动优化方法的调用,并且如果页面不是激活状态下的话,动画会自动暂停,有效节省了 CPU 开销

性能对比:

clipboard.png

requestAnimationFrame的回调会在每一帧确定执行,属于高优先级任务,而requestIdleCallback的回调则不一定,属于低优先级任务。

我们所看到的网页,都是浏览器一帧一帧绘制出来的,通常认为FPS为60的时候是比较流畅的,而FPS为个位数的时候就属于用户可以感知到的卡顿了,那么在一帧里面浏览器都要做哪些事情呢,如下所示:

clipboard.png

图中一帧包含了用户的交互、js的执行、以及requestAnimationFrame的调用,布局计算以及页面的重绘等工作。

假如某一帧里面要执行的任务不多,在不到16ms(1000/60)的时间内就完成了上述任务的话,那么这一帧就会有一定的空闲时间,这段时间就恰好可以用来执行requestIdleCallback的回调,如下图所示:

clipboard.png

5.简述浏览器的EventloopNode.jsEventloop

浏览器的EventLoop

clipboard.png

不想解释太多,看图

Node.jsEventLoop

clipboard.png

特别提示:网上大部分Node.jsEventLoop的面试题,都会有BUG,代码量和计算量太少,很可能还没有执行到微任务的代码,定时器就到时间被执行了

6.闭包与V8垃圾回收机制:

JS 的垃圾回收机制的基本原理是:

找出那些不再继续使用的变量,然后释放其占用的内存,垃圾收集器会按照固定的时间间隔周期性地执行这一操作。

V8 的垃圾回收策略主要基于分代式垃圾回收机制,在 V8 中,将内存分为新生代和老生代,新生代的对象为存活时间较短的对象,老生代的对象为存活事件较长或常驻内存的对象。

clipboard.png

V8 堆的整体大小等于新生代所用内存空间加上老生代的内存空间,而只能在启动时指定,意味着运行时无法自动扩充,如果超过了极限值,就会引起进程出错。

Scavenge 算法

在分代的基础上,新生代的对象主要通过 Scavenge 算法进行垃圾回收,在 Scavenge 具体实现中,主要采用了一种复制的方式的方法—— Cheney 算法。

Cheney 算法将堆内存一分为二,一个处于使用状态的空间叫 From 空间,一个处于闲置状态的空间称为 To 空间。分配对象时,先是在 From 空间中进行分配。

当开始进行垃圾回收时,会检查 From 空间中的存活对象,将其复制到 To 空间中,而非存活对象占用的空间将会被释放。完成复制后,From 空间和 To 空间的角色发生对换。

clipboard.png
当一个对象经过多次复制后依然存活,他将会被认为是生命周期较长的对象,随后会被移动到老生代中,采用新的算法进行管理。

还有一种情况是,如果复制一个对象到 To 空间时,To 空间占用超过了 25%,则这个对象会被直接晋升到老生代空间中。

标记-清除和标记-整理算法

对于老生代中的对象,主要采用标记-清除和标记-整理算法。标记-清除 和前文提到的标记一样,与 Scavenge 算法相比,标记清除不会将内存空间划为两半,标记清除在标记阶段会标记活着的对象,而在内存回收阶段,它会清除没有被标记的对象。

而标记整理是为了解决标记清除后留下的内存碎片问题。

增量标记(Incremental Marking)算法

前面的三种算法,都需要将正在执行的 JavaScript 应用逻辑暂停下来,待垃圾回收完毕后再恢复。这种行为叫作“全停顿”(stop-the-world)。

在 V8 新生代的分代回收中,只收集新生代,而新生代通常配置较小,且存活对象较少,所以全停顿的影响不大,而老生代就相反了。

为了降低全部老生代全堆垃圾回收带来的停顿时间,V8将标记过程分为一个个的子标记过程,同时让垃圾回收标记和JS应用逻辑交替进行,直到标记阶段完成。

clipboard.png

经过增量标记改进后,垃圾回收的最大停顿时间可以减少到原来的 1/6 左右。

内存泄漏

内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

内存泄漏的常见场景:

  • 缓存:存在内存中数据一只没有被清掉
  • 作用域未释放(闭包)
  • 无效的 DOM 引用
  • 没必要的全局变量
  • 定时器未清除(React中的合成事件,还有原生事件的绑定区别)
  • 事件监听为清空
  • 内存泄漏优化

7.你熟悉哪些通信协议,它们的优缺点?

通信协议全解

我的这篇文章非常详细介绍了 http1.0 http1.1 http2.0 https websocket等协议

8.从输入url地址栏,发生了什么?由此来介绍如何性能优化:

性能优化不完全手册

如何优化你的超大型React应用

我的这两篇文章基本上涵盖了前端基础的性能优化,后期我会再出专栏。

9.浏览器的缓存实现,请您介绍:

1.preload,prefetch,dns-prefetch

什么是preload

使用 preload 指令的好处包括:

允许浏览器来设定资源加载的优先级因此可以允许前端开发者来优化指定资源的加载。

赋予浏览器决定资源类型的能力,因此它能分辨这个资源在以后是否可以重复利用。

浏览器可以通过指定 as 属性来决定这个请求是否符合 content security policy。

浏览器可以基于资源的类型(比如 image/webp)来发送适当的 accept 头。

Prefetch

Prefetch 是一个低优先级的资源提示,允许浏览器在后台(空闲时)获取将来可能用得到的资源,并且将他们存储在浏览器的缓存中。一旦一个页面加载完毕就会开始下载其他的资源,然后当用户点击了一个带有 prefetched 的连接,它将可以立刻从缓存中加载内容。

DNS Prefetching

DNS prefetching 允许浏览器在用户浏览页面时在后台运行 DNS 的解析。如此一来,DNS 的解析在用户点击一个链接时已经完成,所以可以减少延迟。可以在一个 link 标签的属性中添加 rel="dns-prefetch' 来对指定的 URL 进行 DNS prefetching,我们建议对 Google fonts,Google Analytics CDN 进行处理。

2.servece-worker,PWA渐进式web应用

PWA文档

clipboard.png

3.localstorage,sessionstorage,cookie,session等。
浏览器的会话存储和持久性存储
4.浏览器缓存的实现机制的实现

clipboard.png

10.同源策略是什么,跨域解决办法,cookie可以跨域吗?

跨域解决的办法

Q:为什么会出现跨域问题?

A:出于浏览器的同源策略限制,浏览器会拒绝跨域请求。

  • 注:严格的说,浏览器并不是拒绝所有的跨域请求,实际上拒绝的是跨域的读操作。浏览器的同源限制策略是这样执行的:

通常浏览器允许进行跨域写操作(Cross-origin writes),如链接,重定向;

通常浏览器允许跨域资源嵌入(Cross-origin embedding),如 img、script 标签;

通常浏览器不允许跨域读操作(Cross-origin reads)。

Q:什么情况才算作跨域?

A:非同源请求,均为跨域。名词解释:同源 —— 如果两个页面拥有相同的协议(protocol),端口(port)和主机(host),那么这两个页面就属于同一个源(origin)。

Q:为什么有跨域需求?

A:场景 —— 工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能需要对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。

方法:JSONP,CORS,postmessage,webscoket,反向代理服务器等。

上篇已经结束,欢迎你关注,等待下篇非常硬核的文章出炉~

期待你加入我们哦~

现在一群满了,所以新开了一个二群

clipboard.png

查看原文

yvonne 发布了文章 · 2020-03-28

【nodejs公众号开发记录】半小时带你开发微信公众号

先打个广告!!(不知道能不能打)腾讯IVWEB团队广招前端,大量hc,有兴趣的同学加我vx:chenxy712联系或发送简历至我邮箱:yvonnexchen@tencent.com

前期准备

最近需求有点不饱和,摸鱼lu了一个微信公众号(来个电影)。首先明确一下开发一个带后台的微信公众号需要准备的东西

  1. 公众号开发者文档 请收好
  2. 一个用以部署公众号服务的服务器(可以是云服务器也可以是个人主机)

公众号配置

注册成功后进入 > 基本配置,需要关注这三个值,其中前两个都是注册后自动生成的,第三个自己随意填写一个适合的token就行,建议英文数字组合,这里主要是微信公众号请求验证的时候用以匹配的。

  • AppID(公众号验证及发消息需要)
  • AppSecret(公众号验证及发消息需要)
  • Token (公众号验证及发消息需要,注意是token不是access_token

关于access_token,简单的个人号是用不上的,IP白名单也是获取access_token需要配置的请求IP。这里也不需要填写。

明确需求

我的公众号最主要的功能是:能针对用户消息请求,推荐他们想要的影视资讯。加上一些增色功能,所以后台的主要任务是

  1. 消息处理与回复
  2. 用户事件处理与响应(如:最基本的 关注/取消关注 事件)

基础框架

作为一个小前端,服务框架的选型我用的是:nodejs + koa2 + mongodb
基本架构如下:

其中:

  • controllers 请求处理类
  • models 数据模型
  • service 第三方服务
  • router 路由
  • app.js 服务主入口

这里是仓库地址。可以根据README安装配置,再将./config/constants.js文件下的微信公众号配置换成你的,然后在根目录下运行npm run dev即可。

这时访问http://127.0.0.1:8001,如果能看到dismatch就说明运行成功啦!

使用 ngork 进行内网穿透

意思就是将自己本机的服务让外网能访问到。官网下载安装一下ngrok。到ngrok解压目录下运行

// 8001是上面框架提供的端口,你也可以自己改变
$ ./ngrok http 8001

然后再次打开公众号后台

URL改成ngrok映射的Forwarding域名,点击提交,如果看到绿色提示: 提交成功。那么恭喜你,公众号后台开发技能已达成!

关于详细开发记录以及服务器配置,后面的文章再见啦~~

emmm...最后给自己最近做的公众号打个广告,一个为你推荐优质电影的公众号(还有隐藏玩法自行体验~)

查看原文

赞 6 收藏 4 评论 5

yvonne 发布了文章 · 2019-08-31

【译】让React组件如文档般展示的6大工具

原文 6 Tools for Documenting Your React Components Like a Pro

如果没有人能够理解并找到如何使用我们的组件,那它们有什么用呢?

React鼓励我们使用组件构建模块化程序。模块化给我们带来了非常多的好处,包括提高了可重用性。然而,如果你是为了贡献和复用组件,最好得让你的组件容易被找到、理解和使用。你需要将其文档化。

目前,使用工具可以帮助我们实现自动化文档工作流程,并使我们的组件文档变得丰富、可视化和可交互。有些工具甚至将这些文档组合为共享组件的工作流程的组成部分。

为了轻而易举地将我们的组件文档化,我收集了一些业界流行的工具,如果你有推荐的组件也可以评论留言。

1. Bit

共享组件的平台

clipboard.png

Bit不仅是一个将组件文档化的工具,它还是一个开源工具,支持你使用所有文件和依赖项封装组件,并在不同应用程序中开箱即用地运行它们。
Bit,你可以跨应用地共享和协作组件,你所有共享组件都可以被发现,以便你的团队在项目中查找和使用,并轻松共享他们自己的组件。

clipboard.png
在Bit中,你共享的组件可以在你们团队中的组件共享中心找到,你可以根据上下文、bundle体积、依赖项搜索组件,你可以非常快地找到已经渲染好的组件快照,并且选择使用它们。

浏览bit.dev上的组件

clipboard.png
当你进入组件详情页时,Bit提供了一个可交互的页面实时渲染展示组件,如果该组件包含js或md代码,我们可以对其进行代码修改和相关调试。

找到想要使用的组件后,只需使用NPM或Yarn进行安装即可。你甚至可以使用Bit直接开发和安装组件,这样你的团队就可以协作并一起构建。

clipboard.png

通过Bit共享组件,就不需要再使用存储库或工具,也不需要重构或更改代码,共享、文档化、可视化组件都集中在一起,并且也能实现开箱即用。

快速上手:
Share reusable code components as a team · Bit
teambit/bit

2. StoryBook & Styleguidist

StoryBook和StyleGuidist是非常棒的项目,可以帮助我们开发独立的组件,同时可以直观地呈现和文档化它们。

clipboard.png

StoryBook 提供了一套UI组件的开发环境。它允许你浏览组件库,查看每个组件的不同状态,以及交互式开发和测试组件。在构建库时,StoryBook提供了一种可视化和记录组件的简洁方法,不同的AddOns让你可以更轻松地集成到不同的工具和工作流中。你甚至可以在单元测试中重复使用示例来确认细微差别的功能。

clipboard.png

StyleGuidist是一个独立的React组件开发环境并且具备实时编译指引。它提供了一个热重载的服务器和即时编译指引,列出了组件propTypes并展示基于.md文件的可编辑使用示例。它支持ES6,Flow和TypeScript,并且可以直接使用Create React App。自动生成的使用文档可以帮助Styleguidist作为团队不同组件的文档门户。

类似的工具还有UiZoo

3. Codesandbox, Stackblitz & friends

组件在线编译器是一种非常巧妙的展示组件和理解他们如何运行的工具。当你可以将它们组合为文档的一部分(或作为共享组件的一部分)时,在线编译器可帮助你快速了解代码的工作方式并决定是否要使用该组件。

clipboard.png

Codesandbox是一个在线编辑器,用于快速创建和展示组件等小项目。创建一些有趣的东西后,你可以通过共享网址向他人展示它。CodeSandbox具有实时预览功能,可以在你输入代码时显示运行结果,并且可以集成到你的不同工具和开发工作流程中去。

clipboard.png

Stackblitz是一个由Visual Studio Code提供支持的“Web应用程序在线IDE”。与Codesnadbox非常相似,StackBlitz是一个在线IDE,你可以在其中创建通过URL共享的Angular和React项目。与Codesandbox一样,它会在你编辑时自动安装依赖项,编译,捆绑和热重载。

其他类似工具:
11 React UI Component Playgrounds for 2019

4. Docz

clipboard.png

Docz使你可以更轻松地为你的代码构建Gtabsy支持的文档网站。它基于MDX(Markdown + JSX),即利用markdown进行组件文档化。基本上,你可以在项目的任何位置编写.mdx文件,Docz会将其转换并部署到Netlify,简化你自己设计的文档门户的过程。非常有用不是吗?
pedronauck / docz

5. MDX-docs

clipboard.png

MDX-docs允许你使用MDX和Next.js记录和开发React组件。您可以将markdown与内联JSX混合以展示React组件。像往常一样写下markdown并使用ES导入语法在文档中使用自定义组件。内置组件会将JSX代码块渲染为具有可编辑代码并提供实时预览功能,由react-live提供支持。

jxnblk / MDX-文档

6. React Docgen

clipboard.png

React DocGen是一个用于从React组件文件中提取信息的CLI和工具箱,以便生成文档。它使用ast-types@babel/parser将源解析为AST,并提供处理此AST的方法。输出/返回值是JSON blob/JavaScript对象。它通过React.createClassES2015类定义或功能(无状态组件)为React组件提供了一个默认的定义。功能十分强大。

reactjs/react-docgen
callstack/component-docs

查看原文

赞 33 收藏 19 评论 2

yvonne 发布了文章 · 2019-07-31

【译】教你如何避开「Cannot read property of undefined」

Uncaught TypeError: Cannot read property 'foo' of undefined.这种错误想必在我们日常开发中都到过,这有可能是我们的api返回了一个空的状态而我们没有预料到,也可能是其他,我们无从得知,因为这种问题十分的常见且涉及的因素相当多。

我最近遇到了一个问题,某些环境变量由于某种原因没有被引入,导致各种各样的问题,一堆错误出现在我的眼前。无论原因是什么,它可能是一个灾难性的错误,那么我们如何才能首先防止它呢?

让我们来看看解决办法。

工具库

如果你已经使用了某种工具库,很可能内部就包含了相关的错误处理函数。比如lodash_.getRamdaR.path提供访问安全对象的支持。

如果没有使用呢?那就看看下面的解决办法。

使用&&实现「短路与」

Javascript中一个有趣的事情是,逻辑运算操作不一定返回boolean值。根据规范,&&||运算符返回的值不一定需要为boolean类型,它的返回值为该运算符左右两边表达式的其中一个。

&&中,如果第一个表达式为false则直接返回,否则就用第二个。举个例子:0&&1 -> 02&&3 -> 3。如果是链式使用&&的话,将会判断第一个假值或者最后一个真值。比如:1 && 2 && 3 && null && 4​ -> null1 && 2 && 3 -> 3

这对安全地访问嵌套对象属性有什么作用呢?JavaScript中的逻辑运算符将「短路」。在使用&&的情况下,这意味着表达式在达到其第一个假值后将停止向后执行。

​​const foo = false && destroyAllHumans();
​​console.log(foo); // false, and humanity is safe

在上面代码中,因为&&遇到了false值,destroyAllHumans将永远不会被执行。

这便可用于安全地访问嵌套属性。

const meals = {
​​  breakfast: null, // I skipped the most important meal of the day! :(
​​  lunch: {
​​    protein: 'Chicken',
​​    greens: 'Spinach',
​​  },
​​  dinner: {
​​    protein: 'Soy',
​​    greens: 'Kale',
​​  },
​​};
​​
​​const breakfastProtein = meals.breakfast && meals.breakfast.protein; // null
​​const lunchProtein = meals.lunch && meals.lunch.protein; // 'Chicken

这种办法不仅简单,而且在处理短链时也非常的简洁。但是当访问更深层的对象时,这可能就会变得非常冗长。

const favorites = {
​​  video: {
​​    movies: ['Casablanca', 'Citizen Kane', 'Gone With The Wind'],
​​    shows: ['The Simpsons', 'Arrested Development'],
​​    vlogs: null,
​​  },
​​  audio: {
​​    podcasts: ['Shop Talk Show', 'CodePen Radio'],
​​    audiobooks: null,
​​  },
​​  reading: null, // Just kidding -- I love to read
​​};
​​
​​const favoriteMovie = favorites.video && favorites.video.movies && favorites.video.movies[0];
​​// Casablanca
​​const favoriteVlog = favorites.video && favorites.video.vlogs && favorites.video.vlogs[0];
​​// null

嵌套的对象越深,它就越难以描述。

Maybe Monad

Oliver Steele提出了这种方法,并在他的博客文章Monads on the Cheap I:The Maybe Monad中进行更详细地介绍。我将在这里作一个简单的解释。

const favoriteBook = ((favorites.reading||{}).books||[])[0]; // undefined
​​const favoriteAudiobook = ((favorites.audio||{}).audiobooks||[])[0]; // undefined
​​const favoritePodcast = ((favorites.audio||{}).podcasts||[])[0]; // 'Shop Talk Show'

与上述短路示例类似,此方法通过检查值是否为假来进行操作。如果是,它将尝试访问空对象上的下一个属性。在上面的例子中,favorites.readingnull,所以books从一个空对象进行访问。而此时的返回值也将是undefined,于是[0]将从一个空数组中获取。

该方法优于该&&方法的地方是避免属性名称的重复。在更深层的对象上,这可能是一个非常重要的优势。主要的缺点是可读性较差。它不是一种常见的模式,可能需要读者花一点时间来解析它是如何工作的。

try/catch

JavaScript中​​try...catch语句允许某函数安全地访问属性。

try {
​​  console.log(favorites.reading.magazines[0]);
​​} catch (error) {
​​  console.log("No magazines have been favorited.");
​​}

但问题在于,​​try...catch不是一个表达式,在某些语言中它不具备直接返回值的能力,一种可行的方法是直接在try...catch定义变量。

let favoriteMagazine;
​​try { 
​​  favoriteMagazine = favorites.reading.magazines[0]; 
​​} catch (error) { 
​​  favoriteMagazine = null; /* any default can be used */
​​};

即使要写不少的代码,但其实你也只能定义一个变量,但如果同时定义多个话,就会有问题。

let favoriteMagazine, favoriteMovie, favoriteShow;
​​try {
​​  favoriteMovie = favorites.video.movies[0];
​​  favoriteShow = favorites.video.shows[0];
​​  favoriteMagazine = favorites.reading.magazines[0];
​​} catch (error) {
​​  favoriteMagazine = null;
​​  favoriteMovie = null;
​​  favoriteShow = null;
​​};
​​
​​console.log(favoriteMovie); // null
​​console.log(favoriteShow); // null
​​console.log(favoriteMagazine); // null

如果访问该属性的任何尝试失败,则会导致所有这些尝试都回退到其默认值。

另一种方法是将try...catch可重用的实用程序函数包装起来。

const tryFn = (fn, fallback = null) => {
​​  try {
​​    return fn();
​​  } catch (error) {
​​    return fallback;
​​  }
​​} 
​​
​​const favoriteBook = tryFn(() => favorites.reading.book[0]); // null
​​const favoriteMovie = tryFn(() => favorites.video.movies[0]); // "Casablanca"

未完待续...

查看原文

赞 3 收藏 2 评论 0

yvonne 关注了专栏 · 2019-05-28

前端大哈

不定期的发布关于前端技术的理解,首发翻译国外第一手的前端技术资料

关注 194

yvonne 收藏了文章 · 2019-05-07

精读《React16 新特性》

1 引言

于 2017.09.26 Facebook 发布 React v16.0 版本,时至今日已更新到 React v16.6,且引入了大量的令人振奋的新特性,本文章将带领大家根据 React 更新的时间脉络了解 React16 的新特性。

2 概述

按照 React16 的更新时间,从 React v16.0 ~ React v16.6 进行概述。

React v16.0

  • render 支持返回数组和字符串、Error Boundaries、createPortal、支持自定义 DOM 属性、减少文件体积、fiber;

React v16.1

  • react-call-return;

React v16.2

  • Fragment;

React v16.3

  • createContext、createRef、forwardRef、生命周期函数的更新、Strict Mode;

React v16.4

  • Pointer Events、update getDerivedStateFromProps;

React v16.5

  • Profiler;

React v16.6

  • memo、lazy、Suspense、static contextType、static getDerivedStateFromError();

React v16.7(~Q1 2019)

  • Hooks;

React v16.8(~Q2 2019)

  • Concurrent Rendering;

React v16.9(~mid 2019)

  • Suspense for Data Fetching;

下面将按照上述的 React16 更新路径对每个新特性进行详细或简短的解析。

3 精读

React v16.0

render 支持返回数组和字符串

// 不需要再将元素作为子元素装载到根元素下面
render() {
  return [
    <li/>1</li>,
    <li/>2</li>,
    <li/>3</li>,
  ];
}

Error Boundaries

React15 在渲染过程中遇到运行时的错误,会导致整个 React 组件的崩溃,而且错误信息不明确可读性差。React16 支持了更优雅的错误处理策略,如果一个错误是在组件的渲染或者生命周期方法中被抛出,整个组件结构就会从根节点中卸载,而不影响其他组件的渲染,可以利用 error boundaries 进行错误的优化处理。

class ErrorBoundary extends React.Component {
  state = { hasError: false };

  componentDidCatch(error, info) {
    this.setState({ hasError: true });

    logErrorToMyService(error, info);
  }

  render() {
    if (this.state.hasError) {
      return <h1>数据错误</h1>;
    }
    
    return this.props.children;
  }
}

createPortal

createPortal 的出现为 弹窗、对话框 等脱离文档流的组件开发提供了便利,替换了之前不稳定的 API unstable_renderSubtreeIntoContainer,在代码使用上可以做兼容,如:

const isReact16 = ReactDOM.createPortal !== undefined;

const getCreatePortal = () =>
  isReact16
    ? ReactDOM.createPortal
    : ReactDOM.unstable_renderSubtreeIntoContainer;

使用 createPortal 可以快速创建 Dialog 组件,且不需要牵扯到 componentDidMount、componentDidUpdate 等生命周期函数。

并且通过 createPortal 渲染的 DOM,事件可以从 portal 的入口端冒泡上来,如果入口端存在 onDialogClick 等事件,createPortal 中的 DOM 也能够被调用到。

import React from 'react';
import { createPortal } from 'react-dom';

class Dialog extends React.Component {
  constructor() {
    super(props);

    this.node = document.createElement('div');
    document.body.appendChild(this.node);
  }

  render() {
    return createPortal(
      <div>
        {this.props.children}
      </div>,
      this.node
    );
  }
}

支持自定义 DOM 属性

以前的 React 版本 DOM 不识别除了 HTML 和 SVG 支持的以外属性,在 React16 版本中将会把全部的属性传递给 DOM 元素。这个新特性可以让我们摆脱可用的 React DOM 属性白名单。笔者之前写过一个方法,用于过滤非 DOM 属性 filter-react-dom-props,16 之后即可不再需要这样的方法。

减少文件体积

React16 使用 Rollup 针对不同的目标格式进行代码打包,由于打包工具的改变使得库文件大小得到缩减。

  • React 库大小从 20.7kb(压缩后 6.9kb)降低到 5.3kb(压缩后 2.2kb)
  • ReactDOM 库大小从 141kb(压缩后 42.9kb)降低到 103.7kb(压缩后 32.6kb)
  • React + ReactDOM 库大小从 161.7kb(压缩后 49.8kb)降低到 109kb(压缩后 43.8kb)

Fiber

Fiber 是对 React 核心算法的一次重新实现,将原本的同步更新过程碎片化,避免主线程的长时间阻塞,使应用的渲染更加流畅。

在 React16 之前,更新组件时会调用各个组件的生命周期函数,计算和比对 Virtual DOM,更新 DOM 树等,这整个过程是同步进行的,中途无法中断。当组件比较庞大,更新操作耗时较长时,就会导致浏览器唯一的主线程都是执行组件更新操作,而无法响应用户的输入或动画的渲染,很影响用户体验。

Fiber 利用分片的思想,把一个耗时长的任务分成很多小片,每一个小片的运行时间很短,在每个小片执行完之后,就把控制权交还给 React 负责任务协调的模块,如果有紧急任务就去优先处理,如果没有就继续更新,这样就给其他任务一个执行的机会,唯一的线程就不会一直被独占。

因此,在组件更新时有可能一个更新任务还没有完成,就被另一个更高优先级的更新过程打断,优先级高的更新任务会优先处理完,而低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来。所以 React Fiber 把一个更新过程分为两个阶段:

  • 第一个阶段 Reconciliation Phase,Fiber 会找出需要更新的 DOM,这个阶段是可以被打断的;
  • 第二个阶段 Commit Phase,是无法别打断,完成 DOM 的更新并展示;

在使用 Fiber 后,需要要检查与第一阶段相关的生命周期函数,避免逻辑的多次或重复调用:

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

与第二阶段相关的生命周期函数:

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

React v16.1

Call Return(react-call-return npm)

react-call-return 目前还是一个独立的 npm 包,主要是针对 父组件需要根据子组件的回调信息去渲染子组件场景 提供的解决方案。

在 React16 之前,针对上述场景一般有两个解决方案:

  • 首先让子组件初始化渲染,通过回调函数把信息传给父组件,父组件完成处理后更新子组件 props,触发子组件的第二次渲染才可以解决,子组件需要经过两次渲染周期,可能会造成渲染的抖动或闪烁等问题;
  • 首先在父组件通过 children 获得子组件并读取其信息,利用 React.cloneElement 克隆产生新元素,并将新的属性传递进去,父组件 render 返回的是克隆产生的子元素。虽然这种方法只需要使用一个生命周期,但是父组件的代码编写会比较麻烦;

React16 支持的 react-call-return,提供了两个函数 unstable_createCall 和 unstable_createReturn,其中 unstable_createCall 是 父组件使用,unstable_createReturn 是 子组件使用,父组件发出 Call,子组件响应这个 Call,即 Return。

  • 在父组件 render 函数中返回对 unstable_createCall 的调用,第一个参数是 props.children,第二个参数是一个回调函数,用于接受子组件响应 Call 所返回的信息,第三个参数是 props;
  • 在子组件 render 函数返回对 unstable_createReturn 的调用,参数是一个对象,这个对象会在unstable_createCall 第二个回调函数参数中访问到;
  • 当父组件下的所有子组件都完成渲染周期后,由于子组件返回的是对 unstable_createReturn 的调用所以并没有渲染元素,unstable_createCall 的第二个回调函数参数会被调用,这个回调函数返回的是真正渲染子组件的元素;

针对普通场景来说,react-call-return 有点过度设计的感觉,但是如果针对一些特定场景的话,它的作用还是非常明显,比如,在渲染瀑布流布局时,利用 react-call-return 可以先缓存子组件的 ReactElement,等必要的信息足够之后父组件再触发 render,完成渲染。

import React from 'react';
import { unstable_createReturn, unstable_createCall } from 'react-call-return';

const Child = (props) => {
  return unstable_createReturn({
    size: props.children.length,
    renderItem: (partSize, totalSize) => {
      return <div>{ props.children } { partSize } / { totalSize }</div>;
    }
  });
};

const Parent = (props) => {
  return (
    <div>
      {
        unstable_createCall(
          props.children,
          (props, returnValues) => {
            const totalSize = returnValues.map(v => v.size).reduce((a, b) => a + b, 0);
            return returnValues.map(({ size, renderItem }) => {
              return renderItem(size, totalSize);
            });
          },
          props
        )
      }
    </div>
  );
};

React v16.2

Fragment

Fragment 组件其作用是可以将一些子元素添加到 DOM tree 上且不需要为这些元素提供额外的父节点,相当于 render 返回数组元素。

render() {
  return (
    <Fragment>
      Some text.
      <h2>A heading</h2>
      More text.
      <h2>Another heading</h2>
      Even more text.
    </Fragment>
  );
}

React v16.3

createContext

全新的 Context API 可以很容易穿透组件而无副作用,其包含三部分:React.createContext,Provider,Consumer。

  • React.createContext 是一个函数,它接收初始值并返回带有 Provider 和 Consumer 组件的对象;
  • Provider 组件是数据的发布方,一般在组件树的上层并接收一个数据的初始值;
  • Consumer 组件是数据的订阅方,它的 props.children 是一个函数,接收被发布的数据,并且返回 React Element;
const ThemeContext = React.createContext('light');

class ThemeProvider extends React.Component {
  state = {theme: 'light'};

  render() {
    return (
      <ThemeContext.Provider value={this.state.theme}>
        {this.props.children}
      </ThemeContext.Provider>
    );
  }
}

class ThemedButton extends React.Component {
  render() {
    return (
      <ThemeContext.Consumer>
        {theme => <Button theme={theme} />}
      </ThemeContext.Consumer>
    );
  }
}

createRef / forwardRef

React16 规范了 Ref 的获取方式,通过 React.createRef 取得 Ref 对象。

// before React 16
···

  componentDidMount() {
    const el = this.refs.myRef
  }

  render() {
    return <div ref="myRef" />
  }

···

// React 16+
  constructor(props) {
    super(props)
    
    this.myRef = React.createRef()
  }

  render() {
    return <div ref={this.myRef} />
  }
···

React.forwardRef 是 Ref 的转发, 它能够让父组件访问到子组件的 Ref,从而操作子组件的 DOM。 React.forwardRef 接收一个函数,函数参数有 props 和 ref。

const TextInput = React.forwardRef((props, ref) => (
  <input type="text" placeholder="Hello forwardRef" ref={ref} />
))

const inputRef = React.createRef()

class App extends Component {
  constructor(props) {
    super(props)
    
    this.myRef = React.createRef()
  }

  handleSubmit = event => {
    event.preventDefault()
    
    alert('input value is:' + inputRef.current.value)
  }
  
  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <TextInput ref={inputRef} />
        <button type="submit">Submit</button>
      </form>
    )
  }
}

生命周期函数的更新

React16 采用了新的内核架构 Fiber,Fiber 将组件更新分为两个阶段:Render Parse 和 Commit Parse,因此 React 也引入了 getDerivedStateFromProps 、 getSnapshotBeforeUpdate 及 componentDidCatch 等三个全新的生命周期函数。同时也将 componentWillMount、componentWillReceiveProps 和 componentWillUpdate 标记为不安全的方法。

static getDerivedStateFromProps(nextProps, prevState)

getDerivedStateFromProps(nextProps, prevState) 其作用是根据传递的 props 来更新 state。它的一大特点是无副作用,由于处在 Render Phase 阶段,所以在每次的更新都会触发该函数, 在 API 设计上采用了静态方法,使其无法访问实例、无法通过 ref 访问到 DOM 对象等,保证了该函数的纯粹高效。

为了配合未来的 React 异步渲染机制,React v16.4 对 getDerivedStateFromProps 做了一些改变, 使其不仅在 props 更新时会被调用,setState 时也会被触发。

  • 如果改变 props 的同时,有副作用的产生,这时应该使用 componentDidUpdate;
  • 如果想要根据 props 计算属性,应该考虑将结果 memoization 化;
  • 如果想要根据 props 变化来重置某些状态,应该考虑使用受控组件;
static getDerivedStateFromProps(props, state) {
  if (props.value !== state.controlledValue) {
    return {
      controlledValue: props.value,
    };
  }
  
  return null;
}

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate(prevProps, prevState) 会在组件更新之前获取一个 snapshot,并可以将计算得的值或从 DOM 得到的信息传递到 componentDidUpdate(prevProps, prevState, snapshot) 函数的第三个参数,常常用于 scroll 位置定位等场景。

componentDidCatch(error, info)

componentDidCatch 函数让开发者可以自主处理错误信息,诸如错误展示,上报错误等,用户可以创建自己的 Error Boundary 来捕获错误。

componentWillMount(nextProps, nextState)

componentWillMount 被标记为不安全,因为在 componentWillMount 中获取异步数据或进行事件订阅等操作会产生一些问题,比如无法保证在 componentWillUnmount 中取消掉相应的事件订阅,或者导致多次重复获取异步数据等问题。

componentWillReceiveProps(nextProps) / componentWillUpdate(nextProps, nextState)

componentWillReceiveProps / componentWillUpdate 被标记为不安全,主要是因为操作 props 引起的 re-render 问题,并且对 DOM 的更新操作也可能导致重新渲染。

Strict Mode

StrictMode 可以在开发阶段开启严格模式,发现应用存在的潜在问题,提升应用的健壮性,其主要能检测下列问题:

  • 识别被标志位不安全的生命周期函数
  • 对弃用的 API 进行警告
  • 探测某些产生副作用的方法
  • 检测是否使用 findDOMNode
  • 检测是否采用了老的 Context API
class App extends React.Component {
  render() {
    return (
      <div>
        <React.StrictMode>
          <ComponentA />
        </React.StrictMode>
      </div>
    )
  }
}

React v16.4

Pointer Events

指针事件是为指针设备触发的 DOM 事件。它们旨在创建单个 DOM 事件模型来处理指向输入设备,例如鼠标,笔 / 触控笔或触摸(例如一个或多个手指)。指针是一个与硬件无关的设备,可以定位一组特定的屏幕坐标。拥有指针的单个事件模型可以简化创建 Web 站点和应用程序,并提供良好的用户体验,无论用户的硬件如何。但是,对于需要特定于设备的处理的场景,指针事件定义了一个 pointerType 属性,用于检查产生事件的设备类型。

React 新增 onPointerDown / onPointerMove / onPointerUp / onPointerCancel / onGotPointerCapture / onLostPointerCapture / onPointerEnter / onPointerLeave / onPointerOver / onPointerOut 等指针事件。

这些事件只能在支持 指针事件 规范的浏览器中工作。如果应用程序依赖于指针事件,建议使用第三方指针事件 polyfill。

React v16.5

Profiler

React 16.5 添加了对新的 profiler DevTools 插件的支持。这个插件使用 React 的 Profiler 实验性 API 去收集所有 component 的渲染时间,目的是为了找出 React App 的性能瓶颈,它将会和 React 即将发布的 时间片 特性完全兼容。

React v16.6

memo

React.memo() 只能作用在简单的函数组件上,本质是一个高阶函数,可以自动帮助组件执行shouldComponentUpdate(),但只是执行浅比较,其意义和价值有限。

const MemoizedComponent = React.memo(props => {
  /* 只在 props 更改的时候才会重新渲染 */
});

lazy / Suspense

React.lazy() 提供了动态 import 组件的能力,实现代码分割。

Suspense 作用是在等待组件时 suspend(暂停)渲染,并显示加载标识。

目前 React v16.6 中 Suspense 只支持一个场景,即使用 React.lazy() 和 <React.Suspense> 实现的动态加载组件。

import React, {lazy, Suspense} from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <OtherComponent />
    </Suspense>
  );
}

static contextType

static contextType 为 Context API 提供了更加便捷的使用体验,可以通过 this.context 来访问 Context。

const MyContext = React.createContext();

class MyClass extends React.Component {
  static contextType = MyContext;
  
  componentDidMount() {
    const value = this.context;
  }
  
  componentDidUpdate() {
    const value = this.context;
  }
  
  componentWillUnmount() {
    const value = this.context;
  }
  
  render() {
    const value = this.context;
  }
}

getDerivedStateFromError

static getDerivedStateFromError(error) 允许开发者在 render 完成之前渲染 Fallback UI,该生命周期函数触发的条件是子组件抛出错误,getDerivedStateFromError 接收到这个错误参数后更新 state。

class ErrorBoundary extends React.Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }
  
  componentDidCatch(error, info) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, info);
  }
  
  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something went wrong.</h1>;
    }
    
    return this.props.children; 
  }
}

React v16.7(~Q1 2019)

Hooks

Hooks 要解决的是状态逻辑复用问题,且不会产生 JSX 嵌套地狱,其特性如下:

  • 多个状态不会产生嵌套,依然是平铺写法;
  • Hooks 可以引用其他 Hooks;
  • 更容易将组件的 UI 与状态分离;

Hooks 并不是通过 Proxy 或者 getters 实现,而是通过数组实现,每次 useState 都会改变下标,如果 useState 被包裹在 condition 中,那每次执行的下标就可能对不上,导致 useState 导出的 setter 更新错数据。

更多 Hooks 使用场景可以阅读下列文章:

function App() {
  const [open, setOpen] = useState(false);
  
  return (
    <>
      <Button type="primary" onClick={() => setOpen(true)}>
        Open Modal
      </Button>
      <Modal
        visible={open}
        onOk={() => setOpen(false)}
        onCancel={() => setOpen(false)}
      />
    </>
  );
}

React v16.8(~Q2 2019)

Concurrent Rendering

Concurrent Rendering 并发渲染模式是在不阻塞主线程的情况下渲染组件树,使 React 应用响应性更流畅,它允许 React 中断耗时的渲染,去处理高优先级的事件,如用户输入等,还能在高速连接时跳过不必要的加载状态,用以改善 Suspense 的用户体验。

目前 Concurrent Rendering 尚未正式发布,也没有详细相关文档,需要等待 React 团队的正式发布。

React v16.9(~mid 2019)

Suspense for Data Fetching

Suspense 通过 ComponentDidCatch 实现用同步的方式编写异步数据的请求,并且没有使用 yield / async / await,其流程:调用 render 函数 -> 发现有异步请求 -> 暂停渲染,等待异步请求结果 -> 渲染展示数据。

无论是什么异常,JavaScript 都能捕获,React就是利用了这个语言特性,通过 ComponentDidCatch 捕获了所有生命周期函数、render函数等,以及事件回调中的错误。如果有缓存则读取缓存数据,如果没有缓存,则会抛出一个异常 promise,利用异常做逻辑流控制是一种拥有较深的调用堆栈时的手段,它是在虚拟 DOM 渲染层做的暂停拦截,代码可在服务端复用。

import { fetchMovieDetails } from '../api';
import { createFetch } from '../future';

const movieDetailsFetch = createFetch(fetchMovieDetails);

function MovieDetails(props) {
  const movie = movieDetailsFetch.read(props.id);

  return (
    <div>
      <MoviePoster data-original={movie.poster} />
      <MovieMetrics {...movie} />
    </div>
  );
}

4 总结

从 React16 的一系列更新和新特性中我们可以窥见,React 已经不仅仅只在做一个 View 的展示库,而是想要发展成为一个包含 View / 数据管理 / 数据获取 等场景的前端框架,以 React 团队的技术实力以及想法,笔者还是很期待和看好 React 的未来,不过它渐渐地已经对开发新手们不太友好了。

5 更多讨论

讨论地址是:[精读《React16 新特性》 · Issue #115 · dt-fe/weekly]https://github.com/dt-fe/week...

如果你想参与讨论,请点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

查看原文

yvonne 发布了文章 · 2019-04-30

AE动画转Web代码工具指北-Lottie

简介

Lottie 是 Airbnb 开源的一套跨平台的完整的动画效果解决方案,设计师可以使用 Adobe After Effects 设计出漂亮的动画之后,使用 Lottic 提供的 Bodymovin 插件将设计好的动画导出成 JSON 格式,就可以直接运用在 iOS、Android、Web 和 React Native之上,无需其他额外操作。

简单来说,Lottie就是一个可以将AE动画转成可运行在IOS、Android、Web、React Native上的AE插件。

使用

工具

使用前请确保已安装这以下工具。

其他。

使用步骤

  1. 安装并解压bodymovin
  2. 打开AE,添加bodymovin扩展
  3. 导出data.json文件

详细可参考:炫酷神器,AE插件Bodymovin.zxp的安装与使用

API

bodymovin导出的data.json实际就是动画的数据文件,我们引入的bodymovin.js库会对其做相应的解析。接下来我们只需要添加简单的初始化代码就可以在运行代码看到相应的动画效果了。

以下是最常用的api

1. 初始化

let animation = bodymovin.loadAnimation({
  animationData, // [必须] data.json文件
  path, // data.json文件路径(animationData/path选其一传入即可)
  container,// [必须] 父容器
  renderer, // [必须] 渲染方式
  loop,
  autoplay
})

2. 暂停/停止/播放

bodymovin.play()
bodymovin.pause()
bodymovin.stop() // 回到第0帧

3. 跳转之某帧并播放

animation.gotoAndStop(time)
OR animation.gotoAndStop(frame)
----
animation.gotoAndPlay(time)
OR animation.gotoAndPlay(frame)

4. 设置fp

animation.setSubframe()
// true: 使用本地环境的fps [默认]
// false: 使用ae原本的fps

5. 事件监听

animation.onComplete = function () {} // 动画结束
animation.onLoopComplete = function () {} // 当前循环结束
// 使用addEventListener方式
animation.addEventListener('complete', function () {})
animation.addEventListener('loopComplete', function () {})

一般来说以上的api即可满足大部分的动画展示需求了。更详细内容可参考官网

Bodymovin库

最后再分项目框架提供两个bodymovin的库

查看原文

赞 14 收藏 10 评论 1