这篇文章将带大家全面理解vue
的watcher
、computed
和user watcher
,其实computed
和user watcher
都是基于Watcher
来实现的,我们通过一个一个功能点去敲代码,让大家全面理解其中的实现原理和核心思想。所以这篇文章将实现以下这些功能点:
实现数据更新触发渲染watcher
执行,从而更新ui界面 废话不要多说,先看下面的最终例子。
例子看完之后我们就直接开工了。
准备工作 首先我们准备了一个index.html
文件和一个vue.js
文件,先看看index.html
的代码
<!DOCTYPE html> <html lang='en' > <head> <meta charset='UTF-8' > <title>全面理解vue的渲染watcher、computed和user atcher</title> </head> <body> <div id='root' ></div> <script src='./vue.js' ></script> <script> const root = document.querySelector('#root' ) var vue = new Vue({ data () { return { name: '张三' , age: 10 } }, render () { root.innerHTML = `${this.name} ----${this.age} ` } }) </script> </body> </html>
index.html
里面分别有一个id是root的div节点,这是跟节点,然后在script标签里面,引入了vue.js
,里面提供了Vue构造函数,然后就是实例化Vue,参数是一个对象,对象里面分别有data 和 render 函数。然后我们看看vue.js
的代码:
function Vue (options) { this._init(options) // 初始化 this.$mount () // 执行render函数 } Vue.prototype._init = function (options) { const vm = this vm.$options = options // 把options挂载到this上 if (options.data) { initState(vm) // 数据响应式 } if (options.computed) { initComputed(vm) // 初始化计算属性 } if (options.watch) { initWatch(vm) // 初始化watch } }
vue.js
代码里面就是执行this._init()
和this.$mount()
,this._init
的方法就是对我们的传进来的配置进行各种初始化,包括数据初始化initState(vm)
、计算属性初始化initComputed(vm)
、自定义watch初始化initWatch(vm)
。this.$mount
方法把render
函数渲染到页面中去、这些方法我们后面都写到,先让让大家了解整个代码结构。下面我们正式去填满我们上面写的这些方法。
实现数据响应式 要实现这些watcher
首先去实现数据响应式,也就是要实现上面的initState(vm)
这个函数。相信大家都很熟悉响应式这些代码,下面我直接贴上来。
function initState(vm) { let data = vm.$options .data; // 拿到配置的data属性值 // 判断data 是函数还是别的类型 data = vm._data = typeof data === 'function' ? data.call(vm, vm) : data || {}; const keys = Object.keys(data); let i = keys.length; while (i--) { // 从this上读取的数据全部拦截到this._data到里面读取 // 例如 this.name 等同于 this._data.name proxy(vm, '_data' , keys[i]); } observe(data); // 数据观察 } // 数据观察函数function observe(data) { if (typeof data !== 'object' && data != null) { return ; } return new Observer(data) } // 从this上读取的数据全部拦截到this._data到里面读取 // 例如 this.name 等同于 this._data.namefunction proxy(vm, source , key) { Object.defineProperty(vm, key, { get () { return vm[source ][key] // this.name 等同于 this._data.name }, set (newValue) { return vm[source ][key] = newValue } }) } class Observer{ constructor(value) { this.walk(value) // 给每一个属性都设置get set } walk(data) { let keys = Object.keys(data); for (let i = 0, len = keys.length; i < len; i++) { let key = keys[i] let value = data[key] defineReactive(data, key, value) // 给对象设置get set } } }function defineReactive(data, key, value) { Object.defineProperty(data, key, { get () { return value }, set (newValue) { if (newValue == value) return observe(newValue) // 给新的值设置响应式 value = newValue } }) observe(value); // 递归给数据设置get set }
重要的点都在注释里面,主要核心就是给递归给data
里面的数据设置get
和set
,然后设置数据代理,让 this.name
等同于 this._data.name
。设置完数据观察,我们就可以看到如下图的数据了。
console.log(vue.name) // 张三 console.log(vue.age) // 10
ps: 数组的数据观察大家自行去完善哈,这里重点讲的是watcher的实现。
首次渲染 数据观察搞定了之后,我们就可以把render
函数渲染到我们的界面上了。在Vue
里面我们有一个this.$mount()
函数,所以要实现Vue.prototype.$mount
函数:
// 挂载方法 Vue.prototype.$mount = function () { const vm = this new Watcher(vm, vm.$options .render, () => {}, true ) }
以上的代码终于牵扯到我们Watcher
这个主角了,这里其实就是我们的渲染wather
,这里的目的是通过Watcher
来实现执行render
函数,从而把数据插入到root节点里面去。下面看最简单的Watcher实现
let wid = 0 class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm // 把vm挂载到当前的this上 if (typeof exprOrFn === 'function' ) { this.getter = exprOrFn // 把exprOrFn挂载到当前的this上,这里exprOrFn 等于 vm.$options .render } this.cb = cb // 把cb挂载到当前的this上 this.options = options // 把options挂载到当前的this上 this.id = wid++ this.value = this.get() // 相当于运行 vm.$options .render() } get () { const vm = this.vm let value = this.getter.call(vm, vm) // 把this 指向到vm return value } }
通过上面的一顿操作,终于在render
中终于可以通过this.name
读取到data
的数据了,也可以插入到root.innerHTML
中去。阶段性的工作我们完成了。如下图,完成的首次渲染✌️
数据依赖收集和更新 首先数据收集,我们要有一个收集的地方,就是我们的Dep
类,下面呢看看我们去怎么实现这个Dep
。
// 依赖收集let dId = 0 class Dep{ constructor () { this.id = dId++ // 每次实例化都生成一个id this.subs = [] // 让这个dep实例收集watcher } depend () { // Dep.target 就是当前的watcher if (Dep.target) { Dep.target.addDep(this) // 让watcher,去存放dep,然后里面dep存放对应的watcher,两个是多对多的关系 } } notify () { // 触发更新 this.subs.forEach(watcher => watcher.update()) } addSub(watcher) { this.subs.push(watcher) } }let stack = [] // push当前watcher到stack 中,并记录当前watcerfunction pushTarget(watcher) { Dep.target = watcher stack.push(watcher) } // 运行完之后清空当前的watcherfunction popTarget () { stack.pop() Dep.target = stack[stack.length - 1] }
Dep
收集的类是实现了,但是我们怎么去收集了,就是我们数据观察的get
里面实例化Dep
然后让Dep
收集当前的watcher
。下面我们一步步来:
1、在上面this.$mount()
的代码中,我们运行了new Watcher(vm, vm.$options.render, () => {}, true)
,这时候我们就可以在Watcher
里面执行this.get()
,然后执行pushTarget(this)
,就可以执行这句话Dep.target = watcher
,把当前的watcher
挂载Dep.target
上。下面看看我们怎么实现。 class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm if (typeof exprOrFn === 'function' ) { this.getter = exprOrFn } this.cb = cb this.options = options this.id = wid++ this.id = wId++ + this.deps = [] + this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了 this.value = this.get() } get () { const vm = this.vm + pushTarget(this) let value = this.getter.call(vm, vm) // 执行函数 + popTarget() return value } + 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() + } }
2、知道Dep.target
是怎么来之后,然后上面代码运行了this.get()
,相当于运行了vm.$options.render
,在render
里面回执行this.name
,这时候会触发Object.defineProperty·get
方法,我们在里面就可以做些依赖收集(dep.depend)了,如下代码 function defineReactive(data, key, value) { let dep = new Dep() Object.defineProperty(data, key, { get () { + if (Dep.target) { // 如果取值时有watcher + dep.depend() // 让watcher保存dep,并且让dep 保存watcher,双向保存 + } return value }, set (newValue) { if (newValue == value) return observe(newValue) // 给新的值设置响应式 value = newValue + dep.notify() // 通知渲染watcher去更新 } }) // 递归给数据设置get set observe(value); }
3、调用的dep.depend()
实际上是调用了 Dep.target.addDep(this)
, 此时Dep.target
等于当前的watcher
,然后就会执行 addDep(dep) { let id = dep.id if (!this.depsId.has(id)) { this.depsId.add(id) this.deps.push(dep) // 当前的watcher收集dep dep.addSub(this); // 当前的dep收集当前的watcer } }
这里双向保存有点绕,大家可以好好去理解一下。下面我们看看收集后的des
是怎么样子的。
4、数据更新,调用this.name = '李四'
的时候回触发Object.defineProperty.set
方法,里面直接调用dep.notify()
,然后循环调用所有的watcer.update
方法更新所有watcher
,例如:这里也就是重新执行vm.$options.render
方法。 有了依赖收集个数据更新,我们也在index.html
增加修改data
属性的定时方法:
// index.html <button onClick='changeData()' >改变name和age</button> // ----- // .....省略代码function changeData () { vue.name = '李四' vue.age = 20 }
运行效果如下图
到这里我们渲染watcher
就全部实现了。
实现computed 首先我们在index.html
里面配置一个computed,script
标签的代码就如下:
const root = document.querySelector('#root' ) var vue = new Vue({ data () { return { name: '张三' , age: 10 } }, computed: { info () { return this.name + this.age } }, render () { root.innerHTML = `${this.name} ----${this.age} ----${this.info} ` } })function changeData () { vue.name = '李四' vue.age = 20 }
上面的代码,注意computed
是在render
里面使用了。
在vue.js中,之前写了下面这行代码。
if (options.computed) { // 初始化计算属性 initComputed(vm) }
我们现在就实现这个initComputed
,代码如下
// 初始化computedfunction initComputed(vm) { const computed = vm.$options .computed // 拿到computed配置 const watchers = vm._computedWatchers = Object.create(null) // 给当前的vm挂载_computedWatchers属性,后面会用到 // 循环computed每个属性 for (const key in computed) { const userDef = computed[key] // 判断是函数还是对象 const getter = typeof userDef === 'function' ? userDef : userDef.get // 给每一个computed创建一个computed watcher 注意{ lazy: true } // 然后挂载到vm._computedWatchers对象上 watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }) if (!(key in vm)) { defineComputed(vm, key, userDef) } } }
大家都知道computed
是有缓存的,所以创建watcher
的时候,会传一个配置{ lazy: true }
,同时也可以区分这是computed watcher
,然后到watcer
里面接收到这个对象
class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm if (typeof exprOrFn === 'function' ) { this.getter = exprOrFn } + if (options) { + this.lazy = !!options.lazy // 为computed 设计的 + } else { + this.lazy = false + } + this.dirty = this.lazy this.cb = cb this.options = options this.id = wId++ this.deps = [] this.depsId = new Set() + this.value = this.lazy ? undefined : this.get() } // 省略很多代码 }
从上面这句this.value = this.lazy ? undefined : this.get()
代码可以看到,computed
创建watcher
的时候是不会指向this.get
的。只有在render
函数里面有才执行。
现在在render
函数通过this.info
还不能读取到值,因为我们还没有挂载到vm上面,上面defineComputed(vm, key, userDef)
这个函数功能就是让computed
挂载到vm
上面。下面我们实现一下。
// 设置comoputed的 set 个set function defineComputed(vm, key, userDef) { let getter = null // 判断是函数还是对象 if (typeof userDef === 'function' ) { getter = createComputedGetter(key) } else { getter = userDef.get } Object.defineProperty(vm, key, { enumerable: true , configurable: true , get: getter, set : function () {} // 又偷懒,先不考虑set 情况哈,自己去看源码实现一番也是可以的 }) } // 创建computed函数function createComputedGetter(key) { return function computedGetter () { const watcher = this._computedWatchers[key] if (watcher) { if (watcher.dirty) {// 给computed的属性添加订阅watchers watcher.evaluate() } // 把渲染watcher 添加到属性的订阅里面去,这很关键 if (Dep.target) { watcher.depend() } return watcher.value } } }
上面代码有看到在watcher
中调用了watcher.evaluate()
和watcher.depend()
,然后去watcher
里面实现这两个方法,下面直接看watcher
的完整代码。
class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm if (typeof exprOrFn === 'function' ) { this.getter = exprOrFn } if (options) { this.lazy = !!options.lazy // 为computed 设计的 } else { this.lazy = false } this.dirty = this.lazy this.cb = cb this.options = options this.id = wId++ this.deps = [] this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了 this.value = this.lazy ? undefined : this.get() } get () { const vm = this.vm pushTarget(this) // 执行函数 let value = this.getter.call(vm, vm) popTarget() return value } addDep(dep) { let id = dep.id if (!this.depsId.has(id)) { this.depsId.add(id) this.deps.push(dep) dep.addSub(this); } } update (){ if (this.lazy) { this.dirty = true } else { this.get() } } // 执行get,并且 this.dirty = false + evaluate () { + this.value = this.get() + this.dirty = false + } // 所有的属性收集当前的watcer + depend () { + let i = this.deps.length + while (i--) { + this.deps[i].depend() + } + } }
代码都实现王完成之后,我们说下流程,
1、首先在render
函数里面会读取this.info
,这个会触发createComputedGetter(key)
中的computedGetter(key)
; 2、然后会判断watcher.dirty
,执行watcher.evaluate()
; 3、进到watcher.evaluate()
,才真想执行this.get
方法,这时候会执行pushTarget(this)
把当前的computed watcher
push到stack里面去,并且把Dep.target 设置成当前的
computed watcher`; 4、然后运行this.getter.call(vm, vm)
相当于运行computed
的info: function() { return this.name + this.age }
,这个方法; 5、info
函数里面会读取到this.name
,这时候就会触发数据响应式Object.defineProperty.get
的方法,这里name
会进行依赖收集,把watcer
收集到对应的dep
上面;并且返回name = '张三'
的值,age
收集同理; 6、依赖收集完毕之后执行popTarget()
,把当前的computed watcher
从栈清除,返回计算后的值('张三+10'),并且this.dirty = false
; 7、watcher.evaluate()
执行完毕之后,就会判断Dep.target
是不是true
,如果有就代表还有渲染watcher
,就执行watcher.depend()
,然后让watcher
里面的deps
都收集渲染watcher
,这就是双向保存的优势。 8、此时name
都收集了computed watcher
和 渲染watcher
。那么设置name
的时候都会去更新执行watcher.update()
9、如果是computed watcher
的话不会重新执行一遍只会把this.dirty
设置成 true
,如果数据变化的时候再执行watcher.evaluate()
进行info
更新,没有变化的的话this.dirty
就是false
,不会执行info
方法。这就是computed缓存机制。 实现了之后我们看看实现效果:
这里conputed的对象set配置没有实现,大家可以自己看看源码
watch实现 先在script标签配置watch配置如下代码:
const root = document.querySelector('#root' ) var vue = new Vue({ data () { return { name: '张三' , age: 10 } }, computed: { info () { return this.name + this.age } }, watch: { name(oldValue, newValue) { console.log(oldValue, newValue) } }, render () { root.innerHTML = `${this.name} ----${this.age} ----${this.info} ` } })function changeData () { vue.name = '李四' vue.age = 20 }
知道了computed
实现之后,自定义watch
实现很简单,下面直接实现initWatch
function initWatch(vm) { let watch = vm.$options .watch for (let key in watch) { const handler = watch[key] new Watcher(vm, key, handler, { user: true }) } }
然后修改一下Watcher,直接看Wacher的完整代码。
let wId = 0 class Watcher { constructor(vm, exprOrFn, cb, options) { this.vm = vm if (typeof exprOrFn === 'function' ) { this.getter = exprOrFn } else { + this.getter = parsePath(exprOrFn) // user watcher } if (options) { this.lazy = !!options.lazy // 为computed 设计的 + this.user = !!options.user // 为user wather设计的 } else { + this.user = this.lazy = false } this.dirty = this.lazy this.cb = cb this.options = options this.id = wId++ this.deps = [] this.depsId = new Set() // dep 已经收集过相同的watcher 就不要重复收集了 this.value = this.lazy ? undefined : this.get() } get () { const vm = this.vm pushTarget(this) // 执行函数 let value = this.getter.call(vm, vm) popTarget() return value } addDep(dep) { let id = dep.id if (!this.depsId.has(id)) { this.depsId.add(id) this.deps.push(dep) dep.addSub(this); } } update (){ if (this.lazy) { this.dirty = true } else { + this.run() } } // 执行get,并且 this.dirty = false evaluate () { this.value = this.get() this.dirty = false } // 所有的属性收集当前的watcer depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } + run () { + const value = this.get() + const oldValue = this.value + this.value = value // 执行cb + if (this.user) { + try{ + this.cb.call(this.vm, value, oldValue) + } catch(error) { + console.error(error) + } + } else { + this.cb && this.cb.call(this.vm, oldValue, value) + } + } }function parsePath (path) { const segments = path.split('.' ) return function (obj) { for (let i = 0; i < segments.length; i++) { if (!obj) return obj = obj[segments[i]] } return obj } }
最后看看效果
当然很多配置没有实现,比如说options.immediate
或者options.deep
等配置都没有实现。篇幅太长了。自己也懒~~~ 完结撒花
详细代码:https://github.com/naihe138/write-vue