4fun

4fun 查看完整档案

南京编辑石家庄新东方烹饪学校  |  ii 编辑公众号  |  正义的程序猿 编辑 xydida.com 编辑
编辑

Everyday life is like programming, I guess.
If you love something you can put beauty into it.
---- Donald Knuth

┏┳━━━━━━━━━ ━┓
┃┃██████████ █┃
┣┫████┏━━━━ ┓█┃
┃┃████┃ 陈原 ┃█┃
┣┫████┃ 独装 ┃█┃
┃┃████┃ 秀正 ┃█┃
┣┫████┃ 传版 ┃█┃
┃┃████┗━━━━ ┛█┃
┣┫██████████ █┃
┃┃██████████ █┃
┗┻━━━━━━━━━ ━┛

个人动态

4fun 赞了文章 · 4月7日

你心中最高大上最牛X的技术到底是什么

图片.png

图片.png

**高以下为基,贵以贱为本
互联网技术的核心根基就是TCP/IP,TCP/IP的实现依赖于Linux socket API【我们的项目大部分运行在上面】
没有它们各种高大上牛逼的技术就无从建立起来。而这根基对大家所用的java,go,py,php,c,c++,nodejs...都是一样的,只不过是基于Linux api做了各种各样的封装百家争鸣,百花齐放,跟易经里的阴阳构成64卦一样,当你刚开始撸程序时,可能并不会觉得基础的重要性,甚至可能几年内一直是框架crud boy,你并不会察觉到基础核心Linux socket api【一般来说linux内核的api很少变动,比较稳定,国内linux内核开发工程师估计也不会天天没事干,天天修改linux内核api】!
每天被各种新技术词汇遮蔽双眼,而基础知识你一直的错失和鄙视低估,就想一步飞龙在天,达到九五爻之位,并没有“以下为基” “以贱为本” **
甚至对于其它知识点你都持“下贱之态”只想与天齐名,从不脚踏实地从坤做起。

先给总结图 你再看下面的内容
图片.png

下面就是要告诉你所谓的牛逼技术到底是什么回事!!!
我们测试一下数据库,java,python,go,c/c++,php,redis,docker进行测试【测完你会发现点东西】
然后你再看图比较好

  • 先测试数据库接收数据时是不是用了Linux网络 socket API

图片.png
好启动了,没有什么可说的,ELF文件启动。
图片.png
.ibd是创建数据表时生成的文件,没啥可说的,DBA专业都知道
图片.png

我画线的地方调用了ACCEPT SOCKET API函数
图片.png
调用了SENDTO,RECVFROM SOCKET API 函数

mysql怎么实现我们管不着,但是数据来回的传输依赖于LINUX SOCKET API,这些都是网络接口API
图片.png
图片.png
调用了系统其它API函数库,我们看一下accept,sendto,recvfrom,setsockopt,getsockopt read,write,epoll相关函数
图片.png
图片.png
图片.png
图片.png
图片.png
图片.png
图片.png

  • redis 数据库

图片.png

启动redis
图片.png
测试
图片.png
**epoll_wait 得到就绪的文件描述符读事件返回,然后调用read,其实跟RECVFROM功能一样
它的数据是:3rn$3rnsetrn4rnbfzsrn5rn10000rn
这一堆数据被各种大佬称为 redis的二进制通信请求协议!!!返回是+OKrn**
图片.png

它们的数据来回传输大部分用read,write函数来实现
图片.png
图片.png
图片.png
图片.png

  • docker

图片.png

图片.png
dockerd服务ELF文件调用的linux api相关函数库
图片.png
启动docker服务,跟mysql一样默认启动一堆进程和线程
图片.png
相关命令运行【对不起,我不喜欢背东西,你要是面试我时,问我docker有哪些命令选项,对不起我回家种地放牛了】
图片.png
来用下测试
图片.png
运行过程
图片.png
图片.png
都在调用connect,socket,getpeerame,setsockopt,getsockopt,accept,sendto等LINUX SOCKET API函数
docker调用的LINUX API 函数库
图片.png
图片.png
图片.png
图片.png
图片.png

  • go语言写个网络程序

我直接复制粘粘给你运行对不起我背不了函数,要用就直接复制粘粘就好了
图片.png
图片.png

go ELF 文件
图片.png

来运行那个大家认为的源码文件
图片.png
先运行哪个函数,你自己看着办哦
图片.png

重点
图片.png
熟悉的一批,socket 创建socket文件描述符,然后命名【把ip,端口绑定到此文件上】,然后监听,并阻塞在accept函数上
图片.png
图片.png
图片.png

好了,go就这样子,它封装的比较骚,go elf编译器封装的牛逼,语法换了一套就称为编译型语言了。

  • python语言也写个网络程序测试下

图片.png
py的语法就是好,随便一撸就可以了,简直是粗暴又骚,语法嘛就这样,长得跟少妇一样
来先认识一下python elf文件 毕竟好多爬虫大佬可能没有见过
图片.png
图片.png
.php .py .go .java里的东西只是个文本内容,你们嘛叫源码,我没有文化,只能叫ascii text ^_^
图片.png

启动测试
图片.png
图片.png
有没有发现,熟悉的一批
图片.png
图片.png
图片.png
好了,到这里够意思了。

这么简单的语言,你去学语法就行了,简单又粗暴谁不喜欢呢。我都喜欢。 ^_^

  • java 语言网络程序测试

测试源码

cat GreetingServer.java
import java.net.*;
import java.io.*;

public class GreetingServer extends Thread
{
   private ServerSocket serverSocket;

   public GreetingServer(int port) throws IOException
   {
      serverSocket = new ServerSocket(port);
      serverSocket.setSoTimeout(100000000);
   }

   public void run()
   {
      while(true)
      {
         try
         {
            System.out.println("等待远程连接,端口号为:" + serverSocket.getLocalPort() + "...");
            Socket server = serverSocket.accept();
            System.out.println("远程主机地址:" + server.getRemoteSocketAddress());
            DataInputStream in = new DataInputStream(server.getInputStream());
            System.out.println(in.readUTF());
            DataOutputStream out = new DataOutputStream(server.getOutputStream());
            out.writeUTF("谢谢连接我:" + server.getLocalSocketAddress() + "\nGoodbye!");
            server.close();
         }catch(SocketTimeoutException s)
         {
            System.out.println("Socket timed out!");
            break;
         }catch(IOException e)
         {
            e.printStackTrace();
            break;
         }
      }
   }
   public static void main(String [] args)
   {
      int port = Integer.parseInt(args[0]);
      try
      {
         Thread t = new GreetingServer(port);
         t.run();
      }catch(IOException e)
      {
         e.printStackTrace();
      }
   }
}

看一下java elf文件,我相信java大佬肯定知道我就不费话了
图片.png
图片.png
编译一下
图片.png
我没有学过java,但是看一下报错就知道了,对不对,这么明显的提示,我phper都晓得 ^_^
图片.png
编译ok
图片.png
编译好是啥文件
图片.png
启动java程序开始测试
图片.png
图片.png
有没有发现,熟悉的一批
图片.png
图片.png
图片.png
好了,就这么多就行了,没有必要再截图了。

