CHENGKANG

CHENGKANG 查看完整档案

海外编辑University of Southampton  |  Web Technology 编辑Utility Warehouse  |  前端工程师 编辑 chengkang.me 编辑
编辑

LOADING...
...

GETTING TO KNOW THE WORLD.
WITH CURIOSITY, WITH PASSTION, WITH LOVE.
WITH CATHERINE.

...

个人动态

CHENGKANG 评论了文章 · 2018-08-25

Hexo deploy git permission denied

原因

前几天在自己电脑上帮别人在Github上搭Hexo,部署的时候自然就填写了他的github账号密码。

然后今天给自己搭的时候,一直都报permission denied错误。可以从错误提示里面看到,提交代码用的账号是之前别人的账号。

试着改github config.ssh/id_rsa这些东西,结果都没用。

最后在stackoverflow上面找到了一个答案。原链接被我关掉了……所以记录一下。

解决办法

原因是Mac保存了上次输入的账号密码,自动填写了。

所以解决办法是cmd+space,输入keychain access,选择左上方login+左下方password,搜索github,找到对应的记录,删除就好了。

以上。

查看原文

CHENGKANG 赞了回答 · 2018-02-18

解决Vue如何动态修改template?

@SayGoodBey ,恰好看到你问了,那就写出我的方法吧。
我是在一个子组件中实现的,你可以动态的添加该子组件:
下面的content是markdown格式的数据,
../common/markdown文件是自己写好的基于marked的解析函数,它会将Markdown格式析为Vue格式的字符串:

![图片文字](url)
// 上面会解析为:
<img data-original="url" @click="showInfo('图片文字')">

用下面的方法即可以实现点击图片时,会输出信息。当然其他的vue处理方法同样支持。

<template >
  <div ref="markedContent"></div>
</template>

<script>
import Vue from 'vue'
import markdown from '../common/markdown'
export default {
  name: 'wf-marked-content',
  props: ['content'],
  mounted () {

    // 调用compile方法。你也可以将写在这里。
    // 但是代码太多,我个人不喜欢
    this.compile()
  },
  methods: {
    compile () {
      // 变量html是生成好的vue格式的HTML模板字符串,
      // 这个模板里面可以包含各种vue的指令,数据绑定等操作,
      // 比如 v-if, :bind, @click 等。
      const html = markdown(this.content)
      
      // Vue.extend是vue的组件构造器,专门用来构建自定义组件的,
      // 但是不会注册,类似于js中的createElement,
      // 创建但是不会添加。
      // 在这里创建出一个子组件对象构造器。
      const Component = Vue.extend({
      
        // 模板文件。由于Markdown解析之后可能会有多个根节点,
        // 因此需要包裹起来。
        // 实际的内容是:
        // `<div><img data-original="url" @click="showInfo(`图片文字')"></div>`
        template: `<div> ${html} </div>`,
        
        // 这里面写的就是这个动态生成的新组件中的方法了,
        // 当然你也可加上data、mounted、updated、watch、computed等等。
        methods: {
        
          // 上面模板中将点击事件绑定到了这里,因此点击了之后就会调用这个函数。
          // 你可以写多个函数在这里,但是这里的函数的作用域只限在这个子组件中。
          showInfo (title) {
            console.log(title)
          }
        }
      })
      
      // new Component()是将上面构建的组件对象给实例化,
      // $mount()是将实例化的组件进行手动挂载,
      // 将虚拟dom生成出实际渲染的dom,
      // 这里的markedComponent是完成挂载以后的子组件
      const markedComponent = new Component().$mount()
      
      // 将挂载以后的子组件dom插入到父组件中
      // markedComponent.$el就是挂载后生成的渲染dom
      this.$refs['markedContent'].appendChild(markedComponent.$el)
    }
  }
  // 本质上来讲,这个子组件不是任何组件的子组件,
  // 它是由vue直接在全局动态生成的一个匿名组件,然后将它插入到当前位置的。
  // 也正是因此,它才能够完成动态的生成和添加。
}
</script>

额,解决这个问题是为了写一个支持Markdown格式的评论系统。所以顺便在这里推荐一下:
Wildfire: A drop-in replacement for other comment plug-ins.
一定要点进去,好看又好用的评论系统。

关注 8 回答 7

CHENGKANG 发布了文章 · 2017-12-19

我和廖总的新项目:Wildfire 野火评论系统 :-D

Wildfire

如果你的网站需要一个方便的评论系统的话,不妨试试 Wildfire 野火评论系统。

