Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

深入浅出 - vue1.0之State原理 #13

Open
berwin opened this issue Jan 5, 2017 · 8 comments
Open

深入浅出 - vue1.0之State原理 #13

berwin opened this issue Jan 5, 2017 · 8 comments
Assignees

Comments

@berwin
Copy link
Owner

berwin commented Jan 5, 2017

深入浅出 - vue之State

本文讲的内容是 vue 1.0 版本~

有些同学可能不知道state是什么,可能还会有疑问,这个跟vuex中的state是不是有啥联系?

在vue文档当中没有在任何地方提到过关于state这个单词,所以同学们发蒙是正常的,不用担心

所以在一开始我先说说state是什么以及它都包含哪些内容。

State

state 是源码当中的一个概念,State中包含了大家非常熟悉的PropsMethodsDataComputed,vue内部把他们划分为state中方便管理

所以本篇文章会详细介绍State中这四个大家常用的api的内部是怎样工作的

Methods

Methods 在我们日常使用vue的时候,使用频率可能是最高的一个功能了,那么它的内部实现其实也特别简单,我先贴一段代码

Vue.prototype._initMethods = function () {
  var methods = this.$options.methods
  if (methods) {
    for (var key in methods) {
      this[key] = bind(methods[key], this)
    }
  }
}

在看逻辑之前有几个地方我先翻译一下:

_initMethods 这个内部方法是在初始化Methods时执行,就是上面的流程图中的初始化Methods

this 是当前vue的实例

this.$options 是初始化当前vue实例时传入的参数,举个栗子

const vm = new Vue({
  data: data,
  methods: {},
  computed: {},
  ...
})

上面实例化Vue的时候,传递了一个Object字面量,这个字面量就是 this.$options

清楚了这些之后,我们看这个逻辑其实就是把 this.$options.methods 中的方法绑定到this上,这也就不难理解为什么我们可以使用 this.xxx 来访问方法了

Data

Data 跟 methods 类似,但是比 methods 高级点,主要高级在两个地方,proxyobserve

proxy

Data 没有直接写到 this 中,而是写到 this._data 中(注意:this.$options.data 是一个函数,data是执行函数得到的),然后在 this 上写一个同名的属性,通过绑定setter和getter来操作 this._data 中的数据

proxy的实现:

Vue.prototype._proxy = function (key) {
  // isReserved 判断 key 的首字母是否为 $ 或 _
  if (!isReserved(key)) {
    var 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
      }
    })
  }
}

observe

observe 是用来观察数据变化的,先看一段源码:

Vue.prototype._initData = function () {
  var dataFn = this.$options.data
  var data = this._data = dataFn ? dataFn() : {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object.',
      this
    )
  }
  var props = this._props
  // proxy data on instance
  var keys = Object.keys(data)
  var i, key
  i = keys.length
  while (i--) {
    key = keys[i]
    // there are two scenarios where we can proxy a data key:
    // 1. it's not already defined as a prop
    // 2. it's provided via a instantiation option AND there are no
    //    template prop present
    if (!props || !hasOwn(props, key)) {
      this._proxy(key)
    } else if (process.env.NODE_ENV !== 'production') {
      warn(
        'Data field "' + key + '" is already defined ' +
        'as a prop. To provide default value for a prop, use the "default" ' +
        'prop option; if you want to pass prop values to an instantiation ' +
        'call, use the "propsData" option.',
        this
      )
    }
  }
  // observe data
  observe(data, this)
}

上面源码中可以看到先处理 _proxy,之后把 data 传入了 observe 中, observe 会把 data 中的key转换成getter与setter,当触发getter时会收集依赖,当触发setter时会触发消息,更新视图,具体可以看之前写的一篇文章《深入浅出 - vue之深入响应式原理》

这地方可能有一个地方不容易理解,observe 在转换 getter 和 setter 的时候是这样转换的

