Vue 的响应式原理

97 阅读3分钟

Object.defineProperty

我们都知道在 Vuedata 中声明的数据是 响应式 的,当被声明的数据被修改之后,Vue 就可以捕捉到这个修改并进行一系列操作,捕捉数据的变化依靠的就是 Object.defineProperty

简单的说,Object.defineProperty 可以监听对象上属性的变动:

let person={    
  name:"xiaoming"
}

Object.defineProperty(person, "name",{
  get(){
    console.log('get value')
  },
  set(val){        
    console.log('set value')
  }
})
person.name //get value
person.name = 'xiaohong' //set value

如果我们在 Object.definePropertyset 方法中做操作 dom 的话,那么我们可以实现修改 name 属性的同时让页面跟着变化:

<div id="personName">xiaoming</div>
<input type="text" id="input" value="xiaoming" />
let person = {
  name: "xiaoming"
};
let _name = person.name;
Object.defineProperty(person, "name", {
  get() {
    console.log("get value");
    return _name;
  },
  set(val) {
    console.log("set value");
    _name = val;
    document.getElementById("personName").innerHTML = val;
  }
});

document.getElementById("input").addEventListener("input", function(e) {
  person.name = e.target.value;
});

上面我们实现了一个简单的数据视图的联动,但是这里只是监听对象的一个属性,我们需要监听对象的所有属性。

observe

要完成所有属性的监听,可以创建一个 observe 函数,然后通过递归循环来完成监听:

function observe (obj){
  if (!obj || typeof obj !== 'object') {
    return
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key)
  })
}
function defineReactive (obj, key) {
  let val = obj[key]
  observe(val)
  Object.defineProperty(obj, key, {
    get() {
      console.log("get value");
      return val;
    },
    set(newVal) {
      console.log("set value");
      val = newVal;
    }
  })
}

Dep

直接在 set 方法中写逻辑是非常不灵活的,我们需要增加一个 Dep 类,用于消息的订阅与发布。

class Dep {
  constructor() {
    this.subs = []
  }
  depend(sub) {
    this.subs.push(sub)
  }
  notify() {
    this.subs.forEach(fn => {
      fn()
    })
  }
}
Dep.target = null

我们在 get 的时候,将 set 时需要做的事情,放到订阅数组中,然后在 set 时发布这个订阅。

function defineReactive (obj, key) {
  let val = obj[key]
  observe(val)
  let dep = new Dep()
  Object.defineProperty(obj, key, {
    get() {
      // 收集依赖,加入subs数组
      if (Dep.target) {
        dep.depend(Dep.target)
      }
      return val;
    },
    set(newVal) {
      val = newVal;
      // 触发依赖,取出subs数组并执行
      dep.notify()
    }
  })
}

现在假设我们想在 person.name 被修改时触发一些操作,那么我们可以将这些操作写成一个函数赋值给 Dep.target,然后触发该属性的 get,就可以在 set 时触发这个方法,如下:

let person = {
  name:'xiaoming',
  age:'18'
}
observe(person)
Dep.target = function(){
	console.log(person.name)
}
person.name
person.name = 'xiaohong'

Watcher

现在我们必须每次强行触发属性的 get 才可以将需要触发的依赖收集到 subs 数组,我们可以创建一个 Watcher 类来处理

class Watcher {
  constructor(obj, key, callback) {
    Dep.target = this
    this.obj = obj
    this.key = key
    this.value = obj[key]
    this.callback = callback
    Dep.target = null
  }
  update() {
    this.callback(this.obj[this.key])
  }
}
// 修改 Dep 的 notify 方法
class Dep {
  constructor() {
    this.subs = []
  }
  depend(watcher) {
    this.subs.push(watcher)
  }
  notify() {
    this.subs.forEach(watcher => {
      watcher.update()
    })
  }
}
let person = {
  name:'xiaoming',
  age:'18'
}
observe(person)

new Watcher(person, 'name', function(newValue){
  console.log(newValue)
  // Vnode 对比逻辑
})
person.name = 'xiaohong'

Watcher 何时初始化

Watcher 函数在 Vue 中什么时候初始化呢?

Vue 中,template 最终会被编译成 render 函数, 在 render 函数执行时会触发 new Watcher 完成数据的 getter ,具体在 src/core/instance/lifecycle.js 中。

官网中的图也描述了这个过程:

最后

最后来总结整体监听数据与依赖收集的过程:

首先进行 observe() 监听数据,当监听结束后,此时 person 的属性已经拥有了 getset 方法,每个属性的中都通过闭包引用了一个 dep 实例。

然后调用 Watcher 构造函数,通过 this.value = obj[key] 赋值。触发了属性的 get 方法。

get 方法中,将当前的 Watcher 实例添加到 dep 实例的 subs 中。

最后对 person.name 进行赋值,触发 person.nameset 方法,调用 dep 实例上的 notify 方法,触发 subs 中的 Watcher 实例的 update 方法。


原文地址

参考