小白如何看懂vue源码-理解数据双向绑定的实现

120 阅读7分钟

如果你是一名前端从业者,并且在简历上写了会使用vue框架,那么在拿着这份简历去面试的时候,面试官有很大的概率会问你vue的数据双向绑定是如何实现的。 打开goole,输入vue双向绑定,有非常多优秀的博主已经对vue数据双向绑定作了一个全方位的刨析,阅读之后,你会大概了解,双向绑定涉及到javascript的核心api是Object.defineProperty,通过setget这俩个存取描述符来监听数据的实时改变,并且在对模版作出相应改变。 那么为了更加了解vue是如何实现数据双向绑定的,我花了一下午的时间阅读vue的源码,并将我的对vue实现数据双向绑定的方式理解记录了下来。

打开vue源码目录

这几个文件夹都是分别负责什么的,我们暂且不管(其实是我不知道),我们找到入口文件src/core/index.js。 看到一大推第一次见并且不熟的代码,谁都会感动头疼。所以我看源码的基本方针是

  • 不清楚应用方法的具体实现,先靠他的命名猜一下(所以英文好很关键,哭)。
  • 如果有一大堆if..else-if..else,先找到按正常流程走的代码,其他分支先放一放...
  • 不用钻牛角尖,看的懂的代码就好好理解,看不懂的了解个大概足已!看源码的目的是更好的理解框架的实现原理,并不是要把整个框架吃透(关键也吃不透啊,vue源代码那么多,咱也不是啥大神,难道看不懂去问尤雨溪吗,咱也不敢问讷)
// src/core/index.js
import Vue from './instance/index' //从Vue这个关键词来看,这个应该是vue的核型方法
import { initGlobalAPI } from './global-api/index' // 初始化全局API?
import { isServerRendering } from 'core/util/env' // 判断是不是ssr?
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
// 调用方法咯,初始化全局变量
initGlobalAPI(Vue)
// 给vue原型添加$isServer属性 --当前 Vue 实例是否运行于服务器。
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
// 给vue原型添加$ssrContext 不认识这玩意
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// 不认识
// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})

Vue.version = '__VERSION__'

export default Vue

我就是以上面这种方式来一点点看源码的。根据上面得到的提示,我们应该去看看./instance/index里写了啥。

// src/core/instance/index
import { initMixin } from './init'
...
initMixin(Vue)
...
export default Vue

其他初始化函数我们先不看,从initMixin这个名字和第一个引入的骄傲位置来说,他应该和我们要找的data属性有一腿。所以我们打开./init看一下。

// src/core/instance/init
import { initState } from './state'
...
initState(vm)
...

从命名上来讲,state应该是与data联系更多的,也许是因为在react里,初始化数据就叫作state吧,所以我们打开./state找到initState方法

// src/core/instance/state
export function initState (vm: Component) {
  vm._watchers = [] // 看起来像清空一个观察者队列
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props) // 初始化props参数
  if (opts.methods) initMethods(vm, opts.methods) // 初始化methods参数
  if (opts.data) {
    initData(vm) // 如果有data参数,初始化data参数
  } else {
    observe(vm._data = {}, true /* asRootData */) // 如果没有,触发observe方法(这个方法很关键!),给一个{}作为默认值并且作为rootdata
  }
  if (opts.computed) initComputed(vm, opts.computed) // 初始化computed参数
  if (opts.watch && opts.watch !== nativeWatch) {
      // watch存在并且 这个watch不是Firefox(火狐浏览器)在Object.prototype上有一个“监视”功能,初始化
    initWatch(vm, opts.watch)
  }
}

从上面的代码中,我们看到很多脸熟的代码了,并且终于找到我们想找的data属性,顺水推舟继续往下走吧,找到initData的方法定义。

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
    // 判断data是不是个函数,如果时执行getData(往一个targetStack push进去?)
  if (!isPlainObject(data)) {
    // 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)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    // 判断data里定义的key是否与methods和props的冲突
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

到这里,我们已经很接近实现数据双向绑定的函数了,那就observe,接下来去../observer/index里看看,observe函数到底写了些什么东西。 在export function observe()的函数里,return出来的是一个名为Observer的类

// src/core/observer/index.js
export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

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

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  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])
    }
  }
}

当我们调用new Oberver(value)的时候,会执行this.walk(value)这个方法,看方法里的作用应该是,遍历value,执行defineReactive方法,而在defineReactive方法里主要就是通过Object.defineProperty方法来定义响应式数据。

// src/core/observer/index.js
export function defineReactive (
 obj: Object,
 key: string,
 val: any,
 customSetter?: ?Function,
 shallow?: boolean
) {
  const dep = new Dep()
  ...
  Object.defineProperty(obj, key, {
      ...
      get: function reactiveGetter () {
        ...
        dep.depend()
        ...
        return value
      },
      set: function reactiveSetter (newVal) {
        ...
        dep.notify()
      }
    })
}

省略了部分代码后,我们注意到在getset里分别执行了dep.depend()dep.notify(),而Dep就是我们常说的订阅发布管理中心,这时候我们来看一张,vue实现数据双向绑定的示例图。

大概解释一下上图,上图实现的设计模式为 订阅-发布 模式。可以从俩个入口分别说起

**1.**从init Data说起,比如我们在vue实例中定义了初始化的data属性,接着会触发new Observer(),data里所有的数据都会通过上面介绍的那样,通过defineReactive这个方法为每一个属性挂载Object.defineProperty(也可以说在get里为每一个属性都添加了一个订阅,在set里做一个通知订阅者的操作),如果触发了setter,也就是在业务代码里改变了data里的值,会通知WatcherWathcer更新指令系统对应绑定的data值 **2.**从编译侧说起,Dom 上通过指令或者双大括号绑定的数据,经过编译以后,会为数据进行添加观察者Watcher,当实例化Watcher的时候 会触发属性的getter方法,此时会调用dep.depend(),并且会将Watcher的依赖收集起来。

那么我们可以看一下dep.depend()dep.depend()

// src/core/observer/dep.js
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  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++) {
      subs[i].update()
    }
  }

首先我们得先知道注入到Dep里的一般都是Watcher类,像Dep.target.addDep(this)subs[i].update()这俩个方法是可以在定义Watcher的文件下找到的。

// src/core/observer/watcher.js
  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)
      }
    }
  }
  ...
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }

一系列操作的主要作用就是让DepWathcer建立双向的联系。

代码真是太多了,解释不完,感觉要烂尾了

最后vue有一个很关键的指令解析系统,在src/compiler/directives文件中可以找到v-bind,v-on,v-model相应的源码。能力有限,看不下去了。越挖越深。

说的我自己都乱了

言简意赅的总结一下,Observer就是对data里到所有值进行一个数据劫持,强行给每个数据注入set(能监听到数据改变,没有return)与get(该数据具体呈现出来的值,能return出数据)方法,Observer操作完以后,data可以理解成房子资源。然后Dep是个订阅器(订阅管理中心,可以理解成房地产中介),Watcher是订阅者(有钱买房的人),Watcher把需求和联系方式通过dep.depend()告诉中介depdep中介找到了合适的房子通过dep.notify()打电话通知我们忽悠买房。那Wathcer没有钱之前就是被绑定在dom上的一些数据,通过了v-model,v-test,双大括号等途径赚到了钱(也就是vue的compile编译系统),升级成了一个Wathcer,赚钱和买房总是无穷无尽的,dom发生了更新(比如input事件),赚到钱了就去问中介dep有没有房,同时如果房源发生了变化(data发生了更新),中介dep会通知Wathcer买房不?

最后祝大家早日能买到房。