vue的源码学习之五——3.数据驱动(Vue 实例挂载的实现)

  • 介绍

        版本:2.5.17。

       我们使用vue-vli创建基于Runtime+Compiler的vue脚手架。

       学习文档:https://ustbhuangyi.github.io/vue-analysis/data-driven/mounted.html

  • 挂载到DOM

        src/core/instance/init.js : 在初始化的最后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM。

if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  • $mount

    Vue 中我们是通过 $mount 实例方法去挂载 vm 的,$mount 方法在多个文件中都有定义,如 src/platform/web/entry-runtime-with-compiler.jssrc/platform/web/runtime/index.jssrc/platform/weex/runtime/index.js。因为 $mount 这个方法的实现是和平台、构建方式都相关的。

    先来看一下 src/platform/web/entry-runtime-with-compiler.js 文件中定义:

    /* @flow */
    
    import config from 'core/config'
    import { warn, cached } from 'core/util/index'
    import { mark, measure } from 'core/util/perf'
    
    import Vue from './runtime/index'
    import { query } from './util/index'
    import { compileToFunctions } from './compiler/index'
    import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
    
    const idToTemplate = cached(id => {
      const el = query(id)
      return el && el.innerHTML
    })
    
    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      /* 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
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
        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) {
          template = getOuterHTML(el)
        }
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
    
          const { render, staticRenderFns } = compileToFunctions(template, {
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          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')
          }
        }
      }
      return mount.call(this, el, hydrating)
    }
    
    /**
     * Get outerHTML of elements, taking care
     * of SVG elements in IE as well.
     */
    function getOuterHTML (el: Element): string {
      if (el.outerHTML) {
        return el.outerHTML
      } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
      }
    }
    
    Vue.compile = compileToFunctions
    
    export default Vue
    

          这段代码首先缓存了原型上的 $mount 方法,再重新定义该方法,我们先来分析这段代码。首先,它对 el 做了限制,Vue 不能挂载在 bodyhtml 这样的根节点上。   

          我们会发现该js重新定义了Vue.prototype.$mount方法,而该方法来自于src/platform/web/runtime/index.js : 

    • 重新定义Vue.prototype.$mount方法

    const mount = Vue.prototype.$mount
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
    
      /* 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
      }
    
      const options = this.$options
      // resolve template/el and convert to render function
      if (!options.render) {
        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) {
          template = getOuterHTML(el)
        }
        if (template) {
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
            mark('compile')
          }
    
          const { render, staticRenderFns } = compileToFunctions(template, {
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            delimiters: options.delimiters,
            comments: options.comments
          }, this)
          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')
          }
        }
      }
      return mount.call(this, el, hydrating)
    }

    那是因为,src/platform/web/entry-runtime-with-compiler.js 中的是适用于 Runtime+Compiler 版本的。而src/platform/web/runtime/index.js 中的 Vue.prototype.$mount 方法是适用于 Runtime Only 版本的,

    我们可以看到对于Vue.prototype.$mount参数是可以传递 字符串 和 DOM对象的。 
    我们来看一下 query 方法 ,src/platform/web/util/index.js

    • 将传入的参数转为DOM

      Vue.prototype.$mount = function (
       el?: string | Element,
        hydrating?: boolean
      ): Component {
        el = el && query(el)

             我们可以看到对于Vue.prototype.$mount参数是可以传递 字符串 和 DOM对象的。 
               我们来看一下 query 方法 :src/platform/web/util/index.js

    export function query (el: string | Element): Element {
      if (typeof el === 'string') {
        const selected = document.querySelector(el)
        if (!selected) {
          process.env.NODE_ENV !== 'production' && warn(
            'Cannot find element: ' + el
          )
          return document.createElement('div')
        }
        return selected
      } else {
        return el
      }
    }

             这个方法是说如果说是字符串,就是用 document.querySelector(el) 方法获得字符串代表的DOM对象,如果发现没有,就会抱一个错误并且返回一个空div。所以 el = el && query(el) 代表的一定是一个DOM

    • 不得挂载在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
        }
    • 判断是否有render函数

      如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法。这里我们要牢记,在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法,那么这个过程是 Vue 的一个“在线编译”的过程,它是调用 compileToFunctions 方法实现的 

      const options = this.$options
        // resolve template/el and convert to render function
        if (!options.render) {
          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) {
            template = getOuterHTML(el)
          }
          if (template) {
            /* istanbul ignore if */
            if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
              mark('compile')
            }
      
            const { render, staticRenderFns } = compileToFunctions(template, {
              shouldDecodeNewlines,
              shouldDecodeNewlinesForHref,
              delimiters: options.delimiters,
              comments: options.comments
            }, this)
            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')
            }
          }
        }

     上文说道el是DOM对象,如果el 是 body 或者 html元素的话就会报错,那是因为挂载是覆盖的,如果挂载在body或html上, 那么整个HTML文档就不对了。 所以我们一般采用的都是挂载在div上的形式。 

              如果没有render函数,则获取template,template可以是#id、模板字符串、dom元素,如果没有template,则获取el以及其子内容作为模板。 compileToFunctions是对我们最后生成的模板进行解析,生成render函数。

        compileToFunctions对生成的模板进行解析

           该方法来自于:.src/platform/compiler/index.js,如果我们的例子是:

    {{message}}

        1.解析template,生成ast。

    {
      type: 1,
      tag: 'div',
      plain: false,
      parent: undefined,
      attrs: [{name:'id', value: '"app"'}],
      attrsList: [{name:'id', value: 'app'}],
      attrsMap: {id: 'app'},
      children: [{
        type: 1,
        tag: 'p',
        plain: true,
        parent: ast,
        attrs: [],
        attrsList: [],
        attrsMap: {},
        children: [{
          expression: "_s(message)",
          text: "{{message}}",
          type: 2
        }]
    }

    2.对ast进行优化,分析出静态不变的内容部分,增加了部分属性: 
    因为我们这里只有一个动态的{{message}},所以static和staticRoot都是false。

    {
      type: 1,
      tag: 'div',
      plain: false,
      parent: undefined,
      attrs: [{name:'id', value: '"app"'}],
      attrsList: [{name:'id', value: 'app'}],
      attrsMap: {id: 'app'},
      static: false,
      staticRoot: false,
      children: [{
        type: 1,
        tag: 'p',
        plain: true,
        parent: ast,
        attrs: [],
        attrsList: [],
        attrsMap: {},
        static: false,
        staticRoot: false,
        children: [{
          expression: "_s(message)",
          text: "{{message}}",
          type: 2,
          static: false
        }]
      }
    

    3.ast生成render函数和staticRenderFns数组。

    render = function () {
        with(this){return _c('div',{attrs:{"id":"app"}},[_c('p',[_v(_s(message))])])}
    }

     4.在src/core/instance/render.js中,我们曾经添加过如下多个函数,这里和render内返回值调用一一对应。

    Vue.prototype._o = markOnce
    Vue.prototype._n = toNumber
    Vue.prototype._s = _toString
    Vue.prototype._l = renderList
    Vue.prototype._t = renderSlot
    Vue.prototype._q = looseEqual
    Vue.prototype._i = looseIndexOf
    Vue.prototype._m = renderStatic
    Vue.prototype._f = resolveFilter
    Vue.prototype._k = checkKeyCodes
    Vue.prototype._b = bindObjectProps
    Vue.prototype._v = createTextVNode
    Vue.prototype._e = createEmptyVNode
    Vue.prototype._u = resolveScopedSlots

    这里的staticRenderFns目前是一个空数组,其实它是用来保存template中,静态内容的render,比如我们把例子中的模板改为:

    这是静态内容

    {{message}}

     staticRenderFns就会变为:

    staticRenderFns = function () {
        with(this){return _c('p',[_v("这是"),_c('span',[_v("静态内容")])])}
    }
    • 调用原先原型上的 $mount 方法挂载

      mount.call(this, el, hydrating)

      我们知道该js保存了mount = Vue.prototype.mount,然后又重新定义了Vue.prototype.上的方法 
      该js的最后又调用了mount方法。原先原型上的 $mount 方法在 src/platform/web/runtime/index.js 中定义,之所以这么设计完全是为了复用,因为它是可以被 runtime only 版本的 Vue 直接使用的。

      // public mount method
      Vue.prototype.$mount = function (
        el?: string | Element,
        hydrating?: boolean
      ): Component {
        el = el && inBrowser ? query(el) : undefined
        return mountComponent(this, el, hydrating)
      }

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

      $mount 方法实际上会去调用 mountComponent 方法,这个方法定义在 src/core/instance/lifecycle.js 文件中:

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    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
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* 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)
    }
  }

  // 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) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  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
}

