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); // 添加原型的方法

浙公网安备 33010602011771号