XingYun blog
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具

XingYun

冲!
  • JS基础

    • 图解js原型链
    • JS Event Loop
    • 对象的底层数据结构
    • 让你的JavaScript代码简单又高效
    • 函数参数按值传递
    • 判断数据类型
    • 浮点数精度问题和解决办法
    • 常用方法snippet
    • 实现Promise
    • 防抖和节流
    • 巧用sort排序
  • CSS && HTML

    • CSS也需要性能优化
    • class命名规范
    • em、px、rem、vh、vw 区别
    • CSS揭秘阅读笔记
  • 浏览器

    • 浏览器是如何渲染页面的
    • 重排和重绘
    • BOM浏览器对象模型
    • DOM事件
    • 浏览器存储
  • 数据结构

    • JS实现链表
    • JS实现栈与栈应用
    • JS实现常见排序
    • 哈夫曼编码
    • MD5算法
  • vue原理浅析

    • Vue虚拟dom与Diff算法
    • 前端打包文件的缓存机制
    • vue数组为什么不是响应式
    • v-for为什么不能用index做key
  • 前端工程化

    • 浏览器是如何渲染页面的
    • 前端打包需要gzip压缩吗
    • 前端打包文件的缓存机制
    • webpack loader和plugin
  • 轮子&&组件库

    • 实现水波浪进度球
  • 文字转语音mp3文件
  • 文件上传前后端实现
  • moment.js给定时间获取自然月、周的时间轴
  • 实现文件上传功能
  • 批量下载照片
  • leaflet改变坐标原点
  • 网络

    • 有了MAC地址 为什么还需要IP地址
    • 为什么IP地址老是变
    • 我们为什么需要IPV6
    • TCP与UDP
  • 计算机组成原理

    • ASCII、Unicode、UTF-8和UTF-16
  • VSCode

    • VSCode图片预览插件 Image preview
    • rsync:linux间的高效传输工具
  • 3个提升Vue性能的写法
  • 重读Vue文档
  • moment.js给定时间获取自然月、周的时间轴
  • Vue虚拟dom与Diff算法
  • 深入响应式原理
  • Echart样例
  • 我的Vue指令库
  • 滚动到底部加载更多
  • 实现一键换肤
  • 手写Vue数据劫持
  • vue-router核心原理与手写实现
  • computed和watch
    • computed 和 watch 浅析
      • 一、computed
      • 二、watch
      • 三、源码浅析
      • 四、应用场景
  • vue数组为什么不是响应式
  • v-for为什么不能用index做key
  • webpack loader和plugin
  • keep-alive组件原理
  • vue插槽进化
  • Vue多层嵌套组件
  • vue生命周期hook
  • vue监听dom元素的resize事件
  • 前端打包需要gzip压缩吗
  • 实现水波浪进度球
  • Vue
XingYun
2021-11-29
目录
computed 和 watch 浅析
一、computed
二、watch
三、源码浅析
四、应用场景

computed和watch

# computed 和 watch 浅析

# 一、computed

在 Vue 的 template 模板内是可以写一些简单的 js 表达式的很便利,如上直接计算 this.firstName + ' ' + this.lastName,因为在模版中放入太多声明式的逻辑会让模板本身过重,尤其当在页面中使用大量复杂的逻辑表达式处理数据时,会对页面的可维护性造成很大的影响,而 computed 的设计初衷也正是用于解决此类问题。

# 1.特点

计算属性是基于它们的响应式依赖进行缓存的

  1. computed 具有缓存机制,依赖值不变的情况下其会直接读取缓存进行复用

  2. computed 是依赖已有的变量来计算一个目标变量,大多数情况都是多个变量凑在一起计算出一个变量

  3. computed 不能进行异步操作

Ps: 计算属性内的值须是响应式数据才能触发重新计算。

# 二、watch

watcher 更像是一个 data 的数据监听回调,当依赖的 data 的数据变化,执行回调,在方法中会传入 newVal 和 oldVal。可以提供输入值无效,提供中间值 特场景。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。如果你需要在某个数据变化时做一些事情,使用 watch。

特点:

  1. watch 是监听某一个变量的变化,并执行相应的回调函数,通常是一个变量的变化决定多个变量的变化
  2. watch 可以进行异步操作

watch 的原理就是为需要观察的数据创建并收集user-watcher

当数据改变时通知到user-watcher将新值和旧值传递给用户自己定义的回调函数

