前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >5. 「vue@2.6.11 源码分析」组件渲染之创建虚拟DOM

5. 「vue@2.6.11 源码分析」组件渲染之创建虚拟DOM

作者头像
tinyant
发布2023-02-24 10:24:03
9700
发布2023-02-24 10:24:03
举报
文章被收录于专栏:webpack@4.46.0源码分析

vue@2.x中用到了虚拟DOM技术,基于第三方虚拟DOM库sanbbdom修改。建议阅读本文之前对snabbdom的使用和原理 有一定的了解,可以参考 snabbdom@3.5.1 源码分析

vue2中组件渲染的核心入口如下:

代码语言:javascript
复制
// src/core/instance/lifecycle.js
export function mountComponent (vm: Component, el: ?Element, hydrating?: boolean): Component {
  vm.$el = el
  //...

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

 return vm
}

其中vm._render用来生产虚拟节点树的,就像snabbdom中使用h函数创建虚拟节点树一样。而vm._update用来将上一步即vm._render生成的虚拟节点树经过patch操作同步到界面上。

new Wacher(...)用法在上一节数据驱动详细分析过。updateComponent在首次创建watcher实例时会执行一次,当updateComponent依赖的响应式数据变化时会再次执行。

因此上面new Watcher(vm, updateComponent,..)方法中的两个操作_render() -> _update(),相当于snabbdom的如下操作

初始化时类比

代码语言:javascript
复制
const container = document.getElementById("container");
const vnode = h(...); // 创建虚拟节点树
patch(container, vnode); // 同步虚拟DOM树同步到界面

响应式数据更新时类比

代码语言:javascript
复制
// 如果此时有数据变更引起界面变更
const newVnode = h(...); // 新的虚拟节点树
patch(vnode, newVnode); // 和上一次的虚拟节点树进行diff,将差异同步到界面上

这里的巧妙出是new Watcher两个步骤合并到一起。

下面我们重点看下vue@2.x中关于虚拟DOM的相关逻辑。主要逻辑在src/core/vdom文件夹中。

从入口讲起(对应snabbdom库 init 方法)

patch方法是跨平台的,因此在编译入口处便做了区分,web平台下

运行时的编译入口在:src/platforms/web/runtime/index.js,此时就定义了__patch__方法,然后在vm._update会调用vm.__patch__实现diff能力

代码语言:javascript
复制
// src/platforms/web/runtime/index.js

import { patch } from './patch'
//...
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
//...
代码语言:javascript
复制
// src/platforms/web/runtime/patch.js

import * as nodeOps from 'web/runtime/node-ops'
import { createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

export const patch: Function = createPatchFunction({ nodeOps, modules })

createPatchFunction就相当于snabbdom中init方法,nodeOps是因为跨平台的原因放在这里(私有化),这里重点关注modules,在snabbdom中说到module会借助patch过程中触发的各种钩子参与DOM的修改。这里都有哪些module呢,分为两类:基础module和跨平台module,如下:

可能会单独出一个小节分析这些module

小结

这里主要是创建patch方法的过程

vnode

vue@2.x中vnode在snabbdom定义的vnode基础上增加了很多其他的属性,

  • 后面用到逐个介绍?最终统一在这里汇总一次?

_render:创建虚拟节点树

我们先看下vm._render方法的定义

代码语言:javascript
复制
  Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      //... slot相关,暂忽略
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
        //... 异常处理
    } finally {
      currentRenderingInstance = null
    }
    //... 
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

这里关注三个地方

render 函数的执行,render函数长什么样子呢?

代码语言:javascript
复制
<!-- 原始模板 -->
<div id="app"> {{ message }} </div>
代码语言:javascript
复制
// 编译后的render函数
(function anonymous() {
  with (this) {
    return _c('div', {attrs: {"id": "app"}}, [_v("\n  " + _s(message) + "\n")])
  }
})

render来自哪里?

  1. render函数可以由开发者自己提供
  2. 也提供了编译 + 运行时版本,即可有运行时编译,框架会自动处理将模板处理成render函数
  3. 更为常见的是.vue单文件开发,vue-loader会将其自动将template部分处理成render函数

currentRenderingInstance 的设置

