在掘金上买了一个关于解读 vue 源码的小册, 因为是付费的, 所以还比较放心
在小册里看到了关于 vue 双向绑定和依赖收集的部分, 总感觉有些怪怪的, 然后就自己跟着敲了一遍. 敲完后, 发现完全无法运行, 坑啊, 写书人完全没有测试过.
然后自己完善代码, 越写越发现坑, 问题有些大......
最后自己重新实现了一遍, 代码较多. 用到观察订阅者模式实现依赖收集, Object.defineProperty() 实现双向绑定
- /*
- 自己写的代码, 实现 vue 的双向绑定和依赖收集
- 场景: 多个子组件用到父组件 data 中的数据, 当父组件 data 中的此数据发生改变时,
- 所有依赖它的 子组件全部更新
- 通常子组件的从父组件中拿取的数据不允许发生改变
- */
- // 订阅者 Dep
- // 一个订阅者只管理一个数据
- class Dep {
- constructor () {
- this.subs = [] // 存放 vue 组件
- }
- addSubs (sub) {
- this.subs.push(sub)
- console.log('add watcher:', sub._name)
- }
- notify () {
- this.subs.forEach( sub => { // 通知 vue 组件更新
- sub.update()
- })
- }
- }
- // 监听者
- // 一个 vue 实例包含一个 Watcher 实例
- class Watcher {
- // 在实例化 Watcher 时, 将 Dep 的 target 指向此实例, 在依赖收集中使用
- // 因为依赖收集是在组件初始化时触发的, 而数据变更后视图相应变更是在初始化后
- // 所以让 Dep.target 指向此实例, 当此 vue 实例初始化完成后, 再指向下一个正在初始化的 vue 实例完成依赖收集
- constructor (name) {
- Dep.target = this
- this._name = name
- }
- update () {
- // 这里模拟视图更新
- // 其实还应该让子组件的 props 相应值与父组件更新的数据同步
- console.log("子组件视图更新了..." + this._name)
- }
- }
- // 对 data 中的数据设置读写监听, 并且创建订阅者, 用于收集子组件的依赖和发布
- function defineReactive (obj, key, value) {
- // 对 vue 实例中 data 对象的每一个属性都 设置一个订阅者 Dep
- let dep = new Dep()
- // 第二个 vue 实例的监听 覆盖了第一个 vue 实例的监听, 因为引用的 obj 是同一个
- Object.defineProperty(obj, key, {
- configurable: true,
- enumerable: true,
- get () {
- // 在读此属性时, 将当前 watcher 对象收集到此属性的 dep 对象中
- // 在实例化 vue 时将 Dep.target 指向当前 Watcher
- // get() 依赖收集的时候是 vue 组件初始化的时候, set() 是在初始化后
- if (dep.subs.indexOf(Dep.target) === -1) {
- dep.addSubs(Dep.target)
- }
- //return obj[key] 此写法报错 提示栈溢出 原因是无限调用 get()
- return value
- },
- set (newVal) { // 此属性改变时, 通知所有视图更新
- if (newVal !== value) {
- value = newVal
- dep.notify()
- }
- }
- })
- }
- // 接收一个对象作为参数, 将该对象的所有属性调用 defineReactive 设置读写监听
- function observer (obj) {
- if (!obj || (typeof obj !== 'object')) {
- return
- }
- Object.keys(obj).forEach( key => {
- defineReactive(obj, key, obj[key])
- })
- }
- // 构造函数, 监听 配置 options 中的 data() 方法返回的对象的所有属性 的读写
- class Vue {
- constructor (options) {
- this._name = options.name
- this._data = options.data
- // 每个 vue 组件都是一个 vue 实例, 在一个页面中有多个 vue 实例
- // 在初始化该 vue 实例时, new 一个 Watcher 对象, 使 Dep.target 指向此实例
- new Watcher(options.name)
- // 给 data 中的数据挂载读写监听
- observer(this._data)
- // 模拟 vue 解析 template 过程, 获取从父组件传递过来的 props
- // 在这里进行依赖收集
- this._props = options.props ? getProps() : {}
- // 实例化该组件的子组件
- this._children = options.render ? (options.render() || {}) : {}
- }
- }
- // 父组件数据
- let data = {
- first: "hello",
- second: 'world',
- third: ['啦啦啦']
- }
- let times = 0
- // 第一次调用返回的是第一个子组件的从父组件继承的数据 (vue 中 props 属性的值)
- // 第二次调用返回的是第二个子组件的从父组件继承的数据 (vue 中 props 属性的值)
- function getProps () {
- times++
- if (times == 1) {
- let obj = {first: "", second:""}
- Object.keys(obj).forEach( key => {
- // 如果是对象, 则进行深拷贝
- // 这里使用到了父组件的数据, 触发依赖收集
- if (data[key] instanceof Object) {
- obj[key] = JSON.parse(JSON.stringify(data[key]))
- } else {
- obj[key] = data[key]
- }
- })
- return obj
- } else if (times == 2) {
- let obj = {first: "", third:""}
- Object.keys(obj).forEach( key => {
- if (data[key] instanceof Object) {
- obj[key] = JSON.parse(JSON.stringify(data[key]))
- } else {
- obj[key] = data[key]
- }
- })
- return obj
- }
- }
- let vue_root = new Vue({
- name: 'vue_root',
- data,
- // 模拟编译 template 和实例化 vue 的过程
- // 在编译父组件 并且传递参数给子组件时, 将子组件的 watcher 添加进父组件的 dep
- render () {
- let vue_1 = new Vue({
- name: 'vue_1',
- data: {},
- props: true,
- render () {}
- })
- let vue_2 = new Vue({
- name: 'vue_2',
- data: {},
- props: true,
- render () {}
- })
- return {
- vue_1,
- vue_2
- }
- }
- })
- console.log(vue_root)
- vue_root._data.first = 'hello hello' // vue_1 和 Vue_2 都依赖此数据, 都更新
- vue_root._data.third = "aaa" // 只有 vue_2 依赖到了此数据, 更新
来源: https://www.cnblogs.com/zhaowj/p/10034506.html