Vue源码核心

115 阅读4分钟

准备

Flow

为什么是flow

不仅是因为它能够很好的实现代码静态检测,主要是Babel和ESLint提供了flow相关插件,对它的支持很友好,可以以非常小的成本实现静态类型检查能力。

工作方式

flow的工作方式主要有2种:

  • 类型推断
/*@flow*/

function f1(str) {
    return str.split(',')
}

这样,就能推测出str是需要传字符串。

  • 类型注释
/*@flow*/

function add(x, y) {
    return x + y
}

如果是这样x+y,类型推测就不能确定了,因为+可能是字符串也可能是数字,如果你想确定只能是数字,则需要用到类型注释,如下:

/*@flow*/

function add(x: number, y: number): number {
    return x + y
}
自定义类型
flow所能识别的类型有限,如果想要自定义的话,需要在flow提供的配置文件.flowconfig中配置,如下图:

可以看出,这里配置的是个路径,具体类型在主目录下flow文件夹中,如果在阅读源码的时候遇到了,可以打开详细看下。

数据驱动

数据驱动是Vue的一个核心思想,它的意思是我们不会对DOM进行直接操作,而是通过定义数据从而来完成对视图的更新,这样做好处非常明显:使代码结构非常清晰并且易于维护。

new Vue()发生了什么

在上文已经找到,Vue构造函数是定义在src/core/instance/index.js中:
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')
  }
  // 执行initMixin函数中定义的_init()初始化
  // 若不定义,则不会执行
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

其中,Mixin类函数如initMixin(Vue)等定义的是实例全局方法。new Vue()执行的

this._init(options)

定义在同级下init.js文件initMixins(Vue)方法内,实现了一系列值初始化:

initLifecycle(vm)
initEvents(vm)
initRender(vm) // $attrs、$listeners
callHook(vm, 'beforeCreate') // 暴露beforeCreate钩子
/**
 * injection之所以在provide之前初始化,是因为在同一个组件内,必须先初始化injecti* on获取到父传递的值,再初始化provide给子传值。就是必须先拿到父值才能传给子
 */
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

从这里,我们也就能知道了,在组件开发中,beforeCreate和created钩子函数分别能获取到哪些值。
在本节,我们要实现的是数据通过模板生成DOM展示出来,现在数据一系列初始化完成了,下面来看下模板。

挂载

记得在入口文件分析的时候,就提到了它的主要功能是扩展了$mount方法,既然是扩展,那就说明已经定义了原方法,实际上,在src/platform/web/runtime/index.js和src/platform/weex/runtime/index.js文件中都分别定义了$mount方法,说明挂载与平台有关。我们应该也很熟悉,在很多实际项目开发中,是用的webpack构建,其中有用到了vue-loader插件,它的功能就是对vue模板进行编译,正因为此,我们引入的vue则不需要选择带编译器的版本。如果是不带编译器的vue版本,则$mount方法就是src/platform/web/runtime/index.js中定义的。在$mount原方法中,实现非常简单,就返回了mountComponent方法:
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent方法定义在了core/instance/lifecycle文件下。核心代码:

const vnode = vnode || vm._render()
vm._update(vnode, hydrating) // 核心代码

内部_render()方法作用是生成虚拟DOM,_update()方法是通过虚拟DOM生成真实DOM。下面,继续研究下这2个核心方法。

render

_render()方法被定义在src/core/instance/render.js中:
vnode = render.call(vm._renderProxy, vm.$createElement)

可以看出,render函数的参数是vm.$createElement,根据我们平时手写render函数的经验,可以推测createElement就是传说中的h函数,它定义在当前文件initRender()函数中:

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

继续深入看看createElement方法,它是生成VDOM的核心。定义在src/core/vdom/create-element.js,返回VDOM。createElement方法返回内部函数_createElement,在方法中,根据tag类型不同,会存在不同生成VDOM的方式,如果是标签是组件,则是通过createComponent方法生成,其他是通过new VNode()方式产生。

if (config.isReservedTag(tag)) {
   // platform built-in elements
   if (process.env.NODE_ENV !== 'production' && isDef(data) &&
      isDef(data.nativeOn)) {
        warn(`The .native modifier for v-on is only valid on components but it 
            was used on <${tag}>.`, context)
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = 
      resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }

update

_render方法定义在src\core\instance\lifecycle.js中lifecycleMixin函数中:
vm.$el = vm.__patch__(prevVnode, vnode)

核心方法是__patch__,在src\platforms\web\runtime\index.js中在Vue原型上定义:

Vue.prototype.__patch__ = inBrowser ? patch : noop

继续往下找到src\platforms\web\runtime\patch.js文件

export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction方法定义在src\core\vdom\patch.js中,里面函数特别多,功能主要有2个:

  • VDOM对比(如果是首次渲染,则不必要;如果非首次,则新生成的VDOM与现真实DOM映射的VDOM进行对比)
  • 更新渲染到真实DOM

响应式原理

在上文中,我们大体走了一遍Vue的运行过程,在本节中,一起来看下他的响应式,主要有2个部分:依赖收集派发更新。依赖收集是让定义的数据响应式,而派发更新则需要根据监听数据的变化而更新页面。Vue中可响应式的数据类型很多,如props、data等,以下我们以定义更多的data为例来说。

data的初始化在src\core\instance\state.js中方法initData(),核心代码就一行

observe(data, true /* asRootData */)

observe方法定义在src\core\observer\index.js中,里面进行了判断,如果传入的对象是响应式的,则直接返回;若不是,则执行:

ob = new Observer(value)

Observer类也在相同文件中,根据是否是数组,进行了判断,数组的响应式处理后面再说。在构造函数中执行了内部walk(),而walk方法则定义了对象响应式的核心方法:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

definedReactive方法也在当前文件:

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter () {
    const value = getter ? getter.call(obj) : val
    // Watcher
    // 执行依赖收集
    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
    /* eslint-disable no-self-compare */
    if (newVal === value || (newVal !== newVal && value !== value)) {
      return
    }
    /* eslint-enable no-self-compare */
    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()
  }
})

可以看到Vue数据响应式原理的核心是采用了对象的defineProperty方法里面定义的getter/setter,在get方法中:

dep.depend() // 执行依赖收集

在set方法中:

dep.notify() // 通知更新

那你一定好奇,dep到底是什么呢?Dep类就是管理watcher的工具。内部有4个方法:

constructor () {
  this.id = uid++
  this.subs = []
}
addSub (sub: Watcher) {
  this.subs.push(sub)
}

removeSub (sub: Watcher) {
  remove(this.subs, sub)
}

// 对象响应式依赖收集时执行
depend () {
  if (Dep.target) {
    // 在Watcher中定义
    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()
  }
}

在构造函数中定义了subs数组用于管理watcher,addSub()和removeSub()就是对subs数组的增删操作,而下面depend()和notify()方法在defineProperty的get/set方法都调用了,分别用于数据的依赖收集和通知更新。既然Sub是对watcher的管理,那watcher监听的是什么?整个应用或是单个组件,或是组件内的某个属性呢?来看看Watcher类:src\core\observer\watcher.js,它的第一个入参vm: Component,是一个组件,上文说的Dep类,它其实存在一个静态属性target,类型就是Watcher,指向的就是当前正在处理的组件。所以说,watcher监听的是单个组件内响应式数据。

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  ...
}