前端笔记1205

Vue源码解析之nextTick

前言

nextTick是Vue的一个核心功能,在Vue内部实现中也经常用到nextTick。但是,很多新手不理解nextTick的原理,甚至不清楚nextTick的作用。

那么,我们就先来看看nextTick是什么。

nextTick功能

看看官方文档的描述:

在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。

再看看官方示例:

2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE:你们都看我干嘛),你得自己提供 polyfill。

可以看到,nextTick主要功能就是改变数据后让回调函数作用于dom更新后。很多人一看到这里就懵逼了,为什么需要在dom更新后再执行回调函数,我修改了数据后,不是dom自动就更新了吗?

这个和JS中的Event Loop有关,网上教程不计其数,在此就不再赘述了。建议明白Event Loop后再继续向下阅读本文。

举个实际的例子:

我们有个带有分页器的表格,每次翻页需要选中第一项。正常情况下,我们想的是点击翻页器,向后台获取数据,更新表格数据,操纵表格API选中第一项。

但是,你会发现,表格数据是更新了,但是并没有选中第一项。因为,你选中第一项时,虽然数据更新了,但是DOM并没有更新。此时,你可以使用nextTick,在DOM更新后再操纵表格第一项的选中。

那么,nextTick到底做了什么了才能实现在DOM更新后执行回调函数?

一道关于变量提升和运算符的前端面试题

题目如下:

答案是:

此题涉及的知识点众多,包括变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等等。

此题包含7小问,分别说下。

第一问

先看此题的上半部分做了什么,首先定义了一个叫Foo的函数,之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数,之后为Foo的原型对象新创建了一个叫getName的匿名函数。之后又通过函数变量表达式创建了一个getName的函数,最后再声明一个叫getName函数。

第一问的 Foo.getName 自然是访问Foo函数上存储的静态属性,自然是2,没什么可说的。

第二问

第二问,直接调用 getName 函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以跟1 2 3都没什么关系。此题有无数面试者回答为5。此处有两个坑,一是变量声明提升,二是函数表达式。

变量声明提升

即所有声明变量或声明函数都会被提升到当前函数的顶部。

例如下代码:

代码执行时js引擎会将声明语句提升至代码最上方,变为:

函数表达式

var getName 与 function getName 都是声明语句,区别在于 var getName 是函数表达式,而 function getName 是函数声明。关于JS中的各种函数创建方式可以看 大部分人都会做错的经典JS闭包面试题 这篇文章有详细说明。

函数表达式最大的问题,在于js会将此代码拆分为两行代码分别执行。

例如下代码:

实际执行的代码为,先将 var x=1 拆分为 var x; 和 x = 1; 两行,再将 var x; 和 function x(){} 两行提升至最上方变成:

所以最终函数声明的x覆盖了变量声明的x,log输出为x函数。

同理,原题中代码最终执行时的是:

第三问

第三问的 Foo().getName(); 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数。

Foo函数的第一句 getName = function () { alert (1); }; 是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}。

此处实际上是将外层作用域内的getName函数修改了。

注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

之后Foo函数的返回值是this,而JS的this问题博客园中已经有非常多的文章介绍,这里不再多说。

简单的讲,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。

遂Foo函数返回的是window对象,相当于执行 window.getName() ,而window中的getName已经被修改为alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题。

第四问

直接调用getName函数,相当于 window.getName() ,因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1

第五问

第五问 new Foo.getName(); ,此处考察的是js的运算符优先级问题。

js运算符优先级:

js运算符优先级

参考链接:<https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/Operator_Precedence>;

通过查上表可以得知点(.)的优先级高于new操作,遂相当于是:new (Foo.getName)();

所以实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问

第六问 new Foo().getName() ,首先看运算符优先级括号高于new,实际执行为(new Foo()).getName()

遂先执行Foo函数,而Foo此时作为构造函数却有返回值,所以这里需要说明下js中的构造函数返回值问题。

构造函数的返回值

在传统语言中,构造函数不应该有返回值,实际执行的返回值就是此构造函数的实例化对象。

而在js中构造函数可以有返回值也可以没有。

