風魔小次郎

風魔小次郎 查看完整档案

深圳编辑  |  填写毕业院校  |  填写所在公司/组织 www.xhfkindergarten.cn 编辑
编辑

web前端
母校: 小哈佛幼儿园
github:XHFkindergarten

个人动态

風魔小次郎 提出了问题 · 1月14日

snowpack如何指定根目录呢?

问题描述

想把当前的某个库的 playgroundwebpackDevServer 迁移为 snowpack ,但是发现根据官网的配置文档没有办法实现指定启动目录。

{
  "mount": {
    "src": "/dist",
    "public": "/"
  }
}

你期待的结果是什么?实际看到的错误信息又是什么?

想求教如何配置 snowpack 命令的启动目录为项目目录下的 playground 文件夹呢~

关注 1 回答 0

風魔小次郎 赞了文章 · 1月4日

用 Puppeteer + Nodejs 构建高效海报 & 截图生成服务

image

前言:在前端日常开发中,toC的业务难免会有一些截图或者生成海报的业务需求受限于html2canvas的不兼容性,或者canvas画图的样式局限性,目前我们又有了一种新的解决方案,node + puppeteer的组合,

puppeteer

puppeteer 初识

Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具。Chrome 作为浏览器市场的领头羊,Chrome Headless 将成为 web 应用 自动化测试 的行业标杆

那么我们可以用puppeteer来些什么?

  • 生成页面的截图或者PDF
  • 自动模拟用户行为,表单提交、按钮点击等
  • 创建自动化测试环境
  • 捕获站点的时间线跟踪,以帮助诊断性能问题。
  • 爬取 SPA 页面并进行预渲染(即'SSR')
可见第一种特性正式我们用的到的

puppeteer 安装

npm install puppeteer
// 下载内核步骤可能会失败,cnpm 或科学上网

puppeteer 主要api

只介绍截图相关,如果需要其他特性的查询,请移步:https://github.com/puppeteer/...

1.启动实例

import * as puppeteer from 'puppeteer'

(async () => {
  await puppeteer.launch()
})()

2.网页截图

png截图

await page.setViewport({width, height})
  await page.goto(url)
  //对整个页面截图
  await page.screenshot({
      path: path.resolve(`./screenshot/${fileName}.png`),  //图片保存路径
      type: 'png',
      fullPage: false //边滚动边截图
  })

不仅如此 还可以模拟手机的device环境

import * as puppeteer from "puppeteer";
import * as devices from "puppeteer/DeviceDescriptors";
const iPhone = devices["iPhone 6"];

(async () => {
  const browser = await puppeteer.launch({
    headless: false
  });
  const page = await browser.newPage();
  await page.emulate(iPhone);
  await page.goto("https://baidu.com/");
  await browser.close();
})();

pdf截图

(async () => {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.goto("https://example.com/");
  await page.pdf({
    displayHeaderFooter: true,
    path: 'example.pdf',
    format: 'A4',
    headerTemplate: '<b style="font-size: 30px">Hello world<b/>',
    footerTemplate: '<b style="font-size: 30px">Some text</b>',
    margin: {
      top: "100px",
      bottom: "200px",
      right: "30px",
      left: "30px",
    }
  });
  await browser.close();
})()
好~有了这些我们就可以满足我们基本截图需求了

基本架构

说明

截图请求接口:此api会直接请求到 node 服务,会携带参数包括:

  • 托管页地址 (为要截取网页的地址)
  • 要截图的宽高
  • 截图的类型 (png/pdf)

截图托管页:静态的html页面。可url传参数去控制变量

node服务收到接口请求后,会请求托管页,构建一个要截取的网页,执行截图操作的api,返回截图buffer数据给请求的接口,到此为止,我们基础架构就整理完毕了

上 demo 代码

前置环境,假设我们现在已经有一个可用的node环境(node + koa2)
// router层
const screenshotControllers = require('../controllers/puppeteer')

const router = new KoaRouter()
exports.router = router.post('/api/puppeteer/v1/screenshot', screenshotControllers.api.screenshot)
// conteoller层
const service = require('../service/puppeteer')

const screenshot = (ctx, next) => {
    return service.api.screenshot(ctx);
}

exports.api = {
    screenshot
}
// service层
const { getScreenshot } = require('../puppeteer/index')
exports.api = {
    screenshot: async (ctx) => {
        const { width, height, url, tid} = ctx.request.body
        const res = await getScreenshot(url, width, height, tid)
        console.log(res)
        if (res) {
            ctx.body = {
                code: 200,
                message: '成功',
                data: res
            }
        }
    }
}
//具体业务层
const puppeteer = require('puppeteer');
const path = require('path')

exports.getScreenshot = async (url, width = 800, height = 600, fileName) => {
  const browser = await puppeteer.launch()
  const page = await browser.newPage()
  //设置可视区域大小,默认的页面大小为800x600分辨率
  await page.setViewport({width, height})
  await page.goto(url)
  //对整个页面截图
  await page.screenshot({
      path: path.resolve(`./screenshot/${fileName}.png`),  //图片保存路径
      type: 'png',
      fullPage: false //边滚动边截图
  })
  
  //执行cos 或 oss 脚本,把图片上传到cdn环境,此处由于调试,暂时省略

  await page.close()
  await browser.close()

  return `${fileName}.png`
}

测试服务可用性

启动 node 服务

启动 psotman 模拟请求接口

1.百度


查看结果

嗯,看起来也没啥问题

具体业务应用

目前有生成海报的需求,那么我们应该怎么搞?

1.根据UI图,搞定托管页
2.根据传入的变量id,写好后端逻辑,

3.调用node服务,获得图片buffer 或图片cdn地址

await axios({
       method: 'POST',
       url: 'http://xxx/api/puppeteer/v1/screenshot',
       data: {
        url: `http://xxx/postFrame/home?lecCode=lec${i}`,
        width: 335,
        height: 525,
        tid: `lec${i}`
    }
   })
至此就大功搞成了,这种解决方案可以磨平各种运行环境的差异性,减少本地生成图片的失败率

更多知识点和福利,请👇
image

查看原文

赞 16 收藏 11 评论 2

風魔小次郎 赞了文章 · 2020-12-05

手写防抖与节流函数

我们经常会绑定一些持续触发的事件,比如resize、scroll、mousemove等等,如果事件调用无限制,会加重浏览器负担,导致用户体验差,我们可以使用debounce(防抖)和throttle(节流)的方式来减少频繁的调用,同时也不会影响实际的效果。

防抖的概念

触发事件后n秒后才执行函数,如果在n秒内触发了事件,则会重新计算函数执行时间
防抖函数可以分为立即执行,和非立即执行两个版本

  • 非立即执行版本
function debounce(fn, wait) {
    let timer = null
    return function () {
        const context = this
        const args = arguments
        timer && clearTimeout(timer)
        timer = setTimeout(() => {
            fn.apply(context, args)   
        }, wait)
    }
}

此函数一开始不会马上执行,而是等到用户操作结束之后等待wait秒后才执行,如果在wait之内用户又触发了事件,则会重新计算

  • 立即执行版本
function debounce(fn, wait) {
    let timer = null
    return function () {
        const args = arguments
        const now = !timer
        timer && clearTimeout(timer)
        timer = setTimeout(() => {
            timer = null
        }, wait)
        if (now) {
            fn.apply(this, args)
        }
    }
}

立即执行就是触发事件后马上先执行一次,直到用户停止执行事件等待wait秒后再执行一次

我们可以将两种版本合并成一种

/**
 * @desc 函数防抖
 * @param fn 函数
 * @param wait 延迟执行毫秒数
 * @param immediate true 表立即执行,false 表示非立即执行
 */
 function debounce(fn, wait, immediate) {
    let timer = null
    return function () {
        const context = this
        const args = arguments
        timer && clearTimeout(timer)
        if (immediate) {
            const now = !timer
            timer = setTimeout(() => {
                timer = null
            }, wait)
            now && fn.apply(context, args)
        } else {
            timer = setTimeout(() => {
                fn.apply(context, args)
            }, wait)
        }
    }
 }

节流的概念

连续触发事件但在n秒内只执行一次函数
对于节流有时间戳和定时器两种版本

  • 时间戳版本
function throttle(fn, wait) {
    var prev = 0
    return function () {
        let now = Date.now()
        let context = this
        let args = arguments
        if (now - prev > wait) {
            fn.apply(context, args)
            prev = now
        }
    }
}

在持续触发事件的过程中,函数会立即执行,用户在wait秒内不管执行多少次事件,都会等待wait秒后再执行。

  • 定时器版本
function throttle(fn, wait) {
    var timer = null
    return function () {
        const context = this
        const args = arguments
        if (!timer) {
            timer = setTimeout(() => {
                timer = null
                fn.apply(context, args)
            }, wait)
        }
    }
}

在触发事件的过程中,不会立即执行,并且每wait秒执行一次,在停止触发事件后还会再执行一次。

时间戳版的函数触发是在时间段内开始的时候,而定时器版的函数触发是在时间段内结束的时候。

将两种方式合并

/**
 * @desc 函数节流
 * @param fn 函数
 * @param wait 延迟执行毫秒数
 * @param type 1 表时间戳版,2 表定时器版
 */
 function throttle(fn, wait, type) {
    if (type === 1) {
        var prev = 0
    } else {
        var timer = null
    }
     return function() {
        const context = this
        const args = arguments
         if (type === 2) {
             if (!timer) {
                 timer = setTimeout(() => {
                   timer = null
                   fn.apply(context, args) 
                 }, wait)
             }
         } else if(type === 1) {
            const now = Date.now()
            if (now - prev > wait) {
                fn.apply(context, args)
                prev = now
            }
         }
     }
 }

参考链接

查看原文

赞 3 收藏 0 评论 0

風魔小次郎 赞了文章 · 2020-12-01

聊一聊前端性能优化 CRP

image

什么是 CRP?

CRP又称关键渲染路径,引用MDN对它的解释:

关键渲染路径是指浏览器通过把 HTML、CSS 和 JavaScript 转化成屏幕上的像素的步骤顺序。优化关键渲染路径可以提高渲染性能。关键渲染路径包含了 Document Object Model (DOM),CSS Object Model (CSSOM),渲染树和布局。

优化关键渲染路径可以提升首屏渲染时间。理解和优化关键渲染路径对于确保回流和重绘可以每秒 60 帧、确保高性能的用户交互和避免无意义渲染至关重要。

如何结合CRP进行性能优化?

我想对于性能优化,大家都不陌生,无论是平时的工作还是面试,是一个老生常谈的话题。

如果单纯针对一些点去泛泛而谈,我想是不太严谨的。

今天我们结合一道非常经典的面试题:从输入URL到页面展示,这中间发生了什么?来从其中的某些环节,来深入谈谈前端性能优化 CRP

从输入 URL 到页面展示,这中间发生了什么?

这道题的经典程度想必不用我多说,这里我用一张图梳理了它的大致流程:
image
这个过程可以大致描述为如下:

1、URI 解析

2、DNS 解析(DNS 服务器)

3、TCP 三次握手(建立客户端和服务器端的连接通道)

4、发送 HTTP 请求

5、服务器处理和响应

6、TCP 四次挥手(关闭客户端和服务器端的连接)

7、浏览器解析和渲染

8、页面加载完成

本文我会从浏览器渲染过程、缓存、DNS 优化几方面进行性能优化的说明。

浏览器渲染过程

构建 DOM 树

构建DOM树的大致流程梳理为下图:
image

我们以下面这段代码为例进行分析:

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <link href="style.css" rel="stylesheet" />
    <title>构建DOM树</title>
  </head>
  <body>
    <p>森林</p>
    <div>之晨</div>
  </body>
</html>

首先浏览器从磁盘或网络中读取 HTML 原始字节,并根据文件的指定编码将它们转成字符。

然后通过分词器将字节流转换为 Token,在Token(也就是令牌)生成的同时,另一个流程会同时消耗这些令牌并转换成 HTML head 这些节点对象,起始和结束令牌表明了节点之间的关系。
image

当所有的令牌消耗完以后就转换成了DOM(文档对象模型)。

最终构建出的DOM结构如下:
image

构建 CSSOM 树

DOM树构建完成,接下来就是CSSOM树的构建了。

HTML的转换类似,浏览器会去识别CSS正确的令牌,然后将这些令牌转化成CSS节点。

子节点会继承父节点的样式规则,这里对应的就是层叠规则和层叠样式表。

构建DOM树的大致流程可梳理为下图:
image

我们这里采用上面的HTML为例,假设它有如下 css:

body {
  font-size: 16px;
}
p {
  font-weight: bold;
}
div {
  color: orange;
}

那么最终构建出的CSSOM树如下:
image

有了 DOMCSSOM,接下来就可以合成布局树(Render Tree)了。

构建渲染树

DOMCSSOM 都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM 树的结构,不同之处在于 DOM 树中那些不需要显示的元素会被过滤掉,如 display:none 属性的元素、head 标签、script 标签等。

复制好基本的布局树结构之后,渲染引擎会为对应的 DOM 元素选择对应的样式信息,这个过程就是样式计算。

样式计算

样式计算的目的是为了计算出 DOM 节点中每个元素的具体样式,这个阶段大体可分为三步来完成。

把 CSS 转换为浏览器能够理解的结构

HTML 文件一样,浏览器也是无法直接理解这些纯文本的 CSS 样式,所以当渲染引擎接收到 CSS 文本时,会执行一个转换操作,将 CSS 文本转换为浏览器可以理解的结构——styleSheets

转换样式表中的属性值,使其标准化

现在我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。

什么是属性值标准化?我们来看这样的一段CSS

body {
  font-size: 2em;
}
div {
  font-weight: bold;
}
div {
  color: red;
}

可以看到上面的 CSS 文本中有很多属性值,如 2em、bold、red,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。

那标准化后的属性值是什么样子的?

image
从图中可以看到,2em 被解析成了 32pxbold 被解析成了 700red 被解析成了 rgb(255,0,0)……

计算出 DOM 树中每个节点的具体样式

现在样式的属性已被标准化了,接下来就需要计算 DOM 树中每个节点的样式属性了,如何计算呢?

这其中涉及到两点:CSS 的继承规则层叠规则

这里由于不是本文的重点,我简单做下说明:

  • CSS 继承就是每个 DOM 节点都包含有父节点的样式
  • 层叠是 CSS 的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS 处于核心地位,CSS 的全称“层叠样式表”正是强调了这一点。

样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局。

计算布局

现在,我们有 DOM 树和 DOM 树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM 元素的几何位置信息。那么接下来就需要计算出 DOM 树中可见元素的几何位置,我们把这个计算过程叫做布局

绘制

通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。

到这里,浏览器的渲染过程就基本结束了,通过下面的一张图来梳理下:
image

到这里我们已经把浏览器解析和渲染的完整流程梳理完成了,那么这其中有那些地方可以去做性能优化呢?

从浏览器的渲染过程中可以做的优化点

通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。

  • 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络和 JavaScript 脚本。
  • 交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是 JavaScript 脚本。
  • 关闭阶段,主要是用户发出关闭指令后页面所做的一些清理操作。

这里我们需要重点关注加载阶段交互阶段,因为影响到我们体验的因素主要都在这两个阶段,下面我们就来逐个详细分析下。

加载阶段

我们先来分析如何系统优化加载阶段中的页面,来看一个典型的渲染流水线,如下图所示:

image
通过上面对浏览器渲染过程的分析我们知道JavaScript、首次请求的 HTML 资源文件、CSS 文件是会阻塞首次渲染的,因为在构建 DOM 的过程中需要 HTMLJavaScript 文件,在构造渲染树的过程中需要用到 CSS 文件。

这些能阻塞网页首次渲染的资源称为关键资源。而基于关键资源,我们可以继续细化出三个影响页面首次渲染的核心因素:

  • 关键资源个数。关键资源个数越多,首次页面的加载时间就会越长。
  • 关键资源大小。通常情况下,所有关键资源的内容越小,其整个资源的下载时间也就越短,那么阻塞渲染的时间也就越短。
  • 请求关键资源需要多少个RTT(Round Trip Time)RTT 是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。

