vue简单实现理解原理

Mr.Hotsuitor小于 1 分钟vuevuejavascript

vue简单实现理解原理

核心原理理解open in new window一步步读懂vue源码open in new window

阅读源码心得

  • 读源码,先从整体入手,不必一开始拘泥于细节。
  • 整体阅读下来,了解主要流程,每一模块是干什么的即可,需要的时候再回台看实现
  • 读细节的时候,写代码注释,思考每个代码块的功能,理解作者为什么这样写,哪些是可以借鉴的

阅读源码 不是为了把源码写出来,是为了学习思想,实现是次要的,思想是主要的

/**
 * make data reactive
 * when data changes, the `update` method will be invoked automatically and thus updates the DOM
 * @Tips
 * some code may be useless but helpful for understanding
 * some code is incompleted and heavily simplified but also for better understanding
 */

;(function() {
  /**
   * @description: 响应化数据
   * @param {Object} obj 响应化对象
   * @param {String} key
   * @param {String} val
   * @return:
   */
  function defineReactive(obj, key, val) {
    var dep = new Dep() // 收集依赖
    Object.defineProperty(obj, key, {
      get: function() {
        if (Dep.target) {
          Dep.target.addDep(dep)
        }
        return val
      },
      set: function(newVal) {
        // 数据没有变化,不作更新
        if (newVal === val) return
        val = newVal
        // 通知依赖更新
        dep.notify()
      },
    })
  }

  // 观察者
  function observe(obj) {
    for (var key in obj) {
      // 把数据响应化
      defineReactive(obj, key, obj[key])
    }
  }

  var uid$1 = 0

  // 订阅者,用来存放 Watcher 对象的实例
  function Dep() {
    this.subs = [] // 保存 Watcher 实例
    this.id = uid$1++
  }

  Dep.target = null

  Dep.prototype.addSub = function(sub) {
    this.subs.push(sub)
  }

  // 通知事件,通知响应数据和更新dom
  Dep.prototype.notify = function() {
    var subs = this.subs
    // 遍历通知每一个 watch
    for (var i = 0, l = subs.length; i < l; i++) {
      // 触发更新dom
      subs[i].update()
    }
  }

  // 观察者,监测数据
  function Watcher(vm, expOrFn, cb) {
    this.vm = vm // vue实例
    this.getter = expOrFn // 监测的表达式 or function
    this.cb = cb // 回调
    this.depIds = [] // 依赖Id池
    this.value = this.get()
  }

  Watcher.prototype.get = function() {
    Dep.target = this /* ! 保存当前上下文 this的指向 */
    //! 这里是关键
    var value = this.getter.call(this.vm) // 传入的expOrFn this 指向 vue 实例
    Dep.target = null
    return value
  }

  // Watcher 更新监测的值
  Watcher.prototype.update = function() {
    var value = this.get()
    if (this.value !== value) { // 内部优化,值没有变化不做处理
      var oldValue = this.value
      this.value = value
      this.cb.call(this.vm, value, oldValue) // 回调,watch 方法的function(newValue, oldValue)
    }
  }

  // Watcher 绑定对应的订阅者
  Watcher.prototype.addDep = function(dep) {
    var id = dep.id
    // to avoid depending the watcher to the same dep more than once
    if (this.depIds.indexOf(id) === -1) {
      this.depIds.push(id)
      dep.addSub(this)
    }
  }

  /**
   * @description: 虚拟dom
   * @param {String} tag 标签名
   * @param {Object} data 标签属性对象
   * @param {Arrar} children
   * @param {String} text 标签内容文本
   * @param {Element} elm dom 元素
   * @return:
   */
  function vnode(tag, data, children, text, elm) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
  }

  /**
   * @description: 标准化子节点
   * @param {Arrar|String}  children
   * @return: {Arrar}
   */
  function normalizeChildren(children) {
    if (typeof children === 'string') {
      return [createTextVNode(children)]
    }
    return children
  }


  // 这里使用了 [外观模式](/zh/javascript/15外观模式/),
  // 统一对外接口 vnode 用不同的方法包装初始化得到不同的结构
  /**
   * @description: 创建文本virtual node
   * @param {String} val 文本内容
   * @return: {vnode}
   */
  function createTextVNode(val) {
    return new vnode(undefined, undefined, undefined, String(val))
  }

  /**
   * @description: 创建 tag virtual node
   * @param {String} tag 标签
   * @param {String} data
   * @param {Array} children 子元素
   * @return:
   */
  function createElement(tag, data, children) {
    return new vnode(tag, data, normalizeChildren(children), undefined, undefined)
  }

  /**
   * @description: 创建真实dom的方法
   * @param {vnode} vnode
   * @return:
   */
  function createElm(vnode) {
    var tag = vnode.tag
    var data = vnode.data
    var children = vnode.children

    // tag 不为空,创建 element
    if (tag !== undefined) {
      vnode.elm = document.createElement(tag)

      // 设置了标签属性,创建标签属性
      if (data.attrs !== undefined) {
        var attrs = data.attrs
        for (var key in attrs) {
          vnode.elm.setAttribute(key, attrs[key])
        }
      }

      // 有子节点,创建子节点
      if (children) {
        createChildren(vnode, children)
      }
    } else {
      vnode.elm = document.createTextNode(vnode.text)
    }

    return vnode.elm
  }

  /**
   * @description: 创建子节点
   * @param {vnode} vnode
   * @param {Array} children
   * @return:
   */
  function createChildren(vnode, children) {
    // 遍历所有子节点
    for (var i = 0; i < children.length; ++i) {
      // 父级元素append子节点,若子节点又有子节点,递归调用创建
      vnode.elm.appendChild(createElm(children[i]))
    }
  }

  /**
   * @description: 简单对比两个节点是否相同
   * @param {vnode} vnode1
   * @param {vnode} vnode2
   * @return: {Boolean} true 相同
   */
  function sameVnode(vnode1, vnode2) {
    return vnode1.tag === vnode2.tag
  }

  function emptyNodeAt(elm) {
    return new vnode(elm.tagName.toLowerCase(), {}, [], undefined, elm)
  }

  /**
   * @description: 渲染vnode到真实dom
   * @param {type}
   * @return:
   */
  function patchVnode(oldVnode, vnode) {
    var elm = (vnode.elm = oldVnode.elm)
    var oldCh = oldVnode.children
    var ch = vnode.children

    // 不是文本元素
    if (!vnode.text) {
      // if有旧子节点和新子节点,遍历更新子节点
      if (oldCh && ch) {
        updateChildren(oldCh, ch)
      }
    } else if (oldVnode.text !== vnode.text) {
      // 更新的是文本,直接从vnode赋值更新dom文本
      elm.textContent = vnode.text
    }
  }

  /**
   * @description: 更新子节点
   * @param {vnode}
   * @return:
   */
  function updateChildren(oldCh, newCh) {
    // assume that every element node has only one child to simplify our diff algorithm
    // 假设每个元素只有一个子节点,简化diff算法
    // dom层级节点相同,继续遍历下一层节点
    if (sameVnode(oldCh[0], newCh[0])) {
      // 递归调用 patchVnode
      patchVnode(oldCh[0], newCh[0])
    } else {
      // dom层级节点不同或没有子节点直接更新
      patch(oldCh[0], newCh[0])
    }
  }

  /**
   * @description: 真实更新 vnode 方法
   * @param {type}
   * @return:
   */
  function patch(oldVnode, vnode) {
    var isRealElement = oldVnode.nodeType !== undefined // virtual node has no `nodeType` property
    // 旧节点是 virtual node 而且 新旧 vnode 相同,更新 vnode
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode)
    } else {
      // 旧节点时真实 dom,创建一个 空 dom,需要用到其父元素
      if (isRealElement) {
        oldVnode = emptyNodeAt(oldVnode)
      }
      var elm = oldVnode.elm
      var parent = elm.parentNode

      createElm(vnode)

      parent.insertBefore(vnode.elm, elm) // 插入新节点
      parent.removeChild(elm) // 移除旧节点
    }

    return vnode.elm
  }

  function initData(vm) {
    var data = (vm.$data = vm.$options.data)
    var keys = Object.keys(data)
    var i = keys.length
    // proxy data so you can use `this.key` directly other than `this.$data.key`
    while (i--) {
      proxy(vm, keys[i])
    }

    observe(data)
  }

  // 代理访问
  function proxy(vm, key) {
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function() {
        return vm.$data[key]
      },
      set: function(val) {
        vm.$data[key] = val
      },
    })
  }

  function Vue(options) {
    var vm = this
    vm.$options = options

    initData(vm)
    vm.mount(document.querySelector(options.el))
  }

  Vue.prototype.mount = function(el) {
    var vm = this
    vm.$el = el
    new Watcher(vm, function() {
      vm.update(vm.render())
    })
  }

  /**
   * @description: 调用path方法更新dom
   * @param {type}
   * @return:
   */
  Vue.prototype.update = function(vnode) {
    var vm = this
    var prevVnode = vm._vnode // 保存旧vnode,这里是引用关系,可以与下面赋值换位置
    vm._vnode = vnode
    // 没有旧的 vnode,直接传 vnode渲染
    if (!prevVnode) {
      vm.$el = vm.patch(vm.$el, vnode)
    } else {
      // 传入旧的 vnode 对比
      vm.$el = vm.patch(prevVnode, vnode)
    }
  }

  Vue.prototype.patch = patch

  /**
   * @description: 渲染函数,入口渲染
   * @param {type}
   * @return:
   */
  Vue.prototype.render = function() {
    var vm = this
    return vm.$options.render.call(vm)
  }

  var vm = new Vue({
    el: '#app',
    data: {
      message: 'Hello world',
      isShow: true,
    },
    render() {
      return createElement(
        'div',
        {
          attrs: {
            class: 'wrapper',
          },
        },
        [
          this.isShow
            ? createElement(
                'p',
                {
                  attrs: {
                    class: 'inner',
                  },
                },
                this.message
              )
            : createElement(
                'h1',
                {
                  attrs: {
                    class: 'inner',
                  },
                },
                'Hello world'
              ),
        ]
      )
    },
  })

  // test
  setTimeout(function() {
    vm.message = 'Hello'
  }, 1000)

  setTimeout(function() {
    vm.isShow = false
  }, 2000)
})()