10. watch的实现原理
watch的实现原理
watch和computed一样, 也是基于 Watcher 的
组件内部使用的watch 和 外部使用的 vm.$watch()都是调用的Vue.prototype.$watch方法
当依赖的属性发生变化, 更新的时候执行回调就行了
vue'中watch有多种写法, 这里只简单观察2种
// watch就是一个观察者, dep发生变化, 就执行对应的回调
watch: {
// 字符串形式
firstname(newValue, oldValue) {
console.log(newValue, oldValue)
}
// 有多种写法:
// 1. 字符串, 内容定义在method里面
// 2. 函数
// 3. 数组
}
vm.$watch(() => vm.firstname, (newValue, oldValue) => {
console.log(newValue, oldValue, 'ppp');
})
在Vue.prototype上拓展$watch方法, 里面创建一个用户watcher
// 监控的值, 回调, 选项
Vue.prototype.$watch = function(exprOrFn,cb, options = {}){
// console.log('333', exprOrFn,cb, options)
//exprOrFn 可能是fitstname 也可能是 () => vm.firstname
// cb就是定义的函数
// 这个watcher功能, exprOrFn变化了, 执行cb
new Watcher(this, exprOrFn, {user: true}, cb)
}
在state.js种处理 watch 部分
if(opts.data) {
initData(vm)
}
if(opts.computed) {
initComputed(vm)
}
if(opts.watch) {
initWatch(vm)
}
...
// initWatch的实现, 获取所有的watch(数组),
function initWatch(vm) {
let watch = vm.$options.watch
for(let key in watch) {
const handler = watch[key] // 可能时字符串 数组 函数
if(Array.isArray(handler)) {
for(let i = 0; i < handler.length; i ++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher(vm, key, handler) {
if(typeof handler === 'string') { // 如: firstname: 'fn' 的形式, 但是fn是定义在methods上面的
handler = vm[handler]
}
return vm.$watch(key, handler)
}
在改造Watcher, 适配用户watcher
// 添加cb属性, 原来的fn,改成exprOrFn, 可能是字符串, 也可能是function'
constructor(vm, exprOrFn, options, cb) {
// watch的watcher添加了一个cb回调
this.cb = cb
this.user = options.user // 标识是否是用户自己的watcher
// 如果是字符串, 要变成函数
if(typeof exprOrFn === 'string') {
this.getter = function() {
return vm[exprOrFn] // return vm.firstname
}
} else {
this.getter = exprOrFn
}
// 注意这个watcher也会立即执行, 要获取返回值, 作为旧的value
this.value = this.lazy ? undefined : this.get()
// 在更新的时候, 获取新旧值, 执行回调
run() {
// 获取新旧值
let oldValue = this.value
let newValue = this.get()
if(this.user) {
this.cb.call(this.vm, newValue, oldValue)
}
具体 代码
dist/8.watcher.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>计算属性</title>
</head>
<body>
<div id="app" style="color:yellow;backgroundColor:blue;">
{{fullname}} {{fullname}} {{fullname}}
</div>
<script src="vue.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.14/vue.js"></script> -->
<script>
const vm = new Vue({
data() {
return {
firstname: 'yang',
lastname: 'jerry'
}
},
el: '#app', // 将数据解析到el元素上
// 急速属性, 依赖的指发生变化才会重新执行, 要维护一个dirty属性, 默认计算属性不会立即执行
// 计算属性就是一个defineProperty
// 计算属性也是一个watcher
computed: {
// 写法1
fullname() {
return this.firstname + '-' + this.lastname
}
},
// watch就是一个观察者, dep发生变化, 就执行对应的回调
watch: {
// 字符串形式
firstname(newValue, oldValue) {
console.log(newValue, oldValue)
}
// 有多种写法:
// 1. 字符串, 内容定义在method里面
// 2. 函数
// 3. 数组
}
})
// 4. $watch 最终都是调用下面这个
// 这个是函数形式
vm.$watch(() => vm.firstname, (newValue, oldValue) => {
console.log(newValue, oldValue, 'ppp');
})
// 如果有数组嵌套
setTimeout(() => {
vm.firstname = '888'
},1000)
</script>
</body>
</html>
src/index.js
// Vue 类是通过构造函数来实现的
// 如果通过 class来实现, 里面的类和方法就会有很多, 不利于维护
// 1. 新建一个Vue构造函数, 默认导出, 这样就有了全局 Vue
// 2. Vue中执行一个初始化方法, 参数是用户的选项
// 3. 在Vue的原型上添加这个方法, (注意: 添加的这个方法在引入vue的时候就执行了, 而不是在new Vue()的时候执行的)
import { initGlobalApi } from "./globalApi"
import { initMixin } from "./init"
import { initLifeCycle } from "./lifecycle"
import { nextTick, Watcher } from "./observe/watcher"
function Vue(options) {
this._init(options)
}
initMixin(Vue)
initLifeCycle(Vue)
initGlobalApi(Vue)
Vue.prototype.$nextTick = nextTick
// 监控的值, 回调, 选项
Vue.prototype.$watch = function(exprOrFn,cb, options = {}){
// console.log('333', exprOrFn,cb, options)
//exprOrFn 可能是fitstname 也可能是 () => vm.firstname
// cb就是定义的函数
// 这个watcher功能, exprOrFn变化了, 执行cb
new Watcher(this, exprOrFn, {user: true}, cb)
}
export default Vue
src/state.js
import { observe } from "./observe"
import { Dep } from "./observe/dep"
import { Watcher } from "./observe/watcher"
export function initState(vm) {
const opts = vm.$options
if(opts.data) {
initData(vm)
}
if(opts.computed) {
initComputed(vm)
}
if(opts.watch) {
initWatch(vm)
}
}
function initWatch(vm) {
let watch = vm.$options.watch
for(let key in watch) {
const handler = watch[key] // 可能时字符串 数组 函数
if(Array.isArray(handler)) {
for(let i = 0; i < handler.length; i ++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}
function createWatcher(vm, key, handler) {
if(typeof handler === 'string') { // 如: firstname: 'fn' 的形式, 但是fn是定义在methods上面的
handler = vm[handler]
}
return vm.$watch(key, handler)
}
// 初始化数据的具体方法
function initData(vm) {
let data = vm.$options.data
data = typeof data === 'function' ? data.call(vm) : data
vm._data = data
// 进行数据劫持, 关键方法, 放在另一个文件里面, 新建 observe/index.js
observe(data)
// 设置代理, 这个代理只有最外面这一层
// 希望访问 vm.name 而不是 vm._data.name, 使用vm 来代理 vm._data
// 在vm上取值时, 实际上是在vm._data上取值
// 设置值时, 实际上是在vm._data上设置值
// 每一个属性都需要代理
for(let key in data) {
proxy(vm, '_data', key)
}
}
// 属性代理 vm._data.name => vm.name
function proxy(vm, target, key) {
Object.defineProperty(vm, key, {
get() {
return vm[target][key]
},
set(newValue) {
vm[target][key] = newValue
}
})
}
// 初始化计算属性
function initComputed(vm) {
// 得到的computed时一个数组
const computed = vm.$options.computed
for(let key in computed) {
// 获取computed
let userDef = computed[key]
// 定义watcher并挂载到实例上, 方便后续通过实例获取, 把key和watcher一一对应
const watchers = vm._computedWatchers = {}
let fn = typeof userDef === 'function' ? userDef : userDef.get
// 创建一个计算属性watcher
watchers[key] = new Watcher(vm, fn, {lazy: true})
// 在vue实例上定义这些属性, 所以可以通过vm.fullname访问到
defineComputed(vm, key, userDef)
}
}
function defineComputed(target, key, userDef) {
const setter = userDef.set || (() => {})
Object.defineProperty(target, key, {
get: createComputedGetter(key),
set: setter
})
}
function createComputedGetter(key) {
return function() {
// 这里的this指向上面的target, 也就是vm
const watcher = this._computedWatchers[key]
// 如果是脏值, 求值
if(watcher.dirty) {
// 求值之后, dirty变成false, 下次就不求值了 需要在watcher上添加evaluate方法
watcher.evaluate()
}
// 上面取值之后会将计算属性watcher pop出来, 如果stack里面还有watcher, 那就是渲染watcher, 需要计算属性里面的deps去记住上层的watcher
// 因为计算属性watcher不能更新视图, 只有渲染watcher可以
if(Dep.target) {
// 添加depend方法
watcher.depend()
}
// 新增value属性表示计算属性的值
return watcher.value
}
}
observe/watcher.js
import { Dep, popTarget, pushTarget } from "./dep"
let id = 0
export class Watcher {
constructor(vm, exprOrFn, options, cb) {
this.id = id ++
this.vm = vm
this.deps = []
this.depsId = new Set()
// 是否时渲染watcher
this.renderWatcher = options
this.lazy = options.lazy
this.dirty = this.lazy
// watch的watcher添加了一个cb回调
this.cb = cb
this.user = options.user // 标识是否是用户自己的watcher
// 重新渲染的方法
// 加入watch的watcher之后, exprOrFn可能不是函数, 是个字符串, 需要变成函数
if(typeof exprOrFn === 'string') {
this.getter = function() {
return vm[exprOrFn] // return vm.firstname
}
} else {
this.getter = exprOrFn
}
// 渲染watcher需要立即执行一次, 计算属性watcher初始化时不执行
// 用户的watcher也会执行, 获取上一次的旧值
this.value = this.lazy ? undefined : this.get()
}
get() {
// 开始渲染时, 让静态属性Dep.target指向当前的watcher, 那么在取值的时候, 就能在对应的属性中记住当前的watcher
// Dep.target = this
pushTarget(this)
let value = this.getter.call(this.vm)
// 渲染完毕之后清空
// Dep.target = null
popTarget()
return value
}
// watcher里面添加dep, 去重
addDep(dep) {
if(!this.depsId.has(dep.id)) {
this.deps.push(dep)
this.depsId.add(dep.id)
// 去重之后, 让当前的dep,去记住当前的watcher
dep.addSub(this)
}
}
// 让计算属性watcher里面的dep收集外层的watcher
depend() {
let length = this.deps.length
while(length--) {
this.deps[length].depend()
}
}
update() {
if(this.lazy) {
this.dirty = true
} else {
// 更新, 需要重新收集依赖
queueWatcher(this) // 把当前的watcher暂存起来
}
}
run() {
// 获取新旧值
let oldValue = this.value
let newValue = this.get()
if(this.user) {
this.cb.call(this.vm, newValue, oldValue)
}
}
evaluate() {
this.value = this.get()
this.dirty = false
}
}
let queue = [] // 用于存放需要更新吧的watcher
let has = {} // 用于去重
let pending = false // 防抖
function flushScheduleQueue() {
let flushQueue = queue.slice(0) // copy一份
queue = [] // 刷新过程中, 有新的watcher, 重新放到queue中
has = {}
pending = false
flushQueue.forEach(q => q.run()) // 添加一个run方法,真正的渲染
}
function queueWatcher(watcher) {
const id = watcher.id
if(!has[id]) { // 对watch进行去重
queue.push(watcher)
has[id] = true
// 不管update执行多少次, 但是最终值执行一次刷新操作
if(!pending) {
// 开一个定时器 里面的方法只执行一次, 并且是在所有的watcher都push进去之后才执行的
// setTimeout(() => {
// console.log('杀心')
// }, 0)
// setTimeout(flushScheduleQueue, 0)
nextTick(flushScheduleQueue, 0) // 内部使用的是nextTick, 第二个参数估计可以不要
pending = true
}
}
}
let callbacks = []
let waiting = false
// 跟上面的套路一样
function flushCallBacks() {
let cbs = callbacks.slice(0)
waiting = false
callbacks = []
cbs.forEach(cb => cb())
}
// vue内部 没有直接使用某个api, 而是采用优雅降级的方式
// 内部先使用的是promise(ie不兼容), MutationObserver (h5的api) ie专享的 setImmediate 最后setTimeout
let timerFunc
if(Promise) {
timerFunc = () => {
Promise.resolve().then(flushCallBacks)
}
} else if(MutationObserver) {
let observer = new MutationObserver(flushCallBacks) // 这里传入的回调时异步执行的
let textNode = document.createTextNode(1) // 应该是固定用法
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
textNode.textContent = 2
}
} else if(setImmediate) {
timerFunc = () => {
setImmediate(flushCallBacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallBacks)
}
}
export function nextTick(cb) { // setTimeout是过一段事件后, 执行cb, nextTick是维护了一个队列, 后面统一执行
callbacks.push(cb)
if(!waiting) {
// setTimeout(() => {
// flushCallBacks()
// }, 0)
timerFunc()
waiting = true
}
}
整个流程: 在初始化状态的时候, 如果有watche(数组),遍历watche,并为每一项生成一个用户watcher, 默认这个watcher会立即执行并取值, 记录在value上, 作为oldValue, 当依赖的属性发生变化, 会重新取值, 此时的值就是newValue, 然后判断是否是用户的watcher, 如果是, 执行传入的回调