看接下,我们撸c[c++一样]

你们应该看出点熟悉的地方了
int main(int argc,char *argv[])
{

        if(argc<=2){

                printf("useage:%s ip_address port_number\n",basename(argv[0]));
                return 1;
        }

        const char *ip = argv[1];
        int port = atoi(argv[2]);

        int ret = 0;
        struct sockaddr_in address;
        bzero(&address,sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET,ip,&address.sin_addr);
        address.sin_port = htons(port);

        int listenfd = socket(PF_INET,SOCK_STREAM,0);
        assert(listenfd>=0);

        ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));

        assert(ret!=-1);

        ret = listen(listenfd,5);
        assert(ret!=-1);

        bzero(&address,sizeof(address));
        address.sin_family = AF_INET;
        inet_pton(AF_INET,ip,&address.sin_addr);
        address.sin_port = htons(port);

        int udpfd = socket(PF_INET,SOCK_DGRAM,0);
        assert(udpfd>=0);

        ret = bind(udpfd,(struct sockaddr*)&address,sizeof(address));
        assert(ret!=-1);

        struct epoll_event events[MAX_EVENT_NUMBER];
        int epollfd = epoll_create(5);
        assert(epollfd!=-1);

        addfd(epollfd,listenfd);
        addfd(epollfd,udpfd);

        while(1){

                int number = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
                if(number<0){

                        printf("epoll failre\n");
                        break;
                }

                for(int i=0;i<number;i++){

                        int sockfd = events[i].data.fd;
                        if(sockfd ==listenfd){

                                struct sockaddr_in client_address;
                                socklen_t client_addrlength = sizeof(client_address);
                                int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
                                addfd(epollfd,connfd);
                        }
                        else if(sockfd == udpfd){

                                char buf[UDP_BUFFER_SIZE];
                                memset(buf,0,UDP_BUFFER_SIZE);
                                struct sockaddr_in client_address;
                                socklen_t client_addrlength = sizeof(client_address);

                                ret = recvfrom(udpfd,buf,UDP_BUFFER_SIZE-1,0,(struct sockaddr*)&client_address,&client_addrlength);
                                if(ret>0){

                                        sendto(udpfd,buf,UDP_BUFFER_SIZE-1,0,(struct sockaddr*)&client_address,client_addrlength);
                                }
                        }
                        else if(events[i].events & EPOLLIN){

                                char buf[TCP_BUFFER_SIZE];
                                while(1){

                                        memset(buf,0,TCP_BUFFER_SIZE);
                                        ret = recv(sockfd,buf,TCP_BUFFER_SIZE-1,0);
                                        if(ret<0){

                                                if((errno==EAGAIN)||(errno=EWOULDBLOCK)){

                                                        break;
                                                }
                                                close(sockfd);
                                                break;
                                        }
                                        else if(ret==0){

                                                close(sockfd);
                                        }
                                        else{

                                                send(sockfd,buf,ret,0);
                                        }
                                }
                        }else{

                                printf("something else happened\n");
                        }
                }
        }

        close(listenfd);
        return 0;
}

然后编译,编译好就这样
图片.png
图片.png
测试
图片.png
图片.png

好了,都不用我废话了,接下来还是测试下php吧,我觉得,虽然有的朋友觉得php咋样咋样,拍黄片嘛,在打黄打非的压力之下当然没啥好名声了。

  • php 网络程序测试
  • 图片.png

看下php elf文件
图片.png
启动并测试跟踪
图片.png
好了,不用我废话,大家也知道是怎么回事了。
图片.png

这些api函数怎么用呢?
man socket
图片.png

linux socket api 是所有语言,数据库等应用的核心低层技术知识,你框架掌握的再6,没有多少意义,语言掌握得再6也只是工具
分布式,集群,高大上的技术都要TCP/IP支持,而它的实现就是网络编程,各语言写法不同,但是核心基础知识没有变化,正所谓天下大事必作于细天下难事必作于易,一上来撸c/c++,java如果不合适你,那么上来就撸PHP掌握了共通的知识再换语言又何妨呢?

查看原文

赞 17 收藏 12 评论 2

4fun 赞了文章 · 4月7日

3-5年的PHPer 应该具备~

又到了,金三银四,换工作季。我呢没准变换工作,倒是上网翻了翻招聘信息,由于我做PHP,就看了下当下3-5年的招聘需求,发现这些招聘信息都有如下要求:

3-5年PHP需要具备:

  • TCP/UDP协议, socket 通信,熟练使用workman,swoole,swoft等rpc框架
  • 精通PHP,熟悉golang语言,
  • 熟悉html,css,javascript,会nodejs,vue优先。
  • mysql, 以及SQL优化,熟悉索引应用和优化,独立设计数据库、数据表,
  • nosql,mongodb, redis,memcache缓存。熟悉后端缓存技术、了解缓存使用场景,高并发、高性能服务系统设计经验及能力,熟悉大规模集群系统的开发
  • 常用Linux,shell命令编写,熟悉云、容器使用
  • 精通LNMP架构,熟悉http 协议,RestFul API开发,熟悉tp,laravel,yii主流框架。
  • 熟练使用svn,git,Hg版本管理工具,
  • 良好的书写习惯,注释,设计模式,编写高质量的,整洁简单,可维护性的代码,遵循公司研发规范,产品技术文档的整理
  • 分析和快速排查定位解决线上问题,保障系统功能的稳定性,优化现有系统,提升运作性能
  • 主导/参与项目的架构设计、技术选型、架构原型实现以及服务端核心模块的开发,与各技术人员紧密合作,完成工作任务
  • 有个人博客,个人开源项目,有个人独立完成项目。
  • 乐于持续学习,乐观开朗,抗压性强,良好的沟通能力和合作精神,自我驱动力强,有强烈的事业心和责任感

大家可以看看自己是否达到了主流的用人标准,如果你是超出预期,那么你可以选择跳得更高。3-5年时间,足够把一个学生培养成一个合格的打工人了。可以看到企业还是把PHPer当作多面手看待,希望还会能会一些其他语言。3-5年的phper,企业还希望有一定的带团能力,由此可见phper的成熟期是较短的。

我同时对比了3-5年的NodeJS,Python、Java、Golang就职要求,PHP岗位对技术之外的要求是最多的,这样会,还要那样会,而且范围还广。我自己也觉得phper的成熟时间是较短的,对技术高低的评判主要是对高性能、高并发的设计,这个时候php写的漂不漂亮不重要,会不会用第三方工具(Redis,ES),了不了解限流、队列、削峰、缓存这些原理就尤为重要。所以到这个份上,就别死磕PHP了,多看看周边。

我又查了下5-10年的PHP岗位,数据很少。有的,我看了下任职要求,只有一句话要求了PHP。好到里,我就想从整个职业道路上谈谈,如果你关注过我的博客,会发现,我发布些golang相关的技术博客了。如果你还是坚持——PHP是这世界上最好的语言,你可以把这篇文章关闭了。本来我还是安安心心写PHP,捣鼓捣鼓Swoole,觉得会用Swoole,能玩转PHP协程就很牛皮。这除了自嗨,然并卵。

