完全理解Vue的渲染watcher、computed和user watcher
source link: http://mp.weixin.qq.com/s?__biz=MzA4Nzg0MDM5Nw%3D%3D&%3Bmid=2247486369&%3Bidx=1&%3Bsn=96634ed1ab84c24b0d4ca4839f9d6d74
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
作者:Naice
https://segmentfault.com/a/1190000023196603
这篇文章将带大家全面理解 vue
的 watcher
、 computed
和 user watcher
,其实 computed
和 user watcher
都是基于 Watcher
来实现的,我们通过一个一个功能点去敲代码,让大家全面理解其中的实现原理和核心思想。所以这篇文章将实现以下这些功能点:
-
实现数据响应式
-
基于渲染
wather
实现首次数据渲染到界面上 -
数据依赖收集和更新
-
实现数据更新触发渲染
watcher
执行,从而更新ui界面 -
基于
watcher
实现computed
-
基于
watcher
实现user watcher
废话不要多说,先看下面的最终例子。
例子看完之后我们就直接开工了。
准备工作
首先我们准备了一个 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.name function 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
中去。阶段性的工作我们完成了。如下图,完成的首次渲染:v:
数据依赖收集和更新
首先数据收集,我们要有一个收集的地方,就是我们的 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 中,并记录当前watcer function pushTarget(watcher) { Dep.target = watcher stack.push(watcher) } // 运行完之后清空当前的watcher function popTarget() { stack.pop() Dep.target = stack[stack.length - 1] }
Dep
收集的类是实现了,但是我们怎么去收集了,就是我们数据观察的 get
里面实例化 Dep
然后让 Dep
收集当前的 watcher
。下面我们一步步来:
-
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() + } }
-
Dep.target this.get() vm.$options.render render this.name Object.defineProperty·get
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); }
-
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
是怎么样子的。
-
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
,代码如下
// 初始化computed function 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() + } + } }
代码都实现王完成之后,我们说下流程,
-
render this.info createComputedGetter(key) computedGetter(key)
-
2、然后会判断
watcher.dirty
,执行watcher.evaluate()
; -
watcher.evaluate() this.get pushTarget(this) computed watcher Dep.target 设置成当前的
-
this.getter.call(vm, vm) computed info: function() { return this.name + this.age }
-
info this.name Object.defineProperty.get name watcer dep name = '张三' age
-
popTarget() computed watcher this.dirty = false
-
watcher.evaluate() Dep.target true 渲染watcher watcher.depend() watcher deps 渲染watcher
-
name computed watcher 渲染watcher name watcher.update()
-
computed watcher this.dirty true watcher.evaluate() info this.dirty false info
实现了之后我们看看实现效果:
这里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
》》面试官都在用的题库,快来看看《《
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK