vue源码的总结

  observe(data); // 观测这个数据

 

当我们new Vue之后,做了什么事情?

1,当我们new Vue之后,调用了Vue构造函数,传入配置项

2,Vue构造函数传入的配置项,调用this._init(options)方法

// Object.defineProperty() vue2版本的数据劫持
// 构造函数或者类
function Vue(options) {
  // console.log(options)
  // 内部要进行初始化的操作
  this._init(options); // 初始化操作
}

// 原型模式
initMixin(Vue); // 添加原型的方法
renderMixin(Vue);
lifeCycleMixin(Vue);


// initGlobalApi 给构造函数来扩展全局的方法
initGlobalAPI(Vue);
Vue.prototype.$nextTick = nextTick

3,_init方法时通过原型模式进行注入的

4,提供一个_initMixin方法,传入Vue构造函数

export function initMixin(Vue) {
  // 传入的构造函数的原型上添加方法
  Vue.prototype._init = function (options) {
    // Vue的内部 $options 就是用户传递的所有参数
    const vm = this; // this指向initMixin的实例
    // 这个options 就包含了用户创建实例时传入的所有属性 Vue.options
    vm.$options = mergeOptions(vm.constructor.options, options); // 用户传入的参数
    callHook(vm, 'beforeCreate') // 调用生命周期函数
    // vm._data = vm.$options.data; // 获取用户传入的data
    initState(vm); // 初始化状态 data/methods/props/computed/watcher/provide/inject

    callHook(vm, 'created') // 执行了created函数
    // 需要通过模板进行渲染
    if (vm.$options.el) { // 用户传入了el属性
      vm.$mount(vm.$options.el)
    }
  }
  Vue.prototype.$mount = function (el) { // 可能是字符串 也可以传入一个dom对象 #app
    const vm = this;
    el = vm.$el = document.querySelector(el); // 获取el属性

    // 如果同时传入 template 和render  默认会采用render 抛弃template,如果都没传就使用id="app"中的模板
    const opts = vm.$options; // 获取用户传入的所有参数

    if (!opts.render) { // 没有render方法
      let template = opts.template; // 获取模板
      if (!template && el) { // 应该使用外部的模板
        template = el.outerHTML; // 获取外部模板
        console.log(template) // <div id="app"><p>hello</p></div>
      }

      const render = compileToFunctions(template); // div v-if v-show {{  }}
      // compileToFunctions => render函数  相当于把template模板编译成了render函数,这个函数一致性就会返回当前组件的虚拟dom
      opts.render = render; // render函数执行返回一个当前组件的虚拟dom结构
    }
    // 走到这里说明不需要编译了 ,因为用户传入的就是 一个render函数
    mountComponent(vm, el); // 组件的挂载流程
  }
}

5,initMixin方法内部会把_init挂载到Vue原型上

6,_init方法内部在执行数据挂载之前,先通过callHook函数调用beforeCreate函数

    initState(vm); // 初始化状态 data/methods/props/computed/watcher/provide/inject

7,内部会判断是否有data选项,如果有,通过initData执行数据的响应式处理initData(vm)

8,处理data数据:判断data是不是一个函数,如果是函数通过call方法,this执行vm之后再带哦用【使用的时候data函数内部的this指向的是vue实例】,如果不是一个函数,直接赋值

function initData(vm) {
  // 数据响应式原理 data中的数据需要做一个数据劫持,当我改变数据时,应该更新视图
  let data = vm.$options.data; // 用户传入的数据挂载到了vm.$options上
  // this.data = vm.$options.data
  // vm._data 就是检测后的数据了
  // vm._data是做什么的? 为了方便后续的操作,将用户传入的data数据,放到vm._data上
  // 为data做一个代理,方便用户直接通过vm.key 获取到data里面的属性,用this.key也可以获取到data里面的属性
  // 为什么可以使用this进行访问到? 因为在initState中,已经将data代理到了vm实例上了
  // 修改this指向,指向vm实例
  data = vm._data = typeof data === 'function' ? data.call(vm) : data;
  // 观测数据
  // 将数据全部代理到vm的实例上

  // this.msg
  // this.data.msg
  for (let key in data) { // 将data上的所有属性都代理到vm上
    proxy(vm, '_data', key);
  }
  // 效果:已经可以使用this.key 获取到data里面的属性了,也可以设置属性了,经过了数据的代理
  observe(data); // 观测这个数据
}

 

