Vue提高17 Vue响应式原理

原文地址 - 从零开始 - Vue 响应式原理白话

响应式过程

简单来说,依赖收集的过程是:

  1. 在组件init的过程中,将data中的属性添加getter/setter方法
  2. 在组件渲染过程中(render函数执行时),每个组件实例内部会实例化一个Watcher对象,data中的属性会被touch,触发getter方法,记录组件和属性的对应关系
  3. 当属性更新时,访问setter方法,会调用对应的wachter重新计算,调用render函数,导致关联组件更新

datagetter/setter

init阶段,data中的属性会被添加gettersetter方法,手段就是调用Object.defineProperty方法

function defineReactive(obj: Object, key: string, ...) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {....
      dep.depend()
      return value....
    },
    set: function reactiveSetter(newVal) {...
      val = newVal
      dep.notify()...
    }
  })
}

data中的每个属性都有一个dep对象,getter中会调用dep.depend()方法

Watcher的创建

Vue对象在init之后进入mount阶段,关键函数是mountComponent

mountComponent(vm: Component, el: ? Element, ...) {
  vm.$el = el

  ...

  updateComponent = () = > {
    vm._update(vm._render(), ...)
  }

  new Watcher(vm, updateComponent, ...)
  ...
}

在组件渲染过程中,实例化了一个Watcher对象,它有两个参数,第一个参数vm就是当前组件实例,第二个参数updateComponent会调用vm._rendervm._update方法,主要目的就是更新页面

vm._render目的是将Vue对象渲染为虚拟DOM
``vm._update`目的是将虚拟DOM创建或更新为真实DOM

在组件需要更新的时候,Watcher就会被调用,更新页面

收集依赖

lass Watcher {
  getter: Function;

  // 代码经过简化
  constructor(vm: Component, expOrFn: string | Function, ...) {
      ...
    this.getter = expOrFn
    Dep.target = this // 暂且不管
    this.value = this.getter.call(vm, vm) // 调用组件的更新函数
    ...
  }
}

Watcher的构造函数中,会调用expOrFn方法,这个expOrFn就是上面的updateComponent方法,进而调用vm._render方法

在这个过程中,会访问data中的属性的getter(也就是touch的过程),这是会调用上面提到的dep.depend()方法

为了弄清dep.depend()方法究竟做了什么,在dep对象的构造函数中看:

class Dep {
  static target: ? Watcher;
  subs : Array < Watcher > ;

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

dep.depend()实际上调用了Dep.target.addDep(this),而在Watcher的构造函数中有这样的代码Dep.target = this,所以Dep.target.addDep(this)也就是调用了WatcheraddDep方法

Watcher中的addDep方法简化后:

class Watcher {
  addDep(dep: Dep) {
      ...
    this.newDeps.push(dep)
    dep.addSub(this)
      ...
  }
}

class Dep {
  addSub(sub: Watcher) {
    this.subs.push(sub)
  }
}

Watcher将这个Dep保存了下来,然后调用了Dep的addSub方法,将Watcher存了进去

我这样理解,Dep记录了所有依赖这个属性的组件和组件的Watcher实例,同样,在Watcher中也记录了当前组件实例都使用了哪些属性

经过这些步骤,Dep.depend的导致addSub方法被调用,将当前的Watcher记录到了组件的Depsubs

派发更新

修改data中已经双向绑定后的属性,会调用setter方法,调用了dep.notify方法

class Dep {
  static target: ? Watcher;
  subs : Array < Watcher > ;

  depend() {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify() {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

dep.notify方法调用所有依赖这个属性的组件的Watcher实例,运行render方法,更新页面

render-Watcher-组件是一一对应的,data中的每个属性都有对应的dep实例

更详细的细节:

问题

由于在Vue的init过程中,对data中的属性执行了gettersetter转化过程,如果是未初始化话添加的data根属性,则无法被追踪:

var vm = new Vue({
  data: {
    a: 1
  }
})

// `vm.a` 是响应的

vm.b = 2
// `vm.b` 是非响应的

还有一个问题:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})

vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的

items是数组,数组的索引并不是属性,很难用Dep去绑定,长度length也没有被处理,解决方法:

vm.items.splice(indexOfItem, 1, newValue)
vm.items.splice(newLength)

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值