这是我第一次正式阅读大型框架源码,刚开始的时候彻底不知道该如何入手。Vue源码clone下来以后这么多文件夹,Vue的这么多方法和概念都在哪,彻底没有头绪。如今也只是很粗略的了解一下,我的认为这篇只是能作到你们阅读Vue的参考导航,能够较快的找到须要看的文件或方法。不少细节依然没有理解到位,可是能够慢慢来,先分享一波~vue
- benchmarks 暂时不知道是什么node
- dist 存放打包后的文件夹webpack
- examples 示例,这个地方能够本身写一些简单例子,而后经过调试看整个代码运行的过程来了解源码是怎么写的web
- flow 静态类型检查,好比 (n:number)即n须要是number类型算法
- packages 查资料说是vue还能够分别生成其余的npm包npm
- scripts 打包相关的配置文件夹json
- src 咱们研究的主要文件夹,下面会详细再说明api
- test 测试文件夹数组
- types 暂时不知道是什么缓存
接下来重点说src这个文件夹,这里面须要重点看core这个文件夹,这里面才是咱们真正须要研究的地方以下图:
- components 组件,如今里面只有KeepAlive一个
- global-api 全局api,能够给Vue添加全局方法,好比里面咱们常使用的Vue.use()
-instance 核心文件夹,里面是实例相关的一些方法,例如初始化实例、实例事件绑定、渲染、状态、生命周期等
- observe 双向数据绑定相关文件(暂时不太清楚)
- util 工具方法,看到里面有props、nextTick之类的方法(暂时不太清楚)
- vdom 虚拟dom
大致文件结构说了一下,可是不少还不是很清晰。对于我这样的小白来讲,个人建议是能够从 npm run dev
开始一步步开始看,采起的方法是“倒序”
打开这个文件找到'dev'命令,以下
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
'rollup' 是Vue使用的打包工具,从上面能够看出执行这个命令是到 'scripts/config.js' 那就打开这个文件
if (process.env.TARGET) { module.exports = genConfig(process.env.TARGET) } else { exports.getBuild = genConfig exports.getAllBuilds = () => Object.keys(builds).map(genConfig) }
genConfig 方法是设置一些配置,和webpack里的设置差很少,而后找到 web-full-dev
'web-full-dev': { entry: resolve('web/entry-runtime-with-compiler.js'), dest: resolve('dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner }
能够看到入口文件是'web/entry-runtime-with-compiler.js'
在这个文件里能够看到
import Vue from './runtime/index'
该方法中有一个$mount须要注意,这个就是渲染的入口,接下来会说这个方法
`import Vue from 'core/index'`
import Vue from './instance/index'
终于进入到最重要的方法,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
Vue.prototype._init = function (options?: Object) {
这里有一个options?:object
的校验,刚开始即只是引入<script src="../../dist/vue.js"></script>
这个文件,当var vm = new Vue()
以后才进入_init方法内部。
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
mergeOptions这个方法是合并option,第一个参数是往$options塞入下面的参数
第二个参数就是咱们本身设置的option,好比data、el;第三个参数以下:
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')
接下来将会一个个初始化方法说明,初次以外_init方法还有一些变量的初始化,好比_uid、_isVue、_name、_renderProxy的初始化
if (vm.$options.el) { vm.$mount(vm.$options.el) }
调用$mount挂载根元素,这个方法就是以前提到的
第一点: stateMixin是对于Vue原型对象(Vue.prototype)加上$data、$props、$delete、$watch、$set属性。而且经过Object.defineProperty对$data、$props属性进行set和get
export function initState(vm: Component) { // 首先在vm上初始化一个_watchers数组,缓存这个vm上的全部watcher 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、props由于须要双向绑定,在initData、initProps中都有一个proxy方法对这两个属性进行set和get的设置
第一点: eventsMixin是对于Vue原型对象(Vue.prototype)绑定一些事件方法,好比$on、$once、$off、$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) } }
建立_events一个空对象以后用来存放事件,_hasHookEvent是一个优化标记(能够暂时不理会),而后初始化父级事件。根据是否有父级监听事件,若是有则更新父级事件
if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) }
$forceUpdate强制从新渲染实例自己和插入插槽内容的子组件;$destroy销毁一个实例,清理它与其它实例的链接,解绑它的所有指令及事件监听器,触发 beforeDestroy 和 destroyed 的钩子
export function initLifecycle(vm: Component) { const options = vm.$options // locate first non-abstract parent 建立第一个非抽象父组件,抽象组件:它自身不会渲染一个 DOM 元素,也不会出如今父组件链中,例如<keep-alive> 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 // watcher对象 vm._inactive = null // 和keep-alive中组件状态有关系 vm._directInactive = false // 和keep-alive中组件状态有关系 vm._isMounted = false //当前实例是否被挂载 vm._isDestroyed = false // 当前实例是否被销毁 vm._isBeingDestroyed = false // 当前实例是否正在被销毁或者没销毁彻底 }
callHook(vm, 'beforeCreate')
callHook(vm, 'created')
Vue.prototype._render = function (): VNode { …… // 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 { vnode = render.call(vm._renderProxy, vm.$createElement) } catch (e) { handleError(e, vm, `render`) if (process.env.NODE_ENV !== 'production') { if (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 } } else { vnode = vm._vnode } } …… return vnode }
在这个方法中主要是try……catch这里建立了vnode。 vnode = render.call(vm._renderProxy, vm.$createElement)
建立一个vnode而且返回,若是失败则返回一个空的vnode vnode = createEmptyVNode()
export function initRender(vm: Component) { …… 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) …… }
这里着重关注一下createElement方法,传入vnode以及dom的属性建立真正dom节点。
在_init方法中调用了initProvide、initInjections两个方法,这两个方法在实际应用中不是不少,查看Vue API说provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。因此这里不作说明,有须要的能够到这个文件查看相关方法
if (vm.$options.el) { vm.$mount(vm.$options.el) }
渲染入口,调用$mount方法开始
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && query(el) …… if (!options.render) { let template = options.template if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { // 图一 template = idToTemplate(template) …… } } else if (template.nodeType) { // 图一 template = template.innerHTML } else { // 图二 …… return this } } else if (el) { // 图三 template = getOuterHTML(el) } if (template) { …… const { render, staticRenderFns } = compileToFunctions(template, { shouldDecodeNewlines, shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this) options.render = render options.staticRenderFns = staticRenderFns …… } } return mount.call(this, el, hydrating) }
能够从上面大体看出结构,template是能够从el传入,也能够是options中的template以及render方法三种方式传入,对应Vue官网以下:
其中能够看到,经过el或者template的方式都须要调用compileToFunctions将字符串转换成方法,而render是不须要,这里能够看出render的性能应该会好一些,可是el和template咱们使用较易理解。可是不论是哪种最后都是生成render方法,而后再绑定到实例对象上。另外方法中的mount是从runtime/index.js中建立的。
export function mountComponent( vm: Component, el: ?Element, hydrating?: boolean ): Component { vm.$el = el …… callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { …… const vnode = vm._render() …… vm._update(vnode, hydrating) …… } } else { updateComponent = () => { vm._update(vm._render(), hydrating) } } new Watcher(vm, updateComponent, noop, { …… callHook(vm, 'beforeUpdate') …… }, true /* isRenderWatcher */) …… if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
而后调用前一步调用的_render方法是在render.js中的_render方法中try……catch地方调用了第二步中生成的render方法。经过_render方法生成vnode,传入_update方法
以上就是一个大致渲染的过程。
本文只是作了Vue构造函数总体的一个流程展现,哪些参数是在哪一个文件中挂载上去的以及vue渲染的一个简单流程。但其实每一个环节均可以拓展出不少知识,好比响应式的数据绑定、虚拟DOM、diff算法、patch、生命周期等等,这些能够在以后再一个个点进行了解。下图是没有参数的vue实例的参数