之前比较喜欢看些别人的博客,php100从断更到关闭。其他综合博客平台,PHP的文章更新占比也很少,这么说吧,有些培训机构都不开班了。我感觉最近php的热度是有所下降的。所以,大家考虑换工作好好考虑下未来5年,10年你准备干什么。同时,别死磕语言,多积累算法、计算机底层、架构方面的知识,这些不随你换语言而过时的。

查看原文

赞 5 收藏 1 评论 7

4fun 关注了用户 · 4月7日

小白要生发 @xiaobaiyaoshengfa

PHP工程师

公众号: phpitjsz
juejin: 同名
learnku: 同名

关注 1194

4fun 回答了问题 · 4月6日

解决这里的闭包是啥意思?

闭包可以理解为:一个函数及其周围封闭词法环境中的引用构成闭包。可能这句话还是不好理解,
示例:

function createAction() {
    var message = "封闭环境内的变量";
    
    return function() {
        console.log(message);
    }
}

const showMessage = createAction();
showMessage();    // output: 封闭环境内的变量

更加详细的可以看这个大佬写的文章:还搞不懂闭包算我输(JS 示例)

关注 5 回答 5

4fun 赞了文章 · 4月5日

Vue2.x 的双向绑定原理及实现

Vue 数据双向绑定原理

Vue 是利用的 Object.defineProperty() 方法进行的数据劫持,利用 set、get 来检测数据的读写。

https://jsrun.net/RMIKp/embed...

MVVM 框架主要包含两个方面,数据变化更新视图,视图变化更新数据。

视图变化更新数据,如果是像 input 这种标签,可以使用 oninput 事件..

数据变化更新视图可以使用 Object.definProperty() 的 set 方法可以检测数据变化,当数据改变就会触发这个函数,然后更新视图。

实现过程

我们知道了如何实现双向绑定了,首先要对数据进行劫持监听,所以我们需要设置一个 Observer 函数,用来监听所有属性的变化。

如果属性发生了变化,那就要告诉订阅者 watcher 看是否需要更新数据,如果订阅者有多个,则需要一个 Dep 来收集这些订阅者,然后在监听器 observer 和 watcher 之间进行统一管理。

还需要一个指令解析器 compile,对需要监听的节点和属性进行扫描和解析。

因此,流程大概是这样的:

  1. 实现一个监听器 Observer,用来劫持并监听所有属性,如果发生变动,则通知订阅者。
  2. 实现一个订阅者 Watcher,当接到属性变化的通知时,执行对应的函数,然后更新视图,使用 Dep 来收集这些 Watcher。
  3. 实现一个解析器 Compile,用于扫描和解析的节点的相关指令,并根据初始化模板以及初始化相应的订阅器。

仿Vue导图.png

显示一个 Observer

Observer 是一个数据监听器,核心方法是利用 Object.defineProperty() 通过递归的方式对所有属性都添加 setter、getter 方法进行监听。

var library = {
  book1: {
    name: "",
  },
  book2: "",
};
observe(library);
library.book1.name = "vue权威指南"; // 属性name已经被监听了,现在值为:“vue权威指南”
library.book2 = "没有此书籍"; // 属性book2已经被监听了,现在值为:“没有此书籍”

// 为数据添加检测
function defineReactive(data, key, val) {
  observe(val); // 递归遍历所有子属性
  let dep = new Dep(); // 新建一个dep
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      if (Dep.target) {
        // 判断是否需要添加订阅者,仅第一次需要添加,之后就不用了,详细看Watcher函数
        dep.addSub(Dep.target); // 添加一个订阅者
      }
      return val;
    },
    set: function(newVal) {
      if (val == newVal) return; // 如果值未发生改变就return
      val = newVal;
      console.log(
        "属性" + key + "已经被监听了,现在值为:“" + newVal.toString() + "”"
      );
      dep.notify(); // 如果数据发生变化,就通知所有的订阅者。
    },
  });
}

// 监听对象的所有属性
function observe(data) {
  if (!data || typeof data !== "object") {
    return; // 如果不是对象就return
  }
  Object.keys(data).forEach(function(key) {
    defineReactive(data, key, data[key]);
  });
}
// Dep 负责收集订阅者,当属性发生变化时,触发更新函数。
function Dep() {
  this.subs = {};
}
Dep.prototype = {
  addSub: function(sub) {
    this.subs.push(sub);
  },
  notify: function() {
    this.subs.forEach((sub) => sub.update());
  },
};

思路分析中,需要有一个可以容纳订阅者消息订阅器 Dep,用于收集订阅者,在属性发生变化时执行对应的更新函数。

从代码上看,将订阅器 Dep 添加在 getter 里,是为了让 Watcher 初始化时触发,,因此,需要判断是否需要订阅者。

在 setter 中,如果有数据发生变化,则通知所有的订阅者,然后订阅者就会更新对应的函数。

到此为止,一个比较完整的 Observer 就完成了,接下来开始设计 Watcher.

实现 Watcher

订阅者 Watcher 需要在初始化的时候将自己添加到订阅器 Dep 中,我们已经知道监听器 Observer 是在 get 时执行的 Watcher 操作,所以只需要在 Watcher 初始化的时候触发对应的 get 函数去添加对应的订阅者操作即可。

那给如何触发 get 呢?因为我们已经设置了 Object.defineProperty(),所以只需要获取对应的属性值就可以触发了。

我们只需要在订阅者 Watcher 初始化的时候,在 Dep.target 上缓存下订阅者,添加成功之后在将其去掉就可以了。

function Watcher(vm, exp, cb) {
  this.cb = cb;
  this.vm = vm;
  this.exp = exp;
  this.value = this.get(); // 将自己添加到订阅器的操作
}

Watcher.prototype = {
  update: function() {
    this.run();
  },
  run: function() {
    var value = this.vm.data[this.exp];
    var oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.cb.call(this.vm, value, oldVal);
    }
  },
  get: function() {
    Dep.target = this; // 缓存自己,用于判断是否添加watcher。
    var value = this.vm.data[this.exp]; // 强制执行监听器里的get函数
    Dep.target = null; // 释放自己
    return value;
  },
};

到此为止, 简单的额 Watcher 设计完毕,然后将 Observer 和 Watcher 关联起来,就可以实现一个简单的的双向绑定了。

因为还没有设计解析器 Compile,所以可以先将模板数据写死。

将代码转化为 ES6 构造函数的写法,预览试试。

https://jsrun.net/8SIKp/embed...

这段代码因为没有实现编译器而是直接传入了所绑定的变量,我们只在一个节点上设置一个数据(name)进行绑定,然后在页面上进行 new MyVue,就可以实现双向绑定了。

并两秒后进行值得改变,可以看到,页面也发生了变化。

// MyVue
proxyKeys(key) {
    var self = this;
    Object.defineProperty(this, key, {
        enumerable: false,
        configurable: true,
        get: function proxyGetter() {
            return self.data[key];
        },
        set: function proxySetter(newVal) {
            self.data[key] = newVal;
        }
    });
}

上面这段代码的作用是将 this.data 的 key 代理到 this 上,使得我可以方便的使用 this.xx 就可以取到 this.data.xx。

实现 Compile

虽然上面实现了双向数据绑定,但是整个过程都没有解析 DOM 节店,而是固定替换的,所以接下来要实现一个解析器来做数据的解析和绑定工作。

解析器 compile 的实现步骤:

  1. 解析模板指令,并替换模板数据,初始化视图。
  2. 将模板指定对应的节点绑定对应的更新函数,初始化相应的订阅器。

为了解析模板,首先需要解析 DOM 数据,然后对含有 DOM 元素上的对应指令进行处理,因此整个 DOM 操作较为频繁,可以新建一个 fragment 片段,将需要的解析的 DOM 存入 fragment 片段中在进行处理。

function nodeToFragment(el) {
  var fragment = document.createDocumentFragment();
  var child = el.firstChild;
  while (child) {
    // 将Dom元素移入fragment中
    fragment.appendChild(child);
    child = el.firstChild;
  }
  return fragment;
}

接下来需要遍历各个节点,对含有相关指令和模板语法的节点进行特殊处理,先进行最简单模板语法处理,使用正则解析“{{变量}}”这种形式的语法。

function compileElement (el) {
    var childNodes = el.childNodes;
    var self = this;
    [].slice.call(childNodes).forEach(function(node) {
        var reg = /\{\{(.*)\}\}/; // 匹配{{xx}}
        var text = node.textContent;
        if (self.isTextNode(node) && reg.test(text)) {  // 判断是否是符合这种形式{{}}的指令
            self.compileText(node, reg.exec(text)[1]);
        }
        if (node.childNodes && node.childNodes.length) {
            self.compileElement(node);  // 继续递归遍历子节点
        }
    });
},
function compileText (node, exp) {
    var self = this;
    var initText = this.vm[exp];
    updateText(node, initText);  // 将初始化的数据初始化到视图中
    new Watcher(this.vm, exp, function (value) {  // 生成订阅器并绑定更新函数
        self.updateText(node, value);
    });
},
function updateText (node, value) {
    node.textContent = typeof value == 'undefined' ? '' : value;
}

获取到最外层的节点后,调用 compileElement 函数,对所有的子节点进行判断,如果节点是文本节点切匹配{{}}这种形式的指令,则进行编译处理,初始化对应的参数。

然后需要对当前参数生成一个对应的更新函数订阅器,在数据发生变化时更新对应的 DOM。

这样就完成了解析、初始化、编译三个过程了。

接下来改造一个 myVue 就可以使用模板变量进行双向数据绑定了。

https://jsrun.net/K4IKp/embed...

添加解析事件

添加完 compile 之后,一个数据双向绑定就基本完成了,接下来就是在 Compile 中添加更多指令的解析编译,比如 v-model、v-on、v-bind 等。

添加一个 v-model 和 v-on 解析:

function compile(node) {
  var nodeAttrs = node.attributes;
  var self = this;
  Array.prototype.forEach.call(nodeAttrs, function(attr) {
    var attrName = attr.name;
    if (isDirective(attrName)) {
      var exp = attr.value;
      var dir = attrName.substring(2);
      if (isEventDirective(dir)) {
        // 事件指令
        self.compileEvent(node, self.vm, exp, dir);
      } else {
        // v-model 指令
        self.compileModel(node, self.vm, exp, dir);
      }
      node.removeAttribute(attrName); // 解析完毕,移除属性
    }
  });
}
// v-指令解析
function isDirective(attr) {
  return attr.indexOf("v-") == 0;
}
// on: 指令解析
function isEventDirective(dir) {
  return dir.indexOf("on:") === 0;
}

上面的 compile 函数是用于遍历当前 dom 的所有节点属性,然后判断属性是否是指令属性,如果是在做对应的处理(事件就去监听事件、数据就去监听数据..)

完整版 myVue

在 MyVue 中添加 mounted 方法,在所有操作都做完时执行。

class MyVue {
  constructor(options) {
    var self = this;
    this.data = options.data;
    this.methods = options.methods;
    Object.keys(this.data).forEach(function(key) {
      self.proxyKeys(key);
    });
    observe(this.data);
    new Compile(options.el, this);
    options.mounted.call(this); // 所有事情处理好后执行mounted函数
  }
  proxyKeys(key) {
    // 将this.data属性代理到this上
    var self = this;
    Object.defineProperty(this, key, {
      enumerable: false,
      configurable: true,
      get: function getter() {
        return self.data[key];
      },
      set: function setter(newVal) {
        self.data[key] = newVal;
      },
    });
  }
}

然后就可以测试使用了。

https://jsrun.net/Y4IKp/embed...

总结一下流程,回头在哪看一遍这个图,是不是清楚很多了。
vue2.x流程图2.png

可以查看的代码地址:Vue2.x 的双向绑定原理及实现

参考

查看原文

赞 24 收藏 10 评论 0

4fun 赞了回答 · 4月4日

闭包 JS基础 编程题

  • 根据结果是通过 f1.getValue() 来取值,说明 foo() 返回一个对象
  • 根据结果 foo(1)(...) 说明 foo() 返回一个函数,可以理解为参数只有 1 个时返回函数
  • 根据结果 foo(...)(4).getValue() 说明 foo() 在参数只有1 个的时候也可能返回对象

综上,foo() 返回一个带额外属性(.getValue)的函数。

  • 当它作为一个函数使用的时候,其行为和 foo() 是一致的,可以考虑递归调用 foo()
  • 当它作为对象的时候,需要通过 getValue() 返回计算结果,所以这个结果可以在 getValue() 中计算出来,也可以作为一个对象状态(属性)保存起来
  • 由于存在函数连调 foo()(),而且后面的调用会累加前面的结果,所以上面一条中提及的计算结果用状态保存比较好,即可以用于后面的计算,也可以通过 getValue() 返回出去

综上分析:

var foo = function (...args) {
    // 函数,是将当前状态值和参数累加
    const add = (...moreArgs) => foo(add.sum, ...moreArgs);
    
    // sum 状态是当前参数的和
    add.sum = args.reduce((s, n) => s + n, 0);
    
    add.getValue = () => add.sum;
    return add;
}

这个问题的关键点不在于闭包(当然会用到闭包),而是柯里化的过程分析。

不过如果你想深入了解闭包可以看看这篇:还搞不懂闭包算我输(JS 示例)

@circle 的答案比我的简单,更容易理解。他那个是闭包实现的答案,更符合题主要求!把那个答案改写一下,不保存参数列表,直接保存中间结果是这样:

var foo = function (...args) {
    let sum = 0;

    const add = (...more) => {
        sum = more.reduce((s, n) => s + n, sum);
        return add;
    };
    add.getValue = () => sum;

    add(...args);
    return add;
}

关注 3 回答 2

4fun 赞了回答 · 4月4日

闭包 JS基础 编程题