1、没有返回值则按照其他语言一样返回实例化对象。

2、若有返回值则检查其返回值是否为引用类型。如果是非引用类型,如基本(string,number,boolean,null,undefined)则与无返回值相同,实际返回其实例化对象。

3、若返回值是引用类型,则实际返回值为这个引用类型。

原题中,返回的是this,而this在构造函数中本来就代表当前实例化对象,遂最终Foo函数返回实例化对象。

之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性,遂到当前对象的原型对象(prototype)中寻找getName,找到了。

遂最终输出3。

第七问

第七问, new new Foo().getName(); 同样是运算符优先级问题。

最终实际执行为:new ((new Foo()).getName)();

先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new。

遂最终结果为3

详细的解释了第7问:

这里确实是(new Foo()).getName(),但是跟括号优先级高于成员访问没关系,实际上这里成员访问的优先级是最高的,因此先执行了 .getName,但是在进行左侧取值的时候, new Foo() 可以理解为两种运算:new 带参数(即 new Foo())和函数调用(即 先 Foo() 取值之后再 new),而 new 带参数的优先级是高于函数调用的,因此先执行了 new Foo(),或得 Foo 类的实例对象,再进行了成员访问 .getName。

css书写规范

前端面试题-Day1

var的变量提升的底层原理是什么?

JS如何计算浏览器的渲染时间?

JS的回收机制?

垂直水平居中的方式

实现一个三栏布局 中间板块自适应

JSON对象和JSON字符串的区别

JSON对象是直接可以使用JQuery操作的格式,如C#中可以用对象(类名)点出属性(方法)一样

var str2 = { "name": "deluyi", "sex": "man" };

JSON字符串仅仅只是一个字符串,一个整体,不截取的话没办法取出其中存储的数据,不能直接使用,除非你只想alert()它;

var str1 = '{ "name": "deyuyi", "sex": "man" }';

将"JSON字符串"转化为"JSON对象"的方法

一:使用$.parseJSON(str);此种方式仅支持标准格式:var str='{ "name": "John" }';

二:JSON.parse(str);此种方式仅支持标准格式:var str='{ "name": "John" }';

三:使用eval('('+str+')');

将"JSON对象"转化为"JSON字符串"的方法?

一:使用全局方法JSON.stringify()与toJSONString()

秒懂this

日常开发中经常会遇到 this 指向的 bug,郁闷好久才猛然醒悟,痛定思痛,将 this 做个汇总,以便在日后的开发工作中少走弯路。

一:全局执行

1. 浏览器

2. node

可以看出在全局作用域中 this 指向当前的全局对象(浏览器端是 Window,node 中是 global)。

二:函数中执行

1. 非严格模式中

2. 严格模式中

三:作为对象的方法调用

当一个函数被当作一个对象的方法调用的时候,this 指向当前的对象 obj:


如果把对象的方法赋值给一个变量,调用该方法时,this 指向 Window:

四:作为一个构造函数使用

在 JS 中,为了实现类,我们需要定义一些构造函数,在调用一个构造函数的时候加上 new 这个关键字:

此时,this 指向这个构造函数调用的时候实例化出来的对象。

当然了,构造函数其实也是一个函数,若将构造函数当做普通函数来调用,this 指向 Window:

五:在定时器中使用

如果没有特殊指向(指向更改请看下方:怎么改变 this 的指向),setInterval 和setTimeout 的回调函数中 this 的指向都是 Window 。这是因为 JS 的定时器方法是定义在 Window 下的。

六:箭头函数

在全局环境中调用:


作为对象的一个函数调用:

不难发现,普通函数作为对象的一个函数被调用,this 指向 Window,箭头函数作为对象的一个函数被调用,this 指向定义时所在的对象,也就是 func 中的 this,即 obj。

箭头函数中 this 的值取决于该函数外部非箭头函数的 this 的值,且不能通过 call() 、 apply() 和 bind() 方法来改变 this 的值。

七:call、apply、bind

call:

它会立即执行函数,第一个参数是指定执行函数中 this 的上下文,后面的参数是执行函数需要传入的参数;


apply:

