this 指向问题

在 Vue 中,this 的指向是一个常见的问题。理解 this 的绑定规则,可以避免很多难以排查的 bug。

this 是什么

在 Vue 实例中,this 指向 Vue 实例本身:

new Vue({
  data: {
    message: 'Hello'
  },
  created() {
    console.log(this.message)
    console.log(this === vm)
  }
})

this 绑定规则

普通函数

普通函数中的 this 由调用方式决定:

const obj = {
  name: 'John',
  greet() {
    console.log(this.name)
  }
}

obj.greet()

const fn = obj.greet
fn()

箭头函数

箭头函数没有自己的 this,继承外层作用域的 this:

const obj = {
  name: 'John',
  greet: () => {
    console.log(this.name)
  }
}

obj.greet()

Vue 中的 this 问题

问题一:回调函数中丢失 this

new Vue({
  data: {
    message: 'Hello'
  },
  methods: {
    fetchData() {
      setTimeout(function() {
        console.log(this.message)
      }, 1000)
    }
  }
})

setTimeout 的回调是普通函数,this 指向 window。

解决方案一:使用箭头函数

methods: {
  fetchData() {
    setTimeout(() => {
      console.log(this.message)
    }, 1000)
  }
}

解决方案二:保存 this 引用

methods: {
  fetchData() {
    const self = this
    setTimeout(function() {
      console.log(self.message)
    }, 1000)
  }
}

解决方案三:使用 bind

methods: {
  fetchData() {
    setTimeout(function() {
      console.log(this.message)
    }.bind(this), 1000)
  }
}

问题二:事件处理器中丢失 this

new Vue({
  methods: {
    handleClick() {
      console.log(this.message)
    }
  },
  mounted() {
    document.addEventListener('click', this.handleClick)
  }
})

事件监听器的 this 指向触发事件的元素。

解决方案:使用 bind

mounted() {
  this.handleClick = this.handleClick.bind(this)
  document.addEventListener('click', this.handleClick)
},
beforeDestroy() {
  document.removeEventListener('click', this.handleClick)
}

解决方案:使用箭头函数

mounted() {
  this.handler = (e) => this.handleClick(e)
  document.addEventListener('click', this.handler)
},
beforeDestroy() {
  document.removeEventListener('click', this.handler)
}

问题三:数组方法中丢失 this

new Vue({
  data: {
    numbers: [1, 2, 3]
  },
  methods: {
    double(n) {
      return n * 2
    },
    process() {
      this.numbers.forEach(function(n) {
        console.log(this.double(n))
      })
    }
  }
})

解决方案:使用箭头函数

process() {
  this.numbers.forEach(n => {
    console.log(this.double(n))
  })
}

解决方案:传递 this

process() {
  this.numbers.forEach(function(n) {
    console.log(this.double(n))
  }, this)
}

问题四:Promise 中丢失 this

new Vue({
  methods: {
    fetchData() {
      fetch('/api/data')
        .then(function(response) {
          return response.json()
        })
        .then(function(data) {
          this.data = data
        })
    }
  }
})

解决方案:使用箭头函数

methods: {
  async fetchData() {
    const response = await fetch('/api/data')
    this.data = await response.json()
  }
}

不要用箭头函数定义选项

错误示例

new Vue({
  data: {
    message: 'Hello'
  },
  methods: {
    greet: () => {
      console.log(this.message)
    }
  },
  computed: {
    upper: () => this.message.toUpperCase()
  },
  created: () => {
    console.log(this.message)
  }
})

箭头函数没有自己的 this,会指向外层作用域。

正确示例

new Vue({
  data: {
    message: 'Hello'
  },
  methods: {
    greet() {
      console.log(this.message)
    }
  },
  computed: {
    upper() {
      return this.message.toUpperCase()
    }
  },
  created() {
    console.log(this.message)
  }
})

何时使用箭头函数

适合使用箭头函数

回调函数:

methods: {
  fetchData() {
    fetch('/api/data')
      .then(response => response.json())
      .then(data => {
        this.data = data
      })
  }
}

数组方法:

methods: {
  process() {
    return this.items
      .filter(item => item.active)
      .map(item => item.value)
  }
}

定时器:

methods: {
  delayedAction() {
    setTimeout(() => {
      this.doSomething()
    }, 1000)
  }
}

不适合使用箭头函数

methods 定义:

methods: {
  greet: () => console.log(this.message)
}

生命周期钩子:

created: () => {
  console.log(this.message)
}

computed 定义:

computed: {
  upper: () => this.message.toUpperCase()
}

watch 定义:

watch: {
  message: (newVal) => {
    console.log(this)
  }
}

实际案例

防抖函数

new Vue({
  data: {
    searchQuery: ''
  },
  created() {
    this.debouncedSearch = debounce((query) => {
      this.search(query)
    }, 300)
  },
  methods: {
    handleInput() {
      this.debouncedSearch(this.searchQuery)
    },
    search(query) {
      console.log('Searching:', query)
    }
  }
})

function debounce(fn, delay) {
  let timer = null
  return function(...args) {
    clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

事件总线

const bus = new Vue()

Vue.component('listener', {
  created() {
    bus.$on('event', (data) => {
      console.log(this.name, data)
    })
  },
  data() {
    return {
      name: 'Listener'
    }
  }
})

异步操作

new Vue({
  data: {
    user: null,
    loading: false
  },
  methods: {
    async fetchUser(id) {
      this.loading = true
      try {
        const response = await fetch(`/api/users/${id}`)
        this.user = await response.json()
      } catch (error) {
        console.error(error)
      } finally {
        this.loading = false
      }
    }
  }
})

事件监听清理

new Vue({
  mounted() {
    this.handleResize = () => {
      this.windowWidth = window.innerWidth
    }
    window.addEventListener('resize', this.handleResize)
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.handleResize)
  },
  data() {
    return {
      windowWidth: window.innerWidth
    }
  }
})

调试 this

使用 console.log

methods: {
  someMethod() {
    console.log('this:', this)
    console.log('this is Vue instance:', this instanceof Vue)
  }
}

使用 debugger

methods: {
  someMethod() {
    debugger
  }
}

使用 Vue Devtools

Vue Devtools 可以查看组件实例的 this。

最佳实践

规则一:选项使用普通函数

new Vue({
  methods: {
    greet() {}
  },
  computed: {
    foo() {}
  },
  watch: {
    bar() {}
  },
  created() {}
})

规则二:回调使用箭头函数

methods: {
  fetchData() {
    fetch('/api/data')
      .then(data => {
        this.data = data
      })
  }
}

规则三:需要 this 的地方不用箭头函数

const obj = {
  name: 'John',
  greet() {
    console.log(this.name)
  }
}

规则四:解构时保存 this

methods: {
  process() {
    const { items, filter } = this
    return items.filter(item => item.includes(filter))
  }
}