Coder Social home page Coder Social logo

yunydemo's People

Contributors

yangpengfe1 avatar ypf5491633 avatar

Stargazers

 avatar  avatar

Watchers

 avatar

yunydemo's Issues

关于Promise:你可能不知道的6件事

  • promise 的工作机制与 callback 类似,都采用内部的抽象操作 Job 来实现异步
  • Promise 构造函数里的 resolve/reject 函数是内部创建的,在调用它们时传入的参数就是要解析的结果,把它和 promise 已经存储的用户传入的处理函数一起插入到 Job 队列中。传入的参数也可以是一个 promise,在 Promise.all/race 的内部就有用到。
  • Promise.prototype.then 根据当前的 promise 的状态来决定是立即将 promise 中存储的结果取出并和参数中的处理函数一起直接插入到 Job 队列中还是先与 promise 关联起来作为结果处理函数。then 会隐式调用 Promise 构建函数构建新的 promise 并返回。
  • Promise.all 先创建一个新的 promise,然后先、初始化一个空的结果数组和一个计数器来对已经 resolve 的 promise进行计数,之后会进行迭代,对于每个迭代值它都会为其创造一个promise,并设定这个promise的then为向结果数组里添加结果以及计数器--,当计数器减至0时就会resolve最终结果。
  • Promise.race 也是会创建一个新的主 promise,之后主要是根据 promise 只能 resolve 一次的限制,对于每个迭代值都会创造另一个promise,先resolve的也就会先被主 promise resolve 返回结果。
  • Promise 的价值在于使得异步代码以一个更可读的风格结构化,而不是因异步函数嵌套显得混乱不堪。这篇文章会接触到 6 个你可能不知道的关于 Promise 的事。
    开始列举之前,先看看怎么创建 Promise:
let p = new Promise((resolve, reject) => {
      resolve('hello world')
})
p.then((str) => {
      console.log(str) // 'hello world'
})

1、then() 返回一个 forked Promise(分叉的 Promise)

下面两段代码有什么不同?

// Exhibit A
var p = new Promise(/*...*/);
p.then(func1);
p.then(func2);
// Exhibit B
var p = new Promise(/*...*/);
p.then(func1)
.then(func2);

如果你认为两段代码等价,那么你可能认为 promise 仅仅就是一维回调函数的数组。然而,这两段代码并不等价。p 每次调用 then() 都会返回一个 forked promise。因此,在A中,如果 func1 抛出一个异常,func2 依然能执行,而在B中,func2 不会被执行,因为第一次调用返回了一个新的 promise,由于 func1 中抛出异常,这个 promise 被 rejected了,结果 func2 被跳过不执行了。

2、回调函数应该传递结果

var p = new Promise(function(resolve, reject) {
  resolve("hello world");
});

p.then(function(str) {})
.then(function(str) {
  alert(str);
});

第二个 then() 中的alert显示undefined,因为在 promise 的上下文中,回调函数像普通的回调函数一样传递结果。promise 期望你的回调函数或者返回同一个结果,或者返回其它结果,返回的结果会被传给下一个回调。

3、只能捕获来自上一级的异常

下面的两段代码有什么不同:

// Exhibit A
new Promise(function(resolve, reject) {
  resolve("hello world");
})
.then(
  function(str) {
    throw new Error("uh oh");
  },
  undefined
)
.then(
  undefined,
  function(error) {
    alert(error);
  }
);
// Exhibit B
new Promise(function(resolve, reject) {
  resolve("hello world");
})
.then(
  function(str) {
    throw new Error("uh oh");
  },
  function(error) {
    alert(error);
  }
);

在A中,当第一个 then 抛出异常时,第二个 then 能捕获到该异常,并会弹出 'uh oh'。这符合只捕获来自上一级异常的规则。

在B中,正确的回调函数和错误的回调函数在同一级,也就是说,尽管在回调中抛出了异常,但是这个异常不会被捕获。事实上,B中的错误回调只有在 promise 被 rejected 或者 promise 自身抛出一个异常时才会被执行。

4、错误能被恢复

在一个错误回调中,如果没有重新抛出错误,promise 会认为你已经恢复了该错误,promise 的状态会转变为 resolved。在下面的例子中,会弹出’I am saved’ 是因为第一个 then() 中的错误回调函数并没有重新抛出异常。

var p = new Promise(function(resolve,reject){
    reject(new Error('pebkac'));
});  

p.then(
    undefined,
    function(error){ }
)
 .then(
    function(str){
        alert('I am saved!');
    },
    function(error){
     alert('Bad computer!');
    }
); 

Promise 可被视为洋葱的皮层,每一次调用 then 都会被添加一层皮层,每一个皮层表示一个能被处理的状态,在皮层被处理之后,promise 会认为已经修复了错误,并准备进入下一个皮层。

5、Promise 能被暂停

仅仅因为你已经在一个 then() 函数中执行过代码,并不意味着你不能够暂停 promise 去做其他事情。为了暂停当前的 promise,或者要它等待另一个 promise 完成,只需要简单地在 then() 函数中返回另一个 promise。

var p = new Promise(/*...*/);   

p.then(function(str){
    if(!loggedIn){
        return new Promise(/*...*/);
    }
}) 
 .then(function(str){
    alert("Done!");
 });

在上面的代码中,直到新的 promise 的状态是 resolved解析后,alert 才会显示。如果要在已经存在的异步代码中引入更多的依赖,这是一个很便利的方式。例如,你发现用户会话已经超时了,因此,你可能想要在继续执行后面的代码之前发起第二次登录。

6、resolved 状态的 Promise 不会立即执行

function runme() {
  var i = 0;

  new Promise(function(resolve) {
    resolve();
  })
  .then(function() {
    i += 2;
  });
  alert(i);
}

你可能会认为弹出2,因为 promise 已经是 resolved ,then() 会立即执行(同步)。然而,promise 规范要求所有回调都是异步的,因此,alert 执行时 i 的值还没有被修改。

浅说 XSS 和 CSRF

在 Web 安全领域中,XSS 和 CSRF 是最常见的攻击方式。本文将会简单介绍 XSS 和 CSRF 的攻防问题。

声明:本文的示例仅用于演示相关的攻击原理

XSS

XSS,即 Cross Site Script,中译是跨站脚本攻击;其原本缩写是 CSS,但为了和层叠样式表(Cascading Style Sheet)有所区分,因而在安全领域叫做 XSS。

XSS 攻击是指攻击者在网站上注入恶意的客户端代码,通过恶意脚本对客户端网页进行篡改,从而在用户浏览网页时,对用户浏览器进行控制或者获取用户隐私数据的一种攻击方式。

攻击者对客户端网页注入的恶意脚本一般包括 JavaScript,有时也会包含 HTML 和 Flash。有很多种方式进行 XSS 攻击,但它们的共同点为:将一些隐私数据像 cookie、session 发送给攻击者,将受害者重定向到一个由攻击者控制的网站,在受害者的机器上进行一些恶意操作。

XSS攻击可以分为3类:反射型(非持久型)、存储型(持久型)、基于DOM。

反射型

反射型 XSS 只是简单地把用户输入的数据 “反射” 给浏览器,这种攻击方式往往需要攻击者诱使用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。

看一个示例。我先准备一个如下的静态页:

反射型xss1

恶意链接的地址指向了 localhost:8001/?q=111&p=222。然后,我再启一个简单的 Node 服务处理恶意链接的请求:

const http = require('http');
function handleReequest(req, res) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
    res.write('<script>alert("反射型 XSS 攻击")</script>');
    res.end();
}

const server = new http.Server();
server.listen(8001, '127.0.0.1');
server.on('request', handleReequest);

当用户点击恶意链接时,页面跳转到攻击者预先准备的页面,会发现在攻击者的页面执行了 js 脚本:

执行脚本

这样就产生了反射型 XSS 攻击。攻击者可以注入任意的恶意脚本进行攻击,可能注入恶作剧脚本,或者注入能获取用户隐私数据(如cookie)的脚本,这取决于攻击者的目的。

存储型

存储型 XSS 会把用户输入的数据 "存储" 在服务器端,当浏览器请求数据时,脚本从服务器上传回并执行。这种 XSS 攻击具有很强的稳定性。

比较常见的一个场景是攻击者在社区或论坛上写下一篇包含恶意 JavaScript 代码的文章或评论,文章或评论发表后,所有访问该文章或评论的用户,都会在他们的浏览器中执行这段恶意的 JavaScript 代码。

举一个示例。

先准备一个输入页面:

<input type="text" id="input">
<button id="btn">Submit</button>   

<script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');

    let val;
     
    input.addEventListener('change', (e) => {
        val = e.target.value;
    }, false);

    btn.addEventListener('click', (e) => {
        fetch('http://localhost:8001/save', {
            method: 'POST',
            body: val
        });
    }, false);
</script>     

启动一个 Node 服务监听 save 请求。为了简化,用一个变量来保存用户的输入:

const http = require('http');

let userInput = '';

function handleReequest(req, res) {
    const method = req.method;
    res.setHeader('Access-Control-Allow-Origin', '*');
    res.setHeader('Access-Control-Allow-Headers', 'Content-Type')
    
    if (method === 'POST' && req.url === '/save') {
        let body = '';
        req.on('data', chunk => {
            body += chunk;
        });

        req.on('end', () => {
            if (body) {
                userInput = body;
            }
            res.end();
        });
    } else {
        res.writeHead(200, {'Content-Type': 'text/html; charset=UTF-8'});
        res.write(userInput);
        res.end();
    }
}

const server = new http.Server();
server.listen(8001, '127.0.0.1');

server.on('request', handleReequest);

当用户点击提交按钮将输入信息提交到服务端时,服务端通过 userInput 变量保存了输入内容。当用户通过 http://localhost:8001/${id} 访问时,服务端会返回与 id 对应的内容(本示例简化了处理)。如果用户输入了恶意脚本内容,则其他用户访问该内容时,恶意脚本就会在浏览器端执行:

存储型xss

基于DOM

基于 DOM 的 XSS 攻击是指通过恶意脚本修改页面的 DOM 结构,是纯粹发生在客户端的攻击。

看如下代码:

<h2>XSS: </h2>
<input type="text" id="input">
<button id="btn">Submit</button>
<div id="div"></div>
<script>
    const input = document.getElementById('input');
    const btn = document.getElementById('btn');
    const div = document.getElementById('div');

    let val;
     
    input.addEventListener('change', (e) => {
        val = e.target.value;
    }, false);

    btn.addEventListener('click', () => {
        div.innerHTML = `<a href=${val}>testLink</a>`
    }, false);
</script>

点击 Submit 按钮后,会在当前页面插入一个链接,其地址为用户的输入内容。如果用户在输入时构造了如下内容:

'' onclick=alert(/xss/)

用户提交之后,页面代码就变成了:

<a href onlick="alert(/xss/)">testLink</a>

此时,用户点击生成的链接,就会执行对应的脚本:

dom-xss

XSS 攻击的防范

现在主流的浏览器内置了防范 XSS 的措施,例如 CSP。但对于开发者来说,也应该寻找可靠的解决方案来防止 XSS 攻击。

HttpOnly 防止劫取 Cookie

HttpOnly 最早由微软提出,至今已经成为一个标准。浏览器将禁止页面的Javascript 访问带有 HttpOnly 属性的Cookie。

上文有说到,攻击者可以通过注入恶意脚本获取用户的 Cookie 信息。通常 Cookie 中都包含了用户的登录凭证信息,攻击者在获取到 Cookie 之后,则可以发起 Cookie 劫持攻击。所以,严格来说,HttpOnly 并非阻止 XSS 攻击,而是能阻止 XSS 攻击后的 Cookie 劫持攻击。

输入检查

不要相信用户的任何输入。 对于用户的任何输入要进行检查、过滤和转义。建立可信任的字符和 HTML 标签白名单,对于不在白名单之列的字符或者标签进行过滤或编码。

在 XSS 防御中,输入检查一般是检查用户输入的数据中是否包含 <> 等特殊字符,如果存在,则对特殊字符进行过滤或编码,这种方式也称为 XSS Filter。

而在一些前端框架中,都会有一份 decodingMap, 用于对用户输入所包含的特殊字符或标签进行编码或过滤,如 <>script,防止 XSS 攻击:

// vuejs 中的 decodingMap
// 在 vuejs 中,如果输入带 script 标签的内容,会直接过滤掉
const decodingMap = {
  '&lt;': '<',
  '&gt;': '>',
  '&quot;': '"',
  '&amp;': '&',
  '&#10;': '\n'
}

输出检查

用户的输入会存在问题,服务端的输出也会存在问题。一般来说,除富文本的输出外,在变量输出到 HTML 页面时,可以使用编码或转义的方式来防御 XSS 攻击。例如利用 sanitize-html 对输出内容进行有规则的过滤之后再输出到页面中。

CSRF

CSRF,即 Cross Site Request Forgery,中译是跨站请求伪造,是一种劫持受信任用户向服务器发送非预期请求的攻击方式。

通常情况下,CSRF 攻击是攻击者借助受害者的 Cookie 骗取服务器的信任,可以在受害者毫不知情的情况下以受害者名义伪造请求发送给受攻击服务器,从而在并未授权的情况下执行在权限保护之下的操作。

在举例子之前,先说说浏览器的 Cookie 策略。

浏览器的 Cookie 策略

Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。Cookie 主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 个性化设置(如用户自定义设置、主题等)

而浏览器所持有的 Cookie 分为两种:

  • Session Cookie(会话期 Cookie):会话期 Cookie 是最简单的Cookie,它不需要指定过期时间(Expires)或者有效期(Max-Age),它仅在会话期内有效,浏览器关闭之后它会被自动删除。
  • Permanent Cookie(持久性 Cookie):与会话期 Cookie 不同的是,持久性 Cookie 可以指定一个特定的过期时间(Expires)或有效期(Max-Age)。
