Vue源码解析10-patch函数入口

前面简单介绍了compile的最后一步:生成render函数,还剩下很多分支细节没说,主要是各种内置指令以及自定义组件和slot,这些结合之后的patch过程说效果会好一些。

这篇文章主要是介绍patch函数的入口,把patch过程当做黑盒子,从整体的角度讲patch的作用。

patch 入口

在生成render函数后,下一步就是调用位于src/core/instance/lifecycle.jsmountComponent函数,其中一个关键步骤是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
updateComponent = () => {
vm._update(vm._render(), hydrating);
};

new Watcher(
vm,
updateComponent,
noop,
{
before() {
if (vm._isMounted) {
callHook(vm, 'beforeUpdate');
}
},
},
true /* isRenderWatcher */,
);

mount过程中通过将updateComponent传给Watcher实例,可以在watcher对应的Deps发生改变时重新执行updateComponent

updateComponent又分为两个子步骤:_render_update。前者用于执行render函数生成虚拟dom,后者用于比较新旧vnode。具体看一下_update的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.prototype._update = function(vnode: VNode, hydrating?: boolean) {
const vm: Component = this;
const prevVnode = vm._vnode;
vm._vnode = vnode;
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode);
}
// ...
};

Vue初始化第一次调用_update时,prevVnodeundefined,这样__patch__实际上做的事情就是将vnode转成真实的 dom 绘制到页面上;之后再次调用_update时新旧vnode都会存在,此时会执行diff算法,以最小粒度更新 dom。

Vue.prototype.$destroy中也会调用__patch__:

1
2
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null);

传入的第二个参数为null,此时会删除所有vnode

那么__patch__在何处定义的呢?答案是在src/platforms/web/runtime/index.js中:

1
2
// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop;

只在浏览器环境下才有patch操作,patch函数的定义:

1
2
3
4
5
// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules);

export const patch: Function = createPatchFunction({ nodeOps, modules });

涉及到几个配置项,挨个说下:

  1. nodeOps:封装了许许多多对原生 dom 操作的方法,都比较简单,例如

    1
    2
    3
    export function createComment(text: string): Comment {
    return document.createComment(text);
    }
  2. platformModules: 平台相关的一些属性的处理,包括attrsclassdomPropsonstyleshow。代码位于src/platforms/web/runtime/modules/index.js,每个子module都会包含createupdate两个钩子。

  3. baseModules:是webweex都有的处理,包括directivesref属性的处理。代码位于src/core/vdom/modules/index.js,每个子module同样会包含createupdate两个钩子。

最后我们的createPatchFunction就是真正的patch函数定义之处了,代码非常的长,这里先只展示最重要的轮廓:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const hooks = ['create', 'activate', 'update', 'remove', 'destroy'];

export function createPatchFunction(backend) {
let i, j;
const cbs = {};

const { modules, nodeOps } = backend;

/**
* 将每个modules中每个子moudle定义的各种钩子统一放到cbs中,最后cbs的结构示范
* {
* create: [module1Create, module2Create],
* update: [module1Update, module2Update],
* }
*/
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]]);
}
}
}

// 很多的帮助函数...

return function patch(oldVnode, vnode, hydrating, removeOnly) {
// ...
};
}

到这里我们就从整体上了解了patch的入口在哪以及它的作用,会在接下来的文章里详细描述patch的具体代码。