记录一些web前端面试问题

angluar.js的工作原理

首先上一小段代码(index.html),结合代码我们来看看,angular一步一步都做了些什么。

<!doctype html>
<html ng-app>
  <head>
    <script src="angular.js"></script>
  </head>
  <body>
    <png-init=" name='World' ">Hello {{name}}!</p>
  </body> 
</html>

当你用浏览器去访问index.html的时候,浏览器依次做了如下一些事情:

加载html,然后解析成DOM;
加载angular.js脚本;
AngularJS等待DOMContentLoaded事件的触发;
AngularJS寻找ng-app指令,根据这个指令确定应用程序的边界;
使用ng-app中指定的模块配置 i n j e c t o r ; 使 用 i n j e c t o r 创 建 i n j e c t o r 创 建 c o m p i l e 服 务 和 injector; 使用injector创建injector创建compile服务和 injector使injectorinjectorcompilerootScope;
使用compile服务编译DOM并把它链接到compile服务编译DOM并把它链接到rootScope上;
ng-init指令对scope里面的变量name进行赋值;
对表达式{{name}}进行替换,于是乎,显示为“Hello World!”

整个过程可以用这张图来表示:

在这里插入图片描述
好了,通过上面的例子我们清楚了AngularJS是怎样一步一步渲染出一个页面的。那么它又是如何和浏览器的事件回路来交互的呢?或者说是如何跟用户来交互的呢?粗略来讲,主要分为三个阶段:
  1. 浏览器的事件回路一直等待着事件的触发,事件包括用户的交互操作、定时事件或者网络事件(如服务器的响应等);
  2. 一旦有事件触发,就会进入到Javascript的context中,一般通过回调函数来修改DOM;
  3. 等到回调函数执行完毕之后,浏览器又根据新的DOM来渲染新的页面。

正如下面一张图所示,交互过程主要由几个循环组成:
在这里插入图片描述
AngularJS修改了一般的Javascript工作流,并且提供了它自己的事件处理机制。这样就把Javascript的context分隔成两部分,一部分是原生的Javascript的context,另一部分是AngularJS的context。只有处在AngularJS的context中的操作才能享受到Angular的data-binding、exception handling、property watching等服务,但是对于外来者(如原生的Javascript操作、自定义的事件回调、第三方的库等)Angular也不是一概不接见,可以使用AngularJS提供的 a p p l y ( ) 函 数 将 这 些 外 来 者 包 进 A n g u l a r J S 的 c o n t e x t 中 , 让 A n g u l a r 感 知 到 他 们 产 生 的 变 化 。 接 下 来 , 让 我 们 一 起 来 看 看 交 互 过 程 中 的 这 几 个 循 环 是 怎 么 工 作 的 ?     1. 首 先 , 浏 览 器 会 一 直 处 于 监 听 状 态 , 一 旦 有 事 件 被 触 发 , 就 会 被 加 到 一 个 e v e n t q u e u e 中 , e v e n t q u e u e 中 的 事 件 会 一 个 一 个 的 执 行 。     2. e v e n t q u e u e 中 的 事 件 如 果 是 被 apply()函数将这些外来者包进AngularJS的context中,让Angular感知到他们产生的变化。 接下来,让我们一起来看看交互过程中的这几个循环是怎么工作的?   1. 首先,浏览器会一直处于监听状态,一旦有事件被触发,就会被加到一个event queue中,event queue中的事件会一个一个的执行。   2. event queue中的事件如果是被 apply()AngularJScontextAngular  1.eventqueueeventqueue  2.eventqueueapply()包起来的话,就会进入到AngularJS的context中,这里的fn()是我们希望在AngularJS的context中执行的函数。
  3. AngularJS将执行fn()函数,通常情况下,这个函数会改变应用的某些状态。
  4. 然后AngularJS会进入到由两个小循环组成的digest循环中,一个循环是用来处理digest循环中,一个循环是用来处理evalAsync队列(用来schedule一些需要在渲染视图之前处理的操作,通常通过setTimeout(0)实现,速度会比较慢,可能会出现视图抖动的问题)的,一个循环是处理watch列表(是一些表达式的集合,一旦有改变发生,那么watch列表(是一些表达式的集合,一旦有改变发生,那么watch函数就会被调用)的。digest循环会一直迭代知道digest循环会一直迭代知道evalAsync队列为空并且 w a t c h 列 表 也 为 空 的 时 候 , 即 m o d e l 不 再 有 任 何 变 化 。     5. 一 旦 A n g u l a r J S 的 watch列表也为空的时候,即model不再有任何变化。   5. 一旦AngularJS的 watchmodel  5.AngularJSdigest循环结束,整个执行就会离开AngularJS和Javascript的context,紧接着浏览器就会把数据改变后的视图重新渲染出来。
  
接下来,我们还是结合代码来解析一下:

<!doctype html>
<html ng-app>
  <head>
    <script src="angular.js"></script>
  </head>
  <body>
    <input ng-model="name">
    <p>Hello {{name}}!</p>
  </body> 
</html>

这段代码和上一段代码唯一的区别就是有了一个input来接收用户的输入。在用浏览器去访问这个html文件的时候,input上的ng-model指令会给input绑上keydown事件,并且会给name变量建议一个 w a t c h 来 接 收 变 量 值 改 变 的 通 知 。 在 交 互 阶 段 主 要 会 发 生 以 下 一 系 列 事 件 :     1. 当 用 户 按 下 键 盘 上 的 某 一 个 键 的 时 候 ( 比 如 说 A ) , 触 发 i n p u t 上 的 k e y d o w n 事 件 ;     2. i n p u t 上 的 指 令 察 觉 到 i n p u t 里 值 的 变 化 , 调 用 watch来接收变量值改变的通知。在交互阶段主要会发生以下一系列事件:   1. 当用户按下键盘上的某一个键的时候(比如说A),触发input上的keydown事件;   2. input上的指令察觉到input里值的变化,调用 watch  1.Ainputkeydown  2.inputinputapply(“name=‘A’”)更新处于AngularJS的context中的model;
  3. AngularJS将’A’赋值给name;
  4. digest循环开始,digest循环开始,watch列表检测到name值的变化,然后通知{{name}}表达式,更新DOM;
  5. 退出AngularJS的context,然后退出Javascript的context中的keydown事件;
  6. 浏览器重新渲染视图。

vue的底层实现原理

这是一段典型的体现了Vue特点的代码:

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button> //点击这个button,word的值会发生改变
</div>

<script src="./js/observer.js"></script>
<script src="./js/watcher.js"></script>
<script src="./js/compile.js"></script>
<script src="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

Vue实现这种数据双向绑定的效果,需要三大模块:

Observer:能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
Compile:对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
Watcher:作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
在这里插入图片描述
Observer
Observer的核心是通过Obeject.defineProperty()来监听数据的变动,这个函数内部可以定义setter和getter,每当数据发生变化,就会触发setter。这时候Observer就要通知订阅者,订阅者就是Watcher。

Watcher
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:

在自身实例化时往属性订阅器(dep)里面添加自己
自身必须有一个update()方法
待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调
Compile
Compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set(‘property’, value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

DOM事件,譬如用户输入文本,点击按钮等。( ng-click )

XHR响应事件 ( $http )

浏览器Location变更事件 ( $location )

Timer事件( $timeout , $interval )

执行 $digest() 或 $apply()

数据劫持: vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

vue的生命周期

在这里插入图片描述

如何理解angular的脏检查?

脏检查是一种模型到视图的数据映射机制,由 $apply 或 $digest 触发。
脏检查的范围是整个页面,不受区域或组件划分影响
使用尽量简单的绑定表达式提升脏检查执行速度
尽量减少页面上绑定表达式的个数(单次绑定和ng-if)
给 ng-repeat 添加 track by 让 angular 复用已有元素

比如有如下代码:

<p ng-bind="content1"></p>
<p ng-bind="content2"></p>
<button ng-click="onClick()">Click Me</button>

用户点击了 button,angular 执行了一个叫 onClick 的方法。这个 onClick 的方法体对于 angular 来说是黑盒,它到底做了什么不知道。可能改了 $scope.content1 的值,可能改了 $scope.content2 的值,也可能两个值都改了,也可能都没改。

那么 angular 到底应该怎样得知 onClick() 这段代码后是否应该刷新 UI,应该更新哪个 DOM 元素?

angular 必须去挨个检查这些元素对应绑定表达式的值是否有被改变。这就是脏数据检查的由来(脏数据检查以下简称脏检查)。

脏检查如何被触发?
angular 会在可能触发 UI 变更的时候进行脏检查。实际上,脏检查是 d i g e s t 执 行 的 , 另 一 个 更 常 用 于 触 发 脏 检 查 的 函 数 digest执行的,另一个更常用于触发脏检查的函数 digestapply其实就是$digest的一个简单封装。
通常写代码时我们无需主动调用 $apply 或 $digest 是因为 angular 在外部对我们的回调函数做了包装。例如常用的 ng-click,这是一个指令(Directive),内部实现则 类似于

 DOM.addEventListener('click', function ($scope) {
  $scope.$apply(() => userCode());
 });

可以看到:ng-click 帮我们做了 $apply 这个操作。类似的不只是这些事件回调函数,还有 h t t p 、 http、 httptimeout 等。我听很多人抱怨说 angular 这个库太大了什么都管,其实你可以不用它自带的这些服务(Service),只要你记得手工调用 s c o p e . scope. scope.apply。

脏检查的范围
前面说到:angular 会对所有绑定到 UI 上的表达式做脏检查。其实,在 angular 实现内部,所有绑定表达式都被转换为 s c o p e . scope. scope.watch()。每个 $watch 记录了上一次表达式的值。有 ng-bind=“a” 即有 s c o p e . scope. scope.watch(‘a’, callback),而 s c o p e . scope. scope.watch 可不会管被 watch的表达式是否跟触发脏检查的事件有关。

<div ng-show="false">
  <span id="span1" ng-bind="content"></span>
</div>
<span id="span2" ng-bind="content"></span>
<button ng-click="">TEST</button>

问:点击 TEST 这个按钮时会触发脏检查吗?触发几次?

首先:ng-click="" 什么都没有做。angular 会因为这个事件回调函数什么都没做就不进行脏检查吗?不会。

然后:#span1 被隐藏掉了,会检查绑定在它上面的表达式吗?尽管用户看不到,但是 s c o p e . scope. scope.watch(‘content’, callback) 还在。就算你直接把这个 span 元素干掉,只要 watch 表达式还在,要检查的还会检查。

再次:重复的表达式会重复检查吗?会。

最后:别忘了 ng-show=“false”。可能是因为 angular 的开发人员认为这种绑定常量的情况并不多见,所以 $watch 并没有识别所监视的表达式是否是常量。常量依旧会重复检查。

所以:

答:触发三次。一次 false,一次 content,一次 content

所以说一个绑定表达式只要放在当前 DOM 树里就会被监视,不管它是否可见,不管它是否被放在另一个 Tab 里,更不管它是否与用户操作相关。

另外,就算在不同 Controller 里构造的 $scope 也会互相影响,别忘了 angular 还有全局的 $rootScope,你还可以 s c o p e . scope. scope.emit。angular 无法保证你绝对不会在一个 controller 里更改另一个 controller 生成的 scope,包括 自定义指令(Directive)生成的 scope 和 Angular 1.5 里新引入的组件(Component)。

所以说不要怀疑用户在输入表单时 angular 会不会监听页面左边导航栏的变化。

页面的重绘与回流及优化

首先要清楚页面呈现的具体过程:

  1. 浏览器把获取到的HTML代码解析成1个DOM树,HTML中的每个tag都是DOM树中的1个节点,根节点就是我们常用的document对象。DOM树里包含了所有HTML标签,包括display:none隐藏,还有用JS动态添加的元素等。

  2. 浏览器把所有样式(用户定义的CSS和用户代理)解析成样式结构体,在解析的过程中会去掉浏览器不能识别的样式,比如IE会去掉-moz开头的样式,而FF会去掉_开头的样式。

3、DOM Tree 和样式结构体组合后构建render tree, render tree类似于DOM tree,但区别很大,render tree能识别样式,render tree中每个NODE都有自己的style,而且 render tree不包含隐藏的节点 (比如display:none的节点,还有head节点),因为这些节点不会用于呈现,而且不会影响呈现的,所以就不会包含到 render tree中。注意 visibility:hidden隐藏的元素还是会包含到 render tree中的,因为visibility:hidden 会影响布局(layout),会占有空间。根据CSS2的标准,render tree中的每个节点都称为Box (Box dimensions),理解页面元素为一个具有填充、边距、边框和位置的盒子。
3. 一旦render tree构建完毕后,浏览器就可以根据render tree来绘制页面了。如下图:在这里插入图片描述
回流与重绘

  1. 当render tree中的一部分(或全部)因为元素的规模尺寸,布局,隐藏等改变而需要重新构建。这就称为回流(reflow)。每个页面至少需要一次回流,就是在页面第一次加载的时候。在回流的时候,浏览器会使渲染树中受到影响的部分失效,并重新构造这部分渲染树,完成回流后,浏览器会重新绘制受影响的部分到屏幕中,该过程成为重绘。

  2. 当render tree中的一些元素需要更新属性,而这些属性只是影响元素的外观,风格,而不会影响布局的,比如background-color。则就叫称为重绘。

注意:回流必将引起重绘,而重绘不一定会引起回流。 我们需要明白,页面若发生回流则需要付出很高的代价。

回流何时发生:

当页面布局和几何属性改变时就需要回流。下述情况会发生浏览器回流:

1、添加或者删除可见的DOM元素;

2、元素位置改变;

3、元素尺寸改变——边距、填充、边框、宽度和高度

4、内容改变——比如文本改变或者图片大小改变而引起的计算值宽度和高度改变;

5、页面渲染初始化;

6、浏览器窗口尺寸改变——resize事件发生时;

从一个无限大数组里面筛选出前5个数字最大的

先sort()排序,然后reverse()大小颠倒。最后slice(0,6)取出最大的5个值

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值