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

10. watch的实现原理的更多相关文章

  1. 【10】css hack原理及常用hack

    [10]css hack原理及常用hack 原理:利用不同浏览器对CSS的支持和解析结果不一样编写针对特定浏览器样式.常见的hack有1)属性hack.2)选择器hack.3)IE条件注释 IE条件注 ...

  2. 10分钟理解BFC原理

    10 分钟理解 BFC 原理 一.常见定位方案 在讲 BFC 之前,我们先来了解一下常见的定位方案,定位方案是控制元素的布局,有三种常见方案: 普通流 (normal flow) 在普通流中,元素按照 ...

  3. Spring框架系列(10) - Spring AOP实现原理详解之AOP代理的创建

    上文我们介绍了Spring AOP原理解析的切面实现过程(将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor).本文在此基 ...

  4. docker 10 docker的镜像原理

    镜像是什么? 镜像是一个轻量级,可执行的软件包,用来打包运行环境和基于运行环境开发的软件包,它包含某个软件运行环境的所有内容.包括代码,运行时的库,配置文件和环境变量 UnionFs(联合文件系统) ...

  5. [iOS基础控件 - 6.10.4] 项目启动原理 项目中的文件

    A.项目中的常见文件 1.单元测试Test   2.Frameworks(xCode6 创建的SingleView Project没有) 依赖框架   3.Products 打包好的文件   4. p ...

  6. 10 分钟理解 BFC 原理

    一.常见定位方案 在讲 BFC 之前,我们先来了解一下常见的定位方案,定位方案是控制元素的布局,有三种常见方案: 普通流 (normal flow) 在普通流中,元素按照其在 HTML 中的先后位置至 ...

  7. kafka原理和实践(一)原理:10分钟入门

    系列目录 kafka原理和实践(一)原理:10分钟入门 kafka原理和实践(二)spring-kafka简单实践 kafka原理和实践(三)spring-kafka生产者源码 kafka原理和实践( ...

  8. Atiti 数据库系统原理 与数据库方面的书籍 attilax总结 v3 .docx

    Atiti 数据库系统原理 与数据库方面的书籍 attilax总结 v3 .docx 1.1. 数据库的类型,网状,层次,树形数据库,kv数据库.oodb2 1.2. Er模型2 1.3. Sql2 ...

  9. Atitit.编译原理与概论

    Atitit.编译原理与概论 编译原理 词法分析 Ast构建,语法分析 语意分析 6 数据结构  1. ▪ 记号 2. ▪ 语法树 3. ▪ 符号表 4. ▪ 常数表 5. ▪ 中间代码 1. ▪ 临 ...

  10. kafka原理和实践(二)spring-kafka简单实践

    系列目录 kafka原理和实践(一)原理:10分钟入门 kafka原理和实践(二)spring-kafka简单实践 kafka原理和实践(三)spring-kafka生产者源码 kafka原理和实践( ...

随机推荐

  1. vue3实现一个抽奖小项目

    前言 在公司年会期间我做了个抽奖小项目,我把它分享出来,有用得着的可以看下. 浏览链接:http://xisite.top/original/luck-draw/index.html 项目链接:htt ...

  2. 【ASP.NET Core】用配置文件来设置授权角色

    在开始之前,老周先祝各个次元的伙伴们新春快乐.生活愉快.万事如意. 在上一篇水文中,老周介绍了角色授权的一些内容.本篇咱们来聊一个比较实际的问题--把用于授权的角色名称放到外部配置,不要硬编码,以方便 ...

  3. vue 中引入iframe,动态设置其src,遇到的一些小问题总结

    1.重置其样式,去掉外框以及滚动条等 <iframe id="myIframe" ref="iframe_a" :src="mySrc" ...

  4. 记一次线上FGC问题排查

    引言 本文记录一次线上 GC 问题的排查过程与思路,希望对各位读者有所帮助.过程中也走了一些弯路,现在有时间沉淀下来思考并总结出来分享给大家,希望对大家今后排查线上 GC 问题有帮助. 背景 服务新功 ...

  5. ClickHouse(12)ClickHouse合并树MergeTree家族表引擎之AggregatingMergeTree详细解析

    目录 建表语法 查询和插入数据 数据处理逻辑 ClickHouse相关资料分享 AggregatingMergeTree引擎继承自 MergeTree,并改变了数据片段的合并逻辑.ClickHouse ...

  6. 2023牛客寒假算法基础集训营5 A-L

    比赛链接 A 题解 知识点:前缀和,二分. 找到小于等于 \(x\) 的最后一个物品,往前取 \(k\) 个即可,用前缀和查询. 时间复杂度 \(O(n + q\log n)\) 空间复杂度 \(O( ...

  7. JAVA虚拟机11-Class文件结构

    1.平台无关性和语言无关性 Oracle公司以及其他虚拟机发行商发布过许多可以运行在各种不同硬件平台和操作系统上的Java虚拟机,这些虚拟机都可以载入和执行同一种平台无关的字节码,从而实现了程序的&q ...

  8. IIS服务器SSL证书安装 (pfx文件不能直接运行时)

    在证书控制台下载IIS版本证书,下载到本地的是一个压缩文件,解压后里面包含.pfx文件是证书文件,pfx_password.txt是证书文件的密码. 友情提示: 每次下载都会产生新密码,该密码仅匹配本 ...

  9. Rainbond ubuntu20.04单主机(allinone)部署及简单应用构建

    1.Rainbond是什么? Rainbond 是一个云原生应用管理平台,使用简单,不需要懂容器.Kubernetes和底层复杂技术,支持管理多个Kubernetes集群,和管理企业应用全生命周期. ...

  10. SpringMVC的文件、数据校验(Vaildator、Annotation JSR-303)

    SpringMvc的文件上传下载: 文件上传 单文件上传 1.底层使用的是Apache fileupload组件进行上传的功能,Springmvc 只是对其进行了封装,简化开发, pom.xml &l ...