彻底理解Vue数据响应式原理

当创建了一个实例后,通过将数据传入实例的data属性中,会将数据进行劫持,实现响应式,也就是说当这些数据发生变化时,对应的视图也会发生改变。

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

console.log(vm.a === data.a);  // true

vm.a = 2;
console.log(data.a)            // 2

data.a = 3;
console.log(vm.a)              // 3

可能大部分人都已知道了Vue2.0是采用Object.defineProperty()这个API进行实现。

今天我们来从简单到深入彻底理解一些vue的响应式原理。

vue2.x响应式原理

1、getter 和 setter

实现一个转换函数(或者说是拦截函数),对对象中的属性进行setget

function convert(obj) {
  Object.keys(obj).forEach(key => {
    let val = obj[key];
    Object.defineProperty(obj, key, {
      get() {
        console.log('get val')
        return val;
      },
      set(newVal) {
        console.log(`set ${newVal} to ${key}`)
        val = newVal;
      }
    })
  })
}

对属性进行监听后,后面就是如何确定哪里调用了这些属性,这样当数据发生变动后才能进行通知。

2、依赖追踪

追踪当data改变时哪些代码应该执行。

我们先想想完成这个功能需要几部分呢?

  1. 对调用data的代码先进行收集
  2. 当数据发生变动时通知执行该代码

那么就可以先写成

class Dep{
  depend() {}
  notify() {}
}

同时需要定义一个数组用于保存已经收集的信息

class Dep{
  constructor() {
    this.subs = new Set()
  }
  depend(dep) {
    if(dep) {
      this.subs.add(dep)
    }
  }
  notify() {
    // 执行所有收集到的更新方法
    this.subs.forEach(function(sub) {
      // 调用收集到的代码块中的update方法
      sub.update()
    });
  }
}

get方法中添加依赖方法,在set方法中添加唤醒方法,可能有些看过源码的好兄弟好集美们反映过来了,这不就是observe方法嘛。是的,那就改一下函数名称。

let context = this;

function observe(obj) {
  Object.keys(obj).forEach(key => {
    let val = obj[key];
    let dep = new Dep()
    Object.defineProperty(obj, key, {
      get() {
      // 收集
        dep.depend(context);
        return val;
      },
      set(newVal) {
      // 判断是否更新
        const hasChanged = val !== newVal
        if(hasChanged) {
          console.log(`set ${newVal} to ${key}`)
          val = newVal;
          dep.notify()
        }
      }
    })
  })
}

我们重点看一下get方法中的dep.depend()。它是怎么将代码进行收集的呢?传入一个上下文,这就是将当前要收集的代码块全部都收集了,我们都知道在某个作用域中定义的变量及方法都是处于同一个上下文中,比如全局作用域window,所有的属性都可以通过window.xx进行访问。

这样在Depnotify的方法中调用的update方法,调用的就是收集的所有上下文中的update方法。

// 定义上下文
let context = this;

function observe(obj) {
  // 遍历data中的所有键
  Object.keys(obj).forEach(key => {
    // 获取键对应的值
    let value = obj[key];
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        // 收集,是哪块代码在进行收集
        dep.depend(context);
        console.log(`${key}': `, value);
        return value;
      },
      set(newVal) {
        if(value !== newVal) {
          console.log(`set ${newVal} to ${key}`);
          value = newVal;
          // 更新
          dep.notify();
        }
      }
    })
  })
}

// 定义数据
const obj = {
  count: 0
}
observe(obj);

// 在observe文件中调用了data中的值
console.log(obj.count);

// 更新方法
function update() {
  console.log("updated")
  console.log(obj.count);
}

// 更改值,此时就会触发更新
obj.count = 2;

为了更好的传入上下文,而不是手动为context赋值,我们写一个watcher进行监听。

class Watcher {
  constructor(key) {
    Dep.target = this;
    this.key = key;
  }
  update() {
    console.log(`属性${this.key}更新了`);
  }
}

Dep.target就是当前执行上下文。当不同的代码块需要被监听时,只需要实例化一个Watcher即可。

Dep中需要加上一个target属性。

class Dep{
  constructor() {
    this.subs = new Set()
  }
  depend(dep) {
    if(dep) {
      this.subs.add(dep)
    }
  }
  notify() {
    // 执行所有收集到的更新方法
    this.subs.forEach(function(sub) {
      // 调用收集到的代码块中的update方法
      sub.update()
    });
  }
}
Dep.target = null;

再更新一下observe方法