参数:

 personInfo = {
   name:'lili',
   age:12,
   Hobbies:['singing', 'dance']
 }

@Watch('personInfo', { immediate: true, deep: true })
  onPersonInfoChange(newValue,oldValue) {
    code...
  }
1
2
3
4
5
6
7
8
9
10

以上参数的含义是初始化立即执行一次 onPersonInfoChange 函数,在 personInfo 里的任何字段变化时,执行 onPersonInfoChange

immediate、deep 实现原理是:

immediate 初始化时立即执行一次回调函数

deep 是递归的对它的子值进行依赖收集,任何依赖发生变化就执行回调

# 三、源码浅析

一个简单的例子

<div id="app">
  <h2>{{ this.text }}</h2>
  <h2>{{ this.count }}</h2>
  <button @click="changeName">Change name</button>
  <button @click="add">Add</button>
</div>
<script>
  const vm = new Vue({
    el: '#app',
    data() {
      return {
        name: 'xiaoming',
        count: 0
      }
    },
    computed: {
      text() {
        return `Hello, ${this.name}!`
      }
    },
    methods: {
      changeName() {
        this.name = 'onlyil'
      },
      add() {
        this.count += 1
      }
    }
  })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

还是从 vue 初始化看起,从 new Vue() 开始,构造函数会执行 this._init,在 _init 中会进行合并配置、初始化生命周期、事件、渲染等,最后执行 vm.$mount 进行挂载。

# 1.初始化 computed

// src/core/instance/index.js
function Vue(options) {
  // ...
  this._init(options)
}

