从new Vue到Hello World!(上)

963 阅读4分钟

前言

自打8个月前发布了自己的第一篇文章开始就立下了flag,会继续出vue的相关知识,但那时候继续深入的时候才发现自己基础知识与一些思维还没有跟的上,看的也是云里雾里的感觉,遂暂时“鸽”置了计划。大半年的磨练,以及前一段时间对element-ui的尝试性探究,感觉到应该已经到了那个时机,于是也就有了本篇文章。好了废话不多说直接进入主题。

ps:本文重点阐述从new Vue到页面上显示Hello World的过程其他的功能将跳过!请配合源码食用!

一、构造函数

我们知道调用vue都是new的形式开始的,所以vue一定有一个构造函数,经过查阅其在src/core/instance/index.js目录下

  • 1、判断vue是否被new调用,否则将提示
  • 2、this._init(options)将我们调用new Vue时传入的配置项作为参数传入

该方法是在调用initMixin(Vue)挂载到Vue上的,而initMixin方法被定义在src/core/instance/init.js

------initMixin-----

其首先const vm: Component = this定义了一个vm并指向自己也就是vue的实例(记住这个vm,很经常使用到)接着

vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      ) 

由函数名可以得知这边是将传入的options和其他vue构造函数的一些配置项合并后并赋值到vm.$option

--------------------

现在来创建一个最简单的例子

new Vue({
  el: '#app',
  data: {
    message: 'Hello World!'
  }
})

这边的el指的是vue要挂载在dom位置,也就与渲染有关。所以在之前的_init中找到相关方法

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

也就是说在执行完$mount后页面就能渲染出来。

二、$mount

$mount定义在platforms/web/entry-runtime-with-compiler.js中,一开始用mount缓存vue上原来的$mount方法

const mount = Vue.prototype.$mount 

而被缓存的这个$mount是在runtime/index的Vue上先跳过,这边重写了是因为这是一个带解析器的版本vue,需要对传入的template进行处理。先看传入的两个参数

el?: string | Element,
hydrating?: boolean

第一个传入的是字符串或者dom类型也就是说options中的el除了可以传入css选择器也可以传dom,下面这个我们没有传入省略。 一开始将el赋值为本身或者调用query方法

el = el && query(el)

这里也就是赋值为通过query依据传入的字符串获取的dom,query方法在util/index中

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获取dom,但如果没有找到就发出警告,并返回一个空的div dom,如果不是字符串即传的是dom则直接返回dom。

也就是说这里的el为options传入的dom或者通过传入的字符串获取的dom。接下来判断了这个el是否等于body和html,如果等于就会报警告,并且返回。也就是说vue是无法挂载在body或者html标签下的,原因是渲染后会替换整个dom导致html必须的标签欠缺。

然后就是判断options中是否有render函数,如果没有就是使用了模板,这边接下来的就是将模板转化成为render函数,最后在调用原先缓存的mount方法。也就是说vue其实所有的渲染都是通过render函数,即使是使用template的方式,最终也会通过compiler把他编译成render函数,最后再统一处理。

再来看看原先的mount方法

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

很简单,首先看是否在浏览器中并且el有值,符合以上条件的话使用query来获取dom并作为参数传给mountComponent方法,如果以上都不满足则el赋为undefined。

mountComponent方法在core/instance/lifecycle中定义,首先在实例上定义了$el为传入的el(dom),接着判断是否有render函数,如果没有那么将其设置为createEmptyVNode的返回,猜测是一个空的vnode对象,而后判断是否有template选项,如果有则代表写了template却没有用带有compiler版本的vue(有带的话按照上面会转化成为render函数),此时给了警告,如果没有template那说明,既没有写render也没有写模板进行其他的警告。接着

callHook(vm, 'beforeMount')

这个应该是和生命周期有关系,接下来是一个与vue性能监控有关的代码,因为我们没有写,所以进行到else,定义了一个updateComponent的函数

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

再下来new了一个watcher用于监听渲染

new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

五个参数为:实例,刚刚定义的updateComponent,一个空方法,一个对象,以及true。这时候我们看watcher的定义(core/observer/watcher),传入的参数定义为

vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean

一路下来是一系列的定义,直到一个判断传入的expOrFn是否为方法,如果是的话将其赋值到getter

if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    }

后面查看this.lazy有没有值,而this.lazy又为option中的lazy

this.lazy = !!options.lazy

所以最后面

this.value = this.lazy
      ? undefined
      : this.get()

调用了get方法。这边的get其实就是直接调用了this.getter

value = this.getter.call(vm, vm)

综上,在初始化的时候这边实际上是调用了一次updateComponent的方法。但实际上除了初始化,这边的函数也包含了更新的相关内容。先记住。再返回来看调用updateComponent实际上就是执行了

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

三、_render

_render函数定义在core/instance/render.js中,看的出来返回的是一个vnode对象,首先将render函数赋值到render

const { render, _parentVnode } = vm.$options

接着使用call来调用render函数

vnode = render.call(vm._renderProxy, vm.$createElement)

首先call方法第一个为上下文,第二个为参数,先看$createElement,该方法在initRender的时候挂载在实例上,并且在_init中就已经调用了,所以这样就能执行。可以看得出来其调用了createElement的方法并将带有的四个参数传入

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

之后我们先将demo改为手写render的方式

render(createEle){
     return createEle('div',{
          attrs:{
            id:'app'
          }
     },this.message)
  }

所以这里实际上是将我们写的三个参数传给了createElement,而这个方法定义在core/vdom/create-element中先不延伸,这边猜测就是通过传入的参数,返回出对应的vnode对象。现在再来看_renderProxy,其定义在_init中

if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

如果是开发环境就调用initProxy,如果为正式环境其实_renderProxy就是指的它自己,现在看下initProxy。

 if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }

这边的hasProxy实际上就是看浏览器是否支持es6中的proxy,如果不支持的话_renderProxy还是等于Vue实例,如果支持的话,在此demo中这边handlers为hasHandler,hasHandler其实就是各种判断,当访问实例的key无法得到值得时候就会报错,最后再用proxy代理,这边总体就是我们在实例中访问对应值得时候如果之前没有定义或者无法访问就会报警告,作为我们在开发时候提示作用,固在生产环境就没有。

回到_render,使用$createElement返回vnode后

vnode = render.call(vm._renderProxy, vm.$createElement)
if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }

这边首先判断了生成的vnode 是否为VNode对象如果不是并且为数组的话,实际上就是代表根节点有多个,这样vnode无法正确渲染。所以这边直接把其直接替换成一个空的vnode, 经过上面的一系列操作,把最终的vnode返回。

至此vm._update(vm._render(), hydrating)中的vm._render()很明白了就是通过配置返回对应的vnode。

结语

本章看过去代码量比较少,但是实际上vue在各种初始化的时候,是做了很多其他的事情,但是单纯的从我们这条线来看,整个框架下来还是非常清晰的,实际上就是一个mount方法中定义了一个watcher,并且在初次渲染的时候触发,并调用了render方法,而在render方法中,通过createElement加上传入的配置项以生成一个vnode用于最后的渲染。下篇就是探究如何通过vnode生成对应的dom并呈现出来,尽请期待!