前端面试总结八

1.介绍一下标准的CSS的盒子模型?低版本IE的盒子模型有什么不同的?
有两种, IE 盒子模型、W3C 盒子模型

盒模型: 内容(content)、填充(padding)、边界(margin)、 边框(border);

区 别: IE的content部分把 border 和 padding计算了进去

2.PNG,GIF,JPG的区别及如何选
GIF:

  • 256色
  • 无损,编辑 保存时候,不会损失。
  • 支持简单动画。
  • 支持boolean透明,也就是要么完全透明,要么不透明

JPEG:

  • millions of colors
  • 有损压缩, 意味着每次编辑都会失去质量。
  • 不支持透明。
  • 适合照片,实际上很多相机使用的都是这个格式。

PNG:
无损,其实PNG有好几种格式的,一般分为两类:PNG8和truecolor PNGs;
(1)与GIF相比:

  • 它通常会产生较小的文件大小。
  • 它支持阿尔法(变量)透明度。
  • 无动画支持

(2)与JPEG相比:

  • 文件更大
  • 无损
  • 因此可以作为JPEG图片中间编辑的中转格式。

结论:

  • JPEG适合照片
  • GIF适合动画
  • PNG8适合其他任何种类——图表,buttons,背景,图表等等。

3.CSS3动画(简单动画的实现,如旋转等)
依靠CSS3中提出的三个属性:transition、transform、animation

  • transition:定义了元素在变化过程中是怎么样的,包含transition-property、transition-duration、transition-timing-function、transition-delay。
  • transform:定义元素的变化结果,包含rotate、scale、skew、translate。
  • animation:动画定义了动作的每一帧(@keyframes)有什么效果,包括animation-name,animation-duration、animation-timing-function、animation-delay、animation-iteration-count、animation-direction

4.Base64
为什么Base64编码可以内联到HTML中?
我们知道HTTP协议是文本协议,不同于常规的二进制协议那样直接进行二进制传输。Base64编码是 从二进制到字符的过程,可用于在HTTP环境下传递较长的标识信息。

什么是Base64编码
首先Base64是一种编码算法,为什么叫做Base64呢?其实原因也很简单,是因为该算法共包含64个字符。包括大小写拉丁字母各26个、数字10个、加号 + 和斜杠 / ,共64个字符。此外还有等号 = 用来作为后缀用途。
但,为什么Base64编码算法只支持64个字符呢?
首先,我们先回顾下ASCII码。ASCII码的范围是0-127,其中0-31和127是控制字符,共33个。其余95个,即32-126是可打印字符,包括数字、大小写字母、常用符号等。
早期的一些传输协议,例如邮件传输协议SMTP,只能传输可打印的ASCII字符。这样原本的8bit字节码(0-255)就会超出使用范围,从而导致无法传输。
这时,就产生了Base64编码,它利用 6bit字符来表达原本的8bit字符

Base64编码原理
首先,6bit显然不够容纳8bit的数据。6和8的最小公倍数是24,所以我们用4个Base64字符刚好能够表示三个传统的8bit字符。如下所示,字符串 Man 的编码图解如下:
在这里插入图片描述
Man 的编码结果为 TWFu ,显然,Base64编码会多1/3的长度,这也解释了文中开头的疑问,为什么Base64编码后的体积会大1/3。
Man 这个字符串的长度刚好是3,我们能用4个Base64来表示。如果待编码的字符串长度不是三的倍数时应该怎么处理呢?
这是需要做一个特殊处理,假设待编码字符串长度为10。这前9个字符可以用12个Base64字符表示。第10个字符的前6bit作为一个Base64字符, 剩下的2bit后面需要先补0,补到6位(此处补4个0) 作为第二个Base64字符,至于第三个和第四个Base64字符,虽然没有相对应的内容,我们仍需 以 = 填充 。
如下图所示, A 对应的Base64编码为 QQ== , BC 对应的Base64编码为 QkM= 。
在这里插入图片描述
最后的问题就是解码啦,解码的过程比较简单。 去掉末尾的等号 = 。剩下的Base64字符,每8bit组成一个8bit字节,最后剩余不足8位的丢弃即可。

图片转换成base64格式的优缺点
优点
(1)base64格式的图片是文本格式,占用内存小,转换后的大小比例大概为1/3,降低了资源服务器的消耗;
(2)网页中使用base64格式的图片时,不用再请求服务器调用图片资源,减少了服务器访问次数。

缺点
(1)base64格式的文本内容较多,存储在数据库中增大了数据库服务器的压力;
(2)网页加载图片虽然不用访问服务器了,但因为base64格式的内容太多,所以加载网页的速度会降低,可能会影响用户的体验。
(3)base64无法缓存,要缓存只能缓存包含base64的文件,比如js或者css,这比直接缓存图片要差很多,而且一般HTML改动比较频繁,所以等同于得不到缓存效益。

参考:base64原理浅析

5.new操作符具体干了什么呢

var Func=function(){
};
var func=new Func ();

new共经过了4几个阶段
(1)创建一个空对象

var obj=new Object();

(2)设置原型链

obj.__proto__= Func.prototype;

