后知后觉

后知后觉 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

后知后觉 回答了问题 · 2020-08-25

vue动态绑定:class,值改变,但并未动态添加类目

:class='{ "active" : result.isCollection}'

关注 5 回答 3

后知后觉 赞了文章 · 2020-08-19

学习Vue3.0,先来了解一下Proxy

产品经理身旁过,需求变更逃不过。
测试姐姐眯眼笑,今晚bug必然多。

据悉Vue3.0的正式版将要在本月(8月)发布,从发布到正式投入到正式项目中,还需要一定的过渡期,但我们不能一直等到Vue3正式投入到项目中的时候才去学习,提前学习,让你更快一步掌握Vue3.0,升职加薪迎娶白富美就靠它了。不过在学习Vue3之前,还需要先了解一下Proxy,它是Vue3.0实现数据双向绑定的基础。

本文是作者关于Vue3.0系列的第一篇文章,后续作者将会每周发布一篇Vue3.0相关,如果喜欢,麻烦给小编一个赞,谢谢

了解代理模式

一个例子

作为一个单身钢铁直男程序员,小王最近逐渐喜欢上了前台小妹,不过呢,他又和前台小妹不熟,所以决定委托与前端小妹比较熟的UI小姐姐帮忙给自己搭桥引线。小王于是请UI小姐姐吃了一顿大餐,然后拿出一封情书委托它转交给前台小妹,情书上写的 我喜欢你,我想和你睡觉,不愧钢铁直男。不过这样写肯定是没戏的,UI小姐姐吃人嘴短,于是帮忙改了情书,改成了我喜欢你,我想和你一起在晨辉的沐浴下起床,然后交给了前台小妹。虽然有没有撮合成功不清楚啊,不过这个故事告诉我们,小王活该单身狗。

其实上面就是一个比较典型的代理模式的例子,小王想给前台小妹送情书,因为不熟所以委托UI小姐姐UI小姐姐相当于代理人,代替小王完成了送情书的事情。

引申

通过上面的例子,我们想想Vue的数据响应原理,比如下面这段代码


const xiaowang = {
  love: '我喜欢你,我想和你睡觉'
}
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '流氓,滚'
}
console.log(sendToMyLove(xiaowang))

如果没有UI小姐姐代替送情书,显示结局是悲惨的,想想Vue2.0的双向绑定,通过Object.defineProperty来监听的属性 get,set方法来实现双向绑定,这个Object.defineProperty就相当于UI小姐姐

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
// UI小姐姐代理
Object.defineProperty(xiaowang,'love', {
  get() {
    return xiaowang.loveLetter.replace('睡觉','一起在晨辉的沐浴下起床')
  }
})

// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.love)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(xiaowang))

虽然依然是一个悲惨的故事,因为送奔驰的成功率可能会更高一些。但是我们可以看到,通过Object.defineproperty可以对对象的已有属性进行拦截,然后做一些额外的操作。

存在的问题

Vue2.0中,数据双向绑定就是通过Object.defineProperty去监听对象的每一个属性,然后在get,set方法中通过发布订阅者模式来实现的数据响应,但是存在一定的缺陷,比如只能监听已存在的属性,对于新增删除属性就无能为力了,同时无法监听数组的变化,所以在Vue3.0中将其换成了功能更强大的Proxy

了解Proxy

ProxyES6新推出的一个特性,可以用它去拦截js操作的方法,从而对这些方法进行代理操作。

用Proxy重写上面的例子

比如我们可以通过Proxy对上面的送情书情节进行重写:

const xiaowang = {
  loveLetter: '我喜欢你,我想和你睡觉'
}
const proxy = new Proxy(xiaowang, {
  get(target,key) {
    if(key === 'loveLetter') {
      return target[key].replace('睡觉','一起在晨辉的沐浴下起床')
    }
  }
})
// 送给小姐姐情书
function sendToMyLove(obj) {
    console.log(obj.loveLetter)
    return '小伙子还挺有诗情画意的么,不过老娘不喜欢,滚'
}
console.log(sendToMyLove(proxy))

再看这样一个场景

请分别使用Object.definePropertyProxy完善下面的代码逻辑.

function observe(obj, callback) {}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.sex = '女'

看了上面的代码,希望大家可以先自行实现以下,下面我们分别用Object.definePropertyProxy去实现上面的逻辑.

  1. 使用Object.defineProperty
/**
 * 请实现这个函数,使下面的代码逻辑正常运行
 * @param {*} obj 对象
 * @param {*} callback 回调函数
 */
