从 vue 源码看问题 —— vue 中的 render helper 是什么?(三)

简介: 从 vue 源码看问题 —— vue 中的 render helper 是什么?

extractPropsFromVNodeData() 方法

文件位置:/src/core/vdom/helpers/extract-props.js

/**
 * <comp msg="hello vue"></comp>
 * 
 * 提取 props,得到 res[key] = val 
 * 以 props 配置中的属性为 key,父组件中对应的的数据为 value
 * 
 * 当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里
 * 这样子组件中相应的数据就会被更新 
 */
export function extractPropsFromVNodeData (
  data: VNodeData,
  Ctor: Class<Component>,
  tag?: string
): ?Object {
  // 组件的 props 选项,{ props: { msg: { type: String, default: xx } } }
  // 这里只提取原始值,验证和默认值在子组件中处理
  // we are only extracting raw values here.
  // validation and default values are handled in the child
  // component itself.
  const propOptions = Ctor.options.props
  // 没有定义 props 直接返回
  if (isUndef(propOptions)) {
    return
  }
  /*
   以组件 props 配置中的属性为 key,父组件传递下来的值为 value
   当父组件中数据更新时,触发响应式更新,重新执行 render,生成新的 vnode,又走到这里
   这样子组件中相应的数据就会被更新
  */ 
  const res = {}
  const { attrs, props } = data
  if (isDef(attrs) || isDef(props)) {
    // 遍历 propsOptions
    for (const key in propOptions) {
      // 将小驼峰形式的 key 转换为 连字符 形式
      const altKey = hyphenate(key)
      /*
       提示,如果声明的 props 为小驼峰形式(testProps),但由于 html 不区分大小写,
            所以在 html 模版中应该使用 test-props 代替 testProps
      */ 
      if (process.env.NODE_ENV !== 'production') {
        const keyInLowerCase = key.toLowerCase()
        if (
          key !== keyInLowerCase &&
          attrs && hasOwn(attrs, keyInLowerCase)
        ) {
          tip(
            `Prop "${keyInLowerCase}" is passed to component ` +
            `${formatComponentName(tag || Ctor)}, but the declared prop name is` +
            ` "${key}". ` +
            `Note that HTML attributes are case-insensitive and camelCased ` +
            `props need to use their kebab-case equivalents when using in-DOM ` +
            `templates. You should probably use "${altKey}" instead of "${key}".`
          )
        }
      }
      checkProp(res, props, key, altKey, true) ||
      checkProp(res, attrs, key, altKey, false)
    }
  }
  return res
}
复制代码

checkProp() 方法

文件位置:/src/core/vdom/helpers/extract-props.js

// 得到 res[key] = val
function checkProp (
  res: Object,
  hash: ?Object,
  key: string,
  altKey: string,
  preserve: boolean
): boolean {
  if (isDef(hash)) {
    /*
      判断 hash(props/attrs)对象中是否存在 key 或 altKey
      存在则设置给 res => res[key] = hash[key]
    */ 
    if (hasOwn(hash, key)) {
      res[key] = hash[key]
      if (!preserve) {
        delete hash[key]
      }
      return true
    } else if (hasOwn(hash, altKey)) {
      res[key] = hash[altKey]
      if (!preserve) {
        delete hash[altKey]
      }
      return true
    }
  }
  return false
}
复制代码

createFunctionalComponent() 方法

文件位置:/src/core/vdom/create-functional-component.js

/**
 * 执行函数式组件的 render 函数生成组件的 VNode :
 *   1、设置组件的 props 对象
 *   2、设置函数式组件的渲染上下文,传递给函数式组件的 render 函数
 *   3、调用函数式组件的 render 函数生成 vnode
 * 
 * @param {*} Ctor 组件的构造函数 
 * @param {*} propsData 额外的 props 对象
 * @param {*} data 节点属性组成的 JSON 字符串
 * @param {*} contextVm 上下文
 * @param {*} children 子节点数组
 * @returns Vnode or Array<VNode>
 */
export function createFunctionalComponent (
  Ctor: Class<Component>,
  propsData: ?Object,
  data: VNodeData,
  contextVm: Component,
  children: ?Array<VNode>
): VNode | Array<VNode> | void {
  // 组件配置项
  const options = Ctor.options
  // 获取 props 对象
  const props = {}
  // 组件本身的 props 选项
  const propOptions = options.props
  // 设置函数式组件的 props 对象
  if (isDef(propOptions)) {
    /*
     说明该函数式组件本身提供了 props 选项,
     则将 props.key 的值设置为组件上传递下来的对应 key 的值
    */ 
    for (const key in propOptions) {
      props[key] = validateProp(key, propOptions, propsData || emptyObject)
    }
  } else {
    // 当前函数式组件没有提供 props 选项,则将组件上的 attribute 自动解析为 props
    if (isDef(data.attrs)) mergeProps(props, data.attrs)
    if (isDef(data.props)) mergeProps(props, data.props)
  }
  // 实例化函数式组件的渲染上下文
  const renderContext = new FunctionalRenderContext(
    data,
    props,
    children,
    contextVm,
    Ctor
  )
  // 调用 render 函数,生成 vnode,并给 render 函数传递 _c 和 渲染上下文
  const vnode = options.render.call(null, renderContext._c, renderContext)
  // 在最后生成的 VNode 对象上加一些标记,表示该 VNode 是一个函数式组件生成的,最后返回 VNode
  if (vnode instanceof VNode) {
    return cloneAndMarkFunctionalResult(vnode, data, renderContext.parent, options, renderContext)
  } else if (Array.isArray(vnode)) {
    const vnodes = normalizeChildren(vnode) || []
    const res = new Array(vnodes.length)
    for (let i = 0; i < vnodes.length; i++) {
      res[i] = cloneAndMarkFunctionalResult(vnodes[i], data, renderContext.parent, options, renderContext)
    }
    return res
  }
}
复制代码

installComponentHooks() 方法

文件位置:/src/core/vdom/create-component.js

const hooksToMerge = Object.keys(componentVNodeHooks)
/**
 * 在组件的 data 对象上设置 hook 对象,
 * hook 对象增加四个属性,init、prepatch、insert、destroy,
 * 负责组件的创建、更新、销毁
 */
function installComponentHooks (data: VNodeData) {
  const hooks = data.hook || (data.hook = {})
  // 遍历 hooksToMerge 数组,hooksToMerge = ['init', 'prepatch', 'insert' 'destroy']
  for (let i = 0; i < hooksToMerge.length; i++) {
    // 比如 key = init
    const key = hooksToMerge[i]
    // 从 data.hook 对象中获取 key 对应的方法
    const existing = hooks[key]
    // componentVNodeHooks 对象中 key 对象的方法
    const toMerge = componentVNodeHooks[key]
    // 合并用户传递的 hook 方法和框架自带的 hook 方法,其实就是分别执行两个方法
    if (existing !== toMerge && !(existing && existing._merged)) {
      hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge
    }
  }
}
复制代码

componentVNodeHooks 对象

文件位置:/src/core/vdom/create-component.js

/*
  patch 期间在组件 vnode 上调用内联钩子
  inline hooks to be invoked on component VNodes during patch
*/
const componentVNodeHooks = {
  // 初始化
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if (
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      /*
        被 keep-alive 包裹的组件
        kept-alive components, treat as a patch
      */
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      // 创建组件实例,即 new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      /*
       执行组件的 $mount 方法,进入挂载阶段,接下来就是通过编译器得到 render 函数,
       接着走 挂载、patch 流程,直到组件渲染到页面
      */
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
  // 更新 VNode,用新的 VNode 配置更新旧的 VNode 上的各种属性
  prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
    // 新 VNode 的组件配置项
    const options = vnode.componentOptions
    // 旧 VNode 的组件配置项
    const child = vnode.componentInstance = oldVnode.componentInstance
    // 用 vnode 上的属性更新 child 上的各种属性
    updateChildComponent(
      child,
      options.propsData, // updated props
      options.listeners, // updated listeners
      vnode, // new parent vnode
      options.children // new children
    )
  },
  // 执行组件的 mounted 声明周期钩子
  insert (vnode: MountedComponentVNode) {
    const { context, componentInstance } = vnode
    // 如果组件未挂载,则调用 mounted 声明周期钩子
    if (!componentInstance._isMounted) {
      componentInstance._isMounted = true
      callHook(componentInstance, 'mounted')
    }
    // 处理 keep-alive 组件的异常情况
    if (vnode.data.keepAlive) {
      if (context._isMounted) {
        // vue-router#1212
        // During updates, a kept-alive component's child components may
        // change, so directly walking the tree here may call activated hooks
        // on incorrect children. Instead we push them into a queue which will
        // be processed after the whole patch process ended.
        queueActivatedComponent(componentInstance)
      } else {
        activateChildComponent(componentInstance, true /* direct */)
      }
    }
  },
  /*
   销毁组件
     1、如果组件被 keep-alive 组件包裹,则使组件失活,不销毁组件实例,从而缓存组件的状态
     2、如果组件没有被 keep-alive 包裹,则直接调用实例的 $destroy 方法销毁组件
   */
  destroy (vnode: MountedComponentVNode) {
     // 从 vnode 上获取组件实例
    const { componentInstance } = vnode
    // 如果组件实例没有被销毁
    if (!componentInstance._isDestroyed) {
      // 组件没有被 keep-alive 组件包裹,则直接调用 $destroy 方法销毁组件
      if (!vnode.data.keepAlive) {
        componentInstance.$destroy()
      } else {
        // 组件被 keep-alive 组件包裹,负责让组件失活,不销毁组件实例,从而缓存组件的状态
        deactivateChildComponent(componentInstance, true /* direct */)
      }
    }
  }
}
复制代码

createComponentInstanceForVnode 对象

文件位置:/src/core/vdom/create-component.js

