手写Vue2.0源码(四)-渲染更新原理|技术点评

前言

此篇主要手写 Vue2.0 源码-渲染更新原理javascript

上一篇我们主要介绍了 Vue 初始渲染原理 完成了数据到视图层的映射过程 可是当咱们改变数据的时候发现页面并不会自动更新 咱们知道 Vue 的一个特性就是数据驱动 当数据改变的时候 咱们无需手动操做 dom 视图会自动更新 回顾第一篇 响应式数据原理 此篇主要采用观察者模式 定义 Watcher 和 Dep 完成依赖收集和派发更新 从而实现渲染更新html

适用人群: 没时间去看官方源码或者看源码看的比较懵而不想去看的同窗前端

提示:此篇难度稍大 是整个 Vue 源码很是核心的内容 后续的计算属性和自定义 watcher 以及$set $delete 等 Api 的实现 都须要理解此篇的思路 小编看源码这块也是看了有好几遍才搞懂 但愿你们克服困难一块儿去实现一遍吧!vue


正文

<script>
      // Vue实例化
      let vm = new Vue({
        el: "#app",
        data() {
          return {
            a: 123,
          };
        },
        // render(h) {
        // return h('div',{id:'a'},'hello')
        // },
        template: `<div id="a">hello {{a}}</div>`,
      });

        // 咱们在这里模拟更新
      setTimeout(() => {
        vm.a = 456;
        // 此方法是刷新视图的核心
        vm._update(vm._render());
      }, 1000);
    </script>
复制代码

上段代码 咱们在 setTimeout 里面调用 vm._update(vm._render())来实现更新功能 由于从上一篇初始渲染的原理可知 此方法就是渲染的核心 可是咱们不可能每次数据变化都要求用户本身去调用渲染方法更新视图 咱们须要一个机制在数据变更的时候自动去更新java

1.定义 Watcher

// src/observer/watcher.js

// 全局变量id 每次new Watcher都会自增
let id = 0;

export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm;
    this.exprOrFn = exprOrFn;
    this.cb = cb; //回调函数 好比在watcher更新以前能够执行beforeUpdate方法
    this.options = options; //额外的选项 true表明渲染watcher
    this.id = id++; // watcher的惟一标识
    // 若是表达式是一个函数
    if (typeof exprOrFn === "function") {
      this.getter = exprOrFn;
    }
    // 实例化就会默认调用get方法
    this.get();
  }
  get() {
    this.getter();
  }
}
复制代码

在 observer 文件夹下新建 watcher.js 表明和观察者相关 这里首先介绍 Vue 里面使用到的观察者模式 咱们能够把 Watcher 当作观察者 它须要订阅数据的变更 当数据变更以后 通知它去执行某些方法 其实本质就是一个构造函数 初始化的时候会去执行 get 方法面试

2.建立渲染 Watcher

// src/lifecycle.js
export function mountComponent(vm, el) {
  // _update和._render方法都是挂载在Vue原型的方法 相似_init

  // 引入watcher的概念 这里注册一个渲染watcher 执行vm._update(vm._render())方法渲染视图

  let updateComponent = () => {
    console.log("刷新页面");
    vm._update(vm._render());
  };
  new Watcher(vm, updateComponent, null, true);
}
复制代码

咱们在组件挂载方法里面 定义一个渲染 Watcher 主要功能就是执行核心渲染页面的方法算法

3.定义 Dep

// src/observer/dep.js

// dep和watcher是多对多的关系

// 每一个属性都有本身的dep

let id = 0; //dep实例的惟一标识
export default class Dep {
  constructor() {
    this.id = id++;
    this.subs = []; // 这个是存放watcher的容器
  }
}
// 默认Dep.target为null
Dep.target = null;
复制代码

Dep 也是一个构造函数 能够把他理解为观察者模式里面的被观察者 在 subs 里面收集 watcher 当数据变更的时候通知自身 subs 全部的 watcher 更新vue-router

Dep.target 是一个全局 Watcher 指向 初始状态是 nullvuex

