vue源码解读

整体架构分析

image.png
首先我们先分析一下这张图:在vue中每一个指令(包含v-if/v-show/v-model)都会被解析成一个Directive,而每一个Directive也会被相应的被一个Watcher监听,对应着一个唯一的Watcher实例。那么当数据变化的时候,如何实现数据对视图的映射呢?这里就涉及到了观察者模式:观察者模式定义了对象见一种一对多的依赖关系,当一个对象的状态发生改变的时候,所以依赖它的对象都将接收到通知,并作出更新。在vue中提出了Dep的概念用于收集依赖,每一个Watcher实例都会被收集到Dep中,当Observer观察到数据发生变化时,通知Dep数据已经发生了改变,然后Dep就会遍历它收集到的Watcher,通知Watcher数据的改变,然后Watcher将数据的改变传给Directive,由指令完成对应视图的更新。

上述分析中少了virtual dom、vnode、patch等步骤,后续会更新

接下来让我们阅读vue的源码,一步一步的分析vue的内部工作原理。

初始化

src/core/instance/index.js

我们找到vue的入口文件,从这里开始,我们逐步分析vue在初始化的时候做了什么。

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

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)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

在上述代码中我们可以很容易的看出,我们平时所使用的new Vue就是从这里到处的一个Vue函数,在函数内部初始化state、events、liftcycle、render等,并将Vue传入。

initMixin

src/core/instance/init.js

export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    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
    // 初始化组件component 合并参数
    if (options && options._isComponent) {
      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)
    //初始化events
    initEvents(vm)
    //初始化render
    initRender(vm)
    //触发beforeCreate生命函数钩子
    callHook(vm, 'beforeCreate')
    //初始化injection 用于接收父组件传下的数据
    initInjections(vm) // resolve injections before data/props
    //初始化数据
    initState(vm)
    //初始化provide 用于向自组件传递数据
    initProvide(vm) // resolve provide after data/props
    //触发created生命函数钩子
    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)
    }
        //绑定入口dom
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

从上述代码中,我们可以了解到,在initMixin的时候,vue做了一下事情:

  • 初始化组件 合并参数
  • 初始化生命周期函数
  • 初始化events
  • 初始化render函数
  • 触发beforeCreated生命函数钩子
  • 初始化injection 等待父组件数据传入
  • 初始化state
  • 初始化provide用于向子组件传数据
  • 触发craeted生命函数钩子
  • 绑定vue实例到dom

initLifecycle

初始化生命周期钩子

src/core/instance/lifecycle.js

export function initLifecycle (vm: Component) {
  const options = vm.$options

  // locate first non-abstract parent
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }

  vm.$parent = parent
  //找到跟元素
  vm.$root = parent ? parent.$root : vm

  vm.$children = []
  vm.$refs = {}

  vm._watcher = null
  vm._inactive = null
  vm._directInactive = false
  vm._isMounted = false
  vm._isDestroyed = false
  vm._isBeingDestroyed = false
}

initEvents

初始化Events

src/core/instance/initEvents.js

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

src/core/vdom/helpers/update-listeners.js

export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  createOnceHandler: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur, vm)
      }
      if (isTrue(event.once)) {
        cur = on[name] = createOnceHandler(event.name, cur, event.capture)
      }
      add(event.name, cur, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

initRender

src/core/instance/render.js

初始化render函数。

export function initRender(vm: Component) {
    //初始化根结点
  vm._vnode = null; // the root of the child tree
  vm._staticTrees = null; // v-once cached trees
  //获取初始化参数
  const options = vm.$options;
  //获取父级vNode
  const parentVnode = (vm.$vnode = options._parentVnode); // the placeholder node in parent tree
  //获取context
  const renderContext = parentVnode && parentVnode.context;
  //获取插槽
  vm.$slots = resolveSlots(options._renderChildren, renderContext);
  vm.$scopedSlots = emptyObject;
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  //绑定createEelment函数到vue实例
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false);
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true);

  // $attrs & $listeners are exposed for easier HOC creation.
  // they need to be reactive so that HOCs using them are always updated
  //获取父组件data数据
  const parentData = parentVnode && parentVnode.data;

  /* istanbul ignore else */
  //监听vm实例上的$attrs $listeners属性
  if (process.env.NODE_ENV !== "production") {
     defineReactive( vm,  "$attrs", (parentData && parentData.attrs) || emptyObject, () => {
        !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm);
      },
      true
    );
    defineReactive( vm, "$listeners", options._parentListeners || emptyObject, () => {
        !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm);
      },
      true
    );
  } else {
    defineReactive( vm,  "$attrs", (parentData && parentData.attrs) || emptyObject, null,true
    );
    defineReactive( vm, "$listeners", options._parentListeners || emptyObject, null,true
    );
  }
}

