相信不少使用 Framer 的设计师在学习一段时间之后都会遇到这个问题——在循环中给图层添加事件总是出错。
场景
一般我们会在这种场景下遇到这个错误:通过循环生成一系列元素,同时给每个元素添加事件以实现点击该元素时改变元素样式的效果。为了更清楚地说明,我举一个例子。假如我们要做这么一个原型,在屏幕中间有一个可以左右移动的轮播图,一共 5 张,下面有其对应的 5 张缩略图。当点击对应的缩略图时,中间的相册就会自动滚动到对应的那一页。
请先点击这里在线预览或下载原型查看代码。首先我们需要创建一个页面组件,并往里面添加 5 个页面。为了区分它们,我给它们添加了不一样的背景色,同时在中间标上了数字。这一段代码不再赘述,我们重点看一下缩略图的代码。
# 设置数组变量 thumbs ,用来保存 5 个缩略图
thumbs = []
# 循环生成缩略图,并添加点击事件
for i in [0...5]
thumbs[i] = new Layer
x: i *62+10
width: 50
height: 50
y: 500
borderWidth: 1
backgroundColor: "#00AAFF"
hueRotate: i * 20
thumbs[i].onClick ->
thumbs[i].borderColor = '#FFF'
pageScroller.snapToPage(pages[i])可以看到,在每一次的循环中我们做了两件事情,创建了一个新图层并给这个图层添加了一个点击事件,我们预期的效果是点击第 i 个缩略图时,第 i 个缩略图的边框会变成白色,并且页面组件跳转到第 i 个页面。写完代码的你搓搓双手,迫不及待地点了一下缩略图,却被一个错误提示吓得猝不及防。
问题分析
遇到错误怎么办呢?很多人可能习惯立即截图去问别人,但这是一个不太好的习惯。在使用 Framer 遇到错误时,我们应该先看一下底部红色的错误提示是什么,从提示中找关键词去谷歌搜索答案,实在找不到时再去问别人。
我们看一下这句错误提示:TypeError: undefined is not an object (evaluating 'thumbs[i].borderColor = '#FFF'') ,翻译成中文就是:类型错误:未定义的值不是一个对象(当解析 thumbs[i].borderColor = '#FFF' 发生) 。再看一下代码,发现第 39 行的 thumbs[i] 被标红了,就说明它不是一个对象。但是 thumbs[i] 不应该是第 i 个缩略图吗?我们先验证一下:注释这两行代码,同时加上一行 print i 看输出的是不是点击的缩略图序号。
无论我点击哪一个缩略图,输出的都是 5 。好了,抓住罪魁祸首了。这个循环一共执行了 5 次,第一次时 i 等于 0 ,以后每次 i 都要加 1 ,所以整个过程是这样的。
- i = 0 新建图层并赋值给 thumbs[0]
- i = 1 新建图层并赋值给 thumbs[1]
- i = 2 新建图层并赋值给 thumbs[2]
- i = 3 新建图层并赋值给 thumbs[3]
- i = 4 新建图层并赋值给 thumbs[4]
最多就到 thumbs[4] ,而我们刚才点击时输出的 i 却是 5 ,thumbs[5] 当然是不存在的,更何况无论我点击哪一个缩略图输出的结果都是 5 ,这显然与我们预期的不一样。那么,在这个过程中究竟发生了什么不为人知的事情呢?
我来给大家解释一下,我们在添加点击事件时,只是把一个处理点击事件的函数 () -> print i 赋给了图层的 onClick 属性。
for i in [0...5]
thumbs[i] = new Layer
# ...
# 省略一堆代码
# 完整的添加事件写法
thumbs[i].onClick ( () -> print i )当我们点击缩略图时,循环早就完成了。完成最后一次循环之后,i 已经加到了 5 。所以这个时候执行这个函数,输出的自然是 5 了。那怎么解决呢?
解决方法
1、使用上下文指代关键字 this
我们的 Framer 代码,其实是由一块块作用域组成的,每块作用域中都包含了自己的环境上下文,它被保存在关键词 this 中。一个函数就包含了一块作用域和它的上下文,就好比一个函数开始执行时会告诉系统:我要开始执行啦,我有自己的上下文,里面存了我自己的环境。
thumbs[i].onClick ( () -> print i )对于上面这段代码,函数的上下文 this 指代的就是当前点击的图层。所以我们可以这么写:
thumbs[i].onClick ->
# 在这里,this 指代的就是当前被点击的图层
this.borderColor = '#FFF'2、让函数立即执行
修改代码后,我们再次点击缩略图,完美运行。可是还有一行代码 pageScroller.snapToPage(pages[i]) ,必须要用到序号 i ,this 也指代不到页面组件中的页面。遇到这种情况,我们可以让函数在循环中立即执行,这样就可以读取到当前的 i 了。
不过不是让点击的执行函数立即执行,而是将整个点击事件包裹在一个函数中,将 i 当参数传进去,在每次循环时立即执行它。
for i in [0...5]
thumbs[i] = new Layer
# ...
# 省略一堆代码
# 定义一个函数包裹它
handler = (x) ->
thumbs[x].onClick ->
pageScroller.snapToPage(pages[x])
# 将 i 作为参数传递进去并立即执行
handler(i)这样我们就完美解决了这个奇怪的错误。为了使代码保持简洁,我们将代码合并一下。
for i in [0...5]
thumbs[i] = new Layer
# ...
# 省略一堆代码
# 定义一个函数包裹它
handler = (x) ->
thumbs[x].onClick ->
this.borderColor = '#FFF'
pageScroller.snapToPage(pages[x])
# 将 i 作为参数传递进去并立即执行
handler(i)这篇文章如果你没有看懂,请不要灰心,要知道作为设计师我们学习用 Framer 来写代码是为了服务于设计,你不必像工程师那样面面俱到。所以你可以先记住两种解决方式,多使用 Framer 来写原型,写多了自然会理解。
如果还有疑问可以查阅中文文档:www.framercn.com,或者订阅我的豆瓣专栏《Framer 原型设计指南》。
谢谢阅读!
如需转载请联系,默默转载不是雷锋,是抄袭。