4.对象的依赖收集

// src/observer/index.js

// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  observe(value);

  let dep = new Dep(); // 为每一个属性实例化一个Dep

  Object.defineProperty(data, key, {
    get() {
      // 页面取值的时候 能够把watcher收集到dep里面--依赖收集
      if (Dep.target) {
        // 若是有watcher dep就会保存watcher 同时watcher也会保存dep
        dep.depend();
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      // 若是赋值的新值也是一个对象 须要观测
      observe(newValue);
      value = newValue;
      dep.notify(); // 通知渲染watcher去更新--派发更新
    },
  });
}
复制代码

上诉代码就是依赖收集和派发更新的核心 其实就是在数据被访问的时候 把咱们定义好的渲染 Watcher 放到 dep 的 subs 数组里面 同时把 dep 实例对象也放到渲染 Watcher 里面去 数据更新时就能够通知 dep 的 subs 存储的 watcher 更新api

5.完善 watcher

// src/observer/watcher.js

import { pushTarget, popTarget } from "./dep";

// 全局变量id 每次new Watcher都会自增
let id = 0;

export default class Watcher {
  constructor(vm, exprOrFn, cb, options) {
    this.vm = vm;
    this.exprOrFn = exprOrFn;
    this.cb = cb; //回调函数 好比在watcher更新以前能够执行beforeUpdate方法
    this.options = options; //额外的选项 true表明渲染watcher
    this.id = id++; // watcher的惟一标识
    this.deps = []; //存放dep的容器
    this.depsId = new Set(); //用来去重dep
    // 若是表达式是一个函数
    if (typeof exprOrFn === "function") {
      this.getter = exprOrFn;
    }
    // 实例化就会默认调用get方法
    this.get();
  }
  get() {
    pushTarget(this); // 在调用方法以前先把当前watcher实例推到全局Dep.target上
    this.getter(); //若是watcher是渲染watcher 那么就至关于执行 vm._update(vm._render()) 这个方法在render函数执行的时候会取值 从而实现依赖收集
    popTarget(); // 在调用方法以后把当前watcher实例从全局Dep.target移除
  }
  // 把dep放到deps里面 同时保证同一个dep只被保存到watcher一次 一样的 同一个watcher也只会保存在dep一次
  addDep(dep) {
    let id = dep.id;
    if (!this.depsId.has(id)) {
      this.depsId.add(id);
      this.deps.push(dep);
      // 直接调用dep的addSub方法 把本身--watcher实例添加到dep的subs容器里面
      dep.addSub(this);
    }
  }
  // 这里简单的就执行如下get方法 以后涉及到计算属性就不同了
  update() {
    this.get();
  }
}
复制代码

watcher 在调用 getter 方法先后分别把自身赋值给 Dep.target 方便进行依赖收集 update 方法用来更新

6.完善 dep

// src/observer/dep.js

// dep和watcher是多对多的关系
// 每一个属性都有本身的dep
let id = 0; //dep实例的惟一标识
export default class Dep {
  constructor() {
    this.id = id++;
    this.subs = []; // 这个是存放watcher的容器
  }
  depend() {
    // 若是当前存在watcher
    if (Dep.target) {
      Dep.target.addDep(this); // 把自身-dep实例存放在watcher里面
    }
  }
  notify() {
    // 依次执行subs里面的watcher更新方法
    this.subs.forEach((watcher) => watcher.update());
  }
  addSub(watcher) {
    // 把watcher加入到自身的subs容器
    this.subs.push(watcher);
  }
}
// 默认Dep.target为null
Dep.target = null;
// 栈结构用来存watcher
const targetStack = [];

export function pushTarget(watcher) {
  targetStack.push(watcher);
  Dep.target = watcher; // Dep.target指向当前watcher
}
export function popTarget() {
  targetStack.pop(); // 当前watcher出栈 拿到上一个watcher
  Dep.target = targetStack[targetStack.length - 1];
}
复制代码

定义相关的方法把收集依赖的同时把自身也放到 watcher 的 deps 容器里面去

