Vue computed以及watch简单实现
Vue computed以及watch简单实现这一次就不先上效果图了,这次先说需求:在vue中,有两个很好用的api,就是computed以及watch,computed为可以自定义计算属性,这个计算属性有哪些优点呢,先说一些相比较于计算属性的其他实现办法,一个是方法,另一个是get属性,实际上两个都是方法,也就是说,模板中绑定的值是一个函数的计算结果,比如这样的{{reverseMe...
Vue computed以及watch简单实现
Vue computed以及watch简单实现
本文又长又臭,是我写过的博客中最长的博客了,不对vue响应式原理做深入研究的同学还是别往下看了。
需求,实际应用场景说明
这一次就不先上效果图了,这次先说需求:
- 在vue中,有两个很好用的api,就是computed以及watch,computed为可以自定义计算属性,这个计算属性有哪些优点呢,先说一些相比较于计算属性的其他实现办法,一个是方法,另一个是get属性,实际上两个都是方法,也就是说,模板中绑定的值是一个函数的计算结果,比如这样的{{reverseMessage()}},reverseMessage为methods中定义的函数,这样也能够实现计算的功能,不过computed实现的计算属性更为强大的一点是,它是基于它的响应式依赖进行缓存的;什么意思呢?看下面这段代码:
export default {
name: "demo-test2",
data() {
return {
name: 'Vue'
}
},
computed: {
helloComputed() {
console.warn('exec computed: hello')
return `hello, ${this.name}`
},
},
methods: {
helloMethod() {
console.warn('exec method: hello')
return `hello, ${this.name}`
},
},
mounted() {
console.log(this.helloMethod())
console.log(this.helloMethod())
console.log(this.helloMethod())
console.log(this.helloComputed)
console.log(this.helloComputed)
console.log(this.helloComputed)
this.name = 'vue-router'
console.log(this.helloMethod())
console.log(this.helloMethod())
console.log(this.helloMethod())
console.log(this.helloComputed)
console.log(this.helloComputed)
console.log(this.helloComputed)
},
}
这里是执行结果:
可以看到是,method每次获取,都会重新计算,而computed是当helloComputed所依赖的a变换了之后,第一次获取才会重新计算,这样一个特性可以大幅度地优化应用的性能;
- 第二个是watch,在vue中,watcher是监听器,用来监听data中或者prop中属性的变化,当我们页面上想要根据某个值的变化动态地调用自己的回调函数的时候,这个功能显得尤为重要,但是这个又不是简单地去拦截对象的set函数,简单拦截set函数与vue的watch监听属性的区别,看代码:
export default {
name: "demo-test2",
data() {
const that = this
return {
hello: '111',
p_world: '222',
get world() {
return that.p_world
},
set world(val) {
console.log('world change:' + val)
that.p_world = val
},
}
},
watch: {
hello(val) {
console.log('hello change:' + val)
},
},
mounted() {
let count = 0
this.hello = ++count
this.world = ++count
this.hello = ++count
this.world = ++count
this.hello = ++count
this.world = ++count
},
执行结果:
可以看到,手动地去拦截某一个属性的set函数是可以的,但是每一次触发set都会触发一次自定义的回调函数,而vue的watch监听属性就不是这样,而是等待js执行完毕之后,才会使用最后一次更新的值触发监听函数,这样也能够给我们的日常业务逻辑开发带来巨大的便利;
结果展示
plain-ui组件库已经写得差不多了,在研究react,打算写一套react版本的组件库,但是麻烦的是,跟angular一样,没有computed以及watch这两个api,计算属性只能使用函数或者get属性,性能十分地差,同理要监听某个属性变化,得在组件更新的回调函数中各种判断自行调用回调函数,真的是挺麻烦的。经过对vue响应式的一番研究后,写了一套将单个对象响应式,对象这种设置计算属性,监听对象属性变化的api,简单来说就是computed和watch在简答对象上的简单实现,不过基础的功能还是有的,看效果图:
接下来展示效果图
执行的代码图(完整可执行的html代码在文章底部):
这个初始化的步骤说明:
- 首先是声明一个简答的对象hero;
- initData初始化hero对象中的属性,响应式初始化hero中的属性;
- computed初始化hero中的计算属性,添加ab,cd,abab三个计算属性;
- watch初始化hero中的监听属性,监听a以及b的变化;
初始化结束,接下来是测试:
- 首先第一句以及第二句warn日志测试的是多次获取同一个计算属性,是否根据其依赖进行缓存的功能,结果是有的,只有第一次获取,或者计算属性所依赖的响应式属性(这里ab依赖a和b)变化之后再次获取,的时候才会调用用户函数计算;
- 第二句warn同时也在测试,没有获取使用的计算属性,其依赖变化之后是否会触发重新计算,结果是不会,这里abab依赖于ab,但是ab并不是响应式属性,只是计算属性,而ab依赖于响应式属性a和b,所以实际上abab依赖于a和b,当a变化的时候,因为没有获取abab所以abab没有重新计算,第三局warn测试的是,b变化之后,获取abab,结果是abab重新计算了,而且使用的是a和b的最新值计算,符合预期,而且性能上也没有冗余的逻辑;
- 第四句测试的是,计算属性所依赖的所有响应式属性变化之后,是否会有冗余的计算,可以看到结果是,同时修改了a和b,但是第一次获取ab以及abab的时候,都只是计算了一次,所以性能上没有冗余的计算;ab并没有因为a变化以及b变化而计算了两次;
- 第五局warn测试的是,多次修改同一个响应式属性,是否会多次触发监听属性的回调函数,结果是不会,在一次js执行过程中,a一共被修改了5次,而b被修改了2次,而只将最后一次的修改结果作为参数触发了用户的监听回调函数,符合预期;
代码中一些对象属性的概念解释
- data:vue中data为函数,本文则是直接一个对象,相当于vue中data函数执行得到的结果,data必须为一个对象,本次实验就是代码中的hero对象;
- Dep:依赖,是一个js对象,data中每一个响应式属性对应一个Dep实例,也就是说,每一个data的key属性,在响应式初始化的时候,都会存有一个new Dep(),在调用key属性的get函数的时候,调用dep.depend()收集依赖,在调用key属性的set函数的时候,调用dep.notify()函数通知变更;至于是如何收集依赖以及通知变更,接下来几点讲解;
- Watcher,观察对象,是一个js对象,用来接收Dep的通知。这个Watcher与本文标题的【watch】不是同一个东西,看原理图,其实是computed watcher、user watcher、render watcher,这三个都是Watcher的实例,而标题中的【watch】是vue的api中的watch功能,就是监听vue的data或者props变化的那个api,实际上就是原理图中的user watcher;本文只简单实现computed watcher以及user watcher,不讲解render watcher。
- computed watcher:每一个计算属性对应一个computed watcher,也就是一个new Watcher() 实例,这个computed watcher会在data的计算属性key设置get函数,在get函数中首先判断watcher的dirty(脏数据)属性做检查,如果为true则重新调用用户定义的计算函数计算结果,计算完之后设置dirty为false,然后判断当前全局属性Dep.target是否存在,存在则调用watcher.depend(),让watcher中所有的deps(数组,每一个响应式属性变量所属依赖)收集当前的全局观察者对象;
- expressOrFunction:watcher用来获取值的函数,最后转化为watcher中的getter属性,创建Watcher实例的时候需要用到,如果是计算属性computed watcher,getter=expressOrFunction,为用户定义的计算函数,如果是监听属性user watcher,则值为一个字符串,字符串是监听属性key,getter=(data)=>data[key](这里只是简写,细写看源码),这里相当于调用了响应式属性key的get方法。
- Dep.target:全局watcher实例,watcher每次在执行自己的上述的getter的时候,都会将Dep.target指向自己,也就是Dep.target = this,然后在getter执行内部,如果是获取了响应式属性,属性的dep就会将当前的watcher收集到subs,如果是获取了计算属性,计算属性涉及到的deps继续将当前watcher收集到各自的subs,这些响应式属性在变化触发setter的时候,通知subs,如果sub为computed watcher,设置dirty为true,也就是处于脏数据的状态,等待计算属性第一次被获取的时候会检查脏数据,重新计算;如果sub为user watcher,会调用watcher的run函数,run函数中再setTimeout(()=>{},0)中执行user watcher的回调函数,这样就可以在js执行过程结束之后再执行watch的回调函数了。
上原理图
描述一下响应式过程
属性响应式初始化
这个过程是将对象中的属性响应式初始化,就是吧原本一个简单的对象属性使用get和set函数代理:
/*之前*/
const data = {
a:"hello vue!"
}
/*之后*/
const data = {
temp_a:'hello vue!',
get a(){
return this.temp_a
},
set a(val){
this.temp_a = val
},
}
可以看到,这样初始化之后,对a的操作实际上就是对temp_a的操作。响应式初始化函数:
Dep与Watcher在响应式初始化的作用
/**
* 初始化响应式属性
* @author 韦胜健
* @date 2019/5/22 09:51
* @param data 响应式属性所属对象
* @param key 响应式属性名
* @param val 响应式属性初始值
*
*/
function defineReactive(data, key, val) {
const dep = new Dep()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal
dep.notify()
},
})
}
响应式属性touch【被获取】的时候,实际上就是调用属性的get函数,此时响应式属性的dep就会收集依赖,如何收集依赖,看Dep中的depend方法:
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
这个Dep.target是Watcher对象实例,【计算属性】在调用用户自定义计算逻辑计算值的时候,是通过调用Watcher的getter函数(看【代码中一些对象属性的概念解释】中的expressOrFunction)得到的,getter函数中会先把全局对象Dep.target设置为Watcher实例自己,这个时候,再调用用户自定义计算逻辑,用户自定义计算逻辑中会touch响应式属性,这时候这些响应式属性的dep就会将Dep.target收集到依赖数组中,计算属性的getter执行快结束的时候,由于计算属性可能会调用另一个计算属性,此时需要还原Dep.target为上一层的watcher,这个实现是通过targetStack数组以及pushTarget以及popTarget函数实现的;
/*Watcher中的addDep函数*/
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
/*Dep中的addSub函数*/
addSub(sub) {
this.subs.push(sub)
}
在addDep中先不管newDepIds和depIds,只看最里面的dep.addSub(this),可以理解为就是,最终watcher与dep是相互引用,多对多的关系;
计算属性
- 上面说了,计算属性在执行getter的时候,会收集响应式依赖,根据watcher中的dirty(脏数据,意思为依赖属性已经变化,需要重新计算值)判断是否需要重新计算值,然后将结果值返回给get函数,所以get函数中是return watcher.value;watcher中的value起到一个缓存的作用;
- 当响应式属性变化,以hero.a为例,会触发hero中a的set函数,此时会触发dep.notify(),看看这个函数里面做了什么:
notify() {
const subs = this.subs.slice()
subs.sort((a, b) => a.id - b.id)
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
简单来说就是先按照创建的时候的id排序,然后一个个执行watcher的update函数,看update函数中做了什么:
update() {
this.dirty = true
!!this.user && this.run()
}
计算属性,只需要关注第一行this.dirty = true,表明此时处于脏数据状态,等到下一次获取计算属性的时候,在get函数中,因为这个dirty是true,那时候会重新计算属性值;计算属性的watcher的user一直是false,只有监听属性的user是true;
监听属性
- 初始化监听属性(稍微看一下就行,看第三点解释)
function watch(data, watch) {
Object.keys(watch).forEach(key => {
const handler = watch[key]
new Watcher(data, key, key, handler, true)
})
}
- Watcher对象初始化(稍微看一下就行,看第三点解释)
constructor(data, key, expressOrFunction, callback, user) {
this.data = data
this.key = key
this.id = ++watchId
this.dirty = true
this.value = undefined
this.callback = callback
this.active = true
this.user = user
switch (typeOf(expressOrFunction)) {
case 'function':
this.getter = expressOrFunction
break;
case 'string':
this.getter = parsePath(expressOrFunction)
break
}
if (!!this.user) {
this.value = this.get()
}
}
- 监听属性如何收集依赖?用户在使用watch监听属性的时候并没有显示地touch hero中的响应式属性,那么如何收集依赖呢?首先是在watch(data,watch)函数中,创建的Watcher实例的第四个参数user是true,然后在Watcher的constructor中,作为监听属性,做了两件事件;
- 这个时候expressionOrFunction是key,然后在switch判断中将getter设置为parsePath(expressionOrFunction),这个过程做一个非常简单的描述就是,比如new Watcher(hero,‘a’,‘a’,true),最后这个getter就是是一个function(data){return data[‘a’]},此时调用getter就相当于touch了hero的a属性。 所以接着就是判断是否user,是的话,马上触发get函数(内部执行了getter,进行依赖收集);这个监听属性user watcher相比较于计算属性computed watcher的最大不同可能就是,computed watcher可能是依赖于多个响应式属性,也就是收集到多个dep,而user watcher只依赖于一个响应式属性,也就是只能收集到一个dep;
- 响应式属性变化了之后,之前说过会触发watcher.update(),此时update中的第一行代码就已经没用了,此时会执行第二行代码,执行run方法:
run() {
if (!!this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
if (this.active) {
const value = this.get()
if (value !== this.value || typeOf(value) === 'object' || this.deep) {
const oldValue = this.value
this.value = value
if (!!this.callback) {
this.callback(value, oldValue)
}
}
}
this.timer = null
}, 0)
}
由于本文只是做简单实现,只用了定时器这种办法做了监听优化,在vue中是有多种方式的;仔细体会这一段代码,当多次修改a的值:
/*这里只是演示,不是实际测试代码*/
let count = 0
hero.a = ++count
hero.a = ++count
hero.a = ++count
这里修改三次a的值,我想要的预期结果是,无论变更多少次a的值,在一次js执行过程中只执行一次回调函数,而且得到的参数是最新的值以及js执行过程开始之前的旧值,在看一眼实际测试代码以及结果:
可以看到a以及b的监听函数只触发了一次,并且a的新值是最后一次执行的3,旧值是js执行开始时候声明的hero中的a:‘111’,同理b也是;
至此,关于computed以及watch的简单实现以及原理讲解已经完了,这里做一下总结;
- 计算属性以及监听属性都是通过Watcher实现的,只不过两者在Watcher的一些执行过程中做了不同的工作而已;
- Watcher只跟Dep有依赖关系,Watcher跟Watcher实例之间没有依赖关系,比如计算属性abab引用了计算属性ab,此时abab的computed watcher会收集ab的依赖,最终是abab也是依赖于a和b;
- 在Watcher中是通过构造参数user区分computed watcher以及 user watcher,在vue源码中,这个更为复杂,这里只是简单化;
- 计算属性收集依赖,只需要执行它的用户定义的计算逻辑函数即可,因为这个计算的逻辑函数里面显示地touch了hero中的响应式属性;监听属性收集依赖,就只是watcher在constructor中显示地调用了hero[key]而已;
- 计算属性的计算函数是计算属性在被获取的时候,同时处于脏数据状态的时候才会被调用计算新的值;监听属性的回调函数是响应式属性被修改,js执行过程执行完毕之后才会被调用;
最后附上完整html可执行代码
html中使用了es6语法,请使用chrome浏览器打开。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap 101 Template</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
html, body {
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
#app {
display: flex;
align-items: center;
padding: 0 100px;
}
.btn-success{
margin-left: 20px;
}
</style>
</head>
<body id="app">
<button type="button" class="btn btn-primary" onclick="changeA(getCount())">change a</button>
<button type="button" class="btn btn-success" onclick="changeB(getCount())">change b</button>
<script type="text/javascript">
console.log('--->>>start')
/*---------------------------------------Dep-------------------------------------------*/
let depId = 0
class Dep {
static target
id
subs
constructor() {
this.subs = []
this.id = ++depId
Dep.target = null
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
const index = this.subs.indexOf(sub)
if (index > -1) this.subs.splice(index, 1)
}
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify() {
const subs = this.subs.slice()
subs.sort((a, b) => a.id - b.id)
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
const targetStack = []
Dep.target = undefined
function pushTarget(target) {
targetStack.push(target)
Dep.target = target
}
function popTarget() {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
/*---------------------------------------Watcher-------------------------------------------*/
function typeOf(obj) {
const toString = Object.prototype.toString;
const map = {
'[object Boolean]': 'boolean',
'[object Number]': 'number',
'[object String]': 'string',
'[object Function]': 'function',
'[object Array]': 'array',
'[object Date]': 'date',
'[object RegExp]': 'regExp',
'[object Undefined]': 'undefined',
'[object Null]': 'null',
'[object Object]': 'object'
};
return map[toString.call(obj)];
}
let watchId = 0
const unicodeRegExp = /a-zA-Z\u00B7\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u037D\u037F-\u1FFF\u200C-\u200D\u203F-\u2040\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD/
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
function parsePath(path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}
class Watcher {
getter
key
callback
user
data
id
dirty
value
deps = []
newDeps = []
depIds = new Set()
newDepIds = new Set()
active
constructor(data, key, expressOrFunction, callback, user) {
this.data = data
this.key = key
this.id = ++watchId
this.dirty = true
this.value = undefined
this.callback = callback
this.active = true
this.user = user
switch (typeOf(expressOrFunction)) {
case 'function':
this.getter = expressOrFunction
break;
case 'string':
this.getter = parsePath(expressOrFunction)
break
}
if (!!this.user) {
this.value = this.get()
}
}
addDep(dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}
depend() {
this.deps.forEach(dep => dep.depend())
}
cleanupDeps() {
let i = this.deps.length
while (i--) {
const dep = this.deps[i]
if (!this.newDepIds.has(dep.id)) {
dep.removeSub(this)
}
}
let tmp = this.depIds
this.depIds = this.newDepIds
this.newDepIds = tmp
this.newDepIds.clear()
tmp = this.deps
this.deps = this.newDeps
this.newDeps = tmp
this.newDeps.length = 0
}
/**
* Evaluate the getter, and re-collect dependencies.
*/
get() {
pushTarget(this)
let value
try {
value = this.getter.call(this.data, this.data)
} catch (e) {
console.error(e)
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
update() {
this.dirty = true
!!this.user && this.run()
}
evaluate() {
this.value = this.get()
this.dirty = false
}
run() {
if (!!this.timer) {
clearTimeout(this.timer)
}
this.timer = setTimeout(() => {
if (this.active) {
const value = this.get()
if (value !== this.value || typeOf(value) === 'object' || this.deep) {
const oldValue = this.value
this.value = value
if (!!this.callback) {
this.callback(value, oldValue)
}
}
}
this.timer = null
}, 0)
}
}
function defineReactive(data, key, val) {
const dep = new Dep()
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function () {
dep.depend()
return val
},
set: function (newVal) {
if (newVal === val) return
val = newVal
dep.notify()
},
})
}
function computed(data, computed) {
let keys = Object.keys(computed)
keys.reduce((ret, key) => {
const watcher = new Watcher(data, key, computed[key])
Object.defineProperty(data, key, {
configurable: true,
enumerable: true,
get: function () {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
},
set: function () {
},
})
return ret
}, {})
}
function watch(data, watch) {
Object.keys(watch).forEach(key => {
const handler = watch[key]
new Watcher(data, key, key, handler, true)
})
}
function initData(data) {
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
const hero = {
a: 111,
b: 222,
c: 333,
d: 444,
}
initData(hero)
computed(hero, {
ab() {
console.log('----------------reset ab-----------------')
return hero.a + '' + hero.b
},
cd() {
console.log('----------------reset cd-----------------')
return hero.c + '' + hero.d
},
abab() {
console.log('----------------reset abab-----------------')
return hero.ab + '+++' + hero.ab
},
})
watch(hero, {
a(newVal, oldVal) {
console.log(`a change from [${oldVal}] to [${newVal}]`)
},
b(newVal, oldVal) {
console.log(`b change from [${oldVal}] to [${newVal}]`)
},
})
function changeA(val) {
hero.a = val
console.log(hero.a)
console.log(hero.a)
console.log(hero.a)
}
function changeB(val) {
hero.b = val
console.log(hero.b)
console.log(hero.b)
console.log(hero.b)
}
let count = 0
function getCount() {
return ++count
}
console.warn('这里测试读取三次ab以及读取两次abab,结果是第一次读取ab的时候执行了一次ab,第一次读取abab的时候执行了一次abab,符合预期')
console.log('ab\t\t\t\t', hero.ab)
console.log('ab\t\t\t\t', hero.ab)
console.log('ab\t\t\t\t', hero.ab)
console.log('abab\t\t\t\t', hero.abab)
console.log('abab\t\t\t\t', hero.abab)
console.log('')
console.log('||||||||||||||||结束||||||||||||||||')
console.log('')
console.warn('这里测试重置a的值,然后读取两次ab,不读取abab,结果是第一次读取ab的时候执行了一次ab,没有执行abab,符合预期,因为此时没有使用abab,所以不需要重新计算abab')
console.log('set a=aaa')
hero.a = 'aaa'
console.log('ab\t\t\t\t', hero.ab)
console.log('ab\t\t\t\t', hero.ab)
console.log('')
console.log('||||||||||||||||结束||||||||||||||||')
console.log('')
console.warn('这里测试重置b的值,然后重新读取ab以及abab各两次,结果都是第一次读取的时候执行了一次,符合预期')
console.log('set b=bbb')
hero.b = 'bbb'
console.log('ab\t\t\t\t', hero.ab)
console.log('abab\t\t\t\t', hero.abab)
console.log('ab\t\t\t\t', hero.ab)
console.log('abab\t\t\t\t', hero.abab)
console.log('')
console.log('||||||||||||||||结束||||||||||||||||')
console.log('')
console.warn('这里测试同时重置a,b的值,然后读取三次ab,读取两次abab,结果是ab以及abab在第一次读取的时候执行了一次,符合预期')
console.log('set a=111,b=222')
hero.a = 'mmm'
hero.b = 'nnn'
console.log('ab\t\t\t\t', hero.ab)
console.log('ab\t\t\t\t', hero.ab)
console.log('ab\t\t\t\t', hero.ab)
console.log('abab\t\t\t\t', hero.abab)
console.log('abab\t\t\t\t', hero.abab)
console.log('')
console.log('||||||||||||||||结束||||||||||||||||')
console.log('')
console.warn('这里测试在一次执行过程中,多次修改同一个值,是否会导致watch中的handle函数触发多次,结果是只触发了一次,符合预期')
console.warn('整个过程执行完,a一共变化了5次,a=aaa,a=mmm,以及三次a=getCount(),但是这个在a频繁变化的过程中,最后只触发a的watch handler一次,而且是在所有动作执行完毕才触发的,同理b也是,一共改变了2次,符合预期')
console.log(hero.a)
hero.a = getCount()
hero.a = getCount()
hero.a = getCount()
console.log(hero.a)
</script>
</body>
</html>
更多推荐
所有评论(0)