Vue2源码梳理:关于vm.$mount的实现

$mount

  • vue实例挂载的实现,也就是执行 vm.$mount 的方法

  • 在 Runtime + Compiler 版本,入口文件是: src/platform/web/entry-runtime-with-compiler.js

  • $mount 方法也是在这个文件中被定义的

    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      // 首先对于传入的这个 el 参数,做了一个处理,可以看到 el 参数,可以是个字符串,也可以是个 Element
      // 这里调用了query方法,query方法实际上就是原生的方法,有则document.querySelector,无则,document.createElement 返回一个div 等处理
      el = el && query(el)
    
      // 拿到这个 el 以后,这里就已经被转化成了这个 dom 对象,然后它又做了一个简单的判断
      // 也就是说我的 el 如果是 body 或者是 html 标签的话,它就会报一个错
      // 就是 vue 不可以直接挂载到这个 body 或者 html 上,因为它是会覆盖的,你挂载的话,你会把整个body覆盖,那整个HTML文档不对了
      // 所以说这里在开发环境下,就报了这个警告
      /* istanbul ignore if */
      if (el === document.body || el === document.documentElement) {
        process.env.NODE_ENV !== 'production' && warn(
          `Do not mount Vue to  or  - mount to normal elements instead.`
        )
        return this
      }
      // 拿到这个options。判断有没有定义render方法,因为平时开发的过程中,代码都是脚手架生成,会有一个 render
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
        // 再次判断你有没有写template,一般模板默认是有index.html的,在组件内部也可以使用 template, 有则进行处理
        let template = options.template
        if (template) {
          if (typeof template === 'string') {
            if (template.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
                )
              }
            }
          } else if (template.nodeType) {
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          // 如果没有 templete 则执行 getOuterHTML 拿到html
          template = getOuterHTML(el)
        }
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
    
          // 这一块就是跟编译相关了, 编译的话,它其实是调用这个 compileToFunction
          // 拿到生成的一个 render 函数,还有 staticRenderFns
          const { render, staticRenderFns } = compileToFunctions(template, {
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          // 这个 options.render 会在渲染 vnode 的时候会用到
          options.render = render
          options.staticRenderFns = staticRenderFns
    
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile end')
            measure(`vue ${this._name} compile`, 'compile', 'compile end')
          }
        }
      }
      // 调用 mount, 这个mount 是 runtimeOnly 时的 $mount
      return mount.call(this, el, hydrating)
    }
    
  • 它首先获得了 Vue.prototype.$mount 方法,用这个 mount 变量缓存起来,然后又重新定义了一遍这个方法

  • 回到最初的定义,在 src/platforms/web/runtime/index.js 中,是最原始的定义

    // public mount method
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    
    // devtools global hook
    /* istanbul ignore next */
    if (inBrowser) {
      setTimeout(() => {
        if (config.devtools) {
          if (devtools) {
            devtools.emit('init', Vue)
          } else if (
            process.env.NODE_ENV !== 'production' &&
            process.env.NODE_ENV !== 'test'
          ) {
            console[console.info ? 'info' : 'log'](
              'Download the Vue Devtools extension for a better development experience:\n' +
              'https://github.com/vuejs/vue-devtools'
            )
          }
        }
        if (process.env.NODE_ENV !== 'production' &&
          process.env.NODE_ENV !== 'test' &&
          config.productionTip !== false &&
          typeof console !== 'undefined'
        ) {
          console[console.info ? 'info' : 'log'](
            `You are running Vue in development mode.\n` +
            `Make sure to turn on production mode when deploying for production.\n` +
            `See more tips at https://vuejs.org/guide/deployment.html`
          )
        }
      }, 0)
    }
    
  • 在我们的这个入口, 为什么会重新定义一遍 $mount?

  • 实际上, 上面这块最原始的代码是给 RuntimeOnly 版本复用用的一个函数

  • 在 Runtime + Compiler 版本,在 src/core/instance/init.js 的 initMixin 中

  • 执行 vm.$mount 时候,实际上调的就是 src/platform/web/entry-runtime-with-compiler.js 这个入口文件

  • 中的 Vue.prototype.$mount 这个函数,现在回到入口文件中的代码中,查看相关代码上的注释

  • 在执行了入口文件中的 .$mount, 最终调用了 RuntimeOnly 的 $mount, 最终执行 mountComponent 方法

  • 而 mountComponent 是在 src/core/instance/lifecycle.js 中定义的

    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      // 首先会把这个 el 的dom 被 vm.$el 缓存起来
      vm.$el = el
      // 判断有没有 render 函数(或没有 template转换来的 render), 没有则创建一个空的 vnode, 并在开发环境警告
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        // 这个警告就是写了 template,但是没有使用含有编译的版本,或者两者都没有写
        if (process.env.NODE_ENV !== 'production') {
          /* 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
            )
          }
        }
      }
    
      // 这里先执行 beforeMount 的钩子函数,跳过
      callHook(vm, 'beforeMount')
    
      let updateComponent
      // 在dev环境上是更多做了性能埋点相关的处理,当性能比较卡顿时,可以利用这些东西,看文档就行,跳过
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
          const name = vm._name
          const id = vm._uid
          const startTag = `vue-perf-start:${id}`
          const endTag = `vue-perf-end:${id}`
    
          mark(startTag)
          const 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 = () => {
          vm._update(vm._render(), hydrating)
        }
      }
    
      // 这个函数就是调用了 vm._update,第一个参数是通过 vm._render渲染出来一个vnode, 第二个参数理解为 false 就可以了
      // 之后,调用 new Watcher 这里实际上是 渲染 watcher
      // 因为 watcher 这个东西其实是跟响应式原理强相关的一个类
      // 它实际上就是一个观察者模式,它其实有很多自定义 watcher,也会有一个叫渲染 watcher
      // 调用 new Watcher 的的时候,三个参数,第一个vm, 第二个 updateComponent函数, 第三个 noop(空函数), 第四个配置对象,第五个布尔值
      // 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 () {
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true /* isRenderWatcher */) // 注意这里第5个参数是 true, 表示是一个渲染watcher
      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
    }
    
    • 进入 Watcher 的定义,在 src/core/observer/watcher.js 中
      /**
       * A watcher parses an expression, collects dependencies,
      * and fires callback when the expression value changes.
      * This is used for both the $watch() api and directives.
      */
      export default class Watcher {
        vm: Component;
        expression: string;
        cb: Function;
        id: number;
        deep: boolean;
        user: boolean;
        lazy: boolean;
        sync: boolean;
        dirty: boolean;
        active: boolean;
        deps: Array<Dep>;
        newDeps: Array<Dep>;
        depIds: SimpleSet;
        newDepIds: SimpleSet;
        before: ?Function;
        getter: Function;
        value: any;
      
        constructor (
          vm: Component,
          expOrFn: string | Function,
          cb: Function,
          options?: ?Object,
          isRenderWatcher?: boolean // 是否是渲染 watcher
        ) {
          this.vm = vm
          if (isRenderWatcher) {
            vm._watcher = this
          }
          vm._watchers.push(this) // 收集watcher
          // 这里忽略这个options
          // options
          if (options) {
            this.deep = !!options.deep
            this.user = !!options.user
            this.lazy = !!options.lazy
            this.sync = !!options.sync
            this.before = options.before
          } else {
            this.deep = this.user = this.lazy = this.sync = false
          }
          this.cb = cb
          this.id = ++uid // 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()
          this.expression = process.env.NODE_ENV !== 'production'
            ? expOrFn.toString()
            : ''
          // 注意这里,这里就是调用方传进来的 updateComponent
          // parse expression for getter
          if (typeof expOrFn === 'function') {
            this.getter = expOrFn
          } else {
            this.getter = parsePath(expOrFn)
            if (!this.getter) {
              this.getter = noop
              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()
        }
      
        // 这里有依赖收集,相关的
        /**
         * Evaluate the getter, and re-collect dependencies.
        */
        get () {
          pushTarget(this)
          let value
          const vm = this.vm
          try {
            value = this.getter.call(vm, vm) // 这里会调用 getter, 也就是上层调用方传进来的 updateComponent 方法,就会执行方法内部的 update
          } 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
        }
      
        /**
         * Add a dependency to this directive.
        */
        addDep (dep: Dep) {
          const id = dep.id
          if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
              dep.addSub(this)
            }
          }
        }
      
        /**
         * Clean up for dependency collection.
        */
        cleanupDeps () {
          let i = this.deps.length
          while (i--) {
            const dep = this.deps[i]
            if (!this.newDepIds.has(dep.id)) {
              dep.removeSub(this)
            }
          }
          let tmp = this.depIds
          this.depIds = this.newDepIds
          this.newDepIds = tmp
          this.newDepIds.clear()
          tmp = this.deps
          this.deps = this.newDeps
          this.newDeps = tmp
          this.newDeps.length = 0
        }
      
        /**
         * Subscriber interface.
        * Will be called when a dependency changes.
        */
        update () {
          /* istanbul ignore else */
          if (this.lazy) {
            this.dirty = true
          } else if (this.sync) {
            this.run()
          } else {
            queueWatcher(this)
          }
        }
      
        /**
         * Scheduler job interface.
        * Will be called by the scheduler.
        */
        run () {
          if (this.active) {
            const value = this.get()
            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
              const oldValue = this.value
              this.value = value
              if (this.user) {
                const info = `callback for watcher "${this.expression}"`
                invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
              } else {
                this.cb.call(this.vm, value, oldValue)
              }
            }
          }
        }
      
        /**
         * Evaluate the value of the watcher.
        * This only gets called for lazy watchers.
        */
        evaluate () {
          this.value = this.get()
          this.dirty = false
        }
      
        /**
         * Depend on all deps collected by this watcher.
        */
        depend () {
          let i = this.deps.length
          while (i--) {
            this.deps[i].depend()
          }
        }
      
        /**
         * Remove self from all dependencies' subscriber list.
        */
        teardown () {
          if (this.active) {
            // remove self from vm's watcher list
            // this is a somewhat expensive operation so we skip it
            // if the vm is being destroyed.
            if (!this.vm._isBeingDestroyed) {
              remove(this.vm._watchers, this)
            }
            let i = this.deps.length
            while (i--) {
              this.deps[i].removeSub(this)
            }
            this.active = false
          }
        }
      }
      

