Vue.js源码学习一 —— 数据选项 State 学习

关于Vue源码学习的博客, HcySunYangVue2.1.7源码学习是我所见过讲的最清晰明了的博客了,很是适合想了解Vue源码的同窗入手。本文是在看了这篇博客以后进一步的学习心得。
注意:本文所用Vue版本为 2.5.13
PS:本文有点草率,以后会重写改进。

关于源码学习

关于学习源码,我有话要说~
一开始我学习Vue的源码,是将 Vue.js 这个文件下载下来逐行去看……由于我听信了我同事说的“不过一万多行代码,实现也很简单,能够直接看。”结果可想而知,花了十几个小时看完代码,还经过打断点看流程,除了学习到一些新的js语法、一些优雅的代码写法、和对整个代码熟悉了以外,没啥其余收获。
其实,这是一个丢西瓜捡芝麻的行为,没有明确的目的笼统的看源码,最终迷失在各类细枝末节上了。
因此呢,我看源码的经验教训有以下几点:html

  • 看代码,必须带着问题去找实现代码。
  • 保持主线,不要纠结于细枝末节。永远记住你要解决什么问题。
  • 找到一篇优质的博客、向前辈学习,让前辈带着你去学习事半功倍。
  • 想看某编程语言的代码,必需要有扎实的语言基础。走路不稳就想跑,会摔得很惨~
  • 学习之道,不能盲目。应该找到一种快速有效的方法,来有目的的实现学习目标。不要用战术上的勤奋来掩盖战略上的失误。看代码如此、看书学习亦如此~

如何开始

这里咱们来解决从哪里开始看代码的流程,重点是找到Vue构造函数的实现
首先,找到 package.json 文件,从中找到编译命令 "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",这里 rollup 是相似于 Webpack 的打包工具,打包文件在 script/config.js 中,找到该文件。找 entry 入口关键字(不会rollup,但配置方式和 Webpack 差不太多)。入口文件有好多配置,咱们就找到会生成 dist/vue.js 的配置项:前端

// Runtime+compiler development build (Browser)
  'web-full-dev': {
    entry: resolve('web/entry-runtime-with-compiler.js'),
    dest: resolve('dist/vue.js'),
    format: 'umd',
    env: 'development',
    alias: { he: './entity-decoder' },
    banner
  },

好,这里就找到了 web/entry-runtime-with-compiler.js 这个路径,完整路径应该是 src/platform/web/entry-runtime-with-compiler.js。在这个文件中咱们找到一个Vue对象import进来了。vue

import Vue from './runtime/index'

咱们顺着找到到 src/platform/web/runtime/index.js 这个文件,在文件中发现导入文件git

import Vue from 'core/index'

就顺着这个思路找,最终找到 src/core/instance/index.js 这个文件。
完整找到Vue实例入口文件的流程以下:github

package.json
script/config.js
src/platform/web/entry-runtime-with-compiler.js
src/platform/web/runtime/index.js
src/core/index.js
src/core/instance/index.js

简单看看Vue构造函数的样子~web

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue) // 初始化
stateMixin(Vue) // 状态混合
eventsMixin(Vue) // 事件混合
lifecycleMixin(Vue) // 生命周期混合
renderMixin(Vue) // 渲染混合

export default Vue

能够看到Vue的构造函数,里面只作了 this._init(options) 行为。这个 _init 方法在执行 initMixin 方法的时候定义了。找到同目录下的 init.js 文件。express

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      // 合并配置项
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm) // 初始化代理
    } else {
      vm._renderProxy = vm
    }
    
    vm._self = vm // 暴露对象自身
    initLifecycle(vm) // 初始化生命周期
    initEvents(vm) // 初始化事件:on,once,off,emit
    initRender(vm) // 初始化渲染:涉及到Virtual DOM
    callHook(vm, 'beforeCreate') //  触发 beforeCreate 生命周期钩子
    initInjections(vm) // 在初始化 data/props 前初始化Injections
    initState(vm) // 初始化状态选项
    initProvide(vm) // 在初始化 data/props 后初始化Provide
    // 有关inject和provide请查阅 https://cn.vuejs.org/v2/api/#provide-inject
    callHook(vm, 'created') // 触发 created 生命周期钩子

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 若是Vue配置项中有el,直接挂在到DOM中
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

抓住重点,咱们是要来学习State的。从上面代码中能够找到initState方法的执行,这就是咱们此行的目的——State数据选项。除此以外还有其余重要方法的初始化方式,这将会在以后的博客中继续讨论和学习。编程

学习State

以前是简单提一下学习源码的方法论和如何开始学习Vue源码学习。而且找到了咱们要学习的State所在,如今进入正题:json

了解Vue的数据选项的运行机制。

Vue2.1.7源码学习中,做者已经很是很是很是清晰明了的帮咱们分析了data的实现。在此基础上开始好好学习其余数据选项的实现逻辑。api

经过data理解mvvm

这里我经过本身的思路再来整理下项目中data的实现。
注:因为这一部分已经被各种源码解析博客讲烂了,而要把这部分讲清楚要大量篇幅。因此我就不贴代码了。仍是那句话,抓重点!咱们主要研究的是data以外的实现方式。关于data的实现和mvvm的逐步实现,Vue2.1.7源码学习中讲的很是清晰明了。

如下是我整理的思路,有兴趣的同窗能够顺着个人思路去看看。

在 state.js 中找到 initState,并顺利找到 initData 函数。initData中主要作了如下几步操做:

  1. 获取data数据,data数据一般是一个方法,执行方法返回data数据。因此说咱们要将data写成函数方法的形式。
  2. 遍历data数据,判断是否有data与props的key同名,若是没有执行proxy方法,该方法用于将data中的数据同步到vm对象上,因此咱们能够经过 vm.name 来修改和获取 data 中的 name 的值。
  3. 执行observe方法,监听data的变化。

重点在 observe 方法,因而咱们根据 import 关系找到 src/core/observer/index.js 文件。observe 方法经过传入的值最终返回一个Observer类的实例对象。
找到Observer类,在构造函数中为当前类建立Dep实例,而后判断数据,若是是数组,触发 observeArray 方法,遍历执行 observe 方法;若是是对象,触发walk方法。
找到walk方法,方法中遍历了数据对象,为对象每一个属性执行 defineReactive 方法。
找到 defineReactive 方法,该方法为 mvvm 数据变化检测的核心。为对象属性添加 set 和 get 方法。重点来了, vue 在 get 方法中执行 dep.depend() 方法,在 set 方法中执行 dep.notify() 方法。这个先很少讲,最后进行联结说明。
找到同目录下的 dep.js 文件,文件不长。定义了 Dep 类和pushTargetpopTarget 方法。在 Dep 类中有咱们以前提到的 dependnotify 方法。看下两个方法的实现:

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

depend 方法中,Dep.target 就是一个 Watcher 实例,它的 addDep 方法最终会调用到 Dep 的 addSubs 方法。subs 是 Watcher 数组。即将当前 watcher 存到 Dep 的 subs 数组中。
notify 方法中,将 Watcher 数组 subs 遍历,执行他们的 update 方法。update 最终会去执行 watcher 的回调函数。
即在 get 方法中将 watcher 添加到 dep,在 set 方法中经过 dep 对 watcher 进行回调函数触发。
这里其实已经实现了数据监听,接着咱们来看看 Watcher,其实 Watcher 就是Vue中 watch 选项的实现了。说到 watch 选项咱们都知道它用来监听数据变化。Watcher 就是实现这个过程的玩意啦~
Watcher的构造函数最终调用了 get 方法,代码以下:

get () {
    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
  }

get 方法作了以下几步:

  1. 将当前 Watcher 实例传递给 Dep 的 Dep.target。
  2. 执行 Watcher 所监测的数据的 getter 方法。
  3. 最终,将 Dep.target 恢复到上一个值,而且将当前 Watcher 从 Dep 的 subs 中去除。

