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

相关推荐
哟哟耶耶1 小时前
React-04React组件状态(state),构造器初始化state以及数据读取,添加点击事件并更改state状态值
前端·javascript·react.js
kiramario1 小时前
用IconContext.Provider修改react-icons的icon样式
前端·javascript·react.js
destinyol1 小时前
React首页加载速度优化
前端·javascript·react.js·webpack·前端框架
程序员小续1 小时前
React 多个 HOC 嵌套太深,会带来哪些隐患?
java·前端·javascript·vue.js·python·react.js·webpack
大猫会长1 小时前
用AbortController取消事件绑定
前端
程序员小杰@2 小时前
AI前端组件库Ant DesIgn X
开发语言·前端·人工智能
致微2 小时前
Vue项目 bug 解决
前端·vue.js·bug
慕斯策划一场流浪3 小时前
fastGPT—nextjs—mongoose—团队管理之部门相关api接口实现
前端·javascript·html·fastgpt部门创建·fastgpt团队管理·fastgpt部门成员更新·fastgpt部门成员创建
我自纵横20234 小时前
事件处理程序
开发语言·前端·javascript·css·json·ecmascript
坊钰4 小时前
【MySQL 数据库】数据类型
java·开发语言·前端·数据库·学习·mysql·html