res.setHeader('Set-Cookie', ['mycookie=222', 'test=3333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);

上述代码创建了两个 Cookie:mycookietest,前者属于会话期 Cookie,后者则属于持久性 Cookie。当我们去查看 Cookie 相关的属性时,不同的浏览器对会话期 Cookie 的 Expires 属性值会不一样:

Firefox:

firefox cookie

Chrome:

chrome cookie

此外,每个 Cookie 都会有与之关联的域,这个域的范围一般通过 donmain 属性指定。如果 Cookie 的域和页面的域相同,那么我们称这个 Cookie 为第一方 Cookie(first-party cookie),如果 Cookie 的域和页面的域不同,则称之为第三方 Cookie(third-party cookie)。一个页面包含图片或存放在其他域上的资源(如图片)时,第一方的 Cookie 也只会发送给设置它们的服务器。

通过 Cookie 进行 CSRF 攻击

假设有一个 bbs 站点:http://www.c.com,当登录后的用户发起如下 GET 请求时,会删除 ID 指定的帖子:

http://www.c.com:8002/content/delete/:id

如发起 http://www.c.com:8002/content/delete/87343 请求时,会删除 id 为 87343 的帖子。当用户登录之后,会设置如下 cookie:

res.setHeader('Set-Cookie', ['user=22333; expires=Sat, 21 Jul 2018 00:00:00 GMT;']);

user

user 对应的值是用户 ID。然后构造一个页面 A:

<p>CSRF 攻击者准备的网站:</p>
<img src="http://www.c.com:8002/content/delete/87343">

页面 A 使用了一个 img 标签,其地址指向了删除用户帖子的链接:

A

可以看到,当登录用户访问攻击者的网站时,会向 www.c.com 发起一个删除用户帖子的请求。此时若用户在切换到 www.c.com 的帖子页面刷新,会发现ID 为 87343 的帖子已经被删除。

由于 Cookie 中包含了用户的认证信息,当用户访问攻击者准备的攻击环境时,攻击者就可以对服务器发起 CSRF 攻击。在这个攻击过程中,攻击者借助受害者的 Cookie 骗取服务器的信任,但并不能拿到 Cookie,也看不到 Cookie 的内容。而对于服务器返回的结果,由于浏览器同源策略的限制,攻击者也无法进行解析。因此,攻击者无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。

但若 CSRF 攻击的目标并不需要使用 Cookie,则也不必顾虑浏览器的 Cookie 策略了。

CSRF 攻击的防范

当前,对 CSRF 攻击的防范措施主要有如下几种方式。

验证码

验证码被认为是对抗 CSRF 攻击最简洁而有效的防御方法。

从上述示例中可以看出,CSRF 攻击往往是在用户不知情的情况下构造了网络请求。而验证码会强制用户必须与应用进行交互,才能完成最终请求。因为通常情况下,验证码能够很好地遏制 CSRF 攻击。

但验证码并不是万能的,因为出于用户考虑,不能给网站所有的操作都加上验证码。因此,验证码只能作为防御 CSRF 的一种辅助手段,而不能作为最主要的解决方案。

Referer Check

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址。通过 Referer Check,可以检查请求是否来自合法的"源"。

比如,如果用户要删除自己的帖子,那么先要登录 www.c.com,然后找到对应的页面,发起删除帖子的请求。此时,Referer 的值是 http://www.c.com;当请求是从 www.a.com 发起时,Referer 的值是 http://www.a.com 了。因此,要防御 CSRF 攻击,只需要对于每一个删帖请求验证其 Referer 值,如果是以 www.c.com 开头的域名,则说明该请求是来自网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是 CSRF 攻击,可以拒绝该请求。

针对上文的例子,可以在服务端增加如下代码:

if (req.headers.referer !== 'http://www.c.com:8002/') {
    res.write('csrf 攻击');
    return;
}

referer check

Referer Check 不仅能防范 CSRF 攻击,另一个应用场景是 "防止图片盗链"。

添加 token 验证

CSRF 攻击之所以能够成功,是因为攻击者可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 Cookie 中,因此攻击者可以在不知道这些验证信息的情况下直接利用用户自己的 Cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入攻击者所不能伪造的信息,并且该信息不存在于 Cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

总结

本文主要介绍了 XSS 和 CSRF 的攻击原理和防御措施。当然,在 Web 安全领域,除了这两种常见的攻击方式,也存在这 SQL 注入等其它攻击方式,这不在本文的讨论范围之内,如果你对其感兴趣,可以阅读SQL注入技术专题的专栏详细了解相关信息。最后,总结一下 XSS 攻击和 CSRF 攻击的常见防御措施:

  1. 防御 XSS 攻击

    • HttpOnly 防止劫取 Cookie
    • 用户的输入检查
    • 服务端的输出检查
  2. 防御 CSRF 攻击

    • 验证码
    • Referer Check
    • Token 验证

<完>

参考资料

JavaScript 执行机制.

我们经常会遇到这样的情况:给定的几行代码,我们需要知道其输出内容和顺序。因为javascript是一门单线程语言,所以我们可以得出结论:

  • javascript是按照语句出现的顺序执行的

看到这里读者要打人了:我难道不知道js是一行一行执行的?还用你说?稍安勿躁,正因为js是一行一行执行的,所以我们以为js都是这样的:

let a = '1';
console.log(a);

let b = '2';
console.log(b);

然而实际上js是这样的:

setTimeout(function(){
    console.log('定时器开始啦')
});

new Promise(function(resolve){
    console.log('马上执行for循环啦');
    for(var i = 0; i < 10000; i++){
        i == 99 && resolve();
    }
}).then(function(){
    console.log('执行then函数啦')
});

console.log('代码执行结束');

依照js是按照语句出现的顺序执行这个理念,我自信的写下输出结果:

//"定时器开始啦"
//"马上执行for循环啦"
//"执行then函数啦"
//"代码执行结束"

去chrome上验证下,结果完全不对,瞬间懵了,说好的一行一行执行的呢?
我们真的要彻底弄明白javascript的执行机制了。

你真的了解 http code?

HttpStatus = {
      // Informational 1xx 信息
      '100': 'Continue', // 继续
      '101': 'Switching Protocols', // 交换协议
      // Successful 2xx 成功
      '200': 'OK', // OK
      '201': 'Created', // 创建
      '202': 'Accepted', // 已接受
      '203': 'Non-Authoritative Information', // 非权威信息
      '204': 'No Content', // 没有内容
      '205': 'Reset Content', // 重置内容
      '206': 'Partial Content', // 部分内容
      // Redirection 3xx 重定向
      '300': 'Multiple Choices', // 多种选择
      '301': 'Moved Permanently', // 永久移动
      '302': 'Found', // 找到
      '303': 'See Other', // 参见其他
      '304': 'Not Modified', // 未修改
      '305': 'Use Proxy', // 使用代理
      '306': 'Unused', // 未使用
      '307': 'Temporary Redirect', // 暂时重定向
      // Client Error 4xx 客户端错误
      '400': 'Bad Request', // 错误的请求
      '401': 'Unauthorized', // 未经授权
      '402': 'Payment Required', // 付费请求
      '403': 'Forbidden', // 禁止
      '404': 'Not Found', // 没有找到
      '405': 'Method Not Allowed', // 方法不允许
      '406': 'Not Acceptable', // 不可接受
      '407': 'Proxy Authentication Required', // 需要代理身份验证
      '408': 'Request Timeout', // 请求超时
      '409': 'Conflict', // 指令冲突
      '410': 'Gone', // 文档永久地离开了指定的位置
      '411': 'Length Required', // 需要Content-Length头请求
      '412': 'Precondition Failed', // 前提条件失败
      '413': 'Request Entity Too Large', // 请求实体太大
      '414': 'Request-URI Too Long', // 请求URI太长
      '415': 'Unsupported Media Type', // 不支持的媒体类型
      '416': 'Requested Range Not Satisfiable', // 请求的范围不可满足
      '417': 'Expectation Failed', // 期望失败
      // Server Error 5xx 服务器错误
      '500': 'Internal Server Error', // 内部服务器错误
      '501': 'Not Implemented', // 未实现
      '502': 'Bad Gateway', // 错误的网关
      '503': 'Service Unavailable', // 服务不可用
      '504': 'Gateway Timeout', // 网关超时
      '505': 'HTTP Version Not Supported' // HTTP版本不支持
    }

http code (1)

关于vue的diff算法

vue2.0加入了virtual DOM, vue的diff位于patch.js文件中,复杂度为O(n)。 听大神说了解diff过程可以让我们更高效的使用框架,工作和女朋友都更加好找了,我们赶快了解哈~。 了解diff过程,我们先从虚拟dom开始。

虚拟dom

  • 所谓的virtual dom,也就是虚拟节点。它通过JS的Object对象模拟DOM中的节点,然后再通过特定的render方法将其渲染成真实的DOM节点
    dom diff 则是通过JS层面的计算,返回一个patch对象,即补丁对象,在通过特定的操作解析patch对象,完成页面的重新渲染,
    上一张图让大家更加清晰点:

Virtual Dom

  • 到这里有童鞋可能会问,模拟DOM是干嘛为什么要这样做?虚拟dom对应的是真实dom, 使用document.CreateElement 和 document.CreateTextNode创建的就是真实节点。
    我们可以做个试验。打印出一个空元素的第一层属性,可以看到标准让元素实现的东西太多了。如果每次都重新生成新的元素,对性能是巨大的浪费。
var odiv = document.createElement('div');
for(var k in odiv ){
  console.log(k)
}
// align
// title
// lang
// translate
// dir
// dataset
// hidden
// tabIndex
// accessKey
// ...
  • 打印出了大量的属性以及方法

实现步骤

  • 用JavaScript对象模拟DOM
  • 把此虚拟DOM转成真实DOM并插入页面中
  • 如果有事件发生修改了虚拟DOM
  • 比较两棵虚拟DOM树的差异,得到差异对象
  • 把差异对象应用到真正的DOM树上

代码实现

class crtateElement {
    constructor(el, attr, child) {
      this.el = el
      this.attrs = attr
      this.child = child || []
    }
    render() {
      let virtualDom = document.createElement(this.el)
      // attr是个对象所以要遍历渲染
      for (const attr in attrs) {
        virtualDom.setAttribute(attr, this.attrs[attr])
      }

      // 深度遍历chlid
      this.child.forEach(element => {
        console.log(element instanceof crtateElement)
        // 如果子节点是一个元素的话,就调用它的render方法创建子节点的真实DOM,如果是一个字符串的话,创建一个文件节点就可以了
        // 判断一个对象是否是某个对象的实力
        let childElement = (element instanceof crtateElement) ? element.render() : document.createTextNode(element)
        virtualDom.appendChild(childElement)
      });

      return virtualDom;
    }
  }

  function element(el, attr, child) {
    return new crtateElement(el, attr, child)
  }

  module.exports = element

用JavaScript对象结构表示DOM树的结构;然后用这个树构建一个真正的DOM树,插到文档当中

let element = require('./crtateElement')
let myobj = {
  'class': 'big_div'
}
let ul = element('div', myobj, [
  '我是文字',
  element('div', { 'id': 'xiao' }, ['1']),
  element('div', { 'id': 'xiao1' }, ['2']),
  element('div', { 'id': 'xiao2' }, ['3']),
])
console.log(ul)
ul = ul.render()
document.body.appendChild(ul)

DOM DIFF

  • 比较两棵DOM树的差异是Virtual DOM算法最核心的部分.简单的说就是新旧虚拟dom 的比较,如果有差异就以新的为准,然后再插入的真实的dom中,重新渲染

DOM DIFF

比较只会在同层级进行, 不会跨层级比较。

比较后会出现四种情况:
1、此节点是否被移除 -> 添加新的节点
2、属性是否被改变 -> 旧属性改为新属性
3、文本内容被改变-> 旧内容改为新内容
4、节点要被整个替换 -> 结构完全不相同 移除整个替换

看diff.js 的简单代码实现,下面都有相应的解释说明:

let utils = require('./utils');
let keyIndex = 0;

function diff(oldTree, newTree) {
  //记录差异的空对象。key就是老节点在原来虚拟DOM树中的序号,值就是一个差异对象数组
  let patches = {};
  keyIndex = 0;
  let index = 0; // 儿子要起另外一个标识
  walk(oldTree, newTree, index, patches);
  return patches;
}

function walk(oldNode, newNode, index, patches) {

  let currentPatches = []; //这个数组里记录了所有的oldNode的变化
  if (!newNode) { //如果新节点没有了,则认为此节点被删除了
    currentPatches.push({ type: utils.REMOVE, index })
  } else if (utils.isString(oldNode) && utils.isString(newNode)) { //如果说老节点的新的节点都是文本节点的话
    //如果新的字符符值和旧的不一样
    if (oldNode !== newNode) {
      ///文本改变 
      currentPatches.push({ type: utils.TEXT, content: newNode });
    }
  } else if (oldNode.tagName == newNode.tagName) {
    //比较新旧元素的属性对象
    let attrsPatch = diffAttr(oldNode.attrs, newNode.attrs);
    //如果新旧元素有差异 的属性的话
    if (Object.keys(attrsPatch).length > 0) {
      //添加到差异数组中去
      currentPatches.push({ type: utils.ATTRS, attrs: attrsPatch });
    }
    //自己比完后再比自己的儿子们
    diffChildren(oldNode.children, newNode.children, index, patches, currentPatches);
  } else {
    currentPatches.push({ type: utils.REPLACE, node: newNode });
  }

  if (currentPatches.length > 0) {
    patches[index] = currentPatches;
  }

}

//老的节点的儿子们 新节点的儿子们 父节点的序号 完整补丁对象 当前旧节点的补丁对象
function diffChildren(oldChildren, newChildren, index, patches, currentPatches) {
  oldChildren.forEach((child, idx) => {
    walk(child, newChildren[idx], ++keyIndex, patches);
  });
}

function diffAttr(oldAttrs, newAttrs) {
  let attrsPatch = {};
  for (let attr in oldAttrs) {
    //如果说老的属性和新属性不一样。一种是值改变 ,一种是属性被删除 了
    if (oldAttrs[attr] != newAttrs[attr]) {
      attrsPatch[attr] = newAttrs[attr];
    }
  }
  for (let attr in newAttrs) {
    if (!oldAttrs.hasOwnProperty(attr)) {
      attrsPatch[attr] = newAttrs[attr];
    }
  }
  return attrsPatch;
}

module.exports = diff;


  • 其中有个需要注意的是新旧虚拟dom比较的时候,是先同层比较,同层比较完看看时候有儿子,有则需要继续比较下去,直到没有儿子。搞个简单的图来说明一下吧:

DOM DIFF

  • 同层比较,比较顺序是上面的数字来,把不同的打上标记,放到数组里面去,统一交给patch处理。
    patch.js的简单实现
let keyIndex = 0;
let utils = require('./utils');
let allPatches;//这里就是完整的补丁包

function patch(root, patches) {
  allPatches = patches;
  walk(root);
}

function walk(node) {
  let currentPatches = allPatches[keyIndex++];
  (node.childNodes || []).forEach(child => walk(child));
  if (currentPatches) {
    doPatch(node, currentPatches);
  }
}

function doPatch(node, currentPatches) {
  currentPatches.forEach(patch => {
    switch (patch.type) {
      case utils.ATTRS:
        for (let attr in patch.attrs) {
          let value = patch.attrs[attr];
          if (value) {
            utils.setAttr(node, attr, value);
          } else {
            node.removeAttribute(attr);
          }
        }
        break;
      case utils.TEXT:
        node.textContent = patch.content;
        break;
      case utils.REPLACE:
        let newNode = (patch.node instanceof Element) ? path.node.render() : document.createTextNode(path.node);
        node.parentNode.replaceChild(newNode, node);
        break;
      case utils.REMOVE:
        node.parentNode.removeChild(node);
        break;
    }
  });
}
module.exports = patch;
  • 以上只是patch方法的简单实现,我们看一下源码的核心部分:
function patch (oldVnode, vnode) {
    // some code
    if (sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode)
    } else {
        const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点
        let parentEle = api.parentNode(oEl)  // 父元素
        createEle(vnode)  // 根据Vnode生成新元素
        if (parentEle !== null) {
            api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素
            api.removeChild(parentEle, oldVnode.el)  // 移除以前的旧元素节点
            oldVnode = null
        }
    }
    // some code 
    return vnode
}

patch函数接收两个参数oldVnode和Vnode分别代表新的节点和之前的旧节点

  • 判断两节点是否值得比较,值得比较则执行patchVnode
function sameVnode (a, b) {
  return (
    a.key === b.key &&  // key值
    a.tag === b.tag &&  // 标签名
    a.isComment === b.isComment &&  // 是否为注释节点
    // 是否都定义了data,data包含一些具体信息,例如onclick , style
    isDef(a.data) === isDef(b.data) &&  
    sameInputType(a, b) // 当标签是<input>的时候,type必须相同
  )
}
  • 不值得比较则用Vnode替换oldVnode

如果两个节点都是一样的,那么就深入检查他们的子节点。如果两个节点不一样那就说明Vnode完全被改变了,就可以直接替换oldVnode。

虽然这两个节点不一样但是他们的子节点一样怎么办?别忘了,diff可是逐层比较的,如果第一层不一样那么就不会继续深入比较第二层了。(我在想这算是一个缺点吗?相同子节点不能重复利用了...)

patchVnode

当我们确定两个节点值得比较之后我们会对两个节点指定patchVnode方法。那么这个方法做了什么呢?

patchVnode (oldVnode, vnode) {
    const el = vnode.el = oldVnode.el
    let i, oldCh = oldVnode.children, ch = vnode.children
    if (oldVnode === vnode) return
    if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
        api.setTextContent(el, vnode.text)
    }else {
        updateEle(el, vnode, oldVnode)
        if (oldCh && ch && oldCh !== ch) {
            updateChildren(el, oldCh, ch)
        }else if (ch){
            createEle(vnode) //create el's children dom
        }else if (oldCh){
            api.removeChildren(el)
        }
    }
}

