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

vue

分享前啰嗦

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

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

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

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

本文分享什么

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

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

本分实现什么

  1. const demo = new Vue({ 
  2.   data: { 
  3.     text: "before", 
  4.   }, 
  5.   //对应的template 为 <div><span>{{text}}</span></div> 
  6.   render(h){ 
  7.     return h('div', {}, [ 
  8.       h('span', {}, [this.__toString__(this.text)]) 
  9.     ]) 
  10.   } 
  11. }) 
  12.  setTimeout(function(){ 
  13.    demo.text = "after" 
  14.  }, 3000)  

对应的虚拟dom会从

<div><span>before</span></div> 变为 <div><span>after</span></div>

好,开始吧!!!

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

来来来先看代码吧

  1. class Vue { 
  2.       constructor(options) { 
  3.         this.$options = options 
  4.         this._data = options.data 
  5.         observer(options.data, this._update) 
  6.         this._update() 
  7.       } 
  8.       _update(){ 
  9.         this.$options.render() 
  10.       } 
  11.     } 
  12.  
  13.  
  14.     function observer(value, cb){ 
  15.       Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb)) 
  16.     } 
  17.  
  18.     function defineReactive(obj, key, val, cb) { 
  19.       Object.defineProperty(obj, key, { 
  20.         enumerable: true, 
  21.         configurable: true, 
  22.         get: ()=>{}, 
  23.         set:newVal=> { 
  24.           cb() 
  25.         } 
  26.       }) 
  27.     } 
  28.  
  29.     var demo = new Vue({ 
  30.       el: '#demo', 
  31.       data: { 
  32.         text: 123, 
  33.       }, 
  34.       render(){ 
  35.         console.log("我要render了") 
  36.       } 
  37.     }) 
  38.  
  39.      setTimeout(function(){ 
  40.        demo._data.text = 444 
  41.      }, 3000)  

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

  1. var demo = new Vue({ 
  2.      el: '#demo', 
  3.      data: { 
  4.        text: 123, 
  5.      }, 
  6.      render(){ 
  7.        console.log("我要render了") 
  8.      } 
  9.    })  

中data 里面所有的属性置于 observer,然后data里面的属性,比如 text

以改变,就引起_update()函数调用进而重新渲染,是怎样做到的呢,我们知道其实就是赋值的时候就要改变对吧,当我给data下面的text

赋值的时候 set 函数就会触发,这个时候 调用_update 就ok了,但是

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

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

  1. _proxy(key) { 
  2.       const self = this 
  3.       Object.defineProperty(self, key, { 
  4.         configurable: true, 
  5.         enumerable: true, 
  6.         get: function proxyGetter () { 
  7.           return self._data[key] 
  8.         }, 
  9.         set: function proxySetter (val) { 
  10.           self._data[key] = val 
  11.         } 
  12.       }) 
  13.     }  

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

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

第一步先说到这里,我们会发现一个问题,data中任何一个属性的值改变,都会引起

_update的触发进而重新渲染,属性这显然不够精准啊

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

比如考虑下面代码

  1. new Vue({ 
  2.      template: ` 
  3.        <div> 
  4.          <section> 
  5.            <span>name:</span> {{name}} 
  6.          </section> 
  7.          <section> 
  8.            <span>age:</span> {{age}} 
  9.          </section> 
  10.        <div>`, 
  11.      data: { 
  12.        name: 'js', 
  13.        age: 24, 
  14.        height: 180 
  15.      } 
  16.    }) 
  17.  
  18.    setTimeout(function(){ 
  19.      demo.height = 181 
  20.    }, 3000)  

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

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

