Vue源码解析之 数据驱动

424 阅读4分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 3 篇,关注专栏

前言

数据驱动是 Vue.js 的核心思想。所谓数据驱动,是指视图是由数据生成的,我们对视图的修改不会直接操作DOM,而是通过修改数据进行视图的更新。通过如下案例,我们从源码角度来分析 Vue 是如何实现的。

<div id="app">
  {{ message }}
</div>

const app = new Vue({
  el: '#app',
  data: {
    message: 'Hello World'
  }
})

准备工作

源码目录

src
├── compiler       // 编译相关 
├── core           // 核心代码 
├── platforms      // 不同平台的支持
├── server         // 服务端渲染
├── sfc            // .vue 文件解析
├── shared         // 共享代码
  • compiler
    • 该目录包含 Vue.js 所有 编译相关 的代码,包括将模板解析成 AST 语法树,AST 语法优化,代码生成等功能
  • core
    • 该目录为 Vue.js 的 核心代码,包含 Vue 实例化、全局 API 分装、内置组件、虚拟 DOM等
  • platforms
    • 该目录是 Vue.js 入口,主要对 Vue 代码如何在浏览器上运行起来
  • server
    • 该目录是运行服务端相关代码,所有服务端渲染相关逻辑都在该目录下,如运行在服务端的Node.js,与运行在客户端的 Vue.js 有所不同
  • sfc
    • 该目录下的代码逻辑会把 .vue 文件内容解析成一个 JavaScript 的对象
  • shared
    • 定义一些工具方法,供全局调用

源码构建

通常我们利用 vue-cli 初始化项目,会询问我们选择 Runtime Only 还是 Runtime + Compiler 版本,它们区别如下:

  • Runtime Only

在使用该版本时,需借助 webpack 的 vue-loader 工具把 .vue 文件编译成 js,代码体积会更轻量

  • Runtime + Compiler

我们如果没有对代码做预编译,但⼜使⽤了 Vue 的 template 属性并传⼊⼀个字符串,则需要在客户端编译模板,如下所⽰:

// 需要编译器的版本
new Vue({
    template: '<div>{{ hi }}</div>'
})

// 这种情况不需要
new Vue({
    render (h) {
        return h('div', this.hi)
    }
})

因为在 Vue.js 2.0 中,最终渲染都是通过 render 函数,如果写 template 属性,则需要编译成 render 函数,那么这个编译过程会发⽣运⾏时,所以需要带有编译器的版本。

很显然,这个编译过程对性能会有⼀定损耗,所以通常我们更推荐使⽤ Runtime-Only 的 Vue.js。

从入口出发

  • 入口文件被定义在 src/platforms/web/entry-runtime-with-compiler.js
  • 找到入口文件下的引入文件 import Vue from './runtime/index
  • 再找到引入文件 import Vue from 'core/index'
  • 最后找到 Vue 的引入文件 import Vue from './instance/index'

至此,我们要找的 Vue 被定义在 src/core/instance/index.js 中,它实际是一个构造函数

function Vue (options) {
    // 初始化配置
    this._init(options)
}

// 以下方法均是给 vue 原型即 vue.prototype 挂载相应的方法

// 挂载_init方法
initMixin(Vue)
// 挂载 $set、$delete、$watch 等方法
stateMixin(Vue)
// 挂载 $on、$once 等方法
eventsMixin(Vue)
// 挂载 $forceUpdate 等方法
lifecycleMixin(Vue)
// 挂载 _render 渲染方法
renderMixin(Vue)

export default Vue

_init 方法被定义在 src/core/instance/init.js,该函数会初始化生命周期、初始化事件中心、初始化渲染、初始化data、watcher、props、computed等,最后检测到如果有 el 属性,则调用 vm.$mount 挂载,目标就是把模板渲染成最终 DOM。

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
  }
  // expose real self
  vm._self = 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')

  /* 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)
  }
  
  // 检测 是否存在 el属性 存在则调用vm.$mount 其目的是将模板渲染成最终 DOM
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

Vue 实例的挂载过程

上文讲述 Vue 实例初始完后,最终会检测是否含有 el 属性,如果有则调用 vm.$mount 挂载,目标就是把模板渲染成最终 DOM。$mount 被定义在 src/platforms/web/runtime/index 中,其核心就是调用mountComponent 方法

import { mountComponent } from 'core/instance/lifecycle'

// public mount method
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent 函数被定义在 src/core/instance/lifecycle

export function mountComponent (
    vm: Component,
    el: ?Element,
    hydrating?: boolean
  ): Component {
    vm.$el = el
    // 省略
    let updateComponent
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      // 省略
    } else {
      updateComponent = () => {
        vm._update(vm._render(), hydrating)
      }
    }
  
    new Watcher(vm, updateComponent, noop, {
      before () {
        if (vm._isMounted) {
          callHook(vm, 'beforeUpdate')
        }
      }
    }, true /* isRenderWatcher */)
    
    // 省略
    return vm
  }

我们可以很清晰看到 mountComponent 方法实际就是生成一个 watcher 实例,其中第二个参数updateComponent 是关键,它实际是调用 _update 方法,之后又再调用 _render 方法。_render 方法定义在 src/core/instance/render,实际是调用 createElement 方法

// createElement引入
import { createElement } from '../vdom/create-element'

export function initRender (vm: Component) {
  // 省略
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  // 省略
}

createElement 方法定义在 src/core/vdom/create-element,该方法实际是创建 VDOM 虚拟 DOM 过程

export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 省略
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  if (isDef(data) && isDef((data: any).__ob__)) {
    // 省略
    return createEmptyVNode()
  }
  // 省略

  if (Array.isArray(children) &&
      typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

最后回到 _update 方法,它被定义在 src/core/instance/lifecycle.js,该方法核心是调用 __patch__ 方法,里面会执行 diff 算法,将真实 DOM 解析成虚拟 DOM,并执行插入和替换操作,最终渲染完成

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const prevActiveInstance = activeInstance
    activeInstance = vm
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    activeInstance = prevActiveInstance
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
  }
}

总结

new Vue实例化过程主要发生如下几件事:

  • init初始化一些配置、初始化生命周期、初始化事件中心、初始化data、watcher、props、computed等
  • 通过 $mount实例方法挂载 vm
  • compile 编译生成 render 方法,如果模板中直接编写 render 方法,那直接跳过 compile 过程
  • render 方法的核心 是调用 createElement 函数
  • 之后会执行 update 方法,该方法核心是执行 patch 方法,patch 函数会将真实 DOM 解析成 虚拟 DOM,并执行替换和插入操作,最终渲染完成

new-vue.png

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick