vue源码分析系列二:$mount()和new Watcher()的执行过程

本文深入解析Vue的挂载过程,从newVue初始化到$mount调用,再到mountComponent执行,逐步剖析Vue如何将组件挂载到DOM,并探讨渲染Watcher的作用及其实现细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

vue源码分析系列一:new Vue的初始化过程
initMixin()里面调用了$mount()

if (vm.$options.el) {
     vm.$mount(vm.$options.el);// 挂载dom元素
}

$mount()方法定义

Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义,如 src/platform/web/entry-runtime-with-compiler.js、src/platform/web/runtime/index.js、src/platform/weex/runtime/index.js。因为 $mount 这个方法的实现是和平台、构建方式都相关的。接下来我们重点分析带 compiler 版本的 $mount 实现,因为抛开 webpack 的 vue-loader,我们在纯前端浏览器环境分析 Vue 的工作原理,有助于我们对原理理解的深入。
在这里插入图片描述
compiler 版本的 $mount 实现非常有意思,先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

Vue.prototype.$mount = function() {
    el = el && query(el); // 表示如果el存在就执行query(el)方法
    
    if (el === document.body || el === document.documentElement) {
        // Vue 不能挂载在 body、html 这样的根节点上,因为它会替换掉这些元素
        process.env.NODE_ENV !== 'production' && warn(
          "Do not mount Vue to <html> or <body> - mount to normal elements instead."
        );
        return this
    }
    
    // 如果有render方法,就直接 return 并调用原先原型原型上的 $mount 方法
    // 如果没有render方法,就判断有没有template
    // 如果没有template就会调用template = getOuterHTML(el)
    // 总之,最终还是会将template转换成render函数
    // 最后再调用 mountComponent 方法
    if (!this.$options.render) {
        if(this.$options.template){
            if (template.charAt(0) === '#') { // 我们这里的 charAt(0) 是 '<'
              template = idToTemplate(template);
              /* istanbul ignore if */
              if (process.env.NODE_ENV !== 'production' && !template) {
                warn(
                  ("Template element not found or is empty: " + (options.template)),
                  this
                );
              }
            }
        }
    }
  
    // 然后开始编译
    if (template) {
        // 通过compileToFunction 生成 render 函数,渲染vnode的时候会用到
        var ref = compileToFunctions(); 
        var render = ref.render;
        var render = ref.render;
        var staticRenderFns = ref.staticRenderFns; // 静态 render函数
        options.render = render; // 会在渲染 Vnode 的时候用到
        options.staticRenderFns = staticRenderFns;
    }
    
    return mount.call(this, el, hydrating)
    // 最后,调用原先原型上的 $mount 方法挂载。
    // 并执行 mountComponent() 方法
    // mountComponent 在 core/instance/lifecycle 中   
}

这段代码首先缓存了原型上的 $mount 方法,再重新定义该方法,我们先来分析这段代码。首先,它对 el 做了限制,Vue 不能挂载在 body、html 这样的根节点上。接下来的是很关键的逻辑 —— 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的,编译过程我们之后会介绍。最后,调用原先原型上的 $mount 方法挂载。

原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的。

// public mount method
Vue.prototype.$mount = function ( // 这是最开始定义的$mount方法,在runtime-only版本中
  el,
  hydrating
) {
    //var inBrowser = typeof window !== 'undefined';
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating) // 然后执行 mountComponent方法
};

$mount 方法支持传入 2 个参数,第一个是 el,它表示挂载的元素,可以是字符串,也可以是 DOM 对象,如果是字符串在浏览器环境下会调用 query 方法转换成 DOM 对象的。第二个参数是和服务端渲染相关,在浏览器环境下我们不需要传第二个参数。

query(el)获取dom元素

/**
 * Query an element selector if it's not an element already.
 */
function query (el) {
  if (typeof el === 'string') {
    var selected = document.querySelector(el);
    if (!selected) { // 如果存在 el 元素,就直接返回 selected,否则如下
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      );
      return document.createElement('div') // 如果不存在el元素,就返回一个空div
    }
    return selected
  } else { // 否则直接返回dom元素
    return el
  }
}

mountComponent()方法

function mountComponent (
  vm,
  el,
  hydrating
) {
  debugger
  vm.$el = el; // 对 el 进行缓存
  if (!vm.$options.render) { // 如果没有render函数,包括 template 没有正确的转换成render函数,就执行 if 语句
    vm.$options.render = createEmptyVNode; // createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        // 如果使用了template而不是render函数但是使用的runtime-only版本,就报这个警告
        // 如果使用了template 但是不是用的 compile 版本,也会报警告
        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 {
        // 如果没有使用 template 或者 render 函数,就报这个警告
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
      }
    }
  }
  callHook(vm, 'beforeMount');

  var updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    // mark 是 util 工具函数中的perf,这里先不作深入研究,主要研究主线。
    // 性能埋点相关
    // 提供程序的运行状况,
    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._render() 方法渲染出来一个 VNode
      // jydrating 跟服务端渲染相关,如果没有启用的话,其为 false

      // 当收集好了依赖之后,会通过 Watcher 的 this.getter(vm, vm) 来调用 updateComponent() 方法
      vm._update(vm._render(), hydrating); // 然后执行 vm._render()方法
    };
  }