function observe(obj, callback) {
  const newObj = {}
  Object.keys(obj).forEach(key => {
    Object.defineProperty(newObj, key, {
      configurable: true,
      enumerable: true,
      get() {
        return obj[key]
      },
      // 当属性的值被修改时,会调用set,这时候就可以在set里面调用回调函数
      set(newVal) {
        obj[key] = newVal
        callback(key, newVal)
      }
    })
  })
  return newObj
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'
  1. 使用Proxy
function observe(obj, callback) {
  return new Proxy(obj, {
    get(target, key) {
      return target[key]
    },
    set(target, key, value) {
      target[key] = value
      callback(key, value)
    }
  })
}

const obj = observe(
  {
    name: '子君',
    sex: '男'
  },
  (key, value) => {
    console.log(`属性[${key}]的值被修改为[${value}]`)
  }
)

// 这段代码执行后,输出 属性[name]的值被修改为[妹纸]
obj.name = '妹纸'

// 这段代码执行后,输出 属性[sex]的值被修改为[女]
obj.name = '女'

通过上面两种不同实现方式,我们可以大概的了解到Object.definePropertyProxy的用法,但是当给对象添加新的属性的时候,区别就出来了,比如

// 添加公众号字段
obj.gzh = '前端有的玩'

使用Object.defineProperty无法监听到新增属性,但是使用Proxy是可以监听到的。对比上面两段代码可以发现有以下几点不同

  • Object.defineProperty监听的是对象的每一个属性,而Proxy监听的是对象自身
  • 使用Object.defineProperty需要遍历对象的每一个属性,对于性能会有一定的影响
  • Proxy对新增的属性也能监听到,但Object.defineProperty无法监听到。

初识Proxy

概念与语法

MDN中,关于Proxy是这样介绍的: Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。什么意思呢?Proxy就像一个拦截器一样,它可以在读取对象的属性,修改对象的属性,获取对象属性列表,通过for in循环等等操作的时候,去拦截对象上面的默认行为,然后自己去自定义这些行为,比如上面例子中的set,我们通过拦截默认的set,然后在自定义的set里面添加了回调函数的调用

Proxy的语法格式如下

/**
* target: 要兼容的对象,可以是一个对象,数组,函数等等
* handler: 是一个对象,里面包含了可以监听这个对象的行为函数,比如上面例子里面的`get`与`set`
* 同时会返回一个新的对象proxy, 为了能够触发handler里面的函数,必须要使用返回值去进行其他操作,比如修改值
*/
const proxy = new Proxy(target, handler)

在上面的例子里面,我们已经使用到了handler里面提供的getset方法了,接下来我们一一看一下handler里面的方法。

handler 里面的方法列表

handler里面的方法可以有以下这十三个,每一个都对应的一种或多种针对proxy代理对象的操作行为

  1. handler.get

    当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

  2. handler.set

    当通过proxy去为对象设置修改属性的时候,会进入到set钩子函数里面

  3. handler.has

    当使用in判断属性是否在proxy代理对象里面时,会触发has,比如

    const obj = {
      name: '子君'
    }
    console.log('name' in obj)
  4. handler.deleteProperty

    当使用delete去删除对象里面的属性的时候,会进入deleteProperty`钩子函数

  5. handler.apply

    proxy监听的是一个函数的时候,当调用这个函数时,会进入apply钩子函数

  6. handle.ownKeys

    当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数

  7. handler.construct

    当使用new操作符的时候,会进入construct这个钩子函数

  8. handler.defineProperty

    当使用Object.defineProperty去修改属性修饰符的时候,会进入这个钩子函数

  9. handler.getPrototypeOf

    当读取对象的原型的时候,会进入这个钩子函数

  10. handler.setPrototypeOf

    当设置对象的原型的时候,会进入这个钩子函数

  11. handler.isExtensible

    当通过Object.isExtensible去判断对象是否可以添加新的属性的时候,进入这个钩子函数

  12. handler.preventExtensions

    当通过Object.preventExtensions去设置对象不可以修改新属性时候,进入这个钩子函数

  13. handler.getOwnPropertyDescriptor

    在获取代理对象某个属性的属性描述时触发该操作,比如在执行 Object.getOwnPropertyDescriptor(proxy, "foo") 时会进入这个钩子函数

Proxy提供了十三种拦截对象操作的方法,本文主要挑选其中一部分在Vue3中比较重要的进行说明,其余的建议可以直接阅读MDN关于Proxy的介绍。

详细介绍

get

当通过proxy去读取对象里面的属性的时候,会进入到get钩子函数里面

当我们从一个proxy代理上面读取属性的时候,就会触发get钩子函数,get函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要访问的属性名称
 * receiver: receiver相当于是我们要读取的属性的this,一般情况
 *           下他就是proxy对象本身,关于receiver的作用,后文将具体讲解
 */
handle.get(target,key, receiver)
示例

我们在工作中经常会有封装axios的需求,在封装过程中,也需要对请求异常进行封装,比如不同的状态码返回的异常信息是不同的,如下是一部分状态码及其提示信息:

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

// 使用方式
const code = 404
const message = errorMessage[code]
console.log(message)

但这存在一个问题,状态码很多,我们不可能每一个状态码都去枚举出来,所以对于一些异常状态码,我们希望可以进行统一提示,如提示为系统异常,请联系管理员,这时候就可以使用Proxy对错误信息进行代理处理

// 状态码提示信息
const errorMessage = {
  400: '错误请求',
  401: '系统未授权,请重新登录',
  403: '拒绝访问',
  404: '请求失败,未找到该资源'
}

const proxy = new Proxy(errorMessage, {
  get(target,key) {
    const value = target[key]
    return value || '系统异常,请联系管理员'
  }
})

// 输出 错误请求
console.log(proxy[400])
// 输出 系统异常,请联系管理员
console.log(proxy[500])

set

当为对象里面的属性赋值的时候,会触发set

当给对象里面的属性赋值的时候,会触发set,set函数的结构如下

/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要赋值的属性名称
 * value: 目标属性要赋的新值
 * receiver: 与 get的receiver 基本一致
 */
handle.set(target,key,value, receiver)
示例

某系统需要录入一系列数值用于数据统计,但是在录入数值的时候,可能录入的存在一部分异常值,对于这些异常值需要在录入的时候进行处理, 比如大于100的值,转换为100, 小于0的值,转换为0, 这时候就可以使用proxyset,在赋值的时候,对数据进行处理

const numbers = []
const proxy = new Proxy(numbers, {
  set(target,key,value) {
    if(value < 0) {
      value = 0
    }else if(value > 100) {
      value = 100
    }
    target[key] = value
    // 对于set 来说,如果操作成功必须返回true, 否则会被视为失败
    return true
  }
})

proxy.push(1)
proxy.push(101)
proxy.push(-10)
// 输出 [1, 100, 0]
console.log(numbers)
对比Vue2.0

在使用Vue2.0的时候,如果给对象添加新属性的时候,往往需要调用$set, 这是因为Object.defineProperty只能监听已存在的属性,而新增的属性无法监听,而通过$set相当于手动给对象新增了属性,然后再触发数据响应。但是对于Vue3.0来说,因为使用了Proxy, 在他的set钩子函数中是可以监听到新增属性的,所以就不再需要使用$set

const obj = {
  name: '子君'
}
const proxy = new Proxy(obj, {
  set(target,key,value) {
    if(!target.hasOwnProperty(key)) {
      console.log(`新增了属性${key},值为${value}`)
    }
    target[key] = value
    return true
  }
})
// 新增 公众号 属性
// 输出 新增了属性gzh,值为前端有的玩
proxy.gzh = '前端有的玩'

has

当使用in判断属性是否在proxy代理对象里面时,会触发has
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要判断的key是否在target中
 */
 handle.has(target,key)
示例

一般情况下我们在js中声明私有属性的时候,会将属性的名字以_开头,对于这些私有属性,是不需要外部调用,所以如果可以隐藏掉是最好的,这时候就可以通过has在判断某个属性是否在对象时,如果以_开头,则返回false

const obj =  {
  publicMethod() {},
  _privateMethod(){}
}
const proxy = new Proxy(obj, {
  has(target, key) {
    if(key.startsWith('_')) {
      return false
    }
    return Reflect.get(target,key)
  }
})

// 输出 false
console.log('_privateMethod' in proxy)

// 输出 true
console.log('publicMethod' in proxy)

deleteProperty

当使用delete去删除对象里面的属性的时候,会进入deleteProperty`拦截器
/**
 * target: 目标对象,即通过proxy代理的对象
 * key: 要删除的属性
 */
 handle.deleteProperty(target,key)
示例

现在有一个用户信息的对象,对于某些用户信息,只允许查看,但不能删除或者修改,对此使用Proxy可以对不能删除或者修改的属性进行拦截并抛出异常,如下

const userInfo = {
  name: '子君',
  gzh: '前端有的玩',
  sex: '男',
  age: 22
}
// 只能删除用户名和公众号
const readonlyKeys = ['name', 'gzh']
const proxy = new Proxy(userInfo, {
  set(target,key,value) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被修改`)
    }
    target[key] = value
    return true
  },
   deleteProperty(target,key) {
    if(readonlyKeys.includes(key)) {
      throw new Error(`属性${key}不能被删除`)
      return
    }
    delete target[key]
    return true
  }
})
// 报错 
delete proxy.name
对比Vue2.0

其实与$set解决的问题类似,Vue2.0是无法监听到属性被删除的,所以提供了$delete用于删除属性,但是对于Proxy,是可以监听删除操作的,所以就不需要再使用$delete

其他操作

在上文中,我们提到了Proxyhandler提供了十三个函数,在上面我们列举了最常用的三个,其实每一个的用法都是基本一致的,比如ownKeys,当通过Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownKeys去获取对象的信息的时候,就会进入ownKeys这个钩子函数,使用这个我们就可以对一些我们不像暴露的属性进行保护,比如一般会约定_开头的为私有属性,所以在使用Object.keys去获取对象的所有key的时候,就可以把所有_开头的属性屏蔽掉。关于剩余的那些属性,建议大家多去看看MDN中的介绍。

Reflect

在上面,我们获取属性的值或者修改属性的值都是通过直接操作target来实现的,但实际上ES6已经为我们提供了在Proxy内部调用对象的默认行为的API,即Reflect。比如下面的代码

const obj = {}
const proxy = new Proxy(obj, {
  get(target,key,receiver) {
    return Reflect.get(target,key,receiver)
  }
})

大家可能看到上面的代码与直接使用target[key]的方式没什么区别,但实际上Reflect的出现是为了让Object上面的操作更加规范,比如我们要判断某一个prop是否在一个对象中,通常会使用到in,即

const obj = {name: '子君'}
console.log('name' in obj)

但上面的操作是一种命令式的语法,通过Reflect可以将其转变为函数式的语法,显得更加规范

Reflect.has(obj,'name')

除了has,get之外,其实Reflect上面总共提供了十三个静态方法,这十三个静态方法与Proxyhandler上面的十三个方法是一一对应的,通过将ProxyReflect相结合,就可以对对象上面的默认操作进行拦截处理,当然这也就属于函数元编程的范畴了。

总结

有的同学可能会有疑惑,我不会ProxyReflect就学不了Vue3.0了吗?其实懂不懂这个是不影响学习Vue3.0的,但是如果想深入 去理解Vue3.0,还是很有必要了解这些的。比如经常会有人在使用Vue2的时候问,为什么我数组通过索引修改值之后,界面没有变呢?当你了解到Object.defineProperty的使用方式与限制之后,就会恍然大悟,原来如此。本文之后,小编将为大家带来Vue3.0系列文章,欢迎关注,一起学习。同时本文首发于公众号【前端有的玩】,用玩的姿势学前端,就在【前端有的玩】

查看原文

赞 36 收藏 22 评论 0

后知后觉 收藏了文章 · 2019-04-22

45个值得收藏的 CSS 形状

个人专栏 ES6 深入浅出已上线,深入ES6 ,通过案例学习掌握 ES6 中新特性一些使用技巧及原理,持续更新中,←点击可订阅。

点赞再看,养成习惯

本文 GitHubhttps://github.com/qq44924588... 上已经收录,更多往期高赞文章的分类,也整理了很多我的文档,和教程资料。欢迎Star和完善,大家面试可以参照考点复习,希望我们一起有点东西。


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

CSS能够生成各种形状。正方形和矩形很容易,因为它们是 web 的自然形状。添加宽度和高度,就得到了所需的精确大小的矩形。添加边框半径,你就可以把这个形状变成圆形,足够多的边框半径,你就可以把这些矩形变成圆形和椭圆形。

我们还可以使用 CSS 伪元素中的 ::before::after,这为我们提供了向原始元素添加另外两个形状的可能性。通过巧妙地使用定位、转换和许多其他技巧,我们可以只用一个 HTML 元素在 CSS 中创建许多形状。

虽然我们现在大都使用字体图标或者svg图片,似乎使用 CSS 来做图标意义不是很大,但怎么实现这些图标用到的一些技巧及思路是很值得我们的学习。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

1.正方形

clipboard.png

#square {
  width: 100px;
  height: 100px;
  background: red;
}

2.长方形

clipboard.png

#rectangle {
  width: 200px;
  height: 100px;
  background: red;
}


3.圆形

clipboard.png

#circle {
  width: 100px;
  height: 100px;
  background: red;
  border-radius: 50%
}

4.椭圆形

clipboard.png

#oval {
  width: 200px;
  height: 100px;
  background: red;
  border-radius: 100px / 50px;
}


5.上三角

clipboard.png

#triangle-up {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
}

6.下三角

clipboard.png

#triangle-down {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 100px solid red;
}

7.左三角

clipboard.png

#triangle-left {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-right: 100px solid red;
  border-bottom: 50px solid transparent;
}

8.右三角

clipboard.png

#triangle-right {
  width: 0;
  height: 0;
  border-top: 50px solid transparent;
  border-left: 100px solid red;
  border-bottom: 50px solid transparent;
}

9.左上角

clipboard.png

#triangle-topleft {

  width: 0;
  height: 0;
  border-top: 100px solid red;
  border-right: 100px solid transparent;
}

10.右上角

clipboard.png

#triangle-topright {
  width: 0;
  height: 0;
  border-top: 100px solid red;
  border-left: 100px solid transparent;
}

11.左下角

clipboard.png

#triangle-bottomleft {
  width: 0;
  height: 0;
  border-bottom: 100px solid red;
  border-right: 100px solid transparent;
}

12.右下角

clipboard.png

#triangle-bottomright {
  width: 0;
  height: 0;
  border-bottom: 100px solid red;
  border-left: 100px solid transparent;
}

13.箭头

clipboard.png

#curvedarrow {
  position: relative;
  width: 0;
  height: 0;
  border-top: 9px solid transparent;
  border-right: 9px solid red;
  transform: rotate(10deg);
}
#curvedarrow:after {
  content: "";
  position: absolute;
  border: 0 solid transparent;
  border-top: 3px solid red;
  border-radius: 20px 0 0 0;
  top: -12px;
  left: -9px;
  width: 12px;
  height: 12px;
  transform: rotate(45deg);
}

14.梯形

clipboard.png

#trapezoid {
  border-bottom: 100px solid red;
  border-left: 25px solid transparent;
  border-right: 25px solid transparent;
  height: 0;
  width: 100px;
}

15.平行四边形

clipboard.png

#parallelogram {
  width: 150px;
  height: 100px;
  transform: skew(20deg);
  background: red;
}

16.星星 (6角)

clipboard.png

#star-six {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 100px solid red;
  position: relative;
}
#star-six:after {
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 100px solid red;
  position: absolute;
  content: "";
  top: 30px;
  left: -50px;
}

17.星星 (5角)

clipboard.png

#star-five {
  margin: 50px 0;
  position: relative;
  display: block;
  color: red;
  width: 0px;
  height: 0px;
  border-right: 100px solid transparent;
  border-bottom: 70px solid red;
  border-left: 100px solid transparent;
  transform: rotate(35deg);
}
#star-five:before {
  border-bottom: 80px solid red;
  border-left: 30px solid transparent;
  border-right: 30px solid transparent;
  position: absolute;
  height: 0;
  width: 0;
  top: -45px;
  left: -65px;
  display: block;
  content: '';
  transform: rotate(-35deg);
}
#star-five:after {
  position: absolute;
  display: block;
  color: red;
  top: 3px;
  left: -105px;
  width: 0px;
  height: 0px;
  border-right: 100px solid transparent;
  border-bottom: 70px solid red;
  border-left: 100px solid transparent;
  transform: rotate(-70deg);
  content: '';
}

18.五边形

clipboard.png

#pentagon {
  position: relative;
  width: 54px;
  box-sizing: content-box;
  border-width: 50px 18px 0;
  border-style: solid;
  border-color: red transparent;
}
#pentagon:before {
  content: "";
  position: absolute;
  height: 0;
  width: 0;
  top: -85px;
  left: -18px;
  border-width: 0 45px 35px;
  border-style: solid;
  border-color: transparent transparent red;
}

19.六边形

clipboard.png

#hexagon {
  width: 100px;
  height: 55px;
  background: red;
  position: relative;
}
#hexagon:before {
  content: "";
  position: absolute;
  top: -25px;
  left: 0;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-bottom: 25px solid red;
}
#hexagon:after {
  content: "";
  position: absolute;
  bottom: -25px;
  left: 0;
  width: 0;
  height: 0;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  border-top: 25px solid red;
}

20.八边形

clipboard.png

#octagon {
  width: 100px;
  height: 100px;
  background: red;
  position: relative;
}
#octagon:before {
  content: "";
  width: 100px;
  height: 0;
  position: absolute;
  top: 0;
  left: 0;
  border-bottom: 29px solid red;
  border-left: 29px solid #eee;
  border-right: 29px solid #eee;
}
#octagon:after {
  content: "";
  width: 100px;
  height: 0;
  position: absolute;
  bottom: 0;
  left: 0;
  border-top: 29px solid red;
  border-left: 29px solid #eee;
  border-right: 29px solid #eee;
}  

21.爱心

clipboard.png

#heart {
  position: relative;
  width: 100px;
  height: 90px;
}
#heart:before,
#heart:after {
  position: absolute;
  content: "";
  left: 50px;
  top: 0;
  width: 50px;
  height: 80px;
  background: red;
  border-radius: 50px 50px 0 0;
  transform: rotate(-45deg);
  transform-origin: 0 100%;
}
#heart:after {
  left: 0;
  transform: rotate(45deg);
  transform-origin: 100% 100%;
}

22.无穷大

clipboard.png

#infinity {
  position: relative;
  width: 212px;
  height: 100px;
  box-sizing: content-box;
}
#infinity:before,
#infinity:after {
  content: "";
  box-sizing: content-box;
  position: absolute;
  top: 0;
  left: 0;
  width: 60px;
  height: 60px;
  border: 20px solid red;
  border-radius: 50px 50px 0 50px;
  transform: rotate(-45deg);
}
#infinity:after {
  left: auto;
  right: 0;
  border-radius: 50px 50px 50px 0;
  transform: rotate(45deg);
}

23.菱形

clipboard.png

#diamond {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom-color: red;
  position: relative;
  top: -50px;
}
#diamond:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 50px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top-color: red;
}

24.钻石

clipboard.png

#diamond-shield {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom: 20px solid red;
  position: relative;
  top: -50px;
}
#diamond-shield:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 20px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top: 70px solid red;
}

25.钻戒

clipboard.png

#diamond-narrow {
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-bottom: 70px solid red;
  position: relative;
  top: -50px;
}
#diamond-narrow:after {
  content: '';
  position: absolute;
  left: -50px;
  top: 70px;
  width: 0;
  height: 0;
  border: 50px solid transparent;
  border-top: 70px solid red;
}

26.钻石2

clipboard.png

#cut-diamond {
  border-style: solid;
  border-color: transparent transparent red transparent;
  border-width: 0 25px 25px 25px;
  height: 0;
  width: 50px;
  box-sizing: content-box;
  position: relative;
  margin: 20px 0 50px 0;
}
#cut-diamond:after {
  content: "";
  position: absolute;
  top: 25px;
  left: -25px;
  width: 0;
  height: 0;
  border-style: solid;
  border-color: red transparent transparent transparent;
  border-width: 70px 50px 0 50px;
}

27.蛋蛋

clipboard.png

#egg {
  display: block;
  width: 126px;
  height: 180px;
  background-color: red;
  border-radius: 50% 50% 50% 50% / 60% 60% 40% 40%;
}

28.吃豆人

clipboard.png

#pacman {
  width: 0px;
  height: 0px;
  border-right: 60px solid transparent;
  border-top: 60px solid red;
  border-left: 60px solid red;
  border-bottom: 60px solid red;
  border-top-left-radius: 60px;
  border-top-right-radius: 60px;
  border-bottom-left-radius: 60px;
  border-bottom-right-radius: 60px;
}

29.对话泡泡

clipboard.png

#talkbubble {
  width: 120px;
  height: 80px;
  background: red;
  position: relative;
  -moz-border-radius: 10px;
  -webkit-border-radius: 10px;
  border-radius: 10px;
}
#talkbubble:before {
  content: "";
  position: absolute;
  right: 100%;
  top: 26px;
  width: 0;
  height: 0;
  border-top: 13px solid transparent;
  border-right: 26px solid red;
  border-bottom: 13px solid transparent;
}

30. 12点 爆发

clipboard.png

#burst-12 {
  background: red;
  width: 80px;
  height: 80px;
  position: relative;
  text-align: center;
}
#burst-12:before,
#burst-12:after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 80px;
  width: 80px;
  background: red;
}
#burst-12:before {
  transform: rotate(30deg);
}
#burst-12:after {
  transform: rotate(60deg);
}

31. 8点 爆发

clipboard.png

#burst-8 {
  background: red;
  width: 80px;
  height: 80px;
  position: relative;
  text-align: center;
  transform: rotate(20deg);
}
#burst-8:before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  height: 80px;
  width: 80px;
  background: red;
  transform: rotate(135deg);
}

32.太极

clipboard.png

#yin-yang {
  width: 96px;
  box-sizing: content-box;
  height: 48px;
  background: #eee;
  border-color: red;
  border-style: solid;
  border-width: 2px 2px 50px 2px;
  border-radius: 100%;
  position: relative;
}
#yin-yang:before {
  content: "";
  position: absolute;
  top: 50%;
  left: 0;
  background: #eee;
  border: 18px solid red;
  border-radius: 100%;
  width: 12px;
  height: 12px;
  box-sizing: content-box;
}
#yin-yang:after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  background: red;
  border: 18px solid #eee;
  border-radius: 100%;
  width: 12px;
  height: 12px;
  box-sizing: content-box;
}  

33.徽章丝带

clipboard.png

#badge-ribbon {
  position: relative;
  background: red;
  height: 100px;
  width: 100px;
  border-radius: 50px;
}
#badge-ribbon:before,
#badge-ribbon:after {
  content: '';
  position: absolute;
  border-bottom: 70px solid red;
  border-left: 40px solid transparent;
  border-right: 40px solid transparent;
  top: 70px;
  left: -10px;
  transform: rotate(-140deg);
}
#badge-ribbon:after {
  left: auto;
  right: -10px;
  transform: rotate(140deg);
}
 

34.太空入侵者(电脑游戏名)

clipboard.png


#space-invader {
  box-shadow: 0 0 0 1em red,
  0 1em 0 1em red,
  -2.5em 1.5em 0 .5em red,
  2.5em 1.5em 0 .5em red,
  -3em -3em 0 0 red,
  3em -3em 0 0 red,
  -2em -2em 0 0 red,
  2em -2em 0 0 red,
  -3em -1em 0 0 red,
  -2em -1em 0 0 red,
  2em -1em 0 0 red,
  3em -1em 0 0 red,
  -4em 0 0 0 red,
  -3em 0 0 0 red,
  3em 0 0 0 red,
  4em 0 0 0 red,
  -5em 1em 0 0 red,
  -4em 1em 0 0 red,
  4em 1em 0 0 red,
  5em 1em 0 0 red,
  -5em 2em 0 0 red,
  5em 2em 0 0 red,
  -5em 3em 0 0 red,
  -3em 3em 0 0 red,
  3em 3em 0 0 red,
  5em 3em 0 0 red,
  -2em 4em 0 0 red,
  -1em 4em 0 0 red,
  1em 4em 0 0 red,
  2em 4em 0 0 red;
  background: red;
  width: 1em;
  height: 1em;
  overflow: hidden;
  margin: 50px 0 70px 65px;
}    

35.电视

clipboard.png


#tv {
  position: relative;
  width: 200px;
  height: 150px;
  margin: 20px 0;
  background: red;
  border-radius: 50% / 10%;
  color: white;
  text-align: center;
  text-indent: .1em;
}
#tv:before {
  content: '';
  position: absolute;
  top: 10%;
  bottom: 10%;
  right: -5%;
  left: -5%;
  background: inherit;
  border-radius: 5% / 50%;
}
  

36.雪佛龙

clipboard.png

#chevron {
  position: relative;
  text-align: center;
  padding: 12px;
  margin-bottom: 6px;
  height: 60px;
  width: 200px;
}
#chevron:before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  height: 100%;
  width: 51%;
  background: red;
  transform: skew(0deg, 6deg);
}
#chevron:after {
  content: '';
  position: absolute;
  top: 0;
  right: 0;
  height: 100%;
  width: 50%;
  background: red;
  transform: skew(0deg, -6deg);
}   

37.放大镜

clipboard.png

#magnifying-glass {
  font-size: 10em;
  display: inline-block;
  width: 0.4em;
  box-sizing: content-box;
  height: 0.4em;
  border: 0.1em solid red;
  position: relative;
  border-radius: 0.35em;
}
#magnifying-glass:before {
  content: "";
  display: inline-block;
  position: absolute;
  right: -0.25em;
  bottom: -0.1em;
  border-width: 0;
  background: red;
  width: 0.35em;
  height: 0.08em;
  transform: rotate(45deg);
}

38.Facebook图标

clipboard.png

#facebook-icon {
  background: red;
  text-indent: -999em;
  width: 100px;
  height: 110px;
  box-sizing: content-box;
  border-radius: 5px;
  position: relative;
  overflow: hidden;
  border: 15px solid red;
  border-bottom: 0;
}
#facebook-icon:before {
  content: "/20";
  position: absolute;
  background: red;
  width: 40px;
  height: 90px;
  bottom: -30px;
  right: -37px;
  border: 20px solid #eee;
  border-radius: 25px;
  box-sizing: content-box;
}
#facebook-icon:after {
  content: "/20";
  position: absolute;
  width: 55px;
  top: 50px;
  height: 20px;
  background: #eee;
  right: 5px;
  box-sizing: content-box;
}

39.月亮

clipboard.png


#moon {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  box-shadow: 15px 15px 0 0 red;
}  

40.旗

clipboard.png

#flag {
  width: 110px;
  height: 56px;
  box-sizing: content-box;
  padding-top: 15px;
  position: relative;
  background: red;
  color: white;
  font-size: 11px;
  letter-spacing: 0.2em;
  text-align: center;
  text-transform: uppercase;
}
#flag:after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 0;
  border-bottom: 13px solid #eee;
  border-left: 55px solid transparent;
  border-right: 55px solid transparent;
}

41.圆锥

clipboard.png

 #cone {
  width: 0;
  height: 0;
  border-left: 70px solid transparent;
  border-right: 70px solid transparent;
  border-top: 100px solid red;
  border-radius: 50%;
}

42.十字架

clipboard.png

#cross {
  background: red;
  height: 100px;
  position: relative;
  width: 20px;
}
#cross:after {
  background: red;
  content: "";
  height: 20px;
  left: -40px;
  position: absolute;
  top: 40px;
  width: 100px;
}

43.根基

clipboard.png

 #base {
  background: red;
  display: inline-block;
  height: 55px;
  margin-left: 20px;
  margin-top: 55px;
  position: relative;
  width: 100px;
}
#base:before {
  border-bottom: 35px solid red;
  border-left: 50px solid transparent;
  border-right: 50px solid transparent;
  content: "";
  height: 0;
  left: 0;
  position: absolute;
  top: -35px;
  width: 0;
}

44.指示器

clipboard.png

#pointer {
  width: 200px;
  height: 40px;
  position: relative;
  background: red;
}
#pointer:after {
  content: "";
  position: absolute;
  left: 0;
  bottom: 0;
  width: 0;
  height: 0;
  border-left: 20px solid white;
  border-top: 20px solid transparent;
  border-bottom: 20px solid transparent;
}
#pointer:before {
  content: "";
  position: absolute;
  right: -20px;
  bottom: 0;
  width: 0;
  height: 0;
  border-left: 20px solid red;
  border-top: 20px solid transparent;
  border-bottom: 20px solid transparent;
}

45.锁

clipboard.png

#lock {
  font-size: 8px;
  position: relative;
  width: 18em;
  height: 13em;
  border-radius: 2em;
  top: 10em;
  box-sizing: border-box;
  border: 3.5em solid red;
  border-right-width: 7.5em;
  border-left-width: 7.5em;
  margin: 0 0 6rem 0;
}
#lock:before {
  content: "";
  box-sizing: border-box;
  position: absolute;
  border: 2.5em solid red;
  width: 14em;
  height: 12em;
  left: 50%;
  margin-left: -7em;
  top: -12em;
  border-top-left-radius: 7em;
  border-top-right-radius: 7em;
}
#lock:after {
  content: "";
  box-sizing: border-box;
  position: absolute;
  border: 1em solid red;
  width: 5em;
  height: 8em;
  border-radius: 2.5em;
  left: 50%;
  top: -1em;
  margin-left: -2.5em;
}

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:https://css-tricks.com/the-sh...

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

交流

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

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

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

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

clipboard.png

查看原文

后知后觉 提出了问题 · 2019-03-07

echarts surface 曲面图 option 属性怎么配置

题目描述

echarts surface 曲面图 option 属性怎么配置;为什么自己写出来的和官网差距这么大; 是数据本身的原因, 还是因为自己什么属性没配置正确

题目来源及自己的思路

网上搜的相关代码看起来都很平滑,感觉像是自己的data没组对,试了几次都没什么大的差异;不知道究竟是什么原因
求大神指点下, 怎么配置才能使曲面看起来平滑一些

相关代码

// 请把代码文本粘贴到下方(请勿用图片代替代码)

<body>
    <div id="test" style="height: 600px; width: 900px; margin: 220x auto 0; border: 1px solid red;">

    </div>
</body>
<script data-original="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script data-original="https://cdn.bootcss.com/echarts/4.1.0.rc2/echarts-en.common.min.js" type="text/javascript" charset="utf-8"></script>
<script data-original="../js/echarts-gl.js" type="text/javascript" charset="utf-8"></script>
<script type="text/javascript">
    var arrX = ["1W", "2W", "1M", "2M", "3M", "6M", "9M", "1Y"];
    var arrY = ["10D P", "15D P", "-10D P", "25D P", "35D P", "40D P", "45D P", "ATM", "45D C", "40D C", "35D C", "30D C", "25D C", "20D C", "15D C", "10D C", "30D P"];
    var arrZ = [
        [3.57, 2.87, 2.44, 2.65, 3.05, 3.55, 4.07, 4.17],
        [3.01, 2.58, 2.32, 2.5, 2.87, 3.39, 3.92, 4.03],
        [2.56, 2.34, 2.22, 2.39, 2.74, 3.28, 3.83, 3.95],
        [2.26, 2.2, 2.17, 2.33, 2.67, 3.24, 3.82, 3.95],
        [2.02, 2.14, 2.2, 2.34, 2.69, 3.34, 3.96, 4.12],
        [2.02, 2.17, 2.24, 2.38, 2.74, 3.42, 4.07, 4.24],
        [2.03, 2.2, 2.28, 2.42, 2.8, 3.51, 4.18, 4.34],
        [2.04, 2.21, 2.29, 2.45, 2.83, 3.56, 4.24, 4.4],
        [2.03, 2.19, 2.27, 2.45, 2.84, 3.58, 4.28, 4.44],
        [2.0, 2.15, 2.23, 2.45, 2.85, 3.6, 4.32, 4.48],
        [2.0, 2.14, 2.2, 2.47, 2.88, 3.65, 4.39, 4.56],
        [2.06, 2.17, 2.22, 2.53, 2.96, 3.78, 4.54, 4.72],
        [2.23, 2.28, 2.3, 2.67, 3.13, 4.01, 4.81, 5.02],
        [2.52, 2.48, 2.46, 2.86, 3.37, 4.33, 5.18, 5.41],
        [2.97, 2.77, 2.66, 3.08, 3.65, 4.67, 5.58, 5.83],
        [3.53, 3.11, 2.87, 3.31, 3.94, 5.01, 5.99, 6.25],
        [2.1, 2.14, 2.17, 2.32, 2.66, 3.27, 3.87, 4.02]
    ];

    var datas = [];
    arrX.forEach((x, i) => {
        arrY.forEach((y, j) => {
            arrZ.forEach((z, k) => {
                console.log([x, y, z[i]]);
                datas.push([y, x, z[i]]);
            })

        })
    })
    var option3d = {
        tooltip: {},
        backgroundColor: '#fff',
        visualMap: {
            show: true,
            type: 'continuous',
            dimension: 2,
            min: -1,
            max: 1,
            inRange: {
                color: ['#313695', '#4575b4', '#74add1', '#abd9e9', '#e0f3f8', '#ffffbf', '#fee090', '#fdae61', '#f46d43', '#d73027', '#a50026']
            }
        },
        grid3D: {
            viewControl: {
                //                     projection: 'orthographic'
            }
        },
        xAxis3D: {
            type: 'category',

        },
        yAxis3D: {
            type: 'category',
            step: 1
        },
        zAxis3D: {
            type: 'value'
        },
        color: "#383f52",
        ![series: \[{][1]
            type: 'surface',
            wireframe: {
                show: false,
                lineStyle: {
                    color: '#66AEFF'
                },
            },
            data: datas ,

        }]
    };
    var box = document.getElementById("test");
    var myChar = echarts.init(box);
    myChar.setOption(option3d);
</script>

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

关注 2 回答 1

后知后觉 赞了文章 · 2018-11-29

echarts堆叠图tooltip中如何仅展示鼠标当前位置相应的指标数据

项目创建到现在快小半年了,期间遇到了大大小小非常多的问题,在不断遇到问题和解决问题的过程中,对vue和element-ui还有echarts的认知一点点的加深,也累积了不少对应各种问题的奇技淫巧。记录一下。

这篇记录一个使用echarts时遇到的实际问题。

堆叠图tooltip问题

echarts生成的堆叠图,鼠标浮动时默认会将相应位置的所有指标信息全部展示出来:

clipboard.png

当指标特别多的时候,这样的鼠标浮动提示信息会失去其说明数据的意义:

clipboard.png

用户无法从浮动提示中一眼就辨认出他想看的那个指标在某个区间的具体数值,甚至可以说,要从这个浮动提示中找出指定区域对应的数值是件想想就令人头皮发麻的事情(尤其对于色弱的我来说T_T)。

这种情况下,比较合适的做法是鼠标移动到哪个位置,就只显示当前位置对应的指标数据。

clipboard.png

可惜翻遍echarts文档也没找到这个需求的相关配置项。只好自己另谋出路。

问题一定和tooltip的formatter相关,只要在formatter方法中找出鼠标位置所在区域对应的series,问题就能迎刃而解。然而问题的答案却不在formatter这个方法内部,echarts提供的formatter参数中,只是一个包含所有series数据的数组,无法定位具体是哪个series。

为获取这个信息,目光需要转移到能获取这个信息的其他配置项上——tooltip.axisPointer

当tooltip.axisPointer.type为cross时,tooltip.axisPointer.label.formatter方法的参数是个二维数组,而数组的第一项,记录着鼠标所对应的yAxis的数据。而且值得庆幸的是,tooltip.axisPointer.label.formatter将会先于tooltip.formatter触发,这让我有机会获取到这个关键信息,将其存到外部变量mouseCurValue中,然后在tooltip.formatter方法使用它。

axisPointer: {
    type: "cross",
    label: {
        formatter: function (params) {
            if (params.seriesData.length === 0) {
                // 就是这里,可以获取到我想要的那个数据
                mouseCurValue = params.value;
            }
        }
    }
},

有了mouseCurValue,就能计算鼠标当前位置究竟落在哪一个series中了。回到tooltip.formatter方法,tooltip.formatter的数组参数parameters是个有序数组,即越靠近x轴的线所对应的series,在数组中的位置越靠前。所以只要遍历数组并累加其data数值,然后与mouseCurValue对比,当这个累积数一旦超过或等于mouseCurValue,那么当前series即对应鼠标当前位置。

formatter: function (params) {
    let res = "", sum = 0;

    // 先取消所有当前dataIndex点的高亮
    if (chartInstance && params.length > 0) {
        chartInstance.dispatchAction({
            type: "downplay",
            dataIndex: params[0].dataIndex
        });
    }

    for (let i = 0; i < params.length; i++) {
        let series = params[i];
        // 累计series.data,与mouseCurValue做比较,找出鼠标位置对应的series
        sum += Number(series.data);
        if (sum >= mouseCurValue) {
            res = series.axisValue + "<br/>" + series.marker + series.seriesName + ":" + series.data + "<br/>";

            // 再高亮当前鼠标所在区域所代表的点
            if (chartInstance) {
                chartInstance.dispatchAction({
                    type: "highlight",
                    seriesIndex: series.seriesIndex,
                    dataIndex: series.dataIndex
                });
            }
            break;
        }
    }
    return res;
}

代码中还添加了触发downplay和highlight事件,避免echarts默认事件将所有在当前范围的点全部高亮。

用下面代码在http://echarts.baidu.com/demo...echarts官网实例上替换掉原有tooltip属性后验证可行

tooltip : {
    trigger: 'axis',
    axisPointer: {
        type: "cross",
        label: {
            formatter: function (params) {
                if (params.seriesData.length === 0) {
                    window.mouseCurValue = params.value;
                }
            }
        }
    },
    formatter: function (params) {
        let res = "", sum = 0;
        for (let i = 0; i < params.length; i++) {
            let series = params[i];
            sum += Number(series.data);
            if (sum >= window.mouseCurValue) {
                res = series.axisValue + "<br/>" + series.marker + series.seriesName + ":" + series.data + "<br/>";
                break;
            }
        }
        return res;
    },
}

clipboard.png

剩下的一些显示问题官网上都有相关配置。
OK,搞定收工~

查看原文

赞 13 收藏 11 评论 13

后知后觉 赞了文章 · 2018-11-23

Vue数据双向绑定探究

前面的啰嗦话,写一点吧,或许就有点用呢

使用过vue的小伙伴都会感觉,哇,这个框架对开发者这么友好,简直都要笑出声了。

确实,使用过vue的框架做开发的人都会感觉到,以前写一大堆操作dom,bom的东西,现在用不着了,对开发者来说更容易去注重对操作逻辑的思考和实现,省了不少事儿呢!!!

我是直接从原生js,jq的开发用过度到使用vue,对这个框架也是喜爱有加,闲来无事,去看了看它的一些实现原理。

下面来介绍一下vue的一个非常"牛逼"的功能,数据双向绑定,也就是我们在项目里用到的v-model指令。

v-model在vue官方文档上是介绍在"表单输入绑定"那一节。

对于表单,大家肯定用得都已经超级熟练了,对于<input>、<textarea>和<select>标签在项目里面使用都已经没话说了

官方提到的v-model是一个语法糖,为什么这么说呢?下面看个例子:

<div id="test1">
  <input v-model="input">
  <span>input: {{ input }}</span>
</div>

如上,是一个简单的使用v-model的双向绑定,我们在改变input这个变量的值,即在输入框中去写内容的时候,在span标签内的插值(mustache)会同步更新我们刚刚输入的值

其实上面的也可以这样写:

<div id="test1">
  <input v-on:input="input = $event.target.value" v-bind:value='input'>
  <span>input: {{ input }}</span>
</div>

好了,前面啰里啰嗦半天,现在进入正题

想对比react和angular的双向绑定实现,我也不清楚,哈哈哈,直接说vue吧,不扯了

Reactivity 响应式系统

拿尤雨溪大佬做vue测试的的那个例子来说吧(购物车的例子)

<div id='app'>
    <div>
      <span>价格:</span>
      <input v-model.number="price">
    </div>
    <div>
      <span>数量:</span>
      <input v-model.number="quantity">
    </div>
    <p>价格:{{ price }}</p>
    <p>数量:{{ quantity }}</p>
    <p>总计:{{ total }}</p>
  </div>
 data() {
      return {
        price: 5,
        quantity: 3
      }
    },
    computed: {
      total() {
        return this.price * this.quantity;
      }
    }

当我们在使用输入框的值的时候,下面的total会更新,我们对应输入值的变量也会更新

哇,好神奇,为什么呢,这不是JavaScript编程常规的工作方式!!!

因为我们用原生js写的时候是这样的:

let price = 5;
let quantity = 3;
let total = price * quantity;     // 等于15吧

price = 10;    // 改变价格;
console.log(total);    // bingo,打印的还是15

我们需要在找一种办法,把要运行计算的total放到别的时候去运行,当我们的价格、数量变化的时候执行

let price = 5;
let quantity = 3;
let total = 0;
let storage = [];   // 储存将要计算的操作,等到变量变化的时候去执行

let target= () => { total = price * quantity;}
function record () {
    storage.push(target);
}
function replay() {
    storage.forEach(run => run());
}

record();
target();

price = 10;
console.log(total);    // 依然是15
replay();
console.log(total);    // 执行结果是30

目的达到,但是这样肯定不是vue用来扩展使用的方式,我们用ES6的class类来做一个可维护的扩展,实现一个标准的观察者模式的依赖类

class Depend {
  constructor () {
    this.subscribers = [];
  }
  depend() {
    if(target && this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach(sub => sub());
  }
}

// 来执行上面写的class
const dep = new Depend();
let price = 5;
let quantity = 3;
let total = 0;
let target = () => { total = price * quantity };

dep.depend();
target();

console.log(total);    // total是15
price = 10;
console.log(total);    // 因为没有执行target,依旧是15
dep.notify();
console.log(total);    // 执行了存入的target,total为30

为了给每一个变量都设置一个Depend类。并且很好地控制监视更新的匿名函数的行为,我们把上面的代码做一些调整:

let target = () => { total = price * quantity };
dep.depend();
target();

修改为:

watcher(() => { total = price * quantity });

然后我们在watcher函数里面来做刚刚上面的result的设置和执行的功能

function watcher(fn) {
  target = fn;
  dep.depend();
  target();
  target = null;           // 重置一下,等待储存和执行下一次
}

这儿就是官方文档提到的订阅者模式:在每次watcher函数执行的时候,把参数fn设置成为我们全局目标属性,调用dep.depend()将目标添加为订阅者,调用然后重置

然后再继续
我们的目标是把每一个变量都设置一个Depend类,但是这儿有个问题:
先存一下数据:

let data = { price: 5, quantity: 3}

假设我们每个属性都有自己的内部Depend类

图片描述

当我们运行代码时:

watcher(() => { total = data.price * data.quantity})

由于访问了data.price值,希望price属性的Depend类将我们的匿名函数(存储在目标中)推送到其订阅者数组(通过调用dep.depend())。由于访问了data.quantity,还希望quantity属性Depend类将此匿名函数(存储在目标中)推送到其订阅者数组中。

图片描述

如果有另一个匿名函数,只访问data.price,希望只推送到价格属性Depend类。

图片描述

什么时候想要在价格订阅者上调用dep.notify()?我希望在设定价格时调用它们。为此,我们需要一些方法来挂钩数据属性(价格或数量),所以当它被访问时我们可以将目标保存到我们的订阅者数组中,当它被更改时,运行存储在我们的订阅者数组中的函数。let's go

Object.defineProperty来解决这个问题

Object.defineProperty函数是简单的ES5 JavaScript。它允许我们为属性定义getter和setter函数。继续啃

let data = { price: 5, quantity: 3};
let value = data.price
Object.defineProperty(data, 'price', {
  getter() {
    console.log(`获取price的值: ${value}`);
    return value;
  },
  setter(newValue) {
    console.log(`更改price的值': ${newValue}`);
    value = newValue;
  }
})
total = data.price * data.quantity;
data.price = 10;      // 更改price的值