callHook

src/core/instance/lifecycle.js

触发各个生命函数钩子。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  //收集依赖 入栈
  pushTarget()
  const handlers = vm.$options[hook]
  const info = `${hook} hook`
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  //出栈
  popTarget()
}

initInjections

src/core/instance/inject.js

export function initInjections (vm: Component) {
  //获取vm上所有的inject
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    //避免调用defineReactive时触发依赖收集
    toggleObserving(false)
    Object.keys(result).forEach(key => {
        defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

initState

src/core/instance/state.js

//vue初始化state
export function initState(vm: Component) {
  vm._watchers = [];
  //获取参数options
  const opts = vm.$options;
  //如果存在props 则初始化initProps
  if (opts.props) initProps(vm, opts.props);
  //如果存在methods 则初始化initMethods
  if (opts.methods) initMethods(vm, opts.methods);
  //如果存在data 则initData(vm)
  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);
  }
}

function initProps(vm: Component, propsOptions: Object) {
  const propsData = vm.$options.propsData || {};
  const props = (vm._props = {});
  // cache prop keys so that future props updates can iterate using Array
  // instead of dynamic object key enumeration.
  const keys = (vm.$options._propKeys = []);
  //判断是否是根结点
  const isRoot = !vm.$parent;
  // root instance props should be converted
  //修改observer/index.js中shouldObserve变量  将根结点的数据转化成响应式的
  if (!isRoot) {
    toggleObserving(false);
  }
  for (const key in propsOptions) {
    keys.push(key);
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") {
      const hyphenatedKey = hyphenate(key);
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) {
        warn(
          `"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      }
      defineReactive(props, key, value, () => {
        if (!isRoot && !isUpdatingChildComponent) {
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "${key}"`,
            vm
          );
        }
      });
    } else {
      //将props变成响应式的数据 监听每一个props的改变
      defineReactive(props, key, value);
    }
    // static props are already proxied on the component's prototype
    // during Vue.extend(). We only need to proxy props defined at
    // instantiation here.
    if (!(key in vm)) {
      proxy(vm, `_props`, key);
    }
  }
  //最后将observer/index.js中shouldObserve变量 变为true
  toggleObserving(true);
}

function initData(vm: Component) {
  let data = vm.$options.data;
  //如果data是function类型 则使用getData获取数据 否则为{}
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
  if (!isPlainObject(data)) {
    data = {};
  // proxy data on instance
  const keys = Object.keys(data);
  //获取当前vue实例中的props methods 检查是否和data中的属性有重复的
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) {
    const key = keys[i];
    proxy(vm, `_data`, key);
  }
  // observe data 将data转化成响应式的
  observe(data, true /* asRootData */);
}

function initMethods(vm: Component, methods: Object) {
  const props = vm.$options.props;
  for (const key in methods) {
    /**
     * 如果methods中的method不为funciton 则使用一个空函数替代
     * export function noop (a?: any, b?: any, c?: any) {}
     * 
     * 否则使用bind绑定this
     * export const bind = Function.prototype.bind
        ? nativeBind
        : polyfillBind
     */
    vm[key] =
      typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
  }
}

function initComputed(vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = (vm._computedWatchers = Object.create(null));
  // computed properties are just getters during SSR
  const isSSR = isServerRendering();

  for (const key in computed) {
    const userDef = computed[key];
    const getter = typeof userDef === "function" ? userDef : userDef.get;

    if (!isSSR) {
      // create internal watcher for the computed property.
      watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
    }

    // component-defined computed properties are already defined on the
    // component prototype. We only need to define computed properties defined
    // at instantiation here.
    if (!(key in vm)) {
      defineComputed(vm, key, userDef);
    }
  }
}
//初始化watch 遍历watch对象 将其每一个属性都调用createWatcher变成响应式的
function initWatch(vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key];
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i]);
      }
    } else {
      createWatcher(vm, key, handler);
    }
  }
}
//创建watcher 返回一个响应式对象
function createWatcher(
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler;
    handler = handler.handler;
  }
  if (typeof handler === "string") {
    handler = vm[handler];
  }
  return vm.$watch(expOrFn, handler, options);
}

provide

src/core/instance/provide.js

初始化provide 并将其绑定在vue实例上

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

stateMixin

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;
  };
  Object.defineProperty(Vue.prototype, "$data", dataDef);
  Object.defineProperty(Vue.prototype, "$props", propsDef);
  //挂载$set API
  Vue.prototype.$set = set;
  //挂载$delete API
  Vue.prototype.$delete = del;
  //实现$watch方法
  Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this;
    //如果是对象 则监听对象
    if (isPlainObject(cb)) {
      return createWatcher(vm, expOrFn, cb, options);
    }
    options = options || {};
    options.user = true;
    const watcher = new Watcher(vm, expOrFn, cb, options);
    //如果设置了immediate 则立即执行一次
    if (options.immediate) {
      try {
        cb.call(vm, watcher.value);
      } catch (error) {
        handleError(
          error,
          vm,
          `callback for immediate watcher "${watcher.expression}"`
        );
      }
    }
    //返回一个取消监听的函数unwatchFn
    return function unwatchFn() {
      watcher.teardown();
    };
  };
}

在stateMixin中,vue做了这些事:

  • 重写了Vue原型上$data 和 $props的get方法。
  • 在Vue原型上挂载$set、$delete、$watch方法。

vm.$watch的实现已经在上述代码中,我们看看vm.$set和vm.$delete的实现

src/core/observer/index.js

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 * 新增响应式数据
 */
export function set(target: Array<any> | Object, key: any, val: any): any {
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    //调用splice 新增一个元素 并触发数组的setter将其转成响应式数据
    target.splice(key, 1, val);
    return val;
  }
  if (key in target && !(key in Object.prototype)) {
    target[key] = val;
    return val;
  }
  //获取observer实例
  const ob = (target: any).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    return val;
  }
  //如果不存在__ob__ 则该对象不是响应式的 直接操作属性即可
  if (!ob) {
    target[key] = val;
    return val;
  }
  defineReactive(ob.value, key, val);
  //通知watcher数据的变动
  ob.dep.notify();
  return val;
}

/**
 * Delete a property and trigger change if necessary.
 * 调用splice删除元素 整体流程同set
 */
export function del(target: Array<any> | Object, key: any) {

  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.splice(key, 1);
    return;
  }
  const ob = (target: any).__ob__;

  if (!hasOwn(target, key)) {
    return;
  }
  delete target[key];
  if (!ob) {
    return;
  }
  ob.dep.notify();
}

eventsMixin

src/core/instance/event.js

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
   //在vue原型上挂载$on方法
  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[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
  }
  //在vue原型上挂载$once方法
  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
  }
  //在vue原型上挂载$off方法
  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 获取所有对应的事件
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }
  //在vue原型上挂载$emit方法
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this;
    let cbs = vm._events[event]
    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
  }
}

在eventsMixin中,使用了观察者模式,实现在vue挂载了$on、$once、$off、 $emit的方法。

  • $on:订阅某个事件
  • $once:订阅某个事件 回调只会触发一次
  • $off:取消订阅的 某个事件
  • $emit:发布某个事件

lifecycleMixin

src/core/instance/lifecycle.js

export function lifecycleMixin (Vue: Class<Component>) {
  //在vue原型上挂载私有方法_update
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el//真实dom
    const prevVnode = vm._vnode//虚拟dom
    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
      //如果prevVnode不存在 执行新增操作
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      //否则 执行dom diff
      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.
  }
  //在vue原型上挂载公共方法$forceUpdate
  Vue.prototype.$forceUpdate = function () {
    const vm: Component = this
    //调用私有方法update触发更新操作
    if (vm._watcher) {
      vm._watcher.update()
    }
  }
  //在vue原型上挂载公共方法$destory
   Vue.prototype.$destroy = function () {
    const vm: Component = this
    if (vm._isBeingDestroyed) {
      return
    }
    //触发boforeDestory生命函数钩子
    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)
    }
    //去除组件上所有的watcher

    // teardown watchers
    //去除组件自带的watch
    if (vm._watcher) {
      vm._watcher.teardown()
    }
    //去除用户主动使用vm.$watch实现的监听
    let i = vm._watchers.length
    while (i--) {
      vm._watchers[i].teardown()
    }
    // remove reference from data ob
    // frozen object may not have observer.
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--
    }
    // call the last hook...
    //如果为true 则组件正在执行销毁
    vm._isDestroyed = true
    // invoke destroy hooks on current rendered tree
    //解除模版中所有的指令
    vm.__patch__(vm._vnode, null)
    // fire destroyed hook
    //触发destrotyed
    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
    }
  }
}

在lifecycleMixin中,vue做了这些事:

  • 在vue原型上挂载了私有方法_update
  • 挂载了公共方法$forceUpdate方法
  • 挂载了公共方法$destroy方法

renderMixin

src/core/instance/render.js

