diff --git a/README.md b/README.md index a8a2edd..30a926b 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,1296 @@ + ***English document***: https://github.com/ygs-code/vue/blob/master/README_EN.md +# 开始 + vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了 。这个vue源码逐行分析,我基本每一行都打上注释,加上整个框架的流程思维导图,基本上是小白也能看懂的vue源码了。 + 说的非常的详细,里面的源码注释,有些是自己多年开发vue经验而获得的,有些是自己跑上下文程序知道的, 如果有不足的地方可以联系我QQ群 :302817612 修改,或者发邮件给我281113270@qq.com 谢谢。 如果大家觉得不错请动动小手指,帮我点一个satr,你们的支持就是我的动力。 +vue 如何去看vue源码呢?其实mvvm源码并没有想象中那么神秘,从12年开始到至今mvvm发展已经有了十几年历史了,从以前直接操作dom的jq发展有十几年历史,但是这十几年历史发展,并没有多大的改变,思想还是那些,模块还是分为几大块: - vue源码业余时间差不多看了一年,以前在网上找帖子,发现很多帖子很零散,都是一部分一部分说,断章的很多,所以自己下定决定一行行看,经过自己坚持与努力,现在基本看完了,差diff那部分,因为考虑到自己要换工作了,所以暂缓下来先,diff那块后期我会补上去。这个vue源码逐行分析,我基本每一行都打上注释,加上整个框架的流程思维导图,基本上是小白也能看懂的vue源码了。 - - 说的非常的详细,里面的源码注释,有些是参考网上帖子的,有些是自己多年开发vue经验而猜测的,有些是自己跑上下文程序知道的,本人水平可能有限,不一定是很正确,如果有不足的地方可以联系我QQ群 :302817612 修改,或者发邮件给我281113270@qq.com 谢谢。 +## 1.模板转换: - 1.vue源码解读流程 1.new Vue 调用的是 Vue.prototype._init 从该函数开始 经过 $options 参数合并之后 initLifecycle 初始化生命周期标志 初始化事件,初始化渲染函数。初始化状态就是数据。把数据添加到观察者中实现双数据绑定。 + 就是我们写的 vue 模板 或者是 react jsx 我们都可以理解是模板,然后他会经过 模板编译转换,像vue的话是进过一个方法paseHTML方法转换成ast树,里面的paseHTML用while 循环模板,然后经过正则 匹配到vue指令,还有vue的属性,事件方法等,收集到一个ast树中。 - 2.双数据绑定原理是:obersve()方法判断value没有没有__ob___属性并且是不是Obersve实例化的, - value是不是Vonde实例化的,如果不是则调用Obersve 去把数据添加到观察者中,为数据添加__ob__属性, Obersve 则调用defineReactive方法,该方法是连接Dep和wacther方法的一个通道,利用Object.definpropty() 中的get和set方法 监听数据。get方法中是new Dep调用depend()。为dep添加一个wacther类,watcher中有个方法是更新视图的是run调用update去更新vonde 然后更新视图。 然后set方法就是调用dep中的notify 方法调用wacther中的run 更新视图 - 3.vue从字符串模板怎么到真实的dom呢?是通过$mount挂载模板,就是获取到html,然后通过paseHTML这个方法转义成ast模板,他大概算法是 while(html) 如果匹配到开始标签,结束标签,或者是属性,都会截取掉html,然后收集到一个对象中,知道循环结束 html被截取完。最后变成一个ast对象,ast对象好了之后,在转义成vonde 需要渲染的函数,比如_c('div' s('')) 等这类的函数,编译成vonde 虚拟dom。然后到updata更新数据 调用__patch__ 把vonde 通过diff算法变成正真正的dom元素。 +## 2.数据相应: + vue是一个双数据相应的框架,底层用的是Object.defineProperty 监听和挟持数据改变,然后调用回调方法更新视图更新。双数据绑定原理是:obersve()方法判断value没有没有__ob___属性并且是不是Obersve实例化的, value是不是Vonde实例化的,如果不是则调用Obersve 去把数据添加到观察者中,为数据添加__ob__属性, Obersve 则调用defineReactive方法,该方法是连接Dep和wacther方法的一个通道,利用Object.definpropty() 中的get和set方法 监听数据。get方法中是new Dep调用depend()。为dep添加一个wacther类,watcher中有个方法是更新视图的是run调用update去更新vonde 然后更新视图。 然后set方法就是调用dep中的notify 方法调用wacther中的run 更新视图 +## 3.虚拟dom: -具体看我源码和流程图,这里文字就不描述这么多了,流程图是下面这中的网盘,源码是vue.js,基本每一行都有注释,然后diff待更新中 + vnode,在vue用vnode是通过 ast对象,在转义成vonde 需要渲染的函数,比如_c('div' s('')) 等这类的函数,编译成vonde 虚拟dom。然后到updata更新数据 调用__patch__ 把vonde 通过diff算法变成正真正的dom元素。 + +## 4.diif算法: + +​ vue2 的diff 算法是深度优先算法遍历,然后对比算法是通过 新旧的vnode对比先对比他们的基本属性,比如key 标签等,如果是相同则通过diff算法对比然后diff算法是新旧的vnode对比,然后有四个指针索引,两个新的vnode开始指针和新的 vnode 结束指针,两个旧的vnode开始指针和旧的 vnode 结束指针。然后先判断vnode是否为空,如果为空就往中间靠拢 开始的指针++ 结束的指针 --。然后两头对比之后,在交叉对比,直到找不到相同的vnode之后如果多出的就删除,如果少的话就新增,然后对比完之后在更新到真实dom。 + + + + + +源码入口流程 vue源码解读流程 1.new Vue 调用的是 Vue.prototype._init 从该函数开始 经过 $options 参数合并之后 initLifecycle 初始化生命周期标志 初始化事件,初始化渲染函数。初始化状态就是数据。把数据添加到观察者中实现双数据绑定。 + +# new Vue实例化程序入口 + +``` + Vue.prototype._init = function (options) { //初始化函数 + //... 省略code + + initLifecycle(vm); //初始化生命周期 标志 + initEvents(vm); //初始化事件 + initRender(vm); // 初始化渲染 + callHook(vm, 'beforeCreate'); //触发beforeCreate钩子函数 + initInjections(vm); // resolve injections before data/props 在数据/道具之前解决注入问题 //初始化 inject + initState(vm); // //初始化状态 + initProvide(vm); // resolve provide after data/props 解决后提供数据/道具 provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,用于组件之间通信。 + callHook(vm, 'created'); //触发created钩子函数 + + + //... 省略code + // 然后挂载模板,这里大概就是把模板转换成ast的入口 + vm.$mount(vm.$options.el); + + } +``` + + + +# 查找和挂载模板 + +​ vm.$mount 进入这个挂载模板方法,判断是否有 render 函数 或者是template,如果没有则使用el.outerHTML , 实际上这里就是要拿到模板的html内容 + +``` + Vue.prototype.$mount = function (el, hydrating) { + //... 省略code + el = el && query(el); //获取dom + if (!options.render) { + if (template) { + + }else if (template.nodeType) { + template = template.innerHTML; + } else if (el) { + template = getOuterHTML(el); + } + } + + + // render 函数 也是 ast 转换 方法 + var ref = compileToFunctions( + template, //模板字符串 + { + shouldDecodeNewlines: shouldDecodeNewlines, //flase //IE在属性值中编码换行,而其他浏览器则不会 + shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, //true chrome在a[href]中编码内容 + delimiters: options.delimiters, //改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}} delimiters: ['${', '}']之后变成这样 ${mgs} + comments: options.comments //当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。 + }, + this + ); + + + + + //... 省略code + //执行$mount方法 用$mount的方法把扩展挂载到dom上 + return mount.call( + this, + el, //真实的dom + hydrating //undefined + ) + + } +``` + + + +# 编译AST和render函数 + +调用 Vue.prototype.$mount 方法之后 拿到模板之后 就会进入以下这几个方法,这几个方法用了很多函数式编程 + +``` +compileToFunctions + +createCompiler + +createCompilerCreator + +baseCompile + +parse + +parseHTML + +``` + +这里比较重点的是parseHTML 他是 while (html) { //循环html 然后 然后经过正则 匹配到vue指令,还有vue的属性,事件方法等,收集到一个ast树中。 + +``` + function parseHTML( + html, //字符串模板 + options //参数 + ) { + var stack = []; // parseHTML 节点标签堆栈 + var expectHTML = options.expectHTML; //true + var isUnaryTag$$1 = options.isUnaryTag || no; //函数匹配标签是否是 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen, link,meta,param,source,track,wbr' + var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; //函数 //判断标签是否是 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source' + var index = 0; + var last, // + lastTag; // + console.log(html) + + + + while (html) { //循环html + last = html; // + // Make sure we're not in a plaintext content element like script/style 确保我们不在像脚本/样式这样的纯文本内容元素中 + if ( + !lastTag || //lastTag 不存在 + !isPlainTextElement(lastTag) // 如果标签不是script,style,textarea + ) { + + var textEnd = html.indexOf('<'); //匹配开始标签或者结束标签的位置 + if (textEnd === 0) { //标识是开始标签 + // Comment: + if (comment.test(html)) { //匹配 开始字符串为'); //获取注释标签的结束位置 + + if (commentEnd >= 0) { //如果注释标签结束标签位置大于0,则有注释内容 + console.log(html.substring(4, commentEnd)) + if (options.shouldKeepComment) { //shouldKeepComment为真时候。获取注释标签内容 + + //截取注释标签的内容 + options.comment(html.substring(4, commentEnd)); + } + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(commentEnd + 3); + continue + } + } + + //这里思路是先匹配到注释节点,在匹配到这里的ie浏览器加载样式节点 + // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment + if (conditionalComment.test(html)) { //匹配开始为 匹配这样动态加ie浏览器的 字符串 + //匹配ie浏览器动态加样式结束符号 + var conditionalEnd = html.indexOf(']>'); + + if (conditionalEnd >= 0) { + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(conditionalEnd + 2); + continue + } + } + + // Doctype: + //匹配html的头文件 + var doctypeMatch = html.match(doctype); + if (doctypeMatch) { + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(doctypeMatch[0].length); + continue + } + + // End tag: + //匹配开头必需是]*> + var endTagMatch = html.match(endTag); + if (endTagMatch) { + + var curIndex = index; + //标签分隔函数 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(endTagMatch[0].length); + console.log(endTagMatch) + console.log(curIndex, index) + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag( + endTagMatch[1], + curIndex, + index + ); + continue + } + + // Start tag: + //解析开始标记 标记开始标签 + // 获取开始标签的名称,属性集合,开始位置和结束位置,并且返回该对象 + var startTagMatch = parseStartTag(); + + if (startTagMatch) { + //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + //为parseHTML 节点标签堆栈 插入一个桟数据 + //调用options.start 为parse函数 stack标签堆栈 添加一个标签 + handleStartTag(startTagMatch); + //匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键 + if (shouldIgnoreFirstNewline(lastTag, html)) { + //去除回车键空格 + advance(1); + } + continue + } + } + + var text = (void 0), + rest = (void 0), + next = (void 0); + if (textEnd >= 0) { + + rest = html.slice(textEnd); //截取字符串 var textEnd = html.indexOf('<'); //匹配开始标签或者结束标签的位置 + console.log(rest) + + while ( + !endTag.test(rest) && //匹配开头必需是]*>)', 'i')); + var rest$1 = html.replace(reStackedTag, function (all, text, endTag) { + endTagLength = endTag.length; + if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { + text = text + .replace(//g, '$1') // #7298 + .replace(//g, '$1'); + } + //匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键 + if (shouldIgnoreFirstNewline(stackedTag, text)) { + text = text.slice(1); + } + if (options.chars) { + options.chars(text); + } + return '' + }); + index += html.length - rest$1.length; + html = rest$1; + parseEndTag(stackedTag, index - endTagLength, index); + } + + if (html === last) { + options.chars && options.chars(html); + if ("development" !== 'production' && !stack.length && options.warn) { + options.warn(("Mal-formatted tag at end of template: \"" + html + "\"")); + } + break + } + } + + + + + + + // Clean up any remaining tags + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(); + //while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + function advance(n) { + index += n; //让索引叠加 + html = html.substring(n); //截取当前索引 和 后面的字符串。 + } + + //获取开始标签的名称,收集属性集合,开始位置和结束位置,并且返回该对象 + function parseStartTag() { + var start = html.match(startTagOpen); //匹配开始标签 匹配开头必需是< 后面可以忽略是任何字符串 ^<((?:[a-zA-Z_][\\w\\-\\.]*\\:)?[a-zA-Z_][\\w\\-\\.]*) + console.log(start) + console.log(start[0].length) + + if (start) { + var match = { + tagName: start[1], //标签名称 + attrs: [], //标签属性集合 + start: index //标签的开始索引 + }; + //标记开始标签的位置,截取了开始标签 + advance(start[0].length); + var end, attr; + + while ( + !(end = html.match(startTagClose)) //没有到 关闭标签 > 标签 + && (attr = html.match(attribute)) //收集属性 + ) { + console.log(html) + //截取属性标签 + advance(attr[0].length); + match.attrs.push(attr); //把属性收集到一个集合 + } + if (end) { + match.unarySlash = end[1]; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + console.log(end) + + //截取掉开始标签,并且更新索引 + advance(end[0].length); + match.end = index; //开始标签的结束位置 + return match + } + } + } + + //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + //为parseHTML 节点标签堆栈 插入一个桟数据 + //调用options.start 为parse函数 stack标签堆栈 添加一个标签 + function handleStartTag(match) { + /* + * match = { + tagName: start[1], //标签名称 + attrs: [], //标签属性集合 + start: index, //开始标签的开始索引 + match:index , //开始标签的 结束位置 + unarySlash:'' //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + }; + * */ + + var tagName = match.tagName; //开始标签名称 + var unarySlash = match.unarySlash; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + console.log(expectHTML) + console.log('lastTag==') + console.log(lastTag) + console.log(tagName) + + if (expectHTML) { //true + + if ( + lastTag === 'p' //上一个标签是p + /* + 判断标签是否是 + 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' + + 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' + + 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' + + 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' + + 'title,tr,track' + */ + && isNonPhrasingTag(tagName) + ) { + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(lastTag); + } + if ( + canBeLeftOpenTag$$1(tagName) && //判断标签是否是 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source' + lastTag === tagName //上一个标签和现在标签相同
  • 编译成
  • 但是这种情况是不会出现的 因为浏览器解析的时候会自动补全如果是
  • 我是li标签
  • 浏览器自动解析成
  • 我是li标签
  • + ) { + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(tagName); + } + } + + var unary = isUnaryTag$$1(tagName) || //函数匹配标签是否是 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen, link,meta,param,source,track,wbr' + !!unarySlash; //如果是/> 则为真 + + var l = match.attrs.length; + var attrs = new Array(l); //数组属性对象转换正真正的数组对象 + for (var i = 0; i < l; i++) { + var args = match.attrs[i]; //获取属性对象 + // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 + //对FF bug进行黑客攻击:https://bugzilla.mozilla.org/show_bug.cgi?id=369778 + if ( + IS_REGEX_CAPTURING_BROKEN && //这个应该是 火狐浏览器私有 标志 + args[0].indexOf('""') === -1 + ) { + if (args[3] === '') { + delete args[3]; + } + if (args[4] === '') { + delete args[4]; + } + if (args[5] === '') { + delete args[5]; + } + } + var value = args[3] || args[4] || args[5] || ''; + var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' + ? options.shouldDecodeNewlinesForHref // true chrome在a[href]中编码内容 + : options.shouldDecodeNewlines; //flase //IE在属性值中编码换行,而其他浏览器则不会 + + attrs[i] = { //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + name: args[1], //属性名称 + //属性值 + value: decodeAttr(value, shouldDecodeNewlines) //替换html 中的特殊符号,转义成js解析的字符串,替换 把 <替换 < , > 替换 > , "替换 ", &替换 & , 替换\n , 替换\t + + }; + + } + + console.log('==!unary==') + console.log(!unary) + + if (!unary) { //如果不是单标签 + + // 为parseHTML 节点标签堆栈 插入一个桟数据 + stack.push({ //标签堆栈 + tag: tagName, //开始标签名称 + lowerCasedTag: tagName.toLowerCase(), //变成小写记录标签 + attrs: attrs //获取属性 + }); + //设置结束标签 + lastTag = tagName; + console.log('== parseHTML handleStartTag stack==') + console.log(stack) + + } + + + // + if (options.start) { + + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, //标签名称 + attrs, //标签属性 + unary, // 如果不是单标签则为真 + match.start, //开始标签的开始位置 + match.end //开始标签的结束的位置 + ); + } + + + } + + + + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + function parseEndTag( + tagName, //标签名称 + start, //结束标签开始位置 + end //结束标签结束位置 + ) { + var pos, + lowerCasedTagName; + if (start == null) { //如果没有传开始位置 + start = index; //就那当前索引 + } + if (end == null) { //如果没有传结束位置 + end = index; //就那当前索引 + } + + if (tagName) { //结束标签名称 + lowerCasedTagName = tagName.toLowerCase(); //将字符串转化成小写 + } + + // Find the closest opened tag of the same type 查找最近打开的相同类型的标记 + if (tagName) { + // 获取stack堆栈最近的匹配标签 + for (pos = stack.length - 1; pos >= 0; pos--) { + //找到最近的标签相等 + if (stack[pos].lowerCasedTag === lowerCasedTagName) { + break + } + } + } else { + // If no tag name is provided, clean shop + //如果没有提供标签名称,请清理商店 + pos = 0; + } + + + if (pos >= 0) { //这里就获取到了stack堆栈的pos索引 + // Close all the open elements, up the stack 关闭所有打开的元素,向上堆栈 + console.log(pos) + + for (var i = stack.length - 1; i >= pos; i--) { + + if ("development" !== 'production' && //如果stack中找不到tagName 标签的时候就输出警告日志,找不到标签 + (i > pos || !tagName) && + options.warn + ) { + options.warn( + ("tag <" + (stack[i].tag) + "> has no matching end tag.") + ); + } + if (options.end) { + console.log(options.end) + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + options.end( + stack[i].tag,//结束标签名称 + start, //结束标签开始位置 + end //结束标签结束位置 + ); + } + } + // Remove the open elements from the stack + //从堆栈中删除打开的元素 + // console.log(stack[pos].tag) + // 为parseHTML 节点标签堆栈 出桟当前匹配到的标签 + stack.length = pos; + //获取到上一个标签,就是当前节点的父节点 + lastTag = pos && stack[pos - 1].tag; + console.log(stack) + console.log(lastTag) + + + + + } else if (lowerCasedTagName === 'br') { + if (options.start) { + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, + [], true, + start, + end + ); + } + } else if (lowerCasedTagName === 'p') { + if (options.start) { + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, + [], false, + start, + end); + } + if (options.end) { + //删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + options.end( + tagName, + start, + end + ); + } + } + console.log(lastTag) + + } + } +``` + + + +一些匹配模板正则 + +``` + var onRE = /^@|^v-on:/;//判断是否是 @或者v-on:属性开头的 + var dirRE = /^v-|^@|^:/; //判断是否是 v-或者@或者: 属性开头的 + var forAliasRE = /([^]*?)\s+(?:in|of)\s+([^]*)/; //匹配 含有 字符串 in 字符串 或者 字符串 of 字符串 + var forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/; //匹配上, 但是属于两边是 [{ , 点 , }] 所以匹配上 ,+字符串 + var stripParensRE = /^\(|\)$/g; //匹配括号 () + + var argRE = /:(.*)$/; //匹配字符串是否含有: + var bindRE = /^:|^v-bind:/; //开始匹配是 :或者是v-bind + var modifierRE = /\.[^.]+/g; // 匹配以点开头的分组 不属于点 data.object.info.age 匹配到 ['.object','.info' , '.age'] + + var decodeHTMLCached = cached(he.decode); //获取 真是dom的textContent文本 +``` + + + +## 双数据响应 + + 双数据绑定 入口 方法在defineReactive函数中 ,不管是 prop 还是 state 还是 属性监听方法 set 方法,还是initInjections 入口都是这里。 + +首先他会实例化 var dep = new Dep(); 依赖收集 Dep,get方法会添加一个 + +​ //添加一个dep +​ dep.depend(); + +​ if (childOb) { //如果子节点存在也添加一个dep +​ childOb.dep.depend(); +​ if (Array.isArray(value)) { //判断是否是数组 如果是数组 +​ dependArray(value); //则数组也添加dep +​ } +​ } + + + +set 方法是触发更新视图的 + +//observe 添加 观察者 + + // 然后在添加依赖 + + childOb = !shallow && observe(newVal); + //更新数据 + dep.notify(); + + + +``` + /** + * Define a reactive property on an Object. + * 在对象上定义一个无功属性。 + * 更新数据 + * 通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改 + * 添加观察者 get set方法 + */ + function defineReactive(obj, //对象 + key,//对象的key + val, //监听的数据 返回的数据 + customSetter, // 日志函数 + shallow //是否要添加__ob__ 属性 + ) { + //实例化一个主题对象,对象中有空的观察者列表 + var dep = new Dep(); + //获取描述属性 + var property = Object.getOwnPropertyDescriptor(obj, key); + var _property = Object.getOwnPropertyNames(obj); //获取实力对象属性或者方法,包括定义的描述属性 + console.log(property); + console.log(_property); + + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + + var getter = property && property.get; + console.log('arguments.length=' + arguments.length) + + if (!getter && arguments.length === 2) { + val = obj[key]; + } + var setter = property && property.set; + console.log(val) + //判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性递归把val添加到观察者中 返回 new Observer 实例化的对象 + var childOb = !shallow && observe(val); + //定义描述 + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + + var value = getter ? getter.call(obj) : val; + if (Dep.target) { //Dep.target 静态标志 标志了Dep添加了Watcher 实例化的对象 + //添加一个dep + dep.depend(); + if (childOb) { //如果子节点存在也添加一个dep + childOb.dep.depend(); + if (Array.isArray(value)) { //判断是否是数组 如果是数组 + dependArray(value); //则数组也添加dep + } + } + } + return value + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare 新旧值比较 如果是一样则不执行了*/ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare + * 不是生产环境的情况下 + * */ + if ("development" !== 'production' && customSetter) { + customSetter(); + } + if (setter) { + //set 方法 设置新的值 + setter.call(obj, newVal); + } else { + //新的值直接给他 + val = newVal; + } + console.log(newVal) + + //observe 添加 观察者 + childOb = !shallow && observe(newVal); + //更新数据 + dep.notify(); + } + }); + } + +``` + + + + + + + +## 依赖收集 Dep + + 在vue数据get获取中,谁读取了该数据,就把它收集起来,所以dep是一个集合,在数据set时,通过遍历dep去触发每个dep的notify方法通过视图更新 +dep的主要功能是只作为收集,那在收集了依赖后,如何使视图更新呢 +所以需要定义一个新的Watcher类,改类是会实现对视图的更新 + dep每收集的一个依赖实际就是一个Watcher + +``` + //主题对象Dep构造函数 主要用于添加发布事件后,用户更新数据的 响应式原理之一函数 + var Dep = function Dep() { + //uid 初始化为0 + this.id = uid++; + /* 用来存放Watcher对象的数组 */ + this.subs = []; + }; + + Dep.prototype.addSub = function addSub(sub) { + /* 在subs中添加一个Watcher对象 */ + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub(sub) { + /*删除 在subs中添加一个Watcher对象 */ + remove(this.subs, sub); + }; + //this$1.deps[i].depend(); + //为Watcher 添加 为Watcher.newDeps.push(dep); 一个dep对象 + Dep.prototype.depend = function depend() { + //添加一个dep target 是Watcher dep就是dep对象 + if (Dep.target) { + //像指令添加依赖项 + Dep.target.addDep(this); + } + }; + /* 通知所有Watcher对象更新视图 */ + Dep.prototype.notify = function notify() { + // stabilize the subscriber list first + var subs = this.subs.slice(); + for (var i = 0, l = subs.length; i < l; i++) { + //更新数据 + subs[i].update(); + } + }; + + // the current target watcher being evaluated. + // this is globally unique because there could be only one + // watcher being evaluated at any time. + //当前正在评估的目标监视程序。 + //这在全球是独一无二的,因为只有一个 + //观察者在任何时候都被评估。 + Dep.target = null; + var targetStack = []; + + function pushTarget(_target) { + //target 是Watcher dep就是dep对象 + if (Dep.target) { //静态标志 Dep当前是否有添加了target + //添加一个pushTarget + targetStack.push(Dep.target); + } + Dep.target = _target; + } + + // + function popTarget() { + // 出盏一个pushTarget + Dep.target = targetStack.pop(); + } +``` + +## 数据检测 Watcher + + Watcher的功能主要是接口到Dep的通知,然后调用update方法更新视图 +在update方法中会触发回调,回调函数实际就是已生成render函数 + +在调用render函数是,函数里的值就会获取到已经更改后值,所以就会生成新的vnode +新的vnode生成后,就是patch的过程,用新的vnode与旧的vnode进行比对,最终将比对后的vnode转换为实际的dom添加到模板挂载节点上 +新的模板挂载后,将旧的模板删除,这样视图就更新完成 + +``` + * *观察者分析表达式,收集依赖项, + *并在表达式值更改时触发回调。 + *这用于$watch() api和指令。 + * 当前vue实例、updateComponent函数、空函数。 + */ + var Watcher = function Watcher( + vm, //vm dom + expOrFn, //获取值的函数,或者是更新viwe试图函数 + cb, //回调函数,回调值给回调函数 + options, //参数 + isRenderWatcher//是否渲染过得观察者 + ) { + console.log('====Watcher====') + this.vm = vm; + //是否是已经渲染过得观察者 + if (isRenderWatcher) { //把当前 Watcher 对象赋值给 vm._watcher上 + vm._watcher = this; + } + //把观察者添加到队列里面 当前Watcher添加到vue实例上 + vm._watchers.push(this); + // options + if (options) { //如果有参数 + this.deep = !!options.deep; //实际 + this.user = !!options.user; //用户 + this.lazy = !!options.lazy; //懒惰 ssr 渲染 + this.sync = !!options.sync; //如果是同步 + } else { + + this.deep = this.user = this.lazy = this.sync = false; + } + this.cb = cb; //回调函数 + this.id = ++uid$1; // uid for batching uid为批处理 监听者id + this.active = true; //激活 + this.dirty = this.lazy; // for lazy watchers 对于懒惰的观察者 + this.deps = []; // 观察者队列 + this.newDeps = []; // 新的观察者队列 + // 内容不可重复的数组对象 + this.depIds = new _Set(); + this.newDepIds = new _Set(); + // 把函数变成字符串形式 + this.expression = expOrFn.toString(); + // parse expression for getter + //getter的解析表达式 + if (typeof expOrFn === 'function') { + //获取值的函数 + this.getter = expOrFn; + } else { + //如果是keepAlive 组件则会走这里 + //path 因该是路由地址 + if (bailRE.test(path)) { // 匹配上 返回 true var bailRE = /[^\w.$]/; //匹配不是 数字字母下划线 $符号 开头的为true + return + } + + // //匹配不上 path在已点分割 + // var segments = path.split('.'); + // return function (obj) { + // + // for (var i = 0; i < segments.length; i++) { + // //如果有参数则返回真 + // if (!obj) { + // return + // } + // //将对象中的一个key值 赋值给该对象 相当于 segments 以点拆分的数组做obj 的key + // obj = obj[segments[i]]; + // } + // //否则返回一个对象 + // return obj + // } + + //匹配不是 数字字母下划线 $符号 开头的为true + + this.getter = parsePath(expOrFn); + if (!this.getter) { //如果不存在 则给一个空的数组 + this.getter = function () { + }; + "development" !== 'production' && warn( + "Failed watching path: \"" + expOrFn + "\" " + + 'Watcher only accepts simple dot-delimited paths. ' + + 'For full control, use a function instead.', + vm + ); + } + } + this.value = this.lazy ? // lazy为真的的时候才能获取值 这个有是组件才为真 + undefined : + this.get(); //计算getter,并重新收集依赖项。 获取值 + }; + +``` + +在Watcher实例构造函数执行时,会触发get +触发了get后就会该Watcher实例进行收集 +update为接到Dep通知时触发的方法 +update内会调用run方法 +在run方法内会调用cb回调方法 +cb回到方法实际就是模板编译时render方法 + + + + + +# 虚拟DOM + +vue中的虚拟DOM,实际就是通过定义一个Vnode类,在该类上添加了dom的一些属性来标识一个dom + +主要的作用是降低对实际dom的操作,来减轻对浏览器性能的耗费 + +``` + /* + * 创建标准的vue vnode + * + * */ + + var VNode = function VNode( + tag, /*当前节点的标签名*/ + data, /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ + children, //子节点 + text, //文本 + elm, /*当前节点的dom */ + context, /*编译作用域*/ + componentOptions, /*组件的option选项*/ + asyncFactory/*异步工厂*/) { + /*当前节点的标签名*/ + this.tag = tag; + + /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ + this.data = data; + + /*当前节点的子节点,是一个数组*/ + this.children = children; + + /*当前节点的文本*/ + this.text = text; + + /*当前虚拟节点对应的真实dom节点*/ + this.elm = elm; + + /*当前节点的名字空间*/ + this.ns = undefined; + + /*编译作用域 vm*/ + this.context = context; + + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + + /*节点的key属性,被当作节点的标志,用以优化*/ + this.key = data && data.key; + + /*组件的option选项*/ + this.componentOptions = componentOptions; + + /*当前节点对应的组件的实例*/ + this.componentInstance = undefined; + + /*当前节点的父节点*/ + this.parent = undefined; + + /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/ + this.raw = false; + + /*静态节点标志*/ + this.isStatic = false; + + /*是否作为跟节点插入*/ + this.isRootInsert = true; + + /*是否为注释节点*/ + this.isComment = false; + + /*是否为克隆节点*/ + this.isCloned = false; + + /*是否有v-once指令*/ + this.isOnce = false; + + /*异步工厂*/ + this.asyncFactory = asyncFactory; + + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; +``` + +# diff算法 + +patch ,sameVnode, patchVnode ,updateChildren 这几个方法 + +入口是patch 然后调用sameVnode + +``` + //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + function sameVnode(a, b) { + return ( + + a.key === b.key && ( //如果a的key 等于b的key + ( + + a.tag === b.tag && // 如果a的tag 等于b的tag + a.isComment === b.isComment && // 如果a和b 都是注释节点 + isDef(a.data) === isDef(b.data) && //如果a.data 和 b.data 都定义后,是组件,或者是都含有tag属性 + sameInputType(a, b) //相同的输入类型。判断a和b的属性是否相同 + ) || ( + isTrue(a.isAsyncPlaceholder) && //判断是否是异步的 + a.asyncFactory === b.asyncFactory && + isUndef(b.asyncFactory.error) + ) + ) + ) + } +``` + +如果调用sameVnode 条件成立 则进入patchVnode 方法, + +patchVnode 方法主要是对vnode 进行增加和删除,主要还有key更新等。然后 判断 两个虚拟dom都不为空,并且他们不相等的时候oldCh !== ch 就进入updateChildren diff更新算法。 + +``` + // 对比 虚拟dom + function patchVnode( + oldVnode, // 旧的虚拟dom + vnode, // 新的虚拟dom + insertedVnodeQueue, // 删除虚拟dom队列 + removeOnly + ) { + if (oldVnode === vnode) { //如果他们相等 + return + } + + var elm = vnode.elm = oldVnode.elm; //获取真实的dom + + // 判断是否有isAsyncPlaceholder 属性 + if (isTrue(oldVnode.isAsyncPlaceholder)) { + //判断数据 是否不等于 undefined或者null + if (isDef(vnode.asyncFactory.resolved)) { + // ssr 渲染 + hydrate(oldVnode.elm, vnode, insertedVnodeQueue); + } else { + vnode.isAsyncPlaceholder = true; + } + return + } + + // reuse element for static trees. + // note we only do this if the vnode is cloned - + // if the new node is not cloned it means the render functions have been + // reset by the hot-reload-api and we need to do a proper re-render. + //为静态树重用元素。 + //注意,只有当vnode被克隆时,我们才这样做 + //如果新节点没有克隆,则表示渲染函数已经克隆 + //由hot-reload api重置,我们需要做一个适当的重新渲染。 + if (isTrue(vnode.isStatic) && + isTrue(oldVnode.isStatic) && + vnode.key === oldVnode.key && + (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) + ) { + vnode.componentInstance = oldVnode.componentInstance; + return + } + + var i; + var data = vnode.data; + // 钩子函数 + if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { + i(oldVnode, vnode); + } + + var oldCh = oldVnode.children; + var ch = vnode.children; + //循环组件实例 是否定义有 tag标签 + if (isDef(data) && isPatchable(vnode)) { + // 触发钩子函数 更新钩子函数 + for (i = 0; i < cbs.update.length; ++i) { + cbs.update[i](oldVnode, vnode); + } + // 触发钩子函数 + if (isDef(i = data.hook) && isDef(i = i.update)) { + i(oldVnode, vnode); + } + } + + //如果是文本虚拟dom + if (isUndef(vnode.text)) { + // 两个虚拟dom都存在 + if (isDef(oldCh) && isDef(ch)) { + // 如果他们不相等 + if (oldCh !== ch) { + // diff算法更新 + updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); + } + } else if (isDef(ch)) { // 如果是有新的虚拟dom + // 如果是文本虚拟dom 则 设置 空 + if (isDef(oldVnode.text)) { + nodeOps.setTextContent(elm, ''); + } + // 添加 vnode + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { // 如果旧的有 新的虚拟dom没有则删除 虚拟dom + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } else if (isDef(oldVnode.text)) { // 如果是文本虚拟dom则设置文本 + nodeOps.setTextContent(elm, ''); + } + + + } else if (oldVnode.text !== vnode.text) { + // 如果新旧的文本不相同则设置文本 + nodeOps.setTextContent(elm, vnode.text); + } + if (isDef(data)) { + // 触发钩子 + if (isDef(i = data.hook) && isDef(i = i.postpatch)) { + i(oldVnode, vnode); + } + } + } + +``` + + + +# ddif 算法updateChildren + +diif算法,vue2 的diff 算法是深度优先算法遍历,然后对比算法是通过 新旧的vnode对比先对比他们的基本属性,比如key 标签等,如果是相同则通过diff算法对比然后diff算法是新旧的vnode对比,然后有四个指针索引,两个新的vnode开始指针和新的 vnode 结束指针,两个旧的vnode开始指针和旧的 vnode 结束指针。然后先判断vnode是否为空,如果为空就往中间靠拢 开始的指针++ 结束的指针 --。然后两头对比之后,在交叉对比,直到找不到相同的vnode之后如果多出的就删除,如果少的话就新增,然后对比完之后 在调用patchVnode去增删虚拟dom。然后如果有vnode不相同在调用updateChildren,这样就做到深层递归,也叫深度优先搜索,然后子vnode没有了在更新到真实dom。 + +``` + + // ddif 算法 + function updateChildren( + parentElm, // 父亲dom + oldCh, // 旧的虚拟dom + newCh, // 新的虚拟dom + insertedVnodeQueue, + removeOnly + ) { + var oldStartIdx = 0; // 旧的虚拟dom开始指针 + var newStartIdx = 0; // 新的虚拟dom开始指针 + var oldEndIdx = oldCh.length - 1; // 旧的虚拟dom结束指针 + var newEndIdx = newCh.length - 1;// 新的虚拟dom结束指针 + + var oldStartVnode = oldCh[0]; // 旧的虚拟dom开始节点 + var newStartVnode = newCh[0]; // 新的虚拟dom开始节点 + + var oldEndVnode = oldCh[oldEndIdx]; // 旧的虚拟dom结束节点 + var newEndVnode = newCh[newEndIdx];// 新的虚拟dom结束节点 + + var oldKeyToIdx, idxInOld, vnodeToMove, refElm; + + // removeOnly is a special flag used only by + // to ensure removed elements stay in correct relative positions + // during leaving transitions + var canMove = !removeOnly; + + { + // 检查同一个兄弟节点是否有重复的key,如果有则发出警告日志 + checkDuplicateKeys(newCh); + } + + /* + diff 算法开始 + 这里diff算法其实就是 + + */ + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + + if (isUndef(oldStartVnode)) { + // 如果旧的开始节点不存在或者为空 + // 如果旧的开始节点指针往中间偏移 + oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left + } else if (isUndef(oldEndVnode)) { + // 如果旧的结束节点不存在或者为空 + // 如果旧的结束节点指针往中间偏移 + oldEndVnode = oldCh[--oldEndIdx]; + + } else if (sameVnode(oldStartVnode, newStartVnode)) { //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + + + + // 在对比下虚拟dom + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + + //开始指针 两个都往中间偏移 + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + + } else if (sameVnode(oldEndVnode, newEndVnode)) { //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + // 在对比下虚拟dom + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + // 结束指针 两个都往中间偏移 + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + + // 交叉对比 深度优先算法入口 + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + // 交叉对比 + canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)); + + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left + // 交叉对比 + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + // 交叉对比 + canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + // 如果没有key 则给塔新的key + if (isUndef(oldKeyToIdx)) { + + // 创建key 如果没有key 则用索引作为key + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + + // 获取 旧的vnode key + idxInOld = isDef(newStartVnode.key) + ? oldKeyToIdx[newStartVnode.key] + // 查找旧的vnode key + : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); + // 如果旧的 vnode key 未定义则创建新的真实dom + if (isUndef(idxInOld)) { // New element + //创建真实 dom 节点 + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } else { + + vnodeToMove = oldCh[idxInOld]; + if (sameVnode(vnodeToMove, newStartVnode)) { + // 对比虚拟dom + patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue); + + oldCh[idxInOld] = undefined; + // 真实节点交换 + canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); + } else { + // same key but different element. treat as new element + // 创建真实dom + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx > oldEndIdx) { + refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; + // 添加虚拟dom + addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else if (newStartIdx > newEndIdx) { + // 删除虚拟dom + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + +``` + +具体看我源码和流程图,这里文字就不描述这么多了,流程图是下面这中的网盘,源码是vue.js,基本每一行都有注释 链接:https://pan.baidu.com/s/10IxV6mQ2TIwkRACKu2T0ng 提取码:1fnu - -上面的vue.js 就是我基于vue源码中每行加有注释的vue.js, 其他文件就是我看vue.js源码的时候抽出来的vue.js 源码小demo + +上面的vue.js 就是我基于vue源码中每行加有注释的vue.js, 其他文件就是我看vue.js源码的时候抽出来的vue.js 源码小demo。如果大家觉得不错请动动小手指,帮我点一个satr,你们的支持就是我的动力 - - + + 作者:姚观寿 diff --git a/README_EN.md b/README_EN.md new file mode 100644 index 0000000..a622c8e --- /dev/null +++ b/README_EN.md @@ -0,0 +1,1296 @@ +# begin + + vue source code spare time to see almost a year, before looking for posts on the Internet, found that many posts are very scattered, are part of a part said, a lot of chapters, so they decided to see line by line, after their own persistence and efforts, now basically read. This vue source line by line analysis, I basically every line on the annotation, plus the whole framework of the process mind map, is basically a small white can also understand the vue source code. + +Said very detailed, inside the source code notes, some are their own years of experience in developing vue, some are their own context program to know, if there are shortcomings can contact me QQ group: 302817612 modification, or send an email to me 281113270@qq.com thank you. If you feel good, please move your little finger to help me click a satr, your support is my motivation. + +vue How to see vue source code? In fact, mvvm source code is not as mysterious as imagined, from the beginning of 12 years to the present mvvm development has more than a decade of history, from the previous direct operation of the dom jq development has more than a decade of history, but this decade of historical development, and there is not much change, the idea is still those, the module is still divided into several chunks: + +## 1. Template conversion: + +Is we write a vue template or react jsx we can understand is a template, and then it will go through the template compilation conversion, like vue is into a method paseHTML method converted into the ast tree, paseHTML inside the while loop template, Then through the RE match to the vue instructions, as well as vue properties, event methods, etc., collected into an ast tree. + +## 2. Corresponding data: + +vue is a dual data corresponding framework, the underlying use is Object.defineProperty to listen for and hijack data changes, and then call callback methods to update the view update. The principle of dual data binding is as follows: The obersve() method determines whether value has no __ob___ attribute and is not Obersve instantiated, and whether value is Vonde instantiated. If not, it calls Obersve to add the data to the observer and add the __ob__ attribute to the data. Obersve calls the defineReactive method, which is a channel connecting the Dep and wacther methods, and listens for data using the get and set methods in Object.definpropty(). In the get method, new Dep calls depend(). To add a wacther class to dep, watcher has a method to update the view. run calls update to update the vonde and then updates the view. Then the set method is to call the notify method in dep to call the run update view in wacther + +## 3. Virtual dom: + + + + + +vnode, used in vue, is via ast objects, escaped into vonde needs to render functions, such as _c('div' s('')) and such functions, compiled into vonde virtual dom. Then update the data to updata and call __patch__ to turn vonde into a true dom element through diff algorithm. + +## 4.diif algorithm: + +​ The diff algorithm of vue2 is depth-first traversal, and then the comparison algorithm compares the old vnode with the new vnode, first compares their basic attributes, such as key labels, etc. If they are the same, the diff algorithm compares the old Vnode with the new Vnode, and then there are four pointer indexes. Two new vnode start Pointers and two new vnode end Pointers, two old vnode start Pointers and old vnode end Pointers. Then first determine whether the vnode is empty, if it is empty, move to the center of the start pointer ++ end pointer --. Then after comparing the two sides, cross-compare until you can't find the same vnode, if there are more, delete it, if there are fewer, add it, and then update it to the real dom after comparing. + + + +new Vue calls vue.prototype. _init. From this function, after merging with the $options parameter, initLifecycle initializes the life cycle, marking the initialization event, and initializing the rendering function. The initialization state is the data. Add data to the observer for double data binding. + +# new Vue instantiates the program entry + +``` + Vue.prototype._init = function (options) { //初始化函数 + //... 省略code + + initLifecycle(vm); //初始化生命周期 标志 + initEvents(vm); //初始化事件 + initRender(vm); // 初始化渲染 + callHook(vm, 'beforeCreate'); //触发beforeCreate钩子函数 + initInjections(vm); // resolve injections before data/props 在数据/道具之前解决注入问题 //初始化 inject + initState(vm); // //初始化状态 + initProvide(vm); // resolve provide after data/props 解决后提供数据/道具 provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性,用于组件之间通信。 + callHook(vm, 'created'); //触发created钩子函数 + + + //... 省略code + // 然后挂载模板,这里大概就是把模板转换成ast的入口 + vm.$mount(vm.$options.el); + + } +``` + + + +# Find and mount templates + + vm.$mount goes to the mount template method and determines whether it has a render function or a template, and if not, uses el.outerHTML, which is essentially getting the html content of the template + +``` + Vue.prototype.$mount = function (el, hydrating) { + //... 省略code + el = el && query(el); //获取dom + if (!options.render) { + if (template) { + + }else if (template.nodeType) { + template = template.innerHTML; + } else if (el) { + template = getOuterHTML(el); + } + } + + + // render 函数 也是 ast 转换 方法 + var ref = compileToFunctions( + template, //模板字符串 + { + shouldDecodeNewlines: shouldDecodeNewlines, //flase //IE在属性值中编码换行,而其他浏览器则不会 + shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref, //true chrome在a[href]中编码内容 + delimiters: options.delimiters, //改变纯文本插入分隔符。修改指令的书写风格,比如默认是{{mgs}} delimiters: ['${', '}']之后变成这样 ${mgs} + comments: options.comments //当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。 + }, + this + ); + + + + + //... 省略code + //执行$mount方法 用$mount的方法把扩展挂载到dom上 + return mount.call( + this, + el, //真实的dom + hydrating //undefined + ) + + } +``` + + + +# Compile the AST and render functions + +After you call the Vue.prototype.$mount method and get the template, you enter the following methods, which use a lot of functional programming + +``` +compileToFunctions + +createCompiler + +createCompilerCreator + +baseCompile + +parse + +parseHTML + +``` + +The important thing here is that parseHTML is a while (html) {// loop through the html and then through the re match to the vue directive, as well as vue properties, event methods, etc., collected into an ast tree. + +``` + function parseHTML( + html, //字符串模板 + options //参数 + ) { + var stack = []; // parseHTML 节点标签堆栈 + var expectHTML = options.expectHTML; //true + var isUnaryTag$$1 = options.isUnaryTag || no; //函数匹配标签是否是 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen, link,meta,param,source,track,wbr' + var canBeLeftOpenTag$$1 = options.canBeLeftOpenTag || no; //函数 //判断标签是否是 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source' + var index = 0; + var last, // + lastTag; // + console.log(html) + + + + while (html) { //循环html + last = html; // + // Make sure we're not in a plaintext content element like script/style 确保我们不在像脚本/样式这样的纯文本内容元素中 + if ( + !lastTag || //lastTag 不存在 + !isPlainTextElement(lastTag) // 如果标签不是script,style,textarea + ) { + + var textEnd = html.indexOf('<'); //匹配开始标签或者结束标签的位置 + if (textEnd === 0) { //标识是开始标签 + // Comment: + if (comment.test(html)) { //匹配 开始字符串为'); //获取注释标签的结束位置 + + if (commentEnd >= 0) { //如果注释标签结束标签位置大于0,则有注释内容 + console.log(html.substring(4, commentEnd)) + if (options.shouldKeepComment) { //shouldKeepComment为真时候。获取注释标签内容 + + //截取注释标签的内容 + options.comment(html.substring(4, commentEnd)); + } + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(commentEnd + 3); + continue + } + } + + //这里思路是先匹配到注释节点,在匹配到这里的ie浏览器加载样式节点 + // http://en.wikipedia.org/wiki/Conditional_comment#Downlevel-revealed_conditional_comment + if (conditionalComment.test(html)) { //匹配开始为 匹配这样动态加ie浏览器的 字符串 + //匹配ie浏览器动态加样式结束符号 + var conditionalEnd = html.indexOf(']>'); + + if (conditionalEnd >= 0) { + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(conditionalEnd + 2); + continue + } + } + + // Doctype: + //匹配html的头文件 + var doctypeMatch = html.match(doctype); + if (doctypeMatch) { + //截取字符串重新循环 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(doctypeMatch[0].length); + continue + } + + // End tag: + //匹配开头必需是]*> + var endTagMatch = html.match(endTag); + if (endTagMatch) { + + var curIndex = index; + //标签分隔函数 while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + advance(endTagMatch[0].length); + console.log(endTagMatch) + console.log(curIndex, index) + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag( + endTagMatch[1], + curIndex, + index + ); + continue + } + + // Start tag: + //解析开始标记 标记开始标签 + // 获取开始标签的名称,属性集合,开始位置和结束位置,并且返回该对象 + var startTagMatch = parseStartTag(); + + if (startTagMatch) { + //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + //为parseHTML 节点标签堆栈 插入一个桟数据 + //调用options.start 为parse函数 stack标签堆栈 添加一个标签 + handleStartTag(startTagMatch); + //匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键 + if (shouldIgnoreFirstNewline(lastTag, html)) { + //去除回车键空格 + advance(1); + } + continue + } + } + + var text = (void 0), + rest = (void 0), + next = (void 0); + if (textEnd >= 0) { + + rest = html.slice(textEnd); //截取字符串 var textEnd = html.indexOf('<'); //匹配开始标签或者结束标签的位置 + console.log(rest) + + while ( + !endTag.test(rest) && //匹配开头必需是]*>)', 'i')); + var rest$1 = html.replace(reStackedTag, function (all, text, endTag) { + endTagLength = endTag.length; + if (!isPlainTextElement(stackedTag) && stackedTag !== 'noscript') { + text = text + .replace(//g, '$1') // #7298 + .replace(//g, '$1'); + } + //匹配tag标签是pre,textarea,并且第二个参数的第一个字符是回车键 + if (shouldIgnoreFirstNewline(stackedTag, text)) { + text = text.slice(1); + } + if (options.chars) { + options.chars(text); + } + return '' + }); + index += html.length - rest$1.length; + html = rest$1; + parseEndTag(stackedTag, index - endTagLength, index); + } + + if (html === last) { + options.chars && options.chars(html); + if ("development" !== 'production' && !stack.length && options.warn) { + options.warn(("Mal-formatted tag at end of template: \"" + html + "\"")); + } + break + } + } + + + + + + + // Clean up any remaining tags + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(); + //while 跳出循环就是靠该函数,每次匹配到之后就截取掉字符串,知道最后一个标签被截取完没有匹配到则跳出循环 + function advance(n) { + index += n; //让索引叠加 + html = html.substring(n); //截取当前索引 和 后面的字符串。 + } + + //获取开始标签的名称,收集属性集合,开始位置和结束位置,并且返回该对象 + function parseStartTag() { + var start = html.match(startTagOpen); //匹配开始标签 匹配开头必需是< 后面可以忽略是任何字符串 ^<((?:[a-zA-Z_][\\w\\-\\.]*\\:)?[a-zA-Z_][\\w\\-\\.]*) + console.log(start) + console.log(start[0].length) + + if (start) { + var match = { + tagName: start[1], //标签名称 + attrs: [], //标签属性集合 + start: index //标签的开始索引 + }; + //标记开始标签的位置,截取了开始标签 + advance(start[0].length); + var end, attr; + + while ( + !(end = html.match(startTagClose)) //没有到 关闭标签 > 标签 + && (attr = html.match(attribute)) //收集属性 + ) { + console.log(html) + //截取属性标签 + advance(attr[0].length); + match.attrs.push(attr); //把属性收集到一个集合 + } + if (end) { + match.unarySlash = end[1]; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + console.log(end) + + //截取掉开始标签,并且更新索引 + advance(end[0].length); + match.end = index; //开始标签的结束位置 + return match + } + } + } + + //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + //为parseHTML 节点标签堆栈 插入一个桟数据 + //调用options.start 为parse函数 stack标签堆栈 添加一个标签 + function handleStartTag(match) { + /* + * match = { + tagName: start[1], //标签名称 + attrs: [], //标签属性集合 + start: index, //开始标签的开始索引 + match:index , //开始标签的 结束位置 + unarySlash:'' //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + }; + * */ + + var tagName = match.tagName; //开始标签名称 + var unarySlash = match.unarySlash; //如果是/>标签 则unarySlash 是/。 如果是>标签 则unarySlash 是空 + console.log(expectHTML) + console.log('lastTag==') + console.log(lastTag) + console.log(tagName) + + if (expectHTML) { //true + + if ( + lastTag === 'p' //上一个标签是p + /* + 判断标签是否是 + 'address,article,aside,base,blockquote,body,caption,col,colgroup,dd,' + + 'details,dialog,div,dl,dt,fieldset,figcaption,figure,footer,form,' + + 'h1,h2,h3,h4,h5,h6,head,header,hgroup,hr,html,legend,li,menuitem,meta,' + + 'optgroup,option,param,rp,rt,source,style,summary,tbody,td,tfoot,th,thead,' + + 'title,tr,track' + */ + && isNonPhrasingTag(tagName) + ) { + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(lastTag); + } + if ( + canBeLeftOpenTag$$1(tagName) && //判断标签是否是 'colgroup,dd,dt,li,options,p,td,tfoot,th,thead,tr,source' + lastTag === tagName //上一个标签和现在标签相同
  • 编译成
  • 但是这种情况是不会出现的 因为浏览器解析的时候会自动补全如果是
  • 我是li标签
  • 浏览器自动解析成
  • 我是li标签
  • + ) { + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + parseEndTag(tagName); + } + } + + var unary = isUnaryTag$$1(tagName) || //函数匹配标签是否是 'area,base,br,col,embed,frame,hr,img,input,isindex,keygen, link,meta,param,source,track,wbr' + !!unarySlash; //如果是/> 则为真 + + var l = match.attrs.length; + var attrs = new Array(l); //数组属性对象转换正真正的数组对象 + for (var i = 0; i < l; i++) { + var args = match.attrs[i]; //获取属性对象 + // hackish work around FF bug https://bugzilla.mozilla.org/show_bug.cgi?id=369778 + //对FF bug进行黑客攻击:https://bugzilla.mozilla.org/show_bug.cgi?id=369778 + if ( + IS_REGEX_CAPTURING_BROKEN && //这个应该是 火狐浏览器私有 标志 + args[0].indexOf('""') === -1 + ) { + if (args[3] === '') { + delete args[3]; + } + if (args[4] === '') { + delete args[4]; + } + if (args[5] === '') { + delete args[5]; + } + } + var value = args[3] || args[4] || args[5] || ''; + var shouldDecodeNewlines = tagName === 'a' && args[1] === 'href' + ? options.shouldDecodeNewlinesForHref // true chrome在a[href]中编码内容 + : options.shouldDecodeNewlines; //flase //IE在属性值中编码换行,而其他浏览器则不会 + + attrs[i] = { //把数组对象属性值循环变成对象,这样可以过滤相同的属性 + name: args[1], //属性名称 + //属性值 + value: decodeAttr(value, shouldDecodeNewlines) //替换html 中的特殊符号,转义成js解析的字符串,替换 把 <替换 < , > 替换 > , "替换 ", &替换 & , 替换\n , 替换\t + + }; + + } + + console.log('==!unary==') + console.log(!unary) + + if (!unary) { //如果不是单标签 + + // 为parseHTML 节点标签堆栈 插入一个桟数据 + stack.push({ //标签堆栈 + tag: tagName, //开始标签名称 + lowerCasedTag: tagName.toLowerCase(), //变成小写记录标签 + attrs: attrs //获取属性 + }); + //设置结束标签 + lastTag = tagName; + console.log('== parseHTML handleStartTag stack==') + console.log(stack) + + } + + + // + if (options.start) { + + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, //标签名称 + attrs, //标签属性 + unary, // 如果不是单标签则为真 + match.start, //开始标签的开始位置 + match.end //开始标签的结束的位置 + ); + } + + + } + + + + //查找parseHTML的stack栈中与当前tagName标签名称相等的标签, + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + function parseEndTag( + tagName, //标签名称 + start, //结束标签开始位置 + end //结束标签结束位置 + ) { + var pos, + lowerCasedTagName; + if (start == null) { //如果没有传开始位置 + start = index; //就那当前索引 + } + if (end == null) { //如果没有传结束位置 + end = index; //就那当前索引 + } + + if (tagName) { //结束标签名称 + lowerCasedTagName = tagName.toLowerCase(); //将字符串转化成小写 + } + + // Find the closest opened tag of the same type 查找最近打开的相同类型的标记 + if (tagName) { + // 获取stack堆栈最近的匹配标签 + for (pos = stack.length - 1; pos >= 0; pos--) { + //找到最近的标签相等 + if (stack[pos].lowerCasedTag === lowerCasedTagName) { + break + } + } + } else { + // If no tag name is provided, clean shop + //如果没有提供标签名称,请清理商店 + pos = 0; + } + + + if (pos >= 0) { //这里就获取到了stack堆栈的pos索引 + // Close all the open elements, up the stack 关闭所有打开的元素,向上堆栈 + console.log(pos) + + for (var i = stack.length - 1; i >= pos; i--) { + + if ("development" !== 'production' && //如果stack中找不到tagName 标签的时候就输出警告日志,找不到标签 + (i > pos || !tagName) && + options.warn + ) { + options.warn( + ("tag <" + (stack[i].tag) + "> has no matching end tag.") + ); + } + if (options.end) { + console.log(options.end) + //调用options.end函数,删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + options.end( + stack[i].tag,//结束标签名称 + start, //结束标签开始位置 + end //结束标签结束位置 + ); + } + } + // Remove the open elements from the stack + //从堆栈中删除打开的元素 + // console.log(stack[pos].tag) + // 为parseHTML 节点标签堆栈 出桟当前匹配到的标签 + stack.length = pos; + //获取到上一个标签,就是当前节点的父节点 + lastTag = pos && stack[pos - 1].tag; + console.log(stack) + console.log(lastTag) + + + + + } else if (lowerCasedTagName === 'br') { + if (options.start) { + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, + [], true, + start, + end + ); + } + } else if (lowerCasedTagName === 'p') { + if (options.start) { + //标签开始函数, 创建一个ast标签dom, 判断获取v-for属性是否存在如果有则转义 v-for指令 把for,alias,iterator1,iterator2属性添加到虚拟dom中 + //获取v-if属性,为el虚拟dom添加 v-if,v-eles,v-else-if 属性 + //获取v-once 指令属性,如果有有该属性 为虚拟dom标签 标记事件 只触发一次则销毁 + //校验属性的值,为el添加muted, events,nativeEvents,directives, key, ref,slotName或者slotScope或者slot,component或者inlineTemplate 标志 属性 + // 标志当前的currentParent当前的 element + //为parse函数 stack标签堆栈 添加一个标签 + options.start( + tagName, + [], false, + start, + end); + } + if (options.end) { + //删除当前节点的子节点中的最后一个如果是空格或者空的文本节点则删除, + //为stack出栈一个当前标签,为currentParent变量获取到当前节点的父节点 + options.end( + tagName, + start, + end + ); + } + } + console.log(lastTag) + + } + } +``` + + + +一些匹配模板正则 + +``` + var onRE = /^@|^v-on:/;//判断是否是 @或者v-on:属性开头的 + var dirRE = /^v-|^@|^:/; //判断是否是 v-或者@或者: 属性开头的 + var forAliasRE = /([^]*?)\s+(?:in|of)\s+([^]*)/; //匹配 含有 字符串 in 字符串 或者 字符串 of 字符串 + var forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/; //匹配上, 但是属于两边是 [{ , 点 , }] 所以匹配上 ,+字符串 + var stripParensRE = /^\(|\)$/g; //匹配括号 () + + var argRE = /:(.*)$/; //匹配字符串是否含有: + var bindRE = /^:|^v-bind:/; //开始匹配是 :或者是v-bind + var modifierRE = /\.[^.]+/g; // 匹配以点开头的分组 不属于点 data.object.info.age 匹配到 ['.object','.info' , '.age'] + + var decodeHTMLCached = cached(he.decode); //获取 真是dom的textContent文本 +``` + + + +## Dual data response + +The dual data binding entry method is in the defineReactive function, whether it is prop or state or property listener set method or initInjections entry. + +First he instantiates var dep = new Dep(); Depending on the collection Dep, the get method adds one + +​ //添加一个dep +​ dep.depend(); + +​ if (childOb) { //如果子节点存在也添加一个dep +​ childOb.dep.depend(); +​ if (Array.isArray(value)) { //判断是否是数组 如果是数组 +​ dependArray(value); //则数组也添加dep +​ } +​ } + + + +The set method is the trigger for updating the view + +//observe Add an observer + +// Then add dependencies + + childOb = !shallow && observe(newVal); + //更新数据 + dep.notify(); + + + +``` + /** + * Define a reactive property on an Object. + * 在对象上定义一个无功属性。 + * 更新数据 + * 通过defineProperty的set方法去通知notify()订阅者subscribers有新的值修改 + * 添加观察者 get set方法 + */ + function defineReactive(obj, //对象 + key,//对象的key + val, //监听的数据 返回的数据 + customSetter, // 日志函数 + shallow //是否要添加__ob__ 属性 + ) { + //实例化一个主题对象,对象中有空的观察者列表 + var dep = new Dep(); + //获取描述属性 + var property = Object.getOwnPropertyDescriptor(obj, key); + var _property = Object.getOwnPropertyNames(obj); //获取实力对象属性或者方法,包括定义的描述属性 + console.log(property); + console.log(_property); + + if (property && property.configurable === false) { + return + } + + // cater for pre-defined getter/setters + + var getter = property && property.get; + console.log('arguments.length=' + arguments.length) + + if (!getter && arguments.length === 2) { + val = obj[key]; + } + var setter = property && property.set; + console.log(val) + //判断value 是否有__ob__ 实例化 dep对象,获取dep对象 为 value添加__ob__ 属性递归把val添加到观察者中 返回 new Observer 实例化的对象 + var childOb = !shallow && observe(val); + //定义描述 + Object.defineProperty(obj, key, { + enumerable: true, + configurable: true, + get: function reactiveGetter() { + + var value = getter ? getter.call(obj) : val; + if (Dep.target) { //Dep.target 静态标志 标志了Dep添加了Watcher 实例化的对象 + //添加一个dep + dep.depend(); + if (childOb) { //如果子节点存在也添加一个dep + childOb.dep.depend(); + if (Array.isArray(value)) { //判断是否是数组 如果是数组 + dependArray(value); //则数组也添加dep + } + } + } + return value + }, + set: function reactiveSetter(newVal) { + var value = getter ? getter.call(obj) : val; + /* eslint-disable no-self-compare 新旧值比较 如果是一样则不执行了*/ + if (newVal === value || (newVal !== newVal && value !== value)) { + return + } + /* eslint-enable no-self-compare + * 不是生产环境的情况下 + * */ + if ("development" !== 'production' && customSetter) { + customSetter(); + } + if (setter) { + //set 方法 设置新的值 + setter.call(obj, newVal); + } else { + //新的值直接给他 + val = newVal; + } + console.log(newVal) + + //observe 添加 观察者 + childOb = !shallow && observe(newVal); + //更新数据 + dep.notify(); + } + }); + } + +``` + + + + + + + +## Depends on collecting Dep + + In the vue data get, whoever reads the data will collect it. Therefore, dep is a set. When the data is set, the notify method of each dep is triggered by traversing the dep to update through the view +The main function of dep is to act only as a collection, so how do you update the view after collecting the dependencies +So you need to define a new Watcher class that will update the view +Each dependency collected by the dep is actually a Watcher + +``` + //主题对象Dep构造函数 主要用于添加发布事件后,用户更新数据的 响应式原理之一函数 + var Dep = function Dep() { + //uid 初始化为0 + this.id = uid++; + /* 用来存放Watcher对象的数组 */ + this.subs = []; + }; + + Dep.prototype.addSub = function addSub(sub) { + /* 在subs中添加一个Watcher对象 */ + this.subs.push(sub); + }; + + Dep.prototype.removeSub = function removeSub(sub) { + /*删除 在subs中添加一个Watcher对象 */ + remove(this.subs, sub); + }; + //this$1.deps[i].depend(); + //为Watcher 添加 为Watcher.newDeps.push(dep); 一个dep对象 + Dep.prototype.depend = function depend() { + //添加一个dep target 是Watcher dep就是dep对象 + if (Dep.target) { + //像指令添加依赖项 + Dep.target.addDep(this); + } + }; + /* 通知所有Watcher对象更新视图 */ + Dep.prototype.notify = function notify() { + // stabilize the subscriber list first + var subs = this.subs.slice(); + for (var i = 0, l = subs.length; i < l; i++) { + //更新数据 + subs[i].update(); + } + }; + + // the current target watcher being evaluated. + // this is globally unique because there could be only one + // watcher being evaluated at any time. + //当前正在评估的目标监视程序。 + //这在全球是独一无二的,因为只有一个 + //观察者在任何时候都被评估。 + Dep.target = null; + var targetStack = []; + + function pushTarget(_target) { + //target 是Watcher dep就是dep对象 + if (Dep.target) { //静态标志 Dep当前是否有添加了target + //添加一个pushTarget + targetStack.push(Dep.target); + } + Dep.target = _target; + } + + // + function popTarget() { + // 出盏一个pushTarget + Dep.target = targetStack.pop(); + } +``` + +## Data detection Watcher + + Watcher's main function is to interface to the Dep notification, and then call the update method to update the view +The callback is triggered in the update method, and the callback function is actually the generated render function + +Upon calling the render function, the values in the function will get the changed value, so a new vnode will be generated +After the new vnode is generated, it is the patch process. The new vnode is compared with the old vnode. Finally, the vnode after comparison is converted into the actual dom and added to the node to which the template is mounted +After the new template is mounted, delete the old template so that the view is updated + +``` + * *观察者分析表达式,收集依赖项, + *并在表达式值更改时触发回调。 + *这用于$watch() api和指令。 + * 当前vue实例、updateComponent函数、空函数。 + */ + var Watcher = function Watcher( + vm, //vm dom + expOrFn, //获取值的函数,或者是更新viwe试图函数 + cb, //回调函数,回调值给回调函数 + options, //参数 + isRenderWatcher//是否渲染过得观察者 + ) { + console.log('====Watcher====') + this.vm = vm; + //是否是已经渲染过得观察者 + if (isRenderWatcher) { //把当前 Watcher 对象赋值给 vm._watcher上 + vm._watcher = this; + } + //把观察者添加到队列里面 当前Watcher添加到vue实例上 + vm._watchers.push(this); + // options + if (options) { //如果有参数 + this.deep = !!options.deep; //实际 + this.user = !!options.user; //用户 + this.lazy = !!options.lazy; //懒惰 ssr 渲染 + this.sync = !!options.sync; //如果是同步 + } else { + + this.deep = this.user = this.lazy = this.sync = false; + } + this.cb = cb; //回调函数 + this.id = ++uid$1; // uid for batching uid为批处理 监听者id + this.active = true; //激活 + this.dirty = this.lazy; // for lazy watchers 对于懒惰的观察者 + this.deps = []; // 观察者队列 + this.newDeps = []; // 新的观察者队列 + // 内容不可重复的数组对象 + this.depIds = new _Set(); + this.newDepIds = new _Set(); + // 把函数变成字符串形式 + this.expression = expOrFn.toString(); + // parse expression for getter + //getter的解析表达式 + if (typeof expOrFn === 'function') { + //获取值的函数 + this.getter = expOrFn; + } else { + //如果是keepAlive 组件则会走这里 + //path 因该是路由地址 + if (bailRE.test(path)) { // 匹配上 返回 true var bailRE = /[^\w.$]/; //匹配不是 数字字母下划线 $符号 开头的为true + return + } + + // //匹配不上 path在已点分割 + // var segments = path.split('.'); + // return function (obj) { + // + // for (var i = 0; i < segments.length; i++) { + // //如果有参数则返回真 + // if (!obj) { + // return + // } + // //将对象中的一个key值 赋值给该对象 相当于 segments 以点拆分的数组做obj 的key + // obj = obj[segments[i]]; + // } + // //否则返回一个对象 + // return obj + // } + + //匹配不是 数字字母下划线 $符号 开头的为true + + this.getter = parsePath(expOrFn); + if (!this.getter) { //如果不存在 则给一个空的数组 + this.getter = function () { + }; + "development" !== 'production' && warn( + "Failed watching path: \"" + expOrFn + "\" " + + 'Watcher only accepts simple dot-delimited paths. ' + + 'For full control, use a function instead.', + vm + ); + } + } + this.value = this.lazy ? // lazy为真的的时候才能获取值 这个有是组件才为真 + undefined : + this.get(); //计算getter,并重新收集依赖项。 获取值 + }; + +``` + +get is triggered when the Watcher instance constructor executes +After the get is triggered, the Watcher instance is collected +update is the method that is triggered when a Dep notification is received +The run method is called in update +The cb callback method is called inside the run method +The cb back method is actually the template compile-time render method + + + + + +# Virtual DOM + +Virtual DOM in vue actually identifies a dom by defining a Vnode class and adding some dom attributes to the class + +The main effect is to reduce the manipulation of the actual dom to reduce the cost of browser performance + +``` + /* + * 创建标准的vue vnode + * + * */ + + var VNode = function VNode( + tag, /*当前节点的标签名*/ + data, /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ + children, //子节点 + text, //文本 + elm, /*当前节点的dom */ + context, /*编译作用域*/ + componentOptions, /*组件的option选项*/ + asyncFactory/*异步工厂*/) { + /*当前节点的标签名*/ + this.tag = tag; + + /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ + this.data = data; + + /*当前节点的子节点,是一个数组*/ + this.children = children; + + /*当前节点的文本*/ + this.text = text; + + /*当前虚拟节点对应的真实dom节点*/ + this.elm = elm; + + /*当前节点的名字空间*/ + this.ns = undefined; + + /*编译作用域 vm*/ + this.context = context; + + this.fnContext = undefined; + this.fnOptions = undefined; + this.fnScopeId = undefined; + + /*节点的key属性,被当作节点的标志,用以优化*/ + this.key = data && data.key; + + /*组件的option选项*/ + this.componentOptions = componentOptions; + + /*当前节点对应的组件的实例*/ + this.componentInstance = undefined; + + /*当前节点的父节点*/ + this.parent = undefined; + + /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/ + this.raw = false; + + /*静态节点标志*/ + this.isStatic = false; + + /*是否作为跟节点插入*/ + this.isRootInsert = true; + + /*是否为注释节点*/ + this.isComment = false; + + /*是否为克隆节点*/ + this.isCloned = false; + + /*是否有v-once指令*/ + this.isOnce = false; + + /*异步工厂*/ + this.asyncFactory = asyncFactory; + + this.asyncMeta = undefined; + this.isAsyncPlaceholder = false; + }; +``` + +# diff algorithm + +patch, sameVnode, patchVnode, updateChildren these methods + +The entry point is patch and then the sameVnode is called + +``` + //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + function sameVnode(a, b) { + return ( + + a.key === b.key && ( //如果a的key 等于b的key + ( + + a.tag === b.tag && // 如果a的tag 等于b的tag + a.isComment === b.isComment && // 如果a和b 都是注释节点 + isDef(a.data) === isDef(b.data) && //如果a.data 和 b.data 都定义后,是组件,或者是都含有tag属性 + sameInputType(a, b) //相同的输入类型。判断a和b的属性是否相同 + ) || ( + isTrue(a.isAsyncPlaceholder) && //判断是否是异步的 + a.asyncFactory === b.asyncFactory && + isUndef(b.asyncFactory.error) + ) + ) + ) + } +``` + +If the sameVnode condition is valid, enter the patchVnode method. + +The patchVnode method is mainly used to add and delete vnodes and update key. Then determine when neither virtual dom is empty and they are not equal oldCh! == ch enters the updateChildren diff update algorithm. + +``` + // 对比 虚拟dom + function patchVnode( + oldVnode, // 旧的虚拟dom + vnode, // 新的虚拟dom + insertedVnodeQueue, // 删除虚拟dom队列 + removeOnly + ) { + if (oldVnode === vnode) { //如果他们相等 + return + } + + var elm = vnode.elm = oldVnode.elm; //获取真实的dom + + // 判断是否有isAsyncPlaceholder 属性 + if (isTrue(oldVnode.isAsyncPlaceholder)) { + //判断数据 是否不等于 undefined或者null + if (isDef(vnode.asyncFactory.resolved)) { + // ssr 渲染 + hydrate(oldVnode.elm, vnode, insertedVnodeQueue); + } else { + vnode.isAsyncPlaceholder = true; + } + return + } + + // reuse element for static trees. + // note we only do this if the vnode is cloned - + // if the new node is not cloned it means the render functions have been + // reset by the hot-reload-api and we need to do a proper re-render. + //为静态树重用元素。 + //注意,只有当vnode被克隆时,我们才这样做 + //如果新节点没有克隆,则表示渲染函数已经克隆 + //由hot-reload api重置,我们需要做一个适当的重新渲染。 + if (isTrue(vnode.isStatic) && + isTrue(oldVnode.isStatic) && + vnode.key === oldVnode.key && + (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) + ) { + vnode.componentInstance = oldVnode.componentInstance; + return + } + + var i; + var data = vnode.data; + // 钩子函数 + if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { + i(oldVnode, vnode); + } + + var oldCh = oldVnode.children; + var ch = vnode.children; + //循环组件实例 是否定义有 tag标签 + if (isDef(data) && isPatchable(vnode)) { + // 触发钩子函数 更新钩子函数 + for (i = 0; i < cbs.update.length; ++i) { + cbs.update[i](oldVnode, vnode); + } + // 触发钩子函数 + if (isDef(i = data.hook) && isDef(i = i.update)) { + i(oldVnode, vnode); + } + } + + //如果是文本虚拟dom + if (isUndef(vnode.text)) { + // 两个虚拟dom都存在 + if (isDef(oldCh) && isDef(ch)) { + // 如果他们不相等 + if (oldCh !== ch) { + // diff算法更新 + updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); + } + } else if (isDef(ch)) { // 如果是有新的虚拟dom + // 如果是文本虚拟dom 则 设置 空 + if (isDef(oldVnode.text)) { + nodeOps.setTextContent(elm, ''); + } + // 添加 vnode + addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue); + } else if (isDef(oldCh)) { // 如果旧的有 新的虚拟dom没有则删除 虚拟dom + removeVnodes(elm, oldCh, 0, oldCh.length - 1); + } else if (isDef(oldVnode.text)) { // 如果是文本虚拟dom则设置文本 + nodeOps.setTextContent(elm, ''); + } + + + } else if (oldVnode.text !== vnode.text) { + // 如果新旧的文本不相同则设置文本 + nodeOps.setTextContent(elm, vnode.text); + } + if (isDef(data)) { + // 触发钩子 + if (isDef(i = data.hook) && isDef(i = i.postpatch)) { + i(oldVnode, vnode); + } + } + } + +``` + + + +# ddif algorithm updateChildren + +diif algorithm, the diff algorithm of vue2 is a depth-first algorithm for traversal, and then the comparison algorithm compares the old vnode with the new vnode, first compares their basic attributes, such as key labels, etc. If they are the same, the diff algorithm compares the old Vnode with the new Vnode, and then has four pointer indexes. Two new vnode start Pointers and two new vnode end Pointers, two old vnode start Pointers and old vnode end Pointers. Then first determine whether the vnode is empty, if it is empty, move to the center of the start pointer ++ end pointer --. Then, after comparing the two sides, cross-compare until the same vnode is not found. If there are more Vnodes, delete them; if there are fewer, add them. After comparing, call patchVnode to add or delete virtual dom. Then if there are Vnodes that are not the same, updateChildren is called, so deep recursion, also called depth-first search, is done, and then the child Vnodes are not updated to the real dom. + +``` + + // ddif 算法 + function updateChildren( + parentElm, // 父亲dom + oldCh, // 旧的虚拟dom + newCh, // 新的虚拟dom + insertedVnodeQueue, + removeOnly + ) { + var oldStartIdx = 0; // 旧的虚拟dom开始指针 + var newStartIdx = 0; // 新的虚拟dom开始指针 + var oldEndIdx = oldCh.length - 1; // 旧的虚拟dom结束指针 + var newEndIdx = newCh.length - 1;// 新的虚拟dom结束指针 + + var oldStartVnode = oldCh[0]; // 旧的虚拟dom开始节点 + var newStartVnode = newCh[0]; // 新的虚拟dom开始节点 + + var oldEndVnode = oldCh[oldEndIdx]; // 旧的虚拟dom结束节点 + var newEndVnode = newCh[newEndIdx];// 新的虚拟dom结束节点 + + var oldKeyToIdx, idxInOld, vnodeToMove, refElm; + + // removeOnly is a special flag used only by + // to ensure removed elements stay in correct relative positions + // during leaving transitions + var canMove = !removeOnly; + + { + // 检查同一个兄弟节点是否有重复的key,如果有则发出警告日志 + checkDuplicateKeys(newCh); + } + + /* + diff 算法开始 + 这里diff算法其实就是 + + */ + while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { + + if (isUndef(oldStartVnode)) { + // 如果旧的开始节点不存在或者为空 + // 如果旧的开始节点指针往中间偏移 + oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left + } else if (isUndef(oldEndVnode)) { + // 如果旧的结束节点不存在或者为空 + // 如果旧的结束节点指针往中间偏移 + oldEndVnode = oldCh[--oldEndIdx]; + + } else if (sameVnode(oldStartVnode, newStartVnode)) { //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + + + + // 在对比下虚拟dom + patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); + + //开始指针 两个都往中间偏移 + oldStartVnode = oldCh[++oldStartIdx]; + newStartVnode = newCh[++newStartIdx]; + + } else if (sameVnode(oldEndVnode, newEndVnode)) { //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + // 在对比下虚拟dom + patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); + // 结束指针 两个都往中间偏移 + oldEndVnode = oldCh[--oldEndIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right //sameVnode(oldVnode, vnode)2个节点的基本属性相同,那么就进入了2个节点的diff过程。 + + // 交叉对比 深度优先算法入口 + patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue); + // 交叉对比 + canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)); + + oldStartVnode = oldCh[++oldStartIdx]; + newEndVnode = newCh[--newEndIdx]; + } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left + // 交叉对比 + patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); + // 交叉对比 + canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm); + oldEndVnode = oldCh[--oldEndIdx]; + newStartVnode = newCh[++newStartIdx]; + } else { + // 如果没有key 则给塔新的key + if (isUndef(oldKeyToIdx)) { + + // 创建key 如果没有key 则用索引作为key + oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); + } + + // 获取 旧的vnode key + idxInOld = isDef(newStartVnode.key) + ? oldKeyToIdx[newStartVnode.key] + // 查找旧的vnode key + : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx); + // 如果旧的 vnode key 未定义则创建新的真实dom + if (isUndef(idxInOld)) { // New element + //创建真实 dom 节点 + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } else { + + vnodeToMove = oldCh[idxInOld]; + if (sameVnode(vnodeToMove, newStartVnode)) { + // 对比虚拟dom + patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue); + + oldCh[idxInOld] = undefined; + // 真实节点交换 + canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm); + } else { + // same key but different element. treat as new element + // 创建真实dom + createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx); + } + } + newStartVnode = newCh[++newStartIdx]; + } + } + if (oldStartIdx > oldEndIdx) { + refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm; + // 添加虚拟dom + addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); + } else if (newStartIdx > newEndIdx) { + // 删除虚拟dom + removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); + } + } + +``` + +Specifically look at my source code and flow chart, here the text does not describe so much, the flow chart is the following network disk, source code is vue.js, basically every line has comments + +link:https://pan.baidu.com/s/10IxV6mQ2TIwkRACKu2T0ng +password:1fnu + +The above vue.js is my vue.js based on each line of vue source code with comments, and the other files are the vue.js source code small demo that I pulled out when I looked at Vue.js source code. If you feel good, please move your little finger to help me click a satr, your support is my motivation,thank you + + + + +Author: Yao Guanshou diff --git a/vue.js b/vue.js index 4940753..a8f2eac 100644 --- a/vue.js +++ b/vue.js @@ -15,13 +15,13 @@ 'use strict'; /* */ -//Object.freeze()阻止修改现有属性的特性和值,并阻止添加新属性。 + //Object.freeze()阻止修改现有属性的特性和值,并阻止添加新属性。 var emptyObject = Object.freeze({}); -// these helpers produces better vm code in JS engines due to their -// explicitness and function inlining // these helpers produces better vm code in JS engines due to their -// explicitness and function inlining + // explicitness and function inlining + // these helpers produces better vm code in JS engines due to their + // explicitness and function inlining //判断数据 是否是undefined或者null function isUndef(v) { return v === undefined || v === null @@ -71,7 +71,7 @@ /** * Get the raw type string of a value e.g. [object Object] */ - //获取toString 简写 + //获取toString 简写 var _toString = Object.prototype.toString; function toRawType(value) { @@ -117,8 +117,8 @@ return val == null ? '' : typeof val === 'object' - ? JSON.stringify(val, null, 2) - : String(val) + ? JSON.stringify(val, null, 2) + : String(val) } /** @@ -139,7 +139,7 @@ * 并且传进一个key值取值,这里用到策略者模式 */ function makeMap(str, - expectsLowerCase) { + expectsLowerCase) { var map = Object.create(null); //创建一个新的对象 var list = str.split(','); //按字符串,分割 for (var i = 0; i < list.length; i++) { @@ -147,11 +147,11 @@ } return expectsLowerCase ? function (val) { - return map[val.toLowerCase()]; - } //返回一个柯里化函数 toLowerCase转换成小写 + return map[val.toLowerCase()]; + } //返回一个柯里化函数 toLowerCase转换成小写 : function (val) { - return map[val]; - } //返回一个柯里化函数 并且把map中添加一个 属性建 + return map[val]; + } //返回一个柯里化函数 并且把map中添加一个 属性建 } /** @@ -290,8 +290,8 @@ var l = arguments.length; return l ? l > 1 - ? fn.apply(ctx, arguments) - : fn.call(ctx, a) + ? fn.apply(ctx, arguments) + : fn.call(ctx, a) : fn.call(ctx) } @@ -304,7 +304,7 @@ return fn.bind(ctx) } -//bing 改变this上下文 + //bing 改变this上下文 var bind = Function.prototype.bind ? nativeBind : polyfillBind; @@ -409,11 +409,11 @@ */ function genStaticKeys(modules) { return modules.reduce( - function (keys, m) { - //累加staticKeys的值变成数组 - return keys.concat(m.staticKeys || []) - }, - [] + function (keys, m) { + //累加staticKeys的值变成数组 + return keys.concat(m.staticKeys || []) + }, + [] ).join(',') //转换成字符串 } @@ -435,16 +435,16 @@ if (isArrayA && isArrayB) { //如果a和b都是数组 // every 条件判断 return a.length === b.length && a.every(function (e, i) { //如果a长度和b长度一样的时候 - return looseEqual(e, b[i]) //递归 - }) + return looseEqual(e, b[i]) //递归 + }) } else if (!isArrayA && !isArrayB) { //或者a和b都不是数组 var keysA = Object.keys(a); // 获取到a的key值 变成一个数组 var keysB = Object.keys(b); // 获取到b的key值 变成一个数组 //他们的对象key值长度是一样的时候 则加载every 条件函数 return keysA.length === keysB.length && keysA.every(function (key) { - //递归 a和b的值 - return looseEqual(a[key], b[key]) - }) + //递归 a和b的值 + return looseEqual(a[key], b[key]) + }) } else { //如果不是对象跳槽循环 /* istanbul ignore next */ @@ -463,8 +463,8 @@ } } -// 判断 arr数组中的数组 是否和val相等。 -// 或者 arr数组中的对象,或者对象数组 是否和val 相等 + // 判断 arr数组中的数组 是否和val相等。 + // 或者 arr数组中的对象,或者对象数组 是否和val 相等 function looseIndexOf(arr, val) { for (var i = 0; i < arr.length; i++) { if (looseEqual(arr[i], val)) { @@ -488,7 +488,7 @@ } } -//ssr标记属性 + //ssr标记属性 var SSR_ATTR = 'data-server-rendered'; var ASSET_TYPES = [ @@ -503,8 +503,8 @@ 'created', //生命周期 结束实例化完 vue 指令 'beforeMount', //生命周期 开始渲染虚拟dom ,挂载event 事件 指令 'mounted', //生命周期 渲染虚拟dom ,挂载event 事件 完 指令 - 'beforeUpdate', //生命周期 开始更新wiew 数据指令 - 'updated', //生命周期 结束更新wiew 数据指令 + 'beforeUpdate', //生命周期 开始更新view 数据指令 + 'updated', //生命周期 结束更新view 数据指令 'beforeDestroy', //生命周期 开始销毁 new 实例 指令 'destroyed', //生命周期 结束销毁 new 实例 指令 'activated', //keep-alive组件激活时调用。 @@ -682,18 +682,18 @@ /* */ -// can we use __proto__? + // can we use __proto__? var hasProto = '__proto__' in {}; -// Browser environment sniffing -//判断设备和浏览器 + // Browser environment sniffing + //判断设备和浏览器 var inBrowser = typeof window !== 'undefined'; -//如果不是浏览器 + //如果不是浏览器 var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform; //weex 环境 一个 vue做app包的框架 var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();//weex 环境 一个 vue做app包的框架 -//window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,通过这个属性来判断浏览器类型 + //window.navigator.userAgent属性包含了浏览器类型、版本、操作系统类型、浏览器引擎类型等信息,通过这个属性来判断浏览器类型 var UA = inBrowser && window.navigator.userAgent.toLowerCase(); //获取浏览器 var isIE = UA && /msie|trident/.test(UA); //ie var isIE9 = UA && UA.indexOf('msie 9.0') > 0; //ie9 @@ -702,10 +702,10 @@ var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios'); //ios var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge; //谷歌浏览器 -// Firefox has a "watch" function on Object.prototype... + // Firefox has a "watch" function on Object.prototype... var nativeWatch = ({}).watch; -//兼容火狐浏览器写法 + //兼容火狐浏览器写法 var supportsPassive = false; if (inBrowser) { try { @@ -721,11 +721,11 @@ } } -// this needs to be lazy-evaled because vue may be required before -// vue-server-renderer can set VUE_ENV + // this needs to be lazy-evaled because vue may be required before + // vue-server-renderer can set VUE_ENV //vue 服务器渲染 可以设置 VUE_ENV var _isServer; -//判断是不是node 服务器环境 + //判断是不是node 服务器环境 var isServerRendering = function () { if (_isServer === undefined) { /* istanbul ignore if */ @@ -743,7 +743,7 @@ return _isServer }; -// detect devtools + // detect devtools //检测开发者工具。 var devtools = inBrowser && window.__VUE_DEVTOOLS_GLOBAL_HOOK__; @@ -762,7 +762,7 @@ return typeof Ctor === 'function' && /native code/.test(Ctor.toString()) } -//判断是否支持Symbol 数据类型 + //判断是否支持Symbol 数据类型 var hasSymbol = //Symbol es6新出来的一种数据类型,类似于string类型,声明唯一的数据值 typeof Symbol !== 'undefined' && isNative(Symbol) && @@ -772,8 +772,8 @@ var _Set; /* istanbul ignore if */ // $flow-disable-line -//ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 -// Set 本身是一个构造函数,用来生成 Set 数据结构。 + //ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 + // Set 本身是一个构造函数,用来生成 Set 数据结构。 //判断是否有set这个方法 if (typeof Set !== 'undefined' && isNative(Set)) { // use native Set when available. @@ -811,7 +811,7 @@ var hasConsole = typeof console !== 'undefined'; var classifyRE = /(?:^|[-_])(\w)/g; -//非捕获 匹配不分组 。 就是可以包含,但是不匹配上 + //非捕获 匹配不分组 。 就是可以包含,但是不匹配上 //过滤掉class中的 -_ 符号 并且把字母开头的改成大写 var classify = function (str) { return str.replace(classifyRE, @@ -852,8 +852,8 @@ if (hasConsole && (!config.silent)) { // console.warn("[Vue tip]: " + msg + ( - vm ? generateComponentTrace(vm) : '' - )); + vm ? generateComponentTrace(vm) : '' + )); } }; @@ -964,19 +964,19 @@ vm = vm.$parent; } return '\n\nfound in\n\n' + tree - .map(function (vm, i) { - //如果i是0 则输出 ‘---->’ - //如果i 不是0的时候输出组件名称 - return ("" + (i === 0 ? - '---> ' : repeat(' ', 5 + i * 2)) + - ( - Array.isArray(vm) ? - ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") - : formatComponentName(vm) - ) - ); - }) - .join('\n') + .map(function (vm, i) { + //如果i是0 则输出 ‘---->’ + //如果i 不是0的时候输出组件名称 + return ("" + (i === 0 ? + '---> ' : repeat(' ', 5 + i * 2)) + + ( + Array.isArray(vm) ? + ((formatComponentName(vm[0])) + "... (" + (vm[1]) + " recursive calls)") + : formatComponentName(vm) + ) + ); + }) + .join('\n') } else { //如果没有父组件则输出一个组件名称 return ("\n\n(found in " + (formatComponentName(vm)) + ")") @@ -998,13 +998,13 @@ * directives subscribing to it.订阅它的指令。 * */ - //主题对象Dep构造函数 主要用于添加发布事件后,用户更新数据的 响应式原理之一函数 + //主题对象Dep构造函数 主要用于添加发布事件后,用户更新数据的 响应式原理之一函数 var Dep = function Dep() { - //uid 初始化为0 - this.id = uid++; - /* 用来存放Watcher对象的数组 */ - this.subs = []; - }; + //uid 初始化为0 + this.id = uid++; + /* 用来存放Watcher对象的数组 */ + this.subs = []; + }; Dep.prototype.addSub = function addSub(sub) { /* 在subs中添加一个Watcher对象 */ @@ -1034,12 +1034,12 @@ } }; -// the current target watcher being evaluated. -// this is globally unique because there could be only one -// watcher being evaluated at any time. -//当前正在评估的目标监视程序。 -//这在全球是独一无二的,因为只有一个 -//观察者在任何时候都被评估。 + // the current target watcher being evaluated. + // this is globally unique because there could be only one + // watcher being evaluated at any time. + //当前正在评估的目标监视程序。 + //这在全球是独一无二的,因为只有一个 + //观察者在任何时候都被评估。 Dep.target = null; var targetStack = []; @@ -1064,14 +1064,14 @@ * */ var VNode = function VNode( - tag, /*当前节点的标签名*/ - data, /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ - children, //子节点 - text, //文本 - elm, /*当前节点的dom */ - context, /*编译作用域*/ - componentOptions, /*组件的option选项*/ - asyncFactory/*异步工厂*/) { + tag, /*当前节点的标签名*/ + data, /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/ + children, //子节点 + text, //文本 + elm, /*当前节点的dom */ + context, /*编译作用域*/ + componentOptions, /*组件的option选项*/ + asyncFactory/*异步工厂*/) { /*当前节点的标签名*/ this.tag = tag; @@ -1134,9 +1134,9 @@ this.isAsyncPlaceholder = false; }; //当且仅当该属性描述符的类型可以被改变并且该属性可以从对应对象中删除。默认为 false - var prototypeAccessors = {child: {configurable: true}}; + var prototypeAccessors = { child: { configurable: true } }; -// DEPRECATED: alias for componentInstance for backwards compat. + // DEPRECATED: alias for componentInstance for backwards compat. /* istanbul ignore next */ prototypeAccessors.child.get = function () { @@ -1175,14 +1175,14 @@ ) } -// optimized shallow clone -// used for static nodes and slot nodes because they may be reused across -// multiple renders, cloning them avoids errors when DOM manipulations rely -// on their elm reference. -//优化浅克隆 -//用于静态节点和时隙节点,因为它们可以被重用。 -//多重渲染,克隆它们避免DOM操作依赖时的错误 -//他们的榆树参考。 + // optimized shallow clone + // used for static nodes and slot nodes because they may be reused across + // multiple renders, cloning them avoids errors when DOM manipulations rely + // on their elm reference. + //优化浅克隆 + //用于静态节点和时隙节点,因为它们可以被重用。 + //多重渲染,克隆它们避免DOM操作依赖时的错误 + //他们的榆树参考。 //克隆节点 把节点变成静态节点 function cloneVNode(vnode, deep) { @@ -1390,7 +1390,7 @@ } }; -// helpers + // helpers /** * Augment an target Object or Array by intercepting @@ -1481,10 +1481,10 @@ * 添加观察者 get set方法 */ function defineReactive(obj, //对象 - key,//对象的key - val, //监听的数据 返回的数据 - customSetter, // 日志函数 - shallow //是否要添加__ob__ 属性 + key,//对象的key + val, //监听的数据 返回的数据 + customSetter, // 日志函数 + shallow //是否要添加__ob__ 属性 ) { //实例化一个主题对象,对象中有空的观察者列表 var dep = new Dep(); @@ -1690,7 +1690,7 @@ *值为最终值。 */ - //选择策略 + //选择策略 var strats = config.optionMergeStrategies; /** @@ -1739,77 +1739,77 @@ * mergeDataOrFn递归合并数据 深度拷贝。如果vm不存在,并且childVal不存在就返回parentVal。如果vm不存在并且parentVal不存在则返回childVal。如果vm不存在parentVal和childVal都存在则返回mergedDataFn。如果vm存在则返回 mergedInstanceDataFn函数 */ function mergeDataOrFn( - parentVal, - childVal, - vm - ) { - //vm不存在的时候 - if (!vm) { - // in a Vue.extend merge, both should be functions Vue。扩展合并,两者都应该是函数 - if (!childVal) { - return parentVal - } - if (!parentVal) { - return childVal - } - // when parentVal & childVal are both present, - // we need to return a function that returns the - // merged result of both functions... no need to - // check if parentVal is a function here because - // it has to be a function to pass previous merges. - //当父母和孩子都在场时, - //我们需要返回一个函数,该函数返回 - //两个函数的合并结果…不需要 - //检查parentVal是否是一个函数,因为 - //它必须是一个函数来传递以前的合并。 - return function mergedDataFn() { - //如果childVal,parentVal是函数 先改变this - return mergeData( - typeof childVal === 'function' ? childVal.call(this, this) : childVal, - typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal - ) - } - } else { - //如果vm 存在 则是合并vm的数据 - return function mergedInstanceDataFn() { - // instance merge - var instanceData = typeof childVal === 'function' - ? childVal.call(vm, vm) - : childVal; - - var defaultData = typeof parentVal === 'function' - ? parentVal.call(vm, vm) - : parentVal; - - if (instanceData) { - return mergeData(instanceData, defaultData) - } else { - return defaultData - } - } - } + parentVal, + childVal, + vm + ) { + //vm不存在的时候 + if (!vm) { + // in a Vue.extend merge, both should be functions Vue。扩展合并,两者都应该是函数 + if (!childVal) { + return parentVal + } + if (!parentVal) { + return childVal + } + // when parentVal & childVal are both present, + // we need to return a function that returns the + // merged result of both functions... no need to + // check if parentVal is a function here because + // it has to be a function to pass previous merges. + //当父母和孩子都在场时, + //我们需要返回一个函数,该函数返回 + //两个函数的合并结果…不需要 + //检查parentVal是否是一个函数,因为 + //它必须是一个函数来传递以前的合并。 + return function mergedDataFn() { + //如果childVal,parentVal是函数 先改变this + return mergeData( + typeof childVal === 'function' ? childVal.call(this, this) : childVal, + typeof parentVal === 'function' ? parentVal.call(this, this) : parentVal + ) + } + } else { + //如果vm 存在 则是合并vm的数据 + return function mergedInstanceDataFn() { + // instance merge + var instanceData = typeof childVal === 'function' + ? childVal.call(vm, vm) + : childVal; + + var defaultData = typeof parentVal === 'function' + ? parentVal.call(vm, vm) + : parentVal; + + if (instanceData) { + return mergeData(instanceData, defaultData) + } else { + return defaultData + } + } + } } strats.data = function ( - parentVal, - childVal, - vm - ) { - if (!vm) { - if (childVal && typeof childVal !== 'function') { - "development" !== 'production' && warn( - 'The "data" option should be a function ' + - 'that returns a per-instance value in component ' + - 'definitions.', - vm - ); - - return parentVal - } - return mergeDataOrFn(parentVal, childVal) - } + parentVal, + childVal, + vm + ) { + if (!vm) { + if (childVal && typeof childVal !== 'function') { + "development" !== 'production' && warn( + 'The "data" option should be a function ' + + 'that returns a per-instance value in component ' + + 'definitions.', + vm + ); - return mergeDataOrFn(parentVal, childVal, vm) + return parentVal + } + return mergeDataOrFn(parentVal, childVal) + } + + return mergeDataOrFn(parentVal, childVal, vm) }; /** @@ -1820,17 +1820,17 @@ * 如果不是数组把childVal变成数组在返回出去 */ function mergeHook( - parentVal, - childVal - ) { - return childVal ? (parentVal ? - parentVal.concat(childVal) : - (Array.isArray(childVal) ? - childVal : - [childVal] - ) - ):parentVal - } + parentVal, + childVal + ) { + return childVal ? (parentVal ? + parentVal.concat(childVal) : + (Array.isArray(childVal) ? + childVal : + [childVal] + ) + ) : parentVal + } LIFECYCLE_HOOKS.forEach(function (hook) { strats[hook] = mergeHook; @@ -1851,11 +1851,11 @@ * 如果childVal 存在,则用浅拷贝吧 childVal 合并到res中,返回res对象 */ function mergeAssets( - parentVal, - childVal, - vm, - key - ) { + parentVal, + childVal, + vm, + key + ) { var res = Object.create(parentVal || null); if (childVal) { "development" !== 'production' && assertObjectType(key, childVal, vm); @@ -1881,10 +1881,10 @@ * 循环childVal。获取到子节点childVal的key如果在父亲节点上面有,则先获取到父亲节点的值,如果父亲节点的上没有值得获取子节点的值。 变成数组存在ret对象中。 */ strats.watch = function ( - parentVal, //父节点值 - childVal, //子节点值 - vm, //vm vue实例化的对象 - key) { // key值 + parentVal, //父节点值 + childVal, //子节点值 + vm, //vm vue实例化的对象 + key) { // key值 // work around Firefox's Object.prototype.watch... 在Firefox的对象周围工作。原型 //// Firefox has a "watch" function on Object.prototype... //var nativeWatch = ({}).watch; @@ -1914,9 +1914,9 @@ parent = [parent]; // } ret[key$1] = parent ? parent.concat(child) - : Array.isArray(child) ? - child : - [child]; + : Array.isArray(child) ? + child : + [child]; } return ret }; @@ -1926,29 +1926,29 @@ */ strats.props = strats.methods = - strats.inject = - strats.computed = function ( - parentVal, - childVal, - vm, - key - ) { - if (childVal && "development" !== 'production') { - //判断是否是对象 - assertObjectType(key, childVal, vm); - } - if (!parentVal) { - return childVal - } - var ret = Object.create(null); - //对象浅拷贝,参数(to, _from)循环_from的值,会覆盖掉to的值 - extend(ret, parentVal); - if (childVal) { - //对象浅拷贝,参数(to, _from)循环_from的值,会覆盖掉to的值 - extend(ret, childVal); - } - return ret - }; + strats.inject = + strats.computed = function ( + parentVal, + childVal, + vm, + key + ) { + if (childVal && "development" !== 'production') { + //判断是否是对象 + assertObjectType(key, childVal, vm); + } + if (!parentVal) { + return childVal + } + var ret = Object.create(null); + //对象浅拷贝,参数(to, _from)循环_from的值,会覆盖掉to的值 + extend(ret, parentVal); + if (childVal) { + //对象浅拷贝,参数(to, _from)循环_from的值,会覆盖掉to的值 + extend(ret, childVal); + } + return ret + }; strats.provide = mergeDataOrFn; /** @@ -1965,7 +1965,7 @@ */ function checkComponents(options) { for (var key in options.components) { - // 验证组件名称 必须是大小写,并且是-横杆 + // 验证组件名称 必须是大小写,并且是-横杆 validateComponentName(key); } } @@ -2015,7 +2015,7 @@ //把含有横岗的字符串 变成驼峰写法 name = camelize(val); - res[name] = {type: null}; + res[name] = { type: null }; } else { //当使用数组语法时,道具必须是字符串。 如果是props 是数组必须是字符串 warn('props must be strings when using array syntax.'); @@ -2027,7 +2027,7 @@ name = camelize(key); //把含有横岗的字符串 变成驼峰写法 res[name] = isPlainObject(val) //判断值是不是对象 ? val - : {type: val}; + : { type: val }; } } else { //如果不是对象和数组则警告 @@ -2054,8 +2054,8 @@ * * */ function normalizeInject(options, vm) { - // provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。 - // 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。 + // provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。 + // 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。 var inject = options.inject; if (!inject) { @@ -2064,16 +2064,16 @@ var normalized = options.inject = {}; if (Array.isArray(inject)) { //如果是数组 for (var i = 0; i < inject.length; i++) { - // * 将数组转化成对象 比如 [1,2,3]转化成 - // * normalized[1]={from: 1} - // * normalized[2]={from: 2} - // * normalized[3]={from: 3} - normalized[inject[i]] = {from: inject[i]}; + // * 将数组转化成对象 比如 [1,2,3]转化成 + // * normalized[1]={from: 1} + // * normalized[2]={from: 2} + // * normalized[3]={from: 3} + normalized[inject[i]] = { from: inject[i] }; } } else if (isPlainObject(inject)) { //如果是对象 for (var key in inject) { var val = inject[key]; - normalized[key] = isPlainObject(val) ? extend({from: key}, val) : {from: val}; + normalized[key] = isPlainObject(val) ? extend({ from: key }, val) : { from: val }; } } else { warn( @@ -2104,7 +2104,7 @@ if (typeof def === 'function') { //如果是函数 //为该函数添加一个对象和值 - dirs[key] = {bind: def, update: def}; + dirs[key] = { bind: def, update: def }; } } } @@ -2129,8 +2129,8 @@ * 用于实例化和继承的核心实用程序。 */ function mergeOptions(parent, //父值 - child, //子值 优选取子值 - vm) { + child, //子值 优选取子值 + vm) { { //检验子组件 @@ -2147,7 +2147,7 @@ // 将数组转化成对象 比如 [1,2,3]转化成 normalizeInject(child, vm); - // * normalizeDirectives获取到指令对象值。循环对象指令的值,如果是函数则把它变成dirs[key] = {bind: def, update: def} 这种形式 + // * normalizeDirectives获取到指令对象值。循环对象指令的值,如果是函数则把它变成dirs[key] = {bind: def, update: def} 这种形式 normalizeDirectives(child); //子组件是否有需要合并的对象继承 方式 @@ -2181,8 +2181,8 @@ // 就返回父节点parentVal,如果有子节点childVal就返回子节点childVal。 function mergeField(key) { //defaultStrat 获取子值还是父组的值 - var strat =strats[key] || // - defaultStrat; //* 如果没有子节点就返回父节点,如果有子节点就返回子节点 + var strat = strats[key] || // + defaultStrat; //* 如果没有子节点就返回父节点,如果有子节点就返回子节点 //获取子值还是父组的值 options[key] = strat(parent[key], child[key], vm, key); } @@ -2199,9 +2199,9 @@ * */ function resolveAsset(options, //参数 - type, // 类型:directives , filters ,components - id, // 指令的key 属性 - warnMissing //警告的信息 true + type, // 类型:directives , filters ,components + id, // 指令的key 属性 + warnMissing //警告的信息 true ) { console.log('==resolveAsset==') console.log(options) @@ -2279,71 +2279,71 @@ * */ function validateProp( - key, //key - propOptions, //原始props 参数 - propsData, //转义过的组件props数据 - vm // VueComponent 组件构造函数 - ) { //vm this属性 - - - var prop = propOptions[key]; //获取组件定义的props 属性 - var absent = !hasOwn(propsData, key); // 如果该为假的那么可能 a-b 这样的key才能获取到值 - var value = propsData[key]; // 获取值 - // boolean casting - //Boolean 传一个布尔值 但是 一般是函数或者数组函数才有意义,而且是函数声明的函数并不是 函数表达式prop.type 也需要是函数 - //返回的是相同的索引 判断 属性类型定义的是否是Boolean - var booleanIndex = getTypeIndex(Boolean, prop.type); - if (booleanIndex > -1) { //如果是boolean值 - - if (absent && !hasOwn(prop, 'default')) { //如果key 不是propsData 实例化,或者 没有定义default 默认值的时候 设置value 为false - value = false; - } else if ( - value === '' //如果value 是空 - || value === hyphenate(key) //或者key转出 - 形式和value 相等的时候 - ) { // - // only cast empty string / same name to boolean if 仅将空字符串/相同名称转换为boolean if - // boolean has higher priority 获取到相同的 - //判断prop.type 的类型是否是string字符串类型 - var stringIndex = getTypeIndex(String, prop.type); - - if ( - stringIndex < 0 || //如果匹配不到字符串 - booleanIndex < stringIndex) { //或者布尔值索引小于字符串 索引的时候 - value = true; - } - } - } - // check default value 检查默认值 - if (value === undefined) { //如果没有值 value 也不是boolean, 也不是string的时候 - // 有可能是 函数 - value = getPropDefaultValue(vm, prop, key); - // since the default value is a fresh copy, 由于默认值是一个新的副本, - // make sure to observe it. 一定要遵守。 - var prevShouldObserve = shouldObserve; - toggleObserving(true); - console.log('===value===') - console.log(value); - //为 value添加 value.__ob__ 属性,把value添加到观察者中 - observe(value); - toggleObserving(prevShouldObserve); - } - { - console.log( prop, - key, - value, - vm, - absent) - - //检查prop 是否合格 - assertProp( - prop, //属性的type值 - key, //props属性中的key - value, //view 属性的值 - vm, // VueComponent 组件构造函数 - absent //false - ); - } - return value + key, //key + propOptions, //原始props 参数 + propsData, //转义过的组件props数据 + vm // VueComponent 组件构造函数 + ) { //vm this属性 + + + var prop = propOptions[key]; //获取组件定义的props 属性 + var absent = !hasOwn(propsData, key); // 如果该为假的那么可能 a-b 这样的key才能获取到值 + var value = propsData[key]; // 获取值 + // boolean casting + //Boolean 传一个布尔值 但是 一般是函数或者数组函数才有意义,而且是函数声明的函数并不是 函数表达式prop.type 也需要是函数 + //返回的是相同的索引 判断 属性类型定义的是否是Boolean + var booleanIndex = getTypeIndex(Boolean, prop.type); + if (booleanIndex > -1) { //如果是boolean值 + + if (absent && !hasOwn(prop, 'default')) { //如果key 不是propsData 实例化,或者 没有定义default 默认值的时候 设置value 为false + value = false; + } else if ( + value === '' //如果value 是空 + || value === hyphenate(key) //或者key转出 - 形式和value 相等的时候 + ) { // + // only cast empty string / same name to boolean if 仅将空字符串/相同名称转换为boolean if + // boolean has higher priority 获取到相同的 + //判断prop.type 的类型是否是string字符串类型 + var stringIndex = getTypeIndex(String, prop.type); + + if ( + stringIndex < 0 || //如果匹配不到字符串 + booleanIndex < stringIndex) { //或者布尔值索引小于字符串 索引的时候 + value = true; + } + } + } + // check default value 检查默认值 + if (value === undefined) { //如果没有值 value 也不是boolean, 也不是string的时候 + // 有可能是 函数 + value = getPropDefaultValue(vm, prop, key); + // since the default value is a fresh copy, 由于默认值是一个新的副本, + // make sure to observe it. 一定要遵守。 + var prevShouldObserve = shouldObserve; + toggleObserving(true); + console.log('===value===') + console.log(value); + //为 value添加 value.__ob__ 属性,把value添加到观察者中 + observe(value); + toggleObserving(prevShouldObserve); + } + { + console.log(prop, + key, + value, + vm, + absent) + + //检查prop 是否合格 + assertProp( + prop, //属性的type值 + key, //props属性中的key + value, //view 属性的值 + vm, // VueComponent 组件构造函数 + absent //false + ); + } + return value } /** @@ -2404,60 +2404,60 @@ function assertProp( - prop, //属性的type值 - name, //props属性中的key - value, //view 属性的值 - vm, //组件构造函数 - absent//false - ) { - //必须有required 和 absent - if (prop.required && absent) { - warn( - 'Missing required prop: "' + name + '"', - vm - ); - return - } - //如果vual 为空 或者 不是必填项 则不执行下面代码 - if (value == null && !prop.required) { - return - } - //类型 - var type = prop.type; - - //如果类型为真 或者类型 不存在 - var valid = !type || type === true; - - var expectedTypes = []; - - if (type) { //如果type存在 - if (!Array.isArray(type)) { //如果不是数组 - type = [type]; //再包裹成数组 - } - for (var i = 0; i < type.length && !valid; i++) { - var assertedType = assertType(value, type[i]); - expectedTypes.push(assertedType.expectedType || ''); - valid = assertedType.valid; - } - } - if (!valid) { - warn( - "Invalid prop: type check failed for prop \"" + name + "\"." + - " Expected " + (expectedTypes.map(capitalize).join(', ')) + - ", got " + (toRawType(value)) + ".", - vm - ); - return - } - var validator = prop.validator; - if (validator) { - if (!validator(value)) { - warn( - 'Invalid prop: custom validator check failed for prop "' + name + '".', - vm - ); - } - } + prop, //属性的type值 + name, //props属性中的key + value, //view 属性的值 + vm, //组件构造函数 + absent//false + ) { + //必须有required 和 absent + if (prop.required && absent) { + warn( + 'Missing required prop: "' + name + '"', + vm + ); + return + } + //如果vual 为空 或者 不是必填项 则不执行下面代码 + if (value == null && !prop.required) { + return + } + //类型 + var type = prop.type; + + //如果类型为真 或者类型 不存在 + var valid = !type || type === true; + + var expectedTypes = []; + + if (type) { //如果type存在 + if (!Array.isArray(type)) { //如果不是数组 + type = [type]; //再包裹成数组 + } + for (var i = 0; i < type.length && !valid; i++) { + var assertedType = assertType(value, type[i]); + expectedTypes.push(assertedType.expectedType || ''); + valid = assertedType.valid; + } + } + if (!valid) { + warn( + "Invalid prop: type check failed for prop \"" + name + "\"." + + " Expected " + (expectedTypes.map(capitalize).join(', ')) + + ", got " + (toRawType(value)) + ".", + vm + ); + return + } + var validator = prop.validator; + if (validator) { + if (!validator(value)) { + warn( + 'Invalid prop: custom validator check failed for prop "' + name + '".', + vm + ); + } + } } //检测数据类型 是否是String|Number|Boolean|Function|Symbol 其中的一个数据类型 @@ -2626,24 +2626,24 @@ } } -// Here we have async deferring wrappers using both microtasks and (macro) tasks. 在这里,我们使用了微任务和宏任务的异步包装器。 -// In < 2.4 we used microtasks everywhere, but there are some scenarios where 在< 2.4中,我们到处使用微任务,但也有一些场景。 -// microtasks have too high a priority and fire in between supposedly 微任务优先级太高,据称介于两者之间。 -// sequential events (e.g. #4521, #6690) or even between bubbling of the same 序贯事件(例如α4521,α6690),甚至在同一气泡之间 -// event (#6566). However, using (macro) tasks everywhere also has subtle problems 事件(α6566)。然而,到处使用(宏)任务也有微妙的问题。 -// when state is changed right before repaint (e.g. #6813, out-in transitions). 当状态在重新绘制之前被正确改变(例如,α6813,在过渡中出现)。 -// Here we use microtask by default, but expose a way to force (macro) task when 这里,我们默认使用微任务,但是暴露一种方法来强制(宏)任务 -// needed (e.g. in event handlers attached by v-on). 需要的(例如在事件处理程序中附加的V-on)。 + // Here we have async deferring wrappers using both microtasks and (macro) tasks. 在这里,我们使用了微任务和宏任务的异步包装器。 + // In < 2.4 we used microtasks everywhere, but there are some scenarios where 在< 2.4中,我们到处使用微任务,但也有一些场景。 + // microtasks have too high a priority and fire in between supposedly 微任务优先级太高,据称介于两者之间。 + // sequential events (e.g. #4521, #6690) or even between bubbling of the same 序贯事件(例如α4521,α6690),甚至在同一气泡之间 + // event (#6566). However, using (macro) tasks everywhere also has subtle problems 事件(α6566)。然而,到处使用(宏)任务也有微妙的问题。 + // when state is changed right before repaint (e.g. #6813, out-in transitions). 当状态在重新绘制之前被正确改变(例如,α6813,在过渡中出现)。 + // Here we use microtask by default, but expose a way to force (macro) task when 这里,我们默认使用微任务,但是暴露一种方法来强制(宏)任务 + // needed (e.g. in event handlers attached by v-on). 需要的(例如在事件处理程序中附加的V-on)。 var microTimerFunc; //微计时器功能 var macroTimerFunc; //宏计时器功能 var useMacroTask = false; //使用宏任务 -// Determine (macro) task defer implementation. 确定(宏)任务延迟实现。 -// Technically setImmediate should be the ideal choice, but it's only available 技术上应该是理想的选择,但它是唯一可用的。 -// in IE. The only polyfill that consistently queues the callback after all DOM 在IE.中,唯一的填充在所有DOM之后始终排队回叫。 -// events triggered in the same loop is by using MessageChannel. 在同一循环中触发的事件是通过使用消息通道。 + // Determine (macro) task defer implementation. 确定(宏)任务延迟实现。 + // Technically setImmediate should be the ideal choice, but it's only available 技术上应该是理想的选择,但它是唯一可用的。 + // in IE. The only polyfill that consistently queues the callback after all DOM 在IE.中,唯一的填充在所有DOM之后始终排队回叫。 + // events triggered in the same loop is by using MessageChannel. 在同一循环中触发的事件是通过使用消息通道。 /* istanbul ignore if */ //判断setImmediate 是否存在,如果存在则判断下是是否是系统内置函数 if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) { @@ -2653,10 +2653,10 @@ setImmediate(flushCallbacks); }; } else if (typeof MessageChannel !== 'undefined' && ( - isNative(MessageChannel) || - // PhantomJS - MessageChannel.toString() === '[object MessageChannelConstructor]' - )) { + isNative(MessageChannel) || + // PhantomJS + MessageChannel.toString() === '[object MessageChannelConstructor]' + )) { //如果有 消息体 内置函数则实例化 var channel = new MessageChannel(); //获取端口2 @@ -2679,7 +2679,7 @@ }; } -// Determine microtask defer implementation. + // Determine microtask defer implementation. //确定微任务延迟执行。 /* istanbul ignore next, $flow-disable-line */ if (typeof Promise !== 'undefined' && isNative(Promise)) { @@ -2712,11 +2712,11 @@ function withMacroTask(fn) { //宏任务 return fn._withTask || (fn._withTask = function () { - useMacroTask = true; - var res = fn.apply(null, arguments); - useMacroTask = false; - return res - }) + useMacroTask = true; + var res = fn.apply(null, arguments); + useMacroTask = false; + return res + }) } //为callbacks 收集队列cb 函数 并且根据 pending 状态是否要触发callbacks 队列函数 @@ -3049,12 +3049,12 @@ //更新事件 并且为新的值 添加函数 旧的值删除函数等功能 function updateListeners( - on, //新的事件 - oldOn, //旧的事件 - add, //添加事件函数 - remove$$1, //删除事件函数 - vm//vue 实例化对象 - ) { + on, //新的事件 + oldOn, //旧的事件 + add, //添加事件函数 + remove$$1, //删除事件函数 + vm//vue 实例化对象 + ) { var name, def, cur, old, event; for (name in on) { // 遍历on @@ -3088,7 +3088,7 @@ event.once, //是否只触发一次的状态 event.capture, // 事件俘获或是冒泡行为 event.passive, // 检测事件修饰符 是否是 '&' - event.params //事件参数 + event.params //事件参数 ); } else if (cur !== old) { @@ -3163,90 +3163,90 @@ * * */ function extractPropsFromVNodeData( - data, //tag标签属性数据 - Ctor, //组件构造函数VueComponent - tag //tag标签名称 - ) { - // we are only extracting raw values here. - // validation and default values are handled in the child - // component itself. - //我们只是在这里提取原始值。 - //验证和默认值在孩子中被处理 - //组件本身。 - - //获取Ctor 参数中的 props - var propOptions = Ctor.options.props; //获取组件的props属性 - console.log(Ctor.options) - - //如果propOptions 属性是空或者不存在 这不执行下面代码 - if (isUndef(propOptions)) { - return - } - - - var res = {}; - var attrs = data.attrs; - var props = data.props; - - //如果data中的属性attrs或者props 属性 数据存在 - if (isDef(attrs) || isDef(props)) { - //遍历propOptions props属性中的值 - for (var key in propOptions) { - - //altKey获取到一个函数,该函数功能是把 abCd 驼峰字母改写成 ab-c 如果是 aB cd 则是 ab cd - //大写字母,加完减号又转成小写了 比如把驼峰 aBc 变成了 a-bc - //匹配大写字母并且两面不是空白的 替换成 '-' + '字母' 在全部转换成小写 - - var altKey = hyphenate(key); - - - { - //把key 转换成小写 - var keyInLowerCase = key.toLowerCase(); - //如果他们key不相同 并且 属性attrs存在 并且keyInLowerCase 属性存在 attrs对象中 - if ( - key !== keyInLowerCase && - attrs && hasOwn(attrs, keyInLowerCase) - ) { - //输出一个警告信息 - tip( - "Prop \"" + keyInLowerCase + "\" is passed to component " + - (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + - " \"" + key + "\". " + - "Note that HTML attributes are case-insensitive and camelCased " + - "props need to use their kebab-case equivalents when using in-DOM " + - "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." - ); - } - } - //检查属性 - checkProp( - res, //空对象 - props, //props 属性 - key, //propOptions 的原始key - altKey, //转换后的 横杆key - true - ) || - checkProp( - res, - attrs, - key, - altKey, - false - ); - } - } - return res + data, //tag标签属性数据 + Ctor, //组件构造函数VueComponent + tag //tag标签名称 + ) { + // we are only extracting raw values here. + // validation and default values are handled in the child + // component itself. + //我们只是在这里提取原始值。 + //验证和默认值在孩子中被处理 + //组件本身。 + + //获取Ctor 参数中的 props + var propOptions = Ctor.options.props; //获取组件的props属性 + console.log(Ctor.options) + + //如果propOptions 属性是空或者不存在 这不执行下面代码 + if (isUndef(propOptions)) { + return + } + + + var res = {}; + var attrs = data.attrs; + var props = data.props; + + //如果data中的属性attrs或者props 属性 数据存在 + if (isDef(attrs) || isDef(props)) { + //遍历propOptions props属性中的值 + for (var key in propOptions) { + + //altKey获取到一个函数,该函数功能是把 abCd 驼峰字母改写成 ab-c 如果是 aB cd 则是 ab cd + //大写字母,加完减号又转成小写了 比如把驼峰 aBc 变成了 a-bc + //匹配大写字母并且两面不是空白的 替换成 '-' + '字母' 在全部转换成小写 + + var altKey = hyphenate(key); + + + { + //把key 转换成小写 + var keyInLowerCase = key.toLowerCase(); + //如果他们key不相同 并且 属性attrs存在 并且keyInLowerCase 属性存在 attrs对象中 + if ( + key !== keyInLowerCase && + attrs && hasOwn(attrs, keyInLowerCase) + ) { + //输出一个警告信息 + tip( + "Prop \"" + keyInLowerCase + "\" is passed to component " + + (formatComponentName(tag || Ctor)) + ", but the declared prop name is" + + " \"" + key + "\". " + + "Note that HTML attributes are case-insensitive and camelCased " + + "props need to use their kebab-case equivalents when using in-DOM " + + "templates. You should probably use \"" + altKey + "\" instead of \"" + key + "\"." + ); + } + } + //检查属性 + checkProp( + res, //空对象 + props, //props 属性 + key, //propOptions 的原始key + altKey, //转换后的 横杆key + true + ) || + checkProp( + res, + attrs, + key, + altKey, + false + ); + } + } + return res } //检查 属性 检查key和altKey 在hash属性对象中有没有,如果有则赋值给res对象 function checkProp( - res, //需要添加值的对象 - hash, // 属性对象 - key, // 原始key - altKey, //转换后的 横杆key - preserve //是否要删除hash 对象中的属性或者方法 状态 布尔值 + res, //需要添加值的对象 + hash, // 属性对象 + key, // 原始key + altKey, //转换后的 横杆key + preserve //是否要删除hash 对象中的属性或者方法 状态 布尔值 ) { //hash 值存在 if (isDef(hash)) { @@ -3274,18 +3274,18 @@ /* */ -// The template compiler attempts to minimize the need for normalization by 模板编译器试图最小化对规范化的需要。 -// statically analyzing the template at compile time. 在编译时静态分析模板。 -// -// For plain HTML markup, normalization can be completely skipped because the 对于普通HTML标记,可以完全跳过标准化,因为 -// generated render function is guaranteed to return Array. There are 生成的渲染函数保证返回数组。有 -// two cases where extra normalization is needed: 需要额外标准化的两种情况: + // The template compiler attempts to minimize the need for normalization by 模板编译器试图最小化对规范化的需要。 + // statically analyzing the template at compile time. 在编译时静态分析模板。 + // + // For plain HTML markup, normalization can be completely skipped because the 对于普通HTML标记,可以完全跳过标准化,因为 + // generated render function is guaranteed to return Array. There are 生成的渲染函数保证返回数组。有 + // two cases where extra normalization is needed: 需要额外标准化的两种情况: -// 1. When the children contains components - because a functional component 当儿童包含组件时,因为函数组件 -// may return an Array instead of a single root. In this case, just a simple 可以返回数组而不是单个根。在这种情况下,只是一个简单的例子 -// normalization is needed - if any child is an Array, we flatten the whole 规范化是必要的-如果任何一个孩子是一个数组,我们扁平化整个 -// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep 和Array.prototype.concat在一起。保证仅为1级深 -// because functional components already normalize their own children. 因为功能组件已经规范了他们自己的孩子。 + // 1. When the children contains components - because a functional component 当儿童包含组件时,因为函数组件 + // may return an Array instead of a single root. In this case, just a simple 可以返回数组而不是单个根。在这种情况下,只是一个简单的例子 + // normalization is needed - if any child is an Array, we flatten the whole 规范化是必要的-如果任何一个孩子是一个数组,我们扁平化整个 + // thing with Array.prototype.concat. It is guaranteed to be only 1-level deep 和Array.prototype.concat在一起。保证仅为1级深 + // because functional components already normalize their own children. 因为功能组件已经规范了他们自己的孩子。 //循环子节点children,把他连在一起,其实就是把伪数组变成真正的数组 function simpleNormalizeChildren(children) { @@ -3297,17 +3297,17 @@ return children } -// 2. When the children contains constructs that always generated nested Arrays, 2。当子类包含总是生成嵌套数组的结构时, -// e.g.