vue2.x响应式原理,vue与react响应式简单对比

原创 2017年05月05日 13:16:11

配合ppt食用更佳ppt

实现的最终目标

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下边的所有属性,转换为响应式

思路

  • 当data下的某个属性变化时,如何触发相应的函数?

方案:ES5中新添加了一个方法:Object.defineProperty,通过这个方法,可以自定义gettersetter函数,那么在获取对象属性或者设置对象属性时就能够执行相应的回调函数

Object.defineProperty MDN

代码如下:

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

function observer(obj, cb) {
  Object.keys(obj).forEach((key) => {
    defineReactive(obj, key, obj[key], cb)
  })
}

function defineReactive(obj, key, val, cb) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      console.log('你访问了' + key)
      return val
    },
    set: newVal => {
      if (newVal === val)
        return
      console.log('你设置了' + key)
      console.log('新的' + key + ' = ' + newVal)
      val = newVal
      cb()
    }
  })
}

var demo1 = new Vue({
  el: '#demo',
  data: {
    text: "before"
  },
  render(){
    console.log("我要render了")
  }
})
  • 引发了第二个问题,如果data中的属性是一个对象还能触发我们的回掉函数么?比如说下边的demo
var demo2 = new Vue({
  el: '#demo',
  data: {
    text: "before",
    o: {
      text: "o-before"
    }
  },
  render(){
    console.log("我要render了")
  }
})

方案:用递归完善上边的响应式,需要在它开始对属性进行响应式转换的时候,前边加个判断,即如下

function observer(obj) {
  Object.keys(obj).forEach((key) => {
    if (typeof obj[key] === 'object') {
      new observer(obj[key], cb)
    }
    defineReactive(obj, key, obj[key])
  })
}
  • 实际写的过程中发现调用data的属性时需要这样写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
      }
    })
  }

然后在构造函数中加上这么一句话

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

到此,我们的data属性已经变为响应式的了,只要data的属性发生变化,那么就会触发render函数。这也是为什么只有vue组件中的data属性才是响应式的,其他地方声明的值均不是响应式的原因。但是这里有个问题,即触发render函数的准确度问题!

第二步,解决准确度问题,引出虚拟dom

比如下边的demo

new Vue({
  template: `
    <div>
      <span>name:</span> {{name}}
    <div>`,
  data: {
    name: 'js',
    age: 24
  }
})

setTimeout(function(){
  demo.age = 25
}, 3000)

template中只用到了data中的name属性,但是当修改age属性的时候,会不会触发渲染呢?答案是:会。但实际是不需要触发渲染机制的

解决这个问题,先要简单说下虚拟dom。vue有两种写法:

// template模板写法(最常用的)
new Vue({
  data: {
    text: "before",
  },
  template: `
    <div>
      <span>text:</span> {{text}}
    </div>`
})

// render函数写法,类似react的jsx写法
new Vue({
  data: {
    text: "before",
  },
  render (h) {
    return (
      <div>
        <span>text:</span> {{text}}
      </div>
    )
  }
})

由于vue2.x引入了虚拟dom的原因,这两种写法最终都会被解析成虚拟dom,但在这之前,他们会先被解析函数转换成同一种表达方式,即如下:

new Vue({
  data: {
    text: "before",
  },
  render(){
    return this.__h__('div', {}, [
      this.__h__('span', {}, [this.__toString__(this.text)])
    ])
  }
})

透过上边的render函数中的this.__h__方法,可以简单了解下虚拟dom

function VNode(tag, data, children, text) {
  return {
    tag: tag, // html标签名
    data: data, // 包含诸如 class 和 style 这些标签上的属性
    children: children, // 子节点
    text: text // 文本节点
  }
}

写一个简单的虚拟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)])
    ])
  }
})

回头看问题,也就是说,我需要知道render函数中依赖了data中的哪些属性,只有这些属性变化,才需要去触发render函数

第三步,依赖收集,准确渲染

思路:在这之前,我们已经把data中的属性改成响应式了,当去获取或者修改这些变量时便能够触发相应函数。那这里就可以利用这个相应的函数做些手脚了。当声明一个vue对象时,在执行render函数获取虚拟dom的这个过程中,已经对render中依赖的data属性进行了一次获取操作,这次获取操作便可以拿到所有依赖。

其实不仅是render,任何一个变量的改别,是因为别的变量改变引起,都可以用上述方法,也就是computedwatch的原理