9,【数据的代理】遍历data数据:initDdata中的observe(data)观测这个数据

export function observe(data) {
  // 对象就是使用defineProperty 来实现响应式原理
  // 如果这个数据不是对象 或者是null 那就不用监控了
  if (!isObject(data)) {
    return
  }
  // 对象或者数组
  // 当前数据是否已经被响应式劫持过
  if (data.__ob__) {
    return
  }
  // if (data.__ob__ instanceof Observer) { // 防止对象被重复观测
  //   return;
  // }

  // 对数据进行defineProperty
  return new Observer(data); // 可以看到当前数据是否被观测过
}

 

 

使用Object.defineProperty进行数据劫持

【获取属性】组件内部通过this.【data里面的键】获取值的时候,会返回vm上的_data里面的数据

【设置属性】通过this.【data里面的键】设置键的时候,属性设置给数据,而不是直接添加到vm实例上

10,【数据劫持】调用observe(data),开始执行真正的响应式操作

11,第一步:coustructor会先给当前的数据打一个标记,__ob__添加了这个属性就不需要二次处理,添加一个,不可枚举,不可以遍历。添加了一个,不可删除,不能修改,不可以重新定义,不可配置。

12,先判断是不是一个数组,因为数组和对象的处理方法不一样,如果是一个数组的话:

// 先判断是不是一个数组,因为数组的索引是可以被改变的,所以需要对数组的索引进行拦截
    // 为什么这里的data会有数组类型? 因为vue是可以监控数组的变化的,所以这里的data可能是数组
    if (Array.isArray(data)) { // 如果是数组的话,就要重写数组的方法
      // vue如何对数组进行处理呢? 数组用的是重写数组的方法  函数劫持
      // 改变数组本身的方法我就可以监控到了
      data.__proto__ = arrayMethods; // 重写当前数组的原型方法
      // 每一级的数组和对象都要进行劫持,如果是数组的话,就要重写数组的方法,如果是对象的话,就要重写对象的方法
      // 劫持之后,做成响应式数据
      this.observeArray(data);
      //   1、重写数组的原型方法(push,....7个)
      //   2、遍历数组,判断每一项是不是对象或者数组,如果是继续处理,上一个数组的内部
    } else {
      // data是一个对象
      this.walk(data); // 可以对数据一步一步的处理
    }

 

这里数组的响应式和对象的响应式是不一样的,但是我们先判断是不是一个数组,再判断是不是一个对象。因为:数组也是一个对象,如果先判断对象,没有办法分辨出是一个数组还是一个对象了。

13,数组劫持了数组的七个方法,vue重写可以改变数组的七种方法;如果是对象的话:劫持Object.defineProperty;

处理数组的方法,进行递归遍历,一直遍历到普通数据类型

创建一个新的原型对象,传入了原生数组的原型对象,作为空对象的原型对象,这样就可以找到数组原型上的方法,而且修改对象的时候,不会影响到原数组的原型方法

把数组的原型指向这个新的对象,当调用数组的方法的时候,先从这个新对象上去找,如果找不到的话,去数组的原型上找

重新编写数组使用了AOP的切片编程

let oldArrayMethods = Array.prototype; // 获取数组原型上的方法

// 创建一个全新的对象,传入了原生数组的原型对象,作为空对象的原型对象,可以找到数组原型上的方法,而且修改对象时不会影响原数组的原型方法
// 把数组的原型指向这个新的对象,当调用数组的方法时,先从这个新的对象上找,如果找不到再去数组原型上找
export let arrayMethods = Object.create(oldArrayMethods); // {}.__proto__

let methods = [ // 这七个方法都可以改变原数组
  'push',
  'pop',
  'shift',
  'unshift',
  'sort',
  'reverse',
  'splice'
]

// arr.filter()

// arrayMethods = {
//   push () {},
//   pop() {},
//   // ...
// }