function observe(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key];
    let dep = new Dep();
    Object.defineProperty(obj, key, {
      get() {
        // 更改
        Dep.target && dep.depend(Dep.target);
        console.log(`${key}': `, value);
        return value;
      },
      set(newVal) {
        if(value !== newVal) {
          console.log(`set ${newVal} to ${key}`);
          value = newVal;
          dep.notify();
        }
      }
    })
  })
}

const obj = {
  count: 0
}
observe(obj);

// 传入需要监听的代码块
new Watcher(this, "count");

// 触发收集
console.log(obj.count);

// 触发更新
obj.count = 2;

理解了大概的思路后我们就把上面这些代码完善一下,实现自己的vue响应式代码。

class Leo {
  constructor(options) {
    this.$options = options;
    this.$data = options.data;
    // 进行响应化
    this.observe(this.$data);
  }

  observe(value) {
    // 对象类型
    if(!value || typeof value !== 'object') {
      return;
    }
    Object.keys(value).forEach(key => {
      // 响应化
      this.defineReactive(value, key, value[key]);
      
      // 执行代理
      this.proxyData(key);
    })
  }
  defineReactive(obj, key, val) {
    // 递归
    this.observe(val);

    const dep = new Dep();

    Object.defineProperty(obj, key, {
      get() {
        Dep.target && dep.addDep(Dep.target);
        return val;
      },
      set(newVal) {
        if(newVal === val) {
          return;
        }
        val = newVal;
        dep.notify();
      } 
    })
  }
  
  proxyData(key) {
    // 在实例上定义属性的话是需要this.$data.xxx的方法,我们采用defineProperty方式进行代理,使得能够通过this.xxx 进行处理
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key];
      },
      set(newVal) {
        this.$data[key] = newVal;
      }
    })
  }
}

class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(dep) {
    this.deps.push(dep);
  }
  notify() {
    this.deps.forEach(dep => dep.update())
  }
}

class Watcher {
  constructor(key) {
    Dep.target = this;
    this.key = key;
  }
  update() {
    console.log(`属性${this.key}更新了`);
  }
}

由于目前还没有实现编译部分代码,所以对于watcher的监听只能局限与实例里边,watcher创建代码如下:

class Leo {
  constructor(options) {
    // ...
    // 代码测试
    new Watcher(this, 'test');
    this.test
  }
  // ...
}

// 测试
const leo = new Leo({
  data: {
    test: "test"
  }
})
leo.test = 'change'

经过上面的代码,你应该对Vue的响应式有一定的理解了,现在我们将开始讲解Vue的响应式源码。

vue2.x响应式源码解读

Vue一大特点是数据响应式,数据的变化会作用于UI而不用进行DOM操作。原理上来讲,是利用了JS语言特性Object.defineProperty(),通过定义对象属性setter方法拦截对象属性变更,从而将数值的变化转换为UI的变化。

具体实现是在Vue初始化时,会调用initState,它会初始化dataprops等,这里着重关注data初始化.

初始化数据源码目录:src/core/instance/state.js

核心代码便是initData:

function initData (vm: Component) { // 初始化数据
  let data = vm.$options.data // 获取data
  data = vm._data = typeof data === 'function' // data(){return {}} 这种情况跟 data:{} 这种
    ? getData(data, vm)
    : data || {}
  
  // ..
  
  // proxy data on instance // 将data代理到实例上
  proxy(vm, `_data`, key) // 代理,通过this.dataName就可以直接访问定义的数据,而不用通过this.$data.dataName
  // observe data
  
  // ..

  observe(data, true /* asRootData */) // 执行数据的响应化
}

core/observer/index.js
observe方法返回一个Observer实例

function observe (value: any, asRootData: ?boolean): Observer | void {  // 返回一个Observer实例,data里的属性都对应着一个__ob__(有value,dep这两者个数相同)

  // ..

  ob = new Observer(value) // 新建一个Observer实例,通过将这个实例添加在value的__ob__属性中

  // ..

  return ob
}

Observer

core/observer/index.js
Observer对象根据数据类型执行对应的响应化操作。

class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value // data本身
    // 最开始是data调用了一次
    this.dep = new Dep() // dependence 依赖收集,data中的每一个键都对应着一个dep

    // ..

    def(value, '__ob__', this) // 定义一个property 设置_ob__,它的值就是Observer实例
    if (Array.isArray(value)) { // data中可能具有object或者array类型的数据,需要进行不同的处理方式
      
      // ..

      // 循环遍历所有的value进行observe操作
      this.observeArray(value)
    } else {
      this.walk(value) // 如果不是array类型,就直接进行操作
    }
  }

  // 处理对象类型数据
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) { 
      defineReactive(obj, keys[i]) // 添加响应式处理
    }
  }

  // 遍历数组添加监听(data里边的数组数据)
  observeArray(items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive定义对象属性的getter/settergetter负责添加依赖,setter负责通知更新.

// 定义响应式
function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 为每一个属性也添加依赖收集
  const dep = new Dep()

  // 获取对象描述符(用于判断是否可以配置)
  const property = Object.getOwnPropertyDescriptor(obj, key) // 获取键的属性描述符
  if (property && property.configurable === false) { // 如果是不可配置的属性,直接返回
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get // 获取键的getter
  const setter = property && property.set // 获取键的setter

  if ((!getter || setter) && arguments.length === 2) { // 如果键没有设置getter或者setter而且传入的参数只有两个(会设置其他非用户传进来的参数($attrs,$listeners等))
    val = obj[key] // 就直接保存这个键对应的值,进行下面的操作
  }

  // 判断是否观察子对象
  let childOb = !shallow && observe(val) // 判断是否具有子对象(返回undefined或者observer) 例如 data:{name:{lastName:'lau',firstName:'leo'}} // 给子对象也添加observer
  
  // 拦截获取
  Object.defineProperty(obj, key, { // 设置getter和setter
    enumerable: true,
    configurable: true,
    // 获取
    get: function reactiveGetter () { // 设置响应化获取
      const value = getter ? getter.call(obj) : val // 如果有配置getter,就绑定到data中,否则就直接输出结果
      if (Dep.target) { // watch实例
        dep.depend() // 给watcher添加dep
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
  
    // 拦截
    set: function reactiveSetter (newVal) {
      // 获取之前的值 且不为 NaN
      const value = getter ? getter.call(obj) : val

      // .. 判断新值

      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 如果存在子对象,且非浅监听,直接观察
      childOb = !shallow && observe(newVal)

      // 触发更新
      dep.notify()
    }
  })
}

Dep

core/observer/dep.js
Dep负责管理一组Watcher,包括watcher实例的增删及通知更新。

class Dep { // 依赖,管理watcher
  static target: ?Watcher;  // target就是watcher实例
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }
  addSub (sub: Watcher) { // 添加订阅者,有多个访问了data中的某个属性
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) { // 删除订阅者
    remove(this.subs, sub)
  }

  depend () { // watcher中添加dep实例
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {

    // stabilize the subscriber list
    // ..

    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() // 通知更新
    }
  }
}

function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
}

Watcher

Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。
每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染。

core/observer/watcher.js

class Watcher {
  constructor () {}
  // 重新收集依赖
  get () {
    pushTarget(this) // 将Dep.target设置成当前的watcher实例,将当前的watcher添加进watcher队列中
  }
  // 给当前组件的watcher添加依赖
  addDep (dep: Dep) {
    // ..
    
    // 添加watcher实例
    dep.addSub(this)
  }
  // 清除数据
  cleanupDeps () {}

  // 更新 懒更新和同步
  update () { // 通知更新
    
    // ..
    // 同步执行更新渲染
    this.run()

    // ..
    // 异步就添加到watcher队列之后统一更新
  }

  // 执行更新
  run() {}

以上就是一些数据响应式相关的源码,在使用Vue时,数组是特别注意的。

数组响应化
数组数据变化的侦测跟对象不同,我们操作数组通常使用pushpopsplice等方法,此时没有办法得知数据变化。所以vue中采取的策略是拦截这些方法并通知dep

可以用之前的observe方法来验证一下数组的变化其实是不会触发更新。

让我们来看看vue中是如何处理数组的。

src/core/observer/array.js
为数组原型中的7个可以改变内容的方法定义拦截器。

const methodsToPatch = [ // 定义拦截器
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse' 
];

// 重写以上方法并添加相应的处理
methodsToPatch.forEach(function (method) {
  // 获取原始方法
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push': 
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

src/core/observer/index.js
Observer类中覆盖数组原型

if (Array.isArray(value)) { // data中可能具有object或者array类型的数据,需要进行不同的处理方式
  // protoAugment 方法,将 arrayMethods 重写到原型上。
  protoAugment(value, arrayMethods)
  // 循环遍历所有的value进行observe操作
  this.observeArray(value)
}

// 改变目标的_proto__指向,使其指向添加了拦截器的Array原型对象上
function protoAugment (target, src) {
  target.__proto__ = src
}

// 观察数组
function observeArray(items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

原型链扩展

上方设计到原型链的一些知识,简单介绍一下原型链,如果已经熟悉原型链,可以跳过。

  1. 原型

原型对象:在声明了一个函数之后,浏览器会自动按照一定的规则创建一个对象,这个对象就叫做原型对象。这个原型对象其实是储存在了内存当中。

在js中,每个构造函数内部都有一个prototype属性,该属性的值是个对象(原型对象),该对象包含了该构造函数所有实例共享的属性和方法。当我们通过构造函数创建对象的时候,在这个对象中有一个指针(__proto__),这个指针指向构造函数的prototype的值,我们将这个指向prototype的指针称为原型。或者用另一种简单却难理解的说法是:js中的对象都有一个特殊的[[Prototype]]内置属性,其实这就是原型。

Object来举一个例子:

Object.prototype指向图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QgJ8iiGN-1645358030247)(./images/prototype指向图.png)]

Object.prototype对象
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gJYHKkpU-1645358030251)(./images/Object.prototype对象图.png)]

使用Object构造函数来创建一个对象:

var obj = new Object();

obj就是构造函数Object创造出的对象,它不存在prototype属性,但是obj却有一个__proto__属性,这个属性指向了构造函数Object的原型对象(也就是说,obj.__proto__ === Object.prototype)。每个原型对象都有constructor属性,指向了它的构造函数(也就是说,Object.prototype.constructor === Object
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Aiy5xWfU-1645358030252)(./images/__proto__指向图.png)]