它也会立即执行函数,第一个参数是指定执行函数中 this 的上下文,第二个参数是一个数组,是传给执行函数的参数(与 call 的区别);


bind:

它不会执行函数,而是返回一个新的函数,这个新的函数被指定了 this 的上下文,后面的参数是执行函数需要传入的参数;

我们来看个示例:

在这个示例中,call、apply 和 bind 的 this 都指向了 obj,都能正常运行;call、apply 会立即执行函数,call 和 apply 的区别就在于传递的参数,call 接收多个参数列表,apply 接收一个包含多个参数的数组;bind 不是立即执行函数,它返回一个函数,需要执行 p2 才能返回结果,bind 接收多个参数列表。

应用:怎么改变 this 的指向

为什么讲这个模块呢,为了便于大家更加透彻的理解上面所讲述的 this 指向问题,以及更加彻底的理解 JS 函数中重要的三种方法:call、apply、bind 的使用;并且在实际的项目开发中,我们经常会遇到需要改变 this 指向的情况。

我们来看下都有哪些方法:

1. 使用 es6 的箭头函数

这时会报错,因为 setTimeout 里函数的 this 指向 Window,而 Window 对象上是没有 func1 这个函数的。下面我们来修改成箭头函数:

这时候,没有报错,因为箭头函数的 this 的值取决于该函数外部非箭头函数的 this 的值,也就是 func2 的 this 的值, 即 obj。

2. 在函数内部使用 _this = this

此时,func2 也能正常运行。在 func2 中,首先设置 var _this = this,这里的 this 是指向 func2 的对象 obj,为了防止在 func2 中的 setTimeout 被 window 调用而导致的在 setTimeout 中的 this 为 window。我们将 this (指向变量 obj) 赋值给一个变量 _this,这样,在 func2 中我们使用 _this 就是指向对象 obj 了。

3. 使用 call、apply、bind

call:


apply:


bind:

call、apply、bind 都能改变 this 的上下文对象,所以也没有报错,可以正常执行。

具体原因可看上述第七点,call、apply、bind。

4. new 实例化一个对象

如上:第四点,作为一个构造函数使用。

mobx中的数组需要注意的地方

mobx中如果将数组作为可观察. 可以通过添加修饰符observable或者调用observable方法.

很多的时候, 我们将此修饰为可观察的对象后, 就随处可用了.

比如,采用 map forEach indexOf find 等原生数组可用的方法在此都可以使用.

但是没有注意到一个问题, 其实这个对象在控制台中打印的时候已经变成了 Observable 的 Array 已经不是 Array 对象了.

在使用 Lodash 的 isArray 等方法时候, 也返回的 false

这个时候可以通过 slice()方法来转换成原生的数组. 这个在官方文档上也有说明.

换个思路理解Javascript中的this

从call方法开始

call 方法允许切换函数执行的上下文环境(context),即 this 绑定的对象。

大多数介绍 this 的文章中都会把 call 方法放到最后介绍,但此文我们要把 call 方法放在第一位介绍,并从 call 方法切入来研究 this ,因为 call 函数是显式绑定 this 的指向,我们来看看它如何模拟实现(不考虑传入 nullundefined 和原始值):

从以上代码我们可以看到,把调用 call 方法的函数作为第一个参数对象的方法,此时相当于把第一个参数对象作为函数执行的上下文环境,而 this 是指向函数执行的上下文环境的,因此 this 就指向了第一个参数对象,实现了 call 方法切换函数执行上下文环境的功能。

对象方法中的this

在模拟 call 方法的时候,我们使用了对象方法来改变 this 的指向。调用对象中的方法时,会把对象作为方法的上下文环境来调用。

既然 this 是指向执行函数的上下文环境的,那我们先来研究一下调用函数时的执行上下文情况。

下面我门来看看调用对象方法时执行上下文是如何的:

从上图中,我们可以看出getX方法的调用者的上下文是foo,因此getX方法中的 this 指向调用者上下文foo,转换成 call 方法为foo.getX.call(foo)

下面我们把其他函数的调用方式都按调用对象方法的思路来转换。

构造函数中的this