这个函数做了以下事情:

  • 找到对应的真实dom,称为el
  • 判断Vnode和oldVnode是否指向同一个对象,如果是,那么直接return
  • 如果他们都有文本节点并且不相等,那么将el的文本节点设置为Vnode的文本节点。
  • 如果oldVnode有子节点而Vnode没有,则删除el的子节点
  • 如果oldVnode没有子节点而Vnode有,则将Vnode的子节点真实化之后添加到el
  • 如果两者都有子节点,则执行updateChildren函数比较子节点,这一步很重要
  • 其他几个点都很好理解,我们详细来讲一下updateChildren

updateChildren

代码量很大,不方便一行一行的讲解,所以下面结合一些示例图来描述一下。

updateChildren (parentElm, oldCh, newCh) {
    let oldStartIdx = 0, newStartIdx = 0
    let oldEndIdx = oldCh.length - 1
    let oldStartVnode = oldCh[0]
    let oldEndVnode = oldCh[oldEndIdx]
    let newEndIdx = newCh.length - 1
    let newStartVnode = newCh[0]
    let newEndVnode = newCh[newEndIdx]
    let oldKeyToIdx
    let idxInOld
    let elmToMove
    let before
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
        if (oldStartVnode == null) {   // 对于vnode.key的比较,会把oldVnode = null
            oldStartVnode = oldCh[++oldStartIdx] 
        }else if (oldEndVnode == null) {
            oldEndVnode = oldCh[--oldEndIdx]
        }else if (newStartVnode == null) {
            newStartVnode = newCh[++newStartIdx]
        }else if (newEndVnode == null) {
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newStartVnode)) {
            patchVnode(oldStartVnode, newStartVnode)
            oldStartVnode = oldCh[++oldStartIdx]
            newStartVnode = newCh[++newStartIdx]
        }else if (sameVnode(oldEndVnode, newEndVnode)) {
            patchVnode(oldEndVnode, newEndVnode)
            oldEndVnode = oldCh[--oldEndIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldStartVnode, newEndVnode)) {
            patchVnode(oldStartVnode, newEndVnode)
            api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
            oldStartVnode = oldCh[++oldStartIdx]
            newEndVnode = newCh[--newEndIdx]
        }else if (sameVnode(oldEndVnode, newStartVnode)) {
            patchVnode(oldEndVnode, newStartVnode)
            api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
            oldEndVnode = oldCh[--oldEndIdx]
            newStartVnode = newCh[++newStartIdx]
        }else {
           // 使用key时的比较
            if (oldKeyToIdx === undefined) {
                oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
            }
            idxInOld = oldKeyToIdx[newStartVnode.key]
            if (!idxInOld) {
                api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                newStartVnode = newCh[++newStartIdx]
            }
            else {
                elmToMove = oldCh[idxInOld]
                if (elmToMove.sel !== newStartVnode.sel) {
                    api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
                }else {
                    patchVnode(elmToMove, newStartVnode)
                    oldCh[idxInOld] = null
                    api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
                }
                newStartVnode = newCh[++newStartIdx]
            }
        }
    }
    if (oldStartIdx > oldEndIdx) {
        before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
        addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
    }else if (newStartIdx > newEndIdx) {
        removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
    }
}

先说一下这个函数做了什么

将Vnode的子节点Vch和oldVnode的子节点oldCh提取出来
oldCh和vCh各有两个头尾的变量StartIdx和EndIdx,它们的2个变量相互比较,一共有4种比较方式。如果4种比较都没匹配,如果设置了key,就会用key进行比较,在比较的过程中,变量会往中间靠,一旦StartIdx>EndIdx表明oldCh和vCh至少有一个已经遍历完了,就会结束比较。

图解updateChildren

终于来到了这一部分,上面的总结相信很多人也看得一脸懵逼,下面我们好好说道说道:

粉红色的部分为oldCh和vCh


oldCh和vCh

我们将它们取出来并分别用s和e指针指向它们的头child和尾child


oldCh和vCh

现在分别对oldS、oldE、S、E两两做sameVnode比较,有四种比较方式,当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,这句话有点绕,打个比方

  • 如果是oldS和E匹配上了,那么真实dom中的第一个节点会移到最后
  • 如果是oldE和S匹配上了,那么真实dom中的最后一个节点会移到最前,匹配上的两个指针向中间移动
  • 如果四种匹配没有一对是成功的,那么遍历oldChild,S挨个和他们匹配,匹配成功就在真实dom中将成功的节点移到最前面,如果依旧没有成功的,那么将S对应的节点插入到dom中对应的oldS位置,oldS和S指针向中间移动。

oldCh和vCh

第一步

oldS = a, oldE = d;
S = a, E = b;
  • oldS和S匹配,则将dom中的a节点放到第一个,已经是第一个了就不管了,此时dom的位置为:a b d

第二步

oldS = b, oldE = d;
S = c, E = b;
  • oldS和E匹配,就将原本的b节点移动到最后,因为E是最后一个节点,他们位置要一致,这就是上面说的:当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,此时dom的位置为:a d b

第三步

oldS = d, oldE = d;
S = c, E = d;

oldE和E匹配,位置不变此时dom的位置为:a d b

第四步

oldS++;
oldE--;
oldS > oldE;
  • 遍历结束,说明oldCh先遍历完。就将剩余的vCh节点根据自己的的index插入到真实dom中去,此时dom位置为:a c d b
一次模拟完成。

这个匹配过程的结束有两个条件:

  • oldS > oldE表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去(如上图)
  • S > E表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉

oldCh和vCh

下面再举一个例子,可以像上面那样自己试着模拟一下

oldCh和vCh

当这些节点sameVnode成功后就会紧接着执行patchVnode了,可以看一下上面的代码

if (sameVnode(oldStartVnode, newStartVnode)) {
    patchVnode(oldStartVnode, newStartVnode)
}

就这样层层递归下去,直到将oldVnode和Vnode中的所有子节点比对完。也将dom的所有补丁都打好啦。那么现在再回过去看updateChildren的代码会不会容易很多呢?

总结

以上为diff算法的全部过程,放上一张文章开始就发过的总结图,可以试试看着这张图回忆一下diff的过程。

diff

git clone一个github上的仓库,报错或者太慢解决办法。

  • 1.首先第一步前提是已经打开了SS代理。

  • 2.如果要设置全局代理,可以依照这样设置。

git config --global http.proxy http://127.0.0.1:1080
git config --global https.proxy https://127.0.0.1:1080

但请注意,需要查看自己的端口是不是也是1080,可以打开你的SS查看代理设置

  • 3.完成上面一步后,此时输入git clone xxxxxxx就可以利用代理进行下载了.
    但同时,也请注意,这里指的是https协议,也就是
git clone https://www.github.com/xxxx/xxxx.git

这种对于SSH协议,也就是

git clone [email protected]:xxxxxx/xxxxxx.git

这种,依旧是无效的

  • 4.以上为总结,但我不推荐直接用全局代理
    因为如果挂了全局代理,这样如果需要克隆coding之类的国内仓库,会奇慢无比
    所以我建议使用这条命令,只对github进行代理,对国内的仓库不影响
git config --global http.https://github.com.proxy https://127.0.0.1:1080
git config --global https.https://github.com.proxy https://127.0.0.1:1080

同时,如果在输入这条命令之前,已经输入全局代理的话,可以输入进行取消

git config --global --unset http.proxy
git config --global --unset https.proxy
  • 5.git 设置 socks5 代理。 ss暴露的是socks5。socks5代理的方法.
git config --global http.proxy 'socks5://127.0.0.1:1080'  
git config --global https.proxy 'socks5://127.0.0.1:1080'

开始 clone,如果觉得仓库太大,可以在 git clone 中加入参数 --depth=1,只拉取最近的一个 revision。

git clone --depth=1 https://github.com/torvalds/linux.git

如果后面想看历史的版本,那么也很好办,使用 git fetch 即可

git fetch --unshallow

浏览器渲染机制

浏览器的内核是指支持浏览器运行的最核心的程序,分为两个部分的,一是渲染引擎,另一个是JS引擎。渲染引擎在不同的浏览器中也不是都相同的。目前市面上常见的浏览器内核可以分为这四种:Trident(IE)、Gecko(火狐)、Blink(Chrome、Opera)、Webkit(Safari)。这里面大家最耳熟能详的可能就是 Webkit 内核了,Webkit 内核是当下浏览器世界真正的霸主。



页面加载过程



在介绍浏览器渲染过程之前,我们简明扼要介绍下页面的加载过程,有助于更好理解后续渲染过程。
要点如下:

  • 浏览器根据 DNS 服务器得到域名的 IP 地址
  • 向这个 IP 的机器发送 HTTP 请求
  • 服务器收到、处理并返回 HTTP 请求
  • 浏览器得到返回内容

例如在浏览器输入https://juejin.im/,然后经过 DNS 解析,juejin.im对应的 IP36.248.217.149(不同时间、地点对应的 IP 可能会不同)。然后浏览器向该 IP 发送 HTTP 请求。
服务端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容如下:



返回文本


其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。接下来就是浏览器的渲染过程。



浏览器渲染过程




浏览器渲染过程大体分为如下三部分:


1)浏览器会解析三个东西:


  • 一是HTML/SVG/XHTML,HTML字符串描述了一个页面的结构,浏览器会把HTML结构字符串解析转换DOM树形结构。


  • 二是CSS,解析CSS会产生CSS规则树,它和DOM结构比较像。

  • 三是Javascript脚本,等到Javascript 脚本文件加载后, 通过 DOM API 和 CSSOM API 来操作 DOM Tree 和 CSS Rule Tree。



2)解析完成后,浏览器引擎会通过DOM Tree 和 CSS Rule Tree 来构造 Rendering Tree。


  • Rendering Tree 渲染树并不等同于DOM树,渲染树只会包括需要显示的节点和这些节点的样式信息。
  • CSS 的 Rule Tree主要是为了完成匹配并把CSS Rule附加上Rendering Tree上的每个Element(也就是每个Frame)。
  • 然后,计算每个Frame 的位置,这又叫layout和reflow过程。

3)最后通过调用操作系统Native GUI的API绘制。



> 接下来我们针对这其中所经历的重要步骤详细阐述

构建DOM

浏览器会遵守一套步骤将HTML 文件转换为 DOM 树。宏观上,可以分为几个步骤:

  • 浏览器从磁盘或网络读取HTML的原始字节,并根据文件的指定编码(例如 UTF-8)将它们转换成字符串。

在网络中传输的内容其实都是 01这些字节数据。当浏览器接收到这些字节数据以后,它会将这些字节数据转换为字符串,也就是我们写的代码。

  • 将字符串转换成Token,例如:<html>、<body>等。Token中会标识出当前Token是“开始标签”或是“结束标签”亦或是“文本”等信息。

这时候你一定会有疑问,节点与节点之间的关系如何维护? 事实上,这就是Token要标识“起始标签”和“结束标签”等标识的作用。例如“title”Token的起始标签和结束标签之间的节点肯定是属于“head”的子节点。

上图给出了节点之间的关系,例如:Hello, Token位于title开始标签与title结束标签之间,表明Hello, Tokentitle, Token的子节点。同理title, Tokenhead, Token的子节点。

  • 生成节点对象并构建DOM
    事实上,构建DOM的过程中,不是等所有Token都转换完成后再去生成节点对象,而是一边生成Token一边消耗Token来生成节点对象。换句话说,每个Token被生成后,会立刻消耗这个Token创建出节点对象。

注意:带有结束标签标识的Token不会创建节点对象。

接下来我们举个例子,假设有段HTML文本:

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

上面这段HTML会解析成这样:
html

构建CSSOM


DOM会捕获页面的内容,但浏览器还需要知道页面如何展示,所以需要构建CSSOM。

构建CSSOM的过程与构建DOM的过程非常相似,当浏览器接收到一段CSS,浏览器首先要做的是识别出Token,然后构建节点并生成CSSOM。





在这一过程中,浏览器会确定下每一个节点的样式到底是什么,并且这一过程其实是很消耗资源的。因为样式你可以自行设置给某个节点,也可以通过继承获得。在这一过程中,浏览器得递归 CSSOM 树,然后确定具体的元素到底是什么样式。

注意:CSS匹配HTML元素是一个相当复杂和有性能问题的事情。所以,DOM树要小,CSS尽量用id和class,千万不要过渡层叠下去。



构建渲染树

当我们生成 DOM 树和 CSSOM 树以后,就需要将这两棵树组合为渲染树。




在这一过程中,不是简单的将两者合并就行了。渲染树只会包括需要显示的节点和这些节点的样式信息,如果某个节点是 display: none 的,那么就不会在渲染树中显示。

我们或许有个疑惑:浏览器如果渲染过程中遇到JS文件怎么处理?

