写于vue3.0发布前夕的helloworld之二
接着,继续走,来到了vm.$mount。
开始生成render函数,生成VNode,由于是第一次加载,所以patch机制为只删除前一个dom节点机制,下面都会讲到。
先到$mount:
Vue.prototype.$mount = function ( el, hydrating ) { el = el && query(el); /* istanbul ignore if */ if (el === document.body || el === document.documentElement) { warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead." ); return this } var options = this.$options; // resolve template/el and convert to render function if (!options.render) { var template = options.template; if (template) { if (typeof template === 'string') { if (template.charAt(0) === '#') { template = idToTemplate(template); /* istanbul ignore if */ if (!template) { warn( ("Template element not found or is empty: " + (options.template)), this ); } } } else if (template.nodeType) { template = template.innerHTML; } else { { warn('invalid template option:' + template, this); } return this } } else if (el) { template = getOuterHTML(el); } if (template) { /* istanbul ignore if */ if (config.performance && mark) { mark('compile'); } var ref = compileToFunctions(template, { outputSourceRange: "development" !== 'production', shouldDecodeNewlines: shouldDecodeNewlines, shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, delimiters: options.delimiters, comments: options.comments }, this); var render = ref.render; var staticRenderFns = ref.staticRenderFns; options.render = render; options.staticRenderFns = staticRenderFns; /* istanbul ignore if */ if (config.performance && mark) { mark('compile end'); measure(("vue " + (this._name) + " compile"), 'compile', 'compile end'); } } } return mount.call(this, el, hydrating) };
一句句来讲,这里边是挺长,但是足够简单,首先判断el是否为body或者html元素,是则警告,然后退出当前函数,然后根据条件拿到template,因为我们没有设置render和template,所以template走else分支getOuterHTML,拿到template之后,会将当前的template编译成一个render函数,这个编译函数叫做compileToFunctions:这个函数是真的长,而且与主题干系不大,这里我们用文字解释一下就行,首先他是一个闭包函数,闭包了一个叫cache的对象这个对象是缓存当前已经编译过的模板的,进入函数之后有缓存直接返回缓存,没有的话就会执行compile函数,这些都是编译过程,最终compile会调用parseHTML方法使用正则加字符串分析字符串,返回一个这样的对象:
{ "type":1, "tag":"div", "attrsList":[ {"name":"id","value":"app","start":5,"end":13} ], "attrsMap":{ "id":"app" }, "rawAttrsMap":{ "id":{ "name":"id", "value":"app", "start":5, "end":13 } }, "children": [ { "type":2, "expression":"_s(msg)", "tokens":[{"@binding":"msg"}], "text":"{{msg}}", "start":14,"end":21 } ], "start":0, "end":27, "plain":false, "attrs":[{"name":"id","value":"\"app\"","start":5,"end":13}] }
这就是我们的ast语法树,之后会根据ast生成code,这个方法叫做generate:
function generate ( ast, options ) { var state = new CodegenState(options); var code = ast ? genElement(ast, state) : '_c("div")'; return { render: ("with(this){return " + code + "}"), staticRenderFns: state.staticRenderFns } }
这个方法调用了genElement方法,简而言之,里边的方法生成code时都会以ast上的值为原料,进行字符串拼接,最后code长这样:
_c('div',{attrs:{"id":"app"}},[_v(_s(msg))])
这里做一下解释_c,_v都是vue内部创建虚拟dom的函数,_s其实就是将当前值转化成字符串,继续走,generate执行完毕之后返回到compileToFunctions,这个时候拼接好的render字符串是这样:
"with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(msg))])}"
然后会使用new Function 将其生成一个函数,这个函数就是我们的render,组建上的render,执行完compileToFunctions,继续回到$mount函数里,执行最终的$mount:
Vue.prototype.$mount = function ( el, hydrating ) { el = el && inBrowser ? query(el) : undefined; return mountComponent(this, el, hydrating) };
可见mout调用了mountComponent方法:
function mountComponent ( vm, el, hydrating ) { vm.$el = el; if (!vm.$options.render) { vm.$options.render = createEmptyVNode; { /* 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'); var updateComponent; /* istanbul ignore if */ if (config.performance && mark) { updateComponent = function () { var name = vm._name; var id = vm._uid; var startTag = "vue-perf-start:" + id; var endTag = "vue-perf-end:" + id; mark(startTag); var 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 = function () { 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: function before () { if (vm._isMounted && !vm._isDestroyed) { 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 }
mountComponent方法一进来,先判断是否有render,之后会根据条件警告一些信息。然后开始调起beforeMount钩子函数,下来初始化一个updateComponent方法,他的正常版本长这样:
updateComponent = function () { vm._update(vm._render(), hydrating); };
实际上在组建的dom更新,包括第一次挂载,和随后的相应式更新上,本质都用的是这一个函数。然后继续走,重头戏来啦,watcher在这里开始初始化,注意传给他的第四个参数,在哪里我们把要调用beforeUpdate给传了过去。
我们的下面来到watcher的构造函数: