Vue2 源码学习

Vue2 源码分析

基于 Vue 2.6.10 版本
vue2.0 在实现“响应数据绑定”的同时引入了 virtual-dom

目录结构

  • dist ---------------------------------- 构建后文件的输出目录
  • xamples ----------------------------- 存放使用Vue开发的的例子
  • flow --------------------------------- 类型声明(V3 使用的TS)
  • packages --------------------------- xxx
  • scripts ------------------------------ 构建相关的脚本
  • package.json ----------------------- 项目依赖
  • test --------------------------------- 测试文件相关
  • src ---------------------------------- 源码目录
    • platforms ------------------------- 平台相关的代码
      • web
        • compiler
        • runtime
        • entry-compiler.js ------------------ 只有 compiler
        • entry-rumtime-with-compiler.js --- 独立构建
        • entry-runtime.js ------------------- 运行时构建(默认构建方式)
        • entry-server-basic-renderer.js
        • entry-server-renderer.js
      • weex
        • ...
    • compiler ----------------------- 模板编译器,将 template 编译为 render()
      • codegen -------------------- AST 转 render()
      • parser ---------------------- 模板字符串转 AST
      • index.js --------------------- 入口文件
      • optimizer.js ---------------- 分析静态树,优化vdom渲染
    • core ----------------------- 通用的,与平台无关代码(核心)
      • components ------------------ 抽象的通用组件
      • global-api --------------------- Vue构造函数挂载全局方法(静态方法)或属性的代码
      • instance ---------------------- Vue构造函数设计相关代码
      • observer ----------------- 数据收集与订阅的代码
      • util --- 工具方法
      • vdom --- vdom创建与patch的代码
      • index.js --- 入口文件
      • config.js
    • server ---- Vue SSR
    • sfc --- 对.vue文件的解析逻辑
      • parser.js
    • shared --- 提供全局可用的工具函数
      • constants.js --- 常量
      • utils.js ----- 工具函数

vue.js 的组成是由 core + 对应的平台相关代码构成。

独立构建运行时构建只是 platforms 下 web 平台的两种选择
运行时构建,是不包含模板(template)到render函数的编译器
独立构建,包含模板(template)到render函数的编译器

dist 目录下,可以看到各种构建后的输出文件

dist目录
  • 针对不同环境的的构建输出,如:dev、prod、browser
  • 有经过压缩的构建输出,如:min
  • 有不同模块标准的构建输出,如:CommonJS、ES Module、UMD
  • 有不同行为的构建输出,如:包含runtime 与不包含runtime

new Vue()

在 package.json,scripts很多命令行脚本,任取其中一条,以此为入口开始探索 new Vue() 时会发生什么

scripts

在 scripts/config.js 中,根据 TARGET: 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', // 构建模式(开发 or 生产)
  alias: { he: './entity-decoder' },
  banner
}

通过入口文件溯源到Vue构造函数的整个流程

process

./instance/index

vue 核心代码,不平台共用的内容

按照上述路径,找到了Vue构造函数的定义,调用初始化函数 _init(),导出 Vue


import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'

// Vue 构造函数
function Vue (options) {
  // 初始化函数
  this._init(options)
}
// Vue.prototype 挂载属性或方法
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

  • initMixin()

export function initMixin(Vue) {
  // Vue 原型上添加 _init
  Vue.prototype._init = function () { }
}

  • stateMixin()

export function stateMixin (Vue) {
  Object.defineProperty(Vue.prototype, '$data', dataDef)
  Object.defineProperty(Vue.prototype, '$props', propsDef)
  Vue.prototype.$set = set
  Vue.prototype.$delete = del
  Vue.prototype.$watch = function () {}
}


  • eventsMixin()

export function eventsMixin (Vue>) {
  Vue.prototype.$on = function () { }
  Vue.prototype.$once = function () {}
  Vue.prototype.$off = function () { }
  Vue.prototype.$emit = function () {}
}

  • lifecycleMixin()

export function lifecycleMixin (Vue) {
  Vue.prototype._update = function () { }
  Vue.prototype.$forceUpdate = function () { }
  Vue.prototype.$destroy = function () {}
}

  • renderMixin()

export function renderMixin (Vue) {
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype);
  
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this)
  }
  Vue.prototype._render = function () {}
}

// installRenderHelpers/index.js
export function installRenderHelpers (target) {
   // target is Vue.prototype
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
  target._d = bindDynamicKeys
  target._p = prependModifier
}

通过多个mixin,在 Vue.prototype 中挂载了若干方法和属性

属性或方法 文件 备注
Vue.prototype._init() initMixin
Vue.prototype.$set() stateMixin observer/index..js 中定义的方法
Vue.prototype.$delete() stateMixin observer/index..js 中定义的方法
Vue.prototype.$watch() stateMixin
Vue.prototype.$data stateMixin 属性
Vue.prototype.$props stateMixin 属性
Vue.prototype.$on() eventsMixin
Vue.prototype.$once() eventsMixin
Vue.prototype.$off() eventsMixin
Vue.prototype.$emit() eventsMixin
Vue.prototype._update() lifecycleMixin
Vue.prototype.$forceUpdate() lifecycleMixin
Vue.prototype.$destroy() lifecycleMixin
Vue.prototype.$nextTick() renderMixin
Vue.prototype._render() renderMixin
Vue.prototype._[x] installRenderHelpers/index.js [x] 表示若干单字母方法