总结:

  • 所有引用类型都有一个__proto__(隐式原型)属性,属性值是一个普通的对象
  • 所有的函数,都有一个 prototype(显式原型)属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数),__proto__(隐式原型)属性值指向它的构造函数的prototype属性值。
  1. 原型链
    当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会去它的__proto__隐式原型上查找,即它的构造函数的prototype,如果还没有找到就会再在构造函数的prototype__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。

以上就是Vue2.x响应式源码的阅读,强烈建议大家去阅读一遍vue源码,毕竟这篇文章只是粗略介绍了一下原理,可能有很多人对原理还是一知半解的。

vue3.x响应式原理

Object.definePropertyProxy对比

Vue3.0 中,响应式数据部分弃用了 Object.defineProperty,使用Proxy来代替它。

在阅读vue3.源码之前,我们先对Object.definePropertyProxy进行分析一下。

  1. Object.defineProperty
  • 由于 Object.defineProperty 只能对属性进行劫持,需要遍历对象的每个属性,如果属性值也是对象,则需要深度遍历。
  • 由于 Object.defineProperty 劫持的是对象的属性,所以新增属性时,需要重新遍历对象,对其新增属性再使用Object.defineProperty 进行劫持(使用 Vuedata 中的数组或对象新增属性时,需要使用 vm.$set 才能保证新增的属性也是响应式的)。

来看看vue2.x中的set方法。

core/observer/index.js

function set (target: Array<any> | Object, key: any, val: any): any {
  // 如果 target 是数组,且 key 是有效的数组索引,会调用数组的 splice 方法,
  // 我们上面说过,数组的 splice 方法会被重写,重写的方法中会手动 Observe
  // 所以 vue 的 set 方法,对于数组,就是直接调用重写 splice 方法
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 对于对象,如果 key 本来就是对象中的属性,直接修改值就可以触发更新
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // vue 的响应式对象中都会添加了 __ob__ 属性,所以可以根据是否有 __ob__ 属性判断是否为响应式对象
  const ob = (target: any).__ob__
  // 如果不是响应式对象,直接赋值
  if (!ob) {
    target[key] = val
    return val
  }
  // 调用 defineReactive 给数据添加了 getter 和 setter,
  // 所以 vue 的 set 方法,对于响应式的对象,就会调用 defineReactive 重新定义响应式对象,defineReactive 函数
  defineReactive(ob.value, key, val)
  ob.dep.notify()
  return val
}
  1. Proxy
  • 直接代理对象,不需要遍历属性操作。

  • 通过 set(target, propKey, value, receiver) 拦截对象属性的设置,是可以拦截到对象的新增属性的。

    const handler = {
      set(target, key, value) {
        console.log(`set ${key} to target`);
        target[key] = value;
        return true;
      }
    }
    const obj = {};
    const proxy = new Proxy(obj, handler);
    proxy['name'] = 'leo'
    
  • 可以检测到数组变动,不需要像Vue2.x源码中对数组方法进行重写。

    const handler = {
      set(target, key, value) {
        console.log(`set ${key} to target`);
        target[key] = value;
        return true;
      }
    }
    const obj = [];
    const proxy = new Proxy(obj, handler);
    proxy.push('leo')
    
  • 支持 13 种拦截操作,这是 Object.defineProperty 所不具有的.

    • get(target, propKey, receiver):拦截对象属性的读取,比如 proxy.fooproxy['foo']
    • set(target, propKey, value, receiver):拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
    • has(target, propKey):拦截 propKey in proxy 的操作,返回一个布尔值。
    • deleteProperty(target, propKey):拦截 delete proxy[propKey] 的操作,返回一个布尔值。
    • ownKeys(target):拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而 Object.keys() 的返回结果仅包括目标对象自身的可遍历属性。
    • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
    • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
    • preventExtensions(target):拦截 Object.preventExtensions(proxy),返回一个布尔值。
    • getPrototypeOf(target):拦截 Object.getPrototypeOf(proxy),返回一个对象。
    • isExtensible(target):拦截 Object.isExtensible(proxy),返回一个布尔值。
    • setPrototypeOf(target, proto):拦截 Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
    • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
    • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

