Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
yygmind opened this issue Oct 22, 2018 · 5 comments
Open

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

yygmind opened this issue Oct 22, 2018 · 5 comments

Comments

@yygmind
Copy link
Owner

yygmind commented Oct 22, 2018

Vue进阶系列汇总如下,欢迎阅读,欢迎加群讨论(文末)。

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

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

什么是响应式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。

<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": 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()
}

Job Done!!!

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

交流

欢迎加我微信进一步交流或者找我内推,前行的路上,共勉!
image

@huiliangShen
Copy link

感谢分享

@yygmind yygmind changed the title Vue 进阶系列之响应式原理及实现 Vue 进阶系列(一)之响应式原理及实现 Nov 1, 2018
@yygmind
Copy link
Owner Author

yygmind commented Dec 2, 2018

感谢分享

谢谢支持

@mingxuan0624
Copy link

下次出期关于vue生命周期函数的.谢谢

@saviour2008
Copy link

谢谢分享,有点没太看懂最后一大段代码autorun方法在哪里执行?

@CodeDreamfy
Copy link

谢谢分享,有点没太看懂最后一大段代码autorun方法在哪里执行?

作者只是没有写出来使用的代码,使用autorun注册多个需要更改dom的回调,类似于我这样:

// test
var state = {
    name: 'abc',
    age: 20,
    changeName() {
      $('.v-modal h3').text(this.name);
    },
    changeAge() {
      $('.v-modal p').text(this.age);
    }
}
observe(state);
autorun(function() {
    state.changeName();	
})
autorun(function() {
    state.changeAge();
})

setTimeout(()=> {
    state.name = 'javascript';
    state.age= 30;
}, 1200)
<div class="v-modal">
   <h3>heiheihei</h3>
   <p>123</p>
</div>

autorun的update参数执行的时候会调用getting方法,会注册一个set,当数据更改的时候会setting中的notify将所有注册的回调执行一次。

另外observe的setting中,将set赋值如果放在notify判断后的话会导致赋值失败的情况。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants