zoukankan      html  css  js  c++  java
  • vue响应式原理

      vue是一个轻量级、数据驱动的渐进式框架,其核心就是数据驱动。一直以来,对vue响应式原理的理解还是停留在利用Object.defineProperty中的get和set进行数据劫持,至于内部是如何运转的,并没有一个全面的认知。本文就简要概述下,vue响应式原理具体的实现。

      就一般而言,props、data、computed、watch四个模块构建成vue数据处理系统的骨架,绝大部分的数据声明、转换、传递,都是通过这几个模块进行。盗用vue.js官网的一张图,vue的数据流如下图所示:

      在说数据流之前,我们先明确一点,Object.defineProperty(obj, prop, descriptor)主要作用是声明对象的属性,obj是对象名称,prop是对象的键值,descriptor是属性的数据描述符和存取描述符,其中属性的存取描述符就是上图中的getter/setter,getter触发条件是取,setter的触发条件是存。如下述代码,

    let o ={}
    let bValue;
    Object.defineProperty(o, "b", {
      get : function(){
        console.log('执行了取操作')
        return bValue;
      },
      set : function(newValue){
        console.log('执行了存操作')
        bValue = newValue;
      },
      enumerable : true,
      configurable : true
    });
    
    o.b = 1; //
    
    console.log(o.b) //
    console.log(bValue) // o.b === bValue

      在vue中,声明响应式属性用的方法和上述大致类似,只是中间加了许多业务处理逻辑。先简要概述下vue数据流,主要分为以下两步:

      1、通过getter收集依赖

         什么是依赖?依赖可以理解为一个观察者对象,里面包含数据必须的属性(比如属性作用域对象)、方法(比如watch的回调)、字符串表达式(比如渲染函数)等,简单来说,观察者对象Watcher的主要作用就是数据与视图的粘合剂,通过发布-订阅模式,响应式的进行视图渲染。

        为什么在getter中能够收集到依赖信息呢?首先,视图要想渲染数据,肯定要有对数据的取操作,取值时,会触发对数据的取操作,此时,可以将依赖的数据信息添加到属性的订阅者列表中,这样后续数据如果有变化,就可以从数据的订阅者列表中,依次执行对应的处理逻辑。

      2、通过setter派发更新

          setter是承接getter的作用的。在第一次数据渲染时,会通过getter,收集到属性的所有订阅者信息,在给属性赋值时,会触发属性的setter方法,在setter里,会遍历订阅者列表,根据订阅者的表达式,回调,处理对应的逻辑方法,实现数据的响应式更新。setter在响应式系统中,更多的可能是承接一个启后的角色。

      在vue.js中,进行响应式数据声明的代码如下所示:

    export function defineReactive (
      obj: Object,
      key: string,
      val: any,
      customSetter?: ?Function,
      shallow?: boolean
    ) {
      // 初始化一个发布-订阅模型,每个对象都包含一个dep实例
      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
      const setter = property && property.set
      // 处理obj的值
      if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
      }
      // 如果val值存在Object,则需要侦听val值的变化
      let childOb = !shallow && observe(val)
      Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: function reactiveGetter () {
          const value = getter ? getter.call(obj) : val
          // 每次取值时,如果存在依赖的target对象,将watcher对象添加到订阅列表中,当数据发生改变时,通知watcher进行更新
          if (Dep.target) {
            // 将watcher加入到订阅列表
            dep.depend()
            if (childOb) {
              childOb.dep.depend()
              if (Array.isArray(value)) {
                dependArray(value)
              }
            }
          }
          return value
        },
        set: function reactiveSetter (newVal) {
          // 派发更新
          // 获取到value数据
          const value = getter ? getter.call(obj) : val
          /* eslint-disable no-self-compare */
          if (newVal === value || (newVal !== newVal && value !== value)) {
            return
          }
          /* eslint-enable no-self-compare */
          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()
        }
      })
    }

        上述代码量比较多,以defineReactive方法响应式声明属性为中心,往前往后,分析下整个依赖收集与派发更新的过程。在开始之前,可以参考【vue实例化过程】,来熟悉下vue实例化的大致流程。

      1、依赖收集

        依赖收集中,需要重点关注的是dep.depend方法,在说dep.depend之前,需要关注上面有个判断条件,Dep.target,只有存在Dep.target时,才能将观察者对象添加到订阅列表中。以实例化Vue为例,在实例化组件时,会实例化一个watcher,watcher中包含组件挂载的相关信息。其中比较重要的一环就是会调用Watcher的get方法,get方法具体执行逻辑如下:

    get () {
        // 设置当前处理的Watcher
        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 {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          // 深度监听
          if (this.deep) {
            traverse(value)
          }
          popTarget()
          this.cleanupDeps()
        }
        return value
      }

      其中,this.getter是指Watcher的一个表达式,在组件挂载中是指方法mountComponent,此方法主要作用是根据render函数生成vnode,根据vnode转换成真实的DOM节点;在watch中,this.getter是指根据属性名获取属性值的一个方法。在代码的第一句,有个pushTarget操作,这一步就是为后面get的依赖收集服务的,是将当前的Watcher实例,挂载到Dep上,表明当前正在处理的Watcher,在this.getter中,往往会伴随着对数据的取操作。在getter调用完毕后,会有一个popTarget,执行完当前的Watcher后,会清除对当前Watcher的依赖,pushTarget/popTarget的实现过程如下所示,在一个时间点,只能有一个被执行的Watcher:

    // The current target watcher being evaluated.
    // This is globally unique because only one watcher
    // can be evaluated at a time.
    Dep.target = null
    const targetStack = []
    // 进栈
    export function pushTarget (target: ?Watcher) {
      targetStack.push(target)
      Dep.target = target
    }
    // 出栈
    export function popTarget () {
      targetStack.pop()
      Dep.target = targetStack[targetStack.length - 1]
    }

       2、派发更新

        在我们对响应式数据更改时,会执行属性声明时的set方法,在set方法里,有一个dep.notify操作,主要就是遍历执行属性的订阅者列表Watcher的update方法,dep.notify执行的方法如下:

    notify () {
        // stabilize the subscriber list first
        const subs = this.subs.slice()
        if (process.env.NODE_ENV !== 'production' && !config.async) {
          // subs aren't sorted in scheduler if not running async
          // we need to sort them now to make sure they fire in correct
          // order
          subs.sort((a, b) => a.id - b.id)
        }
        for (let i = 0, l = subs.length; i < l; i++) {
          // 执行订阅对象批量更新???@todo
          // 每个subs都有update方法
          subs[i].update()
        }
      }

      更新执行操作比较多,但是需要注意的点是,视图和响应式回调更新的时机,当我们改变数据时,直接获取DOM中绑定的数据,可能还是原先的数据,视图并没有随之更新。如果想要在数据渲染完成后再进行回调处理,通常使用this.$nextTick。在派发更新时,会调用nextTick方法,传入更新数据的回调,nextTick在一般情况下是一个微任务,在支持promise的浏览器中是在promise中处理回调。我们都知道,在浏览器中,代码一般的执行顺序是 同步任务>微任务>宏任务。微任务常用的就是promise.then,宏任务常用的setTimeout。所以,捕获视图更新的回调,一般在setTimeout里也是可行的。微任务、宏任务与同步任务的处理流程可以参考下面代码:

    console.log('开始执行测试代码')
    let p = Promise.resolve()
    p.then(function () {
      console.log('执行了微任务')
    })
    
    setTimeout(function () {
      console.log('执行了宏任务')
    })
    
    console.log('执行结束')

      代码的执行顺序如下图所示:

      派发更新操作,到最后都会执行到Watcher的run方法,最终都会执行到Watcher的getter,继续走页面的渲染流程。其实vue响应式原理核心就是利用Object.defineProperty进行数据劫持,对引用的数据进行依赖收集,数据改变时在进行派发更新消息。Watcher中存储的是数据依赖更新的消息。

      附Watcher.js中代码:

    export default class Watcher {
      vm: Component;
      expression: string;
      cb: Function;
      id: number;
      deep: boolean;
      user: boolean;
      lazy: boolean;
      sync: boolean;
      dirty: boolean;
      active: boolean;
      deps: Array<Dep>;
      newDeps: Array<Dep>;
      depIds: SimpleSet;
      newDepIds: SimpleSet;
      before: ?Function;
      getter: Function;
      value: any;
    
      constructor (
        vm: Component,
        expOrFn: string | Function,
        cb: Function,
        options?: ?Object,
        isRenderWatcher?: boolean
      ) {
        debugger
        this.vm = vm
        // 渲染组件的watcher,通常computed 和 watch 也会实例化watcher
        if (isRenderWatcher) {
          vm._watcher = this
        }
        // 组件上的watcher队列
        vm._watchers.push(this)
        // options
        if (options) {
          this.deep = !!options.deep
          this.user = !!options.user
          this.lazy = !!options.lazy
          this.sync = !!options.sync
          this.before = options.before
        } else {
          this.deep = this.user = this.lazy = this.sync = false
        }
        this.cb = cb
        this.id = ++uid // uid for batching
        this.active = true
        this.dirty = this.lazy // for lazy watchers
        this.deps = []
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        // updateComponents, watch, computed callback function
        this.expression = process.env.NODE_ENV !== 'production'
          ? expOrFn.toString()
          : ''
        // parse expression for getter
        // expression是function的情况:computed,mountComponent
        if (typeof expOrFn === 'function') {
          this.getter = expOrFn
        } else {
          // watch走这个分支,watch传入的expression是字符串
          // 将字符串按照.进行字符串分割解析,exp: 'a.b.c' 获取到具体对象的值
          this.getter = parsePath(expOrFn)
          if (!this.getter) {
            this.getter = noop
            process.env.NODE_ENV !== 'production' && warn(
              `Failed watching path: "${expOrFn}" ` +
              'Watcher only accepts simple dot-delimited paths. ' +
              'For full control, use a function instead.',
              vm
            )
          }
        }
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      /**
       * Evaluate the getter, and re-collect dependencies.
       * 执行更新组件的方法,先将当前要执行的watcher推入到执行队列
       */
      get () {
        // 设置当前处理的Watcher
        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 {
          // "touch" every property so they are all tracked as
          // dependencies for deep watching
          // 深度监听
          if (this.deep) {
            traverse(value)
          }
          popTarget()
          this.cleanupDeps()
        }
        return value
      }
    
      /**
       * Add a dependency to this directive.
       * 添加依赖
       */
      addDep (dep: Dep) {
        const id = dep.id
        // 防止数据的重复订阅
        if (!this.newDepIds.has(id)) {
          this.newDepIds.add(id)
          this.newDeps.push(dep)
          if (!this.depIds.has(id)) {
            dep.addSub(this)
          }
        }
      }
    
      /**
       * Clean up for dependency collection.
       */
      cleanupDeps () {
        let i = this.deps.length
        while (i--) {
          const dep = this.deps[i]
          if (!this.newDepIds.has(dep.id)) {
            dep.removeSub(this)
          }
        }
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
      }
    
      /**
       * Subscriber interface.
       * Will be called when a dependency changes.
       * 依赖改变,会触发watcher的update方法
       */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          this.run()
        } else {
          queueWatcher(this)
        }
      }
    
      /**
       * Scheduler job interface.
       * Will be called by the scheduler.
       */
      run () {
        if (this.active) {
          const value = this.get()
          if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
          ) {
            // set new value
            const oldValue = this.value
            this.value = value
            if (this.user) {
              try {
                this.cb.call(this.vm, value, oldValue)
              } catch (e) {
                handleError(e, this.vm, `callback for watcher "${this.expression}"`)
              }
            } else {
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      }
    
      /**
       * Evaluate the value of the watcher.
       * This only gets called for lazy watchers.
       */
      evaluate () {
        this.value = this.get()
        this.dirty = false
      }
    
      /**
       * Depend on all deps collected by this watcher.
       */
      depend () {
        let i = this.deps.length
        while (i--) {
          this.deps[i].depend()
        }
      }
    
      /**
       * Remove self from all dependencies' subscriber list.
       * 移除事件订阅
       */
      teardown () {
        if (this.active) {
          // remove self from vm's watcher list
          // this is a somewhat expensive operation so we skip it
          // if the vm is being destroyed.
          if (!this.vm._isBeingDestroyed) {
            remove(this.vm._watchers, this)
          }
          let i = this.deps.length
          while (i--) {
            this.deps[i].removeSub(this)
          }
          this.active = false
        }
      }
    }
  • 相关阅读:
    JavaScript--截取字符串
    C#--Dictionary字典学习
    根据条件把A表数据更新到B表中一个字段中(查询更新)
    zip多文件的压缩下载和解压
    C# 创建一个新的DataTable,并根据查询的DataTable结果进行重新赋值
    C# string.formate() 中 如何使用string 循环出来的一串字符
    asp.net mvc 之jqgird 列表数据 根据字典显示中文
    asp.net mvc 之显示24小时折线对比数据
    hightchart扇形图asp.net mvc 实现
    hightchart单个柱状图实现之asp.net mvc
  • 原文地址:https://www.cnblogs.com/gerry2019/p/12168481.html
Copyright © 2011-2022 走看看