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

原创 2016-11-03 14:56:34 623
摘要:分享前啰嗦我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考。不过不看也没关系,但是最好了解下Object.definePr

分享前啰嗦

我之前介绍过vue1.0如何实现observer和watcher。本想继续写下去,可是vue2.0横空出世..所以

直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现

以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考。

不过不看也没关系,但是最好了解下Object.defineProperty

本文分享什么

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

531421-20161102144723533-301936887.jpg

顺带介绍他比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>

好,开始吧!!!

第一步, 讲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)

为了好演示我们只考虑最简单的情况,如果看了vue 源码分析之如何实现 observer 和 watcher可能就会很好理解,不过没关系,我们三言两语再说说,这段代码要实现的功能就是将

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] 
        }, 
        set: function proxySetter (val) { 
          self._data[key] = val 
        } 
      }) 
    }

然后在Vue的constructor加上下面这句

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上的两个属性name和age,但是当我改变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,tag是html 标签名,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)

我们看一下运行结果

wKioL1gayqiA24f3AAGc29LICkg728.png

好我们解释一下 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 要实现性能最大化,要做大量工作,比如我以前分享的

react如何性能达到最大化(前传),暨react为啥非得使用immutable.js

react 实现pure render的时候,bind(this)隐患。

而 vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class属性,

vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比 达到性能最大化的react 还要快的一个原因



发布手记

热门词条