VueJs源码分析-如何实现 observer 和 watcher

Posted VueJs技术分享

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VueJs源码分析-如何实现 observer 和 watcher相关的知识,希望对你有一定的参考价值。

本文分享什么


理解vue2.0的响应式架构,就是下面这张图

顺带介绍他比react快的其中一个原因

本分实现什么


const demo = new Vue({  data: {    text: "before",  },  
//对应的template 为
//<div><span>{{text}}</span></div>
 render(h){    
   return h(
     'div', {},
     [ h('span',
     {},
     [this.__toString__(this.text)]
   )   ])  } }) setTimeout(function(){   demo.text = "after" }, 3000)

对应的虚拟dom会从
<div><span>before</span></div>

 变为

<div><span>after</span></div>

ok,let begin!!!

第一步, 讲data 下面所有属性变为observable

来来来先看代码吧

 class Vue {  constructor(options) {
 this.$options = options
 this._data = options.data  observer(options.data, this._update)
 this._update() }  

_update(){        
   this.$options.render()   } }  
 
function observer(value, cb){
   Object.keys(value).forEach((key) =>
   defineReactive(value, key, value[key] , cb)) }
          
function defineReactive(obj, key, val, cb) {
   Object.defineProperty(obj, key, {    enumerable: true,    configurable: true,    get: ()=>{},    set:newVal=> {    cb()  }  }) }    
   
var demo = new Vue({    el: '#demo',    data: {    text: 123,    },    render(){        
    console.log("我要render了")    } }) setTimeout(function(){    demo._data.text = 444 }, 3000)


为了好演示我们只考虑最简单的情况


var demo = new Vue({   el: '#demo',   data: {    text: 123,    render(){
   console.log("我要render了")   }
})


其中data 里面所有的属性置于 observer,然后data里面的属性,比如 text 以改变,就引起_update()函数调用进而重新渲染,是怎样做到的呢,我们知道其实就是赋值的时候就要改变对吧,当我给data下面的text 赋值的时候 set 函数就会触发,这个时候 调用 _update 就ok了,但是

 setTimeout(function(){   demo._data.text = 444 }, 3000)


demo._data.text没有demo.text用着爽,没关系,我们加一个代理

_proxy(key) {        
 const self = this  Object.defineProperty(self, key, {  configurable: true,  enumerable: true,  get: function proxyGetter () {          
  return self._data[key]  },  et: function proxySetter (val) {   self._data[key] = val  }  })
}

然后在Vueconstructor加上下面这句

Object.keys(options.data).forEach
(key => this._proxy(key))

第一步先说到这里,我们会发现一个问题,data中任何一个属性的值改变,都会引起
_update的触发进而重新渲染,属性这显然不够精准啊

第二步,详细阐述第一步为什么不够精准

比如考虑下面代码

new Vue({      template: `        <div>
         <section>            <span>name:</span> {{name}}        
         </section>          <section>            <span>age:</span> {{age}}          
         </section>        <div>`,      data: {        name: 'js',        age: 24,        height: 180      }    })    setTimeout(function(){      demo.height = 181    }, 3000)

template里面只用到了data上的两个属性nameage,但是当我改变height的时候,用第一步的代码,会不会触发重新渲染?会!,但其实不需要触发重新渲染,这就是问题所在!!

第三步,上述问题怎么解决

简单说说虚拟 DOM

首先,template最后都是编译成render函数的(具体怎么做,就不展开说了,以后我会说的),然后render 函数执行完就会得到一个虚拟DOM,为了好理解我们写写最简单的虚拟DOM

function VNode(tag, data, children, text) {     
return { tag: tag, data: data, children: children, text: text }
}    
class Vue { constructor(options) {        
this.$options = options        
const vdom = this._update()      
console.log(vdom)
}       
_update() {        
return this._render.call(this) }
_render() {        
const vnode = this.$options.render.call(this)        
return vnode }        __h__(tag, attr, children) {        
return VNode(tag, attr, children.map((child)=>{        
if(typeof child === 'string'){
 return
 VNode(undefined, undefined, undefined, child)
}else{
  return child } }))}
            __toString__(val) {               return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val);} }    
var
demo = new Vue({ el: '#demo', data: {  text: "before", },            render(){       return this.__h__('div', {}, [this.__h__('span', {}, [this.__toString__(this.text)]) ])} })

我们运行一下,他会输出

{ tag: 'div', data: {}, children:[{ tag: 'span',  data: {},  children: [{  children: undefined,  data: undefined,  tag: undefined,  text: '' 
   // 正常情况为 字符串 before,
   //因为我们为了演示就不写代理的代码,所以这里为空
  }  ] } ]}

这就是 虚拟最简单虚拟DOM,taghtml 标签名,data 是包含诸如 class 和 style 这些标签上的属性,childen就是子节点,关于虚拟DOM就不展开说了。
回到开始的问题,也就是说,我得知道,render 函数里面依赖了vue实例里面哪些变量(只考虑render 就可以,因为template 也会是帮你编译成render)。叙述有点拗口,还是看代码吧

    var demo = new Vue({      el: '#demo',      data: {        text: "before",        name: "123",        age: 23      },      render(){        
      return this.__h__('div', {},
      [this.__h__('span', {},
      [this.__toString__(this.text)])        ])      }    })

就像这段代码,render 函数里其实只依赖text,并没有依赖 name和 age,所以,我们只要text改变的时候
我们自动触发 render 函数 让它生成一个虚拟DOM就ok了(剩下的就是这个虚拟DOM和上个虚拟DOM做比对,然后操作真实DOM,只能以后再说了),那么我们正式考虑一下怎么做

第三步,'touch' 拿到依赖

回到最上面那张图,我们知道data上的属性设置defineReactive后,修改data 上的值会触发 set
那么我们取data上值是会触发 get了。
对,我们可以在上面做做手脚,我们先执行一下render,我们看看data上哪些属性触发了get,我们岂不是就可以知道 render 会依赖data 上哪些变量了。
然后我么把这些变量做些手脚,每次这些变量变的时候,我们就触发render
上面这些步骤简单用四个子概括就是 计算依赖。
(其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computed 和 watch 的原理,也是mobx的核心)

第一步,

我们写一个依赖收集的类,每一个data 上的对象都有可能被render函数依赖,所以每个属性在defineReactive
时候就初始化它,简单来说就是这个样子的

    class Dep {      constructor() {       
     this.subs = []      }      add(cb) {        
     this.subs.push(cb)      }      notify() {        
     console.log(this.subs);        
     this.subs.forEach((cb) => cb())      }    }    
  function defineReactive(obj, key, val, cb) {      
   const dep = new Dep()    
   Object.defineProperty(obj, key,
   {
    // 省略    })   }

然后,当执行render 函数去'touch'依赖的时候,依赖到的变量get就会被执行,然后我们就可以把这个 render 函数加到 subs 里面去了。
当我们,set 的时候 我们就执行 notify 将所有的subs数组里的函数执行,其中就包含render 的执行。
至此就完成了整个图,好我们将所有的代码展示出来

function VNode(tag, data, children, text) {
     return {        tag: tag,        data: data,        children: children,        text: text      }    }  
class Vue { constructor(options) {    
   this.$options = options      
   this._data = options.data
   Object.keys(options.data)
   .forEach(key => this._proxy(key))   observer(options.data)        
   const vdom = watch(this, this._render.bind(this),
   this._update.bind(this))
   console.log(vdom)   }
   _proxy(key) {        
    const self = this    Object.defineProperty(self, key, {    configurable: true,    enumerable: true,    get: function proxyGetter () {          
    return self._data[key]    },    set: function proxySetter (val) {    self._data.text = val    }    })   }   _update() {        
    console.log("我需要更新");      
    const vdom = this._render.call(this)        
    console.log(vdom);   }   _render() {        
     return this.$options.render.call(this)   }   __h__(tag, attr, children) {        
     return VNode(tag, attr, children.map((child)=>{        
     if(typeof child === 'string'){          
      return VNode(undefined, undefined, undefined, child)     }else{            
      return child     }     }))    }   __toString__(val) {        
      return val == null ? '' : typeof val === 'object' ?
      JSON.stringify(val, null, 2) : String(val);      }    }       
    function observer(value, cb){    
     Object.keys(value).forEach((key) =>
     defineReactive(value, key, value[key] , cb))    }    
    function defineReactive(obj, key, val, cb) {      
        const dep = new Dep()      
        Object.defineProperty(obj, key, {        enumerable: true,        configurable: true,        get: ()=>{          
          if(Dep.target){            dep.add(Dep.target)          }    
          return val        },
       set: newVal => {        
        if(newVal === val)            
        return  val = newVal          dep.notify()        }      })    }    
    function watch(vm, exp, cb){      Dep.target = cb    
      return exp()    }    
    class Dep {      constructor() {        
      this.subs = []      }      add(cb) {        
      this.subs.push(cb)      }      notify() {        
      this.subs.forEach((cb) => cb())      }    }    Dep.target = null    var demo = new Vue({      el: '#demo',      data: {        text: "before",      },      render(){        
       return this.__h__('div', {},
       [this.__h__('span', {},
       [this.__toString__(this.text)])        ])      }    })    setTimeout(function(){       demo.text = "after"     }, 3000)

我们看一下运行结果

好我们解释一下 Dep.target 因为我们得区分是,普通的get,还是在查找依赖的时候的get
所有我们在查找依赖时候,我们将

function watch(vm, exp, cb){  Dep.target = cb 
  return exp() }

Dep.target 赋值,相当于 flag 一下,然后 get 的时候

get: () => {
 if (Dep.target) {    dep.add(Dep.target)  }
 return val }

判断一下,就好了。到现在为止,我们再看那张图是不是就清楚很多了?

总结

vue2.0 以上代码为了好展示,都采用最简单的方式呈现。
不过整个代码执行过程,甚至是命名方式都和vue2.0一样
对比react,vue2.0 自动帮你监测依赖,自动帮你重新渲染,而
react 要实现性能最大化,要做大量工作,
而 vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class属性,
vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比 达到性能最大化的react 还要快的一个原因
后续,我会简单聊聊,vue2.0的diff。


以上是关于VueJs源码分析-如何实现 observer 和 watcher的主要内容,如果未能解决你的问题,请参考以下文章

vuejs源码用了啥设计模式,具体点的

vue2.0源码分析之理解响应式架构

JDK自带的观察者模式源码分析以及和自定义实现的取舍

如何在 vuejs 中获取价值 [__ob__: Observer]

vue源码解读Observer/Dep/Watcher是如何实现数据绑定的

vue系列---响应式原理实现及Observer源码解析