(function() {

    let foo = function(...args) {
        let argsArr = args

        function _add() {
            argsArr.push(...arguments)
            return _add;

        }
        _add.getValue = function() {
            let sum = argsArr.reduce(function(a, b) {
                return a + b
            })
            console.log(sum)

            return sum
        }
        return _add;
    }
    let f1 = foo(1, 2, 3)
    f1.getValue()
    let f2 = foo(1)(2, 3)
    f2.getValue()

    let f3 = foo(1)(2)(3)(4)
    f3.getValue()
}
)()

关注 3 回答 2

4fun 赞了文章 · 3月20日

Laravel学习笔记之Seeder填充数据小技巧

说明:本文主要聊一聊Laravel测试数据填充器Seeder的小技巧,同时介绍下Laravel开发插件三件套,这三个插件挺好用哦。同时,作者会将开发过程中的一些截图和代码黏上去,提高阅读效率。

备注:在设计个人博客软件时,总会碰到有分类Category、博客Post、给博客贴的标签Tag、博客内容的评论Comment。
而且,Category与Post是一对多关系One-Many:一个分类下有很多Post,一个Post只能归属于一个Category;Post与Comment是一对多关系One-Many:一篇博客Post下有很多Comment,一条Comment只能归属于一篇Post;Post与Tag是多对多关系Many-Many:一篇Post有很多Tag,一个Tag下有很多Post。
开发环境:Laravel5.2 + MAMP + PHP7 + MySQL5.5

开发插件三件套

在先聊测试数据填充器seeder之前,先装上开发插件三件套,开发神器。先不管这能干些啥,装上再说。
1、barryvdh/laravel-debugbar

composer require barryvdh/laravel-debugbar --dev

2、barryvdh/laravel-ide-helper

composer require barryvdh/laravel-ide-helper --dev

3、mpociot/laravel-test-factory-helper

composer require mpociot/laravel-test-factory-helper --dev

然后在config/app.php文件中填上:

        /**
         *Develop Plugin
        */
        Barryvdh\Debugbar\ServiceProvider::class,
        Mpociot\LaravelTestFactoryHelper\TestFactoryHelperServiceProvider::class,
        Barryvdh\LaravelIdeHelper\IdeHelperServiceProvider::class,

设计表的字段和关联

设计字段

按照上文提到的Category、Post、Comment和Tag之间的关系创建迁移Migration和模型Model,在项目根目录输入:

php artisan make:model Category -m
php artisan make:model Post -m
php artisan make:model Comment -m
php artisan make:model Tag -m

在各个表的迁移migrations文件中根据表的功能设计字段:

//Category表
class CreateCategoriesTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('categories', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->comment('分类名称');
            $table->integer('hot')->comment('分类热度');
            $table->string('image')->comment('分类图片');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('categories');
    }
}
    
//Post表
class CreatePostsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('posts', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('category_id')->unsigned()->comment('外键');
            $table->string('title')->comment('标题');
            $table->string('slug')->unique()->index()->comment('锚点');
            $table->string('summary')->comment('概要');
            $table->text('content')->comment('内容');
            $table->text('origin')->comment('文章来源');
            $table->integer('comment_count')->unsigned()->comment('评论次数');
            $table->integer('view_count')->unsigned()->comment('浏览次数');
            $table->integer('favorite_count')->unsigned()->comment('点赞次数');
            $table->boolean('published')->comment('文章是否发布');
            $table->timestamps();
            //Post表中category_id字段作为外键,与Category一对多关系
            $table->foreign('category_id')
                  ->references('id')
                  ->on('categories')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        //删除表时要删除外键约束,参数为外键名称
        Schema::table('posts', function(Blueprint $tabel){
            $tabel->dropForeign('posts_category_id_foreign');
        });
        Schema::drop('posts');
    }
}

//Comment表
class CreateCommentsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('comments', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned()->comment('外键');
            $table->integer('parent_id')->comment('父评论id');
            $table->string('parent_name')->comment('父评论标题');
            $table->string('username')->comment('评论者用户名');
            $table->string('email')->comment('评论者邮箱');
            $table->string('blog')->comment('评论者博客地址');
            $table->text('content')->comment('评论内容');
            $table->timestamps();
            //Comment表中post_id字段作为外键,与Post一对多关系
            $table->foreign('post_id')
                  ->references('id')
                  ->on('posts')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
    //删除表时要删除外键约束,参数为外键名称
        Schema::table('comments', function(Blueprint $tabel){
            $tabel->dropForeign('comments_post_id_foreign');
        });
        Schema::drop('comments');
    }
}

//Tag表
class CreateTagsTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('tags', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name')->comment('标签名称');
            $table->integer('hot')->unsigned()->comment('标签热度');
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::drop('tags');
    }
}

由于Post表与Tag表是多对多关系,还需要一张存放两者关系的表:

//多对多关系,中间表的命名laravel默认按照两张表字母排序来的,写成tag_post会找不到中间表
php artisan make:migration create_post_tag_table --create=post_tag

然后填上中间表的字段:

class CreatePostTagTable extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('post_tag', function (Blueprint $table) {
            $table->increments('id');
            $table->integer('post_id')->unsigned();
            $table->integer('tag_id')->unsigned();
            $table->timestamps();
            //post_id字段作为外键
            $table->foreign('post_id')
                  ->references('id')
                  ->on('posts')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
            //tag_id字段作为外键      
            $table->foreign('tag_id')
                  ->references('id')
                  ->on('tags')
                  ->onUpdate('cascade')
                  ->onDelete('cascade');
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::table('post_tag', function(Blueprint $tabel){
            $tabel->dropForeign('post_tag_post_id_foreign');
            $tabel->dropForeign('post_tag_tag_id_foreign');
        });
        Schema::drop('post_tag');
    }
}

设计关联

写上Migration后,还得在Model里写上关联:

class Category extends Model
{
    //Category-Post:One-Many
    public function posts()
    {
        return $this->hasMany(Post::class);
    }
}

class Post extends Model
{
    //Post-Category:Many-One
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    //Post-Comment:One-Many
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    //Post-Tag:Many-Many
    public function tags()
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }
}

class Comment extends Model
{
    //Comment-Post:Many-One
    public function post()
    {
        return $this->belongsTo(Post::class);
    }
}

class Tag extends Model
{
    //Tag-Post:Many-Many
    public function posts()
    {
        return $this->belongsToMany(Post::class)->withTimestamps();
    }
}

然后执行迁移:

php artisan migrate

数据库中会生成新建表,表的关系如下:
图片描述

Seeder填充测试数据

好,在聊到seeder测试数据填充之前,看下开发插件三件套能干些啥,下文中命令可在项目根目录输入php artisan指令列表中查看。
1、barryvdh/laravel-ide-helper
执行php artisan ide-helper:generate指令前:
图片描述
执行php artisan ide-helper:generate指令后:
图片描述
不仅Facade模式的Route由之前的反白了变为可以定位到源码了,而且输入Config Facade时还方法自动补全auto complete,这个很方便啊。

输入指令php artisan ide-helper:models后,看看各个Model,如Post这个Model:

<?php

namespace App;

use Illuminate\Database\Eloquent\Model;

