资讯专栏INFORMATION COLUMN

Vue 进阶系列(一)之响应式原理及实现

MonoLog / 1117人阅读

摘要:进阶系列一之响应式原理及实现进阶系列二之插件原理及实现进阶系列三之函数原理及实现什么是响应式表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。

(关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)

Vue进阶系列汇总如下,欢迎阅读,欢迎加高级前端进阶群一起学习(文末)。

Vue 进阶系列(一)之响应式原理及实现

Vue 进阶系列(二)之插件原理及实现

Vue 进阶系列(三)之Render函数原理及实现

什么是响应式Reactivity

Reactivity表示一个状态改变之后,如何动态改变整个系统,在实际项目应用场景中即数据如何动态改变Dom。

需求

现在有一个需求,有a和b两个变量,要求b一直是a的10倍,怎么做?

简单尝试1:
let a = 3;
let b = a * 10;
console.log(b); // 30

乍一看好像满足要求,但此时b的值是固定的,不管怎么修改a,b并不会跟着一起改变。也就是说b并没有和a保持数据上的同步。只有在a变化之后重新定义b的值,b才会变化。

a = 4;
console.log(a); // 4
console.log(b); // 30
b = a * 10;
console.log(b); // 40
简单尝试2:

将a和b的关系定义在函数内,那么在改变a之后执行这个函数,b的值就会改变。伪代码如下。

onAChanged(() => {
    b = a * 10;
})

所以现在的问题就变成了如何实现onAChanged函数,当a改变之后自动执行onAChanged,请看后续。

结合view层

现在把a、b和view页面相结合,此时a对应于数据,b对应于页面。业务场景很简单,改变数据a之后就改变页面b。



document
    .querySelector(".cell.b")
    .textContent = state.a * 10

现在建立数据a和页面b的关系,用函数包裹之后建立以下关系。



onStateChanged(() => {
    document
        .querySelector(‘.cell.b’)
        .textContent = state.a * 10
})

再次抽象之后如下所示。


    {{ state.a * 10 }}


onStateChanged(() => {
    view = render(state)
})

view = render(state)是所有的页面渲染的高级抽象。这里暂不考虑view = render(state)的实现,因为需要涉及到DOM结构及其实现等一系列技术细节。这边需要的是onStateChanged的实现。

实现

实现方式是通过Object.defineProperty中的gettersetter方法。具体使用方法参考如下链接。

MDN之Object.defineProperty

需要注意的是getset函数是存取描述符,valuewritable函数是数据描述符。描述符必须是这两种形式之一,但二者不能共存,不然会出现异常。

实例1:实现convert()函数

要求如下:

1、传入对象obj作为参数

2、使用Object.defineProperty转换对象的所有属性

3、转换后的对象保留原始行为,但在get或者set操作中输出日志

示例:

const obj = { foo: 123 }
convert(obj)


obj.foo // 输出 getting key "foo": 123
obj.foo = 234 // 输出 setting key "foo" to 234
obj.foo // 输出 getting key "foo": 234

在了解Object.definePropertygettersetter的使用方法之后,通过修改getset函数就可以实现onAChangedonStateChanged

实现:

function convert (obj) {

  // 迭代对象的所有属性
  // 并使用Object.defineProperty()转换成getter/setters
  Object.keys(obj).forEach(key => {
  
    // 保存原始值
    let internalValue = obj[key]
    
    Object.defineProperty(obj, key, {
      get () {
        console.log(`getting key "${key}": ${internalValue}`)
        return internalValue
      },
      set (newValue) {
        console.log(`setting key "${key}" to: ${newValue}`)
        internalValue = newValue
      }
    })
  })
}
实例2:实现Dep

要求如下:

1、创建一个Dep类,包含两个方法:dependnotify

2、创建一个autorun函数,传入一个update函数作为参数

3、在update函数中调用dep.depend(),显式依赖于Dep实例

4、调用dep.notify()触发update函数重新运行

示例:

const dep = new Dep()

autorun(() => {
  dep.depend()
  console.log("updated")
})
// 注册订阅者,输出 updated

dep.notify()
// 通知改变,输出 updated

首先需要定义autorun函数,接收update函数作为参数。因为调用autorun时要在Dep中注册订阅者,同时调用dep.notify()时要重新执行update函数,所以Dep中必须持有update引用,这里使用变量activeUpdate表示包裹update的函数。

实现代码如下。

let activeUpdate = null 

function autorun (update) {
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate    // 引用赋值给activeUpdate
    update()                        // 调用update,即调用内部的dep.depend
    activeUpdate = null             // 绑定成功之后清除引用
  }
  wrappedUpdate()                   // 调用
}

wrappedUpdate本质是一个闭包,update函数内部可以获取到activeUpdate变量,同理dep.depend()内部也可以获取到activeUpdate变量,所以Dep的实现就很简单了。

实现代码如下。

