_明么

_明么 查看完整档案

北京编辑  |  填写毕业院校Mingme  |  前端工程师 编辑 zhangquanming.github.io 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

_明么 发布了文章 · 9月16日

模块化-CommonJs、AMD、CMD、ES6

一、模块化理解

1.什么是模块

  • 将复杂的程序依据一定的规则(规范)拆分成多个模块(文件)
  • 模块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信

2. 模块化的进化过程

全局function : 将不同的功能封装成不同的全局函数

`缺点`:虽说可以实现一定的封装效果,但是大量的全局函数,污染全局命名空间,容易引起命名冲突
function module1 () {
  //...
}
function module2 () {
  //...
}

命名空间 : 简单对象封装

`缺点`:减少了全局变量,解决命名冲突,但是外部可以直接修改模块内部的数据
let module = {
  data: 'aaa',
  func () {
    console.log(`${this.data}`)
  }
}
module.data = 'bbb' // 直接修改模块内部的数据
module.fn() // bbb

IIFE:(自执行函数)

`缺点`:实现数据私有, 外部只能通过暴露的方法操作,如果当前这个模块依赖另一个模块怎么办?
  // module.js文件()
(function (window) {
  let data = 'aaa'
  function func () {
    console.log(`${this.data}`)
  }
  //暴露接口
  window.module = { func }
})(window)
// index.html文件
<script type="text/javascript" data-original="module.js"></script>
<script type="text/javascript">
  module.func() // aaa
  console.log(module.data) // undefined 不能访问模块内部数据
  module.data = 'bbb' // 不能修改的模块内部数据
  module.func() // aaa
</script>

IIFE增强 : 引入依赖

  // module.js文件
(function (window, $) {
  let data = 'aaa'
  function func () {
    console.log(`${this.data}`)
  }
  function func2 () {
    $('body').css('background', 'red')
  }
  //暴露接口
  window.module = { func, func2 }
})(window, jQuery)
 // index.html文件
  <!-- 引入的js必须有一定顺序 -->
  <script type="text/javascript" data-original="jquery.js"></script>
  <script type="text/javascript" data-original="module.js"></script>
  <script type="text/javascript">
    module.func2()
  </script>

上面引入jQuery库,就把这个库当作参数传入,保证模块的独立性,使得模块之间的依赖关系变得明显。

3. 模块化的作用

通过上面的模块拆分,我们发现:

  • 减少了全局变量,有效的避免了命名污染
  • 更好的分离,按需加载
  • 提高了复用性,维护性

但是比较复杂的应用,模块比较多,难免需要引入多个<script>,这样又会出现其他问题:

  • 请求过多
  • 依赖关系模糊

模块化固然有多个好处,然而一个页面需要引入多个js文件,还得按一定的顺序引入,就可能出现因为引入顺序错误而导致整个项目出现严重问题。而这些问题可以通过模块化规范来解决。

模块化规范

CommonJs

CommonJS经node.js应运而生,根据CommonJS规范,每一个模块都是一个单独的作用域。也就是说,在该模块内部定义的变量,无法被其他模块读取。在服务器端,模块的加载是运行时同步加载的;在浏览器端,模块需要提前编译打包处理。

其核心思想就是一个单独文件就是一个模块,通过require方法来同步加载要依赖的模块,然后通过extportsmodule.exports来导出需要暴露的接口。

// module1.js
var data = 5;
var doSomething = function (value) {
  return value + data;
};
// 暴露的接口
module.exports.data = data;
module.exports.doSomething = doSomething;

上面代码通过 module.exports 输出变量 data 和函数 doSomething

var example = require('./module1.js');
console.log(example.data); // 5
console.log(example.doSomething(1)); // 6

require命令用于加载模块文件。require命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。

优点:服务器端模块复用性,NPM中模块包多,有将近20万个。

缺点:加载模块是同步的,只有加载完成后才能执行后面的操作,也就是说现加载现用,不仅加载速度慢,而且还会导致性能、可用性、调试和跨域访问等问题。由于Node.js主要用于服务器编程,加载的模块文件一般都存在本地硬盘,加载起来比较快,不用考虑异步加载的方式,因此,CommonJS规范比较适用。然而,这并不适合在浏览器环境,同步意味着阻塞加载,浏览器资源是异步加载的,鉴于浏览器的情况,为了解决上述同步加载问题,实现异步加载依赖模块,因此有了AMD、CMD解决方案。

AMD (Asynchronous Module Definition)

AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。AMD规范是异步加载模块,允许指定回调函数。对于依赖的模块,AMD 推崇提前执行(依赖前置),不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。

其核心接口是:define(id?, dependencies?, factory) ,它要在声明模块的时候指定所有的依赖 dependencies ,并且还要当做形参传到factory 中,对于依赖的模块提前执行,依赖前置。

// a.js (定义没有依赖的模块)
define(function () {
    let data = 'aaa'
    function doSomething () {
        console.log(data)
    }
    return { doSomething } // 暴露接口
})
// b.js (定义有依赖的模块)
define(['c'], function (c) {
    let data = 'bbb'
    function doSomething () {
        console.log(data + c.getData())
    }
    return { doSomething } // 暴露接口
})
// c.js (此模块为 b.js 依赖)
define(function () {
    let data = 'ccc'
    function getData () {
        return data
    }
    return { getData } // 暴露接口
})
// 引入依赖的模块
require(['./a', './b'], function (a, b) { // 依赖必须一开始就写好
  a.doSomething()
  // ...
  b.doSomething()
  // ...
})
<body>
    <!-- 引入require.js并指定js主文件的入口 -->
    <script data-main="./index" data-original="https://cdn.staticfile.org/require.js/2.3.6/require.min.js"></script>
    <script>
        setTimeout(() => {
            console.log('setTimeout')
        }, 0)
    </script>
</body>

require()函数在加载依赖的函数的时候是异步加载的,这也是我在这里放了个setTimeout证实一下,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题。AMD的异步加载解决了阻塞加载、性能问题,模块之间的依赖关系也能清楚的显示出来。

CMD (Common Module Definition)

CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。CMD规范和 AMD 很相似,解决同样问题,只是运行机制不同。对于依赖的模块,CMD 推崇延迟执行(依赖就近)

// a.js(定义没有依赖的模块)
define(function (require, exports, module) {
    let data = 'aaa'
    function doSomething () {
        console.log(data)
    }
    exports.doSomething = doSomething // 暴露接口
})
// b.js (定义有依赖的模块)
define(function (require, exports, module) {
    let data = 'bbb'
    function doSomething () {
        var c = require('./c') // 依赖可以就近书写  
        console.log(data + c.data)
    }
    exports.doSomething = doSomething // 暴露接口
})
// c.js (此模块为 b.js 依赖)
define(function (require, exports, module) {
    let data = 'ccc'
    exports.data = data // 暴露模块
})
// 引入依赖的模块
define(function (require, exports, module) {
  //引入依赖模块(异步)
  require.async('./a', function (a) {
    a.doSomething()
    console.log('a是异步的')
  })
  //引入依赖模块(同步)
  var b = require('./b') // 依赖可以就近书写  
  b.doSomething()
  // ... 
  var c = require('./c') // 依赖可以就近书写  
  console.log(c.data)
  // ...
})
<body>
    <script data-original="https://cdn.staticfile.org/seajs/3.0.3/sea.js"></script>
    <script>
        setTimeout(() => {
            console.log('setTimeout')
        }, 0)
        seajs.use('./index')
    </script>
</body>

ES6

ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。

export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。为了提供方便,不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

// a.js (定义模块)
var data = 'aaa'
var doSomething = function () {
  console.log('log: ' + data)
};
export { data, doSomething }

// 引用模块 
import { data, doSomething } from './a'

这里在语法不做过多介绍,主要说一说 ES6 模块CommonJS 模块 的差异。

它们有两个重大差异:

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

我们来看看第一个差异,CommonJS模块的加载机制:

// module1.js
var data = 5;
var doSomething = function () {
  data++;
};
// 暴露的接口
module.exports.data = data;
module.exports.doSomething = doSomething;
var example = require('./module1.js');
console.log(example.data); // 5
example.doSomething(); 
console.log(example.data); // 5

ES6 模块的加载机制:

// module1.js
let data = 5;
function doSomething() {
  data++;
}
export { data, doSomething }
import { data, doSomething } from './module1';
console.log(data); // 5
doSomething();
console.log(data); // 6

ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

总结

  • CommonJS 模块输出的是一个值的拷贝,CommonJS 模块是运行时加载,CommonJS规范主要用于服务端编程,加载模块是同步的,同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD、CMD解决方案。
  • AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。AMD 的 API 默认是一个当多个用,对于依赖的模块,AMD 推崇提前执行(依赖前置)
  • CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。CMD 的 API 严格区分,推崇职责单一加载模块是异步的,CMD 推崇延迟执行(依赖就近)。
  • ES6 模块输出的是值的引用,ES6 模块是编译时输出接口,ES6 在语言标准的层面上,实现了模块功能简单,完全可以成为浏览器和服务器通用的模块解决方案。
查看原文

赞 1 收藏 1 评论 0

_明么 发布了文章 · 8月13日

CSS-圣杯布局、双飞翼布

三栏布局中,经典中的经典应该就是圣杯布局、双飞翼布局没跑了。

先熟悉一下圣杯布局、双飞翼布局中的特点:

  • 两侧定宽,中间自适应
  • 主要内容优先渲染

圣杯布局

<div class="container">
    <div class="main">main</div>
    <div class="left">left</div>
    <div class="right">right</div>
</div>

(1) 首先设置好.main.left.right的宽度并浮动,为左右两列预留出空间。

.container {
  padding-left: 200px;  /* 预留左侧空间,为.left宽度*/
  padding-right: 300px; /* 预留右侧空间,为.right宽度*/
}
.main {
  float: left;
  width: 100%;
  height: 300px;
  background: #67c23a;
}
.left {
  float: left;
  width: 200px;
  height: 300px;
  background: #e6a23c;
}
.right {
  float: left;
  width: 300px;
  height: 300px;
  background: #f56c6c;
}


(2) 通过负marginposition<div class="left">left</div>移动到左侧预留位置。

.left {
  float: left;
  margin-left: -100%;   /* 移动到左侧,100%是一个父元素宽度,这里也就是.container的宽度 */
  position: relative;   /* 因为.container设置了padding */
  right: 200px;         /* 所以需要再向左移动自身宽度,left: -200px;也是可以的 */
  width: 200px;
  height: 300px;
  background: #e6a23c;
}


(3) 通过负margin<div class="right">right</div>移动到右侧预留位置。

.right {
  float: left;
  margin-right: -300px; /* 移动到右侧,自身宽度*/
  width: 300px;
  height: 300px;
  background: #f56c6c;
}


完整代码:

