vue源码分析(二)

448 阅读3分钟

二、数据驱动

vue的的其中一个核心思想是数据驱动,数据驱动指视图随着数据的改变而发生变化,即数据驱动视图层,相比于使用jquery,使用vue并不会直接去对DOM进行操作,而是通过修改数据即可完成对DOM的操作,DOM操作的部分,由vue根据数据的变化,进行改变。传统的直接操作DOM的方式,会导致项目不易维护。

1、new Vue()到拿到data数据发生了什么

new Vue({
  data() {
    return {
      name: 'cherish'
    }
  },
  created() {
    console.log(this.name) // cherish
  }
}).$mount('#app')
// src/core/instance/index.js
import { initMixin } from './init'
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)
}

在new Vue的时候,首先会判断 this instanceof Vue 来判断Vue是否被new成一个实例对象,然后去触发this._init(options)_init是在initMixin(Vue)时被挂载到Vue上的,

// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
  	...
    // 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
      )
    }
    ...
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

_init这个函数中,主要有3个步骤。

1、merge options,做一些合并配置,这样就可以通过$options拿到我们写入的相关配置。

2、initXXX,初始化一系列的比如生命周期,事件,state等

3、 if (vm.$options.el) { vm.$mount(vm.$options.el); }如果我们在配置当中定义了el这个属性,那么就会触发$mount这个方法,进行渲染。

2、 data如何挂载到this

假如我们写了以下的一段代码,我们是可以通过this.name 访问在data当中定义的name,那么Vue做了哪些操作使我们可以通过this直接访问到我们写在data中的值的

在_init初始化的时候,会调用initState(vm),首先会把vm传入initState

// src/core/instance/init.js
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
  	const vm: Component = this
  	...
  	initState(vm)
}

判断如果有vm.$options.data,则触发initData(vm)

// src/core/instance/state.js
export function initState (vm: Component) {
  ...
  const opts = vm.$options
  ...
  if (opts.data) {
    initData(vm)
  }
 ...
}

initData对我们定义的data属性进行相应的处理,绑定到 vm._data,通过遍历data的key,如果没有和props,methods命名冲突,则触发 proxy(vm, `_data`, key)

// src/core/instance/state.js
function initData (vm: Component) {
  // 从配置中拿到data
  let data = vm.$options.data
  /** 
    *  判断data是否是一个function
    *  如果是,则调用getData,并且把函数的结果赋值给vm._data和data
    *  export function getData (data: Function, vm: Component): any {
    *  	 // #7573 disable dep collection when invoking data getters
    *  	 pushTarget()
    *  	 try {
    *    	return data.call(vm, vm)
    *  	 } catch (e) {
    *    	handleError(e, vm, `data()`)
    *    	return {}
    *  	 } finally {
    *    	popTarget()
    *  	 }
    *  }
    *  如果不是函数,则直接返回data||{}
    */
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ...
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判断是否与 methods props 命名有冲突
  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函数
      proxy(vm, `_data`, key)
    }
  }
  // 响应式相关逻辑
  // observe data
  observe(data, true /* asRootData */)
}

通过循环的调用proxy函数中的Object.defineProperty,进行数据劫持,在vm上绑定了 name 属性,当访问this.name,则会访问到this._data.name

// src/core/instance/state.js
const sharedPropertyDefinition = {
  enumerable: true,
  configurable: true,
  get: noop,
  set: noop
}
// target=vm,sourceKey='_data' key='name'
export function proxy (target: Object, sourceKey: string, key: string) {
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]
  }
  sharedPropertyDefinition.set = function proxySetter (val) {
    this[sourceKey][key] = val
  }
  Object.defineProperty(target, key, sharedPropertyDefinition)
}

3.Vue实例挂载($mount)

runtim + compiler(entry-runtime-with-compiler.js)分析$mount

  new Vue({
    el: '#app'
  })

