前言
当组件进行更新时,new Watcher(vm, updateComponent, ...)
中的 updateComponent
方法会被执行,而更新时的 updateComponent
方法如下:
// 负责更新组件 updateComponent = () => { // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode vm._update(vm._render(), hydrating) } 复制代码
首先会执行 vm._render()
得到组件的 VNode
,接着将 VNode
传递给 vm._update
方法,就正式开始 patch
阶段.
vue1.x 到 vue2.x 的转变
vue1.x
1.x
中并没有 2.x
中对应的 VNode
和 diff
的概念,因为 1.x
的核心是 响应式,即 Object.defineProperty
、Dep
、Watcher
.
Object.defineProperty
: 负责数据的拦截(本质就是对象属性的劫持),对数据属性key
设置getter
和setter
,在getter
触发时进行依赖收集,在setter
触发时通过dep
通知对应的watcher
进行更新Dep
:每个dep
实例和data
选项中返回对象的key
是 一对一 的关系Watcher
:data
选项中返回对象的key
和watcher
是 一对多 的关系,模版中每使用一次key
就会生成一个对应的watcher
由于 watcher
和 DOM
属于 一对一 的关系,当数据发生更新时,dep
会通知 watcher
直接更新对应的 DOM
,即 定向更新; 所以更新的效率是非常高的,因为 watcher
可以明确知道与它对应的 key
在组件模版中的具体位置(即对应的 dom
元素).
但是这种高效的更新并不适用于复杂场景,因为当页面足够复杂时(即包含很多组件),对应的页面会就产生大量的 watcher
与真实 dom
进行强关联,这是非常耗资源.
vue2.x
由于
vue1.x
存在的问题,于是Vue 2.0
就引入了VNode
和diff
算法解决问题.
针对复杂页面 watcher
太多导致性能下降的问题,Vue 2.0
中将 watcher
的粒度放大,即一个组件对应一个 watcher
(渲染 watcher
),这样 watcher
的维度就属于是组件级别,而不是单个 DOM
级别.
当响应式数据更新时,dep
通知 watcher
去更新组件内容,这对于 vue1.x
来说是很简单的,但是 vue2.0
中的 watcher
是组件级别,因此这个 watcher
并不知道要更新模板中的哪些位置.
于是 vue2.0
中通过引入 VNode
来查找本次组件需要更新的内容
当组件中数据更新时,会通过调用 render
方法为组件生成一个新的 VNode
,将新的 VNode
与 旧的 VNode
通过 diff
算法进行比较,查找本次需要更新的内容,接着执行 DOM
操作去更新对应节点.
深入源码
入口
文件位置:src\core\instance\lifecycle.js
export function mountComponent ( vm: Component, el: ?Element, hydrating?: boolean ): Component { ... callHook(vm, 'beforeMount') let updateComponent /* istanbul ignore if */ if (process.env.NODE_ENV !== 'production' && config.performance && mark) { updateComponent = () => { ... } } else { // 负责更新组件 updateComponent = () => { // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode vm._update(vm._render(), hydrating) } } ... if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted') } return vm } 复制代码
vm._update()
文件位置:src\core\instance\lifecycle.js
// 组件初始化渲染和更新时的入口 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { 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 不存在,代表是初始化渲染 if (!prevVnode) { // patch 阶段:patch、diff 算法 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // prevVnode 存在,代表是后续更新阶段 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
在 Vue
原型上挂载 __patch__
方法
// install platform patch function Vue.prototype.__patch__ = inBrowser ? patch : noop 复制代码
patch()
文件位置:src\platforms\web\runtime\patch.js
通过 createPatchFunction()
工厂函数,为其传入平台特有的一些操作,然后返回一个 patch
函数
export const patch: Function = createPatchFunction({ nodeOps, modules }) 复制代码
nodeOps
文件位置:src\platforms\web\runtime\node-ops.js
/** * web 平台的 DOM 操作 API */ // 创建标签名为 tagName 的元素节点 export function createElement (tagName: string, vnode: VNode): Element { // 创建元素 const elm = document.createElement(tagName) // 非 select 元素直接返回 if (tagName !== 'select') { return elm } // false or null will remove the attribute but undefined will not // 如果是 select 元素,则为它设置 multiple 属性 if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) { elm.setAttribute('multiple', 'multiple') } return elm } // 创建带命名空间的元素节点 export function createElementNS (namespace: string, tagName: string): Element { return document.createElementNS(namespaceMap[namespace], tagName) } // 创建文本节点 export function createTextNode (text: string): Text { return document.createTextNode(text) } // 创建注释节点 export function createComment (text: string): Comment { return document.createComment(text) } // 在指定节点前插入节点 export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { parentNode.insertBefore(newNode, referenceNode) } // 移除子节点 export function removeChild (node: Node, child: Node) { node.removeChild(child) } // 添加子节点 export function appendChild (node: Node, child: Node) { node.appendChild(child) } // 返回指定节点的父节点 export function parentNode (node: Node): ?Node { return node.parentNode } // 返回指定节点的下一个兄弟节点 export function nextSibling (node: Node): ?Node { return node.nextSibling } // 返回指定节点的标签名 export function tagName (node: Element): string { return node.tagName } // 为指定节点设置文本 export function setTextContent (node: Node, text: string) { node.textContent = text } // 为节点设置指定的 scopeId 属性,属性值为 '',如 <div scopeId></div> export function setStyleScope (node: Element, scopeId: string) { node.setAttribute(scopeId, '') } 复制代码
modules
文件位置:src\core/vdom/modules/index.js
+ web/runtime/modules/index.js
平台特有的一些操作,如:attr、class、style、event
等,还有核心的 directive
和 ref
,它们会向外暴露一些特有的方法,比如:create、activate、update、remove、destroy
,这些方法在 patch
阶段时会被调用,从而做相应的操作,比如创建 attr
、指令等.
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) 复制代码
createPatchFunction()
文件位置:src\core\vdom\patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'] /* 工厂函数: 注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数 */ export function createPatchFunction (backend) { let i, j const cbs = {} /* modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 } nodeOps: { 对元素的增删改查 API } */ const { modules, nodeOps } = backend /* hooks = ['create', 'activate', 'update', 'remove', 'destroy'] 遍历这些钩子,然后从 modules 的各个模块中找到相应的方法, 比如:directives 中的 create、update、destroy 方法 让这些方法放到 cbs[hook] = [hook 方法] 中,比如: cb.create = [fn1, fn2, ...] 然后在合适的时间调用相应的钩子方法完成对应的操作 */ for (i = 0; i < hooks.length; ++i) { // 比如 cbs.create = [] cbs[hooks[i]] = [] for (j = 0; j < modules.length; ++j) { if (isDef(modules[j][hooks[i]])) { // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中 cbs[hooks[i]].push(modules[j][hooks[i]]) } } } ... /* vm.__patch__ 1、新节点不存在,老节点存在,调用 destroy,销毁老节点 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode */ return function patch (oldVnode, vnode, hydrating, removeOnly){ ... } } 复制代码
createPatchFunction 中返回的 patch()
/* vm.__patch__ 1、新节点不存在,老节点存在,调用 destroy,销毁老节点 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode */ function patch(oldVnode, vnode, hydrating, removeOnly) { // 新节点不存在,老节点存在,则调用 destroy,销毁老节点 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 老的 VNode 不存在 if (isUndef(oldVnode)) { /* 老的 VNode 不存在,新的 VNode 存在,这种情况会在一个组件初次渲染的时候出现, 比如:<div id="app"> <comp></comp> </div> 这里的 comp 组件初次渲染时就会走这儿 empty mount (likely as component), create new root element */ isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { // 老的 VNode 存在 // 判断老的 VNode 是否是一个真实的 dom 节点 const isRealElement = isDef(oldVnode.nodeType) /* 老的 VNode 不是真实元素,但是老节点和新节点是同一个节点, 则属于更新阶段,需要执行 patch 更新节点 patch existing root node */ if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { // 老的 VNode 是真实元素 if (isRealElement) { /* 挂载到真实元素以及处理服务端渲染的情况 oldVnode.nodeType === 1 代表的是 html 元素 */ if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } 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.' ) } } /* 走到这儿说明不是服务端渲染,或者 hydration 失败, 则根据 oldVnode 创建一个 vnode 节点替换 oldVnode */ oldVnode = emptyNodeAt(oldVnode) } // replacing existing element // 获取老节点的真实元素 const oldElm = oldVnode.elm // 获取老节点的父元素,即 body 元素 const parentElm = nodeOps.parentNode(oldElm) // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下 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 } } // 移除老节点 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm } 复制代码