美文网首页
vue源码-深入响应式原理

vue源码-深入响应式原理

作者: cd2001cjm | 来源:发表于2019-12-16 16:46 被阅读0次

    前言

    随着前后端分离成为Web开发的常态,Mvvm框架越来越普及。让前端开发从关注Dom,变为关注数据,提高了开发效率,降低了学习成本。同时也能有效避免低级的Dom操作错误。

    在享受Mvvm框架带来的便利的同时,我们也会对它的具体实现产生兴趣。笔者认为Mvvm框架重要的有两个部分

    1. 数据变化的捕获,通知与响应

    2. vDom对通知产生响应,并对Dom进行相应的操作

    今天我们先来看一下变化的捕获,通知与响应,分为下面四个部分

    1. 数据变化的捕获

    2. 监听器的创建

    3. 数据变化与监听器的关联

    4. 变化的响应

    一,数据变化的捕获

    日常项目中,我们常用的与数据变化相关的,有以下三个:

    • Data: 包括定义数据模型设置的初始值,Prop传递的值

    • Watch:监听某一个值的变化进行后续业务处理

    • Computed:页面展示的值是多个值的组合变化

    image.png

    除了上述三个,其实vue框架本身,还有一个组件层面的变化,比如路由变化会重新渲染组件。虽然都是变化,但这四者既有联系也有区别,关系如下图

    image

    数据变化会触发监听器,会触发组件渲染,而组件渲染的时候,会重新计算属性。那么该如何监听数据变化呢?

    监听数据变化
    JavaScript中监听数据变化API:Getter和Setter,先看一个简单的示例:

    var user = {}
    var name;
    Object.defineProperty(user, 'current', {
      get: function(){
          console.log('获取名称')
          return name
      },
      set:function(val){
          console.log('设置名称')
          name = val
      }
    })
    
    user.current = '张三';
    console.log(user.current);
    

    控制台输出:

    设置名称

    获取名称

    张三

    API getter和setter就是数据劫持的基础,通过这个例子我们看到,在设置数据或获取数据的时候,我们都可以加入自己的处理逻辑。从而达到我们监听数据变化的目的。接下来我们再对比一下vue的代码实现。

    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          if (Dep.target) {
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          const value = getter ? getter.call(obj) : val
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          if (process.env.NODE_ENV !== 'production' && customSetter) {
            customSetter()
          }
          // #7981: for accessor properties without setter
          if (getter && !setter) return
          if (setter) {
            setter.call(obj, newVal)
          } else {
            val = newVal
          }
          childOb = !shallow && observe(newVal)
          dep.notify()
        }
      })
    

    同样的,Vue也是通过这种方式劫持数据,然后拦截到变化后,去通知订阅者。Vue捕获变化并发送通知的流程图如下(放大查看):

    image.png
    • 当监听到数据变化时,我们通过Watcher来发送通知

    • 当获取数据的时候,我们把Watcher加入到通知列表

      什么是Watcher呢?

    二,监听器的创建

    Watcher的分类

    image

    Watcher什么时候创建的呢?先看下面这段熟悉的代码:

    new Vue({
     el: '#app',
     router, 
     components: { App }, 
     template: '<App/>’
    })
    

    上面这块代码简单来说就是创建了一个Vue的实例。声明了一个组件App,渲染绑定的节点是#app,还有路由。

    Vue实例化的处理流程(本文无关的部分略过)。

    image

    和变化相关的有三个方法:

    • InitRender阶段,绑定组件的渲染方法

    • InitState阶段,创建数据模型,监听器,计算属性的Watcher

    • $mount阶段,创建组件Watcher

    我们分别看一下三种监听器的创建

    监听器Watcher

    Vue.prototype.$watch = function (
      expOrFn: string | Function,
      cb: any,
      options?: Object
    ): Function {
      const vm: Component = this
      if (isPlainObject(cb)) {
        return createWatcher(vm, expOrFn, cb, options)
      }
      options = options || {}
      options.user = true
      const watcher = new Watcher(vm, expOrFn, cb, options)
      if (options.immediate) {
        try {
          cb.call(vm, watcher.value)
        } catch (error) {
          handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
        }
      }
      return function unwatchFn () {
        watcher.teardown()
      }
    }
    

    注意这一行:const watcher = new Watcher(vm, expOrFn, cb, options)

    $watch方法也是Vue对外提供的API

    var vm = new Vue({
      el: '#demo',
      data: {
        firstName: 'Foo',
        lastName: 'Bar',
        fullName: 'Foo Bar'
      },
      watch: {
        firstName: function (val) {
          this.fullName = val + ' ' + this.lastName
        },
        lastName: function (val) {
          this.fullName = this.firstName + ' ' + val
        }
      }
    })
    

    我们结合Vue官网watch例子来看new Wacher的参数:

    vm:Vue实例本身

    expOrFn:firstName

    cb:对应的函数

    option:额外的参数,从上面我们也看到,有个immediate属性,如果为true就先调用一次

    计算属性Watcher

    计算属性也是和上面类似,但有个重要的参数,lazy为true。这就代表着,在创建的时候,并不会立即执行。

    const computedWatcherOptions = { lazy: true }
    function initComputed (vm: Component, computed: Object) {
      ……略
      for (const key in computed) {
         ……略
    
        if (!isSSR) {
          // create internal watcher for the computed property.
          watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
        }
         ……略
      }
     ……略
    }
    
    function createComputedGetter (key) {
      return function computedGetter () {
        const watcher = this._computedWatchers && this._computedWatchers[key]
        if (watcher) {
          if (watcher.dirty) {
            watcher.evaluate()
          }
          if (Dep.target) {
            watcher.depend()
          }
          return watcher.value
        }
      }
    }
    

    而调用的时机是在渲染组件的时候触发,然后watcher.evaluate()

    组件Watcher

    
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      ………略
      let updateComponent
      if (process.env.NODE_ENV !== ‘production’ && config.performance && mark) {
        updateComponent = () => {
          ………略
        }
      } else {
        updateComponent = () => {
          vm._update(vm._render(), hydrating)
        }
      }
      new Watcher(vm, updateComponent, noop, {
        before () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */)
      ………略
      return vm
    }
    

    当组件发生变化的时候就会触发vm._update(vm._render(), hydrating),在创建的时候也会执行一次,进行第一次渲染。具体见下图:

    image.png
    1. Init方法中,会进行各种Watcher的创建

    2. $mount中会创建组件Watcher并执行

    3. 组件Watcher触发渲染

    4. 渲染过程发现有子组件,对子组件再走一遍上面的流程

    注意:上面我们说了三种Watcher的创建,计算属性的Watcher不会立即执行,而其他两个都会立即执行一次。

    三,数据变化与监听器的关联

    到目前为止,我们解决了变化的监听,以及观察者的创建,那么两者又是如何联系起来的呢?
    再来看一下数据劫持的getter方法,我们发现只有在Dep.target(Watcher)存在的时候才建立关联

    get: function reactiveGetter () {
        const value = getter ? getter.call(obj) : val
        if (Dep.target) {
          dep.depend()
          if (childOb) {
            childOb.dep.depend()
            if (Array.isArray(value)) {
              dependArray(value)
            }
          }
        }
        return value
      },
    

    再看一下Watcher类的get方法(这个就是一个普通的方法名称,不要和getter混淆)

    get () {
      pushTarget(this)
      let value
      const vm = this.vm
      try {
        value = this.getter.call(vm, vm)
      } catch (e) {
        if (this.user) {
          handleError(e, vm, `getter for watcher "${this.expression}"`)
        } else {
          throw e
        }
      } finally {
        if (this.deep) {
          traverse(value)
        }
        popTarget()
        this.cleanupDeps()
      }
      return value
    }
    

    pushTarget(this):将当前watcher压入栈,同时将this赋值给Dep.target

    popTarget():出栈

    哪这个栈又是个什么东东呢?

    Watcher Stack
    组件是按树形的结构递归解析。如果不考虑出栈的情况,那么整个栈的情况如图所示:

    image

    而在实际过程中当关联设置结束后,会进行出栈操作。整个解析过程按照从根节点到子节点,也就是监听先压入栈,然后解析的时候发现栈里有监听,就会绑定。

    是不是有点乱?没关系,我们再捋一遍。

    1. new Watcher的时候调用其内部get方法,在这个方法中会将当前监听压入栈,并赋值给target。

    2. 继续向下执行,解析组件时第一次必然获取数据,这个时候就会触发数据劫持的getter,在getter里判断当前target是否有值,有值就把当前数据和Watcher进行关联,没有就忽略继续向下

    3. 出栈并清空target

    结合上面的文字,再具体看一下这三个Watcher关联的流程

    组件Watcher关联

    image.png

    监听器Watcher关联

    image.png

    组件Watcher和监听器Watcher的区别,是组件Watcher要进行渲染。这当然也比较好理解,监听的目的,归根结底是要渲染到页面用户才能看到变化。比如vue-router,就是利用组件Watcher进行的触发。

    计算属性Watcher关联

    计算属性和前面两个不同,它在创建watcher的时候,并不会触发get。

    在初始化的时候创建好Watcher,渲染的时候才会触发,同时把组件Watcher也追加进订阅

    image

    四,变化的响应

    变化劫持,通知Watcher,Watcher响应具体的动作。这部分内容相对就比较简单了。唯一需要注意的是,计算属性因为没有入栈,所以它的响应会被丢弃。

      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true //不执行具体动作
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    

    通过代码可以看到,当是lazy的时候,设置dirty=true,但并没有进行具体的操作。

    我们最后再整体回顾一下开始的关系图:

    image

    结语

    数据响应式可以说是Mvvm框架的精髓,希望通过本文的描述,可以让大家更好的理解它的实现原理,只是通过文章,依然不能完全的描述透彻,细节部分还是需要去阅读源码,对照分析和研究。前端水越来越深,一起共勉。本文都是作者自己的理解,有不当之处欢迎批评指正。关于vDom的渲染部分,会在下篇文章中分享。


    image.png

    相关文章

      网友评论

          本文标题:vue源码-深入响应式原理

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