朋友們你們好,我是林三心,仍是那句話:改變不了,那就適應它
,源碼的理解在當今前端市場愈來愈重要了,理解源碼,可使咱們在開發中更快地捕捉到問題所在,今天講到computed,watch的原理
,我的建議朋友們先看這個系列的前幾篇文章,或許能更好地理解本章內容,固然,我會盡我所能讓你們能更好地理解computed,watch原理
,我儘可能講的通俗易懂一些。大家不要嫌我囉嗦哦。
😂😂😂
「Vue源碼學習」
文章:html
或者對個人其餘vue源碼文章
感興趣的能夠看個人這些文章:前端
須要懂基本的npm命令,ES6語法,以及webpack基本打包vue
npm init
npm i @babel/core @babel/preset-env babel-loader clean-webpack-plugin html-webpack-plugin webpack webpack-cli webpack-dev-server -D
複製代碼
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() // 更新後不會刷新,保留後加的數據
]
}
複製代碼
"scripts": {
"dev": "webpack-dev-server --config ./webpack.config.js"
},
複製代碼
.babelrc
文件// .babelrc
{
"presets":["@babel/preset-env"]
}
複製代碼
目的:存放本章原理代碼webpack
// 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}歲了 } }) 複製代碼
npm run dev
而後將index.html在谷歌瀏覽器裏打開(live server)
複製代碼
你們要注意,這裏說的是
Watcher
,要跟vue裏使用的watch
屬性區分一下哦web
舉個例子,請看下面代碼:面試
// 例子代碼,與本章代碼無關
<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
修改了呢?那就是Watcher
了npm
上面所說的三處地方就剛恰好表明了三種Watcher
,分別是:數組
渲染Watcher
:變量修改時,負責通知HTML裏的從新渲染computed Watcher
:變量修改時,負責通知computed裏依賴此變量的computed屬性變量的修改user Watcher
:變量修改時,負責通知watch屬性裏所對應的變量函數的執行任何類型的
Watcher
都是基於數據響應式
的,也就是說,要想實現Watcher
,就須要先實現數據響應式
,而數據響應式
的原理就是經過Object.defineProperty
去劫持變量的get
和set
屬性瀏覽器
這裏的響應式只作了簡單的響應式處理,若是想看詳細的,請移步「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)
}
}
複製代碼
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
去完成本身該作的事
// 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]
}
複製代碼
上面說到了,dep是name
的管家,他的職責是:name
更新時,dep會帶着主人的命令去通知subs裏的Watcher
去作該作的事,那麼,dep收集Watcher
很合理。那爲何watcher也須要反過來收集dep呢?這是由於computed屬性裏的變量沒有本身的dep,也就是他沒有本身的管家,看如下例子:
這裏先說一個知識點:若是html裏不依賴
name
這個變量,那麼不管name
再怎麼變,他都不會主動
去刷新視圖,由於html沒引用他(說專業點就是:name
的dep
裏沒有渲染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
裏收集的name
的dep
就派上用場了,能夠藉助這些dep
去更新視圖,達到更新html裏的person
的效果。具體會在下面computed裏實現。
這裏邏輯確實有點繞,由於dep和watcher互相採集,你們在調試過程當中能夠本身console.log
一下dep
的subs
看看,這樣會更能看清邏輯。這裏能夠看到,dep
收集watcher
,而watcher
也會反過來收集dep
。 此時輸出了兩個dep
,由於有name
和age
,一個變量有一個dep
因此總共兩個dep
,因爲這兩個變量都被html所依賴,因此每一個dep
的subs
裏都收集了渲染Watcher
,反過來,渲染Watcher
也要收集這兩個dep
,如圖:
// 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
}
}
複製代碼
上面說過,只要把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文件,看到如下效果,則證實首次渲染成功:
如今的數據是死的,那咱們如何改變呢?
// 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.defineProperty
的set
屬性,直接改變數據層的的數據,可是問題來了,數據是修改了,那視圖該怎麼更新呢?這時候dep
就排上用場了,dep會觸發notify
方法,通知渲染Watcher
去更新視圖(此時dep裏只有一個Watcher
,後續會更多),效果如圖:
修改一下代碼:
// 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
多了evaluate
和depend
兩個方法,讓咱們去實現一下吧,如下是此時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()
}
}
}
複製代碼
render
函數,render
函數裏會讀取info
變量,這個會觸發createComputedGetter(key)
中的computedGetter(key)
;dirty
這個變量,看是否須要從新計算,如需從新計算則執行watcher.evaluate
watcher.evaluate
方法中,執行了this.get
方法,這時候會執行pushTarget(this)
把當前的computed watcher
push到stack
裏面去,而且把Dep.target
設置成當前的computed watcher
this.getter.call(vm, vm)
,也就是運行了info() {return this.name + this.age}
這個函數info函數
後,函數裏會讀取name
和age
兩個變量,那麼就會觸發兩次Object.defineProperty.get
的方法,那麼name
和age
二者的dep都會把此computed Watcher
收集起來popTarget()
,把當前的computed watcher
從棧清除,返回計算後的值('林三心' + '18'),而且this.dirty = false
watcher.evaluate()
執行完畢以後,就會判斷Dep.target
是否是true
,若是有就表明還有渲染watcher
,就執行watcher.depend()
,而後讓watcher
裏面的deps
都收集渲染watcher
,這就是雙向保存的優點。name
和age
都收集了computed watcher
和 渲染watcher
。那麼設置name
的時候都會去更新執行watcher.update(),age
也同理computed watcher
的話不會從新執行一遍只會把this.dirty
設置成 true
,若是數據變化的時候再執行watcher.evaluate()
進行info
更新,沒有變化的的話this.dirty
就是false
,不會執行info方法。這就是computed緩存機制
。看看此時的效果:
修改一下代碼:
// 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
}
}
複製代碼
最後來看看效果:
加油,各位!!!點贊,點起來