理解Vue响应式原理

我想用很简单的话说明这个原理,第一遍理解去理解,可以不看代码。

假设我有一个模板:

 <div id="app">
     <span>{{msg}}</span>
     <div v-bind="msg"></div>
     <input v-model="msg" />
   </div>
复制代码

假设我创建了一个vue实例:

 const vm = new Vue({
     el: '#app',
     data: {
       msg: 'Hello Vue'
     }
   }) 
复制代码

假装我有一张图:

未命名文件.jpg

数据劫持

第一,Vue拿到data,使用Object.definePropertydata中的msg添加getset方法,分别会在获取msg和给msg赋值的时候触发。— 处理这个过程叫“observe”(数据劫持)

这样的:

 class Observer {
   constructor (data) {
     this.walk(data)
   }
   walk (data) {
     // 判断data是否是对象
     if (!data || typeof data!== 'object') return
     // 遍历对象的所有属性
     Object.keys(data).forEach(key => {
       this.defineReactive(data, key, data[key])
     })
   }
   defineReactive (obj, key, val) {
     // 如果val是对象,会把内部也转换成响应式数据
     this.walk(val)
     let _this = this
     // 负责收集依赖,并发送通知
     let dep = new Dep()
     Object.defineProperty(obj, key, {
       enumerable: true,
       configurable: true,
       get () {
         // 收集依赖
         Dep.target && dep.addSub(Dep.target)
         return val
       },
       set (newValue) {
         if (newValue === val) return
         val = newValue
         _this.walk(newValue)
         // 发送通知

     dep.notify()
   }
 })




}
}
复制代码

} }

模板编译

第二,Vue会对通过el获取到模板,对这段模板进行解析,<span>{{msg}}</span>比如这个,用正则匹配出来msg,然后就去取data[msg],放在span里面。— 这个解析叫做“compiler”模板解析。更新完值之后compiler告诉观察者watcher我用到了data[msg]这个值,如果它set了你就把更新的值给我,我要改变内容(回调)

这样的:

 /***
  * 功能:
  * 负责编译末班,解析指令/差值表达式
  * 负责页面的首次渲染
  * 当数据变化后重新渲染视图
  * ****/