core/index

vue 核心代码,平台共用的部分


// 在 Vue 上挂载静态方法和属性
initGlobalAPI(Vue);
// Vue 原型上挂载与SSR相关的内容
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})
Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    return this.$vnode && this.$vnode.ssrContext
  }
})
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
Vue.version = '__VERSION__'
export default Vue

进入 initGlobalAPI()


export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  // ...
  Object.defineProperty(Vue, 'config', configDef);
 
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  Vue.set = set; // observer/index.js
  Vue.delete = del; // observer/index.js
  Vue.nextTick = nextTick;
  // 2.6 explicit observable API
  Vue.observable = (obj: T): T => {
    observe(obj)
    return obj
  }
  Vue.options = Object.create(null); // 静态属性 options
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null);
    // options: { components: {}, filters: {}, directives: {} }
  })
   // _base 关联到自身
  Vue.options._base = Vue;
  extend(Vue.options.components, builtInComponents)
  // 可以继续深入...
  initUse(Vue)
  initMixin(Vue)
  initExtend(Vue)
  initAssetRegisters(Vue)
}

./runtime/index

安装平台特有工具


// 安装平台特有的工具
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// 安装平台运行时的指令和组件
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 () {}

// other code....

export default Vue

web/entry-runtime-with-compiler.js


// 缓存 $mount (来自 web/runtime/index.js)
const mount = Vue.prototype.$mount;

// 覆盖 $mount
Vue.prototype.$mount = function () {
    
   const options = this.$options
    // 没有render函数
    if (!options.render) { }

    return mount.call(this, el, hydrating)
}
// Vue 挂载 compile
// compileToFunctions : 将模板编译为 render 函数
Vue.compile = compileToFunctions
export default Vue

以上还原了 Vue 构造函数的细节

Vue实例代码分析

通过 new Vue() 创建一个 vue 实例,通过代码来探究在创建实例时,做了哪些事情?如:怎么就实现了响应式数据绑定?

new Vue()

// 子组件
const child = {
    props: {
        msg: String,
        count: Number,
    },
    template: `<div><p>{{msg}}</p><p>{{count}}</p></div>`,
};
// 父组件
const app = Vue.new({
    el: '#app',
    data: {
        message: '父组件值',
        count: 1,
    },
    methods: {},
    components: {
        child,
    },
});

<div id="app">
    <child :msg="message" :count="count"></child>
</div>

_init()

调用 new Vue(),首先进入 Vue.prototype._init() (构造函数挂载的第一个方法)


let uid = 0;

Vue.prototype._init = function (options) {
    const vm = this
    // vue uid
    vm._uid = uid++
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    if (options && options._isComponent) {
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );
    }
    // 省略部分代码
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate');
    initInjections(vm)
    initState(vm)
    initProvide(vm)
    callHook(vm, 'created');
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

_init() 在开始时,在 this 对象上定义了两个属性 _uid 和 _isVue,
然后判断有没有定义 options._isComponent这里会走 else 分支,即 mergeOptions()

接着调用initLifecycle、initEvents、initRender、initState,且在 initState 前后分别回调了生命周期钩子 beforeCreate 和 created。看到这里也就明白了为什么 created 的时候不能操作DOM了。因为这个时候还没有渲染真正的DOM元素到文档中。created 仅仅代表数据状态的初始化完成

initState(vm)


export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm); 
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

initData(vm)

data 属性存在,调用 initData(vm)


function initData (vm: Component) {
  let data = vm.$options.data;
  // _data 是 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;
  // 循环
  while (i--) {
    const key = keys[i];
   
    if (props && hasOwn(props, key)) { 
      // warning code......
    } else if (!isReserved(key)) { // key 不是以 $ 或 _  开头
      // 将 data 中的属性,都通过Object.defineProperty()绑定到了vue实例上,可以直接通过‘this.属性’进行访问
      proxy(vm, `_data`, key)
    }
  }
  //  第一步:observe data (双向数据绑定的核心)
  observe(data, true /* asRootData */); // data 就是 Vue 实例中的 data
}

observe(data, true)

// 源码 :src/core/observer/index.js

export function observe(value: any, asRootData: ?boolean): Observer | void {
  // 不是object 或是 VNode 实例时,直接返回
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void;
  //TODO: __ob__ 在哪里定义的?
  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
  ) {
    // 第二步
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  // 返回 Observer 实例
  return ob
}

Observer()


