博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
vue2.0源码分析之理解响应式架构
阅读量:5786 次
发布时间:2019-06-18

本文共 8989 字,大约阅读时间需要 29 分钟。

分享前啰嗦

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

直接看vue2.0吧。这篇文章在公司分享过,终于写出来了。我们采用用最精简的代码,还原vue2.0响应式架构实现
以前写的那篇 可以作为本次分享的参考。
不过不看也没关系,但是最好了解下

本文分享什么

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

vuedeptrace.jpg

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

本分实现什么

const demo = new Vue({  data: {    text: "before",  },  //对应的template 为 
{
{text}}
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)

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

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          }        })      }

然后在Vueconstructor加上下面这句

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

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

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

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

比如考虑下面代码

new Vue({      template: `        
name: {
{name}}
age: {
{age}}
`, 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 是包含诸如 classstyle 这些标签上的属性,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,并没有依赖 nameage,所以,我们只要text改变的时候

我们自动触发 render 函数 让它生成一个虚拟DOM就ok了(剩下的就是这个虚拟DOM和上个虚拟DOM做比对,然后操作真实DOM,只能以后再说了),那么我们正式考虑一下怎么做

第三步,'touch' 拿到依赖

回到最上面那张图,我们知道data上的属性设置defineReactive后,修改data 上的值会触发 set

那么我们取data上值是会触发 get了。
对,我们可以在上面做做手脚,我们先执行一下render,我们看看data上哪些属性触发了get,我们岂不是就可以知道 render 会依赖data 上哪些变量了。
然后我么把这些变量做些手脚,每次这些变量变的时候,我们就触发render
上面这些步骤简单用四个子概括就是 计算依赖。
(其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computedwatch 的原理,也是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)

我们看一下运行结果

D4A9A9B8-03CA-4A73-A12F-30409E08D99D.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 要实现性能最大化,要做大量工作,比如我以前分享的
而 vue2.0 天然帮你做到了最优,而且对于像万年不变的 如标签上静态的class属性,
vue2.0 在重新渲染后做diff 的时候是不比较的,vue2.0比 达到性能最大化的react 还要快的一个原因
然后源码,喜欢的记得给个 star 哦?
后续,我会简单聊聊,vue2.0的diff。
如果有疑问,可以在评论区留言哈

转载地址:http://xjtyx.baihongyu.com/

你可能感兴趣的文章
详解区块链中EOS的作用。
查看>>
我的友情链接
查看>>
mysql-error 1236
查看>>
sshd_config设置参数笔记
查看>>
循序渐进Docker(一)docker简介、安装及docker image管理
查看>>
jsp页面修改后浏览器中不生效
查看>>
大恶人吉日嘎拉之走火入魔闭门造车之.NET疯狂架构经验分享系列之(四)高效的后台权限判断处理...
查看>>
信号量实现进程同步
查看>>
Spring4-自动装配Beans-通过构造函数参数的数据类型按属性自动装配Bean
查看>>
win10.64位wnmp-nginx1.14.0 + PHP 5. 6.36 + MySQL 5.5.59 环境配置搭建 结合Thinkphp3.2.3
查看>>
如何查看python selenium的api
查看>>
Python_Mix*random模块,time模块,sys模块,os模块
查看>>
iframe刷新问题
查看>>
数据解码互联网行业职位
查看>>
我所见的讲的最容易理解,逻辑最强的五层网络模型,来自大神阮一峰
查看>>
vue-cli项目打包需要修改的路径问题
查看>>
js实现复选框的操作-------Day41
查看>>
数据结构化与保存
查看>>
[SpringBoot] - 配置文件的多种形式及优先级
查看>>
chrome浏览器开发者工具之同步修改至本地
查看>>