关系链接

  1. vm.vnode = _parentVnode,当前组件实例的 vnode指向父虚拟节点
  2. 虚拟节点父子关系建立:vnode.parent = _parentVnode
  3. 这里返回的vnode,会在vm.update中被设置给vm:vm._vnode = vnode

下面重点看下render函数的执行,还是以上面的render函数为例,如下

代码语言:javascript
复制
<!-- 原始模板 -->
<div id="app"> {{ message }} </div>
代码语言:javascript
复制
// 编译后的render函数
(function anonymous() {
  with (this) {
    return _c('div', {attrs: {"id": "app"}}, [_v("\n  " + _s(message) + "\n")])
  }
})

显然里面用到了的_c、_v都是函数,主要是_c,该函数等价于snabbdom的h函数,用来创建虚拟DOM。

需要注意到with的用法,with中的this就是组件实例,该实例上挂载_c这些方法,以及render函数中用到数据如上面demo中的messagewith特性

下面看下_c,_v的定义

代码语言:javascript
复制
// src/core/instance/render.js
import { createElement } from '../vdom/create-element'
export function initRender (vm: Component) {
    //...
    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)
    //...
}

当vue运行时代码执行时就会执行 renderMixin -> installRenderHelpers(Vue.prototype),该方法挂载了一些工具方法和创建DOM节点的方法。

代码语言:javascript
复制
export function installRenderHelpers (target: any) { // target: Vue.prototype
  //...
  target._s = toString
  //...
  target._v = createTextVNode
  //...
}

我们重点关注_c指向的createElement方法

createElement:创建vnode

代码语言:javascript
复制
import VNode, { createEmptyVNode } from './vnode'
import { createComponent } from './create-component'
//...

// alwaysNormalize: 调用 vm.$createElement 方法时,传递ture,看到 _render() -> render.call(vm, vm.$createElement),也就是执行用户自己提供的render函数时会走这里
// createFunctionComponent 又有可能
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> {
  //... vnode data 不能是响应式数据,如果是返回空vnode
  
  // object syntax in v-bind
  if (isDef(data) &amp;&amp; isDef(data.is)) { // 动态组件
    tag = data.is
  }
  
  //... 如果没有tag,返回空vnode  
  //... 规范化孩子,不重要
  
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode &amp;&amp; context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) { 
      vnode = new VNode(config.parsePlatformTagName(tag), data, children, undefined, undefined, context)
    } else if ((!data || !data.pre) &amp;&amp; isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // 组件
      // component
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else { // new Vue({render: h => h(App)}) // 用户手动提供 render函数
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  
  ,
  if (Array.isArray(vnode)) { 
     // 如果vnode是数组,取第一个
  } else if (isDef(vnode)) {
    //...
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    // 如果没有返回空vnode
  }
}

// ref #5318
// necessary to ensure parent re-render when deep bindings like :style and
// :class are used on slot nodes
function registerDeepBindings (data) {
  if (isObject(data.style)) {
    traverse(data.style)
  }
  if (isObject(data.class)) {
    traverse(data.class)
  }
}
  • 上面注释提到了children的规范化,解释参考黄轶-vue技术揭秘
  • 记个todo,验证下(虽然不影响整体流程)

下面看下核心逻辑,实际上很清晰了

  1. 如果tag是对象或者是组件构造函数,则调用createComponent创建组件的虚拟节点(注意,这里并不会创建组件的vue实例,更不会进入组件内部去创建组件的实际内容),createComponent仅仅是创建组件标签(如<todo-item>)对应的vnode,本质上和div并无太多区别,主要是会挂载很多信息(props, events等等)
  2. 如果是保留tagdiv,直接new vnode
  3. 如果不是保留tag如todo-item,并从 vm.components 中查找有没有定义该组件
    1. 如果有则 createComponent
    2. 否则就是创建一个位置节点,同样会new vnode (和div的vnode的创建没啥区别)
  4. 记个todo, registerDeepBindings 作用?看起来是处理slot场景的bug❓❓❓

下面看下组件vnode创建场景:createComponent

createComponent:创建【组件tag】的vnode(并不会进入组件内部)

代码语言:javascript
复制
export function createComponent (Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string): VNode | Array<VNode> | void {
  if (isUndef(Ctor)) return

  const baseCtor = context.$options._base
  if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
  }
 
  if (typeof Ctor !== 'function') return

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
   // return ... 异步组件,单独的逻辑,后面会单独小节说
  }

  data = data || {}

  // resolve constructor options in case global mixins are applied after
  // component constructor creation
  resolveConstructorOptions(Ctor)

  // transform component v-model data into props &amp; events
  if (isDef(data.model)) {
    transformModel(Ctor.options, data)
  }

  // extract props
  const propsData = extractPropsFromVNodeData(data, Ctor, tag)

  if (isTrue(Ctor.options.functional)) {
      // return ... 函数式组件的创建 是单独的逻辑,后面有可能单独小节说下
  }
  
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  data.on = data.nativeOn

  if (isTrue(Ctor.options.abstract)) {
    //... 抽象组件的slot需要特殊处理? 如果时间允许单独看看
  }

  // install component management hooks onto the placeholder node
  installComponentHooks(data)

  // return a placeholder vnode
  const name = Ctor.options.name || tag
  const vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, 
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children }, asyncFactory)
 

  return vnode
}