执行 new 如果不考虑原型链,只考虑上下文的切换,就相当于先创建一个空的对象,然后把这个空的对象作为构造函数的上下文,再去执行构造函数,最后返回这个对象。

DOM事件处理函数中的this

把函数绑定到DOM事件时,可以当作在DOM上增加一个函数方法,当触发这个事件时调用DOM上对应的事件方法。

普通函数中的this

这种情况下,我们创建一个虚拟上下文对象,然后普通函数作为这个虚拟上下文对象的方法调用,此时普通函数中的this就指向了这个虚拟上下文。

那这个虚拟上下文是什么呢?在非严格模式下是全局上下文,浏览器里是 window ,NodeJs里是 Global ;在严格模式下是 undefined

闭包中的this

这段代码的上下文如下图:

这里需要注意的是,我们再研究函数中的 this 指向时,只需要关注 this 所在的函数是如何调用的, this 所在函数外的函数调用都是浮云,是不需要关注的。因此在所有的图示中,我们只需要关注红色框中的内容。

因此这段代码我们关注的部分只有:

与普通函数调用一样,创建一个虚拟上下文对象,然后普通函数作为这个虚拟上下文对象的方法立即调用,匿名函数中的 this 也就指向了这个虚拟上下文。

参数中的this

函数参数是值传递的,因此上面代码等同于以下代码:

然后我们又回到了普通函数调用的问题。

全局中的this

全局中的 this 指向全局的上下文

复杂情况下的this

看到上面的情况是有很多个函数,但我们只需要关注 this 所在函数的调用方式,首先我们来简化一下如下:

this 所在的函数 foo 是个普通函数,我们创建一个虚拟上下文对象,然后普通函数作为这个虚拟上下文对象的方法立即调用。因此这个 this指向了这个虚拟上下文。在非严格模式下是全局上下文,浏览器里是 window ,NodeJs里是 Global ;在严格模式下是 undefined

总结

在需要判断 this 的指向时,我们可以安装这种思路来理解:

  • 判断 this 在全局中OR函数中,若在全局中则 this 指向全局,若在函数中则只关注这个函数并继续判断。
  • 判断 this 所在函数是否作为对象方法调用,若是则 this 指向这个对象,否则继续操作。
  • 创建一个虚拟上下文,并把this所在函数作为这个虚拟上下文的方法,此时 this 指向这个虚拟上下文。
  • 在非严格模式下虚拟上下文是全局上下文,浏览器里是 window ,Node.js里是 Global ;在严格模式下是 undefined

图示如下:

浏览器的工作原理

浏览器的结构

浏览器的主要组件包括:

  1. 用户界面——包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗口显示用户请求的页面外,其他显示的各个部分都属于用户界面。
  2. 用户界面后端——用于绘制基本的窗口小部件,比如组合框和窗口。其公开了与web应用无关的通用接口,而在底层使用操作系统的用户界面方法。
  3. 浏览器引擎——在用户界面和渲染引擎之间传递指令。
  4. 渲染引擎——负责显示请求的内容。如果请求的内容是HTML,它就解析HTML和css内容,并将解析后的内容显示在窗口上。
  5. 网络——用于网络调用。比如http请求。其接口与web应用无关,并为所有web应用提供底层实现。
  6. JavaScript解释器。用于解析和执行 JavaScript 代码,比如chrome的javascript解释器是V8。
  7. 数据存储。这是持久层。浏览器需要在硬盘上保存各种数据,例如cookie。新的HTML5规范定义了“网络数据库”,这是一个完整的且轻便的浏览器内数据库。

值得注意的是,不同于大多数浏览器,chrome浏览器为每个标签页都分配了各自的渲染引擎实例,每个标签页都是一个独立的进程(即每个标签页都在独立的“沙箱”内运行,在提高安全性的同时,一个标签页面崩溃也不会导致其他的标签页被关闭)

浏览器进程与线程

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

浏览器是多进程的,有一个主控进程,以及每一个tab页面都会新开一个进程(某些情况下多个tab会合并进程)