了解了影响加载过程中的几个核心因素之后,接下来我们就可以系统性地考虑优化方案了。总的优化原则就是减少关键资源个数降低关键资源大小降低关键资源的 RTT 次数

  • 如何减少关键资源的个数?一种方式是可以将 JavaScriptCSS 改成内联的形式,比如上图的 JavaScriptCSS,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。另一种方式,如果 JavaScript 代码没有 DOM 或者 CSSOM 的操作,则可以改成 sync 或者 defer 属性
  • 如何减少关键资源的大小?可以压缩 CSSJavaScript 资源,移除 HTMLCSSJavaScript 文件中一些注释内容
  • 如何减少关键资源 RTT 的次数?可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN 来减少每次 RTT 时长。

交互阶段

接下来我们再来聊聊页面加载完成之后的交互阶段以及应该如何去优化。

先来看看交互阶段的渲染流水线:
image
其实这块大致有以下几点可以优化:

  • 避免DOM的回流。也就是尽量避免重排重绘操作。
  • 减少 JavaScript 脚本执行时间。有时JavaScript 函数的一次执行时间可能有几百毫秒,这就严重霸占了主线程执行其他渲染任务的时间。针对这种情况我们可以采用以下两种策略:

    • 一种是将一次执行的函数分解为多个任务,使得每次的执行时间不要过久。
    • 另一种是采用 Web Workers
  • DOM操作相关的优化。浏览器有渲染引擎JS引擎,所以当用JS操作DOM时,这两个引擎要通过接口互相“交流”,因此每一次操作DOM(包括只是访问DOM的属性),都要进行引擎之间解析的开销,所以常说要减少 DOM 操作。总结下来有以下几点:

    • 缓存一些计算属性,如let left = el.offsetLeft
    • 通过DOMclass来集中改变样式,而不是通过style一条条的去修改。
    • 分离读写操作。现代的浏览器都有渲染队列的机制。
    • 放弃传统操作DOM的时代,基于vue/react等采用virtual dom的框架
  • 合理利用 CSS 合成动画。合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript 或者一些布局任务占用,CSS 动画依然能继续执行。所以要尽量利用好 CSS 合成动画,如果能让 CSS 处理动画,就尽量交给 CSS 来操作。
  • CSS选择器优化。我们知道CSS引擎查找是从右向左匹配的。所以基于此有以下几条优化方案:

    • 尽量不要使用通配符
    • 少用标签选择器
    • 尽量利用属性继承特性
  • CSS属性优化。浏览器绘制图像时,CSS的计算也是耗费性能的,一些属性需浏览器进行大量的计算,属于昂贵的属性(box-shadowsborder-radiustransformsfiltersopcity:nth-child等),这些属性在日常开发中经常用到,所以并不是说不要用这些属性,而是在开发中,如果有其它简单可行的方案,那可以优先选择没有昂贵属性的方案。
  • 避免频繁的垃圾回收。我们知道 JavaScript 使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会让用户产生掉帧、不流畅的感觉。

缓存

缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。下图是浏览器缓存的查找流程图:
image
浏览器缓存相关的知识点还是很多的,这里我有整理一张图:
image
关于浏览器缓存的详细介绍说明,可以参考我之前的这篇文章,这里就不赘述了。

DNS 相关优化

DNS全称Domain Name System。它是互联网的“通讯录”,它记录了域名与实际ip地址的映射关系。每次我们访问一个网站,都要通过各级的DNS服务器查询到该网站的服务器ip,然后才能访问到该服务器。

DNS相关的优化一般涉及到两点:浏览器DNS缓存和DNS预解析。

DNS缓存

一图胜千言:
image

  • 浏览器会先检查浏览器缓存(浏览器缓存有大小和时间限制),时间过长可能导致IP地址变化,无法解析正确IP地址,过短就会让浏览器重复解析域名,一般为几分钟。
  • 如果浏览器缓存没有对应域名,则会去操作系统缓存中查找。
  • 如果还没有找到,域名就会发送到本地区的域名服务器(一般由互联网供应商提供,电信、联通之类),一般在本地区的域名服务器上都能找到了。
  • 当然也可能本地域名服务器也没找到,那本地域名服务器就开始递归查找。

一般而言,浏览器解析DNS需要20-120ms,因此DNS解析可优化之处几乎没有。但存在这样一个场景,网站有很多图片在不同域名下,那如果在登录页就提前解析了之后可能会用到的域名,使解析结果缓存过,这样缩短了DNS解析时间,提高网站整体上的访问速度了,这就是DNS预解析

DNS预解析

来看下 MDN 对于DNS预解析的定义吧:

X-DNS-Prefetch-Control 头控制着浏览器的 DNS 预读取功能。 DNS 预读取是一项使浏览器主动去执行域名解析的功能,其范围包括文档的所有链接,无论是图片的,CSS 的,还是 JavaScript 等其他用户能够点击的 URL

因为预读取会在后台执行,所以 DNS 很可能在链接对应的东西出现之前就已经解析完毕。这能够减少用户点击链接时的延迟。

我们这里就简单看一下如何去做DNS预解析

  • 在页面头部加入,这样浏览器对整个页面进行预解析
<meta http-equiv="x-dns-prefetch-control" content="on" />
  • 通过 link 标签手动添加要解析的域名,比如:
<link rel="dns-prefetch" href="//img10.360buyimg.com" />

参考

李兵 「浏览器工作原理与实践」

❤️ 爱心三连击

1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~

2.关注公众号前端森林,定期为你推送新鲜干货好文。

3.特殊阶段,带好口罩,做好个人防护。

4.添加微信fs1263215592,拉你进技术交流群一起学习 🍻
image

查看原文

赞 50 收藏 36 评论 0

風魔小次郎 赞了文章 · 2020-11-03

尤大 3 天前发在 GitHub 上的 vue-lit 是啥?

未经授权,不得转载,原文地址:https://github.com/axuebin/ar...

写在前面

尤大北京时间 9月18日 下午的时候发了一个微博,人狠话不多。看到这个表情,大家都知道有大事要发生。果然,在写这篇文章的时候,上 GitHub 上看了一眼,刚好碰上发布:

我们知道,一般开源软件的 release 就是一个 最终版本,看一下官方关于这个 release 版本的介绍:

Today we are proud to announce the official release of Vue.js 3.0 "One Piece".

更多关于这个 release 版本的信息可以关注:https://github.com/vuejs/vue-next/releases/tag/v3.0.0

除此之外,我在尤大的 GitHub 上发现了另一个东西 vue-lit,直觉告诉我这又是一个啥面向未来的下一代 xxx,所以我就点进去看了一眼是啥新玩具。

Hello World

Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.

看上去是尤大的一个验证性的尝试,看到 custom elementlit-html,盲猜一把,是一个可以直接在浏览器中渲染 vue 写法的 Web Component 的工具。

这里提到了 lit-html,后面会专门介绍一下。

按照尤大给的 Demo,我们来试一下 Hello World

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module">
      import {
        defineComponent,
        reactive,
        html,
        onMounted
      } from 'https://unpkg.com/@vue/lit@0.0.2';
  
      defineComponent('my-component', () => {
        const state = reactive({
          text: 'Hello World',
        });
        
        function onClick() {
          alert('cliked!');
        }
  
        onMounted(() => {
          console.log('mounted');
        });
  
        return () => html`
          <p>
            <button @click=${onClick}>Click me</button>
            ${state.text}
          </p>
        `;
      })
    </script>
  </head>
  <body>
    <my-component />
  </body>
</html>

不用任何编译打包工具,直接打开这个 index.html,看上去没毛病:

!

可以看到,这里渲染出来的是一个 Web Component,并且 mounted 生命周期也触发了。

关于 lit-html 和 lit-element

vue-lit 之前,我们先了解一下 lit-htmllit-ement,这两个东西其实已经出来很久了,可能并不是所有人都了解。

lit-html

lit-html 可能很多人并不熟悉,甚至没有见过。

所以是啥?答案是 HTML 模板引擎

如果没有体感,我问一个问题,React 核心的东西有哪些?大家都会回答:jsxVirtual-DOMdiff,没错,就是这些东西构成了 UI = f(data)React

来看看 jsx 的语法:

function App() {
  const msg = 'Hello World';
  return <div>{msg}</div>;
}

再看看 lit-html 的语法:

function App() {
  const msg = 'Hello World';
  return html`
    <div>${msg}</div>
  `;
}

我们知道 jsx 是需要编译的它的底层最终还是 createElement....。而 lit-html 就不一样了,它是基于 tagged template 的,使得它不用编译就可以在浏览器上运行,并且和 HTML Template 结合想怎么玩怎么玩,扩展能力更强,不香吗?

当然,无论是 jsx 还是 lint-html,这个 App 都是需要 render 到真实 DOM 上。

lint-html 实现一个 Button 组件

直接上代码(省略样式代码):

<!DOCTYPE html>
<html lang="en">
<head>
  <script type="module">
    import { html, render } from 'https://unpkg.com/lit-html?module';

    const Button = (text, props = {
      type: 'default',
      borderRadius: '2px'
    }, onClick) => {
      // 点击事件
      const clickHandler = {
        handleEvent(e) { 
          alert('inner clicked!');
          if (onClick) {
            onClick();
          }
        },
        capture: true,
      };

      return html`
        <div class="btn btn-${props.type}" @click=${clickHandler}>
          ${text}
        </div>
      `
    };
    render(Button('Defualt'), document.getElementById('button1'));
    render(Button('Primary', { type: 'primary' }, () => alert('outer clicked!')), document.getElementById('button2'));
    render(Button('Error', { type: 'error' }), document.getElementById('button3'));
  </script>
</head>
<body>
  <div id="button1"></div>
  <div id="button2"></div>
  <div id="button3"></div>
</body>
</html>

效果:

性能

lit-html 会比 React 性能更好吗?这里我没仔细看过源码,也没进行过相关实验,无法下定论。

但是可以大胆猜测一下,lit-html 没有使用类 diff 算法而是直接基于相同 template 的更新,看上去这种方式会更轻量一点。

但是,我们常问的一个问题 “在渲染列表的时候,key 有什么用?”,这个在 lit-html 是不是没法解决了。我如果删除了长列表中的其中一项,按照 lit-html 的基于相同 template 的更新,整个长列表都会更新一次,这个性能就差很多了啊。

// TODO:埋个坑,以后看

lit-element

lit-element 这又是啥呢?

关键词:web components

例子

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      msg: { type: String },
    };
  }
  constructor() {
    super();
    this.msg = 'Hello World';
  }
  render() {
    return html`
      <p>${this.msg}</p>
    `;
  }
}

customElements.define('my-element', MyElement);

效果

结论:可以用类 React 的语法写 Web Component

so, lit-element 是一个可以创建 Web Componentbase class。分析一下上面的 Demo,lit-element 做了什么事情:

  1. static get properties: 可以 setterstate
  2. constructor: 初始化 state
  3. render: 通过 lit-html 渲染元素,并且会创建 ShadowDOM

总之,lit-element 遵守 Web Components 标准,它是一个 class,基于它可以快速创建 Web Component

更多关于如何使用 lit-element 进行开发,在这里就不展开说了。

Web Components

浏览器原生能力香吗?

Web Components 之前我想先问问大家,大家还记得 jQuery 吗,它方便的选择器让人难忘。但是后来 document.querySelector 这个 API 的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery

浏览器原生 API 已经足够好用,我们并不需要为了操作 DOM 而使用 jQuery

You Dont Need jQuery

再后来,是不是很久没有直接操作过 DOM 了?

是的,由于 React / Vue 等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API 来操作 DOM

我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React 等是不是也会像 jQuery 一样被浏览器原生能力替代?

组件化

React / Vue 等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM 中的任意位置。

现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components 这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API 或者是使用基于 Web Components 标准的框架和库来开发了?

当然,未来是不可知的

我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。

来看看 Web Components 的一些主要功能吧。

Custom elements: 自定义元素

自定义元素顾名思义就是用户可以自定义 HTML 元素,通过 CustomElementRegistrydefine 来定义,比如:

window.customElements.define('my-element', MyElement);

然后就可以直接通过 <my-element /> 使用了。

根据规范,有两种 Custom elements

  • Autonomous custom elements: 独立的元素,不继承任何 HTML 元素,使用时可以直接 <my-element />
  • Customized buld-in elements: 继承自 HTML 元素,比如通过 { extends: 'p' } 来标识继承自 p 元素,使用时需要 <p is="my-element"></p>

两种 Custom elements 在实现的时候也有所区别:

// Autonomous custom elements
class MyElement extends HTMLElement {
  constructor() {
    super();
  }
}

// Customized buld-in elements:继承自 p 元素
class MyElement extends HTMLParagraphElement {
  constructor() {
    super();
  }
}

更多关于 Custom elements

生命周期函数

Custom elements 的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。

  • connectedCallback:元素首次被插入文档 DOM
  • disconnectedCallback:元素从文档 DOM 中删除时
  • adoptedCallback:元素被移动到新的文档时
  • attributeChangedCallback: 元素增加、删除、修改自身属性时

我们这里留意一下 attributeChangedCallback,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:

attributeChangedCallback(name, oldValue, newValue) {
  // TODO
}

需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback() 回调函数,你必须监听这个属性

class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['my-name'];
  }
  constructor() {
    super();
  }
}

元素的 my-name 属性发生变化时,就会触发回调方法。

Shadow DOM

Web Components 一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM 实现。

关于 Shadow DOM,这里主要想说一下 CSS 样式隔离的特性。Shadow DOM 里外的 selector 是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。

这样有什么好处呢?划重点,样式隔离,Shadow DOM 通过局部的 HTMLCSS,解决了样式上的一些问题,类似 vuescope 的感觉,元素内部不用关心 selectorCSS rule 会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector 非常简单:title / item 等,不需要任何的工具或者命名的约束。

更多关于 Shadow DOM

Templates: 模板

可以通过 <template> 来添加一个 Web ComponentShadow DOM 里的 HTML 内容:

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: white;
        background-color: #666;
        padding: 5px;
      }
    </style>
    <p>My paragraph</p>
  </template>
  <script>
    customElements.define('my-paragraph',
      class extends HTMLElement {
        constructor() {
          super();
          let template = document.getElementById('my-paragraph');
          let templateContent = template.content;

          const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
        }
      }
    )
  </script>
  <my-paragraph></my-paragraph>
</body>

效果:

我们知道,<template> 是不会直接被渲染的,所以我们是不是可以定义多个 <template> 然后在自定义元素时根据不同的条件选择渲染不同的 <template>?答案当然是:可以。

更多关于 Templates

vue-lit

介绍了 lit-html/elementWeb Components,我们回到尤大这个 vue-lit

首先我们看到在 Vue 3.0Release 里有这么一段:

The @vue/reactivity module exports functions that provide direct access to Vue's reactivity system, and can be used as a standalone package. It can be used to pair with other templating solutions (e.g. lit-html) or even in non-UI scenarios.

意思大概就是说 @vue/reactivity 模块和类似 lit-html 的方案配合,也能设计出一个直接访问 Vue 响应式系统的解决方案。

巧了不是,对上了,这不就是 vue-lit 吗?

源码解析

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
  • lit-html 提供核心 render 能力
  • @vue/reactiity 提供 Vue 响应式系统的能力

这里稍带解释一下 shallowReactiveeffect,不展开:

shallowReactive:简单理解就是“浅响应”,类似于“浅拷贝”,它仅仅是响应数据的第一层

const state = shallowReactive({
  a: 1,
  b: {
    c: 2,
  },
})

state.a++ // 响应式
state.b.c++ // 非响应式

effect:简单理解就是 watcher

const state = reactive({
  name: "前端试炼",
});
console.log(state); // 这里返回的是Proxy代理后的对象
effect(() => {
  console.log(state.name); // 每当name数据变化将会导致effect重新执行
});

接着往下看:

export function defineComponent(name, propDefs, factory) {
  // propDefs
  // 如果是函数,则直接当作工厂函数
  // 如果是数组,则监听他们,触发 attributeChangedCallback 回调函数
  if (typeof propDefs === 'function') {
    factory = propDefs
    propDefs = []
  }
  // 调用 Web Components 创建 Custom Elements 的函数
  customElements.define(
    name,
    class extends HTMLElement {
      // 监听 propDefs
      static get observedAttributes() {
        return propDefs
      }
      constructor() {
        super()
        // 创建一个浅响应
        const props = (this._props = shallowReactive({}))
        currentInstance = this
        const template = factory.call(this, props)
        currentInstance = null
        // beforeMount 生命周期
        this._bm && this._bm.forEach((cb) => cb())
        // 定义一个 Shadow root,并且内部实现无法被 JavaScript 访问及修改,类似 <video> 标签
        const root = this.attachShadow({ mode: 'closed' })
        let isMounted = false
        // watcher
        effect(() => {
          if (!isMounted) {
            // beforeUpdate 生命周期
            this._bu && this._bu.forEach((cb) => cb())
          }
          // 调用 lit-html 的核心渲染能力,参考上文 lit-html 的 Demo
          render(template(), root)
          if (isMounted) {
            // update 生命周期
            this._u && this._u.forEach((cb) => cb())
          } else {
            // 渲染完成,将 isMounted 置为 true
            isMounted = true
          }
        })
      }
      connectedCallback() {
        // mounted 生命周期
        this._m && this._m.forEach((cb) => cb())
      }
      disconnectedCallback() {
        // unMounted 生命周期
        this._um && this._um.forEach((cb) => cb())
      }
      attributeChangedCallback(name, oldValue, newValue) {
        // 每次修改 propDefs 里的参数都会触发
        this._props[name] = newValue
      }
    }
  )
}

// 挂载生命周期
function createLifecycleMethod(name) {
  return (cb) => {
    if (currentInstance) {
      ;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
    }
  }
}

// 导出生命周期
export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')

// 导出 lit-hteml 和 @vue/reactivity 的所有 API
export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

简化版有助于理解

整体看下来,为了更好地理解,我们不考虑生命周期之后可以简化一下:

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

export function defineComponent(name, factory) {
  customElements.define(
    name,
    class extends HTMLElement {
      constructor() {
        super()
        const root = this.attachShadow({ mode: 'closed' })
        effect(() => {
          render(factory(), root)
        })
      }
    }
  )
}

也就这几个流程:

  1. 创建 Web ComponentsCustom Elements
  2. 创建一个 Shadow DOMShadowRoot 节点
  3. 将传入的 factory 和内部创建的 ShadowRoot 节点交给 lit-htmlrender 渲染出来

回过头来看尤大提供的 DEMO:

import {
  defineComponent,
  reactive,
  html,
} from 'https://unpkg.com/@vue/lit'

defineComponent('my-component', () => {
  const msg = 'Hello World'
  const state = reactive({
    show: true
  })
  const toggle = () => {
    state.show = !state.show
  }
  
  return () => html`
    <button @click=${toggle}>toggle child</button>
    ${state.show ? html`<my-child msg=${msg}></my-child>` : ``}
  `
})

my-component 是传入的 name,第二个是一个函数,也就是传入的 factory,其实就是 lit-html 的第一个参数,只不过引入了 @vue/reactivityreactive 能力,把 state 变成了响应式。

没毛病,和 Vue 3.0 Release 里说的一致,@vue/reactivity 可以和 lit-html 配合,使得 VueWeb Components 结合到一块儿了,是不是还挺有意思。

写在最后

可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。

猜测不久将来这些关键词会突然就爆发:Unbundled / ES Modules / Web components / Custom Element / Shadow DOM...

是不是值得期待一下?

思考可能还比较浅,文笔有限,不足之处欢迎大家指出。

更多文章

可以关注公众号「前端试炼」,或者加微信好友 qianduanshilian,加入交流群。

查看原文

赞 15 收藏 5 评论 2

風魔小次郎 收藏了文章 · 2020-11-03

尤大 3 天前发在 GitHub 上的 vue-lit 是啥?

未经授权,不得转载,原文地址:https://github.com/axuebin/ar...

写在前面

尤大北京时间 9月18日 下午的时候发了一个微博,人狠话不多。看到这个表情,大家都知道有大事要发生。果然,在写这篇文章的时候,上 GitHub 上看了一眼,刚好碰上发布:

我们知道,一般开源软件的 release 就是一个 最终版本,看一下官方关于这个 release 版本的介绍:

Today we are proud to announce the official release of Vue.js 3.0 "One Piece".

更多关于这个 release 版本的信息可以关注:https://github.com/vuejs/vue-next/releases/tag/v3.0.0

除此之外,我在尤大的 GitHub 上发现了另一个东西 vue-lit,直觉告诉我这又是一个啥面向未来的下一代 xxx,所以我就点进去看了一眼是啥新玩具。

Hello World

Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.

看上去是尤大的一个验证性的尝试,看到 custom elementlit-html,盲猜一把,是一个可以直接在浏览器中渲染 vue 写法的 Web Component 的工具。

这里提到了 lit-html,后面会专门介绍一下。

按照尤大给的 Demo,我们来试一下 Hello World

<!DOCTYPE html>
<html lang="en">
  <head>
    <script type="module">
      import {
        defineComponent,
        reactive,
        html,
        onMounted
      } from 'https://unpkg.com/@vue/lit@0.0.2';
  
      defineComponent('my-component', () => {
        const state = reactive({
          text: 'Hello World',
        });
        
        function onClick() {
          alert('cliked!');
        }
  
        onMounted(() => {
          console.log('mounted');
        });
  
        return () => html`
          <p>
            <button @click=${onClick}>Click me</button>
            ${state.text}
          </p>
        `;
      })
    </script>
  </head>
  <body>
    <my-component />
  </body>
</html>

不用任何编译打包工具,直接打开这个 index.html,看上去没毛病:

!

可以看到,这里渲染出来的是一个 Web Component,并且 mounted 生命周期也触发了。

关于 lit-html 和 lit-element

vue-lit 之前,我们先了解一下 lit-htmllit-ement,这两个东西其实已经出来很久了,可能并不是所有人都了解。

lit-html

lit-html 可能很多人并不熟悉,甚至没有见过。

所以是啥?答案是 HTML 模板引擎

如果没有体感,我问一个问题,React 核心的东西有哪些?大家都会回答:jsxVirtual-DOMdiff,没错,就是这些东西构成了 UI = f(data)React

来看看 jsx 的语法:

function App() {
  const msg = 'Hello World';
  return <div>{msg}</div>;
}

再看看 lit-html 的语法:

function App() {
  const msg = 'Hello World';
  return html`
    <div>${msg}</div>
  `;
}

我们知道 jsx 是需要编译的它的底层最终还是 createElement....。而 lit-html 就不一样了,它是基于 tagged template 的,使得它不用编译就可以在浏览器上运行,并且和 HTML Template 结合想怎么玩怎么玩,扩展能力更强,不香吗?

当然,无论是 jsx 还是 lint-html,这个 App 都是需要 render 到真实 DOM 上。

lint-html 实现一个 Button 组件

直接上代码(省略样式代码):

<!DOCTYPE html>
<html lang="en">
<head>
  <script type="module">
    import { html, render } from 'https://unpkg.com/lit-html?module';

    const Button = (text, props = {
      type: 'default',
      borderRadius: '2px'
    }, onClick) => {
      // 点击事件
      const clickHandler = {
        handleEvent(e) { 
          alert('inner clicked!');
          if (onClick) {
            onClick();
          }
        },
        capture: true,
      };

      return html`
        <div class="btn btn-${props.type}" @click=${clickHandler}>
          ${text}
        </div>
      `
    };
    render(Button('Defualt'), document.getElementById('button1'));
    render(Button('Primary', { type: 'primary' }, () => alert('outer clicked!')), document.getElementById('button2'));
    render(Button('Error', { type: 'error' }), document.getElementById('button3'));
  </script>
</head>
<body>
  <div id="button1"></div>
  <div id="button2"></div>
  <div id="button3"></div>
</body>
</html>

效果:

性能

lit-html 会比 React 性能更好吗?这里我没仔细看过源码,也没进行过相关实验,无法下定论。

但是可以大胆猜测一下,lit-html 没有使用类 diff 算法而是直接基于相同 template 的更新,看上去这种方式会更轻量一点。

但是,我们常问的一个问题 “在渲染列表的时候,key 有什么用?”,这个在 lit-html 是不是没法解决了。我如果删除了长列表中的其中一项,按照 lit-html 的基于相同 template 的更新,整个长列表都会更新一次,这个性能就差很多了啊。

// TODO:埋个坑,以后看

lit-element

lit-element 这又是啥呢?

关键词:web components

例子

import { LitElement, html } from 'lit-element';

class MyElement extends LitElement {
  static get properties() {
    return {
      msg: { type: String },
    };
  }
  constructor() {
    super();
    this.msg = 'Hello World';
  }
  render() {
    return html`
      <p>${this.msg}</p>
    `;
  }
}

customElements.define('my-element', MyElement);

效果

结论:可以用类 React 的语法写 Web Component

so, lit-element 是一个可以创建 Web Componentbase class。分析一下上面的 Demo,lit-element 做了什么事情:

  1. static get properties: 可以 setterstate
  2. constructor: 初始化 state
  3. render: 通过 lit-html 渲染元素,并且会创建 ShadowDOM

总之,lit-element 遵守 Web Components 标准,它是一个 class,基于它可以快速创建 Web Component

更多关于如何使用 lit-element 进行开发,在这里就不展开说了。

Web Components

浏览器原生能力香吗?

Web Components 之前我想先问问大家,大家还记得 jQuery 吗,它方便的选择器让人难忘。但是后来 document.querySelector 这个 API 的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery

浏览器原生 API 已经足够好用,我们并不需要为了操作 DOM 而使用 jQuery

You Dont Need jQuery

再后来,是不是很久没有直接操作过 DOM 了?

是的,由于 React / Vue 等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API 来操作 DOM

我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React 等是不是也会像 jQuery 一样被浏览器原生能力替代?

组件化

React / Vue 等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM 中的任意位置。

现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components 这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API 或者是使用基于 Web Components 标准的框架和库来开发了?

当然,未来是不可知的

我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。

来看看 Web Components 的一些主要功能吧。

Custom elements: 自定义元素

自定义元素顾名思义就是用户可以自定义 HTML 元素,通过 CustomElementRegistrydefine 来定义,比如:

window.customElements.define('my-element', MyElement);

然后就可以直接通过 <my-element /> 使用了。

根据规范,有两种 Custom elements

  • Autonomous custom elements: 独立的元素,不继承任何 HTML 元素,使用时可以直接 <my-element />
  • Customized buld-in elements: 继承自 HTML 元素,比如通过 { extends: 'p' } 来标识继承自 p 元素,使用时需要 <p is="my-element"></p>

两种 Custom elements 在实现的时候也有所区别:

// Autonomous custom elements
class MyElement extends HTMLElement {
  constructor() {
    super();
  }
}

// Customized buld-in elements:继承自 p 元素
class MyElement extends HTMLParagraphElement {
  constructor() {
    super();
  }
}

更多关于 Custom elements

生命周期函数

Custom elements 的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。

  • connectedCallback:元素首次被插入文档 DOM
  • disconnectedCallback:元素从文档 DOM 中删除时
  • adoptedCallback:元素被移动到新的文档时
  • attributeChangedCallback: 元素增加、删除、修改自身属性时

我们这里留意一下 attributeChangedCallback,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:

attributeChangedCallback(name, oldValue, newValue) {
  // TODO
}

需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback() 回调函数,你必须监听这个属性

class MyElement extends HTMLElement {
  static get observedAttributes() {
    return ['my-name'];
  }
  constructor() {
    super();
  }
}

元素的 my-name 属性发生变化时,就会触发回调方法。

Shadow DOM

Web Components 一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM 实现。

关于 Shadow DOM,这里主要想说一下 CSS 样式隔离的特性。Shadow DOM 里外的 selector 是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。

这样有什么好处呢?划重点,样式隔离,Shadow DOM 通过局部的 HTMLCSS,解决了样式上的一些问题,类似 vuescope 的感觉,元素内部不用关心 selectorCSS rule 会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector 非常简单:title / item 等,不需要任何的工具或者命名的约束。

更多关于 Shadow DOM

Templates: 模板

可以通过 <template> 来添加一个 Web ComponentShadow DOM 里的 HTML 内容:

<body>
  <template id="my-paragraph">
    <style>
      p {
        color: white;
        background-color: #666;
        padding: 5px;
      }
    </style>
    <p>My paragraph</p>
  </template>
  <script>
    customElements.define('my-paragraph',
      class extends HTMLElement {
        constructor() {
          super();
          let template = document.getElementById('my-paragraph');
          let templateContent = template.content;

          const shadowRoot = this.attachShadow({mode: 'open'}).appendChild(templateContent.cloneNode(true));
        }
      }
    )
  </script>
  <my-paragraph></my-paragraph>
</body>

效果:

我们知道,<template> 是不会直接被渲染的,所以我们是不是可以定义多个 <template> 然后在自定义元素时根据不同的条件选择渲染不同的 <template>?答案当然是:可以。

更多关于 Templates

vue-lit

介绍了 lit-html/elementWeb Components,我们回到尤大这个 vue-lit

首先我们看到在 Vue 3.0Release 里有这么一段:

The @vue/reactivity module exports functions that provide direct access to Vue's reactivity system, and can be used as a standalone package. It can be used to pair with other templating solutions (e.g. lit-html) or even in non-UI scenarios.

意思大概就是说 @vue/reactivity 模块和类似 lit-html 的方案配合,也能设计出一个直接访问 Vue 响应式系统的解决方案。

巧了不是,对上了,这不就是 vue-lit 吗?

源码解析

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'
  • lit-html 提供核心 render 能力
  • @vue/reactiity 提供 Vue 响应式系统的能力

这里稍带解释一下 shallowReactiveeffect,不展开:

shallowReactive:简单理解就是“浅响应”,类似于“浅拷贝”,它仅仅是响应数据的第一层

const state = shallowReactive({
  a: 1,
  b: {
    c: 2,
  },
})

state.a++ // 响应式
state.b.c++ // 非响应式

effect:简单理解就是 watcher

const state = reactive({
  name: "前端试炼",
});
console.log(state); // 这里返回的是Proxy代理后的对象
effect(() => {
  console.log(state.name); // 每当name数据变化将会导致effect重新执行
});

接着往下看:

export function defineComponent(name, propDefs, factory) {
  // propDefs
  // 如果是函数,则直接当作工厂函数
  // 如果是数组,则监听他们,触发 attributeChangedCallback 回调函数
  if (typeof propDefs === 'function') {
    factory = propDefs
    propDefs = []
  }
  // 调用 Web Components 创建 Custom Elements 的函数
  customElements.define(
    name,
    class extends HTMLElement {
      // 监听 propDefs
      static get observedAttributes() {
        return propDefs
      }
      constructor() {
        super()
        // 创建一个浅响应
        const props = (this._props = shallowReactive({}))
        currentInstance = this
        const template = factory.call(this, props)
        currentInstance = null
        // beforeMount 生命周期
        this._bm && this._bm.forEach((cb) => cb())
        // 定义一个 Shadow root,并且内部实现无法被 JavaScript 访问及修改,类似 <video> 标签
        const root = this.attachShadow({ mode: 'closed' })
        let isMounted = false
        // watcher
        effect(() => {
          if (!isMounted) {
            // beforeUpdate 生命周期
            this._bu && this._bu.forEach((cb) => cb())
          }
          // 调用 lit-html 的核心渲染能力,参考上文 lit-html 的 Demo
          render(template(), root)
          if (isMounted) {
            // update 生命周期
            this._u && this._u.forEach((cb) => cb())
          } else {
            // 渲染完成,将 isMounted 置为 true
            isMounted = true
          }
        })
      }
      connectedCallback() {
        // mounted 生命周期
        this._m && this._m.forEach((cb) => cb())
      }
      disconnectedCallback() {
        // unMounted 生命周期
        this._um && this._um.forEach((cb) => cb())
      }
      attributeChangedCallback(name, oldValue, newValue) {
        // 每次修改 propDefs 里的参数都会触发
        this._props[name] = newValue
      }
    }
  )
}

// 挂载生命周期
function createLifecycleMethod(name) {
  return (cb) => {
    if (currentInstance) {
      ;(currentInstance[name] || (currentInstance[name] = [])).push(cb)
    }
  }
}

// 导出生命周期
export const onBeforeMount = createLifecycleMethod('_bm')
export const onMounted = createLifecycleMethod('_m')
export const onBeforeUpdate = createLifecycleMethod('_bu')
export const onUpdated = createLifecycleMethod('_u')
export const onUnmounted = createLifecycleMethod('_um')

// 导出 lit-hteml 和 @vue/reactivity 的所有 API
export * from 'https://unpkg.com/lit-html?module'
export * from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

简化版有助于理解

整体看下来,为了更好地理解,我们不考虑生命周期之后可以简化一下:

import { render } from 'https://unpkg.com/lit-html?module'
import {
  shallowReactive,
  effect
} from 'https://unpkg.com/@vue/reactivity/dist/reactivity.esm-browser.js'

export function defineComponent(name, factory) {
  customElements.define(
    name,
    class extends HTMLElement {
      constructor() {
        super()
        const root = this.attachShadow({ mode: 'closed' })
        effect(() => {
          render(factory(), root)
        })
      }
    }
  )
}

也就这几个流程:

  1. 创建 Web ComponentsCustom Elements
  2. 创建一个 Shadow DOMShadowRoot 节点
  3. 将传入的 factory 和内部创建的 ShadowRoot 节点交给 lit-htmlrender 渲染出来

回过头来看尤大提供的 DEMO:

import {
  defineComponent,
  reactive,
  html,
} from 'https://unpkg.com/@vue/lit'

defineComponent('my-component', () => {
  const msg = 'Hello World'
  const state = reactive({
    show: true
  })
  const toggle = () => {
    state.show = !state.show
  }
  
  return () => html`
    <button @click=${toggle}>toggle child</button>
    ${state.show ? html`<my-child msg=${msg}></my-child>` : ``}
  `
})

my-component 是传入的 name,第二个是一个函数,也就是传入的 factory,其实就是 lit-html 的第一个参数,只不过引入了 @vue/reactivityreactive 能力,把 state 变成了响应式。

没毛病,和 Vue 3.0 Release 里说的一致,@vue/reactivity 可以和 lit-html 配合,使得 VueWeb Components 结合到一块儿了,是不是还挺有意思。

写在最后

可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。

猜测不久将来这些关键词会突然就爆发:Unbundled / ES Modules / Web components / Custom Element / Shadow DOM...

是不是值得期待一下?

思考可能还比较浅,文笔有限,不足之处欢迎大家指出。

更多文章

可以关注公众号「前端试炼」,或者加微信好友 qianduanshilian,加入交流群。

查看原文

風魔小次郎 收藏了文章 · 2020-10-12

浏览器是如何工作的:Chrome V8让你更懂JavaScript

Chrome V8
  V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行

上图清晰版

记得那年花下,深夜,初识谢娘时

为什么需要 JavaScript 引擎

  我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码
  JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收

# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000  #机器指令
mov ax,bx         #汇编指令
资料拓展: 汇编语言入门教程【阮一峰】

热门 JavaScript 引擎

  • V8 (Google),用 C++编写,开放源代码,由 Google 丹麦开发,是 Google Chrome 的一部分,也用于 Node.js。
  • JavaScriptCore (Apple),开放源代码,用于 webkit 型浏览器,如 Safari ,2008 年实现了编译器和字节码解释器,升级为了 SquirrelFish。苹果内部代号为“Nitro”的 JavaScript 引擎也是基于 JavaScriptCore 引擎的。
  • Rhino,由 Mozilla 基金会管理,开放源代码,完全以 Java 编写,用于 HTMLUnit
  • SpiderMonkey (Mozilla),第一款 JavaScript 引擎,早期用于 Netscape Navigator,现时用于 Mozilla Firefox。
  • Chakra (JScript 引擎),用于 Internet Explorer。
  • Chakra (JavaScript 引擎),用于 Microsoft Edge。
  • KJS,KDE 的 ECMAScript/JavaScript 引擎,最初由哈里·波顿开发,用于 KDE 项目的 Konqueror 网页浏览器中。
  • JerryScript — 三星推出的适用于嵌入式设备的小型 JavaScript 引擎。
  • 其他:Nashorn、QuickJSHermes

V8

  Google V8 引擎是用 C ++编写的开源高性能 JavaScript 和 WebAssembly 引擎,它已被用于 Chrome 和 Node.js 等。可以运行在 Windows 7+,macOS 10.12+和使用 x64,IA-32,ARM 或 MIPS 处理器的 Linux 系统上。 V8 最早被开发用以嵌入到 Google 的开源浏览器 Chrome 中,第一个版本随着第一版Chrome于 2008 年 9 月 2 日发布。但是 V8 是一个可以独立运行的模块,完全可以嵌入到任何 C ++应用程序中。著名的 Node.js( 一个异步的服务器框架,可以在服务端使用 JavaScript 写出高效的网络服务器 ) 就是基于 V8 引擎的,Couchbase, MongoDB 也使用了 V8 引擎。

  和其他 JavaScript 引擎一样,V8 会编译 / 执行 JavaScript 代码,管理内存,负责垃圾回收,与宿主语言的交互等。通过暴露宿主对象 ( 变量,函数等 ) 到 JavaScript,JavaScript 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作

how-v8-works

资料拓展:v8 logo | V8 (JavaScript engine)) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssembly

V8一词最早见于“V-8 engine”,即V8发动机,一般使用在中高端车辆上。8个气缸分成两组,每组4个,成V型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar和NASCAR都要求使用V8发动机。

与君初相识,犹如故人归

什么是 D8

  d8 是一个非常有用的调试工具,你可以把它看成是 debug for V8 的缩写。我们可以使用 d8 来查看 V8 在执行 JavaScript 过程中的各种中间数据,比如作用域、AST、字节码、优化的二进制代码、垃圾回收的状态,还可以使用 d8 提供的私有 API 查看一些内部信息

V8源码编译出来的可执行程序名为d8。d8作为V8引擎在命令行中可以使用的交互shell存在。Google官方已经不记得d8这个名字的由来,但是做为"delveloper shell"的缩写,用首字母d和8结合,恰到好处。
还有一种说法是d8最初叫developer shell,因为d后面有8个字符,因此简写为d8,类似于i18n(internationalization)这样的简写。
参考:Using d8

安装 D8

  • 方法一:自行下载编译

  • 方法二:使用编译好的 d8 工具

    // 解压文件,点击d8打开(mac安全策略限制的话,按住control,再点击,弹出菜单中选择打开)
      V8 version 8.4.109
      d8> 1 + 2
        3
      d8> 2 + '4'
        "24"
      d8> console.log(23)
        23
        undefined
      d8> var a = 1
        undefined
      d8> a + 2
        3
      d8> this
        [object global]
      d8>

    本文后续用于 demo 演示时的文件目录结构:

     V8:
        # d8可执行文件
        d8
        icudtl.dat
        libc++.dylib
        libchrome_zlib.dylib
        libicui18n.dylib
        libicuuc.dylib
        libv8.dylib
        libv8_debug_helper.dylib
        libv8_for_testing.dylib
        libv8_libbase.dylib
        libv8_libplatform.dylib
        obj
        snapshot_blob.bin
        v8_build_config.json
        # 新建的js示例文件
        test.js
    • 方法三:mac

        # 如果已有HomeBrew,忽略第一条命令
        ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
        brew install v8
    • 方法四:使用 node 代替,比如可以用node --print-bytecode ./test.js,打印出 Ignition(解释器)生成的 Bytecode(字节码)。

都有哪些 d8 命令可供使用?

  • 查看 d8 命令

      # 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
      ./d8 --help
  • 过滤特定的命令

      # 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
      ./d8 --help |grep print

    如:

    • print-bytecode 查看生成的字节码
    • print-opt-code 查看优化后的代码
    • print-ast 查看中间生成的 AST
    • print-scopes 查看中间生成的作用域
    • trace-gc 查看这段代码的内存回收状态
    • trace-opt 查看哪些代码被优化了
    • trace-deopt 查看哪些代码被反优化了
    • turbofan-stats 打印优化编译器的一些统计数据

使用 d8 进行调试

// test.js
function sum(a) {
  var b = 6;
  return a + 6;
}
console.log(sum(3));
  # d8 后面跟上文件名和要执行的命令,如执行下面这行命令,就会打印出 test.js 文件所生成的字节码。
  ./d8 ./test.js --print-bytecode
  # 执行以下命令,输出9
  ./d8 ./test.js

内部方法

  你还可以使用 V8 所提供的一些内部方法,只需要在启动 V8 时传入 --allow-natives-syntax 命令,你就可以在 test.js 中使用诸如HasFastProperties(检查一个对象是否拥有快属性)的内部方法(索引属性、常规属性、快属性等下文会介绍)。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);
// 检查一个对象是否拥有快属性
console.log(%HasFastProperties(bar));
delete bar.property2;
console.log(%HasFastProperties(bar));
  ./d8 --allow-natives-syntax ./test.js
  # 依次打印:true false

心似双丝网,中有千千结

V8 引擎的内部结构

  V8 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:

  • Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)

    确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:
    Scanner
  • Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。

    通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
    基于寄存器的解释器架构基于寄存器的解释器架构
    资料参考:解释器是如何解释执行字节码的?
  • TurboFan:compiler,即编译器,利用 Ignition 所收集的类型信息,将 Bytecode 转换为优化的汇编代码;
  • Orinoco:garbage collector,垃圾回收模块,负责将程序不再需要的内存空间回收。

  其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
V8流程
  简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 Machine Code(实际上是汇编代码)

  • 如果函数没有被调用,则 V8 不会去编译它。
  • 如果函数只被调用 1 次,则 Ignition 将其编译 Bytecode 就直接解释执行了。TurboFan 不会进行优化编译,因为它需要 Ignition 收集函数执行时的类型信息。这就要求函数至少需要执行 1 次,TurboFan 才有可能进行优化编译。
  • 如果函数被调用多次,则它有可能会被识别为热点函数,且 Ignition 收集的类型信息证明可以进行优化编译的话,这时 TurboFan 则会将 Bytecode 编译为 Optimized Machine Code(已优化的机器码),以提高代码的执行性能。

  图片中的红色虚线是逆向的,也就是说Optimized Machine Code 会被还原为 Bytecode,这个过程叫做 Deoptimization。这是因为 Ignition 收集的信息可能是错误的,比如 add 函数的参数之前是整数,后来又变成了字符串。生成的 Optimized Machine Code 已经假定 add 函数的参数是整数,那当然是错误的,于是需要进行 Deoptimization。

function add(x, y) {
  return x + y;
}

add(3, 5);
add('3', '5');

  在运行 C、C++以及 Java 等程序之前,需要进行编译,不能直接执行源码;但对于 JavaScript 来说,我们可以直接执行源码(比如:node test.js),它是在运行的时候先编译再执行,这种方式被称为即时编译(Just-in-time compilation),简称为 JIT。因此,V8 也属于 JIT 编译器

资料拓展参考:V8 引擎是如何工作的?

V8 是怎么执行一段 JavaScript 代码的

  • V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。而 V8 率先引入了即时编译(JIT)双轮驱动的设计(混合使用编译器和解释器的技术),这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。V8 出现之后,各大厂商也都在自己的 JavaScript 虚拟机中引入了 JIT 机制,所以目前市面上 JavaScript 虚拟机都有着类似的架构。另外,V8 也是早于其他虚拟机引入了惰性编译、内联缓存、隐藏类等机制,进一步优化了 JavaScript 代码的编译执行效率
  • V8 执行一段 JavaScript 的流程图:
    V8执行一段JavaScript流程图

    资料拓展:V8 是如何执行一段 JavaScript 代码的?
  • V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:

    • 第一种是将高级代码转换为二进制代码,再让计算机去执行;
    • 另外一种方式是在计算机安装一个解释器,并由解释器来解释执行。
  • 解释执行和编译执行都有各自的优缺点,解释执行启动速度快,但是执行时速度慢,而编译执行启动速度慢,但是执行速度快。为了充分地利用解释执行和编译执行的优点,规避其缺点,V8 采用了一种权衡策略,在启动过程中采用了解释执行的策略,但是如果某段代码的执行频率超过一个值,那么 V8 就会采用优化编译器将其编译成执行效率更加高效的机器代码
  • 总结:

    V8 执行一段 JavaScript 代码所经历的主要流程包括:

    • 初始化基础环境;
    • 解析源码生成 AST 和作用域;
    • 依据 AST 和作用域生成字节码;
    • 解释执行字节码;
    • 监听热点代码;
    • 优化热点代码为二进制的机器代码;
    • 反优化生成的二进制机器代码。

一等公民与闭包

一等公民的定义

  • 在编程语言中,一等公民可以作为函数参数,可以作为函数返回值,也可以赋值给变量。
  • 如果某个编程语言的函数,可以和这个语言的数据类型做一样的事情,我们就把这个语言中的函数称为一等公民。例如,字符串在几乎所有编程语言中都是一等公民,字符串可以做为函数参数,字符串可以作为函数返回值,字符串也可以赋值给变量。对于各种编程语言来说,函数就不一定是一等公民了,比如 Java 8 之前的版本。
  • 对于 JavaScript 来说,函数可以赋值给变量,也可以作为函数参数,还可以作为函数返回值,因此 JavaScript 中函数是一等公民

动态作用域与静态作用域

  • 如果一门语言的作用域是静态作用域,那么符号之间的引用关系能够根据程序代码在编译时就确定清楚,在运行时不会变。某个函数是在哪声明的,就具有它所在位置的作用域。它能够访问哪些变量,那么就跟这些变量绑定了,在运行时就一直能访问这些变量。即静态作用域可以由程序代码决定,在编译时就能完全确定。大多数语言都是静态作用域的。
  • 动态作用域(Dynamic Scope)。也就是说,变量引用跟变量声明不是在编译时就绑定死了的。在运行时,它是在运行环境中动态地找一个相同名称的变量。在 macOS 或 Linux 中用的 bash 脚本语言,就是动态作用域的。

闭包的三个基础特性

  • JavaScript 语言允许在函数内部定义新的函数
  • 可以在内部函数中访问父函数中定义的变量
  • 因为 JavaScript 中的函数是一等公民,所以函数可以作为另外一个函数的返回值
// 闭包(静态作用域,一等公民,调用栈的矛盾体)
function foo() {
  var d = 20;
  return function inner(a, b) {
    const c = a + b + d;
    return c;
  };
}
const f = foo();

  关于闭包,可参考我以前的一篇文章,在此不再赘述,在此主要谈下闭包给 Chrome V8 带来的问题及其解决策略。

惰性解析

  所谓惰性解析是指解析器在解析的过程中,如果遇到函数声明,那么会跳过函数内部的代码,并不会为其生成 AST 和字节码,而仅仅生成顶层代码的 AST 和字节码。

  • 在编译 JavaScript 代码的过程中,V8 并不会一次性将所有的 JavaScript 解析为中间代码,这主要是基于以下两点:

    • 首先,如果一次解析和编译所有的 JavaScript 代码,过多的代码会增加编译时间,这会严重影响到首次执行 JavaScript 代码的速度,让用户感觉到卡顿。因为有时候一个页面的 JavaScript 代码很大,如果要将所有的代码一次性解析编译完成,那么会大大增加用户的等待时间;
    • 其次,解析完成的字节码和编译之后的机器代码都会存放在内存中,如果一次性解析和编译所有 JavaScript 代码,那么这些中间代码和机器代码将会一直占用内存
  • 基于以上的原因,所有主流的 JavaScript 虚拟机都实现了惰性解析。
  • 闭包给惰性解析带来的问题:上文的 d 不能随着 foo 函数的执行上下文被销毁掉。