class Dep {

  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }

  // 订阅update函数列表
  depend () {
    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }

  // 所有update函数重新运行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}

结合上面两部分就是完整实现。

实例3:实现响应式系统

要求如下:

1、结合上述两个实例,convert()重命名为观察者observe()

2、observe()转换对象的属性使之响应式,对于每个转换后的属性,它会被分配一个Dep实例,该实例跟踪订阅update函数列表,并在调用setter时触发它们重新运行

3、autorun()接收update函数作为参数,并在update函数订阅的属性发生变化时重新运行。

示例:

const state = {
  count: 0
}

observe(state)

autorun(() => {
  console.log(state.count)
})
// 输出 count is: 0

state.count++
// 输出 count is: 1

结合实例1和实例2之后就可以实现上述要求,observe中修改obj属性的同时分配Dep的实例,并在get中注册订阅者,在set中通知改变。autorun函数保存不变。
实现如下:

class Dep {

  // 初始化
  constructor () {          
    this.subscribers = new Set()
  }

  // 订阅update函数列表
  depend () {
    if (activeUpdate) {     
      this.subscribers.add(activeUpdate)
    }
  }

  // 所有update函数重新运行
  notify () {              
    this.subscribers.forEach(sub => sub())
  }
}

function observe (obj) {

  // 迭代对象的所有属性
  // 并使用Object.defineProperty()转换成getter/setters
  Object.keys(obj).forEach(key => {
    let internalValue = obj[key]

    // 每个属性分配一个Dep实例
    const dep = new Dep()

    Object.defineProperty(obj, key, {
    
      // getter负责注册订阅者
      get () {
        dep.depend()
        return internalValue
      },

      // setter负责通知改变
      set (newVal) {
        const changed = internalValue !== newVal
        internalValue = newVal
        
        // 触发后重新计算
        if (changed) {
          dep.notify()
        }
      }
    })
  })
  return obj
}

let activeUpdate = null

function autorun (update) {

  // 包裹update函数到"wrappedUpdate"函数中,
  // "wrappedUpdate"函数执行时注册和注销自身
  const wrappedUpdate = () => {
    activeUpdate = wrappedUpdate
    update()
    activeUpdate = null
  }
  wrappedUpdate()
}

结合Vue文档里的流程图就更加清晰了。

Job Done!!!

本文内容参考自VUE作者尤大的付费视频
交流

本人Github链接如下,欢迎各位Star

http://github.com/yygmind/blog

我是木易杨,网易高级前端工程师,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!

如果你想加群讨论每期面试知识点,公众号回复[加群]即可

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98945.html

相关文章

  • Vue 进阶系列(三)Render函数原理实现

    摘要:进阶系列一之响应式原理及实现进阶系列二之插件原理及实现进阶系列三之函数原理及实现函数原理根据第一篇文章介绍的响应式原理,如下图所示。在初始化阶段,本质上发生在函数中,然后通过函数生成,根据生成。负责收集依赖,清除依赖和通知依赖。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)showImg(https://segmentfa...

    geekidentity 评论0 收藏0
  • Vue 进阶系列(二)插件原理实现

    摘要:示例输出第一步先不考虑插件,在已有的中是没有这个公共方法的,如果要简单实现的话可以通过钩子函数来,即在里面验证逻辑。按照插件的开发流程,应该有一个公开方法,在里面使用全局的方法添加一些组件选项,方法包含一个钩子函数,在钩子函数中验证。 (关注福利,关注本公众号回复[资料]领取优质前端视频,包括Vue、React、Node源码和实战、面试指导)showImg(https://segmen...

    wuaiqiu 评论0 收藏0
  • 关于Vue2些值得推荐的文章 -- 五、六月份

    摘要:五六月份推荐集合查看最新的请点击集前端最近很火的框架资源定时更新,欢迎一下。苏幕遮燎沈香宋周邦彦燎沈香,消溽暑。鸟雀呼晴,侵晓窥檐语。叶上初阳乾宿雨,水面清圆,一一风荷举。家住吴门,久作长安旅。五月渔郎相忆否。小楫轻舟,梦入芙蓉浦。 五、六月份推荐集合 查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。 苏...

    sutaking 评论0 收藏0
  • 关于Vue2些值得推荐的文章 -- 五、六月份

    摘要:五六月份推荐集合查看最新的请点击集前端最近很火的框架资源定时更新,欢迎一下。苏幕遮燎沈香宋周邦彦燎沈香,消溽暑。鸟雀呼晴,侵晓窥檐语。叶上初阳乾宿雨,水面清圆,一一风荷举。家住吴门,久作长安旅。五月渔郎相忆否。小楫轻舟,梦入芙蓉浦。 五、六月份推荐集合 查看github最新的Vue weekly;请::点击::集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。 苏...

    khs1994 评论0 收藏0

发表评论

0条评论

MonoLog

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<