此篇文章
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:CancelTokenfetch: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 值包括
scriptstyleimagemediadocumentonload方法是资源加载完成的回调函数
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加速)transformopacityfiltersWill-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)连接已关闭或者没有链接成功
掘金不能发布六万字文章,所以拆成了三部分