(3)让Func中的this指向obj,并执行Func的函数体。

var result =Func.call(obj);

(4)判断Func的返回值类型:
如果是值类型,返回obj。如果是引用类型,就返回这个引用类型的对象。

if (typeof(result) == "object"){
  func=result;
}
else{
    func=obj;;
}

6.面向过程与面向对象编程的区别和优缺点
面向过程编程 POP(Process-oriented programming)
面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步步实现 ,使用的时候再一个一个的依次调用就可以了。

面向对象编程OOP (Object Oriented Programming)
面向对象是把事务分解成为一个个对象,然后由对象之间分工与合作。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工。
面向对象编程具有灵活、代码可复用、容易维护和开发的优点,更适合多人合作的大型软件项目。
面向对象的特性:封装、继承、多态

面向过程与面向对象的优缺点
面向过程
  优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源,比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  缺点:没有面向对象易维护、易复用、易扩展

面向对象
  优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护
  缺点:性能比面向过程低

7.HTML全局属性(global attribute)有哪些?

  • accesskey 规定激活元素的快捷键;
  • class 规定元素的一个或多个类名(引用样式表中的类);
  • contenteditable 规定元素内容是否可编辑;
  • contextmenu 规定元素的上下文菜单。上下文菜单在用户点击元素时显示。
  • data-* 用于存储页面或应用程序的私有定制数据。
  • dir 规定元素中内容的文本方向。
  • draggable 规定元素是否可拖动。
  • dropzone 规定在拖动被拖动数据时是否进行复制、移动或链接。
  • hidden 样式上会导致元素不显示,但是不能用这个属性实现样式。
  • id 规定元素的唯一 id。
  • lang 规定元素内容的语言。
  • spellcheck 规定是否对元素进行拼写和语法检查。
  • style 规定元素的CSS行内元素。
  • tabindex 规定元素的tab键次序。
  • title 规定有关元素的额外信息。
  • translate 规定是否应该翻译元素内容。

参考:https://www.nowcoder.com/questionTerminal/5505ebcd04df48dd91e32977cb9a9b11

8.说说超链接target属性的取值和作用?

  • _blank:在新窗口中打开链接文档
  • _self:默认。在相同的框架中打开链接文档
  • _top:在整个窗口中打开链接文档
  • _parent:在父级框架中集中打开
  • _framename:在指定的框架中打开链接文档

9.data-属性的作用是什么?
data-为H5新增的为前端开发者提供自定义的属性,这些属性集可以通过对象的 dataset 属性获取,不支持该属性的浏览器可以通过 getAttribute 方法获取 :

需要注意的是:data-之后的以连字符分割的多个单词组成的属性,获取的时候使用驼峰风格。 所有主流浏览器都支持 data-* 属性。
即:当没有合适的属性和元素时,自定义的 data 属性是能够存储页面或 App 的私有的自定义数据。

10.介绍一下你对浏览器内核的理解?
主要分成两部分:渲染引擎(layout engineer或 Rendering Engine) 和 JS 引擎。

渲染引擎:负责取得网页的内容(HTML、 XML 、图像等等)、整理讯息(例如加入 CSS 等),以及计算网页的显示方式,然后会输出至显示器或打印机。浏览器的内核的不同对于网页的语法解释会有不同,所以渲染的效果也不相同。所有网页浏览器、电子邮件客户端以及其它需要编辑、显示网络内容的应用程序都需要内核。

JS引擎则:解析和执行 javascript 来实现网页的动态效果。

最开始渲染引擎和JS引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。

11.Label的作用是什么,是怎么用的?
label标签来定义表单控制间的关系 , 当用户选择该标签时,浏览器会自动将焦点转到和标签相关的表单控件上。

<label for='Name'>Number:</label>
<input type= text  name='Name' id='Name'/>
<label>Date:<input type='text' name='B'/></label>

注意:label的for属性值要与后面对应的input标签id属性值相同

<label for='Name'>Number:</label>
<input type= text  name='Name' id='Name'/>

12.如何实现浏览器内多个标签页之间的通信?
(1)使用localStorage
使用localStorage.setItem(key,value);添加内容
使用storage事件监听添加、修改、删除的动作

window.addEventListener("storage",function(event){
        $("#name").val(event.key+=+event.newValue);
});

(2)使用cookie+setInterval

<input id="name"><input type="button" id="btnOK" value="发送">

JS代码-页面1

 $(function(){
        $("#btnOK").click(function(){
            varname=$("#name").val();
            document.cookie="name="+name;
        });
    });

JS代码-页面2

    //获取Cookie天的内容
    function getKey(key) {
        return JSON.parse("{\""+ document.cookie.replace(/;\s+/gim,"\",\"").replace(/=/gim, "\":\"") +"\"}")[key];
    }
    //每隔1秒获取Cookie的内容
    setInterval(function(){
        console.log(getKey("name"));
     },1000);

13.如何在页面上实现一个圆形的可点击区域?
(1)纯html: map+area;

