Vue源码学习之Render

Vue源码学习之Render

根据前两章,讲述了Vue的依赖收集的工作原理以及Vue的数据响应式。今天这块砖我们先从initWatch开始敲起,最后会敲到initRender,从而开始讲解dom渲染。

initwatcher 初始化

为了了解watch监听函数是什么工作原理?我们先从initWatch的代码说起:

1
2
3
4
5
6
7
8
9
10
11
12
13
function initWatch (vm, watch) {
for (var key in watch) {
var handler = watch[key];
if (Array.isArray(handler)) {
for (var i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i]);
}
} else {
createWatcher(vm, key, handler);
}
}
}

这一段代码主要对watch对象的属性值进行区分判断,从而对每个属性加上一个Watcher来监听数据的变化,那我们看看它是怎样对每个属性创建Watcher?

1
2
3
4
5
6
7
8
9
10
11
function createWatcher (vm,expOrFn,handler,options) {
if (isPlainObject(handler)) {
options = handler;
handler = handler.handler;
}
if (typeof handler === 'string') {
handler = vm[handler];
}
return vm.$watch(expOrFn, handler, options)
}

函数createWatcher最后调用的事$watch函数去实现监听data中的属性值改变。那么$watch里面是如何工作的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.prototype.$watch = function (expOrFn,cb,options) {
var vm = this;
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {};
options.user = true;
var watcher = new Watcher(vm, expOrFn, cb, options);
if (options.immediate) {
try {
cb.call(vm, watcher.value);
} catch (error) {
handleError(error, vm, ("callback for immediate watcher \"" + (watcher.expression) + "\""));
}
}
return function unwatchFn () {
watcher.teardown();
}
};
}

根据其中的代码体现出来首先会创建一个Watcher,同时变量expOrFn是一个字符串,在创建Watcher中,有一段代码的作用给getter赋值,需要对变量expOrFn进行解析,返回一个函数。

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
  if (typeof expOrFn === 'function') {
this.getter = expOrFn;
} else {
this.getter = parsePath(expOrFn);
if (!this.getter) {
this.getter = noop;
}
}

/**
* Parse simple path.
*/
var bailRE = /[^\w.$]/;
function parsePath (path) {
if (bailRE.test(path)) {
return
}
var segments = path.split('.');
return function (obj) {
for (var i = 0; i < segments.length; i++) {
if (!obj) { return }
obj = obj[segments[i]];
}
return obj
}
}

从创建Watcher的代码中看出最后触发this.get函数,在get函数会将当前Watcher置为全局变量,同时会使调用getter函数,触发到data中的属性的getter函数,会将当前的Watcher也就是依赖最后添加到Dep.subs中。整个initWatch过程就是这样实现的。

render渲染

接下来还有,initRender是如何渲染的,当数据改变watcher是如何通知render函数去渲染的?首先会调用$mount函数去挂载dom元素。

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
31
32
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el,hydrating) {
el = el && query(el);

/* ... */

var options = this.$options;
// resolve template/el and convert to render function
if (!options.render) {
var template = options.template;
/* ... */
if (template) {
/* istanbul ignore if */
if (config.performance && mark) {
mark('compile');
}

var ref = compileToFunctions(template, {
shouldDecodeNewlines: shouldDecodeNewlines,
shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this);
var render = ref.render;
var staticRenderFns = ref.staticRenderFns;
options.render = render;
options.staticRenderFns = staticRenderFns;
/* ... */
}
}
return mount.call(this, el, hydrating)
};

initRender执行时假如不存在render函数,就会直接调用compileToFunctions将模板编译成render函数,然后再调用mount函数。执行compileToFunctions函数过程中,最后会触发到函数createFunction函数,从而生成render函数。

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
31
32
function createCompileToFunctionFn (compile) {
var cache = Object.create(null);

return function compileToFunctions (template,options,vm) {
options = extend({}, options);
var warn$$1 = options.warn || warn;
delete options.warn;

/* ... */

// check cache
var key = options.delimiters
? String(options.delimiters) + template
: template;
if (cache[key]) {
return cache[key]
}
/* ... */
// compile
var compiled = compile(template, options);
/* ... */
// turn code into functions
var res = {};
var fnGenErrors = [];
res.render = createFunction(compiled.render, fnGenErrors);
res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
return createFunction(code, fnGenErrors)
});
return (cache[key] = res)
}
}

