问题描述
想把当前的某个库的 playground
从 webpackDevServer
迁移为 snowpack
,但是发现根据官网的配置文档没有办法实现指定启动目录。
{
"mount": {
"src": "/dist",
"public": "/"
}
}
你期待的结果是什么?实际看到的错误信息又是什么?
想求教如何配置 snowpack
命令的启动目录为项目目录下的 playground
文件夹呢~
web前端
母校: 小哈佛幼儿园
github:XHFkindergarten
没有足够的数据
風魔小次郎 提出了问题 · 1月14日
想把当前的某个库的 playground
从 webpackDevServer
迁移为 snowpack
,但是发现根据官网的配置文档没有办法实现指定启动目录。
{
"mount": {
"src": "/dist",
"public": "/"
}
}
想求教如何配置 snowpack
命令的启动目录为项目目录下的 playground
文件夹呢~
想把当前的某个库的 playground 从 webpackDevServer 迁移为 snowpack ,但是发现根据官网的配置文档没有办法实现指定启动目录。
关注 1 回答 0
風魔小次郎 赞了文章 · 1月4日
前言:在前端日常开发中,toC的业务难免会有一些截图或者生成海报的业务需求受限于html2canvas的不兼容性,或者canvas画图的样式局限性,目前我们又有了一种新的解决方案,node + puppeteer的组合,
Puppeteer 是 Google Chrome 团队官方的无界面(Headless)Chrome 工具。Chrome 作为浏览器市场的领头羊,Chrome Headless 将成为 web 应用 自动化测试 的行业标杆
那么我们可以用puppeteer来些什么?
可见第一种特性正式我们用的到的
npm install puppeteer
// 下载内核步骤可能会失败,cnpm 或科学上网
只介绍截图相关,如果需要其他特性的查询,请移步: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 服务,会携带参数包括:
截图托管页:静态的html页面。可url传参数去控制变量
node服务收到接口请求后,会请求托管页,构建一个要截取的网页,执行截图操作的api,返回截图buffer数据给请求的接口,到此为止,我们基础架构就整理完毕了
前置环境,假设我们现在已经有一个可用的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`
}
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}`
}
})
至此就大功搞成了,这种解决方案可以磨平各种运行环境的差异性,减少本地生成图片的失败率
更多知识点和福利,请👇
前言:在前端日常开发中,toC的业务难免会有一些截图或者生成海报的业务需求受限于html2canvas的不兼容性,或者canvas画图的样式局限性,目前我们又有了一种新的解决方案,node + puppeteer的组合,
赞 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
}
}
}
}
查看原文我们经常会绑定一些持续触发的事件,比如resize、scroll、mousemove等等,如果事件调用无限制,会加重浏览器负担,导致用户体验差,我们可以使用debounce(防抖)和throttle(节流)的方式来减少频繁的调用,同时也不会影响实际的效果。
赞 3 收藏 0 评论 0
風魔小次郎 赞了文章 · 2020-12-01
CRP
又称关键渲染路径,引用MDN
对它的解释:
关键渲染路径是指浏览器通过把 HTML、CSS 和 JavaScript 转化成屏幕上的像素的步骤顺序。优化关键渲染路径可以提高渲染性能。关键渲染路径包含了 Document Object Model (DOM),CSS Object Model (CSSOM),渲染树和布局。
优化关键渲染路径可以提升首屏渲染时间。理解和优化关键渲染路径对于确保回流和重绘可以每秒 60 帧、确保高性能的用户交互和避免无意义渲染至关重要。
CRP
进行性能优化?我想对于性能优化,大家都不陌生,无论是平时的工作还是面试,是一个老生常谈的话题。
如果单纯针对一些点去泛泛而谈,我想是不太严谨的。
今天我们结合一道非常经典的面试题:从输入URL到页面展示,这中间发生了什么?
来从其中的某些环节,来深入谈谈前端性能优化 CRP
。
这道题的经典程度想必不用我多说,这里我用一张图梳理了它的大致流程:
这个过程可以大致描述为如下:
1、URI 解析
2、DNS 解析(DNS 服务器)
3、TCP 三次握手(建立客户端和服务器端的连接通道)
4、发送 HTTP 请求
5、服务器处理和响应
6、TCP 四次挥手(关闭客户端和服务器端的连接)
7、浏览器解析和渲染
8、页面加载完成
本文我会从浏览器渲染过程、缓存、DNS 优化几方面进行性能优化的说明。
构建DOM
树的大致流程梳理为下图:
我们以下面这段代码为例进行分析:
<!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
这些节点对象,起始和结束令牌表明了节点之间的关系。
当所有的令牌消耗完以后就转换成了DOM
(文档对象模型)。
最终构建出的DOM
结构如下:
DOM
树构建完成,接下来就是CSSOM
树的构建了。
与HTML
的转换类似,浏览器会去识别CSS
正确的令牌,然后将这些令牌转化成CSS
节点。
子节点会继承父节点的样式规则,这里对应的就是层叠规则和层叠样式表。
构建DOM
树的大致流程可梳理为下图:
我们这里采用上面的HTML
为例,假设它有如下 css:
body {
font-size: 16px;
}
p {
font-weight: bold;
}
div {
color: orange;
}
那么最终构建出的CSSOM
树如下:
有了 DOM
和 CSSOM
,接下来就可以合成布局树(Render Tree)了。
等 DOM
和 CSSOM
都构建好之后,渲染引擎就会构造布局树。布局树的结构基本上就是复制 DOM
树的结构,不同之处在于 DOM
树中那些不需要显示的元素会被过滤掉,如 display:none
属性的元素、head
标签、script
标签等。
复制好基本的布局树结构之后,渲染引擎会为对应的 DOM
元素选择对应的样式信息,这个过程就是样式计算。
样式计算的目的是为了计算出 DOM
节点中每个元素的具体样式,这个阶段大体可分为三步来完成。
和 HTML
文件一样,浏览器也是无法直接理解这些纯文本的 CSS
样式,所以当渲染引擎接收到 CSS
文本时,会执行一个转换操作,将 CSS
文本转换为浏览器可以理解的结构——styleSheets
。
现在我们已经把现有的 CSS 文本转化为浏览器可以理解的结构了,那么接下来就要对其进行属性值的标准化操作。
什么是属性值标准化?我们来看这样的一段CSS
:
body {
font-size: 2em;
}
div {
font-weight: bold;
}
div {
color: red;
}
可以看到上面的 CSS
文本中有很多属性值,如 2em、bold、red,这些类型数值不容易被渲染引擎理解,所以需要将所有值转换为渲染引擎容易理解的、标准化的计算值,这个过程就是属性值标准化。
那标准化后的属性值是什么样子的?
从图中可以看到,2em
被解析成了 32px
,bold
被解析成了 700
,red
被解析成了 rgb(255,0,0)
……
现在样式的属性已被标准化了,接下来就需要计算 DOM
树中每个节点的样式属性了,如何计算呢?
这其中涉及到两点:CSS 的继承规则
和层叠规则
。
这里由于不是本文的重点,我简单做下说明:
CSS
继承就是每个 DOM
节点都包含有父节点的样式CSS
的一个基本特征,它是一个定义了如何合并来自多个源的属性值的算法。它在 CSS
处于核心地位,CSS
的全称“层叠样式表”正是强调了这一点。样式计算完成之后,渲染引擎还需要计算布局树中每个元素对应的几何位置,这个过程就是计算布局。
现在,我们有 DOM
树和 DOM
树中元素的样式,但这还不足以显示页面,因为我们还不知道 DOM
元素的几何位置信息。那么接下来就需要计算出 DOM
树中可见元素的几何位置,我们把这个计算过程叫做布局
。
通过样式计算和计算布局就完成了最终布局树的构建。再之后,就该进行后续的绘制操作了。
到这里,浏览器的渲染过程就基本结束了,通过下面的一张图来梳理下:
到这里我们已经把浏览器解析和渲染的完整流程梳理完成了,那么这其中有那些地方可以去做性能优化呢?
通常一个页面有三个阶段:加载阶段、交互阶段和关闭阶段。
JavaScript
脚本。JavaScript
脚本。这里我们需要重点关注加载阶段
和交互阶段
,因为影响到我们体验的因素主要都在这两个阶段,下面我们就来逐个详细分析下。
我们先来分析如何系统优化加载阶段中的页面,来看一个典型的渲染流水线,如下图所示:
通过上面对浏览器渲染过程的分析我们知道JavaScript
、首次请求的 HTML
资源文件、CSS
文件是会阻塞首次渲染的,因为在构建 DOM
的过程中需要 HTML
和 JavaScript
文件,在构造渲染树的过程中需要用到 CSS
文件。
这些能阻塞网页首次渲染的资源称为关键资源
。而基于关键资源,我们可以继续细化出三个影响页面首次渲染的核心因素:
关键资源个数
。关键资源个数越多,首次页面的加载时间就会越长。关键资源大小
。通常情况下,所有关键资源的内容越小,其整个资源的下载时间也就越短,那么阻塞渲染的时间也就越短。请求关键资源需要多少个RTT(Round Trip Time)
。RTT
是网络中一个重要的性能指标,表示从发送端发送数据开始,到发送端收到来自接收端的确认,总共经历的时延。了解了影响加载过程中的几个核心因素之后,接下来我们就可以系统性地考虑优化方案了。总的优化原则就是减少关键资源个数
,降低关键资源大小
,降低关键资源的 RTT 次数
:
JavaScript
和 CSS
改成内联的形式,比如上图的 JavaScript
和 CSS
,若都改成内联模式,那么关键资源的个数就由 3 个减少到了 1 个。另一种方式,如果 JavaScript
代码没有 DOM
或者 CSSOM
的操作,则可以改成 sync
或者 defer
属性CSS
和 JavaScript
资源,移除 HTML
、CSS
、JavaScript
文件中一些注释内容RTT
的次数?可以通过减少关键资源的个数和减少关键资源的大小搭配来实现。除此之外,还可以使用 CDN
来减少每次 RTT
时长。接下来我们再来聊聊页面加载完成之后的交互阶段以及应该如何去优化。
先来看看交互阶段的渲染流水线:
其实这块大致有以下几点可以优化:
避免DOM的回流
。也就是尽量避免重排
和重绘
操作。减少 JavaScript 脚本执行时间
。有时JavaScript
函数的一次执行时间可能有几百毫秒,这就严重霸占了主线程执行其他渲染任务的时间。针对这种情况我们可以采用以下两种策略:
Web Workers
。DOM操作相关的优化
。浏览器有渲染引擎
和JS引擎
,所以当用JS
操作DOM
时,这两个引擎要通过接口互相“交流”,因此每一次操作DOM
(包括只是访问DOM
的属性),都要进行引擎之间解析的开销,所以常说要减少 DOM 操作。总结下来有以下几点:
let left = el.offsetLeft
。DOM
的class
来集中改变样式,而不是通过style
一条条的去修改。DOM
的时代,基于vue/react
等采用virtual dom
的框架合理利用 CSS 合成动画
。合成动画是直接在合成线程上执行的,这和在主线程上执行的布局、绘制等操作不同,如果主线程被 JavaScript
或者一些布局任务占用,CSS
动画依然能继续执行。所以要尽量利用好 CSS
合成动画,如果能让 CSS
处理动画,就尽量交给 CSS
来操作。CSS选择器优化
。我们知道CSS引擎
查找是从右向左匹配的。所以基于此有以下几条优化方案:
CSS属性优化
。浏览器绘制图像时,CSS
的计算也是耗费性能的,一些属性需浏览器进行大量的计算,属于昂贵的属性(box-shadows
、border-radius
、transforms
、filters
、opcity
、:nth-child
等),这些属性在日常开发中经常用到,所以并不是说不要用这些属性,而是在开发中,如果有其它简单可行的方案,那可以优先选择没有昂贵属性的方案。避免频繁的垃圾回收
。我们知道 JavaScript
使用了自动垃圾回收机制,如果在一些函数中频繁创建临时对象,那么垃圾回收器也会频繁地去执行垃圾回收策略。这样当垃圾回收操作发生时,就会占用主线程,从而影响到其他任务的执行,严重的话还会让用户产生掉帧、不流畅的感觉。缓存可以说是性能优化中简单高效的一种优化方式了。一个优秀的缓存策略可以缩短网页请求资源的距离,减少延迟,并且由于缓存文件可以重复利用,还可以减少带宽,降低网络负荷。下图是浏览器缓存的查找流程图:
浏览器缓存相关的知识点还是很多的,这里我有整理一张图:
关于浏览器缓存的详细介绍说明,可以参考我之前的这篇文章,这里就不赘述了。
DNS
全称Domain Name System
。它是互联网的“通讯录”,它记录了域名与实际ip
地址的映射关系。每次我们访问一个网站,都要通过各级的DNS
服务器查询到该网站的服务器ip
,然后才能访问到该服务器。
DNS
相关的优化一般涉及到两点:浏览器DNS
缓存和DNS
预解析。
DNS
缓存一图胜千言:
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 rel="dns-prefetch" href="//img10.360buyimg.com" />
李兵 「浏览器工作原理与实践」
1.如果觉得这篇文章还不错,来个分享、点赞、在看三连吧,让更多的人也看到~
2.关注公众号前端森林,定期为你推送新鲜干货好文。
3.特殊阶段,带好口罩,做好个人防护。
4.添加微信fs1263215592,拉你进技术交流群一起学习 🍻
关键渲染路径是指浏览器通过把 HTML、CSS 和 JavaScript 转化成屏幕上的像素的步骤顺序。优化关键渲染路径可以提高渲染性能。关键渲染路径包含了 Document Object Model (DOM),CSS Object Model (CSSOM),渲染树和布局。
赞 50 收藏 36 评论 0
風魔小次郎 赞了文章 · 2020-11-03
未经授权,不得转载,原文地址: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,所以我就点进去看了一眼是啥新玩具。
Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.
看上去是尤大的一个验证性的尝试,看到 custom element
和 lit-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
生命周期也触发了。
看 vue-lit
之前,我们先了解一下 lit-html
和 lit-ement
,这两个东西其实已经出来很久了,可能并不是所有人都了解。
lit-html 可能很多人并不熟悉,甚至没有见过。
所以是啥?答案是 HTML 模板引擎。
如果没有体感,我问一个问题,React
核心的东西有哪些?大家都会回答:jsx
、Virtual-DOM
、diff
,没错,就是这些东西构成了 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
上。
直接上代码(省略样式代码):
<!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 这又是啥呢?
关键词: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 Component
的 base class
。分析一下上面的 Demo,lit-element
做了什么事情:
setter
的 state
state
lit-html
渲染元素,并且会创建 ShadowDOM
总之,lit-element
遵守 Web Components
标准,它是一个 class
,基于它可以快速创建 Web Component
。
更多关于如何使用 lit-element
进行开发,在这里就不展开说了。
说 Web Components
之前我想先问问大家,大家还记得 jQuery
吗,它方便的选择器让人难忘。但是后来 document.querySelector
这个 API
的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery
。
浏览器原生 API
已经足够好用,我们并不需要为了操作 DOM
而使用 jQuery
。
再后来,是不是很久没有直接操作过 DOM
了?
是的,由于 React
/ Vue
等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API
来操作 DOM
。
我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React
等是不是也会像 jQuery
一样被浏览器原生能力替代?
像 React
/ Vue
等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM
中的任意位置。
现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components
这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API
或者是使用基于 Web Components
标准的框架和库来开发了?
当然,未来是不可知的
我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。
来看看 Web Components
的一些主要功能吧。
自定义元素顾名思义就是用户可以自定义 HTML
元素,通过 CustomElementRegistry
的 define
来定义,比如:
window.customElements.define('my-element', MyElement);
然后就可以直接通过 <my-element />
使用了。
根据规范,有两种 Custom elements
:
HTML
元素,使用时可以直接 <my-element />
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
的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。
DOM
时DOM
中删除时我们这里留意一下 attributeChangedCallback
,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:
attributeChangedCallback(name, oldValue, newValue) {
// TODO
}
需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback()
回调函数,你必须监听这个属性:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['my-name'];
}
constructor() {
super();
}
}
元素的 my-name
属性发生变化时,就会触发回调方法。
Web Components
一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM
实现。
关于 Shadow DOM
,这里主要想说一下 CSS
样式隔离的特性。Shadow DOM
里外的 selector
是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。
这样有什么好处呢?划重点,样式隔离,Shadow DOM
通过局部的 HTML
和 CSS
,解决了样式上的一些问题,类似 vue
的 scope
的感觉,元素内部不用关心 selector
和 CSS rule
会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector
非常简单:title
/ item
等,不需要任何的工具或者命名的约束。
可以通过 <template>
来添加一个 Web Component
的 Shadow 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>
?答案当然是:可以。
介绍了 lit-html/element
和 Web Components
,我们回到尤大这个 vue-lit
。
首先我们看到在 Vue 3.0
的 Release
里有这么一段:
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
响应式系统的能力这里稍带解释一下 shallowReactive
和 effect
,不展开:
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)
})
}
}
)
}
也就这几个流程:
Web Components
的 Custom Elements
Shadow DOM
的 ShadowRoot
节点factory
和内部创建的 ShadowRoot
节点交给 lit-html
的 render
渲染出来回过头来看尤大提供的 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/reactivity
的 reactive
能力,把 state
变成了响应式。
没毛病,和 Vue 3.0 Release
里说的一致,@vue/reactivity
可以和 lit-html
配合,使得 Vue
和 Web Components
结合到一块儿了,是不是还挺有意思。
可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。
猜测不久将来这些关键词会突然就爆发:Unbundled
/ ES Modules
/ Web components
/ Custom Element
/ Shadow DOM
...
是不是值得期待一下?
思考可能还比较浅,文笔有限,不足之处欢迎大家指出。
可以关注公众号「前端试炼」,或者加微信好友 qianduanshilian,加入交流群。
查看原文尤大北京时间 9月18日 下午的时候发了一个微博,人狠话不多。看到这个表情,大家都知道有大事要发生。果然,在写这篇文章的时候,上 GitHub 上看了一眼,刚好碰上发布:
赞 15 收藏 5 评论 2
風魔小次郎 收藏了文章 · 2020-11-03
未经授权,不得转载,原文地址: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,所以我就点进去看了一眼是啥新玩具。
Proof of concept mini custom elements framework powered by @vue/reactivity and lit-html.
看上去是尤大的一个验证性的尝试,看到 custom element
和 lit-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
生命周期也触发了。
看 vue-lit
之前,我们先了解一下 lit-html
和 lit-ement
,这两个东西其实已经出来很久了,可能并不是所有人都了解。
lit-html 可能很多人并不熟悉,甚至没有见过。
所以是啥?答案是 HTML 模板引擎。
如果没有体感,我问一个问题,React
核心的东西有哪些?大家都会回答:jsx
、Virtual-DOM
、diff
,没错,就是这些东西构成了 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
上。
直接上代码(省略样式代码):
<!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 这又是啥呢?
关键词: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 Component
的 base class
。分析一下上面的 Demo,lit-element
做了什么事情:
setter
的 state
state
lit-html
渲染元素,并且会创建 ShadowDOM
总之,lit-element
遵守 Web Components
标准,它是一个 class
,基于它可以快速创建 Web Component
。
更多关于如何使用 lit-element
进行开发,在这里就不展开说了。
说 Web Components
之前我想先问问大家,大家还记得 jQuery
吗,它方便的选择器让人难忘。但是后来 document.querySelector
这个 API
的出现并且广泛使用,大家似乎就慢慢地淡忘了 jQuery
。
浏览器原生 API
已经足够好用,我们并不需要为了操作 DOM
而使用 jQuery
。
再后来,是不是很久没有直接操作过 DOM
了?
是的,由于 React
/ Vue
等框架(库)的出现,帮我们做了很多事情,我们可以不用再通过复杂的 DOM API
来操作 DOM
。
我想表达的是,是不是有一天,如果浏览器原生能力足够好用的时候,React
等是不是也会像 jQuery
一样被浏览器原生能力替代?
像 React
/ Vue
等框架(库)都做了同样的事情,在之前浏览器的原生能力是实现不了的,比如创建一个可复用的组件,可以渲染在 DOM
中的任意位置。
现在呢?我们似乎可以不使用任意的框架和库,甚至不用打包编译,仅是通过 Web Components
这样的浏览器原生能力就可以创建可复用的组件,是不是未来的某一天我们就抛弃了现在所谓的框架和库,直接使用原生 API
或者是使用基于 Web Components
标准的框架和库来开发了?
当然,未来是不可知的
我不是一个 Web Components 的无脑吹,只不过,我们需要面向未来编程。
来看看 Web Components
的一些主要功能吧。
自定义元素顾名思义就是用户可以自定义 HTML
元素,通过 CustomElementRegistry
的 define
来定义,比如:
window.customElements.define('my-element', MyElement);
然后就可以直接通过 <my-element />
使用了。
根据规范,有两种 Custom elements
:
HTML
元素,使用时可以直接 <my-element />
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
的构造函数中,可以指定多个回调函数,它们将会在元素的不同生命时期被调用。
DOM
时DOM
中删除时我们这里留意一下 attributeChangedCallback
,是每当元素的属性发生变化时,就会执行这个回调函数,并且获得元素的相关信息:
attributeChangedCallback(name, oldValue, newValue) {
// TODO
}
需要特别注意的是,如果需要在元素某个属性变化后,触发 attributeChangedCallback()
回调函数,你必须监听这个属性:
class MyElement extends HTMLElement {
static get observedAttributes() {
return ['my-name'];
}
constructor() {
super();
}
}
元素的 my-name
属性发生变化时,就会触发回调方法。
Web Components
一个非常重要的特性,可以将结构、样式封装在组件内部,与页面上其它代码隔离,这个特性就是通过 Shadow DOM
实现。
关于 Shadow DOM
,这里主要想说一下 CSS
样式隔离的特性。Shadow DOM
里外的 selector
是相互获取不到的,所以也没办法在内部使用外部定义的样式,当然外部也没法获取到内部定义的样式。
这样有什么好处呢?划重点,样式隔离,Shadow DOM
通过局部的 HTML
和 CSS
,解决了样式上的一些问题,类似 vue
的 scope
的感觉,元素内部不用关心 selector
和 CSS rule
会不会被别人覆盖了,会不会不小心把别人的样式给覆盖了。所以,元素的 selector
非常简单:title
/ item
等,不需要任何的工具或者命名的约束。
可以通过 <template>
来添加一个 Web Component
的 Shadow 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>
?答案当然是:可以。
介绍了 lit-html/element
和 Web Components
,我们回到尤大这个 vue-lit
。
首先我们看到在 Vue 3.0
的 Release
里有这么一段:
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
响应式系统的能力这里稍带解释一下 shallowReactive
和 effect
,不展开:
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)
})
}
}
)
}
也就这几个流程:
Web Components
的 Custom Elements
Shadow DOM
的 ShadowRoot
节点factory
和内部创建的 ShadowRoot
节点交给 lit-html
的 render
渲染出来回过头来看尤大提供的 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/reactivity
的 reactive
能力,把 state
变成了响应式。
没毛病,和 Vue 3.0 Release
里说的一致,@vue/reactivity
可以和 lit-html
配合,使得 Vue
和 Web Components
结合到一块儿了,是不是还挺有意思。
可能尤大只是一时兴起,写了这个小玩具,但是可以见得这可能真的是一种大趋势。
猜测不久将来这些关键词会突然就爆发:Unbundled
/ ES Modules
/ Web components
/ Custom Element
/ Shadow DOM
...
是不是值得期待一下?
思考可能还比较浅,文笔有限,不足之处欢迎大家指出。
可以关注公众号「前端试炼」,或者加微信好友 qianduanshilian,加入交流群。
查看原文尤大北京时间 9月18日 下午的时候发了一个微博,人狠话不多。看到这个表情,大家都知道有大事要发生。果然,在写这篇文章的时候,上 GitHub 上看了一眼,刚好碰上发布:
風魔小次郎 收藏了文章 · 2020-10-12
V8 是由 Google 开发的开源 JavaScript 引擎,也被称为虚拟机,模拟实际计算机各种功能来实现代码的编译和执行。
上图清晰版
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码。
JavaScirpt 引擎可以将 JS 代码编译为不同 CPU(Intel, ARM 以及 MIPS 等)对应的汇编代码,这样我们就不需要去翻阅每个 CPU 的指令集手册来编写汇编代码了。当然,JavaScript 引擎的工作也不只是编译代码,它还要负责执行代码、分配内存以及垃圾回收。
# 将一个寄存器中的数据移动到另外一个寄存器中
1000100111011000 #机器指令
mov ax,bx #汇编指令
资料拓展: 汇编语言入门教程【阮一峰】
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 可以访问宿主环境中的对象,并在脚本中完成对宿主对象的操作。
资料拓展:v8 logo | V8 (JavaScript engine)) | 《V8、JavaScript+的现在与未来》 | 几张图让你看懂 WebAssemblyV8一词最早见于“V-8 engine”,即V8发动机,一般使用在中高端车辆上。8个气缸分成两组,每组4个,成V型排列。是高层次汽车运动中最常见的发动机结构,尤其在美国,IRL,ChampCar和NASCAR都要求使用V8发动机。
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打开(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 --print-bytecode ./test.js
,打印出 Ignition(解释器)生成的 Bytecode(字节码)。查看 d8 命令
# 如果不想使用./d8这种方式进行调试,可将d8加入环境变量,之后就可以直接`d8 --help`了
./d8 --help
过滤特定的命令
# 如果是 Windows 系统,可能缺少 grep 程序,请自行下载安装并添加环境变量
./d8 --help |grep print
如:
// 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 是一个非常复杂的项目,有超过 100 万行 C++代码。它由许多子模块构成,其中这 4 个模块是最重要的:
Parser:负责将 JavaScript 源码转换为 Abstract Syntax Tree (AST)
确切的说,在“Parser”将 JavaScript 源码转换为 AST前,还有一个叫”Scanner“的过程,具体流程如下:
Ignition:interpreter,即解释器,负责将 AST 转换为 Bytecode,解释执行 Bytecode;同时收集 TurboFan 优化编译所需的信息,比如函数参数的类型;解释器执行时主要有四个模块,内存中的字节码、寄存器、栈、堆。
通常有两种类型的解释器,基于栈 (Stack-based)和基于寄存器 (Register-based),基于栈的解释器使用栈来保存函数参数、中间运算结果、变量等;基于寄存器的虚拟机则支持寄存器的指令操作,使用寄存器来保存参数、中间计算结果。通常,基于栈的虚拟机也定义了少量的寄存器,基于寄存器的虚拟机也有堆栈,其区别体现在它们提供的指令集体系。大多数解释器都是基于栈的,比如 Java 虚拟机,.Net 虚拟机,还有早期的 V8 虚拟机。基于堆栈的虚拟机在处理函数调用、解决递归问题和切换上下文时简单明快。而现在的 V8 虚拟机则采用了基于寄存器的设计,它将一些中间数据保存到寄存器中。
基于寄存器的解释器架构:
资料参考:解释器是如何解释执行字节码的?
其中,Parser,Ignition 以及 TurboFan 可以将 JS 源码编译为汇编代码,其流程图如下:
简单地说,Parser 将 JS 源码转换为 AST,然后 Ignition 将 AST 转换为 Bytecode,最后 TurboFan 将 Bytecode 转换为经过优化的 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 代码的?
V8 本质上是一个虚拟机,因为计算机只能识别二进制指令,所以要让计算机执行一段高级语言通常有两种手段:
总结:
V8 执行一段 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 解析为中间代码,这主要是基于以下两点:
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 属性存储:
总结:
因为 JavaScript 中的对象是由一组组属性和值组成的,所以最简单的方式是使用一个字典来保存属性和值,但是由于字典是非线性结构,所以如果使用字典,读取效率会大大降低。为了提升查找效率,V8 在对象中添加了两个隐藏属性,排序属性和常规属性,element 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性。properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存常规属性。
通过引入这两个属性,加速了 V8 查找属性的速度,为了更加进一步提升查找效率,V8 还实现了内置内属性的策略,当常规属性少于一定数量时,V8 就会将这些常规属性直接写进对象中,这样又节省了一个中间步骤。
但是如果对象中的属性过多时,或者存在反复添加或者删除属性的操作,那么 V8 就会将线性的存储模式降级为非线性的字典存储模式,这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。
资料拓展:快属性和慢属性:V8 是怎样提升对象属性访问速度的?
栈的优势和缺点:
虽然操作速度非常快,但是栈也是有缺点的,其中最大的缺点也是它的优点所造成的,那就是栈是连续的,所以要想在内存中分配一块连续的大空间是非常难的,因此栈空间是有限的。
// 栈溢出
function factorial(n) {
if (n === 1) {
return 1;
}
return n * factorial(n - 1);
}
console.log(factorial(50000));
继承就是一个对象可以访问另外一个对象中的属性和方法,在 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__
来访问或者修改该属性,其主要原因有两个:
__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 团队逐渐发现将 JavaScript 源码直接编译成二进制代码存在两个致命的问题:
这两个问题无疑会阻碍 V8 在移动设备上的普及,于是 V8 团队大规模重构代码,引入了中间的字节码。字节码的优势有如下三点:
// 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
常用字节码指令:
Add:Add a0, [0]
是从 a0 寄存器加载值并将其与累加器中的值相加,然后将结果再次放入累加器。
add a0 后面的[0]称之为 feedback vector slot,又叫反馈向量槽,它是一个数组,解释器将解释执行过程中的一些数据类型的分析信息都保存在这个反馈向量槽中了,目的是为了给 TurboFan 优化编译器提供优化信息,很多字节码都会为反馈向量槽提供运行时信息。
V8 中的字节码指令集 | 理解 V8 的字节码「译」
JavaScript 是一门动态语言,其执行效率要低于静态语言,V8 为了提升 JavaScript 的执行速度,借鉴了很多静态语言的特性,比如实现了 JIT 机制,为了提升对象的属性访问速度而引入了隐藏类,为了加速运算而引入了内联缓存。
静态语言中,如 C++ 在声明一个对象之前需要定义该对象的结构,代码在执行之前需要先被编译,编译的时候,每个对象的形状都是固定的,也就是说,在代码的执行过程中是无法被改变的。可以直接通过偏移量查询来查询对象的属性值,这也就是静态语言的执行效率高的一个原因。
JavaScript 在运行时,对象的属性是可以被修改的,所以当 V8 使用了一个对象时,比如使用了 obj.x 的时候,它并不知道该对象中是否有 x,也不知道 x 相对于对象的偏移量是多少,也就是说 V8 并不知道该对象的具体的形状。那么,当在 JavaScript 中要查询对象 obj 中的 x 属性时,V8 会按照具体的规则一步一步来查询,这个过程非常的慢且耗时。
具体地讲,V8 对每个对象做如下两点假设:
符合这两个假设之后,V8 就可以对 JavaScript 中的对象做深度优化了。V8 会为每个对象创建一个隐藏类,对象的隐藏类中记录了该对象一些基础的布局信息,包括以下两点:
// 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 就会为其复用同一个隐藏类,这样有两个好处:
那么,什么情况下两个对象的形状是相同的,要满足以下两点:
// 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 查找对象属性值的过程中,依然有查找对象的隐藏类和根据隐藏类来查找对象属性值的过程。如果一个函数中利用了对象的属性,并且这个函数会被多次执行:
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 的流程:
内联缓存及其原理:
单态、多态和超态:
总结:
V8 引入了内联缓存(IC),IC 会监听每个函数的执行过程,并在一些关键的地方埋下监听点,这些包括了加载对象属性 (Load)、给对象属性赋值 (Store)、还有函数调用 (Call),V8 会将监听到的数据写入一个称为反馈向量 (FeedBack Vector) 的结构中,同时 V8 会为每个执行的函数维护一个反馈向量。有了反馈向量缓存的临时数据,V8 就可以缩短对象属性的查找路径,从而提升执行效率。但是针对函数中的同一段代码,如果对象的隐藏类是不同的,那么反馈向量也会记录这些不同的隐藏类,这就出现了多态和超态的情况。我们在实际项目中,要尽量避免出现多态或者超态的情况。
回调函数有两种类型:同步回调和异步回调,同步回调函数是在执行函数内部被执行的,而异步回调函数是在执行函数外部被执行的。
通用 UI 线程宏观架构:
UI 线程提供一个消息队列,并将待执行的事件添加到消息队列中,然后 UI 线程会不断循环地从消息队列中取出事件、执行事件。关于异步回调,这里也有两种不同的类型,其典型代表是 setTimeout 和 XMLHttpRequest:
微任务是基于消息队列、事件循环、UI 主线程还有堆栈而来的,然后基于微任务,又可以延伸出协程、Promise、Generator、await/async 等现代前端经常使用的一些技术。
// 不会使浏览器卡死
function foo() {
setTimeout(foo, 0);
}
foo();
微任务:
// 浏览器console控制台可使浏览器卡死(无法响应鼠标事件等)
function foo() {
return Promise.resolve().then(foo);
}
foo();
协程是一种比线程更加轻量级的存在。你可以把协程看成是跑在线程上的任务,一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。比如,当前执行的是 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 有很多,通常包括了以下几种 (但是不止于这几种):
V8 依据代际假说,将堆内存划分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放生存时间久的对象。代际假说有两个特点:
为了提升垃圾回收的效率,V8 设置了两个垃圾回收器,主垃圾回收器和副垃圾回收器。
副垃圾回收器采用了 Scavenge 算法,是把新生代空间对半划分为两个区域(有些地方也称作From和To空间),一半是对象区域,一半是空闲区域。新的数据都分配在对象区域,等待对象区域快分配满的时候,垃圾回收器便执行垃圾回收操作,之后将存活的对象从对象区域拷贝到空闲区域,并将两个区域互换。
主垃圾回收器回收器主要负责老生代中的垃圾数据的回收操作,会经历标记、清除和整理过程。
由于 JavaScript 是运行在主线程之上的,因此,一旦执行垃圾回收算法,都需要将正在执行的 JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)。
V8 最开始的垃圾回收器有两个特点:
由于这两个原因,很容易造成主线程卡顿,所以 V8 采用了很多优化执行效率的方案。
第三个方案是并发回收,回收线程在执行 JavaScript 的过程,辅助线程能够在后台完成的执行垃圾回收的操作。
资料参考:深入解读 V8 引擎的「并发标记」技术
Daniel Clifford 在 Google I/O 2012 上做了一个精彩的演讲“Breaking the JavaScript Speed Limit with V8”。在演讲中,他深入解释了 13 个简单的代码优化方法,可以让你的JavaScript代码在 Chrome V8 引擎编译/运行时更加快速。在演讲中,他介绍了怎么优化,并解释了原因。下面简明的列出了13 个 JavaScript 性能提升技巧:
try{} catch{}
(如果存在 try/catch
代码快,则将性能敏感的代码放到一个嵌套的函数中);演讲资料参考: Performance Tips for JavaScript in V8 | JavaScript V8性能小贴士【译】 | 内网视频 | YouTube
资料参考:How JavaScript works: inside the V8 engine + 5 tips on how to write optimized code | 译文
box 操作参考:JavaScript类型:关于类型,有哪些你不知道的细节? | JavaScript 的装箱和拆箱 | 谈谈JavaScript中装箱和拆箱
资料参考: JavaScript Start-up Performance | JavaScript 启动性能瓶颈分析与解决方案
本文首发于个人博客,欢迎指正和star。
本文同步发布并于掘金社区。
我们写的 JavaScript 代码直接交给浏览器或者 Node 执行时,底层的 CPU 是不认识的,也没法执行。CPU 只认识自己的指令集,指令集对应的是汇编代码。写汇编代码是一件很痛苦的事情。并且不同类型的 CPU 的指令集是不一样的,那就意味着需要给每一种 CPU 重写汇编代码...
風魔小次郎 赞了文章 · 2020-08-29
简介: 手写实现redux基础api
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: '加机关枪' })
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))
中间件机制图
实现步骤
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 }]
}
在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
}, {})
}
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)))
}
查看原文简介: 手写实现redux基础api createStore( )和store相关方法 api回顾: createStore(reducer, [preloadedState], enhancer) {代码...} Store 方法 {代码...} 源码实现: {代码...} demo:验证正确性 {代码...} combineReducers(reducers) api简介 {代码...} {代码......
赞 2 收藏 1 评论 0
風魔小次郎 收藏了文章 · 2020-08-11
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')。果不其然,挂了。浏览器表示,没有鱼丸没有粗面,也不知道这个傻逼模块在哪里。
于是我查看了官方文档,发现有一个黄条提示。
emmm,看来问题出在这里了。
这个现象其实是与webpack import()的实现高度相关的。由于webpack需要将所有import()的模块都进行单独打包,所以在工程打包阶段,webpack会进行依赖收集。
此时,webpack会找到所有import()的调用,将传入的参数处理成一个正则,如:
import('./app'+path+'/util') => /^\.\/app.*\/util$/
也就是说,import参数中的所有变量,都会被替换为【.*】,而webpack就根据这个正则,查找所有符合条件的包,将其作为package进行打包。
因此,如果我们直接传入一个变量,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...
查看原文在代码中所有被import()的模块,都将打成一个单独的包,放在chunk存储的目录下。在浏览器运行到这一行代码时,就会自动请求这个资源,实现异步加载。
風魔小次郎 赞了文章 · 2020-07-29
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是服务端渲染相关的,这块我并不会重点讲解。
调用legacyRenderSubtreeIntoContainer()
const ReactDOM: Object = {
// ......
render(
element: React$Element<any>,//传入的React组件
container: DOMContainer,//挂载的容器节点
callback: ?Function,//挂载后的回调函数
) {
return legacyRenderSubtreeIntoContainer(
null,
element,
container,
false,
callback,
);
},
// ......
}
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);
}
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中, 我们把createContainer返回值赋给了 实例的_internalRoot, 往下看createContainer
function ReactRoot(
container: Container,
isConcurrent: boolean,
hydrate: boolean,
) {
// 这里创建了一个FiberRoot
const root = DOMRenderer.createContainer(container, isConcurrent, hydrate);
this._internalRoot = root;
}
从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);
}
这里牵扯到两种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,
|};
这里就是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,
|};
经过上面的步骤,创建好了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;
};
这个函数里面使用了 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里面。
这里将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);
}
这个函数主要执行了两个操作:
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;
}
以上的过程我画了张图:
虽然我还没有讲解到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非常的相似。其实他们就是同一个更新原理呢~
废话不多说,先上代码。也是在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的对象。那这个对象里面到底是什么呢?充满好奇的我们点开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是一个单向链表,用来存放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;
}
创建了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(fiber, expirationTime)进入React异步渲染的核心:React Scheduler。后续文章详细讲解。
文章如有不妥,欢迎指正~
查看原文注:除了上面的还有react 16.8 引进的hooks 中的useState,这个我们后续再讲。hydrate是服务端渲染相关的,这块我并不会重点讲解。
赞 13 收藏 3 评论 3
查看全部 个人动态 →
(゚∀゚ )
暂时没有
(゚∀゚ )
暂时没有
注册于 2019-07-11
个人主页被 387 人浏览
推荐关注