vue 数据初始化响应式源码解析、简易响应式系统实现

initState

  • 进行props,methods,data,computed和watch数据的初始化处理,并将他们转换为响应式对象
export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    // 保存当前实例
    const vm: Component = this
    // 合并配置
    if (options && options._isComponent) {
      // 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$options
      initInternalComponent(vm, options)
    } else {
      // 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数

    if (vm.$options.el) {
      // 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
      // 如果没有传就需要手动执行 $mount 才会进入下一阶段
      vm.$mount(vm.$options.el)
    }
  }
}

initState

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  // 初始化props
  if (opts.props) { initProps(vm, opts.props); }
  // 初始化methods
  if (opts.methods) { initMethods(vm, opts.methods); }
  // 初始化data
  if (opts.data) {
    initData(vm);
  } else {
    // 如果没有定义data,则创建一个空对象,并设置为响应式
    observe(vm._data = {}, true /* asRootData */);
  }
  // 初始化computed
  if (opts.computed) { initComputed(vm, opts.computed); }
  // 初始化watch
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch);
  }
}

父组件处理传递给子组件的props

<child :test="test"></child>
  • 父组件优先进行模板编译得到一个render函数,在解析过程中遇到子组件的属性,:test=test会被解析成{ attrs: {test: test}}并作为子组件的render函数存在
with(){..._c('child',{attrs:{"test":test}})}

  • render解析Vnode的过程遇到child这个子占位符节点,因此会进入创建子组件Vnode的过程,创建子Vnode过程是调用createComponent
  • 最终会调用new Vnode去创建子Vnode。而对于props的处理,extractPropsFromVNodeData会对attrs属性进行规范校验后,最后会把校验后的结果以propsData属性的形式传入Vnode构造器中
// 创建子组件过程
function createComponent() {
  // props校验
  var propsData = extractPropsFromVNodeData(data, Ctor, tag);
  ···
  // 创建子组件vnode
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );
}

extractPropsFromVNodeData


function extractPropsFromVNodeData (data,Ctor,tag) {
  // Ctor为子类构造器
  ···
  var res = {};
  // 子组件props选项
  var propOptions = Ctor.options.props;
  // data.attrs针对编译生成的render函数,data.props针对用户自定义的render函数
  var attrs = data.attrs;
  var props = data.props;
  
  if (isDef(attrs) || isDef(props)) {
    for (var key in propOptions) {
      // aB 形式转成 a-b
      var altKey = hyphenate(key);
      {
          var keyInLowerCase = key.toLowerCase();
          if (
            key !== keyInLowerCase &&
            attrs && hasOwn(attrs, keyInLowerCase)
          ) {
            // 警告
          }
        }
    }
  }
}

  • HTML对大小写是不敏感的,所有的浏览器会把大写字符解释为小写字符,因此我们在使用DOM中的模板时,cameCase(驼峰命名法)的props名需要使用其等价的 kebab-case (短横线分隔命名) 命代替。
    即: <child :aB=“test”>需要写成<child :a-b=“test”>

子组件处理props

  • 子组件处理props的过程,是发生在父组件_update阶段,这个阶段是Vnode生成真实节点的过程,期间会遇到子Vnode,这时会调用createComponent去实例化子组件。而实例化子组件的过程又回到了_init初始化,此时又会经历选项的合并,针对props选项,最终会统一成{props: { test: { type: null }}}的写法。接着会调用initProps, initProps做的事情,简单概括一句话就是,将组件的props数据设置为响应式数据。

initProps

function initProps (vm, propsOptions) {
  var propsData = vm.$options.propsData || {};
  
  var loop = function(key) {
    ···
    defineReactive(props,key,value,cb)if (!(key in vm)) {
      //进行数据代理,vm.key访问vm._props.key
      proxy(vm, "_props", key);
    }
  }
  // 遍历props,执行loop设置为响应式数据。
  for (var key in propsOptions) loop( key );
}

initMethods

  • 保证methods方法定义必须是函数,且命名不能和props重复,最终会将定义的方法都挂载到根实例上
function initMethods (vm, methods) {
    var props = vm.$options.props;
    for (var key in methods) {
      {
        // method必须为函数形式
        if (typeof methods[key] !== 'function') {
          warn(
            "Method \"" + key + "\" has type \"" + (typeof methods[key]) + "\" in the component definition. " +
            "Did you reference the function correctly?",
            vm
          );
        }
        // methods方法名不能和props重复
        if (props && hasOwn(props, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a prop."),
            vm
          );
        }
        //  不能以_ or $.这些Vue保留标志开头
        if ((key in vm) && isReserved(key)) {
          warn(
            "Method \"" + key + "\" conflicts with an existing Vue instance method. " +
            "Avoid defining component methods that start with _ or $."
          );
        }
      }
      // 直接挂载到实例的属性上,可以通过vm[method]访问。
      vm[key] = typeof methods[key] !== 'function' ? noop : bind(methods[key], vm);
    }
  }

initData

  • 能和props,methods重复。最后的核心方法是observe,observe方法是将数据对象标记为响应式对象,并对对象的每个属性进行响应式处理
function initData(vm) {
  var data = vm.$options.data;
  // 根实例时,data是一个对象,子组件的data是一个函数,其中getData会调用函数返回data对象
  data = vm._data = typeof data === 'function'? getData(data, vm): data || {};
  var keys = Object.keys(data);
  var props = vm.$options.props;
  var methods = vm.$options.methods;
  var i = keys.length;
  while (i--) {
    var key = keys[i];
    {
      // 命名不能和方法重复
      if (methods && hasOwn(methods, key)) {
        warn(("Method \"" + key + "\" has already been defined as a data property."),vm);
      }
    }
    // 命名不能和props重复
    if (props && hasOwn(props, key)) {
      warn("The data property \"" + key + "\" is already declared as a prop. " + "Use prop default value instead.",vm);
    } else if (!isReserved(key)) {
      // 数据代理,用户可直接通过vm实例返回data数据
      proxy(vm, "_data", key);
    }
  }
  // observe data
  observe(data, true /* asRootData */);
}

initComputed

  • computed可以是对象,也可以是函数,但是对象必须有getter方法,因此如果computed中的属性值是对象时需要进行验证。
  • 针对computed的每个属性,要创建一个监听的依赖,也就是实例化一个watcher,watcher的定义,可以暂时理解为数据使用的依赖本身,一个watcher实例代表多了一个需要被监听的数据依赖
function initComputed (vm, computed) {
  ···
  for (var key in computed) {
      var userDef = computed[key];
      var getter = typeof userDef === 'function' ? userDef : userDef.get;
      // computed属性为对象时,要保证有getter方法
      if (getter == null) {
        warn(("Getter is missing for computed property \"" + key + "\"."),vm);
      }
      if (!isSSR) {
        // 创建computed watcher
        watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);
      }
      if (!(key in vm)) {
        // 设置为响应式数据,并生成对应的watcher
        defineComputed(vm, key, userDef);
      } else {
        // 不能和props,data命名冲突
        if (key in vm.$data) {
          warn(("The computed property \"" + key + "\" is already defined in data."), vm);
        } else if (vm.$options.props && key in vm.$options.props) {
          warn(("The computed property \"" + key + "\" is already defined as a prop."), vm);
        }
      }
    }
}

简易响应式系统实现

var vm = new MyVue({
  id: '#app',
  data: {
    test: 12
  }
})
// myVue.js
(function(global) {
  class MyVue {
      constructor(options) {
        this.options = options;
        // 数据的初始化
        this.initData(options);
        let el = this.options.id;
        // 实例的挂载
        this.$mount(el);
      }
      initData(options) {
      }
      $mount(el) {
      }
    }
}(window))

设置响应式对象 - Observer

class MyVue {
  initData(options) {
    if(!options.data) return;
    this.data = options.data;
    // 将数据重置getter,setter方法
    new Observer(options.data);
  }
}
// Observer类的定义
class Observer {
  constructor(data) {
    // 实例化时执行walk方法对每个数据属性重写getter,setter方法
    this.walk(data)
  }

  walk(obj) {
    const keys = Object.keys(obj);
    for(let i = 0;i< keys.length; i++) {
      // Object.defineProperty的处理逻辑
      defineReactive(obj, keys[i])
    }
  }
}

依赖本身 - Watcher

  • 实例化watcher时会将Dep.target设置为当前的watcher,执行完状态更新函数之后,再将Dep.target置空。这样在收集依赖时只要将Dep.target当前的watcher push到Dep的subs数组即可。而在派发更新阶段也只需要重新更新状态即可
// 监听的依赖
class Watcher {
  constructor(expOrFn, isRenderWatcher) {
    this.getter = expOrFn;
    // Watcher.prototype.get的调用会进行状态的更新。
    this.get();
  }

  get() {
    // 当前执行的watcher
    Dep.target = this
    this.getter()
    Dep.target = null;
  }
  update() {
    this.get()
  }

}

初始化Watcher

  • 那么哪个时间点会实例化watcher从而更新数据状态呢?显然在渲染数据到真实DOM时可以创建watcher
class MyVue {
  $mount(el) {
    // 直接改写innerHTML
    const updateView = _ => {
      let innerHtml = document.querySelector(el).innerHTML;
      let key = innerHtml.match(/{(\w+)}/)[1];
      document.querySelector(el).innerHTML = this.options.data[key]
    }
    // 创建一个渲染的依赖。
    new Watcher(updateView, true)
  }
}

依赖管理 - Dep

  • 相应的每个数据对应的watcher也有很多。而我们在更新数据时,如何通知到数据相关的每一个依赖,这就需要Dep进行通知管理了。
  • 并且浏览器同一时间只能更新一个watcher,所以也需要一个属性去记录当前更新的watcher。而Dep这个类只需要做两件事情,将依赖进行收集,派发依赖进行更新
let uid = 0;
class Dep {
  constructor() {
    this.id = uid++;
    this.subs = []
  }
  // 依赖收集
  depend() {
    if(Dep.target) {
      // Dep.target是当前的watcher,将当前的依赖推到subs中
      this.subs.push(Dep.target)
    }
  }
  // 派发更新
  notify() {
    const subs = this.subs.slice();
    for (var i = 0, l = subs.length; i < l; i++) { 
      // 遍历dep中的依赖,对每个依赖执行更新操作
      subs[i].update();
    }
  }
}

Dep.target = null;

响应式 defineReactive

const defineReactive = (obj, key) => {
  const dep = new Dep();
  const property = Object.getOwnPropertyDescriptor(obj);
  let val = obj[key]
  if(property && property.configurable === false) return;
  Object.defineProperty(obj, key, {
    configurable: true,
    enumerable: true,
    get() {
      // 做依赖的收集
      if(Dep.target) {
        dep.depend()
      }
      return val
    },
    set(nval) {
      if(nval === val) return
      // 派发更新
      val = nval
      dep.notify();
    }
  })
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值