从源码理解Vue.js的双向绑定

881 阅读6分钟

写在前面

最近参加春招,面官通常会问一下框架源码的知识,那麽如果面试官问你怎么理解Vue中双向绑定呢?

有句话说的好,知其然知其所以然。从代码底层理解机制能使我们更好的理解框架,以下是我对Vue中双向绑定原理的拙见。既为整合知识,又为以后学习有个参考。

什么叫做双向绑定?

v-model 指令在表单 <input><textarea><select> 元素上创建双向数据绑定。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。->官方文档中的详细描述

简单来说,整个过程通过视图改变数据,数据更新视图。

视图更新数据不难实现,那么如何实现数据更新视图呢? Vue的响应式系统解决这一问题。

当一个Vue实例被创建时,它向Vue的响应式系统中加入其data对象中能找到的所有的属性。当数据发生改变时,视图就会产生“响应”,即匹配更新为新的值。

源码解析

本文Vue版本为2.6.11,Vue3.0利用了proxy来替换Obejct.defineProperty来实现对data中变量赋予get/set属性,笔者不是很了解,所以这里本文只讨论Vue2.0。

Object类

首先我们知道Vue实例创建的过程中有一个生命周期,其中created阶段就是会初始化datamethods

首先在vue/src/core/instance/state.jsinitState初始化函数中

会判断当前实例是否有data属性,如果有则进入initData()方法中,否则生成一个空对象data。接下来进入到initData()方法中,首先判断data属性的类型,如果是‘function’那么会执行getData()方法将获取data函数并遍历data里面的所有属性,initData()方法的源码定义如下:

vue/src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
 ····//some code
  // observe data
  observe(data, true /* asRootData */)
}

在最后将执行observe()方法监听data中的所有属性。observe()方法源码如下:

vue/src/core/observe/index.js

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

首先observe方法会判断每个value是否有'__ob__'属性,它是一个Observe对象的实例。如果有就直接使用,否则会根据一些条件(是否是数组或对象,可拓展,非Vue组件等),创建一个Observe实例对象。然后再来看Observe对象的源码

export class Observer {
  ···//some code
  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)	//	给对象添加set和get属性以达到监听的目的
    }
  }
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

首先会判断value是否是数组,如果是则使用observeArray()递归遍历,最终都会执行walk()方法,并执行最重要的defineReactive()方法。这个方法会利用Object.defineProperty()方法给每一value设置get函数和set函数以达到数据监听的功能,这里贴出具体的Object.defineProperty()方法实现,源码如下:

export function defineReactive (
  ····//some code
  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()	//数据更新时通知dep更新对应的依赖
    }
  })
}

get函数中执行dep.depend()进行依赖收集,以便更快的查找到发生改变的数据。在set函数中,当数据发生改变时就会调用dep.notify()函数会遍历所有的订阅Watcher,调用它们的updata方法,这样就达到了给data对象添加Observe并监听数据的过程。

Watcher类


上面说到dep.depend()dep.notify(),其实这个dep实例是一个Dep类,这个类就是用来订阅所有的Watcher并在Observe监听到数据变化时通知Watcher的,具体的源码如下:

depend () {
    if (Dep.target) {
      Dep.target.addDep(this)	//添加依赖
    }
  }

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

这里顺便提一下,Watcher对象的updata方法,这里贴上updata()源码:

 update () {
     // 默认情况下都会进入else的分支,异步则直接调用watcher的run方法
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()	//通过新的虚拟DOM和老的虚拟DOM进行前后diff算法对比以得到新的DOM
    } else {
      queueWatcher(this)	//推入队列
    }
  }

其中有一个queueWatcher()方法,会将Watcher实例推入队列,通过nextTick方法在下一个事件循环周期处理Watcher队列,这样做是为了减少多次连续更新DOM减少DOM操作的一种性能优化手段。在queueWatcher()中使用nextTick(flushSchedulerQueue)执行异步操作,flushSchedulerQueue()是一个函数为了调用queue中所有watcherrun方法,然后更新DOM。此时nextTick()使得此过程为异步更新队列,更多关于nextTick的原理(这里涉及Vue处理绑定事件的原理)请查找其他相关文章。

总结Watcher的作用就是收到Dep的通知,然后异步去更新DOM。

写到最后

总结一下:vue中ObserveObjcet.defineProperty()data属性添加setget方法,通过get方法中的dep.depend收集依赖并监听数据,当数据发生改变时触发set函数中的dep.notify()通知对应的Watcher实例,再调用watcher.updata进行更新视图,这就是Vue中的响应式系统。而Vue的双向绑定就是基于这一过程实现的:input绑定v-model事件,当输入数据之后,就做到了数据更新(第一步,视图更新数据)。监听到data中属性数据改变后,触发属性set方法然后通过Watcher实例的updata方法更新视图(第二步,数据更新视图),那麽这样就做到了数据的双向绑定,get it!

我是集唱跳rap于一身的前端程序猿Tzyito,关注我,教你打(shuo)篮(sao)球(hua)。 欢迎您留言提供宝贵的意见以及纠正博客中的