经历一次次函数的包装和函数柯里化,最终我们看到进行模板编译,最终我们得到render函数,同时如果已经编译过,会优先读取缓存;说完编译过程,我们来看看挂载过程。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// public mount method
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
// 挂载模板
function mountComponent (vm,el,hydrating) {
vm.$el = el;
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode;
}
// 调用挂载前函数
callHook(vm, 'beforeMount');

var updateComponent;
/* istanbul ignore if */
if (config.performance && mark) {
updateComponent = function () {
var name = vm._name;
var id = vm._uid;
var startTag = "vue-perf-start:" + id;
var endTag = "vue-perf-end:" + id;

mark(startTag);
var vnode = vm._render();
mark(endTag);
measure(("vue " + name + " render"), startTag, endTag);

mark(startTag);
vm._update(vnode, hydrating);
mark(endTag);
measure(("vue " + name + " patch"), startTag, endTag);
};
} else {
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
}

// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);
hydrating = false;

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true;
// 调用dom挂载完之后函数
callHook(vm, 'mounted');
}
return vm
}

对上面代码排除一些if判断和警告,剩下主要的代码就以下几行:

1
2
3
4
5
6
7
8
9
10
11
12
13
......
updateComponent = function () {
vm._update(vm._render(), hydrating);
};
......
new Watcher(vm, updateComponent, noop, {
before: function before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate');
}
}
}, true /* isRenderWatcher */);

代码中定义了函数updateComponent,在这个函数中调用vm._updatevm._render函数,同时挂载后还会new Watcher(),这个依赖主要是用于更新视图,最后在当前Watcher中的getter触发会执行vm._update函数去更新视图的值或元素,vm._update函数中将vm._render函数传递进去作为Wacther的getter函数。让我们先看看函数vm._render做了啥事?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
var _parentVnode = ref._parentVnode;

if (_parentVnode) {
vm.$scopedSlots = _parentVnode.data.scopedSlots || emptyObject;
}

// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode;
// render self

var vnode= render.call(vm._renderProxy, vm.$createElement);
/* .... */
// set parent
vnode.parent = _parentVnode;
return vnode
};
}

vm._render调用之前编译好的render函数生成vnode节点挂载到vm._vnode,也就是虚拟Dom。在执行render的时候,倘若编译成VNode中含有数据绑定会触发data的getter操作进行收集依赖,会将当前的Watcher添加到闭包中定义的dep的subs中。vm._renderProxy其实就是代理模式来触发data的getter函数。我们先来看看使用_update函数怎么更改数据的改变。

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
Vue.prototype._update = function (vnode, hydrating) {
var vm = this;
var prevEl = vm.$el;
var prevVnode = vm._vnode;
var restoreActiveInstance = setActiveInstance(vm);
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);
}
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._update函数用于首次渲染或者数据变化更新视图调用的方法。它内部其实调用的方法是vm.__patch__函数。它内部的代码逻辑是:在首次渲染时,会直接将VNode转化为真实的DOM节点,插入到DOM中,触发浏览器渲染,mount操作结束,调用callHook(vm, ‘mounted’);而如果是更新操作,则会通过diff算法计算出两个VNode节点的差异,然后做视图更新。

总结

从前面两章到现在这章,Vue的初始化到浏览器渲染出的视图就基本结束,同时我们可以用官网的生命周期图来回顾整个Vue的整个过程。

Vue生命周期图

这里我们只是大概的讲述了render函数以及触发更新视图原理,里面VNode的生成和在更新视图触发diff算法,进行VNode差异更新是如何的,接下来的一章会详细讲解。


本文由 Abert 创作,采用 知识共享署名 4.0 国际许可协议。

本站文章除注明转载/出处外,均为本站原创或翻译,转载请务必署名。