vue2首次渲染原理

308 阅读5分钟

vue2渲染原理

通过前面的文章,我们了解了vue2的响应式原理异步更新原理。接下来我们将介绍下vue的渲染原理,一下是一个vue最简单的使用案例,我们将对照案例和源码讲解,vue2是怎么实现从new Vue到页面dom节点渲染的过程。

案例:

 <!DOCTYPE html>
 <html lang="en">
 <head>
   <meta charset="UTF-8">
   <title>Document</title>
   <script src="../../dist/vue.js"></script>
 </head>
 <body>
   <div id="app">
     <div>
         姓名: {{ name }}
     </div>
   </div>
   <script>
     new Vue({
       el: '#app',
       data() {
         return {
           name: '张三'
         }
       }
     })
   </script>
 </body>
 </html>

new Vue

new Vue就是调用Vue的构造函数,看源码

 // core/instance/index.js
 ​
 function Vue (options) {
   // 开发环境,错误提示代码忽略
   if (process.env.NODE_ENV !== 'production' &&
     !(this instanceof Vue)
   ) {
     warn('Vue is a constructor and should be called with the `new` keyword')
   }
   // 调用 内部实例方法 进行初始化
   this._init(options)
 }
 ​
 // 混入 Vue.prototype._init 方法
 initMixin(Vue)
 ​
 // 省略其他 实例方法混入代码

可以看到,new Vue 就仅仅调用了内部实例方法 _init 进行初始化,接下来看 _init 的实现

 // core/instance/init.js
 // 以下代码省略了部分开发环境错误提示代码
 ​
   Vue.prototype._init = function (options?: Object) {
     // 实例
     const vm: Component = this
     // 每个 vue 实例都有一个 _uid,并且是依次递增的
     vm._uid = uid++
 ​
     // 避免vm实例被响应式化的标识
     vm._isVue = true
       
     // 处理组件配置项
     if (options && options._isComponent) {
       // 子组件的配置项处理, 忽略
       initInternalComponent(vm, options)
     } else {
       // 合并构造函数的options和当前实例的options
       vm.$options = mergeOptions(
         resolveConstructorOptions(vm.constructor),
         options || {},
         vm
       )
     }
     // 存下vm, 忽略
     vm._self = vm
     
     initLifecycle(vm) // 将生命周期相关的标识初始化
     initEvents(vm) // 初始化自定义事件
     initRender(vm) // 初始化 渲染相关内容,主要是插槽和 vm.$createElement方法即 h 函数 
     callHook(vm, 'beforeCreate') // 调用 beforeCreate 生命周期钩子
     initInjections(vm) // 处理 injections 
     initState(vm) // 处理 props、methods、data、computed、watch
     initProvide(vm) // 处理 provide 
     callHook(vm, 'created') // 调用 created 生命周期钩子
 ​
     // 重点: 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
     if (vm.$options.el) {
       // 调用$mount方法挂载vue生成的dom
       vm.$mount(vm.$options.el)
     }
   }
 }

上面代码看着不少,其实主要就 3 步,1. 处理options,选项合并,2.初始化和处理传进来的各选项参数 ,3.调用$mount挂载dom

先不管初始化的内容,直接看挂载

 Vue.prototype.$mount = function (
   el?: string | Element,
   hydrating?: boolean
 ): Component {
   el = el && query(el)
 ​
   const options = this.$options
   
   // 将template或者el转化成 render函数
   if (!options.render) {
     let template = options.template
     if (template) {
       if (typeof template === 'string') {
         if (template.charAt(0) === '#') {
           template = idToTemplate(template)
         }
       } else if (template.nodeType) {
         template = template.innerHTML
       } else {
         return this
       }
     } else if (el) {
       template = getOuterHTML(el)
     }
     if (template) {
       
       // compileToFunctions即将template转化成render函数的方法
       // staticRenderFns用来处理使用了v-pre指令的节点,会加快编译的,可以暂时忽略
       // 大致流程:
       // compileToFunctions一共分成四个步骤:
       // parse:把template转成AST语法树
       // optimize:优化静态节点
       // generate:通过ast,重新生成代码
       // 通过new Function生成render函数
       const { render, staticRenderFns } = compileToFunctions(template, {
         outputSourceRange: process.env.NODE_ENV !== 'production',
         shouldDecodeNewlines,
         shouldDecodeNewlinesForHref,
         delimiters: options.delimiters,
         comments: options.comments
       }, this)
       // 挂载 render 函数到 this.$options 上
       options.render = render
       options.staticRenderFns = staticRenderFns
 ​
     }
   }
   // 最后调用 mountComponent
   return mountComponent(this, el, hydrating)
 }

上面的案例经过编译后 render函数就是这样:

 function () {
   with(this){
       return _c('div',{attrs:{"id":"app"}},[_c('div',[_v("\n姓名: "+_s(name)+"\n ")])])
   }
 }

其中,利用with语法实现了template里的变量的自动代理,效果就是写template时不用写this了。

