以前项目中一直在用vue,也边作边学摸滚打爬了近一年。对一些基础原理性的东西有过了解,可是不深刻,例如面试常常问的vue的响应式原理,可能大多数人都能答出来Object.defineProperty进行数据劫持,可是深刻其实现细节,仍是有不少以前没考虑到的东西,例如依赖收集后如何通知订阅器,以及订阅发布模式如何实现等等。过程当中读了部分源码,受益不浅,除此以外,动手去实现它也是个很棒的学习方式,话很少说,看代码,仓库地址。javascript
vue的更新机制咱们简单归纳一下就是,先对template进行解析,若检测到template中使用了data中定义的属性,则生成一个对应的watcher,经过劫持getter进行依赖(即watcher)收集,收集的内容保存在订阅器Dep,经过劫持setter作到改变属性从而通知订阅器更新,那么咱们首先要作的就是对属性进行劫持。
vue2.0中使用的是Object.defineProperty,有传言说vue 3.0将会使用Proxy来代替Object.defineProperty,其有诸多好处:vue
Proxy目前来看惟一的缺点就是兼容性可能存在问题,不过无伤大雅,咱们也顺应潮流,使用Proxy来实现数据劫持,代码很简单:java
/** * 接受一个对象,对属性进行依赖追踪 */ function observable(obj) { const dep = new Dep() const proxy = new Proxy(obj, { get(target, property) { const value = target[property] if (value && typeof value === 'object') { // 若属性为object,递归处理 target[property] = observable(value) } if (Dep.target) { // Dep.target指向当前watcher dep.addWatcher(Dep.target) } return target[property] }, set(target, property, value) { target[property] = value dep.notify() // 通知订阅器 } }) return proxy }
注意该方法须要返回proxy实例,由于只有经过proxy实例访问属性才具备劫持效果。咱们能够看到代码中有一个Dep,这个东西便是订阅器,能够理解为它维护了一个依赖(watcher)的数组,并实现了一些管理数据的方法诸如addWatcher添加依赖,以及须要提供一个notify方法来遍历全部的watcher执行其相应的更新函数,一样代码很简单:git
/** * 依赖收集器,存放全部的watcher,并提供发布功能(notify) */ class Dep { constructor() { this.watchers = [] } addWatcher(watcher) { // 添加watcher this.watchers.push(watcher) } notify() { // 通知方法,调用即依次遍历全部watcher执行更新 this.watchers.forEach((watcher) => { watcher.update() }) } }
最后咱们来看下watcher,咱们知道watcher即咱们所说的依赖,它是在编译template的时候,若找到data中声明的属性,即会生成一个对应的watcher实例,触发依赖收集,加入订阅器。同时还须要提供一个update函数,在触发notify的时候调用来更新视图,代码以下:github
/** * watcher即所谓的依赖,监听具体的某个属性 */ class Watcher { constructor(proxy, property, cb) { this.proxy = proxy this.property = property this.cb = cb this.value = this.get() } update() { // 执行更新 const newValue = this.proxy[this.property] if (newValue !== this.value && this.cb) { // 对比property新旧值,决定是否更新 this.cb(newValue) } } get() { // 只在初始化时调用,用于依赖收集 Dep.target = this // 将自身指向Dep.target,执行完依赖收集再去释放 const value = this.proxy[this.property] Dep.target = null return value } }
至此,响应式原理大体已经成形,接着咱们只要写一个简易的模板解析,demo就能跑起来啦。我这边的实现比较挫,仅仅是经过正则匹配来实现了一个不带diff的virture dom,纯属娱乐,重点仍是在实现响应式原理上,这边贴一下代码:面试
let init = false // 只在初始化时去生成watcher const eventMap = new Map() // 存放事件 const root = document.getElementById('root') // 根节点 /** * 用于将传入RayActive的vm对象进行代理,可经过this.xx访问this.data.xx * @param {Object} vm * @param {Proxy} proxydata 通过proxy代理的vm.data对象,使this.xx操做也能触发视图更新 */ function vmProxy(vm, proxydata) { return new Proxy(vm, { get(target, property) { return target.data[property] || target.methods[property] }, set(target, property, value) { proxydata[property] = value } }) } /** * 编译vm,分别对data和render作相应处理 * @param {Object} vm 须要被编译的vm对象 */ function compile(vm) { const proxydata = compileData(vm.data) compileRender(proxydata, vm.render) bindEvents(vm, vmProxy(vm, proxydata)) } /** * * @param {Object} data 须要被编译的vm中的data对象 */ function compileData(data) { return observable(data) } /** * * @param {*} render 须要被编译的render字符串 * @param {*} proxydata 经proxy转换过的data */ function compileRender(proxydata, render) { if (render) { const variableRegexp = /\{\{(.*?)\}\}/g const variableResult = render.replace(variableRegexp, (a, b) => { // 替换变量为相应的data值 if (!init) { // 只在初始化时去生成watcher new Watcher(proxydata, b, function() { compileRender(proxydata, render) }) } return proxydata[b] }) const eventRegexp = /(?<=<.*)@(.*)="(.*?)"(?=.*>)/ const result = variableResult.replace(eventRegexp, (a, b, c) => { // 为绑定事件的标签添加惟一id标识 const id = Math.random().toString(36).slice(2) eventMap.set(id, { type: b, method: c }) return a + ` id=${id}` }) init = true root.innerHTML = result } } /** * 经过root节点作事件代理,绑定模板中声明的事件 * @param {*} vm * @param {*} proxyvm 通过proxy代理的vm */ function bindEvents(vm, proxyvm) { for (let [key, value] of eventMap) { root.addEventListener(value.type, (e) => { const method = vm.methods[value.method] if (method && e.target.id === key) { method.apply(proxyvm) // 将vm中methods方法的this指向通过proxy的vm对象 } }) } } /** * 可理解为Vue中的Vue类,使用方式为new RayActive(vm) */ class RayActive { constructor(vm) { compile(vm) } }
这个简易实现仅仅是帮助你们学习vue的一些原理性的东西,跟vue比其余来只是冰山一角。这个代码还有很大的优化空间,好比执行notify时这里会通知全部的watcher等等,值得有空去研究一下。同时,咱们能看到订阅发布模式带来的好处。若是不引入订阅器,那咱们更新dom的代码得放到setter中去,那么就耦合了数据劫持与操做dom的逻辑。引入订阅器,能让咱们在proxy中仅仅作依赖收集和通知的操做,剩下的各类复杂的或是个性化的逻辑能够放到watcher中去实现,完美作到了关注点分离。数组