在vue项目中什么时候用watch(从Vue源码角度深挖Watch)

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(1)

作者:Naice

转发链接:https://segmentfault.com/a/1190000023196603

前言

这篇文章将带大家全面理解vue的watcher、computed和user watcher,其实computed和user watcher都是基于Watcher来实现的,我们通过一个一个功能点去敲代码,让大家全面理解其中的实现原理和核心思想。所以这篇文章将实现以下这些功能点:

  • 实现数据响应式
  • 基于渲染wather实现首次数据渲染到界面上
  • 数据依赖收集和更新
  • 实现数据更新触发渲染watcher执行,从而更新ui界面
  • 基于watcher实现computed
  • 基于watcher实现user watcher

废话不要多说,先看下面的最终例子。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(2)

例子看完之后我们就直接开工了。

准备工作

首先我们准备了一个index.html文件和一个vue.js文件,先看看index.html的代码

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(3)

index.html里面分别有一个id是root的div节点,这是跟节点,然后在script标签里面,引入了vue.js,里面提供了Vue构造函数,然后就是实例化Vue,参数是一个对象,对象里面分别有data 和 render 函数。然后我们看看vue.js的代码:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(4)

vue.js代码里面就是执行this._init()和this.$mount(),this._init的方法就是对我们的传进来的配置进行各种初始化,包括数据初始化initState(vm)、计算属性初始化initComputed(vm)、自定义watch初始化initWatch(vm)。this.$mount方法把render函数渲染到页面中去、这些方法我们后面都写到,先让让大家了解整个代码结构。下面我们正式去填满我们上面写的这些方法。

实现数据响应式

要实现这些watcher首先去实现数据响应式,也就是要实现上面的initState(vm)这个函数。相信大家都很熟悉响应式这些代码,下面我直接贴上来。

