Handwritten Vue2 series of computed

When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

cover

foreword

Previous article Handwritten Vue2 series patch - diff implements the DOM diff process and completes the update of page responsive data.

Target

The goal of this article is to implement computed properties and complete the display of computed properties in the template. Knowledge points involved:

  • The nature of computed properties

  • Caching principle of computed properties

accomplish

The next step is to implement computed computed properties.

_init

/src/index.js

/**
 * 初始化配置对象
 * @param {*} options 
 */
Vue.prototype._init = function (options) {
  // ...
  // 初始化 options.data
  // 代理 data 对象上的各个属性到 Vue 实例
  // 给 data 对象上的各个属性设置响应式能力
  initData(this)
  // 初始化 computed 选项,并将计算属性代理到 Vue 实例上
  // 结合 watcher 实现缓存
  initComputed(this)
  // 安装运行时的渲染工具函数
  renderHelper(this)
  // ...
}

initComputed

/src/initComputed.js

/**
 * 初始化 computed 配置项
 * 为每一项实例化一个 Watcher,并将其 computed 属性代理到 Vue 实例上
 * 结合 watcher.dirty 和 watcher.evalute 实现 computed 缓存
 * @param {*} vm Vue 实例
 */
export default function initComputed(vm) {
  // 获取 computed 配置项
  const computed = vm.$options.computed
  // 记录 watcher
  const watcher = vm._watcher = Object.create(null)
  // 遍历 computed 对象
  for (let key in computed) {
    // 实例化 Watcher,回调函数默认懒执行
    watcher[key] = new Watcher(computed[key], { lazy: true }, vm)
    // 将 computed 的属性 key 代理到 Vue 实例上
    defineComputed(vm, key)
  }
}

defineComputed

/src/initComputed.js

/**
 * 将计算属性代理到 Vue 实例上
 * @param {*} vm Vue 实例
 * @param {*} key computed 的计算属性
 */
function defineComputed(vm, key) {
  // 属性描述符
  const descriptor = {
    get: function () {
      const watcher = vm._watcher[key]
      if (watcher.dirty) { // 说明当前 computed 回调函数在本次渲染周期内没有被执行过
        // 执行 evalute,通知 watcher 执行 computed 回调函数,得到回调函数返回值
        watcher.evalute()
      }
      return watcher.value
    },
    set: function () {
      console.log('no setter')
    }
  }
  // 将计算属性代理到 Vue 实例上
  Object.defineProperty(vm, key, descriptor)
}

Watcher

/src/watcher.js

/**
 * @param {*} cb 回调函数,负责更新 DOM 的回调函数
 * @param {*} options watcher 的配置项
 */
export default function Watcher(cb, options = {}, vm = null) {
  // 备份 cb 函数
  this._cb = cb
  // 回调函数执行后的值
  this.value = null
  // computed 计算属性实现缓存的原理,标记当前回调函数在本次渲染周期内是否已经被执行过
  this.dirty = !!options.lazy
  // Vue 实例
  this.vm = vm
  // 非懒执行时,直接执行 cb 函数,cb 函数中会发生 vm.xx 的属性读取,从而进行依赖收集
  !options.lazy && this.get()
}

watcher.get

/src/watcher.js

/**
 * 负责执行 Watcher 的 cb 函数
 * 执行时进行依赖收集
 */
Watcher.prototype.get = function () {
  pushTarget(this)
  this.value = this._cb.apply(this.vm)
  popTarget()
}

watcher.update

/src/watcher.js

/**
 * 响应式数据更新时,dep 通知 watcher 执行 update 方法,
 * 让 update 方法执行 this._cb 函数更新 DOM
 */
Watcher.prototype.update = function () {
  // 通过 Promise,将 this._cb 的执行放到 this.dirty = true 的后面
  // 否则,在点击按钮时,computed 属性的第一次计算会无法执行,
  // 因为 this._cb 执行的时候,会更新组件,获取计算属性的值的时候 this.dirty 依然是
  // 上一次的 false,导致无法得到最新的的计算属性的值
  // 不过这个在有了异步更新队列之后就不需要了,当然,毕竟异步更新对象的本质也是 Promise
  Promise.resolve().then(() => {
    this._cb()
  })
  // 执行完 _cb 函数,DOM 更新完毕,进入下一个渲染周期,所以将 dirty 置为 false
  // 当再次获取 计算属性 时就可以重新执行 evalute 方法获取最新的值了
  this.dirty = true
}

watcher.evalute

/src/watcher.js

Watcher.prototype.evalute = function () {
  // 执行 get,触发计算函数 (cb) 的执行
  this.get()
  // 将 dirty 置为 false,实现一次刷新周期内 computed 实现缓存
  this.dirty = false
}

pushTarget

/src/dep.js

// 存储所有的 Dep.target
// 为什么会有多个 Dep.target?
// 组件会产生一个渲染 Watcher,在渲染的过程中如果处理到用户 Watcher,
// 比如 computed 计算属性,这时候会执行 evalute -> get
// 假如直接赋值 Dep.target,那 Dep.target 的上一个值 —— 渲染 Watcher 就会丢失
// 造成在 computed 计算属性之后渲染的响应式数据无法完成依赖收集
const targetStack = []

/**
 * 备份本次传递进来的 Watcher,并将其赋值给 Dep.target
 * @param {*} target Watcher 实例
 */
export function pushTarget(target) {
  // 备份传递进来的 Watcher
  targetStack.push(target)
  Dep.target = target
}

popTarget

/src/dep.js

/**
 * 将 Dep.target 重置为上一个 Watcher 或者 null
 */
export function popTarget() {
  targetStack.pop()
  Dep.target = targetStack[targetStack.length - 1]
}

result

Well, at this point, the Vue computed property implementation is complete. If you can see the following renderings, it means everything is normal.

Video address: https://gitee.com/liyongning/typora-image-bed/raw/master/202203161832189.image

Jun-20-2021 10-50-02.gif

It can be seen that the computed properties in the page have been displayed normally, and they can also be updated responsively and have the ability to cache (view the computed output through the console).

At this point, the last part of the handwritten Vue series is left - the asynchronous update queue of the handwritten Vue series .

Link

Thank you for your attention , likes , favorites and comments , see you in the next issue.


When learning becomes a habit, knowledge becomes common sense. Thank you for your attention , likes , favorites and comments .

New videos and articles will be sent on the WeChat public account as soon as possible, welcome to follow: Li Yongning lyn

The article has been included in the github repository liyongning/blog . Welcome to Watch and Star.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324118336&siteId=291194637