美文网首页
vue 响应式原理

vue 响应式原理

作者: sunshineLWZL | 来源:发表于2019-04-02 11:56 被阅读0次

    vue 版本2.5.17

    Object.defineProperty

      Object.defineProperty(obj,prop, descriptor)
    // obj 是定义属性的对象,prop 是属性名称,descriptor 是属性描述符,核心点就是 descriptor,vue 主要就是利用 descriptor 的 get 和 set 做响应式处理
    

    依赖收集

    const dep = new Dep()
    dep.depend()
    

    在 get 方法中创建一个 dep 对象,通过 depend()做依赖收集,Dep 是一个 class,有一个静态属性 target,是一个全局唯一 Watcher,保证同一时间只能有一个全局的 Watcher 被计算,Dep 可以理解为是用来管理 Watcher 的

    Watcher

    Watcher 是一个 class,定义了一些与 Dep 相关的属性,保存 Wathcer 实例持有的 Dep 实例的数组

    过程

    知道了在访问数据对象的时候会触发 get 方法收集依赖,那 get 方法是在什么时候触发的呢?这一切需要从 组件mount 开始
    在 mountComponent中有一段主要逻辑代码

    updateComponent = () => {
      vm._update(vm._render(), htdrating
    }
    new Watcher(vm, updateComponent, noop, null, true)
    

    在 mount 的时候会实例化一个 渲染watcher,先执行 Watcher 的构造函数,然后调用this.get()方法,进入get 函数,首先会执行:

    pushTarget(this)
    

    pushTarget的定义在 dep.js 中,定义如下:

    export function pushTarget (_target: Wathcer) {
      if (Dep.target) targetStack.push(Dep.target)
      Dep.target = _target
    }
    

    实际上就是把 Dep.target复制为当前的渲染 watcher 并压栈(恢复的时候用),接着又执行了:

      value = this.getter.call(vm, vm)
    

    this.getter 对应的就是 updateComponent函数,实际上就是执行

    vm._update(vm._render(), hydrating)
    

    会执行 vm._render()方法,这个方法会生成渲染 VNode,并且在这个过程中会访问 vm 上的数据,这个时候就触发了数据对象的 getter,每个对象的 getter 都有一个 dep,在触发 getter 的时候调用 dep.depend()方法,接着调用 Dep.target.addDep(this),即渲染 watcher 的 addDep 方法,定义如下:

    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)
        }
      }
    }
    

    派发更新

    在 defineProperty的 set中有两个关键点,一个是 childOb = !shaoolw && observe(newVal), 将新值变成一个响应式对象;另一个是 dep.notify(),通知所有的订阅者。

    过程分析

    当对组件响应式数据做了修改时,就会触发 setter,调用 dep.notify(),notify()方法在 dep.js 中,定义如下

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

    变量所有的 subs,即 Watcher 的实例数组,然后调用每一个 watcher 实例的 update 方法,update 方法定义在 watcher.js 中:

    update () {
      if (this.lazy) {
        this.dirty = true
      } else if (this.sync) {
        this.run()
      } else {
        queueWatcher(this)
      }
    }
    

    这里针对 Watcher的不同状态,会执行不同的逻辑,在渲染中会执行到 queueWatcher,定义在 scheduler.js:

    const queue: Array<Watcher> = []
    const activatedChildren: Array<Component> = []
    let has: { [key: number]: ?true } = {}
    let circular: { [key: number]: number } = {}
    /**
     * Push a watcher into the watcher queue.
     * Jobs with duplicate IDs will be skipped unless it's
     * pushed when the queue is being flushed.
     */
    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
      if (has[id] == null) {
        has[id] = true
        if (!flushing) {
          queue.push(watcher)
        } else {
          // if already flushing, splice the watcher based on its id
          // if already past its id, it will be run next immediately.
          let i = queue.length - 1
          while (i > index && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(i + 1, 0, watcher)
        }
        // queue the flush
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue)
        }
      }
    }
    

    同一个 watcher 只会触发一次,不是每次数据改变都会触发 watcher 的回调,而是将 watcher 添加到一个队列,在 nextTick 中执行 flushSchedulerQueue,定义如下:

    function flushSchedulerQueue () {
      flushing = true
      let watcher, id
    
      // Sort queue before flush.
      // This ensures that:
      // 1. Components are updated from parent to child. (because parent is always
      //    created before the child)
      // 2. A component's user watchers are run before its render watcher (because
      //    user watchers are created before the render watcher)
      // 3. If a component is destroyed during a parent component's watcher run,
      //    its watchers can be skipped.
      queue.sort((a, b) => a.id - b.id)
    
      // do not cache length because more watchers might be pushed
      // as we run existing watchers
      for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        id = watcher.id
        has[id] = null
        watcher.run()
        // in dev build, check and stop circular updates.
        if (process.env.NODE_ENV !== 'production' && has[id] != null) {
          circular[id] = (circular[id] || 0) + 1
          if (circular[id] > MAX_UPDATE_COUNT) {
            warn(
              'You may have an infinite update loop ' + (
                watcher.user
                  ? `in watcher with expression "${watcher.expression}"`
                  : `in a component render function.`
              ),
              watcher.vm
            )
            break
          }
        }
      }
    
      // keep copies of post queues before resetting state
      const activatedQueue = activatedChildren.slice()
      const updatedQueue = queue.slice()
    
      resetSchedulerState()
    
      // call component updated and activated hooks
      callActivatedHooks(activatedQueue)
      callUpdatedHooks(updatedQueue)
    
      // devtool hook
      /* istanbul ignore if */
      if (devtools && config.devtools) {
        devtools.emit('flush')
      }
    }
    

    首先会依据 id 从小到大排序,主要是确保:1.组件的更新由父到子,2.用户自定义的 watcher 优先于渲染 watcher 执行 3.如果一个组件在父组件的 watcher 执行期间被销毁,那对应的 watcher 不需要执行
    然后遍历队列,执行 watcher.run()。有一个细节就是在遍历的时候会堆 queue.length 求值,因为在执行 watcher.run 的时候,可能会有新的 watcher 添加进来,新增的 watcher 会依据 id 添加到队列,在下一次循环中立即执行,状态恢复,情况队列。
    接下来看 watcher.run 的逻辑

    /**
       * 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)
            }
          }
        }
      }
    

    首先调用 get 方法,拿到当前的值,然后判读是否满足新旧值不等,新值是对象类型、deep 模式任何一个条件,则执行 watcher 的回调,对于渲染 watcher 而言,在执行 this.get()方法的时候会调用 getter 方法,即:

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

    所以这就是当我们修改响应式数据触发组件重新渲染的原因。

    • 问题:数组是怎么收集依赖的?
      回答:在defineProperty 中有一段代码 如下
    let childOb = !shallow && observe(val)
    if (childOb) {
      childOb.dep.depend()
      if (Array.isArray(value)) {
        dependArray(value)
      }
    }
    

    在 Observer的构造函数中有如下代码

    if (Array.isArray(value)) {
          const augment = hasProto
            ? protoAugment
            : copyAugment
          augment(value, arrayMethods, arrayKeys)
          this.observeArray(value)
        } else {
          this.walk(value)
        }
    
    /**
     * Collect dependencies on array elements when the array is touched, since
     * we cannot intercept array element access like property getters.
     */
    function dependArray (value: Array<any>) {
      for (let e, i = 0, l = value.length; i < l; i++) {
        e = value[i]
        e && e.__ob__ && e.__ob__.dep.depend()
        if (Array.isArray(e)) {
          dependArray(e)
        }
      }
    }
    

    如果 val 是数组的话,调用 observe(val)方法将数组变成响应式的,实际上就是给数组添加一个'ob'属性,把该属性值设置为该 Observe的实例,用于标识该数组是一个响应式的数组,并通过数组遍历,将数组里面的每一个元素都变成响应式的,调用 childOb.depend.depend()收集该数组依赖,并且调用 dependArray()遍历收集该数组中每个元素的依赖。在 array.js 中,将改变数组的7个方法做了劫持重写,在调用这些修改数组的方法时,会触发 ob.dep.notify()方法,在之前收集了数组的依赖,所以会在 notify 的时候触发更新,另外对于新增的元素,可以通过ob属性,调用 Observe 的 observeArray 方法,将新增的属性变成响应式的。

    相关文章

      网友评论

          本文标题:vue 响应式原理

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