源码学习VUE之响应式原理

写意风流

首先我们先自己尝试实现一下数据监测。所谓数据监测就是当一个值改变时,用到这个值得地方做出相应改变。里面的核心就是 Object.defineProperty

Object.defineProperty

var obj = {a: 1};
Object.defineProperty(obj, a, {
    enumerable: true,
    configurable: true,
    get: function(){
        // 当调用get方法是,就表明用到了该属性。
        //这边的问题就卡在  需要收集什么
    }
})

收集的依赖是什么

我们收集依赖的目的就是当值改变时,依赖的部分也要做出相应的改变。
但是现在的问题是依赖的地方会有很多,类型也不一样。可能是template里面用到,可能是computed里面计算用到。也有可能用户自己watch监听。先抛开用到地方应该怎么变,想想,一个动作触发一个事件,不就是回调函数的逻辑吗?
拿最简单的watch来说:

var callback = function (newVal, oldVal) {
  // do something
}
vm.$watch(obj.a, callback)

这么一看就简单了,我们收集的依赖就是"回调函数"。
就像上面说的,一个属性用到的地方可能会有很多,因此需要收集的回调函数也很多,因此我们用一个数组来保存。
同时呢,这是个通用方法,我们可以封装一下提出来。
因此上面的代码可以改成

var obj = {a: 1};
function defineReactive (data, key, val) {
    var dep = [];
    Object.defineProperty(obj, a, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.push(callback); // 先不管callback哪来的
        },
        set: function(newVal){
            if(val === newVal) return
            val = newVal;
            dep.forEach(function(callback, index){
                callback();
            })
        }
    })
}

去耦合

可以把dep封装成一个对象

class Dep {
    constructor(id){
        this.id = id;
        this.deps = [];
    }
    addSub( sub ){
        this.subs.push(sub)
    }
    removeSub (sub){
        reomve(this.subs, sub)
    }
    notify(){
        this.deps.forEach(function(callback, index){
            callback();
        })
    }
}

function defineReactive (data, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, a, {
        enumerable: true,
        configurable: true,
        get: function(){
            dep.addSub(callback); // 先不管callback哪来的
        },
        set: function(newVal){
            if(val === newVal) return
            val = newVal;
            dep.notify();
        }
    })
}

Watcher

上面虽然借助callback来帮助理解,但真正实现肯定不可能真是callback。任何一个函数在不同的上下文中执行结果都不相同,光拿到要执行的函数肯定不行,还得有执行的上下文。因此我们可用个类包装一下,observe中不关心怎么执行callback,只需要通知一个监听者自己去做更新操作就好。这个监听者就是watcher.

class Watcher {
    constructors(component, getter, cb){
        this.cb = cb // 对应的回调函数,callback
        this.getter = getter;
        this.component = component; //这就是执行上下文
    }
    
    //收集依赖
    get(){
        Dep.target = this;
        
        this.getter.call(this.component)
        
        Dep.target = null;
    }
    
    update(){
        this.cb()
    }
}

既然我们将callback换成了Watcher实例,注意这边,Dep里面收集的Watcher实例,可不是Wacther构造函数。那么在数据的getter方法中就要想办法拿到。我们将实例存放在Dep中,一个函数对象上。Dep.target = this,当依赖收集完就销毁 Dep.target = null。因此Observe代码可以改成。

function defineReactive (data, key, val) {
    var dep = new Dep();
    Object.defineProperty(obj, a, {
        // ....
        get: function(){
            if(Dep.target){
                 dep.addSub(Dep.target); // Dep.target是Watcher的实例
            }
        },
       // ...
    })
}

class Dep {
    //...
    notify(){
        this.deps.forEach(function(watcher, index){
            watcher.update();
        })
    }
}

VUE源码

大概流程通了,我们再做点完善。Observe,Dep, Watcher三个关系弄清楚了。现在的问题是,怎么收集依赖和回调。举例来说:

<div id="app">
  <input type="text" v-model="name"/>
  <div>{{name}}</div>
</div>

new Vue({
    el: "#app",
    data: {
        name: "默认值",
        age: 29
    }
})

一: template中的依赖

name属性直接在template中用到。那么只要触发render,就可以收集到依赖。当然,收集到依赖后,需要及时更新。把DOM中的{{name}}替换成Data中对应的值。

这部分代码在 lifecycle.jsmountComponent方法中,可以精简为

export function mountComponent(){
    ...
    callHook(vm, 'beforeMount') //生命周期函数
    var updateComponent = () => {
    // 先通过render收集依赖,再通过update将虚拟DOM中的值同步到真实节点中
        vm._update(vm._render(), hydrating)
    }
    new Watcher(vm, updateComponent, emptyFunc, {
            before () {
                if (vm._isMounted) {
                    callHook(vm, 'beforeUpdate') //生命周期函数
                  }
            }
    }
    
    vm._isMounted = true
    callHook(vm, 'mounted')  //生命周期函数
}

每个模板实例化一个Watcher实例。这也与官网的流程图一致

$watch

<div id="app">
  <input type="text" v-model="name"/>
  <div>{{name}}</div>
</div>

new Vue({
    el: "#app",
    data: {
        name: "默认值",
        age: 29
    },
    watch: {
        age: function(newValue, oldValue){
            console.log("新的值为:" + newValue)
        }
    }
})

传进来的options会在initState中处理

export function initState (vm: Component) {
  //vm
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}

.....

function initWatch (vm: Component, watch: Object) {
  for (const key in watch) {
    const handler = watch[key]
    if (Array.isArray(handler)) {
      for (let i = 0; i < handler.length; i++) {
        createWatcher(vm, key, handler[i])
      }
    } else {
      createWatcher(vm, key, handler)
    }
  }
}
function createWatcher (
  vm: Component,
  expOrFn: string | Function,
  handler: any,
  options?: Object
) {
  if (isPlainObject(handler)) {
    options = handler
    handler = handler.handler
  }
  if (typeof handler === 'string') {
    handler = vm[handler]
  }
  return vm.$watch(expOrFn, handler, options)
}

可以看到对于options中的watch其实就是执行$watch方法。

Vue.prototype.$watch = function (
    expOrFn: string | Function,
    cb: any,
    options?: Object
  ): Function {
    const vm: Component = this
    options = options || {}
    options.user = true
    // expOrFn: key, cb: callback watch中的每个key都实例出Wacther实例
    const watcher = new Watcher(vm, expOrFn, cb, options)
    // immediate: 如果为true就立刻执行一次,否则第一次进来不执行,当data改变才会触发执行
    if (options.immediate) {
      cb.call(vm, watcher.value)
    }
    return function unwatchFn () {
      watcher.teardown()
    }
  }

computed

同watch,计算属性computed也是在initData中处理。

function initComputed (vm: Component, computed: Object) {
  // $flow-disable-line
  const watchers = vm._computedWatchers = Object.create(null)
 
   for (const key in computed) {
    const userDef = computed[key]
    const getter = typeof userDef === 'function' ? userDef : userDef.get
    
    // 可以看到,VUE为每个computed属性也都生成了一个watcher实例。
    //而这边的getter就是计算属性的计算函数。必须先计算一次才能触发依赖的属性的get方法,收集依赖
    watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
    
    if (!(key in vm)) {
      defineComputed(vm, key, userDef)
    }
  }
}

结论

这边就不再放出VUE的数据监听部分源码,可以自己阅读watcher.js,dep.js,observer/index.js。总体代码和我们自己实现的很像,只是比我们代码更缜密,多了些其他功能。比如$watcher之后会返回一个取消函数,可以取消监听。
就像上面分析的,一个监听流程的完成必须包含:

  1. 数据本身可被监听(定义了set,get)。
  2. 这个数据被收集了依赖。也就是有人监听。

我们知道VUE为template,watch,和computed中的属性实例化了Watcher。而只有在data中的属性才会再initState时进行监听操作。因此我们可以得出结论,

  1. data中的属性才可以被监听。
  2. 只有在templete中用到的属性或被手动watch的,或计算属性用到的,数值改变时才会执行相应的操作。
阅读 510

前端成长之路
记录前端成长路上的心得体会。

诸事烦于心今日岂能浪迹天涯,

175 声望
8 粉丝
0 条评论

诸事烦于心今日岂能浪迹天涯,

175 声望
8 粉丝
宣传栏