创建组件肯定需要一个构造函数的,如果是组件对象如Ctor(data/props/methods等等),会通过Vue.extend(Ctor),该方法通过原型继承返回一个构造函数,后面会说到。

如果是异步组件,则走异步组件vnode创建逻辑

resolveConstructorOptions:从注释来看,是担心先创建的组件构造函数而后再注册全局mixin

  • 待验证,处理特殊场景,非核心逻辑,不重要

transformModel:(自定义组件v-model),看起来只是语法糖:取值,添加事件回调,

TODO,demo验证下?,不影响主流程

代码语言:javascript
复制
// transform component v-model info (value and callback) into
// prop and event handler respectively.
function transformModel (options, data: any) {
const prop = (options.model &amp;&amp; options.model.prop) || 'value'
const event = (options.model &amp;&amp; options.model.event) || 'input'
;(data.attrs || (data.attrs = {}))[prop] = data.model.value
const on = data.on || (data.on = {})
const existing = on[event]
const callback = data.model.callback
if (isDef(existing)) {
 if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback) {
   on[event] = [callback].concat(existing)
 }
} else {
 on[event] = callback
}
}

extractPropsFromVNodeData:这里只是从传递个组件的数据(vnode.data.attrs || vnode.data.props)中提取属性值(组件定义的props)。这里返回的是一个新对象,该对象后面会变成响应式对象,以被当前组件监听。

注意: 有个细节:从vnode.data.attrs中提取完数据,会对应的属性删除掉,而vnode.data.props则不会。

为什么这么做呢❓❓❓❓

代码语言:javascript
复制
export function extractPropsFromVNodeData (data: VNodeData, Ctor: Class<Component>, tag?: string): ?Object {
// we are only extracting raw values here.
// validation and default values are handled in the child
// component itself.
const propOptions = Ctor.options.props
if (isUndef(propOptions)) {
 return
}
const res = {}
const { attrs, props } = data
if (isDef(attrs) || isDef(props)) {
 for (const key in propOptions) {
   const altKey = hyphenate(key) 
   checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false)
 }
}
return res
}

如果是函数组件,则单独走函数组件vnode创建逻辑

获取事件回调,自定义事件在data.on上,native事件在data.nativeOn,处理后自定义事件保存到vnode.componentOptions.listeners上,native事件保存到vnode.data.on上。(在 _init -> initEvent中会用到)

installComponentHooks:给 vnode.data 添加部分钩子(init、prepatch、insert、destroy),后面会碰到每个vnode钩子调用的时机,碰到时在针对每个钩子细说。

获取组件名称,创建组件标签对应的vnode(new vnode),这里重点是保存了组件的数据(事件、属性数据等),因为在后面_update会深入组件内部,进入组件的渲染,而组件的渲染是需要这些数据支撑的。

总结

创建虚拟DOM树,下一步就是调用vm._update将虚拟DOM树同步到界面上。

下一节,重点分析虚拟DOM到界面的过程(包括初始化和更新)。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 从入口讲起(对应snabbdom库 init 方法)
    • 小结
    • vnode
    • _render:创建虚拟节点树
      • createElement:创建vnode
        • createComponent:创建【组件tag】的vnode(并不会进入组件内部)
        • 总结
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档