本文着重讲解vue2.0的双向绑定,computed,watch,异步更新队列内容,不涉及模版渲染相关内容,但是为了demo能够体现这种功能功能,简单利用模版字符串和正则渲染数据。废话不多说,开始正文
demo结构
为了让大家了解相关功能,先看实例化结构,然后再一步步实现它
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
age: 1
}
},
mounted() {
this.say()
},
methods: {
say() {
this.age = 99
}
},
watch: {
age: {
handler(nv, ov) {
console.log(nv, ov, 'change')
}
}
},
computed: {
ageName() {
return this.age + '我的名字'
}
},
template: `
<div>{{ageName}}</div>
`
})
实例化
首先我们可以看出MyVue是构造函数,并且接收一个参数。所以第一步非常简单,创建一个构造函数并保存参数
// src/index.js
import { initMixin } from './init.js'
function MyVue(options) {
this._init(options)
}
initMixin(MyVue)
export default MyVue
// src/init.js
import { initState } from './state.js'
export function initMixin(MyVue) {
MyVue.prototype._init = function(options) {
const vm = this
vm.$options = options
// 初始化各种属性
initState(vm)
}
}
初始化函数(methods)
想想vue中method的几个特点
- 可以直接使用this调用,例如this.sayAge()
- function内部可以使用this调用data,例如this.age = 11
基于这2点我们可以理解为method里的所有函数都挂载在MyVue上,并且this指向MyVue
// src/state.js
export function initState(vm) {
const opts = vm.$options
if(opts.methods) initMethods(vm, opts.methods)
}
function initMethods(vm, methods) {
// 循环methods对象
for(let key in methods) {
const fn = methods[key]
// 判断是否是函数,不是的话抛出提示
if(typeof fn !== 'function') {
console.log(`${key} not function`)
}
// 把函数挂载到MyVue上,并且使函数的this指向MyVue
vm[key] = fn !== 'function' ? () => {} : fn.bind(vm)
}
}
到这里应该会有疑惑,函数的this绑定到MyVue后也没法通过this.xxx获取data里的数据啊,我们接着往下看~
响应式数据(observeData)
data初始化
首先声明一点,vue组件data必须是一个函数(new Vue可以是对象),不然可能会发生组件间数据引用问题,具体可以看官方文档解释。 然后我们也先想一下data的特性
- 可能是函数也可能是对象,data函数内可以通过this直接调用methods函数
- key不可以与methods中函数重名
- 在生命周期或者methods函数等地方都可以用this获取data里的数据
- 响应式
接下来先一步步实现如上特性
// src/state.js
import { observe } from './observe.js'
export function initState(vm) {
const opts = vm.$options
if(opts.methods) initMethods(vm, opts.methods)
// 初始化data
if(opts.data) initData(vm, opts.data)
}
function initData(vm, data) {
const methods = vm.$options.methods
// 判断data是否是function,function就利用call绑定this到MyVue并且求值,实现了特性1
// vm._data里也保存一份data值,后续代理使用
data = vm._data = typeof data === 'function' ? data.call(vm) : data || {}
for(let key in data) {
// 判断是否和methods重名
if(methods && methods.hasOwnProperty(key)) {
console.log(`data ${key}和methods函数重名了`)
}
// 代理
proxy(vm, '_data', key)
}
// 响应式
observe(data)
}
我们在vm实例里保存了一份_data数据,主要是为了做一层代理,通过Object.defineProperty使this.xxx可以访问到this._data.xxx的数据
// src/state.js
function proxy(vm, source, key) {
// this.name -> this._data.name
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
// this = vm
return this[source][key]
},
set(val) {
this[source][key] = val
}
})
}
这时如果log一下vm,可以直接看到data里的属性直接挂载在vm上了(proxy的原因),所以可以直接使用this.xxx获取this._data.xxx里的值,这时上一章的methods里函数可以直接用this访问数据也得了解释。 接下来到了这一章的重点,响应式
响应式
首先说明为了性能,vue的响应式实现区分了对象和数组。因为很多时候数组中的数据非常大,而Object.defineProperty只能通过遍历的形式为每个key添加响应式,这对性能的开销是非常大的。 所以在实现上我们需要区分数组还是对象,首先我们先实现对象的响应式,依旧使用Object.defineProperty劫持对象中的每一个属性
// src/observe.js
export function observe(data) {
// data不是数组或对象的话不处理
if(!Array.isArray(data) && data.constructor !== Object) {
return
}
let ob = new Observer(data)
// 小细节,后面说
return ob
}
class Observer{
constructor(data) {
// 数组,先不处理
if(Array.isArray(data)) {
this.observeArray(data)
} else {
// 对象就遍历
this.walk(data)
}
}
walk(obj) {
const keys = Object.keys(obj)
// 遍历对象
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray() {
}
}
// 响应式
function defineReactive(data, key) {
let val = data[key]
// 递归,因为data可能是嵌套的{a:{b:1,c:2}}
let childOb = observe(val)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// get劫持
console.log(`get ${key} ${val}`)
return val
},
set(newVal) {
// set劫持,如果是相同的值,直接过滤
if(newVal === val) return
console.log(`set ${key} ${newVal}`)
val = newVal
}
})
}
这时我们写个demo验证一下get和set是不是执行了,修改一下initMixin和index.js,新增mounted验证是否劫持
import { initState } from './state.js'
export function initMixin(MyVue) {
MyVue.prototype._init = function(options) {
const vm = this
vm.$options = options
initState(vm)
// 新增这一段,执行mounted方法
vm.$options.mounted.call(vm)
}
}
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
age: 20
}
},
mounted() {
console.log(this.age)
this.age = 21
},
})
//
// get age 20
// 20
// set age 21
log可以看出,我们的属性劫持成功。 其实后面我们该说到Dep和Watcher,但是如果直接说这部分又不太连贯,因为涉及到渲染Wachter,而且为了更好的演示效果,所以先到下一章,再回到这一部分后续。
字符串模版初始渲染(template)
这一部分因为没有去研究vue的模版渲染的核心原理,所以只是简单的用模版字符串和正则的方法简单实现了一下功能。先说一下实现步骤吧
- 初始化state后去执行$mounted
- 获取el(挂载节点)和template(模版字符串)
- 根据template获取render方法,把模版字符串中{{xxx}}转为data内数据
- 创建渲染Watcher,执行render方法,把dom节点塞入el内
- 执行mounted生命周期
获取render
修改一下initMixin
// src/init.js
import { initState } from './state.js'
import { compileToFunctions } from './compile.js'
import { Watcher } from './watcher.js'
export function initMixin(MyVue) {
MyVue.prototype._init = function(options) {
const vm = this
vm.$options = options
initState(vm)
if(vm.$options.el && vm.$options.template) {
vm.$mounted(el)
}
}
MyVue.prototype.$mounted = function(el) {
const vm = this
// 获取根节点
el = vm.$el = query(el)
let template = vm.$options.template
// 根据template获取render方法
const { render } = compileToFunctions(template, vm)
// 保存在vm内,执行watcher后调用,也就是更新
vm._render = render
// 创建渲染watcher
mountComponent(vm)
}
}
function mountComponent(vm) {
// 创建一个渲染函数,执行render并更新页面
let updateComponent = () => {
const tem = vm._render()
vm.$el.innerHTML = tem
}
console.log('start mount')
// 渲染watcher
new Watcher(vm, updateComponent)
console.log('mounted')
// mounted生命周期
vm.$options.mounted && vm.$options.mounted.call(vm)
}
// 容错,没有找到el节点就创建一个
function query(el) {
let element = document.querySelector(el)
if(!element) {
console.error(`没有找到${el}节点`)
element = document.createElement('div')
}
return element
}
其实compileToFunctions函数就是把template传入后,通过replaceAll替换{{}}内的内容
// src/compile.js
// <div>{{obj.name}}</div> -> <div>my name</div>
export function compileToFunctions(template, vm) {
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g
const render = function () {
// {{xxx}} -> xxx -> vm.xxx
return template.replaceAll(defaultTagRE, (match, key) => {
const value = parseEx(vm, key)
return value ? value : ''
})
}
return {render}
}
// 获取a.b.c的值
function parseEx(vm, key) {
const arr = key.split('.')
let value = vm
for(let i=0;i<arr.length;i++) {
value = value[arr[i]]
}
return value
}
渲染Watcher
渲染Watcher可以简单的理解为一个立即执行updateComponent(执行_render)的构造函数
// src/watcher.js
let uid = 0
export class Watcher{
constructor(vm, expOrFn) {
this.vm = vm
this.id = uid++
this.getter = expOrFn
this.value = this.get()
}
get() {
const value = this.getter.call(this.vm, this.vm)
return value
}
}
然后修改index.js,查看效果,发现页面输出20,所以没毛病
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
age: 20
}
},
template: `
<div>{{age}}</div>
`
})
渲染更新(update)
渲染更新其实就是修改了某个data的值后,template自动更新。那么我们怎么知道什么时候修改了数据呢,又怎么知道哪里用到了这个数据呢? 刚刚在响应式这一小节里我们对data做了一层劫持,本质上来说什么时候修改了数据等于set,哪里用到了这个数据等于get,所以我们只需要在get和set中去收集和触发就可以完成渲染更新。 在这之前,先说明一下几个概念
- Dep:每个属性都有一个Dep,保存每个属性中关联的Watcher(目前只有渲染Watcher),属性值修改后调用Watcher.update执行回调
- Watcher:渲染Watcher(其实还有几种,后面说),保存关联的Dep,去重和后续计算属性使用,与Dep是多对多的关系
- Dep.target:Dep静态属性,保存进行中的Watcher,执行完后消除
实现Dep
首先实现一个Dep,用来保存Watcher,并且可以有一个唯一标识可以去重。接着维护一个Dep.target的静态属性保存进行中的Watcher,还有一个进行中的Watcher栈
// src/dep.js
let uid = 0
export class Dep{
constructor() {
// 唯一标识
this.id = uid++
// 保存Watcher
this.subs = []
}
// 给Watcher添加dep,并且去重
depend() {
if(Dep.target) {
Dep.target.addDep(this)
}
}
// 收集watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 遍历subs,执行update更新
notify() {
let len = this.subs.length
while(len--) {
this.subs[len].update()
}
}
}
// 运行中watcher
Dep.target = null
// watcher栈
const targetStack = []
export function pushTarget(watcher) {
targetStack.push(watcher)
Dep.target = watcher
}
export function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
修改Watcher
然后修改之前的渲染Watcher,这次需要新增几个点
- 在执行getter之前pushTarget,执行完毕popTarget,也就是在模版渲染的时候让Dep.target等于渲染Watcher,渲染完成后清除渲染Watcher
- 新增addDep函数,实现dep去重和存储
- 新增update函数,重新执行get(也就是之前的updateComponent)
// src/watcher.js
import { pushTarget, popTarget } from './dep.js'
let uid = 0
export class Watcher{
constructor(vm, expOrFn, userCb, options = {}) {
this.vm = vm
this.id = uid++
this.getter = expOrFn
this.userCb = userCb
// 存dep
this.deps = []
// 存depId
this.depIds = new Set()
this.value = this.get()
}
get() {
// Dep.target = 渲染Watcher
pushTarget(this)
// 渲染(render) -> 读取data(get) -> 渲染Watcher收集Dep -> Dep收集渲染Watcher
// 如果data修改了(set) -> dep.notify -> 之前dep已经收集了渲染Watcher -> update(_render)
const value = this.getter.call(this.vm, this.vm)
// pop Dep.target
popTarget()
return value
}
addDep(dep) {
const id = dep.id
// 去重
if(!this.depIds.has(id)) {
this.deps.push(dep)
this.depIds.add(id)
// dep保存Watcher
dep.addSub(this)
}
}
// 重新执行get
update() {
this.value = this.get()
}
}
对象劫持
最后修改的defineReactive方法,实现模版渲染期间使用到的数据收集渲染Watcher,修改数据时触发渲染Watcher,其实就是get时收集Watcher,set时遍历Dep下Watcher然后执行get
// src/dep.js
import { Dep } from "./dep"
export function observe(data) {
if(!Array.isArray(data) && data.constructor !== Object) {
return
}
let ob = new Observer(data)
return ob
}
class Observer{
constructor(data) {
if(Array.isArray(data)) {
this.observeArray(data)
} else {
this.walk(data)
}
}
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray() {
}
}
function defineReactive(data, key) {
let val = data[key]
const dep = new Dep()
let childOb = observe(val)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 存在Watcher,给Watcher添加dep,给dep添加Watcher
if(Dep.target) {
dep.depend()
}
return val
},
set(newVal) {
if(newVal === val) return
val = newVal
// 有新的值触发渲染
dep.notify()
}
})
}
到这一步,我们已经完成了对象劫持,实现了对象的响应式,接下来我们实现数组的劫持。
数组劫持
如果此时data里有一个数组,然后给数组push新数据的话,并没有进行更新,因为我们目前为止只劫持了对象里的属性。 之前我们也说过,劫持每一个值的话对性能影响非常大。那么换一种思路,我们重写数组类型中会修改原数组的方法(比如push...),然后取出新增的数据做响应式处理,再去触发更新。但是在这之前我们要先收集对应的Watcher,才能触发更新。怎么去收集呢? 假设存在如下的data
{
ages: [1, [2], [3, [4, 5]]]
}
如果我们执行了ages.push(8),那么是不是可以理解为ages变了。所以说,数组里的所有Watcher其实和ages这个key的Watcher完全是一致的,我们只需要不断的重写每一层数组,和收集每一层数据对应的的Watcher,得到类似这种映射关系
ages -> 渲染Watcher
[1, [2], [3, [4, 5]]] -> 重写,dep保存渲染Watcher
[2] -> 重写,dep保存渲染Watcher
[3, [4, 5]] -> 重写,dep保存渲染Watcher
[4, 5] -> 重写,dep保存渲染Watcher
思路说完了,我们修改一下Observer方法
// src/dep.js
import { Dep } from "./dep.js"
// 重写数组原型方法
import { arrayMethods } from './array.js'
export function observe(data) {
if (!Array.isArray(data) && data.constructor !== Object) {
return
}
// 如果存在__ob__属性,就证明已经是响应式,直接返回实例就可以
let ob
if (data.hasOwnProperty('__ob__') && data.__ob__ instanceof Observer) {
ob = data.__ob__
} else {
ob = new Observer(data)
}
return ob
}
class Observer {
constructor(data) {
// 只有数组使用到了这个dep
this.dep = new Dep()
// 给data添加不可枚举的__ob__属性,标记为响应式
this.insetOb(data)
// 如果是数组类型,重写数组的原型
if (Array.isArray(data)) {
// 当前data原型链继承
data.__proto__ = arrayMethods
this.observeArray(data)
} else {
this.walk(data)
}
}
//
walk(obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 递归,不断重写数组的原型方法
observeArray(data) {
for(let i=0;i<data.length;i++) {
observe(data[i])
}
}
insetOb(data) {
Object.defineProperty(data, '__ob__', {
configurable: true,
enumerable: false,
value: this
})
}
}
function defineReactive(data, key) {
let val = data[key]
const dep = new Dep()
// 值的Observer实例
let childOb = observe(val)
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get() {
// 存在Watcher,给Watcher添加dep,给dep添加Watcher
if (Dep.target) {
dep.depend()
// 如果存在值的Observer实例,也收集key对应的Watcher
if(childOb) {
childOb.dep.depend()
// [1, [2, [3]]],可能存在无限个嵌套数组,就无限收集Watcher
if(Array.isArray(val)) {
dependArray(val)
}
}
}
return val
},
set(newVal) {
if (newVal === val) return
val = newVal
dep.notify()
}
})
}
function dependArray(val) {
for(let i=0;i<val.length;i++) {
let e = val[i]
e && e.__ob__ && e.__ob__.dep.depend()
if(Array.isArray(e)) {
dependArray(e)
}
}
}
// src/array.js
// 数组的原型
const arrayProto = Array.prototype
// 继承数组原型
export const arrayMethods = Object.create(arrayProto)
// 会修改原数据的数组方法
const list = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
list.forEach(method => {
// 原数组方法
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method, {
enumerable: true,
configurable: true,
value(...args) {
console.log(this, args)
// 原型数组方法取值
const value = original.apply(this, args)
// Observer,this代表数据本身,比如{a:[1,2]},this代表a,__ob__在Observer中添加过,代表响应式
const ob = this.__ob__
let inset
// 截取新的数据
switch(method) {
case 'push':
case 'unshift':
inset = args
break;
case 'splice':
inset = args.slice(2)
break;
}
// 新数据响应式处理
if(inset) ob.observeArray(inset)
// 触发更新
ob.dep.notify()
// 返回值
return value
}
})
})
写到这一步,我们就完成了响应式数据的全部内容,写个demo验证一下
// index.js
import MyVue from './src/index.js'
new MyVue({
el: '#app',
data() {
return {
age: 20,
names: ['我', ['zm']]
}
},
mounted() {
setTimeout(() => {
this.age = 21
this.names[1].push('yg')
}, 1000)
},
template: `
<div>{{age}}</div>
<div>{{names}}</div>
`
})
// 先显示20,我,zm
// 1s后显示21,我,zm,yg
// 完工
但是这时我们可以在Watcher的update方法里加个log(1),可以发现1s后控制台有先后log了2次1,因为我们先修改了age的数据,发生了一次update,然后又在names中push了一次数据,又发生了一次update。这显然跟vue不一样。接下来我们实现一个更新队列,让同步的update只执行一次
更新队列(queue)
主要就是利用事件循环机制,利用任务队列包裹更新行为,使其达到异步效果,其实并不是真的异步,只是把更新的顺序放到了同步赋值之后。废话不多说,开始操作。 首先先改一下Watcher,将收集更新和真正触发更新区分开来
// src/watcher.js
import { queueWatcher } from './queue.js'
class Watcher{
// 之前老代码就不写了
...
// 收集更新队列
update() {
queueWatcher(this)
}
// 真正触发更新
run() {
this.value = this.get()
}
}
然后我们需要创建一个Watcher队列用来保存异步任务未执行的这段时间内所有的Watcher并且去重,然后用异步任务执行后遍历执行run更新
// src/queue.js
import { nextTick } from "./nexttick.js"
// Watcher队列
let queue = []
// Watcher id集合
let has = {}
// 循环执行Watcher的更新方法
function flushSchedulerQueue() {
this.queue = queue.slice()
for(let i=0;i<queue.length;i++) {
const watcher = queue[i]
watcher.run()
}
// 执行完成后清空
queue = []
has = {}
}
export function queueWatcher(watcher) {
const id = watcher.id
// 判断当前id是否存在,可能(this.name = 1;this.name = 2;this.age=2)那么就存在3次,我们只保存一次Watcher就可以(渲染watcher id是同一个)
if(!has[id]) {
has[id] = true
queue.push(watcher)
// 异步,详细可以看看事件循环机制
nextTick(flushSchedulerQueue)
}
}
// src/nexttick.js
// 存储flushSchedulerQueue
const cbs = []
let pendding = false
function flushCallbacks() {
pendding = false
// 遍历更新方法,执行所有更新任务
for(let i = 0;i<cbs.length;i++) {
cbs[i]()
}
}
// 优雅降级,让flushCallbacks在同步代码之后执行
let timeFunc
if(typeof Promise !== 'undefined') {
const p = Promise.resolve()
timeFunc = () => {
p.then(flushCallbacks)
}
} else if(typeof MutationObserver !== 'undefined') {
let counter = 1
// 在指定的DOM发生变化时被调用
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true // 观查字符数据变化
})
timerFunc = () => {
// 字符变化后调用flushCallbacks
counter = (counter + 1) % 2
textNode.data = String(counter)
}
} else if (typeof setImmediate !== 'undefined') {
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
export function nextTick(fn) {
cbs.push(fn)
// 同步的update会多次执行nextTick,但是我们只执行一次,等异步队列清空之后再重新执行
if(!pendding) {
pendding = true
timeFunc()
}
}
到这里我们就完成异步更新队列,同步任务下执行多次赋值,只会渲染一次
初始化计算属性(computed)
在实现computed之前我们还是先思考一下computed的一些特性
- 可以直接使用this访问data的数据
- 可以在其它函数里用this访问computed属性
- 写法上存在2种,一种是函数写法,一种是对象写法
- 依赖的值没有变化的话,会直接取缓存的值,不会重新计算
- 懒计算,只有在用到之后才去求值
老步骤,先修改initState里的方法,获取computed属性并执行初始化方法
// src/state.js
export function initState(vm) {
const opts = vm.$options
if(opts.methods) initMethods(vm, opts.methods)
if(opts.data) initData(vm, opts.data)
if(opts.computed) initComputed(vm, opts.computed)
}
然后我们遍历computed获取计算属性,处理计算属性中几种写法的差异,并创建computed Watcher
// src/state.js
function initComputed(vm, computed) {
// 保存watcher
const watchers = vm._computedWatchers = {}
for(let key in computed) {
const userDef = computed[key]
// 可能是function,也可能是对象,是对象获取get作为表达式
const getter = typeof userDef === 'function' ? userDef : userDef.get
// 创建computed Watcher,lazy作为标识。因为使用computed都是在其余函数或者模版中,使用到的时候再求值和获取关联的进行中的Watcher
// 保存每一个watcher
watchers[key] = new Watcher(vm, getter, () => {}, {lazy: true})
// 做一层劫持,用来获取关联的Watcher,也是在这里将computed属性挂载到实例上
if(!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
}
劫持computed属性
我们先实现defineComputed,主要就是利用Object.defineProperty给实例添加computed属性,然后在get内判断依赖值是否更新,如果更新才重新求值,没更新就直接获取缓存值。
// src/state.js
function defineComputed(vm, key, userDef) {
const isFn = typeof userDef === 'function'
// 挂载到实例
Object.defineProperty(vm, key, {
configurable: true,
enumerable: true,
get() {
// 获取computed Watcher
const watcher = vm._computedWatchers[key]
// 如果是脏值就执行Watcher get,获取重新求值并给依赖项dep添加computed Watcher
if(watcher.dirty) {
watcher.evaluate()
}
// 计算属性的依赖关联渲染Watcher
if(Dep.target) {
watcher.depend()
}
// dirty:true,新值,dirty:false,上一次的值(缓存值)
return watcher.value
},
// 取对象的set或者空函数
set: isFn ? () => {} : (userDef.set || function() {})
})
}
这里比较难理解的应该是watcher.depend这一段,为什么要关联渲染Watcher? 因为我们可能是在模版渲染的时候读取了computed属性,这时Dep.target为渲染Watcher,然后在执行evaluate的时候我们会对计算属性求值(具体代码看后面一点),然后此时Dep.target为computed Watcher,给依赖项添加computed Watcher和给computed Watcher添加依赖项的dep,执行完毕popTarget后,Dep.target归还给渲染Watcher,然后我们再给computed Watcher收集到的依赖项Dep添加渲染Watcher,这时依赖项如果修改了值,那么遍历dep的update时也会执行渲染Watcher,达到了更新template的效果。接下来我们就来修改一下watcher达到这一效果。
computed Watcher
首先我们需要watcher在拿到lazy的时候先不执行get,并初始化一个判断依赖项是否有更新的属性(dirty),初始值也是true
// src/watcher.js
export class Watcher{
constructor(vm, expOrFn, userCb, options = {}) {
this.vm = vm
this.id = uid++
// 计算属性,并初始化一个判断依赖项是否有更新的属性
this.dirty = this.lazy = options.lazy
this.userCb = userCb
this.deps = []
this.depIds = new Set()
// lazy,先不求值
this.value = this.lazy ? undefined : this.get()
}
}
然后实现evaluate,也就是简单的求值,然后把dirty赋值为false,代表已经是最新值,如果依赖没变就不求值
// src/watcher.js
evaluate() {
// 求值,获取依赖 -> 例如a(){return this.a+this.b},执行get求值后,a和b的dep收集computed Watcher,computed Watcher的deps收集a和b的dep
this.value = this.get()
// 求值之后如果依赖不更新就不会重新求值
this.dirty = false
}
然后再遍历computed Watcher收集到的依赖项的deps,给它们添加渲染Watcher
// src/watcher.js
depend() {
// 遍历computed Watcher收集到的dep,也就是依赖项的dep,其实这时候的Dep.target为渲染Watcher,所以就是给依赖项添加渲染Watcher
for(let i = 0;i < this.deps.length; i++) {
this.deps[i].depend()
}
}
最重要一点什么时候把dirty重新赋值为true?依赖项更新的时候对啊,依赖项更新的时候会走set,然后走到update,是计算属性的话就把dirty赋值为true,然后在读取到computed属性的时候computed watcher的dirty就是true并重新求值。所以我们只需要在update中判断是不是computed Watcher就可以啦。
// src/watcher.js
update() {
// computed watcher
if(this.lazy) {
// 需要重新计算
this.dirty = true
} else {
// 异步更新
queueWatcher(this)
}
}
附上一张画的图吧
初始化监听属性(watch)
终于快写完了。。。 老规矩,想一想watch的特性
- 可以直接使用this访问data的数据
- 写法上存在4种,函数写法,对象写法,数组写法,字符串写法
- 表达式的值发生变化后再执行回调函数,并返回新老值
- 存在immediate,deep参数
老步骤,先修改initState里的方法,获取watch属性并执行初始化方法
// src/state.js
export function initState(vm) {
const opts = vm.$options
if(opts.methods) initMethods(vm, opts.methods)
if(opts.data) initData(vm, opts.data)
if(opts.computed) initComputed(vm, opts.computed)
if(opts.watch) initWatcher(vm, opts.watch)
}
// src/state.js
function initWatcher(vm, watch) {
// 遍历
for(let key in watch) {
const handler = watch[key]
// 是数组,循环创建user Watcher
if(Array.isArray(handler)) {
let i = handler.length
while(i--) {
createWatcher(vm, key, handler[i])
}
} else {
// 直接创建user Watcher
createWatcher(vm, key, handler)
}
}
}
消除差异
在创建user Watcher之前,我们已经消除了数组创建方式的差异,而且我们需要把回调参数传入watcher,函数就是回调,所以我们只需要再消除字符串和对象之间的差异就可以了
// src/state.js
function createWatcher(vm, key, handler) {
let options = {}
// 对象,获取回调和参数
if(handler.constructor === Object) {
options = handler
handler = handler.handler
}
// 字符串,直接获取methods的方法
if(typeof handler === 'string') {
handler = vm[handler]
}
// user Watcher标识
options.user = true
// 创建watcher
const watcher = new Watcher(vm, key, handler, options)
// 立即执行
if(options.immediate) {
handler.call(vm, watcher.value)
}
}
user Watcher
这时我们把key当作Watcher的第二个参数,因为watch的功能是监听某个项的变化,所以当我们初始化时,需要给对应项的Dep添加user Watcher,跟之前的概念一样,求值即可 改造一下watcher
// src/watcher.js
export class Watcher{
constructor(vm, expOrFn, userCb, options = {}) {
this.vm = vm
this.id = uid++
// computed Watcher和渲染Watcher的expOrFn都是函数
// user Watcher是key,对key求值,给对应data的Dep添加user Watcher
if(typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
}
// user watcher标识
this.user = options.user
// 计算属性
this.dirty = this.lazy = options.lazy
this.userCb = userCb
this.deps = []
this.depIds = new Set()
this.value = this.lazy ? undefined : this.get()
}
}
// 可能是'a.b.c',在执行getter的时候会call(vm, vm),所以等于vm[a][b][c]
// 求值,走data的劫持的get,所以添加了user Watcher
function parsePath(path) {
const arr = path.split('.')
return (obj) => {
for(let i=0;i<arr.length;i++) {
obj = obj[arr[i]]
}
return obj
}
}
实现一下deep参数,其实就是在get里对取到的值再不断取其子属性的值的过程(不断给其关联user Watcher)
// src/watcher.js
get() {
pushTarget(this)
const value = this.getter.call(this.vm, this.vm)
if(this.deep) {
traverse(value)
}
popTarget()
return value
}
function traverse(val) {
const depIds = new Set()
// 递归
function deep(value, seen) {
// 不是对象或者数组直接return
if(!Array.isArray(value) && value.constructor !== Object) {
return
}
// 是否是响应式,如果是添加dep.id,避免重复添加watcher
if(value.__ob__) {
const id = value.__ob__.dep.id
if(seen.has(id)) {
return
}
seen.add(id)
}
// 数组或对象不断取值
if(Array.isArray(value)) {
let i = value.length
while (i--) deep(value[i], seen)
} else {
let keys = Object.keys(value)
let i = keys.length
while (i--) deep(value[keys[i]], seen)
}
}
deep(val, depIds)
}
最后我们处理什么时候执行回调函数,user Watcher update的时候会走异步队列,最后走到run真正求值的地方,然后判断新老值是否判断,有变化执行回调。
// 真正触发更新
run() {
// 老值
const oldVal = this.value
// 新值
const newVal = this.get()
this.value = newVal
// 引用类型的新老值是相等的,指向同一个地址
if(oldVal !== newVal || (newVal !== undefined && (Array.isArray(newVal) || newVal.constructor === Object))) {
// user Watcher返回新老值
if(this.user) {
this.userCb.call(this.vm, newVal, oldVal)
} else {
this.userCb.call(this.vm)
}
}
}
完成
参考资料
文章内容源码:github.com/chenerhong/…
掘金文章:juejin.cn/post/693534…
vue源码
vue文档:cn.vuejs.org/v2/guide/