vue收集依赖的步骤:
updateComponent
更新当前组件时创建一个 Watcher(监听者)
=> Dep.target
。 用来监听该组件执行 render 访问了多少个 data 内的响应式数据,触发了多少 getdefineProperty
前创建一个 Dep(发布者)
。 用来当 1 过程触发了一个 get 时就拿到 1 中的 Dep.target
在其内部记录这个 Dep
, 并在 Dep
内也记录这个 Dep.target
。
(说人话就是:Watcher知道这个组件被多少个响应式数据影响到,Dep知道我这个响应式数据影响了多少组件)触发set
,通过Dep它知道要更新多少个组件,执行多少下 updateComponent (Watcher怎么拿到updateComponent方法:创建Watcher时就存储了该组件的 updateComponent方法)由【系列 2】手写vue模板编译 我们知道生成后的 render 函数如下:
render = new Function('with(this){return _c("div", {id:"app"},_v(_s(name)))}')
复制代码
render 最终将交给 mountComponent
内部调用 -> 生成vnode -> diff对比 vnode 生成 dom 渲染页面
function mountComponent(vm) {
// 实现页面的挂载流程
const updateComponent = () => {
// 调用render函数生成 vnode -> 交给update diff对比 生成真实的dom
vm.update(vm.render());
}
updateComponent(); // 响应式数据变化 也调用这个函数重新执行
}
复制代码
发现了吗? updateComponent 方法内当我们执行 render 时里面获取的 name 就是 this.name, 也就触发了响应式数据的 get 方法 这样为了 获取到该组件被多少个响应式数据影响到 时,我们就需要在 updateComponent 上下功夫了 如: vue-resolve/blob/vue-03/src/lifecycle.js 43行
// 通过 Watcher 监听 updateComponent
new Watcher(vm, updateComponent, ()=>{
console.log('页面重新渲染 updated')
}, true)
复制代码
Watcher 做了什么呢?
let wid = 0
class Watcher {
constructor(vm, updateComponent, cb, options) {
this.vm = vm
this.fn = updateComponent
this.cb = cb
this.options = options
this.deps = []
this.depsId = new Set()
this.id = wid++
this.get() // 实现页面的渲染
}
// 在执行 updateComponent 方法前设定了一个 Dep.target 执行完成后清空 Dep.target
get() {
Dep.target = this
this.fn()
Dep.target = null
}
addDep(dep) {
let id = dep.id
if (!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addWatcher(this)
}
}
update() {
this.fn() // 执行 updateComponent 方法 更新组件
}
}
复制代码
主要是设定一个 Dep.target 让 2.响应式get订阅
时能知道 Watcher 是谁, Watcher里面存有 updateComponent 方法,侧面就知道了该响应式数据对应更新的组件
上面执行的 updateComponent 内部执行了 render 方法, 自然触发了响应式数据的 get 方法
Object.defineProperty(data, key, {
get() {
if (Dep.target) {
dep.depend(); // 依赖收集 要将属性收集对应的 watcher
if (childOb) {
childOb.dep.depend(); // 让数组和对象也记录一下渲染 watcher
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
}
...
}
复制代码
class Dep {
constructor() {
this.id = did++
this.watchers = []
}
depend() {
// 此时 Dep.target 就是上面的 Watcher
Dep.target.addDep(this) // 1. 让 watcher 去记录 dep
}
addWatcher (watcher) { // 2. 让 dep 去记录 watcher
this.watchers.push(watcher)
}
notify () {
this.watchers.forEach(watcher => watcher.update()) // 批量执行 updateComponent 方法 更新组件 (可以看看上面 watcher 中的 update 方法)
}
}
复制代码
主要目的是让 Watcher知道这个组件被多少个响应式数据影响到,Dep知道我这个响应式数据影响了多少组件
Object.defineProperty(data, key, {
get() {
...
}
set(newValue) {
if (newValue === value) return;
childOb = observe(newValue);
value = newValue;
dep.notify(); // 执行 updateComponent 方法 更新组件 (可以看看上面 Dep 中的 notify 方法)
}
}
复制代码
dep.notify() 批量执行 updateComponent 方法 更新组件
由上可知:每次改变一个响应式数据就会调用影响到的所有组件的 updateComponent 方法更新组件 那么我如下操作:
<template>
<div>{{ name }}</div>
</template>
export default {
data: () => ({ name: '小米' })
mounted() {
this.name = 1 // 触发本组件的 updateComponent 方法
this.name = 2 // 触发本组件的 updateComponent 方法
}
}
复制代码
是不是我改多少次响应式数据就更新多少次组件了, 尤大心想: “这我肯定要想办法解决这个问题, 我的vue跟react的区别就在于这,react是手动挡它改完数据需要手动执行 setState() 方法才更新组件,这太low了,我要让数据与视图自动化响应式。” 也就是所谓的 MVVM
那怎么解决多次修改响应式数据频繁更新组件呢 ? ⊙o⊙ 办法有了,就用 js任务队列 来解决它吧!
在事件循环中,每进行一次循环操作称为 tick,每一次 tick 的任务 处理模型 是比较复杂的,但关键步骤如下:
具体实现原理:
<template>
<div>{{ name }}</div>
</template>
export default {
data: () => ({ name: '小米' })
mounted() {
this.name = 1 // 触发本组件的 updateComponent 方法
// 此时我们将 updateComponent 放入一个队列里,等待宏任务执行完成后遍历执行队列里的 updateComponent 了
let queue = []
queue.push(updateComponent)
// 1. 通过 Promise 来实现异步执行 (等所有响应式数据都修改后统一更新组件)
let p = Promise.resolve()
p.then(() => {
// 此时 queue 内就是所有同步代码执行完成后共同push的 updateComponent
queue.forEach(cb => cb())
queue = []
})
// 2. 如果浏览器不支持 Promise 还可以用 setTimeout (其实vue里面共兼容了 Promise, MutationObserver, setImmediate, setTimeout 这 4 种异步实现)
// setTimeout(() => {
// queue.forEach(cb => cb())
// queue = []
// }, 0)
this.name = 2 // 触发本组件的 updateComponent 方法
queue.push(updateComponent)
}
}
复制代码
很简单就是用一个异步事件,让正常代码都执行完成后才更新受影响的组件 当然,当 queue 内已经存在相同 updateComponent 时就不需要再次 push 进去。这样就避免了多个响应式数据对应一个组件的情况
那我们把它包装成一个公共方法,方法名就叫 nextTick
vue 源码中的 next-tick.js ,其实 nextTick 这个公共方法的原理就是开启一个异步微任务,把回调方法 cb 放微任务里面, 等待js宏任务代码都执行完成后才执行 cb 回调, 这样就有效避免了频繁更新组件
// nextTick.js 基本实现
let callbacks = []
let waiting = false
function flushCallbacks() {
callbacks.forEach(cb => cb())
callbacks = []
waiting = false
}
let timeFunc
// 这里的 if 就是实现兼容性, 通过每个浏览器对js的支持程度,选择不同的微任务实现
if (typeof Promise !== 'undefined') {
let p = Promise.resolve()
timeFunc = () => {
p.then(flushCallbacks)
}
} else if (typeof MutationObserver !== 'undefined') {
let observer = new MutationObserver(flushCallbacks) // mutationObserver放的回调是异步执行的
let textNode = document.createTextNode(1) // 文本节点内容先是 1
observer.observe(textNode,{ characterData: true })
timeFunc = () => {
textNode.textContent = 2 // 改成了2 就会触发更新了
}
} else if (typeof setImmediate !== 'undefined') {
timeFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timeFunc = () => {
setTimeout(flushCallbacks, 0) // 所有浏览器都支持 setTimeout,所以最终都没有就使用 setTimeout
}
}
export function nextTick(cb) {
callbacks.push(cb)
if (!waiting) {
waiting = true
timeFunc()
}
}
复制代码
1. 响应式数据修改 触发set 执行 dep.notify()
Object.defineProperty(data, key, {
get() {
...
},
set(newValue) {
if (newValue === value) return;
childOb = observe(newValue);
value = newValue;
dep.notify(); // 通知依赖的watcher去重新渲染
},
});
复制代码
2. notify 遍历执行所有 watcher.update() (本质就是将当前 Watcher 送入 queue 队列)
class Dep {
constructor() {
this.id = did++
this.watchers = []
}
...
notify () {
this.watchers.forEach(watcher => watcher.update()) // 遍历所有 watcher
}
}
复制代码
为什么把 watcher 送入队列, 因为 watcher 内存储了 updateComponent 方法, 等待js宏任务都执行完成后就依次执行 watcher 内的 updateComponent 方法, 组件就一并更新了
class Watcher {
constructor(vm, updateComponent, cb, options) {
this.fn = updateComponent
...
}
get() {
Dep.target = this
this.fn() // fn 就是 updateComponent
Dep.target = null // 只有在渲染的时候才有Dep.target属性
}
update() {
queueWatcher(this) // 就是开启一个异步方法将当前 Watcher 送入 queue 队列
}
run() {
this.get() // 重新执行 updateComponent
}
}
复制代码
queueWatcher 具体实现代码
import { nextTick } from "../utils/nextTick"
let queue = []
let has = {}
let pending = false
function flushSchedularQueue() {
queue.forEach(Watcher => Watcher.run()) // 组件一并更新
queue = []
has = {}
pending = false
}
// 重点在这里
export function queueWatcher(watcher) {
let id = watcher.id
if (has[id] == null) {
queue.push(watcher)
has[id] = true
// 宏任务内第一次更改响应式数据时进来创建一个 nextTick,后续更改直接 push 到创建好的 queue 即可
if (!pending) {
nextTick(() => { // 万一一个属性 对应多个更新,那么可能会开启多个定时器
flushSchedularQueue() // 批处理操作 , 防抖
})
pending = true
}
}
}
复制代码
手写 vue 代码仓库 | 链接 |
---|---|
GitHub | github.com/shunyue1320… |
Gitee | gitee.com/shunyue/vue… |
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文系转载,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。