从上面的代码可以看到,mountComponent 核心就是先实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,在此方法中调用 vm._render 方法先生成虚拟 Node,最终调用 vm._update 更新 DOM。

Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。

函数最后判断为根节点的时候设置 vm._isMountedtrue, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。
mountComponent 方法的逻辑也是非常清晰的,它会完成整个渲染工作

updateComponent()

在这个方法里有两个方法需要调用:vm._render() and vm._update(),先调用 _render 方法生成一个vnode,然后将这个vnode传入到 _update()方法中

updateComponent = function () {
    // vm._render() 方法渲染出来一个 VNode
    // jydrating 跟服务端渲染相关,如果没有启用的话,其为 false
    // 当收集好了依赖之后,会通过 Watcher 的 this.getter(vm, vm) 来调用 updateComponent() 方法
    vm._update(vm._render(), hydrating); // 然后执行 vm._render()方法
};

new Watcher()

渲染watcher,Watcher 在这里起到两个作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数

new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
var Watcher = function Watcher (
  vm,
  expOrFn, // 是一个表达式还是一个 fn
  cb, // 回调
  options, // 配置
  isRenderWatcher // 是否是一个渲染watcher
) {
  this.vm = vm;
  if (isRenderWatcher) { // 如果是渲染 Watcher
    vm._watcher = this; // this 表示 Vue实例, 其中包括了你定义了的数据,也有 $options,还有_data,都可以获取到你定义的数据
  }
  vm._watchers.push(this); // 然后将Vue 实例push到 _watchers中, 在initState中 vm._watchers = []
  // options
  if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$1; // uid for batching
  this.active = true;
  this.dirty = this.lazy; // for lazy watchers
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  
  // expOrFn: updateComponent = function() { vm._update(vm._render(), hydrating); }
  this.expression = process.env.NODE_ENV !== 'production'
    ? expOrFn.toString() 
    : '';
  // parse expression for getter
  if (typeof expOrFn === 'function') { // 判断expOrFn是否是一个函数
    this.getter = expOrFn; // this => Watcher
  } 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
      );
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get(); // this表示Watcher,其原型上定义了get方法
};

Watcher.prototype.get()

在Watcher的构造函数中定义了getter函数:this.getter = expOrFn。这个expOrFn 是updateComponent方法,在Watcher.prototype.get()方法中通过this.getter.call(vm, vm)来调用updateComponent方法,然后执行vm._update(vm._render, hydrating)

Watcher.prototype.get = function get () {
  // 依赖收集
  pushTarget(this);
  var value;
  var vm = this.vm; // this => Watcher,里面有当前的Vue实例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
};

总结

到这里,我们知道updateComponent方法会完成整个渲染工作,在系列三中,将深入分析 vm._render()方法以及vm._update()方法。