class Compiler {
constructor (vm) {
this.el = vm.$el
this.vm = vm
this.compile(this.el)
}
// 编译末班,处理文本节点和元素节点
compile (el) {
let childNodes = el.childNodes



     Array.from(childNodes).forEach(node =&gt; {
         // 处理文本节点
         if (this.isTextNode(node)) {
             this.compileText(node)
         } else if (this.isElementNode(node)) {
             // 处理元素节点
             this.compileElement(node)
         }
         // 判断node节点是否有子节点
         if (node.childNodes &amp;&amp; node.childNodes.length) {
             this.compile(node)
         }
     })
 }
 // 编译元素节点,处理指令
 compileElement (node) {
     // 遍历所有的属性节点
     Array.from(node.attributes).forEach(attr =&gt; {
         let attrName = attr.name
         if (this.isDirective(attrName)) {
             attrName = attrName.substr(2)
             let key = attr.value
             let eventType = &#39;&#39;
             if (attrName.startsWith(&#39;on:&#39;)) {
                 eventType = attrName.slice(3)
                 attrName = attrName.substr(0, 2)
             }
             this.update(node, key, attrName, eventType)
         }
     })
     // 判断是否是指令

 }
 update (node, key, attrName, eventType) {
     let updateFn = this[attrName + &#39;Updater&#39;]
     updateFn &amp;&amp; updateFn.call(this, node, this.vm[key], key, eventType)
 }
 // 处理v-text 指令
 textUpdater (node, value, key) {
     node.textContent = value
     new Watcher(this.vm, key, (newValue) =&gt; {
         node.textContent = newValue
     })
 }
 // 处理 v-on 指令
 onUpdater (node, value, key, eventType) {
     node.addEventListener(eventType, value.bind(this.vm))
     new Watcher(this.vm, key, newValue =&gt; {
         node.removeEventListener(eventType, value.bind(this.vm))
         node.addEventListener(eventType, newValue.bind(this.vm))
     })
 }
 // 处理v-html 指令
 htmlUpdater (node, value, key) {
     node.innerHTML = value
     new Watcher(this.vm, key, (newValue) =&gt; {
         node.innerHTML = newValue
     })
 }
 modelUpdater (node, value, key) {
     node.value = value
     new Watcher(this.vm, key, (newValue) =&gt; {
         node.value = newValue
     })
     // 双向绑定
     node.addEventListener(&#39;input&#39;, () =&gt; {
         this.vm[key] = node.value
     })
 }
 // 编译文本节点,处理差值表达式
 compileText (node){
     let reg = /\{\{(.+?)\}\}/
     let value = node.textContent
     if (reg.test(value)) {
         let key = RegExp.$1.trim()
         node.textContent = value.replace(reg, this.vm[key])


         // 创建watcher对象,当数据改变,更新视图
         new Watcher(this.vm, key, (newValue) =&gt; {
             node.textContent = newValue
         })
     }

 }
 // 判断元素属性是否是指令
 isDirective (attrName) {
     return attrName.startsWith(&#39;v-&#39;)
 }
 // 判断元素属性是否是文本节点
 isTextNode (node) {
     return node.nodeType === 3
 }
 // 判断节点时候是元素节点
 isElementNode (node) {
     return node.nodeType === 1
 }




}
复制代码

}

依赖收集

第三,观察者watcher,收到compiler的操作之后,就可以去dataset方法里面等着。但是可能会有很多watcher,每个使用data[msg]的地方都会创建一个watcher。那还不如叫Dep发布者,把watcher都收集起来,watcher有一个update方法,一旦观察的值一旦变化就要执行的,是span的回调。watcher保存了span给他的回调,获取当前的msg的值,哈哈,重点来了,获取值得时候会触发get方法,等于是说所有的watcher都会经过这里。让Dep在这里等着,一旦watcher来了就给装起来。

这样的:

 class Watcher {
   constructor (vm, key, cb) {
     this.vm = vm
     this.key = key
     // 回调函数,负责更新视图
     this.cb = cb

 // 把watcher类对象记录到Dep类的静态属性target
 Dep.target = this

 // 触发get方法,在get方法中会调用addSub
 this.oldValue = vm[key]

 Dep.target = null




}




// 当数据发生变化的时候更新视图
update () {
let newValue = this.vm[this.key]
if (this.oldValue === newValue) return



 this.cb(newValue)




}
}
复制代码

} }

发布

第四,Depget这里把所有watcher都装到自己的sub(一个数组)里面了。一旦msgset调用了,就看看msg变了没,变了的话就把每个sub里面的watcher拿出来,一个一个执行里面的updateupdate执行了,spaninnerHTML,就更新成msg的新值了

 class Dep {
   constructor () {
     // 存储所有的观察者
     this.subs = []
   }
   // 添加观察者
   addSub (sub) {
     if (sub && sub.update) {
       this.subs.push(sub)
     }
   }
   //  发送通知
   notify () {
     this.subs.forEach(sub => {
       sub.update()
     })
   }
 }
复制代码

从入口文件来调用:

 class Vue {
     constructor(options) {
         // 通过属性保存选项中的数据
         this.$options = options  || {}
         this.$data = options.data || {}
         this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

     // 把data中的成员转换成getter和setter,注入的vue实例中
     this._proxyData(this.$data)
     // 调用observer对象,监听数据的变化
     new Observer(this.$data)
     // 调用compiler对象,解析指令和差值表达式
     new Compiler(this)
     
 }

 _proxyData (data) {
     // 遍历data中的所有属性
     Object.keys(data).forEach(key =&gt; {
         Object.defineProperty(this, key, {
             enumerable: true,
             configurable: true,
             get() {
                 return data[key]
             },
             set (newValue) {
                 if (newValue === data[key]) return
                 data[key] = newValue
             }
         })
     })
     // 把data的属性注入到vue实例中
 }




}
复制代码

}

总体的过程就是这样的,如果把这个简易模型搞明白了,再去看源码思路就会很清晰。

 

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享