【稿件】前言

创新互联建站不只是一家网站建设的网络公司;我们对营销、技术、服务都有自己独特见解,公司采取“创意+综合+营销”一体化的方式为您提供更专业的服务!我们经历的每一步也许不一定是最完美的,但每一步都有值得深思的意义。我们珍视每一份信任,关注我们的网站制作、成都网站制作质量和服务品质,在得到用户满意的同时,也能得到同行业的专业认可,能够为行业创新发展助力。未来将继续专注于技术创新,服务升级,满足企业一站式营销型网站需求,让再小的品牌网站制作也能产生价值!
Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。----官方文档 本文将针对响应式原理做一个详细介绍,并且带你实现一个基础版的响应式系统。本文的代码请猛戳Github博客
什么是响应式
我们先来看个例子:
 Price :¥{{ price }} Total:¥{{ price * quantity }} Taxes: ¥{{ totalPriceWithTax }}
- var app = new Vue({
 - el: '#app',
 - data() {
 - return {
 - price: 5.0,
 - quantity: 2
 - };
 - },
 - computed: {
 - totalPriceWithTax() {
 - return this.price * this.quantity * 1.03;
 - }
 - },
 - methods: {
 - changePrice() {
 - this.price = 10;
 - }
 - }
 - })
 
上例中当price 发生变化的时候,Vue就知道自己需要做三件事情:
发生变化后,会重新对页面渲染,这就是Vue响应式,那么这一切是怎么做到的呢?
想完成这个过程,我们需要:
对应专业俗语分别是:
如何侦测数据的变化
首先有个问题,在Javascript中,如何侦测一个对象的变化? 其实有两种办法可以侦测到变化:使用Object.defineProperty和ES6的Proxy,这就是进行数据劫持或数据代理。这部分代码主要参考珠峰架构课。
方法1.Object.defineProperty实现
Vue通过设定对象属性的 setter/getter 方法来监听数据的变化,通过getter进行依赖收集,而每个setter方法就是一个观察者,在数据变更的时候通知订阅者更新视图。
- function render () {
 - console.log('模拟视图渲染')
 - }
 - let data = {
 - name: '浪里行舟',
 - location: { x: 100, y: 100 }
 - }
 - observe(data)
 - function observe (obj) {
 - // 判断类型
 - if (!obj || typeof obj !== 'object') {
 - return
 - }
 - Object.keys(obj).forEach(key => {
 - defineReactive(obj, key, obj[key])
 - })
 - function defineReactive (obj, key, value) {
 - // 递归子属性
 - observe(value)
 - Object.defineProperty(obj, key, {
 - enumerable: true, //可枚举(可以遍历)
 - configurable: true, //可配置(比如可以删除)
 - get: function reactiveGetter () {
 - console.log('get', value) // 监听
 - return value
 - },
 - set: function reactiveSetter (newVal) {
 - observe(newVal) //如果赋值是一个对象,也要递归子属性
 - if (newVal !== value) {
 - console.log('set', newVal) // 监听
 - render()
 - value = newVal
 - }
 - }
 - })
 - }
 - }
 - data.location = {
 - x: 1000,
 - y: 1000
 - } //set {x: 1000,y: 1000} 模拟视图渲染
 - data.name // get 浪里行舟
 
几个注意点补充说明:
这是因为 Vue 通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个数据是否被修改,无法追踪新增属性和删除属性。如果是删除属性,我们可以用vm.$delete实现,那如果是新增属性,该怎么办呢? 1)可以使用 Vue.set(location, a, 1) 方法向嵌套对象添加响应式属性; 2)也可以给这个对象重新赋值,比如data.location = {...data.location,a:1}
- function render() {
 - console.log('模拟视图渲染')
 - }
 - let obj = [1, 2, 3]
 - let methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push']
 - // 先获取到原来的原型上的方法
 - let arrayProto = Array.prototype
 - // 创建一个自己的原型 并且重写methods这些方法
 - let proto = Object.create(arrayProto)
 - methods.forEach(method => {
 - proto[method] = function() {
 - // AOP
 - arrayProto[method].call(this, ...arguments)
 - render()
 - }
 - })
 - function observer(obj) {
 - // 把所有的属性定义成set/get的方式
 - if (Array.isArray(obj)) {
 - obj.__proto__ = proto
 - return
 - }
 - if (typeof obj == 'object') {
 - for (let key in obj) {
 - defineReactive(obj, key, obj[key])
 - }
 - }
 - }
 - function defineReactive(data, key, value) {
 - observer(value)
 - Object.defineProperty(data, key, {
 - get() {
 - return value
 - },
 - set(newValue) {
 - observer(newValue)
 - if (newValue !== value) {
 - render()
 - value = newValue
 - }
 - }
 - })
 - }
 - observer(obj)
 - function $set(data, key, value) {
 - defineReactive(data, key, value)
 - }
 - obj.push(123, 55)
 - console.log(obj) //[1, 2, 3, 123, 55]
 
这种方法将数组的常用方法进行重写,进而覆盖掉原生的数组方法,重写之后的数组方法需要能够被拦截。但有些数组操作Vue时拦截不到的,当然也就没办法响应,比如:
- obj.length-- // 不支持数组的长度变化
 - obj[0]=1 // 修改数组中***个元素,也无法侦测数组的变化
 
ES6提供了元编程的能力,所以有能力拦截,Vue3.0可能会用ES6中Proxy 作为实现数据代理的主要方式。
方法2.Proxy实现
Proxy 是 JavaScript 2015 的一个新特性。Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。此外**Proxy支持代理数组的变化。**
- function render() {
 - console.log('模拟视图的更新')
 - }
 - let obj = {
 - name: '前端工匠',
 - age: { age: 100 },
 - arr: [1, 2, 3]
 - }
 - let handler = {
 - get(target, key) {
 - // 如果取的值是对象就在对这个对象进行数据劫持
 - if (typeof target[key] == 'object' && target[key] !== null) {
 - return new Proxy(target[key], handler)
 - }
 - return Reflect.get(target, key)
 - },
 - set(target, key, value) {
 - if (key === 'length') return true
 - render()
 - return Reflect.set(target, key, value)
 - }
 - }
 - let proxy = new Proxy(obj, handler)
 - proxy.age.name = '浪里行舟' // 支持新增属性
 - console.log(proxy.age.name) // 模拟视图的更新 浪里行舟
 - proxy.arr[0] = '浪里行舟' //支持数组的内容发生变化
 - console.log(proxy.arr) // 模拟视图的更新 ['浪里行舟', 2, 3 ]
 - proxy.arr.length-- // 无效
 
以上代码不仅精简,而且还是实现一套代码对对象和数组的侦测都适用。不过Proxy兼容性不太好!
我们之所以要观察数据,其目的在于当数据的属性发生变化时,可以通知那些曾经使用了该数据的地方。比如***例子中,模板中使用了price 数据,当它发生变化时,要向使用了它的地方发送通知。那如何收集依赖呢?
收集依赖与发布订阅模式
如何收集依赖,总结起来就一句话,在getter中收集依赖,在setter中触发依赖 我们先来实现一个 Dep 类,用于解耦属性的依赖收集和派发更新操作。
- // 通过 Dep 解耦属性的依赖和更新操作
 - class Dep {
 - constructor() {
 - this.subs = []
 - }
 - // 添加依赖
 - addSub(sub) {
 - this.subs.push(sub)
 - }
 - // 更新
 - notify() {
 - this.subs.forEach(sub => {
 - sub.update()
 - })
 - }
 - }
 - // 全局属性,通过该属性配置 Watcher
 - Dep.target = null
 
当需要依赖收集的时候调用 addSub,当需要派发更新的时候调用 notify。具体如何调用呢?
- let dp = new Dep()
 - dp.addSub(() => {
 - console.log('emit here')
 - })
 - dp.notify()
 
这就是一个简单实现的“事件发布订阅模式”,当然代码只是启发思路,真实应用还比较“粗糙”,没有进行事件名设置,APIs 也并不丰富,但完全能够说明问题了。
接下来我们先来简单的了解下 Vue 组件挂载时添加响应式的过程。在组件挂载时,会先对所有需要的属性调用 Object.defineProperty(),然后实例化 Watcher,传入组件更新的回调。在实例化过程中,会对模板中的属性进行求值,触发依赖收集。我们可以把Watcher理解成一个中介的角色,数据发生变化时通知它,然后它再通知其他地方。
***需要对 defineReactive 函数进行改造,在自定义函数中添加依赖收集和派发更新相关的代码。
- function render () {
 - console.log('模拟视图渲染')
 - }
 - let data = {
 - name: '浪里行舟',
 - location: { x: 100, y: 100 }
 - }
 - observe(data)
 - let dp = new Dep()
 - function observe (obj) {
 - // 判断类型
 - if (!obj || typeof obj !== 'object') {
 - return
 - }
 - Object.keys(obj).forEach(key => {
 - defineReactive(obj, key, obj[key])
 - })
 - function defineReactive (obj, key, value) {
 - // 递归子属性
 - observe(value)
 - Object.defineProperty(obj, key, {
 - enumerable: true, //可枚举(可以遍历)
 - configurable: true, //可配置(比如可以删除)
 - get: function reactiveGetter () {
 - console.log('get', value) // 监听
 - // 将 Watcher 添加到订阅
 - if (Dep.target) {
 - dp.addSub(Dep.target)
 - }
 - return value
 - },
 - set: function reactiveSetter (newVal) {
 - observe(newVal) //如果赋值是一个对象,也要递归子属性
 - if (newVal !== value) {
 - console.log('set', newVal) // 监听
 - render()
 - value = newVal
 - // 执行 watcher 的 update 方法
 - dp.notify()
 - }
 - }
 - })
 - }
 - }
 
以上所有代码实现了一个简易的数据响应式,核心思路就是手动触发一次属性的 getter 来实现依赖收集。
总结
我们再来回顾下整个过程:
参考文章和书籍
作者介绍
浪里行舟:硕士研究生,专注于前端。个人公众号:「前端工匠」,致力于打造适合初中级工程师能够快速吸收的一系列优质文章!
【原创稿件,合作站点转载请注明原文作者和出处为.com】