Chapter 05

JavaScript 逻辑层

深入掌握 App/Page/Component 生命周期、setData 响应式机制、事件系统与页面路由

5.1 逻辑层运行环境

小程序的 JavaScript 代码运行在独立的逻辑层线程中,与 WebView 渲染层分离。这意味着:

5.2 App 生命周期

App() 是小程序的全局入口,整个小程序只有一个 App 实例,贯穿整个运行周期。

// app.js
App({
  // 1. 初始化(只触发一次,冷启动/热启动都会)
  async onLaunch(options) {
    // options.scene: 场景值(1001=发现栏,1011=扫码,1044=转发卡片等)
    // options.query: 启动参数(如分享链接携带的参数)
    // options.path: 启动页面路径

    // 典型用途:初始化云开发、检查登录态
    await this.initCloud()
    await this.checkLogin()
  },

  // 2. 前台显示(每次从后台切换到前台都触发)
  onShow(options) {
    // 可用于检查版本更新
    const updateManager = wx.getUpdateManager()
    updateManager.onUpdateReady(() => {
      wx.showModal({
        title: '更新提示',
        content: '新版本已就绪,是否重启?',
        success(res) {
          if (res.confirm) updateManager.applyUpdate()
        }
      })
    })
  },

  // 3. 后台隐藏(按 Home 键、来电等)
  onHide() {
    // 暂停播放、保存草稿等
  },

  // 4. 全局错误捕获
  onError(error) {
    console.error('全局错误:', error)
    // 上报错误监控(如 Sentry)
  },

  // 5. 页面不存在(路径错误)
  onPageNotFound(options) {
    wx.redirectTo({ url: '/pages/404/404' })
  },

  // 全局共享状态(通过 getApp().globalData 访问)
  globalData: {
    userInfo: null,
    token: '',
    isLoggedIn: false,
    systemInfo: null
  },

  // 全局方法
  async initCloud() {
    wx.cloud.init({ env: 'your-env-id', traceUser: true })
  },

  async checkLogin() {
    try {
      const token = wx.getStorageSync('token')
      if (token) {
        this.globalData.token = token
        this.globalData.isLoggedIn = true
      }
    } catch (e) {
      console.error(e)
    }
  }
})

5.3 Page 生命周期

Page() 注册一个页面,每个页面都有独立的数据和生命周期。

Page({
  // 页面初始数据
  data: {
    list: [],
    loading: true,
    pageNum: 1,
    hasMore: true
  },

  // ═══ 生命周期函数 ═══

  // 1. 页面加载(只触发一次,options 为页面参数)
  async onLoad(options) {
    const { id, type } = options    // 从路由参数获取数据
    this.categoryId = id            // 临时数据不需要 setData
    await this.fetchList()
  },

  // 2. 页面显示(每次进入都触发,包括 onLoad 之后)
  onShow() {
    // 从其他页面返回时刷新数据的常用位置
    if (this.data.needRefresh) {
      this.setData({ needRefresh: false })
      this.fetchList()
    }
  },

  // 3. 页面初次渲染完成(一次)
  onReady() {
    // 此时可以操作页面元素(如获取节点尺寸)
    const query = wx.createSelectorQuery()
    query.select('#banner').boundingClientRect((rect) => {
      console.log('Banner 高度:', rect.height)
    }).exec()
  },

  // 4. 页面隐藏(跳转到其他页面时)
  onHide() {
    // 暂停视频/音频播放
  },

  // 5. 页面卸载(redirectTo 或 navigateBack 时)
  onUnload() {
    // 清除定时器、取消网络请求
    this.timer && clearInterval(this.timer)
  },

  // ═══ 页面事件 ═══

  // 下拉刷新(需在 json 中开启 enablePullDownRefresh)
  async onPullDownRefresh() {
    this.setData({ pageNum: 1, list: [], hasMore: true })
    await this.fetchList()
    wx.stopPullDownRefresh()    // 必须手动停止
  },

  // 触底加载更多
  onReachBottom() {
    if (this.data.hasMore && !this.data.loading) {
      this.fetchList()
    }
  },

  // 页面分享(设置分享信息)
  onShareAppMessage() {
    return {
      title: '这个小程序超好用!',
      path: '/pages/index/index?from=share',
      imageUrl: '/assets/share-cover.jpg'
    }
  },

  // ═══ 自定义方法 ═══

  async fetchList() {
    this.setData({ loading: true })
    try {
      const res = await getProductList({
        page: this.data.pageNum,
        categoryId: this.categoryId
      })
      this.setData({
        list: [...this.data.list, ...res.items],
        hasMore: res.hasMore,
        pageNum: this.data.pageNum + 1
      })
    } finally {
      this.setData({ loading: false })
    }
  }
})