// 伪代码
function observe(value) {
  this.value = value
  Object.defineProperty(this.value, key, {...
}

但是我们操作数据是代理到 _data 上的,实际上操作的是 _data,那这个observe监听的是this.value,好像有点不对劲?后来我才发现有一个地方忽略了。

var data = this._data = dataFn ? dataFn() : {}

其实这个地方是同一个引用,observe 中的 this.value 其实就是 _initData 中的 this._data,所以给 this.value 添加getter 和 setter 就等于给 this._data 设置 gettersetter

总结

总结起来 data 其实做了两件事

  1. this.$options.data 中的数据可以在 this 中访问
  2. 观察数据的变化做出不同的响应

Computed

计算属性在vue中也是一个非常常用的功能,而且好多同学搞不清楚它跟watch有什么区别,这里就详细说说计算属性到底是什么,以及它是如何工作的

简单点说,Computed 其实就是一个 getter 和 setter,经常使用 Computed 的同学可能知道,Computed 有几种用法

var vm = new Vue({
  data: { a: 1 },
  computed: {
    // 用法一: 仅读取,值只须为函数
    aDouble: function () {
      return this.a * 2
    },
    // 用法二:读取和设置
    aPlus: {
      get: function () {
        return this.a + 1
      },
      set: function (v) {
        this.a = v - 1
      }
    }
  }
})

如果不希望Computed有缓存还可以去掉缓存

computed: {
  example: {
    // 关闭缓存
    cache: false,
    get: function () {
      return Date.now() + this.msg
    }
  }
}

先说上面那两种用法,一种 value 的类型是function,一种 value 的类型是对象字面量,对象里面有get和set两个方法,talk is a cheap, show you a code...

function noop () {}
Vue.prototype._initComputed = function () {
  var computed = this.$options.computed
  if (computed) {
    for (var key in computed) {
      var userDef = computed[key]
      var def = {
        enumerable: true,
        configurable: true
      }
      if (typeof userDef === 'function') {
        def.get = makeComputedGetter(userDef, this)
        def.set = noop
      } else {
        def.get = userDef.get
          ? userDef.cache !== false
            ? makeComputedGetter(userDef.get, this)
            : bind(userDef.get, this)
          : noop
        def.set = userDef.set
          ? bind(userDef.set, this)
          : noop
      }
      Object.defineProperty(this, key, def)
    }
  }
}

可以看到对两种不同的类型做了两种不同的操作,function 类型的会把函数当做 getter 赋值给 def.get

object 类型的直接取 def.get 当做 getterdef.set 当做 setter

就是这么easy

但是细心的同学可能发现了一个问题,makeComputedGetter 是什么鬼啊?????直接把 def.get 当做getter就好了啊,为毛要用 makeComputedGetter 生成一个 getter ???

嘿嘿嘿

其实这是vue做的一个优化策略,就是上面最后说的缓存,如果直接把 def.get 当做 getter其实也可以,但是如果当getter中做了大量的计算那么每次用到就会做大量计算比较消耗性能,如果有很多地方都使用到了这个属性,那么程序会变得非常卡。

但如果只有在依赖的数据发生了变化后才重新计算,这样就可以降低一些消耗。

实现这个功能我们需要具备一个条件,就是当 getter 中使用的数据发生变化时能通知到我们这里,也就是说依赖的数据发生变化时,我们能接收到消息,接收到消息后我们在进行清除缓存等操作

而vue中具备这项能力的很明显是 Watcher,当依赖的数据发生变化时 watcher 可以帮助我们接收到消息

function makeComputedGetter (getter, owner) {
  var watcher = new Watcher(owner, getter, null, {
    lazy: true
  })
  return function computedGetter () {
    if (watcher.dirty) {
      watcher.evaluate()
    }
    if (Dep.target) {
      watcher.depend()
    }
    return watcher.value
  }
}

上面就是 makeComputedGetter 的实现原理

代码中 watcher.evaluate() 可以先暂时理解为,执行了 getter 求值的过程,计算后的值会保存在 watcher.value 中。

我们看到求值操作的外面有一个判断条件,当 watcher.dirtytrue 时会执行求值操作

其实,这就相当于缓存了,求值后的值存储在 watcher.value 中,当下一次执行到 computedGetter 时,如果 watcher.dirtyfalse 则直接返回上一次计算的结果

那么这里就有一个问题,watcher.dirty 何时为 true 何时为 false 呢??

默认一开始是 true,当执行了 watcher.evaluate() 后为 false,当依赖发生变化接收到通知后为 true

Watcher.prototype.evaluate = function () {
  // avoid overwriting another watcher that is being
  // collected.
  var current = Dep.target
  this.value = this.get()
  this.dirty = false
  Dep.target = current
}

上面是 evaluate 的实现,就是这么easy~

Watcher.prototype.update = function (shallow) {
  if (this.lazy) {
    this.dirty = true
  } else if (this.sync || !config.async) {
    this.run()
  } else {
    ...
  }
}

watcher 接收到消息时,会执行 update 这个方法,这个方法因为我们的 watcherlazytrue 的所以走第一个判断条件里的逻辑,里面很直接,就是把 this.dirty 设置了 true

这里就又引发了一个问题,我们怎么知道 getter 中到底有哪些依赖,毕竟使用Computed开发的人并不会告诉我们他用到了哪些依赖,那我们怎么知道他使用了哪些依赖?

这个问题非常好

vue在全局弄了一个 Dep.target 用来存当前的watcher,全局只能同时存在一个

当watcher执行get求值的时候,会先把 Dep.target 设为自己,然后在执行 用户写的 getter 方法计算返回值,这时候其实有一个挺有意思的逻辑,data上面我们说过,当数据触发 getter 的时候,会收集依赖,那依赖怎么收集,就是通过全局的 Dep.target 来收集,把Dep.target 添加到观察者列表中,等日后数据发生变化触发 setter 时 执行Dep.targetnotify,到这不知道大家明白过来没???

就是我先把全局的唯一的一个 Dep.target 设置成我自己,然后用户逻辑里爱依赖谁依赖谁,不管你依赖谁都会把我添加到你依赖的那个数据的观察者中,日后只要这个数据发生了变化,我就把this.dirty 设置为 true

所以上面看 Watcher.prototype.evaluate 这个代码的逻辑, this.get() 里会设置Dep.target,等逻辑执行完了他在把 Dep.target 设置回最初的

到这里关于 Computed 就说完了,在使用上其实它跟 watch 没有任何关系,一个是事件,一个是getter和setter,根本不是同一个性质的东西,但在内部实现上 Computed 又是基于watcher实现的。

Props

props 提供了父子组件之间传递数据的能力,在本文讲的vue 1.x.x 版本中,props分三种类型 静态一次(oneTime)单向双向

静态

我们先说静态,什么是静态props?

静态props就是父组件把数据传递给子组件之后,就不在有任何联系,父组件把数据改了子组件中的数据不会变,子组件把数据改了父组件也不会变,数据传过去后他们俩互相之间就没什么事了~

静态的内部工作原理也比较简单:

组件内会通过 props: ['message'] 这样的语法来明确指定子组件组要用到的props,而内部需要做的事就是拿着这些key直接通过 node.getAttribute 在当前el上读一个 value,然后将读到的 value 通过observer绑到子组件的上下文中,绑定后的 value 与当前组件内的 data 数据一样

一次(oneTime)

其实与静态差不多,只有一点不同,oneTime 的值是从父组件中读来的,什么意思呢?

静态的值是通过 node.getAttribute 读来的,读完后直接放到子组件里。

oneTime 的值是通过 node.getAttribute 先读一个key,然后用这个 key 去父组件的上下文读一个值放到子组件里。

所以 oneTime 更强大,因为他可以传递一个用Computed计算后的值,也可以传递一个方法,或什么其他的等等...

单向

单向的意思是说父组件将数据通过props传递给子组件后,父组件把数据改了子组件的数据也会发生变化。

单向props内部的工作原理其实也挺简单的,实现单向props其实我们需要具备一项能力:当数据发生变化时会发出通知,而这项能力就是能够接收到通知。

具备这项能力后,当数据发生变化我们可以得到通知,然后将变化后的数据同步给子组件

而具备这项能力的只有 Watcher,当数据发生变化时,会通知给 Watcher,而 Watcher 在更新子组件内的数据。这样就实现了单向props,废话不多说,上代码:

const parentWatcher = this.parentWatcher = new Watcher(
  parent,
  parentKey,
  function (val) {
    updateProp(child, prop, val)
  }, {
    twoWay: twoWay,
    filters: prop.filters,
    // important: props need to be observed on the
    // v-for scope if present
    scope: this._scope
  }
)

解释一下上面代码:

  • parent 是父组件实例
  • parentKey 是父组件中的一个key,也就是传递给子组件的那个key,是通过这个key在父组件实例中取值然后传递给子组件用的
  • Watcher中的第三个参数是一个更新函数,当 parent 组件的 parentKey 发生变化时,执行这个函数,并把新数据传进来
  • 更新函数中的 updateProp 是用来更新prop的,逻辑很简单,写个伪代码
export function updateProp (vm, prop, value) {
  vm[prop.path] = value
}

所以工作原理就是当 parent 中的 parentKey这个值发生了变化,会执行更新函数,执行函数中拿到新数据把子组件中的数据更新一下

就是这么easy

双向

双向不只是父组件改数据子组件会发生变化,子组件修改数据父组件也会发生变化,实现了父子组件间的数据同步。

双向prop的工作原理与单向的基本一样,只不过多了一个子组件数据变化时,更新父组件内的数据,其实就是多了一个Watcher

self.childWatcher = new Watcher(
  child,
  childKey,
  function (val) {
    parentWatcher.set(val)
  }, {
    // ensure sync upward before parent sync down.
    // this is necessary in cases e.g. the child
    // mutates a prop array, then replaces it. (#1683)
    sync: true
  }
)

其实就是单向prop一个Watcher,双向Prop两个Watcher

const parentWatcher = this.parentWatcher = new Watcher(
  parent,
  parentKey,
  function (val) {
    updateProp(child, prop, val)
  }, {
    twoWay: twoWay,
    filters: prop.filters,
    // important: props need to be observed on the
    // v-for scope if present
    scope: this._scope
  }
)

// set the child initial value.
initProp(child, prop, parentWatcher.value)

// setup two-way binding
if (twoWay) {
  // important: defer the child watcher creation until
  // the created hook (after data observation)
  var self = this
  child.$once('pre-hook:created', function () {
    self.childWatcher = new Watcher(
      child,
      childKey,
      function (val) {
        parentWatcher.set(val)
      }, {
        // ensure sync upward before parent sync down.
        // this is necessary in cases e.g. the child
        // mutates a prop array, then replaces it. (#1683)
        sync: true
      }
    )
  })
}

twoWay 是用来判断当前Prop的类型是单向还是双向用的

下面提供一个关于Props的流程图

总结

State中的PropsMethodsDataComputed这四个在实际应用中是非常常用的功能,如果大家能弄明白它内部的工作原理,对日后开发效率的提升会有很大的帮助

如果有不明白的地方,或者意见或建议都可以在下方评论。

@william-xue
Copy link

九五,来个vue2的

@berwin
Copy link
Owner Author

berwin commented Jun 21, 2017

@william-xue 哈哈哈,过段时间的,最近我们组的项目没怎么用vue,所以就一直没太深究vue2的实现原理。

@liutao
Copy link

liutao commented Jul 24, 2017

@william-xue 哈哈,我有Vue2的https://github.com/liutao/vue2.0-source

@berwin
Copy link
Owner Author

berwin commented Jul 24, 2017

@liutao 厉害了涛哥

@jmx164491960
Copy link

学习了,谢谢博主

@ZengTianShengZ
Copy link

请教个问题:
computed 的缓存内部逻辑还是不太清楚。

如我下面的例子,msg_1 改变了会触发 getMsg1 重新计算,但getMsg2 不会重新计算。上面也提到了是因为 computed 有对依赖做了缓存。问题是怎么知道 getMsg1 的依赖对应的是 msg_1 呢?还望解答。

var vm = new Vue({
  data: { msg_1: '',
            msg_2: '' 
  },
  computed: {
    getMsg1: function () {
      return this.msg_1;
    },
    getMsg2: function () {
      return this.msg_2;
    },
  }
})

@yrl
Copy link

yrl commented Oct 31, 2018

你的书什么时候出版呢。

@berwin
Copy link
Owner Author

berwin commented Oct 31, 2018

@yrl 目前在和出版社审校中,,审校的过程会比较慢,具体什么时候我现在也不太确定~(其实我也很急,哈哈哈)

@berwin berwin changed the title 深入浅出 - vue之State 深入浅出 - vue1之State原理 Apr 11, 2019
@berwin berwin changed the title 深入浅出 - vue1之State原理 深入浅出 - vue1.0之State原理 Apr 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants