为什么在 Framer 中循环给图层添加事件总是出错?

587 阅读6分钟

相信不少使用 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 原型设计指南》

谢谢阅读!

如需转载请联系,默默转载不是雷锋,是抄袭。