/**
 * App\Post
 *
 * @property integer $id
 * @property integer $category_id 外键
 * @property string $title 标题
 * @property string $slug 锚点
 * @property string $summary 概要
 * @property string $content 内容
 * @property string $origin 文章来源
 * @property integer $comment_count 评论次数
 * @property integer $view_count 浏览次数
 * @property integer $favorite_count 点赞次数
 * @property boolean $published 文章是否发布
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read \App\Category $category
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Comment[] $comments
 * @property-read \Illuminate\Database\Eloquent\Collection|\App\Tag[] $tags
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCategoryId($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereTitle($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereSlug($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereSummary($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereContent($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereOrigin($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCommentCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereViewCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereFavoriteCount($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post wherePublished($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereCreatedAt($value)
 * @method static \Illuminate\Database\Query\Builder|\App\Post whereUpdatedAt($value)
 * @mixin \Eloquent
 */
class Post extends Model
{
    //Post-Category:Many-One
    public function category()
    {
        return $this->belongsTo(Category::class);
    }

    //Post-Comment:One-Many
    public function comments()
    {
        return $this->hasMany(Comment::class);
    }

    //Post-Tag:Many-Many
    public function tags()
    {
        return $this->belongsToMany(Tag::class)->withTimestamps();
    }
}

根据迁移到库里的表生成字段属性和对应的方法提示,在控制器里输入方法时会自动补全auto complete字段属性的方法:
图片描述

2、mpociot/laravel-test-factory-helper
输入指令php artisan test-factory-helper:generate后,database/factory/ModelFactory.php模型工厂文件会自动生成各个模型对应字段数据。Faker是一个好用的生成假数据的第三方库,而这个开发插件会自动帮你生成这些属性,不用自己写了。

<?php

/*
|--------------------------------------------------------------------------
| Model Factories
|--------------------------------------------------------------------------
|
| Here you may define all of your model factories. Model factories give
| you a convenient way to create models for testing and seeding your
| database. Just tell the factory how a default model should look.
|
*/

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->safeEmail,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});
$factory->define(App\Category::class, function (Faker\Generator $faker) {
    return [
        'name' =>  $faker->name ,
        'hot' =>  $faker->randomNumber() ,
        'image' =>  $faker->word ,
    ];
});

$factory->define(App\Comment::class, function (Faker\Generator $faker) {
    return [
        'post_id' =>  function () {
             return factory(App\Post::class)->create()->id;
        } ,
        'parent_id' =>  $faker->randomNumber() ,
        'parent_name' =>  $faker->word ,
        'username' =>  $faker->userName ,
        'email' =>  $faker->safeEmail ,
        'blog' =>  $faker->word ,
        'content' =>  $faker->text ,
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'category_id' =>  function () {
             return factory(App\Category::class)->create()->id;
        } ,
        'title' =>  $faker->word ,
        'slug' =>  $faker->slug ,//修改为slug
        'summary' =>  $faker->word ,
        'content' =>  $faker->text ,
        'origin' =>  $faker->text ,
        'comment_count' =>  $faker->randomNumber() ,
        'view_count' =>  $faker->randomNumber() ,
        'favorite_count' =>  $faker->randomNumber() ,
        'published' =>  $faker->boolean ,
    ];
});

$factory->define(App\Tag::class, function (Faker\Generator $faker) {
    return [
        'name' =>  $faker->name ,
        'hot' =>  $faker->randomNumber() ,
    ];
});

在聊第三个debugbar插件前先聊下seeder小技巧,用debugbar来帮助查看。Laravel官方推荐使用模型工厂自动生成测试数据,推荐这么写的:

//先输入指令生成database/seeds/CategoryTableSeeder.php文件: php artisan make:seeder CategoryTableSeeder
<?php

use Illuminate\Database\Seeder;

class CategoryTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        factory(\App\Category::class, 5)->create()->each(function($category){
            $category->posts()->save(factory(\App\Post::class)->make());
        });
    }
}
//然后php artisan db:seed执行数据填充

但是这种方式效率并不高,因为每一次create()都是一次query,而且每生成一个Category也就对应生成一个Post,当然可以在each()里每一次Category继续foreach()生成几个Post,但每一次foreach也是一次query,效率更差。可以用debugbar小能手看看。先在DatabaseSeeder.php文件中填上这次要填充的Seeder:

    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(CategoryTableSeeder::class);
    }

在路由文件中写上:

Route::get('/artisan', function () {
    $exitCode = Artisan::call('db:seed');
    return $exitCode;
});

输入路由/artisan后用debugbar查看执行了15次query,耗时7.11ms:
图片描述

实际上才刚刚输入几个数据呢,Category插入了10个,Post插入了5个。
可以用DB::table()->insert()批量插入,拷贝ModelFactory.php中表的字段定义放入每一个表对应Seeder,当然可以有些字段为便利也适当修改对应假数据。

class CategoryTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
//        factory(\App\Category::class, 20)->create()->each(function($category){
//            $category->posts()->save(factory(\App\Post::class)->make());
//        });

        $faker = Faker\Factory::create();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'name' =>  'category'.$faker->randomNumber() ,
                'hot' =>  $faker->randomNumber() ,
                'image' =>  $faker->url ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('categories')->insert($datas);
    }
}

class PostTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $category_ids = \App\Category::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'category_id' => $faker->randomElement($category_ids),
                'title' =>  $faker->word ,
                'slug' =>  $faker->slug ,
                'summary' =>  $faker->word ,
                'content' =>  $faker->text ,
                'origin' =>  $faker->text ,
                'comment_count' =>  $faker->randomNumber() ,
                'view_count' =>  $faker->randomNumber() ,
                'favorite_count' =>  $faker->randomNumber() ,
                'published' =>  $faker->boolean ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('posts')->insert($datas);
    }
}

class CommentTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $post_ids = \App\Post::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 50) as $key => $value) {
            $datas[] = [
                'post_id' => $faker->randomElement($post_ids),
                'parent_id' =>  $faker->randomNumber() ,
                'parent_name' =>  $faker->word ,
                'username' =>  $faker->userName ,
                'email' =>  $faker->safeEmail ,
                'blog' =>  $faker->word ,
                'content' =>  $faker->text ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('comments')->insert($datas);
    }
}

class TagTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $datas = [];
        foreach (range(1, 10) as $key => $value) {
            $datas[] = [
                'name' =>  'tag'.$faker->randomNumber() ,
                'hot' =>  $faker->randomNumber() ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('tags')->insert($datas);
    }
}

class PostTagTableSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $faker = Faker\Factory::create();
        $post_ids = \App\Post::lists('id')->toArray();
        $tag_ids = \App\Tag::lists('id')->toArray();
        $datas = [];
        foreach (range(1, 20) as $key => $value) {
            $datas[] = [
                'post_id' =>  $faker->randomElement($post_ids) ,
                'tag_id' =>  $faker->randomElement($tag_ids) ,
                'created_at' => \Carbon\Carbon::now()->toDateTimeString(),
                'updated_at' => \Carbon\Carbon::now()->toDateTimeString()
            ];
        }
        DB::table('post_tag')->insert($datas);
    }
}

