美文网首页
最简化 VUE的响应式原理

最简化 VUE的响应式原理

作者: 小豆soybean | 来源:发表于2021-08-11 15:01 被阅读0次

    转:https://zhuanlan.zhihu.com/p/88648401

    前言

    前端目前两个当家花旦框架 VUE React,它们能够流行开来,响应式原理做出了巨大贡献。毕竟,它通过数据的变更就能够更新相应的视图,极大的将我们从繁琐的DOM操作中解放出来。

    所以掌握它们的响应式原理,对掌握前端框架的精髓就很重要了。

    本文用最简单的方式来解释VUE2 最重点的响应式原理,看不懂算我输!

    一、 响应式原理

    什么是响应式原理?

    意思就是在改变数据的时候,视图会跟着更新。这意味着你只需要进行数据的管理,给我们搬砖提供了很大的便利。React也有这种特性,但是React的响应式方式跟VUE完全不同。

    React是通过this.setState去改变数据,然后根据新的数据重新渲染出虚拟DOM,最后通过对比虚拟DOM找到需要更新的节点进行更新。

    也就是说React是依靠着虚拟DOM以及DOM的diff算法做到这一点的。而关于React这方面的文章,我已经写了很多了,还不了解的同学可以自行复习一下

    而VUE则是利用了Object.defineProperty的方法里面的setter 与getter方法的观察者模式来实现。

    所以在学习VUE的响应式原理之前,先学习两个预备知识:
    Object.defineProperty 与 观察者模式。

    如果你已经掌握了,可以直接跳到第三part。

    二、预备知识

    2.1 Object.defineProperty

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

    举个栗子:

    // 在对象中添加一个属性与存取描述符的示例
    var bValue;
    var o = {};
    Object.defineProperty(o, "b", {
      get : function(){
        console.log('监听正在获取b')
        return bValue;
      },
      set : function(newValue){
        console.log('监听正在设置b')
        bValue = newValue;
      },
      enumerable : true,
      configurable : true
    });
    
    o.b = 38;
    console.log(o.b)
    
    

    最终打印

    监听正在设置b
    监听正在获取b
    38
    
    

    从在上述栗子中,可以看到当我们对 o.b 赋值38的时候,就会调用set函数,这时候给bValue赋值,之后我们就可以通过o.b来获取这个值,这时候,get函数被调用。

    掌握到这一步,我们已经可以实现一个极简的VUE双向绑定了。

    <input type="text" id="txt" />
    <span id="sp"></span>
    
    <script>
    var txt = document.getElementById('txt'),
        sp = document.getElementById('sp'),
        obj = {}
    
    // 给对象obj添加msg属性,并设置setter访问器
    Object.defineProperty(obj, 'msg', {
      // 设置 obj.msg  当obj.msg反生改变时set方法将会被调用  
      set: function (newVal) {
        // 当obj.msg被赋值时 同时设置给 input/span
        txt.value = newVal
        sp.innerText = newVal
      }
    })
    
    // 监听文本框的改变 当文本框输入内容时 改变obj.msg
    txt.addEventListener('keyup', function (event) {
      obj.msg = event.target.value
    })
    </script>
    
    

    VUE给data里所有的属性加上set,get这个过程就叫做Reactive化

    2.2 观察者模式

    什么是观察者模式?它分为注册环节跟发布环节

    比如我去买芝士蛋糕,但是店家还没有做出来。这时候我又不想在店外面傻傻等,我就需要隔一段时间来回来问问蛋糕做好没,对于我来说是很麻烦的事情,说不定我就懒得买了。

    店家肯定想要做生意,不想流失我这个吃货客户。于是,在蛋糕没有做好的这段时间,有客户来,他们就让客户把自己的电话留下,这就是观察者模式中的注册环节。然后蛋糕做好之后,一次性通知所有记录了的客户,这就是观察者的发布环节

    这里来简单实现一个观察者模式的类

    function Observer() {
      this.dep = [];
    
      register(fn) {
        this.dep.push(fn)
      }
    
      notify() {
        this.dep.forEach(item => item())
      }
    }
    
    const wantCake = new Oberver();
    // 每来一个顾客就注册一个想执行的函数
    wantCake.register(() => {'console.log("call daisy")'})
    wantCake.register(() => {'console.log("call anny")'})
    wantCake.register(() => {'console.log("call sunny")'})
    
    // 最后蛋糕做好之后,通知所有的客户
    wantCake.notify()
    
    

    三、原理解析

    在学完了前面的铺垫之后,我们终于可以开始讲解VUE的响应式原理了。

    官网用了一张图来表示这个过程,但是刚开始看可能看不懂,等到文章的最后,我们再来看,应该就能看懂了。

    image

    总共分为三步骤:
    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就是一个观察者类,每一个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的桥梁。
    每一个Watcher对应一个vue component。

    这里可以看出new Watcher的时候,constructor 里的this.getter.call(vm, vm)函数会被执行。getter就是updateComponent。这个函数会调用组件的render函数来更新重新渲染。

    而render函数里,会访问data的属性,比如

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

    此时会去调用这个属性blogTitle的getter函数,即:

    // getter函数
    get: function reactiveGetter () {
        ....
        dep.depend()
        return value
        ....
     },
    
    // dep的depend函数
    depend () {
        if (Dep.target) {
          Dep.target.addDep(this)
        }
    }
    
    

    在depend的函数里,Dep.target就是watcher本身(我们在class Watch里讲过,不记得可以往上第三段代码),这里做的事情就是给blogTitle注册了Watcher这个对象。这样每次render一个vue 组件的时候,如果这个组件用到了blogTitle,那么这个组件相对应的Watcher对象都会被注册到blogTitle的Dep中。

    这个过程就叫做依赖收集

    收集完所有依赖blogTitle属性的组件所对应的Watcher之后,当它发生改变的时候,就会去通知Watcher更新关联的组件。

    3、更新阶段:

    当blogTitle 发生改变的时候,就去调用Dep的notify函数,然后通知所有的Watcher调用update函数更新。

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

    可以用一张图来表示:

    image

    由此图我们可以看出Watcher是连接VUE component 跟 data属性的桥梁。

    总结

    最后,我们通过解释官方的图来做个总结。

    image

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

    image

    2、第二步:当data属性发生改变之后,就会遍历sub里所有的watcher对象,通知它们去重新渲染组件。

    整个过程就是那么简单啦~

    彩蛋

    本来这篇文章应该已经结束了,但是我们既然已经学会了响应式原理,那当然要对目前vue的一些规则做点解释啦。算是赠送的彩蛋~。

    • 如果你想要属性是响应式的,就一定要写在data对象里。因为VUE只对data里的属性做reactive化处理。
    var vm = new Vue({
      data:{
        a:1
      }
    })
    
    // `vm.a` 是响应的
    
    vm.b = 2
    // `vm.b` 是非响应的
    

    或者使用Vue.set(vm.someObject, 'b', 2)动态添加。

    参考文档:

    1、https://zhuanlan.zhihu.com/p/67893936
    2、https://www.njleonzhang.com/2018/09/26/vue-reactive.html
    3、https://vuejs.bootcss.com/v2/gu

    相关文章

      网友评论

          本文标题:最简化 VUE的响应式原理

          本文链接:https://www.haomeiwen.com/subject/dqrcbltx.html