Vue源码解析 - 响应式原理

423 阅读8分钟

目标

  • Vue.js 的静态成员和实例成员初始化过程
  • 首次渲染过程
  • 数据响应式原理

准备工作

Vue 源码获取

  • 项目地址: github.com/vuejs/vue
  • Fork 一份到自己仓库,克隆到本地,可以自己写注释提交到github
  • 为什么分析 Vue2.6
    • 到目前为止 Vue3.0的正式版本还没有发布
    • 新版本发布后,现有项目不会升级到3.0,2.x还有很长的一段过渡期
    • 3.0项目地址:github.com/vuejs/vue-n…
  • 源码目录结构
src
  ├─compiler	编译相关
  ├─core	Vue 核心库
  ├─platforms	平台相关代码
  ├─server	SSR,服务端渲染
  ├─sfc		.vue 文件编译为 js 对象
  └─shared	公共的代码

了解 Flow

  • 官网:flow.org
  • JavaScript的静态类型检查器
  • Flow的静态类型检查错误是通过静态类型推断实现实现的
    • 文件开头通过// @flow或者/* @flow */声明
/* @flow */
function square(n: number): number {
  return n * n;
}
square("2"); // Error!   

调试设置

打包

  • 打包工具 Rollup
    • Vue.js 源码的打包工具使用的是 Rollup,比Webpack轻量
    • Webpack 把所有文件当做模块,Rollup只处理js文件更适合在Vue.js这样的库中使用
    • Rollup 打包不会生成冗余的代码
  • 安装依赖
npm i
  • 设置 sourcemap
    • package.josn文件中的dev脚本中添加参数 --sourcemap
"dev":"rollup-w-cscripts/config.js--sourcemap--environmentTARGET:web- full-dev" 
  • 执行 dev
    • npm run dev 执行打包,用的rollup,-w 参数是监听文件的变化,文件变化自动重新打包 调试
  • examples 的示例中引入的vue.min.js改为vue.js
  • 打开 Chrome 的调试工具中的source

Vue 的不同构建版本

  • npm run build 重新打包所有文件
UMDCommonJSES Module
Fullvue.jsvue.common.jsvue.esm.js
Runtime-onlyvue.runtime.jsvue.runtime.common.jsvue.runtime.esm.js
Full (production)vue.min.js
Runtime-only (production)vue.runtime.min.js

术语

  • 完整版:同时包含编译器和运行时的版本。
  • 编辑器:用来将模板字符串编译成为 JavaScript 渲染函数的代码,体积大,效率低。
  • 运行时:用来创建 Vue 示例、渲染并处理虚拟 DOM 等代码,体积小、效率高。基本上就是除去编译器的代码。
  • UMD:UMD 版本通用的模块版本,支持多种模块化方式(例如可直接挂到window对象上)。 vue.js 默认文件就是运行时 + 编译器的UMD版本。
  • CommonJS(cjs): CommonJS 版本用来配合老的打包工具比如 Browserify 或 webpack 1。
  • ES Module:从2.6开始Vue会提供两个ES Modules(ESM)构建文件,为现代打包工具提供的版本。
    • ESM 格式被设计为可以被静态分析(编译时处理),所以打包工具可以利用这一点进行"tree-shaking"并将用不到的代码排除出最终的包。
    • ES6模块与CommonJS模块的差异 Runtime + Compiler vs. Runtime-only
// Compiler
// 需要编译器,把 template 转换成 render 函数 
// const vm = new Vue({
//   el: '#app',
//   template: '<h1>{{ msg }}</h1>',
//   data: {
//     msg: 'Hello Vue'
//   }
// })

// Runtime
// 不需要编译器
const vm = new Vue({
  el: '#app',
  render (h) {
    return h('h1', this.msg) 
  },
  data: {
    msg: 'Hello Vue'
  } 
})
  • 推荐使用运行时版本,因为运行时版本相比完整版体积要小大约 30%
  • 基于 Vue-CLI 创建的项目默认使用的是 vue.runtime.esm.js
    • 通过查看 webpack 的配置文件
    vue inspect > output.js
    
  • 注意: *.vue 文件中的模板是在构建时预编译的,最终打包后的结果不需要编译器,只需要运行时版本即可

寻找入口文件

  • 查看 dist/vue.js 的构建过程

执行构建

npm run dev
# "dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"
# --environment TARGET:web-full-dev 设置环境变量 TARGET
  • script/config.js 的执行过程
    • 作用:生成 rollup 构建的配置文件
    • 使用环境变量 TARGET:web-full-dev
    // 判断环境变量是否有 TARGET
    // 如果有的话 使用 genConfig() 生成 rollup 配置文件 
    if (process.env.TARGET) {
      module.exports = genConfig(process.env.TARGET) 
    } else {
      // 否则获取全部配置
      exports.getBuild = genConfig
      exports.getAllBuilds = () => Object.keys(builds).map(genConfig)
    } 
    
  • genConfig(name)
    • 根据环境变量 TARGET 获取配置信息
    • builds[name] 获取生成配置的信息
    // 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
    },
    
  • resolve()
    • 获取入口和出口文件的绝对路径
const aliases = require('./alias')
const resolve = p => {
  // 根据路径中的前半部分去alias中找别名 
  const base = p.split('/')[0]
  if (aliases[base]) {
    return path.resolve(aliases[base], p.slice(base.length + 1)) 
  } else {
    return path.resolve(__dirname, '../', p) }
}   

结果

  • 把 src/platforms/web/entry-runtime-with-compiler.js 构建成 dist/vue.js,如果设置 -- sourcemap 会生成 vue.js.map
  • src/platform 文件夹下是 Vue 可以构建成不同平台下使用的库,目前有 weex 和 web,还有服务器端渲染的库

从入口开始

  • src/platform/web/entry-runtime-with-compiler.js 通过查看源码解决下面问题
  • 观察以下代码,通过阅读源码,回答在页面上输出的结果
const vm = new Vue({
  el: '#app',
  template: '<h3>Hello template</h3>',
  render (h) {
    return h('h4', 'Hello render')
  }
})
  • 阅读源码记录
    • el 不能是 body 或者 html 标签
    • 如果没有 render,把 template 转换成 render 函数
    • 如果有 render 方法,直接调用 mount 挂载 DOM
// 1. el 不能是 body 或者 html
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
}
const options = this.$options 
if (!options.render) {
  // 2. 把 template/el 转换成 render 函数
......
}
  // 3. 调用 mount 方法,挂载 DOM
return mount.call(this, el, hydrating)
  • 调试代码
    • 调试的方法
const vm = new Vue({
  el: '#app',
  template: '<h3>Hello template</h3>',
  render (h) {
    return h('h4', 'Hello render')
  }
})

Vue 的构造函数在哪?

Vue 实例的成员/Vue 的静态成员从哪里来的?

Vue 初始化的过程

Vue 的构造函数在哪里?

  • src/platform/web/entry-runtime-with-compiler.js 中引用了 './runtime/index'
  • src/platform/web/runtime/index.js
    • 设置 Vue.config
    • 设置平台相关的指令和组件
      • 指令 v-model、v-show
      • 组件 transition、transition-group
    • 设置平台相关的 patch 方法(打补丁方法,对比新旧的 VNode)
    • 设置 $mount 方法,挂载 DOM
// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives) extend(Vue.options.components, platformComponents)
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
// public mount method
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}
  • src/platform/web/runtime/index.js 中引用了 'core/index'
  • src/core/index.js
    • 定义了 Vue 的静态方法
    • initGlobalAPI(Vue)
  • src/core/index.js 中引用了 './instance/index'
  • src/core/instance/index.js
    • 定义了 Vue 的构造函数
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')
}
  // 调用 _init() 方法
  this._init(options) 
}
// 注册 vm 的 _init() 方法,初始化 vm 
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch 
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy 
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

四个导出 Vue 的模块

  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口
    • 重写了平台相关的 $mount() 方法(使其可以编译模板 template->render函数)
    • 注册了 Vue.compile() 方法,传递一个 HTML 字符串返回 render 函数
    • 核心作用:增加了编译的功能
  • src/platforms/web/runtime/index.js
    • web 平台相关
    • 注册和平台相关的全局指令:v-model、v-show
    • 注册和平台相关的全局组件: v-transition、v-transition-group
    • 全局方法:
      • patch:把虚拟 DOM 转换成真实 DOM
      • $mount:挂载方法
  • src/core/index.js
    • 与平台无关
    • 设置了 Vue 的静态方法,initGlobalAPI(Vue)
  • src/core/instance/index.js
    • 与平台无关
    • 定义了构造函数,调用了 this._init(options) 方法
    • 给 Vue 中混入了常用的实例成员

