使用Vue也大概有一年多了,觉得Vue使用起来非常的简单,轻量,出于好奇,在github上将代码拉下来剖解分析。
0. 源码目录结构
- .circleci 持续集成
- benchmarks 性能评测
- dist 输出目录
- examples 案例
- flow flow声明文件
- packages vue中的包
- scripts 工程化
- src 源码目录
- test 测试相关
- types ts声明文件
为了直观的查看目录我们可以通过tree命令来查看src目录下的文件夹。先大概对源码的结构有一个大体的认识。
├─compiler # 编译的相关逻辑
│ ├─codegen
│ ├─directives
│ └─parser
├─core # vue核心代码
│ ├─components # vue中的内置组件 keep-alive
│ ├─global-api # vue中的全局api
│ ├─instance # vue中的核心逻辑
│ ├─observer # vue中的响应式原理
│ ├─util
│ └─vdom # vue中的虚拟dom模块
├─platforms # 平台代码
│ ├─web # web逻辑 - vue
│ │ ├─compiler
│ │ ├─runtime
│ │ ├─server
│ │ └─util
│ └─weex # weex逻辑 - app
│ ├─compiler
│ ├─runtime
│ └─util
├─server # 服务端渲染模块
├─sfc # 用于编译.vue文件
└─shared # 共享的方法和常量
> 通过package.json中scripts查看代码是如何运行的
```json
"build": "node scripts/build.js",
"build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
"build:weex": "npm run build -- weex",
核心是使用node执行 scripts/build.js,通过传递参数来实现不同的打包结果,这里的–代表后面的内容是参数。
// ------ build.js // 1.获取不同的打包的配置 let builds = require('./config').getAllBuilds()
// 2.根据执行打包时的参数进行过滤
if (process.argv[2]) {
const filters = process.argv[2].split(‘,’)
builds = builds.filter(b => {
return filters.some(f => b.output.file.indexOf(f) > -1 || b._name.indexOf(f) > -1)
})
} else {
// 默认不打包weex相关代码
builds = builds.filter(b => {
return b.output.file.indexOf(‘weex’) === -1
})
}
// 3.进行打包
build(builds)
> 打包入口: 我们可以通过打包的配置找到我们需要的入口,这两个区别在于是否涵盖compiler逻辑,我们在开发时一般使用的是entry-runtime,可以减小vue的体积,但是同样在开发时也不能再使用template,.vue文件中的template是通过vue-loader来进行编译的,和我们所说的compiler无关。
```javascript
src/platforms/web/entry-runtime.js
src/platforms/web/entry-runtime-with-compiler.js
源码调试
通过vue编写源码方式调试
- 下载源代码
- 查看package.json scripts打包命令
- npm run dev
- 编写代码debugger
- 可以在打包命令后面加–sourcemap 调试源代码
通过cli项目调试
- vue inspect 查看vue打包后的js文件(vue.runtime.esm.js)
- 在文件中找到需要调试的方法debugger
1. 初始化
initState: 将挂载的data、watch、computed等数据进行劫持
let observe = new Observe(data);
- 如果data是数组类型, 重写data原型方法(对能改变原数组的方法进行重写,Object.create(Array.prototype))
- observeArray 遍历数组,调用observe
- 如果调用数组新增的方法,对新增的数据进行劫持
- 如果是对象
- this.walk(data), 遍历对象调用defineReactive(data, key, data[key])方法(Object.defineProperty 数据劫持)
- 如果 set 值是对象,observe 重新对这个对象进行数据劫持
2. 响应式原理
响应式原理: 每个属性会对应一个Dep实例,get当前属性值,对应的dep会收集渲染watcher,set值时,会通知收集的watcher调用渲染方法,重新渲染页面。如果是数组的话,会给数组增加dep属性,存储Dep实例,并收集watcher,当调用数组特殊方法时,通知dep内的watcher调用渲染方法,重新渲染页面.
// Watch类
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let id = 0;
class Watcher {
constructor(vm,exprOrFn,cb,options){
this.vm = vm;
this.exprOrFn = exprOrFn;
this.cb = cb;
this.options = options;
this.id = id++;
this.getter = exprOrFn;
this.deps = [];
this.depsId = new Set();
this.get(); // 默认初始化 要取值
}
get(){
pushTarget(this); // Dep.target = watcher
this.getter();
popTarget();
}
update(){
queueWatcher(this);
}
run(){
this.get();
}
addDep(dep){
let id = dep.id;
if(!this.depsId.has(id)){
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
}
export default Watcher
// Dep类
let id = 0;
class Dep{ // 每个属性我都给他分配一个dep,dep可以来存放watcher, watcher中还要存放这个dep
constructor(){
this.id = id++;
this.subs = []; // 用来存放watcher的
}
depend(){
if(Dep.target){
Dep.target.addDep(this);
}
}
addSub(watcher){
this.subs.push(watcher);
}
notify(){
this.subs.forEach(watcher=>watcher.update());
}
}
Dep.target = null; // 一份
export function pushTarget(watcher) {
Dep.target = watcher;
}
export function popTarget(){
Dep.target = null
}
export default Dep
import { isObject } from "../utils";
import { arrayMethods } from "./array";
import Dep from './dep'
// 1.如果数据是对象 会将对象不停的递归 进行劫持
// 2.如果是数组,会劫持数组的方法,并对数组中不是基本数据类型的进行检测
// 检测数据变化 类有类型 , 对象无类型
// 如果给对象新增一个属性不会触发视图更新 (给对象本身也增加一个dep,dep中存watcher,如果增加一个属性后,我就手动的触发watcher的更新)
class Observer {
constructor(data) { // 对对象中的所有属性 进行劫持
this.dep = new Dep(); // 数据可能是数组或者对象
Object.defineProperty(data,'__ob__',{
value:this,
enumerable:false // 不可枚举的
})
// data.__ob__ = this; // 所有被劫持过的属性都有__ob__
if(Array.isArray(data)){ // 我希望数组的变化可以触发视图更新?
// 数组劫持的逻辑
// 对数组原来的方法进行改写, 切片编程 高阶函数
data.__proto__ = arrayMethods;
// 如果数组中的数据是对象类型,需要监控对象的变化
this.observeArray(data);
}else{
this.walk(data); //对象劫持的逻辑
}
}
observeArray(data){ // 对我们数组的数组 和 数组中的对象再次劫持 递归了
// [{a:1},{b:2}]
// 如果数组里放的是对象类型,也做了观测,JSON.stringify() 也做了收集一来了
data.forEach(item=>observe(item))
}
walk(data) { // 对象
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
}
function dependArray(value){
for(let i = 0; i < value.length;i++){
let current = value[i]; // current是数组里面的数组 [[[[[]]]]]
current.__ob__ && current.__ob__.dep.depend();
if(Array.isArray(current)){
dependArray(current);
}
}
}
function defineReactive(data,key,value){ // value有可能是对象
let childOb = observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
let dep = new Dep(); // 每个属性都有一个dep属性
// 获取到了数组对应ob
Object.defineProperty(data,key,{
get(){
// 取值时我希望将watcher和dep 对应起来
if(Dep.target){ // 此值是在模板中取值的
dep.depend() // 让dep记住watcher
if(childOb){ // 可能是数组 可能是对象,对象也要收集依赖,后续写$set方法时需要触发他自己的更新操作
childOb.dep.depend(); // 就是让数组和对象也记录watcher
if(Array.isArray(value)){ //取外层数组要将数组里面的也进行依赖收集
dependArray(value);
}
}
}
return value
},
set(newV){
// 更新视图
if(newV !== value){
observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
value = newV;
dep.notify(); // 告诉当前的属性存放的watcher执行
}
}
})
}
export function observe(data) {
// 如果是对象才观测
if (!isObject(data)) {
return;
}
if(data.__ob__){
return data.__ob__;
}
// 默认最外层的data必须是一个对象
return new Observer(data)
}
- 无论对象嵌套多深,我们的对象取值的时候都会通过调用render方法_v的时候调用了JSON.stringify遍历了对象的每一个属性,所以对象内的每个属性会有有对应的dep,也会自动的收集对应的watcher
- 多层数组嵌套,我们需要递归给数组添加dep属性,所以避免写多维数组,尽量的扁平化,减少递归就是提高性能
nextTick异步更新数据原理
多次调用watcher的更新方法update,我们可以先将其缓存到一个栈中,等同步代码执行完,在异步更新渲染页面.
- 每次update调用nextTik方法,延迟到同步代码运行的后面执行,否则每次修改属性都去重新渲染页面,性能及其的差
const callbacks = [];
function flushCallbacks() {
callbacks.forEach(cb => cb());
waiting = false
}
let waiting = false;
function timer(flushCallbacks) {
let timerFn = () => {}
if (Promise) {
timerFn = () => {
Promise.resolve().then(flushCallbacks)
}
} else if (MutationObserver) {
let textNode = document.createTextNode(1);
let observe = new MutationObserver(flushCallbacks);
observe.observe(textNode, {
characterData: true
})
timerFn = () => {
textNode.textContent = 3;
}
// 微任务
} else if (setImmediate) {
timerFn = () => {
setImmediate(flushCallbacks)
}
} else {
timerFn = () => {
setTimeout(flushCallbacks)
}
}
timerFn();
}
// 微任务是在页面渲染前执行 我取的是内存中的dom,不关心你渲染完毕没有
export function nextTick(cb) {
callbacks.push(cb); // flushSchedulerQueue / userCallback
if (!waiting) {
timer(flushCallbacks); // vue2 中考虑了兼容性问题 vue3 里面不在考虑兼容性问题
waiting = true;
}
}
3. watch && computed
watch
- watch可能是个数组,也可能是个函数,也可能是个对象
- new Watcher 传入标识,标识是用户watch对应的watcher
- 重写getter方法,get watch属性的值,让对应属性收集到我们的watch watcher
- 当监听属性值改变,调用set方法,如果我们的标识是 watch watcher 就执行watch监听的方法,并传入Vue实例 vm,并将新值和旧值传入参数
function initWatch(vm, watch) { // Object.keys
for (let key in watch) {
let handler = watch[key];
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
vm.$watch(key, handler[i])
}
} else {
vm.$watch(key, handler)
}
}
}
function stateMixin(Vue) {
Vue.prototype.$watch = function(key, handler, options = {}) {
options.user = true;
new Watcher(this, key, handler, options);
}
}
computed
- 兼容写法,可能是一个函数,也可能是一个函数get和set方法的对象
- 遍历所有计算属性,用Object.defineProperty代理getter,setter
- 创建watchers和每一个计算属性做一个映射,一个计算属性对应一个wathcer
- new Watcher 默认不执行,传入标识,标识是computed watcher
- get拦击属性是,做一个高阶函数 createComputedGetter,最终返回计算结果
- 为了不是每次取值都调用watcher的更新方法,增加了脏值检测,通过dirty判断是否是脏值,如果是true,我们需要重新计算值,就重新调用watcher的更新方法
- 计算属性内用到的属性,不能只是有computed watcher,还要有对应的渲染watcher,将Dep.target改成栈形结构,先存入渲染watcher,当遇到计算属性取值时,记录计算属性watcher,当我们计算完值后,当前计算watcher出栈,Dep.target始终是栈中最后一个,如果还有我们去mapping 的watcher反向记录,因为watcher和dep是多对多的关系,将mapping的watcher对应的所有dep添加上渲染watcher,这样每次修改计算属性内用到的属性时,就会重新计算值,并渲染页面(调用了计算watcher和渲染watcher)
- 当页面重新渲染是,又会重新走watcher的更新方法(run()),脏值(dirty)检测修改为true,重新计算,后面重复取值,脏值为false,不再计算
function initComputed(vm, computed) {
const watchers = vm._computedWatchers = {}
for (let key in computed) {
// 校验
const userDef = computed[key];
// 依赖的属性变化就重新取值 get
let getter = typeof userDef == 'function' ? userDef : userDef.get;
// 每个就算属性本质就是watcher
// 将watcher和 属性 做一个映射
watchers[key] = new Watcher(vm, getter, () => {}, { lazy: true }); // 默认不执行
// 将key 定义在vm上
defineComputed(vm, key, userDef);
}
}
function defineComputed(vm, key, userDef) {
let sharedProperty = {};
if (typeof userDef == 'function') {
sharedProperty.get = userDef;
} else {
sharedProperty.get = createComputedGetter(key);
sharedProperty.set = userDef.set ;
}
Object.defineProperty(vm, key, sharedProperty); // computed就是一个defineProperty
}
function createComputedGetter(key) {
return function computedGetter() { // 取计算属性的值 走的是这个函数
// this._computedWatchers 包含着所有的计算属性
// 通过key 可以拿到对应watcher,这个watcher中包含了getter
let watcher = this._computedWatchers[key]
// 脏就是 要调用用户的getter 不脏就是不要调用getter
if(watcher.dirty){ // 根据dirty属性 来判断是否需要重新求职
watcher.evaluate();// this.get()
}
// 如果当前取完值后 Dep.target还有值 需要继续向上收集
if(Dep.target){
// 计算属性watcher 内部 有两个dep firstName,lastName
watcher.depend(); // watcher 里 对应了 多个dep
}
return watcher.value
}
}
watcher 类修改
// Watcher
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";
let id = 0;
class Watcher {
// vm,updateComponent,()=>{ console.log('更新视图了')},true
constructor(vm, exprOrFn, cb, options) {
// exporOfFn
this.vm = vm;
this.exprOrFn = exprOrFn;
this.user = !!options.user; // 是不是用户watcher
this.lazy = !!options.lazy;
this.dirty = options.lazy; // 如果是计算属性,那么默认值lazy:true, dirty:true
this.cb = cb;
this.options = options;
this.id = id++;
// 默认应该让exprOrFn执行 exprOrFn 方法做了什么是? render (去vm上了取值)
if (typeof exprOrFn == 'string') {
this.getter = function() { // 需要将表达式转化成函数
// 当我数据取值时 , 会进行依赖收集
// age.n vm['age.n'] =》 vm['age']['n']
let path = exprOrFn.split('.'); // [age,n]
let obj = vm;
for (let i = 0; i < path.length; i++) {
obj = obj[path[i]]
}
return obj; // getter方法
}
} else {
this.getter = exprOrFn; // updateComponent
}
this.deps = [];
this.depsId = new Set();
// 第一次的value
this.value = this.lazy ? undefined : this.get(); // 默认初始化 要取值
}
get() { // 稍后用户更新 时 可以重新调用getter方法
// defineProperty.get, 每个属性都可以收集自己的watcher
// 我希望一个属性可以对应多个watcher,同时一个watcher可以对应多个属性
pushTarget(this); // Dep.target = watcher
const value = this.getter.call(this.vm); // render() 方法会去vm上取值 vm._update(vm._render)
popTarget(); // Dep.target = null; 如果Dep.target有值说明这个变量在模板中使用了
return value
}
update() { // vue中的更新操作是异步的
// 每次更新时 this
if(this.lazy){
this.dirty = true;
}else{
queueWatcher(this); // 多次调用update 我希望先将watcher缓存下来,等一会一起更新
}
}
run() { // 后续要有其他功能
let newValue = this.get();
let oldValue = this.value
this.value = newValue; // 为了保证下一次更新时 上一次的最新值是下一次的老值
if (this.user) {
this.cb.call(this.vm, newValue, oldValue);
}
}
addDep(dep) {
let id = dep.id;
if (!this.depsId.has(id)) {
this.depsId.add(id);
this.deps.push(dep);
dep.addSub(this)
}
}
evaluate(){
this.dirty = false; // 为false表示取过值了
this.value = this.get(); // 用户的getter执行
}
depend(){
let i = this.deps.length;
while(i--){
this.deps[i].depend(); //lastName,firstName 收集渲染watcher
}
}
}
// watcher 和 dep
// 我们将更新的功能封装了一个watcher
// 渲染页面前,会将当前watcher放到Dep类上
// 在vue中页面渲染时使用的属性,需要进行依赖收集 ,收集对象的渲染watcher
// 取值时,给每个属性都加了个dep属性,用于存储这个渲染watcher (同一个watcher会对应多个dep)
// 每个属性可能对应多个视图(多个视图肯定是多个watcher) 一个属性要对应多个watcher
// dep.depend() => 通知dep存放watcher => Dep.target.addDep() => 通知watcher存放dep
// 双向存储
export default Watcher
4. 组件 && 生命周期
mergeOptions, 合并传入的选项 – 合并策略
- Vue.mixin(options): 其中的钩子函数会合并成一个数组,其他属性和方法会覆盖传入的相同内容,并挂在掉vm.$options上
- Vue.component(id, options) 组件合并策略: 后面传入的组件通过Object.create()方法,链式查找对应的components,不能直接覆盖options内组件,同名全局组件会遭到污染.
vm.$options = mergeOptions(vm.constructor.options, options);
let strats = {}; // 存放各种策略
// 钩子函数合并策略
function mergeHook(parentVal, childVal) {
if (childVal) {
if (parentVal) {
return parentVal.concat(childVal); // 后续
} else {
return [childVal]; // 第一次
}
} else {
return parentVal
}
}
// 组件合并策略
strats.components = function(parentVal, childVal) {
// Vue.options.components
let options = Object.create(parentVal); // 根据父对象构造一个新对象 options.__proto__= parentVal
if (childVal) {
for (let key in childVal) {
options[key] = childVal[key];
}
}
return options
}
function mergeOptions(parent, child) {
const options = {}; // 合并后的结果
for (let key in parent) {
mergeField(key);
}
for (let key in child) {
if (parent.hasOwnProperty(key)) {
continue;
}
mergeField(key);
}
function mergeField(key) {
let parentVal = parent[key];
let childVal = child[key];
// 策略模式
if (strats[key]) { // 如果有对应的策略就调用对应的策略即可
options[key] = strats[key](parentVal, childVal)
} else {
if (isObject(parentVal) && isObject(childVal)) {
options[key] = { ...parentVal, ...childVal }
} else {
options[key] = child[key] || parent[key];
}
}
}
return options
}
Vue.extend
- 通过传入的选项生成一个类,并继承父类
// 给个对象返回类
Vue.extend = function (opts) { // extend方法就是产生一个继承于Vue的类
// 并且身上应该有父类的所有功能
const Super = this
const Sub = function VueComponent(options){
this._init(options);
}
// 原型继承
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.options = mergeOptions(Super.options,opts);// 只和Vue.options合并
return Sub;
}
组件渲染
全局组件: Vue.component(id, options) 会调用Sub = Vue.extend(option), 然后对options进行映射 options[id] = Sub
- 创建真实DOM时,判断当前标签是不是原生标签,如果不是在判断是不是组件
- 如果是组件,给其data属性上挂载hook对象,并加上init方法。
- init用于初始化组件(因为组件无el元素,手动调用组件的$mounte方法并做到兼容无el元素返回真实DOM),将组件内的模板解析成真是DOM挂载到$el上
- 最后将$el替换定义的组件标签,加载真实DOM
// 如果tag是组件 应该渲染一个组件的vnode isReservedTag定义了所有原生标签
if (isReservedTag(tag)) {
return vnode(vm, tag, data, data.key, children, undefined);
} else {
const Ctor = vm.$options.components[tag]
return createComponent(vm, tag, data, data.key, children, Ctor);
}
// 创建组件的虚拟节点, 为了区分组件和元素 data.hook / componentOptions
function createComponent(vm, tag, data, key, children, Ctor) {
// 组件的构造函数
if(isObject(Ctor)){
Ctor = vm.$options._base.extend(Ctor); // Vue.extend
}
data.hook = { // 等会渲染组件时 需要调用此初始化方法
init(vnode){
let vm = vnode.componentInstance = new Ctor({_isComponent:true}); // new Sub 会用此选项和组件的配置进行合并
vm.$mount(); // 组件挂载完成后 会在 vnode.componentInstance.$el => <button>
}
}
return vnode(vm,`vue-component-${tag}`,data,key,undefined,undefined,{Ctor,children})
}
// patch 方法生成DOM元素 createElm方法
if (createComponent(vnode)) {
// 返回组件对应的真实节点
return vnode.componentInstance.$el;
}
function createComponent(vnode) {
let i = vnode.data; // vnode.data.hook.init
if ((i = i.hook) && (i = i.init)) {
i(vnode); // 调用init方法
}
if(vnode.componentInstance){ // 有属性说明子组件new完毕了,并且组件对应的真实DOM挂载到了componentInstance.$el
return true;
}
}
5. DOM DIFF
每次watcher调度更新dom都是全量更新,消耗比较大,所以需要在patch的时候,最大程序的复用原先的DOM,最小程序的更新DOM。
- 对比标签: 在diff过程中会先比较标签是否一致,如果标签不一致用新的标签替换掉老的标签
- 对比文本: 如果标签一致,有可能都是文本节点,那就比较文本的内容即可
- 对比属性: 标签一直,将新属性赋值给老DOM并删除老DOM多余的属性,不需重新生成DOM,style和class做了特殊处理,因为在vNode中,style是一个对象,需要遍历给el.style设置属性,class不用setAttribute, 直接el.class 赋值即可
- 比对子元素
// 比较孩子节点
let oldChildren = oldVnode.children || [];
let newChildren = vnode.children || [];
// 新老都有需要比对儿子
if(oldChildren.length > 0 && newChildren.length > 0){
updateChildren(el, oldChildren, newChildren)
// 老的有儿子新的没有清空即可
}else if(oldChildren.length > 0 ){
el.innerHTML = '';
// 新的有儿子
}else if(newChildren.length > 0){
for(let i = 0 ; i < newChildren.length ;i++){
let child = newChildren[i];
el.appendChild(createElm(child));
}
}
vue中子节点比较采用了双指针的思想
- 头头比较 => 尾尾比较 => 头尾比较 => 尾头比较
- 如果比对相同,则头指针后移,尾指针前移
- 在比对过程中,可能出现空值情况则直接跳过
- 当头尾指针重合,比对结束
- 结束后,如果新node子节点多,appendChile
- 如果老node子节点多,removeChild
// 1.在开头和结尾新增元素
function isSameVnode(oldVnode,newVnode){
// 如果两个人的标签和key 一样我认为是同一个节点 虚拟节点一样我就可以复用真实节点了
return (oldVnode.tag === newVnode.tag) && (oldVnode.key === newVnode.key)
}
function updateChildren(parent, oldChildren, newChildren) {
let oldStartIndex = 0;
let oldStartVnode = oldChildren[0];
let oldEndIndex = oldChildren.length - 1;
let oldEndVnode = oldChildren[oldEndIndex];
let newStartIndex = 0;
let newStartVnode = newChildren[0];
let newEndIndex = newChildren.length - 1;
let newEndVnode = newChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 优化向后追加逻辑
if(isSameVnode(oldStartVnode,newStartVnode)){
patch(oldStartVnode,newStartVnode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVnode = newChildren[++newStartIndex];
// 优化向前追加逻辑
}else if(isSameVnode(oldEndVnode,newEndVnode)){
patch(oldEndVnode,newEndVnode); // 比较孩子
oldEndVnode = oldChildren[--oldEndIndex];
newEndVnode = newChildren[--newEndIndex];
}
}
if(newStartIndex <= newEndIndex){
for(let i = newStartIndex ; i<=newEndIndex ;i++){
let ele = newChildren[newEndIndex+1] == null? null:newChildren[newEndIndex+1].el;
parent.insertBefore(createElm(newChildren[i]),ele);
}
}
}
// 头移动到尾部
else if(isSameVnode(oldStartVnode,newEndVnode)){
patch(oldStartVnode,newEndVnode);
parent.insertBefore(oldStartVnode.el,oldEndVnode.el.nextSibling);
oldStartVnode = oldChildren[++oldStartIndex];
newEndVnode = newChildren[--newEndIndex]
// 尾部移动到头部
}else if(isSameVnode(oldEndVnode,newStartVnode)){
patch(oldEndVnode,newStartVnode);
parent.insertBefore(oldEndVnode.el,oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVnode = newChildren[++newStartIndex]
}
暴力比对
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index
});
return map;
}
let map = makeIndexByKey(oldChildren);
// 给所有的孩子进行编号
let moveIndex = map[newStartVnode.key];
if (moveIndex == undefined) { // 老的中没有将新元素插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 有的话做移动操作
let moveVnode = oldChildren[moveIndex];
oldChildren[moveIndex] = undefined;
parent.insertBefore(moveVnode.el, oldStartVnode.el);
patch(moveVnode, newStartVnode);
}
newStartVnode = newChildren[++newStartIndex]
// 用新的元素去老的中进行查找,如果找到则移动,找不到则直接插入
if(oldStartIndex <= oldEndIndex){
for(let i = oldStartIndex; i<=oldEndIndex;i++){
let child = oldChildren[i];
if(child != undefined){
parent.removeChild(child.el)
}
}
}
// 如果有剩余则直接删除
if(!oldStartVnode){
oldStartVnode = oldChildren[++oldStartIndex];
}else if(!oldEndVnode){
oldEndVnode = oldChildren[--oldEndIndex]
}