上面通过defineProperty方法给price设置了获取和修改值的操作

如何给data对象所有的都加上这个defineProperty方法去设置值

大家还记得Object.keys这个方法吗?返回对象键的数组,咱们把上面的代码改造一下

let data = { price: 5, quantity: 3 };

Object.keys(data).forEach(key => {
  let value = data[key];
  Object.defineProperty(data, key, {
    getter() {
      console.log(`获取 ${key} 的值: ${value}`);
      return value;
    },
    setter(newValue) {
      console.log(`更改 ${key} 值': ${newValue}`);
      value = newValue;
    }
  })
})
total = data.price * data.quantity;
data.price = 10; // 更改price的值

接着上面的东西,在每次运行完获取key的值,我们希望key能记住这个匿名函数(target),这样有key的值变化的时候,它将触发这个函数来重新计算,大致思路是这样的:
getter函数执行的时候,记住这个匿名函数,当值在发生变化的时候再次运行它
setter函数执行的时候,运行保存的匿名函数,把当前的值存起来

用上面定义的Depend类来说就是:
getter执行,调用dep.depend()来保存当前的target
setter执行,在key上调用dep.notify(),重新运行所有的target

来来来,把上面的东西结合到一起来

let data = { price: 5, quantity: 3 };
let total = 0;
let target = null;

