「Vue源碼學習(四)」立志寫一篇人人都看的懂的computed,watch原理

前言

朋友們你們好,我是林三心,仍是那句話:改變不了,那就適應它,源碼的理解在當今前端市場愈來愈重要了,理解源碼,可使咱們在開發中更快地捕捉到問題所在,今天講到computed,watch的原理,我的建議朋友們先看這個系列的前幾篇文章,或許能更好地理解本章內容,固然,我會盡我所能讓你們能更好地理解computed,watch原理,我儘可能講的通俗易懂一些。大家不要嫌我囉嗦哦。
😂😂😂
「Vue源碼學習」文章:html

或者對個人其餘vue源碼文章感興趣的能夠看個人這些文章:前端

預計實現效果

20210615_220340.gif

知識前提

須要懂基本的npm命令,ES6語法,以及webpack基本打包vue

準備工做

1.建立一個文件夾

npm init

npm i @babel/core @babel/preset-env babel-loader clean-webpack-plugin html-webpack-plugin webpack webpack-cli webpack-dev-server -D
複製代碼

2.建立webpack.config.js文件

目的:配置熱更新打包node

// webpack.config.js

const path = require('path')
// 引入html-webpack-plugin插件
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 引入clean-webpack-plugin
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
// 引入webpack插件
const webpack = require('webpack');
module.exports = {
    mode: 'development',
    devtool: 'eval',
    devServer: {
        contentBase: './dist',
        open: true,
        hot: true
    },
    entry: './src/index.js',
    output: {
        filename: 'bundle.js',
        path: path.resolve(__dirname, '../dist')
    },
    module: {
        rules: [
            {
                test: /\.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({ // 往dist裏塞html而且把bundle搞進去
            template: './src/index.html'
        }),
        new CleanWebpackPlugin(), // 執行時間,在打包以前執行,改變輸出文件後,下一次打包能夠清除老文件
        new webpack.HotModuleReplacementPlugin() // 更新後不會刷新,保留後加的數據
    ]
}
複製代碼

3.package命令行修改

"scripts": {
    "dev": "webpack-dev-server --config ./webpack.config.js"
  },
複製代碼

4.建立.babelrc文件

// .babelrc

{
    "presets":["@babel/preset-env"]
}
複製代碼

5.建立src文件夾

目的:存放本章原理代碼webpack

6.最終目錄

image.png

7.Vue實例

// src/index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>林三心Vue源碼</title>
</head>

<body>
    <div id="root"></div>
</body>
</html>
複製代碼
// src/index.js

import Vue from './init.js'

const root = document.querySelector('#root')
var vue = new Vue({
    data() {
        return {
            name: '林三心',
            age: 18
        }
    },
    render() {
        root.innerHTML = `${this.name}今年${this.age}歲了 } }) 複製代碼

8. 如何調試

npm run dev
而後將index.html在谷歌瀏覽器裏打開(live server)
複製代碼

Watcher是什麼?Watcher的種類有哪些?

你們要注意,這裏說的是Watcher,要跟vue裏使用的watch屬性區分一下哦web

1.什麼是Watcher呢?

舉個例子,請看下面代碼:面試

// 例子代碼,與本章代碼無關

<div>{{name}}</div>

data() {
        return {
            name: '林三心'
        }
    },
    computed: {
        info () {
            return this.name
        }
    },
    watch: {
        name(newVal) {
            console.log(newVal)
        }
    }
複製代碼

上方代碼可知,name變量被三處地方所依賴,分別是html裏,computed裏,watch裏。只要name一改變,html裏就會從新渲染,computed裏就會從新計算,watch裏就會從新執行。那麼是誰去通知這三個地方name修改了呢?那就是Watchernpm

2.Watcher的種類有哪些呢?

上面所說的三處地方就剛恰好表明了三種Watcher,分別是:數組

  • 渲染Watcher:變量修改時,負責通知HTML裏的從新渲染
  • computed Watcher:變量修改時,負責通知computed裏依賴此變量的computed屬性變量的修改
  • user Watcher:變量修改時,負責通知watch屬性裏所對應的變量函數的執行

實現數據響應式

任何類型的Watcher都是基於數據響應式的,也就是說,要想實現Watcher,就須要先實現數據響應式,而數據響應式的原理就是經過Object.defineProperty去劫持變量的getset屬性瀏覽器

這裏的響應式只作了簡單的響應式處理,若是想看詳細的,請移步「Vue源碼學習(一)」你不知道的-數據響應式原理,也就是此係列的第一篇。

1.初始化Vue

// src/init.js

import initState from './initState.js'
import initComputed from './initComputed.js'
import initWatch from './initWatch'
import Watcher from './Watcher.js'

export default function Vue(options) {

    // 初始化函數
    this._init(options)
}

Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options
    if (options.data) {
        // 初始化數據
        initState(vm)
    }
}
複製代碼

2.什麼是Dep?

Dep是什麼呢?舉個例子,仍是以前的例子代碼:

// 例子代碼,與本章代碼無關

<div>{{name}}</div>

data() {
        return {
            name: '林三心'
        }
    },
    computed: {
        info () {
            return this.name
        }
    },
    watch: {
        name(newVal) {
            console.log(newVal)
        }
    }
複製代碼

這裏name變量被三個地方所依賴,三個地方表明了三種Watcher,那麼name會直接本身管這三個Watcher嗎?答案是不會的,name會實例一個Dep,來幫本身管這幾個Wacther,相似於管家,當name更改的時候,會通知dep,而dep則會帶着主人的命令去通知這些Wacther去完成本身該作的事

3.響應式實現

// src/initState.js

import { Dep } from "./Dep"

export default 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--) {
        // 變量代理
        // 這樣作的好處就是操做data裏的變量時,只須要this.xxx而不用this.data.xxx
        proxy(vm, '_data', keys[i])
    }
    observe(data)
}

class Observer {
    constructor(value) {
        this.walk(value)
    }

    walk(data) {
        let keys = Object.keys(data)
        // 遍歷data的key,並進行響應式判斷處理
        for (let i = 0; i < keys.length; i++) {
            defineReactive(data, keys[i], data[keys[i]])
        }
    }
}

function defineReactive(data, key, value) {
    // 每一個對象都有本身dep
    const dep = new Dep()
    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) {
                // 若是Dep.target指向某個Watcher,則把此Watcher收入此dep的隊列裏
                dep.depend()
            }
            return value
        },
        set(newVal) {
            // 設置值時,若是相等則返回
            if (newVal === value) return
            value = newVal
            // 新設置的值也須要響應式判斷處理
            observe(newVal)

            // 通知dep裏的全部Wacther進行傳達更新
            dep.notify()
        }
    })

    // 遞歸,由於可能對象裏有對象
    observe(value)
}

function observe(data) {
    // 只有當data爲數組或者對象時才進行響應式處理
    if (typeof data === 'object' && data !== null) {
        return new Observer(data)
    }
}

// 代理函數
function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[source][key]
        },
        set(newVal) {
            return vm[source][key] = newVal
        }
    })
}
複製代碼
// src/Dep.js
let dId = 0

export class Dep {
    constructor() {
        // 每一個dep的id都是獨一無二的
        this.id = dId++
        // 用來存儲Watcher的數組
        this.subs = []
    }

    depend() {
        if (Dep.target) {
            // 此時Dep.target指向的是某個Wacther,Wacther也要把此dep給收集起來
            Dep.target.addDep(this)
        }
    }

    notify() {
        // 通知subs裏的每一個Wacther都去通知更新
        const tempSubs = this.subs.slice()
        tempSubs.reverse().forEach(watcher => watcher.update())
    }

    addSub(watcher) {
        // 將Watcher收進subs裏
        this.subs.push(watcher)
    }
}

let stack = []
export function pushTarget(watcher) {
    // 改變target的指向
    Dep.target = watcher
    stack.push(watcher)
}

export function popTarget() {
    stack.pop()
    Dep.target = stack[stack.length - 1]
}

複製代碼

4.Watcher爲什麼也要反過來收集Dep?

上面說到了,dep是name的管家,他的職責是:name更新時,dep會帶着主人的命令去通知subs裏的Watcher去作該作的事,那麼,dep收集Watcher很合理。那爲何watcher也須要反過來收集dep呢?這是由於computed屬性裏的變量沒有本身的dep,也就是他沒有本身的管家,看如下例子:

這裏先說一個知識點:若是html裏不依賴name這個變量,那麼不管name再怎麼變,他都不會主動去刷新視圖,由於html沒引用他(說專業點就是:namedep裏沒有渲染Watcher),注意,這裏說的是不會主動,但這並不表明他不會被動去更新。什麼狀況下他會被動去更新呢?那就是computed有依賴他的屬性變量。

// 例子代碼,與本章代碼無關

<div>{{person}}</div>

computed: {
    person {
        return `名稱:${this.name}`
        }
    }
複製代碼

這裏的person事依賴於name的,可是person是沒有本身的dep的(由於他是computed屬性變量),而name是有的。好了,繼續看,請注意,此例子html裏只有person的引用沒有name的引用,因此name一改變,按理說雖然person跟着變了,可是html不會從新渲染,由於name雖然有dep,有更新視圖的能力,可是奈何人家html不引用他啊!person想要本身去更新視圖,但他卻沒這個能力啊,畢竟他沒有dep這個管家!這個時候computed Watcher裏收集的namedep就派上用場了,能夠藉助這些dep去更新視圖,達到更新html裏的person的效果。具體會在下面computed裏實現。

5.邏輯有點繞

這裏邏輯確實有點繞,由於dep和watcher互相採集,你們在調試過程當中能夠本身console.log一下depsubs看看,這樣會更能看清邏輯。這裏能夠看到,dep收集watcher,而watcher也會反過來收集dep。 此時輸出了兩個dep,由於有nameage,一個變量有一個dep因此總共兩個dep,因爲這兩個變量都被html所依賴,因此每一個depsubs裏都收集了渲染Watcher,反過來,渲染Watcher也要收集這兩個dep,如圖:

image.png

實現Watcher

// src/Watcher.js

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
  }
}
複製代碼

首次渲染(渲染Watcher)

上面說過,只要把render函數傳進Wacther,那麼此Watcher渲染Watcher渲染Watcher的做用是:首次渲染,而且HTML模板所依賴的變量改變時也會從新渲染。

export default function Vue(options) {

    // 初始化函數
    this._init(options)

    // 渲染函數
    this.$mount()
}

Vue.prototype.$mount = function() {
    const vm = this
    // 建立渲染Watcher
    // 這裏第二個參數傳render函數進去,則此Watcher爲渲染Watcher
    // 由於在此例子裏render爲渲染dom的函數
    new Watcher(vm, vm.$options.render, () => {}, true)
}
複製代碼

此時在終端裏運行npm run dev,並live server打開index.html文件,看到如下效果,則證實首次渲染成功:

image.png

更新數據

如今的數據是死的,那咱們如何改變呢?

// src/index.html
<body>
    <div id="root"></div>
    <button id="btn1">改變name</button>
    <button id="btn2">改變age</button>
</body>

// src/index.js
const root = document.querySelector('#root')
var vue = new Vue({
    data() {
        return {
            name: '林三心',
            age: 18
        }
    },
    render() {
        root.innerHTML = `${this.name}今年${this.age}歲了`
    }
})

document.getElementById('btn1').onclick = () => {
    vue.name = 'sunshine_Lin'
}
document.getElementById('btn2').onclick = () => {
    vue.age = 20
}
複製代碼

由本章以前內容代碼可知,當data裏的變量被改變時,會觸發Object.definePropertyset屬性,直接改變數據層的的數據,可是問題來了,數據是修改了,那視圖該怎麼更新呢?這時候dep就排上用場了,dep會觸發notify方法,通知渲染Watcher去更新視圖(此時dep裏只有一個Watcher,後續會更多),效果如圖:

845bd71610cbe1c567506c62e64b2245 (1).gif

實現computed

1.代碼實現

修改一下代碼:

// src/index.js
const root = document.querySelector('#root')
var vue = new Vue({
    data() {
        return {
            name: '林三心',
            age: 18
        }
    },
    computed: { // 新增
        info() {
            return this.name + this.age
        }

    },
    render() {
        root.innerHTML = `${this.name}今年${this.age}歲了-----${this.info}` // 新增info
    }
})
複製代碼
// src/init.js

import initState from './initState.js'
import initComputed from './initComputed.js'

Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options
    if (options.data) {
        // 初始化數據
        initState(vm)
    }
    if (options.computed) { // 新增
        // 初始化computed
        initComputed(vm)
    }
}
複製代碼

咱們須要在這個initComputed方法裏實現computed的邏輯

// src/initComputed.js

import { Dep } from "./Dep"
import Watcher from "./Watcher"

export default 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,而後到watcher裏面接收到這個對象

// src/Watcher.js


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上面。下面咱們實現一下。

// src/initComputed.js


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多了evaluatedepend兩個方法,讓咱們去實現一下吧,如下是此時Watcher的完整代碼:

import { pushTarget, popTarget } from './Dep'

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()
    }
  }
}
複製代碼

2.流程講解

  • 1.首次渲染會執行render函數,render函數裏會讀取info變量,這個會觸發createComputedGetter(key)中的computedGetter(key)
  • 2.而後會判斷dirty這個變量,看是否須要從新計算,如需從新計算則執行watcher.evaluate
  • 3.在watcher.evaluate方法中,執行了this.get方法,這時候會執行pushTarget(this)把當前的computed watcher push到stack裏面去,而且把Dep.target 設置成當前的computed watcher
  • 4.運行this.getter.call(vm, vm),也就是運行了info() {return this.name + this.age}這個函數
  • 5.執行info函數後,函數裏會讀取nameage兩個變量,那麼就會觸發兩次Object.defineProperty.get的方法,那麼nameage二者的dep都會把此computed Watcher收集起來
  • 6.依賴收集完畢以後執行popTarget(),把當前的computed watcher從棧清除,返回計算後的值('林三心' + '18'),而且this.dirty = false
  • 7.watcher.evaluate()執行完畢以後,就會判斷Dep.target 是否是true,若是有就表明還有渲染watcher,就執行watcher.depend(),而後讓watcher裏面的deps都收集渲染watcher,這就是雙向保存的優點。
  • 8.此時nameage都收集了computed watcher渲染watcher。那麼設置name的時候都會去更新執行watcher.update(),age也同理
  • 9.若是是computed watcher的話不會從新執行一遍只會把this.dirty 設置成 true,若是數據變化的時候再執行watcher.evaluate()進行info更新,沒有變化的的話this.dirty 就是false,不會執行info方法。這就是computed緩存機制

看看此時的效果:

38980e0ca8c5f6ee438aa4981c01ac21.gif

watch的實現

修改一下代碼:

// src/index.js

const root = document.querySelector('#root')
var vue = new Vue({
    data() {
        return {
            name: '林三心',
            age: 18
        }
    },
    computed: {
        info() {
            return this.name + this.age
        }

    },
    watch: {
        name(oldValue, newValue) {
            console.log('觸發watch', oldValue, newValue)
        },
        age(oldValue, newValue) {
            console.log('觸發watch', oldValue, newValue)
        }
    },
    render() {
        root.innerHTML = `${this.name}今年${this.age}歲了-----${this.info}`
    }
})
複製代碼
// src/init.js

Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options
    if (options.data) {
        // 初始化數據
        initState(vm)
    }
    if (options.computed) {
        // 初始化computed
        initComputed(vm)
    }
    if (options.watch) {
        // 初始化watch
        initWatch(vm)
    }
}
複製代碼

實現一下initWatch:

// src/initWatch.js

import Watcher from './Watcher'

export default function initWatch (vm) {
    const watch = vm.$options.watch
    for(let key in watch) {
        const handler = watch[key]
        new Watcher(vm, key, handler, {user: true})
    }
}
複製代碼

修改一下Watcher.js的代碼

// src/Watcher.js

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
  }
}
複製代碼

最後來看看效果:

6ff71566df6adac8414a885725a61f8c.gif

結語

加油,各位!!!點贊,點起來

相關文章
相關標籤/搜索