Vue源码翻译之渲染逻辑链

  本篇文章主要要记录说明的是,Vue在Vdom的创建上的相关细节。这也是描绘了Vue在界面的创建上的一个逻辑顺序,同时我也非常拜服作者编码的逻辑性,当然或许这么庞大复杂的编码不是一次性铸就的,我想应该也是基于多次的需求变动而不断完善至现在如此庞大的结构和复杂度。

  首先我们回顾 上一篇文章 中,讲到了Vue实例initMixin,就是实例初始化,但是,我们在看Vue的源码时,经常会遇到某个变量或方法,好像还没定义,怎么就用上了。那是因为,其实我们在使用Vue,即 new Vue(options) 的时候,其实Vue的整个类,已经在我们js的头部import时,就已经完全定义好了,Vue的类因为过于庞大,内部复杂,并且还有抽象分层,所以类的整个写法,会比较分散,但是当你在用它的时候(new Vue()),其实它已经完全初始化完毕,整个类的装配已经齐全,所以我们在看源码时,是根据工程目录来看,但Vue是建立在文本pack上,所以最终这些工程目录是会整合到一个文件里,所以我们遇到没看到的变量,不要感到困惑,你只要知道,它一定是在其他的某个地方初始化过。

  So,我们这次要说的,是整个Vue再界面的绘制逻辑。

  整个Vue组件的绘制过程,是这样一个方法链条:

  vm.$mount() -> mountComponent -> new Watcher()的构造函数 -> watcher.get() -> vm._update -> vm.__patch__()-> patch.createElm -> patch.createComponent -> componentVNodeHooks.init() -> createComponentInstanceForVnode -> child.$mount

  好了,从vm.$mount() -----> child.$mount,我相信大家应该看出个名堂来了,其实就是递归调用。在执行createComponentInstanceForVnode的时候,就把创建好的Vnode与父级Vnode进行关联,通过这么一长串的递归调用去创建整个Vnode Tree,然后在整个树创建完了以后呢,在patch那部分的代码,会继续后续逻辑,后续逻辑自然就是把这个创建好的局部Vnode树,替换掉对应的旧的Vnode节点,相当于更新了局部的页面内容。但这只是执行界面绘制的动作链条,要理解整个过程,要区分一下,区分成执行,和初始化两个步骤。我们来看看定义是从哪里开始的。

  首先要看的肯定是上一篇文章中讲到的 vm._c 以及 vm.$createElement ,这个函数的定义,是整个界面绘制逻辑的入口,但是并不是动作触发的入口,就像这个函数的名字一样,initRender,初始化绘制方法,实际上,就是对绘制动作进行了定义,但是并不是从这里执行。

InitRender



path:src/core/instance/render.js

 1 export function initRender (vm: Component) {
 2   vm._vnode = null // the root of the child tree
 3   vm._staticTrees = null // v-once cached trees
 4   const options = vm.$options
 5   const parentVnode = vm.$vnode = options._parentVnode // the placeholder node in parent tree
 6   const renderContext = parentVnode && parentVnode.context
 7   vm.$slots = resolveSlots(options._renderChildren, renderContext)
 8   vm.$scopedSlots = emptyObject
 9   // bind the createElement fn to this instance
10   // so that we get proper render context inside it.
11   // args order: tag, data, children, normalizationType, alwaysNormalize
12   // internal version is used by render functions compiled from templates
13   vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
14   // normalization is always applied for the public version, used in
15   // user-written render functions.
16   vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
17
18   // $attrs & $listeners are exposed for easier HOC creation.
19   // they need to be reactive so that HOCs using them are always updated
20   const parentData = parentVnode && parentVnode.data
21
22   /* istanbul ignore else */
23   if (process.env.NODE_ENV !== ‘production‘) {
24     defineReactive(vm, ‘$attrs‘, parentData && parentData.attrs || emptyObject, () => {
25       !isUpdatingChildComponent && warn(`$attrs is readonly.`, vm)
26     }, true)
27     defineReactive(vm, ‘$listeners‘, options._parentListeners || emptyObject, () => {
28       !isUpdatingChildComponent && warn(`$listeners is readonly.`, vm)
29     }, true)
30   } else {
31     defineReactive(vm, ‘$attrs‘, parentData && parentData.attrs || emptyObject, null, true)
32     defineReactive(vm, ‘$listeners‘, options._parentListeners || emptyObject, null, true)
33   }
34 }

  由此,我们再去查看createElement,这是一个又一个代码的封装,整个方法链的调用是这样子:

   createElement -> _createElement -> createComponent

  最终返回Vnode对象或Vnode对象数组(应该是在v-for的情况下返回数组)。中间的片段包含着一些校验逻辑,我就不说了,不是什么特别难理解的地方,我们直接看createComponent的方法

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

  // plain options object: turn it into a constructor
  if (isObject(Ctor)) {
    // Vue.extend(Component)
    Ctor = baseCtor.extend(Ctor)
  }

  // if at this stage it‘s not a constructor or an async component factory,
  // reject.
  if (typeof Ctor !== ‘function‘) {
    if (process.env.NODE_ENV !== ‘production‘) {
      warn(`Invalid Component definition: ${String(Ctor)}`, context)
    }
    return
  }

  // async component
  let asyncFactory
  if (isUndef(Ctor.cid)) {
    asyncFactory = Ctor
    Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)
    if (Ctor === undefined) {
      // return a placeholder node for async component, which is rendered
      // as a comment node but preserves all the raw information for the node.
      // the information will be used for async server-rendering and hydration.
      return createAsyncPlaceholder(
        asyncFactory,
        data,
        context,
        children,
        tag
      )
    }
  }

  data = data || {}

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

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

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

  // functional component
  if (isTrue(Ctor.options.functional)) {
    return createFunctionalComponent(Ctor, propsData, data, context, children)
  }

  // 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)) {
    // abstract components do not keep anything
    // other than props & listeners & slot

    // work around flow
    const slot = data.slot
    data = {}
    if (slot) {
      data.slot = slot
    }
  }

  // merge component management hooks onto the placeholder node
  mergeHooks(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
  )

  // Weex specific: invoke recycle-list optimized @render function for
  // extracting cell-slot template.
  // https://github.com/Hanks10100/weex-native-directive/tree/master/component
  /* istanbul ignore if */
  if (__WEEX__ && isRecyclableComponent(vnode)) {
    return renderRecyclableComponentTemplate(vnode)
  }

  return vnode
}

  首先,说明一下,入参Ctor是什么。其实这个Ctor,就是你平时写Vue文件时,components 对象里的那些东西,就是你写的单个Component对象。

  这个可以从上层_createElement方法中得知,如下图:

  其中调用的resolveAsset方法,就是从你的options,即你写的Component中,获取components属性,并且同时验证一下,与对应的tag是否存在于你定义的文件中,这个tag,是标签,是html标签,我们在使用自定义Vue组件的时候,都是自定义标签或<div is=‘componentName‘></div>  这样的方式。而这个tag就是要吗是is的值,要吗是你使用的html标签。

  再来。回到createComponent方法中,可以看到,代码一开始会去判断你这个组件对象是否是undefind,如果是undefind,那就直接退出。再往下看,有一行其实我们很熟悉,但可能有点懵逼的代码,就是 Ctor = baseCtor.extend(Ctor) ,这里怎么感觉有点熟悉,是的,这里其实就是我们经常在文档中看到的 Vue.extend(Component) 这么一个方法。这个baseCtor可以看到是从contentx.$options._base来的,这个contex 上级方法追溯就可以知道是一个vm对象,但是这个_base从何而来?不要着急,前面说了,遇到这种好像没看过的,它一定是在某处已经初始化过了,我们不用怀疑它,只需要找到他。

  其实它在 src/core/global-api/index.js文件中,initGlobalAPI方法中就定义了,并且他指的就是Vue对象。

  然后我们再回到 createComponent 方法这个主线任务中,继续往下打怪,我们会发现遇到一个函数是mergeHooks,

 1 function mergeHooks (data: VNodeData) {
 2   if (!data.hook) {
 3     data.hook = {}
 4   }
 5   for (let i = 0; i < hooksToMerge.length; i++) {
 6     const key = hooksToMerge[i]
 7     const fromParent = data.hook[key]
 8     const ours = componentVNodeHooks[key]
 9     data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours
10   }
11 }

  所谓hook,就是钩子,那再Vue中,这个钩子自然就是在代码中的某处可能会执行的方法,类似Vue实例的生命周期钩子一样。细看这个方法,它涉及到了一个对象,就是componentVNodeHooks对象,这个方法其实就是把这个对象里的init、prepath、insert、destory方法存进data.hook这个对象中罢了,那你回头要问,这个data又是从哪里来?一直追溯你会发现,这个是$createElement函数上的参数,咦?好像线索就断了= =?这个时候如果想要简单理解,只需要查找 Vue文档——深入data对象 你大概就知道这个data是神马了。

  而此处正定义了,最开头说的界面渲染的执行动作链条中的递归调用创建子节点的部分。但是大家可能会觉得,奇怪,这个函数最终是走到了$createElement,可是跟先前提到的那个动作链条似乎没有相关,就算定义了data.hook,让动作链条就有componentVNodeHooks.init() 这个方法,可是什么地方触发这个定义呢?最开始的动作链条似乎没有涉及定义这部分呀?没地方触发这些定义的方法呀?

  大家稍安勿躁,所以我说真的是很绕,不可能没定义,否则到执行data.hook.init的时候就undefind了。

  我们要回头看一下,在Vue进行初始化装配的时候,有执行这么一个方法 renderMixin(Vue) :

  

 1 export function renderMixin (Vue: Class<Component>) {
 2   // install runtime convenience helpers
 3   installRenderHelpers(Vue.prototype)
 4
 5   Vue.prototype.$nextTick = function (fn: Function) {
 6     return nextTick(fn, this)
 7   }
 8
 9   Vue.prototype._render = function (): VNode {
10     const vm: Component = this
11     const { render, _parentVnode } = vm.$options
12
13     // reset _rendered flag on slots for duplicate slot check
14     if (process.env.NODE_ENV !== ‘production‘) {
15       for (const key in vm.$slots) {
16         // $flow-disable-line
17         vm.$slots[key]._rendered = false
18       }
19     }
20
21     if (_parentVnode) {
22       vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject
23     }
24
25     // set parent vnode. this allows render functions to have access
26     // to the data on the placeholder node.
27     vm.$vnode = _parentVnode
28     // render self
29     let vnode
30     try {
31       vnode = render.call(vm._renderProxy, vm.$createElement)
32     } catch (e) {
33       handleError(e, vm, `render`)
34       // return error render result,
35       // or previous vnode to prevent render error causing blank component
36       /* istanbul ignore else */
37       if (process.env.NODE_ENV !== ‘production‘) {
38         if (vm.$options.renderError) {
39           try {
40             vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
41           } catch (e) {
42             handleError(e, vm, `renderError`)
43             vnode = vm._vnode
44           }
45         } else {
46           vnode = vm._vnode
47         }
48       } else {
49         vnode = vm._vnode
50       }
51     }
52     // return empty vnode in case the render function errored out
53     if (!(vnode instanceof VNode)) {
54       if (process.env.NODE_ENV !== ‘production‘ && Array.isArray(vnode)) {
55         warn(
56           ‘Multiple root nodes returned from render function. Render function ‘ +
57           ‘should return a single root node.‘,
58           vm
59         )
60       }
61       vnode = createEmptyVNode()
62     }
63     // set parent
64     vnode.parent = _parentVnode
65     return vnode
66   }
67 }

  可能有的人就看明白了,我们看看,我们平时写组件的时候,如果你有用到render的方式来写组件样式,那是如何工作的。在Vue.prototype._render这个方法体内,你会看到render从vm.$options中取出(vm.$options就是你写的Component内的那些data、props等等的属性),然后再看上面截出的代码的第31行,render.call(vm._renderProxy,vm.$createElement),然后返回一个vnode,所以说,$createElemment在此处就会被调用,然后进行上面说的那些乱七八糟的代码。但是你可能又会问:render.call?我平时写Component的时候从来没用render函数来做界面绘制呀!这个render又是在什么时候被定义在$options的呢?否则直接从$options中取出肯定是会报错的呀。还是我刚才那句话,不是没定义,只是没找到,实际上是定义了,定义在哪儿了?定义在mountComonent的最开始的部分了。

  然后你可能又会想,那按照代码的执行顺序,能确保在使用前就定义了吗?答案自然是肯定的。我们刚才看到$createElement这个方法,是被定义在vm._render当中,别忘了我们还有一个很重要的任务,就是找到$createElement是在哪里被执行的,那也就是说,vm._render()是在哪里被执行的。其实它就在mountComponent当中执行的,而且还一定是在render被定义之后才执行的。

  其实这段代码不是简单地从上至下执行那么容易理解,你可以看到updateComponent的写法,其实它只是被定义了,而且在定义的时候,vm._update实际上是没有执行的,并且vm._render()也是没有被执行的,他们实际上是到了下面new Watcher()的构造函数当中才被执行,同时我们也可以看到,整个定义和动作执行两个过程中,在watcher的构造函数里,执行updateComponent方法时,vm._render()一定先执行然后返回一个vnode,然后才是到了vm._update开始执行,也就是说,此时data.hook已经被装填了init等函数,所以在最开始的执行链不会因为属性尚未定义而报出undefind被打断。

  哈哈,真的很绕。说实在话,看了良久才看明白这绕来绕去的逻辑。

  另外,在我研读这份源码时,我才发现(额,我并木有什么偏见),src/platforms 包下,除了web,多了一个weex。然后我就又回过头理解了一圈,发现vue是把vm.$mount以及相关界面的模块整个都抽出来单独写,然后在不同的平台,就可以使用不同的渲染方式,然后我们在使用webpack打包时,只修要针对自己想要的平台打包对应的模块。如此将界面渲染层分开写,真的是增加了Vue的扩展性,整个工程就很好扩展和管理。拜服大神的设计。

原文地址:https://www.cnblogs.com/wuxinzhe/p/8496256.html

时间: 2024-12-13 23:30:48

Vue源码翻译之渲染逻辑链的相关文章

Vue源码中compiler部分逻辑梳理(内有彩蛋)

目录 一. 简述 二. 编译流程 三. 彩蛋环节 示例代码托管在:http://www.github.com/dashnowords/blogs 博客园地址:<大史住在大前端>原创博文目录 华为云社区地址:[你要的前端打怪升级指南] 一. 简述 compiler模块Vue框架中用于模板编译的,它的作用就是将Vue中的组件模板转换成render函数,render函数在运行时可以生成虚拟节点vnode,它是Vue中虚拟DOM树的基本实现流程.完整版的Vue是包含runtime和compiler的,

Vue源码探究-虚拟DOM的渲染

Vue源码探究-虚拟DOM的渲染 在虚拟节点的实现一篇中,除了知道了 VNode 类的实现之外,还简要地整理了一下DOM渲染的路径.在这一篇中,主要来分析一下两条路径的具体实现代码. 按照创建 Vue 实例后的一般执行流程,首先来看看实例初始化时对渲染模块的初始处理.这也是开始 mount 路径的前一步.初始包括两部分,一是向 Vue 类原型对象上挂载渲染相关的方法,而是初始化渲染相关的属性. 渲染的初始化 下面代码位于vue/src/core/instance/render.js 相关属性初始

