阅读 Vue 的源码第一篇,本文主要分享 Vue 的大体代码结构。
在源代码中有一个 src 目录,这个目录就是存放 Vue 源码的文件,里面有 complier、core、platforms、server、sfc、shared
├─ src │ ├─ compiler // 模版解析 │ │ ├─ codegen // 把 AST(抽象语法树) 转换为 Render 函数 │ │ ├─ directives // 生成 Render 函数之前需处理的指令 │ │ ├─ parser // 解析模版成 AST │ ├─ core // Vue 核心代码,包括内置组件,全局API封装,Vue 实例化,观察者,虚拟DOM, 工具函数等等。 │ │ ├─ components // 组件相关属性,主要是Keep-Alive │ │ ├─ global-api // Vue 中的一些全局 API,比如:Vue.use, Vue.extend, Vue.mixin 等 │ │ ├─ instance // 实例化相关内容,生命周期、事件等 │ │ ├─ observer // 响应式代码,双向数据绑定相关文件 │ │ ├─ util // 工具方法 │ │ └─ vdom // 包含虚拟 DOM 创建(creation)和打补丁(patching) 的代码 │ ├─ platforms // 和平台相关的内容,Vue.js 是一个跨平台的 MVVM 框架 │ │ ├─ web // web 端 │ │ │ ├─ compiler // web 端编译相关代码,用来编译模版成render函数 basic.js │ │ │ ├─ runtime // web 端运行时相关代码,用于创建Vue实例等 │ │ │ ├─ server // 服务端渲染 │ │ │ └─ util // 工具类 │ │ └─ weex // 基于通用跨平台的 Web 开发语言和开发经验,来构建 Android、iOS 和 Web 应用 │ ├─ server // 服务端渲染 │ ├─ sfc // 转换单文件组件(*.vue) │ └─ shared // 全局共享的方法、常量 复制代码
Vue 的代码结果非常地清晰,一个 Vue 类代码分散到多个文件中,方便管理。也不会让人看起源码来感觉到无比恐惧,真是看者无意,作者有心。
core 目录中 instance 目录就是定义 Vue 构造函数的相关代码的一些文件,其中最主要是 index.js、init.js、inject.js、lifecycle.js、proxy.js、render.js、state.js、event.js 。
首先我们得从入口文件开始
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' // 定义 Vue 构造函数 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') } // 当通过 new 创建 Vue 实例时,调用 _init() 方法,对 Vue 实例进行初始 this._init(options) } // 给 Vue.prototype 添加 _init() initMixin(Vue) // 给 Vue.prototype 添加 $data 对象、$props 对象、$set()、$delete()、$watch() stateMixin(Vue) // 给 Vue.prototype 添加 $on()、$once()、$off()、$emit()、 eventsMixin(Vue) // 给 Vue.prototype 添加 _update()、$forceUpdate()、$destroy()、 lifecycleMixin(Vue) // 给 Vue.prototype 添加 $nextTick()、_render() renderMixin(Vue) export default Vue // 从上面的代码可以看出这个 Vue 文件表面上看起来非常地简单,但是其内部的实现其实是相当的复杂。 // 官方在代码的规划上可以说是相当的清晰。把有相似逻辑的代码都抽到一块,然后通过引入的方法进行代码的组装。 // 我们可以把这个文件当做是 Vue 的一个结构大纲,至于一些具体的实现结节则在各代码块中单独实现。 // 当我们在 main.js 中 new Vue({}) 时,会调用 _init() 所以我们可以把 _init() 方法看作是生成 Vue 实例的一个入口。 复制代码
从代码中可以看出 Vue 类的代码组织非常地清晰,即使我们知道它本身的代码量不会少,但通过作者这么一组织,把代码分别放在不同的方法和文件中,通过引入调用并传入 Vue 类实现 Vue 类的组装。这样就可以把代码很好的作归类,有利于代码的维护,最后在这个主文件直接把挂载好属性和方法的 Vue 构造函数暴露出去。
当我们通过 new 关键字来创建实例时,也只是执行一个 this._init(options) 方法,所有的初始化都封装在了这个方法里面。
export function initMixin(Vue: Class<Component>) { Vue.prototype._init = function (options?: Object) { const vm: Component = this // a uid vm._uid = uid++ let startTag, endTag /* istanbul ignore if */ 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 // merge options // options._isComponent 是在 createComponentInstanceForVnode() 方法中初始化的,这个方法在 vue-dev\src\core\vdom\create-component.js if (options && options._isComponent) { // optimize internal component instantiation // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. initInternalComponent(vm, options) } else { // 合并选项,并挂载到 this.$options 上 vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), // 返回 Vue 构造函数自身的配置项 options || {}, // 用户配置项 vm ) } /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self vm._self = vm // 在实例上挂载一些属性:$parent,$root,$children,$refs,_watcher,_inactive,_directInactive,_isMounted,_isDestroyed,_isBeingDestroyed initLifecycle(vm) // 添加事件监听 initEvents(vm) // 在实例上挂载一些属性:_vnode,_staticTrees,$slots,$scopedSlots,$attrs,$listeners initRender(vm) // 触发 beforeCreate 钩子函数 callHook(vm, 'beforeCreate') // 注入一些父级 provide(提供)出来的属性,inject 配全后面的 provide 就是我们平时在组件中解决跨多级组件通信问题的一种方法 initInjections(vm) // resolve injections before data/props // 给实例添加 _watchers,使用方法 initProps(),initMethods(),initData(),initWatch() // 分别初始化 props,methods,data,watch // 这就是为什么特定的属性或者方法只能在特定的钩子函数中才能访问到的原因 initState(vm) // 向子孙组件提供数据,由于这个方法在 initState() 方法之后执行,所以我们可以把当前组件中的状态(如:props,methods,data)数据传到子子孙组件中。 initProvide(vm) // resolve provide after data/props // 触发 created 钩子函数,当 created 钩子函数触发时,组件中所需要的东西(prop,data,methods,watch)已经创建完成 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) } // 这个是在什么情况下才会调用??? // 是在根组织 new Vue 实例的时候会被调用。因为只有在哪 option 中才有显示的给了 el 这个属性 if (vm.$options.el) { vm.$mount(vm.$options.el) } } } 复制代码
在 this._init() 方法里我们基本可以看到创建一个 Vue 实例的整个过程。更多细节则封装在各个方法中,在 _init() 方法中直接调用 ,对生命周期、事件、渲染、状态、注入等进行初始化。
上面的 _init() 方法里又调用了很多其它的初始化方法。这样做的一个好处就是代码结构清晰,也方便管理。比如:想改一下事件相关的代码,那你就可以直接找到 initEvents() 方法的定义。当然这里作者还把一些方法的代码 放在了一个单独的文件上。实在太友好的,即使是我们这些学习源码的人也可以享受到这种代码组织所带来的好处。
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 } 复制代码
上面的这个方法就是初始化了一些属性状态。
export function initEvents (vm: Component) { vm._events = Object.create(null) vm._hasHookEvent = false const listeners = vm.$options._parentListeners if (listeners) { updateComponentListeners(vm, listeners) } } 复制代码
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 const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree 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 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 const parentData = parentVnode && parentVnode.data /* istanbul ignore else */ 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) } } 复制代码
上面的也有一些代码值得学习的,对函数进行二次封闭。比如:
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false) vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true) 复制代码
作者为什么这么写,我觉得至少有一点,我们可以看到 createElement() 方法第一个参数和最后一个参数是不变的,如果我不进行二次封闭的话我们第次都需要重复的传这两个不变的参数,那也够累的。
虽然只是一个小小的点,但在平时的开发当中,这样不好的习惯也是常有发生。不管是别人还是自己,毕竟身在其中,不易自拔,迷的都是当局者。我觉得一个团队里面应该也必需要抽出时间阅读别人的代码的并分享自己的一些看法,不管是对是错,怎么说也是交流,这样大家都能得到提高。
export function initInjections(vm: Component) { const result = resolveInject(vm.$options.inject, vm) if (result) { toggleObserving(false) Object.keys(result).forEach(key => { /* istanbul ignore else */ if (process.env.NODE_ENV !== 'production') { defineReactive(vm, key, result[key], () => { warn( `Avoid mutating an injected value directly since the changes will be ` + `overwritten whenever the provided component re-renders. ` + `injection being mutated: "${key}"`, vm ) }) } else { defineReactive(vm, key, result[key]) } }) toggleObserving(true) } } 复制代码
这个方法是和后面的 initProvide() 方法的配合来完成一个跨组件数据交互的功能的。
// 这个方法主要用于把访问实例 vm 的属性代理到实例 vm._data vm._props 的属性上 export function proxy(target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter() { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter(val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) } 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) { // 初始化 data initData(vm) } else { observe(vm._data = {}, true /* asRootData */) } if (opts.computed) initComputed(vm, opts.computed) // 如果组件中存在 watch ,并且不是原生的 watch,那么就初始化 watch // 这里的 watch 为我们在组件中定义的用来监听属性变化的那个 watch if (opts.watch && opts.watch !== nativeWatch) { // 传入实例以及组件中添加的 watch 对象 initWatch(vm, opts.watch) } } 复制代码
initState(vm) 方法中对 props 属性、methods 属性、data 属性、 computed 计算属性、watch 属性等进行初始化。props 属性 和 data 属性以及 computed 计算属性的初始化主要是给其定义的属性添加观察者(拦截)。
export function initProvide(vm: Component) { const provide = vm.$options.provide if (provide) { // 把 provide 挂到 vm._provide 上,供 inject 中 resolveInject() 方法递归时使用 vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } } 复制代码
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 } 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 if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) } options = options || {} options.user = true // vm 为 vue 实例,expOrFn 为监听的属性名,cb 为监听的属性名所对应的监听的回调函数 const watcher = new Watcher(vm, expOrFn, cb, options) // 如果有设置 immediate 属性,则立即执行一遍函数 if (options.immediate) { try { // watcher.value 的值 cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn() { watcher.teardown() } } } 复制代码
export function eventsMixin (Vue: Class<Component>) { const hookRE = /^hook:/ // event 参数可以是一个字符串,也可以是一个字符串数组,fn 事件监听的回调 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] 不存在则创建对应的事件名为 key 的数组,并且把其对应的回调函数放到此数组中,也就是说一个事件名,会对应一个数组,里面存在了此事件名所对应的所有回调 (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.prototype.$once = function (event: string, fn: Function): Component { const vm: Component = this // 定义一个事件回调函数 function on () { // 取消 event 事件的监听 vm.$off(event, on) // 执行传入的函数 fn fn.apply(vm, arguments) } // 给 on 挂载 fn 属性 on.fn = fn // 监听 event 事件,从这里可以知道,$once() 方法其内部实现最终也是通过 vm.$on() 方法实现的,但它是怎么做到多次触发,但只会执行一次的呢? // 关键在于上面定义的 on() 方法,这个方法对传进来的事件回调重新封装了一层,而里面的实现则是先取消实例上此事件的监听,而后再执行传入的函数。 vm.$on(event, on) return vm } Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component { const vm: Component = this // 如果没有参数 if (!arguments.length) { vm._events = Object.create(null) return vm } // 如果 第一个参数为数组 if (Array.isArray(event)) { // 遍历调用(递归) vm.$off() for (let i = 0, l = event.length; i < l; i++) { vm.$off(event[i], fn) } return vm } // 取出已经声明的事件对应的回调函数数组 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.prototype.$emit = function (event: string): Component { const vm: Component = this if (process.env.NODE_ENV !== 'production') { // 转为小写字符串 const lowerCaseEvent = event.toLowerCase() // 如果转换存在大小写,并且父组件有监听此事件的小写事件名,则会打印出提示 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}".` ) } } // vm._events 是在 Vue.prototype.$on 方法中定义的, // 取出对应事件中的函数数组 let cbs = vm._events[event] if (cbs) { cbs = cbs.length > 1 ? toArray(cbs) : cbs // 取出参数。this.$emit() 方法的第一个参数为事件名,后面的都被视为参数,所以在使用 this.$emit() 时,我们可以传任意个参数 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 } } 复制代码
export function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. } Vue.prototype.$forceUpdate = function () { const vm: Component = this 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 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. 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 } } } 复制代码
export function renderMixin (Vue: Class<Component>) { // install runtime convenience helpers installRenderHelpers(Vue.prototype) Vue.prototype.$nextTick = function (fn: Function) { return nextTick(fn, this) } 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 } } 复制代码
这就是源码给我们的第我的第一印象。至于这些方法的具体实现,后面文章慢慢呈现。