原理解析

Vue 3.0 的响应式系统是独立的模块,可以完全脱离 Vue 而使用,所以我们在 clone 了源码下来以后,可以直接在 packages/reactivity 模块下调试。

在项目根目录中先安装依赖,后运行 yarn dev reactivity,然后引入 packages/reactivity 目录中的 dist/reactivity.global.js 文件即可。

reactive的使用

直接在源码中使用打包后的文件

创建一个html文件,并引入"dist/reactivity.global.js"

<script src="./dist/reactivity.global.js"></script>
<script>
  const { reactive, effect } = VueReactivity
  const data = reactive({ 
    count: 1
  })
  // 观测变化
  effect(() => console.log('changed to ', data.count))
  // 重新赋值会执行 () => console.log('changed to ', data.count)
  data.count = 2
</script>

看到这个例子,对于没有学习过Vue3.0的同学来说是不是有点蒙蔽,下面来写一个简单vue3.0响应式。

同样,vue3.0的响应式也分为3个阶段:

  1. 初始化阶段
  2. 依赖收集
  3. 响应阶段
1. 初始化阶段

就是使用Proxy拦截各种取值、赋值操作。

function reactive(target) {
  const observed = new Proxy(target, handler)
  return observed
}

vue3.0使用effect来将函数变成响应式,而该函数在被包裹时会被立即调用一次。因为在该函数内有引用到proxy对象的属性(上次例子的data.count),可以发现这一步其实就是触发了对象的getter方法,从而进行依赖收集。

// 保存所有的响应函数,用于后续的依赖收集
let effectStack = [];

function effect (fn) {
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  // 执行run方法
  effect()
  return effect
}

function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      effectStack.push(effect)
      // fn() 执行过程会完成依赖收集,会用到 effect
      return fn(...args)
    } finally {
      // 完成依赖收集后从池子中扔掉这个 effect
      effectStack.pop()
    }
  }
}
2. 依赖收集

再上一步的run方法中,我们可以看到其实是调用了fn这个方法,就是用来触发proxy对象的getter方法。

在这个阶段,需要建立起一个WeakMap来为原始对象跟响应式依赖建立联系(数据 -> 依赖)。

来个简单例子讲解:

const data = reactive({ 
  count: 1
})

effect(() => console.log('changed to ', data.count))

创建一个targetMapkey就是data,而value是也是一个map,里边存放着数据对应的所有响应式依赖。

targetMap: {
  {count: 1}: depsMap
}

depsMap: {
  count: Set { effection }
}

这样一层层的下去,就可以通过 targetMap 找到 data 属性中对应的更新函数effection了。

let targetMap = new Map();
function track (target, operationType, key) {
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (dep === void 0) {
      depsMap.set(key, (dep = new Set()))
    }

    if (!dep.has(effect)) {
      dep.add(effect)
    }
  }
}
3. 响应阶段

当修改对象的某个属性值的时候,会触发对应的 setter

setter 里面的 trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到 effectscomputedEffects(计算属性) 队列中,最后通过 scheduleRun() 挨个执行里面的 effect

function trigger (target, operationType, key) {
  // 取得对应的 depsMap
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    return
  }
  // 取得对应的各个 dep
  const effects = new Set()
  if (key !== void 0) {
    const dep = depsMap.get(key)
    dep && dep.forEach(effect => {
      effects.add(effect)
    })
  }
  // 简化版 scheduleRun,挨个执行 effect
  effects.forEach(effect => {
    effect()
  })
}

至此,vue3.0的响应式的简单实现就完成了。

Logo

前往低代码交流专区

更多推荐