.container {
  padding-left: 200px;  /* 预留左侧空间,为.left宽度*/
  padding-right: 300px; /* 预留左侧空间,为.right宽度*/
}
.main {
  float: left;
  width: 100%;
  height: 300px;
  background: #67c23a;
}
.left {
  float: left;
  margin-left: -100%;   /* 移动到左侧,100%是一个父元素宽度,这里也就是.container的宽度 */
  position: relative;   /* 因为.container设置了padding*/
  right: 200px;         /* 所以需要再向左移动自身宽度,left: -200px;也是可以的 */
  width: 200px;
  height: 300px;
  background: #e6a23c;
}
.right {
  float: left;
  margin-right: -300px; /* 移动到右侧,自身宽度*/
  width: 300px;
  height: 300px;
  background: #f56c6c;
}

双飞翼布局

<div class="main-wrap">
    <div class="main">main</div>
</div>
<div class="left">left</div>
<div class="right">right</div>

(1) 首先设置好.wrap.main-wrap.left.right的宽度并浮动,为左右两列预留出空间。

.main-wrap {
  float: left;
  width: 100%;  /* 这个必须设置,不然浮动起来,没宽度 */
}
.main {
  margin-left: 200px;   /* 预留左侧空间,为.left宽度 */
  margin-right: 300px;  /* 预留左侧空间,为.right宽度 */
  height: 300px;
  background: #67c23a;
}
.left {
  float: left;
  width: 200px;
  height: 300px;
  background: #e6a23c;
}
.right {
  float: left;
  width: 300px;
  height: 300px;
  background: #f56c6c;
}


(2) 通过负margin<div class="left">left</div>移动到左侧预留位置。

.left {
  float: left;
  margin-left: -100%;   /* 移动到左侧,100%是一个父元素宽度,这里也就是body的宽度*/
  width: 200px;
  height: 300px;
  background: #e6a23c;
}


(3) 通过负margin<div class="right">right</div>移动到右侧预留位置。

.right {
  float: left;
  margin-left: -300px;  /* 移动到右侧,自身宽度*/
  width: 300px;
  height: 300px;
  background: #f56c6c;
}


完整代码:

.main-wrap {
  float: left;
  width: 100%;
}
.main {
  margin-left: 200px;   /* 预留左侧空间,为.left宽度*/
  margin-right: 300px;  /* 预留左侧空间,为.right宽度*/
  height: 300px;
  background: #67c23a;
}
.left {
  float: left;
  margin-left: -100%;   /* 移动到左侧,100%是一个父元素宽度,这里也就是body的宽度*/
  width: 200px;
  height: 300px;
  background: #e6a23c;
}
.right {
  float: left;
  margin-left: -300px;  /* 移动到右侧,自身宽度*/
  width: 300px;
  height: 300px;
  background: #f56c6c;
}
查看原文

赞 1 收藏 1 评论 0

_明么 发布了文章 · 8月11日

CSS-两栏布局

两栏布局(一侧固定宽度,一侧自适应),在工作中应该是经常使用到,可以说是前端基础了。然而在一次面试中,面试官:在纸上写出你能想到的两栏布局所有方式。我心想:这还不简单。仔细想了想...猝!

话不多说,上菜!

  • absolute + margin 方式

<div class="container">
    <div class="sidebar">固定</div>
    <div class="main">自适应</div>
</div>
.container {
  position: relative;
}
.sidebar {
  position: absolute;
  top: 0;
  left: 0;
  height: 300px;
  width: 200px;
  background: #67c23a;
}
.main {
  margin-left: 200px;
  height: 300px;
  background: #e6a23c;
}


修改 css 就可实现 位置调换,如下:

.sidebar {
  right: 0;
  /* ... */
}
.main {
  margin-right: 200px;
  /* ... */
}


优点: 交换<div class="sidebar">固定</div><div class="main">自适应</div>顺序 ,实现主要内容优先加载渲染。

缺点:absolute 定位,脱离文档流,当 sidebar 列的高度,超过 main 列的高度,会遮住下面的元素。需要给父盒子设置 overflow 属性。

  • float + margin 方式

<div class="container">
    <div class="sidebar">固定</div>
    <div class="main">自适应</div>
</div>
.sidebar {
  float: left;
  top: 0;
  right: 0;
  height: 300px;
  width: 200px;
  background: #67c23a;
}
.main {
  margin-left: 200px;
  height: 300px;
  background: #e6a23c;
}


也支持位置调换:

.sidebar {
  float: right;
  /* ... */
}
.main {
  margin-right: 200px;
  /* ... */
}


缺点:不能实现主要内容优先加载渲染。

  • float + 负margin 方式

<div class="wrap">
    <div class="main">自适应</div>
</div>
<div class="sidebar">固定</div>
.wrap {
  float: left;
  width: 100%;
}
.main {
  margin-left: 200px;
  height: 300px;
  background: #e6a23c;
}
.sidebar {
  float: left;
  margin-left: -100%;
  height: 300px;
  width: 200px;
  background: #67c23a;
}


位置调换:

.main {
  margin-right: 200px;
  /* ... */
}
.sidebar {
  float: right;
  margin-left: -200px;
  /* ... */
}

  • flex 方式

<div class="container">
    <div class="main">自适应</div>
    <div class="sidebar">固定</div>
</div>
.container {
  display: flex;
}
.main {
  flex: 1;
  height: 300px;
  background: #e6a23c;
}
.sidebar {
  flex: none;
  /* height: 300px; */
  width: 200px;
  background: #67c23a;
}

这里有一点需要注意:.sidebar没有设置高度,会和.container保持一样的高度。.container的高度是被.main撑开的,也就是和.main高度一样。

位置调换:

.container {
  display: flex;
  flex-direction: row-reverse;
}

  • grid 方式

<div class="container">
    <div class="main">自适应</div>
    <div class="sidebar">固定</div>
</div>
.container {
  display: grid;
  grid-template-columns: auto 200px;
  grid-template-rows: 300px;
}
.main {
  background: #e6a23c;
}
.sidebar {
  background: #67c23a;
}

这里.main.sidebar高度不单独设置的话,也是同样的高度。

位置调换:

.container {
  display: grid;
  grid-template-columns: 200px auto;
  grid-template-rows: 300px;
  grid-template-areas: 'a b';
}
.main {
  grid-area: b;
  background: #e6a23c;
}
.sidebar {
  grid-area: a;
  background: #67c23a;
}

  • float + BFC 方式

<div class="container">
    <div class="sidebar">固定</div>
    <div class="main">自适应</div>
</div>
.sidebar {
  float: left;
  width: 200px;
  height: 300px;
  background: #67c23a;
}
.main {
  overflow: hidden;
  height: 300px;
  background: #e6a23c;
}


位置调换:

.sidebar {
  float: right;
 /* ... */
}


这里让.main成为BFC主要是消除.sidebarfloat带来的影响,只要能让.main成为BFC就行。

此外留给大家一个思考题,还有没有其他方式呢?

查看原文

赞 0 收藏 0 评论 0

_明么 收藏了文章 · 1月3日

12道vue高频原理面试题,你能答出几道?

vue-home.jpg

前言

本文分享 12 道 vue 高频原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 Vue 掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握 Vue ❤️

1. Vue 响应式原理

vue.png

核心实现类:

Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。

Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
  3. render()的过程,触发 render watcher 依赖收集
  4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。

派发更新

  1. 组件中对响应的数据进行了修改,触发 setter 的逻辑
  2. 调用 dep.notify()
  3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

原理

当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

一句话总结:

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

2. computed 的实现原理

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

3. computed 和 watch 有什么区别及运用场景?

区别

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用  watch  选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

4. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

5. Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数  a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

6. 谈一谈 nextTick 的原理

JS 运行机制

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

event-loop.png

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

在浏览器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

7. vue 是如何对数组方法进行变异的 ?

我们先来看看源码

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

8. Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue {
  constructor() {
    //  事件通道调度中心
    this._events = Object.create(null);
  }
  $on(event, fn) {
    if (Array.isArray(event)) {
      event.map(item => {
        this.$on(item, fn);
      });
    } else {
      (this._events[event] || (this._events[event] = [])).push(fn);
    }
    return this;
  }
  $once(event, fn) {
    function on() {
      this.$off(event, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.$on(event, on);
    return this;
  }
  $off(event, fn) {
    if (!arguments.length) {
      this._events = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      event.map(item => {
        this.$off(item, fn);
      });
      return this;
    }
    const cbs = this._events[event];
    if (!cbs) {
      return this;
    }
    if (!fn) {
      this._events[event] = null;
      return this;
    }
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  $emit(event) {
    let cbs = this._events[event];
    if (cbs) {
      const args = [].slice.call(arguments, 1);
      cbs.map(item => {
        args ? item.apply(this, args) : item.call(this);
      });
    }
    return this;
  }
}

10. 说说 Vue 的渲染过程

render.png

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
  • generate 函数生成 render 函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

11. 聊聊 keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },

  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },

  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }

      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名
  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
lru.png

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

12. vm.$set()实现原理是什么?

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

export function set(target: Array<any> | Object, key: any, val: any): any {
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }
  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值
  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

后记

如果你和我一样喜欢前端,也爱动手折腾,欢迎关注我一起玩耍啊~ ❤️

博客

我的博客

公众号

前端时刻

公众号

查看原文

_明么 赞了文章 · 1月3日

12道vue高频原理面试题,你能答出几道?

vue-home.jpg

前言

本文分享 12 道 vue 高频原理面试题,覆盖了 vue 核心实现原理,其实一个框架的实现原理一篇文章是不可能说完的,希望通过这 12 道问题,让读者对自己的 Vue 掌握程度有一定的认识(B 数),从而弥补自己的不足,更好的掌握 Vue ❤️

1. Vue 响应式原理

vue.png

核心实现类:

Observer : 它的作用是给对象的属性添加 getter 和 setter,用于依赖收集和派发更新

Dep : 用于收集当前响应式对象的依赖关系,每个响应式对象包括子对象都拥有一个 Dep 实例(里面 subs 是 Watcher 实例数组),当数据有变更时,会通过 dep.notify()通知各个 watcher。

Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

Watcher 和 Dep 的关系

watcher 中实例化了 dep 并向 dep.subs 中添加了订阅者,dep 通过 notify 遍历了 dep.subs 通知每个 watcher 更新。

依赖收集

  1. initState 时,对 computed 属性初始化时,触发 computed watcher 依赖收集
  2. initState 时,对侦听属性初始化时,触发 user watcher 依赖收集
  3. render()的过程,触发 render watcher 依赖收集
  4. re-render 时,vm.render()再次执行,会移除所有 subs 中的 watcer 的订阅,重新赋值。

派发更新

  1. 组件中对响应的数据进行了修改,触发 setter 的逻辑
  2. 调用 dep.notify()
  3. 遍历所有的 subs(Watcher 实例),调用每一个 watcher 的 update 方法。

原理

当创建 Vue 实例时,vue 会遍历 data 选项的属性,利用 Object.defineProperty 为属性添加 getter 和 setter 对数据的读取进行劫持(getter 用来依赖收集,setter 用来派发更新),并且在内部追踪依赖,在属性被访问和修改时通知变化。

每个组件实例会有相应的 watcher 实例,会在组件渲染的过程中记录依赖的所有数据属性(进行依赖收集,还有 computed watcher,user watcher 实例),之后依赖项被改动时,setter 方法会通知依赖与此 data 的 watcher 实例重新计算(派发更新),从而使它关联的组件重新渲染。