承接上次提到的_init方法, if (vm.$options.el) { vm.$mount(vm.$options.el) },vm.mount会最终调用`src/platforms/web/entry-runtime-with-compiler.js`中的\mount

// src/platforms/web/entry-runtime-with-compiler.js
// 使用mount保存上次的Vue.prototype.$mount
const mount = Vue.prototype.$mount
// 给Vue.prototype.$mount重新赋值
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
/**
  * 通过query函数拿到el中填入的元素
  * export function query (el: string | Element): Element {
  *    if (typeof el === 'string') {
  *      const selected = document.querySelector(el)
  *      if (!selected) {
  *        process.env.NODE_ENV !== 'production' && warn(
  *          'Cannot find element: ' + el
  *        )
  *        return document.createElement('div')
  *      }
  *      return selected
  *    } else {
  *      return el
  *    }
  * }
  */
  // el = div #app DOM元素
  el = el && query(el)
  // 判断元素不可以为一个body或html元素
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
  // 拿到配置options
  const options = this.$options
  // resolve template/el and convert to render function
  // 如果options中没有render函数
  if (!options.render) {
    let template = options.template
    // 本例子中没有template,则不执行
    if (template) {
      ...
    } else if (el) {
      template = getOuterHTML(el)
    }
    // 本例子中没有template,则不执行
    if (template) {
      // 拿到编译相关配置,这也是template转换为render函数的过程
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      ...
    }
  }
  // 最终执行开头缓存的mount(之前的)
  return mount.call(this, el, hydrating)
}

执行完entry-runtime-with-compiler中的$mount,会执行src/platforms/web/runtime/index.js中的$mount,最终调用mountComponent

 Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 执行query,el在此时已经是一个dom元素,并不是string,因此被直接返回
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent会给updateComponent赋值函数,updateComponent函数的目的,是把虚拟Dom转换为真实Dom,拿到updateComponent后把他作为第二个参数,执行new Watcher,

// src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 首先给vm.$el赋值el
  vm.$el = el
  // 此处判断如果没有render函数,那么会抛出一些警告
  // 此处正是说明了,vue实际接收的是一个render函数
  // template在complier版本下,会被转换为render函数
  if (!vm.$options.render) {
    ...
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
    ...
  }
  // 触发beforeMount钩子
  callHook(vm, 'beforeMount')

  let updateComponent
  // 性能埋点相关
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   ...
  } else {
  // updateComponent被赋值为一个函数,函数中为 vm._update(vm._render(), hydrating)
    updateComponent = () => {
      // vm._render() 为实例生成 Virtual Dom
      // vm._update() 把 Virtual Dom 渲染为真实Dom
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  // 把 updateComponent 作为第二个参数传入 Watcher noop
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

Watcher在Vue中是一个比较核心的概念,渲染Watcher,computed Wathcer 还有user Watch都是通过 Watcher这个class实现,承接updateComponent expOrFn = updateComponent 最终this.value = updateComponent() = vm._update(vm._render(), hydrating)

// src/core/observer/watcher.js
export default class Watcher {
  vm: Component;
  lazy: boolean;
  getter: Function;
  value: any;
  ...
  constructor (
    vm: Component,
    // expOrFn = updateComponent
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    // options
    // options为{bofore:...},则 this.lazy = false
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } 
    ...
    if (typeof expOrFn === 'function') {
      //  this.getter = expOrFn = updateComponent
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      ...
    }
    // this.lazy = false 则触发 this.get()
    this.value = this.lazy
      ? undefined
      : this.get()
  }

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  // get 触发 this.getter
  get () {
    ...
    let value
    const vm = this.vm
    try {
      // value = vm._update(vm._render(), hydrating)
      value = this.getter.call(vm, vm)
    }
    ...
    return value
  }
  ...
}

下一章主要分析 vm._render()如何生成虚拟dom,以及vm._update是如何把虚拟dom映射为了真实dom