// vue劫持数组方法的目的是什么?
// object.defineProerty
methods.forEach(method => {
  arrayMethods[method] = function (...args) { // 函数劫持 AOP
    // 当用户调用数组方法时 会先执行我自己改造的逻辑 在执行数组默认的逻辑
    const ob = this.__ob__;
    // oldArrayMethods原生数组的方法
    // this指向数组,args是用户传递的参数,apply可以改变this指向,让this指向数组
    // 为什么要用apply?因为apply可以传递多个参数
    // AOP 面向切片编程,在不改变原有逻辑的基础上,对原有逻辑进行扩展,比如在原有逻辑之前或之后执行一些逻辑,这就是AOP
    let result = oldArrayMethods[method].apply(this, args); // 调用数组原生的方法,指向数组
    // this.arr.push('23')
    let inserted;
    // push unshift splice 都可以新增属性  (新增的属性可能是一个对象类型)
    // 内部还对数组中引用类型也做了一次劫持  [].push({name:'hm'})
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break;
      case 'splice': // 也是新增属性  可以修改 可以删除  [].splice(arr,1,'div')
        inserted = args.slice(2);
        break;
      default:
        break;
    }
    inserted && ob.observeArray(inserted);
    // render() // 重新渲染界面
    return result;
  }
})

 

如果是对象的话,进行递归,判断里面的每一项,直至递归到都是普通数据类型,给每一项添加一个get和set方法,进行数据的渲染和数据的更新,添加一个dep进行watcher的收集

// vue2响应式有什么缺点
// 1,如果数据层次过多,递归会很消耗性能【如果是一次性使用的数据,使用Object.freeze进行冻结】
// 2,初次渲染的时候,性能不好,因为需要递归的去遍历对象,把属性都进行defineProperty
// 3,无法检测到对象属性的新增和删除,因为vue2是在初始化的时候,对属性进行defineProperty,所以无法检测到对象属性的新增和删除,需要使用$set方法进行变化,或者使用数组的索引进行变化
// 4,数组的变化无法检测到,因为数组的变化方法没有被重写,所以无法检测到数组的变化,需要使用$set方法进行变化,或者使用数组的索引进行变化
// 5,如果数据对象中嵌套了太多层次的对象,那么递归的去遍历对象,会造成性能的浪费
// 数据劫持的是数组的方法
// 为什么数组不适用Object.defineProperty
// 初始化的时候只劫持data已经存在的属性
// 检测不到数据key的删除
function defineReactive(data, key, value) { // data对象  key msg  value 23·
    // 如果值是一个对象的话,就继续递归循环检测,如果是基本类型的话,就不用递归了
    // 如果传入的值还是一个对象的话 就做递归循环检测
    // 数据对象,每个人都会获取到一个dep,相当于收集依赖的容器
  observe(value);
    // 给数据的每一个属性都增加一个get和set方法
  let dep = new Dep(); // msg.dep =[watcher]  age.dep = [watcher]  // 渲染watcher中.deps [msg.dep,age.dep]
  // 观察者,get收集依赖,set触发依赖。收集watcher,触发watcher
  Object.defineProperty(data, key, {
    get() {
      // 获取数据的时候会触发,初始化数据的时候,不会触发get
      // 编译模板的时候会获取数据,获取数据会触发当前数据的get方法
      // get触发的时候,会把当前的组件更新方法,添加到dep中
      // vue的更新操作是组件级别的
      // 这里会有取值的操作,给这个属性增加一个dep,这个dep 要和刚才我放到全局变量的上的watcher 做一个对应关系
      // 添加观察者
      // 当前数据被哪个组件使用到了,当前组件的更新方法添加到观察者
      if (Dep.target) {
        dep.depend(); // 让这个dep 去收集watcher
      }
      return value
    },
    set(newValue) {
      // 修改数据的时候会触发,初始化数据的时候,不会触发set
      if (newValue == value) return; // 如果新值和老值一样,就不用更新了
      observe(newValue); // 监控当前设置的值,有可能用户给了一个新值,用户设置的最新的值
      value = newValue;
      // 当我们更新数据后 要把当前自己对应的watcher 去重新执行以下.
      // 手机进去的依赖进行更新
      dep.notify(); // 更新数据的时候,把get方法中收集的watcher全部执行一遍
    }
  })
}

 

编译模板的步骤 AST

initMixin给给构造函数的原型上添加方法,添加_init和this指向initMixin赋值给vm

vm获取用户传入的参数

使用Vue.prototypr.$mount函数,获取到vm的实例化对象,然后获取到el的属性

  Vue.prototype.$mount = function (el) { // 可能是字符串 也可以传入一个dom对象 #app
    const vm = this;
    el = vm.$el = document.querySelector(el); // 获取el属性
// 如果同时传入 template 和render  默认会采用render 抛弃template,如果都没传就使用id="app"中的模板
const opts = vm.$options; // 获取用户传入的所有参数

获取到用户传入的参数,进行判断有没有render方法

    if (!opts.render) { // 没有render函数
      let template = opts.template; // 获取模板
      if (!template && el) { // 应该使用外部的模板
        template = el.outerHTML; // 获取外部模板
        console.log(template) // <div id="app"><p>hello</p></div>
      }

      const render = compileToFunctions(template); // div v-if v-show {{  }}
      // compileToFunctions => render函数  相当于把template模板编译成了render函数,这个函数一致性就会返回当前组件的虚拟dom
      opts.render = render; // render函数执行返回一个当前组件的虚拟dom结构
    }

 

如果调用的时候没有render函数,就会去获取模板,首先获取外部模板,编译程div的格式,这个时候的div格式是字符串类型

      const render = compileToFunctions(template); // div v-if v-show {{  }}

 

如果调用的时候有render函数,直接走编译

  Vue.prototype.$mount = function (el) { // 可能是字符串 也可以传入一个dom对象 #app
    const vm = this;
    el = vm.$el = document.querySelector(el); // 获取el属性

    // 如果同时传入 template 和render  默认会采用render 抛弃template,如果都没传就使用id="app"中的模板
    const opts = vm.$options; // 获取用户传入的所有参数

    if (!opts.render) { // 没有render方法
      let template = opts.template; // 获取模板
      if (!template && el) { // 应该使用外部的模板
        template = el.outerHTML; // 获取外部模板
        console.log(template) // <div id="app"><p>hello</p></div>
      }

      const render = compileToFunctions(template); // div v-if v-show {{  }}
      // compileToFunctions => render函数  相当于把template模板编译成了render函数,这个函数一致性就会返回当前组件的虚拟dom
      opts.render = render; // render函数执行返回一个当前组件的虚拟dom结构
    }
    // 走到这里说明不需要编译了 ,因为用户传入的就是 一个render函数
    mountComponent(vm, el); // 组件的挂载流程
  }

 

1,parseHTML编译出来ast语法树(静态节点编译优化)【进行正则匹配】

  let ast = parseHTML(template); // '<div></div>'进行页面编译,把字符串编译成ast语法树

 

创建AST树,进行正则的匹配

//              字母a-zA-Z_ - . 数组小写字母 大写字母
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名
// ?:匹配不捕获   <aaa:aaa>
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// startTagOpen 可以匹配到开始标签 正则捕获到的内容是 (标签名)
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 标签开头的正则 捕获的内容是标签名
// 闭合标签 </xxxxxxx>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配标签结尾的 </div>
// <div aa   =   "123"  bb=123  cc='123'
// 捕获到的是 属性名 和 属性值 arguments[1] || arguments[2] || arguments[2]
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // 匹配属性的
// <div >   <br/>
const startTagClose = /^\s*(\/?)>/; // 匹配标签结束的 >
// 匹配动态变量的  +? 尽可能少匹配
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

export function parseHTML(html) {
  // ast 树 表示html的语法
  let root; // 树根
  let currentParent; // 标识当前父亲是谁
  let stack = []; // 用来判断标签是否正常闭合  [div]  解析器可以借助栈型结构
  // <div id="app" style="color:red"><span>    helloworld {{msg}}   </span></div>

  // vue2.0 只能有一个根节点 必须是html 元素

  // 常见数据结构 栈 队列 数组 链表 集合 hash表 树
  function createASTElement(tagName, attrs) { // 产生ast元素的
    return { // ast语法树 用对象来描述原生语法
      tag: tagName, // 标签名
      attrs,  // 属性
      children: [], // 子元素
      parent: null, // 父亲是谁
      type: 1 // 1 普通元素  3 文本
    }
  }

  // console.log(html)
  function start(tagName, attrs) { // 开始标签 每次解析开始标签 都会执行此方法
    let element = createASTElement(tagName, attrs);
    if (!root) {
      root = element; // 只有第一次是根
    }
    currentParent = element; // 标识当前父亲是谁
    stack.push(element); // 将开始标签存放到栈中
  }

  // <div> <span></span> hello world</div>   [div,span]
  function end(tagName) { // 结束标签  确立父子关系
    let element = stack.pop(); // 取出栈中的最后一个
    currentParent = stack[stack.length - 1]; // 取出当前的父亲是谁
    if (currentParent) { // 在闭合时可以知道这个标签的父亲是谁
      element.parent = currentParent; // 在闭合时可以知道这个标签的父亲是谁
      currentParent.children.push(element); // 实现了一个树的父子关系
    }
  }

  function chars(text) { // 文本
    text = text.replace(/\s/g, ''); // 去掉空格
    if (text) { // 如果是空字符串 不处理
      currentParent.children.push({ // 将文本放到当前父亲的children中
        type: 3, // 文本类型
        text // 文本内容
      })
    }
  }

  // 根据 html 解析成树结构  </span></div>
  while (html) { // 只要html不为空字符串 就一直解析
    let textEnd = html.indexOf('<'); // 查找<的位置
    if (textEnd == 0) { // 如果当前索引为0 说明是一个标签 开始标签或者结束标签
      const startTageMatch = parseStartTag(); // 通过这个方法获取到匹配的结果 tagName,attrs

      if (startTageMatch) {
        // 开始标签
        start(startTageMatch.tagName, startTageMatch.attrs) // 开始标签的处理
      }
      const endTagMatch = html.match(endTag); // 匹配结束标签

      if (endTagMatch) {
        advance(endTagMatch[0].length); // 删除结束标签
        end(endTagMatch[1]) // 将结束标签传入
      }
      // 结束标签
    }

    // 如果不是0 说明是文本
    let text; // 文本
    if (textEnd > 0) {
      text = html.substring(0, textEnd); // 是文本就把文本内容进行截取
      chars(text); // 将文本进行处理
    }
    if (text) {
      advance(text.length); // 删除文本内容
    }
  }

  function advance(n) {
    html = html.substring(n); // 删除指定的内容
  }

  function parseStartTag() {
    const start = html.match(startTagOpen); // 匹配开始标签
    if (start) {
      const match = {
        tagName: start[1], // 匹配到的标签名
        attrs: [] // 匹配到的属性
      }
      advance(start[0].length);
      let end, attr;
      while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) { // 只要不是结尾标签 并且能匹配到属性 就一直解析
        advance(attr[0].length); // 删除属性
        match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5]}) // 将属性放到match的attrs中
      }

      if (end) {
        advance(end[0].length); // 删除开始标签结束的 >
        return match; // 返回匹配的结果
      }
    }
  }

  return root;

}