简单说说虚拟 DOM

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

  1. function VNode(tag, data, children, text) { 

  2.       return { 
  3.         tag: tag, 
  4.         data: data, 
  5.         children: children, 
  6.         text: text 
  7.       } 
  8.     } 
  9.  
  10.     class Vue { 
  11.       constructor(options) { 
  12.         this.$options = options 
  13.         const vdom = this._update() 
  14.         console.log(vdom) 
  15.       } 
  16.       _update() { 
  17.         return this._render.call(this) 
  18.       } 
  19.       _render() { 
  20.         const vnode = this.$options.render.call(this) 
  21.         return vnode 
  22.       } 
  23.       __h__(tag, attr, children) { 
  24.         return VNode(tag, attr, children.map((child)=>{ 
  25.           if(typeof child === 'string'){ 
  26.             return VNode(undefined, undefined, undefined, child) 
  27.           }else{ 
  28.             return child 
  29.           } 
  30.         })) 
  31.       } 
  32.       __toString__(val) { 
  33.         return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val); 
  34.       } 
  35.     } 
  36.  
  37.  
  38.     var demo = new Vue({ 
  39.       el: '#demo', 
  40.       data: { 
  41.         text: "before", 
  42.       }, 
  43.       render(){ 
  44.         return this.__h__('div', {}, [ 
  45.           this.__h__('span', {}, [this.__toString__(this.text)]) 
  46.         ]) 
  47.       } 
  48.     })  

我们运行一下,他会输出

  1.       tag: 'div', 
  2.       data: {}, 
  3.       children:[ 
  4.         { 
  5.           tag: 'span', 
  6.           data: {}, 
  7.           children: [ 
  8.             { 
  9.               children: undefined, 
  10.               data: undefined, 
  11.               tag: undefined, 
  12.               text: '' // 正常情况为 字符串 before,因为我们为了演示就不写代理的代码,所以这里为空 
  13.             } 
  14.           ] 
  15.         } 
  16.       ] 
  17.     }  

这就是 虚拟最简单虚拟DOM,tag是html 标签名,data 是包含诸如 class 和 style 这些标签上的属性,childen就是子节点,关于虚拟DOM就不展开说了。

回到开始的问题,也就是说,我得知道,render 函数里面依赖了vue实例里面哪些变量(只考虑render 就可以,因为template 也会是帮你编译成render)。叙述有点拗口,还是看代码吧

  1. var demo = new Vue({ 
  2.       el: '#demo', 
  3.       data: { 
  4.         text: "before", 
  5.         name: "123", 
  6.         age: 23 
  7.       }, 
  8.       render(){ 
  9.         return this.__h__('div', {}, [ 
  10.           this.__h__('span', {}, [this.__toString__(this.text)]) 
  11.         ]) 
  12.       } 
  13.     })  

就像这段代码,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

时候就初始化它,简单来说就是这个样子的

  1. class Dep { 
  2.       constructor() { 
  3.         this.subs = [] 
  4.       } 
  5.       add(cb) { 
  6.         this.subs.push(cb) 
  7.       } 
  8.       notify() { 
  9.         console.log(this.subs); 
  10.         this.subs.forEach((cb) => cb()) 
  11.       } 
  12.     } 
  13.     function defineReactive(obj, key, val, cb) { 
  14.       const dep = new Dep() 
  15.       Object.defineProperty(obj, key, { 
  16.         // 省略 
  17.       }) 
  18.     }  

然后,当执行render 函数去'touch'依赖的时候,依赖到的变量get就会被执行,然后我们就可以把这个 render 函数加到 subs 里面去了。

当我们,set 的时候 我们就执行 notify 将所有的subs数组里的函数执行,其中就包含render 的执行。