<think>好的,我现在要帮助用户理解Vue2的底层实现原理,特别是响应式机制虚拟DOM。首先,我需要回顾用户提供的引用内容,确保涵盖到用户提到的关键点。用户引用了四个资料,涉及Vue的响应式原理、虚拟DOM、模板编译组件化,以及具体的源码分析。用户的问题集中在Vue2的底层实现,包括源码分析、响应式机制虚拟DOM。 首先,我需要整理响应式原理。根据引用23,Vue的响应式是通过数据劫持结合发布-订阅模式实现的。核心是Observer类,它会遍历数据对象的属性,使用Object.defineProperty将其转换为gettersetter。在getter中收集依赖(Watcher),在setter中通知变化,触发更新。需要提到DepWatcher的关系,Dep负责管理依赖,Watcher作为观察者,当数据变化时执行回调。 接下来是虚拟DOM部分,引用12提到Vue使用虚拟DOM来提高渲染效率。虚拟DOM是对真实DOM的抽象表示,通过diff算法比较新旧虚拟DOM的差异,最小化DOM操作。需要解释为什么使用虚拟DOM,比如直接操作DOM成本高,而虚拟DOM可以批量更新,优化性能。 用户可能还想了解Vue的初始化过程,比如new Vue()时发生了什么。根据引用2,Vue初始化实例成员静态成员,调用_init方法,处理数据响应式,编译模板生成render函数,挂载组件,最后通过patch方法将虚拟DOM转化为真实DOM。 另外,用户提到的$watch方法(引用34)涉及Watcher的类型,包括渲染Watcher、计算属性侦听器。需要说明这三种Watcher的区别,以及deepimmediate选项的作用。 需要注意用户可能存在的疑问点,比如数组的响应式处理,Vue2是如何通过覆盖数组方法实现的。还有Dep.target的作用,依赖收集的过程,以及异步更新队列(nextTick)的实现机制。 最后,确保回答结构清晰,分点说明,结合源码中的关键类方法,如Observer、Dep、Watcher、patch等。同时,生成的相关问题要覆盖用户可能进一步想了解的内容,如数组响应式限制、与Vue3的区别、nextTick原理等。</think>#### 深入解析Vue2底层实现原理 --- ### 一、响应式机制核心实现 Vue2的响应式系统基于**数据劫持 + 发布-订阅模式**实现,核心流程如下: 1. **数据劫持阶段** - 通过`Observer`类递归遍历对象属性,使用`Object.defineProperty`将属性转换为getter/setter ```javascript // 源码截取(src/core/observer/index.js) class Observer { constructor(value) { this.dep = new Dep() def(value, '__ob__', this) if (Array.isArray(value)) { this.observeArray(value) // 对数组特殊处理 } else { this.walk(value) // 遍历对象属性 } } } ``` 2. **依赖收集与触发** - 在getter中通过`Dep.target`收集当前`Watcher`(依赖)到`Dep`订阅器 - 在setter中通知`Dep`触发所有`Watcher`更新 ```javascript function defineReactive(obj, key, val) { const dep = new Dep() Object.defineProperty(obj, key, { get() { if (Dep.target) { dep.depend() // 收集依赖 } return val }, set(newVal) { if (newVal === val) return val = newVal dep.notify() // 触发更新 } }) } ``` 3. **Watcher的三重身份** - **渲染Watcher**:负责组件视图更新 - **计算属性Watcher**:缓存计算结果 - **用户自定义Watcher**:通过`vm.$watch`创建 ```javascript // 源码中的Watcher类(src/core/observer/watcher.js) class Watcher { constructor(vm, expOrFn, cb, options) { this.vm = vm this.getter = parsePath(expOrFn) // 解析表达式或函数 this.value = this.get() // 触发依赖收集 } get() { Dep.target = this const value = this.getter.call(this.vm, this.vm) Dep.target = null return value } update() { queueWatcher(this) // 进入异步更新队列 } } ``` --- ### 、虚拟DOM与Diff算法 #### 1. 为什么需要虚拟DOM? - **性能优化**:直接操作DOM成本高,虚拟DOM通过JS对象抽象描述,批量更新减少重排次数 - **跨平台能力**:虚拟DOM可适配不同渲染目标(Web/Weex/小程序)[^1] #### 2. 实现流程 1. **`render`函数生成VNode** ```javascript // 编译后的render函数示例 function render() { return _c('div', { attrs: { id: 'app' } }, [ _c('p', [_v(_s(msg))]) ]) } ``` 2. **Diff算法核心逻辑** - **同级比较**:通过`sameVnode`判断节点是否可复用 - **Key的作用**:帮助识别列表元素移动/新增/删除 ```javascript // 源码中的patch函数(src/core/vdom/patch.js) function patch(oldVnode, vnode) { if (!sameVnode(oldVnode, vnode)) { // 替换节点 } else { // 更新属性 patchVnode(oldVnode, vnode) } } ``` --- ### 三、关键源码模块联动 1. **初始化流程** ```mermaid graph TD A[new Vue()] --> B[this._init()] B --> C{是否有el选项?} C -->|是| D[调用$mount()] C -->|否| E[需手动调用$mount()] D --> F[编译template为render函数] F --> G[创建渲染Watcher] G --> H[首次执行updateComponent] ``` 2. **数据变更触发更新** ```mermaid graph LR A[数据修改] --> B[setter触发dep.notify()] B --> C[所有Watcher进入队列] C --> D[nextTick后执行队列] D --> E[执行Watcher.run()] E --> F[重新渲染生成新VNode] F --> G[patch新旧VNode差异] G --> H[最小化DOM操作] ``` --- ### 四、设计亮点与局限性 1. **亮点设计** - **异步更新队列**:通过`nextTick`合并多次数据变更的渲染操作 - **数组方法劫持**:重写`push/pop/shift/unshift/splice/sort/reverse`方法触发更新[^3] 2. **已知局限性** - **对象属性新增/删除无法检测**:需用`Vue.set/delete` - **数组索引直接赋值无效**:`arr[0] = newVal`不会触发视图更新 --- #### 相关问题 1. Vue2如何检测数组变化?为何直接修改数组长度无效? 2. Vue2与Vue3响应式实现的核心区别是什么? 3. `nextTick`是如何实现异步批处理的? 4. 虚拟DOM Diff算法的时间复杂度是多少?为什么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值