思考? 这时对象的更新已经能够知足了 可是若是是数组 相似{a:[1,2,3]} a.push(4) 并不会触发自动更新 由于咱们数组并无收集依赖

7.数组的依赖收集

// src/observer/index.js

// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  let childOb = observe(value); // childOb就是Observer实例

  let dep = new Dep(); // 为每一个属性实例化一个Dep

  Object.defineProperty(data, key, {
    get() {
      // 页面取值的时候 能够把watcher收集到dep里面--依赖收集
      if (Dep.target) {
        // 若是有watcher dep就会保存watcher 同时watcher也会保存dep
        dep.depend();
        if (childOb) {
          // 这里表示 属性的值依然是一个对象 包含数组和对象 childOb指代的就是Observer实例对象 里面的dep进行依赖收集
          // 好比{a:[1,2,3]} 属性a对应的值是一个数组 观测数组的返回值就是对应数组的Observer实例对象
          childOb.dep.depend();
          if (Array.isArray(value)) {
            // 若是数据结构相似 {a:[1,2,[3,4,[5,6]]]} 这种数组多层嵌套 数组包含数组的状况 那么咱们访问a的时候 只是对第一层的数组进行了依赖收集 里面的数组由于没访问到 因此五大收集依赖 可是若是咱们改变了a里面的第二层数组的值 是须要更新页面的 因此须要对数组递归进行依赖收集
            if (Array.isArray(value)) {
              // 若是内部仍是数组
              dependArray(value); // 不停的进行依赖收集
            }
          }
        }
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      // 若是赋值的新值也是一个对象 须要观测
      observe(newValue);
      value = newValue;
      dep.notify(); // 通知渲染watcher去更新--派发更新
    },
  });
}
// 递归收集数组依赖
function dependArray(value) {
  for (let e, i = 0, l = value.length; i < l; i++) {
    e = value[i];
    // e.__ob__表明e已经被响应式观测了 可是没有收集依赖 因此把他们收集到本身的Observer实例的dep里面
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      // 若是数组里面还有数组 就递归去收集依赖
      dependArray(e);
    }
  }
}
复制代码

若是对象属性的值是一个数组 那么执行 childOb.dep.depend()收集数组的依赖 若是数组里面还包含数组 须要递归遍历收集 由于只有访问数据触发了 get 才会去收集依赖 一开始只是递归对数据进行响应式处理没法收集依赖 这两点须要分清

8.数组的派发更新

// src/observer/array.js

methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    // 这里保留原型方法的执行结果
    const result = arrayProto[method].apply(this, args);
    // 这句话是关键
    // this表明的就是数据自己 好比数据是{a:[1,2,3]} 那么咱们使用a.push(4) this就是a ob就是a.__ob__ 这个属性表明的是该数据已经被响应式观察过了 __ob__对象指的就是Observer实例
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    if (inserted) ob.observeArray(inserted); // 对新增的每一项进行观测
    ob.dep.notify(); //数组派发更新 ob指的就是数组对应的Observer实例 咱们在get的时候判断若是属性的值仍是对象那么就在Observer实例的dep收集依赖 因此这里是一一对应的 能够直接更新
    return result;
  };
});
复制代码

关键代码就是 ob.dep.notify()

9.渲染更新的思惟导图

渲染更新

小结

总体流程

这里放一张 整个 Vue 响应式原理的图片 我们从数据劫持-->模板解析-->模板渲染-->数据变化视图自动更新整个流程已经手写了一遍 尤为是此篇介绍的渲染更新相关的知识点 建议反复理解原理以后本身动手实现一遍 由于Vue 不少核心原理和 api 都跟这里的知识点相关哈

至此 Vue 的渲染更新原理已经完结 遇到不懂或者有争议的地方欢迎评论留言

最后若是以为本文有帮助 记得点赞三连哦 十分感谢!

系列连接(后续都会更新完毕)

鲨鱼哥的前端摸鱼技术群

欢迎你们技术交流 内推 摸鱼 求助皆可 - 连接