此篇文章
Markdown
字数六万六千
多字,HTML
字数五万五千
字,全是知识点...
掘金...六万字发不了,只能拆开,拆成三篇文章。
三十、什么是原型、原型链?
原型:JS
声明构造函数(用来实例化对象的函数)时,会在内存中创建一个对应的对象,这个对象就是原函数的原型。
构造函数默认有一个 prototype
属性,prototype
的值指向函数的原型。同时原型中也有一个 constructor
属性,constructor
的值指向原函数。
通过构造函数实例化出来的对象,并不具有 prototype
属性,其默认有一个 __proto__
属性,__proto__
的值指向构造函数的原型对象。在原型对象上添加或修改的属性,在所有实例化出的对象上都可共享。
当在实例化的对象中访问一个属性时,首先会在该对象内部寻找,如找不到,则会向其
__proto__
指向的原型中寻找,如仍找不到,则继续向原型中 __proto__
指向的上级原型中寻找,直至找到或 Object.prototype
为止,这种链状过程即为原型链。
三十一、实现一个 EventBus
简单实现
class myEventBus {
constructor(props) {
this.events = {}
}
on (event, fn) {
const events = this.events
events[event] ? events[event].push(fn) : (events[event] = [fn])
}
emit (event, ...res) {
this.events[event] && this.events[event].forEach(fn => {
return fn.apply(this, res)
})
}
remove (event, fn) {
if (this.events[event]) {
delete this.events[event]
}
}
}
三十二、js
的垃圾回收(GC
)
1、V8
内存限制
- 64 位系统可用 1.4G 内存
- 32 位系统可用 0.7G 内存
2、V8
内存管理
JS
对象都是通过V8
进行分配管理内存的process.memoryUsage()
返回一个对象,包含了Node
进程的内存占用信息
3、内存占用结构图
var a = {name:‘yuhua’};
这句代码会做如下几步:- 将这句代码放入“代码区域
Code Segment
” - 将变量
a
放入“栈(Stack
):本地变量、指针” - 将
{name:‘yuhua’}
放入“HeapTotal
(堆):对象,闭包”
- 将这句代码放入“代码区域
- 注意:基本数据类型都在栈中,引用类型都在堆中
4、为何限制内存大小
- 因为
V8
垃圾收集工作原理导致的,1.4G 内存完全一次垃圾收集需要 1s 以上 - 这个垃圾回收这段时间(暂停时间)成为
Stop The World
,在这期间,应用的性能和响应能力都会下降
5、V8
的垃圾回收机制
V8
是基于分代的垃圾回收- 不同代垃圾回收机制也不一样,采用的算法不一样
- 按存货的时间分为新生代和老生代
6、分代
- 年龄小的是新生代,由
From
区域和To
区域两个区域组成 - 在 64 位系统里,新生代内存是 32M,
From
区域和To
区域各占 16M - 在 32 位系统里,新生代内存是 16M,
From
区域和To
区域各占 8M - 年龄大的是老生代,默认情况下:
- 64 位系统下老生代内存是 1400M
- 32 位系统下老生代内存是 700M
7、新生代采用 Scavenge
算法
Scavenge
为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。
新生代扫描的时候是一种广度优先的扫描策略
它将内存分为 from
和 to
两个空间。每次 gc
,会将 from
空间的存活对象复制到 to
空间。然后两个空间角色对换(又称反转)。
该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。
1. 过程
- 新生代区域一分为二,每个 16M,一个使用,一个空闲
- 开始垃圾回收的时候,会检查
FROM
区域中的存活对象,如果还活着,拷贝到TO
空间,所有存活对象拷贝完后,清空(释放)FROM
区域 - 然后FROM和To区域互换
2. 特点
- 新生代扫描的时候是一种广度优先的扫描策略
- 新生代的空间小,存活对象少
- 当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较差的对象会被移动到老声带,这个移动过程被称为晋升或升级
- 经历过 5 次以上的回收还存在
TO
的空间使用占比超过 25%,或者超大对象- 浏览器的
memory
中可以通过拍快照看变量是否被垃圾回收 - 置为
undefined
或null
都能将引用计数减去 1
8、老生代采用 Mark-Sweep
和 Mark-Compact
1. 基础
- 老生代垃圾回收策略分为两种
mark-sweep
标记清除- 标记活着的对象,虽然清楚在标记阶段没有标记的对象,只清理死亡对象 会出现的问题:清除后内存不连续,碎片内存无法分配
mark-compact
标记整理- 标记死亡后会对对象进行整理,活着的左移,移动完成后清理掉边界外的内存(死亡的对象)
- 老生代空间大,大部分都是活着的对象,
GC
耗时比较长 - 在
GC
期间无法想听,STOP-THE-WORLD
V8
有一个优化方案,增量处理,把一个大暂停换成多个小暂停INCREMENT-GC
- 也就是把大暂停分成多个小暂停,每暂停一小段时间,应用程序运行一会,这样垃圾回收和应用程序交替进行,停顿时间可以减少到1/6左右
2. 过程
假设有10个大小的内存,内存占用了6个,
1)Mark-Sweep
模式垃圾回收:
- 那么会给每个对象做上标记:
A b C d E f 空 空 空 空
//对上面每个对象做上标记,大写表示活着,小写表示死了
//这时候,会存在一个问题,就是内存碎片无法使用,因为小写的内存没有跟后面空空空空的内存放在一起,不能使用
- 这时候小写(死)的都会被干掉,只保留大写(活)的,导致的问题就是内存碎片无法使用
2)Mark-Compact
模式垃圾回收
- 将活的左移
A C E b d f 空 空 空 空
- 然后回收死了的区域
A C E 空 空 空 空 空 空 空
9、三种算法的对比
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少 | 少 | 双倍空间(无碎片) |
是否移动对象 | 否 | 是 | 是 |
V8
老生代主要用Mark-Sweep
,因为Mark-Compact
需要移动对象,执行速度不快。空间不够时,才会用Mark-Compact
三十三、设计模式
1、设计原则:
1. 单一职责原则(SRP
)
一个对象或方法只做一件事情。
2. 最少知识原则(LKP
)
应当尽量减少对象之间的交互。
3. 开放-封闭原则(OCP
)
软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改
2、策略模式
策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 优点:
- 策略模式利用组合、委托等技术和思想,可以避免很多if条件语句
- 策略模式提供了开放-封闭原则,使代码更容易理解和拓展
示例:
- 绩效等级和薪资计算奖金为
- 表单验证,通常会涉及到多个字段有效性判断
3、缓存代理模式
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果,提供效率以及节省开销。
缓存代理,就是将前面使用的值缓存下来,后续还有使用的话,就直接拿出来用。
4、工厂模式
工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
工厂方法模式的本意是将实际创建对象的工作推迟到子类中,工厂方法模式就是将这个大厂拆分出各个小厂,每次添加新的产品让小厂去生产,大厂负责指挥就好了。
抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。
5、单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
确保了只有一个实例
- 因为只有唯一实例,所以节省了系统资源,记住创建和销毁也需要浪费内存资源
- 避免了对资源的多重占用,比如数据库的连接
- 资源共享
前端应用场景:
- 浏览器的
window
对象。在JavaScript
开发中,对于这种只需要一个的对象,往往使用单例实现。 - 遮罩层、登陆浮窗等。
6、代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式主要有三种:保护代理、虚拟代理、缓存代理
7、迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
JS
中数组的 map
forEach
已经内置了迭代器
8、发布-订阅者模式
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。
JS中的事件就是经典的发布-订阅模式的实现
9、命令模式
用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系
命令(command
)指的是一个执行某些特定事情的指令
三十四、函数&自执行函数
1、自执行函数特点
- 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
- 对于一个常量进行赋值,在
strict
模式下会报错,非strict
模式下静默失败。 IIFE
中的函数是函数表达式,而不是函数声明。
2、函数类型
- 函数声明
- 函数表达式
- 函数构造器创建
1. 函数声明(FD
)
- 有一个特定的名称
- 在源码中的位置:要么处于程序级(
Program level
),要么处于其它函数的主体(FunctionBody
)中 - 在进入上下文阶段创建
- 影响变量对象
- 以下面的方式声明
function funName () {}
2. 函数表达式(FE
)
- 在源码中须出现在表达式的位置
- 有可选的名称
- 不会影响变量对象
- 在代码执行阶段创建
// 函数表达式
var foo = function () {} // 匿名函数表达式赋值给变量foo
var foo2 = function _foo2() {} // 外部FE通过变量“foo”来访问——foo(),而在函数内部(如递归调用),有可能使用名称“_foo”。
// 圆括号(分组操作符)内只能是表达式
(function foo() {});
// 在数组初始化器内只能是表达式
[function bar() {}];
// 逗号也只能操作表达式
1, function baz() {};
// !
!function() {}();
(function foo() {})() // 自执行函数 IIFE
(function () {})() // IIFT
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
foo.bar // 'yes'
3. 函数构造器创建的函数
我们将它与 FD
和 FE
区分开来。其主要特点在于这种函数的[[Scope
]]属性仅包含全局对象
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" 未定义
}
3、如何创建一个函数不需要 () 就可以执行
- 创建对象
- 对象里面表达式定义自执行函数
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
foo.bar // 'yes'
4、为什么有些要加 ()
有些可以不加?
当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成 FE
。
如果解析器知道它处理的是 FE
,就没必要用圆括号。
5、具名函数表达式
当函数表达式 FE
有一个名称(称为命名函数表达式,缩写为 NFE
)时,将会出现一个重要的特点。
从定义(正如我们从上面示例中看到的那样)中我们知道函数表达式不会影响一个上下文的变量对象(那样意味着既不可能通过名称在函数声明之前调用它,也不可能在声明之后调用它)。
但是,FE
在递归调用中可以通过名称调用自身。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" 是可用的
})();
“foo
” 储存在什么地方?在 foo
的活动对象中?不是,因为在 foo
中没有定义任何” foo
”。在上下文的父变量对象中创建 foo
?也不是,因为按照定义—— FE
不会影响 VO
(变量对象)——从外部调用 foo
我们可以实实在在的看到。那么在哪里呢?
当解释器在代码执行阶段遇到命名的 FE
时,在 FE
创建之前,它创建了辅助的特定对象,并添加到当前作用域链的最前端。然后它创建了 FE
,此时(正如我们在第四章 作用域链知道的那样)函数获取了[[Scope
]] 属性——创建这个函数上下文的作用域链)。此后,FE
的名称添加到特定对象上作为唯一的属性;这个属性的值是引用到 FE
上。最后一步是从父作用域链中移除那个特定的对象。
6、自执行函数示例
// 例一
+function foo(){
foo=10;//我的问题代码
console.log(foo);//方法自己
}();
console.log(typeof foo);//undefined 观察是否全局污染
// 例二
var b = 10;
(function b() {
// 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值;
// IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。
// (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等)
b = 20;
console.log(b); // [Function b]
console.log(window.b); // 10,不是20
})();
// 严格模式 会报错
var b = 10;
(function b() {
'use strict'
b = 20;
console.log(b)
})() // "Uncaught TypeError: Assignment to constant variable."
// 普通函数
function a () {
a = 1
console.log(a)
}
a() // 1
a() // a is not a function
三十五、XSS
攻击和 CSRF
攻击
1、XSS
攻击
1. 概念
XSS(Cross Site Scripting)
:跨域脚本攻击。
2. 原理
不需要你做任何的登录认证,它会通过合法的操作(比如在 url
中输入、在评论框中输入),向你的页面注入脚本(可能是 js
、hmtl
代码块等)。
3. 防范
- 编码;对于用户输入进行编码。
- 过滤;移除用户输入和事件相关的属性。(过滤
script
、style
、iframe
等节点) - 校正;使用
DOM Parse
转换,校正不配对的DOM
标签。 HttpOnly
。
4. 分类
- 反射型(非持久):点击链接,执行脚本
- 存储型(持久):恶意输入保存数据库,其他用户访问,执行脚本
- 基于
DOM
:恶意修改DOM
结构,基于客户端
2、CSRF
攻击
1. 概念
SRF(Cross-site request forgery)
:跨站请求伪造。
2. 原理
- 登录受信任网站
A
,并在本地生成Cookie
。(如果用户没有登录网站A
,那么网站B
在诱导的时候,请求网站A
的api
接口时,会提示你登录)。 - 在不登出
A
的情况下,访问危险网站B
(其实是利用了网站A
的漏洞)。
3. 防范
token
验证;- 隐藏令牌;把
token
隐藏在http
请求的head
中。 referer
验证;验证页面来源。
3、两者区别
CSRF
:需要用户先登录网站A
,获取cookie
。XSS
:不需要登录。CSRF
:是利用网站A
本身的漏洞,去请求网站A
的api
。XSS
:是向网站A
注入JS
代码,然后执行JS
里的代码,篡改网站A
的内容。
三十六、input
输入框输入即请求后端接口,频繁请求之后怎样确定最后一次接口的返回值?
1、后端返回请求值(最简单)
前端请求接口的时候会把 input 输入框中的值传给后端,此时后端返回接口数据时把前端传入的值返回回去,页面渲染时只需要进行判断即可。
2、终止上一次请求
当再次请求的时候把上次的请求终止掉:
ajax
:abort()
axios
:CancelToken
fetch
:AbortController
百度用的就是这种取消请求的方式 js:ss1.bdstatic.com/5eN1bjq8AAU…
3. 定义一个全局 ID
,接口请求之前自增,然后请求接口闭包保存此值,返回之后进行两者判断。
此种方式就是不用后端返回值,前端存储对应的值信息,进行判断处理
实现
let id = 1
function ajax() {
++id
console.log(id)
function getData () {
const newId = id
const time = Math.random() * 5000 | 0 // 定义一个随机值
console.log('time', time)
setTimeout(() => {
console.log('id newId', id, newId)
if (id === newId) { // 在此进行数据处理
console.log('this is true-->', id)
}
}, time)
}
getData()
}
// click 频繁点击出发函数
document.getElementById('ajaxbtn').onclick = function () {
ajax()
}
三十七、rem
1、定义
rem(font size of the root element)
是指相对于根元素的字体大小的单位。
1rem
等于根元素 htm
的 font-size
,即只需要设置根元素的 font-size
,其它元素使用 rem
单位时,设置成相应的百分比即可。
2、如何实现
rem(倍数) = width / (html的font-size)=> width = (html的font-size) * rem(倍数)
只要 html
的 font-size
的大小变了,width
就会自动变,所以 rem
是通过动态设置 html
的 font-size
来改变 width
的大小,以达到网页自适应大小的目的
定义公式:rem(倍数) = width / (html的font-size),根据公式我们可以得出: rem(倍数) = 设计稿宽度( imgWidth ) / 你设置的font-size( defalutSize ) rem(倍数) = 网页的实际宽度(screenWidth) / 你需要动态设置的font-size( x ) ,那么得出设置html的font-size的公式为:
<script type="text/javascript">
(function(w,d) {
function setSize() {
var screenWidth = d.documentElement.clientWidth;
var currentFontSize = screenWidth * 100 / 750;
d.documentElement.style.fontSize = currentFontSize + 'px';
}
w.addEventListener('resize',setSize);
w.addEventListener('pageShow',setSize)
w.addEventListener('DOMContentLoaded',setSize)
})(window,document)
</script>
function setHtmlSize(){
var pageWidth = window.innerWidth;
if(typeof pageWidth != "number"){
if(document.compatMode == "number"){
pageWidth = document.documentElement.clientWidth;
}else{
pageWidth = document.body.clientWidth;
}
}
var fontSize = (window.innerWidth * 100) / 750;
if(fontSize<40){
fontSize = 40;
}
//根据屏幕大小确定根节点字号
document.getElementsByTagName('html')[0].style.fontSize = fontSize + 'px';
}
function resize(){
setHtmlSize();
}
if (window.attachEvent) {
window.attachEvent("resize", resize);
} else if (window.addEventListener) {
window.addEventListener("resize", resize, false);
}
setHtmlSize();
3、以 750
宽度来算,1rem = 100px
,iphone6/7/8 plus
中设置 width: 6.5rem
元素的宽为多少?
plus
中宽度为 414
所以宽度为 414 / 750 * 6.5 * 100
0.32 rem
为
414 / 750 * 0.32 * 100
三十八、dns-prefetch
、prefetch
、preload
、defer
、async
1、dns-prefetch
域名转化为 ip
是一个比较耗时的过程,dns-prefetch
能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns
预取。
//来自百度首页
<link rel="dns-prefetch" href="//m.baidu.com">
2、prefetch
prefetch
一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch
的资源。
<link rel="prefetch" href="http://www.example.com/">
3、preload
和 prefetch
不同,prefecth
通常是加载接下来可能用到的页面资源,而 preload
是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload
不是空闲时加载,它的优先级更强,并且会占用 http
请求数量。
<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"
as
值包括
script
style
image
media
document
onload
方法是资源加载完成的回调函数
4、defer
和 async
//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
defer
和 async
都是异步(并行)加载资源,不同点是 async
是加载完立即执行,而 defer
是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded
事件触发之前。
因为 async
加载的资源是加载完执行,所以它比不能保证顺序,而 defer
会按顺序执行脚本。
三十九、浏览器渲染过程
1、浏览器渲染过程如下
- 解析
HTML
,生成DOM
树 - 解析
CSS
,生成CSSOM
树 - 将
DOM
树和CSSOM
树结合,生成渲染树(Render Tree
) Layout
(回流):根据生成的渲染树,进行回流(Layout
),得到节点的几何信息(位置,大小)Painting
(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素Display
:将像素发送给GPU
,展示在页面上。(这一步其实还有很多内容,比如会在GPU
将多个合成层合并为同一个层,并展示在页面中。而css3
硬件加速的原理则是新建合成层)
2、何时触发回流和重绘
1. 回流
- 添加或删除可见的
DOM
元素 - 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 页面一开始渲染的时候(这肯定避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
2. 重绘
- 回流一定会触发重绘
- 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:
color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
3、如果避免触发回流和重绘
1. css
- 避免使用
table
布局。 - 尽可能在
DOM
树的最末端改变class
。 - 避免设置多层内联样式。
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上 - 避免使用
CSS
表达式(例如:calc()
) CSS3
硬件加速(GPU
加速)transform
opacity
filters
Will-change
2. JavaScript
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性,修改style
的cssText
属性或者修改元素的className
值。 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM
操作,最后再把它添加到文档中 - 也可以先为元素设置
display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的DOM
操作不会引发回流和重绘 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
- 使用
css3
硬件加速,可以让transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
4、硬件加速原理
浏览器接收到页面文档后,会将文档中的标记语言解析为 DOM
树。DOM
树和 CSS
结合后形成浏览器构建页面的渲染树。
渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU
形成渲染纹理,而图层在 GPU
中 transform
是不会触发 repaint
的,最终这些使用 transform
的图层都会由独立的合成器进程进行处理。
1. 浏览器什么时候会创建一个独立的复合图层呢?
3D
或者CSS transform
<video>
和<canvas>
标签CSS filters
- 元素覆盖时,比如使用了
z-index
属性
3D
和 2D transform
的区别就在于,浏览器在页面渲染前为 3D
动画创建独立的复合图层,而在运行期间为 2D
动画创建。动画开始时,生成新的复合图层并加载为 GPU
的纹理用于初始化 repaint
。然后由 GPU
的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint
操作删除复合图层。
2. 使用硬件加速的问题
- 内存。如果
GPU
加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。 - 使用
GPU
渲染会影响字体的抗锯齿效果。这是因为GPU
和CPU
具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
四十、JSBridge
1、什么是 JSBridge
JSBridge
是一种 JS
实现的 Bridge
,连接着桥两端的 Native
和 H5
。它在 APP
内方便地让 Native
调用 JS
,JS
调用 Native
,是双向通信的通道。JSBridge
主要提供了 JS
调用 Native
代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。
2、H5
和 native
的区别
name | H5 | Native |
---|---|---|
稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
灵活性 | 版本迭代快,上线灵活 | 迭代慢,需要应用商店审核,上线速度受限制 |
受网速 影响 | 较大 | 较小 |
流畅度 | 有时加载慢,给用户“卡顿”的感觉 | 加载速度快,更加流畅 |
用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 需要维护两套代码 |
3、JSBridge
的用途
JSBridge
就像其名称中的『Bridge
』的意义一样,是 Native
和非 Native
之间的桥梁,它的核心是 构建 Native
和非 Native
间消息通信的通道,而且是 双向通信的通道。
双向通信的通道:
JS
向Native
发送消息 : 调用相关功能、通知Native
当前JS
的相关状态等。Native
向JS
发送消息 : 回溯调用结果、消息推送、通知JS
当前Native
的状态等。
4、JSBridge
流程
H5
->通过某种方式触发一个url
->Native
捕获到url
,进行分析->原生做处理->Native
调用H5
的JSBridge
对象传递回调。
实现流程
- 第一步:设计出一个
Native
与JS
交互的全局桥对象 - 第二步:
JS
如何调用Native
- 第三步:
Native
如何得知api
被调用 - 第四步:分析
url-
参数和回调的格式 - 第五步:
Native
如何调用JS
- 第六步:
H5
中api
方法的注册以及格式
5、JSBridge
的实现原理
JavaScript
调用Native
推荐使用 注入API
的方式(iOS6
忽略,Android 4.2
以下使用WebViewClient
的onJsPrompt
方式)。Native
调用JavaScript
则直接执行拼接好的JavaScript
代码即可。
React Native
的 iOS
端举例:JavaScript
运行在 JSCore
中,实际上可以与上面的方式一样,利用注入 API
来实现 JavaScript
调用 Native
功能。不过 React Native
并没有设计成 JavaScript
直接调用 Object-C
,而是 为了与 Native
开发里事件响应机制一致,设计成 需要在 Object-C
去调 JavaScript
时才通过返回值触发调用。原理基本一样,只是实现方式不同。
1. Native
调 JS
1)安卓
native
调用 js
比较简单,只要遵循:”javascript:
方法名(‘参数,需要转为字符串’)”的规则即可。
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override public void onReceiveValue(String value) { //这里的value即为对应JS方法的返回值 }
});
2)IOS
Native
通过 stringByEvaluatingJavaScriptFromString
调用 Html
绑定在 window
上的函数。
2. JS
调 Native
1)安卓
Native
中通过 addJavascriptInterface
添加暴露出来的 JS
桥对象,然后再该对象内部声明对应的 API
方法。
private Object getJSBridge(){
Object insertObj = new Object(){ @JavascriptInterface public String foo(){ return "foo";
} @JavascriptInterface public String foo2(final String param){ return "foo2:" + param;
}
}; return insertObj;
}
2)IOS
Native
中通过引入官方提供的 JavaScriptCore
库(iOS7
以上),然后可以将 api
绑定到 JSContext
上(然后 Html
中 JS
默认通过 window.top.*
可调用)。
6、JSBridge
接口实现
JSBridge
的接口主要功能有两个:
调用 Native
(给 Native
发消息) 和 接被 Native
调用(接收 Native
消息)。
1. 消息都是单向的,那么调用 Native
功能时 Callback
怎么实现的?
JSBridge
的 Callback
,其实就是 RPC
框架的回调机制。当然也可以用更简单的 JSONP
机制解释:
当发送
JSONP
请求时,url
参数里会有callback
参数,其值是 当前页面唯一 的,而同时以此参数值为key
将回调函数存到window
上,随后,服务器返回script
中,也会以此参数值作为句柄,调用相应的回调函数。
callback
参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge
:用一个自增的唯一 id
,来标识并存储回调函数,并把此 id
以参数形式传递给 Native
,而 Native
也以此 id
作为回溯的标识。这样,即可实现 Callback
回调逻辑。
(function () {
var id = 0,
callbacks = {},
registerFuncs = {};
window.JSBridge = {
// 调用 Native
invoke: function(bridgeName, callback, data) {
// 判断环境,获取不同的 nativeBridge
var thisId = id ++; // 获取唯一 id
callbacks[thisId] = callback; // 存储 Callback
nativeBridge.postMessage({
bridgeName: bridgeName,
data: data || {},
callbackId: thisId // 传到 Native 端
});
},
receiveMessage: function(msg) {
var bridgeName = msg.bridgeName,
data = msg.data || {},
callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回
responstId = msg.responstId;
// 具体逻辑
// bridgeName 和 callbackId 不会同时存在
if (callbackId) {
if (callbacks[callbackId]) { // 找到相应句柄
callbacks[callbackId](msg.data); // 执行调用
}
} elseif (bridgeName) {
if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄
var ret = {},
flag = false;
registerFuncs[bridgeName].forEach(function(callback) => {
callback(data, function(r) {
flag = true;
ret = Object.assign(ret, r);
});
});
if (flag) {
nativeBridge.postMessage({ // 回调 Native
responstId: responstId,
ret: ret
});
}
}
}
},
register: function(bridgeName, callback) {
if (!registerFuncs[bridgeName]) {
registerFuncs[bridgeName] = [];
}
registerFuncs[bridgeName].push(callback); // 存储回调
}
};
})();
7、JSBridge
如何引用
1. 由 Native
端进行注入
注入方式和 Native
调用 JavaScript
类似,直接执行桥的全部代码。
优点:桥的版本很容易与 Native
保持一致,Native 端不用对不同版本的 JSBridge
进行兼容;与此同时,
缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript
端在调用接口时,需要优先。
2. 由 JavaScript
端引用
直接与 JavaScript
一起执行。
优点:JavaScript
端可以确定 JSBridge
的存在,直接调用即可;
缺点:如果桥的实现方式有更改,JSBridge
需要兼容多版本的 Native Bridge
或者 Native Bridge
兼容多版本的 JSBridge
。
四十一、web worker
1、什么是 web worker
?有哪些好处?有哪些问题?
Web Worker
就是为 JavaScript
创造多线程环境,允许主线程创建 Worker
线程,将一些任务分配给后者运行。在主线程运行的同时,Worker
线程在后台运行,两者互不干扰。等到 Worker
线程完成计算任务,再把结果返回给主线程。
好处:
好处就是,一些计算密集型或高延迟的任务,被 Worker
线程负担了,主线程(通常负责 UI
交互)就会很流畅,不会被阻塞或拖慢。
问题:
Worker
线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker
比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
2、使用 web worker
有哪些限制?
1. 同源限制
分配给 worker
的脚本文件,必须与主线程脚本文件同源。
2. DOM
限制
worker
线程无法读取主线程所在网页的 DOM
对象,无法使用 document
、window
、parent
这些对象,可以使用 navigator
和 location
对象。
3. 通信限制
worker
线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。
4. 脚本限制
worker
线程不能执行 alert
方法和 confirm
方法,但是可以发出 ajax
请求。
5. 文件限制
worker
线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file://
文件。
3、worker
线程怎样监听主线程的消息的?如何发送消息的?worker
线程又是如何关闭的?
Worker
线程内部需要有一个监听函数,监听 message
事件。
// 监听
self.addEventListener('message', function (e) {
// 发送消息
self.postMessage('You said: ' + e.data);
}, false);
关闭 worker
线程
1)主线程关闭 worker
线程
worker.terminate()
2)worker
线程关闭
self.close()
4、worker
线程如何加载其他脚本?
importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')
5、主线程和 worker
线程的 API
主线程 | worker 线程 |
---|---|
Worker.onerror :指定 error 事件的监听函数 | self.name : Worker 的名字 |
Worker.onmessage :指定 message 事件的监听函数 | self.onmessage :指定 message 事件的监听函数 |
Worker.onmessageerror :指定 messageerror 事件的监听函数 | self.onmessageerror :指定 messageerror 事件的监听函数 |
Worker.postMessage() :向 Worker 线程发送消息 | self.close() :关闭 Worker 线程 |
Worker.terminate() :立即终止 Worker 线程 | self.postMessage() :向产生这个 Worker 线程发送消息 |
self.importScripts() :加载 JS 脚本 |
四十二、webSocket
1、为什么需要 webSocket
?有什么特点?
1. 优势:
- 支持双向通信,实时性更强;
- 更好的二进制支持;
ws
客户端与服务端数据交换时,数据包头部较小,更好的控制开销;- 支持拓展。
2. 特点:
- 建立在
TCP
协议之上,服务器端的实现比较容易。 - 与
HTTP
协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用HTTP
协议,因此握手时不容易屏蔽,能通过各种HTTP
代理服务器。 - 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是
ws
(如果加密,则为wss
),服务器网址就是URL
。
2、webSocket
的链接状态?
0 (WebSocket.CONNECTING)
正在链接中1 (WebSocket.OPEN)
已经链接并且可以通讯2 (WebSocket.CLOSING)
连接正在关闭3 (WebSocket.CLOSED)
连接已关闭或者没有链接成功
掘金不能发布六万字文章,所以拆成了三部分