<img id="blue" class="click-area" src="blue.gif" usemap="#Map" /> 
<map name="Map" id="Map" class="click-area">  <area shape="circle" coords="50,50,50"/>
</map>
#blue{
 cursor:pointer;
 width:100px;
 height:100px;
}

(2)纯CSS: border-radius: 50%;

//第二种 使用CSS border-radius
<div id="red" class="click-area" ></div>
#red{  
 cursor:pointer;
 background:red;  
 width:100px;  
 height:100px;  
 border-radius:50%;  
} 

(3)纯js: 利用点在圆内点在圆外

//第三种 使用js检测鼠标位置,需要求一个点在不在圆上简单算法、获取鼠标坐标等等
<div id="yellow" class="click-area" ></div>
$("#yellow").on('click',function(e) {    
  var r = 50; 
  var x1 = $(this).offset().left+$(this).width()/2;            
  var y1 = $(this).offset().top+$(this).height()/2;   
  var x2= e.clientX;  
  var y2= e.clientY;    
  var distance = Math.abs(Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2)));    
  if (distance <= 50)  
    alert("Yes!");    
});  

14.title与h3的区别、b与strong的区别、i与em的区别?
title属性没有明确意义只表示是个标题, H1 则表示层次明确的标题,对页面信息的抓取也有很大的影响;
strong是标明重点内容,有语气加强的含义,使用阅读设备阅读网络时: <strong> 会重读,而 <B> 是展示强调内容。
i内容展示为斜体, em 表示强调的文本.

15.实现不使用 border 画出1px高的线,在不同浏览器的标准模式与怪异模式下都能保持一致的效果?

<div style="width:100%;height:1px;background-color:black"></div>

16.Null和undefined的区别
null表示没有对象,即该处不应该有值,undefined表示缺少值,即此处应该有值,但没有定义.
(1)null是一个表示”无”的对象,转为数值是为0,undefined是一个表示”无”的原始值,转为数值时为NaN。当声明的变量还未被初始化时,能量的默认值为undefined
(2)Null用来表示尚未存在的对象,常用来表示函数企图返回一个不存在的对象
(3)Undefined表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:
  a、变量被声明了,但没有赋值时,就等于undefined
  b、调用函数时,应该提供的参数没有提供,该参数等于undefined
  c、对象没有赋值属性,该属性的值为undefined
  d、函数没有返回值时,默认返回undefined
(4)null表示”没有对象”,即该处不应该有值。典型用法是:
  a、作为函数的参数,表示该函数的参数不是对象
  b、作为对象原型链的终点

console.log(null==undefined);    //true  因为两者都默认转换成了false
console.log(typeof undefined);    //"undefined"  
console.log(typeof null);       //"object"  
console.log(null===undefined);    //false   "==="表示绝对相等,null和undefined类型是不一样的,所以输出“false”

17.cookie和session
** 为什么需要cookie和session **
在Web发展史中,我们知道浏览器与服务器间采用的是 http协议,而这种协议是无状态的,所以这就导致了服务器无法知道是谁在浏览网页,但很明显,一些网页需要知道用户的状态,例如登陆,购物车等。
所以为了解决这一问题,先后出现了四种技术,分别是隐藏表单域,URL重写,cookie,session,而用的最多也是比较重要的就是cookie和session了。

** Cookie **
cookie是浏览器保存在用户电脑上的一小段文本,通俗的来讲就是当一个用户通过 http访问到服务器时,服务器会将一些 Key/Value键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器时,数据通过请求头又被完整地给带回服务器,服务器根据这些信息来判断不同的用户。
cookie是浏览器保存在用户电脑上的一小段文本,通俗的来讲就是当一个用户通过 http访问到服务器时,服务器会将一些 Key/Value键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器时,数据通过请求头又被完整地给带回服务器,服务器根据这些信息来判断不同的用户。

Cookie的创建
当前 Cookie有两个版本,分别对应两种设置响应头:“Set-Cookie”和 “Set-Cookie2”。在Servlet中并不支持Set-Cookie2,所以我们来看看Set-Cookie的属性项:
在这里插入图片描述
这些属性项,其他的都说的很清楚了,我们来看看Domain有什么用:
现在,我们假设这里有两个域名:
域名A:a.b.f.com.cn 域名B:c.d.f.com.cn
显然,域名A和域名B都是 f.com.cn的子域名

  • 如果我们在域名A中的Cookie的domain设置为f.com.cn,那么f.com.cn及其子域名都可以获取这个Cookie,即域名A和域名B都可以获取这个Cookie
  • 如果域名A和域名B同时设置Cookie的doamin为f.com.cn,那么将出现覆盖的现象
  • 如果域名A没有显式设置Cookie的domain方法,那么domain就为a.b.f.com.cn,不一样的是,这时,域名A的子域名将无法获取这个Cookie

好的,现在了解完了Set-Cookie的属性项,开始创建Cookie
Web服务器通过发送一个称为Set-Cookie的http消息来创建一个Cookie:
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
这里我们思考一个问题,当我们在服务器创建多个Cookie时,这些Cookie最终是在一个Header项中还是以独立的Header存在的呢?
在这里插入图片描述
我们可以看到,构建http返回字节流时是将Header中所有的项顺序写出,而没有进行任何修改。所以可以想象在浏览器在接收http返回的数据时是分别解析每一个Header项。
接着,在客户端进行保存,如何保存呢?这里又要对Cookie进行进一步的了解

Cookie的分类

  • 会话级别Cookie:所谓会话级别Cookie,就是在浏览器关闭之后Cookie就会失效。
  • 持久级别Cookie:保存在硬盘的Cookie,只要设置了过期时间就是硬盘级别Cookie。

好的,现在cookie保存在了客户端,当我们去请求一个URL时,浏览器会根据这个URL路径将符合条件的Cookie放在请求头中传给服务器。

Session
Cookie是有大小限制和数量限制的,并且越来越多的Cookie代表客户端和服务器的传输量增加,可不可以每次传的时候不传所有cookie值,而只传一个唯一ID,通过这个ID直接在服务器查找用户信息呢?答案是有的,这就是我们的session。
Session是基于Cookie来工作的,同一个客户端每次访问服务器时,只要当浏览器在第一次访问服务器时,服务器设置一个id并保存一些信息(例如登陆就保存用户信息,视具体情况),并把这个id通过Cookie存到客户端,客户端每次和服务器交互时只传这个id,就可以实现维持浏览器和服务器的状态,而这个ID通常是NAME为JSESSIONID的一个Cookie。
实际上,有四种方式让Session正常工作:

  • 通过URL传递SessionID
    当浏览器不支持Cookie功能时,浏览器会将用户的SessionCookieName(默认为JSESSIONID)重写到用户请求的URL参数中。格式:/path/Servlet;name=value;name2=value2?Name3=value3
  • 通过Cookie传递SessionID
  • 通过SSL传递SessionID
    会根据javax.servlet.request.ssl_session属性值设置SessionID。
    注:如果客户端支持Cookie,又通过URL重写,Tomcat仍然会解析Cookie中的SessionID并覆盖URL中的SessionID
  • 通过隐藏表单传递SessionID

*** session工作原理 ***
在这里插入图片描述
一、创建session
当客户端访问到服务器,服务器会为这个客户端通过request.getSession()方法创建一个Session,如果当前SessionID还没有对应的HttpSession对象,就创建一个新的,并添加到org.apache.catalina.Manager的sessions容器中保存,这就做到了对状态的保持。当然,这个SessionID是唯一的

二、session保存
由图可知,session对象已经保存在了Manager类中,StandardManager作为实现类,通过requestedSessionId从StandardManager的sessions集合中取出StandardSession对象。
我们来看看StandardManager时如何对所有StandardSession对象进行生命周期管理
当Servlet容器关闭:

StandardManager将持久化没过期的StandardSession对象(必须调用Servlet容器中的stop和start命令,不能直接kill)

当Servlet容器重启时:

StandardManager初始化会重读这个文件,解析出所有session对象。

三、session的销毁
这里有一个误区,也是我之前的错误理解,就是我将session的生命周期理解成一次会话,浏览器打开就创建,浏览器关闭就销毁,这样理解是错的!!
session的声明周期是从创建到超时过期
也就是说,当session创建后,浏览器关闭,会话级别的Cookie被销毁,如果没有超过设定时间,该SessionID对应的session是没有被销毁的,
检查session失效
检查每个Session是否失效是在Tomcat的一个后台线程完成的(backgroundProcess()方法中);除了后台进程检验session是否失效外,调用request.getSession()也会检查该session是否过期,当然,调用这种方法如果过期的话又会重新创建一个新的session。

二者的异同
相同点(有关系的地方):

  • Session和Cookie都是为了让http协议有状态而存在
  • Session通过Cookie工作,Cookie传输的SessionID让Session知道这个客户端到底是谁

不同点:
Session将信息保存到服务器,Cookie将信息保存在客户端

工作流程
当浏览器第一次访问服务器时,服务器创建Session并将SessionID通过Cookie带给浏览器保存在客户端,同时服务器根据业务逻辑保存相应的客户端信息保存在session中;客户端再访问时上传Cookie,服务器得到Cookie后获取里面的SessionID,来维持状态。

参考:一文彻底搞懂cookie和session

18.cookie、session、sessionStorage、localStorage 之间的区别及应用场景
cookie

  • 由服务端生成,保存在客户端(由于前后端有交互,所以安全性差,且浪费带宽)
  • 存储大小有限(最大 4kb )
  • 存储内容只接受 String 类型
  • 保存位置:
    若未设置过期时间,则保存在 内存 中,浏览器关闭后销毁
    若设置了过期时间,则保存在 系统硬盘 中,直到过期时间结束后才消失(即使关闭浏览器)
  • 数据操作不方便,原生接口不友好,需要自己封装
  • 应用场景
    判断用户是否登陆过网站,以便下次登录时能够实现自动登录(或者记住密码)
    保存登录时间、浏览次数等信息

session

  • 是后端关心的内容,依赖于 cookie(sessionID 保存在cookie中)
  • 保存在服务端
  • 存储大小无限制
  • 支持任何类型的存储内容
  • 保存位置:服务器内存,若访问较多会影响服务器性能

