从Vue源码角度深挖Watch、Computed

实例分析computed和watch

前言

这篇文章将带大家全面理解vue的watcher、computed和user watcher,其实computed和user watcher都是基于Watcher来实现的,我们通过一个一个功能点去敲代码,让大家全面理解其中的实现原理和核心思想。所以这篇文章将实现以下这些功能点:

  • 实现数据响应式
  • 基于渲染wather实现首次数据渲染到界面上
  • 数据依赖收集和更新
  • 实现数据更新触发渲染watcher执行,从而更新ui界面
  • 基于watcher实现computed
  • 基于watcher实现user watcher

准备工作

首先我们准备了一个index.html文件和一个vue.js文件,先看看index.html的代码


<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>全面理解vue的渲染watcher、computed和user atchertitle>
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函数渲染到页面中去。 下面是各个方法的实现:

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
}

首次渲染

利用this.$mount()挂载到Vue上:

// 挂载方法
Vue.prototype.$mount = function () {
  const vm = this
  new Watcher(vm, vm.$options.render, () => {}, true)
}

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
  }
}

数据依赖收集和触发

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]
}

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.然后上面代码运行了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
  }
}

4.数据更新,调用this.name = '李四’的时候回触发Object.defineProperty.set方法,里面直接调用dep.notify(),然后循环调用所有的watcer.update方法更新所有watcher,例如:这里也就是重新执行vm.$options.render方法。

实现Computed

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

// 初始化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()
    }
  }
}

主要流程:
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缓存机制。

实现watch

我们照样初始化实例

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:

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
  }
}

搞定!嘻嘻

转载:完全理解vue的渲染watcher、computed和user watcher

欢迎关注我的博客: https://blog.csdn.net/weixin_42323607

github地址: https://github.com/qdheyongjie

多多支持!本人会持续更新哒 ❤️

你可能感兴趣的:(vue,computed,watch,javascript,vue.js)