预解析器

  V8 引入预解析器,比如当解析顶层代码的时候,遇到了一个函数,那么预解析器并不会直接跳过该函数,而是对该函数做一次快速的预解析。

  • 判断当前函数是不是存在一些语法上的错误,发现了语法错误,那么就会向 V8 抛出语法错误;
  • 检查函数内部是否引用了外部变量,如果引用了外部的变量,预解析器会将栈中的变量复制到堆中,在下次执行到该函数的时候,直接使用堆中的引用,这样就解决了闭包所带来的问题

V8 内部是如何存储对象的:快属性和慢属性

下面的代码会输出什么:

// test.js
function Foo() {
  this[200] = 'test-200';
  this[1] = 'test-1';
  this[100] = 'test-100';
  this['B'] = 'bar-B';
  this[50] = 'test-50';
  this[9] = 'test-9';
  this[8] = 'test-8';
  this[3] = 'test-3';
  this[5] = 'test-5';
  this['D'] = 'bar-D';
  this['C'] = 'bar-C';
}
var bar = new Foo();

for (key in bar) {
  console.log(`index:${key}  value:${bar[key]}`);
}
//输出:
// index:1  value:test-1
// index:3  value:test-3
// index:5  value:test-5
// index:8  value:test-8
// index:9  value:test-9
// index:50  value:test-50
// index:100  value:test-100
// index:200  value:test-200
// index:B  value:bar-B
// index:D  value:bar-D
// index:C  value:bar-C

  在ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。在这里我们把对象中的数字属性称为排序属性,在 V8 中被称为 elements,字符串属性就被称为常规属性,在 V8 中被称为 properties。在 V8 内部,为了有效地提升存储和访问这两种属性的性能,分别使用了两个线性数据结构来分别保存排序属性和常规属性。同时 v8 将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties),不过对象内属性的数量是固定的,默认是 10 个。

function Foo(property_num, element_num) {
  //添加可索引属性
  for (let i = 0; i < element_num; i++) {
    this[i] = `element${i}`;
  }
  //添加常规属性
  for (let i = 0; i < property_num; i++) {
    let ppt = `property${i}`;
    this[ppt] = ppt;
  }
}
var bar = new Foo(10, 10);

  可以通过 Chrome 开发者工具的 Memory 标签,捕获查看当前的内存快照。通过增大第一个参数来查看存储变化。(Console面板运行以上代码,打开Memory面板,通过点击Take heap snapshot记录内存快照,点击快照,筛选出Foo进行查看。可参考使用 chrome-devtools Memory 面板了解Memory面板。)

  我们将保存在线性数据结构中的属性称之为“快属性”,因为线性数据结构中只需要通过索引即可以访问到属性,虽然访问线性结构的速度快,但是如果从线性结构中添加或者删除大量的属性时,则执行效率会非常低,这主要因为会产生大量时间和内存开销。因此,如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是“慢属性”策略,但慢属性的对象内部会有独立的非线性数据结构 (字典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。

v8 属性存储:
v8属性存储

总结:

  因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。

  通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。

  但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度

资料拓展:快属性和慢属性:V8 是怎样提升对象属性访问速度的?

堆空间和栈空间

栈空间

  • 现代语言都是基于函数的,每个函数在执行过程中,都有自己的生命周期和作用域,当函数执行结束时,其作用域也会被销毁,因此,我们会使用栈这种数据结构来管理函数的调用过程,我们也把管理函数调用过程的栈结构称之为调用栈
  • 栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等都会存在在栈上。当一个函数执行结束,那么该函数的执行上下文便会被销毁掉。
  • 栈空间的最大的特点是空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率非常高,但是通常在内存中,很难分配到一块很大的连续空间,因此,V8 对栈空间的大小做了限制,如果函数调用层过深,那么 V8 就有可能抛出栈溢出的错误。
  • 栈的优势和缺点:

    • 栈的结构非常适合函数调用过程。
    • 在栈上分配资源和销毁资源的速度非常快,这主要归结于栈空间是连续的,分配空间和销毁空间只需要移动下指针就可以了。
    • 虽然操作速度非常快,但是栈也是有缺点的,其中最大的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配一块连续的大空间是非常难的,因此栈空间是有限的

      // 栈溢出
      function factorial(n) {
        if (n === 1) {
          return 1;
        }
        return n * factorial(n - 1);
      }
      console.log(factorial(50000));

堆空间

  • 堆空间是一种树形的存储结构,用来存储对象类型的离散的数据,JavaScript 中除了原生类型的数据,其他的都是对象类型,诸如函数、数组,在浏览器中还有 window 对象、document 对象等,这些都是存在堆空间的。
  • 宿主在启动 V8 的过程中,会同时创建堆空间和栈空间,再继续往下执行,产生的新数据都会存放在这两个空间中。

继承

  继承就是一个对象可以访问另外一个对象中的属性和方法,在 JavaScript 中,我们通过原型和原型链的方式来实现了继承特性

  JavaScript 的每个对象都包含了一个隐藏属性 __proto__ ,我们就把该隐藏属性 __proto__ 称之为该对象的原型 (prototype),__proto__ 指向了内存中的另外一个对象,我们就把 __proto__ 指向的对象称为该对象的原型对象,那么该对象就可以直接访问其原型对象的方法或者属性。

  JavaScript 中的继承非常简洁,就是每个对象都有一个原型属性,该属性指向了原型对象,查找属性的时候,JavaScript 虚拟机会沿着原型一层一层向上查找,直至找到正确的属性。

隐藏属性__proto__

var animal = {
  type: 'Default',
  color: 'Default',
  getInfo: function () {
    return `Type is: ${this.type},color is ${this.color}.`;
  },
};
var dog = {
  type: 'Dog',
  color: 'Black',
};

利用__proto__实现继承:

dog.__proto__ = animal;
dog.getInfo();

  通常隐藏属性是不能使用 JavaScript 来直接与之交互的。虽然现代浏览器都开了一个口子,让 JavaScript 可以访问隐藏属性 __proto__,但是在实际项目中,我们不应该直接通过 __proto__ 来访问或者修改该属性,其主要原因有两个:

  • 首先,这是隐藏属性,并不是标准定义的;
  • 其次,使用该属性会造成严重的性能问题。因为 JavaScript 通过隐藏类优化了很多原有的对象结构,所以通过直接修改__proto__会直接破坏现有已经优化的结构,触发 V8 重构该对象的隐藏类!

构造函数是怎么创建对象的?

  在 JavaScript 中,使用 new 加上构造函数的这种组合来创建对象和实现对象的继承。不过使用这种方式隐含的语义过于隐晦。其实是 JavaScript 为了吸引 Java 程序员、在语法层面去蹭 Java 热点,所以就被硬生生地强制加入了非常不协调的关键字 new。

function DogFactory(type, color) {
  this.type = type;
  this.color = color;
}
var dog = new DogFactory('Dog', 'Black');

  其实当 V8 执行上面这段代码时,V8 在背后悄悄地做了以下几件事情:

var dog = {};
dog.__proto__ = DogFactory.prototype;
DogFactory.call(dog, 'Dog', 'Black');

机器码、字节码

V8 为什么要引入字节码

  • 早期的 V8 为了提升代码的执行速度,直接将 JavaScript 源代码编译成了没有优化的二进制机器代码,如果某一段二进制代码执行频率过高,那么 V8 会将其标记为热点代码,热点代码会被优化编译器优化,优化后的机器代码执行效率更高。
  • 随着移动设备的普及,V8 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:

    • 时间问题:编译时间过久,影响代码启动速度;
    • 空间问题:缓存编译后的二进制代码占用更多的内存。
  • 这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:

    • 解决启动问题:生成字节码的时间很短;
    • 解决空间问题:字节码虽然占用的空间比原始的 JavaScript 多,但是相较于机器代码,字节码还是小了太多,缓存字节码会大大降低内存的使用。
    • 代码架构清晰:采用字节码,可以简化程序的复杂度,使得 V8 移植到不同的 CPU 架构平台更加容易。
  • Bytecode 某种程度上就是汇编语言,只是它没有对应特定的 CPU,或者说它对应的是虚拟的 CPU。这样的话,生成 Bytecode 时简单很多,无需为不同的 CPU 生产不同的代码。要知道,V8 支持 9 种不同的 CPU,引入一个中间层 Bytecode,可以简化 V8 的编译流程,提高可扩展性。
  • 如果我们在不同硬件上去生成 Bytecode,会发现生成代码的指令是一样的。

如何查看字节码

// test.js
function add(x, y) {
  var z = x + y;
  return z;
}
console.log(add(1, 2));

运行./d8 ./test.js --print-bytecode:

[generated bytecode for function: add (0x01000824fe59 <SharedFunctionInfo add>)]
Parameter count 3 #三个参数,包括了显式地传入的 x 和 y,还有一个隐式地传入的 this
Register count 1
Frame size 8
         0x10008250026 @    0 : 25 02             Ldar a1 #将a1寄存器中的值加载到累加器中,LoaD Accumulator from Register
         0x10008250028 @    2 : 34 03 00          Add a0, [0]
         0x1000825002b @    5 : 26 fb             Star r0 #Store Accumulator to Register,把累加器中的值保存到r0寄存器中
         0x1000825002d @    7 : aa                Return  #结束当前函数的执行,并将控制权传回给调用方
Constant pool (size = 0)
Handler Table (size = 0)
Source Position Table (size = 0)
3

常用字节码指令

  • Ldar:表示将寄存器中的值加载到累加器中,你可以把它理解为 LoaD Accumulator from Register,就是把某个寄存器中的值,加载到累加器中。
  • Star:表示 Store Accumulator Register, 你可以把它理解为 Store Accumulator to Register,就是把累加器中的值保存到某个寄存器中
  • Add:Add a0, [0]是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。

    add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
  • LdaSmi:将小整数(Smi)加载到累加器寄存器中
  • Return:结束当前函数的执行,并将控制权传回给调用方。返回的值是累加器中的值。

bytecode-ignition

V8 中的字节码指令集 | 理解 V8 的字节码「译」

隐藏类和内联缓存

  JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存

为什么静态语言的效率更高?

  静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。

  JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。

将静态的特性引入到 V8

  • V8 采用的一个思路就是将 JavaScript 中的对象静态化,也就是 V8 在运行 JavaScript 的过程中,会假设 JavaScript 中的对象是静态的。
  • 具体地讲,V8 对每个对象做如下两点假设:

    • 对象创建好了之后就不会添加新的属性;
    • 对象创建好了之后也不会删除属性。
  • 符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:

    • 对象中所包含的所有的属性;
    • 每个属性相对于对象的偏移量。
  • 有了隐藏类之后,那么当 V8 访问某个对象中的某个属性时,就会先去隐藏类中查找该属性相对于它的对象的偏移量,有了偏移量和属性类型,V8 就可以直接去内存中取出对应的属性值,而不需要经历一系列的查找过程,那么这就大大提升了 V8 查找对象的效率。
  • 在 V8 中,把隐藏类又称为 map,每个对象都有一个 map 属性,其值指向内存中的隐藏类;
  • map 描述了对象的内存布局,比如对象都包括了哪些属性,这些数据对应于对象的偏移量是多少。

通过 d8 查看隐藏类

// test.js
let point1 = { x: 100, y: 200 };
let point2 = { x: 200, y: 300 };
let point3 = { x: 100 };
%DebugPrint(point1);
%DebugPrint(point2);
%DebugPrint(point3);
 ./d8 --allow-natives-syntax ./test.js
# ===============
DebugPrint: 0x1ea3080c5bc5: [JS_OBJECT_TYPE]
# V8 为 point1 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
    #y: 200 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c1d: [JS_OBJECT_TYPE]
# V8 为 point2 对象创建的隐藏类
 - map: 0x1ea308284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 200 (const data field 0)
    #y: 300 (const data field 1)
 }
0x1ea308284ce9: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 20
 - inobject properties: 2
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284cc1 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #2: 0x1ea3080c5bf5 <DescriptorArray[2]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

# ===============
DebugPrint: 0x1ea3080c5c31: [JS_OBJECT_TYPE]
# V8 为 point3 对象创建的隐藏类
 - map: 0x1ea308284d39 <Map(HOLEY_ELEMENTS)> [FastProperties]
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - elements: 0x1ea3080406e9 <FixedArray[0]> [HOLEY_ELEMENTS]
 - properties: 0x1ea3080406e9 <FixedArray[0]> {
    #x: 100 (const data field 0)
 }
0x1ea308284d39: [Map]
 - type: JS_OBJECT_TYPE
 - instance size: 16
 - inobject properties: 1
 - elements kind: HOLEY_ELEMENTS
 - unused property fields: 0
 - enum length: invalid
 - stable_map
 - back pointer: 0x1ea308284d11 <Map(HOLEY_ELEMENTS)>
 - prototype_validity cell: 0x1ea3081c0451 <Cell value= 1>
 - instance descriptors (own) #1: 0x1ea3080c5c41 <DescriptorArray[1]>
 - prototype: 0x1ea308241395 <Object map = 0x1ea3082801c1>
 - constructor: 0x1ea3082413b1 <JSFunction Object (sfi = 0x1ea3081c557d)>
 - dependent code: 0x1ea3080401ed <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
 - construction counter: 0

多个对象共用一个隐藏类

  • 在 V8 中,每个对象都有一个 map 属性,该属性值指向该对象的隐藏类。不过如果两个对象的形状是相同的,V8 就会为其复用同一个隐藏类,这样有两个好处:

    • 减少隐藏类的创建次数,也间接加速了代码的执行速度;
    • 减少了隐藏类的存储空间。
  • 那么,什么情况下两个对象的形状是相同的,要满足以下两点:

    • 相同的属性名称;
    • 相等的属性个数。

重新构建隐藏类

  • 给一个对象添加新的属性,删除新的属性,或者改变某个属性的数据类型都会改变这个对象的形状,那么势必也就会触发 V8 为改变形状后的对象重建新的隐藏类。
// test.js
let point = {};
%DebugPrint(point);
point.x = 100;
%DebugPrint(point);
point.y = 200;
%DebugPrint(point);
# ./d8 --allow-natives-syntax ./test.js
DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c7082802d9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284cc1 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...

DebugPrint: 0x32c7080c5b2d: [JS_OBJECT_TYPE]
 - map: 0x32c708284ce9 <Map(HOLEY_ELEMENTS)> [FastProperties]
 ...
  • 每次给对象添加了一个新属性之后,该对象的隐藏类的地址都会改变,这也就意味着隐藏类也随着改变了;如果删除对象的某个属性,那么对象的形状也就随着发生了改变,这时 V8 也会重建该对象的隐藏类;
  • 最佳实践

    • 使用字面量初始化对象时,要保证属性的顺序是一致的;
    • 尽量使用字面量一次性初始化完整对象属性;
    • 尽量避免使用 delete 方法。

通过内联缓存来提升函数执行效率

  虽然隐藏类能够加速查找对象的速度,但是在 V8 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:

function loadX(obj) {
  return obj.x;
}
var obj = { x: 1, y: 3 };
var obj1 = { x: 3, y: 6 };
var obj2 = { x: 3, y: 6, z: 8 };
for (var i = 0; i < 100; i++) {
  // 对比时间差异
  console.time(`---${i}----`)
  loadX(obj);
  console.timeEnd(`---${i}----`)
  loadX(obj1);
  // 产生多态
  loadX(obj2);
}

通常 V8 获取 obj.x 的流程

  • 找对象 obj 的隐藏类;
  • 再通过隐藏类查找 x 属性偏移量;
  • 然后根据偏移量获取属性值,在这段代码中 loadX 函数会被反复执行,那么获取 obj.x 的流程也需要反复被执行;

内联缓存及其原理

  • 函数 loadX 在一个 for 循环里面被重复执行了很多次,因此 V8 会想尽一切办法来压缩这个查找过程,以提升对象的查找效率。这个加速函数执行的策略就是内联缓存 (Inline Cache),简称为 IC;
  • IC 的原理:在 V8 执行函数的过程中,会观察函数中一些调用点 (CallSite) 上的关键中间数据,然后将这些数据缓存起来,当下次再次执行该函数的时候,V8 就可以直接利用这些中间数据,节省了再次获取这些数据的过程,因此 V8 利用 IC,可以有效提升一些重复代码的执行效率。
  • IC 会为每个函数维护一个反馈向量 (FeedBack Vector),反馈向量记录了函数在执行过程中的一些关键的中间数据。
  • 反馈向量其实就是一个表结构,它由很多项组成的,每一项称为一个插槽 (Slot),V8 会依次将执行 loadX 函数的中间数据写入到反馈向量的插槽中。
  • 当 V8 再次调用 loadX 函数时,比如执行到 loadX 函数中的 return obj.x 语句时,它就会在对应的插槽中查找 x 属性的偏移量,之后 V8 就能直接去内存中获取 obj.x 的属性值了。这样就大大提升了 V8 的执行效率。

单态、多态和超态

  • 如果一个插槽中只包含 1 个隐藏类,那么我们称这种状态为单态 (monomorphic);
  • 如果一个插槽中包含了 2 ~ 4 个隐藏类,那我们称这种状态为多态 (polymorphic);
  • 如果一个插槽中超过 4 个隐藏类,那我们称这种状态为超态 (magamorphic)。
  • 单态的性能优于多态和超态,所以我们需要稍微避免多态和超态的情况。要避免多态和超态,那么就尽量默认所有的对象属性是不变的,比如你写了一个 loadX(obj) 的函数,那么当传递参数时,尽量不要使用多个不同形状的 obj 对象。

总结:
  V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况

异步编程与消息队列

V8 是如何执行回调函数的

  回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。
  通用 UI 线程宏观架构:
通用UI线程架构
  UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:

  • setTimeout 的执行流程其实是比较简单的,在 setTimeout 函数内部封装回调消息,并将回调消息添加进消息队列,然后主线程从消息队列中取出回调事件,并执行回调函数。
  • XMLHttpRequest 稍微复杂一点,因为下载过程需要放到单独的一个线程中去执行,所以执行 XMLHttpRequest.send 的时候,宿主会将实际请求转发给网络线程,然后 send 函数退出,主线程继续执行下面的任务。网络线程在执行下载的过程中,会将一些中间信息和回调函数封装成新的消息,并将其添加进消息队列中,然后主线程从消息队列中取出回调事件,并执行回调函数。

宏任务和微任务

  • 调用栈:调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。主线程在执行任务的过程中,如果函数的调用层次过深,可能造成栈溢出的错误,我们可以使用 setTimeout 来解决栈溢出的问题。setTimeout 的本质是将同步函数调用改成异步函数调用,这里的异步调用是将回调函数封装成宏任务,并将其添加进消息队列中,然后主线程再按照一定规则循环地从消息队列中读取下一个宏任务。
  • 宏任务:就是指消息队列中的等待被主线程执行的事件。每个宏任务在执行时,V8 都会重新创建栈,然后随着宏任务中函数调用,栈也随之变化,最终,当该宏任务执行结束时,整个栈又会被清空,接着主线程继续执行下一个宏任务。
  • 微任务:你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
  • JavaScript 中之所以要引入微任务,主要是由于主线程执行消息队列中宏任务的时间颗粒度太粗了,无法胜任一些对精度和实时性要求较高的场景,微任务可以在实时性和效率之间做一个有效的权衡。另外使用微任务,可以改变我们现在的异步编程模型,使得我们可以使用同步形式的代码来编写异步调用。
  • 微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。

    微任务技术栈

    // 不会使浏览器卡死
    function foo() {
      setTimeout(foo, 0);
    }
    foo();

    调用栈、主线程、消息队列
    微任务:

// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
  return Promise.resolve().then(foo);
}
foo();
  • 如果当前的任务中产生了一个微任务,通过 Promise.resolve() 或者 Promise.reject() 都会触发微任务,触发的微任务不会在当前的函数中被执行,所以执行微任务时,不会导致栈的无限扩张
  • 和异步调用不同,微任务依然会在当前任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。因此在函数内部触发的微任务,一定比在函数内部触发的宏任务要优先执行。
  • 微任务依然是在当前的任务中执行的,所以如果在微任务中循环触发新的微任务,那么将导致消息队列中的其他任务没有机会被执行。

前端异步编程方案史

前端异步编程方案史

  • Callback 模式的异步编程模型需要实现大量的回调函数,大量的回调函数会打乱代码的正常逻辑,使得代码变得不线性、不易阅读,这就是我们所说的回调地狱问题
  • Promise 能很好地解决回调地狱的问题,我们可以按照线性的思路来编写代码,这个过程是线性的,非常符合人的直觉。
  • 但是这种方式充满了 Promise 的 then() 方法,如果处理流程比较复杂的话,那么整段代码将充斥着大量的 then,语义化不明显,代码不能很好地表示执行流程。我们想要通过线性的方式来编写异步代码,要实现这个理想,最关键的是要能实现函数暂停和恢复执行的功能。而生成器就可以实现函数暂停和恢复,我们可以在生成器中使用同步代码的逻辑来异步代码 (实现该逻辑的核心是协程)。
  • 但是在生成器之外,我们还需要一个触发器来驱动生成器的执行。前端的最终方案就是 async/await,async 是一个可以暂停和恢复执行的函数,在 async 函数内部使用 await 来暂停 async 函数的执行,await 等待的是一个 Promise 对象,如果 Promise 的状态变成 resolve 或者 reject,那么 async 函数会恢复执行。因此,使用 async/await 可以实现以同步的方式编写异步代码这一目标。和生成器函数一样,使用了 async 声明的函数在执行时,也是一个单独的协程,我们可以使用 await 来暂停该协程,由于 await 等待的是一个 Promise 对象,我们可以 resolve 来恢复该协程。
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 A 协程,要启动 B 协程,那么 A 协程就需要将主线程的控制权交给 B 协程,这就体现在 A 协程暂停执行,B 协程恢复执行;同样,也可以从 B 协程中启动 A 协程。通常,如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。

正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。每一时刻,该线程只能执行其中某一个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。

资料拓展:co 函数库的含义和用法

垃圾回收

垃圾数据

  从“GC Roots”对象出发,遍历 GC Root 中的所有对象,如果通过 GC Roots 没有遍历到的对象,则这些对象便是垃圾数据。V8 会有专门的垃圾回收器来回收这些垃圾数据。

垃圾回收算法

垃圾回收大致可以分为以下几个步骤:

  • 第一步,通过 GC Root 标记空间中活动对象和非活动对象。目前 V8 采用的可访问性(reachability)算法来判断堆中的对象是否是活动对象。具体地讲,这个算法是将一些 GC Root 作为初始存活的对象的集合,从 GC Roots 对象出发,遍历 GC Root 中的所有对象:

    • 通过 GC Root 遍历到的对象,我们就认为该对象是可访问的(reachable),那么必须保证这些对象应该在内存中保留,我们也称可访问的对象为活动对象
    • 通过 GC Roots 没有遍历到的对象,则是不可访问的(unreachable),那么这些不可访问的对象就可能被回收,我们称不可访问的对象为非活动对象
    • 浏览器环境中,GC Root 有很多,通常包括了以下几种 (但是不止于这几种):

      • 全局的 window 对象(位于每个 iframe 中);
      • 文档 DOM 树,由可以通过遍历文档到达的所有原生 DOM 节点组成;
      • 存放栈上变量。
  • 第二步,回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。
  • 第三步,做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们把这些不连续的内存空间称为内存碎片。当内存中出现了大量的内存碎片之后,如果需要分配较大的连续内存时,就有可能出现内存不足的情况,所以最后一步需要整理这些内存碎片。但这步其实是可选的,因为有的垃圾回收器不会产生内存碎片(比如副垃圾回收器)

垃圾回收

  • V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。代际假说有两个特点:

    • 第一个是大部分对象都是“朝生夕死”的,也就是说大部分对象在内存中存活的时间很短,比如函数内部声明的变量,或者块级作用域中的变量,当函数或者代码块执行结束时,作用域中定义的变量就会被销毁。因此这一类对象一经分配内存,很快就变得不可访问;
    • 第二个是不死的对象,会活得更久,比如全局的 window、DOM、Web API 等对象。
  • 为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。

    • 主垃圾回收器负责收集老生代中的垃圾数据,副垃圾回收器负责收集新生代中的垃圾数据。
    • 副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域(有些地方也称作From和To空间),一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。

      • 这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去。
      • 副垃圾回收器每次执行清理操作时,都需要将存活的对象从对象区域复制到空闲区域,复制操作需要时间成本,如果新生区空间设置得太大了,那么每次清理的时间就会过久,所以为了执行效率,一般新生区的空间会被设置得比较小
      • 副垃圾回收器还会采用对象晋升策略,也就是移动那些经过两次垃圾回收依然还存活的对象到老生代中。
    • 主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程

      • 主垃圾回收器主要负责老生代中的垃圾回收。除了新生代中晋升的对象,一些大的对象会直接被分配到老生代里。
      • 老生代中的对象有两个特点:一个是对象占用空间大;另一个是对象存活时间长。

Stop-The-World

  由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)

  • V8 最开始的垃圾回收器有两个特点:

    • 第一个是垃圾回收在主线程上执行,
    • 第二个特点是一次执行一个完整的垃圾回收流程。
  • 由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案。

    • 第一个方案是并行回收,在执行一个完整的垃圾回收过程中,垃圾回收器会使用多个辅助线程来并行执行垃圾回收。
    • 第二个方案是增量式垃圾回收,垃圾回收器将标记工作分解为更小的块,并且穿插在主线程不同的任务之间执行。采用增量垃圾回收时,垃圾回收器没有必要一次执行完整的垃圾回收过程,每次执行的只是整个垃圾回收过程中的一小部分工作。
    • 第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。

      资料参考:深入解读 V8 引擎的「并发标记」技术
    • 主垃圾回收器就综合采用了所有的方案(并发标记,增量标记,辅助清理),副垃圾回收器也采用了部分方案。

似此星辰非昨夜,为谁风露立中宵

Breaking the JavaScript Speed Limit with V8

  Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的JavaScript代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧

  1. 在构造函数里初始化所有对象的成员(所以这些实例之后不会改变其隐藏类);
  2. 总是以相同的次序初始化对象成员;
  3. 尽量使用可以用 31 位有符号整数表示的数;
  4. 为数组使用从 0 开始的连续的主键;
  5. 别预分配大数组(比如大于 64K 个元素)到其最大尺寸,令其尺寸顺其自然发展就好;
  6. 别删除数组里的元素,尤其是数字数组;
  7. 别加载未初始化或已删除的元素;
  8. 对于固定大小的数组,使用”array literals“初始化(初始化小额定长数组时,用字面量进行初始化);
  9. 小数组(小于 64k)在使用之前先预分配正确的尺寸;
  10. 请勿在数字数组中存放非数字的值(对象);
  11. 尽量使用单一类型(monomorphic)而不是多类型(polymorphic)(如果通过非字面量进行初始化小数组时,切勿触发类型的重新转换);
  12. 不要使用 try{} catch{}(如果存在 try/catch 代码快,则将性能敏感的代码放到一个嵌套的函数中);
  13. 在优化后避免在方法中修改隐藏类。
演讲资料参考: Performance Tips for JavaScript in V8 | JavaScript V8性能小贴士【译】 | 内网视频 | YouTube

在 V8 引擎里 5 个优化代码的技巧

  1. 对象属性的顺序: 在实例化你的对象属性的时候一定要使用相同的顺序,这样隐藏类和随后的优化代码才能共享;
  2. 动态属性: 在对象实例化之后再添加属性会强制使得隐藏类变化,并且会减慢为旧隐藏类所优化的代码的执行。所以,要在对象的构造函数中完成所有属性的分配;
  3. 方法: 重复执行相同的方法会运行的比不同的方法只执行一次要快 (因为内联缓存);
  4. 数组: 避免使用 keys 不是递增的数字的稀疏数组,这种 key 值不是递增数字的稀疏数组其实是一个 hash 表。在这种数组中每一个元素的获取都是昂贵的代价。同时,要避免提前申请大数组。最好的做法是随着你的需要慢慢的增大数组。最后,不要删除数组中的元素,因为这会使得 keys 变得稀疏;
  5. 标记值 (Tagged values): V8 用 32 位来表示对象和数字。它使用一位来区分它是对象 (flag = 1) 还是一个整型 (flag = 0),也被叫做小整型(SMI),因为它只有 31 位。然后,如果一个数值大于 31 位,V8 将会对其进行 box 操作,然后将其转换成 double 型,并且创建一个新的对象来装这个数。所以,为了避免代价很高的 box 操作,尽量使用 31 位的有符号数。
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈JavaScript中装箱和拆箱

JavaScript 启动性能瓶颈分析与解决方案

资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案

天长地久有时尽,V8 绵绵无绝期

资料参考

番外篇

  • Chrome插件Console Importer推荐:Easily import JS and CSS resources from Chrome console. (可以在浏览器控制台安装 loadsh、moment、jQuery 等库,在控制台直接验证、使用这些库。)
    效果图:
    BCv8tP.gif

本文首发于个人博客,欢迎指正和star
本文同步发布并于掘金社区

查看原文

風魔小次郎 赞了文章 · 2020-08-29

【React进阶系列】手写redux api

简介: 手写实现redux基础api

createStore( )和store相关方法

api回顾:

createStore(reducer, [preloadedState], enhancer)

创建一个 Redux store 来以存放应用中所有的 state
reducer (Function): 接收两个参数,当前的 state 树/要处理的 action,返回新的 state 树
preloadedState: 初始时的 state
enhancer (Function): store creator 的高阶函数,返回一个新的强化过的 store creator

Store 方法

getState() 返回应用当前的 state 树
dispatch(action) 分发 action。这是触发 state 变化的惟一途径
subscribe(listener) 添加一个变化监听器。每当 dispatch action 的时候就会执行,state 树中的一部分可能已经变化
replaceReducer(nextReducer) 替换 store 当前用来计算 state 的 reducer(高级不常用,不作实现)实现 Redux 热加载机制会用到

源码实现:

./self-redux.js

export function createStore(reducer, enhancer) {
  if(enhancer) {
     return enhancer(createStore)(reducer)
  }
  let currentState = {}
  let currentListeners = []
  function getState() {
    return currentState
  }
  function subscribe(listeners) {
    currentListeners.push(listener)
  }
  function dispatch(action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(v => v())
    return action
  }
  dispatch({ type: '@rainie/init-store' })
  return {
    getState,
    subscribe,
    dispatch
  }
}

demo:验证正确性

// import { createStore } from 'redux'
// 将redux文件替换成自己实现的redux文件
   import { createStore } from './self-redux.js'

// 这就是reducer处理函数,参数是状态和新的action
function counter(state=0, action) {
  // let state = state||0
  switch (action.type) {
    case '加机关枪':
      return state + 1
    case '减机关枪':
      return state - 1
    default:
      return 10
  }
}
// 新建store
const store = createStore(counter)
const init = store.getState()
console.log(`一开始有机枪${init}把`)
function listener(){
  const current = store.getState()
  console.log(`现在有机枪${current}把`)
}
// 订阅,每次state修改,都会执行listener
store.subscribe(listener)
// 提交状态变更的申请
store.dispatch({ type: '加机关枪' })

combineReducers(reducers)

api简介

把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数
实现 Redux 热加载机制会用到
import { combineReducers } from 'redux'
import todos from './todos'
import counter from './counter'

export default combineReducers({
  todos,
  counter
})

实现:

实质就是返回一个大的function 接受state,action,然后根据key用不同的reducer
注:combinedReducer的key跟state的key一样

const reducer = combineReducers({
  a: doSomethingWithA,
  b: processB,
  c: c
})
function reducer(state = {}, action) {
  return {
    a: doSomethingWithA(state.a, action),
    b: processB(state.b, action),
    c: c(state.c, action)
  }
}
function combindReducer(reducers) {
    // 第一个只是先过滤一遍 把非function的reducer过滤掉
  const reducerKeys = Object.keys(reducers)
  const finalReducers = {}
  reducerKeys.forEach((key) => {
      if(typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
      } 
  })
  const finalReducersKeys = Object.keys(finalReducers)
    // 第二步比较重要 就是将所有reducer合在一起
    // 根据key调用每个reducer,将他们的值合并在一起
    let hasChange = false;
    const nextState = {};
    return function combind(state={}, action) {
        finalReducersKeys.forEach((key) => {
            const previousValue = state[key];
            const nextValue = reducers[key](previousValue, action);
            nextState[key] = nextValue;
            hasChange = hasChange || previousValue !== nextValue
        })
        return hasChange ? nextState : state;
    }
}