全新的 Wildfire 主页 (https://wildfire.js.org/#/zh-cn/) 已经启用了,快去那儿看看详细的文档吧。

Please visit the brand new Wildfire Home Page (https://wildfire.js.org). Detailed documentation is available.

Click here to read English version README.md.

简介

wildfire 希望成为一个 就是管用 的个人网站评论系统,比如你的 Hexo 博客。它利用免费的实时数据库(Firebase野狗)来储存你的评论数据,并带给你实时的交流体验。

继续阅读 起步,开始你的 wildire 野火评论之旅吧!

功能

站主:

  • 数据库支持:

  • 站主功能:

    • 删除评论
    • 通过用户 IP 或 Email 进行屏蔽

所有用户(匿名及登录用户):

  • 评论(支持 Markdown 语法)
  • 提及(@其他用户)

登录用户:

  • 赞、踩评论
  • 删除自己的评论
  • 举报包含不恰当言论的评论
  • 更新个人资料:

    • 用户名
    • 头像
  • 个人中心:

    • 通知消息

展示

更多展示,请看这个百科页面:我们正在使用 Wildfire 野火评论系统!

开源许可协议

GNU General Public License v3.0

查看原文

赞 10 收藏 47 评论 4

CHENGKANG 赞了文章 · 2017-10-05

iView 一周年了,同时发布了 2.0 正式版,但这只是开始...

图片描述

两年前,我开始接触 Vue.js 框架,当时就被它的轻量、组件化和友好的 API 所吸引。之后我将 Vue.js 和 Webpack 技术栈引入我的公司(TalkingData)可视化团队,并经过一年多的实践,现已成为整个公司的前端开发规范。
与此同时,我开源了 iView 项目,它是基于 Vue.js 的一套高质量 UI 组件库,从设计规范、工程构建到国际化都提供了完整的解决方案,并支持 SSR。在许多志愿者的帮助下,将文档全部翻译为英文,在 Vue 开发者社区颇受欢迎。

去年的 7 月 28 日,一个名叫 iView(github.com/iview/iview) 的工程立项了,今天正好是它一周岁生日。回想当初只是作为一个团队组件库来开发,风风雨雨经历了一年,已经成为 Vue 社区最受欢迎的组件库之一了,在 GitHub 也收获了近 8000 Star,npm 每月有 10k 多安装。当然,这一切的成果,都离不开公司给予我的支持和 Vue 社区的活跃。

彩蛋


既然是纪念日,就来点好玩的吧。我们重新装饰了iView 官网,今天开始的一周里,可以在首页发弹幕,来和世界各地的朋友们一起畅(chě)聊(dàn)吧。

2.0

iView 一周年,对于开发者来说最好的消息,就是我们终于发布 2.0 的第一个正式版。在过去的 4 个多月里,我们连续发布了 19 个 rc 版,其实早可以发布 2.0 正式版的,并在此基础上迭代。迟迟不发,就是想把最好的一个版本带给大家。当然,即使今天发布了正式版,依然还有不少问题等待解决,一个好的组件库是需要经历不断磨练。

来回顾一下,从 1.0 到 2.0,iView 有哪些重要改变:

最重要的当然是支持 Vue 2 了,其次支持了 SSR 和 Nuxt.js,对常用组件进行了扩展:Table 单元格的渲染基于 Vue 的 Render 函数,并支持展开;Select 支持远程搜索;Cascader 支持异步和搜索。还有 100 多项细节优化。2.0 正式版,更是加强了 Table组件,优化了其 8 个问题,所有的浮层类组件都增加了 transfer 属性,可以选择是否将其插入到 body 内,使用更灵活。具体的更新内容可以查看更新日志

聊聊英文版

上个月,我们已将 iView 文档全部翻译为英文,前前后后进行了 3 个多月才算完成。有了英文版后,更多的国外开发者也参与到 GitHub 讨论之中,相信接下来会有更多来自国外的 contributors。

上图是来自 Vue 社区一位国外开发者的评价。

英文版的翻译离不开社区的贡献,在此特别感谢 lcx960324rijnBigChief45 三位热心志愿者。

版本号的故事

iView 从 2.0 版本开始,版本代号将以获得过 Apple Design Awards 的游戏或优秀的独立游戏来命名。

每个开源项目的版本代号都有它的故事。作为一枚热爱 iOS 独立游戏的玩家,每每看到画风精致、剧情动人的好游戏都会流连忘返,推荐给身边的同事,所以用它来命名,也算是对独立游戏的一种支持。

2.x 的第一个版本(2.0)代号是 Leo's Fortune(里奥的财富)。

这只是开始

iView 已经很好地帮助前端开发工程师们加速完成中后台业务开发,但作为一个有追求的大数据可视化团队,这仅仅只是一个开始。下半年,TalkingData 可视化团队将继续开源两个重磅级项目:

  • InMap:基于 Canvas 和 WebGL 的地理信息可视化框架

  • InChart:基于 Vue.js 和 eCharts 的图表标准库

这两个项目都会给数据可视化带来福音,其中值得详细介绍的是 InMap,事实上它的历史要早于 iView,只是一直属于闭源状态,在 TalkingData 内部使用。这次也是鼓足了劲,在 API 和易用性上进行重构后开源。

在地理可视化的过程中,因为地图的矢量数据不是一次性加载的,矢量数据是随着用户的请求,随着瓦片一起加载到本地的。在这个过程当中存在两部分的计算比较多,限制了可视化绘制的速度,一个就是各种数据的坐标转换,另一个就是各种数据的可视化。目前 InMap 中使用了 web worker 在后台对矢量数据使用多线程计算,保证页面对用户响应的同时对各种数据进行计算,对于其他适合并行计算的坐标数据采用 GPU 进行转换。绘制可视化层使用了 WebGL。

下图是基于 InMap 实现的一个全 3D 地球,可以实现自传、公转、随意拖拽旋转、放大缩小等立体效果。

InMap 和 InChart 预计会在数月后和大家见面,敬请期待!

对开源现状的一些思考

能够完整参与一个开源项目,见证它从 0 到 1 的改变,对我来说确实是一件幸运的事,因为从这段开源经历里,学到了太多的知识,也结实了很多开发者。开源是一件很有意义的事情,然而很多国内开发者却有一个不好的习惯,他们被统称为“伸手党”。相比很多国内开发者提出的 issues,我更喜欢去解答国外友人的英文 issues,这不是因为英文看起来有多高大上,而是国外朋友的提问都很友善,而且是经过深思熟虑的。在求知的态度礼帽上,这点国人做的确实不好,如果你 watch 了 iView 的项目,每天会收到几十封邮件,其中大多没有按照 issue guide 的要求来问,close 掉,还经常被骂,一阵负能量。试问,这些不按要求问题,还态度恶劣的人,你们为开源做过什么贡献呢?开源是免费,但不是说用了开源产品就是大爷,那么牛逼,干嘛不自己造轮子呢。

这样的问题不仅仅发生在 iView 上,我同 VUX(知名移动端 Vue 组件库) 的作者也深刻探讨过,在 VUX 的社区也会带有不少类似的负能量。这种不良现象在国内应该还有很多,VUX 的 readme 曾今写的这段仅有的中文,正是写给这部分人看的:

我相信提 issue 的朋友初衷都是好的,都是来解决问题的,只是聪明的、真正想解决问题的人,都会花费一些时间来认真填写 issue 内容。付出就有回报,任何一个认真的提问,我也会认真的回答,而那些随随便便提的问题,连代码格式化也不会的也会被随随便便关掉。

开源是世界的,所以请善待 GitHub 社区。

最佳实践

从 2016 年下旬开始,TalkingData 的众多新项目开始使用 iView,部分核心项目已逐步开始使用 iView 重构。以下是部分项目的截图:

使用者的心声

TalkingData 数据工程师 王祥:
上半年,部门的两条重要产品线 App Analytics 和智能数据市场 (SDMK),用 Vue+iView 完成了产品重构,效果显著。下半年,其它几个重要产品,也计划引入 Vue+iView 技术栈。iView 的引入,保证了各产品线的视觉统一,设计师几乎不需要参与到产品研发过程,降低了人力成本;工程师们可以更专注在业务上,避免了除了业务代码还要维护基础的组件,极大的缩短了产品研发周期。强烈建议采用 Vue 技术栈的团队,也尝试下 iView。感谢 iView!

TalkingData 数据工程师 杨涛:
从使用 iView 0.9.x 版本到目前的 2.x 版本,见证了 iView 从实现各类组件到丰富组件功能的每一步,使得我们在团队作战中大大提高了项目开发效率。在我们的多个项目中使用了 iView 的各类组件,它完善的文档和示例代码降低了学习成本,同时 iView 也在高频率的更新完善,选择 iView 作为前端 UI 组件简单易用大大加快了项目进度的同时更多的是感受到 iView 的工匠精神。

汉云优品 产品经理 关静凯:
我是一个爱搞技术的产品经理,在看到 iView 之后,被 iView 整体的 API 和 UX 设计所折服,非常容易上手,起初只是在产品设计上使用 iView 快速实现中后台 Demo,iView 使产品的交互和 UI 保持了高度一致性,在对公司前端团队培训后,逐渐完成了前端工程化,现在公司的产品已经使用了 Vue.js 和 iView 进行了重构。iView 的源代码非常规范,二次开发及其友好,目前正结合公司现有业务,进行了一些组件的添加,主要集中在数据可视化和一些业务组件的封装,iView 给公司带来的不仅仅是前端技术的革新,更重要的是推动了产品迭代的速度,因为成功引入 iView,我也获得了公司层面的支持整合设计和前端组建了 UED 部门。再次感谢iView 框架带来的便利。

总结

接下来要做的事情还有很多,我们会通过 iView 在大量实战项目中的使用,在组件的易用性、稳定性上不断提升和改良,也会在 UI 细节上更加规范、漂亮。

这里也要特别感谢所有为 iView 项目付出努力的贡献者们,一个好的项目,是需要大家共同创造和维护的。也借此文,呼吁有工匠精神,热爱开源的开发者们能够加入到 iView 项目里来,一起把它打造成世界级优秀组件库。

福利

为庆祝 iView 一周年暨 2.0 版发布,Vue.js 系列课程一律 6.6 元,持续一周。

特别感谢

最后特别感谢以下技术社区长期以来对 iView 开源项目的推广与支持(排名不分先后):

查看原文

赞 112 收藏 189 评论 47

CHENGKANG 赞了文章 · 2017-09-26

源码分析:Vue的双向数据绑定

虽然工作中一直使用Vue作为基础库,但是对于其实现机理仅限于道听途说,这样对长期的技术发展很不利。所以最近攻读了其源码的一部分,先把双向数据绑定这一块的内容给整理一下,也算是一种学习的反刍。

本篇文章的Vue源码版本为v2.2.0开发版

Vue源码的整体架构无非是初始化Vue对象,挂载数据data/props等,在不同的时期触发不同的事件钩子,如created() / mounted() / update()等,后面专门整理各个模块的文章。这里先讲双向数据绑定的部分,也是最主要的部分。

设计思想:观察者模式

Vue的双向数据绑定的设计思想为观察者模式,为了方便,下文中将被观察的对象称为观察者,将观察者对象触发更新的称为订阅者。主要涉及到的概念有:

  1. Dep对象:Dependency依赖的简写,包含有三个主要属性id, subs, target和四个主要函数addSub, removeSub, depend, notify,是观察者的依赖集合,负责在数据发生改变时,使用notify()触发保存在subs下的订阅列表,依次更新数据和DOM。

    id: 每个观察者(依赖对象)的唯一标识。
    subs: 观察者对象的订阅者列表。
    target: 全局唯一的订阅者对象,因为只能同时计算和更新一个订阅者的值。
    addSub(): 使用`push()`方法添加一个订阅者。
    removeSub(): 使用`splice()`方法移除一个订阅者。
    depend(): 将自己添加到当前订阅者对象的依赖列表。
    notify(): 在数据被更新时,会遍历subs对象,触发每一个订阅者的更新。
  2. Observer对象:即观察者,包含两个主要属性value, dep。做法是使用getter/setter方法覆盖默认的取值和赋值操作,将对象封装为响应式对象,每一次调用时更新依赖列表,更新值时触发订阅者。绑定在对象的__ob__原型链属性上。

    value: 原始值。
    dep: 依赖列表。

源码实战解析

有过Vue开发基础的应该都了解其怎么初始化一个Vue对象:

new Vue({
    el: '#container',
    data: {
        count: 100
    },
    ...
});

那么我们就从这个count说起,看它是怎么完成双向数据绑定的。

下面的代码片段中英文注释为尤雨溪所写,中文注释为我所写,英文注释更能代表开发者的清晰思路。

首先从全局的初始化函数调用:initMixin(Vue$3);,这里的Vue$3对象就是全局的Vue对象,在此之前已经挂载了Vue的各种基本数据和函数。这个函数体就是初始化我们上面声明Vue语句的过程化逻辑,取主体代码来看:

// 这里的options就是上面声明Vue对象的json对象
Vue.prototype._init = function (options) {
    ...
    var vm = this;
    ...
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    // 这里就是我们接下来要跟进的初始化Vue参数
    initState(vm);
    initInjections(vm);
    callHook(vm, 'created');
    ...
  };

这里主要完成了初始化事件、渲染、参数、注入等过程,并不断调用事件钩子的回调函数。下面来到如何初始化参数:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  // 我们的count在这里初始化
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch) { initWatch(vm, opts.watch); }
}