一句话总结:

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

2. computed 的实现原理

computed 本质是一个惰性求值的观察者。

computed 内部实现了一个惰性的 watcher,也就是 computed watcher,computed watcher 不会立刻求值,同时持有一个 dep 实例。

其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,

computed watcher 通过 this.dep.subs.length 判断有没有订阅者,

有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)

没有的话,仅仅把 this.dirty = true。 (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

3. computed 和 watch 有什么区别及运用场景?

区别

computed 计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。

watch 侦听器 : 更多的是「观察」的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。

运用场景

运用场景:

当我们需要进行数值计算,并且依赖于其它数据时,应该使用 computed,因为可以利用 computed 的缓存特性,避免每次获取值时,都要重新计算。

当我们需要在数据变化时执行异步或开销较大的操作时,应该使用 watch,使用  watch  选项允许我们执行异步操作 ( 访问一个 API ),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。

4. 为什么在 Vue3.0 采用了 Proxy,抛弃了 Object.defineProperty?

Object.defineProperty 本身有一定的监控到数组下标变化的能力,但是在 Vue 中,从性能/体验的性价比考虑,尤大大就弃用了这个特性(Vue 为什么不能检测数组变动 )。为了解决这个问题,经过 vue 内部处理后可以使用以下几种方法来监听数组
push();
pop();
shift();
unshift();
splice();
sort();
reverse();

由于只针对了以上 7 种方法进行了 hack 处理,所以其他数组的属性也是检测不到的,还是具有一定的局限性。

Object.defineProperty 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。Vue 2.x 里,是通过 递归 + 遍历 data 对象来实现对数据的监控的,如果属性值也是对象那么需要深度遍历,显然如果能劫持一个完整的对象是才是更好的选择。

Proxy 可以劫持整个对象,并返回一个新的对象。Proxy 不仅可以代理对象,还可以代理数组。还可以代理动态增加的属性。

5. Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速 (对于简单列表页渲染来说 diff 节点也更快,但会产生一些隐藏的副作用,比如可能不会产生过渡效果,或者在某些节点有绑定数据(表单)状态,会出现状态错位。)

diff 算法的过程中,先会进行新旧节点的首尾交叉对比,当无法匹配的时候会用新节点的 key 与旧节点进行比对,从而找到相应旧节点.

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数  a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

6. 谈一谈 nextTick 的原理

JS 运行机制

JS 执行是单线程的,它是基于事件循环的。事件循环大致分为以下几个步骤:

  1. 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
  2. 主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
  3. 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
  4. 主线程不断重复上面的第三步。

event-loop.png

主线程的执行过程就是一个 tick,而所有的异步结果都是通过 “任务队列” 来调度。 消息队列中存放的是一个个的任务(task)。 规范中规定 task 分为两大类,分别是 macro task 和 micro task,并且每个 macro task 结束后,都要清空所有的 micro task。

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}

在浏览器环境中 :

常见的 macro task 有 setTimeout、MessageChannel、postMessage、setImmediate

常见的 micro task 有 MutationObsever 和 Promise.then

异步更新队列

可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。

如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。

然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。

Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。

在 vue2.5 的源码中,macrotask 降级的方案依次是:setImmediate、MessageChannel、setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

7. vue 是如何对数组方法进行变异的 ?

我们先来看看源码

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

8. Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue {
  constructor() {
    //  事件通道调度中心
    this._events = Object.create(null);
  }
  $on(event, fn) {
    if (Array.isArray(event)) {
      event.map(item => {
        this.$on(item, fn);
      });
    } else {
      (this._events[event] || (this._events[event] = [])).push(fn);
    }
    return this;
  }
  $once(event, fn) {
    function on() {
      this.$off(event, on);
      fn.apply(this, arguments);
    }
    on.fn = fn;
    this.$on(event, on);
    return this;
  }
  $off(event, fn) {
    if (!arguments.length) {
      this._events = Object.create(null);
      return this;
    }
    if (Array.isArray(event)) {
      event.map(item => {
        this.$off(item, fn);
      });
      return this;
    }
    const cbs = this._events[event];
    if (!cbs) {
      return this;
    }
    if (!fn) {
      this._events[event] = null;
      return this;
    }
    let cb;
    let i = cbs.length;
    while (i--) {
      cb = cbs[i];
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1);
        break;
      }
    }
    return this;
  }
  $emit(event) {
    let cbs = this._events[event];
    if (cbs) {
      const args = [].slice.call(arguments, 1);
      cbs.map(item => {
        args ? item.apply(this, args) : item.call(this);
      });
    }
    return this;
  }
}

10. 说说 Vue 的渲染过程

render.png

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
  • generate 函数生成 render 函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

11. 聊聊 keep-alive 的实现原理和缓存策略

export default {
  name: "keep-alive",
  abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中
  props: {
    include: patternTypes, // 被缓存组件
    exclude: patternTypes, // 不被缓存组件
    max: [String, Number] // 指定缓存大小
  },

  created() {
    this.cache = Object.create(null); // 缓存
    this.keys = []; // 缓存的VNode的键
  },

  destroyed() {
    for (const key in this.cache) {
      // 删除所有缓存
      pruneCacheEntry(this.cache, key, this.keys);
    }
  },

  mounted() {
    // 监听缓存/不缓存组件
    this.$watch("include", val => {
      pruneCache(this, name => matches(val, name));
    });
    this.$watch("exclude", val => {
      pruneCache(this, name => !matches(val, name));
    });
  },

  render() {
    // 获取第一个子元素的 vnode
    const slot = this.$slots.default;
    const vnode: VNode = getFirstComponentChild(slot);
    const componentOptions: ?VNodeComponentOptions =
      vnode && vnode.componentOptions;
    if (componentOptions) {
      // name不在inlcude中或者在exlude中 直接返回vnode
      // check pattern
      const name: ?string = getComponentName(componentOptions);
      const { include, exclude } = this;
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode;
      }

      const { cache, keys } = this;
      // 获取键,优先获取组件的name字段,否则是组件的tag
      const key: ?string =
        vnode.key == null
          ? // same constructor may get registered as different local components
            // so cid alone is not enough (#3269)
            componentOptions.Ctor.cid +
            (componentOptions.tag ? `::${componentOptions.tag}` : "")
          : vnode.key;
      // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个
      if (cache[key]) {
        vnode.componentInstance = cache[key].componentInstance;
        // make current key freshest
        remove(keys, key);
        keys.push(key);
      }
      // 不命中缓存,把 vnode 设置进缓存
      else {
        cache[key] = vnode;
        keys.push(key);
        // prune oldest entry
        // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode);
        }
      }
      // keepAlive标记位
      vnode.data.keepAlive = true;
    }
    return vnode || (slot && slot[0]);
  }
};

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名
  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
lru.png

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

12. vm.$set()实现原理是什么?

受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。

由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。

那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?

export function set(target: Array<any> | Object, key: any, val: any): any {
  // target 为数组
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    // 修改数组的长度, 避免索引>数组长度导致splice()执行有误
    target.length = Math.max(target.length, key);
    // 利用数组的splice变异方法触发响应式
    target.splice(key, 1, val);
    return val;
  }
  // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  // 以上都不成立, 即开始给target创建一个全新的属性
  // 获取Observer实例
  const ob = (target: any).__ob__;
  // target 本身就不是响应式数据, 直接赋值
  if (!ob) {
    target[key] = val;
    return val;
  }
  // 进行响应式处理
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val;
}
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值
  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

后记

如果你和我一样喜欢前端,也爱动手折腾,欢迎关注我一起玩耍啊~ ❤️

博客

我的博客

公众号

前端时刻

公众号

查看原文

赞 93 收藏 71 评论 4

_明么 收藏了文章 · 1月3日

年前走一波【vue博文】集合,收藏就对了,2020年您好

新的一年,新的起步

查看github最新的Vue weekly;请::点击::
【🔥Vue.js资讯📚】目前web前端开发非常火爆的框架;定时更新,欢迎 Star 一下。

屏幕快照 2019-12-26 22.34.55.png

忆长安·十二月
[唐]谢良辅
忆长安,腊月时,温泉彩仗新移。
瑞气遥迎凤辇,日光先暖龙池。
取酒虾蟆陵下,家家守岁传卮。

Hello, 2020; please be nice to me!

再见2019你好2020
12-01.png12-02.jpeg

最新资讯

继续╰(°▽°)╯

类Hooks(^o^)/~

技巧_(:з」∠)_

