你知道Vue是怎么把Hello World渲染出来的吗?

460 阅读6分钟

一、先看下Hello World的模板和vue代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Hello world</title>
</head>
<body>
    <div id="app">
        <span>{{descText}}</span>
    </div>
</body>

<script src="../../dist/vue.js"></script>
<script src="./app.js"></script>
</html>
new Vue({
    el: '#app',
    data() {
        return { 
            descText: 'Hello World'
        }
    },
    beforeCreate () {
        console.log('beforeCreate----this.descText', this.descText);
    },
    created() {
        console.log('created----this.descText', this.descText);
    },
    beforeMount () {
        console.log('beforeMount----this');
    },
    mounted() {
        console.log('mounted----this');
    },
    methods: {
        changeDesc() {
            this.descText = 'Hello LLS'
        }
    }
})

运行之后结果如下:

image.png

那你知道Vue是怎么把Hello World渲染出来的吗? 接下来从Vue的源码的角度来分析一下执行过程。

先看下过程流程图,下图中右侧打了对钩的部分是本篇所涉及的,顺序是从上到下,其实这个图和Vue官网的图有点区别。

lifecycle.png

二、为何在beforeCreate之前不能获取实例属性的data和methods?

要想回答这个问题,就必须搞清楚在调用生命周期beforeCreate之前Vue到底做了哪些事情?

image.png

从上面的图可以看到beforeCreate阶段做了3个事情,初始化实例对象initLifecycle()、初始化父组件事件相关属性initEvents(vm)、初始化渲染相关initRender(vm)

1、 初始化实例对象initLifecycle(vm)

先看下`initLifecycle`的部分源码
```js
function initLifecycle() {
    vm.$parent = parent
    vm.$root = parent ? parent.$root : vm

    vm.$children = []
    vm.$refs = {}

    vm._watcher = null
    vm._inactive = null
    vm._directInactive = false
    vm._isMounted = false
    vm._isDestroyed = false
    vm._isBeingDestroyed = false
}
```

可以看到initLifecycle主要是把实例属性初始化,比如把监听器_watcher置空、是否挂载完成_isMounted设置为false等。

2、 initEvents(vm)初始化父组件事件相关属性,当有父组件的方法绑定在子组件时候,供子组件调用。

initEvents的代码如下

```js
function initEvents (vm: Component) {
    vm._events = Object.create(null)
    vm._hasHookEvent = false
    // init parent attached events 初始化父组件附加的事件
    // 在经过合并options 阶段后, 
    // 子组件就可以从 vm.$options._parentListeners读取到父组件传过来的自定义事件。 
    // 通过updateListeners方法, 它的作用是借助$on, $emit方法, 完成父子组件事件的通信
    const listeners = vm.$options._parentListeners
    if (listeners) {
        updateComponentListeners(vm, listeners)
    }
}
```

3、 initRender(vm)渲染相关,初始化了一些渲染需要用的属性和方法以及slot相关。

initRender函数的关键代码如下

function initRender (vm: Component) {
  vm.$slots = resolveSlots(options._renderChildren, renderContext)
  vm.$scopedSlots = emptyObject
  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

  defineReactive(vm, '$attrs', parentData && parentData.attrs || emptyObject, null, true)
  defineReactive(vm, '$listeners', options._parentListeners || emptyObject, null, true)

initRender做了以下事情

  • 将组件的插槽编译成虚拟节点 DOM 树, 以列表的形式挂载到 vm 实例,初始化作用域插槽为空对象;

  • 将模板的编译函数_c$createElement属性(把模板编译成虚拟 DOM 树)挂载到vm

  • 最后把父组件传递过来的 attrsattrs 和 listeners生成响应式的。

4、调用生命周期钩子beforeCreate callHook(vm, 'beforeCreate')

先看下callHook代码

function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook] //相当于vm.$options['beforeCreate']
  const info = `${hook} hook`
  if (handlers) { //handlers是个数组,当使用了minx的时候,就可能会有多个生命周期钩子函数,['全局minxin的生命周期钩子函数', '局部minxin的生命周期钩子函数', '组件的生命周期钩子函数'],并且会按照以上顺序循环执行
    for (let i = 0, j = handlers.length; i < j; i++) {
      invokeWithErrorHandling(handlers[i], vm, null, vm, info)
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

invokeWithErrorHandling(handlers[i], vm, null, vm, info)实际上执行的就是handler.apply(context, args) 或者 handler.call(context),外加了异常处理。

 function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

现在beforeCreate生命周期已经执行完了,我们看到此时还没对props、data、methods、computed、watch初始化,所以我们是无法获取data属性和调用method方法。

二、在生命周期beforeCreate之后和生命周期created之前做了啥

1、 将inject属性响应式 initInjections(vm)

function initInjections (vm: Component) {
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production') {
        defineReactive(vm, key, result[key], () => {
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
            `overwritten whenever the provided component re-renders. ` +
            `injection being mutated: "${key}"`,
            vm
          )
        })
      } else {
        defineReactive(vm, key, result[key])
      }
    })
    toggleObserving(true)
  }
}