这里依次检测参数中包含的props/methods/data/computed/watch并进入不同的函数进行初始化,这里我们只关心initData:

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? data.call(vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
  }
  ...
  // observe data
  observe(data, true /* asRootData */);

可以看到Vue的data参数支持对象和回调函数,但最终返回的一定是对象,否则使用空对象。接下来就是重头戏了,我们如何将data参数设置为响应式的:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    /* 为了防止value不是单纯的对象而是Regexp或者函数之类的,或者是vm实例再或者是不可扩展的 */
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

这里的英文注释非常清晰,就是为了给该对象新建一个观察者类,如果存在则返回已存在的(比如互相引用或依赖重复),可以看到这个观察者列表放置在对象的__ob__属性下。下面我们看下这个Observer观察者类:

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // def函数是defineProperty的简单封装
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    // 在es5及更低版本的js里,无法完美继承数组,这里检测并选取合适的函数
    // protoAugment函数使用原型链继承,copyAugment函数使用原型链定义(即对每个数组defineProperty)
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

在Observer类的注释里也清楚的说明,它会被关联到每一个被检测的对象,使用getter/setter修改其默认读写,用于收集依赖和发布更新。其中出现了三个我们需要关心的东西Dep类/observeArray/walk,我们先看observeArray的源码:

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

它不过是在Observer类和observe方法中间的一层递归,因为我们观察的只能是对象,而不能是数字、字符串或者数组(数组的观察比较特殊,事实上是重构了方法来触发更新,后面会讲到)。那我们接下来看下Dep类是做什么用的:

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid$1++;
  this.subs = [];
};

注释里告诉我们Dep类是一个会被多个指令订阅的可被观察的对象,这里的指令就是我们在html代码里书写的东西,如:class={active: hasActive}{{ count }} {{ count * price }},而他们就会订阅hasActive/count/price这些对象,而这些订阅他们的对象就会被放置在Dep.subs列表中。每一次新建Dep对象,就会全局uid递增,然后传给该Dep对象,保证唯一性id。
我们接着看刚才的walk函数做了什么:

/**
 * Walk through each property and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

看来和名字一样,它只是走了一遍,那我们来看下defineReactive$$1做了什么:

/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (obj, key, val, customSetter) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      // 脏检查,排除了NaN !== NaN的影响
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

终于找到重头戏了,这里真正使用了getter/setter代理了对象的默认读写。我们首先新建一个Dep对象,利用闭包准备收集依赖,然后我们使用observe观察该对象,注意此时与上面相比少了一个asRootData = true的参数。
我们先来看取值的代理get,这里用到了Dep.target属性和depend()方法,我们来看看它是做什么的:

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  // stablize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

注释看的出来Dep.target是全局唯一的watcher对象,也就是当前正在指令计算的订阅者,它会在计算时赋值成一个watcher对象,计算完成后赋值为null。而depend是用于对该订阅者添加依赖,告诉它你的值依赖于我,每次更新时应该来找我。另外还有notify()的函数,用于遍历所有的依赖,通知他们更新数据。这里多看一下addDep()的源码:

/**
 * Add a dependency to this directive.
 */
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      // 使用push()方法添加一个订阅者
      dep.addSub(this);
    }
  }
};

可以看到它有去重的机制,当重复依赖时保证相同ID的依赖只有一个。订阅者包含3个属性newDepIds/newDeps/depIds分别存储依赖信息,如果之前就有了这个依赖,那么反过来将该订阅者加入到这个依赖关系中去。
接着看get方法中的dependArray()

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

可以看到我们不能像对象一样监听数组的变化,所以如果获取一个数组的值,那么就需要将数组中所有的对象的观察者列表都加入到依赖中去。
这样get方法读取值就代理完成了,接下来我们看set方法代理赋值的实现,我们先获取原始值,然后与新赋的值进行比较,也叫脏检查,如果数据发生了改变,则对该数据进行重新建立观察者,并通知所有的订阅者更新。
接下来我们看下数组的更新检测是如何实现的:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var arguments$1 = arguments;
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
        inserted = args;
        break
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});

看的出来我们模拟了一个数组对象,代理了push/pop/shift/unshift/splice/sort/reverse方法,用于检测数组的变化,并通知所有订阅者更新。如果有新建元素,会补充监听新对象。
这就是从代码上解释为什么Vue不支持数组下标修改和长度修改的原因,至于为什么这么设计,我后面会再次更新或再开篇文章,讲一些通用的设计问题以及Js机制和缺陷。

总结

从上面的代码中我们可以一步步由深到浅的看到Vue是如何设计出双向数据绑定的,最主要的两点:

  1. 使用getter/setter代理值的读取和赋值,使得我们可以控制数据的流向。
  2. 使用观察者模式设计,实现了指令和数据的依赖关系以及触发更新。
  3. 对于数组,代理会修改原数组对象的方法,并触发更新。

明白了这些原理,其实你也可以实现一个简单的数据绑定,造一个小轮子,当然,Vue的强大之处不止于此,我们后面再来聊一聊它的组件和渲染,看它是怎么一步一步将我们从DOM对象的魔爪里拯救出来的。

参考资料

  1. 数据的响应化:https://github.com/Ma63d/vue-...
  2. Vue v2.2.0 源代码文件
  3. es6 Proxy: http://es6.ruanyifeng.com/#do...
查看原文

赞 11 收藏 34 评论 0

CHENGKANG 赞了文章 · 2017-09-06

个人小结--javascript实用技巧和写法建议

1.前言

从大学到现在,接触前端已经有几年了,感想方面,就是对于程序员而言,想要提高自己的技术水平和编写易于阅读和维护的代码,我觉得不能每天都是平庸的写代码,更要去推敲,去摸索和优化代码,总结当中的技巧,积极听取别人的建议,这样自己的技术水平会提高的更快。那么今天,我在这里就分享一下关于javascript方面的写作的实用技巧和建议,这些技巧和建议是我平常在开发项目上会用到的,希望能让大家学到知识,更希望能起到一个交流意见的作用,也就是说大家有什么好的技巧或者建议,欢迎分享,或者觉得我的想法存在什么问题,欢迎指出!

2.更短的数组去重写法

[...new Set([2,"12",2,12,1,2,1,6,12,13,6])]
//[2, "12", 12, 1, 6, 13]
//es6的新特性

3.对象深浅拷贝

关于对象的深浅拷贝,我个人见解就是有一下几点:

1.深拷贝和浅拷贝只针对像Object, Array这样的引用类型数据。

2.浅拷贝是对对象引用地址进行拷贝,并没有开辟新的栈,也就是拷贝后的结果是两个对象指向同一个引用地址,修改其中一个对象的属性,则另一个对象的属性也会改变。

3.深拷贝则是开启一个新的栈,两个对象对应两个不同的引用地址,修改一个对象的属性,不会改变另一个对象的属性。

