《深入浅出vue.js》中变化侦测的源码以及解析

《深入浅出vue.js》中变化侦测的源码以及解析

背景

最近由于公司知识体系变动,需要学习vue来适应新的开发平台,我选择了《深入浅出vue.js》来进行学习,之前有了解过一些vue知识,这次打算深入学习一下,尤其是第一篇变化侦测,学习过程中对javascript也增加了不少理解,我把代码都手打了出来,代码中有很多按照自己理解打上的注释,这里分享给大家。

开门见山

这里我不给大家解释他是什么,书中总结的很好了,我直接把代码贴出来。下面有两点值得注意:

  • 我使用了require以及 module.exports 这个是因为node不支持es6特性
  • 运行环境为node,需要自行安装

@ Deppro.js 依赖收集,哪里引用了reactive的变量,就把他加入Dep

let uid = 0
class Dep {
    constructor() {
        this.id = uid++
        this.subs = [];
    }

    addSub(sub) {
        this.subs.push(sub)
    }

    removeSub(sub) {
        const index = this.subs.indexOf(sub)
        if (index > -1) {
            this.subs.splice(index, 1)
        }
    }

    depend() {
        if(global.target) {
            this.addSub(global.target)
        }
    }

    notify (msg) {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i< l; i++) {
            if (msg) {
                subs[i].update(msg)
            } else {
                subs[i].update()
            }
        }
    }
}
module.exports = Dep;

@ common.js 这里我提取了一些公共方法,使用的时候直接引用即可

function def (obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

function parsePath(path) {
    const bailRE = /[^\w.$]/
    if (bailRE.test(path)) {
      return
    }
    const segments = path.split('.')
    return function (obj) {
      for (let i = 0; i < segments.length; i++) {
        if (!obj) {
          return // 如果.到当前没有值,则选择返回空
        }
        obj = obj[segments[i]] // 例如返回obj.a,下一次返回(obj.a).b
      }
      return obj
    }
  }

module.exports = {
    def,
    parsePath
}

@ arrayIntercept.js

const {def} = require('./common')
const ArrayProto = Array.prototype
/**
 * 对数组默认的增删方法进行拦截
 */
const ArrayMethods = Object.create(ArrayProto)

;['push','pop','shift','unshift','splice','sort','reverse'].forEach(method => {
    // 缓存原始方法
    const original = ArrayProto[method]
    def(ArrayMethods, method, function mutator (...args) {
            const ob = this.__ob__
            let inserted
            let msg 
            switch (method) {
                case 'push':
                    msg = 'you just execute push opration'
                    break;
                case 'unshift':
                    inserted = args
                    break;
                case 'splice':
                    inserted = args.slice(2) // 获得args第三个到第n个参数,即增加项
                    break;
            
                default:
                    break;
            }
            if (inserted) ob.observeArray(inserted) // 添加对新增元素的变化侦测
            ob.dep.notify(msg)
            /**
             * 改变this的指向,比如original是push函数,调用的时候肯定是[].push,此时push中的this是指向[]的
             * 如果这里不操作,就会导致this链断裂
             */
            return original.apply(this, args) 
    })
});

module.exports = ArrayMethods

@ Observer.js 这里面我定义了 Observer 和 Watcher

const { def, parsePath } = require('./common')
const arrayMethods = require('./arrayIntercept')
const Dep = require('../DepPro')

// 是否支持proto
const hasProto = '__proto__' in {}

const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
/**
 * 不能对全局的Array.prototype 进行直接覆盖,这样会污染全局的Array
 * 实现针对性数组拦截
 */

class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.deps = []
    this.depIds = new Set()
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
    }
    this.cb = cb
    this.value = this.get()
  }

  get() {
    global.target = this
    let value = this.getter.call(this.vm, this.vm)
    global.target = undefined
    return value
  }

  update(msg) {
    if (msg) {
    //   this.cb.call(this.vm, msg)
      this.cb(msg)
    } else {
      const oldValue = this.value
      this.value = this.get() // 这里又会执行一遍Object.definedProperty的get函数
      this.cb.call(this.vm, this.value, oldValue)
    }



  }
  addDep(dep) {
    const id = dep.id
    if (!this.depIds.has(id)) {
      this.depIds.add(id)
      this.deps.push(dep)
      dep.addSub(this)
    }
  }
  teardown() {
    let i = this.deps.length
    while (i--) {
      this.deps[i].removeSub(this)
    }
  }
}
/**
 * 数组增删需要在拦截其中才能触发依赖,setter中并不能监测到,需要同时满足在getter中和拦截器中都能访问到(一个push一个notify),所以放到Observe中
 */
