这篇文章将带大家全面了解 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)
// 执行 render 函数
this.$mount()}
Vue.prototype._init = function (options) {
const vm = this
// 把 options 挂载到 this 上
vm.$options = options
if (options.data) {
// 数据响应式
initState(vm)
}
if (options.computed) {
// 初始化计算属性
initComputed(vm)
}
if (options.watch) {
// 初始化 watch
initWatch(vm)
}
}
vue.js
代码外面就是执行 t his._init()
和 this.$mount()
,而后_init
的办法就是对咱们的传进来的配置进行各种初始化,包含数据初始化 initState(vm)
、计算属性初始化initComputed(vm)
、自定义 watch 初始化initWatch(vm)
。this.$mount
办法把 render
函数渲染到页面中去、这些办法咱们前面都写到,先让让大家理解整个代码构造。上面咱们正式去填满咱们下面写的这些办法。
实现数据响应式
要实现这些 watcher
首先去实现数据响应式,也就是要实现下面的 initState(vm)
这个函数。置信大家都很相熟响应式这些代码,上面我间接贴上来。
function initState(vm) {
// 拿到配置的 data 属性值
let data = vm.$options.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[key] // this.name 等同于 this._data.name
},
set(newValue) {return vm[key] = newValue
}
})
}
class Observer{constructor(value) {
// 给每一个属性都设置 get set
this.walk(value)
}
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]
// 给对象设置 get set
defineReactive(data, key, value)
}
}
}
function defineReactive(data, key, value) {
Object.defineProperty(data, key, {get() {return value},
set(newValue) {if (newValue == value) return
observe(newValue) // 给新的值设置响应式
value = newValue
}
})
// 递归给数据设置 get set
observe(value);
}
重要的点都在正文外面,次要外围就是给递归给 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 中,并记录以后 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
。上面咱们一步步来:
- 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
,代码如下
// 初始化 computed
function initComputed(vm) {
// 拿到 computed 配置
const computed = vm.$options.computed
// 给以后的 vm 挂载_computedWatchers 属性,前面会用到
const watchers = vm._computedWatchers = Object.create(null)
// 循环 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://blog.naice.me/article