实现乞丐版的 vue data + method + computed

16 篇文章 1 订阅

最近有了点空,就想着 把 vue 给搞定了,

看了一遍之后,决定自己写一个乞丐版的 vue

index.html

  <div id="app">
    {{age}}
    <p>{{name}}</p>
    <p>{{name}}</p>
    <div>
      <p>
        <span @click="printName">{{name}}</span>
      </p>
      <input type="text" v-model="inp">
      {{inp}}
    </div>
  </div>
  <script src="./dcue/PoorVue.js"></script>
  <script src="./dcue/analyze.js"></script>
  <script src="./index.js"></script>

index.js

const props = {
  el: document.querySelector('#app'),
  data: {
    name: 'jack',
    age: 123,
    inp: ''
  },
  methods: {
    printName() {
      this.name = this.name + 1
    }
  }
}

new PoorVue(props)

PoorVue.js

由于当时是纯手打,没有看其他的东西,所以函数的命名上、部分代码可能会有出入

class PoorVue {
  constructor(props) {
    // 对当前的 数据进行保存
    this.props = props;
    this.$data = props.data;
    // 在这里对数据进行一个 双向绑定的前期工作,也就是代理工作
    // 大名鼎鼎的  defineProperty 就是在 这里进行的
    this.defineData(this.$data);
    // 对 html 进行解析,这里只会 提取 {{}} 和 @ 事件
    new Analyze(this.props.el, this)
  }
  defineData(data) {
    // 判断当前的 数据是不是一个对象
    if (Object.prototype.toString.call(data) === "[object Object]") {
      Object.keys(data).map(key => {
        // 这里是对 data 中的数据进行一个代理
        // 这样的话,就可以直接使用 this[props]  而不是 this.$data[props]
        this.proxydata(key)
        // 这里真正的 对数据进行代理,并收集 watcher 了
        this.defineProperty(data, key, data[key]);
      })
    }
  }
  // 这里是对 data 中的数据进行一个代理
  // 这样的话,就可以直接使用 this[props]  而不是 this.$data[props]
  proxydata(key) {
    Object.defineProperty(this, key, {
      get() {
        return this.$data[key]
      },
      set(val) {
        this.$data[key] = val
      }
    })
  }
  defineProperty(obj, key, value) {
    // 对数据进行一个遍历
    // 可以对 对象内部的 数据进行深层次的绑定
    this.defineData(value)
    // 建立一个 Dep ,也就是依赖收集工具
    // 把所有 记录着 key 这个参数 要做的操作 都放进这个数据之中
    const dep = new Dep()

    Object.defineProperty(obj, key, {
      get() {
        // 判断 Dep.target 是否存在,如果存在,就 进行依赖收集
        // 这样也是为了防止多次收集
        Dep.target && dep.addDep()
        return value
      },
      set(val) {
        // 当 数据一致的时候,不进行任何操作
        if (value === val) return
        value = val;
        // 进行响应,页面开始进行变化
        // 这里的 dep 实际上已经在 闭包里面了
        // 所以也就是说,一个 key 对应 于一个 dep
        dep.notify()
      }
    })
  }
}

class Dep {
  constructor() {
    // 建立一个收集 依赖的数组
    this.deps = []
  }
  addDep() {
    // 收集 对应的 Watcher
    this.deps.push(Dep.target);
  }
  notify() {
    // 执行所有 的 Watcher
    // 注意,这里的 Watcher 都是对应于 同一个 key 之下的
    this.deps.map(dep => dep.update())
  }
}

class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.cb = cb;
    // 对应了 上面数据之中 的 Dep.target && dep.addDep()
    Dep.target = this;
    // 执行 一下 这个函数,
    // 也就是说 在这个过程之中
    // 先是执行了 之前就定义好的 defineProperty get 函数
    // 这样就 能够 将 当前的 Watcher 给收集到 Dep 当中
    // 同时 也可以对页面进行第一步 的渲染
    this.cb.call(this.vm);
    // 将这个 置为 null, 也就是说 当前依赖已经收集完毕
    // 为 接下来的 Watcher 腾位置的同时
    Dep.target = null;
  }
  update() {
    // 执行当前 的 收集的依赖
    this.cb && this.cb.call(this.vm);
  }
}

analyze.js

