简易 Vue 构建--终

简易 Dep/Watcher

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root">
      <div class="c1">
        <div title="tt1" id="id">233</div>
        <div title="tt2">{{ age }}</div>
        <div title="tt3">{{ gender }}</div>
        <ul>
          <li>111{{name.firstName}}-{{age}}</li>
          <li>{{name.lastName}}</li>
          <li>{{numArr}}</li>
          <li>{{someThing.aaa}}</li>
          <li>3</li>
        </ul>
      </div>
    </div>
    <script>
      let depid = 0

      class Dep {
        constructor() {
          this.id = depid++
          this.subs = [] // 存储的是与 当前 Dep 关联的 watcher
        }

        /** 添加一个 watcher */
        addSub(sub) {
          this.subs.push(sub)
        }
        /** 移除 */
        removeSub(sub) {
          for (let i = this.subs.length - 1; i >= 0; i--) {
            if (sub === this.subs[i]) {
              this.subs.splice(i, 1)
            }
          }
        }

        /** 将当前 Dep 与当前的 watcher ( 暂时渲染 watcher ) 关联*/
        depend() {
          // 就是将 当前的 dep 与当前的 watcher 互相关联
          if (Dep.target) {
            this.addSub(Dep.target) // 将 当前的 watcher 关联到 当前的 dep 上

            Dep.target.addDep(this) // 将当前的 dep 与 当前渲染 watcher 关联起来
          }
        }
        /** 触发与之关联的 watcher 的 update 方法, 起到更新的作用 */
        notify() {
          // 在真实的 Vue 中是依次触发 this.subs 中的 watcher 的 update 方法
          // 此时, deps 中已经关联到 我们需要使用的 那个 watcher 了
          let deps = this.subs.slice()

          deps.forEach((watcher) => {
            watcher.update()
          })
        }
      }

      // 全局的容器存储渲染 Watcher
      Dep.target = null

      let targetStack = []

      /** 将当前操作的 watcher 存储到 全局 watcher 中, 参数 target 就是当前 watcher */
      function pushTarget(target) {
        targetStack.unshift(target) // vue 的源代码中使用的是 push
        Dep.target = target
      }

      /** 将 当前 watcher 踢出 */
      function popTarget() {
        targetStack.shift() // 踢到最后就是 undefined
        Dep.target = targetStack[targetStack.length - 1]
      }

      /**
       * 在 watcher 调用 get 方法的时候, 调用 pushTarget( this )
       * 在 watcher 的 get 方法结束的时候, 调用 popTarget()
       */

      let watcherid = 0

      /** Watcher 观察者, 用于 发射更新的行为 */
      class Watcher {
        /**
         *
         * @param {Object} vm JGVue 实例
         * @param {String|Function} expOrfn 如果是渲染 watcher, 传入的就是渲染函数, 如果是 计算 watcher 传入的就是路径表达式, 暂时只考虑 expOrFn 为函数的情况.
         */
        constructor(vm, expOrfn) {
          this.vm = vm
          this.getter = expOrfn

          this.id = watcherid++

          this.deps = [] // 依赖项
          this.depIds = {} // 是一个 Set 类型, 用于保证 依赖项的唯一性 ( 简化的代码暂时不实现这一块 )

          // 一开始需要渲染: 真实 vue 中: this.lazy ? undefined : this.get()
          this.get()
        }

        /** 计算, 触发 getter */
        get() {
          pushTarget(this)

          this.getter.call(this.vm, this.vm) // 上下文的问题就解决了

          popTarget()
        }

        /**
         * 执行, 并判断是懒加载, 还是同步执行, 还是异步执行:
         * 我们现在只考虑 异步执行 ( 简化的是 同步执行 )
         */
        run() {
          this.get()
          // 在真正的 vue 中是调用 queueWatcher, 来触发 nextTick 进行异步的执行
        }

        /** 对外公开的函数, 用于在 属性发生变化时触发的接口 */
        update() {
          this.run()
        }

        /** 清空依赖队列 */
        cleanupDep() {}

        /** 将 当前的 dep 与 当前的 watcher 关联 */
        addDep(dep) {
          this.deps.push(dep)
        }
      }

      /**
       *  #0.* 属于重要辅助函数
       */

      // 如何得到虚拟dom
      // 即:将真实的DOM元素转换成js对象
      // 构建虚拟dom对象
      class VNode {
        constructor(tag, data, value, type) {
          this.tag = tag && tag.toLowerCase() // 标签名
          this.data = data // 属性
          this.value = value // 标签内容
          this.type = type // 节点类型
          this.children = [] // 子节点
        }
        // 添加虚拟dom子节点
        appendChild(vnode) {
          this.children.push(vnode)
        }
      }
      // TODO:#0.1 将真实dom转换成虚拟dom,这个函数当做 compiler 函数
      function getVNode(node) {
        const type = node.nodeType
        let _vnode = null
        if (type === 1) {
          // 标签节点
          const tag = node.nodeName // 标签名
          const attrs = node.attributes // attributes 真实dom里存放标签属性 数组对象
          const data = {} // 虚拟dom中 属性就只是对象
          for (let i = 0; i < attrs.length; i++) {
            data[attrs[i].nodeName] = attrs[i].nodeValue // 数组每一项
          }
          const value = undefined
          _vnode = new VNode(tag, data, value, type)
          const children = node.childNodes
          // 子节点
          for (let i = 0; i < children.length; i++) {
            _vnode.appendChild(getVNode(children[i]))
          }
        } else if (type === 3) {
          // 文本节点
          _vnode = new VNode(undefined, undefined, node.nodeValue, type)
        }
        // 将每一个虚拟节点返回
        return _vnode
      }

      // TODO:#0.2 将虚拟 DOM 转换成真正的 DOM
      function parseVNode(vnode) {
        let oDom = null
        // 标签节点
        if (vnode.type === 1) {
          oDom = document.createElement(vnode.tag)
          const data = vnode.data
          // 属性节点
          Object.keys(data).forEach((key) => {
            let attrName = key
            let attrValue = data[key]
            oDom.setAttribute(attrName, attrValue)
          })
          // 子节点
          for (let i = 0; i < vnode.children.length; i++) {
            oDom.appendChild(parseVNode(vnode.children[i]))
          }
        } else if (vnode.type === 3) {
          // 创建文本节点
          oDom = document.createTextNode(vnode.value)
        }
        return oDom
      }

      // 对象解析函数 a.b.c.d这种格式
      function getValueByKey(obj, str) {
        const arr = str.split('.')
        let prop = null
        while ((prop = arr.shift())) {
          obj = obj[prop]
        }
        return obj
      }

      const reg = /\{\{(.+?)\}\}/g
      // TODO:#0.3 将带有坑的 Vnode 与数据 data 结合, 得到填充数据的 VNode: 模拟 AST -> VNode
      function combine(vnode, data) {
        let _type = vnode.type
        let _data = vnode.data
        let _value = vnode.value
        let _tag = vnode.tag
        let _children = vnode.children

        let _vnode = null

        if (_type === 3) {
          // 文本节点

          // 对文本处理
          _value = _value.replace(reg, function (_, g) {
            return getValueByKey(data, g.trim())
          })

          // 重新创建虚拟dom对象
          _vnode = new VNode(_tag, _data, _value, _type)
        } else if (_type === 1) {
          // 元素节点
          // 重新创建虚拟dom对象
          _vnode = new VNode(_tag, _data, _value, _type)
          _children.forEach((_subvnode) => _vnode.appendChild(combine(_subvnode, data)))
        }

        return _vnode
      }

      // TODO:#0.4 函数重写
      const ARRAY_METHOD = ['push', 'pop', 'shift', 'unshift', 'reverse', 'splice', 'sort']
      // 原型继承 array_method.__proto__ 指向 Array.prototype
      const array_method = Object.create(Array.prototype)
      // TODO:数组部分方法改造成响应式  方法重写(拦截)
      ARRAY_METHOD.forEach((key) => {
        array_method[key] = function () {
          // 方法重写(拦截)
          // 将新添加的数据进行响应式化
          for (let i = 0; i < arguments.length; i++) {
            if (typeof arguments[i] === 'object' && arguments[i] != null) {
              def(arguments[i], '__dep__', new Dep())
            }
            observe(arguments[i])
          }

          setTimeout(() => {
            this.__dep__.notify()
          }, 0)
          // 执行数组原型上的方法
          return Array.prototype[key].apply(this, arguments)
        }
      })

      function def(target, key, val) {
        Object.defineProperty(target, key, {
          value: val
        })
      }
      // #5.2 定义得到响应式数据的方法
      function defineReactiveProperty(target, key, value, enumerable) {
        let dep = new Dep()
        dep.__propName__ = key
        // TODO:对非数组的引用类型进行拦截,进行响应式注册
        if (typeof value === 'object' && value != null) {
          def(value, '__dep__', dep)
          observe(value)
        }

        Object.defineProperty(target, key, {
          configurable: true, // 可配置的
          enumerable: !!enumerable, // 是否可枚举
          get() {
            // console.log(`读取 ${key} 属性`); // 额外
            // 依赖收集 ( 暂时略 )
            dep.depend()
            return value
          },
          set(newVal) {
            if (value === newVal) return
            // ! 新添加的值响应式化
            if (typeof newVal === 'object' && newVal != null) {
              observe(newVal)
            }
            console.log('赋值成功了')

            value = newVal

            // 派发更新, 找到全局的 watcher, 调用 update
            dep.notify()
          }
        })
      }

      // #5.1 递归实现所有数据的响应式注册
      function observe(data, isRoot) {
        if (typeof data === 'object' && data != null && isRoot) {
          def(data, '__dep__', new Dep())
        }
        if (Array.isArray(data)) {
          // 数组方法重写
          data.__proto__ = array_method
          // 如果是数组则遍历注册,这里是对数组内部进行响应式注册
          data.forEach((item) => {
            observe(item)
          })
        } else {
          Object.keys(data).forEach((key) => {
            let value = data[key]
            // 除数组外的数据类型均在这里进行响应式注册
            defineReactiveProperty(data, key, value, true)
          })
        }
      }

      // #5.3 代理 -> 映射
      function proxy(target, middle, key) {
        Object.defineProperty(target, key, {
          enumerable: true,
          configurable: true,
          get() {
            return target[middle][key]
          },
          set(newVal) {
            target[middle][key] = newVal
          }
        })
      }

      function MyVue(obj) {
        // 习惯: 内部的数据使用下划线 开头, 只读数据使用 $ 开头
        // 数据
        this._data = obj.data

        // 根元素
        let el = document.querySelector(obj.el) // vue 是字符串, 这里是 DOM
        // 根模板
        this._template = el
        // 为什么要拿父节点
        this._parent = this._template.parentNode

        // TODO:#5 响应式化 + 代理 映射
        this.initData()
        // TODO:#1
        this.mount()
      }

      // 数据初始化
      MyVue.prototype.initData = function () {
        observe(this._data, true)

        Object.keys(this._data).forEach((key) => {
          proxy(this, '_data', key)
        })
      }

      MyVue.prototype.mount = function () {
        // TODO:#2 需要提供一个 render 方法: 生成 虚拟 DOM
        // 这里得到了一个未执行的函数  该函数执行后的结果是合并数据后的VNode
        this.render = this.createRenderFunction()
        // 视图更新
        this.mountComponent()
      }

      MyVue.prototype.mountComponent = function () {
        let mount = () => {
          // TODO:#4
          // this.render() 会得到数据合并后的VNode
          // 更新函数执行得到真实DOM并渲染到页面
          this.update(this.render())
        }
        // TODO:#3
        // 这个 Watcher 就是全局的 Watcher, 在任何一个位置都可以访问他了 ( 简化的写法 )
        new Watcher(this, mount) // 相当于这里调用了 mount
      }
      // 渲染函数
      MyVue.prototype.createRenderFunction = function () {
        // #0.1  VNode 模拟 AST   获得VNode
        const ast = getVNode(this._template)

        return function () {
          // #0.3 将 带有 坑的 VNode 转换为 待数据的 VNode
          return combine(ast, this._data)
        }
      }

      // 更新视图
      MyVue.prototype.update = function (vnode) {
        // 转换成真实DOM
        let realDOM = parseVNode(vnode)

        this._parent.replaceChild(realDOM, document.querySelector('#root'))
      }

      // 创建实例
      const mv = new MyVue({
        el: '#root',
        data: {
          name: { firstName: 'wang', lastName: 'wu' },
          age: 18,
          gender: '男',
          someThing: { aaa: 'a' },
          numArr: [1, 2]
        }
      })
    </script>
  </body>
</html>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值