这周工作有点多...只能抽小块的时间来看 别问我为什么周六周日不看 因为我要休息啊~
下面继续上一篇
我们在构造函数上挂载了两个$mount方法,因为这个是最后被修改的,结尾mount又重新调用方法,call改变指向,所以该方法被当做vue实例对象调用的 this是vue实例对象。
然后在最下方加上了Vue.compile。
compileToFunctions 函数的作用,就是将模板 template 编译为render函数。
下面回到_init()方法内,在开发环境内,为vm(实例化函数)添加了两个属性,看第一个方法:
if (process.env.NODE_ENV !== 'production') { initProxy(vm) } else { vm._renderProxy = vm } // expose real self 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') /* 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) } if (vm.$options.el) { vm.$mount(vm.$options.el) }
进入initLifecycle方法:
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 }
parent不存在,其余为vm定义了一些属性(options.abstract用于判断是否是抽象组件,组件的父子关系建立会跳过抽象组件,抽象组件比如keep-alive、transition等。所有的子组件$root都指向顶级组件)。然后下一个方法,_parentListeners是父组件中绑定在自定义标签上的事件,供子组件处理。
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) } }
然后调用initRender方法,为vm添加一些属性,然后调用了beforeCreate钩子函数。
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) } }
export function initProvide (vm: Component) { const provide = vm.$options.provide if (provide) { vm._provided = typeof provide === 'function' ? provide.call(vm) : provide } } 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) } }
这两个配套使用,用于将父组件_provided中定义的值,通过inject注入到子组件,且这些属性不会被观察。简单的例子如下:
<div id="app"> <p>{{message}}</p> <child></child> </div> <script type="text/javascript"> var vm = new Vue({ el: '#app', data: { message: '第一个vue实例' }, components: { child: { template: "<div>{{a}}</div>", inject: ['a'] } }, provide: { a: 'a' } }) </script>
然后initState方法,这里主要就是操作数据了,props、methods、data、computed、watch,从这里开始就涉及到了Observer、Dep和Watcher
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) { 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) } }
然后调用created钩子函数,在经历了init后,我们的Vue变成了如下:
// _init vm._uid = 0 vm._isVue = true vm.$options = { components: { KeepAlive, Transition, TransitionGroup }, directives: { model, show }, filters: {}, _base: Vue, el: '#app', data: function mergedInstanceDataFn(){} } vm._renderProxy = vm vm._self = vm // initLifecycle 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 vm._events = Object.create(null) vm._hasHookEvent = false // initRender vm.$vnode = null vm._vnode = null vm._staticTrees = null vm.$slots = resolveSlots(vm.$options._renderChildren, renderContext) vm.$scopedSlots = emptyObject 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) // 在 initState 中添加的属性 vm._watchers = [] vm._data vm.message
然后我们进入entry-runtime-with-compiler.js
1 const mount = Vue.prototype.$mount 2 Vue.prototype.$mount = function ( 3 el?: string | Element, 4 hydrating?: boolean 5 ): Component { 6 el = el && query(el) 7 /* istanbul ignore if */ 8 if (el === document.body || el === document.documentElement) { 9 process.env.NODE_ENV !== 'production' && warn( 10 `Do not mount Vue to <html> or <body> - mount to normal elements instead.` 11 ) 12 return this 13 } 14 const options = this.$options 15 // resolve template/el and convert to render function 16 if (!options.render) { 17 let template = options.template 18 if (template) { //根组件,即打开时显示的组件 19 if (typeof template === 'string') { 20 if (template.charAt(0) === '#') { 21 template = idToTemplate(template) 22 /* istanbul ignore if */ 23 if (process.env.NODE_ENV !== 'production' && !template) { 24 warn( 25 `Template element not found or is empty: ${options.template}`, 26 this 27 ) 28 } 29 } 30 } else if (template.nodeType) { 31 template = template.innerHTML 32 } else { 33 if (process.env.NODE_ENV !== 'production') { 34 warn('invalid template option:' + template, this) 35 } 36 return this 37 } 38 } else if (el) { 39 template = getOuterHTML(el) 40 } 41 if (template) { 42 /* istanbul ignore if */ 43 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 44 mark('compile') 45 } 46 47 const { render, staticRenderFns } = compileToFunctions(template, { 48 shouldDecodeNewlines, 49 shouldDecodeNewlinesForHref, 50 delimiters: options.delimiters, 51 comments: options.comments 52 }, this) 53 options.render = render 54 options.staticRenderFns = staticRenderFns 55 56 /* istanbul ignore if */ 57 if (process.env.NODE_ENV !== 'production' && config.performance && mark) { 58 mark('compile end') 59 measure(`vue ${this._name} compile`, 'compile', 'compile end') 60 } 61 } 62 } 63 64 return mount.call(this, el, hydrating) 65 }
前边是一个判断,然后获取template,如果没有则用el的ID的全部内容,然后compileToFunctions方法,进入to-function.js
1 export function createCompileToFunctionFn (compile: Function): Function { 2 const cache = Object.create(null) 3 4 return function compileToFunctions ( 5 template: string, 6 options?: CompilerOptions, 7 vm?: Component 8 ): CompiledFunctionResult { 9 options = extend({}, options) 10 const warn = options.warn || baseWarn 11 delete options.warn 12 13 /* istanbul ignore if */ 14 if (process.env.NODE_ENV !== 'production') { 15 // detect possible CSP restriction 16 try { 17 new Function('return 1') 18 } catch (e) { 19 if (e.toString().match(/unsafe-eval|CSP/)) { 20 warn( 21 'It seems you are using the standalone build of Vue.js in an ' + 22 'environment with Content Security Policy that prohibits unsafe-eval. ' + 23 'The template compiler cannot work in this environment. Consider ' + 24 'relaxing the policy to allow unsafe-eval or pre-compiling your ' + 25 'templates into render functions.' 26 ) 27 } 28 } 29 } 30 31 // check cache 32 const key = options.delimiters 33 ? String(options.delimiters) + template 34 : template 35 if (cache[key]) { 36 return cache[key] 37 } 38 39 // compile 40 const compiled = compile(template, options) 41 42 // check compilation errors/tips 43 if (process.env.NODE_ENV !== 'production') { 44 if (compiled.errors && compiled.errors.length) { 45 warn( 46 `Error compiling template:\n\n${template}\n\n` + 47 compiled.errors.map(e => `- ${e}`).join('\n') + '\n', 48 vm 49 ) 50 } 51 if (compiled.tips && compiled.tips.length) { 52 compiled.tips.forEach(msg => tip(msg, vm)) 53 } 54 } 55 56 // turn code into functions 57 const res = {} 58 const fnGenErrors = [] 59 res.render = createFunction(compiled.render, fnGenErrors) 60 res.staticRenderFns = compiled.staticRenderFns.map(code => { 61 return createFunction(code, fnGenErrors) 62 }) 63 64 // check function generation errors. 65 // this should only happen if there is a bug in the compiler itself. 66 // mostly for codegen development use 67 /* istanbul ignore if */ 68 if (process.env.NODE_ENV !== 'production') { 69 if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) { 70 warn( 71 `Failed to generate render function:\n\n` + 72 fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'), 73 vm 74 ) 75 } 76 } 77 78 return (cache[key] = res) 79 } 80 }
到 const compiled = compile(template, options)一句时 我们跳转至compile方法
export function createCompilerCreator (baseCompile: Function): Function { return function createCompiler (baseOptions: CompilerOptions) { function compile ( template: string, options?: CompilerOptions ): CompiledResult { const finalOptions = Object.create(baseOptions) const errors = [] const tips = [] finalOptions.warn = (msg, tip) => { (tip ? tips : errors).push(msg) } if (options) { // merge custom modules if (options.modules) { finalOptions.modules = (baseOptions.modules || []).concat(options.modules) } // merge custom directives if (options.directives) { finalOptions.directives = extend( Object.create(baseOptions.directives || null), options.directives ) } // copy other options for (const key in options) { if (key !== 'modules' && key !== 'directives') { finalOptions[key] = options[key] } } } const compiled = baseCompile(template, finalOptions) //新加warn带原型 if (process.env.NODE_ENV !== 'production') { errors.push.apply(errors, detectErrors(compiled.ast)) } compiled.errors = errors compiled.tips = tips return compiled } return { compile, compileToFunctions: createCompileToFunctionFn(compile) } } }
到 const compiled = baseCompile(template, finalOptions)一句时 我们跳转至baseCompile方法
export const createCompiler = createCompilerCreator(function baseCompile ( template: string, options: CompilerOptions ): CompiledResult { const ast = parse(template.trim(), options) console.log(ast) if (options.optimize !== false) { optimize(ast, options) } const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } })
ast为将template标签内的所有元素内容转为一个对象,转换后的对象如下
{ type: 1, tag: 'div', plain: false, parent: undefined, attrs: [{name:'id', value: '"app"'}], attrsList: [{name:'id', value: 'app'}], attrsMap: {id: 'app'}, children: [{ type: 1, tag: 'p', plain: true, parent: ast, attrs: [], attrsList: [], attrsMap: {}, children: [{ expression: "_s(message)", text: "{{message}}", type: 2 }] }
optimize为对ast进行优化,分析出静态不变的内容部分,增加了部分属性:
{ type: 1, tag: 'div', plain: false, parent: undefined, attrs: [{name:'id', value: '"app"'}], attrsList: [{name:'id', value: 'app'}], attrsMap: {id: 'app'}, static: false, staticRoot: false, children: [{ type: 1, tag: 'p', plain: true, parent: ast, attrs: [], attrsList: [], attrsMap: {}, static: false, staticRoot: false, children: [{ expression: "_s(message)", text: "{{message}}", type: 2, static: false }] }
然后回到方法,generate方法为生成了render函数,函数如下
render = function () { with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])} }
在render.js中,我们曾经添加过如下多个函数,这里和render内返回值调用一一对应。
Vue.prototype._o = markOnce Vue.prototype._n = toNumber Vue.prototype._s = _toString Vue.prototype._l = renderList Vue.prototype._t = renderSlot Vue.prototype._q = looseEqual Vue.prototype._i = looseIndexOf Vue.prototype._m = renderStatic Vue.prototype._f = resolveFilter Vue.prototype._k = checkKeyCodes Vue.prototype._b = bindObjectProps Vue.prototype._v = createTextVNode Vue.prototype._e = createEmptyVNode Vue.prototype._u = resolveScopedSlots
如果我们有静态的内容,则会生成staticRenderFns函数,然后打开plateforms/web/runtime/index.js。
Vue.prototype.$mount = function ( el?: string | Element, hydrating?: boolean ): Component { el = el && inBrowser ? query(el) : undefined return mountComponent(this, el, hydrating) }
在lifecycle.js中找到了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 /* istanbul ignore if */ 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) } } // we set this to vm._watcher inside the watcher's constructor // since the watcher's initial patch may call $forceUpdate (e.g. inside child // component's mounted hook), which relies on vm._watcher being already defined new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */) hydrating = false // manually mounted instance, call mounted on self // mounted is called for render-created child components in its inserted hook if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm }
前面为一个判断,然后调用了钩子函数,然后判断走else,new watcher绑定到了vm上,进入Watcher构造函数内
export default class Watcher { vm: Component; expression: string; cb: Function; id: number; deep: boolean; user: boolean; computed: boolean; sync: boolean; dirty: boolean; active: boolean; dep: Dep; 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 } vm._watchers.push(this) // options if (options) { this.deep = !!options.deep this.user = !!options.user this.computed = !!options.computed this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.computed = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.computed // for computed watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = function () {} process.env.NODE_ENV !== 'production' && warn( `Failed watching path: "${expOrFn}" ` + 'Watcher only accepts simple dot-delimited paths. ' + 'For full control, use a function instead.', vm ) } } if (this.computed) { this.value = undefined this.dep = new Dep() } else { this.value = this.get() } } /** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) let value const vm = this.vm try { 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 } /** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } let tmp = this.depIds this.depIds = this.newDepIds this.newDepIds = tmp this.newDepIds.clear() tmp = this.deps this.deps = this.newDeps this.newDeps = tmp this.newDeps.length = 0 } /** * Subscriber interface. * Will be called when a dependency changes. */ update () { /* istanbul ignore else */ if (this.computed) { // A computed property watcher has two modes: lazy and activated. // It initializes as lazy by default, and only becomes activated when // it is depended on by at least one subscriber, which is typically // another computed property or a component's render function. if (this.dep.subs.length === 0) { // In lazy mode, we don't want to perform computations until necessary, // so we simply mark the watcher as dirty. The actual computation is // performed just-in-time in this.evaluate() when the computed property // is accessed. this.dirty = true } else { // In activated mode, we want to proactively perform the computation // but only notify our subscribers when the value has indeed changed. this.getAndInvoke(() => { this.dep.notify() }) } } else if (this.sync) { this.run() } else { queueWatcher(this) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { this.getAndInvoke(this.cb) } } getAndInvoke (cb: Function) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value this.dirty = false if (this.user) { try { cb.call(this.vm, value, oldValue) } catch (e) { handleError(e, this.vm, `callback for watcher "${this.expression}"`) } } else { cb.call(this.vm, value, oldValue) } } } /** * Evaluate and return the value of the watcher. * This only gets called for computed property watchers. */ evaluate () { if (this.dirty) { this.value = this.get() this.dirty = false } return this.value } /** * Depend on this watcher. Only for computed property watchers. */ depend () { if (this.dep && Dep.target) { this.dep.depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { // remove self from vm's watcher list // this is a somewhat expensive operation so we skip it // if the vm is being destroyed. if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
将watcher附加了若干值和方法并在watcher中调用this.get(),在get方法中又调用了getter,getter方法即是updateComponent方法,
updateComponent中调用了vm._render()函数,该方法在src/core/instance/render.js中。
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
Vue.prototype._render = function (): VNode { const vm: Component = this const { render, _parentVnode } = vm.$options // reset _rendered flag on slots for duplicate slot check if (process.env.NODE_ENV !== 'production') { for (const key in vm.$slots) { // $flow-disable-line vm.$slots[key]._rendered = false } } if (_parentVnode) { vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject } // 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`) // return error render result, // or previous vnode to prevent render error causing blank component /* istanbul ignore else */ 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 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 }
在这个方法里,主要为调用render方法,我们的render方法在上边已经写过
函数调用过程中的this,是vm._renderProxy,是一个Proxy代理对象或vm本身。我们暂且把它当做vm本身。
_c是(a, b, c, d) => createElement(vm, a, b, c, d, false)。我们简单说一下createElement干了什么。a是要创建的标签名,这里是div。接着b是data,也就是模板解析时,添加到div上的属性等。c是子元素数组,所以这里又调用了_c来创建一个p标签。
_v是createTextVNode,也就是创建一个文本结点。_s是_toString,也就是把message转换为字符串,在这里,因为有with(this),所以message传入的就是我们data中定义的第一个vue实例。
所以,从上面可以看出,render函数返回的是一个VNode对象,也就是我们的虚拟dom对象。它的返回值,将作为vm._update的第一个参数。我们接着看该函数,返回src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const prevActiveInstance = activeInstance activeInstance = 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) } activeInstance = prevActiveInstance // 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. }