webStorage
webStorage 是 html5 提供的本地存储方案,包括两种方式:sessionStorage 和 localStorage。相比 cookie 这种传统的客户端存储方式,webStorage 有许多优点:

  • 保存在客户端,不与服务器通信,因此比 cookie 更安全、速度更快
  • 存储空间有限,但比 cookie 大(5MB)
  • 仅支持 String 类型的存储内容(和 cookie 一样)
  • html5 提供了原生接口,数据操作比 cookie 方便
    setItem(key, value) 保存数据,以键值对的方式储存信息。
    getItem(key) 获取数据,将键值传入,即可获取到对应的value值。
    removeItem(key) 删除单个数据,根据键值移除对应的信息。
    clear() 删除所有的数据
    key(index) 获取某个索引的key

localStorage

  • 持久化的本地存储,浏览器关闭重新打开数据依然存在(除非手动删除数据)。
  • 应用场景:长期登录、判断用户是否已登录,适合长期保存在本地的数据。

sessionStorage

  • 浏览器窗口关闭后数据被销毁。
  • 应用场景:敏感账号一次性登录。

总结
综上所述,我们可以知道,cookie 和 webStorage( localStorage、sessionStorage )是前端工程师关心的内容,session 是后端关心的内容。
cookie 存储量小、安全性差、数据操作接口不友好,而 webStorage 存储量较大、安全性较高、数据接口友好。
若要持久的存储方式,推荐使用 localStorage
若要一次性会话的存储,推荐使用 sessionStorage

参考:cookie、session、sessionStorage、localStorage 之间的区别及应用场景

19.Vue的响应式原理(MVVM)深入解析
如何实现一个响应式对象
最近在看 Vue 的源码,其中最核心基础的一块就是 Observer/Watcher/Dep, 简而言之就是,Vue 是如何拦截数据的读写, 如果实现对应的监听,并且特定的监听执行特定的回调或者渲染逻辑的。总的可以拆成三大块来说。这一块,主要说的是 Vue 是如何将一个 plain object 给处理成 reactive object 的,也就是,Vue 是如何拦截拦截对象的 get/set 的

我们知道,用 Object.defineProperty 拦截数据的 get/set 是 vue 的核心逻辑之一。这里我们先考虑一个最简单的情况 一个 plain obj 的数据,经过你的程序之后,使得这个 obj 变成 Reactive Obj (不考虑数组等因素,只考虑最简单的基础数据类型,和对象):

如果这个 obj 的某个 key 被 get, 则打印出 get ${key} - ${val} 的信息
如果这个 obj 的某个 key 被 set, 如果监测到这个 key 对应的 value 发生了变化,则打印出 set ${key} - ${val} - ${newVal} 的信息。
对应的简要代码如下:
Observer.js