class Observer {
  constructor(value) {
    this.value = value;
    this.dep = new Dep() // 一个Observer拥有一个dep
    // 将Ovserver实例赋给value.__ob__,第一,标记已响应,第二,通过__ob__拿到Observer实例,就可以在拦截器中调用__ob__上的dep了
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      this.observeArray(value) // 如果被侦测的数据是数组,则将数组每一个元素都转换成响应式并侦测变化
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
    } else { // 如果是对象,则直接调用walk
      this.walk(value)
    }
  }

  observeArray(items) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }
}

let a = {
  b: [1, 2, 3],
  c: 'abk'
}
new Observer(a)
new Watcher(a, 'b', function (msg) {
    console.log(this.c)
  console.log(msg)
})
a.b.push(4)
console.log(a)
console.log(a.__ob__.value)

/**
 * 进一步封装Observe, 返回Observe实例,如果已经存在则直接返回,不存在返回新建数据
 * 数组对象有了实例,代表已经是响应式数据,调用该方法,返回一个被拦截器修改的数组
 * 总结一下,通过const a = observe(value) 1、value从此拥有拦截器 2、a.value 等于 value ,a 等于 value.__ob__
 * 值得注意的是,value既可以是数组也可以是对象,是兼容的
 * @param {数组对象} value 
 * @param {暂无介绍} asRootData 
 */
function observe(value, asRootData) {
  if (typeof value !== 'object') {
    return null
  }
  let ob
  if (value.hasOwnProperty('__ob__') && value.__ob__ instanceof Observer) {
    ob = value__ob__
  } else {
    ob = new Observer(value)
  }
  return ob
}

function protoAugment(target, src, keys) {
  target.__proto__ = src
}

// 如果不支持,将拦截器中的值一个一个添加到数组对象
function copyAugment(target, src, keys) {
  for (let i = 0, l = keys.length; i < l; i++) { // 只需要机算一次keys.length的值
    const key = keys[i]
    def(target, key, src[key])
  }
}

function defineReactive(data, key, val) {
  let childOb = observe(val) // 对对象或者数组返回Observe实例,其他返回空
  let dep = new Dep()
  Object.defineProperty(data, key, {
    enumerable: true,
    configurable: true,
    get: function () {
      dep.depend() // 如果传值为数组,则此dep监测数组引用变化,下方监测数据增删改等变化
      if (childOb) {
        childOb.dep.depend()
      }
      return val
    },
    set: function (newVal) {
      if (val === newVal) {
        return
      }
      val = newVal
      dep.notify()
    }
  })
}

然后在控制台环境中运行 node Observer.js,便可以得到如果所示的运行结果

在这里插入图片描述

说到最后

最后想说的是自己的一些总结,变化侦测映射到我们平时使用vue的时候,就好比在data中声明了变量data,然后vue会对这个变量使用Observer(data),然后当DOM中有一个地方引用了这个变量,就相当于新建了一个watcher,并将该watcher放入data的dep中,当引用改变,会触发nofity方法中从而调用watcherupdate方法,更新dom,这就实现了变化侦测。

你可能感兴趣的:(Some,meaningful)