/*
  new vnode.componentOptions.Ctor(options) => 得到 Vue 组件实例 
*/
export function createComponentInstanceForVnode (
  // flow 不知道它是 MountedComponentVNode
  vnode: any,
  // 处于生命周期状态的 activeInstance
  parent: any
): Component {
  const options: InternalComponentOptions = {
    _isComponent: true,
    _parentVnode: vnode,
    parent
  }
  // 检查内联模版渲染函数
  const inlineTemplate = vnode.data.inlineTemplate
  if (isDef(inlineTemplate)) {
    options.render = inlineTemplate.render
    options.staticRenderFns = inlineTemplate.staticRenderFns
  }
  return new vnode.componentOptions.Ctor(options)
}
复制代码

总结

组件是如何变成 VNode?

  • 组件实例 init 初始化,最后执行 $mount 进入挂载阶段
  • 如果是只包含 运行时vue.js,只直接进入挂载阶段,因为此时组件已经变成了渲染函数;编译过程 通过 模块打包器 + vue-loader + vue-template-compiler 完成的
  • 如果没有使用 预编译,则必须使用全量的 vue.js
  • 挂载时如果发现组件配置项上没有 render 选项,则进入 编译阶段
  • 将模版字符串编译成 AST 语法树 —— 普通的 JS 对象
  • 然后优化 AST,遍历 AST 对象,标记每一个节点是否为 静态节点;然后再进一步标记出 静态根节点,在组件后续更新时会跳过这些静态节点的更新,以提高性能
  • 接下来从 AST 生成 渲染函数
  • 负责生成动态节点 VNoderender 函数
  • staticRenderFns 数组,其中每个元素都是一个生成静态节点 VNode 的函数,这些函数会作为 render 函数的组成部分,负责生成静态节点的 VNode
  • 接下来将渲染函数放到组件的配置对象上,进入 挂载阶段,即执行 mountComponent 方法
  • 最终负责渲染组件和更新组件的是一个叫 updateComponent 方法,该方法每次执行前首先需要执行 vm._render 函数,该函数负责执行编译器生成的 render,得到组件的 VNode
  • 将组件生成 VNode 的具体工作是由 render 函数中的 _c、_o、_l、_m 等方法完成的,这些方法都被挂载到 Vue 实例上,负责在运行时生成组件 VNode
  • 设置组件配置信息,然后通过 new VNode(组件信息) 生成组件的 VNode

render helper 的作用就是:在 Vue 实例上挂载一些运行时的工具方法,这些方法用在编译器生成的渲染函数中,用于生成组件的 VNode

props 响应式的原理是什么?

  • 在处理组件的 props 时,会提取组件的 props 数据,以组件的 props 配置中的属性为 key,父组件中对应的数据为 value 生成一个 propsData 对象
  • 当组件更新时生成新的 VNode,又会进行上一步,得到更新后的 propsData 对象


目录
相关文章
|
6天前
|
缓存 监控 JavaScript
探讨优化Vue应用性能和加载速度的策略
【5月更文挑战第17天】本文探讨了优化Vue应用性能和加载速度的策略:1) 精简代码和组件拆分以减少冗余;2) 使用计算属性和侦听器、懒加载、预加载和预获取优化路由;3) 数据懒加载和防抖节流处理高频事件;4) 图片压缩和选择合适格式,使用CDN加速资源加载;5) 利用浏览器缓存和组件缓存提高效率;6) 使用Vue Devtools和性能分析工具监控及调试。通过这些方法,可提升用户在复杂应用中的体验。
21 0
|
6天前
|
JavaScript 前端开发
vue(1),小白看完都会了
vue(1),小白看完都会了
|
6天前
|
JavaScript 数据库
ant design vue日期组件怎么清空 取消默认当天日期
ant design vue日期组件怎么清空 取消默认当天日期
|
6天前
|
JavaScript C++
vue高亮显示组件--转载
vue高亮显示组件--转载
12 0
|
1天前
|
JavaScript Java 关系型数据库
基于SprinBoot+vue的租房管理系统2
基于SprinBoot+vue的租房管理系统2
8 0
|
2天前
|
自然语言处理 JavaScript 数据可视化
5个值得推荐的Vue后台管理框架
几个优秀好看的 Vue 后台管理框架,每个框架都有自己的特点和优势,开发者可以根据项目需求选择适合的框架。
12 0
|
5天前
|
监控 安全 NoSQL
采用java+springboot+vue.js+uniapp开发的一整套云MES系统源码 MES制造管理系统源码
MES系统是一套具备实时管理能力,建立一个全面的、集成的、稳定的制造物流质量控制体系;对生产线、工艺、人员、品质、效率等多方位的监控、分析、改进,满足精细化、透明化、自动化、实时化、数据化、一体化管理,实现企业柔性化制造管理。
29 3
|
5天前
|
设计模式 JavaScript 前端开发
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
Vue3报错Property “xxx“ was accessed during render but is not defined on instance
|
5天前
|
JavaScript 开发工具 git
Vue 入门系列:.env 环境变量
Vue 入门系列:.env 环境变量
13 1
|
6天前
|
JavaScript 前端开发 定位技术
Vue使用地图以及实现轨迹回放 附完整代码
Vue使用地图以及实现轨迹回放 附完整代码
Vue使用地图以及实现轨迹回放 附完整代码