前言

本文是vue2.x源码分析的第五篇,主要讲解vue实例的观察者收集、组件渲染挂载以及页面更新过程!

先看调用形式

  1. vm.$mount(vm.$options.el);

1、分析 $mount

  1. Vue$3.prototype.$mount = function (el,hydrating) {
  2. el = el && query(el);
  3. //el不能是html和body元素
  4. if (el === document.body || el === document.documentElement) {
  5. "development" !== 'production' && warn(
  6. "Do not mount Vue to <html> or <body> - mount to normal elements instead."
  7. );
  8. return this
  9. }
  10. var options = this.$options;
  11. // 如果没有提供render函数,尝试用template,若没有提供template,则取el的outerHTML作为template
  12. if (!options.render) {
  13. var template = options.template;
  14. if (template) {
  15. if (typeof template === 'string') {
  16. if (template.charAt(0) === '#') {
  17. template = idToTemplate(template);
  18. /* istanbul ignore if */
  19. if ("development" !== 'production' && !template) {
  20. warn(
  21. ("Template element not found or is empty: " + (options.template)),
  22. this
  23. );
  24. }
  25. }
  26. } else if (template.nodeType) {
  27. template = template.innerHTML;
  28. } else {
  29. {
  30. warn('invalid template option:' + template, this);
  31. }
  32. return this
  33. }
  34. } else if (el) {
  35. template = getOuterHTML(el);
  36. }
  37. if (template) {
  38. /* istanbul ignore if */
  39. if ("development" !== 'production' && config.performance && mark) {
  40. mark('compile');
  41. }
  42. //对template进行编译,主要是用正则对template进行解析
  43. var ref = compileToFunctions(template, { //主要函数1
  44. shouldDecodeNewlines: shouldDecodeNewlines,
  45. delimiters: options.delimiters
  46. }, this);
  47. var render = ref.render;
  48. var staticRenderFns = ref.staticRenderFns;
  49. options.render = render;
  50. options.staticRenderFns = staticRenderFns;
  51. if ("development" !== 'production' && config.performance && mark) {
  52. mark('compile end');
  53. measure(((this._name) + " compile"), 'compile', 'compile end');
  54. }
  55. }
  56. }
  57. return mount.call(this, el, hydrating) //主要函数2
  58. };

2、分析 compileToFunctions

  1. /*
  2. 该函数最终返回一个对象,结构如下:
  3. {
  4. render:function(){...},
  5. staticRenderFns:Array()
  6. }
  7. */
  8. function compileToFunctions (template,options,vm) {
  9. options = options || {};
  10. {
  11. //这里省略了CSP内容安全策略检查相关代码
  12. ...
  13. //若该template之前被编译过,直接返回
  14. var key = options.delimiters
  15. ? String(options.delimiters) + template
  16. : template;
  17. if (functionCompileCache[key]) {
  18. return functionCompileCache[key]
  19. }
  20. // 开始编译
  21. var compiled = compile(template, options); //返回的结果结构如下,下篇详细分析
  22. /*
  23. {
  24. ast:Object, //ast包含了template的所有信息:tag,data,children等
  25. render:'with(this){return _c...}',
  26. staticRenderFns:Array(0),
  27. errors:Array(0),
  28. tips:Array(0),
  29. __proto__:Object
  30. }
  31. */
  32. }
  33. // 编译错误检查
  34. {
  35. if (compiled.errors && compiled.errors.length) {
  36. warn(
  37. "Error compiling template:\n\n" + template + "\n\n" +
  38. compiled.errors.map(function (e) { return ("- " + e); }).join('\n') + '\n',
  39. vm
  40. );
  41. }
  42. if (compiled.tips && compiled.tips.length) {
  43. compiled.tips.forEach(function (msg) { return tip(msg, vm); });
  44. }
  45. }
  46. // compiled.render目前还只是string类型,以下将其变为function。staticRenderFns同理
  47. var res = {};
  48. var fnGenErrors = [];
  49. //将string类型的函数变为真正的函数.实现方式:new Function(compiled.render)
  50. res.render = makeFunction(compiled.render, fnGenErrors);
  51. var l = compiled.staticRenderFns.length;
  52. res.staticRenderFns = new Array(l);
  53. for (var i = 0; i < l; i++) {
  54. res.staticRenderFns[i] = makeFunction(compiled.staticRenderFns[i], fnGenErrors);
  55. }
  56. // 生成函数过程的错误检查
  57. {
  58. if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
  59. warn(
  60. "Failed to generate render function:\n\n" +
  61. fnGenErrors.map(function (ref) {
  62. var err = ref.err;
  63. var code = ref.code;
  64. return ((err.toString()) + " in\n\n" + code + "\n");
  65. }).join('\n'),
  66. vm
  67. );
  68. }
  69. }
  70. //将最终结果缓存在functionCompileCache,避免同样的template再次被编译
  71. return (functionCompileCache[key] = res)
  72. }

3、分析 mount.call(this, el, hydrating)

  1. Vue$3.prototype.$mount = function (el,hydrating) {
  2. el = el && inBrowser ? query(el) : undefined;
  3. return mountComponent(this, el, hydrating) //主要函数
  4. };

来看看 mountComponent(this, el, hydrating)

  1. function mountComponent (vm,el,hydrating) {
  2. vm.$el = el;
  3. //当render函数不存在
  4. if (!vm.$options.render) {
  5. vm.$options.render = createEmptyVNode;
  6. {
  7. if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
  8. vm.$options.el || el) {
  9. warn(
  10. 'You are using the runtime-only build of Vue where the template ' +
  11. 'compiler is not available. Either pre-compile the templates into ' +
  12. 'render functions, or use the compiler-included build.',
  13. vm
  14. );
  15. } else {
  16. warn(
  17. 'Failed to mount component: template or render function not defined.',
  18. vm
  19. );
  20. }
  21. }
  22. }
  23. //生命周期函数beforeMount被调用
  24. callHook(vm, 'beforeMount');
  25. var updateComponent;
  26. if ("development" !== 'production' && config.performance && mark) {
  27. updateComponent = function () {
  28. var name = vm._name;
  29. var id = vm._uid;
  30. var startTag = "vue-perf-start:" + id;
  31. var endTag = "vue-perf-end:" + id;
  32. mark(startTag);
  33. var vnode = vm._render();
  34. mark(endTag);
  35. measure((name + " render"), startTag, endTag);
  36. mark(startTag);
  37. vm._update(vnode, hydrating);
  38. mark(endTag);
  39. measure((name + " patch"), startTag, endTag);
  40. };
  41. } else {
  42. updateComponent = function () { //对updateComponent进行定义
  43. vm._update(vm._render(), hydrating);
  44. };
  45. }
  46. vm._watcher = new Watcher(vm, updateComponent, noop); //对updateComponent建一个watcher,这是最重要的一个watcher,负责页面的渲染和更新,单独保存在vm._watcher上,也会保存在vm._watchers数组中
  47. hydrating = false;
  48. // manually mounted instance, call mounted on self
  49. // mounted is called for render-created child components in its inserted hook
  50. //生命周期函数mounted被调用
  51. if (vm.$vnode == null) {
  52. vm._isMounted = true;
  53. callHook(vm, 'mounted');
  54. }
  55. return vm
  56. }

又要进入Watcher了

  1. var Watcher = function Watcher (vm,expOrFn,cb,options) {
  2. this.vm = vm;
  3. vm._watchers.push(this);
  4. // ... 略过属性的处理
  5. this.expression = expOrFn.toString();
  6. // parse expression for getter
  7. if (typeof expOrFn === 'function') {
  8. this.getter = expOrFn; //updateComponent被赋值给getter
  9. } else {
  10. this.getter = parsePath(expOrFn);
  11. //略过错误处理
  12. }
  13. this.value = this.lazy //这里lazy=false,故执行get函数
  14. ? undefined
  15. : this.get();
  16. };

来看看get

  1. Watcher.prototype.get = function get () {
  2. pushTarget(this); //将Dep.target设为this
  3. var value;
  4. var vm = this.vm;
  5. if (this.user) {
  6. try {
  7. value = this.getter.call(vm, vm);
  8. } catch (e) {
  9. handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
  10. }
  11. } else {
  12. value = this.getter.call(vm, vm); //getter执行,也即updateComponent执行
  13. }
  14. // "touch" every property so they are all tracked as
  15. // dependencies for deep watching
  16. if (this.deep) {
  17. traverse(value);
  18. }
  19. popTarget();
  20. this.cleanupDeps();
  21. return value
  22. };

updateComponent得到了执行,也即

  1. vm._update(vm._render(), hydrating);

得到执行,这会先执行vm._render,再执行vm._update
先看看vm._render

  1. /*
  2. 该函数最终返回一个对象vnode,结构如下(只列出了最重要的几个属性):
  3. {
  4. tag:'',
  5. data:Object,
  6. children:Array(),
  7. elm:
  8. }
  9. 这期间会对vm上的属性进行读取操作,故会触发属性的get函数,get函数里就会进行属性的依赖收集
  10. */
  11. Vue.prototype._render = function () {
  12. var vm = this;
  13. var ref = vm.$options;
  14. var render = ref.render;
  15. var staticRenderFns = ref.staticRenderFns;
  16. var _parentVnode = ref._parentVnode;
  17. if (vm._isMounted) {
  18. // Unkonwn5.1
  19. for (var key in vm.$slots) {
  20. vm.$slots[key] = cloneVNodes(vm.$slots[key]);
  21. }
  22. }
  23. vm.$scopedSlots = (_parentVnode && _parentVnode.data.scopedSlots) || emptyObject;
  24. if (staticRenderFns && !vm._staticTrees) {
  25. vm._staticTrees = [];
  26. }
  27. // set parent vnode. this allows render functions to have access
  28. // to the data on the placeholder node.
  29. vm.$vnode = _parentVnode;
  30. // render self
  31. var vnode;
  32. try {
  33. vnode = render.call(vm._renderProxy, vm.$createElement); //主要函数
  34. } catch (e) {
  35. //略
  36. }
  37. }
  38. // return empty vnode in case the render function errored out
  39. if (!(vnode instanceof VNode)) {
  40. if ("development" !== 'production' && Array.isArray(vnode)) {
  41. warn(
  42. 'Multiple root nodes returned from render function. Render function ' +
  43. 'should return a single root node.',
  44. vm
  45. );
  46. }
  47. vnode = createEmptyVNode();
  48. }
  49. // set parent
  50. vnode.parent = _parentVnode;
  51. return vnode
  52. };

再看看vm._update

  1. /*主要是调用vm.__patch__方法完成最终渲染,期间运用了虚拟DOM算法*/
  2. Vue.prototype._update = function (vnode, hydrating) {
  3. var vm = this;
  4. if (vm._isMounted) {
  5. callHook(vm, 'beforeUpdate');
  6. }
  7. var prevEl = vm.$el;
  8. var prevVnode = vm._vnode;
  9. var prevActiveInstance = activeInstance;
  10. activeInstance = vm;
  11. vm._vnode = vnode;
  12. if (!prevVnode) {
  13. // 初次渲染,vm__patch__执行完毕后vm.$el才得到DOM结构
  14. vm.$el = vm.__patch__(
  15. vm.$el, vnode, hydrating, false /* removeOnly */,
  16. vm.$options._parentElm,
  17. vm.$options._refElm
  18. );
  19. } else {
  20. // 二次渲染,即更新操作
  21. vm.$el = vm.__patch__(prevVnode, vnode);
  22. }
  23. activeInstance = prevActiveInstance;
  24. // update __vue__ reference
  25. if (prevEl) {
  26. prevEl.__vue__ = null;
  27. }
  28. if (vm.$el) {
  29. vm.$el.__vue__ = vm;
  30. }
  31. // if parent is an HOC, update its $el as well
  32. if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  33. vm.$parent.$el = vm.$el;
  34. }
  35. // updated hook is called by the scheduler to ensure that children are
  36. // updated in a parent's updated hook.
  37. };

patch的过程会用到原始的DOM API(见下面的nodeOps),期间包含虚拟DOM算法,后面单独分析。
到这里页面的第一次渲染就完成了!

  1. var nodeOps = Object.freeze({
  2. createElement: createElement$1,
  3. createElementNS: createElementNS,
  4. createTextNode: createTextNode,
  5. createComment: createComment,
  6. insertBefore: insertBefore,
  7. removeChild: removeChild,
  8. appendChild: appendChild,
  9. parentNode: parentNode,
  10. nextSibling: nextSibling,
  11. tagName: tagName,
  12. setTextContent: setTextContent,
  13. setAttribute: setAttribute
  14. });

接着vm._render来看属性的依赖收集是怎么回事,注意调用时传入的是vm._renderProxy,而不是vm实例。vm._renderProxy有个has代理,即访问vm上的属性时,会先调用has函数

  1. render.call(vm._renderProxy, vm.$createElement);

render完全是由字符串拼接而成的函数,长这个样子

  1. (function() {
  2. with(this){return _c('div',{attrs:{"id":"app"}},[_v("\n\t\t"+_s(message)+"\n\t\t"),
  3. _c('div',{domProps:{"textContent":_s(abc)}})])}
  4. })

