Vue 2.5 数据绑定实现逻辑(三)initState

Vue 实例在建立的时候会运行一系列的初始化操作,而在这些初始化操作里面,和数据绑定关联最大的是 initState。这个里面要说的也是比较多,有可能这次的文章里面写不全,先写这看吧。

首先看 initState

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

这里面主要是对 props, methods, data, computed 和 watch 进行初始化(如果还不知道这几个属性都是什么,建议先去看一下官方文档并且写几个小例子)。这些属性都是要在 Dom 渲染时获取的,自然也大都需要进行数据绑定。

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') {
      ......
    } else {
      defineReactive(props, key, value)
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key)
    }
  }
  observerState.shouldConvert = true
}

省略的地方是开发环境中为了方便调试写的代码,Vue 源码中有相当多的地方是这样写的。

整体逻辑就是:

  1. 把所有 prop 的 key 另存在 options 的 _propKeys 中。
  2. 对于每一个 prop,将其 key 添加到 _propKeys 中,获取其 value,并执行 defineReactive 函数。(不了解的可以看上一节)
  3. 对于每一个 prop, 调用 proxy 函数在 Vue 对象上建立一个该值的引用。

在获取 prop 的 value 的时候调用了 validateProp 进行验证并取验证后的返回值。

export function validateProp (
  key: string,
  propOptions: Object,
  propsData: Object,
  vm?: Component
): any {
  const prop = propOptions[key]
  const absent = !hasOwn(propsData, key)
  let value = propsData[key]
  // handle boolean props
  if (isType(Boolean, prop.type)) {
    if (absent && !hasOwn(prop, 'default')) {
      value = false
    } else if (!isType(String, prop.type) && (value === '' || value === hyphenate(key))) {
      value = true
    }
  }
  // check default value
  if (value === undefined) {
    value = getPropDefaultValue(vm, prop, key)
    // since the default value is a fresh copy,
    // make sure to observe it.
    const prevShouldConvert = observerState.shouldConvert
    observerState.shouldConvert = true
    observe(value)
    observerState.shouldConvert = prevShouldConvert
  }
  if (
    process.env.NODE_ENV !== 'production' &&
    // skip validation for weex recycle-list child component props
    !(__WEEX__ && isObject(value) && ('@binding' in value))
  ) {
    assertProp(prop, key, value, vm, absent)
  }
  return value
}

注意,prop 验证只有在开发环境中才会进行,并且并不会影响渲染,只会发出警告。

这里的工作主要是在 prop 没有传值时获取 prop 的默认值(默认值是自己设置的),并对该值执行 observe。对于布尔类型,如果没有默认值则认为默认值是 false。

如果是开发环境,则会进行类型验证,这个验证是典型的根据构造函数名进行类型验证的,这个函数名获取到以后会进行字符串的比对,最近也正想自己写一个比较完善的类型验证组件,所以在这篇文章里就不详述了,免得跑题。

这里多次对 observerState.shouldConvert 进行赋值,这个值的 true or false 直接决定了 Observer 是否会建立。

至于这个 propsData 是什么时候取得的呢,当然是在模板编译的时候取得的。关于 prop 还有很多需要说的,有可能还要另外写一篇文章来说明。

initMethod

对 method 的初始化相对其他来说还是比较简单的

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

主要是在开发环境中检测:

  1. 方法名是否为空
  2. 方法名是否和一个 prop 冲突
  3. 方法名是否和已有的 Vue 实例方法冲突

另外会用 bind 将该方法的作用域绑定到 Vue 实例对象上,且创建一个在 Vue 实例对象上的引用(这点很重要)

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
}

这个 bind 是用apply 和 call 重写的 bind,据说是会比原生的 bind 要快,但是实在才学尚浅,不明白为什么。

initData

如果对上篇文章说到的内容比较熟悉的话,这里应该就没什么难度了。

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)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    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 */)
}

首先会获取 data,如果 data 是函数的话,则调用 getData 获取函数的返回值。
这里面还是在检测一些重名的问题,就不想细说了。

这里最重要的是对 data 运行 observe 函数建立起 Observer 和钩子函数

initComputed

这里就比较麻烦了,由于计算属性并不是值,而是函数,并且返回值还会和一些值有关,同时还要涉及到缓存的问题,就需要一些特殊的方法进行处理了,为了避免文章太长,就放在下一篇说。

initWatch

说到这里就一定要补充一下之前没有说到的关于 Watcher 的问题了,先看代码,一步步往下说。

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}

首先是对于每一个 watch 属性运行 createWatcher(想想也应该知道是建立一个 Watcher 对象)

function createWatcher (
  vm: Component,
  keyOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(keyOrFn, handler, options)
}

这里主要进行了两步预处理,代码上很好理解,主要做一些解释:

第一步,可以理解为用户设置的 watch 有可能是一个 options 对象,如果是这样的话则取 options 中的 handler 作为回调函数。(并且将options 传入下一步的 vm.$watch)

第二步,watch 有可能是之前定义过的 method,则获取该方法为 handler。

下面就要看 $watch 方法了,这个方法是在 stateMixin 中定义的

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(就是前面的 handler)是对象的话则再运行一遍 createWatcher 进行处理,然后建立一个 Watcher 对象进行监听,如果 options 中的 immediate 为 true 则立即执行该回调函数,最后返回一个函数用来停止监听。

接下来就要看看这个回调函数是什么时候运行的了

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

再次看到 Watcher 的 run 方法,这里面判断 user 如果为 true 则运行 cb 函数,这个函数就是之前传入的 handler 回调函数, user 则在 vm.$watch 中赋值为true,其他地方建立的 Watcher 则基本都为 false,其他的几个如 lasy 等参数也是通过 options 传入的,这里就不详细说了,具体可以自己看一下代码或者官方API文档。

结语

到这一步为止(先不算计算属性的初始化),数据绑定的逻辑基本分析完了,这篇文章看完以后重点还是要看看 Watcher 对象的设计,可以说这个监视器设计的相当巧妙,废话不多说了,希望大家有什么见解或者分析有误的可以提出来。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容