export function renderMixin(Vue: Class<Component>) {
  //install runtime convenience helpers
  //在vue原型上挂载一下方法和属性
  installRenderHelpers(Vue.prototype);
  //在vue原型上挂载$nextTick方法 
  Vue.prototype.$nextTick = function (fn: Function) {
    return nextTick(fn, this);
  };
  //在vue原型挂载私有方法_render
  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
      );
    }

    // 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;
      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();
    }
    // set parent
    vnode.parent = _parentVnode;
    return vnode;
  };
}

在renderMixin中,vue做了这些事:

  • 通过函数installRenderHelpers,在vue原型上挂载了一些方法和属性
  • 挂载了公共方法$nextTick方法
  • 挂载了私有方法_render方法

nextTick

src/core/util/next-tick.js

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false

const callbacks = []
let pending = false

function flushCallbacks() {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}

let timerFunc

// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
    const p = Promise.resolve()
    timerFunc = () => {
        p.then(flushCallbacks)
        // In problematic UIWebViews, Promise.then doesn't completely break, but
        // it can get stuck in a weird state where callbacks are pushed into the
        // microtask queue but the queue isn't being flushed, until the browser
        // needs to do some other work, e.g. handle a timer. Therefore we can
        // "force" the microtask queue to be flushed by adding an empty timer.
        if (isIOS) setTimeout(noop)
    }
    isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
    // Use MutationObserver where native Promise is not available,
    // e.g. PhantomJS, iOS7, Android 4.4
    // (#6466 MutationObserver is unreliable in IE11)
    let counter = 1
    //MutationObserver 监听指定节点的变更 如果有变化 则执行回调
    const observer = new MutationObserver(flushCallbacks)
    const textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
        characterData: true
    })
    timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
    }
    isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    // Fallback to setImmediate.
    // Technically it leverages the (macro) task queue,
    // but it is still a better choice than setTimeout.
    timerFunc = () => {
        setImmediate(flushCallbacks)
    }
} else {
    // Fallback to setTimeout.
    timerFunc = () => {
        setTimeout(flushCallbacks, 0)
    }
}

export function nextTick(cb?: Function, ctx?: Object) {
    let _resolve
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
                handleError(e, ctx, 'nextTick')
            }
        } else if (_resolve) {
            _resolve(ctx)
        }
    })
  //如果当前执行队列中无任务正在执行 立即执行
    if (!pending) {
        pending = true
        timerFunc()
    }
    // $flow-disable-line
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
            _resolve = resolve
        })
    }
}

原理:
nextTick接收两个参数:第一个参数是回调函数、第二个参数是上下文对象ctx。
作用:将回调函数延迟到下次Dom更新之后执行。
实现:通过判断执行环境是否支持以下属性:Promise、MutationObserver、setImmdiate 、setTimeout。然后将回调函数放进上述所说的四个宏任务或者微任务的回调中执行。

小结

在上面,我们逐步分离、了解了Vue的初始化、内部实现原理,理清了每一步的实现步骤。总的来说,Vue初始化的时候,做了以下事情:

  1. 初始化生命周期、events、render、触发beforeCreate、初始化injection、state、provide、触发created
  2. 在vue原型对象傻姑娘绑定了一些属性和方法,如
    1. 与数据相关的:$set 、$delete 、$watch
    2. 与事件相关的:$on 、$once 、$off 、$emit
    3. 与生命周期相关的:$foreUpdate、 $destroy、 $nextTick、 $mount
  3. _

那么接下来我们需要了解的就是Vue在运行时如何处理数据更新的。

运行时

Object.definePrototype

vue内部使用Object.definePrototype对对象进行监测,举个🌰:

function reactive(target,key,value){
    return Object.defineProperty(target,key,{
        enumerable:true,
        configurable:true,
        get:function(){
            console.log(`获取${key}的值:`,value)
            return value
        },
        set:function(newValue){
            console.log(`设置${key}的值:`,newValue);
            value = newValue
        }
    })
}

function Observer(data){
    Object.keys(data).forEach(key => {
        reactive(data,key,data[key])
    })
}

let obj = {
    a:10,
    b:20
}


Observer(obj)

obj.a;//获取a的值: 10
obj.a = 30;//设置a的值: 30

从上述代码中我们可以看到,我们可以使用循环遍历的方式,将其中每一个属性都进行监测,这样之后对对象的读取操作都会被监测到。
那么Array数组也是对象,为什么vue源码中会对数组的方法进行重写呢?下面我们来讨论下数组的情况。

let arr = [1,2,3];

Observer(arr);

arr[0]

arr.unshift(0)

image.png
我们可以看到当我们对数组进行操作的时候,是可以监控到数组的变动的。但是,为什么会打印多次数组变动呢?这里就涉及到一点数据结构的知识了,在内存中,数组是存储在堆中的,而且是连续的,如:

…其他内容 1 2 3 4 5 6 …其他内容

那么当我们在操作数组(插入、删除)的时候,就会改变数组的长度,就会移动整个数组,如:arr[i+1] = arr[i],移动整个数组就会逐步触发我们提前定义的setter/getter,所以就会打印多次。

但是这样并不是我们想要达到的结果,我们不希望它多次触发setter/getter。所以,在Vue源码实现中,拦截重写了这些可以改变数组的方法:push、pop、shift、unshift、sort、reverse、splice
具体实现在:src/core/observer/array.js

对于数组类型的数据,由于JavaScript的限制,Vue不能监测到内部的变化,会重写数组的部分方法,重写之后可以达到以下两个目的:

  • 当数组发生变化之后触发notify,通知相关依赖watcher
  • 对于数组新增元素,使用observe进行监听,将新元素也变成响应式的

题外话:Vue2之所以不支持IE8以下,就是因为内部使用了这个API。Vue3使用了Proxy之后,彻底不支持IE了😊。百度之前出的框架SanJS内部使用了defineSetterdefineGetter_ 可以在IE中运行。

Observer

观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。

订阅者模式涉及三个对象:发布者、主题对象、订阅者,三个对象间的是一对多的关系,
每当主题对象状态发生改变时,其相关依赖对象都会得到通知,并被自动更新。
在Vue中Observer会观察数组和对象两种数据类型:

src/core/observer/index.js src/core/observer/array.js

对于数组类型的数据,由于JavaScript的限制,Vue不能监测到内部的变化,会重写数组的部分方法,重写之后可以达到以下两个目的:

  • 当数组发生变化之后触发notify,通知相关依赖watcher
  • 对于数组新增元素,使用observe进行监听,将新元素也变成响应式的

而对于Object类型的数据,则遍历它的每个key,使用 defineProperty 设置 getter
setter,当触发getter的时候,observe开始收集依赖,触发setter的时候,observe触发notify

Observer 对象的标志就是
__ob__ 这个属性,这个属性保存了 Observer 对象自己本身。
对象在转化为 Observer 对象的
过程中是一个递归的过程,对象的子元素如果是对象或数组的话,也会转化为 Observer 对象。

其实 observeArray 方法就是对数组进行遍历,递归调用
observe 方法,最终都会走入
walk 方监控单个元素。而
walk 方法就是遍历对象,结合
defineReactive 方法递归将属
性转化为 gettersetter

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //实例话Dep
  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];
  }

  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) {
        //收集依赖watcher
        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);
      //通知watcher数据变更
      dep.notify();
    },
  });
}
export function observe(value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return;
  }
  let ob: Observer | void;
  //避免重复监听 observer挂载到响应式数据的__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++;
  }
  return ob;
}
/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 * 遍历数组 将每一个元素变成响应式的
 */
function dependArray(value: Array<any>) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}
/**
   * 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]);
    }
  }

Watcher

src/core/observer/watcher.js

Watcher是将模版和Observer链接到一起的纽带。Watcher在发布订阅模式中的订阅者。Watcher接收的参数中:expOrFn: String | Function , cb: Function。其中:expOrFn如果是一个函数,则直接赋值给this.getter,用于指定当前订阅者获取数据的方法。如果是字符串,则调用parsePath函数将其转化为一个可执行函数。cb是数据更新时的回调函数。

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    //将当前的Watcher类推送到Vue实例
    vm._watchers.push(this);
    // parse expression for getter
    if (typeof expOrFn === "function") {
      //如果为函数,则相当于指定类订阅者获取数据的方法 每次订阅者通过getter获取到数据之后,与之前的数据进行对比
      this.getter = expOrFn;
    } else {
      //否则将表达式解析为可执行函数
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
      }
    }
    //如果lazy不为true 则立即搜集依赖
    this.value = this.lazy ? undefined : this.get();
  },
    /**
   * Evaluate the getter, and re-collect dependencies.
   * 收集依赖
   */
  get() {
    pushTarget(this);
    let value;
    const vm = this.vm;
    try {
      //调用getter 收集依赖
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`);
      } else {
        throw e;
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步
      this.run();
    } else {
      queueWatcher(this);
    }
  }

依赖收集的入口就是get函数。getter函数是用来连接监控属性和Watcher的关键。只有通过Watchergetter才会收集依赖。而所谓的搜集的依赖就是当前Watcher实例初始化时传入expOrFn中的每一项数据,然后触发该数据的getter函数,而getter函数通过依赖的Dep.target是否存在来判断是否是初始化调用还是正常的数据读取。如果有target,则进行依赖收集(Observer/index.js)。

我们注意看上面代码中的update方法,该方法会判断是否是同步执行this.sync,如果是,则立即执行;否则,调用queueWatcher方法,将当前watcher放进队列中。
我们来看queueWatcher的实现:

src/core/observe/scheduler.js

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id;
  //判断queuewatcher中是否已存在该watcher
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
        //如果!flushing 则将其push进队列
      queue.push(watcher)
    } else {
        //否则将其插入到queue中
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    //如果没有等待
    if (!waiting) {
      //改变状态 waiting
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

Dep

src/core/observer/dep.js

/**
 * 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++;
    //收集依赖watcher
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }
  //删除依赖watcher
  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
  //添加依赖watcher
  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()
    }
  }
}

当数据更新时,触发setter,会调用notify方法通知触发订阅者(依赖watcher)的update方法,通知订阅者更新视图。

Directive

image.png

当一个组件初始化的时候,会进行数据的初始化和模版编译。初始化数据的时候,会将页面中的数据进行Observer,调用Object.defineProrotype,将数据转化为setter/getter(响应式的数据)。与此同时,模版编译的时候也会将对应的指令转化为Directive,每一个Directive对应一个watcher,然后触发getterwatcher收集进Dep中。当数据发生变化的时候,触发setter,对应的Dep会遍历每一个依赖,通知watcher数据已发生了变化,然后watcher调用update方法触发Directiveupdate更新视图(中间暂时省略vnodedom diffpatch等过程)。

关于编译这块vue分了两种类型,一种是文本节点,一种是元素节点。

image.png
vue内置了这么多的指令, 这些指令都会抛出两个接口 bindupdate,这两个接口 的作用是,编译的最后一步是执行所有用到的指令的bind方 法,而 update 方法则是当 watcher 触发 update 时, Directive会触发指令的update 方法
observe -> 触发setter -> watcher -> 触发update -> Directive -> 触发update -> 指令

this._directive.push(
    new Directive(descriptor,this,node,host,scope,frag)
)
  1. 所有 tagtrue 的数据中的扩展对象拿出来生成一个 Directive 实例并添加到 _directives 中( _directives 是当前 vm 中存储所有 directive 实例的地方)。
  2. 调用所有已绑定的指令的 bind 方法
  3. 实例化一个 Watcher,将指令的 updatewatcher 绑定在一起(这样就实现了 watcher 接收到消息后触发的 update 方法,指令可以做出对应的更新视图操作)
  4. 调用指令的 update,首次初始化视图
  5. 这里有一个点需要注意一下,实例化 Watcher 的时候,Watcher 会将自己主动的推入 Dep 依赖中

Scheduler.js

src/core/observer/scheduler.js

当数据发生变更之后,触发 setter,然后在Dep中会遍历当前数据的 Watcher,执行 Watcher.update( )方法,这里我们看下 Watcher.update()的代码:

/**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //如果是同步 直接执行
      this.run();
    } else {
      //否则放进执行队列中
      queueWatcher(this);
    }
  }

我们可以看到,如果是同步的,则立即执行回调函数,否则,将当前 Watcher 实例放进 watcher queue 中等待执行。

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id;
  //判断queuewatcher中是否已存在该watcher
  if (has[id] == null) {
    has[id] = true
     if (!flushing) {
      //如果队列中无watcher
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      //否则根据优先级,将其插入到queue中
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    //如果没有等待
    if (!waiting) {
      waiting = true
      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher 函数中,vue 会将 watcher 放入watcher queue,并且,如果当前没有任务在执行的时候,则调用 nextTick 执行当前watcher.run,即执行回调函数。
nextTick 函数的源码中,为了让 flush 动作能在当前 Task 结束后尽可能早的开始,Vue 会优先尝试将任务micro-task 队列,具体来说, 在浏览器环境中 Vue 会优先尝试使用 MutationObserver APIPromise,如果两者都不可用,则 fallbacksetTimeout

Keep-alive

src/core/components/keep-alive.js

或许大家都接过这样一个需求,当用户在A列表页面上拉了一会,跳到了详情页面,然后返回列表页面,需要保持列表页面在之前的的操作结果不变。这时候,我们第一时间想到的就是keep-alive组件,用该组件进行缓存列表页面。那么keep-alive内部是如何实现的呢?让我们一起来看下。

function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    //取出缓存中的页面 
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      //如果组件存在 且不满足缓存条件 那么清除
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
//清除缓存组件
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

export default {
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },
  //页面销毁的时候,删除缓存组件及对应的key
  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted () {
    //加载的时候 从缓存中取出来即将加载的组件
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        //如果组件存在缓存中,则将其取出,并置顶
          //LRU 算法,Least Recently Used 将最近使用的移动到哈希链表末尾 如果链表超过预期长度,将最不常用的删除
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else {
        //如果尚未缓存,将其缓存。如果缓存组件已超出最大值,清除队首组件
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}

在上述keep-alive的代码实现中,vue使用了LRU缓存算法(LRU,Least Recently Used)将最后使用组件放在队尾位置,将不常使用的组件放在队首位置,一旦缓存即将超出最大值限制,将队首缓存的组件清除,将新的组件缓存。

Virtual-Dom

什么是Virtual-Dom

Virtual-Dom是时代化的产物。
在web早期,页面的交互效果比现在简单很多,也没有那么多的状态需要管理,所以也不需要频繁的操作Dom,使用jQuery就可以解决大部分问题。但是随着时代的发展,我们的页面功能越来越多,产品需求越来越复杂,随之而来的就是页面交互更多、状态越来越难以管理,对Dom的操作也更加频繁。如果我们仍然使用之前的方式进行开发,那么就有可能导致我们的项目逻辑越来越复杂,后期维护成本剧增,甚至出现一些难以维护的代码。

这就是命令式操作Dom的缺点。在当下业务越来越复杂的情况下,它有着难以言喻的痛点。

现在我们使用的前端三大框架:React、Vue、Angular。它们都是生命式的操作Dom,通过生命式的描述状态和Dom之间的映射关系,使用这种映射就可以将状态渲染成视图。至于关于状态到视图的转换操作,框架会在底层帮我们实现,开发者无需手动操作Dom。

DOM操作很慢是两个原因,一个是本身操作就不快,第二是我们(还有很多框架)处理dom的方式很慢,Virtual Dom解决了我们对Dom的低劣操作,它让我们不需要进行Dom操作,而是将希望展现的最终结果告诉 VueVue通过一个简化的DomVirtual dom进行 render,当你试图改变显示内容时,新生成的Virtual Dom会 与现在的Virtual dom对比,通过diff算法找到区别,这些操作 都是在快速的js中完成的,最后对实际Dom进行最小的Dom操作来完成效果,这就是Virtual Dom的概念。 rective(descriptor, this, node, host, scope, frag)
image.png
这仅仅是第一层。真正的 DOM 元素非常庞大,这是因为标准就是这么设计的。而且操作它们的时候 我们要小心翼翼,轻微的触碰可能就会导致页面重排,这可是杀死性能的罪魁祸首。
Virtual-dom是一系列的模块集合,用来提供声明式的DOM渲染。来看一个简单的 DOM 片段. 本质上就是在 JSDOM 之间做了一个缓存。可以类比 CPU 和硬盘,既然硬盘这么慢,我们就在它们之间加个缓存:既然 DOM 这么慢,我们就在它们 JSDOM 之间加个缓存。CPU(JS)只操作内存(Virtual DOM),最后的时候再把变更写入硬盘(DOM)。

Vue.js中的Virtual-Dom

在Vue.js中使用模版来描述状态和Dom之间的联系。Vue.js通过编译将模版转换成渲染函数Render,然后执行渲染函数即可得到一个Virtual-Dom,通过这个Virtual-Dom即可渲染页面。
image.png
Virtual-Dom的目标就是将vnode渲染到dom上。但是如果直接进行渲染的话,会有一些不必要的麻烦。例如:

<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

如果只有一个li发生了变化,此时根本无需将整个ul替换掉,这样可以避免很大的性能浪费。所以,在vue内部实现时,在更新页面之前,会将vnode和上一次渲染的node节点(oldVNode)进行比较,找出真正需要更新的节点,从而避免操作其他dom节点。
image.png
可以看出,VDOM在vue中一共做了两件事:

  • 提供和真实Dom对应的Virtual-Dom
  • 使用vnode和oldVNpde进行比较,更新视图

其中,vnode只是一个JavaScript对象,它上面绑定了更新Dom所需要的一些数据。对两个vnode、oldVNode进行对比的核心是patch,它在内部判断是哪些节点发生了变化,从而对发生了变化的节点进行更新。

小结

Virtual-Dom是将状态映射成视图的一种解决方案。它的工作原理是使用状态生成vnode,然后使用vnode更新视图。
之所以使用状态先生成vnode,是因为如果直接使用状态改变真是Dom,会有一定程度的性能浪费。而先生成vnode,可以先将vnode缓存,在和上一次生成的渲染时缓存的vnode进行比较,找出真正需要更新的节点,然后更新视图。这样可以避免一部分不必要的Dom操作,节省一部分的性能开销。

vue通过模版来描述状态和视图之间的映射关系,所以它会首先将模版编译成渲染函数Render,执行渲染函数生成vnode,进而更新视图。

因此Virtual-Dom在vue中所做的就是提供和真实Dom对应的vnode,将vnode和oldVNode进行对比,根据对比结果进而更新视图。

VNode

什么是vnode

在Vue.js中存在一个VNode类,在类中定义了VNode的属性的方法。使用该类可以实例化出不同的vnode类型,而不同的vnode类型则表示着不同的dom节点。

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  devtoolsMeta: ?Object; // used to store functional render context for devtools
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

从上面的代码中我们可以看出,vnode只是一个JavaScript对象,是从VNode实例化出来的一个实例,只是使用了JavaScript对象进行了描述,所以dom元素上所存在的所有的属性,在vnode上都可以找到。

VNode的作用

因为每次渲染视图时都需要先创建vnode,然后再用它创建dom插入视图中,所以我们可以将vnode缓存起来,这样,当每次创建vnode时,将新创建的vnode和上次渲染时创建的vnode进行对比,查找它们之间的不同之处,就可以获取具体需要更新的节点位置,基于此再去更新dom。
而且在vue2.0中采用了中等粒度的侦测策略,只侦测到组件级别,当数据改变的时候,通知组件,由组件自身使用vnode进行视图的更新。

VNode的类型

vnode有很多不同的类型,下面让我们看下vnode不同节点类型之间的区别:

  • 注释节点
  • 文本节点
  • 元素节点
  • 组件节点
  • 函数式组件
  • 克隆节点

上面我们介绍过vnode只是一个普通的JavaScript对象类型而已,而不同的节点类型只是属性不同,准确来说是有效属性不同,当实例化一个vnode时,通过参数来设置有效属性,无效的属性会被默认为undefined或null。对应无效的属性,忽略即可。

注释节点

注释节点的创建很简单,我们直接通过代码查看即可。

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

我们可以看到一个注释节点只有两个有效属性:

  • text:注释文本内容
  • isComment

其他属性均为undefined或者null
例如:

<!--这是一个注释节点-->
{
    text:"这是一个注释节点",
  isComment:true
}
文本节点

文本节点的创建也很简单,直接看代码。

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

结合上面的VNode类一起看,我们发现文本节点只有一个有效属性:text。

克隆节点

克隆节点是将现有节点进行克隆,克隆之后具备现有节点的一切属性。它的作用是优化静态节点和插槽节点(slot node)。

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    // #7975
    // clone children array to avoid mutating original in case of cloning
    // a child.
    vnode.children && vnode.children.slice(),
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

可以看出,在克隆时,只需要将被克隆的节点的所有属性赋值到新节点即可。克隆节点和原节点唯一的不同是克隆节点的isClone为true,而被克隆节点的isClone为false。

元素节点

元素节点通常存在以下四个属性:

  • tag:表示当前节点的名称,如p、div等
  • data:表示当前节点上的数据,如style、attrs等
  • children:表示当前节点的子节点列表
  • context:表示当前节点的vue实例

例如:

<div>
  <p>第一个子节点</p>
  <span>第二个自节点</span>
</div>

其对应的vnode:

{
        tag:'div',
    data:{...},
    children:[...],
    context:{...}
}
组件节点

组件节点有以下两个属性:

  • componentOptions:指的是当前组件节点的选项参数,如propData、tag、children等
  • componentInstance:组件的实例,即vue的实例。其实每一个组件都是一个vue实例

例如:

<child></child>

对应的组件节点:

{
  componentOptions:{...},
  componentInstance:{...},
    context:{...},
  data:{...},
  tag:'vue-component-1-child'
}
函数式组件

函数式组件和组件节点类似,但是它有两个独有的属性:functionialContext 和 functionialOptions。

小结

VNode是一个类,可以用于生成不同类型的vnode。而不同类型的vnode代表着不同的真是的dom类型。
Vue.js中并不是直接更新dom节点,而是先生成vnode,将新创建的vnode和上次渲染生成的vnode进行比较,找出真实需要更新的节点,节省了性能的开销。
vnode有很多种类型,它们本质上都是从VNode中实例化出来的对象,只是有效属性不同而已。

Patch


















  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!