applyMiddleware(...middleware)

使用包含自定义功能的 middleware 来扩展 Redux 是
...middleware (arguments): 遵循 Redux middleware API 的函数。
每个 middleware 接受 Store 的 dispatch 和 getState 函数作为命名参数,并返回一个函数。
该函数会被传入 被称为 next 的下一个 middleware 的 dispatch 方法,并返回一个接收 action 的新函数,这个函数可以直接调用 next(action),或者在其他需要的时刻调用,甚至根本不去调用它。
调用链中最后一个 middleware 会接受真实的 store 的 dispatch 方法作为 next 参数,并借此结束调用链。
所以,middleware 的函数签名是 ({ getState, dispatch }) => next => action
import { createStore, combineReducers, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import * as reducers from './reducers'

let reducer = combineReducers(reducers)
// applyMiddleware 为 createStore 注入了 middleware:
let store = createStore(reducer, applyMiddleware(thunk))

中间件机制applyMiddleware的实现

中间件机制图

clipboard.png

实现步骤
1.扩展createStore,使其接受第二个参数(中间件其实就是对createStore方法的一次扩展)
2.实现applyMiddleware,对store的disptach进行处理
3.实现一个中间件

正常调用

import React from 'react'
import ReactDOM from 'react-dom'
// import { createStore, applyMiddleware} from 'redux'
import { createStore, applyMiddleware} from './self-redux'
// import thunk from 'redux-thunk'
import thunk from './self-redux-thunk'
import { counter } from './index.redux'
import { Provider } from './self-react-redux';
import App from './App'
 
const store = createStore(counter, applyMiddleware(thunk))
ReactDOM.render(
  (
    <Provider store={store}>
      <App />
    </Provider>
  ),
  document.getElementById('root')) 
// 便于理解:函数柯利化例子
function add(x) {
  return function(y) {
    return x+y
  }
}
add(1)(2) //3

applymiddleware

// ehancer(createStore)(reducer)
// createStore(counter, applyMiddleware(thunk))
// applyMiddleware(thunk)(createStore)(reducer)
// 写法函数柯利化
export function applyMiddleware(middleware) {
  return function (createStore) {
    return function(...args) {
      // ...
    }
  }
}


// 只处理一个 middleware 时
export function applyMiddleware(middleware) {
   return createStore => (...args) => {
     const store = createStore(...args)
     let dispatch = store.dispatch

     const midApi = {
       getState: store.getState,
       dispatch: (...args) => dispatch(...args)
     }
    // 经过中间件处理,返回新的dispatch覆盖旧的
     dispatch = middleware(midApi)(store.dispatch)
    // 正常中间件调用:middleware(midApi)(store.dispatch)(action)

    return {
      ...store,
      dispatch
    }
   }
 }

// 处理多个middleware时

//  多个 compose
 export function applyMiddleware(...middlewares) {
   return createStore => (...args) => {
     const store = createStore(...args)
     let dispatch = store.dispatch

     const midApi = {
       getState: store.getState,
       dispatch: (...args) => dispatch(...args)
     }

    const middlewareChain = middlewares.map(middleware => middleware(midApi))
    dispatch => compose(...middlewareChain(store.dispatch))
    //  dispatch = middleware(midApi)(store.dispatch)
    // middleware(midApi)(store.dispatch)(action)

    return {
      ...store,
      dispatch
    }
   }
 }

手写redux-thunk异步中间件实现

// middleware(midApi)(store.dispatch)(action)
const thunk = ({ dispatch, getState }) => next => action => {
  // next就是store.dispatch函数
  // 如果是函数,执行以下,参数dispatch和getState
  if (typeof action == 'function') {
    return action(dispatch, getState)
  }
  // 默认 什么都不干
  return next(action)
}
export default thunk

处理异步action
export function addGunAsync() {
  // thunk插件作用,这里可以返回函数
  return dispatch => {
    setTimeout(() => {
      // 异步结束后,手动执行dispatch
      dispatch(addGun())
    }, 2000)
  }
}

趁热打铁,再实现一个中间件: dispatch接受一个数组,一次处理多个action

export arrayThunk = ({ dispatch, getState }) => next => action => {
  if(Array.isArray(action)) {
    return action.forEach(v => dispatch(v))
  }

  return next(action)
}

这类action会被处理
export function addTimes() {
  return [{ type: ADD_GUN },{ type: ADD_GUN },{ type: ADD_GUN }]
}

bindActionCreators的实现

在react-redux connect mapDispatchToProps中使用到了该方法,可以去看那篇blog,有详解~

api: bindActionCreators(actionCreators, dispatch)

把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们

实现:

 function bindActionCreator(creator, dispatch) {
   return (...args) => dispatch(creator(...args))
 }

 export function bindActionCreators(creators, dispatch) {
   let bound = {}
   Object.keys(creators).forEach( v => {
     let creator = creators[v]
     bound[v] = bindActionCreator(creator, dispatch)
   })
   return bound
 }
//  简写
 export function bindActionCreators(creators, dispatch) {
  return Object.keys(creators).reduce((ret, item) => {
     ret[item] =  bindActionCreator(creators[item], dispatch)
     return ret
   }, {})
 }

compose的实现

api: compose(...functions)

从右到左来组合多个函数。
当需要把多个 store 增强器 依次执行的时候,需要用到它
import { createStore, applyMiddleware, compose } from 'redux'
import thunk from 'redux-thunk'
import DevTools from './containers/DevTools'
import reducer from '../reducers'

const store = createStore(
  reducer,
  compose(
    applyMiddleware(thunk),
    DevTools.instrument()
  )
)

实现:
compose(fn1, fn2, fn3)
fn1(fn2(fn3))

 export function compose(...funcs) {
   if(funcs.length == 0) {
     return arg => arg
   }
   if(funcs.length == 1) {
     return funcs[0]
   }
   return funcs.reduce((ret,item) => (...args) => ret(item(...args)))
 }
查看原文

赞 2 收藏 1 评论 0

風魔小次郎 收藏了文章 · 2020-08-11

webpack import() 动态加载模块踩坑

import

webpack根据ES2015 loader 规范实现了用于动态加载的import()方法。

这个功能可以实现按需加载我们的代码,并且使用了promise式的回调,获取加载的包。

在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。

这里是一个简单的demo。

import('lodash').then(_ => {
    // Do something with lodash (a.k.a '_')...
  })

可以看到,import()的语法十分简单。该函数只接受一个参数,就是引用包的地址,这个地址与es6的import以及CommonJS的require语法用到的地址完全一致。可以实现无缝切换【写个正则替换美滋滋】。

并且使用了Promise的封装,开发起来感觉十分自在。【包装一个async函数就更爽了】

然而,以上只是表象。

只是表象。

我在开发的时候就遇到了问题。场景是这样的:一个对象,存储的是各级的路由信息,及其对应的页面组件。为减少主包大小,我们希望动态加载这些页面。

同时使用了react-loadable来简化组件的懒加载封装。代码如下所示。

function lazyLoad(path) {
  return Loadable({
    loader: () => import(path),
    loading: Spin,
  });
}

然后我就开始开心的在代码中写上lazyLoad('./pages/xxx')。果不其然,挂了。浏览器表示,没有鱼丸没有粗面,也不知道这个傻逼模块在哪里。

于是我查看了官方文档,发现有一个黄条提示。

clipboard.png

emmm,看来问题出在这里了。

这个现象其实是与webpack import()的实现高度相关的。由于webpack需要将所有import()的模块都进行单独打包,所以在工程打包阶段,webpack会进行依赖收集。

此时,webpack会找到所有import()的调用,将传入的参数处理成一个正则,如:

import('./app'+path+'/util') => /^\.\/app.*\/util$/

也就是说,import参数中的所有变量,都会被替换为【.*】,而webpack就根据这个正则,查找所有符合条件的包,将其作为package进行打包。

clipboard.png

因此,如果我们直接传入一个变量,webpack就会把 (整个电脑的包都打包进来[不闹]) 认为你在逗他,并且抛出一个WARNING: Critical dependency: the request of a dependency is an expression。

所以import的正确姿势,应该是尽可能静态化表达包所处的路径,最小化变量控制的区域

如我们要引用一堆页面组件,可以使用import('./pages/'+ComponentName),这样就可以实现引用的封装,同时也避免打包多余的内容。

另外一个影响功能封装的点,是import()中的相对路径,是import语句所在文件的相对路径,所以进一步封装import时会出现一些麻烦。

因为import语句中的路径会在编译后被处理成webpack命令执行目录的相对路径.

友情链接:

https://webpack.js.org/api/mo...

查看原文

風魔小次郎 赞了文章 · 2020-07-29

React16源码解析(二)-创建更新

React源码解析系列文章欢迎阅读:
React16源码解析(一)- 图解Fiber架构
React16源码解析(二)-创建更新
React16源码解析(三)-ExpirationTime
React16源码解析(四)-Scheduler
React16源码解析(五)-更新流程渲染阶段1
React16源码解析(六)-更新流程渲染阶段2
React16源码解析(七)-更新流程渲染阶段3
React16源码解析(八)-更新流程提交阶段
正在更新中...

在React中创建更新主要有下面三种方式:

1、ReactDOM.render() || hydrate
2、setState
3、forceUpdate

注:
除了上面的还有react 16.8 引进的hooks 中的useState,这个我们后续再讲。
hydrate是服务端渲染相关的,这块我并不会重点讲解。

一、ReactDOM.render()

ReactDOM.render().png

调用legacyRenderSubtreeIntoContainer()

const ReactDOM: Object = {
  // ......
  render(
    element: React$Element<any>,//传入的React组件
    container: DOMContainer,//挂载的容器节点
    callback: ?Function,//挂载后的回调函数
  ) {
    return legacyRenderSubtreeIntoContainer(
      null,
      element,
      container,
      false,
      callback,
    );
  },
  // ......
}

legacyRenderSubtreeIntoContainer

1、root = 创建ReactRoot
2、调用root.render()

function legacyRenderSubtreeIntoContainer(
  parentComponent: ?React$Component<any, any>,//null
  children: ReactNodeList,//传入进来需要挂在的class component
  container: DOMContainer,//根节点
  forceHydrate: boolean,//false
  callback: ?Function,//挂载完成后的回调函数
) {
  // ......
  // 是否存在根节点   初次渲染是不存在根节点的
  let root: Root = (container._reactRootContainer: any);
  if (!root) {
    // 1、创建ReactRoot 赋值给container._reactRootContainer和root(这里发生了很多事,一件很重要很重要的事   生成了fiber结构树。。)
    root = container._reactRootContainer = legacyCreateRootFromDOMContainer(
      container,
      forceHydrate,
    );
    // ......
  } else {
    // ......
    // 2、调用root.render()
    root.render(children, callback);
  }
  return DOMRenderer.getPublicRootInstance(root._internalRoot);
}

legacyCreateRootFromDOMContainer

1、清除所有子元素
2、创建 new ReactRoot节点

function legacyCreateRootFromDOMContainer(
  container: DOMContainer,//根节点
  forceHydrate: boolean,//false
): Root {
  //  服务端渲染相关 是否合并原先存在的dom节点 一般是false
  const shouldHydrate =
    forceHydrate || shouldHydrateDueToLegacyHeuristic(container);
  // 1、清除所有子元素,通过container.lastchild循环来清除container的所有内容,因为我们的属于首次渲染,container里边不包含任何元素
  if (!shouldHydrate) {
    let warned = false;
    let rootSibling;
    while ((rootSibling = container.lastChild)) {
      // ......
      container.removeChild(rootSibling);
    }
  }
  // Legacy roots are not async by default.
  const isConcurrent = false;
  // 2、创建 new ReactRoot节点
  return new ReactRoot(container, isConcurrent, shouldHydrate);
}

ReactRoot

从ReactRoot中, 我们把createContainer返回值赋给了 实例的_internalRoot, 往下看createContainer

function ReactRoot(
  container: Container,
  isConcurrent: boolean,
  hydrate: boolean,
) {
  // 这里创建了一个FiberRoot
  const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
  this._internalRoot = root;
}

createContainer

从createContainer看出, createContainer实际上是直接返回了createFiberRoot, 而createFiberRoot则是通过createHostRootFiber函数的返回值uninitializedFiber,并将其赋值在root对象的current上, 这里需要注意一个点就是,uninitializedFiber的stateNode的值是root, 即他们互相引用。

创建一个RootFiber -> createHostRootFiber() -> createFiber() -> new FiberNode()

这里创建的这个RootFiber里面的绝大部分属性都是初始值null或者是NoWork。所以具体代码我就没有贴出来了。
这里我提下有意义的点:
RootFiber上的tag会被赋值为 HostRoot。这个之后会用来判断节点类型。

还有这里创建的FiberRoot还有一个containerInfo置为ReactDOM.render第二个参数传入进来的容器节点。这个后续挂载的时候会用到。

export function createContainer(
  containerInfo: Container,
  isConcurrent: boolean,
  hydrate: boolean,
): OpaqueRoot {
  return createFiberRoot(containerInfo, isConcurrent, hydrate);
}

export function createFiberRoot(
  containerInfo: any,
  isConcurrent: boolean,
  hydrate: boolean,
): FiberRoot {
  // 1、创建了一个RootFiber
  const uninitializedFiber = createHostRootFiber(isConcurrent);
  // 2、互相引用
  // RootFiber.stateNode --> FiberRoot
  // FiberRoot.current --> RootFiber
  let root;
  root = {
    current: uninitializedFiber,
    containerInfo: containerInfo,
    // ......
  }
  uninitializedFiber.stateNode = root;
  // 3、return了这个FiberRoot
  return ((root: any): FiberRoot);
}

FiberRoot

这里牵扯到两种react中的数据结构,第一个FiberRoot,也就是上面createFiberRoot函数返回的对象。

type BaseFiberRootProperties = {|
  // root节点,render方法接收的第二个参数
  containerInfo: any,
  // 只有在持久更新中会用到,也就是不支持增量更新的平台,react-dom不会用到
  pendingChildren: any,
  // 当前应用对应的Fiber对象,是Root Fiber
  current: Fiber,

  // 一下的优先级是用来区分
  // 1) 没有提交(committed)的任务
  // 2) 没有提交的挂起任务
  // 3) 没有提交的可能被挂起的任务
  // 我们选择不追踪每个单独的阻塞登记,为了兼顾性能
  // The earliest and latest priority levels that are suspended from committing.
  // 最老和新的在提交的时候被挂起的任务
  earliestSuspendedTime: ExpirationTime,
  latestSuspendedTime: ExpirationTime,
  // The earliest and latest priority levels that are not known to be suspended.
  // 最老和最新的不确定是否会挂起的优先级(所有任务进来一开始都是这个状态)
  earliestPendingTime: ExpirationTime,
  latestPendingTime: ExpirationTime,
  // The latest priority level that was pinged by a resolved promise and can
  // be retried.
  // 最新的通过一个promise被reslove并且可以重新尝试的优先级
  latestPingedTime: ExpirationTime,

  // 如果有错误被抛出并且没有更多的更新存在,我们尝试在处理错误前同步重新从头渲染
  // 在`renderRoot`出现无法处理的错误时会被设置为`true`
  didError: boolean,

  // 正在等待提交的任务的`expirationTime`
  pendingCommitExpirationTime: ExpirationTime,
  // 已经完成的任务的FiberRoot对象,如果你只有一个Root,那他永远只可能是这个Root对应的Fiber,或者是null
  // 在commit阶段只会处理这个值对应的任务
  finishedWork: Fiber | null,
  // 在任务被挂起的时候通过setTimeout设置的返回内容,用来下一次如果有新的任务挂起时清理还没触发的timeout
  timeoutHandle: TimeoutHandle | NoTimeout,
  // 顶层context对象,只有主动调用`renderSubtreeIntoContainer`时才会有用
  context: Object | null,
  pendingContext: Object | null,
  // 用来确定第一次渲染的时候是否需要融合
  +hydrate: boolean,
  // 当前root上剩余的过期时间
  // TODO: 提到renderer里面区处理
  nextExpirationTimeToWorkOn: ExpirationTime,
  // 当前更新对应的过期时间
  expirationTime: ExpirationTime,
  // List of top-level batches. This list indicates whether a commit should be
  // deferred. Also contains completion callbacks.
  // TODO: Lift this into the renderer
  // 顶层批次(批处理任务?)这个变量指明一个commit是否应该被推迟
  // 同时包括完成之后的回调
  // 貌似用在测试的时候?
  firstBatch: Batch | null,
  // root之间关联的链表结构
  nextScheduledRoot: FiberRoot | null,
|};

Fiber

这里就是createHostRootFiber函数返回的fiber对象。注意这里其实每一个节点都对应一个fiber对象,不是Root专有的哦。

// Fiber对应一个组件需要被处理或者已经处理了,一个组件可以有一个或者多个Fiber
type Fiber = {|
  // 标记不同的组件类型
  // export const FunctionComponent = 0;
  // export const ClassComponent = 1;
  // export const IndeterminateComponent = 2; // Before we know whether it is function or class
  // export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
  // export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
  // export const HostComponent = 5;
  // export const HostText = 6;
  // export const Fragment = 7;
  // export const Mode = 8;
  // export const ContextConsumer = 9;
  // export const ContextProvider = 10;
  // export const ForwardRef = 11;
  // export const Profiler = 12;
  // export const SuspenseComponent = 13;
  // export const MemoComponent = 14;
  // export const SimpleMemoComponent = 15;
  // export const LazyComponent = 16;
  // export const IncompleteClassComponent = 17;
  tag: WorkTag,

  // ReactElement里面的key
  key: null | string,

  // ReactElement.type,标签类型,也就是我们调用`createElement`的第一个参数
  elementType: any,

  // The resolved function/class/ associated with this fiber.
  // 异步组件resolved之后返回的内容,一般是`function`或者`class`
  type: any,

  // The local state associated with this fiber.
  // 跟当前Fiber相关本地状态(比如浏览器环境就是DOM节点)
  stateNode: any,

  // 指向他在Fiber节点树中的`parent`,用来在处理完这个节点之后向上返回
  return: Fiber | null,

  // 单链表树结构
  // 指向自己的第一个子节点
  child: Fiber | null,
  // 指向自己的兄弟结构
  // 兄弟节点的return指向同一个父节点
  sibling: Fiber | null,
  index: number,

  // ref属性
  ref: null | (((handle: mixed) => void) & {_stringRef: ?string}) | RefObject,

  // 新的变动带来的新的props
  pendingProps: any, 
  // 上一次渲染完成之后的props
  memoizedProps: any,

  // 该Fiber对应的组件产生的Update会存放在这个队列里面
  updateQueue: UpdateQueue<any> | null,

  // 上一次渲染的时候的state
  memoizedState: any,

  // 一个列表,存放这个Fiber依赖的context
  firstContextDependency: ContextDependency<mixed> | null,

  // 用来描述当前Fiber和他子树的`Bitfield`
  // 共存的模式表示这个子树是否默认是异步渲染的
  // Fiber被创建的时候他会继承父Fiber
  // 其他的标识也可以在创建的时候被设置
  // 但是在创建之后不应该再被修改,特别是他的子Fiber创建之前
  mode: TypeOfMode,

  // Effect
  // 用来记录Side Effect
  // Don't change these two values. They're used by React Dev Tools.
  // export const NoEffect = /*              */ 0b00000000000;
  // export const PerformedWork = /*         */ 0b00000000001;

  // You can change the rest (and add more).
  // export const Placement = /*             */ 0b00000000010;
  // export const Update = /*                */ 0b00000000100;
  // export const PlacementAndUpdate = /*    */ 0b00000000110;
  // export const Deletion = /*              */ 0b00000001000;
  // export const ContentReset = /*          */ 0b00000010000;
  // export const Callback = /*              */ 0b00000100000;
  // export const DidCapture = /*            */ 0b00001000000;
  // export const Ref = /*                   */ 0b00010000000;
  // export const Snapshot = /*              */ 0b00100000000;

  // Update & Callback & Ref & Snapshot
  // export const LifecycleEffectMask = /*   */ 0b00110100100;

  // Union of all host effects
  // export const HostEffectMask = /*        */ 0b00111111111;

  // export const Incomplete = /*            */ 0b01000000000;
  // export const ShouldCapture = /*         */ 0b10000000000;
  effectTag: SideEffectTag,

  // 单链表用来快速查找下一个side effect
  nextEffect: Fiber | null,

  // 子树中第一个side effect
  firstEffect: Fiber | null,
  // 子树中最后一个side effect
  lastEffect: Fiber | null,

  // 代表任务在未来的哪个时间点应该被完成
  // 不包括他的子树产生的任务
  expirationTime: ExpirationTime,

  // 快速确定子树中是否有不在等待的变化
  childExpirationTime: ExpirationTime,

  // 在Fiber树更新的过程中,每个Fiber都会有一个跟其对应的Fiber
  // 我们称他为`current <==> workInProgress`
  // 在渲染完成之后他们会交换位置
  alternate: Fiber | null,

  // 下面是调试相关的,收集每个Fiber和子树渲染时间的

  actualDuration?: number,

  // If the Fiber is currently active in the "render" phase,
  // This marks the time at which the work began.
  // This field is only set when the enableProfilerTimer flag is enabled.
  actualStartTime?: number,

  // Duration of the most recent render time for this Fiber.
  // This value is not updated when we bailout for memoization purposes.
  // This field is only set when the enableProfilerTimer flag is enabled.
  selfBaseDuration?: number,

  // Sum of base times for all descedents of this Fiber.
  // This value bubbles up during the "complete" phase.
  // This field is only set when the enableProfilerTimer flag is enabled.
  treeBaseDuration?: number,

  // Conceptual aliases
  // workInProgress : Fiber ->  alternate The alternate used for reuse happens
  // to be the same as work in progress.
  // __DEV__ only
  _debugID?: number,
  _debugSource?: Source | null,
  _debugOwner?: Fiber | null,
  _debugIsCurrentlyTiming?: boolean,
|};

root.render()

经过上面的步骤,创建好了ReactRoot。初始化完成了。下面开始root.render。
我们回到legacyRenderSubtreeIntoContainer函数,前面一堆讲解的是调用legacyCreateRootFromDOMContainer方法我们得到了一个ReactRoot对象。reactRoot的原型上面我们找到了render方法:

ReactRoot.prototype.render = function(
  children: ReactNodeList,
  callback: ?() => mixed,
): Work {
  // 这个就是我们上面创建的FiberRoot对象
  const root = this._internalRoot;
  // ......
  DOMRenderer.updateContainer(children, root, null, work._onCommit);
  return work;
};

updateContainer

这个函数里面使用了 currentTime 和 expirationTime, currentTime是用来计算expirationTime的,expirationTime代表着优先级, 这个留在后续分析。后续紧接着调用了updateContainerAtExpirationTime。

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): ExpirationTime {
  // 这个current就是FiberRoot对应的RootFiber
  const current = container.current;
  const currentTime = requestCurrentTime();
  const expirationTime = computeExpirationForFiber(currentTime, current);
  return updateContainerAtExpirationTime(
    element,
    container,
    parentComponent,
    expirationTime,
    callback,
  );
}