其中要注意的是,在第二步中数据的 getter 方法会执行到 dep.depend() 方法,depend 方法将当前 watcher 加入到 subs 中。至于步骤一和三还不太理解。挖个坑先~
这样 watcher 就监测上数据了。那怎么使用呢?固然是数据变化时使用咯。当监测的数据变化时,执行数据 setter 方法,而后执行 dep 的 notify 方法。因为咱们以前已经将 watcher 都收集到 dep 的 subs 中,notify 方法遍历执行 watcher 的 update 方法,update 方法最终遍历执行回调函数。

  1. 执行 observe 方法,建立 Observer 执行 walk 为对象数据添加setter 和 getter
  2. 在添加 setter 和 getter 时,建立 Dep,在 getter 方法中执行 dep.depend() 收集 watcher,在 setter 方法中执行 dep.notify() 方法,最终遍历执行 watcher 数组的回调函数。
  3. Dep 相似于 Watcher 和 Observer 的中间件。
  4. Watcher 用于监听变化,并执行回调函数。
  5. 当 Watcher 实例建立时,Watcher 实例会将自身传递给 Dep.target
  6. Watcher 调用监测数据的 getter方法触发 dep.depend()
  7. dep.depend()方法将当前 Watcher(Dep.target)传递给Dep的subs(watcher数组)中。
  8. 当被监测的数据内容发生改变时,执行 setter 方法,触发 dep.notify() 方法,遍历 Dep 中的 subs(watcher数组),执行 Watcher 的回调函数。

嗯……就是这样~以后把挖的坑填上!

watch实现

说完了 Data 的监听流程,说说 watch 应该就不难啦~
找到 src/core/instance/state.jsinitWatch 函数,该方法用来遍历 Vue 实例中的 watch 项,最终全部 watch 都会执行 createWatcher 方法。
继续看 createWatcher 方法,这个方法也很简单,最终返回 vm.$watch(keyOrFn, handler, options)。咱们继续往下找~
stateMixin 方法中找到了定义 Vue 的 $watch 方法属性。来看看怎么实现的:

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    const watcher = new Watcher(vm, expOrFn, cb, options)
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

若是回调函数 cb 是一个对象,那么返回并执行 createWatcher 函数,最终仍是会走到 $watch 方法中。
不然,建立一个 Watcher 实例,当这个实例建立后,目标数据有任何变化 watch 选项中都能监听到了。若是是有 immediate 参数,那么当即执行一次Watcher的回调函数。最后返回一个解除监听的方法,执行了 Watcher 的 teardown 方法。
那么问题来了,为何watch选项监听数据的方法中参数是以下写法呢?

watch: {
  a: function(val, oldVal){
    console.log(val)
  }
}

能够找到 src/core/instance/observer/watcher.js 中找到 run 方法。能够看到 this.cb.call(this.vm, value, oldValue) 这里的 cb 回调函数传递的参数就是 value 和 oldValue。
这里说个基础知识,函数使用 call 方法执行,第一个参数是方法的this值,以后才是真正的参数。

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

小结:watch 选项其实就是为指定数据建立 Watcher 实例,接收回调函数的过程。

props实现

接下来咱们看看props,官网对props的定义以下:

props 能够是数组或对象,用于接收来自父组件的数据。

找到 initProps 方法。

function initProps (vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {}
  const props = vm._props = {}
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = vm.$options._propKeys = []
  const isRoot = !vm.$parent
  // root instance props should be converted
  observerState.shouldConvert = isRoot
  for (const key in propsOptions) {
    keys.push(key)
    const value = validateProp(key, propsOptions, propsData, vm)
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      const hyphenatedKey = hyphenate(key)
      if (isReservedAttribute(hyphenatedKey) ||
          config.isReservedAttr(hyphenatedKey)) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        )
      }
      defineReactive(props, key, value, () => {
        if (vm.$parent && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
            `overwritten whenever the parent component re-renders. ` +
            `Instead, use a data or computed property based on the prop's ` +
            `value. Prop being mutated: "${key}"`,
            vm
          )
        }
      })
    } else {
      defineReactive(props, key, value)
    }

    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  observerState.shouldConvert = true
}

能够看到,props 和 data 相似。在 initProps 中无非作了两步:defineReactiveproxy,这两个方法咱们在提到 data 的时候讲过了。defineReactive 为数据设置 setter、getter,proxy 方法将 props 中的属性映射到 Vue 实例 vm 上,便于咱们能够用 vm.myProps 来获取数据。
至此,我有个疑问:data与props有何不一样呢?
data使用的是observe方法,建立一个Observer对象,Observer对象最终是执行了defineReactive方法。而props是遍历选项属性,执行defineReactive方法。中间可能就多了个Observer对象,那么这个Observer对象的做用到底在哪呢?通过实践props属性改变后界面也会改变。说明mvvm对props也是成立的。
另外,data和props有个不一样的地方就是props是不建议改变的。详见单向数据流
小结:逻辑和data相似,都是监听数据。不一样之处呢……再研究研究~

