理解vue的渲染watch、compute

这篇文章将带大家全面理解vue渲染watchercomputeduser watcher,其实computeduser 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></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代码里面就是执行this._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[source][key] // this.name 等同于 this._data.name

},

set(newValue) {

return vm[source][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里面的数据设置getset,然后设置数据代理,让 this.name 等同于 this._data.name。设置完数据观察,我们就可以看到如下图的数据了。

console.log(vue.name) // 张三

console.log(vue.age) // 10

首次渲染

数据观察搞定了之后,我们就可以把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) 相当于运行computedinfo: 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缓存机制。

实现了之后我们看看实现效果:

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等配置都没有实现。篇幅太长了。自己也懒~~~

以上是 理解vue的渲染watch、compute 的全部内容, 来源链接: utcz.com/a/31880.html

回到顶部