functioninitState(vm){ letdata=vm.$options.data;//拿到配置的data属性值 //判断data是函数还是别的类型 data=vm._data=typeofdata==='function'?data.call(vm,vm):data||{}; constkeys=Object.keys(data); leti=keys.length; while(i--){ //从this上读取的数据全部拦截到this._data到里面读取 //例如this.name等同于this._data.name proxy(vm,'_data',keys[i]); } observe(data);//数据观察 } //数据观察函数 functionobserve(data){ if(typeofdata!=='object'&&data!=null){ return; } returnnewObserver(data) } //从this上读取的数据全部拦截到this._data到里面读取 //例如this.name等同于this._data.name functionproxy(vm,source,key){ Object.defineProperty(vm,key,{ get(){ returnvm[source][key]//this.name等同于this._data.name }, set(newValue){ returnvm[source][key]=newValue } }) } classObserver{ constructor(value){ this.walk(value)//给每一个属性都设置getset } walk(data){ letkeys=Object.keys(data); for(leti=0,len=keys.length;i<len;i ){ letkey=keys[i] letvalue=data[key] defineReactive(data,key,value)//给对象设置getset } } } functiondefineReactive(data,key,value){ Object.defineProperty(data,key,{ get(){ returnvalue }, set(newValue){ if(newValue==value)return observe(newValue)//给新的值设置响应式 value=newValue } }) observe(value);//递归给数据设置getset }

重要的点都在注释里面,主要核心就是给递归给data里面的数据设置get和set,然后设置数据代理,让 this.name 等同于 this._data.name。设置完数据观察,我们就可以看到如下图的数据了。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(5)

console.log(vue.name)//张三 console.log(vue.age)//10

ps: 数组的数据观察大家自行去完善哈,这里重点讲的是watcher的实现。

首次渲染

数据观察搞定了之后,我们就可以把render函数渲染到我们的界面上了。在Vue里面我们有一个this.$mount()函数,所以要实现Vue.prototype.$mount函数:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(6)

以上的代码终于牵扯到我们Watcher这个主角了,这里其实就是我们的渲染wather,这里的目的是通过Watcher来实现执行render函数,从而把数据插入到root节点里面去。下面看最简单的Watcher实现

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(7)

通过上面的一顿操作,终于在render中终于可以通过this.name 读取到data的数据了,也可以插入到root.innerHTML中去。阶段性的工作我们完成了。如下图,完成的首次渲染✌️

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(8)

数据依赖收集和更新

首先数据收集,我们要有一个收集的地方,就是我们的Dep类,下面呢看看我们去怎么实现这个Dep。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(9)

Dep收集的类是实现了,但是我们怎么去收集了,就是我们数据观察的get里面实例化Dep然后让Dep收集当前的watcher。下面我们一步步来:

  • 1、在上面this.$mount()的代码中,我们运行了new Watcher(vm, vm.$options.render, () => {}, true),这时候我们就可以在Watcher里面执行this.get(),然后执行pushtarget(this),就可以执行这句话Dep.target = watcher,把当前的watcher挂载Dep.target上。下面看看我们怎么实现。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(10)

  • 2、知道Dep.target是怎么来之后,然后上面代码运行了this.get(),相当于运行了vm.$options.render,在render里面会执行this.name,这时候会触发Object.defineProperty·get方法,我们在里面就可以做些依赖收集(dep.depend)了,如下代码

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(11)

  • 3、调用的dep.depend() 实际上是调用了 Dep.target.addDep(this), 此时Dep.target等于当前的watcher,然后就会执行

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(12)

这里双向保存有点绕,大家可以好好去理解一下。下面我们看看收集后的des是怎么样子的。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(13)

  • 4、数据更新,调用this.name = '李四'的时候会触发Object.defineProperty.set方法,里面直接调用dep.notify(),然后循环调用所有的watcer.update方法更新所有watcher,例如:这里也就是重新执行vm.$options.render方法。

有了依赖收集个数据更新,我们也在index.html增加修改data属性的定时方法:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(14)

运行效果如下图

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(15)

到这里我们渲染watcher就全部实现了。

实现computed

首先我们在index.html里面配置一个computed,script标签的代码就如下:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(16)

上面的代码,注意computed是在render里面使用了。

在vue.js中,之前写了下面这行代码。

if(options.computed){ //初始化计算属性 initComputed(vm) }

我们现在就实现这个initComputed,代码如下

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(17)

大家都知道computed是有缓存的,所以创建watcher的时候,会传一个配置{ lazy: true },同时也可以区分这是computed watcher,然后到watcer里面接收到这个对象

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(18)

从上面这句this.value = this.lazy ? undefined : this.get()代码可以看到,computed创建watcher的时候是不会指向this.get的。只有在render函数里面有才执行。

现在在render函数通过this.info还不能读取到值,因为我们还没有挂载到vm上面,上面defineComputed(vm, key, userDef)这个函数功能就是让computed挂载到vm上面。下面我们实现一下。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(19)

上面代码有看到在watcher中调用了watcher.evaluate()和watcher.depend(),然后去watcher里面实现这两个方法,下面直接看watcher的完整代码。

classWatcher{ constructor(vm,exprOrFn,cb,options){ this.vm=vm if(typeofexprOrFn==='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=newSet()//dep已经收集过相同的watcher就不要重复收集了 this.value=this.lazy?undefined:this.get() } get(){ constvm=this.vm pushTarget(this) //执行函数 letvalue=this.getter.call(vm,vm) popTarget() returnvalue } addDep(dep){ letid=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(){ leti=this.deps.length while(i--){ this.deps[i].depend() } } }

代码都实现完完成之后,我们说下流程,

  • 1、首先在render函数里面会读取this.info,这个会触发createComputedGetter(key)中的computedGetter(key);
  • 2、然后会判断watcher.dirty,执行watcher.evaluate();
  • 3、进到watcher.evaluate(),才真想执行this.get方法,这时候会执行pushTarget(this)把当前的computed watcher push到stack里面去,并且把Dep.target 设置成当前的computed watcher`;
  • 4、然后运行this.getter.call(vm, vm) 相当于运行computed的info: function() { return this.name this.age },这个方法;
  • 5、info函数里面会读取到this.name,这时候就会触发数据响应式Object.defineProperty.get的方法,这里name会进行依赖收集,把watcer收集到对应的dep上面;并且返回name = '张三'的值,age收集同理;
  • 6、依赖收集完毕之后执行popTarget(),把当前的computed watcher从栈清除,返回计算后的值('张三 10'),并且this.dirty = false;
  • 7、watcher.evaluate()执行完毕之后,就会判断Dep.target 是不是true,如果有就代表还有渲染watcher,就执行watcher.depend(),然后让watcher里面的deps都收集渲染watcher,这就是双向保存的优势。
  • 8、此时name都收集了computed watcher 和 渲染watcher。那么设置name的时候都会去更新执行watcher.update()
  • 9、如果是computed watcher的话不会重新执行一遍只会把this.dirty 设置成 true,如果数据变化的时候再执行watcher.evaluate()进行info更新,没有变化的的话this.dirty 就是false,不会执行info方法。这就是computed缓存机制。

实现了之后我们看看实现效果:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(20)

这里conputed的对象set配置没有实现,大家可以自己看看源码

watch实现

先在script标签配置watch配置如下代码:

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(21)

知道了computed实现之后,自定义watch实现很简单,下面直接实现initWatch

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(22)

然后修改一下Watcher,直接看Wacher的完整代码。

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(23)

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(24)

最后看看效果

在vue项目中什么时候用watch(从Vue源码角度深挖Watch)(25)

当然很多配置没有实现,比如说options.immediate 或者options.deep等配置都没有实现。篇幅太长了。自己也懒~~~ 完结撒花

详细代码:https://github.com/naihe138/write-vue

作者:Naice

转发链接:https://segmentfault.com/a/1190000023196603

,

免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com

    分享
    投诉
    首页