从vue源码看Vue.set()和this.$set()

前言 最近死磕了一段时间vue源码,想想觉得还是要输出点东西,我们先来从Vue提供的Vue.set()和this.$set()这两个api看看它内部是怎么实现的. Vue.set()和this.$set()应用的场景 平时做项目的时候难免不会对数组或者对象进行这样的骚操作操作,结果发现,咦~~,他喵的,怎么页面没有重新渲染. const vueInstance = new Vue({ data: { arr: [1, 2], obj1: { a: 3 } } }); vueInstance.$d

vue源码之响应式数据

分析vue是如何实现数据响应的. 前记 现在回顾一下看数据响应的原因. 之前看了vuex和vue-i18n的源码, 他们都有自己内部的vm, 也就是vue实例. 使用的都是vue的响应式数据特性及$watchapi. 所以决定看一下vue的源码, 了解vue是如何实现响应式数据. 本文叙事方式为树藤摸瓜, 顺着看源码的逻辑走一遍, 查看的vue的版本为2.5.2. 目的 明确调查方向才能直至目标, 先说一下目标行为: vue中的数据改变, 视图层面就能获得到通知并进行渲染. $watchapi监

深入vue - 源码目录及构建过程分析

 公众号原文链接:深入vue - 源码目录及构建过程分析   喜欢本文可以扫描下方二维码关注我的公众号 「前端小苑」 ?“ 本文主要梳理一下vue代码的目录,以及vue代码构建流程,旨在对vue源码整体有一个认知,有助于后续对源码的阅读.” 一.目录结构 上图是对vue的代码的所有目录进行的梳理,其中源码位于src目录下,下面对src下的目录进行介绍. compiler 该目录是编译相关的代码,即将 template 模板转化成 render 函数的代码. vue 提供了 render 函数,r

[Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅

有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了.本文我们一起通过学习双向绑定原理来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsun

[Vue源码]一起来学Vue模板编译原理(一)-Template生成AST

本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascrip

[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串

本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vue模板编译原理(一)-Template生成AST 一起来学Vue模板编译原理(二)-AST生成Render字符串 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法 这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascri

Vue源码之响应式原理(个人向)

浅谈响应式原理 关于响应式原理,其实对于vue这样一个大项目来说肯定有很多细节和优化的地方,在下水平精力有限,不能一一尝试探索,本文仅以将响应式的大致流程个人向的梳理完毕为目的. 对于响应式主要分为三大部分来分析,1.响应式对象:2.依赖收集:3.派发更新. 最后将是个人的分析. 1.响应式对象 (Object.defineProperty) 我们先从初始化数据开始,再介绍几个比较核心的方法. 1.1.initState 文件位置:src/core/instance/state.js 在Vue的