这里的this即vm._renderProxy,由于使用了with,故函数的当前活动对象就是vm._renderProxy对象,所以内部不需要
这样取属性:vm._renderProxy.xx,直接用xx就可拿到。
这里的_c,_v,_s函数在初始化过程中已定义好,如下:

  1. vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); }; //返回VNODE
  2. Vue.prototype._s = _toString; //转成字符串
  3. Vue.prototype._v = createTextVNode; //返回VNODE
  4. function createTextVNode (val) {
  5. return new VNode(undefined, undefined, undefined, String(val))
  6. }

with大家可能用的少,故分析下render函数的执行过程:

  1. 判断第一个_c在不在vm上;
  2. 判断_v在不在vm上;
  3. 判断第一个_s在不在vm上;
  4. 判断message在不在vm上;
  5. 触发message的get函数;
  6. 调用_s(vm.message);
  7. 调用_v,生成text类型的VNODE;
  8. 判断第二个_c在不在vm上;
  9. 判断第二个_s在不在vm上;
  10. 判断abc在不在vm上;
  11. 触发abc的get函数;
  12. 调用_s(vm.abc);
  13. 调用第二个_c,生成VNODE;
  14. 调用第一个_c,生成VNODE,执行完毕

来看看第5和11中如何进行依赖收集(以5为例,11同理)

  • 首先触发vm.message的get函数,如下:
  1. sharedPropertyDefinition.get = function proxyGetter () {
  2. return this[sourceKey][key]//这里sourceKey就是_data,就是取vm._data.message
  3. };

