目标
深入vm._update和vm._render
vm._render取render函数渲染成虚拟dom
- src/core/instance/render.js
上面最关键的是 render ⽅法的调⽤,我们在平时的开发⼯作中⼿写 render ⽅法的场景⽐较少,⽽写的⽐较多的是 template 模板,在之前的 mounted ⽅法的实现中,会把 template 编译成 render ⽅法,然后我们获取到这个render方法然后进行调用。
- render函数写法
在 Vue 的官⽅⽂档中介绍了 render 函数的第⼀个参数是 createElement
- 再回到 _render 函数中的 render ⽅法的调⽤:
vnode = render.call(vm._renderProxy, vm.$createElement)
可以看到, render 函数中的 createElement ⽅法就是 vm.$createElement ⽅法。
- 总结
vm._render 最终是通过执⾏ createElement ⽅法并返回的是 vnode ,它是⼀个虚拟 Node。Vue2.0 相⽐ Vue 1.0 最⼤的升级就是利⽤了 Virtual DOM。因此在分析 createElement 的实现前,我们先了解⼀下 虚拟DOM 的概念。
虚拟Dom
- 产生背景
浏览器中的 DOM 是很贵的,DOM 元素是⾮常巨大,因为浏览器的标准就把 DOM 设计的⾮常复杂。当我们频繁的去做 DOM 更新,会产⽣⼀定的性能问题。
⽽虚拟DOM 就是⽤⼀个原⽣的 JS 对象去描述⼀个 DOM 节点,所以它⽐创建⼀个 DOM 的代价要⼩很多。
- src/core/vdom/vnode.js
- VNode它的核⼼定义⽆⾮就⼏个关键属性,标签名、数据、⼦节点、键值等,其它属性都是⽤来扩展 VNode 的灵活性。
- 由于 VNode只是⽤来映射到真实 DOM 的渲染,不需要包含操作 DOM 的⽅法,因此它是⾮常轻量和简单的。
- 你可以想象虚拟dom就是对象嵌套对象每个对象描述节点和属性等内容。
- Virtual DOM 除了它的数据结构的定义,映射到真实的 DOM 实际上要经历 VNode 的 create、diff、patch 等过程。
- VNode 的创建是通过之前提到的 createElement ⽅法创建的,我们接下来分析这部分的实现。
createElement
- src/core/vdom/create-elemenet.js
- 真正创建 VNode 的函数_createElement
- 首先对 tag 做判断,如果是 string 类型,则接着判断如果是内置的节点,则直接创建⼀个普通 VNode。
- 如果是已注册的组件名,则通过 createComponent 创建⼀个组件类型的 VNode。
- 否则创建⼀个未知的标签的 VNode。
- 如果是 tag ⼀个 Component 类型,则直接调⽤createComponent 创建⼀个组件类型的 VNode 节点。下篇我们再谈组件的操作。
- 总结
- 那么⾄此,我们⼤致了解了 createElement 创建 VNode 的过程,每个 VNode 有children , children 每个元素也是⼀个 VNode,这样就形成了⼀个 VNode Tree,它很好的描述了我们的 DOM Tree。
- 回到 mountComponent 函数的过程,我们已经知道 vm._render 是如何创建了⼀个 VNode,接下来就是要把这个 VNode 渲染成⼀个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的,接下来分析⼀下这个过程。
vm._update 把VNode渲染成⼀个真实的DOM并渲染
- src/core/instance/lifecycle.js
它被调⽤的时机有 2 个,⼀个是⾸次渲染,⼀个是数据更新的时候;由于我们现在只分析⾸次渲染部分,数据更新部分会在后面说响应式原理的时候涉及。
_update 的核⼼就是调⽤ vm.__patch__ ⽅法,
- src/platforms/web/runtime/index.js
上面可以看到会先判断是否浏览器环境,因为在服务端渲染中,没有真实的浏览器 DOM 环境,所以不需要把 VNode 最终转换成 DOM,因此是⼀个空函数,⽽在浏览器端渲染中,它指向了 patch ⽅法。
- src/platforms/web/runtime/patch.js
patch是调⽤ createPatchFunction ⽅法的返回值,这⾥传⼊了⼀个对象,包含 nodeOps参数和 modules 参数。其中, nodeOps 封装了⼀系列 DOM 操作的⽅法, modules 定义了⼀些模块的钩⼦函数的实现。
- src/core/vdom/patch.js
- createPatchFunction 里定义了⼀堆辅助⽅法,最终返回了⼀个 patch ⽅法,这个⽅法就赋值给了vm._update 函数⾥调⽤的 vm.patch 。
- 这⾥⽤到了⼀个函数柯⾥化的技巧,通过createPatchFunction 把差异化参数提前固化,这样不⽤每次调⽤ patch 的时候都传递nodeOps 和 modules 了,然后不同平台就拿到了不同的path函数。
- 由于我们传⼊的 oldVnode 实际上是⼀个真实元素,所以 isRealElement 为 true,接下来⼜通过 emptyNodeAt ⽅法把 oldVnode 转换成 VNode 对象,调⽤ createElm ⽅法传入父节点body参数。
- createElm
createElm 的作⽤是通过虚拟节点创建真实的 DOM 并插⼊到它的⽗节点中。
createComponent ⽅法⽬的是尝试创建⼦组件,这个后面我们说组件化再说,然后接下来判断 vnode 是否包含 tag,看tag是否是⼀个合法标签;然后再去调⽤DOM 的操作去创建⼀个占位符元素elm,这个elm的作用是后续创建子元素插入父元素的标识。
接下来调⽤ createChildren ⽅法去创建⼦元素,递归调⽤ createElm ,这是⼀种常见的深度优先的遍历算法。
- 接着再调⽤ invokeCreateHooks ⽅法执⾏所有的 create 的钩⼦并把 vnode push 到insertedVnodeQueue 中。
- 因为是递归调⽤,⼦元素会优先调⽤ insert插入上级节点 ,所以整个 vnode 树节点的插⼊顺序是先⼦后⽗,最后 insert ⽅法把整个DOM 插⼊到一开始createElm ⽅法传入父节点body中。至此整颗dom就挂载在页面上啦。
- 最后删除el旧节点
6.总结
实际上整个过程就是递归创建了⼀个完整的 DOM 树并插⼊到 Body 中。
总结
下篇我们谈谈组件化