5.4 setData 深度解析

setData 是小程序响应式的核心,它将数据的变化同步到渲染层。理解其工作原理有助于写出高性能代码。

setData 的工作流程

  1. 调用 this.setData({key: value})
  2. 逻辑层将变更数据 JSON 序列化
  3. 通过 Native Bridge 传输到渲染层
  4. 渲染层反序列化并更新 data
  5. WXML 重新渲染变更的节点(局部更新)

setData 性能优化技巧

// ❌ 错误:频繁 setData(每次都有通信开销)
onScroll(e) {
  this.setData({ scrollTop: e.detail.scrollTop })  // 每帧触发!
}

// ✅ 正确:节流处理
onScroll: throttle(function(e) {
  this.setData({ scrollTop: e.detail.scrollTop })
}, 100),

// ❌ 错误:传输大量无变化数据
updateItem(index) {
  const list = [...this.data.list]
  list[index].liked = true
  this.setData({ list })      // 传输整个数组!
},

// ✅ 正确:路径语法只更新变化的字段
updateItem(index) {
  this.setData({
    [`list[${index}].liked`]: true  // 只传输这一个字段
  })
},

// ✅ 合并多次 setData 为一次
afterFetch(data) {
  this.setData({     // 一次性更新所有字段
    list: data.items,
    total: data.total,
    loading: false,
    pageNum: this.data.pageNum + 1
  })
}

5.5 事件系统

小程序事件分为冒泡事件非冒泡事件,通过 bindcatch 前缀绑定。

<!-- bind 绑定:事件会向父节点冒泡 -->
<view bindtap="onViewTap">
  <button bindtap="onBtnTap">按钮</button>
</view>
<!-- 点击按钮:先触发 onBtnTap,再冒泡触发 onViewTap -->

<!-- catch 绑定:阻止冒泡 -->
<view bindtap="onViewTap">
  <button catchtap="onBtnTap">阻止冒泡</button>
</view>
<!-- 点击按钮:只触发 onBtnTap,不会触发 onViewTap -->

<!-- data-* 传递参数 -->
<view
  wx:for="{{list}}"
  wx:key="id"
  bindtap="onItemTap"
  data-id="{{item.id}}"
  data-type="{{item.type}}"
>
  {{item.name}}
</view>
// 事件处理函数中读取 data-* 参数
onItemTap(e) {
  // e.currentTarget.dataset: 当前节点的 data-* 集合
  // e.target.dataset: 实际触发事件的节点(可能是子节点)
  const { id, type } = e.currentTarget.dataset
  console.log('点击了:', id, type)

  // e.detail: 组件自定义事件携带的数据
  // e.touches: 触摸点信息数组
  // e.timeStamp: 事件时间戳
}

5.6 页面路由

// 保留当前页面,跳转到新页面(可返回,最多 10 层页面栈)
wx.navigateTo({ url: '/pages/detail/detail?id=123' })

// 关闭当前页面,跳转(不可返回)
wx.redirectTo({ url: '/pages/login/login' })

// 关闭所有页面,跳转到 TabBar 页面
wx.switchTab({ url: '/pages/index/index' })

// 返回上一页
wx.navigateBack({ delta: 1 })  // delta: 返回几层,默认 1

// 关闭所有页面并打开(常用于登录后跳转)
wx.reLaunch({ url: '/pages/index/index' })

// 页面间通信:EventChannel(navigateTo 携带回调)
wx.navigateTo({
  url: '/pages/picker/picker',
  events: {
    onSelect(data) {    // 接收被打开页面发来的事件
      console.log('选择了:', data)
    }
  },
  success(res) {
    res.eventChannel.emit('init', { current: this.data.selected })
  }
})