解决 [Vue warn]: You may have an infinite update loop in a component render function

在 `v-for` 循环当中,如果用方法或者计算属性对 vm.$data 的属性进行操作,理论上,可能因为修改到循环对象,诱发无限循环。此时 Vue 就会发出警告(并不是真的已经无限循环了)。

今天写着写着,突然发现控制台里有错误:

[Vue warn]: You may have an infinite update loop in a component render function

这个问题很奇怪,之前从来没有遇到过。如果是我自己主导的项目,倒也好办,慢慢 debug 就是;偏偏在公司的项目里遇到这个问题,而公司项目的体系结构很复杂,我还没完全掌握。更恼火的是,因为体系复杂,debug 也非常困难,再加上尚无测试框架,这个难搞啊……

好死不死的,当时是下午3、4点钟,正好到了肚饿的时刻,结果又落入低血糖状态,真是屋漏偏逢连阴雨,船小又碰顶头风,饿得我脑仁生疼……

不过终于还是被我 Google + debug 出来。事实上是这样的,在 v-for 循环当中,如果用方法或者计算属性对 vm.$data 的属性进行操作,理论上,可能因为修改到循环对象,诱发无限循环。此时 Vue 就会发出警告(并不是真的已经无限循环了)。

例如这样一个组件,它里面是用 :checked + <label> 实现的一组按钮。它有以下功能:

  1. 为了能够分组,需要设置它们的 name 属性
  2. 为了能够用 <label> 控制 <input>,需要给 <input> 设置 id
  3. 按钮可以被删除

于是我选择这样做:

<template>
<div>
  <template v-for="(item, index) in items">
    <input type="checkbox" :name="'my-component-' + selfIndex" :id="getID">
    <label :for="getID(false)">
    <button type="button" @click="remove(index)">&times;</button>
  </template>
</div>
</template>

<script>
let count = 0;

export default {
  data() {
    return {
      selfIndex: 0,
      itemIndex: 0,
    }
  },
  methods: {
    getID(increase = true) { // 注意,问题就出在这里
      if (increase) {
        this.itemIndex++;
      }
      return `my-component-${this.selfIndex}-${this.itemIndex}`;
    },
  },
  beforeMount() {
    this.selfIndex = count;
    count++;
  }
}
</script>

这里,为了能生成唯一 ID,我选择每次循环都对 vm.itemIndex++,这就会出现前面说的问题,存在隐患。

解决的方案有两种,一种是把 itemIndex 也放在局部变量里,使它不直接关联在组件上;另一种则是写一个全局的唯一 ID 生成函数,然后引用进来。原理都是一样的。


这两天听评书《乱世枭雄》,学到一句话“拉屎脸朝外”,形容讲义气,不知道咋联系的……

MediaElement 笔记

贵司官网需要放视频,于是需要用播放器,最后选定 MediaElement。然后就开始踩坑之旅。当然,有些是我的问题,不过也有不少是文档没说清楚。这里记录一下。

贵司官网需要放视频,于是需要用播放器。然后就产生如下需求:

  1. 协议好,支持免费商用,最好 MIT
  2. 支持 SRT 字幕
  3. 支持尽可能多的平台

经过筛选,最后选定 MediaElement,一看 WordPress 在用基本就放心了。然后就开始踩坑之旅。当然,有些是我的问题,不过也有不少是文档没说清楚。这里记录一下。


全屏和弹性宽度

当前版本 4.2.5 有个 Bug,如果设置宽度为 100%,那么从全屏恢复到内嵌状态时,会留着全屏的宽度,撑开页面。如果设定一个特定的宽度就不会。但我们的页面是响应式的,需要允许它随页面变化而变化。

这个时候只好手工来做,我的选择是让它 100%,然后侦听从全屏返回的事件,发生后就把宽度恢复。另外,因为 fullscreenchange 事件还没有统一标准,所以不同浏览器里的事件名称也不一样,更不爽的是,IE 下是驼峰,其它浏览器是全小写,我也懒得再写函数转化了,反正就3种情况,全写一遍好了。

