Vue 源码全览 (一)

144 阅读4分钟

我尝试一下将 Vue,以一个图谱的形式展现出来。

Vue 是一个构造函数,通过给 Vue 原型和 Vue 函数添加函数以及对象,这样就可以拓展 Vue,代码如下。

注意 _ 开头的变量为 Vue 的私有变量, $开头的为 Vue 暴露出来的原型方法/属性

function Vue(options) {
  // 初始化生命周期
  this._init(options)
}

initMixin(Vue) // 初始化组件,各种参数,挂载组件。
stateMixin(Vue) // 初始化数据相关的实例方法,下面介绍
eventsMixin(Vue) // 事件方法的初始化 $on、$off、$once 、$emit
lifecycleMixin(Vue) // 生命周期初始化,$forceUpdate $destroy
renderMixin(Vue) // $nextTick _render方法

initMixin

想详细了解这个方法initMixin详解

这个方法做了初始化组件到挂载的整个过程。

stateMixin

在上面的 initMixin 已经初始化 State,但是还有其他操作数据的方法,比如$set$del$watch这些全局方法还没有处理。

这些方法比较简单,就不另外分文章出来解析。

export function stateMixin(Vue: Class<Component>) {
  // flow somehow has problems with directly declared definition object
  // when using Object.defineProperty, so we have to procedurally build up
  // the object here.
  const dataDef = {}
  dataDef.get = function () {
    return this._data
  }
  const propsDef = {}
  propsDef.get = function () {
    return this._props
  }
  // 警告data被替换
  if (process.env.NODE_ENV !== "production") {
    dataDef.set = function () {
      warn("Avoid replacing instance root $data. " + "Use nested data properties instead.", this)
    }
    // 警告属性是只读的
    propsDef.set = function () {
      warn(`$props is readonly.`, this)
    }
  }
  Object.defineProperty(Vue.prototype, "$data", dataDef)
  Object.defineProperty(Vue.prototype, "$props", propsDef)

  Vue.prototype.$set = set
  Vue.prototype.$delete = del

  Vue.prototype.$watch = function (expOrFn: string | Function, cb: any, options?: Object): Function {
    const vm: Component = this
    // 这里还留下了一个 用户可以通过 vm.$watch("xx",()....)的调用方法
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options)
    }
    options = options || {}
    options.user = true
    // 在这里才真正去 watcher类里面进行监听
    const watcher = new Watcher(vm, expOrFn, cb, options) // 创建watcher,数据更新调用cb
    // 如果是immediate为true的话,立即执行传进来的回调函数
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value)
      } catch (error) {
        handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
      }
    }
    // 返回一个闭包去关闭定时器,一般是Vue内部销毁watch时调用的
    return function unwatchFn() {
      watcher.teardown()
    }
  }
}

set

在对象上设置属性。 如果该属性不存在,则添加新属性并触发更改通知。

export function set(target, key, val) {
  // 如果是数组 Vue.set(array,1,100); 调用重写的splice方法 (这样可以更新视图)
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key)
    target.splice(key, 1, val)
    return val
  }
  // 如果是对象本身的属性,则直接添加即可
  if (key in target && !(key in Object.prototype)) {
    target[key] = val
    return val
  }
  // 如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
  const ob = (target: any).__ob__
  if (target._isVue || (ob && ob.vmCount)) {
    process.env.NODE_ENV !== "production" &&
      warn(
        "Avoid adding reactive properties to a Vue instance or its root $data " +
          "at runtime - declare it upfront in the data option."
      )
    return val
  }
  // 如果不是响应式的也不需要将其定义成响应式属性
  if (!ob) {
    target[key] = val
    return val
  }
  // 将属性定义成响应式的
  defineReactive(ob.value, key, val)
  // 通知视图更新
  ob.dep.notify()
  return val
}

del

提供删除响应数据的办法

export function del(target, key) {
  // 如果是数组的话,直接删除一项即可,splice是重写过的
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1)
    return
  }
  if (!hasOwn(target, key)) {
    return
  }
  // 是对象的,直接删除即可
  delete target[key]
  if (!ob) {
    return
  }
  // 通知更新
  ob.dep.notify()
}

eventsMixin

事件方法的初始化 $on$off$once$emit

export function initEvents(vm: Component) {
  vm._events = Object.create(null) // 实现发布订阅模式
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners // 所有的事件
  if (listeners) {
    updateComponentListeners(vm, listeners) // 更新组件的事件
  }
}

let target: any

function add(event, fn) {
  target.$on(event, fn)
}

function remove(event, fn) {
  target.$off(event, fn)
}

function createOnceHandler(event, fn) {
  const _target = target
  return function onceHandler() {
    const res = fn.apply(null, arguments)
    if (res !== null) {
      _target.$off(event, onceHandler)
    }
  }
}

export function updateComponentListeners(vm: Component, listeners: Object, oldListeners: ?Object) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, createOnceHandler, vm)
  target = undefined
}

export function eventsMixin(Vue: Class<Component>) {
  const hookRE = /^hook:/
  // 监听事件,_events 添加监听方法,原理是发布订阅
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = this
    // 如果是数组的话,循环监听事件
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn)
      }
    } else {
      // vm._events.change = [fn,fn,fn]
      ;(vm._events[event] || (vm._events[event] = [])).push(fn)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  // 原理简单,重写监听方法,当执行的时候先取消监听方法,接着使用apply执行用户的方法
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on() {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  // 将方法在 _events 中删除 event 中的方法,
  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    // 是数组则循环找到相应的方法删除
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    // 找不到 _events,则返回实例
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }

    // 不传fn则把所有的方法删除
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      // 找到相应的fn,然后删除对应的方法
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== "production") {
      const lowerCaseEvent = event.toLowerCase()
      // 严格限制 event 为大写,因为这可能会涉及到 v-on 监听属性
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
            `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
            `Note that HTML attributes are case-insensitive and you cannot use ` +
            `v-on to listen to camelCase events when using in-DOM templates. ` +
            `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event] // vm._events.change = []
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        // 函数执行
        invokeWithErrorHandling(cbs[i], vm, args, vm, info)
      }
    }
    return vm
  }
}