export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
  constructor(value: any) {
    this.value = value;
     // // TODO:  猜想:Dep实例是 Watcher 和 Observer 之间的纽带
    this.dep = new Dep();
    this.vmCount = 0;
    // 把自身 this 添加到 value 的 __ob__ 属性上
    // 即:vue 实例的 data.__ob__ 关联到了 Observer 实例
    def(value, '__ob__', this); // def => Object.defineProperty 封装
    //  对 value 的类型进行判断(数组和对象的处理方式不同)
    if (Array.isArray(value)) { // 如果是数组
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      // 数组处理
      this.observeArray(value)
    } else {
      // 对象处理
      this.walk(value)
    }
  }
   // 对 Object 进行响应式处理
   // eg: obj = {a: 1, b:2, c:3 }
  walk(obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]); // 对 obj 的属性循环调用 defineReactive
    }
  }
  // 对数组进行响应式处理
  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

defineReactive(obj, propName)



export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 每个对象属性都会有一个 Dep 实例,用来保存依赖(Watcher 对象)
  const dep = new Dep()
  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]
  }
  // 结合 observe(),将对象的对象也转变成Observer对象
  let childOb = !shallow && observe(val); // 属性值可能是个对象
  // 最核心部分:调用 Object.defineProperty() 给 data(Vue 实例中的data)的每个属性添加 getter 和 setter 方法
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val;
      // TODO: Dep.target 在哪里设置的?Dep.target 本质是什么?
      if (Dep.target) {
        dep.depend(); // 依赖收集
        if (childOb) {
          childOb.dep.depend(); // 处理子元素的依赖 watcher
          if (Array.isArray(value)) { //如果是数组,进一步处理
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter(newVal) {
      const value = getter ? getter.call(obj) : val
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 对数据重新 observe(), 更新数据的依赖关系
      childOb = !shallow && observe(newVal);
      // TODO: 留一个疑问?
      dep.notify(); // 通知 dep 进行数据更新
    }
  })
}

vm.$mount(vm.$options.el)

initData(vm) 执行完成后,完成了响应数据的构建,回到_init()中继续执行callHook(vm, 'created'); 调用钩子created,然后执行 vm.$mount() 方法

mount
// web/entry-runtime-with-compiler.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
   // 1、获取挂载的 el 节点
  el = el && query(el);
 // el 不能是 <body> 或  <html>
  const options = this.$options;
  // 没有render函数
  if (!options.render) { 
    let template = options.template;
    if (template) { // 存在template
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template);
        }
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this;
      }
    } else if (el) {
      template = getOuterHTML(el); // 直接获取页面的html代码作为模板
    }
    // 编译template
    if (template) {
    
      // compileToFunctions 将 模板编译为AST,经过静态优化,最后处理为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
 
    }
  }
  // runtime/index.js
  return mount.call(this, el, hydrating)
}

mountComponent()

生成render 函数之后,调用 mountComponent()

  • 首先调用 beforeMount()
  • 接着执行 new Watcher(vm, updateComponent, noop, {}, true)
  • 最后callHook(vm, ''mounted),进行组件挂载。

export function mountComponent (
  vm: Component, // vue 实例
  el: ?Element, // 挂载节点
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) { // 没有render函数
    vm.$options.render = createEmptyVNode;
    // .....
  }
  //1、 首先
  callHook(vm, 'beforeMount');
  // ......
  let updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  // 2、然后
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) { // vm 未挂载且未销毁
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;
 // 3、最后
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm;
}

new Watcher(....)

export default class Watcher {

constructor (vm, expOrFn, cb, options, isRenderWatcher ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this;// watcher实例 绑定到 vue 实例的 _watcher 属性上
    }
    vm._watchers.push(this); // 将当前 watcher实例推送到对应的 Vue 实例中
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    // 省略部分内容
    // parse expression for getter
    if (typeof expOrFn === 'function') 
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
    }
    // 如果 lazy 不为 true, 则执行 get 函数进行依赖收集
    this.value = this.lazy ? undefined  : this.get()
  }
} 

在构造函数由于 this.lazy=false; 所以 this.value = this.lazy ? undefined : this.get(); 将会调用 get() 方法获取值

 // 执行 getter(),进行依赖收集
  get () {
    // 设置全局变量 Dep.target, 将当前的 watcher 实例保存在这个全局变量中
    pushTarget(this);
    let value;
    const vm = this.vm; // vue 实例
    try {
      // 调用 getter 函数,进入 get() 进行依赖收集操作
      // 此时 this.getter()  就是:() => { vm._update(vm._render(), hydrating) }
      value = this.getter.call(vm, vm);
    } catch (e) {
     // ......
    } finally {
      if (this.deep) {
        traverse(value);
      }
      popTarget(); // 将全局变量 Dep.target 置为 null
      this.cleanupDeps()
    }
    return value
  }

vm._render()

返回一个 VNode (虚拟节点)

// instance/render.js

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }
   
    vm.$vnode = _parentVnode

    let vnode
    try {
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
    } finally {
      currentRenderingInstance = null
    }
 
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // ...
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }


vm._update()

// instance/lifecycle.js

 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
  
    if (!prevVnode) {
      // 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // 更新
      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
    }
  }

总结

网络图

推荐阅读更多精彩内容