扫码阅读
手机扫码阅读

最前端|详解VUE源码初始化流程以及响应式原理

119 2024-03-14

本期摘要

VUE开发是指使用VUE.js框架进行前端开发的过程。VUE.js是一个流行的JavaScript前端框架,用于构建用户界面和单页应用程序。在VUE开发中,开发者可以使用VUE.js提供的各种功能和特性,如组件化、响应式数据绑定、路由管理、状态管理等,来快速构建交互性强、用户体验良好的Web应用。

本次分享着重介绍VUE源码初始化流程以及响应式原理

作者

匿名 | 前端开发工程师

给大家来一点干货,简单易懂,希望对大家在 vue 的开发中有所帮助,废话不多说:

vue 源码的入口就是 src/core/instance/index.js, 这个文件的工作是在 Vue 的 prototype 上注册函数属性等,并执行initMixin中注册的 _init 函数

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)

顺着流程往下看, _init方法就是我们的初始化流程,主体代码如下:

Vue.prototype._init = function (options?: Object) { const vm: Component = this vm._isVue = true 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) // resolve injections before data/props initState(vm)
    initProvide(vm) // resolve provide after data/props callHook(vm, 'created') if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }

如果是组件,则_isComponent才会为 true,其他情况下都会执行 resolveConstructorOptions,这个函数的作用是将用户设置的options 和默认的 options 进行合并。再然后会执行一系列初始化函数,见名知意。 initLifecycle是初始化生命周期, initEvent是初始化事件处理机制, initRender则是对 vnode,插槽以及属性等进行一些初始化。然后就会调用 beforeCreate 的钩子函数。然后是 initInjectionsinitProvide这两个通信相关的组件。

这个地方涉及到到了我们非常熟悉的两个生命周期函数, beforeCreatecreated。可以对比 Vue 的流程图,我们就可以明确知道这两个钩子函数执行的时机。

他们中间实际上就是差了三个初始化的过程。其中重点是 initState 方法:

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

在这个方法中,如果传入了 data 则会执行 initData,否则会初始化一个空对象。下面可以看到 computedwatch也是在这里初始化的。

简化之后的 initData 代码:

