update
-
回到之间 $mount时,mountComponent 函数的过程,vm._render 是如何创建了一个 VNode
-
接下来就是要把这个 VNode 渲染成一个真实的 DOM 并渲染出来,这个过程是通过 vm._update 完成的
-
_update 它是一个vue 的私有方法,它把我们的 vnode 渲染成真实的 dom
-
_update的方法,也是原型上的一个方法, 它的定义在 src/core/instance/lifecycle.js 中
jsexport function lifecycleMixin (Vue: Class<Component>) { Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { // 首先它定义一些变量,这些变量实际上都是给之后数据更新时候用的 // 因为update它的调用实际有两个,一个是我们首次渲染的时候,它会调用update把我们的vnode映射是真实的dom // 还有一个就是当我们去改变数据的时候,数据改变也会驱动视图的变化,它同样也会调update方法 // 可以初步认为它们都是空 const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode const restoreActiveInstance = setActiveInstance(vm) vm._vnode = vnode // Vue.prototype.__patch__ is injected in entry points // based on the rendering backend used. // 这个 prevVnode 也是空,它会走到这个逻辑,就是 initial render,否则会走到 updates if (!prevVnode) { // initial render vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // updates vm.$el = vm.__patch__(prevVnode, vnode) } restoreActiveInstance() // update __vue__ reference if (prevEl) { prevEl.__vue__ = null } if (vm.$el) { vm.$el.__vue__ = vm } // if parent is an HOC, update its $el as well if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) { vm.$parent.$el = vm.$el } // updated hook is called by the scheduler to ensure that children are // updated in a parent's updated hook. } // ... }
-
这里
vm.__patch__
定义在 src/platforms/web/runtime/index.js 中jsVue.prototype.__patch__ = inBrowser ? patch : noop
-
这里浏览器环境,才会有 patch, 这里的 inBrowser
js// src/core/util/env.js export const inBrowser = typeof window !== 'undefined'
-
vue它是可以跑在服务端的,在服务端的 patch 是一个空函数,因为在服务端不会有dom
-
-
进入
patch
, 定义在 src/platforms/web/runtime/patch.jsjsimport * 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) // 这个 patch 方法,调用了这个 createPatchFunction, 返回来是一个函数,特别像高阶函数这样的概念 // nodeOps 是操作dom的集合方法;而 modules 是 platformModules 和 baseModules,这里定义属性,类和钩子函数等 // 这里才是真正的 patch 函数,vue饶了一个大圈子,为什么会定义在这里 // 他利用函数柯里化的技巧,一开始传入 nodeOps, modules 都是和平台相关的 // vue 是跨平台的,可以在node上跑,可以在浏览器上跑,可以在 weex 上 // 在web上真实操作dom的api 和 weex 上是不同的,虽然最终可以生成同样的 vnode // 这块是分离的, 所以 dom操作的api通过参数来传入 // 另外就是 models 模块,它的一些生命周期,在不同的生命周期要做的事情,然后怎么去生成这些模块和我们dom做配合, 这些东西,跟平台也是相关的 // 这里有 baseModules 和 platformModules // 通过函数柯里化,可基于不同参数来干不同的事情,把写代码的差异去搞定 // 等到真实执行 patch 的时候,就不用考虑平台的区别了 // 因为我们是把平台差异化的东西都已经在之前抹平了 // 所以这样的话,就可以每次调用这个 patch 的时候,完全不用关心真实操作是什么 // 因为在之前已经把这个参数提前传入了,相当于通过这种闭包的技巧,实现了对 nodeOps 和 modules 的持有 // 接下来真实调每次的方法的时候,都不用再去传这些差异化的参数,这就是一个非常好的一个js的编程技巧 export const patch: Function = createPatchFunction({ nodeOps, modules })
-
进入
createPatchFunction
, 定义在 src/core/vdom/patch.jsjs// 在patch阶段中,会执行各个钩子 const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] export function createPatchFunction (backend) { let i, j const cbs = {} // 拿到 这两项 const { modules, nodeOps } = backend // 将 hooks 遍历到 cbs 对象中保存 for (i = 0; i < hooks.length; ++i) { cbs[hooks[i]] = [] // 遍历所有模块,拿到所有模块的钩子 for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { cbs[hooks[i]].push(modules[j][hooks[i]]) } } } // ... 这里定义了非常多的辅助函数 // 这个patch实际上是传入了四个参数 // 关注前两个,oldVnode 是 真实的el, vnode 就是vdom, 后面两个都是 false return function patch (oldVnode, vnode, hydrating, removeOnly) { // 销毁时 return 跳过 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 这个 if 一般不会走到,跳过 if (isUndef(oldVnode)) { // empty mount (likely as component), create new root element isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // 这里,判断是否是真实的dom,这里是 true const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // patch existing root node patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly) } else { // 这里执行 if (isRealElement) { // mounting to a real element // check if this is server-rendered content and if we can perform // a successful hydration. // 这里不是 ssr, 这里走不到 if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } // 这里浏览器dom环境下,也走不到 if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } else if (process.env.NODE_ENV !== 'production') { warn( 'The client-side rendered virtual DOM tree is not matching ' + 'server-rendered content. This is likely caused by incorrect ' + 'HTML markup, for example nesting block-level elements inside ' + '<p>, or missing <tbody>. Bailing hydration and performing ' + 'full client-side render.' ) } } // either not server-rendered, or hydration failed. // create an empty node and replace it oldVnode = emptyNodeAt(oldVnode) // 这个方法就是把真实的dom转化为 vnode } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 获取父节点 // 这个函数很重要,把vnode挂载到真实dom上 // create new node createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // update parent placeholder node element, recursively if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } // #6513 // invoke insert hooks that may have been merged by create hooks. // e.g. for directives that uses the "inserted" hook. const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // destroy old node if (isDef(parentElm)) { removeVnodes(parentElm, [oldVnode], 0, 0) // 删除旧的节点 } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } }
-
进入
createElm
jsfunction createElm ( vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index ) { // 这个一版不会走到,跳过 if (isDef(vnode.elm) && isDef(ownerArray)) { // This vnode was used in a previous render! // now it's used as a new node, overwriting its elm would cause // potential patch errors down the road when it's used as an insertion // reference node. Instead, we clone the node on-demand before creating // associated DOM element for it. vnode = ownerArray[index] = cloneVNode(vnode) } vnode.isRootInsert = !nested // for transition enter check if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) { return } const data = vnode.data const children = vnode.children const tag = vnode.tag if (isDef(tag)) { if (process.env.NODE_ENV !== 'production') { if (data && data.pre) { creatingElmInVPre++ } // 这种错误是没有注册组件,却使用组件 if (isUnknownElement(vnode, creatingElmInVPre)) { warn( 'Unknown custom element: <' + tag + '> - did you ' + 'register the component correctly? For recursive components, ' + 'make sure to provide the "name" option.', vnode.context ) } } // 这里生成原生dom节点 vnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode) setScope(vnode) // 跳过 /* istanbul ignore if */ if (__WEEX__) { // ... } else { // 创建子节点 createChildren(vnode, children, insertedVnodeQueue) if (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } insert(parentElm, vnode.elm, refElm) } if (process.env.NODE_ENV !== 'production' && data && data.pre) { creatingElmInVPre-- } } else if (isTrue(vnode.isComment)) { vnode.elm = nodeOps.createComment(vnode.text) insert(parentElm, vnode.elm, refElm) } else { vnode.elm = nodeOps.createTextNode(vnode.text) insert(parentElm, vnode.elm, refElm) } }
-
createElm 的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中
-
这里的 createComponent 方法目的是尝试创建子组件,在当前这个 case 下它的返回值为 false
-
接下来判断 vnode 是否包含 tag,如果包含,先简单对 tag 的合法性在非生产环境下做校验,看是否是一个合法标签
-
然后再去调用平台 DOM 的操作去创建一个占位符元素
jsvnode.elm = vnode.ns ? nodeOps.createElementNS(vnode.ns, tag) : nodeOps.createElement(tag, vnode)
-
进入
createChildren
js// createChildren(vnode, children, insertedVnodeQueue) function createChildren (vnode, children, insertedVnodeQueue) { if (Array.isArray(children)) { // 在 dev 环境对节点 key 做校验 if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(children) } for (let i = 0; i < children.length; ++i) { createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i) } } else if (isPrimitive(vnode.text)) { nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text))) } }
- 如果是array, 遍历children 递归调用createElm
- 其实 createChildren 很简单,如果 这个 vnode 还有子节点
- 就递归调用子节点的 createElm, 一层层去处理
-
接着再调用 invokeCreateHooks 方法执行所有的 create 的钩子并把 vnode push 到 insertedVnodeQueue 中
jsif (isDef(data)) { invokeCreateHooks(vnode, insertedVnodeQueue) } function invokeCreateHooks (vnode, insertedVnodeQueue) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, vnode) } i = vnode.data.hook // Reuse variable if (isDef(i)) { if (isDef(i.create)) i.create(emptyNode, vnode) if (isDef(i.insert)) insertedVnodeQueue.push(vnode) } }
-
最终会调用 insert 操作, 定义在 src/core/vdom/patch.js
-
insert 方法把 DOM 插入到父节点中,因为是递归调用
-
子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父
jsinsert(parentElm, vnode.elm, refElm) function insert (parent, elm, ref) { if (isDef(parent)) { if (isDef(ref)) { if (ref.parentNode === parent) { nodeOps.insertBefore(parent, elm, ref) } } else { nodeOps.appendChild(parent, elm) } } } // src/platforms/web/runtime/node-ops.js export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } export function appendChild (node: Node, child: Node) { node.appendChild(child) }
- 其实就是调用原生 DOM 的 API 进行 DOM 操作
-
-
-
-
-
以上,我们可知,update 操作就是 patch 操作,里面的细节其实是非常多的
-
这里只是梳理整体的调用流程
-
从 new Vue 到渲染dom到页面的大致过程如下
-
new Vue -> init -> $mount -> compile(可选,存在编译版本) -> render -> vnode -> patch -> dom