vue2源码简单实现stage2

前面我们已经实现了虚拟dom转变为真实dom,并且挂载到页面的逻辑了,其实已经很接近vue源码的思想了,但是我们还是稍微调整一下代码,让我们调用的时候更加像vue。现在我们要声明一个vue函数,做以下几件事:

  • 声明一个Vue函数,让我们可以new调用这个函数
  • 获取传入的options参数
  • 初始化数据
  • 挂载生成的dom元素
  function Vue(options){
    this.$options = options;
    // 劫持处理数据,
    initData(this)
    //挂载元素
     this.mount(document.querySelector(options.el))
  }

这里说明以下,为什么我们不直接使用Class类去实现vue,下面是vue源码里面关于vue初始化的代码

  Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++

  let startTag, endTag
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
  }

  // a flag to avoid this being observed
  vm._isVue = true
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    vm._name = formatComponentName(vm, false)
    mark(endTag)
    measure(`vue ${vm._name} init`, startTag, endTag)
  }

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等。把不同的功能逻辑拆成一些单独的函数执行,让主线逻辑一目了然,如果用class类实现vue,那么实现这些逻辑会非常费劲。
// 现在我们开始调用Vue

;(function(){
  function Vue (options) {
      this.$options = options;
      console.log(this);
      initData(this);
    //先初始化数据再考虑挂载
      //this.mount(document.querySelector(options.el))
   }
   new Vue({
      el: '#app',
      data: {
        message: 'Hello world'
      },
      render () {
        return createElement(
          'div',
          {
            attrs: {
              'class': 'wrapper'
            }
          },
          [
            createElement(
              'p',
              { 
                attrs: {
                  'class': 'inner'
                }
              },
              this.message
            )
          ]
        )
      }
    })
})();

上面console.log(this)结果如下图:

我们可以注意到vue 中 render 函数调用的是this.message,但是我们调用的vue的时候,明明message是在this.$options.data对象里面的。接下来我们就要接触vue2的一个核心思想了,那就是通过Object.defineProperty这个方法初始化data数据的时候将this.$options.data 转变成vm.message,同样为了好理解,我们只考虑data下面的message是字符串的情况。

;(function(){
  function initData (vm) {
    //这里将vm指向了vue本身,再vm下生成了$data
      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])
      }
    }
  function proxy (vm, key) {
    //  Object.defineProperty 将 vm.$options.data 下面的值赋值给了vm下面
    Object.defineProperty(vm, key, {
      configurable: true,
      enumerable: true,
      get: function () {
        return vm.$data[key]
      },
      set: function (val) {
        vm.$data[key] = val
      }
    })
  }
  function Vue (options) {
      this.$options = options;
      console.log(this);
      initData(this);
    //先初始化数据再考虑挂载
      //this.mount(document.querySelector(options.el))
   }
   new Vue({
      el: '#app',
      data: {
        message: 'Hello world'
      },
      render () {
        return createElement(
          'div',
          {
            attrs: {
              'class': 'wrapper'
            }
          },
          [
            createElement(
              'p',
              { 
                attrs: {
                  'class': 'inner'
                }
              },
              this.message
            )
          ]
        )
      }
    })
})();

我们来看看initData前后Vue函数中this的变化

好了,经过了初始化数据我们可以再render函数中使用this.message这个变量了,我们就开始挂载元素了,这个昨天已经分析过了,只不过稍稍改动了一点代码而已,下面直接贴出完整的代码。

/**
 * apply virtual dom to real dom
 */

;(function () {

  function vnode (tag, data, children, text, elm) {
    this.tag = tag;
    this.data = data;
    this.children = children;
    this.text = text;
    this.elm = elm;
  }

  function normalizeChildren (children) {
    if (typeof children === 'string') {
      return [createTextVNode(children)]
    }
    return children
  }

  function createTextVNode (val) {
    return new vnode(undefined, undefined, undefined, String(val))
  }

  function createElement (tag, data, children) {
    return new vnode(tag, data, normalizeChildren(children), undefined, undefined);
  }

  function createElm (vnode) {
    var tag = vnode.tag;
    var data = vnode.data;
    var children = vnode.children;
    var text = vnode.text

    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])
          vnode.elm.innerHTML = text!==undefined? text:''
        }
      }

      if (children) {
        createChildren(vnode, children)
      }
    } else {
      vnode.elm = document.createTextNode(vnode.text);
    }

    return vnode.elm;
  }

  function createChildren (vnode, children) {
    for (var i = 0; i < children.length; ++i) {
      vnode.elm.appendChild(createElm(children[i]));
    }
  }

  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])
    }
  }

  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) {
    this.$options = options;
    
    initData(this);
    this.mount(document.querySelector(options.el))
  }

  Vue.prototype.mount = function (el) {
    this.$el = el;
    var vnode = this.$options.render.call(this)
    this.patch(this.$el, vnode)
  }

  Vue.prototype.patch = function (oldVnode, vnode) {
    var isRealElement = oldVnode.nodeType !== undefined; // virtual node has no `nodeType` property
    createElm(vnode);
    if (isRealElement) {
     
      var parent = oldVnode.parentNode;
      if (parent) {
        parent.insertBefore(vnode.elm, oldVnode);
        parent.removeChild(oldVnode);
      }
    }

    return vnode.elm
  }

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

})();
posted @ 2021-12-17 11:02  自在一方  阅读(107)  评论(0编辑  收藏  举报