带你一行一行手写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系列文章(后续更新)

相关推荐
M_emory_6 分钟前
解决 git clone 出现:Failed to connect to 127.0.0.1 port 1080: Connection refused 错误
前端·vue.js·git
Ciito9 分钟前
vue项目使用eslint+prettier管理项目格式化
前端·javascript·vue.js
成都被卷死的程序员42 分钟前
响应式网页设计--html
前端·html
fighting ~1 小时前
react17安装html-react-parser运行报错记录
javascript·react.js·html
老码沉思录1 小时前
React Native 全栈开发实战班 - 列表与滚动视图
javascript·react native·react.js
abments1 小时前
JavaScript逆向爬虫教程-------基础篇之常用的编码与加密介绍(python和js实现)
javascript·爬虫·python
mon_star°1 小时前
将答题成绩排行榜数据通过前端生成excel的方式实现导出下载功能
前端·excel
Zrf21913184551 小时前
前端笔试中oj算法题的解法模版
前端·readline·oj算法
老码沉思录1 小时前
React Native 全栈开发实战班 - 状态管理入门(Context API)
javascript·react native·react.js
文军的烹饪实验室2 小时前
ValueError: Circular reference detected
开发语言·前端·javascript