computed实现

再来讲说computed,找到初始化computed方法 src/core/instance/state.js 中的 initComputed 方法,去除非关键代码后看到其实主要有俩个行为,为 computed 属性建立 Watcher,而后执行 defineComputed 方法。

function initComputed (vm: Component, computed: Object) {
  ...
  for (const key in computed) {
    ...
    if (!isSSR) {
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      )
    }

    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    } 
    ...
  }
}

defineComputed 作了两步行为:一是定义 sharedPropertyDefinition 的 getter 和 setter,二是将 sharedPropertyDefinition 的属性传给vm,即 Object.defineProperty(target, key, sharedPropertyDefinition)。自此,咱们能够经过 vm.computedValue 来获取计算属性结果了。
小结:computed其实也就是一个数据监听行为,与data和props不一样之处就是在get函数中须要进行逻辑计算处理。

methods实现

继续在 state.js 中看到 initMethods 方法。顾名思义,这是初始化methods的方法。实现很简单,代码以下:

function initMethods (vm: Component, methods: Object) {
  const props = vm.$options.props
  for (const key in methods) {
    if (process.env.NODE_ENV !== 'production') {
      if (methods[key] == null) {
        warn(
          `Method "${key}" has an undefined value in the component definition. ` +
          `Did you reference the function correctly?`,
          vm
        )
      }
      if (props && hasOwn(props, key)) {
        warn(
          `Method "${key}" has already been defined as a prop.`,
          vm
        )
      }
      if ((key in vm) && isReserved(key)) {
        warn(
          `Method "${key}" conflicts with an existing Vue instance method. ` +
          `Avoid defining component methods that start with _ or $.`
        )
      }
    }
    vm[key] = methods[key] == null ? noop : bind(methods[key], vm)
  }
}

重点在最后一句。前面都排除重名和空值错误的,最后将 methods 中的方法传给 vm,方法内容若是为空则方法什么都不作。不然调用 bind 方法执行该函数。
找到这个 bind 方法,位置在 src/shared/util.js 中。

export function bind (fn: Function, ctx: Object): Function {
  function boundFn (a) {
    const l: number = arguments.length
    return l
      ? l > 1
        ? fn.apply(ctx, arguments)
        : fn.call(ctx, a)
      : fn.call(ctx)
  }
  // record original fn length
  boundFn._length = fn.length
  return boundFn
}

该方法返回一个执行 methods 中函数的方法(这种方法的执行方式比较快)。
小结:将methods的方法用bind函数优化执行过程。而后将methods中的各个方法传给Vue实例对象。

最后

本文纯属我的理解,若有任何问题,请及时指出,不胜感激~
最后提出一个看源码的当心得:

我发现……看源码、跟流程,尽可能将注意力集中在 方法的执行类的实例化行为上。对于变量的获取和赋值、测试环境警报提示,简略看下就行,避免逐行阅读代码拉低效率。

至此,Vue中的几个数据选项都学习了一遍了。关键在于理解mvvm的过程。data 理解以后,props、watch、computed 都好理解了。methods 和 mvvm 无关……
经过四个早上的时间把文章写出来了~对 Vue 的理解深入了一些,可是仍是能感受到有不少未知的知识点等着我去发掘。加油吧!今年专一于 Vue 前端学习,把 Vue 给弄懂!

Vue.js学习系列

鉴于前端知识碎片化严重,我但愿可以系统化的整理出一套关于Vue的学习系列博客。

Vue.js学习系列项目地址

本文源码已收入到GitHub中,以供参考,固然能留下一个star更好啦^-^。
https://github.com/violetjack/VueStudyDemos

关于做者

VioletJack,高效学习前端工程师,喜欢研究提升效率的方法,也专一于Vue前端相关知识的学习、整理。
欢迎关注、点赞、评论留言~我将持续产出Vue相关优质内容。

新浪微博: http://weibo.com/u/2640909603
掘金:https://gold.xitu.io/user/571...
CSDN: http://blog.csdn.net/violetja...
简书: http://www.jianshu.com/users/...
Github: https://github.com/violetjack

相关文章
相关标签/搜索