2、 执行initState(vm)初始化&响应式propsdatamethodscomputedwatch属性

先看下initState(vm)的源码

function initState (vm: Component) {
 vm._watchers = []
 ;
 const opts = vm.$options
 if (opts.props) initProps(vm, opts.props) //props相关
 if (opts.methods) initMethods(vm, opts.methods) //methods相关
 if (opts.data) { //data相关
   initData(vm)
 } else {
   observe(vm._data = {}, true /* asRootData */)
 }
 if (opts.computed) initComputed(vm, opts.computed) //computed相关
 if (opts.watch && opts.watch !== nativeWatch) { //watch相关
   initWatch(vm, opts.watch)
 }
}

(1) initProps

  • 校验props属性是否合法,
  • 通过defineReactive(props, key, value)设置props响应式
  • 在实例对象vm上设置props的每个key值,方便通过this[key]读取

(2) initMethods

  • 校验props属性是否合法,
  • method挂载到实例对象vm上,并设置method的环境变量为vm
   initMethods (vm: Component, methods: Object) {
       const props = vm.$options.props
       for (const key in methods) {
           //省略的代码是校验方法是否是function、方法名是否在vm存在相同的实例属性
           vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm)
       }

(3) initData

function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
  ? getData(data, vm)
  : data || {}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
//遍历data,并对data的属性变化添加检测
//Object.defineProperty(target, key, sharedPropertyDefinition) 将data的属性添加到vm上,并添加响应式检测
while (i--) {
  const key = keys[i]
  if (process.env.NODE_ENV !== 'production') {
    if (methods && hasOwn(methods, key)) {
      warn(
        `Method "${key}" has already been defined as a data property.`,
        vm
      )
    }
  }
  if (props && hasOwn(props, key)) {
    process.env.NODE_ENV !== 'production' && warn(
      `The data property "${key}" is already declared as a prop. ` +
      `Use prop default value instead.`,
      vm
    )
  } else if (!isReserved(key)) {
    proxy(vm, `_data`, key)
  }
}
// observe data
observe(data, true /* asRootData */)
}
  • 首先判断data是对象还是函数,获取响应的data
  • 校验data属性是否合法,不能和props属性、methods方法名冲突
  • 通过defineReactive(props, key, value)设置data响应式
  • 通过proxy(vm, _data, key)在实例对象vm上设置props的每个key值,方便通过this[key]读取
   function proxy (target: Object, sourceKey: string, key: string) {
   sharedPropertyDefinition.get = function proxyGetter () {
       return this[sourceKey][key]
   }
   sharedPropertyDefinition.set = function proxySetter (val) {
       this[sourceKey][key] = val
   }
   Object.defineProperty(target, key, sharedPropertyDefinition)
   }

(4)、 初始化compute属性 initComputed

  • 设置compute属性到vm
  • 设置监听,compute属性的依赖

(5)、 初始化watch

  • watchhanler添加到vm
  • 通过vm.$watch(expOrFn, handler, options)监控watch属性的变化并触发handler

(6)、初始化provide

3、 调用生命周期钩子created

由于在调用created生命周期钩子之前已经完成了props、data、methods、计算属性、watch等的初始化,因此我们在created的生命周期钩子是可以对以上进行引用的。

三、生命周期created之后beforeMount之前做了啥

mount1.png 我们看到调用created生命周期之后执行了vm.$mount(vm.$options.el), 这就是渲染代码的入口

const mount = Vue.prototype.$mount //缓存初始化时候原型上的$mount
Vue.prototype.$mount = function ( //重新定义原型上的$mount, 上面的vm.$mount(vm.$options.el)执行的$mount就是这里
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el) // document.querySelector(), vue模板
  
  /* istanbul ignore if  校验 */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  // 如果没有定义 render 方法,则会把 el 或者 template 字符串转换成 render 方法
  // 在 Vue 2.0 版本中,所有 Vue 的组件的渲染最终都需要 render 方法,
  // 无论我们是用单文件 .vue 方式开发组件,还是写了 el 或者 template 属性,最终都会转换成 render 方法
  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)//获取到HTML的字符串
    }
    if (template) { //编译,生成render
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = 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')
      }
    }
  }
  // 调用runtime/index.js中的mount,继续后面的流程
  return mount.call(this, el, hydrating)
}

以上代码做了两个事情

  • 首先判断是否有render函数Vue渲染函数,有的话执行后面的渲染逻辑mount.call(this, el, hydrating) 我们看下Hello World的模板写成渲染函数是什么样子,
      render: function (createElement) {
        return createElement(
          'div',   // 标签名称
          {
            attrs: {
              id: 'foo'
            }
          }, 
          [
            createElement('span', this.descText)
          ]
        )
      }
    

其实createElement就是created生命周期的initRender里面在vm上增加的_c$createElement属性

  • 没有render函数的话就去获取模板

先看下options属性上是否有template属性,如果有的话将其作为模板

如果options上没有template属性,会通过template = getOuterHTML(el)获取作为模板

template.png

  • 接着将模板转成render函数
  const { render, staticRenderFns } = compileToFunctions(template, {
    outputSourceRange: process.env.NODE_ENV !== 'production',
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  }, this)
  options.render = render //render表达式

render.png

  • callHook(vm, 'beforeMount')

执行beforeMount钩子函数

其实从createdbeforeMount阶段,主要是获取模板并将模板编译成render函数。

四、Mounted阶段

1、生成Vnode

vm._render()就是将render函数生成vNode,Hello World模板生成的vNode如下图所示,如果想对vNode深入了解,可以找资料学习。

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

image.png

image.png

生成的vNode如下: vNode.png

2、patch阶段

patch阶段做的事情就是将虚拟DOM(vNode)树生成真实Dom并挂载到页面 。这个过程是从父节点开始遍历vNode树获取每个vNode节点,并将vNode节点生成真正的DOM,如果有children节点的话,继续遍历生成真实的DOM节点,并挂载到父vNode已经生成的DOM节点上,最后将根节点挂载到页面上,这样就完成了vNode到真实DOM的生成过程。

其实vNode节点生成真实DOM节点的过程还是用了JavaScript提供的原始DOM操作方法 document创建.png

该例子的vNode渲染成DOM的过程如下:

1、首先渲染最外层的节点,生成<div id="app"></div>

    2、渲染孩子节点children, <span>Hello World</span>

    3、将孩子节点<span>Hello World</span>插入到父节点<div id="app"></div>
    
4、将最外层的节点插入到body

3、 最后执行生命周期mounted。

五、总结

通过上面的介绍,可以看到Vue将模板渲染成真实DOM的过程有下面几个阶段:

  • 初始化实例变量,把实例对象的属性值初始化;
  • 初始化父组件相关事件;
  • 初始化实例渲染相关方法;
  • 执行生命周期钩子函数beforeCreate
  • 初始化injection、provide、props、methods、data、computed、watcher等属性并实现响应式;
  • 执行生命周期钩子函数created
  • 如果没有render函数,则获取Vue模板,可能来自template属性或者通过 el 选项指定的挂载元素中提取出的 HTML 模板;
  • template转成render函数;
  • 执行生命周期钩子函数beforeMount
  • render函数生成vNode(虚拟DOM);
  • 通过遍历vNode(虚拟DOM),生成真实DOM并挂载到页面;
  • 执行生命周期钩子函数mounted

以上就是Vue实现渲染的大致过程,后面将会介绍watcher的原理。