// src/core/instance/init.js
Vue.prototype._init = function(options?: Object) {
  // 合并选项
  // ...
  // 一系列初始化
  // ...
  initState(vm)
  // ...

  // 挂载
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

计算属性的初始化就在 initState 中:

// src/core/instance/state.js
export function initState(vm: Component) {
  const opts = vm.$options
  // ...
  // 初始化 computed
  if (opts.computed) initComputed(vm, opts.computed)
  // ...
}
1
2
3
4
5
6
7
8

看一下 initComputed 做了什么

function initComputed(vm, computed) {
  const watchers = (vm._computedWatchers = Object.create(null))
  // 遍历 computed 选项,依次进行定义
  for (const key in computed) {
    const getter = computed[key]

    // 为计算属性创建内部 watcher
    watchers[key] = new Watcher(
      vm,
      getter || noop, // 计算属性 text 函数
      noop,
      computedWatcherOptions // { lazy: true } ,指定 lazy 属性,表示要实例化 computedWatcher
    )

    // 为计算属性定义 getter
    defineComputed(vm, key, userDef)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

首先定义一个 watchers 空对象,同时挂在 vm._computedWatchers 上,用来存放该 vm 实例的所有 computedWatcher。

接下来看实例化 computedWatcher :

class Watcher {
  constructor(vm, expOrFn, cb, options) {
    // options 为 { lazy: true }
    if (options) {
      // ...
      this.lazy = !!options.lazy
      // ...
    }
    this.dirty = this.lazy // for lazy watchers, 初始 dirty 为 true

    this.getter = expOrFn

    // lazy 为 true,不进行求值,直接返回 undefined
    this.value = this.lazy ? undefined : this.get()
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

回到上边,为计算属性创建内部 watcher 之后的 watchers 对象是这样的:

{
    text: Watcher {
        lazy: true,
        dirty: true,
        deps: [],
        getter: function () {
            return `Hello, ${this.name}!`
        },
        value: undefined, // 直接赋值为 undefined ,
    }
}
1
2
3
4
5
6
7
8
9
10
11

接下来看定义计算属性的 getter

function defineComputed(target, key, userDef) {
  Object.defineProperty(target, key, {
    get: function() {
      const watcher = this._computedWatchers && this._computedWatchers[key]
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate()
        }
        if (Dep.target) {
          watcher.depend()
        }
        return watcher.value
      }
    }
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

到这里 computed 就初始化好了

再回顾一下流程

  1. 定义 vm._computedWatchers 用来存放该 vm 实例的所有 computedWatcher
  2. 遍历 computed 选项并实例化 watcher,不求值,直接将 watcher.value = undefined
  3. 通过 defineComputed 定义计算属性的 getter ,等待后边读取时触发

# 2.读取 computed

初始化完成后,会进入 mount 阶段,在执行 render 生成 vnode 时会读取到计算属性 text ,上例的 render 函数是这样:

function render() {
  var h = arguments[0]
  return h('div', [
    h('h2', [this.text]), // 这里读取了计算属性 text
    h(
      'button',
      {
        on: {
          click: this.changeName
        }
      },
      ['changeName']
    )
  ])
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

render 执行时会触发计算属性的 getter ,也就是上边定义的访问器属性:

get: function () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
        // 此时 dirty 为 true ,进行求值
        if (watcher.dirty) {
            // 求值,对 data 进行依赖收集,使 computedWatcher 订阅 data
            // 这里的 data 就是 "this.name"
            watcher.evaluate()
        }
        if (Dep.target) {
            watcher.depend()
        }
        return watcher.value
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

求值 watcher.evaluate()

取出 vm._computedWatchers 中对应的 watcher ,此时 watcher.dirty 为 true,执行 watcher.evaluate() 。

// watcher.evaluate
evaluate() {
    this.value = this.get()
    this.dirty = false
}
1
2
3
4
5

后面的步骤就是响应式数据进行依赖收集,也就是对示例中的 name 进行依赖收集,收集的是谁呢?上面提到此时的 Dep.target 是渲染 watcher ,那么总结下来,这一步做的是:

让 computedWatcher 订阅的响应式数据收集渲染 watcher

有关响应式数据原理可以在另外的文章了解

# 3.computed 触发更新

当点击按钮时,执行 this.name = 'onlyil' ,会触发 name 的访问器属性 set ,执行 dep.notify() ,依次触发它所收集的 watcher 的更新逻辑,也就是 [ computedWatcher, 渲染 watcher ] 的 update 。

触发 computedWatcher 更新

// watcher.update
update() {
    // computedWatcher 的 lazy 为 true
    if (this.lazy) {
        this.dirty = true
    }
    // ...
}
1
2
3
4
5
6
7
8

将 dirty 置为 true,表示该计算属性“脏”了,需要重新计算

接下来触发渲染 watcher 更新

// watcher.update
update() {
    // ...
    //
    queueWatcher(this)
}
1
2
3
4
5
6

这里就是加入异步更新队列,最终又会执行到 render 函数来生成 vnode ,同首次渲染一样,在 render 过程中又会读取到计算属性 text ,再次触发它的 getter :

get: function () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
        // 此时 dirty 为 true ,"脏"了
        if (watcher.dirty) {
            // 重新求值
            watcher.evaluate()
        }
        // ...
        return watcher.value
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13

这里触发重新求值我们在 computed 选项定义的函数,页面就展示了新值 Hello, onlyil!

computed 更新结束。

# 4.总结

通过上边的过程分析,可以做出如下总结:

  1. 首次渲染时实例化 computedWatcher 并定义属性 dirty: false ,在 render 过程中求值并进行依赖收集;
  2. 当 computedWatcher 订阅的响应式数据也就是 name 改变时,触发 computedWatcher 的更新,修改 dirty: true ;
  3. render 函数执行时读取计算属性 text ,发现 dirty 为 true ,重新求值,页面视图更新。
  4. 可以发现一个关键点,computedWatcher 的更新只做了一件事:修改 dirty: true ,求值操作始终都在 render 过程中。

# 5.问题

点击 Add 按钮 count 会发生改变,那么在重渲染时 computedWatcher 会重新求值吗?

答案是不会,计算属性 text 的 getter 函数并没有读取 count ,所以它的 computedWatcher 不会订阅 count 的变化,即 count 的 dep 也不会收集该 computedWatcher 。

所以细品官方文档里的描述

计算属性是基于它们的响应式依赖进行缓存的

# 四、应用场景

computed:

场景:模板中的某个值需要通过一个或多个数据计算得到并且不用重复数据计算

重点计算得到的新值**

watch:

场景:监听属性主要是监听某个值发生变化后,对新值去进行逻辑处理。

重点: 某一个特定值发生了变化,需要根据它的变化做一些事情

简单记就是:一般情况下computed 是多对一,watch 是一对多
​

#vue
上次更新: 2023/04/05, 09:41:10
vue-router核心原理与手写实现
vue数组为什么不是响应式

← vue-router核心原理与手写实现 vue数组为什么不是响应式→

最近更新
01
JavaScript-test
07-20
02
二维码的原理
07-20
03
利用ChatGPT优化代码
07-20
更多文章>
Theme by Vdoing | Copyright © 2021-2025 XingYun | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式