在DatabaseSeeder.php中按照顺序依次填上Seeder,顺序不能颠倒,尤其有关联关系的表:

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);
        $this->call(CategoryTableSeeder::class);
        $this->call(PostTableSeeder::class);
        $this->call(CommentTableSeeder::class);
        $this->call(TagTableSeeder::class);
        $this->call(PostTagTableSeeder::class);
    }
}

输入路由/artisan后,生成了10个Category、10个Post、50个Comments、10个Tag和PostTag表中多对多关系,共有9个Query耗时13.52ms:
图片描述

It is working!!!

表的迁移Migration和关联Relationship都已设计好,测试数据也已经Seeder好了,就可以根据Repository模式来设计一些数据库逻辑了。准备趁着端午节研究下Repository模式的测试,PHPUnit结合Mockery包来TDD测试也是一种不错的玩法。
M(Model)-V(View)-C(Controller)模式去组织代码,很多时候也未必指导性很强,给Model加一个Repository,给Controller加一个Service,给View加一个Presenter,或许代码结构更清晰。具体可看下面分享的一篇文章。

最近一直在给自己充电,研究MySQL,PHPUnit,Laravel,上班并按时打卡,看博客文章,每天喝红牛。很多不会,有些之前没咋学过,哎,头疼。后悔以前读书太少,书到用时方恨少,人丑还需多读书。

研究生学习机器人的,本打算以后读博搞搞机器人的(研一时真是这么想真是这么准备的,too young too simple)。现在做PHP小码农了,只因当时看到智能机就激动得不行,决定以后做个码农试试吧,搞不好是条生路,哈哈。读书时觉悟太晚,耗费了青春,其实我早该踏入这条路的嘛,呵呵。Follow My Heart!

不扯了,在凌晨两点边听音乐边写博客,就容易瞎感慨吧。。

分享下最近发现的一张好图和一篇极赞的文章:

图片描述

文章链接:Laravel的中大型專案架構

欢迎关注Laravel-China

RightCapital招聘Laravel DevOps

查看原文

赞 26 收藏 63 评论 3

4fun 赞了文章 · 3月13日

CSS 故障艺术

v2-c7a2db16babbdefec756d3eed6bc0fa4_hd

本文的主题是 Glitch Art,故障艺术。

什么是故障艺术?我们熟知的抖音的 LOGO 正是故障艺术其中一种表现形式。它有一种魔幻的感觉,看起来具有闪烁、震动的效果,很吸引人眼球。

故障艺术它模拟了画面信号出现故障导致成像错误的感觉。青色色块与红色色块无法重合就是这种故障的体现。从胶片时代开始到今天的数码时代,这种故障一直是观众非常熟悉的现象。即使抖音的 LOGO 只是静态的,大脑也会自己补完整个效果,甚至还会自己脑补信号干扰的噪音。

当然,广义的故障艺术不仅仅指这种效果,我觉得是很宽泛的,本文将介绍一些 CSS 能够模拟完成的故障艺术效果。

使用混合模式实现抖音 LOGO

首先从静态的开始,抖音的 LOGO 就是很好的一个例子。

image

它看着像是 3 个 J 形重叠在一起。而实际上,是两个 J 形重叠在一起,重叠部分表现为白色,这个特性,使用 CSS 的混合模式 mix-blend-mode 非常好实现,而单个 J 形示意图如下:

image

图片来源于知乎:为什么抖音的标志,看起来具有“电”“闪烁”“震动”的感觉?

单个 J 形其实是由 3/4圆 + 竖线 + 1/4圆组成,使用一个标签即可完成(加上两个伪元素)。

关键点

  • 主要借助伪元素实现了整体 J 结构,借助了 mix-blend-mode 实现融合效果
  • 利用 mix-blend-mode: lighten 混合模式实现两个 J 形结构重叠部分为白色

所以整个效果只需要两个标签:

<div class="j"></div>
<div class="j"></div>

简易 SASS 代码:

// 实现第一个 J
.j {
    position: absolute;

    &::before {
        content: "";
        ...
    }

    &::after {
        content: "";
        ...
    }
}

// 实现第二个 J,加上混合模式
.j:last-child {
    position: absolute;
    mix-blend-mode: lighten;

    &::before {
        content: "";
        ...
    }
    &::after {
        content: "";
        ...
    }
}

示意图如下(为了更好理解,加上了动画):

tiktok

完整的 DEMO:

使用 mix-blend-mode 实现抖音 LOGO

图片的 Glitch Art 风

当然,上面实现的是我们实现的 J 形的叠加,理解了这种技巧之后,我们可以把它运用到图片之上。

这里我们会运用到 background-blend-modemix-blend-mode

假设,我们有这样一张图:

image

只需要一个标签即可

<div class="mix"></div>

给两张同样的图片,叠加上 青色#0ff 和 红色#f00,并且错开一定的距离,两张图都要加上 background-blend-mode: lighten,其中一张再加上 mix-blend-mode: darken

.mix {
    width: 400px;
    height: 400px;
    background: url($img), #0ff;
    background-blend-mode: lighten;

  &::after {
    content: '';
    position: absolute;
    margin-left: 10px;
    width: 400px;
    height: 400px;
    background: url($img), #f00;
    background-blend-mode: lighten;
    mix-blend-mode: darken;
  }
}

得到如下效果:

image

这里与上述抖音 LOGO 的处理是有点不一样的,使用的混合模式也不止一种,简单解释下。

  1. 因为图片本身不是红色和青色的,所以需要通过 background-image 叠加上这两种颜色,并通过 background-blend-mode: lighten 让其表现出来
  2. 为了保持中间叠加部分的原色,需要再叠加一个 mix-blend-mode: darken 反向处理一下。(不理解的同学可以打开调试,手动关掉几个混合模式,自己感受感受即可)

完整的 DEMO:

图片的类抖音 LOGO Glitch 效果

动态类抖音风格 Glitch 效果

OK,有了上面的铺垫,我们接下来可以给这种效果加上动画。

关键点

  • 利用 mix-blend-mode: lighten 混合模式实现两段文字结构重叠部分为白色
  • 利用元素位移完成错位移动动画,形成视觉上的冲击效果

看看效果:

textglitch

本文篇幅有点长,代码就不上了,完整 DEMO 在这里:

类抖音 LOGO 文字故障效果

当然,我们也不是一定要使用混合模式去使得融合部分为白色,可以仅仅是使用这个配色效果,基于上面效果的另外一个版本,没有使用混合模式。

关键点

  • 利用了伪元素生成了文字的两个副本
  • 视觉效果由位移、遮罩、混合模式完成
  • 配色借鉴了抖音 LOGO 的风格

textglitch2

完整 DEMO 在这里:

CSS文字故障效果

仅仅使用配色没有使用混合模式的好处在于,对于每一个文字的副本,有了更大的移动距离和可以处理的空间。

Glitch Art 风格的 404 效果

稍微替换一下文本文案为 404,再添加上一些滤镜效果(hue-rotate()blur())嘿嘿,找到了一个可能实际可用的场景:

效果一:

404

效果二:

404

两个 404 效果的 Demo 如下:

其他配色效果

当然,不仅仅只有这一种红 + 青的配色效果。还有一些其他的配色及混合模式的搭配,如 黄 + 粉红 + 蓝配合 mix-blend-mode: multiply

然后,有的时候,效果不希望和背景混合在一起,可以使用 isolation: isolate 进行隔离。


好,上述效果可以归类为一个分类。接下来开启下一个分类

clip-path 登场

下半篇幅的主角主要是 clip-path

clip-path 一个非常有意思的 CSS 属性。

clip-path CSS 属性可以创建一个只有元素的部分区域可以显示的剪切区域。区域内的部分显示,区域外的隐藏。剪切区域是被引用内嵌的URL定义的路径或者外部 SVG 的路径。

也就是说,使用 clip-path 可以将一个容器切成我们想要的样子。

例如这样:

<div>TXET</div>
div  {
    margin: auto;
    padding: 10px;
    line-height: 1.2;
    font-size: 60px;
    background: #ddd;
}

正常是这样的:

image

使用 clip-path 剪裁为一个平行四边形:

div  {
    margin: auto;
    padding: 10px;
    line-height: 1.2;
    font-size: 60px;
    background: #ddd;
+   clip-path: polygon(35% 0, 85% 0, 75% 100%, 25% 100%);
}

结果如下:

image

那么,思路就有了,我们可以将一个文字复制几个副本,重叠在一起,再分别裁剪这几个副本进行位移动画即可。

使用 clip-path 实现文字断裂动画

我们还是使用元素的 ::before::after 两个伪元素复制两份副本,再分别使用 clip-path 进行剪裁,再使用 transform 进行控制。

核心代码:

<div data-text="Text Crack">
    <span>Text Crack</span>
</div>
div {
    position: relative;
    animation: shake 2.5s linear forwards;
}

div span {
    clip-path: polygon(10% 0%, 44% 0%, 70% 100%, 55% 100%);
}

div::before,
div::after {
    content: attr(data-text);
    position: absolute;
    top: 0;
    left: 0;
}

div::before {
    animation: crack1 2.5s linear forwards;
    clip-path: polygon(0% 0%, 10% 0%, 55% 100%, 0% 100%);
}

div::after {
    animation: crack2 2.5s linear forwards;
    clip-path: polygon(44% 0%, 100% 0%, 100% 100%, 70% 100%);
}

// 元素晃动,断裂前摇
@keyframes shake {    
    ...
}

@keyframes crack1 {
    0%,
    95% {
        transform: translate(-50%, -50%);
    }

    100% {
        transform: translate(-55%, -45%);
    }
}

@keyframes crack2 {
    0%,
    95% {
        transform: translate(-50%, -50%);
    }

    100% {
        transform: translate(-45%, -55%);
    }
}

可以得到这样的效果:

textcrack

完整的 Demo:

clip-path 实现文字断裂效果

这个效果,最早的版本见于这位作者:George W. Park

clip-path 的 Glitch Art

OK,继续,有了上面的铺垫之后,接下来,我们把这个效果作用于图片之上,并且再添加上动画。

随便选一张图片:

image

哇哦,非常的赛博朋克。

实现动画的关键在于:

  • 使用元素的两个伪元素,生成图片的两个副本
  • 使用 clip-path 对两个副本图片元素进行裁剪,然后进行位移、transform变换、添加滤镜等一系列操作。

简单贴一下伪代码:

<div></div>
$img: "https://mzz-files.oss-cn-shenzhen.aliyuncs.com///uploads/U1002433/0cb5e044a1f0f7fc15f61264ee97ac1f.png";

div {
    position: relative;
    width: 658px;
    height: 370px;
    background: url($img) no-repeat;
    animation: main-img-hide 16s infinite step-end;
}

div::before,
div::after {
    position: absolute;
    width: 658px;
    height: 370px;
    top: 0;
    left: 0;
    background: inherit;
}

div::after {
    content: "";
    animation: glitch-one 16s infinite step-end;
}

div::before {
    content: "";
    animation: glitch-two 16s infinite 1s step-end;
}

@keyframes glitch-one {
    @for $i from 20 to 30 {
        #{$i / 2}% {
            left: #{randomNum(200, -100)}px;
            clip-path: inset(#{randomNum(150, 30)}px 0 #{randomNum(150, 30)}px);
        }
    }

    15.5% {
        clip-path: inset(10px 0 320px);
        left: -20px;
    }
    16% {
        clip-path: inset(10px 0 320px);
        left: -10px;
        opacity: 0;
    }
    ....
}

@keyframes glitch-two {
    @for $i from 40 to 50 {
        #{$i / 2}% {
            left: #{randomNum(200, -100)}px;
            clip-path: inset(#{randomNum(180)}px 0 #{randomNum(180)}px);
        }
    }

    25.5% {
        clip-path: inset(10px 0 320px);
        left: -20px;
    }
    26% {
        clip-path: inset(10px 0 320px);
        left: -10px;
        opacity: 0;
    }
   ...
}

@keyframes main-img-hide {
    5% {
        filter: invert(1);
    }
    ...
}

由于动画部分代码量太多,所以使用了 SASS 循环函数随机生成了部分。如果手动控制,效果其实还会更好,当然,调试动画消耗的时间会更多。

看看效果,虽然 CSS 能力有限,但实际的效果也不是说那么的差:

imgglitch

GIF 图太大,掉帧太多,效果大打折扣。完整的 Demo 及效果,你可以戳这里:

clip-path 实现图片的故障艺术风格动画

总结

本文重点介绍了纯 CSS 下使用混合模式和 clip-path 实现的一些故障艺术(Glitch Art),当然,上述的几个效果都不仅仅是靠这两个属性单打独斗就能完成的。

在其中,transformfilter 也在其中发挥了很重要的作用。当然仅仅使用 transformfilter 也能够实现一些基础的故障艺术效果,这个读者们感兴趣的可以自己多加尝试。如果想使用于生产环境,需要考虑 mix-blend-modeclip-path 的兼容性问题。

我自己对 Glitch Art 的理解其实也比较浅显,只是觉得使用 CSS 去实现这样一些类似的效果比较有意思,就动手尝试实践了一番,相关术语或者名词理解错误烦请各位谅解指出。

最后

好了,本文到此结束,希望对你有帮助 :)

更多精彩 CSS 技术文章汇总在我的 Github -- iCSS ,持续更新,欢迎点个 star 订阅收藏。

更多精彩有趣的 CSS 效果,欢迎来这里看看 CSS 灵感 -- 在这里找到写 CSS 的灵感

如果还有什么疑问或者建议,可以多多交流,原创文章,文笔有限,才疏学浅,文中若有不正之处,万望告知。

查看原文

赞 57 收藏 36 评论 5

4fun 关注了用户 · 3月13日

chokcoco @chokcoco

坎坷切图仔

关注 295

认证与成就

  • 获得 100 次点赞
  • 获得 27 枚徽章 获得 1 枚金徽章, 获得 11 枚银徽章, 获得 15 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-06-05
个人主页被 2.7k 人浏览