注:这个函数在ReactFiberReconciler.js里面。

updateContainerAtExpirationTime

这里将current(即Fiber实例)提取出来, 并作为参数传入调用scheduleRootUpdate

export function updateContainerAtExpirationTime(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  const current = container.current;
  // ......
  return scheduleRootUpdate(current, element, expirationTime, callback);
}

scheduleRootUpdate

这个函数主要执行了两个操作:
1、创建更新createUpdate并放到更新队列enqueueUpdate,创建更新的具体细节稍后再讲哈。因为待会我们会发现其他地方也用到了。
2、个是执行sheculeWork函数,进入React异步渲染的核心:React Scheduler,这个我后续文章详细讲解。

function scheduleRootUpdate(
  current: Fiber,
  element: ReactNodeList,
  expirationTime: ExpirationTime,
  callback: ?Function,
) {
  // ......
  // 1、创建一个update对象
  const update = createUpdate(expirationTime);
  update.payload = {element};

  // ......
  // 2、将刚创建的update对象入队到fiber.updateQueue队列中
  enqueueUpdate(current, update);

  // 3、开始进入React异步渲染的核心:React Scheduler
  scheduleWork(current, expirationTime);
  return expirationTime;
}

图解

以上的过程我画了张图:
Fiber架构.png

二、setState

虽然我还没有讲解到class component 的渲染过程,但是这个不影响我现在要讨论的内容~
如下我们调用this.setState方法的时候,调用了this.updater.enqueueSetState

Component.prototype.setState = function(partialState, callback) {
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

先不管this.updater什么时候被赋值的,直接看到ReactFiberClassComponent.js中的enqueueSetState,这就是我们调用setState执行的enqueueSetState方法。

const classComponentUpdater = {
  // ......
  enqueueSetState(inst, payload, callback) {
    // inst 就是我们调用this.setState的this,也就是classComponent实例
    // 获取到当前实例上的fiber
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    // 计算当前fiber的到期时间(优先级)
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    // 创建更新一个更新update
    const update = createUpdate(expirationTime);

    //payload是setState传进来的要更新的对象
    update.payload = payload;

    //callback就是setState({},()=>{})的回调函数
    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'setState');
      }
      update.callback = callback;
    }

    // 把更新放到队列UpdateQueue
    enqueueUpdate(fiber, update);

    // 开始进入React异步渲染的核心:React Scheduler
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到上面的代码,是不是发现和上面ReactDOM.render中scheduleRootUpdate非常的相似。其实他们就是同一个更新原理呢~

三、forceUpdate

废话不多说,先上代码。也是在ReactFiberClassComponent.js中classComponentUpdater对象中。

const classComponentUpdater = {
  // ......
  enqueueForceUpdate(inst, callback) {
    const fiber = ReactInstanceMap.get(inst);
    const currentTime = requestCurrentTime();
    const expirationTime = computeExpirationForFiber(currentTime, fiber);

    const update = createUpdate(expirationTime);

    //与setState不同的地方
    //默认是0更新,需要改成2强制更新
    update.tag = ForceUpdate;

    if (callback !== undefined && callback !== null) {
      if (__DEV__) {
        warnOnInvalidCallback(callback, 'forceUpdate');
      }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  // ......
}

看到代码的我们很开心,简直就是enqueueSetState的孪生兄弟。我就不详说啦。

到这里我们总结一下上面三种更新的流程:
(1)获取节点对应的fiber对象
(2)计算currentTime
(3)根据(1)fiber和(2)currentTime计算fiber对象的expirationTime
(4)根据(3)expirationTime创建update对象
(5)将setState中要更新的对象赋值到(4)update.payload,ReactDOM.render是{element}
(6)将callback赋值到(4)update.callback
(7)update入队updateQueue
(8)进行任务调度

四、update对象

上面三种创建更新的方式中都创建了一个叫update的对象。那这个对象里面到底是什么呢?充满好奇的我们点开createUpdate函数瞧瞧:

export function createUpdate(expirationTime: ExpirationTime): Update<*> {
  return {
    // 过期时间
    expirationTime: expirationTime,

    // export const UpdateState = 0;
    // export const ReplaceState = 1;
    // export const ForceUpdate = 2;
    // export const CaptureUpdate = 3;
    // 指定更新的类型,值为以上几种
    // 提下CaptureUpdate,在React16后有一个ErrorBoundaries功能
    // 即在渲染过程中报错了,可以选择新的渲染状态(提示有错误的状态),来更新页面
    // 0更新 1替换 2强制更新 3捕获性的更新
    tag: UpdateState,

    // 更新内容,比如`setState`接收的第一个参数
    // 第一次渲染ReactDOM.render接收的是payload = {element};
    payload: null,

    // 更新完成后对应的回调,`setState`,`render`都有
    callback: null,

    // 指向下一个更新
    next: null,

    // 指向下一个`side effect`,这块内容后续讲解
    nextEffect: null,
  };
}

就是返回了个简单的对象。对象每个属性的解释我都写在上面了。

五、UpdateQueue

UpdateQueue是一个单向链表,用来存放update。每个update用next连接。它的结构如下:

//创建更新队列
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
  const queue: UpdateQueue<State> = {
    // 应用更新后的state
    // 每次的更新都是在这个baseState基础上进行更新
    baseState,
    // 队列中的第一个update
    firstUpdate: null,
    // 队列中的最后一个update
    lastUpdate: null,
    // 队列中第一个捕获类型的update
    firstCapturedUpdate: null,
    // 队列中最后一个捕获类型的update
    lastCapturedUpdate: null,
    // 第一个side effect
    firstEffect: null,
    // 最后一个side effect
    lastEffect: null,
    // 第一个和最后一个捕获产生的`side effect`
    firstCapturedEffect: null,
    lastCapturedEffect: null,
  };
  return queue;
}

六、enqueueUpdate

创建了update对象之后,紧接着调用了enqueueUpdate,把update对象放到队列enqueueUpdate。同时保证current和workInProgress的updateQueue是一致的,即fiber.updateQueue和fiber.alternate.updateQueue保持一致。

export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
  // 保证current和workInProgress的updateQueue是一致的
  // alternate即workInProgress
  const alternate = fiber.alternate;
  // current的队列
  let queue1;
  // alternate的队列
  let queue2;
  // 如果alternate为空
  if (alternate === null) {
    // There's only one fiber.
    queue1 = fiber.updateQueue;
    queue2 = null;
    // 如果queue1仍为空,则初始化更新队列
    if (queue1 === null) {
      queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
    }
  } else {
    // 如果alternate不为空,则取各自的更新队列
    queue1 = fiber.updateQueue;
    queue2 = alternate.updateQueue;
    if (queue1 === null) {
      if (queue2 === null) {
        // 初始化
        queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
        queue2 = alternate.updateQueue = createUpdateQueue(
          alternate.memoizedState,
        );
      } else {
        // 如果queue2存在但queue1不存在的话,则根据queue2复制queue1
        queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
      }
    } else {
      if (queue2 === null) {
        // Only one fiber has an update queue. Clone to create a new one.
        queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
      } else {
        // Both owners have an update queue.
      }
    }
  }
  if (queue2 === null || queue1 === queue2) {
    // 将update放入queue1中
    appendUpdateToQueue(queue1, update);
  } else {
    // 两个队列共享的是用一个update
    // 如果两个都是空队列,则添加update
    if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
      appendUpdateToQueue(queue1, update);
      appendUpdateToQueue(queue2, update);
    } else {
      // 如果两个都不是空队列,由于两个结构共享,所以只在queue1加入update
      // 在queue2中,将lastUpdate指向update
      appendUpdateToQueue(queue1, update);
      queue2.lastUpdate = update;
    }
  }

总结上面过程:
(1)queue1取的是fiber.updateQueue;

queue2取的是alternate.updateQueue

(2)如果两者均为null,则调用createUpdateQueue()获取初始队列
(3)如果两者之一为null,则调用cloneUpdateQueue()从对方中获取队列
(4)如果两者均不为null,则将update作为lastUpdate

注:两个队列共享的是同一个update。

七、scheduleWork

上面三种更新最后都调用了scheduleWork(fiber, expirationTime)进入React异步渲染的核心:React Scheduler。后续文章详细讲解。

文章如有不妥,欢迎指正~

查看原文

赞 13 收藏 3 评论 3

认证与成就

  • 获得 1 次点赞
  • 获得 10 枚徽章 获得 0 枚金徽章, 获得 2 枚银徽章, 获得 8 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2019-07-11
个人主页被 387 人浏览