将AST进行优化

优化的目标:生成模板AST,检测不需要进行DOM改变的静态子树,一定那检测到这些静态树,我们就能做以下这些事情

(1)把他们变成常熟,这样我们就再也不需要每次重新渲染时创建新的节点了

(2)在patch的过程中直接进行跳过

2,generate编译出可执行的函数字符串【遍历语法树,进行字符串的拼接】

3,使用with包裹【拼接字符串】,拼接字符串的时候会为变量添加this指向,这样模板使用data中的数据的时候,就不用写this了

4,new Function把字符串变成可执行函数

如果走到最后,说不需要进行编译了,用户传入的就是一个render函数,直接给函数vm实例和el进行挂载

使用mountComponent,进行callHook函数调用

调用mountComponent函数进行新旧dom的对比,使用Watcher进行页面的更新

虚拟dom进行对比

callHook挂载前获取vm实例和beforeMount钩子,然后开始编译

在updataComponent函数中进行新旧dom的对比,调用render方法后返回的时虚拟dom

每次数据发生变化,旧执行update方法 new Watcher进行页面的更新

创建Watcher的实例化对象,所以Watcher时更新数据的方法

观察者模式

使用Object.defineProperty对对象数据进行拦截,判断是否有__ob__的属性,如果没有__ob__的情况下,会添加一个__ob__,然后添加不可枚举和不可修改的属性。

进行判断data是不是数组类型,如果是数组类型的情况下,vue进行函数劫持,然后重写数组的原型方法,如果说劫持的是数组就改变数组的七种方法,如果说劫持的是对象就改变对象的方法。