接着触发vm._data中message的get函数,这个get是通过defineReactive1定义的,defineReactive1在第四课也有分析,如下:

  1. function defineReactive$$1 (obj,key,val,customSetter) {
  2. var dep = new Dep();
  3. var property = Object.getOwnPropertyDescriptor(obj, key);
  4. if (property && property.configurable === false) {
  5. return
  6. }
  7. // 引用预先定义的getter/setters
  8. var getter = property && property.get;
  9. var setter = property && property.set;
  10. //val可能是对象,故继续观测
  11. var childOb = observe(val);
  12. Object.defineProperty(obj, key, {
  13. enumerable: true,
  14. configurable: true,
  15. get: function reactiveGetter () { //本节中就是触发这个get函数
  16. var value = getter ? getter.call(obj) : val;
  17. if (Dep.target) { //这里的Dep.target在3中分析mount.call时设为了最主要的那个watch实例
  18. dep.depend();//每个属性都会对应一个dep实例,用于收集该属性的观察者,这里就将最重要的watcher实例进行了收集
  19. //子对象也必须收集父对象的观察者,否则子对象里的属性改变后无法更新,这个最重要的观察者必须被每个属性所收集
  20. if (childOb) {
  21. childOb.dep.depend(); //
  22. }
  23. //对数组的依赖处理
  24. if (Array.isArray(value)) {
  25. dependArray(value);
  26. }
  27. }
  28. return value
  29. },
  30. //set省略
  31. });

现在假如message属性发生了变化,来看看页面是如何更新的

  1. setTimeout(()=>vm.message='world',0)

第一步当然是触发vm.message的set函数

  1. sharedPropertyDefinition.set = function proxySetter (val) {
  2. this[sourceKey][key] = val; //这里sourceKey就是_data,就是设置vm._data.message=val
  3. };

这又会触发vm.data中message的set函数

  1. set: function reactiveSetter (newVal) {
  2. var value = getter ? getter.call(obj) : val;
  3. if (newVal === value || (newVal !== newVal && value !== value)) {
  4. return
  5. }
  6. if ("development" !== 'production' && customSetter) {
  7. customSetter();//报错用
  8. }
  9. if (setter) {
  10. setter.call(obj, newVal);
  11. } else {
  12. val = newVal;
  13. }
  14. childOb = observe(newVal); //设置新值后,对新值进行观测
  15. dep.notify(); //触发观察者的回调或get函数
  16. }

显然这里主要是执行dep.notify(),当然不能忘记对新值进行观测

  1. Dep.prototype.notify = function notify () {
  2. //之前message的观察者就是收集在this.subs数组里
  3. var subs = this.subs.slice();
  4. for (var i = 0, l = subs.length; i < l; i++) {
  5. subs[i].update(); //对message的每个观察者分别调用update,不要忘记观察者就是watcher实例,它保存了回调函数cb,expOrFun等信息
  6. }
  7. };

来看看update

  1. Watcher.prototype.update = function update () {
  2. if (this.lazy) { //本节中lazy=false
  3. this.dirty = true;
  4. } else if (this.sync) { //本节中不是同步watcher
  5. this.run();
  6. } else {
  7. queueWatcher(this); //本节执行该函数
  8. }
  9. };

来看看queueWatcher

  1. function queueWatcher (watcher) {
  2. var id = watcher.id;
  3. if (has[id] == null) {
  4. has[id] = true;
  5. if (!flushing) { //全局变量,默认false
  6. queue.push(watcher); //将watcher放入queue数组
  7. } else {
  8. // if already flushing, splice the watcher based on its id
  9. // if already past its id, it will be run next immediately.
  10. var i = queue.length - 1;
  11. while (i >= 0 && queue[i].id > watcher.id) {
  12. i--;
  13. }
  14. queue.splice(Math.max(i, index) + 1, 0, watcher);
  15. }
  16. // queue the flush
  17. if (!waiting) { //全局变量,默认false
  18. waiting = true;
  19. nextTick(flushSchedulerQueue); //这里的nextTick是作者自己实现的,属于异步微任务,跟setTimeout等函数类似
  20. //这样设计的目的是不让当前watcher马上执行,而是等某个属性的所有watcher都进入queue后再一起执行,在本节里
  21. //例子中,message属性上有三个watcher,分别对应是watch选项、最重要watcher、computed选项,而由于computed选项中
  22. //的watcher的lazy=true,故不会进入到queue.最终只有两个watcher进入queue
  23. }
  24. }
  25. }

接下来就是等宏任务执行完,然后flushSchedulerQueue开始执行

  1. function flushSchedulerQueue () {
  2. flushing = true;
  3. var watcher, id, vm;
  4. // 对queue进行排序
  5. // This ensures that:
  6. // 1. Components are updated from parent to child. (because parent is always
  7. // created before the child)
  8. // 2. A component's user watchers are run before its render watcher (because
  9. // user watchers are created before the render watcher)
  10. // 3. If a component is destroyed during a parent component's watcher run,
  11. // its watchers can be skipped.
  12. // 先创建的watcher排在前面
  13. queue.sort(function (a, b) { return a.id - b.id; });
  14. //不对length进行缓存,因为在执行过程中可能有新watcher加入
  15. for (index = 0; index < queue.length; index++) {
  16. watcher = queue[index];
  17. id = watcher.id;
  18. has[id] = null;
  19. watcher.run();//对每个watcher分别调用其run函数
  20. // in dev build, check and stop circular updates.
  21. if ("development" !== 'production' && has[id] != null) {
  22. circular[id] = (circular[id] || 0) + 1;
  23. if (circular[id] > config._maxUpdateCount) {
  24. warn(
  25. 'You may have an infinite update loop ' + (
  26. watcher.user
  27. ? ("in watcher with expression \"" + (watcher.expression) + "\"")
  28. : "in a component render function."
  29. ),
  30. watcher.vm
  31. );
  32. break
  33. }
  34. }
  35. }
  36. // 调用生命周期函数updated之前重置scheduler
  37. var oldQueue = queue.slice();
  38. resetSchedulerState();
  39. // call updated hooks
  40. index = oldQueue.length;
  41. while (index--) {
  42. watcher = oldQueue[index];
  43. vm = watcher.vm;
  44. if (vm._watcher === watcher && vm._isMounted) {
  45. callHook(vm, 'updated'); //调用生命周期函数updated
  46. }
  47. }
  48. // devtool hook
  49. if (devtools && config.devtools) {
  50. devtools.emit('flush');
  51. }
  52. }

来看看run函数

  1. Watcher.prototype.run = function run () {
  2. if (this.active) {
  3. var value = this.get(); //watch选项中定义的用户watcher会执行,取得新设的值'world';最重要的render watcher也执行这个get.不过user watcher执行时是取值操作,而render watcher执行时执行了updateComponent,就是
  4. //这个:
  5. /*updateComponent = function () {
  6. vm._update(vm._render(), hydrating); //这里就回到了初次渲染的过程,于是完成了更新
  7. };*/
  8. if (
  9. value !== this.value ||
  10. // Deep watchers and watchers on Object/Arrays should fire even
  11. // when the value is the same, because the value may
  12. // have mutated.
  13. isObject(value) ||
  14. this.deep
  15. ) {
  16. // set new value
  17. var oldValue = this.value;
  18. this.value = value;
  19. if (this.user) { //watch选项中定义的watcher属于user=true
  20. try {
  21. this.cb.call(this.vm, value, oldValue); //调用watch选项中对message设置的回调函数cb,输出'message changed'
  22. } catch (e) {
  23. handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
  24. }
  25. } else {
  26. this.cb.call(this.vm, value, oldValue);
  27. }
  28. }
  29. }
  30. };