Vue源码解析系列(三) -- 响应式系统的依赖收集与视图更新

672 阅读2分钟

继上一章Vue源码解析系列(二) -- 响应式系统内部是怎么运行的,我们了解了响应式系统是怎么构建以及运行的,上一章遗留的问题就是,那么整个机制是怎么更新视图的,这一章就着重讲两个点:

  • 响应式系统如何收集依赖
  • 响应式系统如何更新视图 我们知道通过Object.defineProperty做了数据劫持,当数据改变的时候,get方法收集依赖,进而set方法调用dep.notify方法去通知Watcher调用本身update方法去更新视图。那么我们抛开其他问题,就讨论getnotifyupdate等方法,直接上代码:

get( )

  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
    }

我们知道Dep.target在创建Watcher的时候是null,并且它只是起到一个标记的作用,当我们创建Watcher实例的时候,我们的Dep.target就会被赋值到Watcher实例,进而放入target栈中,我们这里调用的是pushTarget函数:

// 将watcher实例赋值给Dep.target,用于依赖收集。同时将该实例存入target栈中
export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

那我们继续执行到if (Dep.target)语句的时候就会调用Dep.depend函数:

 // 将自身加入到全局的watcher中
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

那下面的childOb是啥东西呢?

  let childOb = !shallow && observe(val)

我们通过这个变量判断当前属性下面是否还有ob属性,如果有的话继续调用Dep.depend函数,没有的话则不处理。 我们还需要处理当前传入的value类型,是数组属性的话则会调用dependArray收集数组依赖

// 收集数组依赖
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)
  }
}
}

那么收集依赖部分到这里就完了现在进行下一步触发更新

set( )

   set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 判断NaN的情况
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }

我们看到了下面的 set函数触发了dep.notify()方法

notify( )

  // 通知所有订阅者
  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

notify里面我们就做了一件事情,遍历subs数组里面的所有Watcher,逐一调用update方法,也就是我们说的通知所有的订阅者Watcher调用自身update方法 update( )

  update () {
    if (this.lazy) {
      // 计算属性会进来这段代码块
      // 这里将dirty赋值为true
      // 也不会马上去读取值
      // 当render-watcher的update被触发时
      // 重新渲染页面,计算属性会重新读值
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

那么update方法实现了什么呢?lazydirtysync又是啥?

   if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
     // 这里将lazy的值赋值给了dirty
    // 就是说实例化的时候dirty = lazy = true
    this.dirty = this.lazy // for lazy watchers

那是控制计算属性的,当render—watcher的方法update被调用的时候,this.dirty会变为true会重新计算computed值,渲染视图,我们这里不叙述。 那么我们直接看queueWatcher()函数:

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

我们可以看到一个更新队列,更新队列指向:

function callUpdatedHooks (queue) {
  let i = queue.length
  while (i--) {
    const watcher = queue[i]
    const vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }
}

我们的callback调用updated钩子 讲到这里就有点超纲了,咱们初始化渲染会调用一个initRender函数创建dom,还有上面所述的nextTick,后期都会讲,那么了解了更新机制,下一章我们就来实现一个让面试官都惊呆了双向绑定Vue源码解析系列(四) -- 来实现一个双向绑定吧(吊打面试官)