computed watch原理理解

本文详细探讨了Vue实例化过程中的数据劫持和初步渲染,讲解了Dep Watcher如何在数据变化时更新视图。接着,介绍了Computed的缓存机制,解释了在初始化渲染和后续数据变更时如何执行计算属性。最后,分析了Watch的工作原理,包括深检测、立即执行以及在数据变更时如何触发回调以更新视图。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、new Vue()的执行过程
1.1 数据劫持和初步渲染
function Vue (options) {
  // 初始化
  this._init(options)
  // 执行render函数
  this.$mount()
}
Vue.prototype._init = function (options) {
  const vm = this
  // 把options挂载到this上
  vm.$options = options
  if (options.data) {
    // 数据响应式
    initState(vm)
  }
  if (options.computed) {
    // 初始化计算属性
    initComputed(vm)
  }
  if (options.watch) {
    // 初始化watch
    initWatch(vm)
  }
}

initState()中实现数据劫持和代理:

function initState(vm) {
  // 拿到配置的data属性值
  let data = vm.$options.data;
  // 判断data 是函数还是别的类型
  data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {};
  // 数据代理
  const keys = Object.keys(data);
  let i = keys.length;
  while(i--) {
    // 从this上读取的数据全部拦截到this._data到里面读取
    // 例如 this.name 等同于  this._data.name
    proxy(vm, '_data', keys[i]);
  }
  // 数据观察
  observe(data);
}

// 数据观察函数
function observe(data) {
  if (typeof data !== 'object' && data != null) {
    return;
  }
  return new Observer(data)
}

// 从this上读取的数据全部拦截到this._data到里面读取
// 例如 this.name 等同于  this._data.name
function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key] // this.name 等同于  this._data.name
    },
    set(newValue) {
      return vm[source][key] = newValue
    }
  })
}

class Observer{
  constructor(value) {
    // 给每一个属性都设置get set
    this.walk(value)
  }
  walk(data) {
    let keys = Object.keys(data);
    for (let i = 0, len = keys.length; i < len; i++) {
      let key = keys[i]
      let value = data[key]
      // 给对象设置get set
      defineReactive(data, key, value)
    }
  }
}

function defineReactive(data, key, value) {
  Object.defineProperty(data, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue == value) return
      observe(newValue) // 给新的值设置响应式
      value = newValue
    }
  })
  // 递归给数据设置get set
  observe(value);
}

initComputed()以及initWatch()后面再实现,这时候new Vue()中的this._init(options)执行完成,接着执行this.$mount()

// 挂载方法
Vue.prototype.$mount = function () {
  const vm = this
  new Watcher(vm, vm.$options.render, () => {}, true)  // 这里的参数true表示是渲染Watcher
}

在这里可以看到new了一个Watcher,此处的Watcher就是渲染Watcher,先看看Watcher的初步实现:

let wid = 0
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm // 把vm挂载到当前的this上
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn // 把exprOrFn挂载到当前的this上,这里exprOrFn 等于 vm.$options.render
    }
    this.cb = cb // 把cb挂载到当前的this上
    this.options = options // 把options挂载到当前的this上
    this.id = wid++  // 每个Watcher都有其id
    this.value = this.get() // 相当于运行 vm.$options.render()
  }
  get() {
    const vm = this.vm
    let value = this.getter.call(vm, vm) // 把this指向到vm,此时的getter即是render函数
    return value
  }
}

可以看到,当在this.$mount()new Watcher()渲染Watcher)时,会立刻执行this.get()方法去获取数据渲染,至此,首页渲染就完成了

1.2 Dep Watcher

在首页渲染的过程中,是data订阅的时期,以实现数据修改时能够修改视图

  • Dep类的实现:
// 依赖收集
let dId = 0
class Dep{
  constructor() {
    this.id = dId++ // 每次实例化都生成一个id
    this.subs = [] // 让这个dep实例收集watcher
  }
  depend() {
    // Dep.target 就是当前的watcher
    if (Dep.target) {
      Dep.target.addDep(this) // 让watcher,去存放dep,然后里面dep存放对应的watcher,两个是多对多的关系
    }
  }
  notify() {
    // 触发更新
    this.subs.forEach(watcher => watcher.update())
  }
  addSub(watcher) {
    this.subs.push(watcher)
  }
}

let stack = []
// push当前watcher到stack 中,并记录当前watcer
function pushTarget(watcher) {
  Dep.target = watcher
  stack.push(watcher)
}
// 运行完之后清空当前的watcher
function popTarget() {
  stack.pop()
  Dep.target = stack[stack.length - 1]
}

发现Dep中有一个depend()实例方法,该方法核心代码为Dep.target.addDep(this),即调用当前栈顶WatcheraddDep(),目的是实现每一个Watcher记录所有它订阅的dep以及每一个dep记录它所有收集的Watcher的双向保存,看看Watcher的改进实现:

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    this.cb = cb
    this.options = options
    this.id = wid++
+    this.deps = []
+    this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
    this.value = this.get()
  }
  get() {
    const vm = this.vm
+   pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)
+   popTarget()
    return value
  }
+  addDep(dep) {
+    let id = dep.id
+    if (!this.depsId.has(id)) {  // 当watcher有dep时,也意味着dep中已有该watcher,就不再双向保存了,保证了首页渲染后数据修改,触发执行渲染watcher时,不会再添加渲染watcher到属性的dep中
+      this.depsId.add(id)
+      this.deps.push(dep)  // 当前watcher保存dep
+      dep.addSub(this)  // dep保存当前watcher
+    }
+  }
+  update(){
+    this.get()
+  }
}

DepWatcher都有了,接下来就是watcher什么时候订阅:

function defineReactive(data, key, value) {
  let dep = new Dep()
  Object.defineProperty(data, key, {
    get() {
+      if (Dep.target) { // 如果取值时有watcher
+        dep.depend() // 让watcher保存dep,并且让dep 保存watcher,双向保存
+      }
      return value
    },
    set(newValue) {
      if (newValue == value) return
      observe(newValue) // 给新的值设置响应式
      value = newValue
+      dep.notify() // 通知渲染watcher去更新
    }
  })
  // 递归给数据设置get set
  observe(value);
}
  • initState()时,每一个属性都创建了自己的dep
  • 在首页渲染时,创建渲染Watcher,执行get()pushTarget(this), 此时栈顶是渲染Watcher
  • let value = this.getter.call(vm, vm),即执行render函数,会执行每个属性的get()方法,这个时候每个属性把渲染Watcher都添加到自己的dep中,渲染Watcher中也添加了每一个自己订阅的依赖器dep
  • 当数据变化时,变化属性执行dep.notify(),变化属性的dep中有渲染Watcher,因此执行渲染Watcherupdate(),即get(),此时重新执行render函数,视图得以更新(这里的Watcher还不完善,看后面逐步实现完整的Watcher
二、Computed
  • computed特征是缓存,仅有在数据有变化时才会执行计算取值,否则都是在缓存中取的
  • 初始化渲染时会执行一次

执行代码:

const root = document.querySelector('#root')
var vue = new Vue({
  data() {
    return {
      name: '张三',
      age: 10
    }
  },
  computed: {
    info() {
      return this.name + this.age
    }
  },
  render() {
    root.innerHTML = `${this.name}----${this.age}----${this.info}`
  }
})
function changeData() {
  vue.name = '李四'
  vue.age = 20
}

执行initComputed

// 初始化computed
function initComputed(vm) {
  // 拿到computed配置
  const computed = vm.$options.computed
  // 给当前的vm挂载_computedWatchers属性,后面会用到
  const watchers = vm._computedWatchers = Object.create(null)
  // 循环computed每个属性
  for (const key in computed) {
    const userDef = computed[key]
    // 判断是函数还是对象
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    // 给每一个computed创建一个computed watcher 注意{ lazy: true }
    // 然后挂载到vm._computedWatchers对象上
    watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true })
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)  // 在vm上定义key属性,如果key已在vm中,则会报错警告
    }
  }
}

computed是有缓存的,所以创建watcher的时候,会传一个配置{ lazy: true },同时也可以区分这是computed watcher,然后到watcer里面接收到这个对象,我们继续看watcher的实现:

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
+    if (options) {
+      this.lazy = !!options.lazy // 为computed 设计的
+    } else {
+      this.lazy = false
+    }
+    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set()
+    this.value = this.lazy ? undefined : this.get()
  }
  // 省略很多代码
}

从上面这句this.value = this.lazy ? undefined : this.get()代码可以看到,computed创建watcher的时候是不会指向this.get的,第一次执行get是在render即初次渲染时获取计算属性时
computed watcher创建好了之后,把计算属性挂载到vm实例上,即执行defineComputed(vm, key, userDef)

// 设置comoputed的 set个set
function defineComputed(vm, key, userDef) {
  let getter = null
  // 判断是函数还是对象
  if (typeof userDef === 'function') {
    getter = createComputedGetter(key)
  } else {
    getter = userDef.get
  }
  Object.defineProperty(vm, key, {   // 劫持计算属性
    enumerable: true,
    configurable: true,
    get: getter,
    set: function() {} // 又偷懒,先不考虑set情况哈,自己去看源码实现一番也是可以的
  })
}
// 创建computed函数
function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {// 给computed的属性添加订阅watchers
        watcher.evaluate()
      }
      // 把渲染watcher 添加到属性的订阅里面去,这很关键
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

可以看到,读取计算属性时主要执行了createComputedGetter(key)(这里只考虑了getter是函数的情况),核心代码是watcher.evaluate()watcher.depend(),接下来在watch中实现:

class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    }
    if (options) {
      this.lazy = !!options.lazy // 为computed 设计的
    } else {
      this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)
    popTarget()
    return value
  }
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this);
    }
  }
  update(){
    if (this.lazy) {
      this.dirty = true
    } else {
      this.get()
    }
  }
  // 执行get,并且 this.dirty = false
+  evaluate() {
+    this.value = this.get()
+    this.dirty = false
+  }
  // 所有的属性收集当前的watcer
+  depend() {
+    let i = this.deps.length
+    while(i--) {
+      this.deps[i].depend()
+    }
+  }
}

执行流程:

  • 1、首先在render函数里面会读取this.info,这个会触发createComputedGetter(key)中的computedGetter(key)
  • 2、然后会判断watcher.dirty,执行watcher.evaluate()
  • 3、进到watcher.evaluate(),执行this.get方法,这时候会执行pushTarget(this)把当前的computed watcher pushstack里面去,并且把Dep.target 设置成当前的computed watcher
  • 4、然后运行this.getter.call(vm, vm) 相当于运行computedinfo: function() { return this.name + this.age },这个方法;
  • 5、info函数里面会读取到this.name,这时候就会触发数据响应式this.nameObject.defineProperty.get的方法,这里name会进行依赖收集,把watcher收集到对应的dep上面,computed watcher此时也把namedep记录在了自己的deps中;并且返回name = '张三'的值,age收集同理;
  • 6、依赖收集完毕之后执行popTarget(),把当前的computed watcher从栈清除,返回计算后的值('张三+10'),并且this.dirty = false
  • 7、watcher.evaluate()执行完毕之后,就会判断Dep.target是不是true,如果有就代表还有渲染watcher,就执行watcher.depend(),然后让watcher里面的deps都收集渲染watcher,这就是双向保存的优势。
  • 8、此时name都收集了computed watcher渲染watcher。那么设置name的时候都会去更新执行watcher.update()
  • 9、首页渲染后,数据更改,执行watcher,update(),如果是computed watcher的话不会重新执行一遍,只会把this.dirty设置成true,等渲染的时候需要用到对应计算属性再执行watcher.evaluate()进行info更新。没有变化的话this.dirty就是false,不会执行watcher.evaluate()方法。这就是computed缓存机制
三、watch

执行代码:

const root = document.querySelector('#root')
var vue = new Vue({
  data() {
    return {
      name: '张三',
      age: 10
    }
  },
  computed: {
    info() {
      return this.name + this.age
    }
  },
  watch: {
    name(newValue, oldValue) {
      console.log(oldValue, newValue)
    }
  },
  render() {
    root.innerHTML = `${this.name}----${this.age}----${this.info}`
  }
})
function changeData() {
  vue.name = '李四'
  vue.age = 20
}

initWatch()

function initWatch(vm) {
  let watch = vm.$options.watch
  for (let key in watch) {
    const handler = watch[key]
    new Watcher(vm, key, handler, { user: true })
  }
}

修改watcher

let wId = 0
class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm
    if (typeof exprOrFn === 'function') {
      this.getter = exprOrFn
    } else {
+      this.getter = parsePath(exprOrFn) // user watcher 
    }
    if (options) {
      this.lazy = !!options.lazy // 为computed watcher设计的
+      this.user = !!options.user // 为user watcher设计的
    } else {
+      this.user = this.lazy = false
    }
    this.dirty = this.lazy
    this.cb = cb
    this.options = options
    this.id = wId++
    this.deps = []
    this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了
    this.value = this.lazy ? undefined : this.get()
  }
  get() {
    const vm = this.vm
    pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)  // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
    popTarget()
    return value
  }
  addDep(dep) {
    let id = dep.id
    if (!this.depsId.has(id)) {
      this.depsId.add(id)
      this.deps.push(dep)
      dep.addSub(this);
    }
  }
  update(){
    if (this.lazy) {
      this.dirty = true
    } else {
+      this.run()   // 除了computed都执行这个
    }
  }
  // 执行get,并且 this.dirty = false
  evaluate() {
    this.value = this.get()
    this.dirty = false
  }
  // 所有的属性收集当前的watcer
  depend() {
    let i = this.deps.length
    while(i--) {
      this.deps[i].depend()
    }
  }
+  run () {
+    const value = this.get()
+    const oldValue = this.value
+    this.value = value
    // 执行cb
+    if (this.user) {  // watch执行回调
+      try{
+        this.cb.call(this.vm, value, oldValue)
+      } catch(error) {
+        console.error(error)
+      }
+    } else {
+      this.cb && this.cb.call(this.vm, oldValue, value)
+    }
+  }
}
function parsePath (path) {
  const segments = path.split('.')
  return function (obj) {  // 如name.age,则name和name.age的dep里都有了watch watcher,但如果修改的是name.age.sex,则不会触发watch watcher了,需要使用deep:true选项
    for (let i = 0; i < segments.length; i++) {
      if (!obj) return
      obj = obj[segments[i]]
    }
    return obj
  }
}
  • watch默认特征是初始渲染时会获取值,执行get()方法以给检测属性dep绑定上watch watcher,但不会执行回调cb,而且不会执行深检测
  • 深检测:配置deep: true选项,原理就是遍历值继续绑定,大致代码
// 由于配置选项时,handler不是函数,因此得考虑对象写法,就不详细写了,只写出大致思路,这里deepHas = !deep
class Watcher {
  get() {
    const vm = this.vm
    pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)  // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
    if(!deepHas) {
    	this.deepBind(value)
    	deepHas = true
    }
    popTarget()
    return value
  }
  deepBind (obj) {
  	if(typeof obj === 'object') {
  		for(let key in obj) {
  			this.deepBind(obj[key])  // obj[key]去读取值,此时就绑定上了
  		}
  	}
  }
}
  • 立即执行,配置immediate: true选项
// 由于配置选项时,handler不是函数,因此得考虑对象写法,就不详细写了,只写出大致思路,这里immediateHas = !immediate
class Watcher {
  get() {
    const vm = this.vm
    pushTarget(this)
    // 执行函数
    let value = this.getter.call(vm, vm)  // 如果是watch的话getter函数的作用就是获取值而已,在获取值时就在对应属性dep上添加了该watch watcher
    if(!immediateHas) {
    	this.run()
    	immediateHas = true
    }
    popTarget()
    return value
  }
}

总结一下执行流程:

  • 执行new Vue()
    1. 执行this._init(options)
      • 执行initState()
        • proxy
        • observe
          • 创建属性自身的dep
      • 执行initComputed()
        • 创建computed watcher
          • 不会执行watcher中的get()方法,在这一步仅仅创建了computed watcher,即new Watcher
        • defineComputed()
          • 这一步通过Object.defineProperty将计算属性劫持,设置获取计算属性的get()方法为createComputedGetter(),即
          Object.defineProperty(vm, key, {   // 劫持计算属性
              enumerable: true,
              configurable: true,
              get: createComputedGetter(key)
          }
          
      • 执行initWatch()
        • 创建use watcher
          • 执行watcher中的get()方法,栈顶此时是use watcher,对应属性dep就和use watcher实现了双向保存
    2. 执行this.$mount()
      • 创建渲染 watcher,执行watcher中的get()方法,栈顶此时是渲染watcher,执行render函数取值渲染,渲染过程中取值会引起对应属性dep添加watcher以及watcher添加对应属性dep的双向保存
      • 取计算属性时:会执行createComputedGetter(),最终即执行
      if (watcher) {
            if (watcher.dirty) {// 给computed的属性添加订阅watchers
              watcher.evaluate()
            }
            // 把渲染watcher 添加到属性的订阅里面去,这很关键
            if (Dep.target) {
              watcher.depend()
            }
            return watcher.value
      }
      
      根据watcher.dirty决定是取缓存还是计算取值,因为是第一次渲染,所以是计算取值,取值过程中就实现了computed watcher与对应属性dep的双向保存。继续判断Dep.target,此时是渲染 watcher,因此执行watcher.depend(),这时对应属性的dep上就都挂载了渲染watcher。在这里对应属性的dep上就都挂载了渲染watchercomputed watcher
      1. 取普通属性时,此时栈顶时渲染watcher,因此对应属性dep渲染watcher实现了双向保存
      2. 渲染完毕,渲染watcher出栈,此时栈为空
  • 属性变动时,就会通知dep中的watcher去更新,根据入dep的顺序以及调用的顺序,user watcher先执行,然后是computed watcher,最后是渲染 watcher,这样,所有数值更新后,最后再进行页面渲染,页面就是最新的
    参考:https://segmentfault.com/a/1190000023196603
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值