• 《Vue 进阶系列之响应式原理及实现》


    https://www.bilibili.com/video/av51444410/?p=5

    https://github.com/amandakelake/blog/issues/63

    https://mp.weixin.qq.com/s/X3s4ysLfwclEOXIuKzOK2g

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

    前端大全 3/17
     

    以下文章来源于高级前端进阶 ,作者木

    高级前端进阶

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

    (给前端大全加星标,提升前端技能

    转自: 高级前端进阶

    什么是响应式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); // 4console.log(b); // 30b = 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。

    <span class="cell b"></span>document
       .querySelector('.cell.b')
       .textContent = state.a * 10

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

    <span class="cell b"></span>onStateChanged(() => {    document
           .querySelector(‘.cell.b’)
           .textContent = state.a * 10})

    再次抽象之后如下所示。

    <span class="cell b">
       {{ state.a * 10 }}
    </span>

    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": 123obj.foo = 234 // 输出 setting key "foo" to 234obj.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')
    })// 注册订阅者,输出 updateddep.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: 0state.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 = nullfunction autorun (update) {  // 包裹update函数到"wrappedUpdate"函数中,
     // "wrappedUpdate"函数执行时注册和注销自身
     const wrappedUpdate = () => {
       activeUpdate = wrappedUpdate
       update()
       activeUpdate = null
     }
     wrappedUpdate()
    }

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

    Job Done!!!

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

  • 相关阅读:
    Demo:刮刮卡橡皮擦效果
    养成良好的代码编写习惯
    我的百科
    专业英语词汇
    加载资源的类
    循环滚动翻页+居中项缩放
    学习笔记—Node中模块化规范
    学习笔记—Node中的EventLoop
    学习笔记—Node的全局对象
    学习笔记—Node的基本概念
  • 原文地址:https://www.cnblogs.com/cx2016/p/12039592.html
Copyright © 2020-2023  润新知