类学习笔记(o´ω`o)ノ

英文外资(^o^)/~

更多推荐

查看github最新的Vue weekly;请::点击::
【🔥Vue.js资讯📚】目前web前端开发非常火爆的框架;定时更新,欢迎 Star 一下。

Thanks♪(・ω・)ノ

屏幕快照 2019-12-26 22.39.32.png

查看原文

_明么 收藏了文章 · 2019-11-12

灵活运用CSS开发技巧

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

系列

前言

何为技巧,意指表现在文学、工艺、体育等方面的巧妙技能。代码作为一门现代高级工艺,推动着人类科学技术的发展,同时犹如文字一样承托着人类文化的进步。

每写好一篇文章,都会使用大量的写作技巧。烘托、渲染、悬念、铺垫、照应、伏笔、联想、想象、抑扬结合、点面结合、动静结合、叙议结合、情景交融、首尾呼应、衬托对比、白描细描、比喻象征、借古讽今、卒章显志、承上启下、开门见山、动静相衬、虚实相生、实写虚写、托物寓意、咏物抒情等,这些应该都是我们从小到大写文章而接触到的写作技巧。

作为程序猿的我们,写代码同样也需要大量的写作技巧。一份良好的代码能让人耳目一新,让人容易理解,让人舒服自然,同时也让自己成就感满满(哈哈,这个才是重点)。因此,我整理下三年来自己使用到的一些CSS开发技巧,希望能让你写出耳目一新、容易理解、舒服自然的代码。

目录

既然写文章有这么多的写作技巧,那么我也需要对CSS开发技巧整理一下,起个易记的名字。

  • Layout Skill:布局技巧
  • Behavior Skill:行为技巧
  • Color Skill:色彩技巧
  • Figure Skill:图形技巧
  • Component Skill:组件技巧

备注

  • 代码只作演示用途,不会详细说明语法
  • 部分技巧示例代码过长,使用CodePen进行保存,点击在线演示即可查看
  • 兼容项点击链接即可查看当前属性的浏览器兼容数据,自行根据项目兼容需求考虑是否使用
  • 以下代码全部基于CSS进行书写,没有任何JS代码,没有特殊说明的情况下所有属性和方法都是CSS类型
  • 一部分技巧是自己探讨出来的,另一部分技巧是参考各位前端大神们的,都是一个互相学习的过程,大家一起进步

Layout Skill

使用vw定制rem自适应布局
  • 要点:移动端使用rem布局需要通过JS设置不同屏幕宽高比的font-size,结合vw单位和calc()可脱离JS的控制
  • 场景:rem页面布局(不兼容低版本移动端系统)
  • 兼容:vwcalc())
/* 基于UI width=750px DPR=2的页面 */
html {
    font-size: calc(100vw / 7.5);
}
使用:nth-child()选择指定元素
  • 要点:通过:nth-child()筛选指定的元素设置样式
  • 场景:表格着色边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::nth-child())
  • 代码:在线演示

在线演示

使用writing-mode排版竖文
  • 要点:通过writing-mode调整文本排版方向
  • 场景:竖行文字文言文诗词
  • 兼容:writing-mode
  • 代码:在线演示

在线演示

使用text-align-last对齐两端文本
  • 要点:通过text-align-last:justify设置文本两端对齐
  • 场景:未知字数中文对齐
  • 兼容:text-align-last
  • 代码:在线演示

在线演示

使用:not()去除无用属性
  • 要点:通过:not()排除指定元素不使用设置样式
  • 场景:符号分割文字边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::not())
  • 代码:在线演示

在线演示

使用object-fit规定图像尺寸
  • 要点:通过object-fit使图像脱离background-size的约束,使用<img>来标记图像背景尺寸
  • 场景:图片尺寸自适应
  • 兼容:object-fit
  • 代码:在线演示

在线演示

使用overflow-x排版横向列表
  • 要点:通过flexboxinline-block的形式横向排列元素,对父元素设置overflow-x:auto横向滚动查看
  • 场景:横向滚动列表元素过多但位置有限的导航栏
  • 兼容:overflow-x
  • 代码:在线演示

在线演示

使用text-overflow控制文本溢出

在线演示

使用transform描绘1px边框
  • 要点:分辨率比较低的屏幕下显示1px的边框会显得模糊,通过::before::aftertransform模拟细腻的1px边框
  • 场景:容器1px边框
  • 兼容:transform
  • 代码:在线演示

在线演示

使用transform翻转内容
  • 要点:通过transform:scale3d()对内容进行翻转(水平翻转、垂直翻转、倒序翻转)
  • 场景:内容翻转
  • 兼容:transform
  • 代码:在线演示

在线演示

使用letter-spacing排版倒序文本
  • 要点:通过letter-spacing设置负值字体间距将文本倒序
  • 场景:文言文诗词
  • 兼容:letter-spacing
  • 代码:在线演示

在线演示

使用margin-left排版左重右轻列表
  • 要点:使用flexbox横向布局时,最后一个元素通过margin-left:auto实现向右对齐
  • 场景:右侧带图标的导航栏
  • 兼容:margin
  • 代码:在线演示

在线演示

Behavior Skill

使用overflow-scrolling支持弹性滚动
  • 要点:iOS页面非body元素的滚动操作会非常卡(Android不会出现此情况),通过overflow-scrolling:touch调用Safari原生滚动来支持弹性滚动,增加页面滚动的流畅度
  • 场景:iOS页面滚动
  • 兼容:iOS自带-webkit-overflow-scrolling
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}
使用transform启动GPU硬件加速
  • 要点:有时执行动画可能会导致页面卡顿,可在特定元素中使用硬件加速来避免这个问题
  • 场景:动画元素(绝对定位、同级中超过6个以上使用动画)
  • 兼容:transform
.elem {
    transform: translate3d(0, 0, 0); /* translateZ(0)亦可 */
}
使用attr()抓取data-*
  • 要点:在标签上自定义属性data-*,通过attr()获取其内容赋值到content
  • 场景:提示框
  • 兼容:data-*attr())
  • 代码:在线演示

在线演示

使用:valid和:invalid校验表单

在线演示

使用pointer-events禁用事件触发
  • 要点:通过pointer-events:none禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于<button>disabled
  • 场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)
  • 兼容:pointer-events
  • 代码:在线演示

在线演示

使用+或~美化选项框
  • 要点:<label>使用+~配合for绑定radiocheckbox的选择行为
  • 场景:选项框美化选中项增加选中样式
  • 兼容:+~
  • 代码:在线演示

在线演示

使用:focus-within分发冒泡响应

在线演示

使用:hover描绘鼠标跟随
  • 要点:将整个页面等比划分成小的单元格,每个单元格监听:hover,通过:hover触发单元格的样式变化来描绘鼠标运动轨迹
  • 场景:鼠标跟随轨迹水波纹怪圈
  • 兼容::hover
  • 代码:在线演示

在线演示

使用max-height切换自动高度
  • 要点:通过max-height定义收起的最小高度和展开的最大高度,设置两者间的过渡切换
  • 场景:隐藏式子导航栏悬浮式折叠面板
  • 兼容:max-height
  • 代码:在线演示

在线演示

使用transform模拟视差滚动

在线演示

使用animation-delay保留动画起始帧
  • 要点:通过transform-delayanimation-delay设置负值时延保留动画起始帧,让动画进入页面不用等待即可运行
  • 场景:开场动画
  • 兼容:transformanimation
  • 代码:在线演示

在线演示

使用resize拉伸分栏
  • 要点:通过resize设置横向自由拉伸来调整目标元素的宽度
  • 场景:富文本编辑器分栏阅读
  • 兼容:resize
  • 代码:在线演示

在线演示

Color Skill

使用color改变边框颜色
  • 要点:border没有定义border-color时,设置color后,border-color会被定义成color
  • 场景:边框颜色与文字颜色相同
  • 兼容:color
.elem {
    border: 1px solid;
    color: #f66;
}

在线演示

使用filter开启悼念模式
  • 要点:通过filter:grayscale()设置灰度模式来悼念某位去世的仁兄或悼念因灾难而去世的人们
  • 场景:网站悼念
  • 兼容:filter
  • 代码:在线演示

在线演示

使用::selection改变文本选择颜色
  • 要点:通过::selection根据主题颜色自定义文本选择颜色
  • 场景:主题化
  • 兼容:::selection
  • 代码:在线演示

在线演示

使用linear-gradient控制背景渐变
  • 要点:通过linear-gradient设置背景渐变色并放大背景尺寸,添加背景移动效果
  • 场景:主题化彩虹背景墙
  • 兼容:gradientanimation
  • 代码:在线演示

在线演示

使用linear-gradient控制文本渐变

在线演示

使用caret-color改变光标颜色
  • 要点:通过caret-color根据主题颜色自定义光标颜色
  • 场景:主题化
  • 兼容:caret-color
  • 代码:在线演示

在线演示

使用::scrollbar改变滚动条样式
  • 要点:通过scrollbarscrollbar-trackscrollbar-thumb等属性来自定义滚动条样式
  • 场景:主题化页面滚动
  • 兼容:::scrollbar
  • 代码:在线演示

在线演示

使用filter模拟Instagram滤镜
  • 要点:通过filter的滤镜组合起来模拟Instagram滤镜
  • 场景:图片滤镜
  • 兼容:filter
  • 代码:在线演示css-gram

在线演示

Figure Skill

使用div描绘各种图形
  • 要点:<div>配合其伪元素(::before::after)通过cliptransform等方式绘制各种图形
  • 场景:各种图形容器
  • 兼容:cliptransform
  • 代码:在线演示
使用mask雕刻镂空背景

在线演示

使用linear-gradient描绘波浪线
  • 要点:通过linear-gradient绘制波浪线
  • 场景:文字强化显示文字下划线内容分割线
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘彩带
  • 要点:通过linear-gradient绘制间断颜色的彩带
  • 场景:主题化
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用conic-gradient描绘饼图
  • 要点:通过conic-gradient绘制多种色彩的饼图
  • 场景:项占比饼图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘方格背景
  • 要点:使用linear-gradient绘制间断颜色的彩带进行交互生成方格
  • 场景:格子背景占位图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用box-shadow描绘单侧投影

在线演示

使用filter描绘头像彩色阴影
  • 要点:通过filter:blur() brightness() opacity()模拟阴影效果
  • 场景:头像阴影
  • 兼容:filter
  • 代码:在线演示

在线演示

使用box-shadow裁剪图像
  • 要点:通过box-shadow模拟蒙层实现中间镂空
  • 场景:图片裁剪新手引导背景镂空投射定位
  • 兼容:box-shadow
  • 代码:在线演示

在线演示

使用outline描绘内边框
  • 要点:通过outline设置轮廓进行描边,可设置outline-offset设置内描边
  • 场景:内描边外描边
  • 兼容:outline
  • 代码:在线演示

在线演示

Component Skill

迭代计数器

在线演示

下划线跟随导航栏
  • 要点:下划线跟随鼠标移动的导航栏
  • 场景:动态导航栏
  • 兼容:+
  • 代码:在线演示

在线演示

气泡背景墙

在线演示

滚动指示器

在线演示

故障文本

在线演示

换色器

在线演示

状态悬浮球

在线演示

粘粘球

在线演示

商城票券
  • 要点:边缘带孔和中间折痕的票劵
  • 场景:电影票代金券消费卡
  • 兼容:gradient
  • 代码:在线演示

在线演示

倒影加载条

在线演示

三维立方体

在线演示

动态边框
  • 要点:鼠标悬浮时动态渐变显示的边框
  • 场景:悬浮按钮边框动画
  • 兼容:gradient
  • 代码:在线演示

在线演示

标签页

在线演示

标签导航栏
  • 要点:可切换内容的导航栏
  • 场景:页面切换
  • 兼容:~
  • 代码:在线演示

在线演示

折叠面板
  • 要点:可折叠内容的面板
  • 场景:隐藏式子导航栏
  • 兼容:~
  • 代码:在线演示

在线演示

星级评分
  • 要点:点击星星进行评分的按钮
  • 场景:评分
  • 兼容:~
  • 代码:在线演示

在线演示

加载指示器

在线演示

自适应相册

在线演示

圆角进度条

在线演示

螺纹进度条

在线演示

立体按钮

在线演示

混沌加载圈

在线演示

蛇形边框

在线演示

自动打字
  • 要点:逐个字符自动打印出来的文字
  • 场景:代码演示文字输入动画
  • 兼容:chanimation
  • 代码:在线演示

在线演示

总结

写到最后总结得差不多了,如果后续我想起还有哪些遗漏的CSS开发技巧,会继续在这篇文章上补全。

最后送大家一个键盘!

(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

_明么 赞了文章 · 2019-11-12

灵活运用CSS开发技巧

作者:JowayYoung
仓库:GithubCodePen
博客:掘金思否知乎简书头条CSDN
公众号:IQ前端
联系我:关注公众号后有我的微信
特别声明:原创不易,未经授权不得对此文章进行转载或抄袭,否则按侵权处理,如需转载或开通公众号白名单可联系我,希望各位尊重原创的知识产权

系列

前言

何为技巧,意指表现在文学、工艺、体育等方面的巧妙技能。代码作为一门现代高级工艺,推动着人类科学技术的发展,同时犹如文字一样承托着人类文化的进步。

每写好一篇文章,都会使用大量的写作技巧。烘托、渲染、悬念、铺垫、照应、伏笔、联想、想象、抑扬结合、点面结合、动静结合、叙议结合、情景交融、首尾呼应、衬托对比、白描细描、比喻象征、借古讽今、卒章显志、承上启下、开门见山、动静相衬、虚实相生、实写虚写、托物寓意、咏物抒情等,这些应该都是我们从小到大写文章而接触到的写作技巧。

作为程序猿的我们,写代码同样也需要大量的写作技巧。一份良好的代码能让人耳目一新,让人容易理解,让人舒服自然,同时也让自己成就感满满(哈哈,这个才是重点)。因此,我整理下三年来自己使用到的一些CSS开发技巧,希望能让你写出耳目一新、容易理解、舒服自然的代码。

目录

既然写文章有这么多的写作技巧,那么我也需要对CSS开发技巧整理一下,起个易记的名字。

  • Layout Skill:布局技巧
  • Behavior Skill:行为技巧
  • Color Skill:色彩技巧
  • Figure Skill:图形技巧
  • Component Skill:组件技巧

备注

  • 代码只作演示用途,不会详细说明语法
  • 部分技巧示例代码过长,使用CodePen进行保存,点击在线演示即可查看
  • 兼容项点击链接即可查看当前属性的浏览器兼容数据,自行根据项目兼容需求考虑是否使用
  • 以下代码全部基于CSS进行书写,没有任何JS代码,没有特殊说明的情况下所有属性和方法都是CSS类型
  • 一部分技巧是自己探讨出来的,另一部分技巧是参考各位前端大神们的,都是一个互相学习的过程,大家一起进步

Layout Skill

使用vw定制rem自适应布局
  • 要点:移动端使用rem布局需要通过JS设置不同屏幕宽高比的font-size,结合vw单位和calc()可脱离JS的控制
  • 场景:rem页面布局(不兼容低版本移动端系统)
  • 兼容:vwcalc())
/* 基于UI width=750px DPR=2的页面 */
html {
    font-size: calc(100vw / 7.5);
}
使用:nth-child()选择指定元素
  • 要点:通过:nth-child()筛选指定的元素设置样式
  • 场景:表格着色边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::nth-child())
  • 代码:在线演示

在线演示

使用writing-mode排版竖文
  • 要点:通过writing-mode调整文本排版方向
  • 场景:竖行文字文言文诗词
  • 兼容:writing-mode
  • 代码:在线演示

在线演示

使用text-align-last对齐两端文本
  • 要点:通过text-align-last:justify设置文本两端对齐
  • 场景:未知字数中文对齐
  • 兼容:text-align-last
  • 代码:在线演示

在线演示

使用:not()去除无用属性
  • 要点:通过:not()排除指定元素不使用设置样式
  • 场景:符号分割文字边界元素排版(首元素、尾元素、左右两边元素)
  • 兼容::not())
  • 代码:在线演示

在线演示

使用object-fit规定图像尺寸
  • 要点:通过object-fit使图像脱离background-size的约束,使用<img>来标记图像背景尺寸
  • 场景:图片尺寸自适应
  • 兼容:object-fit
  • 代码:在线演示

在线演示

使用overflow-x排版横向列表
  • 要点:通过flexboxinline-block的形式横向排列元素,对父元素设置overflow-x:auto横向滚动查看
  • 场景:横向滚动列表元素过多但位置有限的导航栏
  • 兼容:overflow-x
  • 代码:在线演示

在线演示

使用text-overflow控制文本溢出

在线演示

使用transform描绘1px边框
  • 要点:分辨率比较低的屏幕下显示1px的边框会显得模糊,通过::before::aftertransform模拟细腻的1px边框
  • 场景:容器1px边框
  • 兼容:transform
  • 代码:在线演示

在线演示

使用transform翻转内容
  • 要点:通过transform:scale3d()对内容进行翻转(水平翻转、垂直翻转、倒序翻转)
  • 场景:内容翻转
  • 兼容:transform
  • 代码:在线演示

在线演示

使用letter-spacing排版倒序文本
  • 要点:通过letter-spacing设置负值字体间距将文本倒序
  • 场景:文言文诗词
  • 兼容:letter-spacing
  • 代码:在线演示

在线演示

使用margin-left排版左重右轻列表
  • 要点:使用flexbox横向布局时,最后一个元素通过margin-left:auto实现向右对齐
  • 场景:右侧带图标的导航栏
  • 兼容:margin
  • 代码:在线演示

在线演示

Behavior Skill

使用overflow-scrolling支持弹性滚动
  • 要点:iOS页面非body元素的滚动操作会非常卡(Android不会出现此情况),通过overflow-scrolling:touch调用Safari原生滚动来支持弹性滚动,增加页面滚动的流畅度
  • 场景:iOS页面滚动
  • 兼容:iOS自带-webkit-overflow-scrolling
body {
    -webkit-overflow-scrolling: touch;
}
.elem {
    overflow: auto;
}
使用transform启动GPU硬件加速
  • 要点:有时执行动画可能会导致页面卡顿,可在特定元素中使用硬件加速来避免这个问题
  • 场景:动画元素(绝对定位、同级中超过6个以上使用动画)
  • 兼容:transform
.elem {
    transform: translate3d(0, 0, 0); /* translateZ(0)亦可 */
}
使用attr()抓取data-*
  • 要点:在标签上自定义属性data-*,通过attr()获取其内容赋值到content
  • 场景:提示框
  • 兼容:data-*attr())
  • 代码:在线演示

在线演示

使用:valid和:invalid校验表单

在线演示

使用pointer-events禁用事件触发
  • 要点:通过pointer-events:none禁用事件触发(默认事件、冒泡事件、鼠标事件、键盘事件等),相当于<button>disabled
  • 场景:限时点击按钮(发送验证码倒计时)、事件冒泡禁用(多个元素重叠且自带事件、a标签跳转)
  • 兼容:pointer-events
  • 代码:在线演示

在线演示

使用+或~美化选项框
  • 要点:<label>使用+~配合for绑定radiocheckbox的选择行为
  • 场景:选项框美化选中项增加选中样式
  • 兼容:+~
  • 代码:在线演示

在线演示

使用:focus-within分发冒泡响应

在线演示

使用:hover描绘鼠标跟随
  • 要点:将整个页面等比划分成小的单元格,每个单元格监听:hover,通过:hover触发单元格的样式变化来描绘鼠标运动轨迹
  • 场景:鼠标跟随轨迹水波纹怪圈
  • 兼容::hover
  • 代码:在线演示

在线演示

使用max-height切换自动高度
  • 要点:通过max-height定义收起的最小高度和展开的最大高度,设置两者间的过渡切换
  • 场景:隐藏式子导航栏悬浮式折叠面板
  • 兼容:max-height
  • 代码:在线演示

在线演示

使用transform模拟视差滚动

在线演示

使用animation-delay保留动画起始帧
  • 要点:通过transform-delayanimation-delay设置负值时延保留动画起始帧,让动画进入页面不用等待即可运行
  • 场景:开场动画
  • 兼容:transformanimation
  • 代码:在线演示

在线演示

使用resize拉伸分栏
  • 要点:通过resize设置横向自由拉伸来调整目标元素的宽度
  • 场景:富文本编辑器分栏阅读
  • 兼容:resize
  • 代码:在线演示

在线演示

Color Skill

使用color改变边框颜色
  • 要点:border没有定义border-color时,设置color后,border-color会被定义成color
  • 场景:边框颜色与文字颜色相同
  • 兼容:color
.elem {
    border: 1px solid;
    color: #f66;
}

在线演示

使用filter开启悼念模式
  • 要点:通过filter:grayscale()设置灰度模式来悼念某位去世的仁兄或悼念因灾难而去世的人们
  • 场景:网站悼念
  • 兼容:filter
  • 代码:在线演示

在线演示

使用::selection改变文本选择颜色
  • 要点:通过::selection根据主题颜色自定义文本选择颜色
  • 场景:主题化
  • 兼容:::selection
  • 代码:在线演示

在线演示

使用linear-gradient控制背景渐变
  • 要点:通过linear-gradient设置背景渐变色并放大背景尺寸,添加背景移动效果
  • 场景:主题化彩虹背景墙
  • 兼容:gradientanimation
  • 代码:在线演示

在线演示

使用linear-gradient控制文本渐变

在线演示

使用caret-color改变光标颜色
  • 要点:通过caret-color根据主题颜色自定义光标颜色
  • 场景:主题化
  • 兼容:caret-color
  • 代码:在线演示

在线演示

使用::scrollbar改变滚动条样式
  • 要点:通过scrollbarscrollbar-trackscrollbar-thumb等属性来自定义滚动条样式
  • 场景:主题化页面滚动
  • 兼容:::scrollbar
  • 代码:在线演示

在线演示

使用filter模拟Instagram滤镜
  • 要点:通过filter的滤镜组合起来模拟Instagram滤镜
  • 场景:图片滤镜
  • 兼容:filter
  • 代码:在线演示css-gram

在线演示

Figure Skill

使用div描绘各种图形
  • 要点:<div>配合其伪元素(::before::after)通过cliptransform等方式绘制各种图形
  • 场景:各种图形容器
  • 兼容:cliptransform
  • 代码:在线演示
使用mask雕刻镂空背景

在线演示

使用linear-gradient描绘波浪线
  • 要点:通过linear-gradient绘制波浪线
  • 场景:文字强化显示文字下划线内容分割线
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘彩带
  • 要点:通过linear-gradient绘制间断颜色的彩带
  • 场景:主题化
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用conic-gradient描绘饼图
  • 要点:通过conic-gradient绘制多种色彩的饼图
  • 场景:项占比饼图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用linear-gradient描绘方格背景
  • 要点:使用linear-gradient绘制间断颜色的彩带进行交互生成方格
  • 场景:格子背景占位图
  • 兼容:gradient
  • 代码:在线演示

在线演示

使用box-shadow描绘单侧投影

在线演示

使用filter描绘头像彩色阴影
  • 要点:通过filter:blur() brightness() opacity()模拟阴影效果
  • 场景:头像阴影
  • 兼容:filter
  • 代码:在线演示

在线演示

使用box-shadow裁剪图像
  • 要点:通过box-shadow模拟蒙层实现中间镂空
  • 场景:图片裁剪新手引导背景镂空投射定位
  • 兼容:box-shadow
  • 代码:在线演示

在线演示

使用outline描绘内边框
  • 要点:通过outline设置轮廓进行描边,可设置outline-offset设置内描边
  • 场景:内描边外描边
  • 兼容:outline
  • 代码:在线演示

在线演示

Component Skill

迭代计数器

在线演示

下划线跟随导航栏
  • 要点:下划线跟随鼠标移动的导航栏
  • 场景:动态导航栏
  • 兼容:+
  • 代码:在线演示

在线演示

气泡背景墙

在线演示

滚动指示器

在线演示

故障文本

在线演示

换色器

在线演示

状态悬浮球

在线演示

粘粘球

在线演示

商城票券
  • 要点:边缘带孔和中间折痕的票劵
  • 场景:电影票代金券消费卡
  • 兼容:gradient
  • 代码:在线演示

在线演示

倒影加载条

在线演示

三维立方体

在线演示

动态边框
  • 要点:鼠标悬浮时动态渐变显示的边框
  • 场景:悬浮按钮边框动画
  • 兼容:gradient
  • 代码:在线演示

在线演示

标签页

在线演示

标签导航栏
  • 要点:可切换内容的导航栏
  • 场景:页面切换
  • 兼容:~
  • 代码:在线演示

在线演示

折叠面板
  • 要点:可折叠内容的面板
  • 场景:隐藏式子导航栏
  • 兼容:~
  • 代码:在线演示

在线演示

星级评分
  • 要点:点击星星进行评分的按钮
  • 场景:评分
  • 兼容:~
  • 代码:在线演示

在线演示

加载指示器

在线演示

自适应相册

在线演示

圆角进度条

在线演示

螺纹进度条

在线演示

立体按钮

在线演示

混沌加载圈

在线演示

蛇形边框

在线演示

自动打字
  • 要点:逐个字符自动打印出来的文字
  • 场景:代码演示文字输入动画
  • 兼容:chanimation
  • 代码:在线演示

在线演示

总结

写到最后总结得差不多了,如果后续我想起还有哪些遗漏的CSS开发技巧,会继续在这篇文章上补全。

最后送大家一个键盘!

(_=>[..."`1234567890-=~~QWERTYUIOP[]\\~ASDFGHJKL;'~~ZXCVBNM,./~"].map(x=>(o+=`/${b='_'.repeat(w=x<y?2:' 667699'[x=["Bs","Tab","Caps","Enter"][p++]||'Shift',p])}\\|`,m+=y+(x+'    ').slice(0,w)+y+y,n+=y+b+y+y,l+=' __'+b)[73]&&(k.push(l,m,n,o),l='',m=n=o=y),m=n=o=y='|',p=l=k=[])&&k.join`
`)()

结语

❤️关注+点赞+收藏+评论+转发❤️,原创不易,鼓励笔者创作更好的文章

关注公众号IQ前端,一个专注于CSS/JS开发技巧的前端公众号,更多前端小干货等着你喔

  • 关注后回复关键词免费领取视频教程
  • 关注后添加我微信拉你进技术交流群
  • 欢迎关注IQ前端,更多CSS/JS开发技巧只在公众号推送

查看原文

赞 223 收藏 198 评论 10

_明么 收藏了文章 · 2019-02-11

Vue经典开源项目汇总

Vue.js(读音 /vjuː/, 类似于 view)是一套用于构建用户界面的渐进式框架,主要用于快速的构建前端界面,与其它大型的前端框架不同,Vue被设计为可以自底向上逐层应用。

相比Angular.js来说,Vue的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合,是初创项目的前端首选框架。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue也完全能够为复杂的单页应用提供驱动。

正式因为Vue.js具有易用、灵活、高性能和渐进式等特点, 才使得Vue.js成为当前三大流行的JavaScript 框架之一。

工欲善其事,必先利其器。Vue的快速发展和崛起也得益于其快速发展的社区环境。为了实现快速的迭代,Vue社区涌现出了一大批的开源项目,借助这些开源项目,我们可以实现快速的迭代升级。

UI组件

element ★34,784 - 饿了么出品的基于Vue2的web UI工具套件
storybook ★33,503 - 响应式UI 开发及测试环境
Vux ★15,061 - 基于Vue和WeUI的组件库
mint-ui ★13,381 - Vue 2的移动UI元素
iview ★19,629 - 基于 Vuejs 的开源 UI 组件库
Keen-UI ★3,615 - 轻量级的UI组件合集
vue-material ★7,542 - 实现Material效果的UI库
muse-ui ★7,077 - 三端样式一致的响应式 UI 库
vuetify ★16,345 - 为移动而生的Vue JS 2组件框架
vonic ★3,009 - 快速构建移动端单页应用
eme ★1,772 - 优雅的Markdown编辑器
vue-multiselect ★3,877 - Vue.js选择框解决方案
vue-table ★1,691 - 简化数据表格
VueCircleMenu ★1,075 - 漂亮的vue圆环菜单
vue-chat ★1,255 - vuejs和vuex及webpack的聊天示例
radon-ui ★885 - 快速开发产品的Vue组件库
vue-waterfall ★1,447 - Vue.js的瀑布布局组件
vue-carbon ★909 - 基于 vue 开发MD风格的移动端组件库
vue-beauty ★1,871 - 由vue和ant design创建的优美UI组件
vue-blu ★1,449 - 帮助你轻松创建web应用
vueAdmin ★4,836 - 基于vuejs2和element的简单的管理员模板
vue-syntax-highlight ★1,202 - Sublime Text语法高亮
vue-infinite-scroll ★1,886 - VueJS的无限滚动指令
Vue.Draggable ★6,459 - 实现拖放和视图模型数组同步
vue-awesome-swiper ★5,817 - vue.js触摸滑动组件
vue-calendar ★1,081 - 日期选择插件
bootstrap-vue ★7,757 - 应用于Vuejs2的Twitter的Bootstrap 4组件
vue-swipe ★866 - VueJS触摸滑块
vue-amap ★1,869 - 基于Vue 2和高德地图的地图组件
vue-chartjs ★2,661 - vue中的Chartjs的封装
vue-datepicker ★681 - 日历和日期选择组件
vue-google-maps ★493 - 带有双向数据绑定Google地图组件
vue-progressbar ★936 - vue轻量级进度条
vue-picture-input ★717 - 移动友好的图片文件输入组件
vue-infinite-loading ★1,524 - VueJS的无限滚动插件
vue-upload-component ★1,395 - Vuejs文件上传组件
vue-datetime-picker ★316 - 日期时间选择控件
vue-scroller ★1,453 - Vonic UI的功能性组件
vue2-calendar ★403 - 支持lunar和日期事件的日期选择器
vue-video-player ★1,734 - VueJS视频及直播播放器
vue-fullcalendar ★963 - 基于vue.js的全日历组件
rubik ★276 - 基于Vuejs2的开源 UI 组件库
VueStar ★637 - 带星星动画的vue点赞按钮
vue-mugen-scroll ★457 - 无限滚动组件
mint-loadmore ★316 - VueJS的双向下拉刷新组件
vue-tables-2 ★1,064 - 显示数据的bootstrap样式网格
vue-virtual-scroller ★1,672 - 带任意数目数据的顺畅的滚动
DataVisualization ★985 - 数据可视化
vue-quill-editor ★3,533 - 基于Quill适用于Vue2的富文本编辑器
Vueditor ★506 - 所见即所得的编辑器
vue-html5-editor ★761 - html5所见即所得编辑器
vue-msgbox ★231 - vuejs的消息框
vue-slider ★646 - vue 滑动组件
vue-core-image-upload ★1,128 - 轻量级的vue上传插件
vue-slide ★212 - vue轻量级滑动组件
vue-lazyload-img ★229 - 移动优化的vue图片懒加载插件
vue-drag-and-drop-list ★305 - 创建排序列表的Vue指令
vue-progressive-image ★510 - Vue的渐进图像加载插件
vuwe ★215 - 基于微信WeUI所开发的专用于Vue2的组件库
vue-dropzone ★1,159 - 用于文件上传的Vue组件
vue-charts ★357 - 轻松渲染一个图表
vue-swiper ★269 - 易于使用的滑块组件
vue-images ★195 - 显示一组图片的lightbox组件
vue-carousel-3d ★449 - VueJS的3D轮播组件
vue-typer ★428 - 模拟用户输入选择和删除文本的Vue组件
vue-impression ★199 - 移动Vuejs2 UI元素
vue-datatable ★120 - 使用Vuejs创建的DataTableView
vue-instant ★313 - 轻松创建自动提示的自定义搜索控件
vue-dragging ★455 - 使元素可以拖拽
vue-slider-component ★1,176 - 在vue1和vue2中使用滑块
vue2-loading-bar ★225 - 最简单的仿Youtube加载条视图
vue-datepicker ★107 - 漂亮的Vue日期选择器组件
vue-video ★244 - Vue.js的HTML5视频播放器
vue-image-crop-upload ★1,124 - vue图片剪裁上传组件
vue-tooltip ★867 - 带绑定信息提示的提示工具
vue-highcharts ★339 - HighCharts组件
vue-touch-ripple ★320 - vuejs的触摸ripple组件
vue-datasource ★389 - 创建VueJS动态表格
vue2-timepicker ★246 - 下拉时间选择器
vue-date-picker ★86 - VueJS日期选择器组件
vue-scrollbar ★104 - 最简单的滚动区域组件
vue-quill ★99 - vue组件构建quill编辑器
vue-google-signin-button ★180 - 导入谷歌登录按钮
vue-svgicon ★517 - 创建svg图标组件的工具
vue-baidu-map ★1,090 - 基于 Vue 2的百度地图组件库
vue-social-sharing ★571 - 社交分享组件
vue2-editor ★1,304 - HTML编辑器
vue-easy-slider ★230 - Vue 2.x的滑块组件
datepicker ★111 - 基于flatpickr的时间选择组件
vue-chart ★47 - 强大的高速的vue图表解析
vue-music-master ★65 - vue手机端网页音乐播放器
handsontable ★44 - 网页表格组件
vue-simplemde ★553 - VueJS的Markdown编辑器组件
vue-popup-mixin ★70 - 用于管理弹出框的遮盖层
cubeex ★85 - 包含一套完整的移动UI
vue-fullcalendar ★405 - vue FullCalendar封装
vue-morris ★148 - Vuejs组件封装Morrisjs库
we-vue ★356 - Vue2及weui1开发的组件
vue-radial-progress ★206 - Vue.js放射性进度条组件
vue-slick ★349 - 实现流畅轮播框的vue组件
vue-pull-to-refresh ★106 - Vue2的上拉下拉
vue-side-nav ★50 - 响应式的侧边导航
mint-indicator ★71 - VueJS移动加载指示器插件
vue-touch-keyboard ★153 - VueJS虚拟键盘组件
vue-chartkick ★444 - VueJS一行代码实现优美图表
vue-ztree ★127- 用 vue 写的树层级组件
vue-m-carousel ★50 - vue 移动端轮播组件
vue-datepicker-simple ★59 - 基于vue的日期选择器
vue-tabs ★123 - 多tab页轻型框架
vue-city ★98- 城市选择器

脚手架

vuex ★18,763 - 专为 Vue.js 应用程序开发的状态管理模式
vuelidate ★3,336 - 简单轻量级的基于模块的Vue.js验证
qingcheng ★864 - qingcheng主题
vue-desktop ★542 - 创建管理面板网站的UI库
vue-meta ★2,215 - 管理app的meta信息
vue-axios ★1,350 - 将axios整合到VueJS的封装
vue-svg-icon ★473 - vue2的可变彩色svg图标方案
avoriaz ★776 - VueJS测试实用工具库
vue-bootstrap-modal ★149 - vue的Bootstrap样式组件
vuep ★557 - 用实时编辑和预览来渲染Vue组件
vue-online ★115 - reactive的在线和离线组件
vue-lazy-render ★201 - 用于Vue组件的延迟渲染
vue-password-strength-meter ★299 - 交互式密码强度计
element-admin ★294 - 支持 vuecli 的 Element UI 的后台模板
vue-electron ★186 - 将选择的API封装到Vue对象中的插件
cleave ★95 - 基于cleave.js的Cleave组件
vue-events ★181 - 简化事件的VueJS插件
vue-shortkey ★440 - 应用于Vue.js的Vue-ShortKey 插件
vue-cordova ★276 - Cordova的VueJS插件
vue-router-transition ★105 - 页面过渡插件
vue-gesture ★134 - VueJS的手势事件插件
http-vue-loader ★456 - 从html及js环境加载vue文件
vue-qart ★259 - 用于qartjs的Vue2指令
vue-websocket ★391 - VueJS的Websocket插件
vue-local-storage ★416 - 具有类型支持的Vuejs本地储存插件
vue-bus ★220 - VueJS的事件总线
vue-reactive-storage ★63 - vue插件的Reactive层
vue-notifications ★400 - 非阻塞通知库
vue-lazy-component ★32 - 懒加载组件或者元素的Vue指令
vue-observe-visibility ★442 - 当元素在页面上可见或隐藏时检测
vuex-i18n ★557 - 定位插件
Vue.resize ★186 - 检测HTML调整大小事件的vue指令
vuex-shared-mutations ★311 - 分享某种Vuex mutations

开发框架

electron-vue ★8,534 - Electron及VueJS快速启动样板
vue-2.0-boilerplate ★771 - Vue2单页应用样板​
vue-spa-template ★623 - 前后端分离后的单页应用开发
Framework7-Vue ★646 - VueJS与Framework7结合
vue-bulma ★373 - 轻量级高性能MVVM Admin UI框架
vue-webgulp ★140 - 仿VueJS Vue loader示例

服务器插件

nuxt.js ★17,821 - 用于服务器渲染Vue app的最小化框架
express-vue ★976 - 简单的使用服务器端渲染vue.js
vue-ssr ★151 - 非常简单的VueJS服务器端渲染模板
vue-easy-renderer ★56 - Nodejs服务端渲染

辅助工具

DejaVue ★764 - Vuejs可视化及压力测试
vue-play ★932 - 展示Vue组件的最小化框架
vscode-VueHelper ★330 - 目前vscode最好的vue代码提示插件
vue-generate-component ★149 - 轻松生成Vue js组件的CLI工具
vue-multipage-cli ★77 - 简单的多页CLI

示例

koel ★9,902 - 基于网络的个人音频流媒体服务
vue2-happyfri ★6,351 - vue2 + vuex整合项目
pagekit ★5,035 - 轻量级的CMS建站系统
vuedo ★1,925 - 博客平台
jackblog-vue ★1,689 - 个人博客系统
vue-cnode ★1,094 - 重写vue版cnode社区
CMS-of-Blog ★670 - 博客内容管理器
rss-reader ★883 - 简单的rss阅读器
vue-ghpages-blog ★401 - 依赖GitHub Pages无需本地生成的静态博客
vue-blog ★768 - 使用Vue2.0 和Vuex的vue-blog
Vue-cnodejs ★3,035 - 基于vue重写Cnodejs.org的webapp
NeteaseCloudWebApp ★2,117 - 高仿网易云音乐的webapp
vue-zhihu-daily ★1,246 - 知乎日报 with Vuejs
vue-wechat ★1,436 - vue.js开发微信app界面
vue2-demo ★1,650 - 从零构建vue2 + vue-router + vuex 开发环境
eleme ★1,532 - 高仿饿了么app商家详情
vue-demo ★1,099 - vue简易留言板
bilibili-vue ★1,436 - 全栈式开发bilibili首页
spa-starter-kit ★916 - 单页应用启动套件
VueDemo_Sell_Eleme ★1,235 - Vue2高仿饿了么外卖平台
vue-music ★1,251 - Vue 音乐搜索播放
douban ★1,989 - 基于vue全家桶的精致豆瓣DEMO
vue-Meizi ★1,193 - vue最新实战项目
maizuo ★759 - vue/vuex/redux仿卖座网
vue-WeChat ★1,328 - 基于Vue2高仿微信App的单页应用
vue-demo-kugou ★896 - vuejs仿写酷狗音乐webapp
vue2-manage ★5,403 - 基于 vue + element-ui 的后台管理系统
zhihudaily-vue ★629 - 知乎日报web版
vue-163-music ★747 - vue仿网易云音乐客户端版
vue-axios-github ★1,954 - 登录拦截登出功能
vue-shopping ★550 - 蘑菇街移动端
vue2.0-taopiaopiao ★912 - vue2.0与express构建淘票票页面
xyy-vue ★1,187 - 大学生交流平台

vue2.x-douban ★714 - Vue2实现简易豆瓣电影webApp
vue2-MiniQQ ★625 - 基于Vue2实现的仿手机QQ单页面应用
mi-by-vue ★387 - VueJS仿小米官网
daily-zhihu ★500 - 基于Vue2的知乎日报单页应用
node-vue-server-webpack ★285 - Node.js+Vue.js+webpack快速开发框架
beauty ★245 - 豆瓣美女clone
vue-adminLte-vue-router ★243 - vue和adminLte整合应用
vue-fis3 ★217 - 流行开源工具集成demo
notepad ★216 - 本地存储的记事本
vue-demo-maizuo ★210 - 使用Vue2全家桶仿制卖座电影
Pixel-Web ★198 - 一个 Vue 微博客户端
netease_yanxuan ★386 - vue版网易严选
tmdb-app ★194 - TMDbVueJS应用
vue-express-mongodb ★189 - 简单的前后端分离案例
vue-zhihudaily ★187 - 知乎日报 Web 版本
Vdo ★179 - VueJS与MD重构豆瓣
vue-blog ★171 - 单用户博客
Wuji ★168 - 吾记网页版
hello-vue-django ★160 - 使用带有Django的vuejs的样板项目
Zhihu-Daily-Vue.js ★157 - Vuejs单页网页应用
tencent-sports ★154 - Vue全家桶仿腾讯体育
gouyan-movie-vue ★151 - 基于vue的在线电影影讯网站
x-blog ★145 - 开源的个人blog项目
vue-musicApp ★132 - 使用vue全家桶制作的音乐播放器
vue-cnode ★131 - vue单页应用demo
webpack-vue-vueRouter ★130 - webpack及vue开发的简单项目实例
vue-koa-demo ★128 - 使用Vue2和Koa1的全栈demo
vueBlog ★127 - 前后端分离博客
websocket_chat ★127 - 基于vue和websocket的多人在线聊天室
houtai ★125 - 基于vue和Element的后台管理系统
vue-toutiao ★121 - vuejs高仿今日头条移动端
vue-cnode ★121 - 开源的CNode社区
vue-mini-shop ★121 - VueJS在线商店
photoShare ★120 - 基于图片分享的社交平台
iview2-management-system ★119 - 后台管理系统解决方案简单示例
doubanMovie ★119 - 豆瓣电影展示
vue-zhihu-daily ★111 - 基于Vue全家桶开发的知乎日报
Vue-News ★107 - 基于vue全家桶的仿知乎日报单页应用
douban-movie ★107 - 仿豆瓣电影wap端
generator-loopback-vue ★104 - 典型前后端分离项目模板
vue-quasar-admin-example ★99 - 将Quasar和VueJS应用于SPA项目
vue-zhihudaily-2.0 ★97 - 使用Vue2.0+vue-router+vuex创建的zhihudaily
vue-todos ★95 - vue最新实战项目教程
vue-music ★91 - 网易云音乐Demo
vue-qqmusic ★90 - Vue全家桶+Mint-Ui打造高仿QQMusic
vue2.x-Cnode ★88 - 基于vue全家桶的Cnode社区
vue-ruby-china ★86 - VueJS框架搭建的rubychina平台
doubanMovie-SSR ★85 - Vue豆瓣电影服务端渲染
vue-jd ★76 - 京东移动web商城
vue-nReader ★73 - 使用vue2.0 + vue-router + vuex 的一个多页面小说阅读webapp
VueBlog ★73 - 前后端分离的个人博客
Zhihu_Daily ★73 - 基于Vue和Nodejs的Web单页应用
vue-koa2-login ★67 - 使用 VueJS & NodeJS 实现的登录注册
webApp ★64 - Vue2的移动端webApp音乐播放器
vue-trip ★64 - vue2做的出行webapp
seeMusic ★63 - 跨平台云音乐播放器
github-explorer ★63 - 寻找最有趣的GitHub库
vue-cli-multipage-bootstrap ★60 - 将vue官方在线示例整合到组件中
vue-XiaoMi-Shop ★59 - 高仿小米商城的项目
Vue-NetEaseCloudMusic ★59 - 模仿IOS版网易云音乐的手机网站
life-app-vue ★59 - 使用vue2完成多功能集合到小webapp
doubanApp ★55 - 用vue2实现仿豆瓣app
ios7-vue ★52 - 使用vue2.0 vue-router vuex模拟ios7
canvas-vue ★50 - 一个Vue+Cnavas酷炫后台管理
vue-bushishiren ★49 - 不是诗人应用
vue-ssr-boilerplate ★48 - 精简版的ofvue-hackernews-2
vuecommunity ★47 - vue.js中文社区
vue-music163 ★47 - 音乐VueJS项目
Vue2-MV ★45 - 仿网易云音乐MV的webapp
musiccloudWebapp ★44 - 用vuejs仿网易云音乐
cnode-vue ★40 - 基于vue和vue-router构建的cnodejs web网站SPA
Framework7-VueJS ★38 - 使用移动框架的示例
m-eleme ★37 - 基于Vue全家桶仿饿了么移动端webapp
sls-vuex2-demo ★37 - vuex2商城购物车demo
eagles ★36 - 各种组件封装
Todos_Vuejs ★35 - vuejs2搭建的极简的todolist
vue-cnode ★35 - 用 Vue 做的 CNode 官网
HyaReader ★35 - 移动友好的阅读器
Vue-Admin ★33 - 基于Vue2的Admin系统
vue2-hybridapp-haoshiqi ★32 - 实现单页面webapp以及hybridapp
zhihu-daily ★32 - 轻松查看知乎日报内容
gank ★32 - gankio资源的阅读应用
vue-h5plus ★31 - 前卫的vue及html5plus跨平台demo
vue-cnode-mobile ★29 - 搭建cnode社区
vue-weather ★26 - 基于vue.js 2.0的百度天气应用
vue-user-center ★26 - vuejs直播类应用web端个人中心
zhihu-daily-vue ★22 - 知乎日报
vue-cnode ★22 - 使用cNode社区提供的接口
vue-starter ★22 - VueJs项目的简单启动页
node-vue-fabaocn ★21 - 基于 node 和 vue 实现的移动官网
vue-memo ★20 - 用 vue写的记事本应用
v-notes ★20 - 简单美观的记事本
vue-flexible-app ★19 - vue开发的一个简易app
simply-calculator-vuejs ★19 - 用VueJS实现简易计算器
vue-dropload ★19 - 用以测试下拉加载与简单路由
Vuejs-SalePlatform ★19 - vuejs搭建的售卖平台demo
vue-shopping-mall ★16 - 基于Vue.js 2.x系列 + vue2-router + axios + iview 商城
qqmusic ★13 - QQ音乐vue
vue-weather ★12 - VueJS天气demo

查看原文

_明么 收藏了文章 · 2019-02-11

创建华丽 UI 的 7条规则 第一部分 (2019年更新)

阿里云最近在做活动,低至2折,有兴趣可以看看:
https://promotion.aliyun.com/...

为了保证的可读性,本文采用意译而非直译。

简介

首先也是比较重要的,先说明点这篇指南并不适合所有人,主要适合以下从业者:

  • 开发者希望能够在必要时设计出自己漂亮的 UI。
  • 用户体验设计师希望他们的产品组合看起来比五角呆板的 PPT 更好看或者让用户得到更好的用户体验。

本文中主要围绕以下 7 规则讲解:

  1. 光来自天空 (Light comes from the sky)
  2. 黑白优先 (Black and white first)
  3. 加倍你的空白 (Double your whitespace)
  4. 学习在图像上叠加文本的方法(Part 2) (Learn the methods of overlaying text on images)
  5. 使文本层次分明 (Part 2)( Make text pop — and un-pop )
  6. 使用好看的字体 (Part 2)(Only use good fonts)
  7. 像艺术家一样借鉴 (Part 2)(Steal like an artist)

规则一: 光来自天空 (Light comes from the sky)

大脑在理解我们看到的界面时,影子是至关重要的因素。

这可能是关于 UI 设计最重要又容易被忽视一个内容:光来自天空。 光线来自天空,从上往上,以至于从下往上的光让人看起来很怪异。

图片描述

当光从天空而来时,它照亮事物的顶部,并在其下方投射阴影,物体的顶部比较亮,底部比较暗。

你不会希望人们的下眼睑都特别的黑吧,所以,如果我们在这些恶魔般的眼睛上面多加一些光亮,突然间他们就变成了你家门前的魔鬼女郎。

UI 也是一样,正如我们在所有的面部特征的下侧都有少量的阴影,大量 UI 元素的底面也有阴影。我们的屏幕是平的,但我们已经投入了大量的艺术创作让元素富有 3D 效果

图片描述

拿按钮举例,即使有了这个相对 “平面” 的按钮,仍然有一些与光线相关的细节:

  1. 未点击的按钮(顶部)底部具有黑色的底部边缘,正如夏天中午的,我们站在太阳时影子的样子。
  2. 未点击的按钮顶部的 亮度略高于底部。这是因为它模仿了一个稍微弯曲的表面,就像你需要把面前的镜子倾斜才能看到太阳一样,倾斜的表面会把更多的阳光反射到你身上。
  3. 未点击的按钮投射出一个稀薄地阴影——在放大的截图中能看的更清楚。
  4. 点击后的按钮,底部依然比顶部还要暗一些,并且整个按钮全都更暗。这是因为它与屏幕本身处于同一个平面,光线就不能轻易的照到它了。有人可能会说,我们在现实生活中看到的所有按键都是暗的,因为我们的手去按按钮时挡住了光线。

这只是个按钮而已,就已经呈现了4个细微的光线效果,我们现在要把光线理论用在所有地方。

iOS 6已经过时了,但它在轻度行为方面提供了一个很好的案例研究。

iOS 6已经过时了,但它在轻度行为方面提供了一个很好的案例研究。

这是 iOS 6的两个设置—— “请勿打扰” 和 “通知”,看看它们有多少光线效果。

  • 嵌套控制面板的上边缘投射一个微小的阴影

* “ON” 滑块轨道也跟着设置了一些阴影
* “ON” 滑块表面是凹的,底部会反射更多光线

  • 顶部的边框颜色比较其它的深点,这代表一个垂直于光源的表面,因此接收到大量的光,因此将大量的光反射到你的眼睛中,导致周围会变暗点。

一个分栏凹槽的样式,来自于我曾经设计的Hubster

常见向内凹陷的视觉元素:

  • 文本输入框
  • 点击后的按钮
  • 滑块
  • 单选按钮(未选中)
  • 复选框

常见向外突出的视觉元素:

  • 按钮 (未点击)
  • 滑块按钮
  • 下拉控件
  • 卡片
  • 选中的单选按钮
  • 弹框

扁平化设如何

扁平化设计是一种视觉风格,其中的元素缺乏模拟的凹痕或凸出,它们只是纯色的线条和形状。

图片描述

我和其他人一样喜欢干净和,但我不认为这是一个长期的趋势。如何将我们的界面用 3D 来在细微处进行模拟的更加自然,似乎很难将这种做法完全放弃。

五年前,我预测我们将会看到“扁平设计”的兴起,至少在 2019 年,这就是我们的现状——扁平干净外观的元素,加上一层阴影,帮助更加直接看到我们所想要看到的内容。

图片描述

在平面设计中,当点击元素时,可以适当加些阴影效果增强体验。

图片描述

扁平化设计的另一个例子:谷歌的 Material Design language

扁平设计现在看起来可真火啊!

这才是我身边最常出现的事物,它使用微妙的现实世界的线索来表达展示事件特征。

也不能说它完全没有模拟真实世界,但是这不同于 2006 年的网页设计风格,并没有使用材质,渐变和光泽的情况出现。我认为扁平化是未来的一种趋势。

扁平设计现在看起来可真火啊!

规则二:黑白优先 (Black and white first)

在添加颜色之前先进行灰度化设计可以简化视觉设计中最复杂的元素——并迫使用户关注元素的间距和布局。

最近用户体验设计师们热衷于“移动优先”的设计。这意味着,在 Retina 屏幕中,得想象页面上的交互在一个手机上是否行得通。

这种限制是有好处的,这有助于简化思想。从较难的问题开始(在小屏幕上可用的应用程序),然后采用更容易的问题的解决方案(在大屏幕上可用的应用程序)。

这里有另一个类似的结束:黑白优先。首先是在没有色彩的帮助下让应用变得美观并且可用,最后添加色彩,仅此而已。

Haraldur Thorleifsson的灰度线框看上去要比少数设计师最终的网页设计作品还好

Haraldur Thorleifsson 的灰度线框图看起来和其他设计师的成品网站一样好。

这是一个可靠和简单的方法,可以让应用程序看起来 “干净” 和 “简单”。在过多的地方使用过多的颜色很容易搞砸设计的简单和干净。

黑白优先 迫使你首先关注空间、大小和布局,这些都是简洁设计的主要关注点。

经典灰度设计

在有些情况下,黑白优先没有那么有用。那些具有强烈的特定主题的设计——“运动”、“华丽”、“卡通”等等——需要一个能很好地运用色彩的设计师。但是大多数应用除了干净和简单之外,并没有特别强烈的需求属性。这些特定需求的设计难度也大得多。

图片描述

对于其他的设计来讲,都是黑和白优先原则

步骤 2:怎么添加颜色

最简单的添加颜色是需要一种色调的。

图片描述

在灰度网站上添加一种颜色可以简单有效地吸引眼球。

图片描述

同样可以采取更深的一步。灰度 + 两种颜色,或者灰度 + 单一色调的多种颜色。

什么是色调

web 通常将颜色称为RGB十六进制代码,RGB 并非在设计中实现颜色的最优框架,更有用的是 HSB(H 代表色调,S 代表饱和度,B 代表亮度)(与HSV 同义,与 HSL 类似)。

HSB 比 RGB 更好,因为它符合我们对颜色自然的看法,并且可以观察到 HSB 值的变化所给你看到颜色来带的影响。

如果 HSB 对你来说是个新的东西,这里 HSB 颜色的 优质入门文章

图片描述

《Smashing》 杂志的金色主题。

图片描述

《Smashing》 杂志的蓝色主题。

通过修改单一色调的饱和度亮度,可以生成多种颜色——暗色调、灯光、背景、重点、吸引眼球的特效——而且不会让人眼花缭乱。

使用一种或两种基本色调的多种颜色是强调和中和元素的最可靠的方法,而且不会使设计变得混乱。

倒数计时器来自 Kerem Suer

倒数计时器来自 Kerem Suer

关于颜色的其他一些补充

色彩是视觉设计中最复杂的领域。虽然很多关于色彩的东西在你完成设计时并不是很实用,但是我却看到了一些非常有用的东西:

* 学习 UI 设计:这是作者创建的一门课程,包含3个小时的彩色设计视频(以及 20 多个小时的 UI设计主题视频),观看地址 learnui.design

* 设计色彩学:一个实用的框架。

  • 永远不要使用黑色 (伊恩·斯托姆·泰勒):这篇文章谈到完全平面化的灰色几乎从来没有出现在现实世界中,同时它也提到了如何饱和灰色阴影 — 尤其是深色阴影 — 为设计增添了视觉丰富性。另外,饱和的灰色其实更贴近现实世界,这是它最美的地方。
  • Adobe Color CC:一个非常棒的工具,用于查找、修改和创建配色方案。
  • Dribbble search-by-color: 看看世界上最好的设计师正在使用什么颜色设计。

规则三:加倍你的空白 (Double your whitespace)

在规则 2 中,黑色优先 迫使设计师在考虑颜色之前考虑间距和布局,接下来谈谈间距和布局了。

如果你从头编写 HTML 代码,那么你可能熟悉默认情况下 HTML 在页面上的布局方式。

图片描述

基本上,所有东西都挤在页面的顶部。字体很小,行与行之间没有空格,段落之间有一小段空白,但不多。段落一直延伸到页面的末尾,不管是 100px 还是 10000 px。

从美学角度来说,这太糟糕了,如果你想让 UI 看起来像设计好的,需要增加很多空白的间距。

以下是 Piotr Kwiatkowski 的音乐播放器概念图。

图片描述

特别要注意左边的菜单。

左侧菜单

菜单项之间的垂直空间是文本本身高度的两倍,上面和下面有同样多的内边距。

或者看看列表标题。“播放列表” 和下划线之间有 15px 的空间。这比字体本身还要高,更别提每个列表之间间隔了 25 个像素了。

图片描述

顶部的导航条有更多的空间。文字“搜索音乐”占了整个导航条高度的20%。图标也使用了类似的高度。

左边栏的文字之间留出了比较充裕的空间,甚至更多。

Piotr 认真考虑在这里增加更多的空白,并且效果很好。尽管这只是它为了更多乐趣(据我所知),就美学而言,它非常漂亮,能够和市面上最好的音乐播放器UI界面相提并论。

适当的空白可以让一些最混乱的界面看起来更吸引人、更简单,就像论坛一样。

Forum 的概念设计,来自 Matt Sisto

或者维基百科

Wikipedia 概念设计,来自 Aurélien Salomon

你会发现对此有很多争论,比如说,维基百科的重新设计舍弃了一些关键的网站的功能,但是你不得不说这是一个很好的学习方式!

  • 在你的线条之间预留空间。
  • 在你的元素之间预留空间。
  • 在你的元素组之间预留空白。

要第二部分继续讨论:

4、学习在图像上叠加文本的方法(Part 2) (Learn the methods of overlaying text on images)

5、使文本层次分明 (Part 2)(Make text pop — and un-pop)

6、只使用好看的字体 (Part 2)(Only use good fonts)

7、像艺术家一样借鉴 (Part 2)(Steal like an artist)


你的点赞是我持续分享好东西的动力,欢迎点赞!

交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

clipboard.png

查看原文

认证与成就

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

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2018-01-04
个人主页被 78 人浏览