总结下 $mount

  • 1 ) 在含有 compiler 版本中,先对 el 做一些处理
  • 2 ) 在没有定义 render 函数的时候,尝试获取一下 render 函数
    • 也就是说,把整个template通过一系列的逻辑判断,也是因为它支持很多很多种写法
      • 可以直接写 template
      • 也可以直接 template 是个dom对象
      • 如果不写 templete 的话,通过 el 的 outHTML 来获取这个 templete
    • 把这个template 通过这个编译的手段也转化成这个render函数
    • 也就是说整个 $mount 在这个带compiler版本的前提,要拿到这个render函数
  • 3 ) 然后就会调用这个 mountComponent 的这个方法
    • 这个方法其实很简单,就是定义了 updateComponent 这个函数
  • 4 ) 这个函数它实际上被用到渲染 watcher 里面,为什么要通过 watcher 呢?
    • 是因为这个 updateComponent 这个方法,实际上执行了一次真实的渲染
    • 这个渲染过程,除了我们首次,之后再更新数据的时候,还是会触发这个渲染watcher
    • 再次执行这个updateComponent方法,它是一个监听到执行的这样一个过程
    • 当我们的数据发生变化,它的更新入口也是这个 updateComponent 方法
    • 这就是渲染 watcher所要做的事情

你可能感兴趣的:(Vue,Weex,vue.js,前端,前端框架)