Vue初始化 - 静态成员

src/core/global-api/index.js

  • 初始化 Vue 的静态方法
// 注册 Vue 的静态属性/方法 
initGlobalAPI(Vue)

src/core/global-api/index.js

// 初始化 Vue.config 对象 
Object.defineProperty(Vue, 'config', configDef)

// exposed util methods.
// NOTE: these are not considered part of the public API - avoid relying on // them unless you are aware of the risk.
// 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们 
Vue.util = {
  warn,
  extend,
  mergeOptions,
  defineReactive
}
// 静态方法 set/delete/nextTick 
Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick

// 2.6 explicit observable API
// 让一个对象可响应
Vue.observable = <T>(obj: T): T => {
  observe(obj)
  return obj 
}
// 初始化 Vue.options 对象,并给其扩展
// components/directives/filters/_base
Vue.options = Object.create(null) ASSET_TYPES.forEach(type => {
  Vue.options[type + 's'] = Object.create(null) 
})
// this is used to identify the "base" constructor to extend all plain- object
// components with in Weex's multi-instance scenarios. 
Vue.options._base = Vue

// 设置 keep-alive 组件
extend(Vue.options.components, builtInComponents)

// 注册 Vue.use() 用来注册插件
initUse(Vue)
// 注册 Vue.mixin() 实现混入 
initMixin(Vue)
// 注册 Vue.extend() 基于传入的 options 返回一个组件的构造函数 
initExtend(Vue)
// 注册 Vue.directive()、 Vue.component()、Vue.filter() 
initAssetRegisters(Vue)

Vue初始化 - 实例成员

src/core/instance/index.js

  • 定义 Vue 的构造函数
  • 初始化 Vue 的实例成员
// 此处不用 class 的原因是因为方便,后续给 Vue 实例混入实例成员 
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) 
}
// 注册 vm 的 _init() 方法,初始化 vm initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)
  • initMixin(Vue)
    • 初始化 _init() 方法

首次渲染过程

  • Vue 初始化完毕,开始真正的执行
  • 调用 new Vue() 之前,已经初始化完毕
  • 通过调试代码,记录首次渲染过程

数据响应式原理

通过查看源码解决下面问题

  • vm.msg = { count: 0 } ,重新给属性赋值,是否是响应式的?
  • vm.arr[0] = 4 ,给数组元素赋值,视图是否会更新
  • vm.arr.length = 0 ,修改数组的 length,视图是否会更新
  • vm.arr.push(4) ,视图是否会更新

响应式处理的入口

  • src\core\instance\init.js
    • initState(vm) vm 状态的初始化
    • 初始化了 _data、_props、methods 等
  • src\core\instance\state.js
// 数据的初始化
if (opts.data) {
  initData(vm)
} else {
  observe(vm._data = {}, true /* asRootData */) 
}
  • initData(vm) vm 数据的初始化
function initData (vm: Component) {
  let data = vm.$options.data
  // 初始化 _data,组件中 data 是函数,调用函数返回结果 
  // 否则直接返回 data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  ......
  // proxy data on instance
  // 获取 data 中的所有属性
  const keys = Object.keys(data) // 获取 props / methods
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  // 判断 data 上的成员是否和 props/methods 重名 ......
  // observe data
  // 数据的响应式处理
  observe(data, true /* asRootData */)
}
  • src\core\observer\index.js
    • observe(value, asRootData)
    • 负责为每一个 Object 类型的 value 创建一个 observer 实例
export function observe (value: any, asRootData: ?boolean): Observer | void
{
  // 判断 value 是否是对象
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 如果 value 有 __ob__(observer对象) 属性 结束
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__ 
  } else if (shouldObserve && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue) {
    // 创建一个 Observer 对象
    ob = new Observer(value)
  }
  if (asRootData && ob) { 
    ob.vmCount++
  }
  return ob 
}