_c,_v,_s 就是vue的一些编译时的转换帮助函数别名,其中_c就是 createElement,其他的可以参见源码

 // core/instance/render-helpers.js
 function installRenderHelpers (target: any) {
   target._o = markOnce
   target._n = toNumber
   target._s = toString
   target._l = renderList
   target._t = renderSlot
   target._q = looseEqual
   target._i = looseIndexOf
   target._m = renderStatic
   target._f = resolveFilter
   target._k = checkKeyCodes
   target._b = bindObjectProps
   target._v = createTextVNode
   target._e = createEmptyVNode
   target._u = resolveScopedSlots
   target._g = bindObjectListeners
   target._d = bindDynamicKeys
   target._p = prependModifier
 }
 // core/instance/render.js
 export function initRender (vm: Component) {
   // 省略其他代码
   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
   vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
 }

生成render函数并挂载到this.$options 上后,就是 调用mountComponent函数继续处理

 // core/instance/lifecycle.js
 // 省略部分开发环境代码
 export function mountComponent (
   vm: Component,
   el: ?Element,
   hydrating?: boolean // ssr相关参数,忽略
 ): Component {
   vm.$el = el
   // 没有render就建个空的
   if (!vm.$options.render) {
     vm.$options.render = createEmptyVNode
   }
   // 调用 beforeMount 生命周期钩子
   callHook(vm, 'beforeMount')
  
   // 更新组件的函数
   let updateComponent = () => {
       // 调用 _render生成VNode, 调用_update将VNode转成dom并挂载
       vm._update(vm._render(), hydrating)
   }
   // 创建 渲染watcher
   new Watcher(vm, updateComponent, noop, {
     before () {
       if (vm._isMounted && !vm._isDestroyed) {
         callHook(vm, 'beforeUpdate')
       }
     }
   }, true /* isRenderWatcher */)
 ​
   // 根组件没有 $vnode, 根组件的 mounted 在这里处理,子组件的在其他位置
   if (vm.$vnode == null) {
     vm._isMounted = true
     // 调用 mounted 生命周期钩子
     callHook(vm, 'mounted')
   }
   return vm
 }

上面mountComponent主要就是创建了渲染watcher,渲染watcher会初始化时立即执行一次,所以就会调用updateComponent方法, 然后调用 _render生成VNode,调用_updateVNode转成dom并挂载。

先看_render

 // core/instance/render.js
 // 省略部分开发环境代码
 Vue.prototype._render = function (): VNode {
     const vm: Component = this
     // 取出先前挂载在$options上的render函数
     const { render, _parentVnode } = vm.$options
 ​
     // 非根组件,处理下slot
     if (_parentVnode) {
       vm.$scopedSlots = normalizeScopedSlots(
         _parentVnode.data.scopedSlots,
         vm.$slots,
         vm.$scopedSlots
       )
     }
     vm.$vnode = _parentVnode
     
     let vnode
     // render函数存在时用户直接编写的情况,所以要做下容错处理
     try {
       currentRenderingInstance = vm
       // 调用render函数,生成VNode
       vnode = render.call(vm, vm.$createElement)
     } catch (e) {
       handleError(e, vm, `render`)
       vnode = vm._vnode
     } finally {
       currentRenderingInstance = null
     }
     // 如果render函数返回了多了VNode,则只取第一个,这里就是要求我们写template时最顶层一定只能有一个节点
     if (Array.isArray(vnode) && vnode.length === 1) {
       vnode = vnode[0]
     }
     if (!(vnode instanceof VNode)) {
       // 返回了非VNode的内容,则直接给个空节点
       vnode = createEmptyVNode()
     }
     // set parent,这里的vnode其实是组件内部template转换出来的虚拟dom,也就是组件的第一元素的vnode,而_parentVnode是整个组件对应的 组件vnode,有点绕可以细细品
     vnode.parent = _parentVnode
     return vnode
   }

可以看到_render就是调用之前根据选项生成的render函数,而render函数内部又会调用_c之类的方法也就是上面的createElement生成VNode,也就是大名鼎鼎的虚拟dom。

 // core/vdom/create-element.js
 // 省略部分开发环境代码和其他功能代码
 ​
 function createElement (
   context: Component,
   tag?: string | Class<Component> | Function | Object,
   data?: VNodeData,
   children?: any,
   normalizationType?: number
 ): VNode | Array<VNode> {
  
   // 处理is
   if (isDef(data) && isDef(data.is)) {
     tag = data.is
   }
   if (!tag) {
     return createEmptyVNode()
   }
 ​
   let vnode
   if (typeof tag === 'string') {
     let Ctor
     // html节点
     if (config.isReservedTag(tag)) {
       vnode = new VNode(
         config.parsePlatformTagName(tag), data, children,
         undefined, undefined, context
       )
     // 组件节点
     } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
       // component
       vnode = createComponent(Ctor, data, context, children, tag)
     // 未知节点
     } else {
       vnode = new VNode(
         tag, data, children,
         undefined, undefined, context
       )
     }
   } else {
     // 直接试 component options 或者 constructor 的情况
     vnode = createComponent(tag, data, context, children)
   }
   // 返回 vnode
   if (isDef(vnode)) {
     return vnode
   } else {
     return createEmptyVNode()
   }
 }

createElement方法就是根据tag不同,去掉new VNode生成不同的虚拟dom。

 // core/instance/vdom/vnode.js
 export default class VNode {
   tag: string | void;
   data: VNodeData | void;
   children: ?Array<VNode>;
   text: string | void;
   elm: Node | void;
   ns: string | void;
   context: Component | void; // rendered in this component's scope
   key: string | number | void;
   componentOptions: VNodeComponentOptions | void;
   componentInstance: Component | void; // component instance
   parent: VNode | void; // component placeholder node
 ​
   // strictly internal
   raw: boolean; // contains raw HTML? (server only)
   isStatic: boolean; // hoisted static node
   isRootInsert: boolean; // necessary for enter transition check
   isComment: boolean; // empty comment placeholder?
   isCloned: boolean; // is a cloned node?
   isOnce: boolean; // is a v-once node?
   asyncFactory: Function | void; // async component factory function
   asyncMeta: Object | void;
   isAsyncPlaceholder: boolean;
   ssrContext: Object | void;
   fnContext: Component | void; // real context vm for functional nodes
   fnOptions: ?ComponentOptions; // for SSR caching
   devtoolsMeta: ?Object; // used to store functional render context for devtools
   fnScopeId: ?string; // functional scope id support
 ​
   constructor (
     tag?: string,
     data?: VNodeData,
     children?: ?Array<VNode>,
     text?: string,
     elm?: Node,
     context?: Component,
     componentOptions?: VNodeComponentOptions,
     asyncFactory?: Function
   ) {
     this.tag = tag
     this.data = data
     this.children = children
     this.text = text
     this.elm = elm
     this.ns = undefined
     this.context = context
     this.fnContext = undefined
     this.fnOptions = undefined
     this.fnScopeId = undefined
     this.key = data && data.key
     this.componentOptions = componentOptions
     this.componentInstance = undefined
     this.parent = undefined
     this.raw = false
     this.isStatic = false
     this.isRootInsert = true
     this.isComment = false
     this.isCloned = false
     this.isOnce = false
     this.asyncFactory = asyncFactory
     this.asyncMeta = undefined
     this.isAsyncPlaceholder = false
   }
 ​
   get child (): Component | void {
     return this.componentInstance
   }
 }
 ​

以上是VNode的全部内容,其实有的多,我们只用关心// strictly internal之上的字段,其他的很多是内部细节逻辑处理需要的字段,暂时可以跳过。

好的,了解了虚拟dom即VNode的生成过程,在看_update方法的实现,再了解虚拟dom是怎么变成真实dom的

 // core/instance/lifecycle.js
 // 省略部分其他代码
   Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
     const vm: Component = this
     // 缓存旧节点
     const prevVnode = vm._vnode
     vm._vnode = vnode
  
     if (!prevVnode) {
       // 首次渲染
       vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
     } else {
       // 更新
       vm.$el = vm.__patch__(prevVnode, vnode)
     }
   }

_update就是调用vm.__patch__方法进行的挂载和更新, vm.__patch__就是patch方法,这么写是为了兼容Weex,所以我们直接看patch的实现,patch同时是支持首次渲染和更新两种情况的

 // core/vdom/patch.js
 // 简化了部分代码
 function patch (oldVnode, vnode, hydrating, removeOnly) {
   
     // 调用到patch时vm.$el会被处理掉,所以这里的oldVnode会不存在,即首次渲染
     if (isUndef(oldVnode)) {
         // 首次渲染
         createElm(vnode)
     } else {
         // 更新时的操作,这里就是大名鼎鼎的 diff 算法的内容,下一章,我们会讲
     }
 ​
     // 返回生成的真实dom
     return vnode.elm
 }
 ​
 function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
 ) {
    
     vnode.isRootInsert = !nested // for transition enter check
      // 创建子组件,并挂载子组件, 子组件的逻辑后面的章节会讲
     if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
         return
     }
 ​
     const data = vnode.data
     const children = vnode.children
     const tag = vnode.tag
     // 正常dom节点
     if (isDef(tag)) {
         // 调用dom的方法创建节点, nodeOps里封装的就是dom的各种方法,insert也是,不直接使用是为了兼容Weex
         vnode.elm = nodeOps.createElement(tag, vnode)
         // 创建子节点
         createChildren(vnode, children, insertedVnodeQueue)
         // 插入进页面
         insert(parentElm, vnode.elm, refElm)
         
     // 注释节点
     } else if (isTrue(vnode.isComment)) {
         vnode.elm = nodeOps.createComment(vnode.text)
         insert(parentElm, vnode.elm, refElm)
     // 文本节点
     } else {
         vnode.elm = nodeOps.createTextNode(vnode.text)
         insert(parentElm, vnode.elm, refElm)
     }
 }

至此,vue的首次渲染流程完成。后面就是数据更新后,导致的上面创建的渲染watcher更新。触发patch里的更新,就是大名鼎鼎的diff算法,下一章讲。

总结一下:

new Vue(options) -> this._init 初始化 -> this.$mounttemplate编译成 render -> mountComponent创建渲染watcher -> this._render 调用刚刚的render 生成 VNode -> this._update -> patch 生成真实dom 并挂载