class Depend {
  constructor() {
    this.subscribers = [];
  }
  depend() {
    if (target && this.subscribers.includes(target)) {
      this.subscribers.push(target);
    }
  }
  notify() {
    this.subscribers.forEach(sub => sub());
  }
}

Object.keys(data).forEach(key => {
  let value = data[key];
  const dep = new Depend();

  Object.defineProperty(data, key, {
    getter() {
      dep.depend();
      return value;
    },
    setter(newValue) {
      value = newValue;
      dep.notify();
    }
  })
});

function watcher(fn) {
  target = fn;
  target();
  target = null;
}

watcher(() => {
  total = data.price * data.quantity;
});

至此,vue的数据双向绑定已经实现,当我们去改变price和quantity的值,total会实时更改

然后咱们来看看vue的文档里面提到的这个插图:

图片描述

是不是感觉这个图很熟悉了?对比咱们上面研究的流程,这个图的data和watcher就很清晰了,大致思路如此,可能vue的内部实现和封装远比我这个研究流程内容大得多、复杂得多,不过有了这样的一个流程思路,再去看vue双向绑定源码估计也能看懂个十之八九了。

听说vue3.0准备把这个数据劫持的操作用ES6提供的proxy来做,效率更高,期待!!!!

参考和学习原文(可能需要翻墙,毕竟是外站啊)