function initData (vm: Component) { let data = vm.$options.data
  data = vm._data = typeof data === 'function' ? getData(data, vm)
    : data || {} 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)) {
      process.env.NODE_ENV !== 'production' && warn( `The data property "${key}" is already declared as a prop. ` + `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  } // observe data observe(data, true /* asRootData */)
}

这个方法中先判断 data 是不是函数,如果是则执行,不是则直接取值,所以我们的 data 既可以函数,也可以是对象。接下来是循环 data 的 key 值,通过 hasOwn(就是hasOwnProperty的简写函数)判断属性是否有重复。

isReserved方法是判断变量名是否是用_或者$开头,这意味着我们不能取_和$开头的属性名。然后进入到了 proxy方法中, proxy方法其实作用很简单,通过 Object.defineProperty设置 get 和 set 来将 data 的属性代理到 vm 上,使我们可以通过 this[propName]来访问到 data 上的属性,而不用通过 this.data[propName], 最后会执行 observe,如下:

function observe (value: any, asRootData: ?boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } let ob: Observer | void 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
}

前面都是在做一些初始化等必要的判断,核心只有一句:

ob = new Observer(value)

从这里开始,我们暂时中止 init 的流程,开始响应式流程这条线。看源码的时候,你总是会被各种支线打断,这是没办法的事情,只要你还记得之前在做什么就好。

Observer类是 vue 实现响应式最重要的三环之一,代码如下

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; constructor (value: any) { this.value = value this.dep = new Dep()
    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 (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } observeArray (items: Array) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }

这里介绍一下 def 函数,这是 vue 封装的方法,在源码中大量使用到,我们可以稍微分析一下,代码如下:

function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true })
}

可以看到,也是使用了 Object.defineProperty方法,上文我们提到过。这个方法是一个非常强大的方法,可以说 vue 的双向绑定就是通过它实现的。它有三个配置项, configurable是配置是否可以重新赋值和删除, writable标识是否可以改写, enumerable表明该属性是否会被遍历到。vue 通过 def 方法来定义哪些属性是不可更改的,哪些属性是不暴露给用户的。这里通过 def方法将 Observer类绑定在了 data 的 __ob__属性上,有兴趣的同学可以去 debugger 看看 data 和 prop 中的 __ob__属性到底是什么样的格式。

再说回 Observer,如果传入的数据是数组,则会调用 observeArray,这个函数会遍历数组,然后每个数组项又会去执行 observe方法,这里显然是一个递归,目的是将所有的属性都调用 observe。这个 observe方法实际上是 vue 实现观察者模式的核心,不光在初始化 data 的时候用到。最终,data 上的每个属性都会走到 defineReactive里面来,重点就在这里:

function defineReactive ( obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean ) { const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } 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) {
        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 if (newVal === value || (newVal !== newVal && value !== value)) { return } if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      } if (getter && !setter) return if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

这个方法的作用就是将普通数据处理成响应式数据,这里的 get 和 set 就是 vue 中依赖收集和派发更新的源头。这里又涉及到了响应式另外一个重要的类: Dep


在这段代码中,通过 Object.getOwnPropertyDescriptor 方法获取对象的属性描述符,如果不存在,则会用过 Object.defineProperty创建。这里的 get 和 set 都是函数,所以 data 和 prop 中所有的值都会由于闭包而缓存在内存中,并且都关联了一个 Dep 对象。

当用户通过 this[propName] 访问属性的时候,就会触发get,并调用 dep.depend 方法(下面的 dependArray 实际上就是递归遍历数组,然后去调用那个数据上的 __ob__.dep.depend 方法),当赋值更新的时候,则会触发 set,并调用 observe去对新的值创建 observer对象,最后调用了 dep.notify方法。

总结起来就是,当赋值时调用 dep.notify;当取值时调用 dep.depend这个方法的作用就在于此,剩下的工作交给了Dep类。


接下来我们可以看一下Dep类中做了什么。

class Dep { static target: ?Watcher;
  id: number;
  subs: Array; 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 () { const subs = this.subs.slice() if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } } Dep.target = null const targetStack = [] export function pushTarget (target: ?Watcher) {
  targetStack.push(target)
  Dep.target = target
} export function popTarget () {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

这里多贴了一些代码,虽然不属于同一个类,但是非常重要。这段代码初始化了一个 subs数组,这个非常眼熟的数组就是我们经常在 vue 的属性中看到的,它是一个观察者列表。

前文说到,当 key 的 getter 触发时会调用 depend,将 Dep.target 添加到观察者列表中。这样,在 set 的时候我们才能 notify 去通知 update。


另外,还要提一点,前面在设置getter时的代码中有这样一段:

if (Dep.target) {
        dep.depend() if (childOb) {
          childOb.dep.depend() if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }

那么既然已经执行了 dep.depend,为什么还要执行 childOb.dep.depend,这又是什么东西呢?

实际上,在数据的增删改查中,响应式的实现方式是不同的。setter 和 getter 只能检测到数据的修改和读取操作,因此这部分是由 dep.depend来实现的。而 data 的新增删除的属性,并不能直接实现响应式,这部分是由 childOb.dep.depend来完成的,这就是我们常用的Vue.set和Vue.delete的实现方式。

接着往下看,我们发现 depend方法 Dep.target推入 subs中。在上面定义可以看到,它是一个 Watcher类的实例,这个类就是响应式系统中的最后一环。

不过,我们暂时不管它,在这里还有一个重要的点: targetStack 可以看到有 pushTargetpopTarget这两个方法,它们遵循着栈的原则,后进先出因此,Vue中的更新也是按照这个原则进行的。另外,大家可能注意到,这里似乎没有实例化 Watcher对象,那么它是在什么地方执行的呢?下文会提到。

Watcher的代码很长,我们这里只看一小段。 notify被触发时,会调用 update方法。需要注意的是,这部分已经不是在init的流程中了,而是在数据更新时调用的。


update () { if (this.lazy) { this.dirty = true } else if (this.sync) { this.run()
    } else {
      queueWatcher(this)
    }
  }

这里正常情况下会执行 queueWatcher:


function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) {
    has[id] = true if (!flushing) {
      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. 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)
    }
  }
}

可以看到,当 data 更新时会将 watcher push 到 queue 中,然后等到 nextTick 执行 flushSchedulerQueuenextTick也是一个大家很熟悉的东西,vue 当然不会蠢到每有一个更新就更新一遍 dom。它就是通过 nextTick来实现优化的,所有的改动都会被 push 到一个 callbacks队列中,然后等待全部完成之后一次清空,一起更新。这就是一轮 tick


言归正传,接着来看 flushSchedulerQueue


function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true let watcher, id
  queue.sort((a, b) => a.id - b.id) for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null watcher.run() if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) {
        warn( 'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"` : `in a component render function.` ),
          watcher.vm
        ) break }
    }
  } const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice()
  resetSchedulerState()
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue) if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

实际核心代码就是遍历所有的 queue,然后执行 watcher.run,最后发出 actived updated两个hook。


watcher.run会更新值然后调用 updateComponent方法去更新 dom。至此,响应式原理的主体流程结束。说了这么多,其实下面这个流程图就能完整概括。

我们回到 init 的流程,上文中 init 的流程并没有执行完,还差这最后一句


if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

即通过传入进来的 options 将 dom 给渲染出来,我们来看$mount 的代码


Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component { el = el && query(el) if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to  or  - mount to normal elements instead.`
    ) return this
  }
  const options = this.$options if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') {
          template = idToTemplate(template) if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } 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)
    } if (template) { if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile')
      }
      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 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  } return mount.call(this, el, hydrating)
}

前面是在获取元素以及进行一系列的类型检查判断,核心就在 compileToFunctions这个方法上

const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult { const ast = parse(template.trim(), options); if (options.optimize !== false) {
    optimize(ast, options);
  } const code = generate(ast, options); return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
});

看到这个 ast 我们就应该知道这个函数的作用了,通过 templete 获取 AST 抽象语法树,然后根据定义的模板规则生成 render 函数。

这个方法执行完之后返回了 render 函数,之后被赋值在了 options 上面,最后调用了 mount.call(this, el, hydrating)


const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult { const ast = parse(template.trim(), options); if (options.optimize !== false) {
    optimize(ast, options);
  } const code = generate(ast, options); return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns,
  };
});

这个方法很简单,就是调用 mountComponent 函数。

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component { vm.$el = el if (!vm.$options.render) { vm.$options.render = createEmptyVNode if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */ if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') || vm.$options.el || el) {
        warn( 'You are using the runtime-only build of Vue where the template ' + 'compiler is not available. Either pre-compile the templates into ' + 'render functions, or use the compiler-included build.', vm )
      } else {
        warn( 'Failed to mount component: template or render function not defined.', vm )
      }
    }
  }
  callHook(vm, 'beforeMount') let updateComponent 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)
    }
  } new Watcher(vm, updateComponent, noop, {
    before () { if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false
components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true
    callHook(vm, 'mounted')
  } return vm }

这里的流程很容易理解。首先触发beforeMount钩子函数,然后通过vm._render生成虚拟DOM(vnode)。这个vnode就是我们常说的虚拟DOM。生成vnode后,再调用update方法将其更新为真实的DOM。在update方法中,会实现diff算法。最后执行mounted钩子函数。需要注意的是,这里的updateComponent只是定义出来了,然后将其作为参数传递给了Watcher。之前提到的Watcher就是在这个地方实例化的。

至此,init的主体流程也结束了。当然,其中还有很多细节没有提到。我也还没有深入研究这些细节,之后有时间会进一步理解和梳理。这篇文章主要是为了自己做个笔记,也分享给大家,希望能有所帮助。如果文中有任何错误之处,请大家指正。
原文链接: http://mp.weixin.qq.com/s?__biz=Mzg5MzUyOTgwMQ==&mid=2247524412&idx=1&sn=4124cbc117e4e3f1018563ffef457ea7&chksm=c02f5b9af758d28cd75eef9b7d4a830ea2becd2c647dab525f4efafb0ff8b6ac680806c96829#rd