【vue2.x原理剖析五】初始渲染及更新原理

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第5天,点击查看活动详情

前言

源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大

组件挂载入口

compiler版会将only版的$mount方法先缓存起来,扩展完功能后再调用,compiler版将template转化成render函数就会执行mountComponent方法挂载组件

// /core/platforms/web/runtime/index.js
Vue.prototype.$mount = function ( //公共的$mount
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating) //组件挂载
}

// /core/platforms/web/entry-runtime-with-compiler.js
//缓存mount方法
const mount = Vue.prototype.$mount
// 做扩展
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  ...
  // 调用组件挂载方法
  return mount.call(this, el, hydrating) 
}
复制代码

组件挂载的方法

// /src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  // 对el做缓存
  vm.$el = el
  
  callHook(vm, 'beforeMount') //beforeMount生命周期函数

  let updateComponent
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    ...
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }
  new Watcher(vm, updateComponent, noop, {
    before () { //更新钩子
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true )// 参数true表示是一个渲染watcher
  hydrating = false //不是服务端渲染
  if (vm.$vnode == null) {
    vm._isMounted = true //已经渲染过了
    callHook(vm, 'mounted')//当前组件挂载完毕 mounted声明周期函数
  }
  return vm
}
复制代码

render函数生虚拟节点

在模板编译时会将ast生成render函数,其中函数部分形式为_c('div', {id: 'app'}, _c('span', {}, 'world'), _v('hello')),其中包括_c、_v、_s三个函数,用于创建不同的元素

  • _c函数: 主要是对解析出来的tag及属性做处理
// /src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// /src/core/vdom/create-element.js
export function createElement (
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  ...
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  ...
  // 如果没有tag返回空vnode
  if (!tag) {
    return createEmptyVNode()
  }
  ...
  let vnode
  if (typeof tag === 'string') {
    let Ctor
  // 如果是html标签,创建vnode,否则创建组件
    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))) {
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    vnode = createComponent(tag, data, context, children)
  }
}
复制代码
  • _v函数: 主要用于创建文本vnode
// /src/core/instance/render-helpers/index.js
target._v = createTextVNode
// /src/core/vdom/vnode.js
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}
复制代码
  • _s函数: 主要对模板里的内容做格式化
// /src/core/instance/render-helpers/index.js
 target._s = toString
// /shared/util.js
export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : String(val)
}
复制代码

虚拟dom转化成真实dom

在初始化时,会将初次生成的vnode储存起来,以便更新的时候做diff对比,初次diff是真实的dom节点和生成的vnode做对比,之后根据vnode调用js方法创建真实dom并替换掉el的位置,初始渲染完成

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}
// /core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode // 将当前虚拟节点保存起来
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode // 当前render函数产生的虚拟节点
    if (!prevVnode) {
      // 初次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
    } else {
      // diff比较更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
  }
复制代码

更新

在初始化渲染挂载时候,会新建一个渲染watcher,在new watcher过程中,会去执行updateComponent方法进行模板渲染,同时会将当前的渲染watcher储存在访问模板值得dep中,当模板值发生变化时,会调用dep.notify方法通知收集的watcher进行更新,也就是会再次调用updateComponent方法从而重新渲染

// /src/core/instance/lifecycle.js
new Watcher(vm, updateComponent, noop, {
  before () { //更新钩子
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true )
复制代码

总结

compiler版$mount方法在only版的基础上做了扩展,将template转化成ast后,再将ast转化成render函数,然后调用_update方法进行初始化渲染,此时会进行diff算法比较后创建真实dom,初次比较的节点是options中的el节点和生成的虚拟节点作比较,并将虚拟节点保存起来作为下次的比较对象。由于初次挂载是会创建一个渲染watcher,所以再访问模板值得时候会将当前渲染watcher收集在响应的dep中,当模板值发生变化时,会触发setter拦截,调用notify方法通知所有收集的watcher进行更新,会再次调用updateComponent方法,经diff比较后更新视图

系列链接

【Vue2.x原理剖析一】响应式原理
【Vue2.x原理剖析二】计算属性原理
【Vue2.x原理剖析三】侦听属性原理
【Vue2.x原理剖析四】模板编译原理
【Vue2.x原理剖析五】初始渲染及更新原理

猜你喜欢

转载自juejin.im/post/7127800967268925471