查看原文

赞 25 收藏 20 评论 0

后知后觉 收藏了文章 · 2018-10-15

如何用Python抓抖音上的小姐姐

爬虫的案例我们已讲得太多。不过几乎都是 网页爬虫 。即使有些手机才能访问的网站,我们也可以通过 Chrome 开发者工具手机模拟 功能来访问,以便于分析请求并抓取。(比如 3分钟破译朋友圈测试小游戏 文章里用的方法)

但有些 App 根本就没有提供网页端,比如今年火得不行的 抖音 。(网上有些教程也是用网页手机模拟的方法,但此法现已失效。)

对于这种情况,我们能不能抓取?要怎么抓取?今天就来分享一下。

手机抓包

本文的重点就在于 如何获取手机 App 发出的请求

手机 App 不像电脑上的网页能直接通过浏览器查看相关信息,在手机设备上也不方便使用工具一边流量一边调试。所以常用的方式就是通过在电脑上装一些 “抓包”软件 ,将手机上的网络请求全部显示出来。

那为什么电脑能看到手机上的网络请求?这里就要提下“ 代理 ”这个概念。我们之前的文章 听说你好不容易写了个爬虫,结果没抓几个就被封了? 中也讲过代理。形象的解释就是字面的理解: 所有你发出的请求不再是直接发到目的地,而是先发给这个代理,再由代理帮你发出 。所以通过代理,可以实现 隐藏 IP、进入专用网络、翻…咳咳那啥 等功能,也包括我们今天说的: 手机抓包

