前言
上一篇中我们主要讲的Vue2.0
的初始化渲染,它主要主要使用过模板编译生成render
函数,创建渲染Watcher
来进行初次渲染。在渲染的过程中响应式数据会对当前渲染Watcher
进行依赖收集。渲染主要通过update
函数来判断是否时初次渲染,初次渲染直接将vnode
转为真实dom
插入到目标位置。本文就是讲的在update
时走更新渲染的流程。
我个人推荐的看源码的方式最好是通过视频文章自己能手写一边然后再去看,否则就会造成这样的结果。
真是卷...
正文
Watcher
kotlin
// core/observe/watcher.js
let id = 0; // watcher唯一标识id
class Watcher{
constructor(vm, fn, options, cb) {
this.vm = vm; // vue实例
this.id = id++; // 赋值id
this.renderWatcher = options; // 是否是渲染watcher
this.getter = fn; // 执行函数
this.deps = []; // 当前watcher依赖的dep
this.value = this.get();
}
get() {
pushTarget(this); // 将当前watcher置为全局
let value = this.getter.call(this.vm);
popTarget(); // 移除当前全局wacher
return value;
}
}
Watcher
也就是我们所说的观察者,它主要用户检测数据的变动从而执行对应的方法。Vue
在初始化渲染时会创建渲染Watcher
,即fn
是渲染函数,在我们的页面中使用了某些响应式数据,Dep
就会对当前Watcher
进行关联,当数据发生改变时,即重新执行渲染函数,达到数据驱动试图的效果。
javascript
// core/instance/lifecycle.js
export function mountComponent(vm, el) {
// 1. 调通render方法生成虚拟dom
vm.$el = el;
const updateComponent = () => { // 渲染函数
vm._update(vm._render());
}
new Watcher(vm, updateComponent, true); // 创建渲染Watcher
}
Dep
ini
let uid = 0;
class Dep {
constructor() {
this.id = uid++; // dep唯一标识id
this.subs = []; // 依赖的Watcher
}
addSub(sub) { // 添加Watcher
this.subs.push(sub);
}
}
Dep.target = null; // 全局Watcher
const targetStack = []
export function pushTarget (target) { // 入栈
targetStack.push(target);
Dep.target = target;
}
export function popTarget () { // 出栈
targetStack.pop();
Dep.target = targetStack[targetStack.length - 1];
}
Dep
也是一个构造函数,是观察者模式中数据驱动的主导者,当响应式数据发生变化时,就会重新执行subs
中所有的Watcher
。
Dep.target
指的当前全局Watcher
,在初始化时依赖收集,收集的就是Dep.target
。
update更新
ini
Vue.prototype._update = function (vnode) {
const vm = this;
const el = vm.$el;
const prevVnode = vm._vnode; // 老vnode
vm._vnode = vnode;
if (!prevVnode) {
// 初始化渲染
} else {
// 更新渲染
vm.$el = patch(prevVnode, vnode);
}
}
通过update
函数将Vnode
转为真实dom
并进行渲染,如果没有老Vnode
就是初次渲染,存在老Vnode
就是更新渲染。
patch打补丁
scss
export function patch(oldVNode, vnode) { // 接受新老vnode
if(!oldVNode) {
return createElm(vnode);
}
const isRealElement = oldVNode.nodeType;
if (isRealElement) {
// 第一次渲染
} else {
// 更新渲染
patchVnode(oldVNode, vnode);
return vnode.el;
}
}
ini
export function patchVnode(oldVNode, vnode) {
if (!isSameVnode(oldVNode, vnode)) { // 如果不是同一个节点直接替换
let el = createElm(vnode);
oldVNode.el.parentNode.replaceChild(el, oldVNode.el);
return el;
}
let el = vnode.el = oldVNode.el;
if (!oldVNode.tag) { // 如果老节点是文本节点直接替换
if (oldVNode.text !== vnode.text) {
el.textContent = vnode.text;
}
}
// 比较儿子节点
let newChildren = vnode.children || [];
let oldChildren = oldVNode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) { // 新老节点都存在儿子节点
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) { // 没有老的 有新的
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) { // 新的没有 老的有
el.innerHTML = '';
}
return el;
}
在Vue
中判断两个节点是否相同,是通过标签名和属性key
来判断的,即标签名相同和key
都相同就认为是相同标签。
- 如果新老节点不相同,就直接暴力删除老的,创建一个新的节点插入即可。
- 如果老节点是一个文本节点,就直接创建一个新的节点替换即可。
- 如果都存在儿子节点,老节点的儿子数量为空,就直接将新的儿子节点创建插入到老节点当中。
- 如果都存在儿子节点,新节点的儿子数量为空,就直接将老的儿子全部删除即可。
- 如果都存在儿子节点,并且数量都不为空,就需要进步打补丁比较,即
Diff
算法。
总结
在初始化渲染的基础上,会在Vue
实例上缓存vnode
。待到数据更新再次渲染时,即存在老vnode
,就会执行更新渲染流程,通过diff
算法逐层进行打补丁。
如果觉得本文有帮助 记得点赞三连哦 十分感谢!