// fix MediaElement's issue
  let fitVideo = function() {
    setTimeout(() => {
    $('.mejs__container').width('100%')
      .find('video').width('100%')
      .end().find('.mejs__layers')
      .children().width('100%');
  }, 50);
};
if ('onwebkitfullscreenchange' in document) {
  document.onwebkitfullscreenchange = function() {
    if (!document.webkitFullscreenElement) {
       fitVideo();
    }
  };
} else if ('onmozfullscreenchange' in document) {
  document.onmozfullscreenchange = function() {
    if (!document.mozFullScreenElement) {
      fitVideo();
    }
  };
} else if ('MSFullscreenChange' in document) {
  document.MSFullscreenChange = function() {
    if (!document.msFullscreenElement) {
      fitVideo();
    }
  };
}

默认字幕

Web 标准是有默认字幕的,<track defualt> 即可。但是 MediaElement 没有,最后在 StackOverflow 上找到答案,原来有个未记录在文档里的初始化属性可以控制:

let player = new MediaElementPlayer('intro-video', {
  startLanguage: 'cn', // 这里的名字应该和 <track> 种的 srclang 属性一致
});

插件与生态

MediaElement 把一些有用但不太常用的功能抽出来做成了插件,放在 MediaElement Plugins。不过看起来这个项目有些属于维护,使用的时候很多问题。比如,直接在 features 里开启特定功能,即使对应的插件没有加载,也不会报错,就是没反应,很令人迷茫。

避免发起又取消请求

网站上线后,老板发现一个问题:从 Chrome 的 Network 面板可以看到,网页打开后,对视频文件发起了一到多个不等的请求,并且都取消了。

经过研究,我认为这个请求是 <video> 发起的,因为 preload 的默认值是 auto(参见:MDN),也即打开页面就预加载。而此时 MediaElement 被初始化,为了正确显示 UI,它会把 <video> 标签挪到自己创建的 <div> 里,这个过程就会导致“预加载 -> 取消”。因为视频默认不播放,所以我给 <video> 加上了 preload="none",问题解决。


其它大体上文档都能找到,就不多说了。

Vue 里 $mount 方法的作用

关于 Vue 的 $mount 方法,一问一答。

今天由同学问道:“vue文档里说如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态,没有关联的 DOM 元素。可以使用 vm.$mount() 手动地挂载一个未挂载的实例。这个方法在项目中有什么用呢,是一个组件可以挂载到多个dom上就这一个作用吗?”

我的答案如下:

有些时候你创建 vm 的时候 DOM 可能还没准备好,或者写 vm 的时候不确定它要挂在哪个 DOM 上,这个时候就有用了。

当项目大的时候,很多情况是开发的时候估计不到的。比如 A 类里面想使用一个 B 类,但是 B 类有 10 个子类,对应不同场景,你写 A 类的时候根本不知道产品中会使用哪个 B 的子类,所以就要想办法把控制权交出去,这就叫“控制反转”——即控制权从开发者的手上,交到使用者的手上。

我觉得 $mount 这里也是类似的,就是你写了一个 Vue 类,但是不能控制使用场景。所以就要有一个 $mount 方法,由使用者控制,绑到目标 DOM 上。

他又问:“懂了,还有一个问题是一个组件能不能利用$mount属性挂载到多个dom上,我刚刚试了一下是可以渲染,这种用法常见吗??”

我答:

我没这么做过,不过我认为是合理的。

我觉得这个跟 Vue 的定位有关。Vue 和 React、Angular 不一样,后者是大公司企业级产品。Vue 其实很想取代 jQuery,所以这方面其实跟 jq 的插件是很相似的。

Webpack 笔记

记录一些 webpack 的使用经验。我是渐进式使用 webpack 的,一开始只用它打包 js。但是现在开始会把越来越多的依赖交给它管理,所以记一篇笔记。这里不会教怎么使用,而是结合开发经验,总结一些特定需求的解决方案。

我使用的版本:3.5.1

我是渐进式使用 webpack 的,一开始只用它打包 js。但是现在开始会把越来越多的依赖交给它管理,所以记一篇笔记。这篇不是教程,而是结合开发经验,总结一些特定需求的解决方案。

继续阅读“Webpack 笔记”

Vue 笔记:事件

记一些 Vue 的笔记。本篇均与事件处理相关。

onload

需要侦听 load

<template>
  <img src="/path/to/img" @load="onLoad">
</template>

<script>
export default {
  methods: {
    onLoad(event) {
      // do something
    }
  }
}
</script>

drop

需要阻止 dragover 的默认行为,不然不触发。

<template>
  <div class="drop-area" @dragover.prevent="onDragOver" @drop="onDrop">
</template>

<script>
export default {
  methods: {
    onDragOver(event) {
      // 此时仍然可以处理
    },
    onDrop(event) {
      console.log(event.dataTransfer);
    }
  }
}
</script>

JavaScript 异步开发全攻略

把之前写的《JavaScript 异步开发全攻略》更新了内容,然后放在 Gitbook 上,https://meathill.gitbooks.io/javascript-async-tutorial/content/ 欢迎阅读分享,因为我会不时维护,请关注,star。

之前在 GitChat 做过一次分享:《JavaScript 异步开发全攻略》。在我看来,原始内容可能不够完美,但通过后来的维护,可以把它打磨得越来越好。昨天在 SF 上回答了一个 Vuex 的 action 里使用 Promise 的问题,然后就想去补充一下这方面的内容。结果发现 GitChat 竟然不支持编辑文章,只能把内容发给运营人工修改。

索性把内容放到 Gitbook 上好了,反正 GitChat SEO 也不做,文章也自然移动到第二页了。试了试,没有被墙,很好。于是简单整理了一下,上传。

欢迎阅读,欢迎分享,因为我会不时更新新内容,请关注 star:

JavaScript 异步开发全攻略

Node.js 8 中的 util.promisify

Node.js 8 内建了“Promise 化”功能,可以把之前回调模式的方法,很简单的封装成支持 Promise 的方法,只需要调用 util.promisify(原函数) 即可。对于非 Node.js 标准风格的函数,也提供自定义转换函数的功能。

Node.js 8 于上个月月底正式发布,带来了很多新特性。其中比较值得注意的,便有 util.promisify() 这个方法。

如果你已经很熟悉 Promise,请继续往下看。如果你还不熟悉 Promise,可以先跳过去看下下章:Promise 介绍

util.promisify()

虽然 Promise 已经普及,但是 Node.js 里仍然有大量依赖回调的异步函数,如果我们把每个函数都封装一遍,那真是齁麻烦齁麻烦的,比齁还麻烦。

所以 Node.js 8 就提供了 util.promisify() 这个方法,方便我们把原来的异步回调方法改成支持 Promise 的方法,接下来,想继续 .then().then().then() 搞队列,还是 await 就看实际需要了。

我们看下范例,让读取目录文件状态的 fs.stat 支持 Promise:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
stat('.')
  .then((stats) => {
    // Do something with `stats`
  })
  .catch((error) => {
    // Handle the error.
  });

怎么样,很简单吧?按照文档的说法,只要符合 Node.js 的回调风格,所有函数都可以这样转换。也就是说,只要满足下面两个条件,无论是不是原生方法,都可以:

  1. 最后一个参数是回调函数
  2. 回调函数的参数为 (err, result),前面是可能的错误,后面是正常的结果

结合 Await/Async 使用

同样是上面的例子,如果想要结合 Await/Async,可以这样使用:

const util = require('util');
const fs = require('fs');

const stat = util.promisify(fs.stat);
async function readStats(dir) {
  try {
    let stats = await stat(dir);
    // Do something with `stats`
  } catch (err) { // Handle the error.
    console.log(err);
  }
}
readStats('.');

自定义 Promise 化处理函数

那如果现有的使用回调的函数不符合这个风格,还能用 util.promisify() 么?答案也是肯定的。我们只要给函数增加一个属性 util.promisify.custom,指定一个函数作为 Promise 化处理函数,即可。请看下面的代码:

const util = require('util');

// 这就是要处理的使用回调的函数
function doSomething(foo, callback) { 
  // ...
}

// 给它增加一个方法,用来在 Promise 化时调用
doSomething[util.promisify.custom] = function(foo) { 
  // 自定义生成 Promise 的逻辑
  return getPromiseSomehow(); 
};

const promisified = util.promisify(doSomething);
console.log(promisified === doSomething[util.promisify.custom]);
// prints 'true'

如此一来,任何时候我们对目标函数 doSomething 进行 Promise 化处理,都会得到之前定义的函数。运行它,就会按照我们设计的特定逻辑返回 Promise 实例。

我们就可以升级以前所有的异步回调函数了。

继续阅读“Node.js 8 中的 util.promisify”

小试 Element UI

经过一个小项目的试用,我觉得 Element UI 1.2.5 设计上过分激进,质量上存在一些影响开发进度的 bug,所以不推荐使用。

不确定写多长,写先结论吧:暂时不推荐使用。原因如下:

  1. 影响使用的小 Bug 有点多
  2. 需要重新学习一门语言


接下来详述。

从前司离职之后,我开始更新技术栈。离开惯用的 Backbone,考虑再三,投入 Vue 怀抱。选择 Vue,而不是竞品 Angular、React 有三个理由:

  1. 文档友好,社区活跃。
  2. 模块拆分的很好,学习曲线平缓。
  3. 基于标准化技术,可以最大限度的避免浪费。

不过实操之后发现,Vue 与我惯用的 Bootstrap 有些冲突,主要在于:

  1. Bootstrap 对过渡效果和切换的操作依赖于样式,比如 .active.in。Vue 在处理模板时会把当前样式先缓存起来,然后根据数据增删绑定的样式。此时就可能出问题,tab 页切不动或者动画突然打断之类的。
  2. Bootstrap 会广播特定的事件,这些事件无法被 Vue 捕获,只能在 mounted() 的钩子里手工绑定。

于是我觉得,既然根基(jQuery)变了,最好把整条线都更新了吧。左右看了看,准备先试下 Element UI。这是饿了么推出的基于 Vue2.0 的组件库,目测组件齐全,文档详细,而且直接以 2.0 为基础,符合我追新的想法。

实际用了之后……唉……有点……遗憾。项目地址

首先,Element UI 把所有组件都封装了,包括布局,比如 <el-row><el-col>,我觉得这样太过了。从现实经验来看,布局元素几乎不可能够用,别人总要补充一些。封装的元素我不太知道最终生成的代码是什么样的,也就不好操作,总不能审查元素一个一个看吧?——对了,Element 的文档里缺少样式列表,也是个问题。

封装的另一个问题,所有元素都要通过后期渲染,总让我感觉不舒服。以及,我几乎无论干什么都要查文档,几乎没法直接动手,这和我选择 Vue 的初衷是相违背的。

接下来,小 Bug,有点多。除去布局和提示之类,我只用到3个组件:<el-button><el-table><el-pagination>,结果就遇到4个 bug,浪费很多时间去调试,有两个我给他们开了 issue,还有两个懒得弄了。这里列一下吧:

  1. <el-button :loading="scope.row.fetching"> 无法把 loading 绑定到数据的 .fetching 属性上
  2. <el-pagination> 设置 total 不更新视图
  3. <el-pagination> 更新 total 之后再次广播 current-change 事件,导致重复刷新
  4. <el-table> 里每行的 ref 属性没法正确生成数组

2017-04-04 更新:

经过 issue 沟通,我的理解的确有些问题,

  1. Vue 绑定属性的时候,如果该属性层次较深,比如 a.b.c,就不会修改它的 getter/setter,于是会失去响应式更新的特性;同时也不会报错。
  2. <el-table> 不是通过 v-for 渲染的,自然 ref 的表现也就正常了。

可能别的组件很健壮吧,我运气不好。

总之,我觉得就目前这个版本,1.2.5,来看,Element UI 还没到让人放心用并且用得好的程度。

下一次我可能会选别家的再试下,或者继续用 Bootstrap 然后自己拼些小组件出来——我这次就是想找个有 loading 的 button 才找 UI 库的。