顺带说句,在公共场所别随便连不确定的免费 wifi,理论上来说,人家也可以抓你的包。

这里,我们要用的工具是 Fiddler 。它是一个较成熟的免费抓包工具。可以抓取网页、桌面软件、手机 App 的网络请求,并可以运行在 Windows、Mac、Linux 平台上,支持 iOS 和 Android。(虽说都支持,但强烈建议 Windows + Android ,后面我会有吐槽)

上周我们的送书活动收到不少同学的项目和代码,其中 @离岛 同学提交了一个 Fiddler 手机抓包的教程。
https://segmentfault.com/a/1190000015571256
本文中部分内容和图片就转自她这篇文章。她的博客上还有不少文章和学习笔记,可以关注交流。也欢迎其他同学给我们投稿。

下载安装

搜索一下 fiddler 很容易找到它们的官网 https://www.telerik.com/fiddler,点击 download 下载即可(有个表格随便填下)。

Windows 下载后正常安装。如果是 Mac,还会有安装步骤提示,告诉你需要先安装一个叫做 Mono 的框架,以便可以执行 Fiddler.exe。另外 Mac 版还有几个小坑:

1. 运行 mono 命令用 sudo

2. 如果报一堆错闪退,请用 mono --arch=32 Fiddler.exe(这个参数还必须放在文件名前面)

3. 第一次正确运行时,程序 会卡住很长时间 ,以至于我以为还是挂了,这时请耐心等待。(我要不是正好有事走开,回来发现成功了,可能就放弃尝试了)

4. 即使正常运行了,Mac 上界面也会有各种显示的 bug,切记不要打开的弹窗的情况下切换程序,不然回来就找不到弹窗了……

5. 软件中无法复制……

6. 在 iOS 上无法抓取 HTTPS 请求(这基本就是废了),需要额外创建一个证书,但这个证书工具只能在 Windows 下运行……

所以可以的话,还是用 Windows 来做。Mac 上还有个比较知名的工具 Charles ,有用过的可以留言评价下。

配置

安装好工具后,需要做一些必要配置才能抓包。

1. Fiddler 配置

设置允许抓取 HTTPS 信息包。打开下载好的 fiddler,找到 Tools - > Options,然后在 HTTPS 的工具栏下勾选 Decrpt HTTPS traffic ,在新弹出的选项栏下勾选 Ignore server certificate errors 。这样,fiddler 就会抓取到 HTTPS 的信息包。

设置允许外部设备发送 HTTP/HTTPS 到 fiddler。设置 端口号 ,并在 Connections 选项栏下勾选 Allow remote computers to connect

配置好后需重启软件。

2. 设置手机代理

在抓包前,确保你的电脑和手机是在一个 可以互访的局域网中 。最简单的情况就是都连在同一个 wifi 上,特殊情况这里不展开讨论(有些商用 wifi 并不能互访)。

打开软件,鼠标放在右上角的 Online 上可以看到 本机的 IP 。或者也可以通过命令行中的 ipconfig 命令(Mac/Linux 是 ifconfig )查看。(截图仅为演示,以你自己的 IP 为准)

手机设置代理 IP。打开手机 无线网络连接 ,选择已经连接的网络连接,点击一个小圆圈叹号进入可以看到下图(安卓也类似),选择 配置代理 ,进入后把刚刚的 IP 地址 输入进去, 端口 就是 fiddler 中设置的 8888。

3. 安装证书

获取 HTTPS 请求必须要 验证证书 。电脑端访问:http://localhost:8888/ 进行安装。

手机访问前面设置的电脑的 IP 地址加端口 8888 访问,比如图中例子是:http://192.168.23.1:8888

有些安卓需要手动从设置里进入并导入证书,否则无法生效。

4. 测试

开启 fiddler 的状态下,打开手机随便一个 APP,应对可以正常访问,并且在 fiddler 中看到所发出的网络请求。

如果能访问但看不到请求,确认下有没有代理有没有生效。如果不能访问,检查下证书是否都下载并验证。还是不行则按照上述步骤再仔细配置一遍。

分析请求

完成这一步之后,接下来的事情就和网页爬虫没太大区别了。无非就是从这些请求中,找到我们需要的那几个。

fiddler 里记录的是所有请求,比较多。在操作 App 前,记得清空已有请求,方便观察。然后再配合上 filter 筛选器 ,定义筛选规则,会较容易找你需要的内容。找到请求后,在软件里查看你要的信息,或者右键点击选择将请求导出。

经过操作+观察,可以定位到获取用户上传视频列表的请求是



https://api.amemv.com/aweme/v1/aweme/post/?…

WebForms 栏里可以查看请求的详细参数信息。返回值是一个组 JSON 数据,里面包含了视频的下载地址。

这是一个需要经验积累的活儿,不同的网站/App,规则都不一样,但套路是相似的。对网页爬虫还不熟悉的话,先看看之前的文章 爬虫必备工具,掌握它就解决了一半的问题

代码抓取

得到地址之后,经过在浏览器和代码里的一番尝试,找到了此请求的正确解锁方式:

1. 需要提供以下参数:max_cursor=0&user_id=94763945245&count=20&aid=1128,其中 user_id 是你要抓取的用户 ID,其他参数都可以固定不用改。

2. 需要使用手机的 User-Agent ,最简单的就是 {'user-agent': 'mobile'}

请求代码:



import requests as rs
uid = 94763945245
url = 'https://api.amemv.com/aweme/v1/aweme/post/?max_cursor=0&user_id=%d&count=20&aid=1128' % uid
h = {'user-agent': 'mobile'}
req = rs.get(url, headers=h, verify=False)
data = req.json()
print(data)

uid 替换成你想抓的用户 ID。获取用户 ID 有个简单方法:在用户页面选择分享,链接发到微信上,从网页打开就可以看到 user_id。

提取视频列表并下载:



import urllib.request
for video in data['aweme_list']:
    name = video['desc'] or video['aweme_id']    
    url_v = video['video']['download_addr']['url_list'][0]
    print(name, url_v, '\n')
    urllib.request.urlretrieve(url_v, name + '.mp4')

此方法截止国庆假期还是有效的,可以通过 Chrome 开发者工具进行模拟。之后能使用多久这就没法保证了,爬虫代码都不会是一劳永逸的。

总结下,重点是 fiddler 的抓取 ,关键是 配置、代理、证书 ,难点是 对请求的分析 。最终代码只有简单两步, 获取视频列表、下载视频

所有代码其实就上面两段,也上传了,获取地址请在公众号( Crossin的编程教室 )回复关键字 抖音
想看其他十多个项目代码实例(电影票、招聘、贪吃蛇、代理池等),回复关键字 项目

下课!

════
其他文章及回答:
如何自学Python | 新手引导 | 精选Python问答 | 如何debug? | Python单词表 | 知乎下载器 | 人工智能 | 嘻哈 | 爬虫 | 我用Python | 高考 | requests | AI平台

欢迎微信搜索及关注: Crossin的编程教室

查看原文

后知后觉 赞了文章 · 2018-10-08

揭密 Vue 的双向绑定

Vue 中需要输入什么内容的时候,自然会想到使用 <input v-model="xxx" /> 的方式来实现双向绑定。下面是一个最简单的示例

<div id="app">
    <h2>What's your name:</h2>
    <input v-model="name" />
    <div>Hello {{ name }}</div>
</div>
new Vue({
    el: "#app",
    data: {
           name: ""
    }
});
JsFiddle 演示

https://jsfiddle.net/0okxhc6f/

在这个示例的输入框中输入的内容,会随后呈现出来。这是 Vue 原生对 <input> 的良好支持,也是一个父组件和子组件之间进行双向数据传递的典型示例。不过 v-model 是 Vue 2.2.0 才加入的一个新功能,在此之前,Vue 只支持单向数据流。

Vue 的单向数据流

Vue 的单向数据流和 React 相似,父组件可以通过设置子组件的属性(Props)来向子组件传递数据,而父组件想获得子组件的数据,得向子组件注册事件,在子组件高兴的时候触发这个事件把数据传递出来。一句话总结起来就是,Props 向下传递数据,事件向上传递数据。

上面那个例子,如果不使用 v-model,它应该是这样的

<input :value="name" @input="name = $event.target.value" />

由于事件处理写成了内联模式,所以脚本部分不需要修改。但是多数情况下,事件一般都会定义成一个方法,代码就会复杂得多

<input :value="name" @input="updateName" />
new Vue({
    // ....
    methods: {
        updateName(e) {
            this.name = e.target.value;
        }
    }
})

从上面的示例来看 v-model 节约了不少代码,最重要的是可以少定义一个事件处理函数。所以 v-model 实际干的事件包括

  • 使用 v-bind(即 :)单向绑定一个属性(示例::value="name"
  • 绑定 input 事件(即 @input)到一个默认实现的事件处理函数(示例:@input=updateName
  • 这个默认的事件处理函数会根据事件对象带入的值来修改被绑定的数据(示例:this.name = e.target.value

自定义组件的 v-model

Vue 对原生组件进行了封装,所以 <input> 在输入的时候会触发 input 事件。但是自定义组件应该怎么呢?这里不妨借助 JsFiddle Vue 样板的 Todo List 示例。

JsFiddle 的 Vue 样板

点击 JsFilddle 的 Logo,在上面弹出面板中选择 Vue 样板即可

样板代码包含 HTML 和 Vue(js) 两个部分,代码如下:

<div id="app">
  <h2>Todos:</h2>
  <ol>
    <li v-for="todo in todos">
      <label>
        <input type="checkbox"
          v-on:change="toggle(todo)"
          v-bind:checked="todo.done">

        <del v-if="todo.done">
          {{ todo.text }}
        </del>
        <span v-else>
          {{ todo.text }}
        </span>
      </label>
    </li>
  </ol>
</div>
new Vue({
  el: "#app",
  data: {
    todos: [
      { text: "Learn JavaScript", done: false },
      { text: "Learn Vue", done: false },
      { text: "Play around in JSFiddle", done: true },
      { text: "Build something awesome", done: true }
    ]
  },
  methods: {
      toggle: function(todo){
        todo.done = !todo.done
    }
  }
})

定义 Todo 组件

JsFiddle 的 Vue 模板默认实现一个 Todo 列表的展示,数据是固定的,所有内容在一个模板中完成。我们首先要做事情是把单个 Todo 改成一个子组件。因为在 JsFiddle 中不能写成多文件的形式,所以组件使用 Vue.component() 在脚本中定义,主要是把 <li> 内容中的那部分拎出来:

Vue.component("todo", {
    template: `
<label>
    <input type="checkbox" @change="toggle" :checked="isDone">
    <del v-if="isDone">
        {{ text }}
    </del>
    <span v-else>
        {{ text }}
    </span>
</label>
`,
    props: ["text", "done"],
    data() {
        return {
            isDone: this.done
        };
    },
    methods: {
        toggle() {
            this.isDone = !this.isDone;
        }
    }
});

原来定义在 App 中的 toggle() 方法也稍作改动,定义在组件内了。toggle() 调用的时候会修改表示是否完成的 done 的值。但由于 done 是定义在 props 中的属性,不能直接赋值,所以采用了官方推荐的第一种方法,定义一个数据 isDone,初始化为 this.done,并在组件内使用 isDone 来控制是否完成这一状态。

相应的 App 部分的模板和代码精减了不少:

<div id="app">
    <h2>Todos:</h2>
    <ol>
        <li v-for="todo in todos">
            <todo :text="todo.text" :done="todo.done"></todo>
        </li>
    </ol>
</div>
new Vue({
    el: "#app",
    data: {
        todos: [
            { text: "Learn JavaScript", done: false },
            { text: "Learn Vue", done: false },
            { text: "Play around in JSFiddle", done: true },
            { text: "Build something awesome", done: true }
        ]
    }
});
JsFiddle 演示

https://jsfiddle.net/0okxhc6f/1/

不过到此为止,数据仍然是单向的。从效果上来看,点击复选框可以反馈出删除线线效果,但这些动态变化都是在 todo 组件内部完成的,不存在数据绑定的问题。

为 Todo List 添加计数

为了让 todo 组件内部的状态变化能在 Todo List 中呈现出来,我们在 Todo List 中添加计数,展示已经完成的 Todo 数量。因为这个数量受 todo 组件内部状态(数据)的影响,这就需要将 todo 内部数据变化反应到其父组件中,这才有 v-model 的用武之地。

这个数量我们在标题中以 n/m 的形式呈现,比如 2/4 表示一共 4 条 Todo,已经完成 2 条。这需要对 Todo List 的模板和代码部分进行修改,添加 countDonecount 两个计算属性:

<div id="app">
    <h2>Todos ({{ countDone }}/{{ count }}):</h2>
    <!-- ... -->
</div>
new Vue({
    // ...
    computed: {
        count() {
            return this.todos.length;
        },
        countDone() {
            return this.todos.filter(todo => todo.done).length;
        }
    }
});

现在计数呈现出来了,但是现在改变任务状态并不会对这个计数产生影响。我们要让子组件的变动对父组件的数据产生影响。v-model 待会儿再说,先用最常见的方法,事件:

  • 子组件 todotoggle() 中触发 toggle 事件并将 isDone 作为事件参数
  • 父组件为子组件的 toggle 事件定义事件处理函数
Vue.component("todo", {
    //...
    methods: {
        toggle(e) {
            this.isDone = !this.isDone;
            this.$emit("toggle", this.isDone);
        }
    }
});
<!-- #app 中其它代码略 -->
<todo :text="todo.text" :done="todo.done" @toggle="todo.done = $event"></todo>

这里为 @toggle 绑定的是一个表达式。因为这里的 todo 是一个临时变量,如果在 methods 中定义专门的事件处理函数很难将这个临时变量绑定过去(当然定义普通方法通过调用的形式是可以实现的)。

事件处理函数,一般直接对应于要处理的事情,比如定义 onToggle(e),绑定为 @toggle="onToggle"。这种情况下不能传入 todo 作为参数。

普通方法,可以定义成 toggle(todo, e),在事件定义中以函数调用表达式的形式调用:@toggle="toggle(todo, $event)"。它和 todo.done = $event` 同属表达式。

注意二者的区别,前者是绑定的处理函数(引用),后者是绑定的表达式(调用)

现在通过事件方式已经达到了预期效果

Js Fiddle 演示

https://jsfiddle.net/0okxhc6f/2/

改造成 v-model

之前我们说了要用 v-model 实现的,现在来改造一下。注意实现 v-model 的几个要素

  • 子组件通过 value 属性(Prop)接受输入
  • 子组件通过触发 input 事件输出,带数组参数
  • 父组件中用 v-model 绑定
Vue.component("todo", {
    // ...
    props: ["text", "value"],   // <-- 注意 done 改成了 value
    data() {
        return {
            isDone: this.value    // <-- 注意 this.done 改成了 this.value
        };
    },
    methods: {
        toggle(e) {
            this.isDone = !this.isDone;
            this.$emit("input", this.isDone);  // <-- 注意事件名称变了
        }
    }
});
<!-- #app 中其它代码略 -->
<todo :text="todo.text" v-model="todo.done"></todo>

.sync 实现其它数据绑定

前面讲到了 Vue 2.2.0 引入 v-model 特性。由于某些原因,它的输入属性是 value,但输出事件叫 inputv-modelvalueinput 这三个名称从字面上看不到半点关系。虽然这看起来有点奇葩,但这不是重点,重点是一个控件只能双向绑定一个属性吗?

Vue 2.3.0 引入了 .sync 修饰语用于修饰 v-bind(即 :),使之成为双向绑定。这同样是语法糖,添加了 .sync 修饰的数据绑定会像 v-model 一样自动注册事件处理函数来对被绑定的数据进行赋值。这种方式同样要求子组件触发特定的事件。不过这个事件的名称好歹和绑定属性名有点关系,是在绑定属性名前添加 update: 前缀。

比如 <sub :some.sync="any" /> 将子组件的 some 属性与父组件的 any 数据绑定起来,子组件中需要通过 $emit("update:some", value) 来触发变更。

上面的示例中,使用 v-model 绑定始终感觉有点别扭,因为 v-model 的字面意义是双向绑定一个数值,而表示是否未完成的 done 其实是一个状态,而不是一个数值。所以我们再次对其进行修改,仍然使用 done 这个属性名称(而不是 value),通过 .sync 来实现双向绑定。

Vue.component("todo", {
    // ...
    props: ["text", "done"],   // <-- 恢复成 done
    data() {
        return {
            isDone: this.done    // <-- 恢复成 done
        };
    },
    methods: {
        toggle(e) {
            this.isDone = !this.isDone;
            this.$emit("update:done", this.isDone);  // <-- 事件名称:update:done
        }
    }
});
<!-- #app 中其它代码略 -->
<!-- 注意 v-model 变成了 :done.sync,别忘了冒号哟 -->
<todo :text="todo.text" :done.sync="todo.done"></todo>
Js Fiddle 演示

https://jsfiddle.net/0okxhc6f/3/

揭密 Vue 双向绑定

通过上面的讲述,我想大家应该已经明白了 Vue 的双向绑定其实就是普通单向绑定和事件组合来完成的,只不过通过 v-model.sync 注册了默认的处理函数来更新数据。Vue 源码中有这么一段

// @file: src/compiler/parser/index.js

if (modifiers.sync) {
    addHandler(
        el,
        `update:${camelize(name)}`,
        genAssignmentCode(value, `$event`)
    )
}

从这段代码可以看出来,.sync 双向绑定的时候,编译器会添加一个 update:${camelize(name)} 的事件处理函数来对数据进行赋值(genAssignmentCode 的字面意思是生成赋值的代码)。

展望

目前 Vue 的双向绑定还需要通过触发事件来实现数据回传。这和很多所的期望的赋值回传还是有一定的差距。造成这一差距的主要原因有两个

  1. 需要通过事件回传数据
  2. 属性(prop)不可赋值

在现在的 Vue 版本中,可以通过定义计算属性来实现简化,比如

computed: {
    isDone: {
        get() {
            return this.done;
        },
        set(value) {
            this.$emit("update:done", value);
        }
    }
}

说实在的,要多定义一个意义相同名称不同的变量名也是挺费脑筋的。希望 Vue 在将来的版本中可以通过一定的技术手段减化这一过程,比如为属性(Prop)声明添加 sync 选项,只要声明 sync: true 的都可以直接赋值并自动触发 update:xxx 事件。

当然作为一个框架,在解决一个问题的时候,还要考虑对其它特性的影响,以及框架的扩展性等问题,所以最终双向绑定会演进成什么样子,我们对 Vue 3.0 拭目以待。

查看原文

赞 96 收藏 71 评论 10

后知后觉 回答了问题 · 2018-08-27

求一个匹配下一个冒号之前的分号

var str =string.split(";")[2]

关注 4 回答 3

后知后觉 收藏了文章 · 2018-06-28

剖析Vue原理&实现双向绑定MVVM

本文能帮你做什么?
1、了解vue的双向数据绑定原理以及核心代码模块
2、缓解好奇心的同时了解如何实现双向绑定
为了便于说明原理与实现,本文相关代码主要摘自vue源码, 并进行了简化改造,相对较简陋,并未考虑到数组的处理、数据的循环依赖等,也难免存在一些问题,欢迎大家指正。不过这些并不会影响大家的阅读和理解,相信看完本文后对大家在阅读vue源码的时候会更有帮助<
本文所有相关代码均在github上面可找到 https://github.com/DMQ/mvvm

相信大家对mvvm双向绑定应该都不陌生了,一言不合上代码,下面先看一个本文最终实现的效果吧,和vue一样的语法,如果还不了解双向绑定,猛戳Google

<div id="mvvm-app">
    <input type="text" v-model="word">
    <p>{{word}}</p>
    <button v-on:click="sayHi">change model</button>
</div>

<script data-original="./js/observer.js"></script>
<script data-original="./js/watcher.js"></script>
<script data-original="./js/compile.js"></script>
<script data-original="./js/mvvm.js"></script>
<script>
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            word: 'Hello World!'
        },
        methods: {
            sayHi: function() {
                this.word = 'Hi, everybody!';
            }
        }
    });
</script>

效果:
图片描述

几种实现双向绑定的做法

目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和 view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。

实现数据绑定的做法有大致如下几种:

发布者-订阅者模式(backbone.js)

脏值检查(angular.js)

数据劫持(vue.js)

发布者-订阅者模式: 一般通过sub, pub的方式实现数据和视图的绑定监听,更新数据方式通常做法是 vm.set('property', value),这里有篇文章讲的比较详细,有兴趣可点这里

这种方式现在毕竟太low了,我们更希望通过 vm.property = value 这种方式更新数据,同时自动更新视图,于是有了下面两种方式

脏值检查: angular.js 是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过 setInterval() 定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:

  • DOM事件,譬如用户输入文本,点击按钮等。( ng-click )
  • XHR响应事件 ( $http )
  • 浏览器Location变更事件 ( $location )
  • Timer事件( $timeout , $interval )
  • 执行 $digest() 或 $apply()

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

思路整理

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的,无疑这个方法是本文中最重要、最基础的内容之一,如果不熟悉defineProperty,猛戳这里
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者

上述流程如图所示:
图片描述

1、实现Observer

ok, 思路已经整理完毕,也已经比较明确相关逻辑和模块功能了,let's do it
我们知道可以利用Obeject.defineProperty()来监听属性变动
那么将需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 settergetter
这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化。。相关代码可以是这样:

var data = {name: 'kindeng'};
observe(data);
data.name = 'dmq'; // 哈哈哈,监听到值变化了 kindeng --> dmq

function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    // 取出所有属性遍历
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};

function defineReactive(data, key, val) {
    observe(val); // 监听子属性
    Object.defineProperty(data, key, {
        enumerable: true, // 可枚举
        configurable: false, // 不能再define
        get: function() {
            return val;
        },
        set: function(newVal) {
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
        }
    });
}

这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:

// ... 省略
function defineReactive(data, key, val) {
    var dep = new Dep();
    observe(val); // 监听子属性

    Object.defineProperty(data, key, {
        // ... 省略
        set: function(newVal) {
            if (val === newVal) return;
            console.log('哈哈哈,监听到值变化了 ', val, ' --> ', newVal);
            val = newVal;
            dep.notify(); // 通知所有订阅者
        }
    });
}

function Dep() {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?
没错,上面的思路整理中我们已经明确订阅者应该是Watcher, 而且var dep = new Dep();是在 defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在 getter里面动手脚:

// Observer.js
// ...省略
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addSub(Dep.target);
        return val;
    }
    // ... 省略
});

// Watcher.js
Watcher.prototype = {
    get: function(key) {
        Dep.target = this;
        this.value = data[key];    // 这里会触发属性的getter,从而添加订阅者
        Dep.target = null;
    }
}

这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能,完整代码。那么接下来就是实现Compile了

2、实现Compile

compile主要做的事情是解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图,如图所示:
图片描述

因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中

function Compile(el) {
    this.$el = this.isElementNode(el) ? el : document.querySelector(el);
    if (this.$el) {
        this.$fragment = this.node2Fragment(this.$el);
        this.init();
        this.$el.appendChild(this.$fragment);
    }
}
Compile.prototype = {
    init: function() { this.compileElement(this.$fragment); },
    node2Fragment: function(el) {
        var fragment = document.createDocumentFragment(), child;
        // 将原生节点拷贝到fragment
        while (child = el.firstChild) {
            fragment.appendChild(child);
        }
        return fragment;
    }
};

compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:

Compile.prototype = {
    // ... 省略
    compileElement: function(el) {
        var childNodes = el.childNodes, me = this;
        [].slice.call(childNodes).forEach(function(node) {
            var text = node.textContent;
            var reg = /\{\{(.*)\}\}/;    // 表达式文本
            // 按元素节点方式编译
            if (me.isElementNode(node)) {
                me.compile(node);
            } else if (me.isTextNode(node) && reg.test(text)) {
                me.compileText(node, RegExp.$1);
            }
            // 遍历编译子节点
            if (node.childNodes && node.childNodes.length) {
                me.compileElement(node);
            }
        });
    },

    compile: function(node) {
        var nodeAttrs = node.attributes, me = this;
        [].slice.call(nodeAttrs).forEach(function(attr) {
            // 规定:指令以 v-xxx 命名
            // 如 <span v-text="content"></span> 中指令为 v-text
            var attrName = attr.name;    // v-text
            if (me.isDirective(attrName)) {
                var exp = attr.value; // content
                var dir = attrName.substring(2);    // text
                if (me.isEventDirective(dir)) {
                    // 事件指令, 如 v-on:click
                    compileUtil.eventHandler(node, me.$vm, exp, dir);
                } else {
                    // 普通指令
                    compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
                }
            }
        });
    }
};

// 指令处理集合
var compileUtil = {
    text: function(node, vm, exp) {
        this.bind(node, vm, exp, 'text');
    },
    // ...省略
    bind: function(node, vm, exp, dir) {
        var updaterFn = updater[dir + 'Updater'];
        // 第一次初始化视图
        updaterFn && updaterFn(node, vm[exp]);
        // 实例化订阅者,此操作会在对应的属性消息订阅器中添加了该订阅者watcher
        new Watcher(vm, exp, function(value, oldValue) {
            // 一旦属性值有变化,会收到通知执行此更新函数,更新视图
            updaterFn && updaterFn(node, value, oldValue);
        });
    }
};

// 更新函数
var updater = {
    textUpdater: function(node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    }
    // ...省略
};

这里通过递归遍历保证了每个节点及子节点都会解析编译到,包括了{{}}表达式声明的文本节点。指令的声明规定是通过特定前缀的节点属性来标记,如<span v-text="content" other-attrv-text便是指令,而other-attr不是指令,只是普通的属性。
监听数据、绑定更新函数的处理是在compileUtil.bind()这个方法中,通过new Watcher()添加回调来接收数据变化的通知

至此,一个简单的Compile就完成了,完整代码。接下来要看看Watcher这个订阅者的具体实现了

3、实现Watcher

Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:
1、在自身实例化时往属性订阅器(dep)里面添加自己
2、自身必须有一个update()方法
3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
如果有点乱,可以回顾下前面的思路整理

function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    // 此处为了触发属性的getter,从而在dep添加自己,结合Observer更易理解
    this.value = this.get(); 
}
Watcher.prototype = {
    update: function() {
        this.run();    // 属性值变化收到通知
    },
    run: function() {
        var value = this.get(); // 取到最新值
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal); // 执行Compile中绑定的回调,更新视图
        }
    },
    get: function() {
        Dep.target = this;    // 将当前订阅者指向自己
        var value = this.vm[exp];    // 触发getter,添加自己到属性订阅器中
        Dep.target = null;    // 添加完毕,重置
        return value;
    }
};
// 这里再次列出Observer和Dep,方便理解
Object.defineProperty(data, key, {
    get: function() {
        // 由于需要在闭包内添加watcher,所以可以在Dep定义一个全局target属性,暂存watcher, 添加完移除
        Dep.target && dep.addDep(Dep.target);
        return val;
    }
    // ... 省略
});
Dep.prototype = {
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update(); // 调用订阅者的update方法,通知变化
        });
    }
};

实例化Watcher的时候,调用get()方法,通过Dep.target = watcherInstance标记订阅者是当前watcher实例,强行触发属性定义的getter方法,getter方法执行的时候,就会在属性的订阅器dep添加当前watcher实例,从而在属性值有变化的时候,watcherInstance就能收到更新通知。

ok, Watcher也已经实现了,完整代码
基本上vue中数据绑定相关比较核心的几个模块也是这几个,猛戳这里 , 在src 目录可找到vue源码。

最后来讲讲MVVM入口文件的相关逻辑和实现吧,相对就比较简单了~

4、实现MVVM

MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

一个简单的MVVM构造器是这样子:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data;
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过var vm = new MVVM({data:{name: 'kindeng'}}); vm._data.name = 'dmq'; 这样的方式来改变数据。

显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:
var vm = new MVVM({data: {name: 'kindeng'}}); vm.name = 'dmq';

所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:

function MVVM(options) {
    this.$options = options;
    var data = this._data = this.$options.data, me = this;
    // 属性代理,实现 vm.xxx -> vm._data.xxx
    Object.keys(data).forEach(function(key) {
        me._proxy(key);
    });
    observe(data, this);
    this.$compile = new Compile(options.el || document.body, this)
}

MVVM.prototype = {
    _proxy: function(key) {
        var me = this;
        Object.defineProperty(me, key, {
            configurable: false,
            enumerable: true,
            get: function proxyGetter() {
                return me._data[key];
            },
            set: function proxySetter(newVal) {
                me._data[key] = newVal;
            }
        });
    }
};

这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈

至此,全部模块和功能已经完成了,如本文开头所承诺的两点。一个简单的MVVM模块已经实现,其思想和原理大部分来自经过简化改造的vue源码,猛戳这里可以看到本文的所有相关代码。
由于本文内容偏实践,所以代码量较多,且不宜列出大篇幅代码,所以建议想深入了解的童鞋可以再次结合本文源代码来进行阅读,这样会更加容易理解和掌握。

总结

本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~

最后,感谢您的阅读!

查看原文

认证与成就

  • 获得 3 次点赞
  • 获得 27 枚徽章 获得 2 枚金徽章, 获得 9 枚银徽章, 获得 16 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-11-21
个人主页被 300 人浏览