渲染过程中,如果遇到 <script> 就停止渲染,执行 JS 代码。因为浏览器有 GUI 渲染线程与 JS 引擎线程,为了防止渲染出现不可预期的结果,这两个线程是互斥的关系。
JavaScript 的加载、解析与执行会阻塞 DOM 的构建,也就是说,在构建 DOM 时,HTML 解析器若遇到了 JavaScript,那么它会暂停构建 DOM ,将控制权移交给 JavaScript 引擎,等 JavaScript 引擎运行完毕,浏览器再从中断的地方恢复 DOM 构建,

也就是说,如果你想首屏渲染的越快,就越不应该在首屏就加载 JS 文件,这也是都建议将 script 标签放在 body 标签底部的原因。当然在当下,并不是说 script 标签必须放在底部,因为你可以给 script 标签添加 defer 或者 async 属性(下文会介绍这两者的区别).

JS文件不只是阻塞DOM的构建,它会导致CSSOM也阻塞DOM的构建。

原本 DOMCSSOM 的构建是互不影响,井水不犯河水,但是一旦引入了 JavaScriptCSSOM 也开始阻塞 DOM 的构建,只有 CSSOM 构建完毕后,DOM 再恢复 DOM 构建。

这是什么情况?

这是因为 JavaScript 不只是可以改 DOM,它还可以更改样式,也就是它可以更改 CSSOM 。因为不完整的CSSOM是无法使用的,如果JavaScript想访问CSSOM并更改它,那么在执行JavaScript时,必须要能拿到完整的CSSOM。所以就导致了一个现象,如果浏览器尚未完成CSSOM的下载和构建,而我们却想在此时运行脚本,那么浏览器将延迟脚本执行和 DOM构建,直至其完成CSSOM的下载和构建。

也就是说,在这种情况下,浏览器会先下载和构建CSSOM,然后再执行JavaScript,最后在继续构建 DOM

布局与绘制

当浏览器生成渲染树以后,就会根据渲染树来进行布局(也可以叫做回流)。这一阶段浏览器要做的事情是要弄清楚各个节点在页面中的确切位置和大小。通常这一行为也被称为“自动重排”。

布局流程的输出是一个“盒模型”,它会精确地捕获每个元素在视口内的确切位置和尺寸,所有相对测量值都将转换为屏幕上的绝对像素。
布局完成后,浏览器会立即发出 Paint SetupPaint 事件,将渲染树转换成屏幕上的像素。

JavaScript 如何工作:对引擎、运行时、调用堆栈的概述

简介

几乎每个人听说过 V8 引擎的概念,而且,大多数人都知道 JavaScript 是单线程的,或者是它是使用回调队列的。
  在这篇文章中,我们将详细的介绍这些概念,并解释 JavaScript 是怎么工作的。通过了解这些细节,你就能利用这些提供的 API 来写出更好的,非阻塞的应用来。如果你对 JavaScript 比较陌生,那么这篇文章将帮助您理解为什么 JavaScript 相较于其他语言显得如此“怪异”。如果您是一位经验丰富的 JavaScript 开发人员,希望它能给你带来一些新的见解,说明 JavaScript 的运行时,尽管你可能每天都会用到它。

JavaScript 引擎

JavaScript 引擎说起来最流行的当然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中,下面有个简单的图能说明他们的关系:
引擎主要由两部分组成

这个引擎主要由两部分组成

  • 内存堆:这是内存分配发生的地方
  • 调用栈:这是你的代码执行时的地方

运行时

有些浏览器的 API 经常被使用到(比如说:setTimeout),但是,这些 API 却不是引擎提供的。那么,他们是从哪儿来的呢?事实上这里面实际情况有点复杂。
事实上这里面实际情况有点复杂
所以说我们还有很多引擎之外的 API,我们把这些称为浏览器提供的 Web API,比如说 DOM、AJAX、setTimeout等等。

  然后我们还拥有如此流行的事件循环和回调队列。

调用栈

JavaScript 是一门单线程的语言,这意味着它只有一个调用栈,因此,它同一时间只能做一件事。
调用栈是一种数据结构,它记录了我们在程序中的位置。如果我们运行到一个函数,它就会将其放置到栈顶。当从这个函数返回的时候,就会将这个函数从栈顶弹出,这就是调用栈做的事情。

  让我们来看一看下面的例子:

function multiply(x, y) {
      return x * y;
}
function printSquare(x) {
      var s = multiply(x, x);
      console.log(s);
}
    printSquare(5);

当程序开始执行的时候,调用栈是空的,然后,步骤如下:
当程序开始执行的时候,调用栈是空的,然后,步骤如下

每一个进入调用栈的都称为__调用帧__。

  这能清楚的知道当异常发生的时候堆栈追踪是怎么被构造的,堆栈的状态是如何的。让我们看一下下面的代码:

function foo() {
      throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
      foo();
}
function start() {
      bar();
}

start();

如果这发生在 Chrome 里(假设这段代码实在一个名为 foo.js 的文件中),那么将会生成以下的堆栈追踪:

F12错误详情

"堆栈溢出",当你达到调用栈最大的大小的时候就会发生这种情况,而且这相当容易发生,特别是在你写递归的时候却没有全方位的测试它。我们来看看下面的代码:

function foo() {
      foo();
}
foo();

当我们的引擎开始执行这段代码的时候,它从 foo 函数开始。然后这是个递归的函数,并且在没有任何的终止条件的情况下开始调用自己。因此,每执行一步,就会把这个相同的函数一次又一次地添加到调用堆栈中。然后它看起来就像是这样的:

像是这样的

然后,在某一时刻,调用栈中的函数调用的数量超过了调用栈的实际大小,浏览器决定干掉它,抛出一个错误,它看起来就像是这样:

像是这样的

在单个线程上运行代码很容易,因为你不必处理在多线程环境中出现的复杂场景——例如死锁。但是在一个线程上运行也非常有限制。由于 JavaScript 只有一个调用堆栈,当某段代码运行变慢时会发生什么?

并发与事件循环

调用栈中的函数调用需要大量的时间来处理,那么这会发生什么情况呢?例如,假设你想在浏览器中使用 JavaScript 进行一些复杂的图片转码。
  你可能会问?这算什么问题?事实上,问题是当调用栈有函数要执行,浏览器就不能做任何事,它会被堵塞住。这意味着浏览器不能渲染,不能运行其他的代码,它被卡住了。如果你想在应用里让 UI 很流畅的话,这就会产生问题。
  而且这不是唯一的问题,一旦你的浏览器开始处理调用栈中的众多任务,它可能会停止响应相当长一段时间。大多数浏览器都会这么做,报一个错误,询问你是否想终止 web 页面。
这样看来,这并不是最好的用户体验,不是吗?

  那么,如何在不阻塞 UI 的情况下执行复杂的代码,让浏览器不会不响应?解决方案就是异步回调。这将在“ JavaScript 如何工作”教程的第2部分中详细解释:“在V8引擎中,如何编写优化代码”。

像是这样的

这样看来,这并不是最好的用户体验,不是吗?

  那么,如何在不阻塞 UI 的情况下执行复杂的代码,让浏览器不会不响应?解决方案就是异步回调。这将在“ JavaScript 如何工作”教程的第2部分中详细解释:“在V8引擎中,如何编写优化代码”。

这里是一些难找的正则

