当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
前面我们说到,当组件更新时,实例化渲染 watcher 时传递的 updateComponent
方法会被执行:
const updateComponent = () => {
// 执行 vm._render() 函数,得到 虚拟 VNode,并将 VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
vm._update(vm._render(), hydrating)
}
首先会先执行 vm._render() 函数,得到组件的 VNode,并将 VNode 传递给 vm._update 方法,接下来就该进入到 patch 阶段了。今天我们就来深入理解组件更新时 patch 的执行过程。
1.x 版本的 Vue 没有 VNode 和 diff 算法,那个版本的 Vue 的核心只有响应式原理:Object.defineProperty
、Dep
、Watcher
。
Object.defineProperty
: 负责数据的拦截。getter 时进行依赖收集,setter 时让 dep 通知 watcher 去更新
Dep
:Vue data 选项返回的对象,对象的 key 和 dep 一一对应
Watcher
:key 和 watcher 时一对多的关系,组件模版中每使用一次 key 就会生成一个 watcher
<template>
<div class="wrapper">
<div class="msg1">{
{ msg }}div>
<div class="msg2">{
{ msg }}div>
div>
template>
<script>
export default {
data() {
return {
// 和 dep 一一对应,和 watcher 一 对 多
msg: 'Hello Vue 1.0'
}
}
}
script>
当数据更新时,dep 通知 watcher 去直接更新 DOM,因为这个版本的 watcher 和 DOM 时一一对应关系,watcher 可以非常明确的知道这个 key 在组件模版中的位置,因此可以做到定向更新,所以它的更新效率是非常高的。
虽然更新效率高,但随之也产生了严重的问题,无法完成一个企业级应用,理由很简单:当你的页面足够复杂时,会包含很多的组件,在这种架构下就意味这一个页面会产生大量的 watcher,这非常耗资源。
这时就在 Vue 2.0 中通过引入 VNode 和 diff 算法去解决 1.x 中的问题。将 watcher 的粒度放大,变成一个组件一个 watcher(就是我们说的渲染 watcher),这时候你页面再大,watcher 也很少,这就解决了复杂页面 watcher 太多导致性能下降的问题。
当响应式数据更新时,dep 通知 watcher 去更新,这时候问题就来了,Vue 1.x 中 watcher 和 key 一一对应,可以明确知道去更新什么地方,但是 Vue 2.0 中 watcher 对应的是一整个组件,更新的数据在组件的的什么位置,watcher 并不知道。这时候就需要 VNode 出来解决问题。
通过引入 VNode,当组件中数据更新时,会为组件生成一个新的 VNode,通过比对新老两个 VNode,找出不一样的地方,然后执行 DOM 操作更新发生变化的节点,这个过程就是大家熟知的 diff。
以上就是 Vue 2.0 为什么会引入 VNode 和 diff 算法的历史原因了,也是 Vue 1.x 到 2.x 的一个发展历程。
/src/core/instance/lifecycle.js
const updateComponent = () => {
// 执行 vm._render() 函数,得到 VNode,并将 VNode 传递给 _update 方法,接下来就该到 patch 阶段了
vm._update(vm._render(), hydrating)
}
/src/core/instance/lifecycle.js
/**
* 页面首次渲染和后续更新的入口位置,也是 patch 的入口位置
*/
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
// 页面的挂载点,真实的元素
const prevEl = vm.$el
// 老 VNode
const prevVnode = vm._vnode
const restoreActiveInstance = setActiveInstance(vm)
// 新 VNode
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// 老 VNode 不存在,表示首次渲染,即初始化页面时走这里
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// 响应式数据更新时,即更新页面时走这里
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.
}
/src/platforms/web/runtime/index.js
/ 在 Vue 原型链上安装 web 平台的 patch 函数
Vue.prototype.__patch__ = inBrowser ? patch : noop
/src/platforms/web/runtime/patch.js
// patch 工厂函数,为其传入平台特有的一些操作,然后返回一个 patch 函数
export const patch: Function = createPatchFunction({
nodeOps, modules })
src/platforms/web/runtime/node-ops.js
/**
* web 平台的 DOM 操作 API
*/
/**
* 创建标签名为 tagName 的元素节点
*/
export function createElement (tagName: string, vnode: VNode): Element {
// 创建元素节点
const elm = document.createElement(tagName)
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 属性,属性值为 ''
*/
export function setStyleScope (node: Element, scopeId: string) {
node.setAttribute(scopeId, '')
}
/src/platforms/web/runtime/modules 和 /src/core/vdom/modules
平台特有的一些操作,比如:attr、class、style、event 等,还有核心的 directive 和 ref,它们会向外暴露一些特有的方法,比如:create、activate、update、remove、destroy,这些方法在 patch 阶段时会被调用,从而做相应的操作,比如 创建 attr、指令等。这部分内容太多了,这里就不一一列举了,在阅读 patch 的过程中如有需要可回头深入阅读,比如操作节点的属性的时候,就去读 attr 相关的代码。
提示:由于该函数的代码量较大, 所以调整了一下代码结构,方便阅读和理解
/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', 'activa