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, 如果是, 执行传入的回调

最新文章

  1. 用php去除bom头
  2. centos 7.0 编译安装php 5.6.7
  3. Go - 数组 和 切片(array、slice)
  4. SQL数据库与excel表格之间的数据 导入
  5. 【吉光片羽】之 Web API
  6. TCPIP,Http,Socket的区别
  7. GCD时间轴
  8. SQL Server中Delete语句表名不能用别名
  9. 常用 Linux 命令
  10. log4j与commons-logging,slf4j的关系(转)
  11. 使用composer下拉组件失败,出现killed解决办法
  12. PIC32MZ 通过USB在线升级 -- USB HID bootloader
  13. APP专业的开发公司都有这样一套开发流程,强烈建议收藏!
  14. Problem F: 平面上的点——Point类 (VI)
  15. 冲刺博客NO.7
  16. android 实现mqtt消息推送,以及不停断线重连的问题解决
  17. 【转】I&#178;C总线上拉电阻阻值如何选择?
  18. 对IIC总线时序的一点理解以及ACK和NACK(NAK)
  19. querystring模块详解
  20. ubuntu17.10 安装ssh

热门文章

  1. Linux c 检测U盘挂载路径方法
  2. Linux c 程序自动启动自己
  3. 为啥要对jvm做优化?
  4. Redis 异步客户端选型及落地实践
  5. 基于昇腾计算语言AscendCL开发AI推理应用
  6. NetCore使用ZipFile 和ZipOutputStream
  7. print()、转义字符、标识符和保留字、变量、数据类型、类型转换
  8. C语言-三子棋项目
  9. 【USACO 2021 US Open, Gold】Permutation
  10. 利用CRT配合VBS脚本实现自动化巡检