jck.isCardNo = function (s) {
    //身份证号码为15位或者18位,15位时全为数字,18位前17位为数字,最后一位是校验位,可能为数字或字符X
    var reg = /(^\d{15}$)|(^\d{17}(\d|X)$)/;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

//获得字符串实际长度,中文2,英文1
//<param name="s">要获得长度的字符串</param>
jck.getLength = function (s) {
    var l = 0, len = s.length, charCode = -1;
    for (var i = 0; i < len; i++) {
        charCode = s.charCodeAt(i);
        if (charCode >= 0 && charCode <= 128) l += 1;
        else l += 2;
    }
    return l;
};


jck.IsURL = function (str_url) {
    var strRegex = "^((https|http|ftp|rtsp|mms)?://)"
  + "?(([0-9a-z_!~*'().&=+$%-]+: )?[0-9a-z_!~*'().&=+$%-]+@)?" //ftp的user@  
        + "(([0-9]{1,3}\.){3}[0-9]{1,3}" // IP形式的URL- 199.194.52.184  
        + "|" // 允许IP和DOMAIN(域名) 
        + "([0-9a-z_!~*'()-]+\.)*" // 域名- www.  
        + "([0-9a-z][0-9a-z-]{0,61})?[0-9a-z]\." // 二级域名  
        + "[a-z]{2,6})" // first level domain- .com or .museum  
        + "(:[0-9]{1,4})?" // 端口- :80  
        + "((/?)|" // a slash isn't required if there is no file name  
        + "(/[0-9a-z_!~*'().;?:@&=+$,%#-]+)+/?)$";
    var re = new RegExp(strRegex);
    //re.test() 
    if (re.test(str_url)) {
        return (true);
    } else {
        return (false);
    }
}

//限制字符只能是中文、字母、数字、_、$、.、@、-、(、)、顿号、*、空格、·、#、全角字符
//<param name="s">要判断的字符串</param>
jck.isLimitString = function (s) {
    var reg = /^[\u4e00-\u9fa5\w_\$\.\@\-\(\)\【\】\ \*\、\#\·\uFF00-\uFFFF]*$/g;
    //var reg = /^[^'\\]+$/
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

//限制字符只能是中文、英文、数字、_、$、.、-、(、)、【、】、顿号、*
//用户帐号限制
jck.isLimitAccount = function (s) {
    var reg = /^[\u4e00-\u9fa5\w_\$\.\-\(\)\【\】\*\、\uFF00-\uFFFF]*$/g;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

//限制字符只能是英文、数字、下划线、减号、“@”、“.”、"【"、"】"、“$”
//用户帐号限制
jck.isLimitPinYin = function (s) {
    var reg = /^[a-zA-Z0-9\w_\@\$\.\-\(\)\【\】\*\、\uFF00-\uFFFF]*$/g;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

//验证电子邮箱
jck.checkEmail = function (s) {
    var reg = /^([a-zA-Z0-9\_\-\.])+@([a-zA-Z0-9_-])+((\.[a-zA-Z0-9_-]{2,10}){1,9})$/;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

//获取URL传入参数值
jck.QueryString = function (Name, LocationSearch) {
    var sValue = null;
    if (LocationSearch == null)
        sValue = location.search.match(new RegExp("[\?\&]" + Name + "=([^\&]*)(\&?)", "i"));
    else
        sValue = LocationSearch.match(new RegExp("[\?\&]" + Name + "=([^\&]*)(\&?)", "i"));
    return sValue ? sValue[1] : sValue;
};

//电话号码验证
jck.checkPhoneNumber = function (s) {
    //var reg = /^[+]{0,1}(\d){1,3}[ ]?([-]?((\d)|[ ]){1,12})+$/;
    var reg = /^[+]{0,1}(\d){1,3}[ ]?([-]?((\d)){1,12})+$/;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};


jck.checkIsNumber = function (s) {
    //var reg = /^[+]{0,1}(\d){1,3}[ ]?([-]?((\d)|[ ]){1,12})+$/;
    var reg = /^\d+$/;
    if (reg.test(s) === false) {
        return false;
    }
    return true;
};

jck.checkIP = function (str) { //IP正则表达式
    IP = '(25[0-5]|2[0-4]\\d|1\\d\\d|\\d\\d|\\d)';
    IPdot = IP + '\\.';
    isIPaddress = new RegExp('^' + IPdot + IPdot + IPdot + IP + '$');
    //验证IP,返回结果
    return (isIPaddress.test(str));
}

//截取字符串
jck.subString = function (str, length, left) {
    var len = this.getLength(str);
    if (len <= length) return str;
    var t1 = str.replace(/([\u0391-\uffe5])/ig, '$1a');
    var idx = (left === false) ? (len - length) : 0;
    var idxEnd = (left === false) ? len : length;
    var t2 = t1.substring(idx, idxEnd);
    return t3 = t2.replace(/([\u0391-\uffe5])a/ig, '$1');
};

///举例:alert(compareDate('2004-12-01','2004-05-02'));目前支持年-月-日这样的格式
///比较日期大小
///哪果第一个日期参数大于第二个日期参数返回值为true,返之返回false
jck.compareDate = function (DateOne, DateTwo) {

    var OneMonth = DateOne.substring(5, DateOne.lastIndexOf("-"));
    var OneDay = DateOne.substring(DateOne.length, DateOne.lastIndexOf("-") + 1);
    var OneYear = DateOne.substring(0, DateOne.indexOf("-"));

    var TwoMonth = DateTwo.substring(5, DateTwo.lastIndexOf("-"));
    var TwoDay = DateTwo.substring(DateTwo.length, DateTwo.lastIndexOf("-") + 1);
    var TwoYear = DateTwo.substring(0, DateTwo.indexOf("-"));

    if (Date.parse(OneMonth + "/" + OneDay + "/" + OneYear) > Date.parse(TwoMonth + "/" + TwoDay + "/" + TwoYear)) {
        return true;
    }
    else {
        return false;
    }

}

///全角空格为12288,半角空格为32 
///其他字符半角(33-126)与全角(65281-65374)的对应关系是:均相差65248 
//半角转换为全角函数
jck.toDBC = function (txtstring) {
    var tmp = "";
    for (var i = 0; i < txtstring.length; i++) {
        var charC = txtstring.charCodeAt(i);
        if (charC == 32) {//空格
            tmp += String.fromCharCode(12288);
        }
        else if (charC < 127) {
            tmp += String.fromCharCode(charC + 65248);
        }
        else tmp += String.fromCharCode(charC);
    }
    return tmp;
}
jck.specialCharToDBC = function (txtstring) {
    var tmp = "";
    for (var i = 0; i < txtstring.length; i++) {
        var charC = txtstring.charCodeAt(i);
        if (charC == 32) {
            tmp += String.fromCharCode(12288);
        }
        else if (charC < 48 || charC > 57 && charC < 65 || charC > 90 && charC < 97 || charC > 122 && charC < 127) {
            tmp += String.fromCharCode(charC + 65248);
        }
        else tmp += String.fromCharCode(charC);
    }
    return tmp;
}
jck.limtCharToDBC = function (txtstring) {
    var tmp = "";
    for (var i = 0; i < txtstring.length; i++) {
        var charC = txtstring.charCodeAt(i);
        //        if (charC == 32) {//空格
        //            tmp += String.fromCharCode(12288);
        //        }
        //        else 
        if (charC == 39) {//“'”号
            tmp += String.fromCharCode(charC + 65248);
        }
        else tmp += String.fromCharCode(charC);
    }
    return tmp;
}

//全角转换为半角函数 
jck.toCDB = function (str) {
    var tmp = "";
    for (var i = 0; i < str.length; i++) {
        if (str.charCodeAt(i) > 65248 && str.charCodeAt(i) < 65375) {
            tmp += String.fromCharCode(str.charCodeAt(i) - 65248);
        }
        else {
            tmp += String.fromCharCode(str.charCodeAt(i));
        }
    }
    return tmp
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.