vue的响应式原理

228 阅读3分钟

一、响应式原理

什么是响应式原理?

意思就是在改变数据的时候,视图也跟着更新。这意味你只需要进行数据的管理。

Vue则是利用Object.defineProperty的方法里面的setter和getter方法的观察者模式来实现。所以在学习Vue的响应式原理之前,先学习两个预备知识:Object.defineProperty和观察者模式

二、预备知识

Object.defineProperty

这个方法就是一个对象上定义一个新的属性,或者改变一个对象现有的属性,并且返回这个对象。里面有两个字段get、set。顾名思义set就是设置属性的值,get就是获取属性的值。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        var bvalue;
        var o ={};
        Object.defineProperty(o,'b',{
           get:function(){
               console.log('监听设置的值')
               return bvalue
           },
           set:function(newvalue){
                console.log('监听设置的值')
                bvalue = newvalue
           },
           enumerable:true,
           configurable:true
        })
        o.b=38;
        console.log(o.b)
        // 监听设置的值
        // 监听获取的值
        // 38
    </script>
</body>
</html>

可以实现一下简单的数据双向绑定

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <div>
        <input type="text" id="txt">
        <span id="sp"></span>
    </div>
    <script>
        var txt = document.getElementById('txt'),sp = document.getElementById('sp'),
        data = {}
        Object.defineProperty(data,'msg',{
            // 这里实现的是数据变化引起内容的变化
            set:function(newvalue){
                txt.value = newvalue
                sp.innerHTML = newvalue
            }
        })
        // 这里是监听文本框中的变化,当文本框中的内容发生变化的时候,改变data中的数据
        txt.addEventListener('keyup',function(e){
            data.msg = e.target.value
        })
    </script>
</body>
</html>

Vue给data里的所有的属性加上get和set的这个过程叫做Reactive化。

观察者模式

之前已经说过了,观察者模式就是一对多的关系,一个Subject发生变化,依赖它的那些Obeserver就会被通知然后自动更新。 实现一个简单的观察者模式:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script>
        // 抽像Subject
        function Subject(){
            this.dep = [],
            //用来添加观察者,(也可以说是添加客户)
            // 因为我这里用的是箭头函数,本身没有this,所以前面的加上this
            // 让他指向实例对象
            this.register=(fn)=>{
                this.dep.push(fn)
            }
            // 发布消息通知观察者(通知客户)
            this.notify=()=>{
                this.dep.forEach(item=>item())
            }
        }
       const cheese = new Subject()
        // 添加观察者
       cheese.register(()=>{console.log('call daisy')})
       cheese.register(()=>{console.log('call tom')})
       cheese.register(()=>{console.log('call tomas')})
        // 发布消息(通知客户)
        cheese.notify()
    </script>
</body>
</html>

三、原理分析

v2-e64787c811b0e8e9a0b1ffc35ad5947c_1440w.jpg 上图是官网的一张表示这个过程的图。 总共分了三个步骤:

1、init阶段:

VUE的data的属性都会被reactive化,也就是加上setter/getter函数。

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()
        ...
      }
    })
  }
  
  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就是一个Subject类,每一个data的属性都会有一个dep对象。当调用getter时,去dep里注册函数, 当调用setter时,就去通知执行刚才注册的函数。

2、mount阶段

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

    ...

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

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

class Watcher {
  getter: Function;

  // 代码经过简化
  constructor(vm: Component, expOrFn: string | Function, ...) {
    ...
    this.getter = expOrFn
    Dep.target = this                      // 注意这里将当前的Watcher赋值给了Dep.target
    this.value = this.getter.call(vm, vm)  // 调用组件的更新函数
    ...
  }
}

在mount的时候会创建一个新的watcher类,这个watcher其实是链接Vue和dep的桥梁,每一个vue component对应一个watcher(监查者)。 这里可以看出new一个watcher的时候,watcher里面的this.getter.call(vm,vm)函数会被执行。getter就是updateComponent。这个函数就会调用组件的render函数来更新重新渲染。 而render函数,会访问data的属性,比如

render:function(createElement){
    return createElement('h1',this.blogTitle)
}

此时会调用这个属性blogTitle的getter属性,也就是:

get: function reactiveGetter () {
    ....
    dep.depend()
    return value
    ....
 },

// dep的depend函数
depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
}

在depend的代码里面,dep.target就是watcher,这里做的事情就是给blogTitle这个组件注册了watcher这个对象。每次render一个组件的时候,如果这个组件用到了blogTitle属性,那么这个组件相对应watcher对象都会被注册到blogTitle的Dep中。这个过程就叫做依赖收集。
收集完所有依赖blogTitle的watcher属性的组件之后,当blogTitle发生改变的时候会notify通知所有Watcher更新关联的组件。
3、更新阶段 当blogTitle发生改变的时候,就去调用Dep的notify函数,通知所有的watcher调用updata函数更新。

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

总结:

1、第一步:初始化组件的时候,先是给每一个data的属性都注册getter和setter,也就是reactive化。然后再new一个自己的watcher,watcher会立刻调用render函数去生成虚拟DOM。在调用render的时候就会用到data的属性值,此时触发属性值的getter函数,然后将watcher注册进属性值对应的sub中。

2、第二步:当data发生变化的时候,就会调用属性的setter方法,然后触发dep.notify,通知所有的watcher去调用updata函数更新