不管读再多的文档,感觉还是自己写 (抄) 一遍记得牢。。
- var demo = new vue({
- el: '#demo',
- data: {
- text: "before change text",
- text2: "before change text2",
- },
- render() {
- return this.__h__('div', {}, [
- this.__h__('span', {}, [this.__toString__(this.text)]),
- this.__h__('span', {}, [this.__toString__(this.text2)])
- ])
- }
- })
- setTimeout(function() {
- demo.text = "after change text"
- demo.text2 = "after change text2"
- }, 2000)
- setTimeout(function() {
- demo.text = "after after change text"
- demo.text2 = "after after change text2"
- }, 3000)
先实现一个小目标,text 和 text2 能在页面上呈现出来,在实现一个大点的,2 秒后和 3 秒后页面中的文本改变。
底板先摆出来:
- class Vue {
- constructor(options) {
- 先将传入的data参数放到实例的_data属性上以供调用
- this._data = options.data
- }
- }
下面要干的第一步是将 new Vue 实例时传入的参数处理下。怎么个处理法?例如:我们的 text 和 text2 属性是放到_data 这个属性上的,那么调用的时候可能就要写 demo._data.text。这样写太复杂,不如 demo.text 方便。
- constructor(options) {
- this.$options = options
- this._data = options.data
- Object.keys(options.data).forEach(key => this._proxy(key))
- }
- _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
- }
- })
- }
接下来就是实现数据与页面绑定的关键了 ===>defineReactive 方法。按照观察者模式,我们希望知道 text 和 text2 是否被调用,如果他们被调用,那么当他们改变的时候我们就需要重新刷新页面了。恰好,Object.defineProperty 就提供对象被调用或被改变的回调。那么 class Dep 是用来干嘛的呢,简单的说是为了收集依赖:当 vue 遍历 data 的参数时,会在每次循环的函数闭包中生成一个 Dep 的实例,可以认为每个参数 (text 和 text2) 都有一个对应的 Dep 实例,当 text 或者 text2 被调用 (即 get() 方法被调用)时,会将注册的事件 (也就是代码里的 Dep.target) 添加到 Dep 实例的 subs 数组里,然后当 text 或者 text2 改变时,取出 subs 数组里收集到的订阅事件,然后循环执行所有的订阅。这样就实现了 data 改变到页面刷新的自动过程。
- constructor(options) {
- ...
- observer(options.data)
- }
- function observer(value, cb) {
- Object.keys(value).forEach((key) => defineReactive(value, key, value[key], cb))
- }
- function defineReactive(obj, key, val, cb) {
- // 每个属性都创建了一个dep实例,所以update方法被添加到了各自的dep.subs数组里
- 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()
- }
- })
- }
- class Dep {
- constructor() {
- this.subs = []
- }
- add(cb) {
- if(this.subs.indexOf(cb) === -1) {
- console.log('被添加到监听了')
- this.subs.push(cb)
- }
- }
- notify() {
- console.log('notify被触发了')
- this.subs.forEach((cb) => cb())
- }
- }
接着看订阅事件 (也就是上面的 Dep.target) 具体指的是啥。
- constructor(options) {
- this.$options = options
- this._data = options.data
- Object.keys(options.data).forEach(key => this._proxy(key))
- observer(options.data)
- watch(this, this._render.bind(this), this._update.bind(this))
- }
- _render() {
- let VNode = this.$options.render.call(this)
- document.getElementById(this.$options.el).innerhtml = JSON.stringify(VNode)
- return VNode
- }
- _update() {
- console.log("我将要更新");
- const vdom = this._render.call(this)
- }
- function watch(vm, exp, cb) {
- // exp==>_render
- // cb==>update
- // 先执行一下render,并且让update方法watch this对象
- // 这一步比较巧妙,先把update这个cb放到target对象上。执行_render时,如果使用到了data上的对象,那么update就会被添加到dep里,也就实现了update watch data.
- Dep.target = cb
- let vdom = exp()
- Dep.target = null
- return vdom
- }
。。其实想想也就知道了,当然是数据改变,页面要重新渲染了。watch 方法里有个很牛叉的地方:首先 Dep.target = cb,将_update 这个订阅事件赋给了 Dep.target,然后执行了 exp 也就是_render 方法,到最后执行的就是我们 new Vue 实例时传入的 render 方法,
- render() {
- return this.__h__('div', {},
- [this.__h__('span', {},
- [this.__toString__(this.text)]), this.__h__('span', {},
- [this.__toString__(this.text2)])])
- }
看,这个方法里调用了 this.text 和 this.text2 哎,于是 text 和 text2 的 get 回调被触发。
- get: () => {
- if (Dep.target) {
- dep.add(Dep.target)
- }
- return val
- },
由于 Dep.target 在上一刻神奇的被赋值 (_update 方法) 了,所以_update 被收进了 text 和 text2 的 dep 实例里,当 render 执行完后,Dep.target = null 又被神奇的置为了空。
就这样当执行到 demo.text = "after change text" 时,_update 方法被执行了,页面被重新渲染了。
当我们在一次 setTimeout() 里既改变 text,又改变 text2 时,由于_update 既被添加到了 text 的 dep 实例中,又被添加到了 text2 的 dep 实例中,所以_render 会被执行两次。第一次_render 后 text 被改变成新的了,document.getElementById(this.options.el).innerHTML = ... 执行,页面又刷新;这看起来是期望得到了。但是由于 js 是同步执行的,这两次页面的改变的间隔几乎可以忽略不计,人眼肯定是无法差觉得。当页面复杂时,页面会由于回流或重绘造成性能问题。
此处解决的方法是使用 Promise,在同步任务执行完后执行 Microtask 时更新页面:
代码
- constructor(options) {
- this.queueNextTick = ''
- }
- _update() {
- if(!this.queueNextTick) {
- this.queueNextTick = new Promise((resolve)=>{
- resolve()
- })
- this.queueNextTick.then(()=>{
- console.log("我将要更新");
- const vdom = this._render.call(this)
- console.log(vdom);
- this.queueNextTick = ''
- })
- }
- }
参考 (嗯 90%):
- [理解vue2.0的响应式架构.md](https://segmentfault.com/a/1190000007334535)
来源: https://juejin.im/post/5a4df4965188252a3d386ae6