class Analyze {
  constructor(el, vm) {
    this.$el = el;
    this.vm = vm;
    // 开始解析 当前的 html 代码
    this.resolve(this.$el)
  }
  createFrgment(el) {
    const frg = document.createDocumentFragment();
    const childNodes = el.childNodes;
    Array.from(childNodes).map(child => {
      frg.appendChild(child)
    })
    return frg
  }
  resolve(el) {
    // 创建 Fragment,并将 当前页面上的 所有节点放到 这里来
    // 实际上也是为了防止 在解析的过程中,会进行 过多的 dom 操作
    // 避免 资源的浪费
    const fragment = this.createFrgment(el);
    // 开始解析
    this.ergodic(fragment)
    this.$el.appendChild(fragment)
  }
  ergodic(frag) {
    const nodeList = frag.childNodes;
    Array.from(nodeList).map(node => {
      // 是节点类型的话,开始解析 当前节点的 属性
      if (node.nodeType === 1) {
        const attr = node.attributes
        this.dealAttribute(node, attr)
      }
      // 是文本节点的话,看看是不是 {{}} 插值 插进去的
      if (node.nodeType === 3) {
        if (this.interP(node.textContent)) {
          // 进行当前的依赖管理
          this.update(node, RegExp.$1, 'text')
        }
      }
      // 如果当前是节点,并且还有子节点的话,继续 解析
      if (node.childNodes && node.childNodes.length > 0) {
        this.ergodic(node)
      }
    })
  }
  dealAttribute(node) {
    Array.from(node.attributes).map(attr => {
      // 这里写的很简单,就是 以  @ 开头的属性的话,就在当前节点进行绑定
      // 注意,vue 是绑定在 document 上,进行了 事件委托的
      if (attr.name.startsWith('@')) {
        const method = attr.value
        node.addEventListener(attr.name.substr(1), this.vm.props.methods[method].bind(this.vm))
      }
      //  这里关于 v-model 就不做太多的校验了,明白意思就好
      if (attr.name === 'v-model') {
        const value = attr.value
        console.log(value)
        this.update(node, value, 'model')
      }
    })
  }
  update(node, exp, type) {
    // 找到 当前处理的 函数 handler
    const handler = this[`${type}Handler`];
    const vm = this.vm
    // 创建一个 Watcher ,把 渲染的函数发过去
    new Watcher(this.vm, exp, function() {
      handler && handler(node, exp, vm)
    })
  }
  // 解析 {{}}
  // 注意,这样解析之后 RegExp.$1 就是匹配的 结果
  interP(text) {
    return /\{\{(.*)\}\}/.test(text)
  }
  // 这个就很简单了,在 update 之中被调用 不赘述
  textHandler(node, exp, vm) {
    node.textContent = vm.$data[exp]
  }
  // v-model 的渲染函数
  modelHandler(node, exp, vm) {
    node.value = vm[exp];
    node.addEventListener('input', (e) => {
      vm[exp] = e.target.value
    })
  }
}

所以说,在自己实现了一遍之后,倒是对于 Dep Watcher 的理解更深刻了

每一个 data 里面的数据,包括深层次对象里面的 数据,每一个 字段都会有一个 Dep,每一个 双向绑定的地方,就会有一个 Watcher

然后每一个 页面,或者 computed ,或者 watch 每一个 都会有 一个 Watcher,

例如上面,

页面上有 两个 {{name}}, 那么每一个 name ,就会有一个 渲染Watcher,然后这些 name 的 Watcher都会被放在 同一个 Dep 之中

------------

那么问题来了,既然 每个 {{}} 都有一个 watch ,那么还要 虚拟 dom 呢?

不过 这上面其实是 vue1.0 的写法,在 vue2.0 之中,使用了虚拟dom

  1. 其实从性能上来看,还真不一定有多大提升,但是需用了 虚拟 dom 之后,可以使用函数式组件,并且运行在其他平台,类似于 node、weex 上了
  2. 这里 使用的时候,每个组件 watcher 通知到 当前 组件的 渲染 render 函数, 然后会使用虚拟 dom 去比较发现更新了哪些内容
  3. 在 2.0 之中,每次执行 watcher 渲染之后,其实都会将 dep 做一次清理,其实就是将 新旧 dep 进行 比对,将新一轮中没有订阅的 watcher 给移除。 cleanupDeps() 这样的好处就是 比如 v-if="false" 里的代码 就不会被订阅到

所以说,复杂页面一定要 拆分组件!!!!减少 dom diff 的过程!!!!

=========

2020.3.20更新

这次在 这个 乞丐版的 vue 上增加 一个 computed 属性

先看 对应的 文件配置,增加了 一个 fullname

<input type="text" v-model="firstname">
<input type="text" v-model="lastname">
{{fullname}}
const props = {
   el: document.querySelector('#app'),
   data: {
     name: 'jack',
     age: 123,
     inp: '',
++   firstname: '',
++   lastname: ''
   },
++ computed: {
++   fullname() {
++     return this.firstname + this.lastname
++   }
   },
   methods: {
     printName() {
       this.name = this.name + 1
     },
     reduce() {
       this.name = this.name.slice(0, -1)
     }
   }
 }

 new PoorVue(props)