浅拷贝

var myInfo={name:'守候',sex:'男'};

clipboard.png


var newInfo=myInfo;

clipboard.png


newInfo.sex='女';

clipboard.png

console.log(myInfo)   //{name: "守候", sex: "女"}

假-深拷贝

假-深拷贝这个是自己随性命名的,大家看看就好,别当真!

var myInfo={name:'守候',sex:'男'}; 

clipboard.png

var newInfo=Object.assign({},myInfo)

clipboard.png

newInfo.sex='女';

clipboard.png

console.log(myInfo)   //{name: "守候", sex: "男"}
console.log(newInfo)   //{name: "守候", sex: "女"}

真-深拷贝

真-深拷贝这个是自己随性命名的,大家看看就好,别当真!

看着深浅拷贝,区别写法很简单,但是那个上面的深拷贝写法是有问题的。看下面案例

var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=Object.assign([],arr)
//截断数组
newArr.length=1
console.log(newArr)//[{a:1,b:2}]
console.log(arr)//[{a:1,b:2},{a:3,b:4}]
//操作newArr,这里看着对arr没影响,实际上已经挖了一个坑,下面就跳进去
newArr[0].a=123
//修改newArr[0]这个对象,也是影响了arr[0]这个对象
console.log(arr[0])//{a: 123, b: 2}

为什么会这样呢,因为Object.assign并不是深拷贝,是披着深拷贝外衣的浅拷贝。最多也是Object.assign会课拷贝第一层的值,对于第一层的值都是深拷贝,而到第二层的时候就是 复制引用。类似的情况还有,slice方法和concat方法等。
要解决这个问题,就得自己封装方法!如下

//利用递归来实现深拷贝,如果对象属性的值是引用类型(Array,Object),那么对该属性进行深拷贝,直到遍历到属性的值是基本类型为止。  
function deepClone(obj){    
  if(!obj&& typeof obj!== 'object'){      
    return;    
  }    
  var newObj= obj.constructor === Array ? [] : {};    
  for(var key in obj){       
    if(obj[key]){          
      if(obj[key] && typeof obj[key] === 'object'){  
        newObj[key] = obj[key].constructor === Array ? [] : {}; 
        //递归
        newObj[key] = deepClone(obj[key]);          
      }else{            
        newObj[key] = obj[key];         
      }       
    }    
  }    
  return newObj; 
}
var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=deepClone(arr)
console.log(arr[0])//{a:1,b:2}
newArr[0].a=123
console.log(arr[0])//{a:1,b:2}

还有一个方法就是简单粗暴法,我现在在用的一个方法!原理很简单,就是先把对象转成字符串,再把字符串转成对象!也能实现同样效果

var newArr2=JSON.parse(JSON.stringify(arr));
console.log(arr[0])//{a:1,b:2}
newArr2[0].a=123
console.log(arr[0])//{a:1,b:2}

上面所说的浅拷贝,真假深拷贝(自己随性命名的),这几种情况,在开发上都有可能要用到,至于要使用哪一种方式,视情况而定!

4.使用事件委托

一个简单的需求,比如想给ul下面的li加上点击事件,点击哪个li,就显示那个li的innerHTML。这个貌似很简单!代码如下!

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <ul id="ul-test">
            <li>0</li>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
            <li>7</li>
            <li>8</li>
            <li>9</li>
        </ul>
    </body>
    <script type="text/javascript">
        var oUl=document.getElementById("ul-test");
        var oLi=oUl.getElementsByTagName("li");
        for(var i=0,len=oLi.length;i<len;i++){
            oLi[i].addEventListener("click",function(){
                alert(this.innerHTML)
            })
        }
    </script>
</html>

很简单,这样就实现了,实际上这里有坑,也待优化!
1.for循环,循环的是li,10个li就循环10次,绑定10次事件,100个就循环了100次,绑定100次事件!
2.如果li不是本来就在页面上的,是未来元素,是页面加载了,再通过js动态加载进来了,上面的写法是无效的,点击li是没有反应的!
所以就者需要用事件委托(即使不考虑上面的第二种情况,也是建议使用事件委托)!代码如下

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <ul id="ul-test">
            <li>0</li>
            <li>1</li>
            <li>2</li>
            <li>3</li>
            <li>4</li>
            <li>5</li>
            <li>6</li>
            <li>7</li>
            <li>8</li>
            <li>9</li>
        </ul>
    </body>
    <script type="text/javascript">
        var oUl=document.getElementById("ul-test");
        oUl.addEventListener("click",function(ev){
            var ev=ev||window.event;
            var target=ev.target||ev.srcElement;
            //如果点击的最底层是li元素
            if(target.tagName.toLowerCase()==='li'){
                alert(target.innerHTML)
            }
        })
    </script>
</html>

这样写,即使是动态添加进来的li点击也有反应,还有一个就是ul只有一个,事件绑定在ul上,无论li多少个,都是添加一次事件!但是也是可能会有问题,如果li下面还有子元素,那么点击的时候,target可能不是li,而是鼠标点击那个位置的最底层元素!如下图,如果鼠标点击白色区域,那个target就是body元素,鼠标点击绿色区域target就是div元素,鼠标点击蓝色区域target就是ul,点击橙色就是li。

clipboard.png

5.使用对象作为函数参数

大家试想下这样一个函数--函数接受几个参数,但是这几个参数都不是必填的,函数该怎么处理?是不是下面这样

function personInfo(name,phone,card){
    ...
}
//以上函数,可以任意传参数。比如我想传card等于1472586326。这下是不是这样写
personInfo('','','1472586326')

有没有觉得上面写法奇怪,不太优雅?下面这里看着舒服一点!

function personInfo(opt){
    ...
}
personInfo({card:'1472586326'})

再想一下,如果一个函数,参数很多,怎么处理?

function test(arg1,arg2,arg3,arg4,arg5,arg6,arg7){
    ...
}

密集恐惧症复发没有复发?下面这样看着会舒服一点!

function personInfo(opt){
    ...
}

最后再想一下,如果需求改了,操作函数也要改!函数也要增加一个参数。

//原来函数
function personInfo(name,phone,card){
    ...
}
//修改后
function personInfo(name,age,phone,card){
    ...
}

这样就是参数修改一次,函数的参数就要修改一次!如果是用对象,就不会出现这样问题!

//修改前后都是这样,变得是函数的操作内容和调用时候的传参!
function personInfo(opt){
    ...
}

看了上面的几个栗子,总结来说,就是当函数的参数不固定的时候,参数多(三个或者三个以上)的时候,建议用一个对象记录参数,这样会比较方便,也为以后如果参数要改留了条后路!

6.使用push和apply合并数组

合并数组这个已经是老生常谈的话题了,方法也是多种多样!

concat

var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1=arr1.concat(arr2)
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

concat会一个全新的数组,表示arr1和arr2两个数组的组合,并让arr1和arr2不变。简单吧?
但如果arr1和arr2的长度都很长,那就产生了一个很长很长的数组,内存又被占用了那么多。但是数组长度没限制!

for

var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
for(var i=0,len=arr2.length;i<len;i++){
    arr1.push(arr2[i])
}
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

这里是往arr1循环添加arr2的元素,但是有一个情况,arr1的长度远小于arr2的长度,是不是循环arr1性能更好,循环次数更少。处理这个很简单,但是万一不知道arr1和arr2到底哪个长度更少呢?而且,for循环不够优雅!(当然,这个可以用迭代方法来替代)

reduce

var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1 = arr2.reduce( function(coll,item){
     coll.push( item );
     return coll;
 }, arr1 );
 console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

逼格高了一点,而且用ES6的箭头函数还可以减少一些代码量,但它仍然需要一个函数,每个元素都需要调用一次。

push.apply

var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1.push.apply(arr1,arr2);
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

逼格看着高,代码少,也不会产生新的数组,也不难理解,就是调用arr1.push这个函数实例的apply方法,同时把arr2当作参数传入,这样arr1.push这个方法就会遍历arr2数组的所有元素,达到合并的效果。相当于arr1.push.apply(arr1,[6,7,8,9,10]);,最后相当于arr1.push(6,7,8,9,10)。遗憾的就是,这个方法对数组长度有限制,网上说法是不同浏览器,不同的长度限制,一般不超过10万!

之前是建议用push.apply,但是现在保留意见,就是大家觉得哪个方式用哪个方式!这个没有一定的对错!

7.toFixed保留整数

在开发上,经常会遇到最多保留多少位小数或者类似的问题,针对这个,使用toFixed可以很简单的解决问题,但是如果数据是要和后台交互的,而且后台存储的数据一般是保存数字类型,而使用toFixed后生成的是一个字符串,这下,就需要把toFixed生成的是一个字符串转成数字类型,转发很多。今天我说一个最简单--+。代码如下

var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=+a
console.log(a)//123.37

PS:a=a|0和~~a也可以实现,但是生成的是一个整数,如下

var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=a|0  
console.log(a)//123 
//---------------------------------分割线
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=~~a  
console.log(a)//123        

8.其它类型数据转布尔数据

下面的转换,大家一看就明白了,不多说。

console.log(!!'123')
//true
!!12
//true
!!-1
//true
!![]
//true
!!''
//false
!!null
//false

9.缓存变量

for循环缓存length

var arr=[1,2,3,4,5,6]
for(var i=0,i<arr.length;i++){
    ...
}
//------------------------分割线
var arr=[1,2,3,4,5,6]
for(var i=0,len=arr.length;i<len;i++){
    ...
}

第一段就是每一次循环的时候,都要查询一次arr.length。第二段代码就是缓存了arr.length,每次对比len就好,理论上是第二段代码的写法比较好,性能比较高!但是随着浏览器的发展,这个细节的性能上的影响貌似远远小于预期,现在还是建议缓存!我写了下面的测试用例(谷歌浏览器测试)!

var arr100=[], arr10000=[];
for(var i=0;i<100;i++){
    arr100.push(i)
}
for(var i=0;i<10000;i++){
    arr10000.push(i)
}
//缓存情况
function testCache(arr){
    console.time();
    for(var i=0,len=arr.length;i<len;i++){
        
    }
    console.timeEnd()
}
//不缓存情况
function testNoCache(arr){
    console.time();
    for(var i=0,len=arr.length;i<len;i++){
        
    }
    console.timeEnd()
}
testCache(arr100)//default: 0.007ms
testCache(arr10000)//default: 0.035ms
testNoCache(arr100)//default: 0.012ms
testNoCache(arr10000)//default: 0.109ms
//这只是一个最简单的数组,如果遍历的是一个nodeList(元素列表),效果可能会更明显。

元素事件

这里我用jquery来讲解,比较容易理解,原生js也是这个道理!如下代码

$('.div1').click(function(){
   ...
})
//--------------------------分割线   
var $div1=$('.div1');
$div1.click(function(){
   ...
})

上面的代码,改变的也是缓存了$('.div1'),但是这里就建议是第二种写法了,因为第一种点击一次就要查询一次.div1,Dom的操作还是能减少就减少!

10.使用innerHTML添加元素

比如有一个需求,往ul里面添加10个li,两种方法,如下代码

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title></title>
    </head>
    <body>
        <ul id="ul-test">
            
        </ul>
    </body>
    <script type="text/javascript">
        var oUl=document.getElementById("ul-test");
        //createElement方式
        console.time();
        for(var i=0;i<10;i++){
            var oLi=document.createElement('li');
            oLi.innerHTML=i;
            oUl.appendChild(oLi);
        }
        console.timeEnd();
        //innerHTML方式
        console.time();
        var _html='';
        for(var i=0;i<10;i++){
            _html+='<li>'+i+'</li>'
        }
        oUl.innerHTML=_html;
        console.timeEnd();
    </script>
</html>

大家把代码用浏览器打开,发现基本是第二种方式更快,第8点也说了,DOM操作能少就少!第一种要操作10次DOM,第二种只需要操作1次DOM。还有一个就是,这个只是很简单的li,如果是下面的列表呢?用第一种方式,得createElement多少次,innerHTML多少次,appendChild多少次?代码多,各个节点的逻辑和嵌套关系也乱!用第二种方式就是一个拼接字符串的操作,比第一种方式好多了,如果用es6的模板字符串,就更简单了!

clipboard.png

11.将参数转成数组

函数里的arguments,虽然拥有length属性,但是arguments不是一个数组,是一个类数组,没有push,slice等方法。有些时候,需要把arguments转成数组,转的方法也不止一个,推荐的是是下面的写法!

var _arguments=Array.prototype.slice.apply(arguments)

12.函数节流

这里拿一个栗子说,比如mousemove,onscroll,onresize这些事件触发的时候,可能已经触发了60次事件,这样很消耗性能,而且实际上,我们并不需要这么频繁的触发,只要大约100毫秒触发一次就好!那么这样就需要函数节流了!

普通写法

var count = 0;
function beginCount() {
    count++;
    console.log(count);
}
document.onmousemove = function () {
   beginCount();
};

效果

clipboard.png

节流写法

var count = 0;
function beginCount() {
    count++;
    console.log(count);
}
function delayFn(method, thisArg) {
    clearTimeout(method.props);
    method.props = setTimeout(function () {
        method.call(thisArg)
    },100)
}
document.onmousemove = function () {
    delayFn(beginCount)
};

效果

clipboard.png

这种方式,其实是有问题的,在不断触发停下来等待100ms才开始执行,中间操作得太快直接无视。于是在网上找到下面这种方案!

第二种节流写法

function delayFn2 (fn, delay, mustDelay){
     var timer = null;
     var t_start;
     return function(){
         var context = this, args = arguments, t_cur = +new Date();
         //先清理上一次的调用触发(上一次调用触发事件不执行)
         clearTimeout(timer);
         //如果不存触发时间,那么当前的时间就是触发时间
         if(!t_start){
             t_start = t_cur;
         }
         //如果当前时间-触发时间大于最大的间隔时间(mustDelay),触发一次函数运行函数
         if(t_cur - t_start >= mustDelay){
             fn.apply(context, args);
             t_start = t_cur;
         }
         //否则延迟执行
         else {
             timer = setTimeout(function(){
                 fn.apply(context, args);
             }, delay);
         }
     };
}
var count=0;
function fn1(){
    count++;
    console.log(count)
} 
//100ms内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔200ms至少执行一次
document.onmousemove=delayFn2(fn1,100,200)

clipboard.png

我现在函数节流用得很少,这两个写法是比较基础的,希望大家能共享下自己的比较好的方法!

13.其他写作建议

关于其它的一些写法技巧和建议,都是比较老生常谈的,比如命名规范,函数单一性原则等。这一部分内容我自己总结和别人写的基本一致!我就不展开说了(感觉展开说也基本是复制粘贴别人的文章,这事我不干),所以我推荐大家去看这篇文章(如何优雅的编写 JavaScript 代码)。有些知识我也是从这里获得的!

14.小结

好了,关于我自己总结的一些实用技巧和建议,就到这里了!关于javascript的技巧和建议,这点大家还是要多看网上的资源,也要自己多总结,毕竟我自己总结的只是我自己发现的,只是冰山一角。但还是希望这篇文章能帮到大家,让大家学习到知识。当然,更希望的是能起到一个交流意见的作用。如果大家有什么建议,技巧。也欢迎分享。觉得我哪里写错了,写得不够好,也欢迎指出!让大家一起互相帮助,互相学习!

-------------------------华丽的分割线--------------------
想了解更多,关注关注我的微信公众号:守候书阁

clipboard.png

查看原文

赞 69 收藏 386 评论 24

CHENGKANG 发布了文章 · 2017-05-30

【译】Core Graphics,第四部分:Path!Path!

原文链接:Core Graphics, Part 4: A Path! A Path!
译文原链:【译】Core Graphics,第四部分:Path!Path!
看看上一篇吧:【译】Core Graphics,第三部分:线

在 Core Graphics 中,一个 path 就是对某种形状的一步一步的描述。它可以是一个圆、一个正方形、一个桃心、一个字频柱状图或者可能是一个笑脸。它并不包含任何诸如像素颜色、线宽或渐变这样的信息。路径主要是用于绘制——将其用颜色填充或者描边——用颜色描出轮廓。你之前看到的各种 GState 参数控制着 path 如何被绘制,包括例如 line join 和 dash pattern 在内的所有线属性

这一次让你看看 path 是什么组成的。下一次你会看到一些用 path 能做的远非简单绘制的很酷的东西。

虽然一个 path 代表了一个理想图形的配方,它需要被渲染出来才能被人真正看到。每一个 Core Graphics context 都尽其可能将 path 渲染出来。当绘制一个位图时,任何曲线和斜线都是反锯齿化的。这意味着使用阴影来欺骗眼睛使其一位这个形状是平滑的即使它是由方形像素点组成的。当在打印机上绘制时,同样的事情发生着,不过用的是极其小的像素点。当绘制 PDF 时,path 大部分仅仅是原位防止,因为 Core Graphics 绘制模型和 PDF 绘制模型基本是一样的。PDF 引擎(例如 Preview 或者 Adobe Acrobat)会去渲染那些 PDF path 而非 Core Graphics 引擎。

你可以试试 GrafDemo 里面的 path。大多数这里的截图都来自 GrafDemo 里的 Path 部分、Arcs 以及 All The Parts 窗口。

路径元素

一个 path 就是由一些被称之为元素的原始形状(曲线、弧和直线)连接起来的一系列点。你可以想象每一个元素是给一个专门的拿着铅笔的机器人的一个指令。你告诉这个机器人要提起铅笔并移动到笛卡尔平面的一个点,但是不要留下任何 印记。你可以告诉这个机器人去把铅笔落下来然后从当前的点到一个新点间画点什么。有五种基本的路径元素:

Move to Point——移动当前的点到一个新的位置但不画任何东西。机器人抬起铅笔并且移动它的手臂。

Add Line To Point——从当前点到一个新点间添加一条线。机器人将铅笔落下来并画出一条直线。以下是一次移动到点(左下方)和其后的两次添加线到点

path.move(to: startPoint)
path.addLine(to: nextPoint)
path.addLine(to: endPoint)

添加二次曲线到点 Add Quad Curve To Point——通过一个控制点,从当前点到一个新点间添加一条二次曲线。机器人落下了铅笔并在绘制一条曲线。这条线并不是直接划到那个控制点——相反这个控制点影响着(线的)形状。控制点离曲线越远,形状就越极端。

path.move(to: firstPoint)
path.addQuadCurve(to: endPoint, control: controlPoint)

Add Curve To Point——通过两个控制点,从当前点到新点添加一条三次贝塞尔曲线。和二次曲线一样,控制点影响着这条线该如何画。二次曲线无法自身形成一个环,但是贝塞尔曲线可以。如果你曾在 Photoshop 或者 Illustrator 中使用过钢笔工具,你就和贝塞尔曲线打过交道了。

path.move(to: firstPoint)
path.addCurve(to: endPoint,
              control1: firstControl,
              control2: secondControl)

Close Subpath——从当前点到路径的第一个点间添加一条直线。更确切地说,最近的那个 move-to-point (的点)。你会希望闭合一个路径而非添加一条线到起始位置。根据你如何计算这些点,累积的浮点化整可能使得计算出的终点和起始点不一样。以下(代码)可以绘制一个三角形:

path.move(to: startPoint)
path.addLine(to: nextPoint)
path.addLine(to: endPoint)
path.closeSubpath()

注意这个名字是 Close Subpath。通过执行一次 move-to 操作,你可以创建一个包含分离部分的路径,例如这个在我们 Advanced iOS bootcamp 中新练习题里的一个柱状图。这些柱形都是用一个路径绘制的。这个路径被用来给它们上色,并且描出轮廓以清楚地区别各个柱形。

那样做方便吗?

简单的图形用那仅有的五个基本路径元素去生成的话(代码、过程)可能会变得很冗长。Core Graphics (或者说 CG)提供了一些简便方法来添加常见的形状,比如矩形、椭圆或者一个圆角矩形。

let squarePath = CGPath(rect: rect1, transform: nil)
let ovalpath = CGPath(ellipseIn: rect2, transform: nil)
let roundedRectanglePath = CGPath(roundedRect: rect3,
                                  cornerWidth: 10.0,
                                  cornerHeight: 10.0,
                                  transform: nil)

这些方法使用了一个 transform 对象作为它们最后一个参数。你会在之后的文章中看到更多关于 transform 的东西,因此暂时只传个 nil 就行了。以上的方法(CGPath(rect:tranform:)CGPath(ellipseIn:transform:)CGPath(roundedRect:cornerWidth:cornerHeight:transform:))生成了这些形状:

同样也还有一些可以让你用一个就能创建更复杂的路径的方法,例如多个矩形或者多个椭圆、多个线段或者一整个别的路径。

Noah 的 ARCtangent

你也可以加一点弧在里面,就是一个圆形边的部分。用哪一个取决于你手上握着什么值。

Arc——需要给定你想要的弧所在的那个圆的圆心、它的半径以及起始和终止角度(用弧度表示)。那个圆从起始角度到终止角度之间的那一段将会被绘制。弧的终点成为了当前点。以下代码绘制了左边的线,外加一个圆:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(center: centerPoint,
            radius: radius,
            startAngle: startAngle,
            endAngle: endAngle,
            clockwise: clockwise)

Relative Arc——这个与正常的弧类似。需要给定圆心、半径和起始角度。但并不是要给定一个终止角度,而是你要说明要从起始角度往前或者往后画多少个弧度:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addRelativeArc(center: centerPoint,
                    radius: radius,
                    startAngle: startAngle,
                    delta: deltaAngle)

Arc to Point——这个就有点奇怪了。你需要给定圆半径和两个控制点。在后台呢,当前的店会和第一个控制点连接,然后与第二个控制点形成一个角度。这些线接下来被用于构建一个有着给定半径并正切于这些线的圆。我将这种弧称作 “Arc to Point” 是因为其底层的 C API 名字叫 CGContextAddArcToPoint

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(tangent1End: tangent1Point,
            tangent2End: tangent2Point,
            radius: radius)

我在试着想出一个这个方法的好的应用场景时,朋友 Jeremy W. Sherman 想到一个很酷的应用:如果你想做一个曲面的交叉影线时可能比较有用,想想“给一把剑的顶部加一点阴影”——你可以重复同样的正切并且改变半径来画一些离顶部越来越远的弧。

你可能已经注意到了这些弧的方法可以用直线段来连接圆弧。用前两个弧方法来创建一个新的路径是不会创建这个连接用的线段的。Arc to point 可能会包含那个初始的部分。

Path vs Context 操作

有两种在代码里创建路径的方法。第一种方式是告诉 context:“嘿,创建一个新的 path” 并开始累积 path 元素。这个 path 在你描边或者填充时就消失了。没了。拜拜了。这个 path 也并没有被保存或者可以在你保存恢复 GState 的时候恢复——它实际上并不是 GState 的一部分。每一个 context 只有一个在用状态的 path。

一下是当前 context 被用于构建和描边一个 path 时的例子:

let context = UIGraphicsGetCurrentContext()
context.beginPath()
context.move(to: controlPoints[0])
context.addQuadCurve(to: controlPoints[1], control: controlPoints[2])
context.strokePath()

这些对于那些一次性的只创建一次、用一次然后被遗忘的 path 非常棒。

你也可以创建一个新的 CGMutablePath path 对象(一个 CGPath 类型的 mutable 子类,与 NSArray / NSMutableArray 间的关系类似)并在其中累积 path 组件。这是一个你可以一直用的实例。要用一个 path 对象绘制的话,你要将这个 path 添加到 context 中然后执行描边与/或填充操作:

let path = CGMutablePath()
path.move(to: controlPoints[0])
path.addQuadCurve(to: controlPoints[1], control: controlPoints[2])

context.addPath(path)
context.strokePath()

对于你常用的形状(比如卡片游戏里的那一套图标),你可能希望创建一个红心 path 和一个方片 path 一次然后用它们一次次绘制。

如何创建?

那么你如何才能创建出有用又有趣的 path 呢,比如心形或者笑脸?一个方法是做好数学功课并计算出点、线、曲线和弧需要怎么走。

另一个方法就是用软件工具。有一些可以让你画形状的的应用,然后返回给你一堆可以直接粘贴到你的应用中的 CG 代码。同时也有一些可以将其他形式数据(例如来自 Illustrator的、PDF 或者 SVG)转换成 path 的库。我在给 Protocols part 2: Delegation 准备的 world demo app 中的可点击地图中使用了 SVG。

路径,结构

Core Graphics 路径是不透明数据类型。你先累积路径元素然后在 context 中渲染它。为了了解内部情况,使用 CGPathapply(info:function:) 方法来遍历路径组件。你可以提供一个被每一个路径元素重复调用的方法(在 Swift 中你可以用闭包)。(你可以忽略 info 参数通过传 nil。这是在 Swift Core Graphics API 底下的 C API 的一个延续。在 C 里面你需要提供一个方法并传递任何你需要在里面使用到的对象。用闭包的话你就只需要用你需要的那些。)

也因为其对 C 的继承,这个传进来的方法或闭包是一个 UnsafePointer<CGPathElement>。这是一个指向内存中 CGPathElement 的指针。你需要通过 pointee 来引用那个指针以得到实际的 CGPathElement。这个 path 元素有一个用于表现其类型的枚举值,还有一个指向一个指针数组里的第一个 CGPointUnsafeMutablePointer<CGPoint>。你需要自己去搞清楚你可以从那个数组里面安全读取多少指针。

下面是一个 CGPath 扩展,它可以让一个 path 倾倒出其内容。你也可以从这个 gist 中找到这段代码。

import CoreGraphics

extension CGPath {
    func dump() {
        self.apply(info: nil) { info, unsafeElement in
            let element = unsafeElement.pointee

            switch element.type {
            case .moveToPoint:
                let point = element.points[0]
                print("moveto - \(point)")
            case .addLineToPoint:
                let point = element.points[0]
                print("lineto - \(point)")
            case .addQuadCurveToPoint:
                let control = element.points[0]
                let point = element.points[1]
                print("quadCurveTo - \(point) - \(control)")
            case .addCurveToPoint:
                let control1 = element.points[0]
                let control2 = element.points[1]
                let point = element.points[2]
                print("curveTo - \(point) - \(control1) - \(control2)")
            case .closeSubpath:
                print("close")
            }
        }
    }
}

打印出之前创建那个 arc to point 图片的 path 展示出这条弧是一系列 curveTo 操作及连接用的直线:

path.move(to: startPoint)
path.addLine(to: firstSegmentPoint)
path.addArc(tangent1End: tangent1Point,
            tangent2End: tangent2Point,
            radius: radius)
path.addLine(to: secondSegmentPoint)
path.addLine(to: endPoint)
moveto - (5.0, 91.0)      // explicit code
lineto - (72.3, 91.0)     // explicit code
lineto - (71.6904767391754, 104.885702433811)   // added by addArc
curveTo - (95.5075588575432, 131.015122621923)
        - (71.0519422129889, 118.678048199439)
        - (81.7152130919145, 130.376588095736)
curveTo - (113.012569145714, 124.955236840146)
        - (101.903264013406, 131.311220082842)
        - (108.168814214539, 129.14221144167)
lineto - (129.666666666667, 91.0) // explicit code
lineto - (197.0, 91.0)   // explicit code

即使是一个用 CGPath(ellipseIn:transform:) 创建的“简单的”椭圆也有些复杂:

curveTo - (62.5, 107.0) - (110.0, 86.4050984922165) - (88.7335256169627, 107.0)
curveTo - (15.0, 61.0) - (36.2664743830373, 107.0) - (15.0, 86.4050984922165)
curveTo - (62.5, 15.0) - (15.0, 35.5949015077835) - (36.2664743830373, 15.0)
curveTo - (110.0, 61.0) - (88.7335256169627, 15.0) - (110.0, 35.5949015077835)

之后

这一次你看到了创建一个 path、绘制它以及其中发生的一切。还有很多你可以用 path 做的事情,下次继续。

查看原文

赞 0 收藏 0 评论 0

CHENGKANG 发布了文章 · 2017-05-26

【译】哥们儿,我的方法哪儿去了?

原文链接:Dude, Where's my Call?
译文原链:【译】哥们儿,我的方法哪儿去了?

想象有一天你正在给 Swift 编译器喂一些看起来无害的代码。

// xcrun -sdk macosx swiftc -emit-executable cg.swift

import CoreGraphics

let path = CGPathCreateMutable()
CGPathMoveToPoint(path, nil, 0.0, 23.0)

然后一个冲击波打来:

cg.swift:7:12: error: 'CGPathCreateMutable()' has been replaced by 'CGMutablePath.init()'
<unknown>:0: note: 'CGPathCreateMutable()' has been explicitly marked unavailable here
cg.swift:8:1: error: 'CGPathMoveToPoint' has been replaced by instance method 'CGMutablePath.moveTo(_:x:y:)'
<unknown>:0: note: 'CGPathMoveToPoint' has been explicitly marked unavailable here

它们哪儿去了?被重命名了。

Swift 3 一个重大的特性就是由 Swift-Evolution 提议 SE-0005 (Better Translation of Objective-C APIs Into Swift)SE-0006 (Apply API Guidelines to the Standard Library) 带来的”超级重命名“,这次超级重命名重命名了 C 和 Objective-C API 中的一些方法以给它们一种更 Swift 的感觉。Xcode 里面有一个移植器会将你的 Swift 2 代码转换成新的风格。它会执行很多机械的改变,给你留一些由于其他语言改变需要扫尾的工作,例如移除 C 的 for 循环

有一些重命名相当轻微,比如 NSView 中的这个:

// Swift 2
let localPoint = someView.convertPoint(event.locationInWindow, fromView: nil)

// Swift 3
let localPoint = someView.convert(event.locationInWindow, from: nil)

在这里 Point 从方法名里移除了。你知道自己正在处理一个 point,所以没必要重复这一事实。fromView 重命名为了 from 因为 View 只是提供了冗余的类型信息,并没有让这个调用更清楚。

其他的改变更大一些,比如 Core Graphics:

// Swift 2 / (Objective-C)
let path = CGPathCreateMutable()
CGPathMoveToPoint (path, nil, points[i].x, points[i].y)
CGPathAddLineToPoint (path, nil, points[i + 1].x, points[i + 1].y)
CGContextAddPath (context, path)
CGContextStrokePath (context)

// Swift 3
let path = CGMutablePath()
path.move (to: points[i])
path.addLine (to: points[i + 1])

context.addPath (path)
context.strokePath ()

喔噢。这变化太大了。这个 API 现在看起来就是让人喜欢的 Swift 风格 API 而不是旧式的 C API。Apple 在 Swift 里面完全改变了 Core Graphics API (还有 GCD)以让它们更好用。你在 Swift 3 里不能再用老式的 CG C 风格的 API,因此你需要开始习惯新的风格。我已经将 GrafDemo (我这些 Core Graphics 博文的示例程序) 在自动翻译器中跑过(两次)了。你可以在这个 pull 请求中看到 Swift 3 第一个版本前后的变化,在这个 pull 请求中看到 Xcode8b6 的 Swift 3 版本前后变化。

他们干什么了?

Core Graphics API 就是一堆全局变量和全局自由方法。就是说,方法并不是直接和某些比如说类或者结构体这样的实例绑定的。用 CGContextAddArcToPoint 来操作 CGContext 仅仅是一个传统,不过你传进去一个 CGColor 也不会有人拦着你。无非就是会在运行时爆炸而已。只是在 C 风格的面向对象你才有一个隐晦类型作为第一个参数传过去,作为某种神奇饼干。CGContext* 方法需要一个 CGContextRefCGColor* 方法需要一个 CGColorRef

通过一些编译器的魔法,Apple 将这些隐晦引用转成了类,并且添加了一些方法给这些类以将其映射到 C API。当编译器看到类似这样的东西时:

let path = CGMutablePath()
path.addLines(between: self.points)
context.addPath(path)
context.strokePath()

实际上,在背后,正在发出这一系列调用:

let path = CGPathCreateMutable()
CGPathAddLines(path, nil, self.points, self.points.count)
CGContextAddPath(context, path)
CGContextStrokePath(context)

“新的”类

以下是已经接受 Swift 3.0 治疗的常见的隐晦类型 (忽略了一些专用的类型比如 CGDisplayMode 或者 CGEvent),还有一两个作为代表的方法:

  • CGAffineTransform - translateBy(x:30, y:50), rotate(by: CGFloat.pi / 2.0)

  • CGPath / CGMutablePath - contains(point, using: evenOdd), .addRelativeArc(center: x, radius: r, startAngle: sa, delta: deltaAngle)

  • CGContext - context.addPath(path), context.clip(to: cgrectArray)

  • CGBitmapContext (folded in to CGContext) - let c = CGContext(data: bytes, width: 30, height: 30, bitsPerComponent: 8, bytesPerRow: 120, space: colorspace, bitmapInfo: 0)

  • CGColor - let color = CGColor(red: 1.0, green: 0.5, blue: 0.333, alpha: 1.0)

  • CGFont - let font = CGFont("Helvetica"), font.fullName

  • CGImage - image.masking(imageMask), image.cropping(to: rect)

  • CGLayer - let layer = GCLayer(context, size: size, auxilaryInfo: aux), layer.size

  • CGPDFContext (folded in to CGContext) / CGPDFDocument - context.beginPDFPage(pageInfo)

CGRectCGPoint 在 Swift 3 之前早已有了一些很不错的扩展。

怎么做到的?

编译器有一个内置的语法转换器,它将 Objective-C 的明明风格转换成更 Swift 些的形式。去掉重复的单词和那些仅仅是重复类型信息的单词。还去掉了一些之前是在方法调用左括号之前的单词并将它们移到括号里面作为参数标签。通过这样自动清理了一大堆调用方法。

当然,人类喜欢搞一些微妙复杂的言辞,因此在 Swift 编译器里有一个允许手动重写自动翻译器翻译的部分的机制。这是具体的实现了(别在输出产品时依靠他们),不过他们提供了深入了解用于让现存 API 出现在 Swift 中所做的那些工作的机会。

其中一个涉及到的机制是 ”overlay“,它是当你引入一个框架或者 C 库时编译器引用的第二个库。Swift Lexicon 将 overlay 形容为”当库在系统中不发被修改时在系统中增强和扩大这个库“。一些一直都存在很棒的 CGRectCGPoint 扩展,例如someRect.divide(30.0, fromEdge: .MinXEdge),怎么来的?他们来自 overlay。工具链想啊”噢,我看到你在链接 Core Graphics。让我再加点方便方法吧。“

还有另外一个机制,apinotes,特别是 CoreGraphics.apinotes,一字一词地控制着 Core Graphics 中地命名和可见性。

例如,在 Swift 中像 CGRectMake 这样用来初始化基础结构体的调用没有作用,因为已经有它们的初始化方法了。所以就让这些调用方法不可用了:

# The below are inline functions that are irrelevant due to memberwise inits
- Name: CGPointMake
  Availability: nonswift
- Name: CGSizeMake
  Availability: nonswift
- Name: CGVectorMake
  Availability: nonswift
- Name: CGRectMake
  Availability: nonswift

然后还有其他的映射——如果你在 Swift 中看到这个,那就调用那个方法:

# The below are fixups that inference didn't quite do what we wanted, and are
# pulled over from what used to be in the overlays
- Name: CGRectIsNull
  SwiftName: "getter:CGRect.isNull(self:)"
- Name: CGRectIsEmpty
  SwiftName: "getter:CGRect.isEmpty(self:)"

如果编译器看到了比如 rect.isEmpty() 这样的东西,它会发送一个请求给 CGRectIsEmpty

以下还是一些方法和功能的重命名:

# The below are attempts at providing better names than inference
- Name: CGPointApplyAffineTransform
  SwiftName: CGPoint.applying(self:_:)
- Name: CGSizeApplyAffineTransform
  SwiftName: CGSize.applying(self:_:)
- Name: CGRectApplyAffineTransform
  SwiftName: CGRect.applying(self:_:)

当编译器看到 rect.applying(transform),它就知道调用 CGRectApplyAffineTransform

编译器只能自动重命名 Objective-C API,因为其遵循良好的系统命名法。C API (比如 Core Graphics)需要通过 overlay 和 apinote 来实现。

你能做什么

你可以通过 NS_SWIFT_NAME 做一些类似 apinote 机制的事情。你可以用这个宏来注释 C/Objective-C 头文件,表示在 Swift 里要用那个名字。编译器会对你的 NS_SWIFT_NAME 采用同样的替换(”如果看到 X,就调用 Y“)。

例如,这是一个 Intents(Siri) 框架中的调用:

- (void)resolveWorkoutNameForEndWorkout:(INEndWorkoutIntent *)intent
                         withCompletion:(void (^)(INSpeakableStringResolutionResult *resolutionResult))completion
     NS_SWIFT_NAME(resolveWorkoutName(forEndWorkout:with:));

从 Objective-C 中调用它的话看起来是这样:

NSObject<INEndWorkoutIntentHandling> *workout = ...;

[workout resolveWorkoutNameForEndWorkout: intent  withCompletion: ^(INSpeakableStringResolutionResult) {
     ...
}];

而在 Swift 中是这样:

let workout: INEndWorkoutIntentHandling = ...
workout.resolveWorkoutName(forEndWorkout: workout) {
    response in
    ...
}

NS_SWIFT_NAME,和 Objective-C 中的轻量级泛型,nullability 注释,以及 Swift 编译器中的自动 Objective-C API 重命名一起,可以让你立刻有一种接口都回到 Swift 世界中的感觉。

使用自制的 overlay 和 apinote 是可以的,但那些原本是在 Swift 和 Apple 的 SDK 结合在一起时用的。你可以在你自己的框架中分发 apinote,但是 overlay 需要从 Swift 编译器树中编译。

为了自己创建更 Swift 的 API,你必须尽可能地做好头文件旁听(比如添加 nullability 注释和 NS_SWIFT_NAME),然后在你的项目中放一些 Swift 文件来伪造 overlay 以覆盖任何多余情况。这些 ”overlay” 文件在有 ABI 稳定性前都需要作为源文件传送。

轻掠过 iOS 10 头文件,看起来新的 API 喜欢用 NS_SWIFT_NAME,而老一点的更久远一些的 API 用 apinote。这样有一些道理因为这些头文件是在不同 Swift 版本中共享的,而给更久远的头文件可能添加新的 NS_SWIFT_NAME 可能会在编译器未改变的情况下破坏当前的代码。而且,apinote 可以由编译器团队或者社区成员添加,而头文件的改变需要拥有这个头文件的团队的注意。而那个团队可能已经准备好正要发布他们的功能了。

它好吗?

Swift 3 版本的 Core Graphics 绝对是更优秀更加 Swift 化。老实说,我也想在 Objective-C 上这样用。你可能因此失掉一些可 Google 性,并且需要当你在 Stack Overflow 的文章或者网上的教程中看到现有的 CG 代码时做一些脑内转换。不过那也不必这些日子普通的 Swift 代码所需的脑力运动多多少。

有一些由于 CG 类似 OO 本质及其如何进入 Swift 中带来的 API 的不协调。在这个 CoreGraphics.apinotes 中:

- Name: CGBitmapContextGetWidth
  SwiftName: getter:CGContext.width(self:)
- Name: CGPDFContextBeginPage
  SwiftName: CGContext.beginPDFPage(self:_:)

CGBitmapContextCGPDFContext 方法都被 CGContext 偷去了。这意味着你可以对任何 CGContext 要它的宽度,或者叫它开始一个 PDF 页面。如果你找一个非位图 context 要它的宽,你会得到这样的运行时错误:

<Error>: CGBitmapContextGetWidth: invalid context 0x100e6c3c0.
If you want to see the backtrace, please set CG_CONTEXT_SHOW_BACKTRACE environmental variable.

因此即使这个 API 非常 Swift 化了,编译器并不能捕获某些类型的 API 错用。Xcode 会高高兴兴地给你其实实际上不合适的方法补全。某种意义上来说,C API 更安全一点,因为 CGBitmapContextGetWidth 很清楚地告诉你它要的是一个位图 context 即使第一个参数从技术上来说就还是一个 CGContextRef。我希望这仅仅是一个 bug (rdar://27626070)。

如果你想了解更多想超级重命名以及像 NS_SWIFT_NAME 这样的工具,看看这个吧 WWDC 2016 Session 403 - iOS API Design Guidelines

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 65 次点赞
  • 获得 6 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 5 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • Wildfire

    一个用 Vue.js 开发的评论系统。

注册于 2014-05-07
个人主页被 1.5k 人浏览