带你一行一行手写Vue2.0源码系列(四) -更新渲染

前言

上一篇中我们主要讲的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都相同就认为是相同标签。

  1. 如果新老节点不相同,就直接暴力删除老的,创建一个新的节点插入即可。
  2. 如果老节点是一个文本节点,就直接创建一个新的节点替换即可。
  3. 如果都存在儿子节点,老节点的儿子数量为空,就直接将新的儿子节点创建插入到老节点当中。
  4. 如果都存在儿子节点,新节点的儿子数量为空,就直接将老的儿子全部删除即可。
  5. 如果都存在儿子节点,并且数量都不为空,就需要进步打补丁比较,即Diff算法。

总结

在初始化渲染的基础上,会在Vue实例上缓存vnode。待到数据更新再次渲染时,即存在老vnode,就会执行更新渲染流程,通过diff算法逐层进行打补丁。

如果觉得本文有帮助 记得点赞三连哦 十分感谢!

vue2.0 和 vue3.0系列文章(后续更新)

相关推荐
dawn1912288 分钟前
SpringMVC 中的域对象共享数据
java·前端·servlet
newxtc1 小时前
【爱给网-注册安全分析报告-无验证方式导致安全隐患】
前端·chrome·windows·安全·媒体
一个很帅的帅哥1 小时前
axios(基于Promise的HTTP客户端) 与 `async` 和 `await` 结合使用
javascript·网络·网络协议·http·async·promise·await
刘志辉1 小时前
vue传参方法
android·vue.js·flutter
dream_ready2 小时前
linux安装nginx+前端部署vue项目(实际测试react项目也可以)
前端·javascript·vue.js·nginx·react·html5
编写美好前程2 小时前
ruoyi-vue若依前端是如何防止接口重复请求
前端·javascript·vue.js
flytam2 小时前
ES5 在 Web 上的现状
前端·javascript
喵喵酱仔__2 小时前
阻止冒泡事件
前端·javascript·vue.js
GISer_Jing2 小时前
前端面试CSS常见题目
前端·css·面试
某公司摸鱼前端2 小时前
如何关闭前端Chrome的debugger反调试
javascript·chrome