lifecycleMixin

在这个方法主要是挂载了 _update用于更新组件操作,$forceUpdate用于强制更新组件、$destroy用于销毁组件

注意 $nextTick$mount 也是关于生命周期的全局函数

$mount 在 initMixin中已经挂载完毕了,可以在 initMiXin详细介绍中了解。

$nextTick 在renderMixin中挂载,在下面可以查看。

// 生命周期混合
export function lifecycleMixin(Vue: Class<Component>) {
  // 组件更新方法,用于内部使用
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this;
    // 以前的dom元素
    const prevEl = vm.$el;
    // 以前的虚拟节点
    const prevVnode = vm._vnode;
    // 将vm设置为活跃组件,存在全局变量,返回一个闭包,设置上一个组件变回全局组件,应用于slot
    const restoreActiveInstance = setActiveInstance(vm);
    // 新的虚拟节点
    vm._vnode = vnode;
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 如果不存在 prevVnode,那就直接渲染即可,因为 patch算法 判断有没有传旧节点进来。
    // patch:core/vdom/patch
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode);
    }
    // 重新把刚刚旧组件设置为激活的组件
    restoreActiveInstance();
    // update __vue__ reference
    // 清空 __vue__ 占用的缓存
    if (prevEl) {
      prevEl.__vue__ = null;
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm;
    }
    // if parent is an HOC「high order component高阶组件」, 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.
  };

  // 更新组件
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this;
    // 拿到渲染 watcher 去将至更新
    if (vm._watcher) {
      vm._watcher.update();
    }
  };

  // 销毁组件
  Vue.prototype.$destroy = function () {
    const vm: Component = this;
    // 如果是正在销毁的话,避免重复执行
    if (vm._isBeingDestroyed) {
      return;
    }
    callHook(vm, "beforeDestroy");
    vm._isBeingDestroyed = true;
    // remove self from parent
    const parent = vm.$parent;
    // 在父组件移除子组件
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // teardown watchers
    // 如果有watch的话就会销毁所有的watch
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    let i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    // remove reference from data ob
    // frozen object may not have observer.
    // 监听data数量 --
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--;
    }
    // call the last hook...
    vm._isDestroyed = true;
    // invoke destroy hooks on current rendered tree
    // 清空真实节点
    vm.__patch__(vm._vnode, null);
    // fire destroyed hook
    callHook(vm, "destroyed");
    // turn off all instance listeners.
    // 销毁所有的事件
    vm.$off();
    // remove __vue__ reference
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    // release circular reference (#6759)
    if (vm.$vnode) {
      vm.$vnode.parent = null;
    }
  };
}

renderMixin

$nextTick在这里定义,还有一个最核心的 _render,将渲染函数生成虚拟dom的方法,它是提供给渲染watch中使用的,因为每一个组件都有一个渲染watch

export function renderMixin(Vue: Class<Component>) {
  // install runtime convenience helpers
  // 给 Vue.prototype 挂载了大量的 helpers 方法
  installRenderHelpers(Vue.prototype);
  // 挂载nextTick方法,返回一个执行函数

  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this);
  };

  // 这个方法是提供给渲染watch使用的
  // mountComponent在这个方法,在core/instance/liftcycle
  Vue.prototype._render = function (): VNode {
    const vm: Component = this;
    // render:为经过模版编译的渲染函数
    // _parentVnode 为父亲的虚拟节点
    const { render, _parentVnode } = vm.$options;
    // 处理插槽
    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        // {scopedSlots:{defalt:fn}}
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      );
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode;
    // render self
    let vnode;
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm;
      // 这个方法调用的是,将渲染函数转换成虚拟dom节点
      vnode = render.call(vm._renderProxy, vm.$createElement);
    } catch (e) {
      handleError(e, vm, `render`);
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== "production" && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(
            vm._renderProxy,
            vm.$createElement,
            e
          );
        } catch (e) {
          handleError(e, vm, `renderError`);
          vnode = vm._vnode;
        }
      } else {
        vnode = vm._vnode;
      }
    } finally {
      currentRenderingInstance = null;
    }
    // if the returned array contains only a single node, allow it
    // 确保虚拟节点是只有一个节点
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0];
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== "production" && Array.isArray(vnode)) {
        warn(
          "Multiple root nodes returned from render function. Render function " +
            "should return a single root node.",
          vm
        );
      }
      vnode = createEmptyVNode();
    }
    // 设置虚拟节点的父亲
    vnode.parent = _parentVnode;
    return vnode;
  };
}

小结

经过上面的方法Vue的主流程已经运行完毕了,但是漏了很多的方法,并且是很有必要的方法将会放在后面。Vue的每一个组件的初始化都需要经过上述的流程,并且通过父子的关系构成层层联系组成组件系统。

其他的补充

补充剩下没有解析到的方法

全局 API 的实现原理

这些api都是挂载在 Vue 上的,是在 core/global-api目录下定义的。initGlobalAPI(Vue)

Vue源码 initGlobalAPI

指令

Vue源码 指令

生命周期

Vue生命周期

各位看官如遇上不理解的地方,或者我文章有不足、错误的地方,欢迎在评论区指出,感谢阅读。