完全理解vue的渲染watchercomputed和user-watcher

1次阅读

共计 11724 个字符,预计需要花费 30 分钟才能阅读完成。

这篇文章将带大家全面了解 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 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 外面的数据设置 getset,而后设置数据代理,让 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) 相当于运行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 缓存机制。

实现了之后咱们看看实现成果:

这里 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

正文完
 0