mountComponent 核心就是先调用 vm._render 方法先生成虚拟 Node,再实例化一个渲染Watcher,在它的回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。

  1.做DOM对象的缓存

vm.$el = el

2.判断是否有render函数 
如果用户没有写render函数,并且template也没有转化为render函数,就会生成一个VNode节点,并在生成环境报警告。

if (!vm.$options.render) {
     vm.$options.render = createEmptyVNode
      if (process.env.NODE_ENV !== 'production') {...}
}

3.实例化一个渲染Watcher。 
Watcher 在这里起到两个作用, 一个是初始化的时候会执行回调函数,另一个是当 vm 实例中的监测的数据发生变化的时候执行回调函数,这块儿我们会在之后的章节中介绍。.new Watcher传的参数1.vue实例,2.updateComponent函数,3.空函数, 4.对象,5布尔值。

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

 Watcher定义在来自文件:src/core/observer/watcher.js

constructor ( 
    vm: Component,
    // 表达式
    expOrFn: string | Function,
    // 回调
    cb: Function,
    // 配置对象
    options?: ?Object,
    // 是否渲染Watcher的标准位
    isRenderWatcher?: boolean
  ) {
    this.vm = vm

    // 如果渲染Watcher为true,则在 vm中添加_watcher
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)

    // options
    if (options) {
     ...
    } else {
    ...
    }

    this.cb = cb
      ...
     // 如果是开发环境就将 expOrFn toString
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''

    // 将expOrFn函数转化为getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } 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
        )
      }
    }
    //计算属性
    if (this.computed) {
      this.value = undefined
      this.dep = new Dep()
    } else {
        // 调用this.get()
      this.value = this.get()
    }
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
        // 调用this.getter,也就是调用expOrFn
      value = this.getter.call(vm, vm)
    } catch (e) {
    } finally {
  }

 我们会把expOrFn也就是updateComponent赋值给this.getter,并且在获取this.value的值时会调用this.get(),进而调用了updateComponent。

4.通过watcher回调函数中会调用 updateComponent 方法,最终调用 vm._update 更新 DOM。

updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }

5.函数最后判断为根节点的时候设置 vm._isMounted 为 true, 表示这个实例已经挂载了,同时执行 mounted 钩子函数。 这里注意 vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例。

if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm

 

你可能感兴趣的:(vue源码解析,vue的源码学习)