前言
本文是vue2.x源码分析的第五篇,主要讲解vue实例的观察者收集、组件渲染挂载以及页面更新过程!
先看调用形式
vm.$mount(vm.$options.el);
1、分析 $mount
Vue$3.prototype.$mount = function (el,hydrating) {el = el && query(el);//el不能是html和body元素if (el === document.body || el === document.documentElement) {"development" !== 'production' && warn("Do not mount Vue to <html> or <body> - mount to normal elements instead.");return this}var options = this.$options;// 如果没有提供render函数,尝试用template,若没有提供template,则取el的outerHTML作为templateif (!options.render) {var template = options.template;if (template) {if (typeof template === 'string') {if (template.charAt(0) === '#') {template = idToTemplate(template);/* istanbul ignore if */if ("development" !== 'production' && !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 ("development" !== 'production' && config.performance && mark) {mark('compile');}//对template进行编译,主要是用正则对template进行解析var ref = compileToFunctions(template, { //主要函数1shouldDecodeNewlines: shouldDecodeNewlines,delimiters: options.delimiters}, this);var render = ref.render;var staticRenderFns = ref.staticRenderFns;options.render = render;options.staticRenderFns = staticRenderFns;if ("development" !== 'production' && config.performance && mark) {mark('compile end');measure(((this._name) + " compile"), 'compile', 'compile end');}}}return mount.call(this, el, hydrating) //主要函数2};
2、分析 compileToFunctions
/*该函数最终返回一个对象,结构如下:{render:function(){...},staticRenderFns:Array()}*/function compileToFunctions (template,options,vm) {options = options || {};{//这里省略了CSP内容安全策略检查相关代码...//若该template之前被编译过,直接返回var key = options.delimiters? String(options.delimiters) + template: template;if (functionCompileCache[key]) {return functionCompileCache[key]}// 开始编译var compiled = compile(template, options); //返回的结果结构如下,下篇详细分析/*{ast:Object, //ast包含了template的所有信息:tag,data,children等render:'with(this){return _c...}',staticRenderFns:Array(0),errors:Array(0),tips:Array(0),__proto__:Object}*/}// 编译错误检查{if (compiled.errors && compiled.errors.length) {warn("Error compiling template:\n\n" + template + "\n\n" +compiled.errors.map(function (e) { return ("- " + e); }).join('\n') + '\n',vm);}if (compiled.tips && compiled.tips.length) {compiled.tips.forEach(function (msg) { return tip(msg, vm); });}}// compiled.render目前还只是string类型,以下将其变为function。staticRenderFns同理var res = {};var fnGenErrors = [];//将string类型的函数变为真正的函数.实现方式:new Function(compiled.render)res.render = makeFunction(compiled.render, fnGenErrors);var l = compiled.staticRenderFns.length;res.staticRenderFns = new Array(l);for (var i = 0; i < l; i++) {res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors);}// 生成函数过程的错误检查{if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {warn("Failed to generate render function:\n\n" +fnGenErrors.map(function (ref) {var err = ref.err;var code = ref.code;return ((err.toString()) + " in\n\n" + code + "\n");}).join('\n'),vm);}}//将最终结果缓存在functionCompileCache,避免同样的template再次被编译return (functionCompileCache[key] = res)}
3、分析 mount.call(this, el, hydrating)
Vue$3.prototype.$mount = function (el,hydrating) {el = el && inBrowser ? query(el) : undefined;return mountComponent(this, el, hydrating) //主要函数};
来看看 mountComponent(this, el, hydrating)
function mountComponent (vm,el,hydrating) {vm.$el = el;//当render函数不存在if (!vm.$options.render) {vm.$options.render = createEmptyVNode;{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);}}}//生命周期函数beforeMount被调用callHook(vm, 'beforeMount');var updateComponent;if ("development" !== 'production' && 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((name + " render"), startTag, endTag);mark(startTag);vm._update(vnode, hydrating);mark(endTag);measure((name + " patch"), startTag, endTag);};} else {updateComponent = function () { //对updateComponent进行定义vm._update(vm._render(), hydrating);};}vm._watcher = new Watcher(vm, updateComponent, noop); //对updateComponent建一个watcher,这是最重要的一个watcher,负责页面的渲染和更新,单独保存在vm._watcher上,也会保存在vm._watchers数组中hydrating = false;// manually mounted instance, call mounted on self// mounted is called for render-created child components in its inserted hook//生命周期函数mounted被调用if (vm.$vnode == null) {vm._isMounted = true;callHook(vm, 'mounted');}return vm}
又要进入Watcher了
var Watcher = function Watcher (vm,expOrFn,cb,options) {this.vm = vm;vm._watchers.push(this);// ... 略过属性的处理this.expression = expOrFn.toString();// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn; //updateComponent被赋值给getter} else {this.getter = parsePath(expOrFn);//略过错误处理}this.value = this.lazy //这里lazy=false,故执行get函数? undefined: this.get();};
来看看get
Watcher.prototype.get = function get () {pushTarget(this); //将Dep.target设为thisvar value;var vm = this.vm;if (this.user) {try {value = this.getter.call(vm, vm);} catch (e) {handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));}} else {value = this.getter.call(vm, vm); //getter执行,也即updateComponent执行}// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value);}popTarget();this.cleanupDeps();return value};
updateComponent得到了执行,也即
vm._update(vm._render(), hydrating);
得到执行,这会先执行vm._render,再执行vm._update
先看看vm._render
/*该函数最终返回一个对象vnode,结构如下(只列出了最重要的几个属性):{tag:'',data:Object,children:Array(),elm:}这期间会对vm上的属性进行读取操作,故会触发属性的get函数,get函数里就会进行属性的依赖收集*/Vue.prototype._render = function () {var vm = this;var ref = vm.$options;var render = ref.render;var staticRenderFns = ref.staticRenderFns;var _parentVnode = ref._parentVnode;if (vm._isMounted) {// Unkonwn5.1for (var key in vm.$slots) {vm.$slots[key] = cloneVNodes(vm.$slots[key]);}}vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;if (staticRenderFns && !vm._staticTrees) {vm._staticTrees = [];}// set parent vnode. this allows render functions to have access// to the data on the placeholder node.vm.$vnode = _parentVnode;// render selfvar vnode;try {vnode = render.call(vm._renderProxy, vm.$createElement); //主要函数} catch (e) {//略}}// return empty vnode in case the render function errored outif (!(vnode instanceof VNode)) {if ("development" !== 'production' && Array.isArray(vnode)) {warn('Multiple root nodes returned from render function. Render function ' +'should return a single root node.',vm);}vnode = createEmptyVNode();}// set parentvnode.parent = _parentVnode;return vnode};
再看看vm._update
/*主要是调用vm.__patch__方法完成最终渲染,期间运用了虚拟DOM算法*/Vue.prototype._update = function (vnode, hydrating) {var vm = this;if (vm._isMounted) {callHook(vm, 'beforeUpdate');}var prevEl = vm.$el;var prevVnode = vm._vnode;var prevActiveInstance = activeInstance;activeInstance = vm;vm._vnode = vnode;if (!prevVnode) {// 初次渲染,vm__patch__执行完毕后vm.$el才得到DOM结构vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */,vm.$options._parentElm,vm.$options._refElm);} else {// 二次渲染,即更新操作vm.$el = vm.__patch__(prevVnode, vnode);}activeInstance = prevActiveInstance;// update __vue__ referenceif (prevEl) {prevEl.__vue__ = null;}if (vm.$el) {vm.$el.__vue__ = vm;}// if parent is an HOC, update its $el as wellif (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.};
patch的过程会用到原始的DOM API(见下面的nodeOps),期间包含虚拟DOM算法,后面单独分析。
到这里页面的第一次渲染就完成了!
var nodeOps = Object.freeze({createElement: createElement$1,createElementNS: createElementNS,createTextNode: createTextNode,createComment: createComment,insertBefore: insertBefore,removeChild: removeChild,appendChild: appendChild,parentNode: parentNode,nextSibling: nextSibling,tagName: tagName,setTextContent: setTextContent,setAttribute: setAttribute});
接着vm._render来看属性的依赖收集是怎么回事,注意调用时传入的是vm._renderProxy,而不是vm实例。vm._renderProxy有个has代理,即访问vm上的属性时,会先调用has函数
render.call(vm._renderProxy, vm.$createElement);
render完全是由字符串拼接而成的函数,长这个样子
(function() {with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n\t\t"+_s(message)+"\n\t\t"),_c('div',{domProps:{"textContent":_s(abc)}})])}})
这里的this即vm._renderProxy,由于使用了with,故函数的当前活动对象就是vm._renderProxy对象,所以内部不需要
这样取属性:vm._renderProxy.xx,直接用xx就可拿到。
这里的_c,_v,_s函数在初始化过程中已定义好,如下:
vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }; //返回VNODEVue.prototype._s = _toString; //转成字符串Vue.prototype._v = createTextVNode; //返回VNODEfunction createTextVNode (val) {return new VNode(undefined, undefined, undefined, String(val))}
with大家可能用的少,故分析下render函数的执行过程:
- 判断第一个_c在不在vm上;
- 判断_v在不在vm上;
- 判断第一个_s在不在vm上;
- 判断message在不在vm上;
- 触发message的get函数;
- 调用_s(vm.message);
- 调用_v,生成text类型的VNODE;
- 判断第二个_c在不在vm上;
- 判断第二个_s在不在vm上;
- 判断abc在不在vm上;
- 触发abc的get函数;
- 调用_s(vm.abc);
- 调用第二个_c,生成VNODE;
- 调用第一个_c,生成VNODE,执行完毕
来看看第5和11中如何进行依赖收集(以5为例,11同理)
- 首先触发vm.message的get函数,如下:
sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]//这里sourceKey就是_data,就是取vm._data.message};
接着触发vm._data中message的get函数,这个get是通过defineReactive1定义的,defineReactive1在第四课也有分析,如下:
function defineReactive$$1 (obj,key,val,customSetter) {var dep = new Dep();var property = Object.getOwnPropertyDescriptor(obj, key);if (property && property.configurable === false) {return}// 引用预先定义的getter/settersvar getter = property && property.get;var setter = property && property.set;//val可能是对象,故继续观测var childOb = observe(val);Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () { //本节中就是触发这个get函数var value = getter ? getter.call(obj) : val;if (Dep.target) { //这里的Dep.target在3中分析mount.call时设为了最主要的那个watch实例dep.depend();//每个属性都会对应一个dep实例,用于收集该属性的观察者,这里就将最重要的watcher实例进行了收集//子对象也必须收集父对象的观察者,否则子对象里的属性改变后无法更新,这个最重要的观察者必须被每个属性所收集if (childOb) {childOb.dep.depend(); //}//对数组的依赖处理if (Array.isArray(value)) {dependArray(value);}}return value},//set省略});
现在假如message属性发生了变化,来看看页面是如何更新的
setTimeout(()=>vm.message='world',0)
第一步当然是触发vm.message的set函数
sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val; //这里sourceKey就是_data,就是设置vm._data.message=val};
这又会触发vm.data中message的set函数
set: function reactiveSetter (newVal) {var value = getter ? getter.call(obj) : val;if (newVal === value || (newVal !== newVal && value !== value)) {return}if ("development" !== 'production' && customSetter) {customSetter();//报错用}if (setter) {setter.call(obj, newVal);} else {val = newVal;}childOb = observe(newVal); //设置新值后,对新值进行观测dep.notify(); //触发观察者的回调或get函数}
显然这里主要是执行dep.notify(),当然不能忘记对新值进行观测
Dep.prototype.notify = function notify () {//之前message的观察者就是收集在this.subs数组里var subs = this.subs.slice();for (var i = 0, l = subs.length; i < l; i++) {subs[i].update(); //对message的每个观察者分别调用update,不要忘记观察者就是watcher实例,它保存了回调函数cb,expOrFun等信息}};
来看看update
Watcher.prototype.update = function update () {if (this.lazy) { //本节中lazy=falsethis.dirty = true;} else if (this.sync) { //本节中不是同步watcherthis.run();} else {queueWatcher(this); //本节执行该函数}};
来看看queueWatcher
function queueWatcher (watcher) {var id = watcher.id;if (has[id] == null) {has[id] = true;if (!flushing) { //全局变量,默认falsequeue.push(watcher); //将watcher放入queue数组} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.var i = queue.length - 1;while (i >= 0 && queue[i].id > watcher.id) {i--;}queue.splice(Math.max(i, index) + 1, 0, watcher);}// queue the flushif (!waiting) { //全局变量,默认falsewaiting = true;nextTick(flushSchedulerQueue); //这里的nextTick是作者自己实现的,属于异步微任务,跟setTimeout等函数类似//这样设计的目的是不让当前watcher马上执行,而是等某个属性的所有watcher都进入queue后再一起执行,在本节里//例子中,message属性上有三个watcher,分别对应是watch选项、最重要watcher、computed选项,而由于computed选项中//的watcher的lazy=true,故不会进入到queue.最终只有两个watcher进入queue}}}
接下来就是等宏任务执行完,然后flushSchedulerQueue开始执行
function flushSchedulerQueue () {flushing = true;var watcher, id, vm;// 对queue进行排序// This ensures that:// 1. Components are updated from parent to child. (because parent is always// created before the child)// 2. A component's user watchers are run before its render watcher (because// user watchers are created before the render watcher)// 3. If a component is destroyed during a parent component's watcher run,// its watchers can be skipped.// 先创建的watcher排在前面queue.sort(function (a, b) { return a.id - b.id; });//不对length进行缓存,因为在执行过程中可能有新watcher加入for (index = 0; index < queue.length; index++) {watcher = queue[index];id = watcher.id;has[id] = null;watcher.run();//对每个watcher分别调用其run函数// in dev build, check and stop circular updates.if ("development" !== 'production' && has[id] != null) {circular[id] = (circular[id] || 0) + 1;if (circular[id] > config._maxUpdateCount) {warn('You may have an infinite update loop ' + (watcher.user? ("in watcher with expression \"" + (watcher.expression) + "\""): "in a component render function."),watcher.vm);break}}}// 调用生命周期函数updated之前重置schedulervar oldQueue = queue.slice();resetSchedulerState();// call updated hooksindex = oldQueue.length;while (index--) {watcher = oldQueue[index];vm = watcher.vm;if (vm._watcher === watcher && vm._isMounted) {callHook(vm, 'updated'); //调用生命周期函数updated}}// devtool hookif (devtools && config.devtools) {devtools.emit('flush');}}
来看看run函数
Watcher.prototype.run = function run () {if (this.active) {var value = this.get(); //watch选项中定义的用户watcher会执行,取得新设的值'world';最重要的render watcher也执行这个get.不过user watcher执行时是取值操作,而render watcher执行时执行了updateComponent,就是//这个:/*updateComponent = function () {vm._update(vm._render(), hydrating); //这里就回到了初次渲染的过程,于是完成了更新};*/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 valuevar oldValue = this.value;this.value = value;if (this.user) { //watch选项中定义的watcher属于user=truetry {this.cb.call(this.vm, value, oldValue); //调用watch选项中对message设置的回调函数cb,输出'message changed'} catch (e) {handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));}} else {this.cb.call(this.vm, value, oldValue);}}}};
