Cyyyiyi

Cyyyiyi 查看完整档案

广州编辑  |  填写毕业院校广州黑胡子游戏开发有限公司  |  前端工程师 编辑填写个人主网站
编辑

前端工程师

个人动态

Cyyyiyi 发布了文章 · 2月28日

ECMAScript2021新增了哪些好东西?

String.prototype.replaceAll

都用过replace,但只会匹配字串中第一个符合条件的字符。如果希望替换全部符合条件的,以往一般会用到正则。这情况下replaceAll提供了更方便的写法。

const str = 'I love JS,I enjoy it.'
console.log(str.replace('I','You')) 
//=>You love JS,You enjoy it.

Private methods and getter/setter

新增在方法前添加#标志表示其为私有方法。

Class Product {
  #showName{
    console.log('book-name') 
  }
  showPrice{
     console.log('19.9')
  }
}

const product = new Product()
person.getName();
// =>报错
同样可以用在获取器getter上:
Class Product {
  get #name{
     return 'book-name'
  }
  get price{
     return 19.9
  }
}
const product = new Product()
console.log(product.name)
// =>undefined

Promise.any()

Promise新增any方法,可接受一个包含多个请求的数组。返回当中最快响应的请求结果。

const API = 'https://api.github.com/users';
Promise.any([
  fetch(`${API}/cyinside`),
  fetch(`${API}/vuejs`),
])
  .then(response => response.json())
  .then(({ login }) => console.log(`hi ${login}`))

// => hi vuejs

Logical assignment operators

a ||= b
//等价于
a || ( a = b )

a &&= b
//等价于
a && ( a = b )

a ??= b
//等价于
a ?? ( a = b )

Numeric separators

新增允许可以在数值中间添加下划线分隔,增加数值可读性。

const x = 1_000_000_000
console.log(x)
// => 1000000000

WeakRef

WeakRef代表弱引用,主要用途是实现对大型对象的缓存或映射。当不想长时间保存大量已经使用的缓存和映射的时候,可以允许内存被垃圾回收。如果以后需要它,再生成新的缓存。
详细解释可看[[MDN] https://developer.mozilla.org... ]

查看原文

赞 0 收藏 0 评论 0

Cyyyiyi 发布了文章 · 2月23日

说一说 Flux的小白知识

MV*

说Flux之前,先说说熟知的MV*模式。 MV*一般指MVC/MVVM.
MVC如我们熟知:

  • M = Model,负责数据的保存,检验,获取等。
  • V = View,数据的展示,DOM元素。
  • C = Controller,与传统的controller定义不同,前端中的controller的定义比较模糊。但一般作为Model和View之间的协调。

而MVVM,就是在MVC的基础上,用VM(ViewModel)替代Controller。
也就是数据绑定,界面与VM的数据状态可以相互影响。

说到这里,其实不难想象,数据可以多方影响,来源不明且多样。混乱的数据流向导致项目后期,开发维护的难度加大。Model的构建数量过多,会影响View的构建结构与渲染优化。

Flux

Flux 的命名来自于拉丁语Flow。是一套基于dispatcher的前端应用架构模式。
\>核心思想是数据和逻辑永远单向流动。
对比MV* 结构,Flux模式有着数据来源单一,数据变动可溯源的优点。让逻辑架构更加谨慎清晰。

这里和React组件间的单向流动不同。React的单向数据流动是一般指基于Props的组件间通信设计。而Flux的单向数据流,则是基于整个架构上的。

1. Dispatcher

一个全局唯一的数据流处理中心。有3个主要API:

  • dispatch(object payload) : void

用于分发action。执行已注册的监听。

* **register(function callback) : string**
注册监听用于响应dispatch。 返回一个token可供waitFor()使用。
* **waitFor(array ids) : void**
当在回调中遇到waitFor()时,该回调暂停执行。在waitFor()完成执行并回调之后,再继续执行原始回调。此方法可以用于等待并执行其他store操作。

*action是一个对象类型,在FSA规范中对其字段进行了列举:type,error,payload,meta。其中type(或者name)为必须,是store根据action修改数据的依据。
在Facebook的Flux实例源码中,在Dispatcher类里定义了一个\_callbacks数组,保存了在register方法中注册的监听器。并在dispatch方法中遍历执行,且把action作为参数传入监听函数。

2. Store

一般有以下几个功能:

  • 保存状态数据(state)。
  • 暴露一个Getter用于获取状态
  • 定义修改数据的逻辑。
  • 调用Dispatcher.register方法将自己注册为一个监听器。
  • 定义一个更新监听方法

当调用dispatch分发action,store在register方法中注册的监听就会被调用,同时得到传入action。
store将根据action携带的type判断是否响应操作。如响应,调用内部的数据修改逻辑。并在执行完毕后触发更新事件。

3. Controller-View/View

Controller-View一般作为最顶层的View,负责Store和View之间的数据传递。

  • **进行store与View的绑定,定义数据更新及传递方式。**这里说的View一般是React,当然也可以是其他框架。
  • **监听Store发出的更新事件。**当Store更新后,Controller-View会重新获取store中的数据,并触发界面重绘。

class CounterContainer extends Component {
static getStores() {
return [CounterStore];
}

static calculateState(prevState) {
return {
counter: CounterStore.getState(),
};
}

render() {
return ;

}
}

const container = Container.create(CounterContainer);
上面是Flux官方文档中将一个React Class创建为Controller-View的简单调用实例。class向外暴露getStores、calculateState两个方法:

  • getStores用于绑定Class关联的Store。
  • calculateState用于获取Store中的数据转化为自身的State,并作为props传给子组件。

Container.create方法中,会获取绑定的Store中dispatchToken,然后根据dispatchToken利用waitFor方法等待Store完成数据更新逻辑,然后执行更新回调。
当Store触发更新后,Container会调用setState方法更新State,同时触发画面渲染更新。
当然关联store、更新回调等可以有多种实现方法,以上只是其中一种思路。

View就是界面表现了,View可以通过props获得Container传入的数据。
如果View需要修改数据,必须使用dispatcher分发action的方式进行修改。
这也是单向数据流的重要特征,view不能直接修改数据。

说起Flux,很多第一句就是单向数据流。但其实数据中心化控制,也是特点之一。
所有的更改,必须通过action发出,dispatcher分配。store中心化控制了数据,令数据管理和问题追查变得清晰容易。
action的管理,令架构不用关心辨别数据更新的触发方式,所有触发方式都抽象成了action。Flux架构并非React独有,也能作为其他组件框架的状态管理方案。
前面说到Flux对于MV*架构的优点,当然Flux本身也有的坑。虽然后面提出的Redux,则对Flux中state与更新逻辑没有分离抽象,没有对URL路由进行管理等问题进行改进。但Flux只是一种设计约定,各人对其都有自己的观点和想法。与其他架构相比其实没有绝对的优势劣势,只是提供解决方案的一种。

查看原文

赞 0 收藏 0 评论 0

Cyyyiyi 发布了文章 · 2月23日

关于Vite的小小笔记

Vite是什么

两大部分组成:

  • Native-ES-modules-based server for development

开发环境原生ES模块构建。

  • Rollup-based build for production

打包构建基于Rollup。

传统构建工具的问题

  1. 传统打包工具会在dev server显示页面前打包引入的所有依赖。
  • 包括对每一个文件的import/export关系完整分析
  • 排序,复写,串联所有模块。

2 应用越大,打包越慢。
3 代码拆分利于生产环境性能,但是对开发环境没帮助。

基于 JavaScript 的工具有性能瓶颈,启动开发服务器较慢,影响开发效率。

Vite的优化办法:

Vite在浏览器请求源代码时进行转换并按需提供源代码,只在当前实际使用时才会被处理。减少了页面画面加载出来前的打包时间。
但在请求较大模块的时候,页面的加载会变慢。而同时请求的模块过多的时候,也会造成浏览器请求堵塞。所以Vite还有以下优化:

  1. 依赖预构建。

    • CommonJS 和 UMD 兼容性。

    Vite 必须先将作为 CommonJS 或 UMD 发布的依赖项转换为 ESM,当转换 CommonJS 依赖时,Vite 会执行智能导入分析。

    * 保证一个模块只请求一次预构建。
    Vite 将有许多内部模块的 ESM 依赖关系转换为单个模块,以提高后续页面加载性能。
  2. etag 与 304 Not Modified。

源代码模块的请求会根据 304 Not Modified 进行协商缓存,而依赖模块请求则会通过 Cache-Control: max-age=31536000,immutable 进行强缓存,因此一旦被缓存它们将不需要再次请求。

  1. 代码拆分

在原生ESM构建模式下,精确地使已编辑的模块与其最近的 HMR 边界之间的链失效,只更新修改的模块本身。这意味着代码拆分对开发环境和生产环境都起到性能优化的作用。

  1. 原生ESM不支持以下写法:

import { someMethod } from ‘my-dep’
Vite会检测这种裸模块导入,并进行预构建,转换为ESM模式。重写导入为合法
URL,以便浏览器正确导入。
import {createApp } from 'vue'
//转换为
import { createApp } from '/@modules/vue'

原生ESM下的HMR

在 Vite 中,HMR 是在原生 ESM 上执行的。当编辑一个文件时,Vite 只需要精确地使已编辑的模块与其最近的 HMR 边界之间的链失效(大多数时候只需要模块本身),使 HMR 更新始终快速,无论应用程序的大小。

  1. 重写引入模块时记录模块引用关系。
  2. import.meta.hot标识修改的模块的HMR边界。
  3. 当一个模块发生改变,会追溯其引入者,并寻找HMR边界。
  4. HMR边界重新请求修改的模块,并进行更新。
  5. 如果引用链追溯到尽头,例如Vue应用中引入的index.js,main.js发生更改。应用还是会进行全部重载。
查看原文

赞 0 收藏 0 评论 0

Cyyyiyi 关注了标签 · 2019-09-10

antd

ant-design ,react 框架

关注 964

Cyyyiyi 收藏了文章 · 2019-02-28

web前端性能优化总结

概括

涉及到的分类

  • 网络层面
  • 构建层面
  • 浏览器渲染层面
  • 服务端层面

涉及到的功能点

  • 资源的合并与压缩
  • 图片编解码原理和类型选择
  • 浏览器渲染机制
  • 懒加载预加载
  • 浏览器存储
  • 缓存机制
  • PWA
  • Vue-SSR

资源合并与压缩

http请求的过程及潜在的性能优化点

  • 理解减少http请求数量减少请求资源大小两个优化要点
  • 掌握压缩合并的原理
  • 掌握通过在线网站fis3两种实现压缩与合并的方法

浏览器的一个请求从发送到返回都经历了什么

动态的加载静态的资源

  • dns是否可以通过缓存减少dns查询时间
  • 网络请求的过程走最近的网络环境
  • 相同的静态资源是否可以缓存
  • 能否减少http请求大小
  • 能否减少http请求数量
  • 服务端渲染

资源的合并与压缩设计到的性能点

  • 减少http请求的数量
  • 减少请求的大小

html压缩

HTML代码压缩就是压缩这些在文本文件中有意义,但是在HTML中不显示的字符,包括空格,制表符,换行符等,还有一些其他意义的字符,如HTML注释也可以被压缩

意义

  • 大型网站意义比较大

如何进行html的压缩

  • 使用在线网站进行压缩(走构建工具多,公司级在线网站手动压缩小)
  • node.js提供了html-minifier工具
  • 后端模板引擎渲染压缩

cssjs压缩

css的压缩

  • 无效代码删除

    • 注释、无效字符
  • css语义合并

css压缩的方式

  • 使用在线网站进行压缩
  • 使用html-minifierhtml中的css进行压缩
  • 使用clean-csscss进行压缩

js的压缩语混乱

  • 无效字符的删除

    • 空格、注释、回车等
  • 剔除注释
  • 代码语意的缩减和优化

    • 变量名缩短(a,b)等
  • 代码保护

    • 前端代码是透明的,客户端代码用户是可以直接看到的,可以轻易被窥探到逻辑和漏洞

js压缩的方式

  • 使用在线网站进行压缩
  • 使用html-minifierhtml中的js进行压缩
  • 使用uglifyjs2js进行压缩

不合并文件可能存在的问题

  • 文件与文件有插入之间的上行请求,又增加了N-1个网络延迟
  • 受丢包问题影响更严重
  • 经过代理服务器时可能会被断开

文件合并缺点

  • 首屏渲染问题

    • 文件合并之后的js变大,如果首页的渲染依赖这个js的话,整个页面的渲染要等js请求完才能执行
    • 如果首屏只依赖a.js,只要等a.js完成后就可执行
    • 没有通过服务器端渲染,现在框架都需要等合并完的文件请求完才能执行,基本都需要等文件合并后的js
  • 缓存失效问题

    • 标记 js`md5`戳
    • 合并之后的js,任何一个改动都会导致大面积的缓存失效

文件合并对应缺点的处理

  • 公共库合并
  • 不同页面的合并

    • 不同页面js单独打包
  • 见机行事,随机应变

文件合并对应方法

  • 使用在线网站进行合并
  • 构建阶段,使用nodejs进行文件合并

图片相关优化

一张JPG的解析过程


jpg有损压缩:虽然损失一些信息,但是肉眼可见影响并不大

png8/png24/png32之间的区别

  • png8   ----256色 + 支持透明
  • png24 ----2^24 + 不支持透明
  • png32  ---2^24 +支持透明

文件大小 + 色彩丰富程度

png32是在png24上支持了透明,针对不同的业务场景选择不同的图片格式很重要

不同的格式图片常用的业务场景

不同格式图片的特点

  • jpg有损压缩,压缩率高,不支持透明
  • png支持透明,浏览器兼容性好
  • webp压缩程度更好,在ios webview中有兼容性问题
  • svg矢量图,代码内嵌,相对较小,图片样式相对简单的场景(尽量使用,绘制能力有限,图片简单用的比较多)

不同格式图片的使用场景

  • jpg:大部分不需要透明图片的业务场景
  • png:大部分需要透明图片的业务场景
  • webpandroid全部(解码速度和压缩率高于jpgpng,但是iossafari还没支持)
  • svg:图片样式相对简单的业务场景

图片压缩的几种情况

  • 针对真实图片情况,舍弃一些相对无关紧要的色彩信息
  • CSS雪碧图:把你的网站用到的一些图片整合到一张单独的图片中

    • 优点:减少HTTP请求的数量(通过backgroundPosition定位所需图片)
    • 缺点:整合图片比较大时,加载比较慢(如果这张图片没有加载成功,整个页面会失去图片信息)facebook官网任然在用,主要pc用的比较多,相对性能比较强
  • Image-inline:将图片的内容嵌到html中(减少网站的HTTP请求)

    • base64信息,减少网站的HTTP请求,如果图片比较小比较多,时间损耗主要在请求的骨干网络
  • 使用矢量图

    • 使用SVG进行矢量图的绘制
    • 使用icon-font解决icon问题
  • 在android下使用webp

    • webp的优势主要体现在它具有更优的图像数据压缩算法,能带来更小的图片体积,而且拥有肉眼识别无差异的图像质量;
    • 同时具备了无损和有损的压缩模式、Alpha透明以及动画的特性,在JPEGPNG上的转化效果都非常优秀、稳定和统一

cssjs的装载与执行

HTML页面加载渲染的过程

一个网站在浏览器端是如何进行渲染的

HTML渲染过程中的一些特点

  • 顺序执行,并发加载

    • 词法分析:从上到下依次解析

      • 通过HTML生成Token对象(当前节点的所有子节点生成后,才会通过next token获取到当前节点的兄弟节点),最终生成Dom Tree
    • 并发加载:资源请求是并发请求的
    • 并发上限

      • 浏览器中可以支持并发请求,不同浏览器所支持的并发数量不同(以域名划分),以Chrome为例,并发上限为6个
      • 优化点: 把CDN资源分布在多个域名下
  • 是否阻塞

    • css阻塞

      • csshead中通过link引入会阻塞页面的渲染

        • 如果我们把css代码放在head中去引入的话,那么我们整个页面的渲染实际上就会等待headcss加载并生成css树,最终和DOM整合生成RanderTree之后才会进行渲染
        • 为了浏览器的渲染,能让页面显示的时候视觉上更好。

避免某些情况,如:假设你放在页面最底部,用户打开页面时,有可能出现,页面先是显示一大堆文字或图片,自上而下,丝毫没有排版和样式可言。最后,页面又恢复所要的效果

    - `css`不阻塞`js`的加载,但阻塞`js`的执行
    - `css`不阻塞外部脚步的加载(`webkit preloader 预资源加载器`)
- `js`阻塞
    -  直接通过`<script src>`引入会阻塞后面节点的渲染
        -  `html parse`认为`js`会动态修改文档结构(`document.write`等方式),没有进行后面文档的变化
        -  `async`、`defer`(`async`放弃了依赖关系)
            - `defer`属性(`<script data-original="" defer></script>`) 

(这是延迟执行引入的js脚本(即脚本加载是不会导致解析停止,等到document全部解析完毕后,defer-script也加载完毕后,在执行所有的defer-script加载的js代码,再触发Domcontentloaded

            - `async`属性(`<script data-original="" async></script>`) 
                - 这是异步执行引入的`js`脚本文件 
                - 与`defer`的区别是`async`会在加载完成后就执行,但是不会影响阻塞到解析和渲染。但是还是会阻塞`load`事件,所以`async-script`会可能在`DOMcontentloaded`触发前或后执行,但是一定会在`load`事件前触发。



懒加载与预加载

懒加载

  • 图片进入可视区域之后请求图片资源
  • 对于电商等图片很多,页面很长的业务场景适用
  • 减少无效资源的加载
  • 并发加载的资源过多会会阻塞js的加载,影响网站的正常使用

img src被设置之后,webkit解析到之后才去请求这个资源。所以我们希望图片到达可视区域之后,img src才会被设置进来,没有到达可视区域前并不现实真正的src,而是类似一个1px的占位符。

场景:电商图片

预加载

  • 图片等静态资源在使用之前的提前请求
  • 资源使用到时能从缓存中加载,提升用户体验
  • 页面展示的依赖关系维护

场景:抽奖

懒加载原生jszepto.lazyload

原理

先将img标签中的src链接设为同一张图片(空白图片),将其真正的图片地址存储再img标签的自定义属性中(比如data-src)。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。

注意问题:
  • 关注首屏处理,因为还没滑动
  • 占位,图片大小首先需要预设高度,如果没有设置的话,会全部显示出来

var viewheight = document.documentElement.clientHeight   //可视区域高度

function lazyload(){
    var eles = document.querySelectorAll('img[data-original][lazyload]')

    Array.prototype.forEach.call(eles,function(item,index){
        var rect;
        if(item.dataset.original === '') return;
        rect = item.getBoundingClientRect(); //返回元素的大小及其相对于视口的

        if(rect.bottom >= 0 && rect.top < viewheight){
            !function(){
                var img = new Image();
                img.src = item.dataset.url;
                img.onload = function(){
                    item.src = img.src
                }
                item.removeAttribute('data-original');
                item.removeAttribute('lazyload');
            }()
        }
    })
}

lazyload()
document.addEventListener('scroll',lazyload)

预加载原生jspreloadJS实现

预加载实现的几种方式

  • 第一种方式:直接请求下来
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b216cbfa18" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b21b70c8d2" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b216e17e26" style="display: none"/>
<img data-original="https://user-gold-cdn.xitu.io/2019/2/21/1690d1b217b3ae59" style="display: none"/>
  • 第二种方式:image对象
var image = new Image();
image.src = "www.pic26.com/dafdafd/safdas.jpg";
  • 第三种方式:xmlhttprequest

    • 缺点:存在跨域问题
    • 优点:好控制
var xmlhttprequest = new XMLHttpRequest();

xmlhttprequest.onreadystatechange = callback;

xmlhttprequest.onprogress = progressCallback;

xmlhttprequest.open("GET","http:www.xxx.com",true);

xmlhttprequest.send();

function callback(){
    if(xmlhttprequest.readyState == 4 && xmlhttprequest.status == 200){
        var responseText = xmlhttprequest.responseText;
    }else{
        console.log("Request was unsuccessful:" + xmlhttprequest.status);
    }
}

function progressCallback(){
    e = e || event;
    if(e.lengthComputable){
        console.log("Received"+e.loaded+"of"+e.total+"bytes")
    }
}   

 

PreloadJS模块

  • 本质权衡浏览器加载能力,让它尽可能饱和利用起来

重绘与回流

css性能让javascript变慢

要把css相关的外部文件引入放进head中,加载css时,整个页面的渲染是阻塞的,同样的执行javascript代码的时候也是阻塞的,例如javascript死循环。

一个线程   =>  javascript解析
一个线程   =>  UI渲染

这两个线程是互斥的,当UI渲染的时候,javascript的代码被终止。当javascript代码执行,UI线程被冻结。所以css的性能让javascript变慢。

频繁触发重绘与回流,会导致UI频繁渲染,最终导致js变慢

什么是重绘和回流

回流

  • render tree中的一部分(或全部)因为元素的规模尺寸布局隐藏等改变而需要重新构建。这就成为回流(reflow)
  • 页面布局和几何属性改变时,就需要回流

重绘

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

关系

用到chrome 分析 performance

回流必将引起重绘,但是重绘不一定会引起回流

避免重绘、回流的两种方法

触发页面重布局的一些css属性

  • 盒子模型相关属性会触发重布局

    • width
    • height
    • padding
    • margin
    • display
    • border-width
    • border
    • min-height
  • 定位属性及浮动也会触发重布局

    • top
    • bottom
    • left
    • right
    • position
    • float
    • clear
  • 改变节点内部文字结构也会触发重布局
  • text-align
  • overflow-y
  • font-weight
  • overflow
  • font-family
  • line-height
  • vertical-align
  • white-space
  • font-size

优化点:使用不触发回流的方案替代触发回流的方案

只触发重绘不触发回流

  • color
  • border-styleborder-radius
  • visibility
  • text-decoration
  • backgroundbackground-imagebackground-positionbackground-repeatbackground-size
  • outlineoutline-coloroutline-styleoutline-width
  • box-shadow

新建DOM的过程

  • 获取DOM后分割为多个图层
  • 对每个图层的节点计算样式结果(Recalculate style 样式重计算)
  • 为每个节点生成图形和位置(Layout 回流和重布局)
  • 将每个节点绘制填充到图层位图中(Paint SetupPaint重绘)
  • 图层作为纹理上传至gpu
  • 符合多个图层到页面上生成最终屏幕图像(Composite Layers 图层重组)

浏览器绘制DOM的过程是这样子的:

  • 获取 DOM 并将其分割为多个层(layer),将每个层独立地绘制进位图(bitmap)中
  • 将层作为纹理(texture)上传至 GPU,复合(composite)多个层来生成最终的屏幕图像
  • left/top/margin之类的属性会影响到元素在文档中的布局,当对布局(layout)进行动画时,该元素的布局改变可能会影响到其他元素在文档中的位置,就导致了所有被影响到的元素都要进行重新布局,浏览器需要为整个层进行重绘并重新上传到 GPU,造成了极大的性能开销。
  • transform 属于合成属性(composite property),对合成属性进行 transition/animation 动画将会创建一个合成层(composite layer),这使得被动画元素在一个独立的层中进行动画。
  • 通常情况下,浏览器会将一个层的内容先绘制进一个位图中,然后再作为纹理(texture)上传到 GPU,只要该层的内容不发生改变,就没必要进行重绘(repaint),浏览器会通过重新复合(recomposite)来形成一个新的帧。

chrome创建图层的条件

将频繁重绘回流的DOM元素单独作为一个独立图层,那么这个DOM元素的重绘和回流的影响只会在这个图层中

  • 3D或透视变换
  • CSS 属性使用加速视频解码的 <video> 元素
  • 拥有 3D (WebGL) 上下文或加速的
  • 2D 上下文的 <canvas> 元素
  • 复合插件(如 Flash)
  • 进行 opacity/transform 动画的元素拥有加速
  • CSS filters 的元素元素有一个包含复合层的后代节点(换句话说,就是一个元素拥有一个子元素,该子元素在自己的层里)
  • 元素有一个 z-index 较低且包含一个复合层的兄弟元素(换句话说就是该元素在复合层上面渲染)
总结:对布局属性进行动画,浏览器需要为每一帧进行重绘并上传到 GPU 中对合成属性进行动画,浏览器会为元素创建一个独立的复合层,当元素内容没有发生改变,该层就不会被重绘,浏览器会通过重新复合来创建动画帧

gif图

总结

  • 尽量避免使用触发回流重绘CSS属性
  • 重绘回流的影响范围限制在单独的图层(layers)之内
  • 图层合成过程中消耗很大页面性能,这时候需要平衡考虑重绘回流的性能消耗

实战优化点总结

  • translate替代top属性

    • top会触发layout,但translate不会
  • opacity代替visibility

    • opacity不会触发重绘也不会触发回流,只是改变图层alpha值,但是必须要将这个图片独立出一个图层
    • visibility会触发重绘
  • 不要一条一条的修改DOM的样式,预先定义好class,然后修改DOMclassName
  • 把DOM离线后修改,比如:先把DOMdisplay:none(有一次reflow),然后你修改100次,然后再把它显示出来
  • 不要把DOM节点的属性值放在一个循环里当成循环的变量

    • offsetHeightoffsetWidth每次都要刷新缓冲区,缓冲机制被破坏
    • 先用变量存储下来
  • 不要使用table布局,可能很小的一个小改动会造成整个table的重新布局

    • div只会影响后续样式的布局
  • 动画实现的速度的选择

    • 选择合适的动画速度
    • 根据performance量化性能优化
  • 对于动画新建图层

    • 启用gpu硬件加速(并行运算),gpu加速意味着数据需要从cpu走总线到gpu传输,需要考虑传输损耗.

      • transform:translateZ(0)
      • transform:translate3D(0)

浏览器存储

cookies

多种浏览器存储方式并存,如何选择?

  • 因为http请求无状态,所以需要cookie去维持客户端状态
  • cookie的生成方式:

    • http-->response header-->set-cookie
    • js中可以通过document.cookie可以读写cookie
    • cookie的使用用处:

      • 用于浏览器端和服务器端的交互(用户状态)
      • 客户端自身数据的存储
  • expire:过期时间
  • cookie的限制:

    • 作为浏览器存储,大小4kb左右
    • 需要设置过期时间 expire
  • 重要属性:httponly 不支持js读写(防止收到模拟请求攻击)
  • 不太作为存储方案而是用于维护客户关系
  • 优化点:cookie中在相关域名下面

    • cdn的流量损耗
    • 解决方案:cdn的域名和主站域名要分开

localStorage

localstorage

  • HTML5设计出来专门用于浏览器存储的
  • 大小为5M左右
  • 仅在客户端使用,不和服务端进行通信
  • 接口封装较好
  • 浏览器本地缓存方案

sessionstorage

  • 会话级别的浏览器存储
  • 大小为5M左右
  • 仅在客户端使用,不和服务器端进行通信
  • 接口封装较好
  • 对于表单信息的维护

indexedDB

  • IndexedDB是一种低级API,用于客户端存储大量结构化数据。该API使用索引来实现对该数据的高性能搜索。虽然Web
  • Storage对于存储叫少量的数据很管用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方案。

为应用创建离线版本

  • cdn域名不要带cookie
  • localstorage存库、图片

cookie种在主站下,二级域名也会携带这个域名,造成流量的浪费

Service Worker产生的意义

PWAService Worker

  • PWA(Progressive Web Apps)是一种Web App新模型,并不是具体指某一种前言的技术或者某一个单一的知识点,我们从英文缩写来看就能看出来,这是一个渐进式的Web App,是通过一系列新的Web特性,配合优秀的UI交互设计,逐步增强Web App的用户体验

PWAService worker

chrome 插件 lighthouse

检测是不是一个渐进式web app
  • 当前手机在弱网环境下能不能加载出来
  • 离线环境下能不能加载出来
特点
  • 可靠:没有网络的环境中也能提供基本的页面访问,而不会出现“未连接到互联网”的页面
  • 快速:针对网页渲染及网络数据访问有较好的优化
  • 融入(Engaging):应用可以被增加到手机桌面,并且和普通应用一样有全屏、推送等特性

service worker

service worker是一个脚本,浏览器独立于当前页面,将其在后台运行,为实现一些不依赖页面的或者用户交互的特性打开了一扇大门。在未来这些特性将包括消息推送,背景后台同步,geofencing(地理围栏定位),但他将推出的第一个首要的特性,就是拦截和处理网络请求的能力,包括以编程方式来管理被缓存的响应。

案例分析

Service Worker学习与实践

了解servie worker

chrome://serviceworker-internals/

chrome://inspect/#service-worker/

service worker网络拦截能力,存储Cache Storage,实现离线应用

indexedDB

callback && callback()写法
相当于 
if(callback){
   callback();
}

cookiesessionlocalStoragesessionStorage基本操作

indexedDB基本操作

object store:对象存储
本身就是结构化存储
 function openDB(name, callback) {
            //建立打开indexdb  indexedDB.open
            var request = window.indexedDB.open(name)
            request.onerror = function(e) {
                console.log('on indexedDB error')
            }
            request.onsuccess = function(e) {
                    myDB.db = e.target.result
                    callback && callback()
                }
                //from no database to first version,first version to second version...
            request.onupgradeneeded = function() {
                console.log('created')
                var store = request.result.createObjectStore('books', {
                    keyPath: 'isbn'
                })
                console.log(store)
                var titleIndex = store.createIndex('by_title', 'title', {
                    unique: true
                })
                var authorIndex = store.createIndex('by_author', 'author')

                store.put({
                    title: 'quarry memories',
                    author: 'fred',
                    isbn: 123456
                })
                store.put({
                    title: 'dafd memories',
                    author: 'frdfaded',
                    isbn: 12345
                })
                store.put({
                    title: 'dafd medafdadmories',
                    author: 'frdfdsafdafded',
                    isbn: 12345434
                })
            }
        }
        var myDB = {
            name: 'tesDB',
            version: '2.0.1',
            db: null
        }

        function addData(db, storeName) {

        }

        openDB(myDB.name, function() {
            // myDB.db = e.target.result
            // window.indexedDB.deleteDatabase(myDB.name)
        });

        //删除indexedDB

indexDB事务

transcationobject store建立关联关系来操作object store
建立之初可以配置

 var transcation = db.transcation('books', 'readwrite')
 var store = transcation.objectStore('books')

 var data =store.get(34314)
 store.delete(2334)
 store.add({
     title: 'dafd medafdadmories',
     author: 'frdfdsafdafded',
     isbn: 12345434
 })

Service Worker离线应用

serviceworker需要https协议

如何实现ServiceWorker与主页面之间的通信

lavas

缓存

期望大规模数据能自动化缓存,而不是手动进行缓存,需要浏览器端和服务器端协商一种缓存机制

  • Cache-Control所控制的缓存策略
  • last-modified 和 etage以及整个服务端浏览器端的缓存流程
  • 基于node实践以上缓存方式

httpheader

可缓存性

  • public:表明响应可以被任何对象(包括:发送请求的客户端,代理服务器,等等)缓存。
  • private:表明响应只能被单个用户缓存,不能作为共享缓存(即代理服务器不能缓存它)。
  • no-cache:强制所有缓存了该响应的缓存用户,在使用已存储的缓存数据前,发送带验证器的请求到原始服务器
  • only-if-cached:表明如果缓存存在,只使用缓存,无论原始服务器数据是否有更新

到期

  • max-age=<seconds>:设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位秒)。与 Expires相反,时间是相对于请求的时间。
  • s-maxage=<seconds>:覆盖max-age 或者 Expires 头,但是仅适用于共享缓存(比如各个代理),并且私有缓存中它被忽略。cdn缓存
  • max-stale[=<seconds>]

表明客户端愿意接收一个已经过期的资源。 可选的设置一个时间(单位秒),表示响应不能超过的过时时间。

  • min-fresh=<seconds>

表示客户端希望在指定的时间内获取最新的响应。

重新验证重新加载

重新验证
  • must-revalidate:缓存必须在使用之前验证旧资源的状态,并且不可使用过期资源。
  • proxy-revalidate:与must-revalidate作用相同,但它仅适用于共享缓存(例如代理),并被私有缓存忽略。
  • immutable :表示响应正文不会随时间而改变。资源(如果未过期)在服务器上不发生改变,因此客户端不应发送重新验证请求头(例如If-None-MatchIf-Modified-Since)来检查更新,即使用户显式地刷新页面。在Firefox中,immutable只能被用在 https:// transactions.
重新加载
  • no-store:缓存不应存储有关客户端请求或服务器响应的任何内容。
  • no-transform:不得对资源进行转换或转变。Content-Encoding, Content-Range, Content-TypeHTTP头不能由代理修改。例如,非透明代理可以对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。 no-transform指令不允许这样做。

Expires

  • 缓存过期时间,用来指定资源到期的时间,是服务器端的时间点
  • 告诉浏览器在过期时间前浏览器可以直接从浏览器缓存中存取数据,而无需再次请求
  • expireshttp1.0的时候的
  • http1.1时候,我们希望cache的管理统一进行,max-age优先级高于expires,当有max-age在的时候expires可能就会被忽略。
  • 如果没有设置cache-control时候会使用expires

Last-modifiedIf-Modified-since

  • 基于客户端和服务器端协商的缓存机制
  • last-modified --> response header

    if-modified-since --> request header
  • 需要与cache-control共同使用
last-modified有什么缺点?
  • 某些服务端不能获取精确的修改时间
  • 文件修改时间改了,但文件的内容却没有变

EtagIf-none-match

  • 文件内容的hash值
  • etag -->reponse header

    if-none-match -->request header
  • 需要与cache-control共同使用

好处:

  • if-modified-since更加准确
  • 优先级比etage更高

流程图


enter image description here

服务端性能优化

服务端用的node.js因为和前端用的同一种语言,可以利用服务端运算能力来进行相关的运算而减少前端的运算

  • vue渲染遇到的问题
  • vue-ssr和原理和引用

vue渲染面临的问题

    先加载vue.js
=>  执行vue.js代码
=>  生成html
以前没有前端框架时,
  • jsp/php在服务端进行数据的填充,发送给客户端就是已经填充好数据`的html
  • 使用jQuery异步加载数据
  • 使用ReactVue前端框架

    • 代价:需要框架全部加载完,才能把页面渲染出来,页面的首屏性能不好

多层次的优化方案

  • 构建层的模板编译。runtime,compile拆开,构建层做模板编译工作。webpack构建时候,统一,直接编译成runtime可以执行的代码
  • 数据无关的prerender的方式
  • 服务端渲染

查看原文

Cyyyiyi 关注了专栏 · 2019-02-26

每天一探

保持专注并持续发布(stay focused and keep shipping)

关注 61

Cyyyiyi 关注了专栏 · 2019-02-18

javascript魔法师

涉及我的学习心得与经验

关注 1314

Cyyyiyi 关注了用户 · 2019-01-16

以乐之名 @yilezhiming

不疯魔不成活。

关注 1344

Cyyyiyi 赞了文章 · 2019-01-16

前端进击的巨人(二):栈、堆、队列、内存空间

面试经常遇到的深浅拷贝,事件轮询,函数调用栈,闭包等容易出错的题目,究其原因,都是跟JavaScript基础知识不牢固有关,下层地基没打好,上层就是豆腐渣工程,新人小白,踏实踩土才是关键。

打地基第二篇:本篇我们将对JavaScript数据结构的知识点详解一二。

前端进击的巨人(二):栈、堆、队列、内存空间

JavaScript中有三种数据结构: 栈(stack) 、堆(heap)、 队列(queue)。

栈(stack)

栈的特点是"LIFO,即后进先出(Last in, first out)"。数据存储时只能从顶部逐个存入,取出时也需从顶部逐个取出。《前端进击的巨人(一):执行上下文与执行栈,变量对象》中解释执行栈时,举了一个乒乓球盒子的例子,来演示栈的存取方式,这里再举个栗子搭积木。

举个栗子:乒乓球盒子/搭建积木
栈栗子:乒乓球盒子

JavaScript中Array数组模拟栈:

var arr = [1, 2, 3, 4, 5];

arr.push(6); // 存入数据 arr -> [1, 2, 3, 4, 5, 6]
arr.pop();   // 取出数据 arr -> [1, 2, 3, 4, 5]

堆(heap)

堆的特点是"无序"key-value"键值对"存储方式。

举个栗子:书架存书
堆栗子:书架

我们想要在书架上找到想要的书,最直接的方式就是通过查找书名,书名就是我们的key。拿着这把key,就可以轻松检索到对应的书籍。

"堆的存取方式跟顺序没有关系,不局限出入口"

队列 (queue)

队列的特点是是"FIFO,即先进先出(First in, first out)"
数据存取时"从队尾插入,从队头取出"

"与栈的区别:栈的存入取出都在顶部一个出入口,而队列分两个,一个出口,一个入口"

举个栗子:排队取餐

队列栗子:排队取餐

JavaScript中Array数组模拟队列:

var arr = [1, 2, 3, 4, 5];

// 队尾in
arr.push(6);    // 存入 arr -> [1, 2, 3, 4, 5, 6]
// 队头out
arr.shift();    // 取出 arr -> [2, 3, 4, 5, 6]

栈、堆、队列在JavaScript中的应用

1. 代码运行方式(栈应用/函数调用栈)

《前端进击的巨人(一):执行上下文与执行栈,变量对象》详解了JavaScript运行时的函数调用过程,而其中执行栈(函数调用栈)就是用到栈的数据结构。

JavaScript中函数的执行过程,其实就是一个入栈出栈的过程:

  1. 当脚本要调用一个函数时,JS解析器把该函数推入栈中(push)并执行
  2. 当函数运行结束后,JS解析器将它从堆栈中推出(pop)

具体执行过程可翻阅上篇文章《前端进击的巨人(一):执行上下文与执行栈,变量对象》,这里不再赘述。

2. 内存存储(栈、堆)

JavaScript中变量类型有两种:

  1. 基础类型(Undefined, Null, Boolean, Number, String, Symbol)一共6种
  2. 引用类型(Object)

基础类型的值保存在栈中,这些类型的值有固定大小,"按值来访问"

引用类型的值保存在堆中,栈中存储的是引用类型的引用地址(地址指针),"按引用访问",引用类型的值没有固定大小,可扩展(一个对象我们可以添加多个属性)。

JS类型存储

3. 事件轮询(队列)

JavaScript中事件轮询(Event Loop)的执行机制,就是采用队列的存取方式,因事件轮询(Event Loop)也是JS基础中的一个比较难理解的知识点,后续另开一篇章再作详细探究。

深浅拷贝

将一个变量的值赋值给另一个变量,相当于在栈内存中创建了一个新的内存空间,然后从栈中复制值,存储到这个新空间中。对于基本类型,栈中存储的就是它自身的值,所以新内存空间存储的也是一个值。直接改变新变量的值,不会影响到旧变量的值,因为他们值存储的内存空间不同。

// 基本类型复制变量
var a = 10;
var b = a;
b = 20;

a // 10
b // 20

而对于引用类型来说,同样是复制栈中存储的值。但是栈存储的只是其引用地址,其具体的值存储在堆中。变量复制仅复制栈中存储的值,不会复制堆中存储的值,所以新变量在栈中的值是一个地址指针。

// 引用类型复制变量
var a = { age: 27 };
var b = a;
b.age = 29;

a.age == b.age; // 29

可见,变量复制赋值,都属于栈存储拷贝,因此深浅拷贝可以这样区分:

  • "浅拷贝:栈存储拷贝"
  • "深拷贝:栈堆存储拷贝"

深拷贝会同时开辟新的栈内存,堆内存空间。

// 利用JSON对象方法实现深拷贝
var a = { age: 27 };
var b = JSON.parse(JSON.stringify(a));
b.age = 29;

a.age // 27
b.age // 29

函数传参数是按值传递?按引用传递?

var person = {
 age: 27
};
function foo (person) {
  person.age = 29;
}
foo(person);
person.age // 29;

函数调用时,会对参数赋值。而参数传递过程其实同样是变量复制的过程,所以它是按值传递。var person = person,因为传递参数是对象时,变量复制仅复制的栈存储(浅拷贝),所以修改对象属性会造成外部变量对象的修改。

至此,当我们理清栈、堆数据结构,以及JS中数据类型存取方式。深浅拷贝问题也就通顺了。

内存空间管理

JavaScript执行过程中内存分配:

  1. 为变量对象分配需要的内存
  2. 在分配到的内存中进行读/写操作
  3. 不再使用时将其销毁,释放内存

内存管理不善,会出现内存泄露,造成浏览器内存占用过多,页面卡顿等问题。(后续性能优化篇章续讲)

垃圾回收机制

JavaScript中有自动垃圾回收机制,会通过标记清除的算法识别哪些变量对象不再使用,对其进行销毁。开发者也可在代码中手动设置变量值为null(a = null)进行标记清除,让其失去引用,以便下一次垃圾回收时进行有效回收。

局部环境中,函数执行完成后,函数局部环境声明的变量不再需要时,就会被垃圾回收销毁(理想的情况下,闭包会阻止这一过程)。

全局环境只有页面退出时才会出栈,解除变量引用。所以开发者应尽量避免在全局环境中创建全局变量,如需使用,也要在不需要时手动标记清除,将其内存释放掉。

垃圾回收算法除了"标记清除",还有一种"引用计数",不常用,仅作了解。


参考文档:

本文首发Github,期待Star!
https://github.com/ZengLingYong/blog

作者:以乐之名
本文原创,有不当的地方欢迎指出。转载请指明出处。
查看原文

赞 95 收藏 79 评论 7

Cyyyiyi 收藏了文章 · 2018-06-15

太原面经分享:如何在vue面试环节,展示你晋级阿里P6+的技术功底?

前言

一年一度紧张刺激的高考开始了,与此同时,我也没闲着,奔走在各大公司的前端面试环节,不断积累着经验,一路升级打怪。

最近两年,太原作为一个准二线城市,各大互联网公司的技术栈也在升级换代,假如你在太原面试前端岗位,而你的技术库里若只有jQuery和Bootstrap这两门冷兵器,不好意思,相信你很快就找不到像样儿的前端工作了。

因为现在太原的前端招聘市场,已然发生了变化,城市在不断地向二线靠拢,技术栈也在不断地向一线城市看齐(虽然薪资水平还在三线城市停留)。仅仅是我知道的一些公司项目里面,已经悄然的用上了vue、react、react native、webpack、小程序、node、hybrid app等等热门的前端技术/框架。

而且在前端面试环节,提及vue框架的次数已经不亚于当年刀耕火种时代但凡面试必问jQuery的架势。

所以,太原未来几年的技术发展趋势,必然是MVVM前后端分离的时代。

好的,以上分析了这么多,接下来就废话少说,直接进入今天的主题,如题说:如何在vue面试环节,展示你晋级阿里P6+的技术功底?

环环相扣的面试

提起vue面试环节,你不得不提vue的生态,它的全家桶,像什么vue-router、vuex、vue ssr等。但是看一个前端er对vue的研究深度,不能仅仅停留在表面,更要深入它的原理背后,探究它的源码。

图片描述

比较唬人的开场白,你不妨先照着这个结构图大概说一下,以便向面试官展示你对vue生态的全局观,然后再娓娓道来。

最起码的,先从简单的聊起,请说出vue.cli项目中src目录每个文件夹和文件的用途,这个你是必须也是一定要知道的。比如说,assets文件夹是放静态资源;components是放组件;router是定义路由相关的配置;view视图;app.vue是一个应用主组件;main.js是入口文件等等。不管业务开发能力如何,首先项目目录你得有个清晰的认知。

这仅仅是开胃菜,既然提到了vue的全家桶,就免不了要考察下vuex。咳咳咳,划重点来了!首先你得知道vuex是什么?怎么使用?哪种功能场景使用它?如果你不懂这个,面试官对你的印象分会直线下降。

你可以这么向面试官回答,vuex是vue生态系统中的状态管理。在main.js引入store,注入,新建一个目录store,….. export 等,常用的场景有:单页应用中,组件之间的状态,音乐播放、登录状态、加入购物车等等。

还有,既然已经是前后端分离了,那你总该知道什么是RESTful API,然后怎么使用?对吧,否则你该怎么面对项目中的前后端联调呢。首先,RESTful是一个api的标准,无状态请求。请求的路由地址是固定的,如果是tp5则先路由配置中把资源路由配置好。标准方法有:.get、.post、 .put、.delete。当你回答出这些问题之后,面试官对你的好感也在慢慢上升。

渐入佳境的博弈

当然,这些都问过之后,还有一个老掉牙的vue面试题,“请详细说下你对vue生命周期的理解”,这个问题很俗气,却又很经典。网上有很多关于vue生命周期的文章,但是数量太多,参差不齐。这里闰土给大家提供一个简短精干的回答,几句话便能解释清楚,而且条理清晰。

vue生命周期总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后。

创建前/后: 在beforeCreated阶段,vue实例的挂载元素el还没有。
载入前/后:在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。
更新前/后:当data变化时,会触发beforeUpdate和updated方法。
销毁前/后:在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。

说完life cycle,我们再来聊一个更加经典的问题,“谈谈你对vue的双向数据绑定原理的理解”。可能你在网上看过了很多款答案,或简单或详细,但很少有一款触及原理/源码深处的答案,请跟着闰土来看看这个问题该如何有深度的进行阐述?

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

具体步骤:

第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter。这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

当你和面试官聊到这个阶段,已经是渐入佳境,引人入胜,面试官可能会再抛出一个问题来探探你的上限,比如问“聊聊你对Vue.js的template编译的理解”。如果你能很好地回答这个问题,基本上vue面试环节,你就顺利通过了。

接下来该划重点了:

简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点)

详情步骤:

首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)

基本上到这儿,Vue面试环节就结束了。当然,你还可以挑战一下自己,例如:

  • event & v-model: 事件和v-model的实现原理
  • slot & keep-alive: 内置组件的实现原理
  • transition: 过渡的实现原理
  • vue-router: 官方路由的实现原理
  • vuex: 官方状态管理的实现原理

写在后面

想要对vue原理有更深入的理解,看源码是一条不错的道路。当然,源码谁都能看,看不看得懂就是另外一回事儿了,你必须有一定的技术功底,此路方可行得通。如果此时有高人指路,带你入门,帮你全方位解析,一定会事半功倍。正好滴滴前端大神黄轶在慕课网刚刚录制好一门实战课程《Vue.js源码全方位深入解析》,值得推荐。

假如你通过阅读vue源码,掌握了对Vue.js的实现原理,对vue生态系统有了充分的认识,那你会在vue面试环节游刃有余,达到晋级阿里P6+的技术功底,从而提高个人竞争力,面试加分更容易拿offer。在日常的工作当中,也能提高工作效率,开发技能如虎添翼。

总之一句话,内功修炼,个人技术能力提升,这才是我们前端工程师的终极目标。

文章预告:最新的面试分享文章将会第一时间更新在我的公众号:<闰土大叔>里面,欢迎关注~

图片描述

查看原文

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-01-22
个人主页被 123 人浏览