Vue源码分析(5)--观察者收集、组件渲染挂载过程

前言

本文是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作为template
  if (!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, {    //主要函数1
        shouldDecodeNewlines: 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 getter
  if (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设为this
  var 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 watching
  if (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.1
      for (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 self
    var vnode;
    try {
      vnode = render.call(vm._renderProxy, vm.$createElement); //主要函数
    } catch (e) {
      //略
      }
    }
    // return empty vnode in case the render function errored out
    if (!(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 parent
    vnode.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__ 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.
  };

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); };  //返回VNODE
Vue.prototype._s = _toString; //转成字符串
Vue.prototype._v = createTextVNode; //返回VNODE
function createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val))
}

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函数,如下:
  sharedPropertyDefinition.get = function proxyGetter () {
    return this[sourceKey][key]//这里sourceKey就是_data,就是取vm._data.message
  };

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

function defineReactive$$1 (obj,key,val,customSetter) {
  var dep = new Dep();
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }
  // 引用预先定义的getter/setters
  var 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=false
    this.dirty = true;
  } else if (this.sync) {  //本节中不是同步watcher
    this.run();
  } else {
    queueWatcher(this);  //本节执行该函数
  }
};

来看看queueWatcher

function queueWatcher (watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {  //全局变量,默认false
      queue.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 flush
    if (!waiting) {  //全局变量,默认false
      waiting = 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之前重置scheduler
  var oldQueue = queue.slice();
  resetSchedulerState();
  // call updated hooks
  index = oldQueue.length;
  while (index--) {
    watcher = oldQueue[index];
    vm = watcher.vm;
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated');  //调用生命周期函数updated
    }
  }
  // devtool hook
  if (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 value
      var oldValue = this.value;
      this.value = value;
      if (this.user) {  //watch选项中定义的watcher属于user=true
        try {
          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);
      }
    }
  }
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容