如果值是一个对象的话defineReactive,就继续递归循环检测,如果说是基本类型的话,就不用进行递归。传入对象就进行递归的循环检测,给每一个数据添加一个dep,收集依赖的容器,用来收集watcher。声明一个new Dep,进行赋值。【开启观察者模式】get进行依赖的watcher的收集,set触发依赖,触发watcher中的run方法以及进行数据的更新。

get获取数据的时候会触发,初始化数据的时候,不会触发get。编译模板的时候会获取数据,获取数据会触发当前的数据的get方法,get触发的时候会把当前组件进行更新,添加到dep中,vue的更新操作是组件级别的,这个会有一个取值的操作,给这个属性添加一个dep要和全局变量的watcher做出一个对应关系。

下一步,把watcher储存到deo中

dep和watcher是典型的观察者模式,dep是目标(有一个subs),watcher是观察者,把watcehr储存到sunes里面,对数据进行观测,当目标数据方法变化的时候,通知所有的观察者,观察者执行对应的方法,更新视图。更新视图的方法是调用notify,进行修改,触发set方法。

为什么让watcher储存到dep中?因为watcher是组件级别的,dep是全局唯一的,所以让watcher进行dep储存,储存之后进行模板编译

观察者模式进行数据更新

触发set方法,然后触发dep.notify()方法,notify中有一个subs数组,存的是需要更新的watcher,然后watcher中调用updata函数触发然后queueWatcher函数,将watcher进行修改,进行页面视图的更新处理

首先要知道一个概念,Vue是组件化更新页面的,Vue一个组件中有多个属性,会产生多个watcher,但是这些watcher的id是相同的。【Vue中不同的组件的watcher id是不相同的,但是同一个组件的watcher id是相同的】

数据更新步骤

第一步:首先给watcher储存到一个队列当中【等待多个组件一起储存,一起更新】

第二步:页面同步更新的时候,当前watcher.id已经存在了,不需要再次进行储存,这样,页面的同步更新和异步更新都会进行更新操作

第二部:等待更新,清空队列,一直执行watcher的set方法,set方法会触发dep.notify(),dep.notify()会触发watcher的updata方法

第四步:执行watcher的run方法,调用get方法,重新进行页面的渲染

nextTick页面的异步的更新

nextTick,想要获取最新的页面dom结构的时候需要使用到nextTick

当修改完数据以后,会触发当前数据的set方法,通知当前数据在get阶段收集到的所有依赖【watcher】进行更新,更新不是同步更新的,而是通过nextTick注册异步任务进行更新,所以修改完数据以后想要立即获取最新的页面结构是获取不到的。

这个时候我们可以使用nextTick继续注册一个异步任务

同步代码走完,开始清空异步任务

 nextTick的异步任务类型是根据宿主环境进行判断promise,mutationObserver,setImmediate,setTimeout。

场景:行内编辑的时候,自动获取焦点

修改完数据之后,会触发set方法,通知set阶段收集watcher触发updata方法,将watcher储存到更新队列中,使用nextTick中的定时器进行异步更新,进行队列的清空,一次执行watcher的run方法,调用get方法,重新进行渲染。也就是当数据进行修改的时候,为什么需要nextTick进行异步操作才能拿到数据?当data中的数据初始化的时候,劫持这个数据,使用get和set方法,触发数据的get方法,页面进行渲染,数据初始化完成之后,编译模板,将模板中的数据和页面进行绑定。get只要触发了,当前组件更新watcher就会被收集到Dep里面【观察者模式】

如果我们修改了data里面的数据,会触发set方法,set方法触发dep.notify(),depnotify()会触发watcher的updata方法。就把收集到所有的依赖【更新watcher】,通过nextTick进行异步更新,清空队列,一次执行watcher的run方法,调用get方法,重新进行页面的渲染。

nextTick的异步根据宿主环境进行何种异步的更新判断。promise,mutationObserver,setImmediate,setTimeout.

步骤详解

 new vue之后传入一个_init方法,使用initMixin(vue),把_init方法挂载到vue的原型上

initMixin(Vue); // 添加原型的方法

 

posted @ 2023-03-26 15:16  帅气丶汪星人  阅读(112)  评论(0)    收藏  举报