前言
跟着鲨鱼大佬的源码看,前面说了初次渲染先经过模版编译=>在挂载时调用_render方法把render函数转化为虚拟dom,虚拟dom通过_update中patch函数渲染为真实dom了。也就是上篇我们调用的vm._update(vm._render())来实现更新功能,但当数据改变时页面并不会自动更新,这是为什么?。
因为我们不可能每次数据变化都要求用户自己去调用渲染方法更新视图 我们需要一个机制在数据变动的时候自动去更新。这个时候就涉及到渲染更新的原理了,vue通过观察者模式定 Watcher和 Dep完成依赖收集和派发更新从而实现渲染更新。
1.定义Watcher
当数据变动之后,通知它去执行某些方法,本质上就是一个构造函数,初始化的时候会去执行 get 方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
let id = 0;
export default class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm; this.exprOrFn = exprOrFn; this.cb = cb; this.options = options; this.id = id++; if (typeof exprOrFn === "function") { this.getter = exprOrFn; } this.get(); } get() { this.getter(); } }
|
2.创建渲染Watcher
我们在组件挂载方法里面 定义一个渲染 Watcher 主要功能就是执行核心渲染页面的方法
1 2 3 4 5 6 7 8 9 10 11 12
| export function mountComponent(vm, el) {
let updateComponent = () => { console.log("刷新页面"); vm._update(vm._render()); }; new Watcher(vm, updateComponent, null, true); }
|
3.定义Dep
Dep 也是一个构造函数 可以把他理解为观察者模式里面的被观察者 在 subs 里面收集 watcher 当数据变动的时候通知自身 subs 所有的 watcher 更新
Dep.target 是一个全局 Watcher 指向 初始状态是 null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
|
let id = 0; export default class Dep { constructor() { this.id = id++; this.subs = []; } }
Dep.target = null;
|
对象的依赖收集
Vue 2中的对象的依赖收集是通过使用getter/setter和观察者模式来实现的。在getter中进行依赖收集,将当前的Watcher实例添加到属性的依赖列表中。在setter中通知依赖该属性的Watcher实例进行更新=,这种机制使得Vue能够自动追踪数据的依赖关系。
在vue2源码学习(一)中,我们初始化了一个vue实例,并使用defineReactive对其进行了响应式处理。接下来我们需要在访问组件的数据属性时完成一下处理来实现依赖收集
- 在getter中,Vue会进行依赖收集。当组件渲染时,会创建一个Watcher实例,并将其设置为全局的Dep.target。当访问组件的数据属性时,会触发属性的getter,并将当前的Watcher实例添加到属性的依赖列表中。
- 在setter中,Vue会通知依赖该属性的Watcher实例进行更新。当属性的值发生变化时,会触发属性的setter,并调用依赖列表中每个Watcher实例的更新方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
|
function defineReactive(data, key, value) { observe(value);
let dep = new Dep();
Object.defineProperty(data, key, { get() { if (Dep.target) { dep.depend(); } return value; }, set(newValue) { if (newValue === value) return; observe(newValue); value = newValue; dep.notify(); }, }); }
|
5.完善watcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
|
import { pushTarget, popTarget } from "./dep";
let id = 0;
export default class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm; this.exprOrFn = exprOrFn; this.cb = cb; this.options = options; this.id = id++; this.deps = []; this.depsId = new Set(); if (typeof exprOrFn === "function") { this.getter = exprOrFn; } this.get(); } get() { pushTarget(this); this.getter(); popTarget(); } addDep(dep) { let id = dep.id; if (!this.depsId.has(id)) { this.depsId.add(id); this.deps.push(dep); dep.addSub(this); } } update() { this.get(); } }
|
6.完善dep
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
let id = 0; export default class Dep { constructor() { this.id = id++; this.subs = []; } depend() { if (Dep.target) { Dep.target.addDep(this); } } notify() { this.subs.forEach((watcher) => watcher.update()); } addSub(watcher) { this.subs.push(watcher); } }
Dep.target = null;
const targetStack = [];
export function pushTarget(watcher) { targetStack.push(watcher); Dep.target = watcher; } export function popTarget() { targetStack.pop(); Dep.target = targetStack[targetStack.length - 1]; }
|
7.数组的依赖收集
Vue中,数组的依赖收集相对于对象稍微复杂一些。由于JavaScript的限制,Vue无法通过getter/setter直接捕获数组的变化。因此,Vue采用了一些特殊的技巧来实现数组的依赖收集。
当Vue观察一个数组时,它会重写数组的一些原型方法(如push、pop、shift等),并在这些方法被调用时触发依赖收集和派发更新。
- 首先,Vue会调用原始的数组方法,以确保数组的正常操作。
- 然后,Vue会遍历数组中的每个元素,并对每个元素进行观察(observe)。这是为了确保数组中的每个元素都是响应式的。
- 接下来,Vue会检查是否有正在进行依赖收集的Watcher实例(即是否存在全局的Dep.target)。如果有,Vue会将这个Watcher实例添加到数组的依赖列表中。
- 最后,Vue会通过调用依赖列表中每个Watcher实例的更新方法来派发更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
function defineReactive(data, key, value) { let childOb = observe(value);
let dep = new Dep();
Object.defineProperty(data, key, { get() { if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { if (Array.isArray(value)) { dependArray(value); } } } } return value; }, set(newValue) { if (newValue === value) return; observe(newValue); value = newValue; dep.notify(); }, }); }
function dependArray(value) { for (let e, i = 0, l = value.length; i < l; i++) { e = value[i]; e && e.__ob__ && e.__ob__.dep.depend(); if (Array.isArray(e)) { dependArray(e); } } }
|
8.数组的派发更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
|
methodsToPatch.forEach((method) => { arrayMethods[method] = function (...args) { const result = arrayProto[method].apply(this, args); 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(); return result; }; });
|
总结
参考文章
手写Vue2.0源码(三)-初始渲染原理|技术点评 - 掘金 (juejin.cn)