至此就完成了整个图,好我们将所有的代码展示出来

  1. function VNode(tag, data, children, text) { 

  2.      return { 
  3.        tag: tag, 
  4.        data: data, 
  5.        children: children, 
  6.        text: text 
  7.      } 
  8.    } 
  9.  
  10.    class Vue { 
  11.      constructor(options) { 
  12.        this.$options = options 
  13.        this._data = options.data 
  14.        Object.keys(options.data).forEach(key => this._proxy(key)) 
  15.        observer(options.data) 
  16.        const vdom = watch(this, this._render.bind(this), this._update.bind(this)) 
  17.        console.log(vdom) 
  18.      } 
  19.      _proxy(key) { 
  20.        const self = this 
  21.        Object.defineProperty(self, key, { 
  22.          configurable: true, 
  23.          enumerable: true, 
  24.          get: function proxyGetter () { 
  25.            return self._data[key] 
  26.          }, 
  27.          set: function proxySetter (val) { 
  28.            self._data.text = val 
  29.          } 
  30.        }) 
  31.      } 
  32.      _update() { 
  33.        console.log("我需要更新"); 
  34.        const vdom = this._render.call(this) 
  35.        console.log(vdom); 
  36.      } 
  37.      _render() { 
  38.        return this.$options.render.call(this) 
  39.      } 
  40.      __h__(tag, attr, children) { 
  41.        return VNode(tag, attr, children.map((child)=>{ 
  42.          if(typeof child === 'string'){ 
  43.            return VNode(undefined, undefined, undefined, child) 
  44.          }else{ 
  45.            return child 
  46.          } 
  47.        })) 
  48.      } 
  49.      __toString__(val) { 
  50.        return val == null ? '' : typeof val === 'object' ? JSON.stringify(val, null, 2) : String(val); 
  51.      } 
  52.    } 
  53.  
  54.    function observer(value, cb){ 
  55.      Object.keys(value).forEach((key) => defineReactive(value, key, value[key] , cb)) 
  56.    } 
  57.  
  58.    function defineReactive(obj, key, val, cb) { 
  59.      const dep = new Dep() 
  60.      Object.defineProperty(obj, key, { 
  61.        enumerable: true, 
  62.        configurable: true, 
  63.        get: ()=>{ 
  64.          if(Dep.target){ 
  65.            dep.add(Dep.target) 
  66.          } 
  67.          return val 
  68.        }, 
  69.        set: newVal => { 
  70.          if(newVal === val) 
  71.            return 
  72.          val = newVal 
  73.          dep.notify() 
  74.        } 
  75.      }) 
  76.    } 
  77.    function watch(vm, exp, cb){ 
  78.      Dep.target = cb 
  79.      return exp() 
  80.    } 
  81.  
  82.    class Dep { 
  83.      constructor() { 
  84.        this.subs = [] 
  85.      } 
  86.      add(cb) { 
  87.        this.subs.push(cb) 
  88.      } 
  89.      notify() { 
  90.        this.subs.forEach((cb) => cb()) 
  91.      } 
  92.    } 
  93.    Dep.target = null 
  94.  
  95.  
  96.    var demo = new Vue({ 
  97.      el: '#demo', 
  98.      data: { 
  99.        text: "before", 
  100.      }, 
  101.      render(){ 
  102.        return this.__h__('div', {}, [ 
  103.          this.__h__('span', {}, [this.__toString__(this.text)]) 
  104.        ]) 
  105.      } 
  106.    }) 
  107.  
  108.  
  109.     setTimeout(function(){ 
  110.       demo.text = "after" 
  111.     }, 3000)  

我们看一下运行结果

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

所有我们在查找依赖时候,我们将

  1. function watch(vm, exp, cb){ 

  2.       Dep.target = cb 
  3.       return exp() 
  4.     }  

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

  1. get: () => { 
  2.           if (Dep.target) { 
  3.             dep.add(Dep.target) 
  4.           } 
  5.           return val 
  6.         },  

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

总结

我非常喜欢,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 还要快的一个原因

然后源码在此,喜欢的记得给个 star 哦

后续,我会简单聊聊,vue2.0的diff。

作者:杨川宝

来源:51CTO

以上是 vue2.0源码分析之理解响应式架构 的全部内容, 来源链接: utcz.com/z/376390.html

回到顶部