浏览器的进程为以下几种:

  • Browser进程:浏览器的主进程(负责协调、主控),只有一个
  • 第三方插件进程:每种类型的插件对应一个进程,仅当使用该插件的时候才创建
  • GPU进程:最多一个,用于3D绘制
  • 浏览器渲染进程(内核):默认每个tab页面一个进程,互不影响,控制页面渲染,脚本执行,事件处理等(有时会优化,如多个空白tab会合成一个进程)

每一个tab页面可以看作是浏览器内核进程,然后这个进程是多线程的,他又几大类子线程:

  • GUI线程
    注意:GUI渲染线程与js引擎线程是互斥的,当js引擎执行时GUI线程会被挂起(相当于被冻结),GUI更新会被保存在一个队列中等到Js引擎空闲时立即被执行
  • JS引擎线程
    js引擎一直等待着任务队列中任务的到来,然后加以处理,一个tab页中无论什么时候都只有一个js线程在运行js程序
  • 事件触发线程
  • 定时器线程
    setInterval与setTimeout所在的线程
    浏览器定时器计数器并不是由js引擎计数的,(因为js引擎时单线程的,如果处于阻塞线程状态就会影响计时的准确)
    因此通过单线程来计时并触发定时(计时完毕后,添加到事件队列中,等待js引擎空闲后执行)
    注意,W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms。
  • 网络请求线程
    在XMLHttpRequest在连接后是通过浏览器新开一个线程请求
    将检测到状态变更时,如果设置有回调函数,异步线程就产生状态变更事件,将这个回调再放入事件队列中。再由JavaScript引擎执行。

每次网络请求时都需要开辟单独的线程进行,譬如如果URL解析到http协议,就会新建一个网络线程去处理资源下载。因此浏览器会根据解析出得协议,开辟一个网络线程,前往请求资源

进程之间的通信

五种通讯方式总结:

  1. 管道:速度慢,容量有限,只有父子进程能通讯
  2. FIFO:任何进程间都能通讯,但速度慢
  3. 消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完的数据的问题
  4. 信号量:不能传递复杂消息,只能用来同步
  5. 共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程种的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没有这个必要,线程间本来就已经共享了统一进程内的一块内存。

由于浏览器每一个tab页面即为一个进程,页面间的通信即为进程的通信

  1. window.open(url,name,featrues,replace)
    url:(可选)为空则打开空白新窗口
    name:(可选)子窗口的句柄。声明新窗口的名称。若名字已存在则在指定窗口打开。仅当同源或该脚本打开了这个窗口才可以通过名字进项指定窗口
    Features (可选) 声明新窗口要显示的标准浏览器的特征(必须是打开空白窗口)。
    Replace(可选) 为true的话则替换浏览历史中的当前条目(返回回不去),默认为false,创建新条目。

渲染引擎

渲染引擎也称为浏览器内核,主要时在浏览器窗口中显示所请求的内容,这是每个浏览器的核心部分。

常见的浏览器渲染引擎有两种:一是firefox使用的Gecko,这是Mozilla公司“自制”的渲染引擎。另一个是safari和chrome使用的都是webkit。

渲染流程:
渲染引擎一开始会从网络层获取请求的文档的内容,通常以8k分块的方式完成,获取了文档内容之后,渲染引擎开始正式工作
渲染引擎解析HTML文档,并将文档中的标签转化为dom节点树,同时,它会解析外部css文件以及style标签中的样式数据。这些样式信息连同HTML中的可见内容一起,被用于构建另一颗树——渲染树(RenderTree)。
渲染树由一些带有视觉属性(如颜色、大小等)的矩形组成,这些矩形将按照正确的顺序显示在屏幕上。
渲染树构建完毕之后,将会进入“布局”处理阶段,即为每一个节点分配一个屏幕坐标。再下一步就是绘制,即遍历renderTree,并使用UI后端层绘制每个节点。
注意:这个过程时逐步完成的,为了更好的用户体验,渲染引擎将会尽可能的早的将内容呈现在屏幕上,并不会等到所有的Html都解析完成之后再去构建和布局renderTree。它时解析完一部分内容就显示一部分内容,同时,可能还在通过网络下载其余内容。

评论 ( 0 )
最新评论
暂无评论

赶紧努力消灭 0 回复