05-如何学习Vue源码

1,326 阅读6分钟

打开Vue源码,两眼一抹黑,连入口文件都找不到,接下来,我带着大家一步步去探索一下vue源码。

获取vue源码

地址:github.com/vuejs/vue

clone项目:git clone https://github.com/vuejs/vue.git

版本:2.6.10

目录结构

├── dist # 所有输出的运行时文件
│   ├── common # nodejs模块cjs,旧版打包器webpack1.0、browserify
│   ├── esm # es模块化,常用语webpack2.0
│   ├── runtime # 说明输出的库仅有核心运行时代码,没有编译器
│   └── vue.js # umd-universal module definition,兼容cjs和amd规范,常用于浏览器环境 
├── examples # 官方准备的很多案例
├── flow # 强类型编译语言,类似于TS,flow这种语言有类型声明文件,告诉你实现了哪些方法,这些方法的基本签名是什么样的
├── packages # 里面是独立的包,比如:服务端的渲染器、weex等
├── scripts # 里面存放的是打包的脚本,比如用什么脚本打包,打包的配置是什么样的,都在这个目录下
├── src # 核心代码存放的地方
│   ├── compiler # 编译器
│   ├── core # vue核心代码
│   ├── platforms # 平台特有代码
│   │   ├── web # 浏览器
│   │   └── weex # 移动端
│   ├── server # 服务端相关
│   ├── sfc # 单文件解释器
│   └── shared # 共享代码
├── test # 测试文件
├── types # TS类型文件,TS受欢迎,所以编写了两套类型文件,vue3.0则完全是TS类型文件了
├── .eslintignore # eslint 忽略文件
├── .eslintrc.js  # eslint 配置项
├── .gitignore  # git 忽略文件
└── package.json # 项目基本信息,包依赖信息等

调试环境搭建

  1. 安装依赖

yarn/npm i

  1. 修改dev脚本
# package.json:添加sourcemap
"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"

sourceMap:
简单说,sourceMap就是一个文件,里面储存着位置信息。
仔细点说,这个文件里保存的,是转换后代码的位置,和对应的转换前的位置。
有了它,出错的时候,通过断点工具可以直接显示原始代码,而不是转换后的代码。
rollup:
打包库,纯js打包

  1. 运行

npm run dev

寻找入口文件

  • package.json

dev脚本中 -c scripts/config.js 指明配置文件所在,参数TARGET:web-full-dev 指明输出文件配置项

"dev": "rollup -w -c scripts/config.js --sourcemap --environment TARGET:web-full-dev"

  • scripts/config.js

config.js中搜索web-full-dev

// 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
  },
  • 运行后也能看到入口文件
bundles /Users/qiaoxu/Desktop/2018-study /优秀源码/vue源码/vue/src/platforms/web/entry-runtime-with-compiler.js → dist/vue.js...
created dist/vue.js in 1.7s

初始化流程

1. 从入口文件开始分析

位置:src/platforms/web/entry-runtime-with-compiler.js

功能:入口文件主要做了一件事,扩展默认$mount方法,处理template与el选项

render优先级最高
render>template>el
$mount做了什么:将用户编写的模板——变成虚拟dom——变成真实dom

2. 寻找Vue的构造函数

位置:src/platforms/web/runtime/index.js

功能:

  • 定义$mount:挂载根组件到指定宿主元素;

  • 定义_patch_:补丁函数,执行patching算法进行更新

3. 继续寻找Vue的构造函数

位置:src/core/index.js

功能:实现全局API

Vue.set = set
Vue.delete = del
Vue.nextTick = nextTick
initUse(Vue) // 实现Vue.use函数
initMixin(Vue) // 实现Vue.mixin函数
initExtend(Vue) // 实现Vue.extend函数
initAssetRegisters(Vue) // 注册实现Vue.component/directive/filter

4. 最终找到Vue的构造函数

位置:src/core/instance/index.js

功能:定义了Vue构造函数,文件中使用了若干混入模式,对Vue进行了扩展

function Vue (options) { 
    // 构造函数仅执行了_init 
    this._init(options)
}
initMixin(Vue) // 实现init函数
stateMixin(Vue) // 状态相关api $data,$props,$set,$delete,$watch 
eventsMixin(Vue)// 事件相关api $on,$once,$off,$emit 
lifecycleMixin(Vue) // 生命周期api _update,$forceUpdate,$destroy 
renderMixin(Vue)// 渲染api _render,$nextTick

5. Vue初始化做了什么

位置:src/core/instance/init.js

功能:

初始化顺序:生命周期、事件监听、渲染、beforeCreate、注入、组件状态、provide、created

initLifecycle(vm) // $parent,$root,$children,$refs 
initEvents(vm) // 事件监听:处理父组件传递的监听器
initRender(vm) // $slots,$scopedSlots,_c,$createElement 
callHook(vm, 'beforeCreate')
initInjections(vm) // 获取注入数据
initState(vm) // 初始化props,methods,data,computed,watch 
initProvide(vm) // 提供数据注入
callHook(vm, 'created')

// 如果存在el宿主,则自动执行挂载,不需要手动挂载
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

6. $mount执行的核心方法mountComponent

位置:mountComponent src/core/instance/lifecycle.js

功能:渲染(生成虚拟dom)与更新(将虚拟dom转为真实dom)

// 定义update方法
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      // 执行patch
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // 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
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
}
// 定义updateComponent
let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`
    
      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)
    
      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
} else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating) // 执行update方法
    }
}

// 执行updateComponent
new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
}, true /* isRenderWatcher */)

数据响应式

1. initData

位置:src\core\instance\state.js

功能:初始化数据

具体实现是在Vue初始化时,会调用initState,它会初始化data,props等,这里着重关注data初始化

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
  ...
  // observe data执行数据响应化
  observe(data, true /* asRootData */)
}

2. observer

位置:src/core/observer/index.js

功能:Observer对象根据数据类型执行对应的响应化操作

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()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {// 数组
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {// 对象
      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])
    }
  }
}

3. 依赖收集Object.defineProperty

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        // depend()是相互添加引用的过程
        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()
    }
  })
}

4. dep与watcher之间的关系

  • dep

位置:src/core/observer/dep.js

功能:Dep负责管理一组Watcher,包括watcher实例的增删及通知更新

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

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

  depend () {
    if (Dep.target) {
      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()
    }
  }
}
  • watcher

位置:src/core/observer/watcher.js

功能:Watcher解析一个表达式并收集依赖,当数值变化时触发回调函数,常用于$watch API和指令中。 每个组件也会有对应的Watcher,数值变化会触发其update函数导致重新渲染

/**
   * Add a dependency to this directive.
   */
addDep (dep: Dep) {
    const id = dep.id
    if (!this.newDepIds.has(id)) {
      // watcher保存dep
      this.newDepIds.add(id)
      this.newDeps.push(dep)
      if (!this.depIds.has(id)) {
      // dep保存watcher
        dep.addSub(this)
      }
    }
}

数组响应式

**1.修改数组原型中的7个可以改变内容的方法 **

位置:src\core\observer\array.js

功能:对数组的原型方法特殊处理

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})

2. Observer中覆盖数组原型

位置:src\core\observer\index.js

功能:将修改过的数组方法覆盖给数组原型

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
} 

function protoAugment (target, src: Object) {
  /* eslint-disable no-proto */
  target.__proto__ = src
  /* eslint-enable no-proto */
}