首先需要写一个依赖收集的类,每一个data中的属性都有可能被依赖,因此每个属性在响应式转化(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函数的时候,依赖到的变量的get就会被执行,然后就把这个 render函数加到subs里面去。
- 当set的时候,就执行notify,将所有的subs数组里的函数执行,其中就包含render的执行。

注:代码中有一个Dep.target值,这个值时用来区分是普通的get还是收集依赖时的get

最后完整代码如下:

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[key] = 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(obj) {
  Object.keys(obj).forEach((key) => {
    if (typeof obj[key] === 'object') {
      new observer(obj[key])
    }
    defineReactive(obj, key, obj[key])
  })
}

function defineReactive(obj, key, val) {
  const dep = new Dep()
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: () => {
      if (Dep.target) {
        dep.add(Dep.target)
        Dep.target = null
      }
      console.log('你访问了' + key)
      return val
    },
    set: newVal => {
      if (newVal === val)
        return
      console.log('你设置了' + key)
      console.log('新的' + key + ' = ' + newVal)
      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",
    test: {
      a: '1'
    },
    t: 1
  },
  render() {
    return this.__h__('div', {}, [
      this.__h__('span', {}, [this.__toString__(this.text)]),
      this.__h__('span', {}, [this.__toString__(this.test.a)])
    ])
  }
})

vue react响应式简单对比

综上发现,利用Object.defineProperty这个特性可以精确的写出订阅发布模式,从这点来说,vue是优于react的,在没经过优化之前,vue的渲染机制一定是比react更加准确的,为了验证这一说法,我用两个框架同时写了两个相同的简单项目进行对比。

没有对比就没有伤害:

通过对比发现,react在正常使用的过程中产生了多余的渲染,在移动端或者组件嵌套非常深的情况下会产生非常大的性能消耗,因此在使用react的过程中,写好react生命周期中的shouldComponentUpdate是非常重要的!

 参考

理解vue2.0响应式架构

版权声明:本文为博主原创文章,转载请注明出处。

相关文章推荐

vue 深入响应式原理 注意事项

对vue.js 的响应式的原理的理解,有助于更加灵活的使用vue,和避开一些坑 所以了解其运行原理十分重要...
  • wngzhem
  • wngzhem
  • 2016年12月13日 17:19
  • 704

vue开发:vue,angular,react数据双向绑定原理分析

传统做法 前端维护状态,手动操作DOM更新视图。前端框架对服务器数据通过模版进行渲染。当用户产生了一个动作之后,我们通过document.getElementBy... 手动进行DOM更新。  ...
  • Generon
  • Generon
  • 2017年06月01日 21:55
  • 2721

理解vue实现原理,实现一个简单的Vue框架

参考: 剖析Vue实现原理 - 如何实现双向绑定mvvm Vue.js源码(1):Hello World的背后 Vue.js官方工程本文所有代码可以在git上找到。       其实对JS我研究...
  • pur_e
  • pur_e
  • 2016年11月07日 14:51
  • 19911

vue-study-2 剖析Vue原理&实现双向绑定MVVM

几种实现双向绑定的做法 实现数据绑定的做法有大致如下几种: 发布者-订阅者模式(backbone.js) 脏值检查(angular.js) 数据劫持(vue.js) ...

react、angularjs、vue原理应用场景总结

React 如图: React的虚拟DOM的生成是可以在任何支持Javascript的环境生成的,所以可以在NodeJS或Iojs环境生成虚拟DOM可以直接转成String ...

React实战-一个非常棒的React与Material风格相结合的UI控件库(Material-UI)

React实战-一个非常棒的React与Material风格相结合的UI控件库(Material-UI) ReactJs提供了JSX、UI更新机制、组件化原则等底层支持,但是正如我们在所有的程序语言...

ReactNative开发——封装原生UI组件

ReactNative开发——封装原生UI组件下文我们将制作一个可以用来显示图片的原生UI组件,这个UI组件可以随着手势放大缩小。(封装PhotoView) PhotoView的开源地址:https...

Vue.js深入响应式原理

原文地址:http://vuejs.org.cn/guide/reactivity.html 因工作需要用到Vue.js,所以看了一下相关知识。 深入响应式原理 大部分的基础内容我们已经讲到了...

Vue.js学习 Item12 – 内部响应式原理探究

深入响应式原理大部分的基础内容我们已经讲到了,现在讲点底层内容。Vue.js 最显著的一个功能是响应系统 —— 模型只是普通对象,修改它则更新视图。这让状态管理非常简单且直观,不过理解它的原理也很重要...

Vue的响应式原理

下面的是一张来自Vue官网的图片:如何追踪变化??当把一个普通的JavaScript对象传递给Vue实例的data选项,Vue将遍历这个对象的所有属性,并且利用Object.definePropert...
  • Dear_Mr
  • Dear_Mr
  • 2017年05月15日 23:09
  • 416
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:vue2.x响应式原理,vue与react响应式简单对比
举报原因:
原因补充:

(最多只允许输入30个字)