export class Observer {
  constructor(obj) {
    this.obj = obj;
    this.transform(obj);
  }
  // 将 obj 里的所有层级的 key 都用 defineProperty 重新定义一遍, 使之 reactive 
  transform(obj) {
    const _this = this;
    for (let key in obj) {
      const value = obj[key];
      makeItReactive(obj, key, value);
    }
  }
}
function makeItReactive(obj, key, val) {
  // 如果某个 key 对应的 val 是 object, 则重新迭代该 val, 使之 reactive 
  if (isObject(val)) {
    const childObj = val;
    new Observer(childObj);
  }
  // 如果某个 key 对应的 val 不是 Object, 而是基础类型,我们则对这个 key 进行 defineProperty 定义 
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.info(`get ${key}-${val}`)
      return val;
    },
    set: (newVal) => {
      // 如果 newVal 和 val 相等,则不做任何操作(不执行渲染逻辑)
      if (newVal === val) {
        return;
      }
      // 如果 newVal 和 val 不相等,且因为 newVal 为 Object, 所以先用 Observer迭代 newVal, 使之 reactive, 再用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
      else if (isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Object`);
        new Observer(newVal);
        val = newVal;
      }
      // 如果 newVal 和 val 不相等,且因为 newVal 为基础类型, 所以用 newVal 替换掉 val, 再执行对应操作(渲染逻辑)
      else if (!isObject(newVal)) {
        console.info(`set ${key} - ${val} - ${newVal} - newVal is Basic Value`);
        val = newVal;
      }
    }
  })
}

function isObject(data) {
  if (typeof data === 'object' && data != 'null') {
    return true;
  }
  return false;
}

index.js

import { Observer } from './source/Observer.js';
// 声明一个 obj,为 plain Object
const obj = {
  a: {
    aa: 1
  },
  b: 2,
}
// 将 obj 整体 reactive 化
new Observer(obj);
// 无输出
obj.b = 2;
// set b - 2 - 3 - newVal is Basic Value
obj.b = 3;
// set b - 3 - [object Object] - newVal is Object
obj.b = {
  bb: 4
}
// get b-[object Object]
obj.b;
// get a-[object Object]
obj.a;
// get aa-1
obj.a.aa
// set aa - 1 - 3 - newVal is Basic Value
obj.a.aa = 3

这样,我们就完成了 Vue 的第一个核心逻辑, 成功把一个任意层级的 plain object 转化成 reactive object

如何实现一个 watcher
前面讲的是如何将 plain object 转换成 reactive object. 接下来讲一下,如何实现一个watcher.
实现的伪代码应如下:
伪代码

// 传入 data 参数新建新建一个 vue 对象
const v = new Vue({
    data: {
        a:1,
        b:2,
    }
});
// watch data 里面某个 a 节点的变动了,如果变动,则执行 cb
v.$watch('a',function(){
    console.info('the value of a has been changed !');
});

//  watch data 里面某个 b 节点的变动了,如果变动,则执行 cb
v.$watch('b',function(){
    console.info('the value of b has been changed !');
})

所以我们自然而然的想到,对某个 key 的 $watch 方法应该是新建了一个 watcher 的实例的. 并且,vue 也是这样实现的. 简要的 Vue 类的实现如下 (只贴出相关代码)
Vue.js

// 引入将上面中实现的 Observer
import { Observer } from './Observer.js';
import { Watcher } from './Watcher.js';

export default class Vue {
  constructor(options) {
    // 在 this 上挂载一个公有变量 $options ,用来暂存所有参数
    this.$options = options
    // 声明一个私有变量 _data ,用来暂存 data
    let data = this._data = this.$options.data
    // 在 this 上挂载所有 data 里的 key 值, 这些 key 值对应的 get/set 都被代理到 this._data 上对应的同名 key 值
    Object.keys(data).forEach(key => this._proxy(key));
    // 将 this._data 进行 reactive 化
    new Observer(data, this)
  }
  // 对外暴露 $watch 的公有方法,可以对某个 this._data 里的 key 值创建一个 watcher 实例
  $watch(expOrFn, cb) {
    // 注意,每一个 watcher 的实例化都依赖于 Vue 的实例化对象, 即 this
    new Watcher(this, expOrFn, cb)
  }
  //  将 this.keyName 的某个 key 值的 get/set 代理到  this._data.keyName 的具体实现
  _proxy(key) {
    var self = this
    Object.defineProperty(self, key, {
      configurable: true,
      enumerable: true,
      get: function proxyGetter() {
        return self._data[key]
      },
      set: function proxySetter(val) {
        self._data[key] = val
      }
    })
  }
}

Watch.js

// 引入Dep.js, 是什么我们待会再说
import { Dep } from './Dep.js';

export class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    // 初始化 watcher 时, vm._data[this.expOrFn] 对应的 val
    this.value = this.get();
  }
  // 用于获取当前 vm._data 对应的 key = expOrFn 对应的 val 值
  get() {
    Dep.target = this;
    const value = this.vm._data[this.expOrFn];
    Dep.target = null;
    return value;
  }
  // 每次 vm._data 里对应的 expOrFn, 即 key 的 setter 被触发,都会调用 watcher 里对应的 update方法
  update() {
    this.run();
  }
  run() {
    // 这个 value 是 key 被 setter 调用之后的 newVal, 然后比较 this.value 和 newVal, 如果不相等,则替换 this.value 为 newVal, 并执行传入的cb.
    const value = this.get();
    if (value !== this.value) {
      this.value = value;
      this.cb.call(this.vm);
    }
  }
}

对于什么是 Dep, 和 Watcher 里的 update() 方法到底是在哪个时候被谁调用的,后面会说

如何收集 watcher 的依赖
前面我们讲了 watcher 的大致实现,以及 Vue 代理 data 到 this 上的原理。现在我们就来梳理一下,Observer/Watcher 之间的关系,来说明它们是如何调用的.

首先, 我们要来理解一下 watcher 实例的概念。实际上 Vue 的 v-model, v-bind , {{ mustache }}, computed, watcher 等等本质上是分别对 data 里的某个 key 节点声明了一个 watcher 实例.

<input v-model="abc">
<span>{{ abc }}</span>
<p :data-key="abc"></p>
...

const v = new Vue({
    data:{
        abc: 111,
    }
    computed:{
        cbd:function(){
            return `${this.abc} after computed`;
        }
    watch:{
        abc:function(val){
            console.info(`${val} after watch`)
        }
     }  
    }
})

这里,Vue 一共声明了 4 个 watcher 实例来监听abc, 1个 watcher 实例来监听 cbd. 如果 abc 的值被更改,那么 4 个 abc - watcher 的实例会执行自身对应的特定回调(比如重新渲染dom,或者是打印信息等等)
不过,Vue 是如何知道,某个 key 对应了多少个 watcher, 而 key 对应的 value 发生变化后,又是如何通知到这些 watcher 来执行对应的不同的回调的呢?
实际上更深层次的逻辑是:
在 Observer阶段,会为每个 key 都创建一个 dep 实例。并且,如果该 key 被某个 watcher 实例 get, 把该 watcher 实例加入 dep 实例的队列里。如果该 key 被 set, 则通知该 key 对应的 dep 实例, 然后 dep 实例会将依次通知队列里的 watcher 实例, 让它们去执行自身的回调方法
dep 实例是收集该 key 所有 watcher 实例的地方.
watcher 实例用来监听某个 key ,如果该 key 产生变化,便会执行 watcher 实例自身的回调
在这里插入图片描述
相关代码如下:
Dep.js

export class Dep {
  constructor() {
    this.subs = [];
  }
  // 将 watcher 实例置入队列
  addSub(sub) {
    this.subs.push(sub);
  }
  // 通知队列里的所有 watcher 实例,告知该 key 的 对应的 val 被改变
  notify() {
    this.subs.forEach((sub, index, arr) => sub.update());
  }
}

// Dep 类的的某个静态属性,用于指向某个特定的 watcher 实例.
Dep.target = null 

observer.js

import {Dep} from './dep'
function makeItReactive(obj, key, val) {
 var dep = new Dep()
Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: () => {
    // 收集依赖! 如果该 key 被某个 watcher 实例依赖,则将该 watcher 实例置入该 key 对应的 dep 实例里
    if(Dep.target){
      dep.addSub(Dep.target)
    }
    return val
  },
  set: (newVal) => {
    if (newVal === val) {
      return;
    }
    else if (isObject(newVal)) {
      new Observer(newVal);
      val = newVal;
    // 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 实例发送通知
    dep.notify()
    }
    else if (!isObject(newVal)) {
      val = newVal;
    // 通知 dep 实例, 该 key 被 set,让 dep 实例向所有收集到的该 key 的 watcher 发送通知
    dep.notify()
    }
  }
})
     }   

watcher.js

import { Dep } from './Dep.js';

export class Watcher {
  constructor(vm, expOrFn, cb) {
    this.cb = cb;
    this.vm = vm;
    this.expOrFn = expOrFn;
    this.value = this.get();
  }
  get() {
    // 在实例化某个 watcher 的时候,会将Dep类的静态属性 Dep.target 指向这个 watcher 实例
    Dep.target = this;
    // 在这一步 this.vm._data[this.expOrFn] 调用了 data 里某个 key 的 getter, 然后 getter 判断类的静态属性 Dep.target 不为null, 而为 watcher 的实例, 从而把这个 watcher 实例添加到 这个 key 对应的 dep 实例里。 巧妙!
    const value = this.vm._data[this.expOrFn];
    // 重置类属性 Dep.target 
    Dep.target = null;
    return value;
  }

  // 如果 data 里的某个 key 的 setter 被调用,则 key 会通知到 该 key 对应的 dep 实例, 该Dep实例, 该 dep 实例会调用所有 依赖于该 key 的 watcher 实例的 update 方法。
  update() {
    this.run();
  }
  run() {
    const value = this.get();
    if (value !== this.value) {
    this.value = value;
    // 执行 cb 回调
    this.cb.call(this.vm);
    }
  }
}

总结:
至此, Watcher, Observer , Dep 的关系全都梳理完成。而这些也是 Vue 实现的核心逻辑之一。再来简单总结一下三者的关系,其实是一个简单的 观察-订阅 的设计模式, 简单来说就是, 观察者观察数据状态变化, 一旦数据发生变化,则会通知对应的订阅者,让订阅者执行对应的业务逻辑 。我们熟知的事件机制,就是一种典型的观察-订阅的模式
Observer, 观察者,用来观察数据源变化.
Dep, 观察者和订阅者是典型的 一对多 的关系,所以这里设计了一个依赖中心,来管理某个观察者和所有这个观察者对应的订阅者的关系, 消息调度和依赖管理都靠它。
Watcher, 订阅者,当某个观察者观察到数据发生变化的时候,这个变化经过消息调度中心,最终会传递到所有该观察者对应的订阅者身上,然后这些订阅者分别执行自身的业务回调即可

参考:Vue的响应式原理(MVVM)深入解析

20.观察者模式
什么是观察者模式
观察者模式又叫做发布—订阅模式,是我们最常用的设计模式之一。它定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知和更新。观察者模式提供了一个订阅模型,其中对象订阅事件并在发生时得到通知,这种模式是事件驱动的编程基石,它有利益于良好的面向对象的设计。

看了上面的这段描述,可能还是不懂什么是观察者模式。我们还可以来看看生活中的观察者模式:现在房价这么高,你肯定是想要早点买房,但你看好的楼盘还没开盘,因此你就将你的电话留给售楼小姐,一旦楼盘推出就让她打电话给你。主动权在售楼方,而你只需要提供一个联系方式就行了这样你就不用担心楼盘出来你不知道了,也不需要每天都打电话去问楼盘推出了没。

观察者模式的使用场景
DOM事件
实际上,只要我们曾经在DOM节点上面绑定过事件函数,那我们就使用过观察者模式,应为JS和DOM之间就是实现了一种观察者模式。

document.body.addEventListener("click", function() {
    alert("Hello World")
}false )
document.body.click() //模拟用户点击

在上面的代码中,需要监听用户点击 document.body 的动作,但是我们是没办法预知用户将在什么时候点击的。因此我们订阅了 document.body 的 click 事件,当 body 节点被点击时,body 节点便会向订阅者发布 “Hello World” 消息。

自定义事件
除了DOM事件外,我们还可以实现一些自定义事件,这种依靠自定义时间完成的观察者模式可以用于任何的JavaScript代码中。

const event = {
    clientList: [],
    listen: function(key , fn) {
        if (this.clientListen[key]) {
            this.clientList[key] = []
        }
        this.clientList[key].push(fn)
    },
    trigger: function() {
        const key = Array.prototype.shift.call(arguments)
        const fns = this.clientList[key]
        if (!fns || fns.length === 0 ) {
            return false
        }
        for (let i = 0, fn ;fn = fns[i++];) {
            fn.apply(this, arguments)
        }
    },
    remove : function(key , fn) {
        const fns = this.clientList[key]
        if (!fns) {
            return false
        }
        if (!fn) {
            fns && (fns.length = 0)
        } else {
            for (let l = fns.length - 1; l>=0; l--) {
                const _fn = fns[l]
                if ( _fn ===fn) {
                    fns.splice(l, 1)
                }
            }
        }
}

const installEvent = (obj) => {
    for (let i in event) {
        obj[i] = event[i]
    }
}

然后就能增加发布和订阅功能了:

const events = {}
installEvent(events)
// 订阅信息
events.listen('newMessage',fn1 = (say) => {
    console.log('say:' + say)
})
// 发布信息
events.trigger('newMessage',"Hello world")
//移除订阅
events.remove('newMessage',fn1)

观察者模式的不足
观察者模式的有点非常明显:一是时间上的解耦,而是对象之间的解耦。既可用于异步编程中,也可以用帮助我们完成更松耦合的代码编写。但它仍然有所不足:

  • 创建订阅者本身要消耗一定的时间和内存
  • 当订阅一个消息时,也许此消息并没有发生,但这个订阅者会始终存在内存中。
  • 观察者模式弱化了对象之间的联系,这本是好事情,但如果过度使用,对象与对象之间的联系也会被隐藏的很深,会导致项目的难以跟踪维护和理解。

Observer模式的角色
Subject(被观察者)
被观察的对象。当需要被观察的状态发生变化时,需要通知队列中所有观察者对象。Subject需要维持(添加,删除,通知)一个观察者对象的队列列表。

ConcreteSubject
被观察者的具体实现。包含一些基本的属性状态及其他操作。

Observer(观察者)
接口或抽象类。当Subject的状态发生变化时,Observer对象将通过一个callback函数得到通知。

ConcreteObserver
观察者的具体实现:得到通知后将完成一些具体的业务逻辑处理。

观察者模式( 又叫发布者-订阅者模式 )应该是最常用的模式之一. 在很多语言里都得到大量应用. 包括我们平时接触的dom事件. 也是js和dom之间实现的一种观察者模式.

观察者模式的实现
一、定义观察者类Pubsub

/* Pubsub */
 function Pubsub(){
     //存放事件和对应的处理方法
    this.handles = {};
 }

二、实现事件订阅on

//传入事件类型type和事件处理handle
 on: function (type, handle) {
     if(!this.handles[type]){
         this.handles[type] = [];
     }
     this.handles[type].push(handle);
 }

三、实现事件发布emit

emit: function () {
     //通过传入参数获取事件类型
    var type = Array.prototype.shift.call(arguments);
     if(!this.handles[type]){
         return false;
     }
 for (var i = 0; i < this.handles[type].length; i++) {
         var handle = this.handles[type][i];
         //执行事件
        handle.apply(this, arguments);
     }
 }

需要说明的是Array.prototype.shift.call(arguments)这句代码,arguments对象是function的内置对象,可以获取到调用该方法时传入的实参数组。
shift方法取出数组中的第一个参数,就是type类型。
四、实现事件取消订阅off

off: function (type, handle) {
     handles = this.handles[type];
     if(handles){
         if(!handle){
             handles.length = 0;//清空数组
        }else{
             for (var i = 0; i < handles.length; i++) {
                 var _handle = handles[i];
                 if(_handle === handle){
                     handles.splice(i,1);
                 }
             }
         }
     }
 }

完整代码:

/* Pubsub */
 function Pubsub(){
     //存放事件和对应的处理方法
    this.handles = {};
 }
 Pubsub.prototype={
     //传入事件类型type和事件处理handle
     on: function (type, handle) {
         if(!this.handles[type]){
             this.handles[type] = [];
         }
         this.handles[type].push(handle);
     },
     emit: function () {
         //通过传入参数获取事件类型
        var type = Array.prototype.shift.call(arguments);
         if(!this.handles[type]){
             return false;
         }
 for (var i = 0; i < this.handles[type].length; i++) {
             var handle = this.handles[type][i];
             //执行事件
            handle.apply(this, arguments);
         }
     },
     off: function (type, handle) {
         handles = this.handles[type];
         if(handles){
             if(!handle){
                 handles.length = 0;//清空数组
            }else{
 for (var i = 0; i < handles.length; i++) {
                     var _handle = handles[i];
                     if(_handle === handle){
                         handles.splice(i,1);
                     }
                 }
             }
         }
     }
 }

参考:JavaScript设计模式之观察者模式
JavaScript原生实现观察者模式

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值