Observer

  • src\core\observer\index.js
    • 对对象做响应化处理
    • 对数组做响应化处理
export class Observer { // 观测对象
  value: any;
  // 依赖对象
  dep: Dep;
  // 实例计数器
  vmCount: number; // number of vms that have this object as root $data
  
  constructor (value: any) { 
    this.value = value 
    this.dep = new Dep()
    // 初始化实例的 vmCount 为0 
    this.vmCount = 0
    // 将实例挂载到观测对象的 __ob__ 属性,设置为不可枚举 
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      // 数组的响应式处理 
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 为数组中的每一个对象创建一个 observer 实例
      this.observeArray(value) 
    } else {
      // 对象的响应化处理
      // 遍历对象中的每一个属性,转换成 setter/getter 
      this.walk(value)
     } 
  }
  
  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when * value type is Object.
   */
  walk (obj: Object) {
    // 获取观察对象的每一个属性
    const keys = Object.keys(obj)
    // 遍历每一个属性,设置为响应式数据
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    } 
  }
  
  /**
   * Observe a list of Array items. 
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  } 
}
  • walk(obj)
    • 遍历 obj 的所有属性,为每一个属性调用 defineReactive() 方法,设置 getter/setter

defineReactive()

  • src\core\observer\index.js
  • defineReactive(obj, key, val, customSetter, shallow)
    • 为一个对象定义一个响应式的属性,每一个属性对应一个 dep 对象
    • 如果该属性的值是对象,继续调用 observe
    • 如果给属性赋新值,继续调用 observe
    • 如果数据更新发送通知

对象响应式处理

// 为一个对象定义一个响应式的属性 
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean 
){
  // 1. 为每一个属性,创建依赖对象实例
  const dep = new Dep()
  // 获取 obj 的属性描述符对象
  const property = Object.getOwnPropertyDescriptor(obj, key) 
  if (property && property.configurable === false) {
    return
  }
  // 提供预定义的存取器函数
  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  // 2. 判断是否递归观察子对象,并将子对象属性都转换成 getter/setter,返回子观察对象 
  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果预定义的 getter 存在则 value 等于getter 调用的返回值 
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      // 如果存在当前依赖目标,即 watcher 对象,则建立依赖
      if (Dep.target) {
        // dep() 添加相互的依赖
        // 1个组件对应一个 watcher 对象
        // 1个watcher会对应多个dep(要观察的属性很多)
        // 我们可以手动创建多个 watcher 监听1个属性的变化,1个dep可以对应多个watcher 
        dep.depend()
        // 如果子观察目标存在,建立子对象的依赖关系,将来 Vue.set() 会用到
        if (childOb) {
          childOb.dep.depend()
          // 如果属性是数组,则特殊处理收集数组对象依赖 
          if (Array.isArray(value)) {
            dependArray(value)
          }
        } 
      }
      // 返回属性值
      return value
    },
    set: function reactiveSetter (newVal) {
      // 如果预定义的 getter 存在则 value 等于getter 调用的返回值
      // 否则直接赋予属性值
      const value = getter ? getter.call(obj) : val
      // 如果新值等于旧值或者新值旧值为null则不执行
      /* 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()
      }
     // 如果没有 setter 直接返回
     // #7981: for accessor properties without setter 
     if (getter && !setter) return
     // 如果预定义setter存在则调用,否则直接更新新值
     if (setter) {
       setter.call(obj, newVal) 
     } else {
       val = newVal 
     }
     // 3. 如果新值是对象,观察子对象并返回 子的 observer 对象 
     childOb = !shallow && observe(newVal)
     // 4. 发布更改通知
     dep.notify()
   }

数组响应式处理

  • Observer 的构造函数中
  • 处理数组修改数据的方法
    • src\core\observer\array.js

Dep 类

  • src\core\observer\dep.js
  • 依赖对象
  • 记录 watcher 对象
  • depend() -- watcher 记录对应的 dep 发布通知

Watcher 类

  • Watcher 分为三种,Computed Watcher、用户 Watcher (侦听器)、渲染 Watcher
  • 渲染 Watcher 的创建时机
    • /src/core/instance/lifecycle.js