由于 只是 简单地实现,以及 了解其原理,就不搞那么复杂了

PoorVue.js

class PoorVue {
  constructor(props) {
    // 对当前的 数据进行保存
    this.props = props;
    this.$data = props.data;
    // 在这里对数据进行一个 双向绑定的前期工作,也就是代理工作
    // 大名鼎鼎的  defineProperty 就是在 这里进行的
    this.defineData(this.$data);
    // 对 computed 属性进行解析
    this.defineComputed(this.props.computed);
    // 对 html 进行解析,这里只会 提取 {{}} 和 @ 事件
    new Analyze(this.props.el, this)
  }
  ....
  ....
  // 可以看到,我其实什么都没有改,
  // 就是在这里增加了这么一段代码,就让 整个函数运行起来了
  defineComputed(computed) {
    const _this = this;
    Object.keys(computed).map(comput => {
      const dep = new Dep()
      Object.defineProperty(this, comput, {
        enumerable: true,
        configurable: true,
        get() {
          Dep.target && dep.addDep()
          return computed[comput].call(_this)
        },
      })
    })
  }
 
 ....
 ....
}
  1. 事实上,在源码中,computed 里的属性是被 定义在了 组件的原型,也就是 Comp.prototype 上的
  2. 首先得了解的一点,在 vue 的源码中, computed 的初始化时相对靠后的位置的,所以 在 computed 里可以使用之前就定义好的  data ,props 属性
  3. 这里的 computed 属性我只弄成了 只读,也就是 只有 get,没有 set
  4. 在 解析 里解析到 computed 的属性之后,就执行 上面定义好的 关于 computed  的 defineproperty
  5. 执行了 computed 的 get,也就是 那个函数之后,也就 立即调用了 里面 依赖的 那两个 data 属性,firstname 和 lastname
  6. 这两个属性 在被 get 的时候,执行了 自己定义的 defineproperty
  7. 这样的话, 在 firstname 和 lastname 里,也就会执行 Dep.target && dep.addDep() 这段代码
  8. Dep.target 在这个时候 指向的是 computed 里的 fullname 的 watcher
  9. Dep.target 有值,就把 这个 watcher 分别插进了 firstname 和 lastname 的 dep 里面
  10. 然后 fullname 的 watcher 执行完毕,Dep.target = null 被清空
  11. 这样 当 firstname 或者 lastname 的值进行了改变的时候,就会 去 执行 computed 里的 fullname 的 watcher,然后渲染到页面上
  12. 要注意的是,在源码中,computed 的 Watcher 会判断是否是 computed,然后 对 这个 属性的值 不做初始化,直接赋值 undefined,只有在被使用的时候,才会 执行。所以会有人在 写了 computed 之后,发现 里面的代码没有执行的情况发生
  13. watch 属性 同理,其实就是 创建自己的 Watcher 之后,将 handler 当作参数 传进去,然后 在 vm 上 获取一遍依赖的数据即可,这样就 将依赖 收集到了 依赖的 字段之中。
  14. watch 的 deep 属性更是如此,只需要 把 依赖字段判断是否是 对象,是的话,就深度遍历 并 执行一遍该字段的 get。也就自然而然地把 依赖收集起来的。
  15. 说到底,其实还是 vue 的依赖收集机制 太强大了

 效果图

 所以说,在这里我们可以看到 这个 computed 属性 fullname 的 watcher 不仅仅是进入了一个 Dep 中,还被放进了 fullname 和 lastname 的 Dep 之中

然后改造一下 watcher

class Watcher {
  ....
  update() {
    this.run()
  }
  // 给这里加个节流
  run = throttle(function () {
    console.log('执行次数')
    // 执行当前 的 收集的依赖
    this.cb && this.cb.call(this.vm);
  }, 0, this)
}

function throttle(fn, delay, ctx) {
  let isAvail = true
  return function () {
    let args = arguments
    // 开关打开时,执行任务
    if (isAvail) {
      isAvail = false
      // delay时间之后,任务开关打开
      Promise.resolve().then(function () {
        fn.apply(ctx, args)
        isAvail = true
      }, delay)
    }
  }
}

这样就能做到 超简单的 异步更新了

使用场景 可以看下面,那个 run 函数里面的 console 只打印了 一次 

<p @click="reduce">{{name}}</p>
 methods: {
    reduce() {
      this.name = this.name + 'h'
      this.name = this.name + 'h'
      this.name = this.name + 'h'
      this.name = this.name + 'h'
    }
  }

 

 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值