啊,最后,还是感谢 Element UI 团队,感谢饿了么。希望你们再接再厉,相信将来这套库会更好。

使用 Webpack 时需避免循环引用

使用 Webpack 等打包工具时,需要注意尽量避免循环引用,即 A 引用 B,B 也引用 A,不然可能出现难以预料的问题。

开发日历控件的时候,对方变更了一下需求,基本上将最终产品分成两个:

  1. 选择连续时间段
  2. 选择多个不连续时间

那么我们知道,对于这种大部分功能一致,只有若干函数逻辑不同的产品,最合适的就是状态模式。于是很自然的,我就拿“2”作为标准模式,“1”作为新模式,将其重构成父类和子类,大概关系如下:

// 父类
// DatePicker.js

import RangeDatePicker from './RangeDatePicker';

class DatePicker {
  ....
  static getInstance(el, options) {
    if (options.scattered) {
      return new DatePicker(el, options);
    } else {
      return new RangeDatePicker(el, options);
    }
}


// 子类
// RangeDatePicker.js

import DatePicker from './DatePicker';

class RangeDatePicker extends DatePicker {

}

因为这个类只有两个成员,所以我把工厂方法 .getInstance() 放到了父类里面,通过判断参数确定应该返回哪一类实例。代码写完,测试的时候却报错:

Super expression must either be null or a function, not undefined

这个意思很明显,被继承的父类不能未定义。然则 DatePicker 明明是定义了的,只是验证两个类文件的话,均未出现任何语法错误。

遇事不决先 Google,还真找到很多结果,不过大多数都和 React.Component 有关,翻了半天一无所获,只好自力更生。打开 Chrome 开发者工具,勾上“Pause on Exceptions”,观察发生异常时的状况,一遍又一遍,我渐渐意识到,发生这个错误的时候,DatePicker 还未能在 webpack 的环境中完成注册。问题找到了!

与其它编译类语言不同,JS 是动态语言,所有 JS 代码都是放到统一的环境里跑的,类的代码如此,import 也是如此。所以对于其他语言,比如 ActionScript、Java,循环引用,即 A 引用 B,B 也引用 A,是没问题的,因为类的代码都会编译到执行文件,执行的时候,都已经在环境中;而 JS 是边执行边置入环境,具体到我这里,在将父类 DatePicker 放入环境时,会先 import 子类 RangeDatePicker 的代码,而子类又会要求 import 父类的代码,父类的代码正在引入中,于是便产生了问题。

想明白这点,后面就好办了,直接创建一个工厂类,把工厂方法放到里面执行,问题便解决了:

import DatePicker from './DatePicker';
import RangeDatePicker from './RangeDatePicker';

export default {
  createDatePicker(el, options) {
    if (options.scattered) {
      return new DatePicker(el, options);
    } else {
      return new RangeDatePicker(el, options);
    }
  }
}

PS:当年写依赖注入和包管理工具的时候,就卡在这个地方,怎么都想不通,于是一直也没写完。没想到这些个浓眉大眼有头发的,也都这么不负责任,这种问题都不解决就搞出来让全世界人用了。

七牛 Node SDK 会导致 Electron 启动新实例

在 Electron 的 main process 里调用七牛云 SDK,会启动一个新实例,原本的上传会暂停。这个时候关掉新实例,上传会继续。当前文件上传完成后,下个文件又会启动一个新实例。如此反复。

如题,暂时不确定是哪里导致的。

总之,在 Electron 的 main process 里调用七牛云 SDK qiniu.io.putFile(),会启动一个新实例,原本的上传会暂停。这个时候关掉新实例,上传会继续。当前文件上传完成后,下个文件又会启动一个新实例。如此反复。

文档中的代码如下:

qiniu.io.putFile(uptoken, key, localFile, extra, function(err, ret) {
      if(!err) {
        // 上传成功, 处理返回值
        console.log(ret.hash, ret.key, ret.persistentId);       
      } else {
        // 上传失败, 处理返回代码
        console.log(err);
      }
});

已开 issue

估计要等春节后修复了。

暂时可以用社区版 SDK 先顶上。